Audit cuts

How the narrative pages under /audit are built.

Audit pages answer "why is this number what it is?" They sit one click below the cockpit and break a topic — revenue, clients, journeys — into a sequence of small charts (cuts) with prose between them.

File layout

Each cut is a Python module under api/audit/cuts/<cut_id>.py exporting two things:

META = {
    "page": "clients",         # /audit/<page>
    "title": "...",
    "window_default": "30d",
}

def compute(conn, *, window: str) -> dict:
    # Returns the JSON payload the renderer consumes.
    ...

The module is the source of truth. api.scripts.refresh_audit auto-syncs heartbeat.audit_cut_registry from disk — no per-file SQL, no DB-level write guard. heartbeat.audit_cut_snapshot caches the payload per (cut_id, window_key).

Rendering

Each leaf page (/audit/<page>/page.tsx) is ~30 lines:

export default function Page() {
  return <AuditPage page="clients" />;
}

The page-level scaffolding lives in dashboard/app/audit/_shared/AuditPage.tsx. A cut_id → React component registry in _shared/cuts/index.ts maps each cut to its renderer.

Adding a chart

  1. Drop a backend module: api/audit/cuts/<cut_id>.py with META + compute.
  2. Drop a renderer: dashboard/app/audit/_shared/cuts/<cut_id>.tsx.
  3. Register the renderer in _shared/cuts/index.ts.
  4. Run ./bin/deploy.sh (or at least re-run python -m api.scripts.refresh_audit).

Without refresh_audit, new cuts surface as 404 on the audit page — the registry is what the page reads from, and the registry only knows what disk-sync has told it.

Verification

Cuts carry the same verification_state and owner columns as metrics, PATCHed via /api/audit/cuts/{id}/verification. Auto-saves on owner blur; both fields are independent.