import { useEffect, useState } from 'react'; import { getJSON } from '../lib/api'; interface HealthStatus { status: string; redis: boolean; timestamp: string; } interface FileStats { count: number; totalSize: number; } interface Stats { records: number; redisKeys: number; redisMemory: string; identities: number; files: FileStats | null; udlUsers: number | null; sweptAt: number; sweeping: boolean; } interface WatcherStatus { id: string; path: string; intervalMs: number; enabled: boolean; active: boolean; lastScan: number; fileCount: number; isScanning: boolean; } interface WatchersResponse { watchers: WatcherStatus[]; count: number; } const cardStyle: React.CSSProperties = { background: '#16213e', borderRadius: '8px', padding: '1.5rem', marginBottom: '1rem', }; const gridStyle: React.CSSProperties = { display: 'grid', // 200px, not 220: five tiles must fit one row inside the 1200px container // (1136 content - 4x16 gap = 1072 / 5 = 214 each) before wrapping. gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginBottom: '1rem', }; const statStyle: React.CSSProperties = { fontSize: '2rem', fontWeight: 'bold', color: '#4ade80', lineHeight: 1.2, }; const labelStyle: React.CSSProperties = { marginBottom: '0.75rem', color: '#888', fontSize: '0.85rem', textTransform: 'uppercase', letterSpacing: '0.04em', }; const noteStyle: React.CSSProperties = { color: '#6b7a99', fontSize: '0.8rem', marginTop: '0.4rem', }; const numberFmt = new Intl.NumberFormat(); function formatCount(n: number | null | undefined): string { return n === null || n === undefined ? '—' : numberFmt.format(n); } // Binary units: this counts bytes on disk, and every other tool the operator // compares against (du, docker, the mount page) reports GiB. function formatBytes(bytes: number | null | undefined): string { if (bytes === null || bytes === undefined) return '—'; const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; let value = bytes; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit++; } return `${value.toFixed(value >= 100 || unit === 0 ? 0 : 1)} ${units[unit]}`; } function Tile({ label, value, note, pending, }: { label: string; value: string; note?: string; pending?: boolean; }) { return (
{label}
{value}
{note &&
{note}
}
); } export default function Dashboard() { const [health, setHealth] = useState(null); const [stats, setStats] = useState(null); const [watchersData, setWatchersData] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; const fetchData = async () => { // Settled, not all: one dead endpoint must not blank every tile. const [healthRes, statsRes, watchersRes] = await Promise.allSettled([ getJSON('/health'), getJSON('/api/stats'), getJSON('/api/watchers'), ]); if (cancelled) return; if (healthRes.status === 'fulfilled') setHealth(healthRes.value); if (statsRes.status === 'fulfilled') setStats(statsRes.value); if (watchersRes.status === 'fulfilled') setWatchersData(watchersRes.value); const failures = [healthRes, statsRes, watchersRes] .filter((r): r is PromiseRejectedResult => r.status === 'rejected') .map((r) => String(r.reason?.message ?? r.reason)); setError(failures.length ? failures.join(' · ') : null); }; fetchData(); const interval = setInterval(fetchData, 5000); return () => { cancelled = true; clearInterval(interval); }; }, []); const handleTriggerScan = async () => { try { await fetch('/api/watchers/scan-all', { method: 'POST' }); } catch (err) { console.error('Failed to trigger scan:', err); } }; const isScanning = watchersData?.watchers.some((w) => w.isScanning) || false; const activeWatchers = watchersData?.watchers.filter((w) => w.active).length || 0; const lastScan = watchersData?.watchers.reduce((max, w) => Math.max(max, w.lastScan || 0), 0) || 0; // Records we hold metadata for but have no local file: gateway/cid-rooted // entries, plus anything whose file went away. The interesting number on a // client box, where it should be most of them. const fileless = stats && stats.files ? Math.max(0, stats.records - stats.files.count) : null; const sweepPending = !!stats && !stats.files; const sweptNote = (() => { if (!stats) return undefined; if (sweepPending) return stats.sweeping ? 'scanning…' : 'not scanned yet'; if (stats.sweeping) return 'refreshing…'; if (stats.sweptAt) return `as of ${new Date(stats.sweptAt).toLocaleTimeString()}`; return undefined; })(); return (

meta-core Dashboard

{error && (
{error}
)}
Service health
{health ? ( <>
{health.status === 'ok' ? 'Healthy' : health.status}

Metadata store: {health.redis ? 'Open' : 'Unavailable'}

) : (

Loading…

)}
File watchers
{watchersData ? ( <>
{activeWatchers} / {watchersData.count}

active watchers

Status: {isScanning ? 'Scanning…' : 'Idle'}

{lastScan > 0 && (

Last scan: {new Date(lastScan).toLocaleString()}

)} ) : (

Loading…

)}
); }