feat(dashboard): App Store runtime — 6 simulated apps emit real events live

Closes the "do the App Store toggles actually do anything?" question:
they now do, for the subset of apps whose algorithms map onto nvsim's
magnetic frame stream as a proxy for their native CSI input.

## New: AppManifest.runtime field

Three values:
- `running`     — algorithm genuinely runs in browser (just nvsim today)
- `simulated`   — pared-down version against nvsim's B-field stream
- `mesh-only`   — needs ESP32-S3 + WS transport (deferred to V2)

Visible in the App Store as a colored badge on every card with hover
tooltip explaining what activation actually does.

## New: appRuntimes.ts — 6 in-browser simulated runtimes

- `vital_trend`     — peak-detect on B_z oscillation → 1 Hz HR/BR
                      events 100/101/102/103/104 + bradycardia/tachypnea
- `occupancy`       — variance threshold on |B| → 300/302
- `intrusion`       — |B| > 1.5× ambient + 0.5 s dwell → 200
- `coherence`       — recent vs baseline z-score → 2
- `adversarial`     — log-jump anomaly in |B| → 3
- `exo_ghost_hunter`— impulsive/drift/random anomaly classification → 651

Each receives an AppRuntimeContext (frame, |B|, history, elapsed-time,
per-app scratch state) and emits real i32 event IDs matching the
event_types mod in wifi-densepose-wasm-edge.

## Runtime dispatcher in main.ts

On every MagFrameBatch from the worker, iterate over activeAppIds.
For each id with a registered runtime, call the runtime fn with the
context, push any returned events into appEvents + the console feed.
mesh-only apps no-op silently (their toggle still persists for the
WS transport).

## App Store UI

- Per-card runtime badge (running / simulated / mesh-only) with tooltip
- "Live runtime feed" panel above the grid: shows last 12 emitted
  events with timestamp, app id, event name + i32 id, detail
- Active simulated-app counter: "5 simulated apps active"
- Per-card event counter " N ev" once events arrive
- Toggle log line includes runtime mode: "live runtime engaged" /
  "queued (needs ESP32 mesh)"

Validated end-to-end on https://ruvnet.github.io/RuView/nvsim/ — toggled
{vital_trend, occupancy, intrusion, coherence}, pressed Run, the feed
filled with real events: COHERENCE_SCORE z=0.87 stable, VITAL_TREND
HR=40 BPM BR=10, BRADYCARDIA, BRADYPNEA. Console log mirrors with
[appId] prefix. Zero browser errors.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-04-27 11:31:33 -04:00
parent 6fe405a5f7
commit cedb28db83
5 changed files with 442 additions and 7 deletions
+42 -2
View File
@@ -9,7 +9,9 @@ import {
pushLog, expectedWitness, framesEmitted, fps, lastB, bMag,
pushTrace, pushStripBar, lastFrame, sceneJson, witnessHex,
replHistory, scenePositions, type SceneItemPos,
activeAppIds, pushAppEvent,
} from './store/appStore';
import { APP_RUNTIMES, type AppRuntimeContext } from './store/appRuntimes';
import { kvGet, kvSet } from './store/persistence';
function applyTheme(t: string): void {
@@ -59,6 +61,11 @@ function applyMotion(reduced: boolean): void {
}
});
// Per-app runtime scratch state + history buffer.
const appState: Record<string, Record<string, number>> = {};
const bMagHistory: number[] = [];
const runtimeStartTs = performance.now();
client.onFrames((batch) => {
if (batch.frames.length === 0) return;
const last = batch.frames[batch.frames.length - 1];
@@ -67,11 +74,44 @@ function applyMotion(reduced: boolean): void {
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.
const bmagT = Math.sqrt(bx * bx + by * by + bz * bz);
bMag.value = bmagT;
pushTrace([bx * 1e9, by * 1e9, bz * 1e9]);
const amp = Math.min(1, Math.abs(bz * 1e9) / 5 + 0.3);
pushStripBar(amp);
bMagHistory.push(bmagT);
while (bMagHistory.length > 256) bMagHistory.shift();
// Dispatch the frame to every active simulated app runtime.
const activeIds = activeAppIds.value;
if (activeIds.size === 0) return;
const elapsedS = (performance.now() - runtimeStartTs) / 1000;
for (const id of activeIds) {
const fn = APP_RUNTIMES[id];
if (!fn) continue; // mesh-only apps — toggle persists, no in-browser runtime
if (!appState[id]) appState[id] = {};
const ctx: AppRuntimeContext = {
frame: last,
bMagT: bmagT,
bRecoveredT: [bx, by, bz],
bHistory: bMagHistory,
elapsedS,
state: appState[id],
};
try {
const result = fn(ctx);
if (!result) continue;
const evs = Array.isArray(result) ? result : [result];
for (const ev of evs) {
pushAppEvent(ev);
pushLog('info',
`<span class="k">[${ev.appId}]</span> <span class="s">${ev.eventName}</span> <span class="n">(${ev.eventId})</span>${ev.detail ? ' · ' + ev.detail : ''}`);
}
} catch (e) {
pushLog('warn', `[${id}] runtime error: ${(e as Error).message}`);
}
}
});
try {