From 876da0c38400d1101cd0b97471b21bf4e933135d Mon Sep 17 00:00:00 2001 From: Jack Mechem Date: Thu, 21 May 2026 19:17:40 -0700 Subject: [PATCH] Plug on/off --- app/api/power/[device]/[action]/route.ts | 25 +++++++++++++++ app/components/PowerCard.tsx | 41 +++++++++++++++++------- app/components/PowerGrid.tsx | 36 +++++++++++++++++++-- app/page.tsx | 24 +++++++------- 4 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 app/api/power/[device]/[action]/route.ts diff --git a/app/api/power/[device]/[action]/route.ts b/app/api/power/[device]/[action]/route.ts new file mode 100644 index 0000000..bfd2e75 --- /dev/null +++ b/app/api/power/[device]/[action]/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server"; + +const ALLOWED_ACTIONS = ["on", "off"]; + +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ device: string; action: string }> }, +) { + const token = req.cookies.get("token")?.value; + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { device, action } = await params; + + if (!ALLOWED_ACTIONS.includes(action)) { + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); + } + + const res = await fetch(`http://localhost:3001/power/${device}/${action}`, { + method: "POST", + }); + + return new NextResponse(null, { status: res.status }); +} diff --git a/app/components/PowerCard.tsx b/app/components/PowerCard.tsx index dd28d09..b373186 100644 --- a/app/components/PowerCard.tsx +++ b/app/components/PowerCard.tsx @@ -1,9 +1,13 @@ +"use client"; + import { type TapoDevice } from "../lib/getPower"; interface PowerCardProps { device: TapoDevice | null; label: string; delay?: number; + toggling?: boolean; + onToggle?: (on: boolean) => void; } function powerColor(watts: number): string { @@ -12,7 +16,7 @@ function powerColor(watts: number): string { return "#3b82f6"; } -export default function PowerCard({ device, label, delay = 0 }: PowerCardProps) { +export default function PowerCard({ device, label, delay = 0, toggling = false, onToggle }: 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; @@ -27,18 +31,33 @@ export default function PowerCard({ device, label, delay = 0 }: PowerCardProps) {label} {device ? ( - +
- {device.on ? "On" : "Off"} - + > + + {device.on ? "On" : "Off"} + + {onToggle && ( + + )} +
) : null} diff --git a/app/components/PowerGrid.tsx b/app/components/PowerGrid.tsx index f98c752..66d5a1b 100644 --- a/app/components/PowerGrid.tsx +++ b/app/components/PowerGrid.tsx @@ -1,18 +1,48 @@ +"use client"; + +import { useState } from "react"; import { type PowerData } from "../lib/getPower"; import PowerCard from "./PowerCard"; interface PowerGridProps { power: PowerData | null; + onRefresh: () => void; } -export default function PowerGrid({ power }: PowerGridProps) { +export default function PowerGrid({ power, onRefresh }: PowerGridProps) { + const [toggling, setToggling] = useState(null); + const server = power?.devices.find((d) => d.name === "server") ?? null; const desktop = power?.devices.find((d) => d.name === "desktop") ?? null; + const handleToggle = async (deviceName: string, on: boolean) => { + setToggling(deviceName); + try { + const action = on ? "on" : "off"; + const res = await fetch(`/api/power/${deviceName}/${action}`, { method: "POST" }); + if (!res.ok) console.error(`Toggle ${deviceName} failed:`, res.status); + else onRefresh(); + } finally { + setToggling(null); + } + }; + return (
- - + handleToggle("server", on)} + /> + handleToggle("desktop", on)} + />
); } diff --git a/app/page.tsx b/app/page.tsx index 7969308..9cb0cd4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState, useRef, useCallback } from "react"; import { getStats, type Stats, type NetworkInterface } from "./lib/getStats"; import NavBar from "./components/NavBar"; @@ -141,20 +141,20 @@ export default function Home() { 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); - } - }; + 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( @@ -209,7 +209,7 @@ export default function Home() { - +