fix(control-pane): reject failed views and coalesce projection sampling

This commit is contained in:
Affaan Mustafa
2026-09-12 03:11:12 -04:00
parent ff03da1dc8
commit 5c2fbce26e
6 changed files with 122 additions and 9 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ Served by `node scripts/control-pane.js` (loopback only, same Host and Origin ga
| `GET /api/control-plane` | The full view document below. |
| `GET /api/control-plane/events` | `{ schemaVersion, generatedAt, thresholds, events, counts }` only, for hooks and pollers. |
The server keeps one projection window per process, so z-scores roll across polls. Options on `createControlPaneServer`: `projection` (`windowSize`, `clipPercentiles`, `minWindowForZscore`), `viewOptions` (`thresholds`, `manifest`, `channelWeights`), `proximityOptions` (passed to the scan).
The server keeps one projection window per process. Both API routes share a snapshot cached for five seconds, and concurrent refresh requests are coalesced. Reads within that interval do not add samples. After expiry, the next read refreshes the snapshot once; idle intervals do not generate synthetic samples. Failed refreshes return errors rather than healthy empty data. The page rejects failed HTTP responses and invalid view envelopes and shows `offline`. Options on `createControlPaneServer`: `projection` (`windowSize`, `clipPercentiles`), `viewOptions` (`thresholds`, `manifest`, `channelWeights`, `minWindowForZscore`), `proximityOptions` (passed to the scan).
## Document
+1 -1
View File
@@ -231,7 +231,7 @@ function projectPairs(links, options = {}) {
const minWindow = Number.isFinite(options.minWindowForZscore) ? options.minWindowForZscore : PROJECTION_DEFAULTS.minWindowForZscore;
const raw = list.map(l => channelVector(l.channels));
if (window) for (const vec of raw) window.push(vec);
if (window && options.sample !== false) for (const vec of raw) window.push(vec);
let stats = null;
let normalization = 'raw';
@@ -207,10 +207,13 @@ function renderControlPlaneViewHtml() {
}
function apply(data) {
view = data || view;
view.tasks = view.tasks || []; view.lanes = view.lanes || []; view.pairs = view.pairs || [];
view.events = view.events || []; view.projection = view.projection || { agents: [] };
view.thresholds = view.thresholds || { ta: 0.35, ra: 0.7 };
if (!data || data.schemaVersion !== 'ecc.control-plane.view.v1' ||
!['tasks', 'lanes', 'pairs', 'events'].every(function (key) { return Array.isArray(data[key]); }) ||
!data.projection || !Array.isArray(data.projection.agents) || !data.thresholds ||
!Number.isFinite(data.thresholds.ta) || !Number.isFinite(data.thresholds.ra)) {
throw new Error('Invalid control-plane view');
}
view = Object.assign({}, data);
renderEvents(); renderLanes(); draw();
var c = view.counts || {};
document.getElementById('status').textContent =
@@ -220,7 +223,10 @@ function renderControlPlaneViewHtml() {
}
function poll() {
fetch('/api/control-plane').then(function (r) { return r.json(); }).then(apply).catch(function () {
fetch('/api/control-plane').then(function (r) {
if (!r.ok) throw new Error('Control-plane request failed');
return r.json();
}).then(apply).catch(function () {
document.getElementById('status').textContent = 'offline';
});
}
+22 -2
View File
@@ -225,6 +225,7 @@ function buildControlPlaneView(snapshot, options = {}) {
const projection = projectPairs(prox.links || [], {
window: options.window,
channelWeights: options.channelWeights,
sample: options.sample,
minWindowForZscore: options.minWindowForZscore
});
const pointByAgent = new Map(projection.agents.map(a => [a.agentId, a]));
@@ -319,11 +320,30 @@ function buildControlPlaneView(snapshot, options = {}) {
*/
function createControlPlaneViewSource(deps = {}) {
const window = deps.window || createProjectionWindow(deps.projection || {});
const clock = deps.clock || Date.now;
const interval = deps.sampleIntervalMs === undefined ? 5000 : deps.sampleIntervalMs;
if (!Number.isFinite(interval) || interval <= 0) throw new Error('sampleIntervalMs must be positive and finite');
let cached = null;
let pending = null;
let expiresAt = 0;
async function refresh() {
const snapshot = await deps.buildSnapshot();
const view = buildControlPlaneView(snapshot, { ...deps.viewOptions, window });
cached = { snapshot, view };
expiresAt = clock() + interval;
return cached;
}
return {
window,
async build(extra = {}) {
const snapshot = await deps.buildSnapshot();
return buildControlPlaneView(snapshot, { ...deps.viewOptions, ...extra, window });
if (!cached || clock() >= expiresAt) {
if (!pending) pending = refresh().finally(() => { pending = null; });
await pending;
}
if (Object.keys(extra).length === 0) return cached.view;
return buildControlPlaneView(cached.snapshot, {
...deps.viewOptions, ...extra, now: extra.now || cached.view.generatedAt, window, sample: false
});
}
};
}
+39
View File
@@ -0,0 +1,39 @@
'use strict';
const assert = require('assert');
const vm = require('vm');
const { renderControlPlaneViewHtml } = require('../../scripts/lib/control-pane/control-plane-view-ui');
async function renderResponse(ok, data) {
const elements = new Map();
const context = new Proxy({}, { get: () => () => {} });
function element() {
return { textContent: '', style: {}, appendChild() {}, getContext: () => context,
clientWidth: 640, clientHeight: 480,
parentElement: { getBoundingClientRect: () => ({ width: 640, height: 480 }) } };
}
const document = {
getElementById(id) { if (!elements.has(id)) elements.set(id, element()); return elements.get(id); },
createElement: element
};
const code = renderControlPlaneViewHtml().match(/<script>([\s\S]*?)<\/script>/)[1];
vm.runInNewContext(code, {
document, window: { addEventListener() {}, devicePixelRatio: 1 }, setInterval() {},
fetch: async () => ({ ok, json: async () => data })
});
await new Promise(resolve => setImmediate(resolve));
return elements;
}
(async () => {
const failed = await renderResponse(false, { ok: false, error: 'snapshot unavailable' });
assert.strictEqual(failed.get('status').textContent, 'offline', 'HTTP errors must not display a healthy empty view');
const malformed = await renderResponse(true, { schemaVersion: 'wrong' });
assert.strictEqual(malformed.get('status').textContent, 'offline', 'invalid schemas must be rejected');
const valid = await renderResponse(true, {
schemaVersion: 'ecc.control-plane.view.v1', tasks: [], lanes: [], pairs: [], events: [],
projection: { agents: [] }, thresholds: { ta: 0.35, ra: 0.7 }, counts: {}
});
assert.ok(valid.get('status').textContent.includes('0 tasks'));
console.log('PASS control-plane UI error and schema handling');
})().catch(error => { console.error(error.message); process.exitCode = 1; });
+48
View File
@@ -264,7 +264,9 @@ function snapshotFor(sessions, extra = {}) {
const sessions = [session('a'), session('b'), session('c'), session('d')];
const snapshot = snapshotFor(sessions);
let builds = 0;
let clock = 1000;
const source = createControlPlaneViewSource({
clock: () => clock,
buildSnapshot: async () => {
builds += 1;
return snapshot;
@@ -273,6 +275,7 @@ function snapshotFor(sessions, extra = {}) {
viewOptions: { now: NOW }
});
const first = await source.build();
clock += 5000;
const second = await source.build();
assert.strictEqual(builds, 2);
assert.strictEqual(first.projection.window.samples, 6);
@@ -281,6 +284,51 @@ function snapshotFor(sessions, extra = {}) {
assert.strictEqual(second.generatedAt, NOW);
});
await test('view source samples once per interval despite repeated and concurrent reads', async () => {
let clock = 1000;
let builds = 0;
const source = createControlPlaneViewSource({
clock: () => clock,
buildSnapshot: async () => { builds += 1; return snapshotFor([session('a'), session('b')]); },
viewOptions: { now: NOW }
});
const views = await Promise.all(Array.from({ length: 10 }, () => source.build()));
assert.strictEqual(builds, 1);
assert.strictEqual(source.window.length, 1);
assert.ok(views.every(view => view.generatedAt === views[0].generatedAt));
await source.build();
await source.build({ thresholds: { ta: 0.2, ra: 1.5 } });
assert.strictEqual(source.window.length, 1, 'alternate read options must not resample');
clock += 5000;
await source.build();
assert.strictEqual(builds, 2);
assert.strictEqual(source.window.length, 2);
});
await test('view source rejects failed refreshes and retries without false healthy data', async () => {
let fail = true;
let clock = 0;
const source = createControlPlaneViewSource({
clock: () => clock,
buildSnapshot: async () => {
if (fail) throw new Error('snapshot unavailable');
return snapshotFor([session('a'), session('b')]);
}
});
await assert.rejects(source.build(), /snapshot unavailable/);
assert.strictEqual(source.window.length, 0);
fail = false;
await source.build();
assert.strictEqual(source.window.length, 1);
clock += 5000;
fail = true;
await assert.rejects(source.build(), /snapshot unavailable/);
assert.strictEqual(source.window.length, 1);
fail = false;
await source.build();
assert.strictEqual(source.window.length, 2);
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
if (failed > 0) process.exit(1);
})();