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
+
+
+
+
+
);