"use client"; import { useState, useCallback } from "react"; import { usePoll } from "@/lib/use-poll"; import { postAPI } from "@/lib/api"; import type { OverviewStats, HealthScore, DiskUsageEntry } from "@/lib/types"; import { StatCard } from "@/components/stat-card"; import { ActivityFeed } from "@/components/activity-feed"; import { JellyfinCard } from "@/components/jellyfin-card"; import { OllamaCard } from "@/components/ollama-card"; import { CalendarCard } from "@/components/calendar-card"; import { HostRow } from "@/components/host-card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CardSkeleton, TableSkeleton } from "@/components/skeleton"; /* --- Quick Action Button --- */ interface ActionButtonProps { label: string; endpoint: string; icon: string; } function ActionButton({ label, endpoint, icon }: ActionButtonProps) { const [state, setState] = useState<"idle" | "loading" | "success" | "error">("idle"); const run = useCallback(async () => { setState("loading"); try { await postAPI(endpoint); setState("success"); } catch { setState("error"); } setTimeout(() => setState("idle"), 2000); }, [endpoint]); const bg = state === "loading" ? "bg-white/[0.08] cursor-wait" : state === "success" ? "bg-green-500/15 border-green-500/30" : state === "error" ? "bg-red-500/15 border-red-500/30" : "bg-white/[0.04] hover:bg-white/[0.08] border-white/[0.08]"; return ( {icon} {state === "loading" ? "Running..." : state === "success" ? "Done!" : state === "error" ? "Failed" : label} ); } /* --- Organizer Toggle --- */ function OrganizerToggle() { const [paused, setPaused] = useState(false); const [state, setState] = useState<"idle" | "loading">("idle"); const toggle = useCallback(async () => { setState("loading"); try { if (paused) { await postAPI("/api/actions/resume-organizers"); setPaused(false); } else { await postAPI("/api/actions/pause-organizers"); setPaused(true); } } catch { // ignore } setState("idle"); }, [paused]); return ( {paused ? ">" : "||"} {state === "loading" ? "..." : paused ? "Resume Organizers" : "Pause Organizers"} ); } /* --- Disk Usage Bar --- */ function DiskBar({ entry }: { entry: DiskUsageEntry }) { const color = entry.used_pct >= 85 ? "from-red-500 to-red-400" : entry.used_pct >= 70 ? "from-amber-500 to-amber-400" : "from-green-500 to-emerald-400"; const hostColors: Record = { atlantis: "text-blue-400", calypso: "text-violet-400", olares: "text-emerald-400", nuc: "text-amber-400", rpi5: "text-cyan-400", homelab: "text-green-400", guava: "text-orange-400", seattle: "text-teal-400", }; const hostCls = hostColors[entry.host.toLowerCase()] ?? "text-foreground"; return ( {entry.host} {entry.mount} {entry.total_gb >= 1000 ? `${(entry.total_gb / 1000).toFixed(1)} TB` : `${Math.round(entry.total_gb)} GB`} {entry.used_pct}% ); } export default function DashboardPage() { const { data } = usePoll("/api/stats/overview", 60000); const { data: health } = usePoll("/api/health-score", 60000); const { data: disks } = usePoll("/api/disk-usage", 300000); // Handle both API field name variants const endpoints = data?.containers?.endpoints || data?.containers?.by_endpoint || {}; const rawEmail = data?.emails_today ?? data?.email_today ?? 0; const emailCount = typeof rawEmail === "object" && rawEmail !== null ? (rawEmail as Record).total ?? 0 : rawEmail; const alertCount = data?.alerts ?? data?.unhealthy_count ?? 0; const running = data?.containers?.running ?? Object.values(endpoints).reduce((s, e) => s + (e.running || 0), 0); const hostsOnline = data?.hosts_online ?? Object.values(endpoints).filter(e => !e.error).length; const gpuPct = data?.gpu?.utilization_pct; const totalHosts = Object.keys(endpoints).length; // Top 5 most-used disks const topDisks = disks ? [...disks].sort((a, b) => b.used_pct - a.used_pct).slice(0, 5) : []; return ( {/* Row 1: Stats */} = 80 ? "green" : health.score >= 60 ? "amber" : "amber" : "blue" } gauge={health ? health.score : undefined} sub={health ? `Grade: ${health.grade}` : undefined} /> {/* Row 1.5: Quick Actions */} Quick Actions {/* Row 2: Calendar + Activity Feed */} {/* Row 3: Jellyfin + GPU + Hosts */} Hosts {data ? `(${hostsOnline}/${totalHosts} online)` : ""} {data ? ( {Object.entries(endpoints).map(([name, info]) => ( ))} ) : ( )} {/* Row 4: Storage / Disk Usage */} Storage {!disks ? ( ) : topDisks.length === 0 ? ( No disk data ) : ( {topDisks.map((d, i) => ( ))} )} ); }
No disk data