Fix active and match timer drift between status polls

Persist occupancy transitions with telemetry and carry timer anchors through live updates. Cover delayed polling, lobby rejoining, and lifecycle boundaries with regression tests.

Fixes #47
This commit is contained in:
2026-09-27 23:29:04 -04:00
parent b904ddbc48
commit 2fa3792482
8 changed files with 210 additions and 15 deletions
+12 -4
View File
@@ -50,14 +50,22 @@ export function selectServerObservation<T extends ServerObservation>(server: T,
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;
// New reports carry persisted anchors through both SSE and HTTP. Keep
// the fallback for telemetry saved before server timing was included.
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
};
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 };
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 };
}
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;