import { db } from "@/db/client"; import { storageAvailability, storageFiles, storageTiers } from "@/db/schema"; import { AGENT_TIMEOUT_SECONDS, formatSince, getAgentStatuses } from "@/lib/agents"; import { desc, sql } from "drizzle-orm"; export const dynamic = "force-dynamic"; function formatBytes(value: number | null) { if (!value || value <= 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let size = value; let unit = 0; while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; } return size.toFixed(unit === 0 ? 0 : 1) + " " + units[unit]; } type AgentPath = { path: string; exists: boolean; mounted: boolean; readable: boolean; freeBytes: number }; function agentPaths(details: unknown): AgentPath[] { const paths = (details as { paths?: unknown } | null)?.paths; return Array.isArray(paths) ? (paths as AgentPath[]) : []; } function formatDate(value: Date | null) { return value ? value.toISOString().slice(0, 16).replace("T", " ") : "Open"; } export default async function AdminStoragePage() { const agents = await getAgentStatuses(); const agentByName = new Map(agents.map((agent) => [agent.agent, agent])); const [tiers, availabilityRows, inventoryRows] = await Promise.all([ db.select().from(storageTiers).orderBy(storageTiers.tier), db.select().from(storageAvailability).orderBy(desc(storageAvailability.detectedAt)).limit(100), db .select({ tierId: storageFiles.tierId, fileCount: sql`count(*)::text`, movieFileCount: sql`count(*) filter (where ${storageFiles.relativePath} like 'Movies/%' or ${storageFiles.relativePath} like 'Movies-EN/%')::text`, tvFileCount: sql`count(*) filter (where ${storageFiles.relativePath} like 'Television/%')::text`, totalSize: sql`pg_size_pretty(coalesce(sum(${storageFiles.sizeBytes}), 0))`, movieSize: sql`pg_size_pretty(coalesce(sum(${storageFiles.sizeBytes}) filter (where ${storageFiles.relativePath} like 'Movies/%' or ${storageFiles.relativePath} like 'Movies-EN/%'), 0))`, tvSize: sql`pg_size_pretty(coalesce(sum(${storageFiles.sizeBytes}) filter (where ${storageFiles.relativePath} like 'Television/%'), 0))`, }) .from(storageFiles) .groupBy(storageFiles.tierId), ]); const availabilityByTier = new Map(); for (const row of availabilityRows) { availabilityByTier.set(row.tierId, [...(availabilityByTier.get(row.tierId) ?? []), row]); } const inventoryByTier = new Map(inventoryRows.map((row) => [row.tierId, row])); return (

Storage

Storage Tiers

Agents

Considered offline after {AGENT_TIMEOUT_SECONDS}s of silence

{agents.length ? (
{agents.map((agent) => { const paths = agentPaths(agent.details); return (

{agent.agent}

{agent.hostname ?? "unknown host"}

{agent.online ? "Online" : "Offline"}

Last beat {formatSince(agent.secondsSinceSeen)}

{paths.length ? (
    {paths.map((path) => (
  • {path.path} {path.readable ? formatBytes(path.freeBytes) + " free" : "unreachable"}
  • ))}
) : null}
); })}
) : (

No agents have reported yet. See agent/README.md to install the herald on silenus or edda.

)}
{tiers.length ? tiers.map((tier) => { const events = availabilityByTier.get(tier.id) ?? []; const latest = events[0]; // When a tier names an agent, that agent's heartbeat is authoritative: // storage on a powered-off host is not reachable no matter what the // last scan observed. const agent = tier.agentName ? agentByName.get(tier.agentName) : undefined; const online = tier.agentName ? Boolean(agent?.online) : tier.alwaysOnline || latest?.online; const inventory = inventoryByTier.get(tier.id); return (

{tier.tier}

{tier.label}

{online ? "Online" : "Offline"}
{tier.agentName ? (

Reachability from agent {tier.agentName} {agent ? ` — last beat ${formatSince(agent.secondsSinceSeen)}` : " — never reported"}

) : null}

{tier.basePath}

{tier.notes ?

{tier.notes}

: null}

Movie files

{inventory?.movieFileCount ?? "0"}

Movie size

{inventory?.movieSize ?? "0 bytes"}

TV files

{inventory?.tvFileCount ?? "0"}

TV size

{inventory?.tvSize ?? "0 bytes"}

All files

{inventory?.fileCount ?? "0"}

All size

{inventory?.totalSize ?? "0 bytes"}

Recent availability

{events.length ? events.slice(0, 5).map((event) => (
{event.online ? "Online" : "Offline"} {formatDate(event.detectedAt)}

Resolved: {formatDate(event.resolvedAt)}

)) :

No availability observations yet.

}
); }) :
No storage tiers configured yet.
}
); }