diff --git a/server/src/integrations/proxmox/adapter.ts b/server/src/integrations/proxmox/adapter.ts index 03f4fed..9c2127d 100644 --- a/server/src/integrations/proxmox/adapter.ts +++ b/server/src/integrations/proxmox/adapter.ts @@ -62,6 +62,8 @@ export interface ProxmoxAdapter { startGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise; stopGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise; restartGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise; + /** Graceful shutdown (ACPI power event for a VM, SIGTERM-then-wait for a container) — unlike stopGuest, this asks the guest OS to shut itself down. */ + shutdownGuest(node: string, type: ProxmoxGuestType, vmid: number): Promise; } function parseSizeToBytes(size: string): number | null { @@ -267,6 +269,7 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter { const startGuest = (node: string, type: ProxmoxGuestType, vmid: number) => statusAction(node, type, vmid, "start"); const stopGuest = (node: string, type: ProxmoxGuestType, vmid: number) => statusAction(node, type, vmid, "stop"); const restartGuest = (node: string, type: ProxmoxGuestType, vmid: number) => statusAction(node, type, vmid, "reboot"); + const shutdownGuest = (node: string, type: ProxmoxGuestType, vmid: number) => statusAction(node, type, vmid, "shutdown"); async function ping(): Promise<{ ok: boolean; latencyMs?: number; error?: string }> { const start = Date.now(); @@ -278,5 +281,5 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter { } } - return { ping, listGuests, getGuestDetail, startGuest, stopGuest, restartGuest }; + return { ping, listGuests, getGuestDetail, startGuest, stopGuest, restartGuest, shutdownGuest }; } diff --git a/server/src/routes/integrations.ts b/server/src/routes/integrations.ts index b377aa7..59440c6 100644 --- a/server/src/routes/integrations.ts +++ b/server/src/routes/integrations.ts @@ -627,7 +627,7 @@ integrationsRouter.get("/:id/proxmox/guests", asyncHandler(async (req, res) => { } })); -const proxmoxActions = ["start", "stop", "restart"] as const; +const proxmoxActions = ["start", "stop", "restart", "shutdown"] as const; for (const action of proxmoxActions) { integrationsRouter.post( diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 7d4db4f..52ce216 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -643,6 +643,8 @@ export const api = { request(`/api/integrations/${integrationId}/proxmox/nodes/${node}/${type}/${vmid}/stop`, { method: "POST" }), restart: (integrationId: number, node: string, type: ProxmoxGuestType, vmid: number) => request(`/api/integrations/${integrationId}/proxmox/nodes/${node}/${type}/${vmid}/restart`, { method: "POST" }), + shutdown: (integrationId: number, node: string, type: ProxmoxGuestType, vmid: number) => + request(`/api/integrations/${integrationId}/proxmox/nodes/${node}/${type}/${vmid}/shutdown`, { method: "POST" }), }, synology: { storage: (integrationId: number) => request(`/api/integrations/${integrationId}/synology/storage`), diff --git a/web/src/pages/Integrations.tsx b/web/src/pages/Integrations.tsx index 2fc28a3..23fd52c 100644 --- a/web/src/pages/Integrations.tsx +++ b/web/src/pages/Integrations.tsx @@ -361,9 +361,10 @@ export default function Integrations({ user }: { user: CurrentUser }) { } } - async function guestAction(g: ProxmoxGuest, action: "start" | "stop" | "restart") { + 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}"?`)) 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 { @@ -845,6 +846,13 @@ export default function Integrations({ user }: { user: CurrentUser }) { > Restart + +