"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 (
);
}
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
| 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 (
| {name} |
{status}
|
);
})}
)}
);
}