Simplify telemetry to three game events
This commit is contained in:
@@ -33,6 +33,16 @@ export async function POST({ params, request }: { params: { id: string }; reques
|
||||
betaState(id, false);
|
||||
} else if (body.action === 'resume') {
|
||||
if (stored.outcome !== 'paused') throw new Error('Only paused plans can resume; failed or expired plans need a fresh attempt');
|
||||
const runs = db().prepare('SELECT * FROM beta_test_runs WHERE beta_id = ? AND retired_at IS NULL').all(id) as Parameters<typeof readMetrics>[0][];
|
||||
for (const run of runs) {
|
||||
const metrics = readMetrics(run);
|
||||
delete metrics.match;
|
||||
delete metrics.lastSample;
|
||||
delete metrics.wasHealthy;
|
||||
metrics.streaks = {};
|
||||
for (const r of plan.requirements) if (r.kind === 'healthy-minutes') metrics.values[r.id] = 0;
|
||||
saveMetrics(run, metrics);
|
||||
}
|
||||
setOutcome(id, 'active', null);
|
||||
} else if (body.action === 'retry') {
|
||||
// Fresh attempts keep their predecessors as audit history. Preserve the original deadline:
|
||||
|
||||
@@ -1,88 +1,27 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getServer, listServers } from '$lib/server/fleet/servers.js';
|
||||
import { parseGameEvent, type GameTelemetry } from '$lib/telemetry.js';
|
||||
import { getServer } from '$lib/server/fleet/servers.js';
|
||||
import { saveTelemetry } from '$lib/server/fleet/telemetry.js';
|
||||
import { emit } from '$lib/server/pipeline/bus.js';
|
||||
import { recordGameEvent } from '$lib/server/fleet/beta-tests.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { acceptTestSignal, parseTestSignal } from '$lib/server/fleet/beta-tests.js';
|
||||
|
||||
// Reporters fetch this once at process startup and keep the token for that boot.
|
||||
export async function GET({ url }: { url: URL }) {
|
||||
const server = url.searchParams.get('server');
|
||||
if (!server || !getServer(server)) return json({ error: 'Unknown server' }, { status: 404 });
|
||||
const runs = db().prepare(`SELECT r.id AS runId, r.beta_id AS betaId FROM beta_test_runs r
|
||||
JOIN betas b ON b.id = r.beta_id JOIN beta_test_plans p ON p.beta_id = b.id
|
||||
WHERE r.server = ? AND r.retired_at IS NULL AND b.state IN ('running','ready-promote')
|
||||
AND p.outcome IN ('active','passed')`).all(server);
|
||||
return json({ runs }, { headers: { 'Cache-Control': 'no-store' } });
|
||||
}
|
||||
import { emit } from '$lib/server/pipeline/bus.js';
|
||||
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
console.log('[telemetry] received POST /api/telemetry');
|
||||
// Encode payloads as JSON so script-supplied newlines cannot forge log lines.
|
||||
// Bound the preview to keep malformed requests from flooding container logs.
|
||||
const raw = await request.text();
|
||||
const context = {
|
||||
contentType: request.headers.get('content-type'),
|
||||
payloadPreview: raw.slice(0, 4096),
|
||||
payloadTruncated: raw.length > 4096
|
||||
};
|
||||
function reject(status: number, error: string, details: Record<string, unknown> = {}) {
|
||||
console.log(`[telemetry] rejected ${status}: ${JSON.stringify({ error, ...context, ...details })}`);
|
||||
return json({ ok: false, error, details }, { status });
|
||||
}
|
||||
if (request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') {
|
||||
return reject(415, 'Content-Type must be application/json', { receivedContentType: context.contentType });
|
||||
return json({ ok: false, error: 'Content-Type must be application/json' }, { status: 415 });
|
||||
}
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
return reject(400, 'Malformed JSON', { parseError: (error as Error).message });
|
||||
}
|
||||
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
||||
return reject(400, 'Payload must be a JSON object');
|
||||
}
|
||||
const issues: string[] = [];
|
||||
if (body.runId === undefined && body.event !== 'round_change') issues.push('Session telemetry requires runId');
|
||||
if (typeof body.server !== 'string' || !body.server.length || body.server.length > 255) {
|
||||
issues.push('server must be a registered server name (1–255 characters)');
|
||||
}
|
||||
for (const field of ['round', 'players']) {
|
||||
if (!Number.isSafeInteger(body[field]) || body[field] < 0) {
|
||||
issues.push(`${field} must be a nonnegative safe integer (JSON number, not a string)`);
|
||||
}
|
||||
}
|
||||
if (issues.length) {
|
||||
return reject(400, 'Invalid telemetry payload', { issues });
|
||||
}
|
||||
const server = getServer(body.server);
|
||||
if (!server) {
|
||||
const registeredServers = listServers().filter((entry) => !entry.destroyed_at).map((entry) => entry.name);
|
||||
return reject(404, 'Server is not registered', {
|
||||
receivedServer: body.server,
|
||||
hint: 'Send the exact Electric Cherry server name in the server field; matching is case-sensitive.',
|
||||
registeredServers
|
||||
});
|
||||
}
|
||||
if (server.destroyed_at) {
|
||||
return reject(404, 'Server has been destroyed', {
|
||||
receivedServer: body.server, destroyedAt: server.destroyed_at
|
||||
});
|
||||
}
|
||||
let test: { accepted: boolean; reason?: string } | undefined;
|
||||
if (body.runId !== undefined) {
|
||||
try {
|
||||
const signal = parseTestSignal(body);
|
||||
test = acceptTestSignal(body.server, body.runId, body.eventId, signal);
|
||||
} catch (error) { return reject(400, (error as Error).message); }
|
||||
if (body.event !== 'round_change') return json({ ok: true, test });
|
||||
}
|
||||
const telemetry = {
|
||||
server: body.server as string, round: body.round as number,
|
||||
players: body.players as number, receivedAt: new Date().toISOString()
|
||||
};
|
||||
saveTelemetry(telemetry);
|
||||
console.log(`[telemetry] accepted round_change ${JSON.stringify(telemetry)}`);
|
||||
let value: unknown;
|
||||
try { value = await request.json(); }
|
||||
catch { return json({ ok: false, error: 'Malformed JSON' }, { status: 400 }); }
|
||||
let telemetry: GameTelemetry;
|
||||
try { telemetry = { ...parseGameEvent(value), receivedAt: new Date().toISOString() }; }
|
||||
catch (error) { return json({ ok: false, error: (error as Error).message }, { status: 400 }); }
|
||||
const server = getServer(telemetry.server);
|
||||
if (!server || server.destroyed_at) return json({ ok: false, error: 'Server is not registered or has been destroyed' }, { status: 404 });
|
||||
db().transaction(() => {
|
||||
saveTelemetry(telemetry);
|
||||
recordGameEvent(telemetry);
|
||||
})();
|
||||
emit({ type: 'server-telemetry', entries: [telemetry] });
|
||||
return json({ ok: true, telemetry, test });
|
||||
return json({ ok: true, telemetry });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user