Split Proxmox node card into system info (left) / storage (right)

Was a single stacked column (uptime/CPU/memory/swap, then the storage
table below); now a two-column layout within the card body — system
stats on the left, storage table (or the missing-permission hint) on
the right — so the card reads more like a dashboard tile at a glance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 20:50:16 +02:00
co-authored by Claude Sonnet 5
parent 846953194c
commit 7b39b7be4d
+58 -56
View File
@@ -186,68 +186,70 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
<div className="card-body">
{n.error && <div className="alert alert-danger">{n.error}</div>}
<div className="row g-3">
<div className="col-sm-6">
<div className="text-secondary small">Uptime</div>
<div>{formatUptime(n.uptime)}</div>
</div>
<div className="col-sm-6">
<div className="text-secondary small">CPU</div>
<div>
{n.cpuUsagePercent !== null ? `${n.cpuUsagePercent.toFixed(1)}%` : "—"}
{n.cpuCores ? <span className="text-secondary"> ({n.cpuCores} cores)</span> : null}
<div className="col-md-6">
<div className="mb-3">
<div className="text-secondary small">Uptime</div>
<div>{formatUptime(n.uptime)}</div>
</div>
{n.loadAverage && (
<div className="text-secondary small">Load: {n.loadAverage.map((l) => l.toFixed(2)).join(", ")}</div>
<div className="mb-3">
<div className="text-secondary small">CPU</div>
<div>
{n.cpuUsagePercent !== null ? `${n.cpuUsagePercent.toFixed(1)}%` : "—"}
{n.cpuCores ? <span className="text-secondary"> ({n.cpuCores} cores)</span> : null}
</div>
{n.loadAverage && (
<div className="text-secondary small">Load: {n.loadAverage.map((l) => l.toFixed(2)).join(", ")}</div>
)}
</div>
<div className={n.swapTotalBytes ? "mb-3" : ""}>
<div className="text-secondary small">Memory</div>
<div>
{formatBytes(n.memUsedBytes)} / {formatBytes(n.memTotalBytes)}
</div>
{usageBar(n.memUsedBytes, n.memTotalBytes)}
</div>
{!!n.swapTotalBytes && (
<div>
<div className="text-secondary small">Swap</div>
<div>
{formatBytes(n.swapUsedBytes)} / {formatBytes(n.swapTotalBytes)}
</div>
{usageBar(n.swapUsedBytes, n.swapTotalBytes)}
</div>
)}
</div>
<div className="col-sm-6">
<div className="text-secondary small">Memory</div>
<div>
{formatBytes(n.memUsedBytes)} / {formatBytes(n.memTotalBytes)}
</div>
{usageBar(n.memUsedBytes, n.memTotalBytes)}
</div>
{!!n.swapTotalBytes && (
<div className="col-sm-6">
<div className="text-secondary small">Swap</div>
<div>
{formatBytes(n.swapUsedBytes)} / {formatBytes(n.swapTotalBytes)}
</div>
{usageBar(n.swapUsedBytes, n.swapTotalBytes)}
</div>
)}
</div>
{n.storages.length > 0 ? (
<div className="mt-3">
<div className="col-md-6">
<div className="text-secondary small mb-1">Storage</div>
<table className="table table-sm table-vcenter mb-0">
<tbody>
{n.storages.map((s) => (
<tr key={s.id}>
<td>
{s.id}
{!s.active && <span className="badge bg-secondary-lt text-secondary ms-1">Inactive</span>}
</td>
<td className="text-secondary">{s.type}</td>
<td>
{formatBytes(s.usedBytes)} / {formatBytes(s.totalBytes)}
{usageBar(s.usedBytes, s.totalBytes)}
</td>
</tr>
))}
</tbody>
</table>
{n.storages.length > 0 ? (
<table className="table table-sm table-vcenter mb-0">
<tbody>
{n.storages.map((s) => (
<tr key={s.id}>
<td>
{s.id}
{!s.active && <span className="badge bg-secondary-lt text-secondary ms-1">Inactive</span>}
</td>
<td className="text-secondary">{s.type}</td>
<td>
{formatBytes(s.usedBytes)} / {formatBytes(s.totalBytes)}
{usageBar(s.usedBytes, s.totalBytes)}
</td>
</tr>
))}
</tbody>
</table>
) : (
!n.error && (
<div className="text-secondary small">
No storage visible to this API token. Proxmox filters the storage list
per-token — grant it <code>Datastore.Audit</code> (e.g. on{" "}
<code>/storage</code>) to see disk usage here.
</div>
)
)}
</div>
) : (
!n.error && (
<div className="text-secondary small mt-3">
No storage visible to this API token. Proxmox filters the storage list per-token —
grant it <code>Datastore.Audit</code> (e.g. on <code>/storage</code>) to see disk
usage here.
</div>
)
)}
</div>
</div>
</div>
</div>