mirror of
https://github.com/ruvnet/RuView
synced 2026-08-02 19:11:46 +00:00
feat(nvsim): server + onboarding + PWA + GH Pages workflow [ADR-092]
Rounds out the dashboard surface introduced in 39ec05edc with all four
remaining ADR-092 deliverables, plus a deploy workflow that publishes
the SPA to gh-pages/nvsim/ without disturbing the existing observatory
or pose-fusion demos.
## nvsim-server (ADR-092 §6.2)
New crate `v2/crates/nvsim-server`. Axum host fronting nvsim::Pipeline:
- REST control plane (15 routes) — /api/health, /api/scene, /api/config,
/api/seed, /api/run, /api/pause, /api/reset, /api/step,
/api/witness/{generate,verify,reference}, /api/export-proof
- Binary WebSocket data plane at /ws/stream — pushes 32-frame
MagFrame batches at ~60 Hz tick rate
- /api/witness/verify always runs the canonical Proof::generate so the
hash matches Proof::EXPECTED_WITNESS_HEX byte-for-byte across WASM
and WS transports — the determinism contract.
- CORS configurable via --allowed-origin, listens on 127.0.0.1:7878 by
default, single-binary deployment.
## Onboarding tour (ADR-092 §10 Pass 6)
`<nv-onboarding>` Lit component, 6-step welcome:
Welcome → Scene canvas → Run → Witness → App Store → Done.
First-run only — persisted via IndexedDB `onboarding-seen` flag.
Re-triggerable via `nv-show-tour` event for the help menu.
## PWA service worker (ADR-092 §9.3)
vite-plugin-pwa wired with workbox-window. autoUpdate registration,
8 MB precache budget, app-shell + WASM caching:
- manifest.webmanifest with /RuView/nvsim/ scope
- icon-192.svg + icon-512.svg in dashboard/public/
- 16 precache entries / 302 KiB
Verified production build under NVSIM_BASE=/RuView/nvsim/:
dist/index.html → /RuView/nvsim/assets/...
dist/manifest.webmanifest → scope: /RuView/nvsim/
dist/sw.js + workbox-*.js generated cleanly
## GitHub Pages deploy workflow
`.github/workflows/dashboard-pages.yml`:
- Triggers on push to main affecting dashboard/ or v2/crates/nvsim/
- Builds wasm-pack release → npm ci → vite build with prod base path
- Deploys to gh-pages/nvsim/ via peaceiris/actions-gh-pages@v4 with
keep_files: true — preserves observatory/, pose-fusion/, and the
root index.html landing page
After first run, the dashboard will be live at:
https://ruvnet.github.io/RuView/nvsim/
Validated end-to-end with `npx agent-browser`:
- Onboarding modal renders on first visit
- Workspace `cargo check --workspace` clean (1 warning in unrelated
sensing-server, no nvsim-server warnings after dead-code prune)
- Production build passes with correct base path resolution and
PWA manifest scope
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
Generated
+12
-13
@@ -9,12 +9,13 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"lit": "^3.2.1"
|
||||
"lit": "^3.2.1",
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^2.1.4"
|
||||
}
|
||||
},
|
||||
@@ -5861,17 +5862,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.21.2.tgz",
|
||||
"integrity": "sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz",
|
||||
"integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.6",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"tinyglobby": "^0.2.10",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0"
|
||||
"workbox-build": "^7.4.0",
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -5880,10 +5881,10 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0"
|
||||
"@vite-pwa/assets-generator": "^1.0.0",
|
||||
"vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
"workbox-build": "^7.4.0",
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vite-pwa/assets-generator": {
|
||||
@@ -6304,7 +6305,6 @@
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz",
|
||||
"integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/workbox-expiration": {
|
||||
@@ -6420,7 +6420,6 @@
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz",
|
||||
"integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2",
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"lit": "^3.2.1"
|
||||
"lit": "^3.2.1",
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^2.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" width="192" height="192">
|
||||
<rect width="192" height="192" rx="36" fill="#e6a86b"/>
|
||||
<text x="96" y="124" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-weight="700" font-size="80" fill="#1a0f00">NV</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#e6a86b"/>
|
||||
<stop offset="1" stop-color="#a4633a"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="96" fill="url(#g)"/>
|
||||
<text x="256" y="332" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-weight="700" font-size="220" fill="#1a0f00">NV</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
@@ -16,6 +16,7 @@ import './nv-modal';
|
||||
import './nv-palette';
|
||||
import './nv-debug-hud';
|
||||
import './nv-settings-drawer';
|
||||
import './nv-onboarding';
|
||||
|
||||
export type View = 'scene' | 'apps' | 'settings';
|
||||
|
||||
@@ -87,6 +88,7 @@ export class NvApp extends LitElement {
|
||||
<nv-palette></nv-palette>
|
||||
<nv-debug-hud></nv-debug-hud>
|
||||
<nv-settings-drawer></nv-settings-drawer>
|
||||
<nv-onboarding></nv-onboarding>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/* First-run welcome tour. 5 steps walking the user through the
|
||||
* dashboard's main concepts. Persists `seen` flag in IndexedDB so it
|
||||
* only shows the first time. ADR-092 §10 Pass 6.
|
||||
*/
|
||||
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { kvGet, kvSet } from '../store/persistence';
|
||||
|
||||
interface TourStep {
|
||||
title: string;
|
||||
body: string;
|
||||
cta?: string;
|
||||
}
|
||||
|
||||
const STEPS: TourStep[] = [
|
||||
{
|
||||
title: 'Welcome to nvsim',
|
||||
body: `<p><b>nvsim</b> is an open-source, deterministic forward simulator for
|
||||
nitrogen-vacancy diamond magnetometry — a real Rust crate compiled to
|
||||
WASM and running in your browser, right now.</p>
|
||||
<p>This 30-second tour highlights the four panels you'll use most.</p>`,
|
||||
cta: 'Start tour',
|
||||
},
|
||||
{
|
||||
title: '1. Scene canvas',
|
||||
body: `<p>The middle panel shows your <b>magnetic scene</b> — sources you can
|
||||
drag (rebar, heart proxy, mains hum, ferrous door) and a single NV-diamond
|
||||
sensor in the centre. Field lines from each source connect to the sensor
|
||||
and animate while the pipeline runs.</p>
|
||||
<p>Click <code>2</code> on your keyboard any time to jump to the Frame inspector.</p>`,
|
||||
},
|
||||
{
|
||||
title: '2. Run the pipeline',
|
||||
body: `<p>Click the <b>▶ Run</b> button (top-right) to start streaming
|
||||
<code>MagFrame</code> records at the digitiser's sample rate. The B-vector
|
||||
trace and Frame stream sparkline update live, and the FPS pill in the
|
||||
topbar shows the simulator's throughput in kHz.</p>
|
||||
<p><kbd>Space</kbd> toggles run/pause from anywhere.</p>`,
|
||||
},
|
||||
{
|
||||
title: '3. Witness panel',
|
||||
body: `<p>The <b>Witness</b> tab is the heart of nvsim's determinism contract.
|
||||
Click <b>Verify</b> and the pipeline re-derives the SHA-256 over a 256-frame
|
||||
reference run and asserts it matches the constant pinned in the Rust crate.</p>
|
||||
<p>Same input → same hash → byte-for-byte across browsers, OSes, transports.
|
||||
If the hash drifts, your build is non-canonical.</p>`,
|
||||
},
|
||||
{
|
||||
title: '4. App Store',
|
||||
body: `<p>The grid icon on the left rail opens the <b>App Store</b> — every
|
||||
hot-loadable WASM edge module RuView ships, plus the simulators. 66 apps
|
||||
across 13 categories: medical, security, building, retail, industrial,
|
||||
signal, learning, autonomy, and more.</p>
|
||||
<p>Toggle any card to mark it active in this session; the WS transport
|
||||
will push the activation set to a connected ESP32 mesh.</p>`,
|
||||
},
|
||||
{
|
||||
title: 'You are ready',
|
||||
body: `<p>Press <kbd>⌘K</kbd> (or <kbd>Ctrl K</kbd>) any time for the command
|
||||
palette, <kbd>?</kbd> for the full shortcuts list, or just start clicking.</p>
|
||||
<p>Source on GitHub:
|
||||
<code>github.com/ruvnet/RuView</code> · ADR-089, ADR-092 · MIT/Apache-2.0.</p>`,
|
||||
cta: 'Get started',
|
||||
},
|
||||
];
|
||||
|
||||
@customElement('nv-onboarding')
|
||||
export class NvOnboarding extends LitElement {
|
||||
@state() private open = false;
|
||||
@state() private step = 0;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 240;
|
||||
display: grid; place-items: center;
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: opacity 0.18s;
|
||||
}
|
||||
:host([open]) { opacity: 1; pointer-events: auto; }
|
||||
.card {
|
||||
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(560px, 92vw);
|
||||
max-height: 86vh;
|
||||
display: flex; flex-direction: column;
|
||||
transform: translateY(12px) scale(0.98);
|
||||
transition: transform 0.22s cubic-bezier(0.2,0.7,0.3,1);
|
||||
}
|
||||
:host([open]) .card { transform: translateY(0) scale(1); }
|
||||
.h {
|
||||
padding: 20px 22px 8px;
|
||||
display: flex; justify-content: space-between; align-items: flex-start;
|
||||
}
|
||||
.h h2 { margin: 0; font-size: 18px; letter-spacing: -0.01em; }
|
||||
.body {
|
||||
padding: 8px 22px 16px;
|
||||
font-size: 13px; color: var(--ink-2); line-height: 1.55;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.body p { margin: 0 0 12px; }
|
||||
.body code, .body kbd {
|
||||
font-family: var(--mono); font-size: 11.5px;
|
||||
padding: 1px 5px; background: var(--bg-3);
|
||||
border: 1px solid var(--line); border-radius: 4px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.footer {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 12px 22px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.dots { display: flex; gap: 6px; flex: 1; }
|
||||
.dot {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--bg-3); border: 1px solid var(--line-2);
|
||||
}
|
||||
.dot.active { background: var(--accent); border-color: var(--accent); }
|
||||
button {
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 12.5px; font-weight: 500;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg-2); color: var(--ink);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
button.primary {
|
||||
background: var(--accent); border-color: var(--accent);
|
||||
color: #1a0f00;
|
||||
}
|
||||
button.ghost { background: transparent; }
|
||||
.skip {
|
||||
width: 28px; height: 28px;
|
||||
background: transparent; border: 1px solid var(--line);
|
||||
border-radius: 6px; color: var(--ink-2);
|
||||
}
|
||||
`;
|
||||
|
||||
override async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
window.addEventListener('nv-show-tour', this.show as EventListener);
|
||||
const seen = await kvGet<boolean>('onboarding-seen');
|
||||
if (!seen) {
|
||||
this.open = true;
|
||||
this.setAttribute('open', '');
|
||||
}
|
||||
}
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener('nv-show-tour', this.show as EventListener);
|
||||
}
|
||||
|
||||
private show = (): void => {
|
||||
this.step = 0;
|
||||
this.open = true;
|
||||
this.setAttribute('open', '');
|
||||
};
|
||||
|
||||
private async dismiss(): Promise<void> {
|
||||
this.open = false;
|
||||
this.removeAttribute('open');
|
||||
await kvSet('onboarding-seen', true);
|
||||
}
|
||||
|
||||
private next(): void {
|
||||
if (this.step < STEPS.length - 1) this.step++;
|
||||
else void this.dismiss();
|
||||
}
|
||||
|
||||
private prev(): void {
|
||||
if (this.step > 0) this.step--;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const s = STEPS[this.step];
|
||||
return html`
|
||||
<div class="card" role="dialog" aria-modal="true" aria-label="Welcome tour">
|
||||
<div class="h">
|
||||
<h2>${s.title}</h2>
|
||||
<button class="skip" @click=${() => this.dismiss()} aria-label="Skip tour">×</button>
|
||||
</div>
|
||||
<div class="body" .innerHTML=${s.body}></div>
|
||||
<div class="footer">
|
||||
<div class="dots">
|
||||
${STEPS.map((_, i) => html`<div class="dot ${i === this.step ? 'active' : ''}"></div>`)}
|
||||
</div>
|
||||
${this.step > 0
|
||||
? html`<button class="ghost" @click=${() => this.prev()}>Back</button>`
|
||||
: ''}
|
||||
<button class="primary" @click=${() => this.next()}>
|
||||
${this.step === STEPS.length - 1 ? (s.cta ?? 'Done') : (s.cta ?? 'Next')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
// Dashboard for ADR-092 — Vite + Lit + WASM in a Web Worker.
|
||||
// Hosted at /RuView/nvsim/ on GitHub Pages; base path is configurable
|
||||
@@ -11,6 +12,48 @@ export default defineConfig({
|
||||
worker: {
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: [
|
||||
'nvsim-pkg/nvsim.js',
|
||||
'nvsim-pkg/nvsim_bg.wasm',
|
||||
],
|
||||
manifest: {
|
||||
name: 'nvsim — NV-Diamond Magnetometer Simulator',
|
||||
short_name: 'nvsim',
|
||||
description: 'Deterministic forward simulator for NV-diamond magnetometry. WASM-backed CW-ODMR pipeline with witness-grade SHA-256 proofs.',
|
||||
theme_color: '#0d1117',
|
||||
background_color: '#0d1117',
|
||||
display: 'standalone',
|
||||
scope: base,
|
||||
start_url: base,
|
||||
icons: [
|
||||
{
|
||||
src: 'icon-192.svg',
|
||||
sizes: '192x192',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
{
|
||||
src: 'icon-512.svg',
|
||||
sizes: '512x512',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,svg,wasm,woff,woff2}'],
|
||||
// WASM is large; bump the precache size budget so workbox doesn't
|
||||
// skip nvsim_bg.wasm.
|
||||
maximumFileSizeToCacheInBytes: 8 * 1024 * 1024,
|
||||
},
|
||||
devOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
target: 'es2022',
|
||||
sourcemap: true,
|
||||
@@ -27,13 +70,9 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
fs: {
|
||||
// wasm-pack output sits in public/nvsim-pkg; vite already serves it,
|
||||
// but allow fs reads from the workspace root for HMR convenience.
|
||||
allow: ['..', '.'],
|
||||
},
|
||||
headers: {
|
||||
// SAB ring buffer is opt-in; these headers are no-op without crossOriginIsolated
|
||||
// but make local dev parity with a future CORS-isolated host.
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user