New modern dashboard at /v2 running alongside the existing UI at /. Same backend, same APIs, same WebSocket — zero backend changes. Stack: React 19 + Vite + TypeScript + Recharts Source: frontend/ — build output: static/v2/ Phase 1 delivers: - Character overview cards in a responsive CSS Grid - Live HP/Stamina/Mana bars via WebSocket vitals - Kills/hr, total kills, deaths, session uptime - VTank state badge (Combat/Nav/Idle) - Location coordinates - Click to expand: combat stats, prismatic count, CPU/RAM - Global stats header: active chars, total kills, total rares, server health - WebSocket hook with auto-reconnect - HTTP poll fallback for initial load + server health - Mobile responsive (single column on narrow screens) - Dark theme matching the MosswartOverlord palette Build: cd frontend && npm run build Access: /v2 (served by existing NoCacheStaticFiles mount) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
44 lines
1.2 KiB
TypeScript
44 lines
1.2 KiB
TypeScript
import { useEffect, useRef, useCallback } from 'react';
|
|
import { wsUrl } from '../api/client';
|
|
import type { WSMessage } from '../types';
|
|
|
|
type MessageHandler = (msg: WSMessage) => void;
|
|
|
|
export function useWebSocket(onMessage: MessageHandler) {
|
|
const wsRef = useRef<WebSocket | null>(null);
|
|
const reconnectTimer = useRef<number>(0);
|
|
const onMessageRef = useRef(onMessage);
|
|
onMessageRef.current = onMessage;
|
|
|
|
const connect = useCallback(() => {
|
|
if (wsRef.current?.readyState === WebSocket.OPEN) return;
|
|
|
|
const ws = new WebSocket(wsUrl());
|
|
wsRef.current = ws;
|
|
|
|
ws.addEventListener('message', (evt) => {
|
|
try {
|
|
const msg = JSON.parse(evt.data) as WSMessage;
|
|
onMessageRef.current(msg);
|
|
} catch { /* ignore parse errors */ }
|
|
});
|
|
|
|
ws.addEventListener('close', () => {
|
|
wsRef.current = null;
|
|
reconnectTimer.current = window.setTimeout(connect, 2000);
|
|
});
|
|
|
|
ws.addEventListener('error', () => {
|
|
ws.close();
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
connect();
|
|
return () => {
|
|
clearTimeout(reconnectTimer.current);
|
|
wsRef.current?.close();
|
|
wsRef.current = null;
|
|
};
|
|
}, [connect]);
|
|
}
|