CUB-125: implement real-time SSE/WebSocket in React frontend
- Add useSSE hook with exponential back-off reconnect (1s → 30s) - Add useRealtimeSync hook: maps SSE events to React Query invalidation (agent.status → agents; agent.task/agent.progress → tasks+agents; fleet.update → all) - Add SSEContext/SSEProvider so connection status is available app-wide - Mount SSEProvider in main.tsx inside QueryClientProvider (no polling) - Show live/connecting/reconnecting/disconnected badge in sidebar + mobile header - Update SettingsPage: replace polling interval UI with SSE status panel - Disable React Query polling (staleTime 60s); all updates pushed via SSE Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { Command, Activity, FolderKanban, Monitor, Settings, Menu, X } from 'lucide-react'
|
||||
import { Command, Activity, FolderKanban, Monitor, Settings, Menu, X, Wifi, WifiOff, Loader } from 'lucide-react'
|
||||
import { useSSEContext } from '../contexts/SSEContext'
|
||||
import type { SSEStatus } from '../hooks/useSSE'
|
||||
|
||||
const navItems = [
|
||||
{ to: '/', icon: Command, label: 'Hub' },
|
||||
@@ -10,9 +12,29 @@ const navItems = [
|
||||
{ to: '/settings', icon: Settings, label: 'Settings' },
|
||||
]
|
||||
|
||||
/** Small status pill shown in the sidebar footer and mobile header. */
|
||||
function SSEStatusBadge({ status, showLabel = false }: { status: SSEStatus; showLabel?: boolean }) {
|
||||
const cfg = {
|
||||
connected: { icon: Wifi, color: 'text-green-500', label: 'Live' },
|
||||
connecting: { icon: Loader, color: 'text-yellow-500 animate-spin', label: 'Connecting' },
|
||||
reconnecting: { icon: Loader, color: 'text-yellow-500 animate-spin', label: 'Reconnecting' },
|
||||
error: { icon: WifiOff, color: 'text-red-500', label: 'Disconnected' },
|
||||
}[status]
|
||||
|
||||
const Icon = cfg.icon
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5" title={cfg.label}>
|
||||
<Icon size={14} className={cfg.color} />
|
||||
{showLabel && <span className={`text-xs ${cfg.color}`}>{cfg.label}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const { sseStatus } = useSSEContext()
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-surface-darkest text-on-surface">
|
||||
@@ -46,6 +68,15 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
{/* SSE connection status — footer of sidebar */}
|
||||
<div className="px-4 py-3 border-t border-surface-light flex items-center gap-2">
|
||||
<SSEStatusBadge status={sseStatus} />
|
||||
{expanded && (
|
||||
<span className="text-xs text-on-surface-muted whitespace-nowrap">
|
||||
{sseStatus === 'connected' ? 'Live updates on' : sseStatus}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Mobile Header + Bottom Nav */}
|
||||
@@ -54,6 +85,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
<div className="flex items-center gap-2">
|
||||
<Command size={22} className="text-primary" />
|
||||
<span className="font-bold">Control Center</span>
|
||||
<SSEStatusBadge status={sseStatus} />
|
||||
</div>
|
||||
<button onClick={() => setMobileOpen(!mobileOpen)} className="p-2">
|
||||
{mobileOpen ? <X size={22} /> : <Menu size={22} />}
|
||||
|
||||
Reference in New Issue
Block a user