import type { HostLibraryCache } from './game-files.js'; export type HostMode = 'active' | 'draining' | 'disabled'; export type HostReadiness = 'preparing' | 'needs-runtime' | 'needs-game-files' | 'ready' | 'degraded' | 'draining' | 'disabled' | 'deleting'; 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', deleting: 'Deleting' }[readiness]; } export interface HostDeletion { allowed: boolean; blockers: string[] } export interface HostDecommission { operationId: string; stage: 'queued' | 'running' | 'cleaned' | 'failed' | 'completed'; requestedAt: string; updatedAt: string; error: string | null; retryable: boolean; } export function hostDeletionMessage(host: Pick): string { if (!host.decommission) return ''; switch (host.decommission.stage) { case 'queued': return host.connected ? 'Host cleanup is queued.' : 'Waiting for the host to reconnect before removing its files and agent.'; case 'running': return 'Removing host files, cached game libraries, and the agent installation.'; case 'cleaned': return 'Host data is removed. Waiting for confirmation that the agent has uninstalled itself.'; case 'failed': return 'Cleanup needs attention. Complete removal has not been confirmed.'; case 'completed': return 'Host files and agent removal confirmed.'; } } export interface HostLibrary { id: string; state: 'missing' | 'downloading' | 'verifying' | 'ready' | 'failed' | 'outdated'; progress?: number; error?: string } export interface HostAgentUpdate { enabled: boolean; state: 'current' | 'checking' | 'deferred' | 'updating' | 'updated' | 'failed'; currentVersion: string; targetVersion?: string; error?: string; checkedAt?: string; } export interface HostAgentLane { name: 'health-1' | 'health-2' | 'read' | 'write'; alive: boolean; busy: boolean; lastPollAt: string | null; pollAgeSeconds: number; } export function hostAgentLaneLabel(name: HostAgentLane['name']): string { return { 'health-1': 'Health check 1', 'health-2': 'Health check 2', read: 'File reads', write: 'Actions' }[name]; } export function hostAgentLaneState(lane: HostAgentLane): 'stopped' | 'stalled' | 'busy' | 'starting' | 'polling' { if (!lane.alive) return 'stopped'; // Reads and mutations can legitimately take minutes. Health work has a // bounded budget; an old poll there indicates a blocked worker even if busy. if (lane.pollAgeSeconds > 90 && (!lane.busy || lane.name.startsWith('health-'))) return 'stalled'; return lane.busy ? 'busy' : lane.lastPollAt === null ? 'starting' : 'polling'; } export function hostAgentUpdatesEnabled(inventory: HostInventory): boolean { return inventory.agentUpdate?.enabled === true && inventory.capabilities?.includes('agent.update') === true; } export function hostAgentUpdateLabel(inventory: HostInventory): string { if (!hostAgentUpdatesEnabled(inventory)) return 'Setup required'; const update = inventory.agentUpdate!; if (update.state === 'current' && !update.checkedAt) return 'Awaiting first check'; return { current: 'Current', checking: 'Checking', deferred: 'Deferred', updating: 'Updating', updated: 'Updated', failed: 'Needs attention' }[update.state]; } export function hostAgentUpdateMessage(inventory: HostInventory): string { if (!hostAgentUpdatesEnabled(inventory)) return 'Run the current installer once on this host to enable automatic agent updates.'; const update = inventory.agentUpdate!; switch (update.state) { case 'current': return update.checkedAt ? 'The last check found no newer compatible agent release.' : 'The host timer will check for a compatible agent release automatically.'; case 'checking': return 'The host is checking the controller release and verifying its download.'; case 'deferred': return 'The updater is waiting for the reported condition to clear. The host checks again automatically.'; case 'updating': return 'The host is replacing agent code and checking that the new process connects successfully.'; case 'updated': return 'The new agent connected successfully. Automatic checks continue.'; case 'failed': return 'The update did not complete successfully. Review the reported error before retrying setup.'; } } export interface HostInventory { hostname?: string; agentVersion?: string; protocolVersion?: number; os?: string; architecture?: string; cpu?: string; cpuCount?: number; cpuUsagePercent?: number; memoryTotal?: number; memoryAvailable?: number; storageTotal?: number; storageAvailable?: number; dockerVersion?: string; dockerRunning?: boolean; directoriesWritable?: boolean; runtimeImages?: string[]; libraries?: HostLibrary[]; currentOperation?: string | null; [key: string]: unknown; capabilities?: string[]; agentUpdate?: HostAgentUpdate; agentLanes?: HostAgentLane[]; instanceResources?: InstanceResources[]; instanceResourcesError?: string; instanceStates?: InstanceState[]; } export interface InstanceState { instanceId: string; generation: number; state: string; healthy: boolean; observedAt: string; } export interface InstanceResources { instanceId: string; generation: number; uptimeSeconds: number; cpuCores: number; memoryBytes: number; } export interface ResourceAllowance { cpuCores: number; memoryBytes: number; source: 'host' | 'fleet' | 'bootstrap'; samples: number; observedAt: string | null; } export interface CapacityCalibration { running: ResourceAllowance; startup: ResourceAllowance } export interface CapacitySample { hostId: string; hostName: string | null; instanceId: string; server: string | null; generation: number; phase: 'startup' | 'running'; timestamp: string; cpuCores: number; memoryBytes: number; } export interface CapacitySamplePage { samples: CapacitySample[]; total: number; page: number; pageSize: number; } export interface Host { id: string; name: string; hostname: string; mode: HostMode; portMin: number; portMax: number; bindAddress: string; publicAddress: string; minimumFreeBytes: number; createdAt: string; lastHeartbeat: string | null; connected: boolean; connectionState: 'online' | 'offline'; readiness: HostReadiness; readinessReasons: string[]; inventory: HostInventory; enrolled: boolean; credentialActive: boolean; decommission: HostDecommission | null; preparation?: HostPreparation; enablingSince?: string | null; } export interface HostOperation { id: string; hostId: string; kind: string; instanceId: string | null; generation: number | null; payload: Record; state: HostOperationState; result: unknown; error: string | null; createdAt: string; updatedAt: string; } export interface HostOperationSummary extends Omit { hostName: string | null; serverName: string | null; targetPath: string | null; source: string | null; routine: boolean; } export type HostOperationFilter = 'important' | 'routine' | 'all'; export interface HostOperationScope { host?: string; server?: string } export function hostOperationScope(params: URLSearchParams): HostOperationScope { const scope: HostOperationScope = {}; for (const key of ['host', 'server'] as const) { const value = params.get(key); if (value && value.length <= 512 && !/[\x00-\x1f\x7f]/.test(value)) scope[key] = value; } return scope; } export function isHostOperationFilter(value: unknown): value is HostOperationFilter { return typeof value === 'string' && ['important', 'routine', 'all'].includes(value); } export interface HostOperationHistoryPage { operations: HostOperationSummary[]; page: number; pageSize: 25; total: number; totalPages: number; filter: HostOperationFilter; } export interface HostPreparationItem { kind: 'runtime' | 'library'; label: string; state: 'waiting' | 'preparing' | 'ready' | 'blocked'; detail: string; operationId?: string; bytesDownloaded?: number; bytesTotal?: number; cache?: HostLibraryCache; } export interface HostPreparation { state: HostPreparationItem['state']; items: HostPreparationItem[]; } export interface NativeDeploymentSelection { libraryIds: Partial>; image: string; bindAddress?: string; publicAddress?: string; publicPort?: number; } /** Queued automatic launches have no assigned host or port until provisioning. */ export interface NativePlacement { hostId?: string; libraryId: string; image?: string; bindAddress?: string; publicAddress?: string; publicPort?: number; automatic?: boolean; network?: { bindAddress?: string; publicAddress?: string; publicPort?: number }; }