/** * dashboard-client/src/tabs/OverviewTab.tsx — Overview tab (full spec). * * Renders all 11 cards from the spec: * 1. Context Window (ContextGauge) * 2. Trigger Status (TriggerStatus) * 3. Vector Store (session) (VectorStoreCard) * 4. Repo (all sessions) (RepoAllSessionsCard) * 5. Data Safety (DataSafetyCard) * 6. Configuration (ConfigSummaryCard) * 7. Model & Cost Savings (CacheStatusPerModel) * 8. Crew / Agents (SessionInfo) * 9. Cache Hits & Compactions (CacheHitsCard) * 10. Time Saved (TimeSavedCard) * 11. Legend (LegendCard) * * All data comes from the snapshot prop passed by App.tsx. */ import type React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useApi } from "../hooks/useApi"; import { useSSE } from "../hooks/useSSE"; import { fetchSessions, fetchPerfSamples } from "../api/client"; import type { SnapshotResponse, SseEvent, SseSessionSample, SessionsResponse, } from "@contracts"; import type { RuntimeSnapshot } from "../utils/types"; import { RepoContextStack } from "../components/RepoContextStack"; import { SessionContextGauges } from "../components/SessionContextGauges"; import { TriggerStatus } from "../components/TriggerStatus"; import { VectorStoreCard } from "../components/VectorStoreCard"; import { RepoAllSessionsCard } from "../components/RepoAllSessionsCard"; import { DataSafetyCard } from "../components/DataSafetyCard"; import { ConfigSummaryCard } from "../components/ConfigSummaryCard"; import { CacheStatusPerModel } from "../components/CacheStatusPerModel"; import { SessionInfo } from "../components/SessionInfo"; import { CacheHitsCard } from "../components/CacheHitsCard"; import { TimeSavedCard } from "../components/TimeSavedCard"; import { LegendCard } from "../components/LegendCard"; import { Badge } from "../components/ui/badge"; import { NEW_UI } from "../config"; import RagHealthCard from "./SetupTab/RagHealthCard"; import { WidgetDetailModal } from "../components/WidgetDetailModal"; import { PerfLineChart } from "../components/charts/PerfLineChart"; import { PerfBarChart } from "../components/charts/PerfBarChart"; import { DndContext, type DragEndEvent } from "@dnd-kit/core"; import { SortableContext } from "@dnd-kit/sortable"; import type { CardId } from "../types/card"; import { useCardPositions } from "../hooks/useCardPositions"; import { SortableCard } from "../components/ui/SortableCard"; /** Type guard: narrows an SSE event to a session_sample event. */ function isSessionSample(e: SseEvent): e is SseSessionSample { return e.type === "session_sample"; } /** * Maps a widget id to its perf sample kind (null for snapshot-backed widgets, * e.g. "model" which derives its drill-down from snapshot data in-component). */ function perfKindFor(widget: string): string | null { switch (widget) { case "cache": return "cache_hit_pct"; case "vector": return "turn_latency_ms"; case "time": return "disk_write_ms"; default: return null; } } export interface OverviewTabProps { snapshot: SnapshotResponse | null; loading: boolean; error: Error | null; } function formatUpdatedAt(ts: string | null): string { if (!ts) return "never"; try { return new Date(ts).toLocaleString(); } catch { return ts; } } export default function OverviewTab({ snapshot, loading, error, }: OverviewTabProps): React.ReactElement { // S40: real-time per-repo context via /api/sessions + SSE. Mirrors the // SessionsTab pattern: 2s poll + session_sample SSE refetch. const { data: sessionsData, error: sessionsErr, loading: sessionsLoading, refetch: refetchSessions, } = useApi( useCallback(() => fetchSessions(), []), { pollInterval: 2_000 }, ); const { events } = useSSE(); const sampleEvents = useMemo(() => events.filter(isSessionSample), [events]); const lastSampleTs = useMemo(() => { if (sampleEvents.length === 0) return null; return sampleEvents[sampleEvents.length - 1].ts; }, [sampleEvents]); useEffect(() => { if (lastSampleTs == null) return; refetchSessions(); }, [lastSampleTs, refetchSessions]); // ── Widget drill-down state ───────────────────────────────────────────── // which widget's modal is open (null = none) const [openWidget, setOpenWidget] = useState(null); // perf samples fetched for the opened perf-backed widget const [perfData, setPerfData] = useState< Array<{ ts: number; value: number }> >([]); const [perfLoading, setPerfLoading] = useState(false); const [perfError, setPerfError] = useState(null); useEffect(() => { setPerfData([]); setPerfError(null); const kind = openWidget ? perfKindFor(openWidget) : null; if (kind === null) { setPerfLoading(false); return; } let cancelled = false; setPerfLoading(true); fetchPerfSamples(kind, 60) .then((res) => { if (!cancelled) { setPerfData(res.samples.map((s) => ({ ts: s.ts, value: s.value }))); } }) .catch((err) => { if (!cancelled) { setPerfError(err instanceof Error ? err : new Error(String(err))); } }) .finally(() => { if (!cancelled) setPerfLoading(false); }); return () => { cancelled = true; }; }, [openWidget]); // Drag-and-drop card reordering, persisted to localStorage. const { order, moveCard } = useCardPositions(); const onDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { moveCard(active.id as CardId, over.id as CardId); } }, [moveCard], ); // Per-model cost breakdown for the Model & Cost drill-down, derived from // snapshot data already in the component (no snapshot.byModel exists). // MUST be called before the early returns below — Rules of Hooks: hooks // cannot be after a conditional return (would cause React error #310 // "Rendered more hooks than during the previous render" when the snapshot // transitions from null → loaded). Guard the null case inline. const modelBarData = useMemo(() => { const ds = snapshot as RuntimeSnapshot | null; const compression = ds?.compression; const model = ds?.model; return [ { label: "Tokens freed", value: compression?.repo.tokensFreed ?? 0 }, { label: "Input rate", value: model ? model.inputRate : 0 }, { label: "Output rate", value: model ? model.outputRate : 0 }, ]; }, [snapshot]); if (loading && !snapshot) return
Loading snapshot…
; if (error && !snapshot) return (

Error loading snapshot: {error.message}

Auto-retried with backoff and will re-poll every 5s — no action needed.

); if (!snapshot) return
No snapshot data.
; // Cast to RuntimeSnapshot for tierPct (runtime-only field). const d = snapshot as RuntimeSnapshot; const { context, compression, trigger, session, store, tier, updatedAt } = d; const cfg = d.config; const crew = d.crew; const integrity = d.integrity; const cacheHits = d.cacheHits; const compacts = d.compacts; const timeSaved = d.timeSaved; const repo = d.repo; const model = d.model; // Renders a single card (wrapped in SortableCard) by its CardId. Cards // with a drill-down get an onClick; the rest are plain sortable items. const renderCard = (cardId: CardId): React.ReactNode => { switch (cardId) { case "trigger": return ( setOpenWidget("cache")}> ); case "vector": return ( setOpenWidget("vector")}> ); case "repo-all": return ( ); case "data-safety": return ( ); case "config": return ( ); case "model": return model ? ( setOpenWidget("model")}> ) : null; case "crew": return ( ); case "cache-hits": return ( setOpenWidget("cache")}> ); case "time-saved": return ( setOpenWidget("time")}> ); case "rag-health": return NEW_UI() ? ( ) : null; case "legend": return ( ); } }; return (
{tier} updated {formatUpdatedAt(updatedAt)}
{/* Full-width live-context row: per-session gauges + per-repo stack * sit above the grid so they get the whole width of the tab. */}
{order .map((cardId) => ({ cardId, node: renderCard(cardId) })) .filter(({ node }) => node != null) .map(({ cardId, node }) => (
{node}
))}
{/* ── Drill-down modals ───────────────────────────────────────── */} setOpenWidget(null)} > {perfLoading ? (

Loading…

) : perfError ? (

{perfError.message}

) : ( )}
setOpenWidget(null)} > {perfLoading ? (

Loading…

) : perfError ? (

{perfError.message}

) : ( )}
setOpenWidget(null)} > {perfLoading ? (

Loading…

) : perfError ? (

{perfError.message}

) : ( )}
setOpenWidget(null)} > v.toLocaleString()} />
); }