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,18 +1,36 @@
|
||||
import { useTheme } from '../hooks/useTheme'
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage'
|
||||
import { Sun, Moon, Monitor, Zap, Clock } from 'lucide-react'
|
||||
import { useSSEContext } from '../contexts/SSEContext'
|
||||
import { Sun, Moon, Monitor, Zap, Radio } from 'lucide-react'
|
||||
|
||||
const REFRESH_PRESETS = [
|
||||
{ label: '5s', value: 5_000 },
|
||||
{ label: '10s', value: 10_000 },
|
||||
{ label: '30s', value: 30_000 },
|
||||
{ label: '60s', value: 60_000 },
|
||||
]
|
||||
const SSE_STATUS_COPY: Record<string, { label: string; description: string; color: string }> = {
|
||||
connected: {
|
||||
label: 'Connected',
|
||||
description: 'Real-time updates are active. Agent status, tasks, and progress stream live.',
|
||||
color: 'text-green-500',
|
||||
},
|
||||
connecting: {
|
||||
label: 'Connecting…',
|
||||
description: 'Establishing SSE connection to the backend.',
|
||||
color: 'text-yellow-500',
|
||||
},
|
||||
reconnecting: {
|
||||
label: 'Reconnecting…',
|
||||
description: 'Connection lost. Retrying with exponential back-off.',
|
||||
color: 'text-yellow-500',
|
||||
},
|
||||
error: {
|
||||
label: 'Disconnected',
|
||||
description: 'Could not connect to the SSE endpoint. Check that the backend is reachable.',
|
||||
color: 'text-red-500',
|
||||
},
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { isDark, toggleTheme } = useTheme()
|
||||
const [gatewayUrl, setGatewayUrl] = useLocalStorage('cc-gateway-url', '')
|
||||
const [refreshInterval, setRefreshInterval] = useLocalStorage('cc-refresh-interval', 30_000)
|
||||
const { sseStatus } = useSSEContext()
|
||||
const sseInfo = SSE_STATUS_COPY[sseStatus] ?? SSE_STATUS_COPY.error
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-2xl">
|
||||
@@ -80,45 +98,31 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Refresh */}
|
||||
{/* Real-time connection status */}
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Clock size={20} className="text-primary" />
|
||||
Auto Refresh
|
||||
<Radio size={20} className="text-primary" />
|
||||
Real-time Updates
|
||||
</h2>
|
||||
|
||||
<div className="p-5 rounded-xl border border-surface-light bg-surface-dark space-y-3">
|
||||
<p className="text-sm text-on-surface-variant">Data refresh interval for agent status and logs</p>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="1"
|
||||
value={REFRESH_PRESETS.findIndex((p) => p.value === refreshInterval)}
|
||||
onChange={(e) => {
|
||||
const idx = parseInt(e.target.value)
|
||||
setRefreshInterval(REFRESH_PRESETS[idx].value)
|
||||
}}
|
||||
className="w-full accent-primary"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-on-surface-muted">
|
||||
{REFRESH_PRESETS.map((p) => (
|
||||
<button
|
||||
key={p.label}
|
||||
onClick={() => setRefreshInterval(p.value)}
|
||||
className={`px-3 py-1 rounded-lg transition-colors ${
|
||||
refreshInterval === p.value
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'hover:bg-surface-light'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">SSE Connection</p>
|
||||
<p className="text-sm text-on-surface-variant mt-0.5">{sseInfo.description}</p>
|
||||
</div>
|
||||
<span className={`text-sm font-semibold whitespace-nowrap ${sseInfo.color}`}>
|
||||
{sseInfo.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-on-surface-muted">
|
||||
Endpoint: <code className="bg-surface-light px-1.5 py-0.5 rounded text-on-surface-variant">/api/events</code>
|
||||
· Events: agent.status, agent.task, agent.progress, fleet.update
|
||||
</p>
|
||||
<p className="text-xs text-on-surface-muted">
|
||||
Polling is disabled. All status updates are pushed from the server over a persistent SSE connection.
|
||||
The client reconnects automatically with exponential back-off on drop.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user