Skip to content

feature(web/monitor): live tail view + pause/resume + bounded buffer#173

Open
jamby77 wants to merge 2 commits into
feature/monitor-web-start-session-modalfrom
feature/monitor-web-live-tail
Open

feature(web/monitor): live tail view + pause/resume + bounded buffer#173
jamby77 wants to merge 2 commits into
feature/monitor-web-start-session-modalfrom
feature/monitor-web-live-tail

Conversation

@jamby77
Copy link
Copy Markdown
Collaborator

@jamby77 jamby77 commented May 11, 2026

Summary

PR 10 of 25 in docs/plans/specs/monitor-command/plan-implementation.md. Stacked on top of #172 (PR 9). Sessions table rows are now clickable, navigating to a per-session detail page that streams the live MONITOR output via the TailGateway from PR 7. Lines appear in real time, Pause freezes the local view while the gateway buffers server-side, Resume drains the buffered window in order.

useMonitorTail(sessionId, bufferSize=5000) hook

  • Opens ws://localhost:3001/monitor/ws?sessionId=X (dev) or /api/monitor/ws (prod, same-origin) per env.
  • Accumulates incoming lines in a ref-buffer; flushes a snapshot to React state at most once per requestAnimationFrame so the UI batches updates at ~60 Hz under thousands of lines/sec without per-line re-renders.
  • Bounds the buffer at 5 000 lines — when it overflows the oldest drop and bufferTrimmed flips true.
  • Maintains a separate totalReceived counter (NOT bounded) so the UI can show "N lines received · showing last K (older lines dropped)".
  • pause() / resume() send {type:'pause'|'resume'} control messages; the server-side per-viewer buffer drains in original order.
  • status state tracks connecting → streaming → paused → session_ended | historical_complete | closed | error.

TailView + MonitorSession page

  • Status badge, line counter, dropped-lines notice, pause/resume controls (only when live).
  • Monospace scrollable panel that auto-scrolls to bottom unless the user scrolls up (followBottomRef).
  • New route /monitor/sessions/:id with header showing session id, started timestamp, source, line count, termination reason.
  • SessionsTable rows are clickable and navigate to the detail page.

Test plan

  • pnpm --filter web exec tsc --noEmit → exit 0
  • pnpm --filter web exec eslint src/hooks/useMonitorTail.ts src/pages/monitor/tail-view.tsx src/pages/MonitorSession.tsx → no errors
  • Run dev: MONITOR_DEV_PREVIEW=true pnpm dev:api and VITE_MONITOR_DEV_PREVIEW=true pnpm dev:web
  • Start a long session (5 min): curl -X POST -H 'content-type: application/json' -d '{"connectionId":"env-default","durationMs":300000}' http://localhost:3001/monitor/sessions
  • Browser: click the new row → detail page shows the live tail; status badge "Streaming"
  • Generate traffic: docker exec betterdb-monitor-valkey valkey-cli -a devpassword -r 80 SET k v → SET lines stream in within ~1 s
  • Click Pause → status flips to amber "Paused"; tail line counter freezes
  • Generate more traffic → counter still frozen
  • Click Resume → buffered SETs drain in original order; status back to "Streaming"
  • Open the same session ID in a second tab → both tabs scroll independently (PR 7 contract)
  • Wait for the session to end → status "Session ended"; pause/resume buttons disappear; lines stay rendered
  • Refresh the session-detail page after the session ended → tail re-streams persisted chunks then flips to "Replay complete"

Notes for reviewers

  • No virtualization library added. A bounded 5 000-line ring buffer + plain DOM rendering is fine for the line sizes we see; modern browsers handle 5 000 short text rows easily and the rAF batching keeps the React commit cost flat. If real captures need larger windows we'll add @tanstack/react-virtual then; flagged.
  • One ESLint suppress in useMonitorTail.ts for react-hooks/set-state-in-effect on the connection-reset block. Those setStates are pre-WS-open initial-state restoration; React 18 batches them into a single commit, no cascade is possible. Documented inline.
  • WebSocket auth model: same as CliGateway and the rest of the gateway — handshake-time gate via MONITOR_DEV_PREVIEW + demo-host check. No per-message auth.
  • Race-condition note from PR 7 still applies: a viewer connecting in the millisecond between the writer terminating and the active-map cleanup may see session_ended instead of historical_complete. Both produce the same close + status frame, so the UI handles them identically.
  • Net diff: 397 lines. Within budget.
  • Screenshots saved to docs/assets/:
    • pr10-live-tail.png (live tail mid-session)
    • pr10-tail-after-resume.png (post-resume drain)

Stacked PR

Base branch is feature/monitor-web-start-session-modal (#172), so the diff shown is ONLY PR 10 changes.

@jamby77 jamby77 force-pushed the feature/monitor-web-start-session-modal branch from ab6acde to 36dedbc Compare May 12, 2026 11:07
@jamby77 jamby77 force-pushed the feature/monitor-web-live-tail branch from 0f03629 to e8e7644 Compare May 12, 2026 11:10
@jamby77 jamby77 force-pushed the feature/monitor-web-start-session-modal branch from 36dedbc to b90faee Compare May 12, 2026 12:39
@jamby77 jamby77 closed this May 12, 2026
@jamby77 jamby77 force-pushed the feature/monitor-web-live-tail branch from e8e7644 to b90faee Compare May 12, 2026 12:39
@github-actions github-actions Bot locked and limited conversation to collaborators May 12, 2026
@jamby77 jamby77 reopened this May 13, 2026
@jamby77 jamby77 force-pushed the feature/monitor-web-start-session-modal branch from 8aefbac to a86cd6b Compare May 13, 2026 12:34
jamby77 added 2 commits May 13, 2026 15:35
The Sessions table rows are now clickable links to a per-session detail
page that streams the live MONITOR output via the TailGateway. Lines
appear in real time, the Pause button freezes the local view while the
gateway buffers server-side, and Resume drains the buffered window in
order.

- New useMonitorTail(sessionId, bufferSize=5000) hook:
  - Opens ws://localhost:3001/monitor/ws?sessionId=X (dev) or
    /api/monitor/ws (prod, same-origin) per env.
  - Accumulates incoming MONITOR text lines in a ref-buffer; flushes
    a snapshot to React state at most once per animation frame so the
    UI batches updates at ~60 Hz under thousands of lines/sec.
  - Bounds the buffer at 5 000 lines — when it overflows the oldest
    drop and bufferTrimmed flips true.
  - Maintains a separate totalReceived counter (NOT bounded) so the
    UI can show "N lines received · showing last K (older lines
    dropped)".
  - pause()/resume() send {type:'pause'|'resume'} control messages;
    the server-side per-viewer buffer drains in original order.
  - Status state tracks connecting → streaming → paused → session_ended
    | historical_complete | closed | error.

- New TailView component: status badge, line counter, dropped-lines
  notice, pause/resume controls (only when live), monospace scrollable
  panel that auto-scrolls to bottom unless the user scrolls up
  (followBottomRef ref).

- New MonitorSession page (route: /monitor/sessions/:id) with header
  showing session id, started timestamp, source, line count, and
  termination reason; live tail panel below.

- SessionsTable rows are now clickable and navigate to the detail page.

- ESLint disable for react-hooks/set-state-in-effect on the connection-
  reset block: those setStates are pre-WS-open initial-state restoration,
  React 18 batches them into a single commit, no cascade is possible.
  Documented inline.

Verification (Playwright, live):
- Started a 5-min session, navigated to /monitor/sessions/:id, sent
  80 SETs from another terminal → all 80 SET lines streamed into the
  view in real time alongside ongoing INFO polling; total reached 173
  lines.
- Clicked Pause → status badge flipped to amber "Paused"; tail froze
  at 37 lines. Sent 100 SETs against Valkey while paused → still 37
  shown. Clicked Resume → instantly drained to 278 lines, all 100
  paused SETs included in original order.

Screenshots:
- docs/assets/pr10-live-tail.png (live tail mid-session)
- docs/assets/pr10-tail-after-resume.png (post-resume drain)

Part of PR 10 of 25 in
docs/plans/specs/monitor-command/plan-implementation.md.
Self-review fixes:
- useMonitorTail cleanup detaches onopen/onmessage/onerror/onclose
  before calling ws.close(). close() is async, so without nulling
  the handlers the old socket's onclose can fire after a new socket
  has been created and flip the new connection's status to 'closed'.
- MonitorSession used listSessions({ limit: 100 }) and filtered
  client-side, which silently failed for older sessions or sessions
  on a non-current connection. Switched to monitorApi.getSession(id)
  (added) which hits GET /monitor/sessions/:id.
@jamby77 jamby77 force-pushed the feature/monitor-web-live-tail branch from 2d42d85 to 997fbc8 Compare May 13, 2026 12:35
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant