diff --git a/scripts/lib/control-pane/proximity-viz.js b/scripts/lib/control-pane/proximity-viz.js new file mode 100644 index 00000000..2780e5bc --- /dev/null +++ b/scripts/lib/control-pane/proximity-viz.js @@ -0,0 +1,191 @@ +'use strict'; + +/** + * Self-contained 3D "agent airspace" visualization, served by the control pane. + * + * Renders each agent as a point in code-space (positions from the proximity + * embedding), sized by working-set size and colored by collision risk, with + * links between converging pairs (amber = transmit advisory, red = steer). The + * scene auto-rotates so you can read the cloud. Dependency-free: a hand-rolled + * 3D2D projection on a , no external scripts (CSP/offline friendly). + * + * This is the operator/Enterprise view of Layer 4: multi-agent observability: + * literally watch the swarm and watch one agent steer away from a collision. + */ + +function renderProximityVizHtml() { + return ` + + + + +ECC Agent Airspace + + + +
+

ECC - Agent Airspace

+ connecting... +
+
+
+ +
+
clear
+
traffic advisory (transmit)
+
resolution (steer)
+
+
+
+

Advisories

+
No advisories - airspace clear.
+
+
+ + +`; +} + +module.exports = { renderProximityVizHtml }; diff --git a/scripts/lib/control-pane/server.js b/scripts/lib/control-pane/server.js index b7b27cd2..81f8a99e 100644 --- a/scripts/lib/control-pane/server.js +++ b/scripts/lib/control-pane/server.js @@ -8,6 +8,7 @@ const { spawn } = require('child_process'); const { buildControlPaneAction } = require('./actions'); const { buildControlPaneSnapshot, resolveControlPaneConfig } = require('./state'); const { renderControlPaneHtml } = require('./ui'); +const { renderProximityVizHtml } = require('./proximity-viz'); const { claimWorkItem, moveWorkItem } = require('./work-item-mutations'); // Run a single write against the local work-item store, then close it. Kept @@ -265,6 +266,25 @@ function createControlPaneServer(options = {}) { return; } + // 3D agent-airspace visualization (Layer 4 observability). + if (req.method === 'GET' && requestUrl.pathname === '/proximity') { + sendText(res, 200, renderProximityVizHtml(), 'text/html; charset=utf-8'); + return; + } + + if (req.method === 'GET' && requestUrl.pathname === '/api/proximity') { + const snapshot = await buildControlPaneSnapshot({ + repoRoot, + dbPath: resolvedConfig.dbPath, + stateDbPath: resolvedConfig.stateDbPath, + config: resolvedConfig, + allowActions, + includeProximity: true + }); + sendJson(res, 200, snapshot.proximity || { enabled: true, advisories: [], positions: [], links: [], counts: {} }); + return; + } + const actionMatch = requestUrl.pathname.match(/^\/api\/actions\/([^/]+)$/); if (req.method === 'POST' && actionMatch) { if (!allowActions) { diff --git a/tests/scripts/control-pane.test.js b/tests/scripts/control-pane.test.js index 7e6df807..ab0673e2 100644 --- a/tests/scripts/control-pane.test.js +++ b/tests/scripts/control-pane.test.js @@ -226,6 +226,49 @@ async function runTests() { passed++; else failed++; + if ( + await test('serves the 3D agent-airspace page and the proximity JSON feed', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-proximity-')); + const dbPath = path.join(tempDir, 'ecc2.db'); + + try { + await writeMinimalDatabase(dbPath); + const app = await createControlPaneServer({ + host: '127.0.0.1', + port: 0, + dbPath, + repoRoot: REPO_ROOT, + allowActions: false + }); + + await app.listen(); + try { + // The Enterprise/Pro 3D observability view: a self-contained HTML page. + const page = await fetchLocal(`${app.url}/proximity`); + assert.strictEqual(page.status, 200); + assert.ok((page.headers.get('content-type') || '').includes('text/html')); + const html = await page.text(); + assert.ok(html.includes('Agent Airspace'), 'page is titled Agent Airspace'); + assert.ok(html.includes(' r.json()); + assert.ok(Array.isArray(prox.positions), 'positions array present'); + assert.ok(Array.isArray(prox.links), 'links array present'); + assert.ok(Array.isArray(prox.advisories), 'advisories array present'); + assert.ok(prox.counts && typeof prox.counts === 'object', 'counts present'); + } finally { + await app.close(); + } + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + if ( await test('serves health, asset, not-found, invalid body, and read-only action responses', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-routes-'));