Tile windowing system
This commit is contained in:
parent
c6e6c5ca48
commit
43318fb8cd
35 changed files with 4659 additions and 360 deletions
189
app/page.tsx
189
app/page.tsx
|
|
@ -1,40 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { getStats, type Stats, type NetworkInterface } from "./lib/getStats";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import SideNav from "./components/SideNav";
|
||||
import Hero from "./components/Hero";
|
||||
import StatsGrid from "./components/StatsGrid";
|
||||
import ServicesCard from "./components/ServicesCard";
|
||||
import UptimeCard from "./components/UptimeCard";
|
||||
import NetworkCard from "./components/NetworkCard";
|
||||
import PowerGrid from "./components/PowerGrid";
|
||||
import LinksGrid from "./components/LinksGrid";
|
||||
import WindowManager from "./components/windows/WindowManager";
|
||||
import DevConsole, { type LogEntry } from "./components/DevConsole";
|
||||
import { useCheckAuth } from "@/hooks/useCheckAuth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { getPower, type PowerData } from "./lib/getPower";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
useCheckAuth();
|
||||
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [power, setPower] = useState<PowerData | null>(null);
|
||||
const [netSpeed, setNetSpeed] = useState<
|
||||
Record<string, { rx: number; tx: number }>
|
||||
>({});
|
||||
|
||||
const [isAuthed, setIsAuthed] = useState<boolean | null>(null);
|
||||
const [online, setOnline] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
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);
|
||||
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => setIsMobile(window.innerWidth < 768);
|
||||
check();
|
||||
|
|
@ -43,8 +25,22 @@ export default function Home() {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const original = window.fetch;
|
||||
fetch("/api/auth/check").then((r) => setIsAuthed(r.ok));
|
||||
}, []);
|
||||
|
||||
// Lightweight online check
|
||||
useEffect(() => {
|
||||
const check = async () => {
|
||||
try { setOnline((await fetch("/api/stats")).ok); } catch { setOnline(false); }
|
||||
};
|
||||
check();
|
||||
const id = setInterval(check, 5000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// Dev console fetch logger
|
||||
useEffect(() => {
|
||||
const original = window.fetch;
|
||||
window.fetch = async (input, init) => {
|
||||
const url =
|
||||
typeof input === "string"
|
||||
|
|
@ -62,11 +58,7 @@ export default function Home() {
|
|||
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
|
||||
}
|
||||
try { path = new URL(url, window.location.origin).pathname; } catch {}
|
||||
const timestamp = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
const start = Date.now();
|
||||
|
||||
|
|
@ -81,147 +73,50 @@ export default function Home() {
|
|||
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))
|
||||
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))
|
||||
prev.map((e) => (e.id === id ? { ...e, status: 0, duration, response: String(err) } : e)),
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
window.fetch = original;
|
||||
};
|
||||
return () => { window.fetch = original; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const data = await getStats();
|
||||
|
||||
if (prevNetRef.current && lastFetchRef.current > 0) {
|
||||
const elapsed = (now - lastFetchRef.current) / 1000;
|
||||
const speeds: Record<string, { rx: number; tx: number }> = {};
|
||||
|
||||
for (const iface of Object.keys(data.network)) {
|
||||
const prev = prevNetRef.current[iface];
|
||||
if (prev) {
|
||||
speeds[iface] = {
|
||||
rx: Math.max(0, (data.network[iface].rx - prev.rx) / elapsed),
|
||||
tx: Math.max(0, (data.network[iface].tx - prev.tx) / elapsed),
|
||||
};
|
||||
}
|
||||
}
|
||||
setNetSpeed(speeds);
|
||||
}
|
||||
|
||||
prevNetRef.current = data.network;
|
||||
lastFetchRef.current = now;
|
||||
setStats(data);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === "UNAUTHORIZED") {
|
||||
router.push(
|
||||
"/auth?callbackUrl=" + encodeURIComponent(window.location.pathname),
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.error("Dashboard fetch failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const id = setInterval(fetchData, 4000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const fetchPower = useCallback(async () => {
|
||||
try {
|
||||
setPower(await getPower());
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === "UNAUTHORIZED") return;
|
||||
console.error("Power fetch failed:", e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPower();
|
||||
const id = setInterval(fetchPower, 10000);
|
||||
return () => clearInterval(id);
|
||||
}, [fetchPower]);
|
||||
|
||||
const primaryIface = stats
|
||||
? Object.keys(stats.network).find(
|
||||
(k) =>
|
||||
!k.startsWith("docker") &&
|
||||
!k.startsWith("br-") &&
|
||||
stats.network[k].rx > 0,
|
||||
)
|
||||
: null;
|
||||
|
||||
const primarySpeed = primaryIface ? netSpeed[primaryIface] || null : null;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-primary text-foreground overflow-hidden flex flex-row">
|
||||
<SideNav
|
||||
online={!!stats}
|
||||
online={online}
|
||||
devConsoleOpen={panelOpen}
|
||||
onToggleDevConsole={() => setPanelOpen((o) => !o)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="flex-1 overflow-y-auto pt-[52px] lg:pt-0 lg:m-[10px_10px_10px_0px] lg:rounded-2xl lg:border lg:border-blue/20 min-w-0"
|
||||
className="flex-1 overflow-hidden pt-[52px] lg:pt-0 lg:m-[10px_10px_10px_0px] lg:rounded-2xl min-w-0"
|
||||
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 pt-8">
|
||||
<Hero lastUpdated={stats?.timestamp ?? null} />
|
||||
|
||||
<div className="flex items-baseline justify-between mb-5">
|
||||
<h2 className="text-lg font-medium tracking-tight text-foreground" style={{ lineHeight: "normal", marginTop: 0 }}>
|
||||
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-foreground" style={{ lineHeight: "normal", marginTop: 0 }}>
|
||||
Power Consumption
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<PowerGrid power={power} onRefresh={fetchPower} />
|
||||
|
||||
<LinksGrid />
|
||||
</div>
|
||||
{mounted && (
|
||||
<WindowManager isAuthed={!!isAuthed} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DevConsole
|
||||
open={panelOpen}
|
||||
width={panelWidth}
|
||||
isMobile={isMobile}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
onWidthChange={setPanelWidth}
|
||||
logs={logs}
|
||||
/>
|
||||
{mounted && isAuthed && (
|
||||
<DevConsole
|
||||
open={panelOpen}
|
||||
width={panelWidth}
|
||||
isMobile={isMobile}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
onWidthChange={setPanelWidth}
|
||||
logs={logs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue