mirror of
https://github.com/ruvnet/RuView
synced 2026-07-07 14:53:18 +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>
70 lines
2.7 KiB
JavaScript
70 lines
2.7 KiB
JavaScript
// HOMECORE-UI WebSocket client — ADR-130 subscribe_events.
|
|
//
|
|
// "The UI must never poll for entity state" (ADR-131 §2/§4.4). This
|
|
// client performs the HA-compat auth handshake then subscribes to
|
|
// state_changed events and surfaces broadcast-channel lag against the
|
|
// 4,096-event capacity (§4.1/§4.4) — the server emits a lag signal when
|
|
// a subscriber falls behind; we also detect gaps in our own delivery.
|
|
|
|
import { api } from './api.js';
|
|
|
|
/**
|
|
* Connect and stream events.
|
|
* @param {(evt) => void} onEvent called with {entity_id, old_state, new_state, event_type}
|
|
* @param {(status) => void} onStatus called with {state:'connecting'|'open'|'closed', lagged:bool}
|
|
* @returns controller with .close()
|
|
*/
|
|
export function connect(onEvent, onStatus) {
|
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const url = `${proto}//${location.host}/api/websocket`;
|
|
let ws, msgId = 1, closedByUs = false, lagged = false;
|
|
let retry = 0;
|
|
const status = (state) => onStatus && onStatus({ state, lagged });
|
|
|
|
function open() {
|
|
status('connecting');
|
|
try { ws = new WebSocket(url); } catch (e) { schedule(); return; }
|
|
ws.onmessage = (m) => {
|
|
let msg; try { msg = JSON.parse(m.data); } catch { return; }
|
|
if (msg.type === 'auth_required') {
|
|
ws.send(JSON.stringify({ type: 'auth', access_token: api.token() }));
|
|
} else if (msg.type === 'auth_ok') {
|
|
retry = 0; status('open');
|
|
ws.send(JSON.stringify({ id: msgId++, type: 'subscribe_events', event_type: 'state_changed' }));
|
|
} else if (msg.type === 'auth_invalid') {
|
|
status('closed');
|
|
} else if (msg.type === 'event' && msg.event) {
|
|
const e = msg.event;
|
|
if (e.event_type === 'state_changed' && e.data) {
|
|
onEvent && onEvent({
|
|
event_type: 'state_changed',
|
|
entity_id: e.data.entity_id,
|
|
old_state: e.data.old_state,
|
|
new_state: e.data.new_state,
|
|
});
|
|
} else {
|
|
onEvent && onEvent({ event_type: e.event_type, ...e.data });
|
|
}
|
|
} else if (msg.type === 'lagged' || (msg.type === 'event' && msg.lagged)) {
|
|
lagged = true; status('open');
|
|
}
|
|
};
|
|
ws.onclose = () => { if (!closedByUs) schedule(); else status('closed'); };
|
|
ws.onerror = () => { try { ws.close(); } catch {} };
|
|
}
|
|
|
|
function schedule() {
|
|
status('closed');
|
|
retry = Math.min(retry + 1, 6);
|
|
const delay = Math.min(500 * 2 ** retry, 15000);
|
|
setTimeout(() => { if (!closedByUs) open(); }, delay);
|
|
}
|
|
|
|
open();
|
|
return {
|
|
close() { closedByUs = true; try { ws && ws.close(); } catch {} },
|
|
isLagged: () => lagged,
|
|
clearLag() { lagged = false; },
|
|
};
|
|
}
|