refactor: Huge pyro servers composable cleanup (#3745)

* refactor: start refactor of pyro servers module-based class

* refactor: finish modules

* refactor: start on type checking + matching api

* refactor: finish pyro servers composable refactor

* refactor: pyro -> modrinth

* fix: import not refactored

* fix: broken power action enums

* fix: remove pyro mentions

* fix: lint

* refactor: fix option pages

* fix: error renames

* remove empty pyro-servers.ts file

---------

Signed-off-by: IMB11 <hendersoncal117@gmail.com>
Co-authored-by: Prospector <prospectordev@gmail.com>
This commit is contained in:
IMB11
2025-06-11 23:32:39 +01:00
committed by GitHub
parent 6955731def
commit 1b1d41605b
77 changed files with 1791 additions and 2513 deletions

View File

@@ -46,7 +46,7 @@ export const NOTICE_LEVELS: Record<
},
}
const DISMISSABLE = {
export const DISMISSABLE = {
name: defineMessage({
id: 'servers.notice.dismissable',
defaultMessage: 'Dismissable',
@@ -57,7 +57,7 @@ const DISMISSABLE = {
},
}
const UNDISMISSABLE = {
export const UNDISMISSABLE = {
name: defineMessage({
id: 'servers.notice.undismissable',
defaultMessage: 'Undismissable',

View File

@@ -7,3 +7,4 @@ export * from './projects'
export * from './types'
export * from './users'
export * from './utils'
export * from './servers'

View File

@@ -23,6 +23,7 @@
"dayjs": "^1.11.10",
"highlight.js": "^11.9.0",
"markdown-it": "^14.1.0",
"ofetch": "^1.3.4",
"xss": "^1.0.14"
}
}

View File

@@ -0,0 +1,3 @@
export * from './modrinth-servers-multi-error'
export * from './modrinth-servers-fetch-error'
export * from './modrinth-server-error'

View File

@@ -0,0 +1,59 @@
import { FetchError } from 'ofetch'
import { V1ErrorInfo } from '../types'
export class ModrinthServerError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly originalError?: Error,
public readonly module?: string,
public readonly v1Error?: V1ErrorInfo,
) {
let errorMessage = message
let method = 'GET'
let path = ''
if (originalError instanceof FetchError) {
const matches = message.match(/\[([A-Z]+)\]\s+"([^"]+)":/)
if (matches) {
method = matches[1]
path = matches[2].replace(/https?:\/\/[^/]+\/[^/]+\/v\d+\//, '')
}
const statusMessage = (() => {
if (!statusCode) return 'Unknown Error'
switch (statusCode) {
case 400:
return 'Bad Request'
case 401:
return 'Unauthorized'
case 403:
return 'Forbidden'
case 404:
return 'Not Found'
case 408:
return 'Request Timeout'
case 429:
return 'Too Many Requests'
case 500:
return 'Internal Server Error'
case 502:
return 'Bad Gateway'
case 503:
return 'Service Unavailable'
case 504:
return 'Gateway Timeout'
default:
return `HTTP ${statusCode}`
}
})()
errorMessage = `[${method}] ${statusMessage} (${statusCode}) while fetching ${path}${module ? ` in ${module}` : ''}`
} else {
errorMessage = `${message}${statusCode ? ` (${statusCode})` : ''}${module ? ` in ${module}` : ''}`
}
super(errorMessage)
this.name = 'PyroServersFetchError'
}
}

View File

@@ -0,0 +1,10 @@
export class ModrinthServersFetchError extends Error {
constructor(
message: string,
public statusCode?: number,
public originalError?: Error,
) {
super(message)
this.name = 'PyroFetchError'
}
}

View File

@@ -0,0 +1,27 @@
export class ModrinthServersMultiError extends Error {
public readonly errors: Map<string, Error> = new Map()
public readonly timestamp: number = Date.now()
constructor(message?: string) {
super(message || 'Multiple errors occurred')
this.name = 'MultipleErrors'
}
addError(module: string, error: Error) {
this.errors.set(module, error)
this.message = this.buildErrorMessage()
}
hasErrors() {
return this.errors.size > 0
}
private buildErrorMessage(): string {
return (
Array.from(this.errors.entries())
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(([_module, error]) => error.message)
.join('\n')
)
}
}

View File

@@ -0,0 +1,2 @@
export * from './errors'
export * from './types'

View File

@@ -0,0 +1,19 @@
import { ModrinthServerError } from '../errors'
export interface V1ErrorInfo {
context?: string
error: string
description: string
}
export interface JWTAuth {
url: string
token: string
}
export interface ModuleError {
error: ModrinthServerError
timestamp: number
}
export type ModuleName = 'general' | 'content' | 'backups' | 'network' | 'startup' | 'ws' | 'fs'

View File

@@ -0,0 +1,28 @@
import type { WSBackupTask, WSBackupState } from './websocket'
export interface Backup {
id: string
name: string
created_at: string
locked: boolean
automated: boolean
interrupted: boolean
ongoing: boolean
task: {
[K in WSBackupTask]?: {
progress: number
state: WSBackupState
}
}
}
export interface AutoBackupSettings {
enabled: boolean
interval: number
}
export interface ServerBackup {
id: string
name: string
created_at: string
}

View File

@@ -0,0 +1,59 @@
import type { Project } from '../../types'
import { Allocation } from './server'
import { ServerBackup } from './backup'
import { Mod } from './content'
export type ServerNotice = {
id: number
message: string
title?: string
level: 'info' | 'warn' | 'critical' | 'survey'
dismissable: boolean
announce_at: string
expires: string
assigned: {
kind: 'server' | 'node'
id: string
name: string
}[]
dismissed_by: {
server: string
dismissed_on: string
}[]
}
export interface Server {
server_id: string
name: string
status: string
net: {
ip: string
port: number
domain: string
allocations: Allocation[]
}
game: string
loader: string | null
loader_version: string | null
mc_version: string | null
backup_quota: number
used_backup_quota: number
backups: ServerBackup[]
mods: Mod[]
project: Project | null
suspension_reason: string | null
image: string | null
upstream?: {
kind: 'modpack'
project_id: string
version_id: string
}
motd: string
flows: {
intro?: boolean
}
}
export interface Servers {
servers: Server[]
}

View File

@@ -0,0 +1,13 @@
export interface Mod {
filename: string
project_id: string | undefined
version_id: string | undefined
name: string | undefined
version_number: string | undefined
icon_url: string | undefined
owner: string | undefined
disabled: boolean
installing: boolean
}
export type ContentType = 'mod' | 'plugin'

View File

@@ -0,0 +1,33 @@
import type { FSQueuedOp, FilesystemOp } from './websocket'
import { JWTAuth } from './api'
export interface DirectoryItem {
name: string
type: 'directory' | 'file'
count?: number
modified: number
created: number
path: string
}
export interface FileUploadQuery {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
promise: Promise<any>
onProgress: (
callback: (progress: { loaded: number; total: number; progress: number }) => void,
) => void
cancel: () => void
}
export interface DirectoryResponse {
items: DirectoryItem[]
total: number
current?: number
}
export interface FSModule {
auth: JWTAuth
ops: FilesystemOp[]
queuedOps: FSQueuedOp[]
opsQueuedForModification: string[]
}

View File

@@ -0,0 +1,8 @@
export * from './api'
export * from './content'
export * from './server'
export * from './backup'
export * from './filesystem'
export * from './websocket'
export * from './stats'
export * from './common'

View File

@@ -0,0 +1,76 @@
import type { Project } from '../../types'
import type { ServerNotice } from './common'
export interface ServerGeneral {
server_id: string
name: string
net: {
ip: string
port: number
domain: string
}
game: string
backup_quota: number
used_backup_quota: number
status: string
suspension_reason: 'moderated' | 'paymentfailed' | 'cancelled' | 'upgrading' | 'other' | string
loader: string
loader_version: string
mc_version: string
upstream: {
kind: 'modpack' | 'mod' | 'resourcepack'
version_id: string
project_id: string
} | null
motd?: string
image?: string
project?: Project
sftp_username: string
sftp_password: string
sftp_host: string
datacenter?: string
notices?: ServerNotice[]
node: {
token: string
instance: string
}
flows?: {
intro?: boolean
}
}
export interface Allocation {
port: number
name: string
}
export interface Startup {
invocation: string
original_invocation: string
jdk_version: 'lts8' | 'lts11' | 'lts17' | 'lts21'
jdk_build: 'corretto' | 'temurin' | 'graal'
}
export type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
export type JDKVersion = 'lts8' | 'lts11' | 'lts17' | 'lts21'
export type JDKBuild = 'corretto' | 'temurin' | 'graal'
export type Loaders =
| 'Fabric'
| 'Quilt'
| 'Forge'
| 'NeoForge'
| 'Paper'
| 'Spigot'
| 'Bukkit'
| 'Vanilla'
| 'Purpur'
export type ServerState =
| 'starting'
| 'running'
| 'restarting'
| 'stopping'
| 'stopped'
| 'crashed'
| 'unknown'

View File

@@ -0,0 +1,20 @@
export interface Stats {
current: {
cpu_percent: number
ram_usage_bytes: number
ram_total_bytes: number
storage_usage_bytes: number
storage_total_bytes: number
}
past: {
cpu_percent: number
ram_usage_bytes: number
ram_total_bytes: number
storage_usage_bytes: number
storage_total_bytes: number
}
graph: {
cpu: number[]
ram: number[]
}
}

View File

@@ -0,0 +1,124 @@
import type { Stats } from './stats'
import type { ServerState } from './server'
export interface WSAuth {
url: string
token: string
}
export interface WSLogEvent {
event: 'log'
message: string
}
type CurrentStats = Stats['current']
export interface WSStatsEvent extends CurrentStats {
event: 'stats'
}
export interface WSAuthExpiringEvent {
event: 'auth-expiring'
}
export interface WSPowerStateEvent {
event: 'power-state'
state: ServerState
// if state "crashed"
oom_killed?: boolean
exit_code?: number
}
export interface WSAuthIncorrectEvent {
event: 'auth-incorrect'
}
export interface WSInstallationResultOkEvent {
event: 'installation-result'
result: 'ok'
}
export interface WSInstallationResultErrEvent {
event: 'installation-result'
result: 'err'
reason: string
}
export type WSInstallationResultEvent = WSInstallationResultOkEvent | WSInstallationResultErrEvent
export interface WSAuthOkEvent {
event: 'auth-ok'
}
export interface WSUptimeEvent {
event: 'uptime'
uptime: number // seconds
}
export interface WSNewModEvent {
event: 'new-mod'
}
export type WSBackupTask = 'file' | 'create' | 'restore'
export type WSBackupState = 'ongoing' | 'done' | 'failed' | 'cancelled' | 'unchanged'
export interface WSBackupProgressEvent {
event: 'backup-progress'
task: WSBackupTask
id: string
progress: number // percentage
state: WSBackupState
ready: boolean
}
export type FSQueuedOpUnarchive = {
op: 'unarchive'
src: string
}
export type FSQueuedOp = FSQueuedOpUnarchive
export type FSOpUnarchive = {
op: 'unarchive'
progress: number // Note: 1 does not mean it's done
id: string // UUID
mime: string
src: string
state:
| 'queued'
| 'ongoing'
| 'cancelled'
| 'done'
| 'failed-corrupted'
| 'failed-invalid-path'
| 'failed-cf-no-serverpack'
| 'failed-cf-not-available'
| 'failed-not-reachable'
current_file: string | null
failed_path?: string
bytes_processed: number
files_processed: number
started: string
}
export type FilesystemOp = FSOpUnarchive
export interface WSFilesystemOpsEvent {
event: 'filesystem-ops'
all: FilesystemOp[]
}
export type WSEvent =
| WSLogEvent
| WSStatsEvent
| WSPowerStateEvent
| WSAuthExpiringEvent
| WSAuthIncorrectEvent
| WSInstallationResultEvent
| WSAuthOkEvent
| WSUptimeEvent
| WSNewModEvent
| WSBackupProgressEvent
| WSFilesystemOpsEvent

View File

@@ -294,22 +294,3 @@ export type Report = {
created: string
body: string
}
export type ServerNotice = {
id: number
message: string
title?: string
level: 'info' | 'warn' | 'critical' | 'survey'
dismissable: boolean
announce_at: string
expires: string
assigned: {
kind: 'server' | 'node'
id: string
name: string
}[]
dismissed_by: {
server: string
dismissed_on: string
}[]
}