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>
55 lines
2.9 KiB
JavaScript
55 lines
2.9 KiB
JavaScript
// Benchmark — ADR-131 §8 / ADR-126 §1.1.
|
||
// HOMECORE exists partly because HA's frontend is a ~5 MB Lit bundle
|
||
// (ADR-126 §1.1). This benchmark enforces a hard bundle budget and
|
||
// measures cold render throughput for all 10 panels.
|
||
// Run: node tests/benchmark.mjs
|
||
import { install } from './dom-shim.mjs';
|
||
install();
|
||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||
import { resolve } from 'node:path';
|
||
|
||
const ROOT = resolve(import.meta.dirname, '..');
|
||
const BUDGET_BYTES = 250 * 1024; // 250 KB total — vs HA's ~5 MB (20× smaller)
|
||
|
||
function walk(dir) {
|
||
let total = 0; const rows = [];
|
||
for (const name of readdirSync(dir)) {
|
||
if (name === 'tests' || name === 'node_modules') continue;
|
||
const p = resolve(dir, name); const s = statSync(p);
|
||
if (s.isDirectory()) { const sub = walk(p); total += sub.total; rows.push(...sub.rows); }
|
||
else if (/\.(js|css|html|json)$/.test(name)) { total += s.size; rows.push([p.replace(ROOT + '/', ''), s.size]); }
|
||
}
|
||
return { total, rows };
|
||
}
|
||
|
||
const { total, rows } = walk(ROOT);
|
||
rows.sort((a, b) => b[1] - a[1]);
|
||
console.log('── Bundle size (uncompressed) ──');
|
||
for (const [f, sz] of rows.slice(0, 8)) console.log(` ${(sz / 1024).toFixed(1).padStart(7)} KB ${f}`);
|
||
console.log(` ${'-'.repeat(40)}`);
|
||
console.log(` ${(total / 1024).toFixed(1).padStart(7)} KB TOTAL across ${rows.length} files`);
|
||
console.log(` budget ${(BUDGET_BYTES / 1024).toFixed(0)} KB · HA baseline ~5120 KB · ratio ${(5120 * 1024 / total).toFixed(1)}× smaller`);
|
||
|
||
// ── render throughput ───────────────────────────────────────────────
|
||
const { api } = await import('../js/api.js');
|
||
const ctx = { api, navigate() {}, params: { id: 'seed-livingroom-a1' }, onEvent() { return () => {}; }, onWs(fn) { fn({ state: 'open', lagged: false }); return () => {}; } };
|
||
const PANELS = ['dashboard', 'fleet', 'seed-detail', 'entities', 'rooms', 'cogs', 'calibration', 'events', 'audit', 'settings'];
|
||
const mods = {};
|
||
for (const p of PANELS) mods[p] = (await import(`../js/panels/${p}.js`)).default;
|
||
|
||
console.log('\n── Cold render throughput (avg of 50 renders each) ──');
|
||
let worst = 0;
|
||
for (const p of PANELS) {
|
||
const N = 50; const t0 = performance.now();
|
||
for (let i = 0; i < N; i++) { const root = document.createElement('div'); const c = await mods[p].render(root, ctx); if (typeof c === 'function') c(); }
|
||
const ms = (performance.now() - t0) / N;
|
||
worst = Math.max(worst, ms);
|
||
console.log(` ${ms.toFixed(3).padStart(7)} ms/render ${p}`);
|
||
}
|
||
|
||
console.log('');
|
||
let exit = 0;
|
||
if (total > BUDGET_BYTES) { console.error(`FAIL — bundle ${(total / 1024).toFixed(1)} KB exceeds ${(BUDGET_BYTES / 1024).toFixed(0)} KB budget`); exit = 1; }
|
||
else console.log(`OK — bundle within budget; slowest panel ${worst.toFixed(2)} ms/render`);
|
||
process.exit(exit);
|