Add start/restart/stop buttons to Proxmox-linked servers' detail page

The Proxmox start/stop/restart routes already existed (used by the
Integrations page's guest table) but weren't reachable from a server's
own detail page, even when that server was linked to a Proxmox guest.

Reuses the existing POST /api/integrations/:id/proxmox/nodes/:node/
:type/:vmid/{start,stop,restart} routes and the operator+ role gate
already enforced there — no server-side changes needed. Buttons only
render for hardware.source === "proxmox", mirroring the Integrations
page's running/stopped button-set logic, with the same confirm-before-
stop prompt for the non-reversible action.

Verified end-to-end against the real dev server with a mock Proxmox
HTTPS server: linked a server to a mock guest, confirmed the detail
endpoint's live status, then triggered restart/stop/start and
confirmed the mock actually received each action and every call was
audit-logged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 20:46:35 +02:00
co-authored by Claude Sonnet 5
parent 1a054d6c2f
commit de9b6a2d6a
2 changed files with 60 additions and 2 deletions
+2 -1
View File
@@ -28,7 +28,8 @@ All modules from the original plan are built:
overview opens a detail page with CPU/RAM/disk status, IP addresses, and
matching DNS names (looked up from the DNS module's cache) — live from
Proxmox for VM/LXC-backed servers, or from the agent's own hardware
report for everything else.
report for everything else. Proxmox-linked servers also get
start/stop/restart buttons right on the detail page.
- **Integrations → Tailscale** — device list with online/authorized status,
and authorize/deauthorize/remove actions; a live device-count widget.
- **Integrations → Gitea** — repo list with each repo's last CI run status,
+58 -1
View File
@@ -79,6 +79,9 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
const [linking, setLinking] = useState(false);
const [linkError, setLinkError] = useState<string | null>(null);
const [actingOnGuest, setActingOnGuest] = useState(false);
const [guestActionError, setGuestActionError] = useState<string | null>(null);
const loadDetail = useCallback(() => {
setRefreshing(true);
return api.servers
@@ -227,6 +230,26 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
const { server, hardware, ipAddresses, dnsMatches } = detail;
async function guestAction(action: "start" | "stop" | "restart") {
if (!server.proxmoxIntegrationId || !server.proxmoxNode || !server.proxmoxGuestType || server.proxmoxVmid === null) return;
if (action === "stop" && !confirm(`Stop ${server.name}? This is an immediate power-off, not a graceful shutdown.`)) return;
setActingOnGuest(true);
setGuestActionError(null);
try {
await api.integrations.proxmox[action](
server.proxmoxIntegrationId,
server.proxmoxNode,
server.proxmoxGuestType,
server.proxmoxVmid,
);
await loadDetail();
} catch (err) {
setGuestActionError(err instanceof Error ? err.message : String(err));
} finally {
setActingOnGuest(false);
}
}
return (
<>
<div className="mb-3">
@@ -330,7 +353,41 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
{hardware.error && <div className="alert alert-danger">{hardware.error}</div>}
{hardware.source === "proxmox" && !hardware.error && (
<>
<div className="text-secondary small mb-3">Synced live from Proxmox just now.</div>
<div className="d-flex align-items-center justify-content-between mb-3">
<div className="text-secondary small">
Synced live from Proxmox just now.
{hardware.status && (
<span
className={`badge ms-2 ${hardware.status === "running" ? "bg-green-lt text-green" : "bg-secondary-lt text-secondary"}`}
>
{hardware.status}
</span>
)}
</div>
{canEditTasks && (
<div className="btn-list flex-nowrap">
{hardware.status === "running" ? (
<>
<button className="btn btn-sm" onClick={() => guestAction("restart")} disabled={actingOnGuest}>
Restart
</button>
<button
className="btn btn-sm btn-outline-danger"
onClick={() => guestAction("stop")}
disabled={actingOnGuest}
>
Stop
</button>
</>
) : (
<button className="btn btn-sm btn-primary" onClick={() => guestAction("start")} disabled={actingOnGuest}>
Start
</button>
)}
</div>
)}
</div>
{guestActionError && <div className="alert alert-danger">{guestActionError}</div>}
<div className="row g-3">
<div className="col-sm-6">
<div className="text-secondary small">CPU</div>