Files
clawstor/dashboard-v2/src/components/ProjectsPanel.js
T
Omar Sobh f31c94607a
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 34s
FleetHealth PR 3: 'Projects' panel — which repos live where
Backend:
* New DashboardProject { repo, cache_bytes, fingerprint_count,
  refs, first_seen_unix, last_seen_unix, tier }.
* Handler build_projects() joins ref-tracking with the ref-store
  and blob-store: for each recorded fp, resolve fp → blob-id via
  RefStore::{get_stamped, get} then sum blob sizes per repo.
* Tier is a wall-clock function of last_seen_unix:
    active < 24h, recent < 7d, else idle.
* DashboardStorageReply gains `projects: Vec<DashboardProject>`
  (serde-default so older clients still parse).

Aggregator:
* New /api/v2/projects endpoint. Fans out DashboardStorage to
  every peer, tags each project row with its originating node,
  returns hottest-first.

Frontend:
* New ProjectsPanel component appended to the FleetHealth
  landing. Groups by repo, one row per project with tier badge
  (active/recent/idle), per-node pill badges, cache-size sum,
  ref list, last-activity age.
* Empty state explains how to populate: claw-cargo build with
  --repo + --git-ref (or CLAWSTOR_REPO/CLAWSTOR_GIT_REF env
  vars in CI).

Data populates automatically as each cache-put runs. Existing
demo entry on tank (clawverse/clawstor · main · c384a4...) will
surface after redeploy.
2026-07-14 19:06:40 -07:00

67 lines
5.3 KiB
JavaScript

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 }));
}