Show memory and disk capacity on dashboard tiles #20
@@ -76,6 +76,10 @@ export class ProxmoxCollector implements Collector {
|
|||||||
cpuPct: round(nodeStatus.cpu * 100),
|
cpuPct: round(nodeStatus.cpu * 100),
|
||||||
memPct: round((nodeStatus.memory.used / nodeStatus.memory.total) * 100),
|
memPct: round((nodeStatus.memory.used / nodeStatus.memory.total) * 100),
|
||||||
diskPct: round((nodeStatus.rootfs.used / nodeStatus.rootfs.total) * 100),
|
diskPct: round((nodeStatus.rootfs.used / nodeStatus.rootfs.total) * 100),
|
||||||
|
memTotalBytes: nodeStatus.memory.total,
|
||||||
|
memUsedBytes: nodeStatus.memory.used,
|
||||||
|
diskTotalBytes: nodeStatus.rootfs.total,
|
||||||
|
diskUsedBytes: nodeStatus.rootfs.used,
|
||||||
memPressurePct: null,
|
memPressurePct: null,
|
||||||
cpuPressurePct: null,
|
cpuPressurePct: null,
|
||||||
uptimeSec: nodeStatus.uptime,
|
uptimeSec: nodeStatus.uptime,
|
||||||
@@ -92,6 +96,10 @@ export class ProxmoxCollector implements Collector {
|
|||||||
cpuPct: running ? round(ct.cpu * 100) : null,
|
cpuPct: running ? round(ct.cpu * 100) : null,
|
||||||
memPct: running && ct.maxmem > 0 ? round((ct.mem / ct.maxmem) * 100) : null,
|
memPct: running && ct.maxmem > 0 ? round((ct.mem / ct.maxmem) * 100) : null,
|
||||||
diskPct: ct.maxdisk > 0 ? round((ct.disk / ct.maxdisk) * 100) : null,
|
diskPct: ct.maxdisk > 0 ? round((ct.disk / ct.maxdisk) * 100) : null,
|
||||||
|
memTotalBytes: running && ct.maxmem > 0 ? ct.maxmem : null,
|
||||||
|
memUsedBytes: running && ct.maxmem > 0 ? ct.mem : null,
|
||||||
|
diskTotalBytes: ct.maxdisk > 0 ? ct.maxdisk : null,
|
||||||
|
diskUsedBytes: ct.maxdisk > 0 ? ct.disk : null,
|
||||||
memPressurePct: ct.pressurememoryfull ? round(Number(ct.pressurememoryfull)) : null,
|
memPressurePct: ct.pressurememoryfull ? round(Number(ct.pressurememoryfull)) : null,
|
||||||
cpuPressurePct: ct.pressurecpusome ? round(Number(ct.pressurecpusome)) : null,
|
cpuPressurePct: ct.pressurecpusome ? round(Number(ct.pressurecpusome)) : null,
|
||||||
uptimeSec: running ? ct.uptime : null,
|
uptimeSec: running ? ct.uptime : null,
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export class SshHostCollector implements Collector {
|
|||||||
cpuPct: null,
|
cpuPct: null,
|
||||||
memPct: null,
|
memPct: null,
|
||||||
diskPct: null,
|
diskPct: null,
|
||||||
|
memTotalBytes: null,
|
||||||
|
memUsedBytes: null,
|
||||||
|
diskTotalBytes: null,
|
||||||
|
diskUsedBytes: null,
|
||||||
memPressurePct: null,
|
memPressurePct: null,
|
||||||
cpuPressurePct: null,
|
cpuPressurePct: null,
|
||||||
uptimeSec: null,
|
uptimeSec: null,
|
||||||
@@ -118,11 +122,19 @@ export class SshHostCollector implements Collector {
|
|||||||
|
|
||||||
const uptimeSec = Number(vars.get("UPTIME") ?? "0") || null;
|
const uptimeSec = Number(vars.get("UPTIME") ?? "0") || null;
|
||||||
|
|
||||||
|
// Assumes the remote script's "size:used" fields are raw bytes (matches
|
||||||
|
// ProxmoxCollector's units) — the script itself lives only on omv/ripper,
|
||||||
|
// not in this repo, so this hasn't been directly confirmed. If the
|
||||||
|
// capacity shown in the UI for these two hosts looks off by a factor of
|
||||||
|
// 1024/1024^2, the remote script is emitting KiB/MiB instead and this
|
||||||
|
// needs a matching scale factor.
|
||||||
const disks = this.cfg.diskPaths.map((d) => {
|
const disks = this.cfg.diskPaths.map((d) => {
|
||||||
const [size, used] = (vars.get(`DISK_${d.label}`) ?? "0:0").split(":").map(Number);
|
const [size, used] = (vars.get(`DISK_${d.label}`) ?? "0:0").split(":").map(Number);
|
||||||
return { label: d.label, pct: size > 0 ? round((used / size) * 100) : null };
|
return { label: d.label, pct: size > 0 ? round((used / size) * 100) : null, size, used };
|
||||||
});
|
});
|
||||||
const primaryDiskPct = disks[0]?.pct ?? null;
|
const primaryDiskPct = disks[0]?.pct ?? null;
|
||||||
|
const primaryDiskTotalBytes = disks[0] && disks[0].size > 0 ? disks[0].size : null;
|
||||||
|
const primaryDiskUsedBytes = disks[0] && disks[0].size > 0 ? disks[0].used : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hostId: `ssh:${this.cfg.id}`,
|
hostId: `ssh:${this.cfg.id}`,
|
||||||
@@ -132,6 +144,10 @@ export class SshHostCollector implements Collector {
|
|||||||
cpuPct,
|
cpuPct,
|
||||||
memPct,
|
memPct,
|
||||||
diskPct: primaryDiskPct,
|
diskPct: primaryDiskPct,
|
||||||
|
memTotalBytes: memTotal > 0 ? memTotal : null,
|
||||||
|
memUsedBytes: memTotal > 0 ? memUsed : null,
|
||||||
|
diskTotalBytes: primaryDiskTotalBytes,
|
||||||
|
diskUsedBytes: primaryDiskUsedBytes,
|
||||||
memPressurePct: null,
|
memPressurePct: null,
|
||||||
cpuPressurePct: null,
|
cpuPressurePct: null,
|
||||||
uptimeSec,
|
uptimeSec,
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export interface MetricSnapshot {
|
|||||||
cpuPct: number | null;
|
cpuPct: number | null;
|
||||||
memPct: number | null;
|
memPct: number | null;
|
||||||
diskPct: number | null;
|
diskPct: number | null;
|
||||||
|
memTotalBytes: number | null;
|
||||||
|
memUsedBytes: number | null;
|
||||||
|
diskTotalBytes: number | null;
|
||||||
|
diskUsedBytes: number | null;
|
||||||
memPressurePct: number | null;
|
memPressurePct: number | null;
|
||||||
cpuPressurePct: number | null;
|
cpuPressurePct: number | null;
|
||||||
uptimeSec: number | null;
|
uptimeSec: number | null;
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export function openDb(path: string): Database.Database {
|
|||||||
cpu_pct REAL,
|
cpu_pct REAL,
|
||||||
mem_pct REAL,
|
mem_pct REAL,
|
||||||
disk_pct REAL,
|
disk_pct REAL,
|
||||||
|
mem_total_bytes REAL,
|
||||||
|
mem_used_bytes REAL,
|
||||||
|
disk_total_bytes REAL,
|
||||||
|
disk_used_bytes REAL,
|
||||||
mem_pressure_pct REAL,
|
mem_pressure_pct REAL,
|
||||||
cpu_pressure_pct REAL,
|
cpu_pressure_pct REAL,
|
||||||
uptime_sec INTEGER,
|
uptime_sec INTEGER,
|
||||||
@@ -70,6 +74,15 @@ export function openDb(path: string): Database.Database {
|
|||||||
is_bootstrap INTEGER NOT NULL DEFAULT 0
|
is_bootstrap INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
// Migration for metric_snapshots pre-dating the raw byte capacity columns
|
||||||
|
// (CREATE TABLE IF NOT EXISTS above doesn't touch already-existing tables).
|
||||||
|
const snapshotColumns = (db.pragma("table_info(metric_snapshots)") as { name: string }[]).map(
|
||||||
|
(c) => c.name
|
||||||
|
);
|
||||||
|
for (const col of ["mem_total_bytes", "mem_used_bytes", "disk_total_bytes", "disk_used_bytes"]) {
|
||||||
|
if (!snapshotColumns.includes(col)) db.exec(`ALTER TABLE metric_snapshots ADD COLUMN ${col} REAL`);
|
||||||
|
}
|
||||||
|
|
||||||
// Migration for the devices table pre-dating mdns_hostname (CREATE TABLE IF
|
// Migration for the devices table pre-dating mdns_hostname (CREATE TABLE IF
|
||||||
// NOT EXISTS above doesn't touch already-existing tables). SQLite's ALTER
|
// NOT EXISTS above doesn't touch already-existing tables). SQLite's ALTER
|
||||||
// TABLE ADD COLUMN has no IF NOT EXISTS clause (unlike CREATE TABLE/INDEX),
|
// TABLE ADD COLUMN has no IF NOT EXISTS clause (unlike CREATE TABLE/INDEX),
|
||||||
@@ -103,9 +116,11 @@ export function upsertSnapshots(db: Database.Database, snapshots: MetricSnapshot
|
|||||||
`);
|
`);
|
||||||
const insertSnapshot = db.prepare(`
|
const insertSnapshot = db.prepare(`
|
||||||
INSERT INTO metric_snapshots
|
INSERT INTO metric_snapshots
|
||||||
(host_id, status, cpu_pct, mem_pct, disk_pct, mem_pressure_pct, cpu_pressure_pct, uptime_sec, meta_json)
|
(host_id, status, cpu_pct, mem_pct, disk_pct, mem_total_bytes, mem_used_bytes,
|
||||||
|
disk_total_bytes, disk_used_bytes, mem_pressure_pct, cpu_pressure_pct, uptime_sec, meta_json)
|
||||||
VALUES
|
VALUES
|
||||||
(@hostId, @status, @cpuPct, @memPct, @diskPct, @memPressurePct, @cpuPressurePct, @uptimeSec, @metaJson)
|
(@hostId, @status, @cpuPct, @memPct, @diskPct, @memTotalBytes, @memUsedBytes,
|
||||||
|
@diskTotalBytes, @diskUsedBytes, @memPressurePct, @cpuPressurePct, @uptimeSec, @metaJson)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const tx = db.transaction((items: MetricSnapshot[]) => {
|
const tx = db.transaction((items: MetricSnapshot[]) => {
|
||||||
@@ -125,6 +140,10 @@ export interface LatestRow {
|
|||||||
cpu_pct: number | null;
|
cpu_pct: number | null;
|
||||||
mem_pct: number | null;
|
mem_pct: number | null;
|
||||||
disk_pct: number | null;
|
disk_pct: number | null;
|
||||||
|
mem_total_bytes: number | null;
|
||||||
|
mem_used_bytes: number | null;
|
||||||
|
disk_total_bytes: number | null;
|
||||||
|
disk_used_bytes: number | null;
|
||||||
mem_pressure_pct: number | null;
|
mem_pressure_pct: number | null;
|
||||||
cpu_pressure_pct: number | null;
|
cpu_pressure_pct: number | null;
|
||||||
uptime_sec: number | null;
|
uptime_sec: number | null;
|
||||||
@@ -136,7 +155,8 @@ export function getLatestSnapshots(db: Database.Database): LatestRow[] {
|
|||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
SELECT h.host_id, h.display_name, h.group_name, s.status, s.cpu_pct, s.mem_pct,
|
SELECT h.host_id, h.display_name, h.group_name, s.status, s.cpu_pct, s.mem_pct,
|
||||||
s.disk_pct, s.mem_pressure_pct, s.cpu_pressure_pct, s.uptime_sec, s.ts
|
s.disk_pct, s.mem_total_bytes, s.mem_used_bytes, s.disk_total_bytes, s.disk_used_bytes,
|
||||||
|
s.mem_pressure_pct, s.cpu_pressure_pct, s.uptime_sec, s.ts
|
||||||
FROM hosts h
|
FROM hosts h
|
||||||
JOIN metric_snapshots s ON s.host_id = h.host_id
|
JOIN metric_snapshots s ON s.host_id = h.host_id
|
||||||
WHERE s.id = (
|
WHERE s.id = (
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export function registerHostRoutes(app: FastifyInstance, db: Database.Database):
|
|||||||
cpuPct: r.cpu_pct,
|
cpuPct: r.cpu_pct,
|
||||||
memPct: r.mem_pct,
|
memPct: r.mem_pct,
|
||||||
diskPct: r.disk_pct,
|
diskPct: r.disk_pct,
|
||||||
|
memTotalBytes: r.mem_total_bytes,
|
||||||
|
memUsedBytes: r.mem_used_bytes,
|
||||||
|
diskTotalBytes: r.disk_total_bytes,
|
||||||
|
diskUsedBytes: r.disk_used_bytes,
|
||||||
memPressurePct: r.mem_pressure_pct,
|
memPressurePct: r.mem_pressure_pct,
|
||||||
cpuPressurePct: r.cpu_pressure_pct,
|
cpuPressurePct: r.cpu_pressure_pct,
|
||||||
uptimeSec: r.uptime_sec,
|
uptimeSec: r.uptime_sec,
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export interface HostSummary {
|
|||||||
cpuPct: number | null;
|
cpuPct: number | null;
|
||||||
memPct: number | null;
|
memPct: number | null;
|
||||||
diskPct: number | null;
|
diskPct: number | null;
|
||||||
|
memTotalBytes: number | null;
|
||||||
|
memUsedBytes: number | null;
|
||||||
|
diskTotalBytes: number | null;
|
||||||
|
diskUsedBytes: number | null;
|
||||||
memPressurePct: number | null;
|
memPressurePct: number | null;
|
||||||
cpuPressurePct: number | null;
|
cpuPressurePct: number | null;
|
||||||
uptimeSec: number | null;
|
uptimeSec: number | null;
|
||||||
|
|||||||
@@ -15,6 +15,21 @@ function Bar({ label, pct }: { label: string; pct: number | null }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Picks a unit off total capacity, applies it to both sides so "used/total"
|
||||||
|
// reads as e.g. "12.3/32 GB" instead of mixing MB and GB.
|
||||||
|
function formatCapacity(used: number | null, total: number | null): string | null {
|
||||||
|
if (used === null || total === null || total <= 0) return null;
|
||||||
|
const units: [number, string][] = [
|
||||||
|
[1024 ** 4, "TB"],
|
||||||
|
[1024 ** 3, "GB"],
|
||||||
|
[1024 ** 2, "MB"],
|
||||||
|
[1024, "KB"],
|
||||||
|
];
|
||||||
|
const [divisor, unit] = units.find(([d]) => total >= d) ?? [1, "B"];
|
||||||
|
const fmt = (n: number) => (n / divisor >= 100 ? (n / divisor).toFixed(0) : (n / divisor).toFixed(1));
|
||||||
|
return `${fmt(used)}/${fmt(total)} ${unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatUptime(sec: number | null): string {
|
function formatUptime(sec: number | null): string {
|
||||||
if (sec === null) return "—";
|
if (sec === null) return "—";
|
||||||
const days = Math.floor(sec / 86400);
|
const days = Math.floor(sec / 86400);
|
||||||
@@ -25,6 +40,9 @@ function formatUptime(sec: number | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HostCard({ host }: { host: HostSummary }) {
|
export function HostCard({ host }: { host: HostSummary }) {
|
||||||
|
const memCapacity = formatCapacity(host.memUsedBytes, host.memTotalBytes);
|
||||||
|
const diskCapacity = formatCapacity(host.diskUsedBytes, host.diskTotalBytes);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`host-card status-${host.status}`}>
|
<div className={`host-card status-${host.status}`}>
|
||||||
<div className="host-card-header">
|
<div className="host-card-header">
|
||||||
@@ -35,6 +53,12 @@ export function HostCard({ host }: { host: HostSummary }) {
|
|||||||
<Bar label="CPU" pct={host.cpuPct} />
|
<Bar label="CPU" pct={host.cpuPct} />
|
||||||
<Bar label="Mem" pct={host.memPct} />
|
<Bar label="Mem" pct={host.memPct} />
|
||||||
<Bar label="Disk" pct={host.diskPct} />
|
<Bar label="Disk" pct={host.diskPct} />
|
||||||
|
{(memCapacity !== null || diskCapacity !== null) && (
|
||||||
|
<div className="capacity-row">
|
||||||
|
{memCapacity !== null && <span>Mem {memCapacity}</span>}
|
||||||
|
{diskCapacity !== null && <span>Disk {diskCapacity}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Sparkline history={host.history} />
|
<Sparkline history={host.history} />
|
||||||
{(host.cpuPressurePct !== null || host.memPressurePct !== null) && (
|
{(host.cpuPressurePct !== null || host.memPressurePct !== null) && (
|
||||||
<div className="pressure-row">
|
<div className="pressure-row">
|
||||||
|
|||||||
@@ -181,6 +181,15 @@ body {
|
|||||||
margin-top: 0.35rem;
|
margin-top: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.capacity-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.device-count {
|
.device-count {
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|||||||
Reference in New Issue
Block a user