feat(dashboard): full-screen Inspector + Witness views (P1.13)

The Inspector and Witness rail buttons previously only flipped which
tab was selected in the small right-rail inspector — visually
underwhelming. They now also mount an `expanded` instance of the
inspector in the main area, giving the click a real spatial payoff.

Closes ADR-093 P1.13 (view-overlay full-screen panel — was deferred to
V2 but materially improves the rail click affordance).

## nv-inspector
- New `expanded` reflected boolean property; when set, host gets a
  radial-gradient backdrop, larger tabs (16/22 px padding), wider body
  (max-width 1400 px, centered), 220 px chart height, 48 px frame
  strip, and a 2-column grid layout for the Signal/Frame panes.
- New per-tab header (h1 + lead paragraph) only renders in expanded
  mode so the small right-rail copy stays compact.
- Expanded Witness pane gets four metadata cards (Reference scene,
  Seed, Sample count, Status) plus a "What this verifies" card
  explaining the determinism contract verbatim.
- ARIA: tabs are now `role=tablist`, each `role=tab` `aria-selected`,
  body is `role=tabpanel`.

## nv-app
- View routing extended: when view ∈ {'inspector','witness'} the main
  area renders <nv-inspector expanded .pinTab=…> and the right-rail
  compact inspector continues to mirror the same data for context.

Validated end-to-end on https://ruvnet.github.io/RuView/nvsim/ —
agent-browser confirms Inspector click → "Signal inspector — live
B-vector trace + frame stream" h1, Witness click → "Witness panel —
SHA-256 determinism gate" h1 with 7 cards.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-04-27 10:27:19 -04:00
parent 18c09d3305
commit eed5feeab2
2 changed files with 167 additions and 25 deletions

View File

@ -102,7 +102,11 @@ export class NvApp extends LitElement {
? html`<nv-app-store></nv-app-store>`
: this.view === 'ghost-murmur'
? html`<nv-ghost-murmur></nv-ghost-murmur>`
: html`<nv-scene></nv-scene>`}
: this.view === 'inspector'
? html`<nv-inspector expanded .pinTab=${'signal'}></nv-inspector>`
: this.view === 'witness'
? html`<nv-inspector expanded .pinTab=${'witness'}></nv-inspector>`
: html`<nv-scene></nv-scene>`}
</main>
<nv-inspector
.pinTab=${this.view === 'inspector' ? 'signal'

View File

@ -15,6 +15,10 @@ export class NvInspector extends LitElement {
@state() private tab: Tab = 'signal';
/** When set by the parent, force the tab and pulse-highlight it. */
@property({ attribute: false }) pinTab: Tab | null = null;
/** When `expanded`, the inspector renders as a full-screen view with bigger
* charts and a wider Witness panel. Used when the rail Inspector/Witness
* button is clicked see ADR-093 P1.13. */
@property({ type: Boolean, reflect: true }) expanded = false;
static styles = css`
:host {
@ -24,6 +28,43 @@ export class NvInspector extends LitElement {
overflow: hidden;
height: 100%;
}
:host([expanded]) {
border-left: 0;
background: radial-gradient(ellipse at 50% 30%, var(--bg-2) 0%, var(--bg-0) 70%);
}
:host([expanded]) .tabs {
padding: 0 24px;
background: var(--bg-1);
}
:host([expanded]) .tab {
padding: 16px 22px;
font-size: 13.5px;
flex: 0 0 auto;
}
:host([expanded]) .body {
padding: 24px 28px;
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
:host([expanded]) .card { padding: 18px 20px; }
:host([expanded]) .card-h .ttl { font-size: 14px; }
:host([expanded]) svg { height: 220px; }
:host([expanded]) .frame-strip { height: 48px; }
:host([expanded]) table { font-size: 12.5px; }
:host([expanded]) td { padding: 6px 0; }
:host([expanded]) .hex { font-size: 12px; padding: 14px; line-height: 1.7; }
:host([expanded]) .witness-box { font-size: 13px; padding: 14px 16px; line-height: 1.6; }
:host([expanded]) .verify-btn { padding: 12px; font-size: 13px; }
:host([expanded]) .grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
:host([expanded]) .grid-2 > .card { margin-bottom: 0; }
@media (max-width: 1024px) {
:host([expanded]) .grid-2 { grid-template-columns: 1fr; }
}
.tabs {
display: flex; border-bottom: 1px solid var(--line);
}
@ -160,6 +201,27 @@ export class NvInspector extends LitElement {
}
}
private renderHeader() {
if (!this.expanded) return '';
const titles: Record<Tab, string> = {
signal: 'Signal inspector — live B-vector trace + frame stream',
frame: 'Frame inspector — MagFrame v1 fields + raw bytes',
witness: 'Witness panel — SHA-256 determinism gate',
};
return html`
<h1 style="margin: 8px 0 14px; font-size: 20px; letter-spacing: -0.01em;">
${titles[this.tab]}
</h1>
<p style="margin: 0 0 18px; font-size: 12.5px; color: var(--ink-3); line-height: 1.55; max-width: 780px;">
${this.tab === 'signal'
? 'Real-time recovered field-vector and frame-stream sparkline. Both update at the running pipeline\'s frame rate. Use the Tunables panel in the sidebar to change f_s, f_mod, dt, and shot-noise behaviour.'
: this.tab === 'frame'
? 'Decoded view of the most recent MagFrame: typed fields plus the raw 60-byte little-endian binary record (magic 0xC51A_6E70).'
: 'Re-derive the SHA-256 witness for the canonical reference scene (seed=42, N=256) right now in your browser and compare against Proof::EXPECTED_WITNESS_HEX. Same inputs → same hash, byte-for-byte, across every machine and transport.'}
</p>
`;
}
private renderSignalTab() {
const W = 320, H = 130, cy = 65, scale = 22;
const cap = 200;
@ -173,27 +235,43 @@ export class NvInspector extends LitElement {
return p;
};
return html`
<div class="card">
<div class="card-h">
<span class="ttl">B-vector trace</span>
<span class="badge">3-axis · nT</span>
</div>
<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">
<line x1="0" y1=${cy} x2=${W} y2=${cy} stroke="var(--line)" stroke-width="0.5"/>
${svg`<path id="trace-x" d=${make(traceX.value)} stroke="oklch(0.78 0.14 70)" stroke-width="1.2" fill="none"/>`}
${svg`<path id="trace-y" d=${make(traceY.value)} stroke="oklch(0.78 0.12 195)" stroke-width="1.2" fill="none" opacity="0.8"/>`}
${svg`<path id="trace-z" d=${make(traceZ.value)} stroke="oklch(0.72 0.18 330)" stroke-width="1.2" fill="none" opacity="0.7"/>`}
</svg>
</div>
const b = lastB.value;
const bnT = [b[0] * 1e9, b[1] * 1e9, b[2] * 1e9];
<div class="card">
<div class="card-h">
<span class="ttl">Frame stream</span>
<span class="badge" id="strip-rate">live</span>
return html`
<div class=${this.expanded ? 'grid-2' : ''}>
<div class="card">
<div class="card-h">
<span class="ttl">B-vector trace</span>
<span class="badge">3-axis · nT</span>
</div>
<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">
<line x1="0" y1=${cy} x2=${W} y2=${cy} stroke="var(--line)" stroke-width="0.5"/>
${svg`<path id="trace-x" d=${make(traceX.value)} stroke="oklch(0.78 0.14 70)" stroke-width="1.2" fill="none"/>`}
${svg`<path id="trace-y" d=${make(traceY.value)} stroke="oklch(0.78 0.12 195)" stroke-width="1.2" fill="none" opacity="0.8"/>`}
${svg`<path id="trace-z" d=${make(traceZ.value)} stroke="oklch(0.72 0.18 330)" stroke-width="1.2" fill="none" opacity="0.7"/>`}
</svg>
${this.expanded ? html`<div style="display:flex;gap:14px;font-size:12px;font-family:var(--mono);margin-top:8px;">
<span style="color:oklch(0.78 0.14 70);">x: ${bnT[0].toFixed(3)} nT</span>
<span style="color:oklch(0.78 0.12 195);">y: ${bnT[1].toFixed(3)} nT</span>
<span style="color:oklch(0.72 0.18 330);">z: ${bnT[2].toFixed(3)} nT</span>
<span style="color:var(--accent);margin-left:auto;">|B| ${(bMag.value * 1e9).toFixed(3)} nT</span>
</div>` : ''}
</div>
<div class="frame-strip" id="frame-strip">
${stripBars.value.map((v) => html`<div class="bar" style=${`height:${Math.max(4, v * 100)}%`}></div>`)}
<div class="card">
<div class="card-h">
<span class="ttl">Frame stream</span>
<span class="badge" id="strip-rate">live</span>
</div>
<div class="frame-strip" id="frame-strip">
${stripBars.value.map((v) => html`<div class="bar" style=${`height:${Math.max(4, v * 100)}%`}></div>`)}
</div>
${this.expanded ? html`
<div style="display:flex;gap:24px;font-family:var(--mono);font-size:12px;color:var(--ink-3);margin-top:12px;">
<span>frames in window: <span style="color:var(--ink);">${stripBars.value.length}</span></span>
<span>noise floor: <span style="color:var(--ink);">${lastFrame.value ? lastFrame.value.noiseFloorPtSqrtHz.toFixed(2) + ' pT/√Hz' : '—'}</span></span>
</div>` : ''}
</div>
</div>
`;
@ -208,6 +286,7 @@ export class NvInspector extends LitElement {
hex = arr.slice(0, 60).join(' ');
}
return html`
<div class=${this.expanded ? 'grid-2' : ''}>
<div class="card">
<div class="card-h">
<span class="ttl">MagFrame v1 fields</span>
@ -232,6 +311,11 @@ export class NvInspector extends LitElement {
<span class="badge">LE</span>
</div>
<div class="hex" id="frame-hex">${hex || '—'}</div>
${this.expanded ? html`
<div style="font-size: 11.5px; color: var(--ink-3); margin-top: 10px; line-height: 1.6;">
Layout (little-endian): <code>magic(u32) version(u16) flags(u16) sensor_id(u16) _reserved(u16) t_us(u64) b_pt[3](f32) sigma_pt[3](f32) noise_floor(f32) temp_K(f32)</code>.
</div>` : ''}
</div>
</div>
`;
}
@ -244,7 +328,34 @@ export class NvInspector extends LitElement {
status === 'ok' ? '✓ Witness verified · determinism gate' :
status === 'fail' ? '✗ Witness mismatch · audit required' :
'Verify witness';
const match = expectedWitness.value && witnessHex.value && expectedWitness.value === witnessHex.value;
return html`
${this.expanded ? html`
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:12px;margin-bottom:18px;">
<div class="card" style="margin:0;">
<div style="font-size:10px;color:var(--ink-3);text-transform:uppercase;letter-spacing:0.06em;">Reference scene</div>
<div style="font-family:var(--mono);font-size:14px;color:var(--ink);margin-top:4px;">Proof::REFERENCE</div>
<div style="font-size:11.5px;color:var(--ink-3);margin-top:2px;">2 dipoles · 1 loop · 1 ferrous · 1 sensor</div>
</div>
<div class="card" style="margin:0;">
<div style="font-size:10px;color:var(--ink-3);text-transform:uppercase;letter-spacing:0.06em;">Seed</div>
<div style="font-family:var(--mono);font-size:14px;color:var(--accent);margin-top:4px;">0x0000002A</div>
<div style="font-size:11.5px;color:var(--ink-3);margin-top:2px;">canonical Proof::SEED</div>
</div>
<div class="card" style="margin:0;">
<div style="font-size:10px;color:var(--ink-3);text-transform:uppercase;letter-spacing:0.06em;">Sample count</div>
<div style="font-family:var(--mono);font-size:14px;color:var(--ink);margin-top:4px;">256</div>
<div style="font-size:11.5px;color:var(--ink-3);margin-top:2px;">Proof::N_SAMPLES</div>
</div>
<div class="card" style="margin:0;">
<div style="font-size:10px;color:var(--ink-3);text-transform:uppercase;letter-spacing:0.06em;">Status</div>
<div style="font-family:var(--mono);font-size:14px;margin-top:4px;color:${status === 'ok' ? 'var(--ok)' : status === 'fail' ? 'var(--bad)' : 'var(--ink-3)'};">
${status === 'ok' ? '✓ matches' : status === 'fail' ? '✗ drift' : status === 'pending' ? '… running' : '— idle'}
</div>
<div style="font-size:11.5px;color:var(--ink-3);margin-top:2px;">${match ? 'byte-equivalent' : 'not yet verified'}</div>
</div>
</div>
` : ''}
<div class="card">
<div class="card-h">
<span class="ttl">Expected (Proof::EXPECTED_WITNESS_HEX)</span>
@ -260,17 +371,44 @@ export class NvInspector extends LitElement {
<div class="witness-box" id="actual-witness">${witnessHex.value || '(not verified yet)'}</div>
<button class="verify-btn ${cls}" id="verify-btn" @click=${this.verify}>${label}</button>
</div>
${this.expanded ? html`
<div class="card">
<div class="card-h">
<span class="ttl">What this verifies</span>
<span class="badge">ADR-089 §5</span>
</div>
<div style="font-size: 12.5px; color: var(--ink-2); line-height: 1.6;">
<p style="margin: 0 0 10px;">Pressing <b>Verify</b> runs the canonical reference pipeline
(<code>Proof::generate</code>) end-to-end inside this browser's WASM Worker:
scene Biot-Savart synthesis material attenuation NV ensemble ADC + lock-in
concatenated <code>MagFrame</code> bytes SHA-256.</p>
<p style="margin: 0 0 10px;">If the resulting hash matches the constant pinned at build time
(<code>cc8de9b01b0ff5bd</code>), every constant γ_e, D_GS, μ, T*, contrast, the PRNG
stream, the frame layout, the pipeline ordering is byte-identical to the published
reference. If it doesn't match, <i>something</i> drifted; the dashboard names which.</p>
<p style="margin: 0;">This is the same regression test that runs in
<code>cargo test -p nvsim</code> running in your browser, against your own WASM build.</p>
</div>
</div>
` : ''}
`;
}
override render() {
return html`
<div class="tabs">
<button class="tab ${this.tab === 'signal' ? 'active' : ''}" data-pane="signal" @click=${() => this.tab = 'signal'}>Signal</button>
<button class="tab ${this.tab === 'frame' ? 'active' : ''}" data-pane="frame" @click=${() => this.tab = 'frame'}>Frame</button>
<button class="tab ${this.tab === 'witness' ? 'active' : ''}" data-pane="witness" @click=${() => this.tab = 'witness'}>Witness</button>
<div class="tabs" role="tablist">
<button class="tab ${this.tab === 'signal' ? 'active' : ''}" data-pane="signal"
role="tab" aria-selected=${this.tab === 'signal'}
@click=${() => this.tab = 'signal'}>Signal</button>
<button class="tab ${this.tab === 'frame' ? 'active' : ''}" data-pane="frame"
role="tab" aria-selected=${this.tab === 'frame'}
@click=${() => this.tab = 'frame'}>Frame</button>
<button class="tab ${this.tab === 'witness' ? 'active' : ''}" data-pane="witness"
role="tab" aria-selected=${this.tab === 'witness'}
@click=${() => this.tab = 'witness'}>Witness</button>
</div>
<div class="body">
<div class="body" role="tabpanel">
${this.renderHeader()}
${this.tab === 'signal' ? this.renderSignalTab()
: this.tab === 'frame' ? this.renderFrameTab()
: this.renderWitnessTab()}