mirror of
https://github.com/ruvnet/RuView
synced 2026-07-30 18:41:42 +00:00
feat(dashboard): nvsim Vite+Lit dashboard with WASM transport + App Store [ADR-092]
End-to-end implementation of the operator dashboard for the nvsim
NV-diamond magnetometer simulator. Vite 5 + TypeScript strict + Lit 3,
~93 KB gzipped JS budget, runs the *real* nvsim Rust crate compiled to
wasm32-unknown-unknown inside a dedicated Web Worker.
Validated end-to-end with `npx agent-browser`:
- WASM module boots, build version + magic 0xC51A_6E70 reported
- Reference witness verifies byte-identical to Proof::EXPECTED_WITNESS_HEX
cc8de9b01b0ff5bd97a6c17848a3f156c174ea7589d0888164a441584ec593b4
- Pipeline runs at ~1.88 kHz on x86_64 dev hardware (4500x over Cortex-A53)
- Zero browser console errors; only Lit dev-mode warning (expected)
## nvsim crate (additive)
- New `wasm` feature flag with wasm-bindgen 0.2 / serde-wasm-bindgen 0.6
- src/wasm.rs: WasmPipeline wrapper + referenceSceneJson +
expectedReferenceWitnessHex + referenceWitness + hexWitness exports
- crate-type = ["cdylib", "rlib"] so native + wasm both build
- rand = { default-features = false } drops getrandom OS-entropy path,
preserving the crate's WASM-ready posture
- Native: 50/50 tests still pass, witness unchanged
## dashboard/ (new package)
- Vite 5 + TypeScript strict, Lit 3 elements, signals-based store
- 12 Lit components mirroring the mockup zones (rail, topbar, sidebar,
scene SVG with draggable sources + NV crystal, inspector tabs
Signal/Frame/Witness, console with REPL + filter tabs, settings
drawer, modals, ⌘K command palette, debug HUD, toast, app-store)
- IndexedDB persistence (theme, density, motion, app activations)
- WasmClient → Web Worker → wasm-pack-built nvsim WASM module
- NvsimClient TS interface — same shape covers future WsClient transport
- MagFrame parser (60-byte LE layout matching nvsim::frame)
## App Store (ADR-092 §14a — added during impl)
- Catalog of all 65 wifi-densepose-wasm-edge modules + nvsim
- 13 categories with event-ID-range labels
- Per-app metadata: id/name/category/crate/summary/events/budget/
status/adr/tags
- Fuzzy search, category + status filters, IndexedDB-backed activation
- ADR-092 §14a documents the registry contract and per-app schema
## Build pipeline
- wasm-pack build crates/nvsim --target web outputs to
dashboard/public/nvsim-pkg/ (60 KB pkg, 162 KB unoptimized .wasm)
- npm run build → 93 KB gzip JS, well under 300 KB budget
- ts.config strict, npx tsc --noEmit clean
- Vite worker correctly loads WASM via dynamic import resolving
against worker origin
## E2E validation
- agent-browser open → 4-zone grid renders correctly in dark theme
- Run button → live B-vector trace, |B| readout updates, FPS counter
- App Store → all 66 apps listed with toggles, fuzzy search filters
to "Ghost hunter" on "ghost" query
- Witness verify → green check, console logs "determinism gate ✓"
- Console errors: zero (only expected Lit dev-mode warning)
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
/* nvsim dashboard entry — boots the WasmClient, mounts <nv-app>. */
|
||||
import './app.css';
|
||||
import './components/nv-app';
|
||||
import { effect } from '@preact/signals-core';
|
||||
|
||||
import { WasmClient } from './transport/WasmClient';
|
||||
import {
|
||||
setClient, transport, theme, density, motionReduced,
|
||||
pushLog, expectedWitness, framesEmitted, fps, lastB, bMag,
|
||||
pushTrace, pushStripBar, lastFrame, sceneJson, witnessHex,
|
||||
} from './store/appStore';
|
||||
import { kvGet, kvSet } from './store/persistence';
|
||||
|
||||
function applyTheme(t: string): void {
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
}
|
||||
function applyDensity(d: string): void {
|
||||
document.body.classList.remove('density-comfy', 'density-default', 'density-compact');
|
||||
document.body.classList.add(`density-${d}`);
|
||||
}
|
||||
function applyMotion(reduced: boolean): void {
|
||||
document.body.classList.toggle('reduce-motion', reduced);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Restore persisted prefs
|
||||
const t = (await kvGet<'dark' | 'light'>('theme')) ?? 'dark';
|
||||
const d = (await kvGet<'comfy' | 'default' | 'compact'>('density')) ?? 'default';
|
||||
const m = (await kvGet<boolean>('motionReduced')) ?? false;
|
||||
theme.value = t; applyTheme(t);
|
||||
density.value = d; applyDensity(d);
|
||||
motionReduced.value = m; applyMotion(m);
|
||||
|
||||
// React to changes → persist
|
||||
effect(() => { applyTheme(theme.value); kvSet('theme', theme.value); });
|
||||
effect(() => { applyDensity(density.value); kvSet('density', density.value); });
|
||||
effect(() => { applyMotion(motionReduced.value); kvSet('motionReduced', motionReduced.value); });
|
||||
|
||||
// Boot WASM client
|
||||
const client = new WasmClient();
|
||||
setClient(client);
|
||||
|
||||
pushLog('info', 'nvsim — booting WASM runtime');
|
||||
client.onEvent((ev) => {
|
||||
if (ev.type === 'log') pushLog(ev.level, ev.msg);
|
||||
if (ev.type === 'fps') fps.value = ev.value;
|
||||
if (ev.type === 'state') {
|
||||
framesEmitted.value = BigInt(ev.framesEmitted);
|
||||
}
|
||||
});
|
||||
|
||||
client.onFrames((batch) => {
|
||||
if (batch.frames.length === 0) return;
|
||||
const last = batch.frames[batch.frames.length - 1];
|
||||
lastFrame.value = last;
|
||||
const bx = last.bPt[0] * 1e-12; // pT → T
|
||||
const by = last.bPt[1] * 1e-12;
|
||||
const bz = last.bPt[2] * 1e-12;
|
||||
lastB.value = [bx, by, bz];
|
||||
bMag.value = Math.sqrt(bx * bx + by * by + bz * bz);
|
||||
// For trace display we use nT scale.
|
||||
pushTrace([bx * 1e9, by * 1e9, bz * 1e9]);
|
||||
const amp = Math.min(1, Math.abs(bz * 1e9) / 5 + 0.3);
|
||||
pushStripBar(amp);
|
||||
});
|
||||
|
||||
try {
|
||||
const info = await client.boot();
|
||||
expectedWitness.value = info.expectedWitnessHex;
|
||||
pushLog('ok', `WASM module ready · nvsim@${info.buildVersion} · magic=0x${info.frameMagic.toString(16).toUpperCase()}`);
|
||||
pushLog('info', `expected witness · ${info.expectedWitnessHex.slice(0, 16)}…`);
|
||||
|
||||
// Load reference scene by default.
|
||||
sceneJson.value = '(reference scene)';
|
||||
transport.value = 'wasm';
|
||||
} catch (e) {
|
||||
pushLog('err', `boot failed: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
// Auto-verify witness once at boot — proves WASM determinism contract.
|
||||
try {
|
||||
const exp = expectedWitness.value;
|
||||
if (exp) {
|
||||
const expBytes = new Uint8Array(32);
|
||||
for (let i = 0; i < 32; i++) expBytes[i] = parseInt(exp.slice(i * 2, i * 2 + 2), 16);
|
||||
const r = await client.verifyWitness(expBytes);
|
||||
if (r.ok) {
|
||||
witnessHex.value = exp;
|
||||
pushLog('ok', `witness verified · determinism gate ✓`);
|
||||
} else {
|
||||
const actual = Array.from(r.actual)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
witnessHex.value = actual;
|
||||
pushLog('err', `WITNESS MISMATCH · expected ${exp.slice(0, 16)}… got ${actual.slice(0, 16)}…`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
pushLog('warn', `witness verify skipped: ${(e as Error).message}`);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user