Files
clawstor/dashboard-v2/src/lib/api.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

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