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:
parent
18c09d3305
commit
eed5feeab2
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
Loading…
Reference in New Issue