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>
80 lines
2.5 KiB
JavaScript
80 lines
2.5 KiB
JavaScript
import { DatabaseSync } from 'node:sqlite';
|
|
import { mkdirSync } from 'node:fs';
|
|
|
|
// Schema for the private incident notebook and the Git fix-candidate inbox.
|
|
// Deployment-receipt tables live in receipts.mjs, next to the code that
|
|
// actually owns that data — this file only owns the notebook's own shape.
|
|
const SCHEMA = `
|
|
PRAGMA journal_mode=WAL;
|
|
PRAGMA foreign_keys=ON;
|
|
CREATE TABLE IF NOT EXISTS incidents(
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
project TEXT,
|
|
status TEXT NOT NULL DEFAULT 'investigating',
|
|
severity TEXT NOT NULL DEFAULT 'annoyance',
|
|
detected_at TEXT NOT NULL,
|
|
symptom TEXT NOT NULL,
|
|
impact TEXT DEFAULT '',
|
|
root_cause TEXT DEFAULT '',
|
|
confidence TEXT DEFAULT 'unknown',
|
|
fix TEXT DEFAULT '',
|
|
verification TEXT DEFAULT '',
|
|
prevention TEXT DEFAULT '',
|
|
remaining_risk TEXT DEFAULT '',
|
|
lesson TEXT DEFAULT '',
|
|
public_summary TEXT DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS timeline(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
incident_id TEXT NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
|
|
at TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
body TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS hypotheses(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
incident_id TEXT NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
|
|
statement TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'untested',
|
|
evidence TEXT DEFAULT ''
|
|
);
|
|
CREATE TABLE IF NOT EXISTS candidates(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
repo TEXT NOT NULL,
|
|
sha TEXT NOT NULL,
|
|
committed_at TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
paths TEXT DEFAULT '',
|
|
reason TEXT NOT NULL,
|
|
state TEXT NOT NULL DEFAULT 'new',
|
|
incident_id TEXT,
|
|
UNIQUE(repo, sha)
|
|
);`;
|
|
|
|
// Additive, best-effort migrations for columns that shipped after the
|
|
// initial schema. SQLite has no "ADD COLUMN IF NOT EXISTS", so each one is
|
|
// tried and a "duplicate column" failure is the expected steady state once
|
|
// it's already applied everywhere.
|
|
const MIGRATIONS = [
|
|
"ALTER TABLE incidents ADD COLUMN published_at TEXT DEFAULT ''",
|
|
"ALTER TABLE incidents ADD COLUMN public_slug TEXT DEFAULT ''",
|
|
];
|
|
|
|
export function openDb(path = 'data/trace.db') {
|
|
mkdirSync('data', { recursive: true });
|
|
mkdirSync('data/exports', { recursive: true });
|
|
const db = new DatabaseSync(path);
|
|
db.exec(SCHEMA);
|
|
for (const sql of MIGRATIONS) {
|
|
try {
|
|
db.exec(sql);
|
|
} catch (error) {
|
|
if (!String(error.message).includes('duplicate column')) throw error;
|
|
}
|
|
}
|
|
return db;
|
|
}
|