Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+149
-65
@@ -3,7 +3,14 @@ import { freshStatusTimestamp } from '../server-freshness.js';
|
||||
import { decodeRconError } from './rconError.js';
|
||||
import { statusPresentation } from './status.js';
|
||||
|
||||
export type ServerHealthState = 'online' | 'host-offline' | 'not-responding' | 'stopped' | 'starting' | 'provisioning' | 'unknown';
|
||||
export type ServerHealthState =
|
||||
| 'online'
|
||||
| 'host-offline'
|
||||
| 'not-responding'
|
||||
| 'stopped'
|
||||
| 'starting'
|
||||
| 'provisioning'
|
||||
| 'unknown';
|
||||
export interface ServerHealthPresentation {
|
||||
state: ServerHealthState;
|
||||
label: string;
|
||||
@@ -24,88 +31,165 @@ function observedAt(value: string | null | undefined, now: number): string | nul
|
||||
export function serverHealth(server: FleetServer, now: number): ServerHealthPresentation {
|
||||
const evidence = healthEvidence(server, now);
|
||||
const error = decodeRconError(server.last_error);
|
||||
const connectionReason = error ? {
|
||||
'no-config': 'Connection not configured.', 'no-port': 'Connection port not configured.',
|
||||
'send-error': 'Game connection failed.', timeout: 'No reply to the status check.',
|
||||
'bad-reply': 'Unreadable game response.', 'no-players': 'Player count not reported.'
|
||||
}[error.reason] : null;
|
||||
const shortReason = evidence.state === 'host-offline' ? `${server.host?.name ?? 'Host'} unavailable; game may still run.`
|
||||
: evidence.state === 'stopped' ? 'Container confirmed stopped.'
|
||||
: evidence.state === 'not-responding' ? connectionReason ?? 'Game report out of date; players unknown.'
|
||||
: evidence.state === 'unknown' ? connectionReason ?? (freshStatusTimestamp(server.last_seen_at, now) ? 'Game replied; container unverified.' : 'Game and container unverified.')
|
||||
: evidence.reason;
|
||||
const connectionReason = error
|
||||
? {
|
||||
'no-config': 'Connection not configured.',
|
||||
'no-port': 'Connection port not configured.',
|
||||
'send-error': 'Game connection failed.',
|
||||
timeout: 'No reply to the status check.',
|
||||
'bad-reply': 'Unreadable game response.',
|
||||
'no-players': 'Player count not reported.'
|
||||
}[error.reason]
|
||||
: null;
|
||||
const shortReason =
|
||||
evidence.state === 'host-offline'
|
||||
? `${server.host?.name ?? 'Host'} unavailable; game may still run.`
|
||||
: evidence.state === 'stopped'
|
||||
? 'Container confirmed stopped.'
|
||||
: evidence.state === 'not-responding'
|
||||
? (connectionReason ?? 'Game report out of date; players unknown.')
|
||||
: evidence.state === 'unknown'
|
||||
? (connectionReason ??
|
||||
(freshStatusTimestamp(server.last_seen_at, now)
|
||||
? 'Game replied; container unverified.'
|
||||
: 'Game and container unverified.'))
|
||||
: evidence.reason;
|
||||
return { ...evidence, shortReason, label: statusPresentation(evidence.state).label };
|
||||
}
|
||||
|
||||
function healthEvidence(server: FleetServer, now: number): Omit<ServerHealthPresentation, 'label' | 'shortReason'> {
|
||||
function healthEvidence(
|
||||
server: FleetServer,
|
||||
now: number
|
||||
): Omit<ServerHealthPresentation, 'label' | 'shortReason'> {
|
||||
const href = `/servers/${encodeURIComponent(server.name)}`;
|
||||
const diagnostics = `${href}?tab=diagnostics`;
|
||||
const host = server.host;
|
||||
const hostId = host?.id || server.host_id;
|
||||
const hostOffline = !!host && (host.connectionState === 'offline' || !host.connected
|
||||
|| !freshStatusTimestamp(host.lastHeartbeat, now));
|
||||
if (hostOffline) return {
|
||||
state: 'host-offline', reason: `${host.name ? `${host.name}'s host agent` : 'The host agent'} is unavailable. The game may still be running.`,
|
||||
actionLabel: hostId ? 'Check host' : 'Open diagnostics', actionHref: hostId ? `/hosts/${encodeURIComponent(hostId)}` : diagnostics,
|
||||
since: observedAt(host.lastHeartbeat, now)
|
||||
};
|
||||
const hostOffline =
|
||||
!!host &&
|
||||
(host.connectionState === 'offline' ||
|
||||
!host.connected ||
|
||||
!freshStatusTimestamp(host.lastHeartbeat, now));
|
||||
if (hostOffline)
|
||||
return {
|
||||
state: 'host-offline',
|
||||
reason: `${host.name ? `${host.name}'s host agent` : 'The host agent'} is unavailable. The game may still be running.`,
|
||||
actionLabel: hostId ? 'Check host' : 'Open diagnostics',
|
||||
actionHref: hostId ? `/hosts/${encodeURIComponent(hostId)}` : diagnostics,
|
||||
since: observedAt(host.lastHeartbeat, now)
|
||||
};
|
||||
|
||||
if (server.health === 'provisioning') return {
|
||||
state: 'provisioning', reason: 'Server setup is in progress. It will be ready after its health checks pass.',
|
||||
actionLabel: 'View progress', actionHref: `${href}?tab=history`, since: observedAt(server.telemetry_after, now)
|
||||
};
|
||||
if (server.health === 'starting') return {
|
||||
state: 'starting', reason: 'The game is starting. Waiting for a successful status report.',
|
||||
actionLabel: 'View progress', actionHref: `${href}?tab=history`,
|
||||
since: observedAt(server.telemetry_after, now) ?? observedAt(server.container_checked_at, now)
|
||||
};
|
||||
if (server.health === 'provisioning')
|
||||
return {
|
||||
state: 'provisioning',
|
||||
reason: 'Server setup is in progress. It will be ready after its health checks pass.',
|
||||
actionLabel: 'View progress',
|
||||
actionHref: `${href}?tab=history`,
|
||||
since: observedAt(server.telemetry_after, now)
|
||||
};
|
||||
if (server.health === 'starting')
|
||||
return {
|
||||
state: 'starting',
|
||||
reason: 'The game is starting. Waiting for a successful status report.',
|
||||
actionLabel: 'View progress',
|
||||
actionHref: `${href}?tab=history`,
|
||||
since:
|
||||
observedAt(server.telemetry_after, now) ??
|
||||
observedAt(server.container_checked_at, now)
|
||||
};
|
||||
|
||||
const containerAt = observedAt(server.container_checked_at, now);
|
||||
const gameAt = observedAt(server.last_seen_at, now);
|
||||
// A last game check cannot renew a container observation. Reports predating
|
||||
// the most recent lifecycle submission describe the previous process.
|
||||
const containerCurrent = freshStatusTimestamp(containerAt, now)
|
||||
&& (!server.telemetry_after || Date.parse(containerAt!) > Date.parse(server.telemetry_after));
|
||||
const gameCurrent = freshStatusTimestamp(gameAt, now)
|
||||
&& (!server.telemetry_after || Date.parse(gameAt!) > Date.parse(server.telemetry_after));
|
||||
const containerCurrent =
|
||||
freshStatusTimestamp(containerAt, now) &&
|
||||
(!server.telemetry_after || Date.parse(containerAt!) > Date.parse(server.telemetry_after));
|
||||
const gameCurrent =
|
||||
freshStatusTimestamp(gameAt, now) &&
|
||||
(!server.telemetry_after || Date.parse(gameAt!) > Date.parse(server.telemetry_after));
|
||||
const hostMissing = !!server.host_id && !host;
|
||||
if (!hostMissing && containerCurrent && ['stopped', 'exited', 'created', 'dead'].includes(server.container_state ?? '')
|
||||
&& (!gameAt || Date.parse(gameAt) < Date.parse(containerAt!))) return {
|
||||
state: 'stopped', reason: 'The latest container check confirmed that the game is stopped.',
|
||||
actionLabel: 'Start server', actionHref: `${href}?action=start`, since: containerAt
|
||||
};
|
||||
if (
|
||||
!hostMissing &&
|
||||
containerCurrent &&
|
||||
['stopped', 'exited', 'created', 'dead'].includes(server.container_state ?? '') &&
|
||||
(!gameAt || Date.parse(gameAt) < Date.parse(containerAt!))
|
||||
)
|
||||
return {
|
||||
state: 'stopped',
|
||||
reason: 'The latest container check confirmed that the game is stopped.',
|
||||
actionLabel: 'Start server',
|
||||
actionHref: `${href}?action=start`,
|
||||
since: containerAt
|
||||
};
|
||||
|
||||
const containerAllowsOnline = !server.container_state || server.container_state === 'unknown'
|
||||
|| server.container_state === 'running' && (!server.container_checked_at || containerCurrent);
|
||||
if (!hostMissing && server.health === 'online' && !server.last_error && gameCurrent && containerAllowsOnline) return {
|
||||
state: 'online', reason: 'The game is responding.', actionLabel: 'View server', actionHref: href, since: gameAt
|
||||
};
|
||||
if (gameCurrent && !server.last_error) return {
|
||||
state: 'unknown', reason: 'The game recently responded, but its current container state has not been verified.',
|
||||
actionLabel: hostMissing ? 'Check host' : 'Open diagnostics',
|
||||
actionHref: hostMissing ? `/hosts/${encodeURIComponent(server.host_id!)}` : diagnostics, since: gameAt
|
||||
};
|
||||
const containerAllowsOnline =
|
||||
!server.container_state ||
|
||||
server.container_state === 'unknown' ||
|
||||
(server.container_state === 'running' &&
|
||||
(!server.container_checked_at || containerCurrent));
|
||||
if (
|
||||
!hostMissing &&
|
||||
server.health === 'online' &&
|
||||
!server.last_error &&
|
||||
gameCurrent &&
|
||||
containerAllowsOnline
|
||||
)
|
||||
return {
|
||||
state: 'online',
|
||||
reason: 'The game is responding.',
|
||||
actionLabel: 'View server',
|
||||
actionHref: href,
|
||||
since: gameAt
|
||||
};
|
||||
if (gameCurrent && !server.last_error)
|
||||
return {
|
||||
state: 'unknown',
|
||||
reason: 'The game recently responded, but its current container state has not been verified.',
|
||||
actionLabel: hostMissing ? 'Check host' : 'Open diagnostics',
|
||||
actionHref: hostMissing ? `/hosts/${encodeURIComponent(server.host_id!)}` : diagnostics,
|
||||
since: gameAt
|
||||
};
|
||||
|
||||
const error = decodeRconError(server.last_error);
|
||||
const errorReason = error ? {
|
||||
'no-config': 'The game connection is not configured.',
|
||||
'no-port': 'The game connection has no port configured.',
|
||||
'send-error': 'The latest game connection attempt could not be completed.',
|
||||
timeout: 'The game did not answer the latest status check.',
|
||||
'bad-reply': 'The latest game response could not be read.',
|
||||
'no-players': 'The latest game report did not include a player count.'
|
||||
}[error.reason] : null;
|
||||
if (!hostMissing && server.container_state === 'running' && containerCurrent) return {
|
||||
state: 'not-responding', reason: errorReason ?? 'The container is running, but the game has no current status report.',
|
||||
actionLabel: 'Test connection', actionHref: diagnostics, since: gameAt ?? observedAt(server.last_checked_at, now)
|
||||
};
|
||||
if (!hostMissing && (server.health === 'stale' || server.health === 'online') && gameAt
|
||||
&& (!server.telemetry_after || Date.parse(gameAt) > Date.parse(server.telemetry_after))) return {
|
||||
state: 'not-responding', reason: errorReason ?? 'The last game report is no longer current. Player count is unknown.',
|
||||
actionLabel: 'Test connection', actionHref: diagnostics, since: gameAt
|
||||
};
|
||||
const errorReason = error
|
||||
? {
|
||||
'no-config': 'The game connection is not configured.',
|
||||
'no-port': 'The game connection has no port configured.',
|
||||
'send-error': 'The latest game connection attempt could not be completed.',
|
||||
timeout: 'The game did not answer the latest status check.',
|
||||
'bad-reply': 'The latest game response could not be read.',
|
||||
'no-players': 'The latest game report did not include a player count.'
|
||||
}[error.reason]
|
||||
: null;
|
||||
if (!hostMissing && server.container_state === 'running' && containerCurrent)
|
||||
return {
|
||||
state: 'not-responding',
|
||||
reason:
|
||||
errorReason ??
|
||||
'The container is running, but the game has no current status report.',
|
||||
actionLabel: 'Test connection',
|
||||
actionHref: diagnostics,
|
||||
since: gameAt ?? observedAt(server.last_checked_at, now)
|
||||
};
|
||||
if (
|
||||
!hostMissing &&
|
||||
(server.health === 'stale' || server.health === 'online') &&
|
||||
gameAt &&
|
||||
(!server.telemetry_after || Date.parse(gameAt) > Date.parse(server.telemetry_after))
|
||||
)
|
||||
return {
|
||||
state: 'not-responding',
|
||||
reason:
|
||||
errorReason ??
|
||||
'The last game report is no longer current. Player count is unknown.',
|
||||
actionLabel: 'Test connection',
|
||||
actionHref: diagnostics,
|
||||
since: gameAt
|
||||
};
|
||||
return {
|
||||
state: 'unknown', reason: errorReason ?? 'There is no current evidence of the game or container state.',
|
||||
state: 'unknown',
|
||||
reason: errorReason ?? 'There is no current evidence of the game or container state.',
|
||||
actionLabel: hostMissing ? 'Check host' : 'Open diagnostics',
|
||||
actionHref: hostMissing ? `/hosts/${encodeURIComponent(server.host_id!)}` : diagnostics,
|
||||
since: gameAt ?? containerAt
|
||||
|
||||
Reference in New Issue
Block a user