Add telemetry dashboard and automatic server names; record Workshop 0.0.137
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user