Dev console

This commit is contained in:
Jack Mechem 2026-05-21 16:35:34 -07:00
parent 882ff7696a
commit 130bed1d1f
Signed by: jackmechem
SSH key fingerprint: SHA256:GjIjMAC33pzYOe+hWcX5uvgnPrVFAXSrquvt84AOJbU
5 changed files with 4790 additions and 60 deletions

View file

@ -11,6 +11,7 @@ import UptimeCard from "./components/UptimeCard";
import NetworkCard from "./components/NetworkCard";
import PowerGrid from "./components/PowerGrid";
import LinksGrid from "./components/LinksGrid";
import DevConsole, { type LogEntry } from "./components/DevConsole";
import { useCheckAuth } from "@/hooks/useCheckAuth";
import { useRouter } from "next/navigation";
import { getPower, type PowerData } from "./lib/getPower";
@ -25,10 +26,79 @@ export default function Home() {
Record<string, { rx: number; tx: number }>
>({});
// We use Refs for these because changing them shouldn't trigger a "refresh"
// but we need them to calculate the delta (speed) between fetches.
const [panelOpen, setPanelOpen] = useState(false);
const [panelWidth, setPanelWidth] = useState(440);
const [isMobile, setIsMobile] = useState(false);
const [logs, setLogs] = useState<LogEntry[]>([]);
const prevNetRef = useRef<Record<string, NetworkInterface> | null>(null);
const lastFetchRef = useRef<number>(0);
const logIdRef = useRef(0);
// Mobile detection
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 768);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
// Fetch interceptor — captures all /api/* requests
useEffect(() => {
const original = window.fetch;
window.fetch = async (input, init) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.href
: (input as Request).url;
const shouldLog =
(url.startsWith("/api/") && !url.startsWith("/api/dev/proxy")) ||
url.includes("localhost:3001");
if (!shouldLog) return original(input, init);
const method = ((init?.method ?? (input instanceof Request ? input.method : "GET")) as string).toUpperCase();
const id = ++logIdRef.current;
let path = url;
try {
path = new URL(url, window.location.origin).pathname;
} catch {
// use raw url
}
const timestamp = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
const start = Date.now();
setLogs((prev) => [
...prev,
{ id, method, path, url, status: null, duration: null, timestamp, response: null },
]);
try {
const res = await original(input, init);
const clone = res.clone();
const text = await clone.text();
const duration = Date.now() - start;
setLogs((prev) =>
prev.map((e) => (e.id === id ? { ...e, status: res.status, duration, response: text } : e))
);
return res;
} catch (err) {
const duration = Date.now() - start;
setLogs((prev) =>
prev.map((e) => (e.id === id ? { ...e, status: 0, duration, response: String(err) } : e))
);
throw err;
}
};
return () => {
window.fetch = original;
};
}, []);
useEffect(() => {
const fetchData = async () => {
@ -36,7 +106,6 @@ export default function Home() {
const now = Date.now();
const data = await getStats();
// 1. Calculate speeds if we have previous data
if (prevNetRef.current && lastFetchRef.current > 0) {
const elapsed = (now - lastFetchRef.current) / 1000;
const speeds: Record<string, { rx: number; tx: number }> = {};
@ -53,12 +122,8 @@ export default function Home() {
setNetSpeed(speeds);
}
// 2. Update our "silent" trackers
prevNetRef.current = data.network;
lastFetchRef.current = now;
// 3. Update the UI state with new data
// React's Virtual DOM will only update the changed text/numbers.
setStats(data);
} catch (e) {
if (e instanceof Error && e.message === "UNAUTHORIZED") {
@ -71,13 +136,8 @@ export default function Home() {
}
};
// Initial fetch
fetchData();
// Start the 4s loop
const id = setInterval(fetchData, 4000);
// Clean up on unmount so we don't have multiple intervals running
return () => clearInterval(id);
}, []);
@ -94,9 +154,8 @@ export default function Home() {
fetchPower();
const id = setInterval(fetchPower, 10000);
return () => clearInterval(id);
}, []); // Empty array means this setup only happens ONCE.
}, []);
// Derived values for the UI
const primaryIface = stats
? Object.keys(stats.network).find(
(k) =>
@ -109,39 +168,61 @@ export default function Home() {
const primarySpeed = primaryIface ? netSpeed[primaryIface] || null : null;
return (
<div className="max-w-5xl mx-auto px-6 pb-20">
<NavBar online={!!stats} />
<Hero lastUpdated={stats?.timestamp ?? null} />
<div className="flex items-baseline justify-between mb-5">
<h2 className="text-lg font-medium tracking-tight text-gray-900">
System Stats
</h2>
</div>
<StatsGrid stats={stats} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-3.5 mb-11">
<ServicesCard services={stats?.services ?? null} delay={200} />
<div className="flex flex-col gap-3.5">
<UptimeCard uptime={stats?.uptime ?? null} delay={250} />
<NetworkCard
iface={primaryIface ?? null}
speed={primarySpeed}
delay={300}
<>
<div
style={{
paddingRight: panelOpen && !isMobile ? panelWidth : 0,
transition: "padding-right 280ms cubic-bezier(0.4,0,0.2,1)",
}}
>
<div className="max-w-5xl mx-auto px-6 pb-20">
<NavBar
online={!!stats}
devConsoleOpen={panelOpen}
onToggleDevConsole={() => setPanelOpen((o) => !o)}
/>
<Hero lastUpdated={stats?.timestamp ?? null} />
<div className="flex items-baseline justify-between mb-5">
<h2 className="text-lg font-medium tracking-tight text-gray-900">
System Stats
</h2>
</div>
<StatsGrid stats={stats} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-3.5 mb-11">
<ServicesCard services={stats?.services ?? null} delay={200} />
<div className="flex flex-col gap-3.5">
<UptimeCard uptime={stats?.uptime ?? null} delay={250} />
<NetworkCard
iface={primaryIface ?? null}
speed={primarySpeed}
delay={300}
/>
</div>
</div>
<div className="flex items-baseline justify-between mb-5">
<h2 className="text-lg font-medium tracking-tight text-gray-900">
Power Consumption
</h2>
</div>
<PowerGrid power={power} />
<LinksGrid />
</div>
</div>
<div className="flex items-baseline justify-between mb-5">
<h2 className="text-lg font-medium tracking-tight text-gray-900">
Power Consumption
</h2>
</div>
<PowerGrid power={power} />
<LinksGrid />
</div>
<DevConsole
open={panelOpen}
width={panelWidth}
isMobile={isMobile}
onClose={() => setPanelOpen(false)}
onWidthChange={setPanelWidth}
logs={logs}
/>
</>
);
}