feat(escalations): subscribe EscalationQueue to live SSE arrivals

Adds the frontend live-arrival slice on top of the test-stabilized SSE
backend. Senior techs now see a junior's escalation slide into the
queue without refresh.

- streamEscalations(handlers, signal) in aiSessions.ts: fetch-based
  ReadableStream parser (native EventSource cannot send auth headers).
  Handles SSE frames, partial frames across chunks, : keepalive
  heartbeats. Dispatches ready and handoff_created.
- HandoffCreatedEvent + EscalationStreamHandlers types mirror the bus
  payload published by HandoffManager.dispatch_escalation_notifications.
- EscalationQueue.tsx: AbortController-managed subscription with
  exponential-backoff reconnect (1s → 30s cap, attempt counter resets
  on ready). On handoff_created, refetch and diff against previous IDs
  via sessionsRef; new arrivals prepended (newest-first) above
  established cards (oldest-first preserved). Slide-in tag held for
  800ms so the locked 200ms animation completes. Tab-title flash
  prefixes (N) while document.hidden, restores on focus / unmount.
  prefers-reduced-motion swaps slide-in for fade-in. ARIA region +
  aria-live=polite + aria-label on heading. Pick Up bumped to py-2.5
  to clear the 44px touch floor.

Verified end-to-end against the running dev stack: subscriber received
the ready frame on connect; after posting a handoff via the API, the
subscriber received the handoff_created frame with the expected
payload — wire format matches the parser. Backend regression: focused
subset still 32 passed in 18.91s. Frontend tsc -b clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 20:57:15 -04:00
parent 02d5c6c08c
commit b8627f4180
3 changed files with 303 additions and 56 deletions

View File

@@ -258,3 +258,23 @@ export interface SimilarSession {
created_at: string | null
similarity: number
}
// ── Escalation SSE bus ──
//
// Mirrors the `event_generator` payload in
// backend/app/api/endpoints/session_handoffs.py — keep this in sync with the
// dict published by `HandoffManager.dispatch_escalation_notifications`.
export interface HandoffCreatedEvent {
type: 'handoff_created'
handoff_id: string
session_id: string
priority: string
engineer_notes: string
created_at: string | null
}
export interface EscalationStreamHandlers {
onReady?: () => void
onHandoffCreated?: (event: HandoffCreatedEvent) => void
}