import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { api, type CurrentUser, type IntegrationSummary, type ProxmoxGuest, type ProxmoxGuestsResponse, type ProxmoxNodeStats, } from "../api/client"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { usePagination } from "../hooks/usePagination"; import Pagination from "../components/Pagination"; import { downloadCsv } from "../utils/csv"; function proxmoxStatusBadge(status: string) { return status === "running" ? ( Running ) : ( Stopped ); } 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`; } 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 usageBar(used: number | null, total: number | null) { if (!total) return null; const pct = Math.min(100, Math.round(((used ?? 0) / total) * 100)); return (
90 ? "bg-red" : pct > 75 ? "bg-yellow" : "bg-green"}`} style={{ width: `${pct}%` }} />
); } export default function Proxmox({ user }: { user: CurrentUser }) { const canEdit = user.role === "admin" || user.role === "operator"; const [integrations, setIntegrations] = useState(null); const [selectedId, setSelectedId] = useState(null); const [error, setError] = useState(null); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [actingOnGuest, setActingOnGuest] = useState(null); const [nodeStats, setNodeStats] = useState(null); const [nodeStatsError, setNodeStatsError] = useState(null); const [loadingNodeStats, setLoadingNodeStats] = useState(false); useEffect(() => { api.integrations .list() .then((res) => { const proxmoxIntegrations = res.integrations.filter((i) => i.type === "proxmox"); setIntegrations(proxmoxIntegrations); if (!selectedId && proxmoxIntegrations.length > 0) setSelectedId(proxmoxIntegrations[0].id); }) .catch((err) => setError(err instanceof Error ? err.message : String(err))); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const selected = integrations?.find((i) => i.id === selectedId) ?? null; function loadGuests(id: number) { setLoading(true); setError(null); api.integrations.proxmox .guests(id) .then((res) => setData(res)) .catch((err) => setError(err instanceof Error ? err.message : String(err))) .finally(() => setLoading(false)); } function loadNodeStats(id: number) { setLoadingNodeStats(true); setNodeStatsError(null); api.integrations.proxmox .nodes(id) .then((res) => setNodeStats(res.nodes)) .catch((err) => setNodeStatsError(err instanceof Error ? err.message : String(err))) .finally(() => setLoadingNodeStats(false)); } useEffect(() => { if (selected?.enabled) { loadGuests(selected.id); loadNodeStats(selected.id); } else { setData(null); setNodeStats(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedId]); 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); setError(null); try { await api.integrations.proxmox[action](selectedId, g.node, g.type, g.vmid); loadGuests(selectedId); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setActingOnGuest(null); } } const { sorted: sortedGuests, sortKey: guestSortKey, sortDir: guestSortDir, requestSort: requestGuestSort } = useSortable(data?.guests); const { pageItems: guestPageItems, page: guestPage, setPage: setGuestPage, pageCount: guestPageCount, totalCount: guestTotalCount } = usePagination(sortedGuests); function exportGuestsCsv() { if (!sortedGuests) return; downloadCsv( "proxmox-guests.csv", ["Name", "VMID", "Node", "Type", "Uptime", "Status"], sortedGuests.map((g) => [g.name, g.vmid, g.node, g.type === "qemu" ? "VM" : "LXC", formatUptime(g.uptime), g.status]), ); } return ( <>

Proxmox

{error &&
{error}
} {integrations?.length === 0 ? (
No Proxmox integration configured yet. Add one under Integrations.
) : ( <> {integrations && integrations.length > 1 && (
)} {selected && !selected.enabled ? (
"{selected.name}" is disabled. Enable it under Integrations → Manage integrations to see its VMs and containers.
) : ( <> {nodeStatsError &&
{nodeStatsError}
}
{nodeStats?.map((n) => (

{n.node} {n.pveVersion && {n.pveVersion}}

{n.error &&
{n.error}
}
Uptime
{formatUptime(n.uptime)}
CPU
{n.cpuUsagePercent !== null ? `${n.cpuUsagePercent.toFixed(1)}%` : "—"} {n.cpuCores ? ({n.cpuCores} cores) : null}
{n.loadAverage && (
Load: {n.loadAverage.map((l) => l.toFixed(2)).join(", ")}
)}
Memory
{formatBytes(n.memUsedBytes)} / {formatBytes(n.memTotalBytes)}
{usageBar(n.memUsedBytes, n.memTotalBytes)}
{!!n.swapTotalBytes && (
Swap
{formatBytes(n.swapUsedBytes)} / {formatBytes(n.swapTotalBytes)}
{usageBar(n.swapUsedBytes, n.swapTotalBytes)}
)}
Storage
{n.storages.length > 0 ? ( {n.storages.map((s) => ( ))}
{s.id} {!s.active && Inactive} {s.type} {formatBytes(s.usedBytes)} / {formatBytes(s.totalBytes)} {usageBar(s.usedBytes, s.totalBytes)}
) : ( !n.error && (
No storage visible to this API token. Proxmox filters the storage list per-token — grant it Datastore.Audit (e.g. on{" "} /storage) to see disk usage here.
) )}
))} {nodeStats?.length === 0 && (
No online Proxmox nodes found.
)}

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

label="Name" sortKeyName="name" activeKey={guestSortKey} direction={guestSortDir} onSort={requestGuestSort} /> label="Node" sortKeyName="node" activeKey={guestSortKey} direction={guestSortDir} onSort={requestGuestSort} /> label="Type" sortKeyName="type" activeKey={guestSortKey} direction={guestSortDir} onSort={requestGuestSort} /> label="Uptime" sortKeyName="uptime" activeKey={guestSortKey} direction={guestSortDir} onSort={requestGuestSort} /> label="Status" sortKeyName="status" activeKey={guestSortKey} direction={guestSortDir} onSort={requestGuestSort} /> {canEdit && } {guestPageItems?.map((g) => ( {canEdit && ( )} ))} {sortedGuests?.length === 0 && ( )}
Actions
{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.
)} )} ); }