mirror of
https://github.com/ruvnet/RuView
synced 2026-07-05 14:33:19 +00:00
20ad75f30c
* feat(ADR-131): HOMECORE-UI operational dashboard + BFF gateway Complete two-tier Cognitum operator dashboard (ADR-131), served by homecore-server at /homecore, plus the single-origin BFF gateway that wires it to real backends. Front-end (zero-dep vanilla TS/JS + CSS, exact Cognitum design tokens): - All 10 panels (§4.1-4.10): dashboard, SEED fleet + detail, fleet map, entities (live WS subscribe_events, never polls), rooms, COGs, calibration wizard, events + automation builder, witness/audit, settings. - §6 UX invariants in code: first-class provenance, prominent stale/veto/ fragility, null(not-trained) vs withheld vs error, --mono everywhere, Hailo vs CPU COG distinction. - api.js calls the gateway routes in production; mock demoted to a dev-only ?demo=1 fixture (no mock in prod); typed error states. - Tests under plain node: import-graph, boot, render-smoke (22), interaction (3), prod-errors (13) — 5 files green; bundle ~137 KB (~37x smaller than HA), <2 ms/cold-render. BFF gateway (homecore-server/src/gateway.rs, compiled + tested on Rust 1.89): - /api/cal/* reverse-proxy to the calibration API (ADR-151). - GET /api/homecore/rooms with the RoomState adapter (breathing->breathing_bpm, heartbeat:null->heart_bpm:null, injected anomaly.threshold/room_id). - GET /api/homecore/cogs supervisor over /var/lib/cognitum/apps/. - GET /api/homecore/appliance from /proc + TCP service probes. - SEED-device/appliance routes return typed 503 upstream_unavailable. - cargo test -p homecore-server = 12/12; run live (curl-verified); fixed a real double-v1 proxy-URL bug found during live testing. Honest scope: W1/W2/W4/W6-appliance functional; W3/W5/W6-Hailo/federation return typed 503 (depend on services/hardware not in this repo). Co-Authored-By: claude-flow <ruv@ruv.net> * fix(homecore-ui): resolve code-review findings — SSRF guard, CORS/trace coverage, §6 honesty, crash guards Addresses the high-effort review of PR #1082: - SECURITY: cal_proxy rejects path-traversal/confused-deputy SSRF (`.`/`..` segments, backslash, %2e%2e/%2f, absolute) on raw+decoded forms → 400, before attaching the server-side calibration bearer. - CORRECTNESS: /api/homecore/* + /api/cal/* now covered by the shared CORS allowlist (build_cors_layer, exported from homecore-api) + TraceLayer — previously merged outside router()'s layers (no CORS, no tracing). - §6 HONESTY (no fabricated data): dashboard renders '—' for null metrics (not "null%"/"null°C"); cogs Hailo pill reflects the REAL appliance probe (not hardcoded "connected"); room anomaly threshold passed through / null, not a fabricated 0.5. - ROBUSTNESS: cogs asArray(hef) guards a non-array manifest field; calibration progress guards target<=0 (no NaN%/Infinity%); restart clears the poll timer. - CLEANUP: mock.js is now a cached DYNAMIC import (demo-only) — never bundled in production (§2.2). - New ui/tests/unit-fixes.mjs pins the above; ADR-131 + CHANGELOG updated. Co-Authored-By: claude-flow <ruv@ruv.net> --------- Co-authored-by: Nick Ruest <127058086+nicholas-ruest@users.noreply.github.com>
102 lines
4.9 KiB
JavaScript
102 lines
4.9 KiB
JavaScript
// Regression tests pinning the ADR-131 PR-1082 review fixes:
|
|
// * dashboard renders a not-available state ('—') for null appliance
|
|
// metrics — never "null%"/"null°C" (§6 honesty / fabricated-data fix).
|
|
// * cogs panel does NOT throw when the gateway forwards a `hef` that is a
|
|
// string (or other non-array) instead of an array (crash/robustness fix).
|
|
// * cogs Hailo worker pill reflects the real probe, not a hardcoded
|
|
// "connected" (§6 honesty fix).
|
|
// Run: node tests/unit-fixes.mjs
|
|
import { install } from './dom-shim.mjs';
|
|
install();
|
|
globalThis.HOMECORE_UI_DEMO = false; // production path — no fixtures
|
|
|
|
const fails = [], passes = [];
|
|
async function t(name, fn) {
|
|
try { await fn(); passes.push(name); }
|
|
catch (e) { fails.push(`${name}: ${e && e.stack ? e.stack.split('\n').slice(0, 3).join(' | ') : e}`); }
|
|
}
|
|
const assert = (c, m) => { if (!c) throw new Error(m || 'assertion failed'); };
|
|
|
|
const { api } = await import('../js/api.js');
|
|
|
|
// Shared ctx; per-test we override the api accessors we need.
|
|
function ctxWith(overrides) {
|
|
return {
|
|
api: Object.assign(Object.create(api), overrides),
|
|
navigate() {},
|
|
params: {},
|
|
onEvent() { return () => {}; },
|
|
onWs(fn) { fn({ state: 'closed', lagged: false }); return () => {}; },
|
|
};
|
|
}
|
|
|
|
// ── dashboard: null metrics → '—', never "null%"/"null°C" ─────────────
|
|
await t('dashboard renders not-available for null hailo metrics (no "null%")', async () => {
|
|
const mod = await import('../js/panels/dashboard.js');
|
|
const root = document.createElement('div');
|
|
const ctx = ctxWith({
|
|
appliance: async () => ({
|
|
cpu_pct: 12.5, ram_pct: 40.1,
|
|
hailo_load_pct: null, hailo_temp_c: null, // the fabricated-data trap
|
|
uptime_s: null,
|
|
services: [{ name: 'ruview-mcp-brain', port: 9876, status: 'unreachable' }],
|
|
event_rate: [], channel_capacity: 4096, channel_lag: 0,
|
|
}),
|
|
seeds: async () => [],
|
|
esp32Warnings: async () => [],
|
|
cogs: async () => [],
|
|
anyDemo: () => false,
|
|
});
|
|
const cleanup = await mod.default.render(root, ctx);
|
|
const text = root.textContent;
|
|
assert(!/null\s*%/.test(text), `dashboard showed "null%": ${text.slice(0, 200)}`);
|
|
assert(!/null\s*°C/.test(text), `dashboard showed "null°C": ${text.slice(0, 200)}`);
|
|
assert(text.includes('—'), 'dashboard should render the "—" not-available marker for null metrics');
|
|
// real values must still concatenate their unit
|
|
assert(text.includes('12.5%'), 'real CPU value must still render with its unit');
|
|
if (typeof cleanup === 'function') cleanup();
|
|
});
|
|
|
|
// ── cogs: string `hef` must not throw ─────────────────────────────────
|
|
await t('cogs does not throw when hef is a string (non-array)', async () => {
|
|
const mod = await import('../js/panels/cogs.js');
|
|
const root = document.createElement('div');
|
|
const ctx = ctxWith({
|
|
cogs: async () => [
|
|
{ id: 'cog-pose', version: '1.0', arch: 'hailo10', status: 'running', pid: 42,
|
|
sha256_verified: true, signature_verified: true, throughput_fps: 30,
|
|
hef: 'pose_estimation.hef' }, // STRING, not array — the crash trap
|
|
],
|
|
cogUpdates: async () => [],
|
|
appliance: async () => ({ services: [{ name: 'ruvector-hailo-worker', port: 50051, status: 'running' }] }),
|
|
isDemo: () => false,
|
|
});
|
|
// If asArray() weren't applied, .forEach/.join/.length on a string would throw.
|
|
const cleanup = await mod.default.render(root, ctx);
|
|
assert(root.children.length > 0, 'cogs rendered nothing');
|
|
// The string hef should surface as a single loaded HEF row.
|
|
assert(root.textContent.includes('pose_estimation.hef'), 'string hef should render as one HEF entry');
|
|
if (typeof cleanup === 'function') cleanup();
|
|
});
|
|
|
|
// ── cogs: Hailo worker pill reflects the real probe, not hardcoded ────
|
|
await t('cogs Hailo worker pill is unknown when appliance probe is unavailable', async () => {
|
|
const mod = await import('../js/panels/cogs.js');
|
|
const root = document.createElement('div');
|
|
const ctx = ctxWith({
|
|
cogs: async () => [],
|
|
cogUpdates: async () => [],
|
|
appliance: async () => { throw new Error('appliance upstream down'); }, // probe fails
|
|
isDemo: () => false,
|
|
});
|
|
const cleanup = await mod.default.render(root, ctx);
|
|
// statusPill('unknown') → grey pill containing the literal label "unknown".
|
|
assert(root.textContent.includes('unknown'), 'worker status should be honestly "unknown" when probe fails');
|
|
assert(!/connected/.test(root.textContent), 'worker pill must not fabricate "connected"');
|
|
if (typeof cleanup === 'function') cleanup();
|
|
});
|
|
|
|
console.log(`\n${passes.length} passed, ${fails.length} failed`);
|
|
if (fails.length) { console.error('\nFAILURES:'); fails.forEach((f) => console.error(' ✗ ' + f)); process.exit(1); }
|
|
console.log('OK — dashboard not-available, cogs string-hef + honest worker pill pinned');
|