diff --git a/README.md b/README.md
index 287c168..e2378b5 100644
--- a/README.md
+++ b/README.md
@@ -49,8 +49,11 @@ All modules from the original plan are built:
clicking a server opens its detail page with CPU/RAM/disk status, IP
addresses, matching DNS names (looked up from the DNS module's cache), and
its scheduled tasks — live hardware from Proxmox for VM/LXC-backed
- servers, or from the agent's own hardware report for everything else.
- Proxmox-linked servers also get start/stop/restart buttons right on the
+ servers, or from the agent's own hardware report for everything else,
+ both showing the same per-disk usage breakdown (an LXC's root filesystem
+ read straight from the host; a QEMU VM's actual mounts via its guest
+ agent, alongside the allocated size Proxmox already knew about without
+ one). Proxmox-linked servers also get start/stop/restart buttons right on the
detail page. The detail page also has an **Admin Links** section
(operator/admin to add/edit/remove) for bookmarking that server's own
admin UIs — Dockge, Webmin, Cockpit, Portainer, or anything else reachable
diff --git a/server/src/integrations/proxmox/adapter.ts b/server/src/integrations/proxmox/adapter.ts
index 975a426..5649f91 100644
--- a/server/src/integrations/proxmox/adapter.ts
+++ b/server/src/integrations/proxmox/adapter.ts
@@ -50,9 +50,17 @@ export interface ProxmoxGuestDetail {
memoryBytes: number | null;
memUsedBytes: number | null;
diskBytes: number | null;
+ /**
+ * Per-mount usage where Proxmox can actually see it: the LXC root
+ * filesystem (host can see straight into it, no agent needed) or, for a
+ * QEMU VM, whatever the QEMU guest agent reports from inside the guest.
+ * Empty when neither is available (e.g. no guest agent) — diskBytes above
+ * (allocated size) is still shown in that case, just not usage.
+ */
+ disks: { mount: string; sizeBytes: number; usedBytes: number }[];
uptime: number | null;
ipAddresses: string[];
- /** QEMU only — false when the guest agent call failed (not installed/running). Always true for LXC (IPs read from config, no agent needed). */
+ /** QEMU only — false when the guest agent call failed (not installed/running). Always true for LXC (IPs/disk usage read directly, no agent needed). */
guestAgentAvailable: boolean;
}
@@ -228,6 +236,7 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
let cpuCores: number | null = null;
let diskBytes: number | null = null;
const ipAddresses: string[] = [];
+ const disks: { mount: string; sizeBytes: number; usedBytes: number }[] = [];
let guestAgentAvailable = true;
if (type === "lxc") {
@@ -241,6 +250,12 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
if (ip) ipAddresses.push(ip);
}
}
+ // The host can see straight into an LXC's root filesystem — no agent
+ // needed — but the API only exposes the root mount this way, not any
+ // additional mount points configured on the container.
+ if (typeof status.disk === "number" && typeof status.maxdisk === "number" && status.maxdisk > 0) {
+ disks.push({ mount: "/", sizeBytes: status.maxdisk, usedBytes: status.disk });
+ }
} else {
const sockets = typeof config.sockets === "number" ? config.sockets : 1;
cpuCores = typeof config.cores === "number" ? config.cores * sockets : null;
@@ -271,6 +286,27 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
} catch {
guestAgentAvailable = false;
}
+
+ // Unlike LXC, the hypervisor can't see inside a QEMU disk image at
+ // all — actual filesystem usage only exists if the guest agent
+ // reports it from inside the guest, same availability caveat as the
+ // network call above (a separate try/catch since one agent command
+ // failing, e.g. on an older guest agent version, shouldn't hide IPs
+ // the other command already got, or vice versa).
+ try {
+ const fsData = await api("GET", `/nodes/${node}/qemu/${vmid}/agent/get-fsinfo`);
+ for (const fs of fsData?.result ?? []) {
+ // Entries with no backing "disk" (tmpfs, proc, overlay, snap loop
+ // mounts, ...) aren't real storage — skip them, same convention
+ // widely used for this endpoint.
+ if (!Array.isArray(fs.disk) || fs.disk.length === 0) continue;
+ if (typeof fs["total-bytes"] !== "number" || typeof fs["used-bytes"] !== "number") continue;
+ disks.push({ mount: fs.mountpoint ?? fs.name ?? "?", sizeBytes: fs["total-bytes"], usedBytes: fs["used-bytes"] });
+ }
+ } catch {
+ // Guest agent unavailable or too old to support get-fsinfo — leave
+ // disks empty, diskBytes (allocated size) is still shown.
+ }
}
const memoryBytes = typeof config.memory === "number" ? config.memory * 1024 * 1024 : null;
@@ -286,6 +322,7 @@ export function createProxmoxAdapter(config: ProxmoxConfig): ProxmoxAdapter {
memoryBytes,
memUsedBytes: typeof status.mem === "number" ? status.mem : null,
diskBytes,
+ disks,
uptime: typeof status.uptime === "number" ? status.uptime : null,
ipAddresses,
guestAgentAvailable: type === "qemu" ? guestAgentAvailable : true,
diff --git a/server/src/routes/servers.ts b/server/src/routes/servers.ts
index 55859e6..95be6a9 100644
--- a/server/src/routes/servers.ts
+++ b/server/src/routes/servers.ts
@@ -180,6 +180,7 @@ serversRouter.get("/:id/detail", asyncHandler(async (req, res) => {
memTotalBytes: detail.memoryBytes,
memUsedBytes: detail.memUsedBytes,
diskBytes: detail.diskBytes,
+ disks: detail.disks,
uptime: detail.uptime,
guestAgentAvailable: detail.guestAgentAvailable,
};
diff --git a/web/src/pages/ServerDetail.tsx b/web/src/pages/ServerDetail.tsx
index 852adac..e217dc7 100644
--- a/web/src/pages/ServerDetail.tsx
+++ b/web/src/pages/ServerDetail.tsx
@@ -49,6 +49,28 @@ function usageBar(used: number | null | undefined, total: number | null | undefi
);
}
+function DisksTable({ disks }: { disks: { mount: string; sizeBytes: number; usedBytes: number }[] }) {
+ if (disks.length === 0) return null;
+ return (
+
+
Disks
+
+
+ {disks.map((d) => (
+
+ | {d.mount} |
+
+ {formatBytes(d.usedBytes)} / {formatBytes(d.sizeBytes)}
+ {usageBar(d.usedBytes, d.sizeBytes)}
+ |
+
+ ))}
+
+
+
+ );
+}
+
const emptyTaskForm = {
scheduleType: "docker" as ScheduleType,
name: "",
@@ -570,11 +592,21 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
{formatUptime(hardware.uptime)}
+
{hardware.guestAgentAvailable === false && (
IP addresses unavailable — the QEMU guest agent isn't installed or running on this VM.
)}
+ {server.proxmoxGuestType === "qemu" &&
+ hardware.guestAgentAvailable !== false &&
+ !hardware.disks?.length &&
+ hardware.status === "running" && (
+
+ Per-disk usage unavailable — the QEMU guest agent doesn't support reporting it (or is still
+ starting up).
+
+ )}
>
)}
{hardware.source === "agent" && (
@@ -601,24 +633,7 @@ export default function ServerDetail({ user }: { user: CurrentUser }) {
{usageBar(hardware.memUsedBytes, hardware.memTotalBytes)}
- {hardware.disks && hardware.disks.length > 0 && (
-
-
Disks
-
-
- {hardware.disks.map((d) => (
-
- | {d.mount} |
-
- {formatBytes(d.usedBytes)} / {formatBytes(d.sizeBytes)}
- {usageBar(d.usedBytes, d.sizeBytes)}
- |
-
- ))}
-
-
-
- )}
+
>
)}