Files
headquarter/openspec/changes/container-monitoring-notifications/apply-pr2.md
T
alex f13a63dc2f feat: container monitoring frontend UI (PR-2)
- Custom ToastContext + ToastProvider + ToastContainer (~170 lines, no deps)
- useEvents() SSE hook with exponential backoff reconnect
- EventProvider context for app-wide SSE stream sharing
- Event-to-toast bridge with severity mapping and deduplication
- Real-time status badge updates replacing 30s polling
- EventSource auth probe (401/429 detection via fetch)
- 14 frontend tests (useEvents + toast-rules)

Quality gates: vitest 14 passed, tsc clean, eslint clean
2026-05-29 10:25:00 +02:00

7.2 KiB
Raw Blame History

PR-2 Apply Report: Frontend UI for Container Monitoring & Notifications

Status: COMPLETE

All assigned PR-2 tasks have been implemented and validated.


Changed Files

New Files (9)

File Purpose
apps/web/src/types/events.ts TypeScript InstanceEventPayload + InstanceEventMetadata interfaces
apps/web/src/api/events.ts Thin EventSource wrapper + probeEventStreamStatus for 401/429 detection
apps/web/src/hooks/use-events.ts useEvents() hook with SSE connect, exponential backoff reconnect, jitter
apps/web/src/hooks/use-events.test.ts Unit tests for useEvents (7 tests)
apps/web/src/components/toast-rules.ts Event-to-toast mapping + deduplication logic
apps/web/src/components/toast-rules.test.ts Unit tests for toast rules (7 tests)
apps/web/src/state/toast.tsx Custom lightweight toast system: ToastContext, ToastProvider, ToastContainer
apps/web/src/state/events.tsx EventProvider context that wraps useEvents() and exposes events to consumers
apps/web/src/components/event-toast-bridge.tsx Bridge component that consumes EventContext and triggers toasts via toast-rules

Modified Files (4)

File Change
apps/web/src/components/app-shell.tsx Mount EventProvider + ToastProvider + EventToastBridge on all authenticated routes
apps/web/src/components/instance-list.tsx Removed 30s health polling; added SSE-driven real-time status updates; retained 60s list refresh
apps/web/src/components/session-card.tsx Updated statusConfig badge colors: starting/probing → blue, unhealthy → amber
apps/web/src/styles.css Added .status-badge.starting, .status-badge.probing, .status-badge.unhealthy + toast animation keyframes

Implementation Summary

MON-PR2-001 / MON-PR2-002: useEvents() Hook + events.ts API Client

  • createEventSource() returns native EventSource with withCredentials: true
  • useEvents() hook maintains events, connected, reconnectCount, and error state
  • Reconnect strategy: delay = min(30000, 1000 * 2^attempts) * (0.8 + Math.random() * 0.4)
  • On 401 (detected via probeEventStreamStatus fetch probe): stops reconnecting and redirects to login
  • On 429: adds 5s penalty before next retry
  • Cleans up EventSource and pending timeouts on unmount

MON-PR2-003 / MON-PR2-004: Custom Toast System (No External Dependencies)

  • Built a pure React + CSS toast stack:
    • ToastContext with addToast / removeToast APIs
    • ToastProvider manages timer-based auto-dismissal
    • ToastContainer renders fixed-position stack with inline styles + CSS animation
  • Supports severity colors: info (blue), success (green), warning (amber), error (red)
  • Auto-dismiss timers: info/success 3s, warning 5s, error 10s (configurable)
  • Manual dismiss via × button on each toast

MON-PR2-005: EventProvider Context

  • EventProvider mounts at app-shell level, calls useEvents() once, shares event stream via React context
  • useEventContext() allows any descendant to subscribe to the shared SSE stream without creating duplicate connections

MON-PR2-006: Real-Time Status Badge Updates + Polling Removal

  • Removed the 30-second checkInstanceHealth polling loop from instance-list.tsx
  • Added useEffect that listens to SSE events and updates instances state in-place for matching instance_id
  • Retained a 60-second setInterval for loadInstances() as a resilience fallback
  • Updated session-card.tsx badge color mapping to match spec:
    • starting / probing → blue CSS class
    • unhealthy → amber CSS class
  • Added corresponding CSS rules in styles.css

MON-PR2-007: Integration into App Shell

  • AppShell now wraps all authenticated routes with EventProvider and ToastProvider
  • EventToastBridge is mounted inside the providers to render toasts from SSE events
  • Mobile terminal view also gets the providers (toasts still work in terminal)

MON-PR2-008: Frontend Tests

  • use-events.test.ts: 7 tests covering event parsing, reconnect backoff, 30s cap, 401 redirect, 429 penalty, unmount cleanup, reconnectCount exposure
  • toast-rules.test.ts: 7 tests covering event-to-toast mapping (started, running, unhealthy, error) and deduplication within 1s window

Test Commands & Exit Codes

# Focused new tests
$ cd apps/web && npx vitest run src/hooks/use-events.test.ts src/components/toast-rules.test.ts
# Exit code: 0  (14 passed)

# Broader regression check on modified page/component tests
$ cd apps/web && npx vitest run src/hooks/use-events.test.ts src/components/toast-rules.test.ts src/pages/dashboard.test.tsx src/components/terminal-session-tabs.test.tsx src/components/protected-route.test.tsx
# Exit code: 0  (25 passed)

# TypeScript type check
$ cd apps/web && npx tsc --noEmit
# Exit code: 0  (no errors)

# Lint on new/modified files only
$ cd apps/web && npx eslint <new ts/tsx files> --ext ts,tsx --report-unused-disable-directives --max-warnings 0
# Exit code: 0  (all clean)

Note: The full npx vitest run shows 4 pre-existing failures in repositories-settings-tab.test.tsx (unrelated to this PR). The full npx eslint also shows 3 pre-existing errors in terminal.tsx and tool-workshop.tsx.


Surprises & Decisions

  1. No sonner dependency: The orchestrator instructed not to install sonner because npm install hangs in this environment. Implemented a custom ~170-line toast system instead using pure React + inline CSS. It supports severity, auto-dismiss, manual dismiss, and stacking with CSS animations.
  2. EventSource 401/429 detection: Native EventSource does not expose HTTP status codes. Implemented a probeEventStreamStatus() helper that does a short fetch() to the SSE endpoint with AbortController timeout to detect 401/429 before reconnecting.
  3. Badge color CSS classes: The existing session-card.tsx used raw color strings ("green", "yellow", etc.) as CSS class names, but no corresponding CSS classes existed. Added explicit .status-badge.starting, .status-badge.probing, and .status-badge.unhealthy rules to styles.css.
  4. Tunnel health removal: The 30s polling loop in instance-list.tsx was the source of healthStatus state used for "tunnel error" badges. After removing polling, tunnel-specific health data is no longer available in real time; instances now rely on SSE status transitions (e.g., unhealthy). The tunnel error badge was removed from instance-list.tsx as redundant with the status badge.
  5. App.tsx vs app-shell.tsx: This codebase has no App.tsx; AppShell in app-shell.tsx is the layout component that wraps all authenticated routes. Providers were mounted there instead.

PR Boundary

This PR includes the complete frontend UI for container monitoring and notifications:

  • SSE client hook with reconnect backoff
  • Custom toast notification system
  • Event provider context
  • Real-time status badge updates
  • Polling removal from instance list

The next PR (PR-3) should cover:

  • Integration tests for lifecycle event flow
  • E2E tests for container start → toast and crash detection
  • Performance tuning (connection limits, queue bounds, jitter)
  • Documentation updates
  • Final cleanup and regression validation