import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { PageHeader } from '../components/ui/PageHeader'; import { Callout } from '../components/ui/Callout'; import { Disclosure } from '../components/ui/Disclosure'; import { Badge } from '../components/ui/Badge'; import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton'; import { useAuthStore } from '../stores/authStore'; import { getRegimeMonitor, getRegimeConfig, updateRegimeConfig, getRegimeFundamentals, updateRegimeFundamentals, refreshRegimeFundamentals, } from '../api/regime'; import type { RegimeBand, RegimeMonitor, RegimeSignal, RegimeConfig, RegimeFundamentals, } from '../lib/types'; const BAND_STYLES: Record = { stable: { text: 'text-emerald-400', bar: 'bg-emerald-400', ring: 'border-emerald-400/30', label: 'Stable' }, watch: { text: 'text-amber-400', bar: 'bg-amber-400', ring: 'border-amber-400/30', label: 'Watch' }, elevated: { text: 'text-orange-400', bar: 'bg-orange-400', ring: 'border-orange-400/30', label: 'Elevated' }, breaking: { text: 'text-red-400', bar: 'bg-red-400', ring: 'border-red-400/30', label: 'Breaking' }, }; function TrendChip({ label, delta }: { label: string; delta: number | null | undefined }) { if (delta == null) { return {label}: n/a; } const rising = delta > 0; const flat = delta === 0; // Higher index = worse, so a rising score is the warning direction. const color = flat ? 'text-gray-400' : rising ? 'text-red-400' : 'text-emerald-400'; const arrow = flat ? '→' : rising ? '↑' : '↓'; return ( {label}: {arrow} {delta > 0 ? '+' : ''}{delta} ); } function Gauge({ data }: { data: RegimeMonitor }) { const band = (data.band ?? 'stable') as RegimeBand; const style = BAND_STYLES[band]; const score = data.total_score ?? 0; const threshold = data.alert_threshold ?? 65; const clamp = (v: number) => Math.min(100, Math.max(0, v)); return (
{Math.round(score)} / 100

{style.label}

{/* Band track with score + threshold markers */}
0306080100

An index (not a calibrated probability) of how far the AI/Tech bull regime has deteriorated. Mostly coincident signals — it shortens reaction time, it doesn't predict the exact turn. {data.date && <> As of {data.date}.} {data.inputs && (data.inputs.vix != null || data.inputs.hy_oas != null) && ( VIX {data.inputs.vix ?? '—'} · HY OAS {data.inputs.hy_oas ?? '—'} )}

); } function Breakdown({ breakdown }: { breakdown: RegimeSignal[] }) { return (
{breakdown.map((s) => ( ))}
Signal Sub-score Weight Contribution
{s.id}{' '} {s.label} {s.available && s.sub_score != null ? (
{s.sub_score}
) : ( n/a )}
{s.weight} {s.available ? s.contribution.toFixed(1) : '—'}
); } function SliderRow({ label, value, onChange }: { label: string; value: number; onChange: (v: number) => void }) { return ( ); } function FundamentalsEditor({ data, onSave, onRefresh, saving, refreshing, }: { data: RegimeFundamentals; onSave: (body: { f1_score?: number; f3_score?: number; locked?: boolean }) => void; onRefresh: () => void; saving: boolean; refreshing: boolean; }) { const [f1, setF1] = useState(Math.round(data.f1_score)); const [f3, setF3] = useState(Math.round(data.f3_score)); return (
Source: {data.source} {data.fetched_at && · {new Date(data.fetched_at).toLocaleDateString()}} {data.locked && }
{data.reasoning &&

{data.reasoning}

}
{data.locked && ( )}
); } function WeightsEditor({ data, onSave, saving, }: { data: RegimeConfig; onSave: (updates: Partial) => void; saving: boolean; }) { const [weights, setWeights] = useState>(() => ({ ...data.weights })); const [threshold, setThreshold] = useState(data.alert_threshold); const setWeight = (key: string, value: string) => { const num = parseFloat(value); setWeights((prev) => ({ ...prev, [key]: isNaN(num) ? 0 : num })); }; return (
{Object.keys(weights).map((key) => ( ))}
); } function AdminControls() { const qc = useQueryClient(); const fundamentals = useQuery({ queryKey: ['regime', 'fundamentals'], queryFn: getRegimeFundamentals }); const config = useQuery({ queryKey: ['regime', 'config'], queryFn: getRegimeConfig }); const invalidate = () => qc.invalidateQueries({ queryKey: ['regime'] }); const refresh = useMutation({ mutationFn: refreshRegimeFundamentals, onSuccess: invalidate }); const saveFund = useMutation({ mutationFn: updateRegimeFundamentals, onSuccess: invalidate }); const saveConfig = useMutation({ mutationFn: updateRegimeConfig, onSuccess: invalidate }); return (
{fundamentals.isLoading && } {fundamentals.data && ( saveFund.mutate(body)} onRefresh={() => refresh.mutate()} saving={saveFund.isPending} refreshing={refresh.isPending} /> )} {refresh.isError && ( Refresh failed: {(refresh.error as Error).message} )} {config.isLoading && } {config.data && ( saveConfig.mutate(updates)} saving={saveConfig.isPending} /> )}
); } export default function RegimePage() { const role = useAuthStore((s) => s.role); const isAdmin = role === 'admin'; const monitor = useQuery({ queryKey: ['regime', 'monitor'], queryFn: getRegimeMonitor }); return (
{monitor.isLoading && ( <> )} {monitor.isError && ( monitor.refetch()}> Failed to load: {(monitor.error as Error).message} )} {monitor.data && !monitor.data.available && ( Not computed yet — run the “Regime Monitor” job from Admin → Jobs, or wait for the daily pipeline. )} {monitor.data && monitor.data.available && ( <> {monitor.data.breakdown && } )} {isAdmin && }
); }