fix(web-dashboard): add 401 auto-logout, error states, WS backoff fix, regime data wiring
- api.ts: intercept 401 responses, clear tokens, redirect to /login - websocket.ts: fix backoff ordering (increase before scheduling retry) - All dashboards: add error banners with retry buttons when API queries fail - MLDashboard: replace hardcoded regime data with WebSocket metrics stream Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { clearTokens } from './auth';
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
/** Fetch wrapper that injects JWT Authorization header */
|
||||
/** Fetch wrapper that injects JWT Authorization header and handles 401 auto-logout */
|
||||
async function apiFetch<T>(
|
||||
path: string,
|
||||
options: RequestInit = {},
|
||||
@@ -22,6 +24,13 @@ async function apiFetch<T>(
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
// Auto-logout on 401 (expired/invalid token)
|
||||
if (res.status === 401) {
|
||||
clearTokens();
|
||||
window.location.href = '/login';
|
||||
throw new Error('Session expired. Please log in again.');
|
||||
}
|
||||
|
||||
const body = await res.json().catch(() => ({ error: res.statusText }));
|
||||
throw new Error(body.error ?? `API error: ${res.status}`);
|
||||
}
|
||||
|
||||
@@ -99,11 +99,12 @@ export class WsManager {
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer) return;
|
||||
const delay = this.backoff;
|
||||
this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.connect();
|
||||
this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
|
||||
}, this.backoff);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,16 @@ import { ModelCard } from '../components/ml/ModelCard';
|
||||
import { EnsemblePanel } from '../components/ml/EnsemblePanel';
|
||||
import { TrainingProgress } from '../components/ml/TrainingProgress';
|
||||
import { useApiQuery } from '../hooks/useApi';
|
||||
import { useWebSocketTopic } from '../hooks/useWebSocket';
|
||||
import type { MlPrediction, TrainingJob } from '../lib/types';
|
||||
|
||||
interface RegimeData {
|
||||
regime?: string;
|
||||
volatility?: string;
|
||||
confidence?: number;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
const MODELS = ['DQN', 'PPO', 'TFT', 'Mamba2'] as const;
|
||||
|
||||
export function MLDashboard() {
|
||||
@@ -19,10 +27,24 @@ export function MLDashboard() {
|
||||
{ refetchInterval: 5000 },
|
||||
);
|
||||
|
||||
const { messages: metricsMessages } = useWebSocketTopic('metrics');
|
||||
|
||||
// Extract regime data from most recent metrics message
|
||||
const latestMetrics = metricsMessages.length > 0
|
||||
? (metricsMessages.at(-1) as { data?: RegimeData })?.data
|
||||
: undefined;
|
||||
|
||||
const preds = predictions.data ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{(predictions.isError || trainingJobs.isError) && (
|
||||
<div className="px-3 py-2 rounded border border-[var(--color-red)] bg-red-500/10 text-sm text-[var(--color-red)] flex items-center justify-between">
|
||||
<span>Failed to load {predictions.isError ? 'predictions' : 'training jobs'}: {(predictions.error ?? trainingJobs.error)?.message}</span>
|
||||
<button onClick={() => { predictions.refetch(); trainingJobs.refetch(); }} className="text-xs underline">Retry</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model Cards */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{MODELS.map((model) => {
|
||||
@@ -55,19 +77,19 @@ export function MLDashboard() {
|
||||
<div className="p-3 space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-[var(--color-text-secondary)]">Current Regime</span>
|
||||
<span className="text-[var(--color-accent)]">Trending</span>
|
||||
<span className="text-[var(--color-accent)]">{latestMetrics?.regime ?? '--'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-[var(--color-text-secondary)]">Volatility</span>
|
||||
<span>Medium</span>
|
||||
<span>{latestMetrics?.volatility ?? '--'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-[var(--color-text-secondary)]">Regime Confidence</span>
|
||||
<span>72.3%</span>
|
||||
<span>{latestMetrics?.confidence !== undefined ? `${(latestMetrics.confidence * 100).toFixed(1)}%` : '--'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-[var(--color-text-secondary)]">Regime Duration</span>
|
||||
<span>4h 23m</span>
|
||||
<span>{latestMetrics?.duration ?? '--'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,13 @@ export function PerformanceDashboard() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{(metrics.isError || pnlData.isError) && (
|
||||
<div className="px-3 py-2 rounded border border-[var(--color-red)] bg-red-500/10 text-sm text-[var(--color-red)] flex items-center justify-between">
|
||||
<span>Failed to load {metrics.isError ? 'metrics' : 'P&L data'}: {(metrics.error ?? pnlData.error)?.message}</span>
|
||||
<button onClick={() => { metrics.refetch(); pnlData.refetch(); }} className="text-xs underline">Retry</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top: Metric cards */}
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
|
||||
|
||||
@@ -16,6 +16,13 @@ export function RiskDashboard() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{riskMetrics.isError && (
|
||||
<div className="px-3 py-2 rounded border border-[var(--color-red)] bg-red-500/10 text-sm text-[var(--color-red)] flex items-center justify-between">
|
||||
<span>Failed to load risk metrics: {riskMetrics.error.message}</span>
|
||||
<button onClick={() => riskMetrics.refetch()} className="text-xs underline">Retry</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top row: Risk gauges */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
|
||||
|
||||
@@ -38,6 +38,13 @@ export function TradingDashboard() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{(positions.isError || orders.isError) && (
|
||||
<div className="px-3 py-2 rounded border border-[var(--color-red)] bg-red-500/10 text-sm text-[var(--color-red)] flex items-center justify-between">
|
||||
<span>Failed to load {positions.isError ? 'positions' : 'orders'}: {(positions.error ?? orders.error)?.message}</span>
|
||||
<button onClick={() => { positions.refetch(); orders.refetch(); }} className="text-xs underline">Retry</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top row: Chart + Order Book */}
|
||||
<div className="grid grid-cols-[1fr_320px] gap-4 h-80">
|
||||
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)] overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user