Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 34s
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.
60 lines
2.3 KiB
JavaScript
60 lines
2.3 KiB
JavaScript
// Thin fetch wrappers around /api/v2/*. Kept minimal — no state
|
|
// library; each page owns its own useEffect + useState.
|
|
// API base:
|
|
// * dev (vite proxy) → '/api'
|
|
// * prod local → '/api' (SPA at :7700/v2/, API at :7700/api/)
|
|
// * prod via Tailscale → '/clawstor/api' (SPA at /clawstor, API at /clawstor/api)
|
|
//
|
|
// Detect at load time by checking the current URL's pathname
|
|
// prefix. Cheap + no build-time coupling.
|
|
const API_BASE = (() => {
|
|
if (typeof window === 'undefined')
|
|
return '/api';
|
|
const p = window.location.pathname;
|
|
if (p.startsWith('/clawstor/') || p === '/clawstor')
|
|
return '/clawstor/api';
|
|
return '/api';
|
|
})();
|
|
async function get(path) {
|
|
const full = `${API_BASE}${path}`;
|
|
const resp = await fetch(full, { headers: { Accept: 'application/json' } });
|
|
if (!resp.ok) {
|
|
throw new Error(`${full} → ${resp.status} ${resp.statusText}`);
|
|
}
|
|
return resp.json();
|
|
}
|
|
export const api = {
|
|
fleet: () => get('/v2/fleet'),
|
|
projects: () => get('/v2/projects'),
|
|
nodeStatus: (name) => get(`/v2/node/${name}/status`),
|
|
blobs: (limit = 200, offset = 0) => get(`/v2/storage/blobs?limit=${limit}&offset=${offset}`),
|
|
tags: (prefix = '') => get(`/v2/storage/tags?prefix=${encodeURIComponent(prefix)}`),
|
|
refs: (limit = 200, offset = 0) => get(`/v2/storage/refs?limit=${limit}&offset=${offset}`),
|
|
snapshots: () => get('/v2/storage/snapshots'),
|
|
refTracking: (repo = '') => get(`/v2/storage/ref-tracking?repo=${encodeURIComponent(repo)}`),
|
|
};
|
|
/** Format bytes as MB / GB / TB as needed. */
|
|
export function fmtBytes(n) {
|
|
if (n < 1024)
|
|
return `${n} B`;
|
|
if (n < 1024 * 1024)
|
|
return `${(n / 1024).toFixed(1)} KiB`;
|
|
if (n < 1024 * 1024 * 1024)
|
|
return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
|
|
if (n < 1024 * 1024 * 1024 * 1024)
|
|
return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
|
return `${(n / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TiB`;
|
|
}
|
|
/** Human-friendly relative time. */
|
|
export function fmtAge(unix) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const diff = now - unix;
|
|
if (diff < 60)
|
|
return `${diff}s ago`;
|
|
if (diff < 3600)
|
|
return `${Math.floor(diff / 60)}m ago`;
|
|
if (diff < 86400)
|
|
return `${Math.floor(diff / 3600)}h ago`;
|
|
return `${Math.floor(diff / 86400)}d ago`;
|
|
}
|