"use client"; import React, { useState, useEffect, useRef, useMemo } from "react"; import { getStats, type Stats, type NetworkInterface } from "../../lib/getStats"; import AnalyticsPanel from "./AnalyticsPanel"; import HelpTooltip from "../HelpTooltip"; // ── Types / constants ───────────────────────────────────────────────────────── const COST_PER_KWH = 0.24; interface DeviceReading { name: string; watts: number; on: boolean; today_wh: number; month_wh: number; } interface HistoryEntry { ts: string; devices: DeviceReading[]; } const PRESETS = [ { label: "1h", h: 1 }, { label: "6h", h: 6 }, { label: "24h", h: 24 }, { label: "3d", h: 72 }, { label: "7d", h: 168 }, { label: "30d", h: 720 }, ]; // ── Helpers ─────────────────────────────────────────────────────────────────── function fmtBytes(mb: number): string { if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`; return `${mb.toFixed(0)} MB`; } function fmtUptime(up: Stats["uptime"]): string { const parts: string[] = []; if (up.days > 0) parts.push(`${up.days}d`); if (up.hours > 0 || up.days > 0) parts.push(`${up.hours}h`); parts.push(`${up.minutes}m`); return parts.join(" "); } function fmtNetBytes(bytes: number): string { if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(2)} GB/s`; if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB/s`; if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(0)} KB/s`; return `${bytes.toFixed(0)} B/s`; } // ── Sub-components ──────────────────────────────────────────────────────────── function StatCard({ label, value, sub, hot }: { label: string; value: string; sub?: string; hot?: boolean }) { return (

{label}

{value}

{sub &&

{sub}

}
); } function PowerSummaryCard({ label, value, sub, accent }: { label: string; value: string; sub?: string; accent?: string }) { return (

{label}

{value}

{sub &&

{sub}

}
); } function BreakdownBar({ label, count, total, color }: { label: string; count: number; total: number; color?: string }) { const pct = total > 0 ? Math.round((count / total) * 100) : 0; return (
{label} {count} · {pct}%
); } function SectionTitle({ children }: { children: React.ReactNode }) { return (

{children}

); } // ── DashboardPanel ──────────────────────────────────────────────────────────── export default function DashboardPanel({ isAuthed }: { isAuthed: boolean }) { const [stats, setStats] = useState(null); const [netSpeed, setNetSpeed] = useState<{ rx: number; tx: number } | null>(null); const prevNetRef = useRef | null>(null); const prevTimeRef = useRef(0); const [hours, setHours] = useState(24); const [readings, setReadings] = useState([]); const [powerLoading, setPowerLoading] = useState(true); // System stats poll useEffect(() => { const go = async () => { try { const now = Date.now(); const data = await getStats(); setStats(data); const primary = Object.keys(data.network).find( (k) => !k.startsWith("docker") && !k.startsWith("br-") && data.network[k].rx > 0, ); if (primary && prevNetRef.current?.[primary] && prevTimeRef.current > 0) { const elapsed = (now - prevTimeRef.current) / 1000; const prev = prevNetRef.current[primary]; setNetSpeed({ rx: Math.max(0, (data.network[primary].rx - prev.rx) / elapsed), tx: Math.max(0, (data.network[primary].tx - prev.tx) / elapsed), }); } prevNetRef.current = data.network; prevTimeRef.current = now; } catch {} }; go(); const id = setInterval(go, 4000); return () => clearInterval(id); }, []); // Power history fetch useEffect(() => { let cancelled = false; setPowerLoading(true); fetch(`/api/power/history?hours=${hours}`) .then(r => r.ok ? r.json() : { readings: [] }) .then(d => { if (!cancelled) { setReadings(d.readings ?? []); setPowerLoading(false); } }) .catch(() => { if (!cancelled) setPowerLoading(false); }); return () => { cancelled = true; }; }, [hours]); // Power summary stats const powerStats = useMemo(() => { if (readings.length === 0) return null; const deviceNames = [...new Set(readings.flatMap(r => r.devices.map(d => d.name)))]; let totalWh = 0; let totalAvgW = 0; for (const name of deviceNames) { const pts = readings .map(r => { const d = r.devices.find(x => x.name === name); return d ? { ts: new Date(r.ts).getTime(), w: d.watts } : null; }) .filter(Boolean).sort((a, b) => a!.ts - b!.ts) as { ts: number; w: number }[]; let wh = 0; for (let i = 1; i < pts.length; i++) { wh += (pts[i].w + pts[i - 1].w) / 2 * (pts[i].ts - pts[i - 1].ts) / 3_600_000; } totalWh += wh; totalAvgW += pts.length ? pts.reduce((s, p) => s + p.w, 0) / pts.length : 0; } const cost = totalWh / 1000 * COST_PER_KWH; const latest = readings[readings.length - 1]; const activeDevices = latest ? latest.devices.filter(d => d.on).length : 0; const totalDevices = latest ? latest.devices.length : 0; return { totalWh, totalAvgW, cost, activeDevices, totalDevices }; }, [readings]); const s = stats; const svcEntries = isAuthed && s ? Object.entries(s.services) : []; const svcRunning = svcEntries.filter(([, v]) => v === "running" || v === "active").length; const svcStopped = svcEntries.filter(([, v]) => v === "inactive" || v === "stopped" || v === "dead").length; const svcFailed = svcEntries.filter(([, v]) => v === "failed").length; const svcTotal = svcEntries.length; return (
{/* System stat cards */}
80} /> 85} /> 90} /> 80 ? "Running hot" : s.temperature > 60 ? "Warm" : "Cool") : ""} hot={s?.temperature != null && s.temperature > 75} />
{/* Power analytics section */}
{/* Header row: title + time range pills */}

Power Analytics

Time Range
{PRESETS.map(p => ( ))}
{/* Summary cards */}
= 1000 ? `${(powerStats.totalWh / 1000).toFixed(2)} kWh` : `${powerStats.totalWh.toFixed(1)} Wh`) : "—"} sub={`over ${PRESETS.find(p => p.h === hours)?.label ?? `${hours}h`}`} />
{/* Mini charts */}
{(["line", "bar", "candle"] as const).map((type) => (

{type === "line" ? "Line" : type === "bar" ? "Bar" : "Candlestick"}

))}
{/* Authenticated: services + system info */} {isAuthed && (
{svcTotal > 0 && (
Services ({svcTotal})
{svcFailed > 0 && }
)} {s?.loadAvg && (
Load Average
{([["1 min", s.loadAvg["1m"]], ["5 min", s.loadAvg["5m"]], ["15 min", s.loadAvg["15m"]]] as [string, number][]).map(([label, val]) => (
{label} {val.toFixed(2)}
))}
)} {s?.uptime && (
Uptime

{fmtUptime(s.uptime)}

{s.uptime.days > 0 ? `${s.uptime.days} days, ${s.uptime.hours} hours, ${s.uptime.minutes} min` : `${s.uptime.hours} hours, ${s.uptime.minutes} min`}

)} {netSpeed && (
Network
↓ Download {fmtNetBytes(netSpeed.rx)}
↑ Upload {fmtNetBytes(netSpeed.tx)}
)}
)} {/* Service status table (authenticated only) */} {isAuthed && svcEntries.length > 0 && (
Service Status
{svcEntries.map(([name, status], i) => { const isRunning = status === "running" || status === "active"; const isFailed = status === "failed"; const color = isFailed ? "#ef4444" : isRunning ? "#5dd776" : "var(--color-foreground-sec)"; return ( ); })}
Service Status
{name} {status}
)}
); }