Backend (Rust): - Create proto/auth.proto with Login and RefreshToken RPCs - Implement AuthGrpcService in services/api with JWT issuance - Register as unauthenticated service in main.rs (pre-auth) - Update JWT issuer from foxhunt-api-gateway to foxhunt-api Dashboard (TypeScript): - Install @connectrpc/connect-web + @bufbuild/protobuf - Generate TypeScript proto clients via buf (src/gen/) - Create grpc.ts transport with JWT interceptor - Rewrite all useApi hooks to typed gRPC calls - Replace WebSocket with useGrpcStream server-streaming hooks - Migrate all 6 pages + 6 components to grpc-web - Delete api.ts, websocket.ts, useWebSocket.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
113 lines
4.6 KiB
TypeScript
113 lines
4.6 KiB
TypeScript
import { ComponentErrorBoundary } from '../components/common/ComponentErrorBoundary';
|
|
import { RiskGauge } from '../components/risk/RiskGauge';
|
|
import { DrawdownChart } from '../components/risk/DrawdownChart';
|
|
import { EmergencyControls } from '../components/risk/EmergencyControls';
|
|
import { useRiskMetrics } from '../hooks/useApi';
|
|
import { useRiskAlertsStream } from '../hooks/useGrpcStream';
|
|
import { RiskAlertSeverity } from '../gen/risk_pb';
|
|
|
|
export function RiskDashboard() {
|
|
const riskMetrics = useRiskMetrics();
|
|
const { messages: alerts } = useRiskAlertsStream();
|
|
|
|
const metrics = riskMetrics.data?.metrics;
|
|
|
|
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-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
|
|
<ComponentErrorBoundary name="VaR Gauge">
|
|
<RiskGauge
|
|
label="VaR Utilization"
|
|
value={metrics?.portfolioVar1d ?? 0}
|
|
/>
|
|
</ComponentErrorBoundary>
|
|
</div>
|
|
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
|
|
<ComponentErrorBoundary name="Volatility Gauge">
|
|
<RiskGauge
|
|
label="Volatility"
|
|
value={metrics?.volatility ?? 0}
|
|
/>
|
|
</ComponentErrorBoundary>
|
|
</div>
|
|
<div className="h-48 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
|
|
<ComponentErrorBoundary name="Drawdown Gauge">
|
|
<RiskGauge
|
|
label="Drawdown"
|
|
value={Math.abs(metrics?.currentDrawdown ?? 0)}
|
|
thresholds={{ warn: 5, danger: 10 }}
|
|
max={20}
|
|
/>
|
|
</ComponentErrorBoundary>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Middle: Drawdown chart */}
|
|
<div className="h-64 rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
|
|
<ComponentErrorBoundary name="Drawdown Chart">
|
|
<DrawdownChart />
|
|
</ComponentErrorBoundary>
|
|
</div>
|
|
|
|
{/* Bottom row: Alerts + Emergency Controls */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
|
|
<div className="px-3 py-1.5 border-b border-[var(--color-border)]">
|
|
<span className="text-sm font-medium">Risk Alerts</span>
|
|
</div>
|
|
<div className="max-h-64 overflow-auto">
|
|
{alerts.length === 0 ? (
|
|
<div className="p-4 text-sm text-[var(--color-text-secondary)] text-center">
|
|
No active alerts
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-[var(--color-border)]">
|
|
{alerts.slice(-20).reverse().map((alert, i) => {
|
|
const severityLabel =
|
|
alert.severity === RiskAlertSeverity.CRITICAL
|
|
? 'CRITICAL'
|
|
: alert.severity === RiskAlertSeverity.EMERGENCY
|
|
? 'EMERGENCY'
|
|
: alert.severity === RiskAlertSeverity.WARNING
|
|
? 'WARNING'
|
|
: 'INFO';
|
|
const isCritical =
|
|
alert.severity === RiskAlertSeverity.CRITICAL ||
|
|
alert.severity === RiskAlertSeverity.EMERGENCY;
|
|
return (
|
|
<div key={`alert-${alerts.length - i}`} className="px-3 py-2 text-xs">
|
|
<span
|
|
className={
|
|
isCritical
|
|
? 'text-[var(--color-red)]'
|
|
: 'text-[var(--color-yellow)]'
|
|
}
|
|
>
|
|
[{severityLabel}]
|
|
</span>{' '}
|
|
{alert.message}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded border border-[var(--color-border)] bg-[var(--color-bg-card)]">
|
|
<EmergencyControls />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|