diff --git a/app/api/power/route.ts b/app/api/power/route.ts new file mode 100644 index 0000000..5deca73 --- /dev/null +++ b/app/api/power/route.ts @@ -0,0 +1,18 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + const token = req.cookies.get("token")?.value; + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const res = await fetch("http://localhost:3001/power", { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!res.ok) { + return NextResponse.json({ error: "Upstream error" }, { status: res.status }); + } + + return NextResponse.json(await res.json()); +} diff --git a/app/components/PowerCard.tsx b/app/components/PowerCard.tsx new file mode 100644 index 0000000..dd28d09 --- /dev/null +++ b/app/components/PowerCard.tsx @@ -0,0 +1,101 @@ +import { type TapoDevice } from "../lib/getPower"; + +interface PowerCardProps { + device: TapoDevice | null; + label: string; + delay?: number; +} + +function powerColor(watts: number): string { + if (watts > 400) return "#ef4444"; + if (watts > 200) return "#f59e0b"; + return "#3b82f6"; +} + +export default function PowerCard({ device, label, delay = 0 }: PowerCardProps) { + const pct = device ? Math.min(100, (device.current_power_w / 500) * 100) : 0; + const runtimeHours = device ? Math.floor(device.today_runtime_min / 60) : 0; + const runtimeMins = device ? device.today_runtime_min % 60 : 0; + + return ( +
+
+ + {label} + + {device ? ( + + + {device.on ? "On" : "Off"} + + ) : null} +
+ + {device ? ( + <> +
+ + {device.current_power_w.toFixed(1)} + + W +
+ + {device.alias} ยท {device.model} + + +
+
+
+ +
+
+ + {(device.today_energy_wh / 1000).toFixed(3)} + kWh + + + Today + +
+
+ + {(device.month_energy_wh / 1000).toFixed(2)} + kWh + + + Month + +
+
+ + {runtimeHours}h {runtimeMins}m + + + Runtime + +
+
+ + ) : ( +
+ )} +
+ ); +} diff --git a/app/components/PowerGrid.tsx b/app/components/PowerGrid.tsx new file mode 100644 index 0000000..f98c752 --- /dev/null +++ b/app/components/PowerGrid.tsx @@ -0,0 +1,18 @@ +import { type PowerData } from "../lib/getPower"; +import PowerCard from "./PowerCard"; + +interface PowerGridProps { + power: PowerData | null; +} + +export default function PowerGrid({ power }: PowerGridProps) { + const server = power?.devices.find((d) => d.name === "server") ?? null; + const desktop = power?.devices.find((d) => d.name === "desktop") ?? null; + + return ( +
+ + +
+ ); +} diff --git a/app/lib/getPower.ts b/app/lib/getPower.ts new file mode 100644 index 0000000..915f382 --- /dev/null +++ b/app/lib/getPower.ts @@ -0,0 +1,24 @@ +export interface TapoDevice { + name: string; + ip: string; + alias: string; + model: string; + on: boolean; + current_power_w: number; + today_energy_wh: number; + month_energy_wh: number; + today_runtime_min: number; + month_runtime_min: number; +} + +export interface PowerData { + timestamp: string; + devices: TapoDevice[]; +} + +export async function getPower(): Promise { + const res = await fetch("/api/power"); + if (res.status === 401) throw new Error("UNAUTHORIZED"); + if (!res.ok) throw new Error(`Failed to fetch power: ${res.status}`); + return res.json() as Promise; +} diff --git a/app/page.tsx b/app/page.tsx index bfb9bb6..8ed87e1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -9,15 +9,18 @@ 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 { 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(null); + const [power, setPower] = useState(null); const [netSpeed, setNetSpeed] = useState< Record >({}); @@ -76,6 +79,21 @@ export default function Home() { // Clean up on unmount so we don't have multiple intervals running return () => clearInterval(id); + }, []); + + useEffect(() => { + const fetchPower = async () => { + try { + setPower(await getPower()); + } catch (e) { + if (e instanceof Error && e.message === "UNAUTHORIZED") return; + console.error("Power fetch failed:", e); + } + }; + + fetchPower(); + const id = setInterval(fetchPower, 10000); + return () => clearInterval(id); }, []); // Empty array means this setup only happens ONCE. // Derived values for the UI @@ -115,6 +133,14 @@ export default function Home() {
+
+

+ Power Consumption +

+
+ + + );