mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-21 22:22:33 -05:00
/api/v1/stream/status returned "fps": 10 whenever tick > 1, whatever the real rate, and both it and /health counted every broadcast subscriber (recorder, MQTT bridge) as a client. The dashboard Streaming card read "IDLE, 0 client(s)" for up to 30 s while the page was streaming, because its first /health poll lands before the page's socket connects; with auth on, the ticket round-trip widens that gap. - New stream_stats module: UpdateRateMeter (sliding 5 s window, bounded at 1024 samples) and WsClientCounter with an RAII guard. - AppStateInner::advance_tick() replaces the five `s.tick += 1` sites and records each sensing update in the meter. - /api/v1/stream/status now returns the measured update rate as `fps` (0 when nothing flows), `active` from that rate, `clients` from the WebSocket counter, `nodes` with each fresh node's measured csi_fps (null until its EMA has 5 samples) and `csi_fps_total`. - /health and the /health/health stream component count WebSocket clients on /ws/sensing and /api/v1/stream/pose; the stream component also carries the measured rate. - DashboardTab re-reads health 500 ms after the sensing socket connects or drops, so the card follows the socket instead of waiting for the next 30 s poll. Fixes #2087 Co-Authored-By: claude-flow <ruv@ruv.net>
71 lines
2.4 KiB
JavaScript
71 lines
2.4 KiB
JavaScript
// Executed regression test for #2087: the dashboard Streaming card read
|
|
// "IDLE, 0 client(s)" while the page's own socket was streaming, because the
|
|
// card is fed by a 30 s /health poll whose first read lands before the socket
|
|
// connects.
|
|
//
|
|
// Run: node --test ui/components/DashboardTab.test.mjs
|
|
//
|
|
// This EXECUTES DashboardTab's refresh wiring in Node with fake timers and a
|
|
// stubbed health service. It does not render the card in a browser.
|
|
|
|
import { test, beforeEach, mock } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
globalThis.localStorage = { getItem: () => null, setItem() {}, removeItem() {} };
|
|
globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => ({}) });
|
|
|
|
const { DashboardTab } = await import('./DashboardTab.js');
|
|
const { healthService } = await import('../services/health.service.js');
|
|
|
|
let healthCalls;
|
|
let tab;
|
|
|
|
beforeEach(() => {
|
|
healthCalls = 0;
|
|
healthService.getSystemHealth = async () => { healthCalls++; return {}; };
|
|
mock.timers.enable({ apis: ['setTimeout'] });
|
|
tab = new DashboardTab({ querySelector: () => null });
|
|
});
|
|
|
|
const advance = () => mock.timers.tick(DashboardTab.STREAM_HEALTH_REFRESH_DELAY_MS);
|
|
|
|
test('health is re-read once the sensing socket connects', () => {
|
|
tab._refreshStreamHealthOn('connecting');
|
|
advance();
|
|
assert.equal(healthCalls, 0, 'nothing to report until the socket is up');
|
|
|
|
tab._refreshStreamHealthOn('connected');
|
|
assert.equal(healthCalls, 0, 'waits for the server to register the socket');
|
|
advance();
|
|
assert.equal(healthCalls, 1);
|
|
mock.timers.reset();
|
|
});
|
|
|
|
test('health is re-read when a connected socket drops', () => {
|
|
tab._refreshStreamHealthOn('connected');
|
|
advance();
|
|
tab._refreshStreamHealthOn('reconnecting');
|
|
advance();
|
|
assert.equal(healthCalls, 2);
|
|
mock.timers.reset();
|
|
});
|
|
|
|
test('repeated notifications of the same state do not refetch', () => {
|
|
// onStateChange also fires on dataSource changes with an unchanged state.
|
|
tab._refreshStreamHealthOn('connected');
|
|
tab._refreshStreamHealthOn('connected');
|
|
tab._refreshStreamHealthOn('connected');
|
|
advance();
|
|
assert.equal(healthCalls, 1);
|
|
mock.timers.reset();
|
|
});
|
|
|
|
test('retry churn while never connected does not poll health', () => {
|
|
for (const s of ['connecting', 'reconnecting', 'connecting', 'reconnecting']) {
|
|
tab._refreshStreamHealthOn(s);
|
|
advance();
|
|
}
|
|
assert.equal(healthCalls, 0);
|
|
mock.timers.reset();
|
|
});
|