Add AIOX Visual Observer dashboard and WebSocket server#599
Add AIOX Visual Observer dashboard and WebSocket server#599devoliverluccas wants to merge 8 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. |
WalkthroughAdds an Observer subsystem: a Node.js HTTP+WebSocket server, an in-memory event store, and a standalone dashboard HTML for real-time visualization. Also updates ESLint config for 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 (JSON)
Server->>Store: addEvent(event)
Note over Store: update circular buffer<br/>update derived dashboard state
Server->>Server: broadcast(event) to WS clients
Server->>Client: send event via WebSocket
Client->>Client: update liveState and UI
Client->>Server: WebSocket connect (/ws)
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)
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
| 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).
`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. |
…entPhase on status_update
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
observer/server.js (1)
229-249:⚠️ Potential issue | 🔴 CriticalBind the observer to loopback and require auth before accepting clients.
server.listen(port)exposes this process on every interface, while/events,/status,/events/recent, and/wsstill accept unauthenticated requests. The localhost CORS header only affects browsers, so any host that can reach the port can still inject or read observer data. Default to127.0.0.1/::1and gate both HTTP and upgrade paths behind a shared token or strict origin check.Also applies to: 395-426, 490-518
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@observer/server.js` around lines 229 - 249, The server currently listens on all interfaces and accepts unauthenticated HTTP and WebSocket requests; update the listener and request handling so the observer binds to loopback and enforces a shared token or strict origin check before accepting clients: change server.listen(...) to bind to 127.0.0.1 and ::1, and in the HTTP routes that serve /events, /status, /events/recent and in handleUpgrade (which uses wsHandshake, wsClients, and store.setConnectedClients) validate a configured secret token (e.g., from env) or verify the Origin header before proceeding—reject and destroy the socket or respond 401 if the token/origin is missing/invalid so both HTTP and websocket upgrade paths are gated by the same auth check.
🧹 Nitpick comments (1)
observer/server.js (1)
43-43: Use the repo's absolute-import convention forevent-store.This new observer runtime is still importing
event-storerelatively. Please switch it to the established absolute import form so the module stays aligned with the repository standard.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, Replace the relative require for the event store with the repo's absolute-import convention: locate the import that uses createEventStore (const { createEventStore } = require('./event-store')) and change it to the project's absolute module path for event-store so it uses the same import pattern as other files (keeping the createEventStore symbol unchanged).
🤖 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 3-4: The observer override currently uses files: ['observer/**']
which matches all assets; change the glob to target only JS files by updating
the observerOverride.files pattern (the observerOverride constant) to something
like ['observer/**/*.js','observer/**/*.cjs','observer/**/*.mjs'] or at minimum
['observer/**/*.js'] so the CommonJS Node.js-specific override applies only to
JavaScript source files and not static assets like observer/dashboard.html.
In `@observer/dashboard.html`:
- Around line 618-620: The footer metrics are rendered before the live event
delta is applied, causing the total and events/min to lag; in appendEventToLog()
call applyEventDelta() first to update liveState.metrics, then use
liveState.metrics.total and liveState.metrics.eventsPerMinute (or the
appropriate metrics field) when calling setText('m-total', ...) and
setText('log-count', ...); make the same change at the other occurrences (around
the setText calls at the other noted locations) so both total and events/min are
computed from liveState.metrics after the delta is applied.
- Around line 672-676: The "Active Terminals" list is only appended in the
BobAgentSpawned path (liveState.bobStatus.active_terminals and renderTerminals)
but not pruned when agents finish; update the BobAgentCompleted and
AgentDeactivated handlers to remove any entries from
liveState.bobStatus.active_terminals that match the completed/deactivated agent
(match by agent and pid), then call
renderTerminals(liveState.bobStatus.active_terminals) to re-render; ensure you
guard for liveState.bobStatus and initialize active_terminals as an array if
needed before manipulating it.
- Around line 726-734: The init handler currently appends recentEvents onto the
existing buffer, causing duplicates after reconnect; in the case 'init' block
(around liveState, applyState, recentEvents, appendEventToLog) either clear the
current UI log buffer before iterating (reset logRows / DOM rows) or filter
recentEvents to skip any event whose id already exists in the log (compare
event.id against existing log row ids) before calling appendEventToLog;
implement one of these fixes so reconnects replace or dedupe the last-50 events
instead of duplicating them.
In `@observer/server.js`:
- Around line 450-480: startStatusWatcher currently creates a persistent
chokidar watcher and discards the handle, causing the watcher to keep the
process alive after shutdown; modify startStatusWatcher to return the watcher
instance (the variable watcher) and update the server shutdown logic in
startServer (or the server 'close' handler) to call watcher.close() (or
watcher.unwatch()/close equivalent) when server.close() runs; also apply the
same change to the similar watcher creation at the other location referenced
(around the 520-522 block) so all persistent watchers are returned and
explicitly closed on server shutdown.
---
Duplicate comments:
In `@observer/server.js`:
- Around line 229-249: The server currently listens on all interfaces and
accepts unauthenticated HTTP and WebSocket requests; update the listener and
request handling so the observer binds to loopback and enforces a shared token
or strict origin check before accepting clients: change server.listen(...) to
bind to 127.0.0.1 and ::1, and in the HTTP routes that serve /events, /status,
/events/recent and in handleUpgrade (which uses wsHandshake, wsClients, and
store.setConnectedClients) validate a configured secret token (e.g., from env)
or verify the Origin header before proceeding—reject and destroy the socket or
respond 401 if the token/origin is missing/invalid so both HTTP and websocket
upgrade paths are gated by the same auth check.
---
Nitpick comments:
In `@observer/server.js`:
- Line 43: Replace the relative require for the event store with the repo's
absolute-import convention: locate the import that uses createEventStore (const
{ createEventStore } = require('./event-store')) and change it to the project's
absolute module path for event-store so it uses the same import pattern as other
files (keeping the createEventStore symbol unchanged).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b388ca8b-45c4-4615-abb6-3ba43a656185
📒 Files selected for processing (3)
eslint.config.jsobserver/dashboard.htmlobserver/server.js
| const observerOverride = { | ||
| files: ['observer/**'], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and read the eslint.config.js file
find . -name "eslint.config.js" -type fRepository: SynkraAI/aiox-core
Length of output: 80
🏁 Script executed:
# List files in the observer directory to see what exists
find . -type d -name "observer" 2>/dev/nullRepository: SynkraAI/aiox-core
Length of output: 72
🏁 Script executed:
# Search for observer directory and its contents
fd -t f "^.*" observer/ 2>/dev/null || echo "Checking with git..."
git ls-files | grep "^observer/" | head -20Repository: SynkraAI/aiox-core
Length of output: 195
🏁 Script executed:
cat -n ./eslint.config.jsRepository: SynkraAI/aiox-core
Length of output: 7205
🏁 Script executed:
# Check ESLint documentation on flat config glob patterns
web_search
ESLint flat config files glob pattern matching behaviorRepository: SynkraAI/aiox-core
Length of output: 157
🏁 Script executed:
# Verify the actual observer directory structure one more time
find observer -type f -printRepository: SynkraAI/aiox-core
Length of output: 128
🌐 Web query:
ESLint flat config files glob pattern matching observer/** behavior
💡 Result:
In ESLint flat config (eslint.config.js), glob patterns in the files and ignores properties of configuration objects use minimatch for matching. The pattern "observer/" matches the observer directory and all files and subdirectories within it recursively. A configuration object applies to a file if: - The file matches at least one pattern in the object's files array (or all files if no files key is present). - The file does not match any pattern in the object's ignores array. Global ignores are defined by a standalone object containing only an ignores array (e.g., { ignores: ["observer/"] }), which completely excludes matching files from linting, regardless of other configs. Local ignores only exclude from that specific config object. Paths in globs are relative to the directory containing eslint.config.js. Patterns use standard minimatch syntax: observer/ matches observer/any/sub/path/file.js but not observer.js or files outside observer/. No specific issues or special behavior were found for "observer/" in official docs or discussions; it behaves as a standard recursive directory glob. For directories, use observer/ (not observer/) in local ignores to exclude contents. Examples: - Apply rules to observer/: { files: ["observer/"], rules: { ... } } - Ignore observer/ globally: [{ ignores: ["observer/"] }, ...other configs] - Ignore observer/ locally: { files: ["/*"], ignores: ["observer/"], rules: { ... } }
Citations:
- 1: https://eslint.org/docs/user-guide/configuring/configuration-files
- 2: https://eslint.org/docs/latest/use/configure/configuration-files
- 3: https://eslint.org/blog/2022/08/new-config-system-part-2/
- 4: https://www.npmjs.com/package/@eslint/config-array
- 5: https://eslint.org/docs/user-guide/configuring/ignoring-code
- 6: https://stackoverflow.com/questions/73915236/how-do-i-configure-eslint-to-ignore-my-root-folder-and-to-only-include-my-src-fo/73915469
Narrow the observer override pattern to JS files only.
The pattern files: ['observer/**'] matches all files under the observer directory, including observer/dashboard.html. Since the override is designed for the CommonJS Node.js runtime (not static assets), restrict it to JavaScript files to explicitly declare the configuration's scope.
🩹 Minimal fix
- files: ['observer/**'],
+ files: ['observer/*.js', 'observer/**/*.js'],📝 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.
| const observerOverride = { | |
| files: ['observer/**'], | |
| const observerOverride = { | |
| files: ['observer/*.js', 'observer/**/*.js'], |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@eslint.config.js` around lines 3 - 4, The observer override currently uses
files: ['observer/**'] which matches all assets; change the glob to target only
JS files by updating the observerOverride.files pattern (the observerOverride
constant) to something like
['observer/**/*.js','observer/**/*.cjs','observer/**/*.mjs'] or at minimum
['observer/**/*.js'] so the CommonJS Node.js-specific override applies only to
JavaScript source files and not static assets like observer/dashboard.html.
| setText('log-count', `(${logRows.length})`); | ||
| setText('m-total', liveState.metrics.total || logRows.length); | ||
|
|
There was a problem hiding this comment.
Render the footer metrics after applying the live event delta.
appendEventToLog() reads liveState.metrics.total before applyEventDelta() increments it, so the total badge stays one event behind during live updates. events/min also never changes after init. Update the metric state first, then render both footer values from liveState.metrics.
Also applies to: 646-647, 738-742
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/dashboard.html` around lines 618 - 620, The footer metrics are
rendered before the live event delta is applied, causing the total and
events/min to lag; in appendEventToLog() call applyEventDelta() first to update
liveState.metrics, then use liveState.metrics.total and
liveState.metrics.eventsPerMinute (or the appropriate metrics field) when
calling setText('m-total', ...) and setText('log-count', ...); make the same
change at the other occurrences (around the setText calls at the other noted
locations) so both total and events/min are computed from liveState.metrics
after the delta is applied.
| if (liveState.bobStatus && data.agent && data.pid) { | ||
| liveState.bobStatus.active_terminals = liveState.bobStatus.active_terminals || []; | ||
| liveState.bobStatus.active_terminals.push({ agent: data.agent, pid: data.pid, task: data.task }); | ||
| renderTerminals(liveState.bobStatus.active_terminals); | ||
| } |
There was a problem hiding this comment.
Remove finished agents from active_terminals too.
BobAgentSpawned appends terminal rows, but BobAgentCompleted and AgentDeactivated only touch activeAgents. If bob-status updates lag—or chokidar is unavailable—the "Active Terminals" card keeps showing completed agents indefinitely. Prune the matching terminal entries in those teardown paths and re-render.
Also applies to: 680-698
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/dashboard.html` around lines 672 - 676, The "Active Terminals" list
is only appended in the BobAgentSpawned path
(liveState.bobStatus.active_terminals and renderTerminals) but not pruned when
agents finish; update the BobAgentCompleted and AgentDeactivated handlers to
remove any entries from liveState.bobStatus.active_terminals that match the
completed/deactivated agent (match by agent and pid), then call
renderTerminals(liveState.bobStatus.active_terminals) to re-render; ensure you
guard for liveState.bobStatus and initialize active_terminals as an array if
needed before manipulating it.
| case 'init': { | ||
| const { state, recentEvents } = msg.data || {}; | ||
| if (state) { | ||
| liveState = Object.assign(liveState, state); | ||
| applyState(state); | ||
| } | ||
| if (recentEvents && Array.isArray(recentEvents)) { | ||
| recentEvents.forEach(appendEventToLog); | ||
| } |
There was a problem hiding this comment.
Clear or de-duplicate the log on each init.
Reconnects append the last 50 recentEvents onto the existing logRows buffer, so a transient socket drop duplicates the log and inflates the visible row count. Reset the current rows on init or skip events whose id is already present.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/dashboard.html` around lines 726 - 734, The init handler currently
appends recentEvents onto the existing buffer, causing duplicates after
reconnect; in the case 'init' block (around liveState, applyState, recentEvents,
appendEventToLog) either clear the current UI log buffer before iterating (reset
logRows / DOM rows) or filter recentEvents to skip any event whose id already
exists in the log (compare event.id against existing log row ids) before calling
appendEventToLog; implement one of these fixes so reconnects replace or dedupe
the last-50 events instead of duplicating them.
| function startStatusWatcher(statusPath) { | ||
| if (!chokidar) { | ||
| console.log('[observer] chokidar not available — file watcher disabled'); | ||
| return; | ||
| } | ||
|
|
||
| // Watch with ignoreInitial=false to get the current value on start | ||
| const watcher = chokidar.watch(statusPath, { | ||
| persistent: true, | ||
| ignoreInitial: false, | ||
| awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }, | ||
| }); | ||
|
|
||
| const onChanged = (filePath) => { | ||
| try { | ||
| const raw = fs.readFileSync(filePath, 'utf8'); | ||
| const data = JSON.parse(raw); | ||
| store.setBobStatus(data); | ||
| broadcast('status_update', data); | ||
| } catch (_e) { | ||
| // File temporarily unreadable (mid-write) — ignore | ||
| } | ||
| }; | ||
|
|
||
| watcher.on('add', onChanged); | ||
| watcher.on('change', onChanged); | ||
|
|
||
| watcher.on('error', (_e) => { | ||
| // Watcher error — non-fatal, server continues | ||
| }); | ||
| } |
There was a problem hiding this comment.
Close the chokidar watcher when startServer() shuts down.
startStatusWatcher() creates a persistent watcher, but its handle is discarded. If a caller imports startServer() and later calls server.close(), the file watcher keeps running and can hold the process open. Return the watcher and close it from the server's 'close' path.
♻️ Minimal fix
function startStatusWatcher(statusPath) {
if (!chokidar) {
console.log('[observer] chokidar not available — file watcher disabled');
- return;
+ return null;
}
@@
watcher.on('error', (_e) => {
// Watcher error — non-fatal, server continues
});
+
+ return watcher;
}
@@
function startServer(port) {
const server = http.createServer(handleRequest);
+ const watcher = startStatusWatcher(BOB_STATUS_PATH);
+
+ server.on('close', () => {
+ if (watcher) {
+ watcher.close().catch(() => {});
+ }
+ });
@@
- startStatusWatcher(BOB_STATUS_PATH);
-
return server;
}Also applies to: 520-522
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@observer/server.js` around lines 450 - 480, startStatusWatcher currently
creates a persistent chokidar watcher and discards the handle, causing the
watcher to keep the process alive after shutdown; modify startStatusWatcher to
return the watcher instance (the variable watcher) and update the server
shutdown logic in startServer (or the server 'close' handler) to call
watcher.close() (or watcher.unwatch()/close equivalent) when server.close()
runs; also apply the same change to the similar watcher creation at the other
location referenced (around the 520-522 block) so all persistent watchers are
returned and explicitly closed on server shutdown.
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