import { useQuery } from "@tanstack/react-query"; import { Activity, Download, Gauge, HardDrive, MemoryStick, ScanSearch, Terminal } from "lucide-react"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { EmptyState, Spinner } from "@/components/ui"; import { api } from "@/lib/api"; import { cn } from "@/lib/cn"; import { formatBytes } from "@/lib/format"; import type { StatJob, SystemStats } from "@/lib/types"; import { useAuth } from "@/store/auth"; const REFRESH_MS = 2000; /** DESIGN.md capacity bars: blue, peach at 80%, red at 95%. */ function meterColor(percent: number): string { if (percent >= 95) return "bg-ctp-red"; if (percent >= 80) return "bg-ctp-peach"; return "bg-ctp-blue"; } function Meter({ percent, className }: { percent: number; className?: string }) { return (
); } function Panel({ title, icon, right, children, className, }: { title: string; icon?: ReactNode; right?: ReactNode; children: ReactNode; className?: string; }) { return (

{icon} {title}

{right}
{children}
); } function formatUptime(seconds: number): string { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (days > 0) return `${days} d ${hours} h`; if (hours > 0) return `${hours} h ${minutes} m`; return `${minutes} m`; } function jobStatusClass(status: string): string { if (status === "error") return "text-ctp-red"; if (status === "cancelled") return "text-ctp-overlay0"; if (status === "complete") return "text-ctp-green"; return "text-ctp-blue"; } function JobRow({ job, showSummary }: { job: StatJob; showSummary?: boolean }) { const Icon = job.kind === "download" ? Download : ScanSearch; return (
  • {job.label} {job.status}
    {showSummary ? (

    {job.summary ?? job.detail}

    ) : ( <>

    {job.progress}% · {job.detail}

    )}
  • ); } function logLineClass(line: string): string { const level = /^(DEBUG|INFO|WARNING|ERROR|CRITICAL)\b/.exec(line)?.[1]; if (level === "ERROR" || level === "CRITICAL") return "text-ctp-red"; if (level === "WARNING") return "text-ctp-peach"; if (level === "DEBUG") return "text-ctp-overlay0"; return "text-ctp-subtext0"; } export default function StatsPage() { const user = useAuth((state) => state.user); const staff = Boolean( user && (user.is_staff || user.is_superuser || user.role === "staff"), ); const query = useQuery({ queryKey: ["stats"], queryFn: () => api("/api/stats/"), enabled: staff, refetchInterval: staff ? REFRESH_MS : false, }); const [autoScroll, setAutoScroll] = useState(true); const logRef = useRef(null); const data = query.data; const logLines = data?.log.lines ?? []; const logCount = logLines.length; useEffect(() => { if (!autoScroll || !logRef.current) return; logRef.current.scrollTop = logRef.current.scrollHeight; }, [logCount, autoScroll]); if (!staff) { return ( ); } if (query.isPending) { return (
    ); } if (query.isError || !data) { return ( ); } const { system, gpus, disk, jobs, log } = data; const memoryPercent = system.memory.percent; const cores = system.cpu.per_cpu ?? []; return (

    Stats

    Host telemetry, background jobs and the application log. Refreshing every {REFRESH_MS / 1000} s.

    up {formatUptime(system.uptime_seconds)}

    } right={ {system.cpu.percent.toFixed(1)}% } >

    {system.cpu.cores} logical cores {system.cpu.physical_cores ? ` · ${system.cpu.physical_cores} physical` : ""} {system.cpu.load.length > 0 ? ` · load ${system.cpu.load.join(" / ")}` : ""}

    {cores.map((value, index) => ( ))}
    } right={ {memoryPercent.toFixed(1)}% } >

    {formatBytes(system.memory.used)} /{" "} {formatBytes(system.memory.total)} · {formatBytes(system.memory.available)} free

    {system.memory.swap_total > 0 ? ( <>

    swap {formatBytes(system.memory.swap_used)} /{" "} {formatBytes(system.memory.swap_total)}

    ) : null}
    {gpus.length > 0 ? ( gpus.map((gpu) => ( } right={ {gpu.utilization ?? "?"}% } >

    {gpu.name}

    {gpu.memory_used !== null && gpu.memory_total !== null ? `vram ${formatBytes(gpu.memory_used)} / ${formatBytes(gpu.memory_total)}` : "vram unknown"} {gpu.temperature !== null ? ` · ${gpu.temperature}°C` : ""}

    )) ) : ( } >

    No NVIDIA GPU reported by nvidia-smi.

    )} } right={ {disk.watched_folder.percent_used.toFixed(1)}% } >

    {formatBytes(disk.watched_folder.used)} /{" "} {formatBytes(disk.watched_folder.total)} · {formatBytes(disk.watched_folder.free)} free

    library {formatBytes(disk.watched_folder.library_size)} ·{" "} {disk.library_items} items

    media {formatBytes(disk.media.size)} · temp{" "} {formatBytes(disk.temp.size)} ({disk.temp.files} files)

    } right={ {jobs.active_count} running } > {jobs.active.length === 0 ? (

    Nothing running — downloads and match scans appear here.

    ) : (
      {jobs.active.map((job) => ( ))}
    )}
    } > {jobs.recent.length === 0 ? (

    No finished jobs yet.

    ) : (
      {jobs.recent.map((job) => ( ))}
    )}
    } right={
    {log.path}
    } > {log.missing ? (

    No log file yet — it is created on the next log write.

    ) : (
    {logLines.map((line, index) => (
    {line}
    ))}
    )}
    ); }