import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useEffect, useMemo, useState } from 'react'; import { api, fmtAge, fmtBytes } from '../lib/api'; // "Which projects live where" — the human answer to what agents // have cached across the fleet. Reads /api/v2/projects (aggregated // from each daemon's ref-tracking → ref-store → blob-store chain). export function ProjectsPanel() { const [rows, setRows] = useState(null); const [err, setErr] = useState(null); const [tick, setTick] = useState(0); useEffect(() => { api .projects() .then((r) => { setRows(r); setErr(null); }) .catch((e) => setErr(String(e))); }, [tick]); useEffect(() => { const id = setInterval(() => setTick((t) => t + 1), 15_000); return () => clearInterval(id); }, []); // Group per-repo so a single project appearing on multiple // nodes surfaces as one card with a badge per node. const grouped = useMemo(() => { if (!rows) return null; const g = new Map(); for (const r of rows) { const arr = g.get(r.repo) ?? []; arr.push(r); g.set(r.repo, arr); } return [...g.entries()] .map(([repo, items]) => ({ repo, items: items.sort((a, b) => b.last_seen_unix - a.last_seen_unix), totalBytes: items.reduce((a, i) => a + i.cache_bytes, 0), latest: Math.max(...items.map((i) => i.last_seen_unix)), tier: hottestTier(items.map((i) => i.tier)), })) .sort((a, b) => b.latest - a.latest); }, [rows]); return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-baseline justify-between mb-3", children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100", children: "Projects" }), _jsx("span", { className: "text-xs text-slate-500", children: rows && `${rows.length} entries · ${grouped?.length ?? 0} repos` })] }), err && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm", children: err })), rows && rows.length === 0 && (_jsxs("div", { className: "rounded border border-slate-800 bg-slate-900/40 p-4 text-sm text-slate-400", children: ["No project activity tracked yet. Once ", _jsx("code", { className: "font-mono text-slate-300", children: "claw-cargo build" }), ' ', "runs with ", _jsx("code", { className: "font-mono", children: "--repo" }), " +", ' ', _jsx("code", { className: "font-mono", children: "--git-ref" }), " (or the equivalent", _jsx("code", { className: "font-mono", children: " CLAWSTOR_REPO" }), "/", _jsx("code", { className: "font-mono", children: "CLAWSTOR_GIT_REF" }), " env vars in CI), each cache-put annotates the producing repo and this pane fills in."] })), grouped && grouped.length > 0 && (_jsx("div", { className: "rounded border border-slate-800 overflow-hidden", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-slate-900/60 text-slate-500 uppercase text-xs tracking-wider", children: _jsxs("tr", { children: [_jsx("th", { className: "text-left px-4 py-2 font-normal", children: "tier" }), _jsx("th", { className: "text-left px-4 py-2 font-normal", children: "repo" }), _jsx("th", { className: "text-left px-4 py-2 font-normal", children: "nodes" }), _jsx("th", { className: "text-left px-4 py-2 font-normal", children: "cache size" }), _jsx("th", { className: "text-left px-4 py-2 font-normal", children: "refs" }), _jsx("th", { className: "text-left px-4 py-2 font-normal", children: "last activity" })] }) }), _jsx("tbody", { children: grouped.map((g) => (_jsxs("tr", { className: "border-t border-slate-900", children: [_jsx("td", { className: "px-4 py-2", children: _jsx(TierBadge, { tier: g.tier }) }), _jsx("td", { className: "px-4 py-2 font-mono text-sm text-emerald-300", children: g.repo }), _jsx("td", { className: "px-4 py-2 space-x-1", children: g.items.map((it) => (_jsx(NodePill, { node: it.node, bytes: it.cache_bytes }, it.node))) }), _jsx("td", { className: "px-4 py-2 font-mono text-xs", children: fmtBytes(g.totalBytes) }), _jsx("td", { className: "px-4 py-2 font-mono text-xs text-slate-400", children: [...new Set(g.items.flatMap((i) => i.refs))] .slice(0, 3) .join(', ') || '—' }), _jsx("td", { className: "px-4 py-2 text-slate-400", children: fmtAge(g.latest) })] }, g.repo))) })] }) }))] })); } function hottestTier(tiers) { if (tiers.includes('active')) return 'active'; if (tiers.includes('recent')) return 'recent'; return 'idle'; } function TierBadge({ tier }) { const cls = { active: 'bg-emerald-900/60 text-emerald-300 border-emerald-700', recent: 'bg-amber-900/60 text-amber-300 border-amber-700', idle: 'bg-slate-800 text-slate-400 border-slate-700', }[tier] ?? 'bg-slate-800 text-slate-400 border-slate-700'; return (_jsx("span", { className: `inline-block rounded border px-2 py-0.5 text-xs font-mono uppercase tracking-wider ${cls}`, children: tier })); } function NodePill({ node, bytes }) { return (_jsx("a", { href: `#/nodes/${node}`, className: "inline-block rounded bg-slate-800 text-emerald-300 text-xs font-mono px-2 py-0.5 hover:bg-slate-700", title: `${fmtBytes(bytes)} on ${node}`, children: node })); }