Add telemetry dashboard and automatic server names; record Workshop 0.0.137
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-01 19:29:54 +01:00
parent b8dfa9184e
commit 6ba3036de6
23 changed files with 1509 additions and 16 deletions
+97 -1
View File
@@ -41,7 +41,7 @@ async function detail(e) {
}
before(async () => {
mf = new Miniflare(convertV4MiniflareOptions({host: '127.0.0.1', port: 0, workers: [{name: 'telemetry',
modules: ['index.js', 'validation.js'].map(name => ({type: 'ESModule', path: new URL(`../src/${name}`, import.meta.url).pathname})),
modules: ['index.js', 'validation.js', 'dashboard-api.js', 'dashboard.js', 'dashboard-client.js'].map(name => ({type: 'ESModule', path: new URL(`../src/${name}`, import.meta.url).pathname})),
compatibilityDate: '2026-10-23', d1Databases: ['DB'],
ratelimits: {
REQUEST_LIMITER: {namespace_id: '1001', simple: {limit: 300, period: 60}},
@@ -302,3 +302,99 @@ test('map records count victories and defeats across servers, excluding other ma
const row = maps.maps.find(m => m.terrain === terrain.toLowerCase());
assert.deepEqual(row, {terrain: terrain.toLowerCase(), completed: 2, wins: 1, losses: 1});
});
test('dashboard shell is public, same-origin and contains no telemetry or credentials', async () => {
for (const path of ['/dashboard', '/dashboard/', '/dashboard/app.js', '/dashboard/style.css']) {
const response = await call(path);
assert.equal(response.status, 200);
assert.match(response.headers.get('content-security-policy'), /frame-ancestors 'none'/);
assert.equal(response.headers.get('cache-control'), 'no-store');
const content = await response.text();
assert.ok(!content.includes(ADMIN) && !content.includes(READ) && !content.includes(token));
}
assert.equal((await call('/dashboard', {method: 'POST'})).status, 405);
assert.equal((await call('/api/dashboard')).status, 401);
assert.equal((await call('/api/servers')).status, 401);
assert.equal((await call('/api/dashboard', {headers: {authorization: `Bearer ${token}`}})).status, 401);
assert.equal((await call('/api/servers', {headers: readAuth})).status, 200);
assert.equal((await call(`/api/servers/${serverId}`, {method: 'DELETE', headers: readAuth})).status, 401);
});
test('dashboard receipt freshness uses raw arrivals, independently of terminal snapshot selection', async () => {
const sid = crypto.randomUUID(), writeToken = 'dashboard-installation-secret-0123456789';
const complete = event({serverId: sid, serverName: '<img src=x onerror=alert(1)>', terrain: 'Everon dashboard', players: []});
assert.equal((await ingest(complete, writeToken)).status, 201);
const finalReceipt = (await db.prepare('SELECT received_at FROM missions WHERE server_id = ?').bind(sid).first()).received_at;
const delayed = {...complete, eventId: crypto.randomUUID(), type: 'heartbeat', phase: 1, outcome: '', occurredAt: 1, elapsedSeconds: 10};
assert.equal((await ingest(delayed, writeToken)).status, 201);
await db.prepare('UPDATE events SET received_at = ? WHERE server_id = ? AND event_id = ?').bind(finalReceipt + 40, sid, delayed.eventId).run();
const rows = await (await call(`/api/servers?q=${sid}`, {headers: readAuth})).json();
assert.equal(rows.servers.length, 1);
const server = rows.servers[0];
assert.equal(server.lastReceivedAt, finalReceipt + 40);
assert.equal(server.lastReportedAt, 1);
assert.equal(server.lastEventType, 'heartbeat');
assert.equal(server.wins, 1); assert.equal(server.losses, 0); assert.equal(server.unfinished, 0);
assert.equal(server.lastMissionId, complete.missionId);
assert.equal(server.name, complete.serverName); // Stored literally; browser renders as text.
assert.ok(!JSON.stringify(rows).includes('token_hash'));
const snapshot = await (await call(`/api/missions/${sid}/${complete.missionId}`, {headers: readAuth})).json();
assert.equal(snapshot.receivedAt, finalReceipt); assert.equal(snapshot.event.outcome, 'victory');
const summary = await (await call(`/api/dashboard?serverId=${sid}`, {headers: readAuth})).json();
assert.equal(summary.totals.missions, 1); assert.equal(summary.totals.events, 2);
assert.equal(summary.totals.wins, 1); assert.equal(summary.totals.winRate, 100);
assert.equal(summary.totals.servers, 1); assert.equal(summary.totals.lastReceivedAt, finalReceipt + 40);
assert.deepEqual(summary.maps.map(m => m.terrain), ['everon dashboard']);
assert.equal((await ingest(delayed, writeToken)).status, 200);
const duplicateSummary = await (await call(`/api/dashboard?serverId=${sid}`, {headers: readAuth})).json();
assert.equal(duplicateSummary.totals.events, 2);
const empty = await (await call(`/api/dashboard?serverId=${crypto.randomUUID()}`, {headers: readAuth})).json();
assert.equal(empty.totals.lastReceivedAt, null); assert.equal(empty.totals.winRate, null);
assert.equal(empty.totals.missions, 0); assert.deepEqual(empty.maps, []);
});
test('dashboard server pagination, never-reporting servers and exact terrain filters', async () => {
const sid = crypto.randomUUID();
assert.equal((await post('/api/servers', {serverId: sid, name: 'Never reported'}, auth)).status, 201);
const first = await (await call('/api/servers?limit=1', {headers: readAuth})).json();
assert.equal(first.servers[0].serverId, sid);
assert.equal(first.servers[0].lastReceivedAt, null); assert.equal(first.servers[0].missions, 0);
const next = await (await call(`/api/servers?limit=1&before=${first.nextBefore}`, {headers: readAuth})).json();
assert.notEqual(next.servers[0].serverId, sid);
const matched = await (await call('/api/missions?terrain=EVERON%20DASHBOARD', {headers: readAuth})).json();
assert.equal(matched.missions.length, 1); assert.equal(matched.missions[0].outcome, 'victory');
const empty = await (await call('/api/missions?terrain=Missing%20island', {headers: readAuth})).json();
assert.equal(empty.missions.length, 0);
assert.equal((await call('/api/servers?q=' + 'a'.repeat(121), {headers: readAuth})).status, 400);
assert.equal((await call('/api/dashboard?serverId=bad', {headers: readAuth})).status, 400);
});
test('automatic server names arrive later, follow renames, and survive blanks, retries and old queued reports', async () => {
const sid = crypto.randomUUID(), secret = 'server-name-test-credential-0123456789';
const original = event({serverId: sid, serverName: '', players: [], occurredAt: 100});
assert.equal((await ingest(original, secret)).status, 201);
const name = async () => (await db.prepare('SELECT name FROM servers WHERE server_id = ?').bind(sid).first()).name;
assert.equal(await name(), '');
const report = (serverName, occurredAt) => event({serverId: sid, serverName, occurredAt, players: []});
const first = report(' Community Escape ', 200);
assert.equal((await ingest(first, secret)).status, 201); assert.equal(await name(), 'Community Escape');
const renamed = report('New name', 300);
assert.equal((await ingest(renamed, secret)).status, 201); assert.equal(await name(), 'New name');
assert.equal((await ingest(report(' ', 400), secret)).status, 201); assert.equal(await name(), 'New name');
assert.equal((await ingest(report('Old name', 150), secret)).status, 201); assert.equal(await name(), 'New name');
assert.equal((await ingest(first, secret)).status, 200); assert.equal(await name(), 'New name');
assert.equal((await ingest({...renamed, serverName: 'Conflict'}, secret)).status, 409); assert.equal(await name(), 'New name');
assert.equal((await ingest(report('Impersonated', 500), 'wrong-credential-0123456789abcdef123456')).status, 401);
assert.equal(await name(), 'New name');
const listing = await (await call(`/api/servers?q=${encodeURIComponent('New name')}`, {headers: readAuth})).json();
assert.equal(listing.servers[0].serverId, sid);
const missions = await (await call(`/api/missions?serverId=${sid}`, {headers: readAuth})).json();
assert.equal(missions.missions.find(m => m.missionId === original.missionId).serverName, 'New name');
assert.equal(missions.missions.find(m => m.missionId === first.missionId).serverName, 'Community Escape');
const snapshot = await (await call(`/api/missions/${sid}/${original.missionId}`, {headers: readAuth})).json();
assert.equal(snapshot.serverName, 'New name'); assert.equal(snapshot.event.serverName, ''); // No historical payload rewriting.
const managed = crypto.randomUUID();
const registered = await (await post('/api/servers', {serverId: managed, name: 'Operator label'}, auth)).json();
assert.equal((await ingest(event({serverId: managed, serverName: 'Game name', players: []}), registered.token)).status, 201);
assert.equal((await db.prepare('SELECT name FROM servers WHERE server_id = ?').bind(managed).first()).name, 'Operator label');
});