feat(dashboard): React frontend scaffold with overview page

- Vite + React 18 + TypeScript + Tailwind CSS
- Dashboard overview with health gauge, pillar bars, alerts
- WebSocket hook for real-time updates
- Layout with sidebar navigation and live indicator
- Placeholder pages for Mesh, Environment, Config, Alerts
- Dark theme ops center aesthetic with JetBrains Mono

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
zvx-echo6 2026-05-12 10:28:12 -06:00
commit 4331bcb7e1
19 changed files with 1088 additions and 0 deletions

View file

@ -0,0 +1,102 @@
import { useEffect, useRef, useState, useCallback } from 'react'
import type { MeshHealth, Alert } from '@/lib/api'
interface WebSocketMessage {
type: string
data: unknown
}
interface UseWebSocketReturn {
connected: boolean
lastHealth: MeshHealth | null
lastAlert: Alert | null
}
export function useWebSocket(): UseWebSocketReturn {
const [connected, setConnected] = useState(false)
const [lastHealth, setLastHealth] = useState<MeshHealth | null>(null)
const [lastAlert, setLastAlert] = useState<Alert | null>(null)
const wsRef = useRef<WebSocket | null>(null)
const reconnectTimeoutRef = useRef<number | null>(null)
const reconnectDelayRef = useRef(1000)
const connect = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
return
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${protocol}//${window.location.host}/ws/live`
try {
const ws = new WebSocket(wsUrl)
wsRef.current = ws
ws.onopen = () => {
setConnected(true)
reconnectDelayRef.current = 1000 // Reset backoff on successful connection
}
ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data)
switch (message.type) {
case 'health_update':
setLastHealth(message.data as MeshHealth)
break
case 'alert_fired':
setLastAlert(message.data as Alert)
break
}
} catch (e) {
console.error('Failed to parse WebSocket message:', e)
}
}
ws.onclose = () => {
setConnected(false)
wsRef.current = null
// Schedule reconnect with exponential backoff
const delay = Math.min(reconnectDelayRef.current, 30000)
reconnectTimeoutRef.current = window.setTimeout(() => {
reconnectDelayRef.current = Math.min(delay * 2, 30000)
connect()
}, delay)
}
ws.onerror = () => {
ws.close()
}
// Keepalive ping every 30 seconds
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send('ping')
}
}, 30000)
ws.addEventListener('close', () => {
clearInterval(pingInterval)
})
} catch (e) {
console.error('Failed to create WebSocket:', e)
}
}, [])
useEffect(() => {
connect()
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
if (wsRef.current) {
wsRef.current.close()
}
}
}, [connect])
return { connected, lastHealth, lastAlert }
}