"use client"; import Link from "next/link"; import { useMemo } from "react"; import { RefreshCw } from "lucide-react"; import { api, type StorageOut } from "@/lib/api"; import { useAsync } from "@/lib/hooks"; import { useAuth } from "@/lib/auth"; import { fmtBytes, fmtCompact, fmtGB, fmtInt, fmtRelative, fmtTs } from "@/lib/format"; import { Banner, Card, Empty, ErrorState, KV, PageHeader, Skeleton, StatusBadge } from "@/components/ui"; import { DataTable } from "@/components/DataTable"; import { Meter, SegmentedBar } from "@/components/charts"; import { JsonView } from "@/components/JsonView"; export default function StoragePage() { const { isOwner } = useAuth(); const { data, error, loading, reload } = useAsync((s) => api.get("/admin/storage", undefined, s), [], { enabled: isOwner }); const perDataset = useMemo(() => { const m = new Map(); for (const r of data?.per_dataset ?? []) { const e = m.get(r.dataset_id) ?? { dataset_id: r.dataset_id, base_files: 0, base_bytes: 0, base_rows: 0, delta_files: 0, delta_bytes: 0, delta_rows: 0, other_files: 0, other_bytes: 0 }; const b = Number(r.bytes ?? 0), rows = Number(r.rows ?? 0), f = Number(r.files ?? 0); if (r.layout === "base") { e.base_files += f; e.base_bytes += b; e.base_rows += rows; } else if (r.layout === "delta") { e.delta_files += f; e.delta_bytes += b; e.delta_rows += rows; } else { e.other_files += f; e.other_bytes += b; } m.set(r.dataset_id, e); } return Array.from(m.values()).sort((a, b) => b.base_bytes + b.delta_bytes - (a.base_bytes + a.delta_bytes)); }, [data]); if (!isOwner) return Storage details require the owner role.; const vol = data?.volume; const used = vol ? vol.total_gb - vol.free_gb : 0; const freeTone = vol && data ? (vol.free_gb < data.budget.hard_min_free_gb ? "crit" : vol.free_gb < data.budget.hard_min_free_gb * 2 ? "warn" : "accent") : "accent"; const curatedGb = (data?.curated_bytes ?? 0) / 1e9; const bytesPerBar = useMemo(() => { const rows = perDataset.reduce((a, d) => a + d.base_rows + d.delta_rows, 0); const bytes = perDataset.reduce((a, d) => a + d.base_bytes + d.delta_bytes, 0); return rows > 0 ? bytes / rows : null; }, [perDataset]); return ( <> Lake root {data.lake_root} : "Lake volume, budget and footprint"} actions={ } /> {error && } {loading && !data && } {data && vol && ( <>
{fmtGB(vol.free_gb, 0)} free of {fmtGB(vol.total_gb, 0)}
], ]} />
{fmtGB(curatedGb, 1)} soft limit {data.budget.soft_limit_gb} GB
data.budget.soft_limit_gb ? "warn" : "accent"} label="Curated bytes vs budget" marks={[{ at: data.budget.soft_limit_gb, label: `budget ${data.budget.soft_limit_gb} GB` }]} className="mb-8 mt-2" />
r.dataset_id} mobile="scroll" columns={[ { key: "dataset_id", header: "Dataset", render: (r) => {r.dataset_id} }, { key: "base_bytes", header: "Base bytes", num: true, render: (r) => fmtBytes(r.base_bytes) }, { key: "base_rows", header: "Base rows", num: true, render: (r) => {fmtCompact(r.base_rows)} }, { key: "base_files", header: "Base files", num: true, hideSm: true, render: (r) => fmtInt(r.base_files) }, { key: "delta_bytes", header: "Delta bytes", num: true, render: (r) => fmtBytes(r.delta_bytes) }, { key: "delta_rows", header: "Delta rows", num: true, render: (r) => {fmtCompact(r.delta_rows)} }, { key: "delta_files", header: "Delta files", num: true, hideSm: true, render: (r) => fmtInt(r.delta_files) }, { key: "other", header: "Other", num: true, hideSm: true, render: (r) => (r.other_files ? `${fmtInt(r.other_files)} · ${fmtBytes(r.other_bytes)}` : "—") }, { key: "ratio", header: "Delta share", num: true, render: (r) => { const t = r.base_bytes + r.delta_bytes; return t ? `${((r.delta_bytes / t) * 100).toFixed(0)} %` : "—"; } }, ]} empty="No lake files registered." />

A high delta share means a compaction job would help (merges deltas into base files and publishes a new version).

r.status} mobile="scroll" columns={[{ key: "status", header: "Status", render: (r) => }, { key: "n", header: "Archives", num: true, render: (r) => fmtInt(r.n) }, { key: "bytes", header: "Bytes", num: true, render: (r) => fmtBytes(r.bytes) }]} empty="No raw archives." /> r.status} mobile="scroll" columns={[{ key: "status", header: "Status", render: (r) => }, { key: "n", header: "Count", num: true, render: (r) => fmtInt(r.n) }, { key: "bytes", header: "Bytes", num: true, render: (r) => fmtBytes(r.bytes) }]} empty="No exports." />
Retired files: {fmtInt(data.retired_files.n)} · {fmtBytes(data.retired_files.bytes)} (reclaimed by gc)
: "—"], ]} /> {data.last_backup &&
} {!data.last_backup && Enqueue a backup job from Ingestion.}

Reference model: {data.capacity_model.bytes_per_1m_bar_curated} bytes per curated 1-minute bar. {bytesPerBar !== null && ( <> {" "}Measured now: {bytesPerBar.toFixed(1)} bytes/row across all datasets. At this density the remaining budget ({fmtGB(Math.max(0, data.budget.soft_limit_gb - curatedGb), 0)}) holds about{" "} {fmtCompact(Math.max(0, (data.budget.soft_limit_gb - curatedGb) * 1e9) / bytesPerBar)} more bars. )}

{data.capacity_model.note}

)} ); }