server.mjs mixed SQL schema, HTML-rendering functions, and route handling in one 476-line file. Split by concern: - src/db.mjs — schema + migrations (openDb()) - src/views.mjs — pure render functions: layout, incidentForm, publicMarkdown/Incident/Detail, no db or network access - src/style.mjs — the stylesheet, now genuinely shared instead of duplicated (receipts.mjs's /events page had its own near-copy, which is exactly what let their <nav> definitions drift out of sync a few commits ago) - src/http-utils.mjs — form()/redirect(), used by more than one route - src/routes/incidents.mjs — the private notebook + Git inbox + publish/unpublish, auth-gated once at the top instead of per-route - src/routes/public-failures.mjs — the read-only /api/v1/public/failures* projection server.mjs is now 79 lines of composition: open the db, build each route module, wire the dispatch order, listen. Also dropped the old POST /api/receipts endpoint and its `receipts` table — dead since the v1 events API replaced it, already flagged as "scheduled for removal" in the README. Verified behavior is unchanged, not just "looks the same": full route smoke test (401/200/404 in the right places, incident create→publish→ public-projection round trip, receipt ingestion), tests pass, and before/after screenshots of every page are pixel-identical. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
72 lines
2.2 KiB
JavaScript
72 lines
2.2 KiB
JavaScript
// The read-only, unauthenticated projection of published incidents —
|
|
// consumed by goonk's /failures page at build and runtime. Never touches
|
|
// an incident until it has been explicitly published (see
|
|
// routes/incidents.mjs's publish/unpublish action); an unpublished
|
|
// incident is invisible here regardless of how much of it is filled in.
|
|
import { publicIncident, publicDetail, publicMarkdown } from '../views.mjs';
|
|
|
|
const jsonHeaders = {
|
|
'content-type': 'application/json; charset=utf-8',
|
|
'access-control-allow-origin': '*',
|
|
'cache-control': 'public, max-age=60',
|
|
};
|
|
|
|
export function createPublicFailuresRoutes(db) {
|
|
async function handle(req, res, path) {
|
|
if (req.method !== 'GET') return false;
|
|
|
|
if (path === '/api/v1/public/failures') {
|
|
const rows = db
|
|
.prepare("SELECT * FROM incidents WHERE published_at<>'' ORDER BY detected_at DESC")
|
|
.all();
|
|
res.writeHead(200, jsonHeaders).end(
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
failures: rows.map(publicIncident),
|
|
}),
|
|
);
|
|
return true;
|
|
}
|
|
|
|
const md = path.match(/^\/api\/v1\/public\/failures\/([^/]+)\.md$/);
|
|
if (md) {
|
|
const x = db
|
|
.prepare("SELECT * FROM incidents WHERE public_slug=? AND published_at<>''")
|
|
.get(decodeURIComponent(md[1]));
|
|
if (!x) {
|
|
res.writeHead(404).end('Not found');
|
|
return true;
|
|
}
|
|
res
|
|
.writeHead(200, { ...jsonHeaders, 'content-type': 'text/markdown; charset=utf-8' })
|
|
.end(publicMarkdown(x));
|
|
return true;
|
|
}
|
|
|
|
const detail = path.match(/^\/api\/v1\/public\/failures\/([^/]+)$/);
|
|
if (detail) {
|
|
const x = db
|
|
.prepare("SELECT * FROM incidents WHERE public_slug=? AND published_at<>''")
|
|
.get(decodeURIComponent(detail[1]));
|
|
if (!x) {
|
|
res
|
|
.writeHead(404, {
|
|
'content-type': 'application/json; charset=utf-8',
|
|
'access-control-allow-origin': '*',
|
|
})
|
|
.end('{"error":"not_found"}');
|
|
return true;
|
|
}
|
|
res
|
|
.writeHead(200, jsonHeaders)
|
|
.end(JSON.stringify({ schemaVersion: 1, failure: publicDetail(x) }));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return { handle };
|
|
}
|