50 lines
3.1 KiB
TypeScript
50 lines
3.1 KiB
TypeScript
import { db } from '../db/index.js';
|
|
import { enrichGameChatTimeline, type ChatMatch, type ChatRoundReport, type ContextualChatEntry } from '../../game-chat.js';
|
|
import { parseGameEvent } from '../../telemetry.js';
|
|
import { TELEMETRY_REQUEST_RETENTION } from '../../telemetry-diagnostics.js';
|
|
import type { ServerRow } from './servers.js';
|
|
import type { TailResult } from './logs.js';
|
|
|
|
/** Historical context is scoped to this registration, including round-one matches. */
|
|
export function serverChatTimeline(server: ServerRow, tail: TailResult): ContextualChatEntry[] {
|
|
const handle = db();
|
|
const matches = handle.prepare(`SELECT started_at AS startedAt, ended_at AS endedAt,
|
|
last_received_at AS lastReceivedAt, status, start_observed AS startObserved
|
|
FROM leaderboard_matches WHERE server = ? AND server_deleted_at IS NULL
|
|
ORDER BY id DESC LIMIT 2000`).all(server.name) as Array<Omit<ChatMatch, 'startObserved'> & { startObserved: number }>;
|
|
|
|
// Diagnostic history survives deletion. Its receipt time must belong to the
|
|
// current registration so a reused server name cannot inherit old rounds.
|
|
const cutoff = new Date(Date.now() - TELEMETRY_REQUEST_RETENTION.maxAgeDays * 86_400_000).toISOString();
|
|
const reports = handle.prepare(`SELECT received_at AS receivedAt, event, round,
|
|
player_count AS playerCount FROM telemetry_requests
|
|
WHERE server = ? AND status = 200 AND received_at >= ?
|
|
AND julianday(received_at) >= julianday(?)
|
|
AND event IN ('game_start', 'round_change', 'heartbeat', 'game_end')
|
|
AND player_count >= 0 AND (event = 'game_start' OR round >= 0)
|
|
ORDER BY id DESC LIMIT ?`).all(server.name, cutoff, server.created_at,
|
|
TELEMETRY_REQUEST_RETENTION.maxPerServer) as ChatRoundReport[];
|
|
|
|
// Saving gameplay does not depend on diagnostic logging succeeding. Keep the
|
|
// surviving accepted snapshot useful when its request log was not recorded.
|
|
const latest = handle.prepare(`SELECT received_at AS receivedAt, payload FROM server_telemetry
|
|
WHERE server = ? AND received_at >= ? AND julianday(received_at) >= julianday(?)`)
|
|
.get(server.name, cutoff, server.created_at) as { receivedAt: string; payload: string | null } | undefined;
|
|
if (latest?.payload) {
|
|
try {
|
|
const event = parseGameEvent(JSON.parse(latest.payload));
|
|
const round = event.event === 'game_start' ? null : event.round;
|
|
if (event.server === server.name && !reports.some(report => report.receivedAt === latest.receivedAt
|
|
&& report.event === event.event && report.round === round && report.playerCount === event.players.length)) {
|
|
reports.push({ receivedAt: latest.receivedAt, event: event.event, round, playerCount: event.players.length });
|
|
}
|
|
} catch { /* Legacy malformed snapshots cannot establish chat context. */ }
|
|
}
|
|
|
|
return enrichGameChatTimeline(tail.lines, {
|
|
mtime: tail.mtime,
|
|
matches: matches.map(match => ({ ...match, startObserved: Boolean(match.startObserved) })),
|
|
reports
|
|
});
|
|
}
|