"use client"; import { useState, useEffect } from "react"; const SERVICES = [ "syncthing", "dashboard", "caddy", "sshd", "cloudflare-dyndns.timer", "cloudflare-dyndns", "docker", "sysapi", ]; type Toast = { message: string; ok: boolean } | null; type ServiceStatuses = Record; export default function ControlPanel({ onClose }: { onClose: () => void }) { const [loading, setLoading] = useState>({}); const [toast, setToast] = useState(null); const [statuses, setStatuses] = useState({}); useEffect(() => { async function fetchStatuses() { try { const res = await fetch("/api/stats"); if (!res.ok) return; const data = await res.json(); setStatuses(data.services ?? {}); } catch {} } fetchStatuses(); }, []); function showToast(message: string, ok: boolean) { setToast({ message, ok }); setTimeout(() => setToast(null), 3000); } async function handleService(action: string, service: string) { setLoading((l) => ({ ...l, [`${service}-${action}`]: action })); const res = await fetch(`/api/services/${service}/${action}`, { method: "POST", }); showToast( res.ok ? `${service} ${action}ed` : `Failed to ${action} ${service}`, res.ok, ); if (res.ok) { setTimeout(async () => { const r = await fetch("/api/stats"); if (r.ok) { const data = await r.json(); setStatuses(data.services ?? {}); } }, 1500); } setLoading((l) => { const n = { ...l }; delete n[`${service}-${action}`]; return n; }); } async function handleLogs(service: string) { const res = await fetch(`/api/services/${service}/logs`); if (!res.ok) { showToast("Failed to fetch logs", false); return; } const data = await res.json(); console.log(`Logs for ${service}:`, data.stdout); showToast(`Logs fetched for ${service} — check console`, true); } async function handleReboot() { if (!confirm("Reboot the server? This will disconnect all sessions.")) return; const res = await fetch("/api/system/reboot", { method: "POST" }); showToast(res.ok ? "Rebooting..." : "Reboot failed", res.ok); } const isLoading = (service: string, action: string) => loading[`${service}-${action}`] !== undefined; const isActive = (svc: string) => statuses[svc] === "active"; const isInactive = (svc: string) => statuses[svc] === "inactive" || statuses[svc] === "failed" || statuses[svc] === "dead"; function statusDot(svc: string) { const s = statuses[svc]; const color = s === "active" ? "bg-green-400" : s === "failed" ? "bg-red-400" : "bg-gray-300"; return ; } function btnClass(svc: string, action: string): string { const active = isActive(svc); const inactive = isInactive(svc); const busy = isLoading(svc, action); const base = "rounded-md px-2.5 py-1 text-[13px] whitespace-nowrap border transition-colors"; if (busy) return `${base} border-gray-200 text-gray-300 cursor-not-allowed`; if (action === "start" && inactive) return `${base} bg-green-50 border-green-200 text-green-700 cursor-pointer`; if (action === "stop" && active) return `${base} bg-red-50 border-red-200 text-red-500 cursor-pointer`; if (action === "restart" && active) return `${base} bg-blue-50 border-blue-200 text-blue-500 cursor-pointer`; return `${base} border-gray-200 text-gray-300 cursor-default`; } return ( <> {/* Backdrop */}
{/* Panel */}
{/* Header */}

Control panel

Manage services

{/* Services */}

Services

{SERVICES.map((svc) => (
{statusDot(svc)}

{svc}

{["start", "stop", "restart"].map((action) => ( ))}
))}
{/* System */}

System

Reboot server

Immediately restarts the machine

{/* Toast */} {toast && (
{toast.message}
)}
); }