Files
Control-Center/frontend/src/pages/SettingsPage.tsx
Joshua 23f9d4a8fb 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>
2026-05-20 12:58:21 -04:00

131 lines
5.1 KiB
TypeScript

import { useTheme } from '../hooks/useTheme'
import { useLocalStorage } from '../hooks/useLocalStorage'
import { useSSEContext } from '../contexts/SSEContext'
import { Sun, Moon, Monitor, Zap, Radio } from 'lucide-react'
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 { sseStatus } = useSSEContext()
const sseInfo = SSE_STATUS_COPY[sseStatus] ?? SSE_STATUS_COPY.error
return (
<div className="space-y-8 max-w-2xl">
<header>
<h1 className="text-2xl font-bold">Settings</h1>
<p className="text-on-surface-variant">Application preferences</p>
</header>
{/* Appearance */}
<section className="space-y-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<Monitor size={20} className="text-primary" />
Appearance
</h2>
<div className="p-5 rounded-xl border border-surface-light bg-surface-dark">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Dark Mode</p>
<p className="text-sm text-on-surface-variant">Toggle between dark and light themes</p>
</div>
<button
onClick={toggleTheme}
className={`relative w-14 h-8 rounded-full transition-colors duration-200 ${
isDark ? 'bg-primary' : 'bg-surface-lighter'
}`}
aria-label="Toggle dark mode"
>
<div
className={`absolute top-1 h-6 w-6 rounded-full bg-white transition-transform duration-200 flex items-center justify-center ${
isDark ? 'translate-x-7' : 'translate-x-1'
}`}
>
{isDark ? <Moon size={14} className="text-surface-darkest" /> : <Sun size={14} className="text-yellow-500" />}
</div>
</button>
</div>
</div>
</section>
{/* Connection */}
<section className="space-y-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<Zap size={20} className="text-primary" />
Connection
</h2>
<div className="p-5 rounded-xl border border-surface-light bg-surface-dark space-y-3">
<div>
<label htmlFor="gateway-url" className="block text-sm font-medium mb-1">
Gateway URL
</label>
<input
id="gateway-url"
type="url"
value={gatewayUrl}
onChange={(e) => setGatewayUrl(e.target.value)}
placeholder="http://localhost:8080"
className="w-full px-3 py-2 rounded-lg border border-surface-light bg-surface-darkest text-on-surface placeholder:text-on-surface-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
/>
<p className="text-xs text-on-surface-muted mt-1">
The backend Gateway address for API requests
</p>
</div>
</div>
</section>
{/* Real-time connection status */}
<section className="space-y-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<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">
<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>
&nbsp;·&nbsp;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>
)
}