Files
trace/src/db.mjs
T
Fredrik JohanssonandClaude Sonnet 5 0d825b4458 Split server.mjs into modules: db, views, routes
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>
2026-07-21 05:27:27 +02:00

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;
}