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

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).

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):

Evidence cart (experimental — ADR-0239)

Collect runs while you investigate, then export them as one signed Evidence Bundle.

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)

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/patch decorators under src/novafabric/serve/, excluding WebSocket routes; 3 of them are the TV-5 router that only nova serve --tv5/--topology mounts). The runtime OpenAPI document lists 210 operations (216 with --topology), because a few routes are excluded from the schema — the full machine list is in api-reference.md (and GET /api/openapi.json at 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:


Security model


Audit log

Every Layer B mutation appends a JSONL record to ~/.novafabric/dashboard-audit.jsonl (override with NOVAFABRIC_DASHBOARD_AUDIT_FILE). Each record has:

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:

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.

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.

Thread tab

Chronological sequence of every model call and tool call, styled as a conversation.

Swimlane tab

Gantt chart with one horizontal lane per agent — designed to make parallel evidence-gathering phases visible at a glance.

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 is nova storage, exposing validate and inspect), nova db status (nova db exposes upgrade and migrate-to-postgres only), and nova eval <name@version> (nova eval is a command group; the runner is nova eval run). The Collector row was also ambiguous: novafabric-collector is the Go gateway binary (source at collector/cmd/novafabric-collector, needs go build first — it is not a Python console script and is not on PATH after a pip/uv install); the Python-side surface is nova 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:


UX conveniences (v0.7)


Roadmap

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:

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