Local Dashboard (nova serve) — User Reference
Status: Experimental. Read this entire page before relying on the dashboard for anything that matters. Per: ADR-0027, Non-goals First shipped: v0.7
The dashboard is an opt-in local-only HTTP server (nova serve --experimental) that renders an interactive view over the same registry SQLite, lineage SQLite, and capsule directories the CLI reads. The CLI is the canonical interface; the dashboard is a satellite that surfaces the equivalent CLI command for every action.
Dashboard tabs (complete inventory)
The dashboard ships 30 tabs in 7 balanced workflow groups (Overview, Runs & Debug, Govern & Promote, Provenance & Trust, Compliance, Platform, Reports & Export). Every tab has a stable two-key navigation shortcut — press g then the tab's key (shown on hover and in the ? overlay). Since v0.98.3 the sidebar groups start collapsed (each showing its tab count); arriving at a tab opens its group, and that group can still be collapsed afterwards (a marker on the collapsed header shows where the active tab is); manual expand/collapse persists per browser in novafabric.sidebar-groups. Below 1024px the sidebar auto-collapses to the icon rail (v0.97.0); expanding it explicitly wins for the rest of the session. (Source of truth:
ui/dashboard/src/components/dashboard/Sidebar.tsx; verified by count against
NAV_GROUPS in that file.)
Deep links. Every tab is addressable as ?tab=<id>, and the Compliance tab is a hub
whose five panel groups are addressable as ?tab=compliance&sub=<group>
(frameworks · audits · privacy · exports · assurance, v0.97.0) — the same groups
the ⌘K palette lists. Since v0.98.2 all three navigation paths write the URL: clicking a
sidebar item, a g-key sequence, and the command palette all update ?tab= (and clear a
stale ?sub= when you leave the hub), so any view you reach by keyboard is shareable and
survives a reload. Tab ids have not changed since before the v0.97.0 regrouping, so existing
links still resolve.
Runs view state in the URL (experimental, since v0.103.0 — ADR-0232 D2). The whole Runs view is in the URL, so a pasted link reproduces exactly what you were looking at:
| Key | Meaning | Example |
|---|---|---|
q |
free-text search on run id / command | q=agent.py |
status |
status chip | status=error |
sort |
list order | sort=longest |
since / until |
date window (YYYY-MM-DD, until inclusive) |
since=2026-09-01 |
f |
filter-bar text (below) | f=status:error%20-model:gpt-4o |
scope |
ADR-0233 scope for f: node · root · tree |
scope=tree |
run |
the selected run — opens the inspector even if the run is not in the loaded page | run=01J… |
view |
inspector view: inspect · trace · integrity · lineage · secrets · forensics · children |
view=integrity |
Applying a filter, changing scope, status or dates adds a browser-history entry, so Back undoes it; typing and keyboard selection do not. Unknown values fall back to the default rather than rendering a state the UI cannot show. Switching to another tab drops these keys. What is deliberately not in the URL: the half-finished Compare selection (ADR-0036), checkboxes, unsubmitted filter text, and the Replay result view (it shows a result produced in your session, so a link to it would open on nothing).
Filter bar (experimental — ADR-0232 D1/D3, ADR-0233)
Under the Runs search box. Type terms, press Enter to apply:
status:error # dimension = value
-model:gpt-4o # exclude
status:error asset:"my agent" # terms are ANDed; quote values with spaces- It is
nova query --where, not a second query language. The server parses with the DSL's own parser, so the filterable fields are exactly the DSL's dimensions —asset,deployment_environment,variant,log_level,model,model_id,status,tag. Metrics (cost:>0.5) and wildcards (model:gpt-4*) are rejected with an explanation, becausenova querycannot filter by them either. The equivalentnova querycommand is shown under the results with a copy button. - Suggestions are observed values. After
model:the bar lists models actually present in the last 30 days (↑/↓ to move, Enter to complete). When more than 100 exist the list says it is partial rather than implying the value does not exist. - Scope (ADR-0233):
nodelists the matching runs;rootlists the root run of every capsule tree containing a match;treelists every capsule in those trees, siblings included. - Honest results (ADR-0234 D2): at most 200 runs are listed; the header says "N of M" and truncated when there are more, and a tree that is still filling or a capsule that could not be read is listed as a reason the result may be incomplete.
- While a filter is applied it owns the list: free-text search and the status chips are
disabled (use
status:in the bar). The date window still applies. - Saved views are
nova viewfiles (ADR-0232 D4, experimental). "Save current as" writes an ADR-0130 view thatnova view showandnova view runread, into the same directory ($NOVAFABRIC_VIEWS_DIRor./.novafabric/views), through the same fail-closed parser; there is no second store. What is saved: the filter bar's predicates (the status chip becomes astatuspredicate), the scope and the date window; sort is an advisory display preference. Free-text search is not saved, becausenova querycannot express it, and the bar says so. A view saved from the CLI is offered back when the bar can express it, and listed disabled with the reason when it cannot (anINor comparison predicate); it is never approximated. If the server routes are unavailable the bar falls back to browser-local views and says so. Verify from the CLI:nova view show <id>,nova view run <id>.
API (all read scope, token required): GET /api/filter/parse?f=,
GET /api/filter/runs?f=&scope=&since=&until=&limit=, GET /api/filter/suggest?dimension=&since=.
Saved views: GET /api/views (read), POST /api/views and DELETE /api/views/{view_id}
(operate, audited).
Dashboards view (experimental — ADR-0235 / ADR-0236)
Sidebar group Overview, key g then 4 (deep link ?tab=dashboards). It lists the
dashboards and widgets under $NOVAFABRIC_HOME/dashboards and renders every schema chart type
(line, bar, area, table, stat). The files are the storage; there is no database of
user dashboards. The same files are managed from the CLI with nova dashboard list|validate|apply|show|export (CLI reference).
- Refusals are visible. A file the validator refuses is listed by name with the validator's reason, so one bad paste cannot blank the page. A dashboard that references a missing or refused widget still shows that panel, says why, and is flagged incomplete.
- Absent is not zero (ADR-0234 D2). An undefined value reads "no value" and charts draw
it as a gap; a measured
0stays0. Aratio()(ADR-0236) is always shown with its numerator and denominator, and an undefined one says why (zero denominator, absent operand). - Export each widget or dashboard as its stored JSON, byte for byte, so unknown fields survive.
- Add or edit one document (paste or upload JSON, or a guided widget form). Validate
is a dry run (
POST /api/dashboards/validate,readscope, writes nothing): the server's own verdict, from the same loadersnova dashboard validate|applyuse, so the UI cannot accept what the CLI refuses. An accepted document is previewed as a diff against the stored bytes; Save (POST /api/dashboards/apply,operatescope, audited asdashboard_apply) stores exactly the previewed bytes, atomically. It refuses an id outside the path pattern, a symlinked target, abuiltindocument, and a body over 256 KiB (413). Saving sends the digest of the file the preview showed, so a file changed meanwhile is a 409 rather than a silent overwrite. - Not built: deleting from the UI, forking a
builtindocument on first edit, and bulk directory installs (usenova dashboard apply <dir>).
Read routes (read scope): GET /api/dashboards, /api/dashboards/{id},
/api/dashboards/{id}/export, /api/dashboard-widgets/{id}/data (runs only the query stored
in the widget file; the request carries no query text) and /api/dashboard-widgets/{id}/export.
Capsule explorer (experimental)
Selecting a run opens the inspector with a summary strip — status, start time, duration, exit code, model/tool call counts (mutating tools called out), the command — and the run's actions: Replay dry-run, Forensic replay, Export evidence (each through the usual confirm dialog with its CLI equivalent) and Copy link. The view switcher is a keyboard tablist (←/→, Home/End):
- Inspect / Trace — manifest, model and tool calls, waterfall (unchanged).
- Integrity — runs
nova verifyon demand. The outcomes are kept distinct: not sealed (nothing to verify — not a failure), verified, and failed with each failing check (signature, RFC 3161 timestamp, Merkle log inclusion) named. Without anovaseal.yamlon the server the capsule is verified from its own files, asnova verifydoes, and the verdict says so ("verified from the capsule alone": no local Merkle log, no operator trust anchors). Sealed but not verifiable here appears only against an older server that returns no checks. The bundled static dashboard picks this up after the next dashboard rebuild. - Lineage — the run's spool-lineage edges as neighbours in both directions; click one to open it (even if it is not in the loaded list) or Compare it with this run.
- Secrets / Forensics / Children — unchanged.
Evidence cart (experimental — ADR-0239)
Collect runs while you investigate, then export them as one signed Evidence Bundle.
- Add: the + cart button on a run's action row, or tick several runs and press + cart in the selection banner. The cart bar appears above the Runs view only once it holds something.
- What the cart holds: references (
run+ id), each stamped with when it was added and the view URL it was added from. Nothing is copied at add time — every reference is read once, at export, so the bundle reflects one point in time. The cart lives in this browser session (sessionStorage) and is never stored on the server; closing the session clears it. - Export cart needs an
admincredential and asks for confirmation. The server resolves every reference, writes the bundle to~/.novafabric/evidence/cart-<id>-<time>.zip(or$NOVAFABRIC_EVIDENCE_DIR), and shows its path, SHA-256 and the command that checks it:nova verify <bundle>. The bundle carries acuration.json(covered by the manifest hash) stating it is an operator-curated subset, never an exhaustive one, with each item's provenance. - Unresolvable references stop the export. A run that no longer exists is listed with
the reason, and nothing is written. Export anyway and record the omissions proceeds and
records the missing items in
curation.json; there is no path that silently produces a smaller bundle. Cart kinds other thanrun/capsule(charts, diffs, lineage queries) have no resolver yet and are reported as unresolved (planned). - Legal holds: held runs export, and the bundle says so (
contains_held_evidence, hold ids per item). The export never places, releases or changes a hold. - Audited, or not exported: each export appends an
evidence.exportentry to the hash-chained audit log ($NOVAFABRIC_AUDIT_LOG_PATH, else$XDG_DATA_HOME/novafabric/audit.jsonl, else~/.local/share/novafabric/audit.jsonl) naming the actor, the items and the bundle digest, plus a dashboard audit record. If the chained entry cannot be written, the bundle is deleted and the export answers 503. - Bounds: at most 50 items and 256 MiB of capsule content per export, one export
at a time per server (429 otherwise). A cart that resolves to a single run is refused
— use the run's own Export evidence action (
nova export-evidence).
API: POST /api/evidence/cart/export (admin scope) with
{items: [{kind, ref, added_at, added_from?, note?}], confirmed: true, accept_unresolved?}.
Honest aggregates and the Runs aggregate strip (experimental — ADR-0234)
Every run aggregate the dashboard serves now carries an aggregate verdict. When the number
cannot be computed faithfully, the endpoint refuses — it says why and what would make it
computable — instead of returning a zero or a partial total.
| Endpoint | Refuses when | What the response holds then |
|---|---|---|
GET /api/analytics/summary |
no runs index while capsules exist (source_unavailable); the index holds fewer runs than the capsules on disk (truncated_source); the caller passes a filter-bar text f, a status other than all, or a search q it cannot apply (unpushable_filter) |
totals: null, no buckets |
GET /api/stats |
the runs index is behind the disk (truncated_source) |
run_count, failed_run_count, passed_run_count: null (asset counts unaffected) |
GET /api/cost/report |
the store did not answer (source_unavailable); every call in the window is unpriced (absent_contributor) |
totals: null, or cost_usd: null with the exact token counts kept |
GET /api/reports/{cost-burn,throughput,executive-summary} and their /export artifacts |
the runs index is behind the disk (truncated_source) |
JSON: no rows; CSV and HTML/PDF export: 409 with the verdict, so no downloaded file carries the numbers without the refusal |
GET /api/runs/cost-summary |
(unchanged — the first consumer of the rule) |
- A measured zero stays zero. An empty window over a complete index is a computable
0; there is no refusal for "the number is small". - Partly unpriced cost is a stated lower bound: computable, with
unpriced_models,unpriced_callsandcost_is_lower_bound: truein the verdict's notes. - Small samples are flagged, not refused. Each analytics bucket carries
duration_samples; buckets whose p95 rests on fewer than 20 runs are listed innotes.small_sample_buckets. - A success rate over zero runs is
null(undefined) in the executive summary, not0.0. - The ClickHouse branch of
/api/cost/reportdoes not read pricing coverage (pricing_coverage_checked: falsein the notes);/api/runs/cost-summarydoes.
The Runs aggregate strip sits above the run list: one bar per day over the current date window, with a runs / failed / p95 toggle and the window's totals. It sends the Runs view state to the server, so with a filter, status chip or search applied the strip shows the refusal — the run aggregate can only be narrowed by date… — and its toggles are disabled with that reason as their tooltip, rather than charting a different set of runs than the list. Analytics, Home and Reports render refusals the same way. Clicking or dragging across bars to filter (ADR-0234 D1 navigation) is planned.
| Tab | Group | Since | What it does |
|---|---|---|---|
| Home | Overview | v0.8 | Journey cards, status bar, resume session |
| Analytics | Overview | v0.62.0 | Time-bucketed run analytics from the runs index: volume + failure stacked bars, duration p50/p95 lines, stat tiles, 7/30/90-day ranges, chart/table toggle (/api/analytics/summary) |
| Alerts | Platform | v0.63.0 | Operational alerts feed (quota/rate-limit/policy/drift/seal/backup) + delivery outcomes from the audit log; severity badges, stat tiles, live refresh (/api/alerts/recent, ADR-0192) |
| Runs | Runs & Debug | v0.7 | Run list, search/filter, inspect capsule, validate, replay, verify; capsule tree, run lineage edges, secret scan (v0.46.0); v0.64.0: per-run Forensics timeline view (/api/runs/{id}/forensics-timeline, P5) + saved filter presets (E2); v0.103.0 (experimental): filter bar (/api/filter/*, ADR-0232/0233), full view state in the URL, capsule explorer with summary strip + Integrity and Lineage views — see Runs view state |
| Diff | Runs & Debug | v0.7 | N-run comparison (2–5), word-level diff, mutation badges |
| Registry | Govern & Promote | v0.7 | Asset lifecycle: eval, promote, rollback, register, suggest-register, unregister |
| Governance | Govern & Promote | v0.16.0 | Classify (EU AI Act/NIST/OMB), audit (6 profiles), export-examiner, policy sign |
| Eval | Govern & Promote | v0.55.0 | Eval suites (eval list), run a suite (eval run), regression compare (eval compare) |
| Risk | Govern & Promote | v0.55.0 | OWASP LLM assurance (assure), secret scan, failure attribution (diagnose), risk-tier classify, MCP scan |
| Lineage | Provenance & Trust | v0.7 | Provenance / blast-radius / replay-chain DAG, interactive query, OpenLineage/PROV export |
| KG | Provenance & Trust | v0.17.0 | Capsule knowledge graph: query, audit, entity queue, alias mgmt, ingest |
| Dashboards | Overview | v0.103.0 (experimental) | Portable ADR-0235 dashboards and widgets from $NOVAFABRIC_HOME/dashboards, ratio() (ADR-0236), validate-and-save editor; g 4 (see Dashboards view) |
| Cost | Overview | v0.17.0 | Cost report, pricing, burn analysis (ClickHouse-backed); v0.64.0: cost-analytics tools trio — attribution / fairness / usage-breakdown (/api/cost/{attribute,fairness,usage-breakdown}, P6) |
| Schema | Compliance | v0.17.0 | Schema registry (JSON Schema + proto3) |
| Evidence | Provenance & Trust | v0.9 | Bundle list, server-side verification (the nova verify <bundle>.zip verifier + TSR + Merkle), download |
| Audit | Provenance & Trust | v0.7 | Dashboard mutation audit log, action-type filter |
| Holds | Govern & Promote | v0.11 | Place/release legal holds, sidebar badge count |
| Policy | Govern & Promote | v0.11 | Interactive OPA/Rego check, ALLOW/DENY badge, explain trace |
| Seal | Provenance & Trust | v0.13.0 | NovaSeal verify, Sigstore sign/verify, linked-envelope proposals, bypass; v0.98.0: trust-surface views — Trust Radar SVG glyph (ADR-0173) and Redaction X-Ray heat overlay + coverage meter (ADR-0174), reading /api/runs/{run_id}/{trust-radar,redaction-xray}. An unverifiable guarantee renders n/a (hollow tick, excluded from the claim polygon), never fail; the X-Ray shows paths and states only — never a field value |
| Spine | Provenance & Trust | v0.56.0 | Accountability Spine: per-run energy receipts + conservation check (ADR-0093), append-only ledger verify (ADR-0094), structured safety case (ADR-0095) — read-only (/api/runs/{id}/{energy,ledger,safety-case}) |
| Compliance | Compliance | v0.15.2 | GDPR RoPA, AI-SBOM, NIST RMF, CRA coverage, erasure, audit bundle/verify (8+ panels) |
| Incidents | Compliance | v0.55.0 | EU AI Act Art. 73 incident records with a live reporting-deadline clock; open/list/transition/export (AIM, NIS2) |
| Capture | Runs & Debug | v0.7 | Capture matrix (4 runners + distributed + MCP/API proxies), adapters, recent capsules |
| Infra | Platform | v0.12.6 | Phase 0–6 component status cards (NovaSeal, Collector, OCS, MetaDB, Lineage, …); v0.64.0: Backups card (/api/infra/backups, P7 — read-only manifest listing) + Maintenance card of idempotent confirm-gated safe mutations (reindex-runs / topology re-seed / KG rebuild, P8-P10) |
| Storage | Platform | v0.55.0 | WORM object-store stats/validate/inspect, manifest chain, collector status, gated DB upgrade + rebuild-metadata-db, system-card export |
| Ops | Platform | v0.55.0 | Installation diagnostics (doctor), warm-daemon liveness (read-only), JWKS flush |
| Admin | Platform | v0.7 | Token management, role management (API v0.14.3; UI partial), JWKS flush, new-run-id, DB ops |
| Commands | Platform | v0.8 | 13 hand-curated command builders at launch; v0.60.0: now mirrors the complete CLI (every nova command, 374 today — the // Commands: header of generatedCommands.ts) via an auto-generated registry — see the CLI-parity row below |
| Reports | Reports & Export | v0.17.0+ | 9 report templates (see below) |
| Export | Reports & Export | v0.64.0 | Export Center hub (E1) — the server-catalog-driven registry export panel (/api/compliance/export/kinds) plus a jump-link directory to the evidence / lineage / reports / compliance export surfaces |
Reports tab — 9 templates: run-history, eval-regression, capsule-compare, cost-burn, throughput, evidence-inventory, policy-audit, seal-verification, release-comparison.
Compliance tab panels: GDPR Art.30 RoPA, AI-SBOM (CycloneDX ML-BOM), NIST AI RMF, AI-SBOM
coverage/CRA deadline, GDPR erasure request/status (real DEK destruction since
ADR-0210, experimental — PII is unreadable afterwards only in capsules captured with --pii-subject (ADR-0314), confirmed: true required), audit coverage/bundle/verify, C2PA,
RO-Crate, EU AI Act Art.12.
Governance tab panels: risk classification, audit-profile coverage, examiner export, policy signing, regulatory vocabularies + manual classification + eval suites/run (v0.46.0).
v0.46.0 parity additions (12 panels across 7 tabs): eval suites list + eval run
(Governance), policy inventory + policy sign (Policy), regulatory vocabularies + manual
classification (Governance), generate AI-SBOM (Compliance), reindex capsules (Admin),
distributed capsule tree + run lineage edges + secret scan with --fail-on gate (Runs),
lineage-store deployment profile (Infra).
v0.55.0 coverage additions: 5 new tabs (Eval, Risk, Storage, Incidents, Ops) plus
Seal → Ratchet (seal ratchet init/rotate/status) and Evidence → Assertions
(evidence completeness/bind) panels, surfacing ~30 previously CLI-only commands. Destructive /
process-control actions (DB rebuild, ratchet rotate) are confirmation-gated and daemon control is
read-only over HTTP ("safe mutations only"). New cross-cutting UX: app-wide toast notifications, a
unified action/confirm pattern, a virtualized data table, per-tab help, deep-linkable tabs
(?tab=), and global entity search (runs/assets/incidents) in the ⌘K palette.
The dashboard server declares 217 REST route handlers (counted 2026-10-23 as
@app.*/@router.*get/post/put/delete/patchdecorators undersrc/novafabric/serve/, excluding WebSocket routes; 3 of them are the TV-5 router that onlynova serve --tv5/--topologymounts). The runtime OpenAPI document lists 210 operations (216 with--topology), because a few routes are excluded from the schema — the full machine list is inapi-reference.md(andGET /api/openapi.jsonat runtime).
Install + run
# 1. Install the optional [serve] extra (FastAPI + uvicorn, both Tier-A licenses)
pip install 'novafabric[serve]'
# 2. Start the server (--experimental is mandatory)
nova serve --experimental
# 3. Open the URL the panel prints (it embeds the session token).
# Or paste the token manually if you prefer to type the URL.Notable flags:
| Flag | Default | Purpose |
|---|---|---|
--experimental |
(required) | Acknowledges the experimental gate. |
--port |
4321 |
TCP port. |
--host |
127.0.0.1 |
Bind address. Refuses non-localhost without the hidden --insecure flag. |
--capsule-dir |
$NOVAFABRIC_CAPSULE_DIR, else $NOVAFABRIC_HOME/capsules |
Where to look for capsules — the directory nova capture writes to. |
--db-path |
$NOVAFABRIC_DB_PATH, else $NOVAFABRIC_HOME/registry.db |
Registry/lineage SQLite path; the startup panel prints the resolved path. |
--no-browser |
off | Don't auto-open a browser tab. |
--topology |
off | Enable the live topology dashboard (/topology/). |
--tv5 |
off | Enable the experimental 3D topology view (implied by --topology). |
--topology-louvain-resolution |
1.0 (or NOVA_TOPOLOGY_LOUVAIN_RESOLUTION) |
Louvain clustering resolution. Lower values merge into fewer/larger clusters; higher values split further. |
--tls-cert / --tls-key |
off (or NOVA_TLS_CERT_PATH / NOVA_TLS_KEY_PATH) |
Serve HTTPS directly (ADR-0241, first-party TLS). Both are required together; the key file must be mode 600. |
Stop with Ctrl+C. The session token is not rotated per start: it persists in
$NOVAFABRIC_HOME/.serve-token (or is pinned with NOVAFABRIC_SERVE_TOKEN) so a restart
keeps open browser sessions. Delete the file to rotate it (serve/auth.py).
Topology view modes
The topology dashboard (/topology/) offers a view-mode switcher over five
complementary lenses on the same data — pick the right one for the task:
| Mode | What it shows | Best for |
|---|---|---|
| Cluster (default) | 2D force layout of cluster super-nodes, sized by agent count; click a cluster to expand its agents | Structure overview + drill-in |
| Call-graph | 2D layered (dagre) layout following the directed run → model → tool flow | Reading call direction/hierarchy |
| Treemap | Rectangles with area proportional to agent count | Spotting the biggest clusters at a glance |
| Table | Sortable, searchable exact list of clusters | Precise lookup; the anti-hairball |
| 3D (experimental) | Three.js orbit view (TV-5); labels appear on hover/selection | Spatial exploration (not default) |
Graph modes provide on-screen + / − / Fit zoom controls. Labels are decluttered — only large, hovered, or selected nodes are labeled — so the full-graph view stays legible at scale. Clusters whose only member is a single agent (e.g. runs with no captured model calls) are collapsed into one "misc" super-node rather than scattered as visual noise.
Capability matrix: dashboard vs CLI
The dashboard exposes the majority of the read-side and many of the write-side capabilities of the CLI, but a number of operations are intentionally CLI-only. Every dashboard action surfaces its equivalent nova command in the confirm dialog.
Reading the "Runs tab → row X button" entries below. Since v0.98.2 the per-row action buttons are revealed, not always visible: they render for the selected row and on hover or keyboard focus of any row. (Ten always-visible buttons per row wrapped onto four lines and left only three runs on screen.) The actions are unchanged and never more than one interaction away — click or tab to the row, then click the action.
Read operations (Layer A — fully covered)
| Capability | CLI | Dashboard | Notes |
|---|---|---|---|
List capsules under .novafabric/runs/ |
nova (no direct command — discovers them) |
✅ Runs tab | Search by run_id or command, filter by status, sort by time/duration, opt-in 5s auto-refresh. v0.12.9 (DU-1): status filter pill-bar (All / running / success / failure / error) above the table. v0.12.9 (DU-2): hover-reveal copy icon on each run ID cell. |
| Inspect a single capsule | nova inspect <run-id> |
✅ Runs tab → click row | File-tree + YAML pane + expandable JSONL rows + in-browser Ajv validate. |
| Validate a capsule against its schema | nova validate <capsule> |
✅ Capsule view → "Validate" button | Same run-capsule.schema.json compiled in-browser via Ajv. |
| List registered assets | nova list [--type] [--status] |
✅ Registry tab | Filters not yet exposed in UI; visible via the live API. |
| Get a single asset | nova inspect <name@version> |
✅ Registry tab → click row | Eval results for the selected asset are lazy-fetched. |
| Lineage queries | nova lineage provenance / blast-radius / replay-chain |
✅ Lineage tab | Full DAG render via React Flow + dagre. Click (or double-click) a node to see incoming/outgoing edges. v0.12.6: interactive QueryPanel — type a ref, pick mode (provenance / blast-radius / replay-chain), hit Run; results appear as a sortable table; CLI equivalent preview updates live. v0.12.7: ref input autocompletes name@version (provenance/blast-radius) or run_id (replay-chain) from loaded data. v0.12.9 (DU-3): when a node is selected, a breadcrumb row above the graph shows the full ancestry chain (root → … → parent → selected); each ancestor is clickable. v0.13.4: zoomOnDoubleClick disabled; double-clicking a node selects it (identical to single-click) instead of triggering zoom-to-fit. |
| Cluster-scale infrastructure status | (no single command — consult each Phase 0–6 component separately) | ✅ Infra tab (v0.12.6) | 10 component cards: NovaSeal, Collector, Object Store, Metadata DB, Lineage at Scale, Parent/Child Capsule, Server Mode, Eval Suites, Policy Gates, Run Capsule — each with a shipped / partial / placeholder / planned status badge plus the relevant CLI commands. Lets operators confirm which Phase 0–6 components are active without leaving the dashboard. |
| CLI command reference with live preview | (all nova sub-commands) |
✅ Commands tab | v0.8: 13 command builders for core capture, registry, and replay. v0.12.6: expanded to 35 hand-curated builders across 4 journey tracks. v0.60.0: now mirrors the complete CLI — every nova command (374 today) has a fillable form. Hand-curated builders still provide richer copy for the most-used commands; the rest are auto-generated from the live Typer app (ui/dashboard/scripts/gen-command-registry.py → generatedCommands.ts), so the tab stays in sync as commands are added/removed (guarded by tests/serve/test_command_registry_coverage.py). Fields, choices, defaults and flags come from the CLI's own parameter metadata. Each builder renders a live command preview with a copy button; a filter box and per-group counts navigate the full surface. Copy-only (Layer C, ADR-0027) — the dashboard never executes commands. |
| Structural diff | nova diff <run-a> <run-b> |
✅ Diff tab | Aligned with diff-report.schema.json; renders environment / model_calls / tool_calls / outputs sections. v0.9 additions: 'Compare against…' in CapsuleInspector, word-level diff for model responses, mutation badge on tool call pairs. v0.12 addition (C-4): Multi-select checkboxes in RunsTab — check any 2 rows, click "Compare selected ⊕", jumps directly to Diff tab with both IDs pre-filled and diff auto-triggered; no copy-paste. See ADR-0036. v0.12.7: both run A and run B inputs autocomplete from the loaded runs list. v0.12.9 (DU-4): last comparison (from/to/result) persisted in sessionStorage — navigating away and back restores the previous comparison without re-fetching. v0.13.3 (C-5): Checkbox cap lifted 2→5; selecting 3–5 runs triggers N-run mode — first run is the baseline, N-1 parallel diffs fire via runMultiDiff, results shown as stacked collapsible MultiDiffCard panels with change-count badges. URL format migrated from ?run_a=&run_b= to ?run_ids=a,b,c (old params still parsed for backward compat). |
| Read evidence bundle | (no read-only command — open the ZIP) | ✅ Evidence tab (v0.9) | Lists all bundles from the evidence directory ($NOVAFABRIC_EVIDENCE_DIR, else $NOVAFABRIC_HOME/evidence/), shows DSSE statement, ZIP download; the Verify button shows the server verifier's verdict (below). See ADR-0037. |
| Verify evidence bundle cryptographic integrity | nova verify <bundle>.zip (--public-key to pin the signer) |
✅ Evidence tab → Verify button (v0.11) | POST /api/evidence/{id}/verify — the same verifier as nova verify <bundle>.zip (digests, manifest_hash, every DSSE signature, signed subjects) + RFC 3161 TSR + NovaSeal Merkle log inclusion. Inline valid/invalid, content, signer, sig, tsr, log badges. signer unanchored (neutral) when the server has no evidence trust list; log – when NovaSeal is not configured locally. The local log check opens configured logs read-only and never creates a Merkle log. |
| Validate a capsule's schema and file structure | nova validate <capsule> |
✅ Runs tab → Validate button (v0.11) | POST /api/runs/{id}/validate — checks required files, YAML parse, JSONL presence. Green ✓ valid or red expandable error list. |
| View secret scan results | nova scan-secrets <capsule> |
✅ Runs tab → Secrets button on card (quick-access, v0.11) or → Secrets tab in detail panel | GET /api/runs/{id}/redaction-proof — shows scanner+packs, targets (hash before/after), per-finding severity badges. Includes re-scan trigger. |
| Report generation | nova report [--format markdown|json|html|pdf] |
◐ Reports tab | The Reports tab has its own registry-driven builders with CSV/JSON download plus self-contained HTML and PDF artifacts with embedded charts (GET /api/reports/{id}/export, ADR-0201; PDF needs the optional WeasyPrint extra — the UI surfaces the 501 install hint). The CLI's asset-inventory report (nova report) additionally supports `--format html |
| Forensics timeline | nova forensics timeline <capsule> |
✅ Runs tab → Forensics view (v0.64.0, P5) | GET /api/runs/{id}/forensics-timeline reconstructs a deterministic timeline from the run's own sealed capsule via the same merge_timeline core. Honest scope: run-lifecycle + model/tool-call events only; missing timestamps and the absent lineage collector are shown as gaps, never fabricated. |
| Cost attribution / fairness / usage-breakdown | nova cost attribute / fairness / usage-breakdown |
✅ Cost tab → tools panel (v0.64.0, P6) | Three pure POST endpoints (/api/cost/{attribute,fairness,usage-breakdown}) wrapping the same cores the CLI calls, given a document instead of a file path. Descriptive only — no cost verdict. |
| Backup-set status | nova backup verify (listing has no direct command) |
✅ Infra tab → Backups card (v0.64.0, P7) | GET /api/infra/backups lists NOVA_BACKUP_DIR archives from their manifest.json (manifest-claimed, not hash-verified — that stays nova backup verify). Degrades to {detected:false} when unconfigured. |
| Lineage time-travel queries | nova lineage time-travel <ref> --asof <ts> |
❌ — | Not yet wired into the dashboard; CLI-only in v0.7. |
| Lineage OpenLineage emission | nova lineage emit-openlineage |
❌ — | Integration target; CLI-only. |
Write operations (Layer B — confirm-gated, audit-logged)
| Capability | CLI | Dashboard | Notes |
|---|---|---|---|
| Register an asset | nova register <spec.yaml> |
✅ Registry tab → "+ Register asset" | Paste YAML into the dialog; spec validated by the same validate_spec the CLI uses. |
| Run eval suites | nova eval agent <name@version> |
✅ Registry tab → row "EVAL" button | Runs the same run_evals entry point as the CLI. v0.9 addition: Eval history sparkline per asset (last 10 results, green/red bars) and full eval history panel in the eval dialog. See ADR-0038. |
| Promote an asset | nova promote direct <name@version> --to <status> [--force] |
✅ Registry tab → row "PROMOTE →" button | Eval gate enforced for agents (use --force only with intent). Force is visually marked as destructive. v0.12.9: invalid target buttons disabled client-side (e.g. archived → production is dimmed); inline error banner shown on server rejection; eval-gate copy conditional on asset_type === 'agent'. v0.13.0 (D-5): nova promote is now a sub-group (direct / propose / approve). The dashboard Promote button maps to direct. For maker-checker SoD flow, use nova promote propose + nova promote approve from the CLI. ADR-0312: nova promote direct --to production is refused without an explicit promote_direct policy allow, and so is the dashboard Promote button: both HTTP promote endpoints call the same gate and answer 403; force does not bypass it. |
| Bulk-promote multiple assets | (run nova promote for each) |
✅ Registry tab → checkbox column + floating action bar (v0.12.9) | Select one or more assets, click "Promote N" in the floating bar; each is promoted to its next valid status in sequence. Select-all master checkbox in header. Floating bar appears only when ≥ 1 asset is checked. |
| Forensic replay | nova replay <capsule> --mode forensic |
✅ Runs tab → row "REPLAY" button | Read-only inspection, no subprocess, safe. |
| Semantic replay analysis | nova replay <capsule> --mode semantic |
✅ Runs tab → row "SEMANTIC" button (v0.11) | Compute pairwise text similarity across model call responses. Returns similarity_score (0–100%) and a gauge. Read-only, no subprocess. |
| Exact replay eligibility | nova replay <capsule> --mode exact |
✅ Runs tab → row "EXACT" button (v0.11) | Check whether the capsule satisfies exact replay requirements: env.lock.lock_mode=deterministic and seed present on every model call. Returns exact_eligible (bool) + blocker list. |
| Re-scan & redact | nova redact <capsule> |
✅ Runs tab → row "REDACT" button | Re-runs SecretScannerV0 and overwrites redaction-proof.json. Limitation: strategy overrides and unsafe-skip marking (--strategy-override, --mark-unsafe-skip, --rationale) are CLI-only. |
| Maintenance recompute (safe) | (reindex has no direct command; KG/topology mirror nova kg ingest --all) |
✅ Infra tab → Maintenance card (v0.64.0, P8-P10) | Three idempotent, lossless, confirm-gated actions: reindex runs cache (POST /api/admin/reindex-runs — full INSERT-OR-REPLACE rebuild, an APIRouter module per the ADR-0183 route freeze), re-seed topology (/api/topology/seed), rebuild KG (/api/kg/ingest-all). None deletes user data. v0.98.0: reindex accepts optional "prune": true (default false) to drop index rows whose capsule directory no longer exists — dangling entries the additive rebuild cannot clear, which otherwise list in the Runs tab and 404 on every drill-in. Capsules are never touched; only the derived index row is removed. |
| Compare two asset spec versions | nova diff <name@v1> <name@v2> |
✅ Registry tab → "Compare…" (v0.11) | GET /api/assets/{name}/diff?from_version=&to_version= — flattened field diff; green +, red −, yellow ~ table rows. Available on assets with ≥ 2 registered versions. |
| Export signed evidence | nova export-evidence <capsule> --output <zip> --key <pem> |
✅ Runs tab → row "EXPORT ↗" button | Signs with the default evidence key, $NOVAFABRIC_HOME/keys/signing_key.pem — the key nova init writes (a pre-existing keys/local-key.pem from an earlier version is used when signing_key.pem is absent); auto-generated there if missing (audit-logged). Output defaults to ~/.novafabric/evidence/<run_id>.zip. Limitation: custom output paths and --allow-unsafe-skips are wired in the API but not surfaced as form controls in the UI yet. |
| Place / release a legal hold | nova hold create <registry> --reason … [--duration-days N] / nova hold release <hold-id> |
✅ Holds tab (v0.11) | POST /api/holds + POST /api/holds/{id}/release. Active holds grouped by registry; inline release button; place-hold form with optional duration. v0.12.7: registry name input autocompletes from the list of registries shown on-screen. |
| List active legal holds | nova hold list <registry> |
✅ Holds tab (v0.11) | GET /api/holds — lists every registry in the hold store shared with nova hold ($NOVAFABRIC_HOME/registries/, plus holds still at the pre-unification <capsule-dir>/../registries/ and ./.novafabric/registries/); sidebar badge shows active count. A hold placed here blocks nova retention and nova capsule delete too. |
| Interactive policy check | (no direct CLI equivalent — there is no nova policy check; the CLI surface is nova policy test for the Rego suite and nova policy explain <decision-id> for a past decision. Corrected 2026-08-01.) |
✅ Policy tab (v0.11) | POST /api/policy/check — evaluates a PolicyInput (action, subject, resource) via the OPA engine and returns a PolicyDecision; large ALLOW/DENY badge + reason + metadata; yellow warning when OPA is not installed. v0.12.7: resource ref autocompletes name@version when kind = asset, or run_id when kind = capsule/replay; loads at mount from GET /api/assets and GET /api/runs. v0.12.9 (DU-9): Rego source textarea with 300 ms debounced client-side syntax check (missing package, unbalanced braces); advisory warning banner, does not block submission. v0.13.2 (DC-5 Explain): "explain" checkbox triggers POST /api/policy/check with explain: true; backend runs a second OPA subprocess with --explain full --format pretty; trace_text returned in PolicyDecision; decision result shows "show trace ↓" toggle that reveals a max-h-64 scrollable monospace trace panel. Scope: checking the packaged policies needs read; submitting your own Rego source needs admin (403 otherwise), and every evaluation runs in a sandboxed opa (no server environment, no opa.runtime/http.send/net.lookup_ip_addr). |
| Export GDPR Art.30 RoPA entry | nova export-ropa <capsule> --output ropa.json |
✅ Compliance tab → GDPR Art.30 RoPA Export (v0.37.0) | POST /api/compliance/export/ropa — optional controller_name / controller_contact; completeness badge + missing-fields list + JSON document display. |
| Export AI-SBOM (CycloneDX 1.7) | nova export-aibom <capsule> [--output aibom.json] |
✅ Compliance tab → AI-SBOM Export (v0.37.0) | POST /api/compliance/export/aibom — component list (type/name/version/description), serial_number, bom_format. CycloneDX ML-BOM 1.7 (ECMA-424 2nd Edition) since v0.39.0. |
| Export NIST AI RMF report | nova export-nist-rmf <capsule> --output report.json |
✅ Compliance tab → NIST AI RMF Report (v0.37.0) | POST /api/compliance/export/nist-rmf — GOVERN/MAP/MEASURE/MANAGE score bars, risk-level badge, missing-evidence list. |
| Check AIBOM coverage (CRA) | nova aibom status [--capsules-dir <dir>] |
✅ Compliance tab → AI-SBOM Coverage Status (v0.37.0) | GET /api/aibom/status — auto-loads on mount; total/covered/missing capsule counts, coverage progress bar, CRA deadline 2026-09-11. |
| Execute GDPR Art.17 erasure | nova pii erase <subject_id> |
✅ Compliance tab → GDPR Erasure Request (experimental, ADR-0210) | POST /api/compliance/erasure/request — real DEK destruction (experimental — makes PII unreadable only in capsules captured with --pii-subject, ADR-0314; replaced the v0.18.0 no-op stub): persists the request in $NOVAFABRIC_HOME/erasure.db, then destroys the subject's AES-256-GCM DEK through the same DEKStore.erase_subject code path as the CLI. Safe-mutations gated — body must carry confirmed: true (400 otherwise, nothing mutated). Terminal states COMPLETED (receipt + receipt_sha256), DEFERRED (Art.17(3)(b) retention window, earliest_erasure_at), HELD (an active legal hold kept the DEK, GDPR Art.17(3)(e) → 409 legal_hold_active), FAILED (error receipt; unknown subject → subject_not_found). Duplicate request while one is PENDING re-attaches to it (idempotency + crash recovery). Both audit surfaces record the action hash-only (subject_sha256, never the raw subject). Gated only on crypto-shredding's own prerequisite (the DEK store loads; 409 crypto_shred_unavailable otherwise) — NOVA_CAP003_ENABLED, which governs the separate dual-object split, does not gate it. GET /api/compliance/erasure/status lists the persisted queue. The /v0 server-mode mirror is planned (ADR-0210 D7), not implemented. |
| Mocked replay (re-execute agent code, cache hits) | nova replay <capsule> --mode mocked |
❌ — | Mocked replay spawns the agent subprocess. That straddles Layer B/C; in v0.7 it is CLI-only until the sandbox model lands per ADR-0027. |
| Scan secrets | nova scan-secrets <capsule> |
❌ — | Use redact from the dashboard, or nova scan-secrets from the terminal. |
Capture and orchestration (Layer C — deferred)
| Capability | CLI | Dashboard | Notes |
|---|---|---|---|
| Capture a command | nova capture <command> |
❌ Deferred to v0.9 | Per ADR-0027 §1 Layer C, launching arbitrary subprocesses from a browser endpoint requires a sandboxed exec model, per-action audit log entries with full command text, and a confirmation flow that shows the command verbatim. None of those primitives are committed yet. Layer C may not ship at all if the security review concludes the risk outweighs the value. The Capture tab in the dashboard documents this and lists the four CLI alternatives (nova capture, --runner docker, nova mcp-proxy, nova api-proxy). |
| MCP proxy | nova mcp-proxy <upstream> |
❌ — | Long-running stdio bridge; CLI-only. |
| LLM API proxy | nova api-proxy --upstream-url <url> |
❌ — | Long-running HTTP server; CLI-only. |
| Capture with custom runner | nova capture --runner {docker,kubernetes,slurm} ... |
❌ — | Same Layer C reasoning. |
Operational / advanced (CLI-only by design)
These are intentionally not in the dashboard because the CLI is the right surface for them:
| Capability | CLI | Why not in the dashboard |
|---|---|---|
| Schema validation of arbitrary files | nova validate <spec.yaml> (asset specs) |
Use the dashboard's Register dialog for assets; pipe through nova validate for batch jobs. |
| Asset diff between two versions | nova diff <name@v1> <name@v2> |
✅ Registry tab → Compare (v0.11) — moved to the Write operations table above. |
| Streaming real-time output during a long-running job | (the capture command itself) | Dashboards aren't the right surface for tailing a single subprocess; use nova capture directly. |
| CI / non-interactive scripted runs | All commands | Dashboards require a browser; CI uses the CLI. |
| Cluster runners (Docker / K8s / Slurm) | nova capture --runner ... |
Driven from the orchestration host, not from a browser tab. |
What the dashboard does that the CLI cannot
A short list, for completeness — the dashboard is not strictly a subset:
- Live filtered runs view. Search across
run_idand command, filter by status, sort by time or duration. The CLI prints a single shell-friendly stream; the dashboard makes 200+ capsules navigable. - Interactive lineage DAG. React Flow + dagre layout, three highlight modes (provenance / blast-radius / replay-chain) without re-querying. CLI returns lineage as JSON; the dashboard visualises the graph.
- In-browser schema validation. The Validate button compiles
run-capsule.schema.jsonvia Ajv in the browser and reports timing. Useful as a sanity check before sharing a capsule. - In-browser ed25519 verification — showcase page only. The static
/showcase/evidencedemo checks a bundled fixture withSubtleCrypto.verify('Ed25519', …)and has a tamper toggle that flips a byte. The live Evidence tab no longer verifies in the browser: a single DSSE signature check there ignored every member digest, so its Verify button shows the server verifier's verdict instead. - Server-side bundle verification (v0.11).
POST /api/evidence/{id}/verifyruns the same verifier asnova verify <bundle>.zip(artifact digests,manifest_hash, every attestation's DSSE signature, signed subjects recomputed from the bundle's content), plus the RFC 3161 TSR and NovaSeal Merkle log inclusion, and returns structured per-check results withsigner_trust. To pin the signer offline, runnova verify <bundle>.zip --public-key <pem>. - Single-pane audit log. Every dashboard mutation appends to
~/.novafabric/dashboard-audit.jsonl; the Audit tab shows recent entries newest-first with the equivalent CLI command. The CLI does not have an equivalent unified audit log for its own invocations (each command logs its own outputs). v0.12.9 (DU-6): action-type filter dropdown — derived dynamically from loaded entries — narrows the list to a single action type (promote / eval / rollback / approve / register / …). - Context-aware autocomplete on all ref inputs (v0.12.7). Every text input that accepts a database reference — lineage query ref, diff run A/B, holds registry name, policy resource ref — shows a live-filtered dropdown populated from data already on the page. Focus the field to browse the top eight items; type to filter. The CLI has shell-level completion (Tab) but no live database-backed suggestions.
- Staleness indicator on Home tab resume cards (v0.12.9, DU-7). Resume cards older than 24 hours show an amber border and a "Last updated X — may be stale" tooltip. Useful for detecting runs that started but never completed.
- Open capsule folder from Capture tab (v0.12.9, DU-8). The Capture tab's "Recent capsules" panel lists the 5 most recent runs; when
capsule_pathis present, an Open folder link opens the local directory in the system file manager via thefile://protocol.
Security model
Localhost only by default.
--host 0.0.0.0is rejected without--insecure. With--insecure, serve over TLS: either first-party (--tls-cert/--tls-key, orNOVA_TLS_CERT_PATH/NOVA_TLS_KEY_PATH; ADR-0241) or behind a TLS-terminating reverse proxy.Token authentication. A cryptographically random URL-safe token (
secrets.token_urlsafe(32)) is generated at start-up and written to$NOVAFABRIC_HOME/.serve-token(mode 0600); the start-up panel prints the resolved path. It is not rotated on every restart — an existing token file is reused so a restart does not invalidate open browser sessions, and the token therefore survives on disk after the server exits until something deletes it. The token must be passed on every/api/*request, either as anAuthorization: Bearer …header or as?token=…. When both are present theBearerheader is authoritative. (Bearer support was added in v0.97.0; this paragraph previously stated that Bearer headers were rejected, which has not been accurate since.)Browser security headers. Every response carries
Content-Security-Policy: object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self',X-Frame-Options: DENY,X-Content-Type-Options: nosniffandReferrer-Policy: no-referrer— the last because the token may travel in?token=and must not leak to a linked site through theRefererheader. Capsule-derived text (run labels, commands) is rendered as text, never as markup.script-srcis not restricted yet (the built dashboard has an inline hydration script).Scope-based authorization (experimental, v0.102.0, ADR-0228). Authentication alone used to grant every endpoint: one token, and
DELETE /api/runs/{run_id},POST /api/compliance/pii/eraseandPOST /api/admin/roleswere all reachable with it. Each route now carries one of four scopes and a credential is checked against it:Scope Covers ADR-0027 layer readevery read-only endpoint — runs, lineage, evidence, compliance reads, reports, exports, /metrics, and the Policy tab's check against the packaged policiesLayer A operatebounded, reversible mutations — register, promote, score, placing a hold, incident, ingest Layer B adminirreversible or privilege-affecting — capsule/run deletion, releasing a legal hold, PII erase, seal bypass, signing, key ratchet, role and token management, and the forensic/semantic/exactreplay routes (POST /api/runs/{run_id}/replay/{mode}; none of them re-executes the workload —exactonly checks eligibility — but they are classifiedadmin, whilereplay/dry-runisoperate)Layer C auditeverything readcovers plus the audit trail and recorded policy decisions (includingGET /api/policy/explainand the/exportof the audit reports) — and neveroperateoradmin, at any levelorthogonal admin⊇operate⊇read, andadminalso reachesaudit(the same super-role rulenova server's RBAC already applies).auditsits outside the chain on purpose: the persona must see more evidence than an ordinary reader — including who did what — while being structurally incapable of changing anything.Nothing changes on a laptop. The
.serve-tokenholdsadmin, and so does any token issued before this landed, so a single-usernova servebehaves exactly as it did. Authorization only bites once you deliberately mint a narrower credential:curl -X POST "http://127.0.0.1:4321/api/admin/tokens?token=$(cat "${NOVAFABRIC_HOME:-$HOME/.novafabric}/.serve-token")" \ -H 'content-type: application/json' \ -d '{"label": "auditor-laptop", "confirmed": true, "scope": "audit"}'The response carries the token once and reports the scope it minted;
GET /api/admin/tokenslists the scope of every issued credential. Omitscopeand you getadmin, i.e. the previous behaviour. The scope must be exactlyread,operate,adminoraudit: a typo such asadmnorAdminis refused with 422 and nothing is stored. If a stored record'sscopeis ever not one of those (for example after a hand edit oftokens.jsonl), that token holds no scope and is refused with 403 everywhere except the public probes. It is listed asnone. It never falls back toadmin. Only a record with noscopefield at all, which predates scopes, reads asadmin.An export is never weaker than the report it exports.
GET /api/reports/{report_id}/exporttakes the scope of the report itself: areadtoken that is refuseddashboard-auditis refused its HTML/PDF export too.Your own Rego needs
admin. The Policy tab's Rego textarea (policy_source) and a custombundle_pathfor the Rego test suite are refused with 403 toread,operateandaudittokens; they can still check and test the packaged policies. Whatever the scope,oparuns sandboxed: no server environment (so noNOVAFABRIC_SERVE_TOKEN), and noopa.runtime,http.sendornet.lookup_ip_addr. A policy that calls one of them is a DENY whose reason names the undefined function.A route with no classification is denied to everyone, including the server token and a caller that sends no credential. That is deliberate: a forgotten classification then breaks a feature loudly instead of disclosing evidence quietly. A CI guard fails on the first unclassified route, so you should never meet this in a release.
Not yet: roles assigned through
POST /api/admin/rolesare not read back as scopes — that surface is nowadmin-gated, which is what closes the cross-mode escalation, but per-request consultation ofrole_assignmentsarrives with tenant scoping (ADR-0229). WebSocket subscriptions (/api/tv5/ws,/topology/stream) still check host and token only, not scope.Tenancy posture (experimental, ADR-0229). Authorization says what a credential may do; tenancy says whose data it may see.
nova servereads eight stores and they do not share a tenancy model:Store Class Why metadata store · evidence fabric · cost store · object capsule store aware each carries a real tenant discriminator — RLS + begin_tenant_context(), atenantcolumn, atenant_idcolumn, and thecapsules/<tenant>/…object key respectivelycapsule directory agnostic isolation is supplied by the deployment via --capsule-dir, not by the storeruns index · knowledge graph · lineage store unsafe no tenant discriminator at all, so they cannot answer "show me only tenant X" Because three stores are unsafe — including the runs index behind
/api/runs— multi-tenant mode is refused rather than served. SettingNOVAFABRIC_SERVE_TENANCY=multimakesnova serveexit before binding a socket, naming every blocking store and why:NOVAFABRIC_SERVE_TENANCY=multi was requested, but 3 store(s) in the serve read path cannot answer a tenant-scoped question: - knowledge_graph: KuzuDB has no RLS analogue and the graph schema carries no tenant property; a tenancy model for it needs its own ADR (ADR-0229 OQ-2) - lineage_store: the SQLite lineage schema carries no tenant column, … - runs_cache: the runs index has no tenant column, so /api/runs and every list built on it would return rows from every tenant (ADR-0229 OQ-1)This is deliberately blunter than ADR-0229 D2's per-endpoint 503: refusing only the KG and lineage panels would keep serving
/api/runsunscoped, and an operator watching some panels refuse would reasonably conclude the rest were scoped. For multi-tenant deployments today, usenova server— the multi-user REST API, which enforces tenancy at the metadata store.GET /api/doctorreports the posture in both modes under thetenancy_posturecheck. Single-tenant is a correct posture, so the check reportsok; read theblockinglist, not the flag.Server-side paths are confined (works today). Several actions take a server-side path in the request body: the evidence export's output and signing key, capsule migration's source and output, policy signing's key, cert and DB, the KG and lineage
capsule_path, the policy-test bundle, and the metadata rebuild target. Each path is resolved (~expanded, symlinks followed) and must land inside an allowlisted root. Writes go only to$NOVAFABRIC_HOME/exports,$NOVAFABRIC_HOME/evidenceor$NOVAFABRIC_EVIDENCE_DIR. Reads may also use$NOVAFABRIC_HOMEand the server's capsule directory. A..segment or a path outside the roots gets 403. Reads never reach the server's own secrets: the token stores,.env,dek.db, the ratchet state,config.yaml, the audit logs, or (for a source path) key material and*.pem/*.keyfiles; a directory containing one, such as$NOVAFABRIC_HOMEitself, is refused too (403). A relative path is taken from$NOVAFABRIC_HOME(reads) or$NOVAFABRIC_HOME/exports(writes). To use another directory, for example a mounted volume, list it inNOVAFABRIC_SERVE_PATH_ROOTS(absolute paths, separated by:on Linux and macOS) before startingnova serve. The former denylist and itsNOVAFABRIC_SERVE_ALLOW_ANY_PATHopt-out are gone;nova servewarns if that variable is still set.Dashboard promotion follows the CLI gate (works today, ADR-0312). The Registry tab's Promote button is a single-actor
nova promote direct, and it is gated the same way:productionis refused (403) unless an OPA policy explicitly allows thepromote_directaction, and force does not bypass that. Usenova promote propose+nova promote approveto reach production.DNS-rebinding defence.
Hostheader must be127.0.0.1,localhost, or::1. Other hosts return 403 even with a valid token.CORS allowlist. Only
http://localhost:4321,http://127.0.0.1:4321,http://localhost:4322andhttp://127.0.0.1:4322are accepted (methodsGET,POST,DELETE). Defence in depth — the token is the authoritative gate.Constant-time token compare.
_consteqcompares bytes equally regardless of where the mismatch is.Server-side state is plain files, shared with the CLI. Stopping the server leaves behind what it persisted: under
$NOVAFABRIC_HOME, the serve token (.serve-token), issued tokens (tokens.jsonl), the dashboard audit log (dashboard-audit.jsonl), the GDPR erasure queue (erasure.db) and dashboards (dashboards/, asnova dashboard); saved views in$NOVAFABRIC_VIEWS_DIR, else./.novafabric/viewsunder the server's working directory (asnova view); and any artifacts the user created (evidence bundles, exports). Registry and lineage live in the sameregistry.dbthe CLI uses; the dashboard keeps no database of its own beyonderasure.db.
Audit log
Every Layer B mutation appends a JSONL record to ~/.novafabric/dashboard-audit.jsonl (override with NOVAFABRIC_DASHBOARD_AUDIT_FILE). Each record has:
audit_id— UUIDts— ISO 8601 timestampaction— e.g.register_asset,eval_asset,promote_asset,forensic_replay,redact,export_evidenceargs— the action-specific arguments (no secrets — the YAML body ofregister_assetis recorded only as a length, not the content)cli_equivalent— thenova …command a human could re-runactor_token_fp— the acting subject. On action records it is a fingerprint of the credential: for the server token, 12 hex characters of a domain-separated SHA-256 (stable, and never a prefix of the token); for an issued token, the start of its listed fingerprint. Onauthz.deniedrecords it islocalfor the server token, the issued token's fingerprint, oranonymous. Never the token itself, nor any part of it. Records written before this fix carried the token's first 8 characters; rotate the serve token if those records left the machineresult—ok,error, ordeniederror— present only on failures
An authorization failure is itself an audited event: a 403 appends an authz.denied record with top-level required_scope, held_scope and resource (ADR-0231 D4 — a SIEM rule that has to reach into a free-form payload to find them breaks the next time that payload changes). A 401 appends nothing — a 401 is usually a stale browser tab, and recording the credential that produced it would put the bearer token back in a log that v0.98.0 took it out of.
Records also carry, since ADR-0231:
actor—{"identity_source": "shared-token" | "credential" | "federated", "id"?: …}. The log states how well it knows the actor rather than implying it knows. With the shared.serve-tokenevery record carries the same fingerprint, soidentity_sourceisshared-tokenandidis absent — not null. An ADR-0228 issued token iscredential.resource,prior,current— before-and-after state for the mutation, captured at the mutation site. A re-read afterwards would race concurrent writers and record a transition that never happened. Supplied today byassign_roleandrevoke_role; the remaining handlers are deferred and enumerated in ADR-0231.redaction—{applied, ruleset, findings}, proof that captured state passed the ADR-0187 ruleset. This log is append-only and deliberately unrotated, so unredacted state would sit on disk permanently.
Exporting to a SIEM is unchanged (ADR-0191): nova audit-log export --source dashboard --format ocsf|cef|jsonl. ADR-0231 added no format and no sink; the same records simply carry more.
⚠ In the OCSF projection a token fingerprint appears as actor.user.uid with user.type_id: 0 (Unknown) and actor.nova_identity_source, never as actor.user.name. It used to be user.name, which a SIEM renders in a column headed User — reading as a person when it is one shared session credential.
extra— action-specific metadata (e.g.asset_id,replay_id,size_bytes,key_autogenerated_at)
The file is append-only with mode 0600. To read it: cat ~/.novafabric/dashboard-audit.jsonl | jq or use the dashboard's Audit tab.
Trace viewer (v0.7.1)
The Trace view (click a run row → "Trace" button) shows three sub-tabs:
Waterfall tab
Renders the trace.jsonl span tree as an indented Gantt chart.
- Span tree — indented rows; collapse/expand branches by clicking the arrow.
- Agent label column — shows the
nova.agent.idof the span's owning agent (or the nearest non-root ancestor name when the attribute is absent). - Outcome coloring — bar colour encodes outcome: green = success/ok, red = any error status. Span type is readable from the span name, not the bar colour.
- Wall-clock concurrency — each span's bar starts at its true wall-clock offset; concurrent spans overlap visually as they did at runtime.
- Timeline ruler — 6-tick ruler across the full run duration.
Thread tab
Chronological sequence of every model call and tool call, styled as a conversation.
- Agent badge — coloured pill showing which agent issued the call. When
nova.agent.idis absent the dashboard infers the agent name from the model call's system prompt (regex:"You are the X"→x; see ADR-0035). - Agent group dividers — a coloured separator appears whenever the active agent
changes, giving visual phase-boundary annotations without requiring
nova.phase. - Outcome avatar border — the avatar circle border is green for success, red for any error status.
- LLM call detail — model name, system (openai / anthropic / ollama / …), separate input↑ / output↓ token counts, cache-hit indicator, finish reason badge (stop / tool_calls / length / …).
- Expandable rows — click any event to expand its full arguments (tool) or last message + completion text (model). Collapsed: shows first 200 chars inline.
- Timestamp — ISO wall-clock time shown above each event (HH:MM:SS.mmm).
Swimlane tab
Gantt chart with one horizontal lane per agent — designed to make parallel evidence-gathering phases visible at a glance.
- Lane per agent — inferred or explicit via
nova.agent.id. - Bar per call — model calls and tool calls appear as bars; width encodes duration, x-position encodes wall-clock start offset.
- Outcome colours — green = success, red = error/failure. Hover for details.
- Graceful degradation — when no agent attribution is available, all calls appear in a single "root" lane with a hint linking to ADR-0035.
Adding agent attribution to your agent code
# When writing a model call record, add nova.agent.id:
capsule.append_model_call({
"model_call_id": ...,
"nova.agent.id": "kubernetes_sentinel",
"nova.agent.type": "evidence",
# ... rest of gen_ai.* fields
})
# Same for tool calls:
capsule.append_tool_call({
"tool_call_id": ...,
"nova.agent.id": "kubernetes_sentinel",
# ... rest of fields
})When nova.agent.id is present the swimlane and Thread views use it directly.
When it is absent the Thread view falls back to system-prompt inference; the
swimlane shows a single root lane.
Evidence Tab (v0.9 + v0.11)
See ADR-0037.
The Evidence Tab is a fully operational view over real bundles on disk. It requires nova export-evidence to have been run at least once (bundles live in $NOVAFABRIC_EVIDENCE_DIR, else $NOVAFABRIC_HOME/evidence/).
Bundle list view — table of all bundles found in the evidence directory, showing run_id, timestamp, file size, and a manifest-hash integrity badge. The badge is a shallow check (SHA-256 of manifest.json fields) that runs at list time; use the Verify button for cryptographic assurance.
Detail pane — selecting a bundle opens a detail pane showing the DSSE statement (pretty-printed JSON), attestation hashes for each artifact, and the signing key fingerprint (first 16 hex chars of the SHA-256 of the public key).
Verify button (v0.11) — calls POST /api/evidence/{bundle_id}/verify server-side. The route runs the same verifier as nova verify <bundle>.zip (evidence/bundle_verify.py), so the dashboard and the CLI reach the same verdict on the same bundle. The badges:
| Badge | What is verified | When neutral (–) |
|---|---|---|
valid / invalid |
The overall verdict: every check below that ran passed. Hover for the list of failures | Never |
content ✓ / ✗ |
Every artifact digest, no missing, extra or duplicate member, manifest_hash, and each signed subject recomputed from the bundle's own run-capsule/ copy and lineage edges.jsonl (manifest.json itself is not signed) |
Against a server older than this check |
signer ✓ / ✗ / unanchored |
Whether every attestation was signed by a key in the operator's evidence trust list | signer unanchored: the server has no trust list, so the signatures verify only under the key shipped in the bundle. That proves the bundle is consistent, not who signed it; hover for the signer's key fingerprint |
sig ✓ / ✗ |
Every listed attestation is signed and its DSSE signature verifies (with the detached .sig) |
Never — always attempted |
tsr ✓ / ✗ / – |
RFC 3161 TSR in manifest.dsse.tsr against the DSSE bytes |
When timestamping was not requested at export time |
log ✓ / ✗ / – |
NovaSeal Merkle log inclusion for the capsule with this run id on the server | When novaseal.yaml is not configured or the capsule dir is not locally available |
A bundle is valid when all non-null checks pass. valid with signer unanchored means "unchanged since signed by whoever holds the key in the bundle": anyone can re-sign an edited bundle with their own key and reach that state.
Pinning the signer. Put the Ed25519 public keys you trust, in PEM (the format nova verify --public-key takes; several blocks may be concatenated), in $NOVAFABRIC_HOME/evidence-trusted-keys.pem, or point NOVAFABRIC_EVIDENCE_TRUSTED_KEYS (in nova serve's environment) at such a file. A bundle signed by any other key then reads signer untrusted and invalid. A key sent in the request is never used. A configured trust list that is missing, empty, unreadable or holds a non-Ed25519 key makes the route return 500: a broken anchor never silently becomes "unanchored". The file is read on every request, so editing it takes effect without a restart. A bundle above 1 GiB uncompressed is refused with 413; run nova verify on it instead.
Download link — direct link to download the raw ZIP for offline verification (openssl ts -verify) or sharing.
Cross-run comparison (v0.9)
Works today (shipped v0.9; see ADR-0036) — the compare-selected flow below is live in the Runs tab.
The current diff workflow has six friction steps: find run A in the Runs tab, copy its run_id, navigate to the Diff tab, paste it, find run B, copy and paste its run_id. The v0.9 additions remove the copy-paste steps entirely.
D2 — Two-click compare from RunsTab. Select any two rows in the Runs tab (checkbox or Shift-click) and a "Compare" button appears in the toolbar. Clicking it navigates directly to the Diff tab with both run_id values pre-filled and the diff auto-triggered.
D3 — Compare against… from CapsuleInspector. A "Compare against…" dropdown inside the CapsuleInspector lets you pick a second run from a recency-sorted list, then jumps to the Diff tab pre-filled.
D4 — Word-level LCS diff for model responses. Model call response text is diffed at the word level using a Longest Common Subsequence algorithm, with insertions highlighted green and deletions struck through in red — making prompt-sensitivity changes legible at a glance.
D5 — Mutation badge on tool call pairs. When a matched tool call pair has different arguments or outputs, a ⚡ mutation badge appears on the pair header. Badge tooltip shows argument key(s) that changed.
Infrastructure tab (v0.12.6)
The Infra sidebar entry (under the Platform group) shows the operational status of all ten Phase 0–6 cluster-scale components as card tiles:
| Component | Status | CLI to verify |
|---|---|---|
| Run Capsule (Phase 0 / v0.1) | shipped |
nova inspect <run-id> |
| NovaSeal v0.1 (Phase 0) | shipped |
nova verify <capsule> |
| Event Envelope v1 (Phase 1) | shipped |
nova validate <capsule> |
| Collector tier (Phase 2) | shipped |
nova collector rebuild (Python side) · novafabric-collector --config <yaml> (Go gateway, after go build) |
| Parent/Child Capsule (Phase 3) | shipped |
nova run show <id> --with-children |
| Object Capsule Store (Phase 4) | shipped |
nova storage validate / nova storage inspect |
| Metadata DB (Phase 5) | shipped |
nova db upgrade |
| Lineage at Scale (Phase 6) | shipped |
nova lineage provenance <ref> |
| Server Mode (v0.7) | shipped |
nova server start |
| Eval Suites (v0.9) | shipped |
nova eval run <capsule> |
Badge colours: shipped (green), partial (accent), placeholder (amber), planned (muted). Each card shows the CLI command(s) for that component so operators can investigate from the terminal without navigating away.
Corrected 2026-08-01. This table previously named three commands that do not exist:
nova store status(the group isnova storage, exposingvalidateandinspect),nova db status(nova dbexposesupgradeandmigrate-to-postgresonly), andnova eval <name@version>(nova evalis a command group; the runner isnova eval run). The Collector row was also ambiguous:novafabric-collectoris the Go gateway binary (source atcollector/cmd/novafabric-collector, needsgo buildfirst — it is not a Python console script and is not onPATHafter apip/uvinstall); the Python-side surface isnova collector rebuild. All ten component statuses themselves are unchanged and correct — only the verification commands were wrong.
Eval trend chart (v0.9)
Works today (shipped v0.9; see ADR-0038) — the sparkline + history panel below are live in the Registry tab.
Per-asset eval sparkline. Every row in the Registry tab gains an 8×32px inline SVG bar chart showing the last 10 eval results for that asset — green bars for pass, red for fail. The sparkline is lazy-loaded via IntersectionObserver so it does not block initial table render.
Eval history panel. Clicking the "EVAL" button for an asset opens an expanded history panel (above the trigger form) showing the last 20 eval results in a table: suite name, score, pass/fail, and relative timestamp (e.g. "3 days ago"). The panel is fetched on demand and cached for the session.
v0.12.8 display fixes:
- Null score — suites that produce only a pass/fail signal (no numeric score) store
{"score": null}. Previously rendered as0.00; now renders as a muted—so it is not confused with a zero-score failure. - Empty suite name — if
suite_nameis blank or absent in the database, the eval row shows italic(unknown suite)instead of empty whitespace.
UX conveniences (v0.7)
- Theme toggle in the header — light / dark / system. Persisted in
localStorage. An inline<script>runs before hydration to prevent flash. - Token-in-URL flow. Click the URL
nova serveprints (it embeds the token); the SPA consumes it, saves tolocalStorage, and strips it from the visible URL so it doesn't sit in browser history. - Auto-bounce on 401. Any in-flight request that returns 401 (e.g. server restart, token rotation) clears the cached token and shows the connect screen again.
- Confirm dialogs for every mutation. Each shows a one-line description, the action's arguments where editable (e.g. promote target dropdown), the equivalent CLI command, and the audit-log notice.
- Toast notifications for success and failure of mutations — visible bottom-right for ~4 seconds.
- Auto-refresh on the runs tab. Opt-in 5-second polling; off by default.
Roadmap
- v0.7 (shipped) — Layer A (read-only) + Layer B partial (register, eval, promote, forensic replay, redact, export evidence). All CLI-equivalent actions are confirm-gated and audit-logged.
- v0.8 (shipped 2026-05-12) — Home tab (journey cards, status bar, resume session), Commands tab (11 CLI command builders with live preview + copy), sidebar reorganized into 5 journey groups.
- v0.9 (shipped 2026-05-12) — Evidence Tab native (list + verify real bundles), cross-run comparison UX (2-click compare shortcut, word-level diff, mutation badges), eval trend sparklines in Registry. See ADR-0037, ADR-0036, ADR-0038.
- v0.11 (shipped 2026-05-14) — Dashboard Completeness: all eight gap-closing tracks shipped.
- DC-8 — Diff compare URL persistence (
?run_a=&run_b=). - DC-3 — Secret scan results viewer (Secrets view in RunsTab,
GET /api/runs/{id}/redaction-proof). - DC-1 — Evidence verification UI (
POST /api/evidence/{id}/verify, three-stage DSSE + TSR + Merkle check, inline badges). - DC-7 — Capsule validation UI (
POST /api/runs/{id}/validate, expandable error list). - DC-6 — Asset spec diff (
GET /api/assets/{name}/diff, Registry tab Compare… modal). - DC-2 — Legal holds tab (place/release holds, sidebar badge count).
- DC-4 — Semantic + exact replay UI (similarity gauge, eligibility card).
- DC-5 — Policy check tab (
POST /api/policy/check, interactive OPA/Rego policy tester, ALLOW/DENY badge).
- DC-8 — Diff compare URL persistence (
- v0.12 (shipped 2026-05-14) — C-4 compare shortcut in RunsTab: multi-select checkboxes (max 2), "Compare selected ⊕" banner, auto-jump to DiffTab with both IDs pre-filled and diff auto-triggered.
nova unregistercommand added. - v0.12.6 (shipped 2026-05-14) — Dashboard coverage Phase 1: expanded Commands tab (13 → 35 builders across 4 journey tracks), Lineage QueryPanel (interactive provenance/blast-radius/replay-chain query with live CLI preview), InfraTab (10 Phase 0–6 component status cards), enriched CaptureTab (all 4 runners, distributed-run commands, full capture matrix). DD-1..DD-8 implementation plans created.
- v0.12.7 (shipped 2026-05-14) — Context-aware autocomplete in every ref input: Lineage QueryPanel, Diff run A/B, Holds registry name, Policy resource ref. Shared
SuggestInputcomponent. Full documentation update. - v0.12.8 (shipped 2026-05-14) — Eval results panel: null score →
—, empty suite name →(unknown suite). Makefilebundle+serve-localtargets. - v0.12.9 (shipped 2026-05-14) — Three promote-dialog bug fixes (Bug-1/2/3: valid transition enforcement, inline error banner, conditional eval-gate copy) and nine UX improvements: DU-1 (RunsTab status filter), DU-2 (copy run ID), DU-3 (lineage ancestry breadcrumb), DU-4 (DiffTab sessionStorage), DU-5 (bulk promote), DU-6 (AuditTab action filter), DU-7 (HomeTab staleness indicator), DU-8 (CaptureTab folder link), DU-9 (PolicyTab Rego lint).
- v0.13.0 (shipped 2026-05-15) — D-5 maker-checker dual-approval:
nova promoterestructured as a sub-group (direct/propose/approve). Ed25519 keypair auto-generated per identity. SoD enforced at the cryptographic level. Opt-inmaker_checker_gate.rego. ADR-0058 + ADR-0018 amendment. 14 new tests. - v0.13.1 (shipped 2026-05-15) — DU-10 shared
<EmptyState>component (bordered/fill/inlinevariants), applied to HoldsTab, AuditTab, RunsTab, EvidenceList, RegistryBrowser, LineageGraph. Eliminates 8 ad-hoc empty-state patterns. - v0.13.2 (shipped 2026-05-15) — DC-5 OPA trace viewer in PolicyTab. "explain" checkbox sends
explain: truetoPOST /api/policy/check; backend firesopa eval --explain full --format pretty;trace_textreturned inPolicyDecision; collapsible "show trace ↓" panel in the decision result card. - v0.13.3 (shipped 2026-05-15) — C-5 N-run diff. RunsTab checkbox cap lifted 2→5. Selecting 3–5 runs and clicking "Compare selected ⊕" enters DiffTab N-run mode: first run is baseline, N-1 parallel diffs fire, results shown as stacked collapsible cards with change-count badges. URL migrated from
?run_a=&run_b=to?run_ids=a,b,c. - v0.13.4 (shipped 2026-05-15) — LineageGraph UX fix: double-clicking a node now selects it instead of triggering zoom-to-fit (
zoomOnDoubleClick=false). Test coverage restored to 90%. - v0.14 P1 (API only; UI in P2) — Role-management REST surface.
POST/DELETE/GET /v0/admin/roles(production) and/api/admin/roles(local). Last-admin lockout invariant at the store layer (returns 409).nova server revoke-roleCLI added. ADR-0060. AdminTab "Roles" panel placeholder remains until P2 lands. - v0.15.2 (shipped 2026-05-20) — Track B dashboard scale: RunsTab cursor-pagination + SSE live feed, RegistryTab load-more, ComplianceTab with 4 panels for cap-001/002/004/005 evidence controls.
- v0.16.0–v0.16.5 (shipped 2026-05-20) — GovernanceTab UI (4 panels: classify, audit, export-examiner, policy sign), SealTab inputClass crash fix, PBS/LSF runner dashboard support.
- v0.17.0 (shipped 2026-05-20) — KGTab (6 panels: query, audit, entity queue, alias, init, ingest), full Evidence Fabric v1.0 coverage, TV-5 3D topology view.
- v0.18.0 (shipped 2026-05-20) — DB-KG-1 new KGTab init/ingest panels; DB-CAP-1/ERA-1/STG-1 extensions in PolicyTab/ComplianceTab/InfraTab.
- v0.19.0 (shipped 2026-05-20) — All CLI surfaces covered: ValidateDistributedBlock, KGInitPanel, NewRunIdPanel, DatabaseOpsPanel.
- v0.20.0 (shipped 2026-05-20) — 7 new endpoints (unregister, doctor, policy test/explain, audit coverage/bundle/verify) + 4 tab panels.
- v0.27.0 (shipped 2026-05-20) — Full CLI→dashboard parity: 11 new panels across SealTab, AdminTab×4, RegistryTab×2, InfraTab, RunsTab, LineageTab, GovernanceTab. 59 new tests.
- v0.29.0 (shipped 2026-05-20) — KG MCPServer topology layer (SERVED_BY edge,
/api/kg/topology, TopologyLayerPanel). - v0.30.0 (shipped 2026-05-20) — 3 new panels: CapsuleVerifyPanel (SealTab), OpenLineageExportPanel (LineageTab), SuggestRegisterPanel (RegistryTab).
- v0.31.0 (shipped 2026-05-20) — KGQueryPanel, KGAuditPanel, EntityQueuePanel, KGAliasPanel; erasure-status + AuditMapPanel (ComplianceTab); children tree (RunsTab).
- v0.34.0 (shipped 2026-05-20) — Dashboard parity for v0.32–v0.33 regulatory CLIs: RoCrateExportPanel, C2paExportPanel, ProvJsonExportPanel, EuAiActStatusPanel, EuAiActExportPanel.
- v0.37.0 (shipped 2026-05-20) — ComplianceTab gains 4 new panels: GDPR Art.30 RoPA Export (optional controller fields, completeness badge), AI-SBOM Export (CycloneDX 1.7 component list, v0.39.0), NIST AI RMF Report (GOVERN/MAP/MEASURE/MANAGE score bars, risk-level badge), AI-SBOM Coverage Status (auto-loads, 3-stat grid, CRA deadline 2026-09-11). Four new
nova serveendpoints:POST /api/compliance/export/ropa|aibom|nist-rmf,GET /api/aibom/status. - v1.0 — Graduation review. The
--experimentalflag is removed only when a follow-up ADR explicitly proposes it.
The dashboard is judged on whether it earns its keep. If at any point it becomes maintenance-heavy without commensurate user value, the entire src/novafabric/serve/ package can be deleted in a single commit and ADR-0027 reverted, with little migration debt: what the dashboard persists is plain files (the token stores, its audit log, erasure.db, views and dashboards — see the security notes above), not a schema of its own.
Downloading chart images (ADR-0201; shipped v0.64.0)
Every chart wrapped in the shared ChartCard (Analytics run-volume + duration
charts, the Reports-tab chart preview, the Cost tab's cost-by-model chart) has a
compact ⬇ SVG / PNG affordance. Export happens entirely client-side: the
on-screen SVG is serialized with its computed styles inlined — so theme
tokens resolve to the concrete colors of your active light/dark theme — and PNG
is rasterized through a canvas at 2× for crisp retina output. No server round
trip, no new dependencies.
Report artifacts: HTML + PDF with charts (ADR-0200/0201; shipped v0.64.0)
The Reports tab is backed by a server-side report registry
(GET /api/reports/catalog) that declares, per report, its filters and — only
where a genuine series exists — a chart spec (throughput stacked-bar, cost-burn
bar, single-suite eval-regression line, release-comparison delta bar; nothing
is fabricated for tabular-by-nature reports). Besides CSV/JSON, every report
exports as:
- HTML — one self-contained file (no JS, no external requests) with the inline-SVG chart, the data table (row-capped with an explicit "showing first N of M" line — never silent truncation), and the canonical JSON embedded.
- PDF — the same page through WeasyPrint. Requires the optional extra
(
pip install 'novafabric[compliance]'); without it the endpoint returns501with that exact hint, which the UI surfaces verbatim.
Parity classification (ADR-0200; shipped v0.64.0)
ui/dashboard/src/components/dashboard/commands/commandParity.json classifies every
nova CLI command as real-panel (a wired dashboard panel exists — claim is
machine-verified against the API client and serve routes), builder-only
(copy-only Commands-tab builder), or cli-only with a mandatory reason
(process lifecycle, destructive filesystem operation, CI-oriented).
tests/serve/test_command_parity_classification.py fails CI when a new CLI
command lands unclassified — parity can no longer regress silently. Destructive
operations staying cli-only with a recorded reason is the intended honest
end-state, not a gap.
Scale characteristics and known limits
Understanding these limits prevents surprises when you grow beyond a small local setup. The limits are documented here so they are visible before they bite.
What works at scale today
| Mechanism | Where it lives | What it handles |
|---|---|---|
Cursor-based pagination on /api/runs/search |
serve/app.py:42–66 |
Fetches large run lists in pages without re-scanning from the start. A cursor is a bookmark: the server encodes (created_at, run_id) of the last returned item into a short base64 token. Send it back as ?cursor=... to get the next page. Nothing to do with the VS Code text cursor. |
LIMIT/OFFSET on assets and runs |
serve/app.py |
Registry and runs list endpoints cap result sizes. |
| TanStack Virtual in RunsTab and RegistryTab | ui/dashboard/src/components/dashboard/tabs/ |
Renders only the rows visible on screen — the DOM stays small even with thousands of rows loaded in memory. |
| SSE live feed for new capsules | /api/runs/stream |
New capsule directories appear in the RunsTab without a full page reload. |
| 5 SQL indexes on registry DB | serve/app.py (v0.14.6) |
Keeps WHERE status = ? and ORDER BY created_at fast at tens of thousands of assets. |
| Shared keyset cursor codec + page envelope (ADR-0199, shipped v0.64.0) | serve/pagination.py |
One (created_at, id) row-value cursor implementation reused by runs, run-history reports, and the audit log. |
| Reverse-block audit-log tail reads (ADR-0199, shipped v0.64.0) | serve/audit.py::read_recent_tail |
/api/audit (and the alerts feed) read O(limit) 64 KiB blocks from EOF with byte-offset cursors instead of readlines() on the whole append-only file; AuditTab is virtualized with server-side action filter + load-more. |
| SQL aggregation pushdown for reports + analytics (ADR-0199, shipped v0.64.0) | registry/runs_cache.py aggregate functions + substr(created_at,1,10) expression index |
Throughput, executive-summary, cost-burn, and the Analytics day buckets are computed in indexed GROUP BYs — the former limit=1_000_000 fetch-all is gone. Run-history is keyset-paged and its CSV export streams in pages (bounded memory at any store size). |
| Nightly 100K-row scale gate (ADR-0199, shipped v0.64.0) | tests/bench/test_dashboard_scale_gate.py + the dashboard-scale CI job |
p95 latency thresholds per endpoint, recorded in docs/ops/dashboard-scale.md; regressions block release. |
True keyset fast path on /api/runs/search (ADR-0199, B2) |
serve/app.py + registry/runs_cache.py::query_runs(after=) |
The decoded (created_at, run_id) cursor now seeks directly in the index — the former cursor→OFFSET COUNT(*) conversion (O(offset) per page) is gone; deep pages cost O(page). |
| Bounded policy audit reads (ADR-0199, B2) | /api/policy/recent-decisions, /api/policy/explain |
Both iterate the dashboard audit log newest-first via audit.tail_lines (O(page) reverse 64 KiB blocks, ≤10,000 scanned lines per request) instead of reading the whole file; byte-offset cursor/next_cursor (same contract as /api/audit) plus truncated. explain gained limit (default 100, max 1000) and now returns matches newest-first. explain needs the audit scope and matches decision_id exactly (an empty or wildcard id is 422). |
Keyset cursor + totals on /api/lineage/edges (ADR-0199, B2) |
serve/app.py |
Additive cursor/next_cursor over (created_at, edge_id), true total edge count, and truncated: true when older edges were not returned; existing count/edges keys unchanged. |
| Bounded KG entity queue (ADR-0199, B2) | /api/kg/entity-queue + kg/review_queue.py::list_pending(limit=) |
limit (default 200, max 1000) is pushed into the SQL read; response gains the true pending total and truncated. |
| Bounded admin listings (ADR-0199, B2) | /api/admin/tokens, /api/admin/api-keys |
Tokens are read newest-first from tokens.jsonl via the reverse tail scan with a byte-offset cursor (limit default 200, max 2000; a revoke rewrites the file and invalidates outstanding cursors). API keys push limit (default 500, max 2000) into SQL; total is now a true COUNT(*) and truncated is additive. |
| Bounded asset eval history (ADR-0199, B2) | /api/assets/{asset_id} |
eval_limit (default 50, max 500) bounds eval_results; additive eval_results_total + eval_results_truncated — no silent truncation. |
Watermark cache on /api/analytics/summary (ADR-0199 rule 3) |
serve/routers/analytics.py |
A cheap indexed (COUNT(*), MAX(created_at)) watermark per window skips the O(rows) duration-percentile pass when nothing changed — the 30 s polling loop recomputes only when new runs actually landed (bounded 32-window cache). |
| Shared truncation affordance in the UI (ADR-0199, shipped v0.97.0) | ui/dashboard/src/components/ui/TruncationNotice.tsx + lib/{useQuery,usePaginatedQuery}.ts |
The client-side half of "no silent truncation": every bounded list renders a footer reading Showing N of M — K more with a Load more button, and a ~ prefix plus an approximate badge whenever the server reported an estimated total (total_approx) rather than an exact COUNT(*). usePaginatedQuery speaks both server models — keyset cursor and limit/offset — so a list never has to choose between honesty and paging; DataTable additionally loads the next page on scroll. If a list can show you less than exists, it says so. |
| Conditional GET on all hot polled reads (ADR-0199 S6, B3) | serve/http_cache.py::conditional_json on /api/stats, /api/runs, /api/alerts/recent, /api/incidents, /api/evidence, /api/kg/topology, /api/reports/catalog (plus the original /api/analytics/summary) |
Content-addressed strong ETags + Cache-Control: private, max-age — the dashboard's 30 s polling loop gets 304 Not Modified (empty body) whenever the underlying data hasn't changed, cutting transfer and client re-render on every idle poll. |
Known hard limits (action items for future versions)
| Limit | Root cause | Planned fix |
|---|---|---|
~10,000 capsules before /api/runs slows noticeably |
list_run_summaries() (serve/capsule_loader.py:51) re-reads every capsule.yaml from disk on every request — O(N disk). |
Scale-S1 (shipped v0.32.0) + Scale-S3 (shipped v0.36.0): runs_cache SQLite index + CapsuleWatcher background indexer; nova ingest-capsule CLI for manual re-index. |
| NovaSeal Merkle log verify is O(N) on either backend | serve/app.py, nova seal log verify |
Scale-S4 shipped v0.38.0 — set NOVAFABRIC_SEAL_DB_PATH=postgresql://… for the Postgres Merkle log. ⚠ Postgres removes the SQLite table scan but not the O(N) cost: the root is recomputed from every leaf either way (~1.9 s at 1M entries). The originally-published p99 < 200 ms figure was never met and is withdrawn by decision (ADR-0268): full recompute is the guarantee and its linear cost is documented — see docs/releases/v0.38.0.md. |
| SQLite registry breaks at ~1M rows | No partitioning in SQLite | Flip NOVAFABRIC_METADATA_BACKEND=postgres — the Postgres tier (PostgresMetadataStore) is already implemented. |
Storage backends — three separate systems
Each backend is upgraded independently. Flipping one does not affect the others. See Storage Architecture for the full reference table and env var names.
| Store | Default | Scalable tier |
|---|---|---|
| Capsule content | Flat files under $NOVAFABRIC_HOME/capsules/ — always filesystem |
Object Capsule Store (NOVA_OCS_BACKEND) |
| Registry + lineage | ~/.novafabric/registry.db (SQLite) |
NOVAFABRIC_METADATA_BACKEND=postgres + NOVAFABRIC_METADATA_DSN=... |
| NovaSeal policy + Merkle log | $NOVAFABRIC_HOME/novaseal-merkle.db (SQLite; or merkle_db: in novaseal.yaml) |
NOVAFABRIC_SEAL_DB_PATH=postgresql://… (PostgresMerkleLog, Scale-S4, shipped v0.38.0) |
KG topology vs. Live Topology
The dashboard exposes two topology views that are easy to confuse:
| KG tab → Multi-Layer Topology | Top-right "Topology" button | |
|---|---|---|
| Backing store | KuzuDB on disk (persistent) | In-memory DuckDB + ring buffer (ephemeral) |
| Node types | Agent, Model, Tool, MCPServer, InferenceEndpoint | Agent only |
| Updated by | Re-ingest All / auto-poll (60 s) | Running agents in real time (< 5 s) |
| Survives restart | ✓ | ✗ (re-seeded from disk at startup) |
| Queryable | ✓ (nova kg query) |
✗ view only |
| Purpose | Historical audit: what called what | Operational: what is running now |
Re-ingest All only updates the KG topology. It has no effect on the live Topology view.
Restarting nova serve re-seeds the live Topology from disk. It does not change KuzuDB.
Full comparison with update-trigger matrix: KG topology vs. Live Topology.
See also
- ADR-0027 — full architectural decision record.
- what NovaFabric is not — the original "no web UI through v1.0" stance and its narrow exception.
docs/cli-reference.md— the canonical CLI reference. Every dashboard action maps to a command listed there.- ADR-0011 — Evidence Bundle — the signing key model the dashboard reuses.
ui/dashboard/(in the repo) — the dashboard's source. Since ADR-0299nova serveships the dashboard only:/opens it, explanatory pages are external links to novafabric.ai, and nothing the dashboard does requires internet access.- ADR-0036 — cross-run comparison UX design (v0.9).
- ADR-0037 — Evidence Tab native implementation (v0.9).
- ADR-0038 — Eval trend chart in Registry (v0.9).