Add AIOX Visual Observer dashboard and WebSocket server#599
Add AIOX Visual Observer dashboard and WebSocket server#599devoliverluccas wants to merge 5 commits intoSynkraAI:mainfrom
Conversation
Adds observer/ — an isolated monitor server that receives events from DashboardEmitter and Python hooks (port 4001) and broadcasts them to a single-file HTML dashboard via native RFC 6455 WebSocket. - observer/event-store.js: in-memory circular buffer (max 200 events), derives state from event stream (agents, phase, pipeline, metrics) - observer/server.js: HTTP + WebSocket server with zero new dependencies; RFC 6455 handshake via Node.js crypto; chokidar watches bob-status.json and broadcasts status_update frames; silent failure on all observer errors - observer/dashboard.html: single-file dark terminal dashboard; shows active agent, pipeline progress (6 stages), terminals, filterable event log; auto-reconnect with exponential backoff; no frameworks, no CDN, no build Zero modifications to existing AIOX code. Observer is purely additive. Usage: node observer/server.js → open http://localhost:4001 https://claude.ai/code/session_01LuDQ7x1o5tJ4G71LZvqGqQ
- event-store.js: DashboardEmitter._postEvent nests session_id/aiox_agent/
aiox_story_id inside data{}, not at top-level — read from both locations
for compatibility with Python hooks (which may send top-level fields)
- server.js: generate UUID for events received via POST (CLI emitter omits id)
- eslint.config.js: add observer/** to ignores — standalone runtime tool
https://claude.ai/code/session_01LuDQ7x1o5tJ4G71LZvqGqQ
|
@claude is attempting to deploy a commit to the Pedro Valério Lopez's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughAdds a new Observer subsystem: a Node.js HTTP+WebSocket server and in-memory event store, plus a standalone dashboard HTML for real-time visualization. Also updates lint and gitignore config to exclude observer runtime and Synapse runtime data patterns. Changes
Sequence DiagramsequenceDiagram
participant External as External System
participant Server as Observer Server
participant Store as Event Store
participant Client as Browser Client
External->>Server: POST /events (event payload)
Server->>Store: addEvent(event)
Note over Store: update circular buffer<br/>update derived dashboard state
Server->>Server: broadcast(event + state) to WS clients
Server->>Client: send via WebSocket
Client->>Client: update liveState and UI
Client->>Server: WebSocket connect
Server->>Store: getState() & getRecentEvents()
Server->>Client: send init + recent events
Client->>Client: populate initial UI
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Welcome to aiox-core! Thanks for your first pull request.
What happens next?
- Automated checks will run on your PR
- A maintainer will review your changes
- Once approved, we'll merge your contribution!
PR Checklist:
- Tests pass (
npm test) - Linting passes (
npm run lint) - Commit messages follow Conventional Commits
Thanks for contributing!
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
observer/server.js (1)
43-43: Use the repo's absolute import style for the event store.The relative
./event-storeimport breaks the JS/TS import rule for this repo. Please switch this to the project's absolute form so the new observer code follows the same convention. As per coding guidelines, "Use absolute imports instead of relative imports in all code".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@observer/server.js` at line 43, The import in observer/server.js uses a relative path for createEventStore; replace the relative require('./event-store') with the repo's absolute import form (use the project's root-level/package absolute path for the event store, e.g., require('event-store') or the repo's designated absolute namespace) so createEventStore is imported via the project's absolute import convention rather than a relative path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@eslint.config.js`:
- Around line 64-65: Remove the blanket ignore for 'observer/**' in
eslint.config.js so the observer files (e.g., observer/event-store.js) remain
linted; instead add an overrides entry targeting "files": ["observer/**"] that
sets appropriate env/parserOptions (CommonJS, ecmaVersion: 2022) and relaxes
only the specific rules you need changed (or turn off a small set like
'no-restricted-syntax' or rule X) rather than excluding the whole directory,
ensuring use-before-declaration and other core rules still run.
In `@observer/dashboard.html`:
- Around line 518-540: The metrics display is being overridden by the retained
log row count instead of using the canonical liveState.metrics; update
appendEventToLog (and any other places that set 'm-total' such as the blocks
around applyState and the sections at lines ~563-598 and ~617-624) to read and
render from liveState.metrics.total (falling back to 0) rather than using
logRows.length or local counters, and ensure any event-delta updates mutate
liveState.metrics.total so setText('m-total', ...) always reflects
liveState.metrics; keep setText('m-rate', ...) using
liveState.metrics.eventsPerMin similarly.
- Around line 480-486: The code is injecting untrusted values via row.innerHTML
(see terminals.forEach and other row-building blocks that interpolate t.agent,
t.pid, t.task, event.type, summary), which allows XSS; replace those innerHTML
constructions with createElement + textContent: create span elements for agent,
pid and task, set their className (e.g., 'terminal-agent', 'terminal-pid',
'terminal-task') and assign the untrusted values to span.textContent (or
properly escape/sanitize where necessary) and append them to row via
appendChild; apply the same change to the other similar block that builds rows
(the block that also uses event.type and summary) to ensure no direct HTML
interpolation of untrusted input.
- Around line 723-735: In the case 'status_update' handler, set
liveState.currentPhase from the incoming pipeline/current stage before calling
updateAgentCard(liveState) so the agent card uses the new phase; specifically,
after you assign liveState.pipeline.current (and call setText('agent-phase',
...)) assign liveState.currentPhase = liveState.pipeline.current || null (or
similar) before updateAgentCard(liveState) so the card no longer reverts to
stale/— values.
In `@observer/event-store.js`:
- Around line 90-102: The code reads nested fields from data (data.session_id,
data.aiox_agent, data.aiox_story_id) before data is declared, causing a
ReferenceError; fix by moving the declaration const data = event.data || {};
above the block that computes session_id, aiox_agent, and aiox_story_id (so the
fallbacks use the defined data), then keep the existing assignments to
state.sessionId, state.currentStory, and state.currentAgent in place; ensure you
update any related comments and retain support for both top-level and nested
shapes when modifying the block that references event and data.
In `@observer/server.js`:
- Around line 228-243: Server is currently exposed broadly: bind server to
localhost by default, remove wildcard CORS, and add origin/token checks for both
HTTP routes and WebSocket upgrades; in handleUpgrade (and any other upgrade
handlers), reject upgrades whose req.url is not '/ws' and validate
req.headers.origin and an auth token (e.g., Authorization or a custom header)
before calling wsHandshake or adding socket to wsClients; for HTTP endpoints
like POST /events and GET /status, enforce the same origin/token validation and
stop sending Access-Control-Allow-Origin: * (use specific origin or omit header
when origin invalid); ensure failures call socket.destroy() or send 401/403
responses and do not add clients to wsClients (references: handleUpgrade,
wsHandshake, wsClients, store.setConnectedClients, POST /events handler).
- Around line 308-320: readBody() currently buffers the entire request with no
limit; change it to enforce a configurable max size (e.g., maxBytes constant or
parameter) by tracking accumulated byte length inside the 'data' handler and
immediately rejecting with a specific error (e.g., a PayloadTooLargeError or
Error with code/name 'PayloadTooLarge') if the limit is exceeded, cleaning up
listeners and optionally destroying the socket; keep JSON.parse on 'end' as
before. Also update the caller/handler that awaits readBody() to catch that
specific error and return a 413 Payload Too Large response when seen. Use the
existing readBody function name and the request handler that calls it to locate
where to add the size check and the 413 mapping.
---
Nitpick comments:
In `@observer/server.js`:
- Line 43: The import in observer/server.js uses a relative path for
createEventStore; replace the relative require('./event-store') with the repo's
absolute import form (use the project's root-level/package absolute path for the
event store, e.g., require('event-store') or the repo's designated absolute
namespace) so createEventStore is imported via the project's absolute import
convention rather than a relative path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: db4dd763-b421-4c2c-94ce-9957f804088d
📒 Files selected for processing (5)
.synapse/.gitignoreeslint.config.jsobserver/dashboard.htmlobserver/event-store.jsobserver/server.js
| // Observer server — standalone runtime tool, not part of TS project | ||
| 'observer/**', |
There was a problem hiding this comment.
Keep observer/** under lint coverage.
The existing JS config already supports CommonJS/ES2022, so this blanket ignore turns off the only automated checks on the new runtime surface added by this PR. It would also hide real regressions here—observer/event-store.js already contains a use-before-declaration bug that this ignore would suppress. If a few rules need relaxing, add a targeted override instead of excluding the directory.
🧹 Minimal fix
- // Observer server — standalone runtime tool, not part of TS project
- 'observer/**',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Observer server — standalone runtime tool, not part of TS project | |
| 'observer/**', |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@eslint.config.js` around lines 64 - 65, Remove the blanket ignore for
'observer/**' in eslint.config.js so the observer files (e.g.,
observer/event-store.js) remain linted; instead add an overrides entry targeting
"files": ["observer/**"] that sets appropriate env/parserOptions (CommonJS,
ecmaVersion: 2022) and relaxes only the specific rules you need changed (or turn
off a small set like 'no-restricted-syntax' or rule X) rather than excluding the
whole directory, ensuring use-before-declaration and other core rules still run.
| terminals.forEach((t) => { | ||
| const row = document.createElement('div'); | ||
| row.className = 'terminal-row'; | ||
| row.innerHTML = | ||
| `<span class="terminal-agent">${t.agent || '?'}</span>` + | ||
| `<span class="terminal-pid">pid:${t.pid || '?'}</span>` + | ||
| `<span class="terminal-task">${t.task || ''}</span>`; |
There was a problem hiding this comment.
Stop feeding untrusted payloads into innerHTML.
observer/server.js Lines 381-383 and 439-442 forward arbitrary event/status data straight to the browser. Interpolating t.agent, t.task, event.type, and summary into HTML strings makes the dashboard script-executable from a crafted payload. Build these rows with createElement/textContent instead.
Also applies to: 575-586
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/dashboard.html` around lines 480 - 486, The code is injecting
untrusted values via row.innerHTML (see terminals.forEach and other row-building
blocks that interpolate t.agent, t.pid, t.task, event.type, summary), which
allows XSS; replace those innerHTML constructions with createElement +
textContent: create span elements for agent, pid and task, set their className
(e.g., 'terminal-agent', 'terminal-pid', 'terminal-task') and assign the
untrusted values to span.textContent (or properly escape/sanitize where
necessary) and append them to row via appendChild; apply the same change to the
other similar block that builds rows (the block that also uses event.type and
summary) to ensure no direct HTML interpolation of untrusted input.
| function applyState(state) { | ||
| if (!state) return; | ||
|
|
||
| if (state.sessionId) setText('session-id', state.sessionId.slice(0, 12) + '…'); | ||
| if (state.uptime !== undefined) serverUptime = state.uptime; | ||
| if (state.connectedClients !== undefined) { | ||
| setText('client-count', state.connectedClients); | ||
| setText('m-clients', state.connectedClients); | ||
| } | ||
|
|
||
| updateAgentCard(state); | ||
| renderPipeline(state.pipeline); | ||
|
|
||
| // Bob status terminals | ||
| if (state.bobStatus && state.bobStatus.active_terminals) { | ||
| renderTerminals(state.bobStatus.active_terminals); | ||
| } | ||
|
|
||
| // Metrics | ||
| if (state.metrics) { | ||
| setText('m-total', state.metrics.total || 0); | ||
| setText('m-rate', state.metrics.eventsPerMin || 0); | ||
| } |
There was a problem hiding this comment.
Keep the metrics bar sourced from liveState.metrics.
The init payload carries both state.metrics and a separate recentEvents buffer (observer/server.js Lines 245-250). applyState() renders the real totals, but appendEventToLog() immediately overwrites m-total with logRows.length, and later event deltas only mutate liveState.metrics.total. After init, the metrics bar drifts from the store state and "total events" becomes retained-row count instead of the cumulative metric.
Also applies to: 563-598, 617-624
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/dashboard.html` around lines 518 - 540, The metrics display is being
overridden by the retained log row count instead of using the canonical
liveState.metrics; update appendEventToLog (and any other places that set
'm-total' such as the blocks around applyState and the sections at lines
~563-598 and ~617-624) to read and render from liveState.metrics.total (falling
back to 0) rather than using logRows.length or local counters, and ensure any
event-delta updates mutate liveState.metrics.total so setText('m-total', ...)
always reflects liveState.metrics; keep setText('m-rate', ...) using
liveState.metrics.eventsPerMin similarly.
| case 'status_update': { | ||
| const data = msg.data; | ||
| liveState.bobStatus = data; | ||
| if (data) { | ||
| if (data.pipeline) { | ||
| liveState.pipeline.current = data.pipeline.current_stage; | ||
| liveState.pipeline.completed = data.pipeline.completed_stages || []; | ||
| renderPipeline(liveState.pipeline); | ||
| setText('agent-phase', liveState.pipeline.current || '—'); | ||
| } | ||
| if (data.current_agent && data.current_agent.id) { | ||
| liveState.currentAgent = data.current_agent.id; | ||
| updateAgentCard(liveState); |
There was a problem hiding this comment.
Update liveState.currentPhase on status_update before re-rendering the agent card.
When the same status payload contains both pipeline and current_agent—the normal shape from observer/server.js Lines 439-442—updateAgentCard(liveState) rewrites the phase label from liveState.currentPhase. Since this branch never updates that field, the card can jump back to stale phase data or —.
🔧 Suggested fix
if (data.pipeline) {
+ liveState.currentPhase = data.pipeline.current_stage || null;
liveState.pipeline.current = data.pipeline.current_stage;
liveState.pipeline.completed = data.pipeline.completed_stages || [];
renderPipeline(liveState.pipeline);
setText('agent-phase', liveState.pipeline.current || '—');
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/dashboard.html` around lines 723 - 735, In the case 'status_update'
handler, set liveState.currentPhase from the incoming pipeline/current stage
before calling updateAgentCard(liveState) so the agent card uses the new phase;
specifically, after you assign liveState.pipeline.current (and call
setText('agent-phase', ...)) assign liveState.currentPhase =
liveState.pipeline.current || null (or similar) before
updateAgentCard(liveState) so the card no longer reverts to stale/— values.
| function handleUpgrade(req, socket) { | ||
| const key = req.headers['sec-websocket-key']; | ||
| if (!key) { | ||
| socket.destroy(); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| wsHandshake(socket, key); | ||
| } catch (_e) { | ||
| socket.destroy(); | ||
| return; | ||
| } | ||
|
|
||
| wsClients.add(socket); | ||
| store.setConnectedClients(wsClients.size); |
There was a problem hiding this comment.
Lock down the HTTP and WebSocket endpoints.
This server listens on all interfaces, returns Access-Control-Allow-Origin: *, and accepts POST /events plus WS upgrades without any auth/origin checks. That lets any local webpage—or any host that can reach the port—inject fake events, read /status, or subscribe to the live feed. Bind to 127.0.0.1 by default, reject non-/ws upgrades, and require an origin/token check for both HTTP and WS.
Also applies to: 329-335, 367-384, 484-490
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/server.js` around lines 228 - 243, Server is currently exposed
broadly: bind server to localhost by default, remove wildcard CORS, and add
origin/token checks for both HTTP routes and WebSocket upgrades; in
handleUpgrade (and any other upgrade handlers), reject upgrades whose req.url is
not '/ws' and validate req.headers.origin and an auth token (e.g., Authorization
or a custom header) before calling wsHandshake or adding socket to wsClients;
for HTTP endpoints like POST /events and GET /status, enforce the same
origin/token validation and stop sending Access-Control-Allow-Origin: * (use
specific origin or omit header when origin invalid); ensure failures call
socket.destroy() or send 401/403 responses and do not add clients to wsClients
(references: handleUpgrade, wsHandshake, wsClients, store.setConnectedClients,
POST /events handler).
| function readBody(req) { | ||
| return new Promise((resolve, reject) => { | ||
| const chunks = []; | ||
| req.on('data', (chunk) => chunks.push(chunk)); | ||
| req.on('end', () => { | ||
| try { | ||
| resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); | ||
| } catch (e) { | ||
| reject(e); | ||
| } | ||
| }); | ||
| req.on('error', reject); | ||
| }); |
There was a problem hiding this comment.
Cap request bodies in readBody().
readBody() buffers the full payload with no limit, so a single oversized or slow client can force unbounded memory growth and take the observer down. Reject over a small maximum and map that path to 413 Payload Too Large.
🛡️ Suggested fix
+const MAX_BODY_BYTES = 1024 * 1024;
+
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
- req.on('data', (chunk) => chunks.push(chunk));
+ let total = 0;
+ req.on('data', (chunk) => {
+ total += chunk.length;
+ if (total > MAX_BODY_BYTES) {
+ reject(Object.assign(new Error('Payload too large'), { statusCode: 413 }));
+ req.destroy();
+ return;
+ }
+ chunks.push(chunk);
+ });
req.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
@@
- } catch (_e) {
+ } catch (e) {
try {
- res.writeHead(400, { 'Content-Type': 'text/plain' });
- res.end('Bad Request');
+ const status = e && e.statusCode ? e.statusCode : 400;
+ res.writeHead(status, { 'Content-Type': 'text/plain' });
+ res.end(status === 413 ? 'Payload Too Large' : 'Bad Request');
} catch (__e) {
// Response already sent
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/server.js` around lines 308 - 320, readBody() currently buffers the
entire request with no limit; change it to enforce a configurable max size
(e.g., maxBytes constant or parameter) by tracking accumulated byte length
inside the 'data' handler and immediately rejecting with a specific error (e.g.,
a PayloadTooLargeError or Error with code/name 'PayloadTooLarge') if the limit
is exceeded, cleaning up listeners and optionally destroying the socket; keep
JSON.parse on 'end' as before. Also update the caller/handler that awaits
readBody() to catch that specific error and return a 413 Payload Too Large
response when seen. Use the existing readBody function name and the request
handler that calls it to locate where to add the size check and the 413 mapping.
`data` was declared at line ~102 but referenced at lines ~93-95,
causing a ReferenceError (temporal dead zone) on every POST /events.
Events were stored in the circular buffer but state derivation
(currentPhase, currentAgent, currentStory) never updated, and the
server returned 400 Bad Request instead of {"ok":true}.
Move `const data = event.data || {}` before the context envelope
extraction block so all references resolve correctly.
https://claude.ai/code/session_01LuDQ7x1o5tJ4G71LZvqGqQ
Auto-generated updates from IDS hook (entity-registry lastVerified timestamps + entityCount) and package-lock.json version sync to 5.0.3. https://claude.ai/code/session_01LuDQ7x1o5tJ4G71LZvqGqQ
|
Projeto ambicioso — dashboard de observabilidade em tempo real é algo que falta no ecossistema AIOX. Algumas observações:
Feature muito útil pra debug de pipelines. Seria legal ver integração futura com o EventEmitter do core. |
Pull Request
📋 Description
Introduces the AIOX Visual Observer — a real-time web-based monitoring dashboard for tracking agent execution, pipeline progress, and system events. This includes:
The observer receives events via HTTP POST from DashboardEmitter and Python hooks, maintains a 200-event circular buffer, watches
bob-status.jsonfor pipeline updates, and streams real-time data to browser clients via WebSocket.🎯 AIOX Story Reference
Story ID: TBD
Story File: TBD
Sprint: TBD
Acceptance Criteria Addressed
🔗 Related Issue
N/A
📦 Type of Change
🎯 Scope
aiox-core/)squads/)tools/)docs/).github/)observer/)📝 Changes Made
New Files
observer/dashboard.html (828 lines)
observer/server.js (506 lines)
POST /events,GET /,GET /status,GET /events/recent,WS /wsbob-status.jsonusing chokidar (graceful degradation if unavailable)observer/event-store.js (267 lines)
.synapse/.gitignore (3 lines)
Modified Files
observer/**to ESLint ignore patterns (observer is a standalone runtime tool, not part of TS project)🧪 Testing
node observer/server.js, openhttp://localhost:4001in browser, POST events to/eventsendpoint📸 Screenshots (if applicable)
N/A (dashboard is interactive web UI; visual verification requires running the server
https://claude.ai/code/session_01LuDQ7x1o5tJ4G71LZvqGqQ
Summary by CodeRabbit
New Features
Chores