import { useMemo } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useQueries } from "@tanstack/react-query"; import { Cell, Pie, PieChart, ResponsiveContainer, Bar, BarChart, XAxis, YAxis, Tooltip, } from "recharts"; import { Clock, GitBranch, Timer } from "lucide-react"; import { getStatsObjectTypes, getStatsReasons, getStatsValidation, listRepos, } from "../api/service"; import type { RepositoryRecord } from "../api/schemas"; import { DataTable, type Column } from "../components/DataTable"; import { KpiCard } from "../components/KpiCard"; import { PageHeader } from "../components/PageHeader"; import { Panel } from "../components/Panel"; import { StatusPill } from "../components/StatusPill"; import { ErrorBlock, LoadingBlock } from "../components/StateBlock"; import { formatDurationMs, formatInt, formatPercent, formatRelative, formatUtc, objectTypeLabel, truncateMiddle, } from "../lib/format"; import { withRunParam } from "../lib/run"; import { useRun, runErrorTitle } from "../lib/useRun"; /** Semantic colors keyed by validation result — never assigned by index. */ const RESULT_COLORS: Record = { ok: "#16a34a", valid: "#16a34a", warnings: "#d97706", warning: "#d97706", error: "#dc2626", invalid: "#dc2626", rejected: "#dc2626", skipped: "#94a3b8", unknown: "#94a3b8", }; const FALLBACK_COLORS = ["#2563eb", "#0ea5e9", "#6366f1", "#8b5cf6", "#64748b"]; const TYPE_PALETTE = ["#1d4ed8", "#2563eb", "#3b82f6", "#60a5fa", "#93c5fd", "#0ea5e9", "#38bdf8", "#7dd3fc"]; function colorForResult(name: string, index: number): string { return RESULT_COLORS[name.toLowerCase()] ?? FALLBACK_COLORS[index % FALLBACK_COLORS.length]; } export default function OverviewPage() { const { runId, runQuery } = useRun(); const navigate = useNavigate(); const [validationQuery, typesQuery, reasonsQuery, reposQuery] = useQueries({ queries: [ { queryKey: ["stats", runId, "validation"], queryFn: () => getStatsValidation(runId), staleTime: 60_000, }, { queryKey: ["stats", runId, "object-types"], queryFn: () => getStatsObjectTypes(runId), staleTime: 60_000, }, { queryKey: ["stats", runId, "reasons"], queryFn: () => getStatsReasons(runId), staleTime: 60_000, }, { queryKey: ["repos", runId, "top"], queryFn: () => listRepos(runId, { limit: 8 }), staleTime: 60_000, }, ], }); const run = runQuery.data; const counts = run?.counts; const validationData = useMemo(() => { const map = validationQuery.data ?? {}; return Object.entries(map) .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value); }, [validationQuery.data]); const validationTotal = useMemo( () => validationData.reduce((sum, entry) => sum + entry.value, 0), [validationData], ); const typeData = useMemo(() => { const map = typesQuery.data ?? {}; return Object.entries(map) .map(([name, value]) => ({ name: objectTypeLabel(name), value })) .sort((a, b) => b.value - a.value) .slice(0, 8); }, [typesQuery.data]); const reasonRows = useMemo(() => { const map = reasonsQuery.data ?? {}; return Object.entries(map) .map(([reason, count]) => ({ reason, count })) .sort((a, b) => b.count - a.count) .slice(0, 8); }, [reasonsQuery.data]); const maxReason = reasonRows[0]?.count ?? 0; if (runQuery.isError) { return (
runQuery.refetch()} title={runErrorTitle(runId, runQuery.error)} />
); } const repoColumns: Column[] = [ { key: "host", header: "Repository", render: (repo) => (
{repo.host}
{truncateMiddle(repo.uri, 56)}
), }, { key: "transport", header: "Transport", render: (repo) => {repo.transport ?? "unknown"}, }, { key: "objects", header: "Objects", numeric: true, render: (repo) => formatInt(repo.objects) }, { key: "rejected", header: "Rejected", numeric: true, render: (repo) => repo.rejectedObjects ? ( {formatInt(repo.rejectedObjects)} ) : ( "0" ), }, { key: "duration", header: "Sync time", numeric: true, render: (repo) => formatDurationMs(repo.syncDurationMsTotal), }, { key: "terminal", header: "Terminal states", render: (repo) => ( {Object.entries(repo.terminalStates ?? {}).map(([state, count]) => ( ))} ), }, ]; return (
{run.runId} {run.syncMode ? ( ) : null} {run.indexStatus ? : null} ) : undefined } /> {runQuery.isPending ? ( ) : ( <>
{validationQuery.isError ? ( validationQuery.refetch()} /> ) : validationQuery.isPending ? ( ) : validationData.length === 0 ? (

No validation stats recorded.

) : ( <>
{validationData.map((entry, index) => ( ))} [ `${formatInt(Number(value))} (${formatPercent(Number(value), validationTotal)})`, String(name), ]} />
{validationData.map((entry, index) => ( {entry.name} · {formatInt(entry.value)} ({formatPercent(entry.value, validationTotal)}) ))}
)}
{typesQuery.isError ? ( typesQuery.refetch()} /> ) : typesQuery.isPending ? ( ) : typeData.length === 0 ? (

No object type stats recorded.

) : (
formatInt(Number(value))} /> {typeData.map((entry, index) => ( ))}
)}
View all } flush > {reposQuery.isError ? (
reposQuery.refetch()} />
) : ( repo.repoId} loading={reposQuery.isPending} emptyTitle="No repositories indexed" onRowClick={(repo) => navigate(withRunParam(`/repositories/${encodeURIComponent(repo.repoId)}`, runId)) } /> )}
Validation } > {reasonsQuery.isError ? ( reasonsQuery.refetch()} /> ) : reasonsQuery.isPending ? ( ) : reasonRows.length === 0 ? (

No rejected objects in this run.

) : (
{reasonRows.map((row) => ( {row.reason} {formatInt(row.count)}
)}
)}
); }