import { useEffect, useState, type CSSProperties } from "react"; import { Link } from "react-router-dom"; import { api, type CurrentUser, type IntegrationSummary, type IntegrationType, type ProxmoxGuest, type ProxmoxGuestsResponse, type SynologyStorageResponse, } from "../api/client"; import IntegrationForm from "../components/IntegrationForm"; import IntegrationEditForm from "../components/IntegrationEditForm"; import { formatDateTime } from "../utils/date"; function proxmoxStatusBadge(status: string) { return status === "running" ? ( Running ) : ( Stopped ); } function healthBadge(status: string) { return status === "normal" ? ( Normal ) : ( {status} ); } function formatBytes(bytes: number | null): string { if (bytes === null) return "—"; const units = ["B", "KB", "MB", "GB", "TB", "PB"]; let value = bytes; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit++; } return `${value.toFixed(1)} ${units[unit]}`; } function formatUptime(seconds: number | null): string { if (!seconds) return "—"; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); if (days > 0) return `${days}d ${hours}h`; const minutes = Math.floor((seconds % 3600) / 60); return `${hours}h ${minutes}m`; } const TYPE_LABELS: Record = { tailscale: "Tailscale", proxmox: "Proxmox", synology: "Synology", semaphore: "Semaphore", gitea: "Gitea", dockhand: "Dockhand", }; function typeBadgeStyle(colors: Record, type: IntegrationType): CSSProperties { const color = colors[type]; if (!color) return {}; return { backgroundColor: `${color}22`, color, border: `1px solid ${color}55` }; } export default function Integrations({ user }: { user: CurrentUser }) { const isAdmin = user.role === "admin"; const canEdit = user.role === "admin" || user.role === "operator"; const [integrations, setIntegrations] = useState(null); const [managing, setManaging] = useState(false); const [editingIntegration, setEditingIntegration] = useState(null); const [selectedId, setSelectedId] = useState(null); const [error, setError] = useState(null); const [integrationColors, setIntegrationColors] = useState>({}); useEffect(() => { api.settings.integrationColors().then((res) => setIntegrationColors(res.integrationColors)).catch(() => {}); }, []); const [proxmoxData, setProxmoxData] = useState(null); const [proxmoxError, setProxmoxError] = useState(null); const [loadingGuests, setLoadingGuests] = useState(false); const [actingOnGuest, setActingOnGuest] = useState(null); const [synologyData, setSynologyData] = useState(null); const [synologyError, setSynologyError] = useState(null); const [loadingStorage, setLoadingStorage] = useState(false); function loadIntegrations() { api.integrations .list() .then((res) => { setIntegrations(res.integrations); if (!selectedId && res.integrations.length > 0) setSelectedId(res.integrations[0].id); }) .catch((err) => setError(String(err))); } useEffect(loadIntegrations, []); const selected = integrations?.find((i) => i.id === selectedId) ?? null; useEffect(() => { if (selected?.type === "proxmox" && selected.enabled && !managing) { loadProxmoxGuests(selected.id); } else { setProxmoxData(null); } if (selected?.type === "synology" && selected.enabled && !managing) { loadSynologyStorage(selected.id); } else { setSynologyData(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedId, managing, integrations]); function loadSynologyStorage(id: number) { setLoadingStorage(true); setSynologyError(null); api.integrations.synology .storage(id) .then((res) => setSynologyData(res)) .catch((err) => setSynologyError(err instanceof Error ? err.message : String(err))) .finally(() => setLoadingStorage(false)); } function loadProxmoxGuests(id: number) { setLoadingGuests(true); setProxmoxError(null); api.integrations.proxmox .guests(id) .then((res) => setProxmoxData(res)) .catch((err) => setProxmoxError(err instanceof Error ? err.message : String(err))) .finally(() => setLoadingGuests(false)); } async function toggleEnabled(i: IntegrationSummary) { try { await api.integrations.update(i.id, { enabled: !i.enabled }); loadIntegrations(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } async function removeIntegration(i: IntegrationSummary) { if (!confirm(`Delete integration "${i.name}"?`)) return; try { await api.integrations.remove(i.id); if (selectedId === i.id) setSelectedId(null); loadIntegrations(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } async function guestAction(g: ProxmoxGuest, action: "start" | "stop" | "restart" | "shutdown") { if (!selectedId) return; if (action === "stop" && !confirm(`Stop ${g.type === "qemu" ? "VM" : "container"} "${g.name}" immediately? Use Shutdown instead for a graceful power-off.`)) return; setActingOnGuest(g.vmid); setProxmoxError(null); try { await api.integrations.proxmox[action](selectedId, g.node, g.type, g.vmid); loadProxmoxGuests(selectedId); } catch (err) { setProxmoxError(err instanceof Error ? err.message : String(err)); } finally { setActingOnGuest(null); } } return ( <>

Integrations

{isAdmin && ( )}
{error &&
{error}
} {managing ? ( <> {editingIntegration ? ( { setEditingIntegration(null); loadIntegrations(); }} onCancel={() => setEditingIntegration(null)} /> ) : ( setManaging(false)} /> )}
{integrations?.map((i) => ( ))} {integrations?.length === 0 && ( )}
Name Type Status Actions
{i.name} {TYPE_LABELS[i.type]}
No integrations configured yet.
) : ( <> {integrations?.length === 0 ? (
No integrations configured yet. {isAdmin && ( <> {" "} . )}
) : ( <>
{selected?.type === "tailscale" ? (
Manage this integration's devices on the Tailscale page.
) : selected?.type === "gitea" ? (
Manage this integration's repositories on the Gitea page.
) : selected?.type === "semaphore" ? (
Manage this integration's templates on the Semaphore page.
) : selected?.type === "proxmox" ? (

VMs & Containers {proxmoxData && ( {proxmoxData.summary.running}/{proxmoxData.summary.total} running )}

{proxmoxError &&
{proxmoxError}
}
{canEdit && } {proxmoxData?.guests.map((g) => ( {canEdit && ( )} ))} {proxmoxData?.guests.length === 0 && ( )}
Name Node Type Uptime StatusActions
{g.name} #{g.vmid} {g.node} {g.type === "qemu" ? "VM" : "LXC"} {formatUptime(g.uptime)} {proxmoxStatusBadge(g.status)}
{g.status === "running" ? ( <> ) : ( )}
No VMs or containers found across any node.
) : selected?.type === "synology" ? (

Volumes

{synologyError &&
{synologyError}
}
{synologyData?.volumes.map((v) => ( ))} {synologyData?.volumes.length === 0 && ( )}
Volume Type Used Status
{v.id} {v.deviceType} {formatBytes(v.sizeUsed)} / {formatBytes(v.sizeTotal)} {healthBadge(v.status)}
No volumes found.

Disks

{synologyData?.disks.map((d) => ( ))} {synologyData?.disks.length === 0 && ( )}
Disk Device Temp S.M.A.R.T. Status
{d.name} {d.device} {d.temp !== null ? `${d.temp}°C` : "—"} {healthBadge(d.smartStatus)} {healthBadge(d.status)} {d.exceedBadSectorThreshold && Bad sectors} {d.belowRemainLifeThreshold && Low life}
No disks found.
) : selected?.type === "dockhand" ? (
Manage this integration's containers on the Docker page.
) : (
{selected ? `${TYPE_LABELS[selected.type]} isn't wired up in the UI yet.` : "Select an integration."}
)} )} )} ); }