Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+204
-58
@@ -1,31 +1,67 @@
|
||||
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 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];
|
||||
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 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;
|
||||
operationId: string;
|
||||
stage: 'queued' | 'running' | 'cleaned' | 'failed' | 'completed';
|
||||
requestedAt: string;
|
||||
updatedAt: string;
|
||||
error: string | null;
|
||||
retryable: boolean;
|
||||
}
|
||||
export function hostDeletionMessage(host: Pick<Host, 'connected' | 'decommission'>): 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.';
|
||||
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 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';
|
||||
@@ -37,50 +73,93 @@ export interface HostAgentUpdate {
|
||||
|
||||
export interface HostAgentLane {
|
||||
name: 'health-1' | 'health-2' | 'read' | 'write';
|
||||
alive: boolean; busy: boolean; lastPollAt: string | null; pollAgeSeconds: number;
|
||||
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];
|
||||
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' {
|
||||
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';
|
||||
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;
|
||||
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];
|
||||
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.';
|
||||
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.';
|
||||
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;
|
||||
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[];
|
||||
plutoniumCache?: { revision: number; bytes: number };
|
||||
agentUpdate?: HostAgentUpdate;
|
||||
@@ -90,37 +169,82 @@ export interface HostInventory {
|
||||
instanceStates?: InstanceState[];
|
||||
}
|
||||
export interface InstanceState {
|
||||
instanceId: string; generation: number; state: string; healthy: boolean; observedAt: string;
|
||||
instanceId: string;
|
||||
generation: number;
|
||||
state: string;
|
||||
healthy: boolean;
|
||||
observedAt: string;
|
||||
}
|
||||
export interface InstanceResources {
|
||||
instanceId: string; generation: number; uptimeSeconds: number; cpuCores: number; memoryBytes: number;
|
||||
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;
|
||||
cpuCores: number;
|
||||
memoryBytes: number;
|
||||
source: 'host' | 'fleet' | 'bootstrap';
|
||||
samples: number;
|
||||
observedAt: string | null;
|
||||
}
|
||||
export interface CapacityCalibration {
|
||||
running: ResourceAllowance;
|
||||
startup: ResourceAllowance;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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<string, unknown>; state: HostOperationState; result: unknown; error: string | null;
|
||||
createdAt: string; updatedAt: string;
|
||||
id: string;
|
||||
hostId: string;
|
||||
kind: string;
|
||||
instanceId: string | null;
|
||||
generation: number | null;
|
||||
payload: Record<string, unknown>;
|
||||
state: HostOperationState;
|
||||
result: unknown;
|
||||
error: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface HostOperationSummary extends Omit<HostOperation, 'payload' | 'result'> {
|
||||
hostName: string | null;
|
||||
@@ -130,11 +254,16 @@ export interface HostOperationSummary extends Omit<HostOperation, 'payload' | 'r
|
||||
routine: boolean;
|
||||
}
|
||||
export type HostOperationFilter = 'important' | 'routine' | 'all';
|
||||
export interface HostOperationScope { host?: string; server?: string; operation?: string }
|
||||
export interface HostOperationScope {
|
||||
host?: string;
|
||||
server?: string;
|
||||
operation?: string;
|
||||
}
|
||||
export function hostOperationScope(params: URLSearchParams): HostOperationScope {
|
||||
const scope: HostOperationScope = {};
|
||||
for (const key of ['host', 'server', 'operation'] as const) {
|
||||
const value = params.get(key);
|
||||
// eslint-disable-next-line no-control-regex -- Reject control characters in operation scope filters.
|
||||
if (value && value.length <= 512 && !/[\x00-\x1f\x7f]/.test(value)) scope[key] = value;
|
||||
}
|
||||
return scope;
|
||||
@@ -143,28 +272,45 @@ export function isHostOperationFilter(value: unknown): value is HostOperationFil
|
||||
return typeof value === 'string' && ['important', 'routine', 'all'].includes(value);
|
||||
}
|
||||
export interface HostOperationHistoryPage {
|
||||
operations: HostOperationSummary[]; page: number; pageSize: 25; total: number; totalPages: number; filter: HostOperationFilter;
|
||||
operations: HostOperationSummary[];
|
||||
page: number;
|
||||
pageSize: 25;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
filter: HostOperationFilter;
|
||||
}
|
||||
|
||||
export interface HostPreparationItem {
|
||||
kind: 'runtime' | 'library' | 'plutonium'; label: string;
|
||||
state: 'waiting' | 'preparing' | 'ready' | 'blocked'; detail: string;
|
||||
operationId?: string; bytesDownloaded?: number; bytesTotal?: number;
|
||||
kind: 'runtime' | 'library' | 'plutonium';
|
||||
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[];
|
||||
state: HostPreparationItem['state'];
|
||||
items: HostPreparationItem[];
|
||||
}
|
||||
|
||||
export interface NativeDeploymentSelection {
|
||||
libraryIds: Partial<Record<'t5' | 't6', string>>; image: string;
|
||||
bindAddress?: string; publicAddress?: string; publicPort?: number;
|
||||
libraryIds: Partial<Record<'t5' | 't6', string>>;
|
||||
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;
|
||||
hostId?: string;
|
||||
libraryId: string;
|
||||
image?: string;
|
||||
bindAddress?: string;
|
||||
publicAddress?: string;
|
||||
publicPort?: number;
|
||||
automatic?: boolean;
|
||||
network?: { bindAddress?: string; publicAddress?: string; publicPort?: number };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user