diff --git a/dashboard/src/components/nv-app.ts b/dashboard/src/components/nv-app.ts
index e4d825f8..546119d0 100644
--- a/dashboard/src/components/nv-app.ts
+++ b/dashboard/src/components/nv-app.ts
@@ -18,6 +18,7 @@ import './nv-debug-hud';
import './nv-settings-drawer';
import './nv-onboarding';
import './nv-ghost-murmur';
+import './nv-help';
export type View = 'scene' | 'apps' | 'inspector' | 'witness' | 'ghost-murmur';
@@ -120,6 +121,7 @@ export class NvApp extends LitElement {
client.loadScene().',
+ },
+ {
+ q: 'Does any of my data leave the browser?',
+ a: 'No. WASM mode is local-only — the worker, the WASM binary, and the IndexedDB persistence all live in your browser. The optional WS transport (off by default) talks to a host of your choosing.',
+ },
+ {
+ q: 'What does the witness mismatch (red ✗) mean?',
+ a: 'The current build of nvsim produced a SHA-256 that doesn\'t match the constant pinned at compile time. Possible causes: a different Rust toolchain, a dependency version drift, a manual edit to a physics constant, or an honest bug. Audit the diff against ADR-089 §5.',
+ },
+ {
+ q: 'Why are the Inspector / Witness rail buttons there if there\'s already a right-side inspector?',
+ a: 'The right-side inspector is the compact live view; the rail buttons open a full-width version with bigger charts, an explainer header, reference-scene metadata cards, and (on Witness) a "what this verifies" panel. Both stay in sync — the right rail is for glancing, the main area is for diving in.',
+ },
+ {
+ q: 'Why is there an "App Store" if this is a magnetometer simulator?',
+ a: 'Because nvsim is one tile in a larger sensing platform. The catalog lists every hot-loadable WASM edge module RuView ships — medical, security, building, retail, industrial, signal, learning, autonomy. The simulators (nvsim today, more in future) are first-class entries in the same catalog.',
+ },
+];
+
+const QUICKSTART = [
+ { step: 1, title: 'Hit ▶ Run', body: 'The big amber button in the topbar starts the live frame stream. The pipeline runs ~1.8 kHz on x86_64 WASM, well above the 1 kHz Cortex-A53 acceptance gate.' },
+ { step: 2, title: 'Watch the B-vector trace', body: 'The Inspector → Signal tab shows the recovered field per axis updating in real time. The frame strip below it is one bar per ~32-frame batch.' },
+ { step: 3, title: 'Verify the witness', body: 'Click the rail Witness button (or REPL: proof.verify). The dashboard re-runs the canonical reference scene and asserts the SHA-256 byte-for-byte.' },
+ { step: 4, title: 'Drag a source', body: 'Grab the rebar / heart proxy / mains loop / ferrous door in the scene canvas; positions persist via IndexedDB.' },
+ { step: 5, title: 'Tweak the tunables', body: 'Sliders in the left sidebar update the running pipeline (f_s, f_mod, integration time, shot-noise). Changes debounce 300 ms then push to the worker.' },
+ { step: 6, title: 'Open the Ghost Murmur view', body: 'The ghost icon in the rail. Move the distance + moment sliders, hit "Run nvsim at this distance" — the live demo runs the real Rust pipeline through WASM and shows which transport tier would actually detect.' },
+ { step: 7, title: 'Browse the App Store', body: 'The grid icon. 65+ edge apps: medical, security, building, retail, industrial, signal, learning. Toggle to mark active in this session.' },
+];
+
+const SHORTCUTS = [
+ { keys: '⌘K / Ctrl K', label: 'Command palette' },
+ { keys: 'Space', label: 'Play / pause pipeline' },
+ { keys: '⌘R / Ctrl R', label: 'Reset pipeline (with confirm)' },
+ { keys: '⌘, / Ctrl ,', label: 'Settings drawer' },
+ { keys: '⌘N / Ctrl N', label: 'New scene' },
+ { keys: '⌘E / Ctrl E', label: 'Export proof bundle' },
+ { keys: '⌘/ / Ctrl /', label: 'Toggle theme (dark / light)' },
+ { keys: '`', label: 'Toggle debug HUD' },
+ { keys: '?', label: 'Open this help center' },
+ { keys: '1 · 2 · 3', label: 'Switch inspector tab (Signal / Frame / Witness)' },
+ { keys: 'Esc', label: 'Close any modal / palette / drawer' },
+ { keys: '/', label: 'Focus the REPL prompt' },
+];
+
+@customElement('nv-help')
+export class NvHelp extends LitElement {
+ @state() private open = false;
+ @state() private section: Section = 'quickstart';
+ @state() private query = '';
+
+ static styles = css`
+ :host {
+ position: fixed; inset: 0;
+ background: rgba(0, 0, 0, 0.55);
+ backdrop-filter: blur(4px);
+ z-index: 230;
+ display: grid; place-items: center;
+ opacity: 0; pointer-events: none;
+ transition: opacity 0.18s;
+ }
+ :host([open]) { opacity: 1; pointer-events: auto; }
+ .modal {
+ background: var(--bg-1);
+ border: 1px solid var(--line-2);
+ border-radius: var(--radius);
+ box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7);
+ width: min(880px, 94vw);
+ max-height: 86vh;
+ display: grid;
+ grid-template-columns: 200px 1fr;
+ grid-template-rows: auto 1fr auto;
+ overflow: hidden;
+ transform: translateY(12px) scale(0.98);
+ transition: transform 0.22s cubic-bezier(0.2,0.7,0.3,1);
+ }
+ :host([open]) .modal { transform: translateY(0) scale(1); }
+ @media (max-width: 700px) {
+ .modal { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr auto; max-height: 92vh; }
+ .nav { border-right: 0; border-bottom: 1px solid var(--line); flex-direction: row; overflow-x: auto; }
+ .nav button { white-space: nowrap; }
+ }
+ .h {
+ grid-column: 1 / -1;
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--line);
+ display: flex; align-items: center; justify-content: space-between;
+ }
+ .h .ttl { font-size: 15px; font-weight: 600; }
+ .nav {
+ border-right: 1px solid var(--line);
+ padding: 12px 8px;
+ display: flex; flex-direction: column; gap: 2px;
+ background: var(--bg-1);
+ }
+ .nav button {
+ text-align: left;
+ padding: 8px 12px;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ color: var(--ink-3);
+ font-size: 12.5px;
+ cursor: pointer;
+ transition: color 0.15s, background 0.15s;
+ }
+ .nav button:hover { color: var(--ink); background: var(--bg-2); }
+ .nav button.on {
+ color: var(--ink); background: var(--bg-3);
+ border-color: var(--line-2);
+ }
+ .body {
+ padding: 18px 22px;
+ overflow-y: auto;
+ font-size: 13px;
+ color: var(--ink-2);
+ line-height: 1.6;
+ }
+ .body h2 {
+ margin: 0 0 8px;
+ font-size: 18px;
+ color: var(--ink);
+ letter-spacing: -0.01em;
+ }
+ .body .lead {
+ color: var(--ink-3);
+ font-size: 12.5px;
+ margin: 0 0 14px;
+ }
+ .body p { margin: 0 0 12px; }
+ .body code {
+ font-family: var(--mono);
+ background: var(--bg-3);
+ padding: 1px 5px;
+ border-radius: 4px;
+ font-size: 11.5px;
+ color: var(--accent);
+ }
+ .body kbd {
+ font-family: var(--mono);
+ padding: 2px 6px;
+ background: var(--bg-3);
+ border: 1px solid var(--line);
+ border-radius: 4px;
+ font-size: 11.5px;
+ color: var(--ink);
+ }
+ .step {
+ display: grid;
+ grid-template-columns: 32px 1fr;
+ gap: 12px;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--line);
+ }
+ .step:last-child { border-bottom: 0; }
+ .step .num {
+ width: 26px; height: 26px;
+ border-radius: 50%;
+ background: var(--accent);
+ color: #1a0f00;
+ font-family: var(--mono);
+ font-size: 12.5px;
+ font-weight: 700;
+ display: grid; place-items: center;
+ }
+ .step .ttl { color: var(--ink); font-weight: 600; font-size: 13.5px; margin-bottom: 2px; }
+ .step .body-text { font-size: 12.5px; color: var(--ink-2); line-height: 1.55; }
+ .glossary-search {
+ width: 100%;
+ padding: 8px 12px;
+ background: var(--bg-3);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ font-family: var(--mono);
+ font-size: 12.5px;
+ color: var(--ink);
+ outline: none;
+ margin-bottom: 14px;
+ }
+ .glossary-search:focus { border-color: var(--accent); }
+ .term {
+ padding: 10px 0;
+ border-bottom: 1px solid var(--line);
+ }
+ .term:last-child { border-bottom: 0; }
+ .term .head {
+ display: flex; align-items: center; gap: 8px; margin-bottom: 4px;
+ }
+ .term .name {
+ font-family: var(--mono);
+ font-size: 13.5px;
+ color: var(--accent);
+ font-weight: 600;
+ }
+ .term .badge {
+ font-family: var(--mono);
+ font-size: 9.5px;
+ padding: 1px 6px;
+ border-radius: 4px;
+ border: 1px solid var(--line);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+ .term .badge.physics { color: var(--accent-2); border-color: oklch(0.78 0.12 195 / 0.4); }
+ .term .badge.rust { color: var(--accent); border-color: oklch(0.78 0.14 70 / 0.4); }
+ .term .badge.ui { color: var(--accent-4); border-color: oklch(0.78 0.14 145 / 0.4); }
+ .term .body-text {
+ font-size: 12.5px;
+ color: var(--ink-2);
+ line-height: 1.55;
+ }
+ .faq-item {
+ padding: 10px 0;
+ border-bottom: 1px solid var(--line);
+ }
+ .faq-item:last-child { border-bottom: 0; }
+ .faq-item .q {
+ color: var(--ink);
+ font-weight: 600;
+ font-size: 13.5px;
+ margin-bottom: 4px;
+ }
+ .faq-item .a { font-size: 12.5px; color: var(--ink-2); line-height: 1.55; }
+ .shortcuts {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 8px 16px;
+ align-items: baseline;
+ }
+ .f {
+ grid-column: 1 / -1;
+ padding: 10px 18px;
+ border-top: 1px solid var(--line);
+ display: flex; align-items: center; justify-content: space-between;
+ font-size: 11.5px; color: var(--ink-3);
+ }
+ .close {
+ width: 28px; height: 28px;
+ background: transparent; border: 1px solid var(--line);
+ border-radius: 6px;
+ color: var(--ink-2);
+ cursor: pointer;
+ }
+ .close:hover { color: var(--ink); border-color: var(--line-2); }
+ `;
+
+ override connectedCallback(): void {
+ super.connectedCallback();
+ window.addEventListener('nv-show-help', this.show as EventListener);
+ window.addEventListener('keydown', this.onKey);
+ }
+ override disconnectedCallback(): void {
+ super.disconnectedCallback();
+ window.removeEventListener('nv-show-help', this.show as EventListener);
+ window.removeEventListener('keydown', this.onKey);
+ }
+
+ private show = (e: Event): void => {
+ const detail = (e as CustomEvent).detail as { section?: Section } | undefined;
+ if (detail?.section) this.section = detail.section;
+ this.open = true;
+ this.setAttribute('open', '');
+ };
+ private close(): void {
+ this.open = false;
+ this.removeAttribute('open');
+ }
+ private onKey = (e: KeyboardEvent): void => {
+ const target = e.target as HTMLElement | null;
+ const isInput = target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA';
+ if (e.key === '?' && !isInput && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ this.show(new CustomEvent('nv-show-help'));
+ } else if (e.key === 'Escape' && this.open) {
+ this.close();
+ }
+ };
+
+ private filteredGlossary(): GlossaryItem[] {
+ if (!this.query.trim()) return GLOSSARY;
+ const q = this.query.toLowerCase();
+ return GLOSSARY.filter((g) =>
+ g.term.toLowerCase().includes(q) || g.body.toLowerCase().includes(q),
+ );
+ }
+
+ private renderQuickstart() {
+ return html`
+
Seven taps to get from "I just opened the dashboard" to "I'm running my own scene with verified determinism."
+ ${QUICKSTART.map((s) => html` +Every piece of jargon in the dashboard, defined in one paragraph each.
+ this.query = (e.target as HTMLInputElement).value} /> + ${items.length === 0 + ? html`No terms match.
` + : items.map((g) => html` +The questions I was asked twice in the first week of demos.
+ ${FAQ.map((item) => html` +Everything is reachable without a mouse.
+What you're looking at, in one screen.
+nvsim is a deterministic forward simulator for nitrogen-vacancy diamond magnetometry.
+ The Rust crate at v2/crates/nvsim is the source of truth; this dashboard is a
+ Vite + Lit single-page app that ships the crate compiled to WebAssembly inside a Web Worker.
The defining commitment is determinism: same (scene, config, seed) →
+ byte-identical SHA-256 witness across browsers, OSes, and transports. Press the
+ Verify witness button on the Witness tab to assert this live.
The codebase is open source (Apache-2.0 OR MIT). Find it on GitHub:
+ github.com/ruvnet/RuView. Decisions are documented in ADRs 089 (nvsim),
+ 090 (Lindblad extension, conditional), 091 (sub-THz radar research),
+ 092 (this dashboard), 093 (UX gap analysis).
This dashboard is one of several RuView demos. Sibling demos at
+ github.io/RuView/ include the Observatory and Pose Fusion views.
Space)
+ to start the live B-vector trace.
+