From 62d7817ea3c6b71d2e8fc1f678a856556b40e367 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Sun, 4 Oct 2026 10:03:43 -0400 Subject: [PATCH] Clarify host readiness when game files or runtime are missing --- docs/native-hosts.md | 5 ++- src/lib/hosts.ts | 7 +++- src/lib/server/hosts/index.ts | 8 +++-- .../ui/components/NativeHostSelection.svelte | 3 +- src/routes/hosts/+page.svelte | 3 +- src/routes/hosts/[id]/+page.svelte | 4 ++- tests/unit/host-deletion.test.ts | 2 +- tests/unit/native-hosts.test.ts | 36 ++++++++++++++++++- 8 files changed, 58 insertions(+), 10 deletions(-) diff --git a/docs/native-hosts.md b/docs/native-hosts.md index 4bfba8b..5464d7b 100644 --- a/docs/native-hosts.md +++ b/docs/native-hosts.md @@ -23,7 +23,10 @@ the agent owns local execution and its durable operation journal. no separate DLC declaration. Uploading also works before enrolling any host. 5. Cache the published version on each destination host. Wait for verification. Hosts report connection and readiness separately; deployment also checks the - particular library, image, capacity and ports you selected. + particular library, image, capacity and ports you selected. A connected host + missing content shows **Needs runtime** or **Needs game files**; these require + setup rather than waiting for an automatic preparation step. **Ready** means + it has both a runtime image and a verified library available. 6. Launch a server or collection. Select the host, immutable game-file version(s) and runtime image. EC selects lobby IDs and reserves UDP/TCP ports atomically. Saved Plutonium server keys remain associated with their exact server names. diff --git a/src/lib/hosts.ts b/src/lib/hosts.ts index 49140e9..b99b9eb 100644 --- a/src/lib/hosts.ts +++ b/src/lib/hosts.ts @@ -1,7 +1,12 @@ export type HostMode = 'active' | 'draining' | 'disabled'; -export type HostReadiness = 'preparing' | 'ready' | 'degraded' | 'draining' | 'disabled'; +export type HostReadiness = 'preparing' | 'needs-runtime' | 'needs-game-files' | 'ready' | 'degraded' | 'draining' | 'disabled'; export type HostOperationState = 'pending' | 'accepted' | 'running' | 'completed' | 'failed' | 'unknown'; +export function hostReadinessLabel(readiness: HostReadiness): string { + return { preparing: 'Preparing', 'needs-runtime': 'Needs runtime', 'needs-game-files': 'Needs game files', + ready: 'Ready', degraded: 'Degraded', draining: 'Draining', disabled: 'Disabled' }[readiness]; +} + export interface HostDeletion { allowed: boolean; blockers: string[] } export interface HostLibrary { id: string; state: 'missing' | 'downloading' | 'verifying' | 'ready' | 'failed' | 'outdated'; progress?: number; error?: string } diff --git a/src/lib/server/hosts/index.ts b/src/lib/server/hosts/index.ts index d58dd9c..f0329a1 100644 --- a/src/lib/server/hosts/index.ts +++ b/src/lib/server/hosts/index.ts @@ -23,14 +23,16 @@ function hostFromRow(row: HostRow): Host { if (!inventory.directoriesWritable) reasons.push('Managed directories are not writable.'); if (typeof inventory.storageAvailable !== 'number' || inventory.storageAvailable < row.minimum_free_bytes) reasons.push('Insufficient available game storage.'); const infrastructureReady = reasons.length === 0; - if (!inventory.runtimeImages?.length) reasons.push('Prepare a Plutainer runtime image.'); - if (!inventory.libraries?.some(library => library.state === 'ready')) reasons.push('Download and verify a game-file library.'); + const runtimeReady = !!inventory.runtimeImages?.length; + const libraryReady = !!inventory.libraries?.some(library => library.state === 'ready'); + if (!runtimeReady) reasons.push('Prepare a Plutainer runtime image.'); + if (!libraryReady) reasons.push('Download and verify a game-file library.'); if (row.mode === 'draining') reasons.push('Host is draining; new deployments are blocked.'); if (row.mode === 'disabled') reasons.push('Host is disabled.'); return { id: row.id, name: row.name, hostname: row.hostname, mode: row.mode, portMin: row.port_min, portMax: row.port_max, bindAddress: row.bind_address, publicAddress: row.public_address, minimumFreeBytes: row.minimum_free_bytes, createdAt: row.created_at, lastHeartbeat: row.last_heartbeat, connected, connectionState: connected ? 'online' : 'offline', - readiness: row.mode === 'draining' ? 'draining' : row.mode === 'disabled' ? 'disabled' : !row.enrolled_at || (infrastructureReady && reasons.length) ? 'preparing' : reasons.length ? 'degraded' : 'ready', + readiness: row.mode === 'draining' ? 'draining' : row.mode === 'disabled' ? 'disabled' : !row.enrolled_at ? 'preparing' : !infrastructureReady ? 'degraded' : !runtimeReady ? 'needs-runtime' : !libraryReady ? 'needs-game-files' : 'ready', readinessReasons: reasons, inventory, enrolled: !!row.enrolled_at, credentialActive: !!row.credential_active }; } export function listHosts(): Host[] { return (database().prepare(hostSql + ' WHERE h.deleted_at IS NULL ORDER BY h.name, h.id').all() as HostRow[]).map(hostFromRow); } diff --git a/src/lib/ui/components/NativeHostSelection.svelte b/src/lib/ui/components/NativeHostSelection.svelte index 2252433..d408e74 100644 --- a/src/lib/ui/components/NativeHostSelection.svelte +++ b/src/lib/ui/components/NativeHostSelection.svelte @@ -1,6 +1,7 @@