Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+114
-27
@@ -36,51 +36,112 @@ export interface ServerObservation extends GameObservation {
|
||||
}
|
||||
|
||||
/** Keep a first transient monitoring failure in diagnostics without demoting health. */
|
||||
export function rconHealthError(status: Pick<GameObservation, 'last_error' | 'rcon_failures'>): string | null {
|
||||
export function rconHealthError(
|
||||
status: Pick<GameObservation, 'last_error' | 'rcon_failures'>
|
||||
): string | null {
|
||||
const reason = status.last_error?.split(':')[0];
|
||||
return status.rcon_failures === 1 && (reason === 'timeout' || reason === 'send-error') ? null : status.last_error;
|
||||
return status.rcon_failures === 1 && (reason === 'timeout' || reason === 'send-error')
|
||||
? null
|
||||
: status.last_error;
|
||||
}
|
||||
|
||||
export function freshTelemetry(report: GameTelemetry | undefined, now: number, after?: string | null): report is GameTelemetry {
|
||||
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; }
|
||||
if (
|
||||
after &&
|
||||
(!Number.isFinite(Date.parse(after)) || Date.parse(report.receivedAt) <= Date.parse(after))
|
||||
)
|
||||
return false;
|
||||
try {
|
||||
parseGameEvent(report);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function playerMatchGuid(guid: string, game: string | null | undefined, source: 'rcon' | 'telemetry'): string {
|
||||
function playerMatchGuid(
|
||||
guid: string,
|
||||
game: string | null | undefined,
|
||||
source: 'rcon' | 'telemetry'
|
||||
): string {
|
||||
if (game !== 't6') return guid;
|
||||
// T6 status prints hexadecimal account IDs; telemetry sends decimal IDs.
|
||||
// Keep integer precision for IDs beyond Number.MAX_SAFE_INTEGER.
|
||||
if (source === 'rcon' && /^(?:0x)?[0-9a-f]+$/i.test(guid)) return BigInt(`0x${guid.replace(/^0x/i, '')}`).toString();
|
||||
if (source === 'rcon' && /^(?:0x)?[0-9a-f]+$/i.test(guid))
|
||||
return BigInt(`0x${guid.replace(/^0x/i, '')}`).toString();
|
||||
if (source === 'telemetry' && /^\d+$/.test(guid)) return BigInt(guid).toString();
|
||||
return guid;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
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, rcon_status: raw, last_error: rconHealthError(raw), telemetry: report, status_source: raw.last_seen_at ? 'rcon' : null };
|
||||
const result: T = {
|
||||
...server,
|
||||
...raw,
|
||||
rcon_status: raw,
|
||||
last_error: rconHealthError(raw),
|
||||
telemetry: report,
|
||||
status_source: raw.last_seen_at ? 'rcon' : null
|
||||
};
|
||||
const containerUnknown = server.container_state === 'unknown';
|
||||
if (['provisioning','starting'].includes(server.health) || (server.container_state && server.container_state !== 'running' && !containerUnknown) || (!server.container_state && server.health === 'offline')) {
|
||||
return { ...result, health: server.health, last_error: null, last_players: null, roster: null, status_source: null };
|
||||
if (
|
||||
['provisioning', 'starting'].includes(server.health) ||
|
||||
(server.container_state && server.container_state !== 'running' && !containerUnknown) ||
|
||||
(!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);
|
||||
const after = !server.telemetry_after || Date.parse(raw.last_seen_at ?? '') > Date.parse(server.telemetry_after);
|
||||
const containerFresh =
|
||||
server.container_state === 'running' && freshStatusTimestamp(checked ?? null, now);
|
||||
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) && !result.last_error;
|
||||
if (containerFresh && report?.server === server.name && freshTelemetry(report, now, server.telemetry_after)) {
|
||||
if (
|
||||
containerFresh &&
|
||||
report?.server === server.name &&
|
||||
freshTelemetry(report, now, server.telemetry_after)
|
||||
) {
|
||||
const players = report.players.length;
|
||||
// Telemetry owns roster membership. Supplement missing pings only with
|
||||
// unambiguous RCON identities from the current map and server lifecycle.
|
||||
const pings = new Map<string, number | null>();
|
||||
const guidKey = (guid: string, source: 'rcon' | 'telemetry') => playerMatchGuid(guid, server.game, source);
|
||||
const guidKey = (guid: string, source: 'rcon' | 'telemetry') =>
|
||||
playerMatchGuid(guid, server.game, source);
|
||||
if (fresh && raw.last_map === report.map) {
|
||||
for (const player of raw.roster ?? []) {
|
||||
const ping = player.ping;
|
||||
if (player.guid) {
|
||||
const guid = guidKey(player.guid, 'rcon');
|
||||
pings.set(guid, pings.has(guid) || ping === null || !Number.isSafeInteger(ping) || ping < 0 ? null : ping);
|
||||
pings.set(
|
||||
guid,
|
||||
pings.has(guid) || ping === null || !Number.isSafeInteger(ping) || ping < 0
|
||||
? null
|
||||
: ping
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,23 +150,49 @@ export function selectServerObservation<T extends ServerObservation>(server: T,
|
||||
const timing = report.timing?.server ?? {
|
||||
activeSince: server.active_since ?? report.receivedAt,
|
||||
inactiveSince: server.inactive_since ?? report.receivedAt,
|
||||
gameStartedAt: report.event === 'game_start' ? report.receivedAt : server.game_started_at,
|
||||
gameEndedAt: report.event === 'game_start' ? null : report.event === 'game_end' ? report.receivedAt : server.game_ended_at
|
||||
gameStartedAt:
|
||||
report.event === 'game_start' ? report.receivedAt : server.game_started_at,
|
||||
gameEndedAt:
|
||||
report.event === 'game_start'
|
||||
? null
|
||||
: report.event === 'game_end'
|
||||
? report.receivedAt
|
||||
: server.game_ended_at
|
||||
};
|
||||
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: player.ping ?? pings.get(guidKey(player.guid, 'telemetry')) ?? null, guid: player.guid, name: player.name })),
|
||||
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: player.ping ?? pings.get(guidKey(player.guid, 'telemetry')) ?? null,
|
||||
guid: player.guid,
|
||||
name: player.name
|
||||
})),
|
||||
active_since: players > 0 ? timing.activeSince : null,
|
||||
inactive_since: players === 0 ? timing.inactiveSince : null,
|
||||
game_started_at: players === 0 ? null : timing.gameStartedAt,
|
||||
game_ended_at: players === 0 ? null : timing.gameEndedAt };
|
||||
game_ended_at: players === 0 ? null : timing.gameEndedAt
|
||||
};
|
||||
}
|
||||
// A current authenticated RCON reply can establish game liveness when
|
||||
// inspection is unavailable, without claiming the container was inspected.
|
||||
const containerExpired = !containerUnknown && !!server.container_checked_at && !containerFresh;
|
||||
const age = now - Date.parse(raw.last_seen_at ?? '');
|
||||
const health = fresh && !containerExpired ? 'online'
|
||||
: after && age >= 0 && age <= 120_000 ? 'stale' : 'offline';
|
||||
return { ...result, health, last_error: health === 'online' ? result.last_error : raw.last_error };
|
||||
const health =
|
||||
fresh && !containerExpired
|
||||
? 'online'
|
||||
: after && age >= 0 && age <= 120_000
|
||||
? 'stale'
|
||||
: 'offline';
|
||||
return {
|
||||
...result,
|
||||
health,
|
||||
last_error: health === 'online' ? result.last_error : raw.last_error
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user