fix: honor Komodo health and prefer fresh telemetry across fleet status

This commit is contained in:
2026-09-27 15:34:30 -04:00
parent 46a89ea4b0
commit 8ea696417f
27 changed files with 488 additions and 315 deletions
+68
View File
@@ -0,0 +1,68 @@
import { freshStatusTimestamp } from './server-freshness.js';
import { parseGameEvent, type GameTelemetry } from './telemetry.js';
export interface StatusPlayer {
num: number | null;
score: number | null;
ping: number | null;
guid: string | null;
name: string;
}
export interface GameObservation {
last_players: number | null;
last_map: string | null;
last_gametype: string | null;
last_seen_at: string | null;
last_error: string | null;
roster: StatusPlayer[] | null;
}
export interface ServerObservation extends GameObservation {
name: string;
health: 'online' | 'stale' | 'offline' | 'starting' | 'provisioning';
container_state?: string | null;
container_checked_at?: string | null;
last_checked_at?: string | null;
telemetry_after?: string | null;
status_source?: 'telemetry' | 'rcon' | null;
rcon_status?: GameObservation;
telemetry?: GameTelemetry;
active_since?: string | null;
inactive_since?: string | null;
game_started_at?: string | null;
game_ended_at?: string | null;
}
export function freshTelemetry(report: GameTelemetry | undefined, now: number, after?: string | null): report is GameTelemetry {
if (!report || !freshStatusTimestamp(report.receivedAt, now)) return false;
if (after && (!Number.isFinite(Date.parse(after)) || Date.parse(report.receivedAt) <= Date.parse(after))) return false;
try { parseGameEvent(report); return true; } catch { return false; }
}
/** Display policy: use one current roster/count source, keeping raw RCON for diagnostics/fallback. */
export function selectServerObservation<T extends ServerObservation>(server: T, report: GameTelemetry | undefined, now: number): T {
if (server.telemetry && (!report || Date.parse(server.telemetry.receivedAt) > Date.parse(report.receivedAt))) report = server.telemetry;
const raw = server.rcon_status ?? server;
const result: T = { ...server, ...raw, telemetry: report, status_source: raw.last_seen_at ? 'rcon' : null };
if (['provisioning','starting'].includes(server.health) || (server.container_state && server.container_state !== 'running') || (!server.container_state && server.health === 'offline')) {
return { ...result, health: server.health, last_error: null, last_players: null, roster: null, status_source: null };
}
const checked = server.container_checked_at ?? server.last_checked_at ?? raw.last_seen_at;
const containerFresh = server.container_state === 'running' && freshStatusTimestamp(checked ?? null, now);
if (containerFresh && report?.server === server.name && freshTelemetry(report, now, server.telemetry_after)) {
const players = report.players.length;
return { ...result, health: 'online', status_source: 'telemetry', last_players: players,
last_map: report.map, last_seen_at: report.receivedAt, last_error: null,
roster: report.players.map(player => ({ num: null, score: 'score' in player ? player.score : null,
ping: null, guid: player.guid, name: player.name })),
active_since: players > 0 ? server.active_since ?? report.receivedAt : null,
inactive_since: players === 0 ? server.inactive_since ?? report.receivedAt : null,
game_started_at: players === 0 ? null : report.event === 'game_start' ? report.receivedAt : server.game_started_at,
game_ended_at: players === 0 || report.event === 'game_start' ? null : report.event === 'game_end' ? report.receivedAt : server.game_ended_at };
}
const after = !server.telemetry_after || Date.parse(raw.last_seen_at ?? '') > Date.parse(server.telemetry_after);
const fresh = after && freshStatusTimestamp(raw.last_seen_at, now) && !raw.last_error;
const containerExpired = !!server.container_checked_at && !containerFresh;
const age = now - Date.parse(raw.last_seen_at ?? '');
return { ...result, health: fresh && !containerExpired ? 'online'
: after && age >= 0 && age <= 120_000 ? 'stale' : 'offline' };
}