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:
1 parent
1a054d6c2f
commit
de9b6a2d6a
2 files changed
+60
-2
No files matched your search
@@ -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
|
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
|
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
|
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,
|
- **Integrations → Tailscale** — device list with online/authorized status,
|
||||||
and authorize/deauthorize/remove actions; a live device-count widget.
|
and authorize/deauthorize/remove actions; a live device-count widget.
|
||||||
- **Integrations → Gitea** — repo list with each repo's last CI run status,
|
- **Integrations → Gitea** — repo list with each repo's last CI run status,
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
|
|||||||
const [linking, setLinking] = useState(false);
|
const [linking, setLinking] = useState(false);
|
||||||
const [linkError, setLinkError] = useState<string | null>(null);
|
const [linkError, setLinkError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [actingOnGuest, setActingOnGuest] = useState(false);
|
||||||
|
const [guestActionError, setGuestActionError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadDetail = useCallback(() => {
|
const loadDetail = useCallback(() => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
return api.servers
|
return api.servers
|
||||||
@@ -227,6 +230,26 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
|
|||||||
|
|
||||||
const { server, hardware, ipAddresses, dnsMatches } = detail;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3">
|
<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.error && <div className="alert alert-danger">{hardware.error}</div>}
|
||||||
{hardware.source === "proxmox" && !hardware.error && (
|
{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="row g-3">
|
||||||
<div className="col-sm-6">
|
<div className="col-sm-6">
|
||||||
<div className="text-secondary small">CPU</div>
|
<div className="text-secondary small">CPU</div>
|
||||||
|
|||||||
Reference in new issue
Block a user