fix(beszel): correct unit scaling for system stats and container charts

Beszel API returns pre-scaled values, not raw bytes as the types claimed:
- Storage (du/mu/mb): GB — remove /BYTES_TO_GB division
- Disk I/O (dr/dw): MB/s — multiply by 1024² for formatters
- Container memory (m): MB — multiply by 1024² for formatters
- Container network (b/ns/nr): bytes/s — passthrough, unchanged

Also fix Recharts width(-1)/height(-1) warnings by adding w=100%
and minHeight:0 to BeszelAreaChart.
This commit is contained in:
Thomas Camlong committed 2026-06-14 17:05:34 +02:00
1 parent f324f58e79
commit f0e20eb3da
4 files changed
+49 -41

No files matched your search

@@ -94,8 +94,8 @@ export interface BeszelSystemDetails {
*
* Units:
* - CPU/GPU/memory/disk percentages: 0–100
* - Memory/disk absolute values (mu, du, etc.): bytes
* - Disk I/O (dr, dw): bytes/s
* - Memory/disk absolute values (mu, du, m, d, etc.): GB
* - Disk I/O (dr, dw): MB/s
* - Network (ns, nr): bytes/s — all interfaces (including loopback, docker bridges)
* - Bandwidth (b): bytes/s [sent, recv] — public interfaces only. Prefer b over ns/nr for user-facing charts
*/
@@ -110,35 +110,35 @@ export interface BeszelSystemStats {
cpus?: number[];
/** load average [1m, 5m, 15m] */
la?: [number, number, number];
/** total memory (bytes) */
/** total memory (GB) */
m: number;
/** memory used (bytes) */
/** memory used (GB) */
mu: number;
/** memory usage (%) */
mp: number;
/** memory buffer/cache (bytes) */
/** memory buffer/cache (GB) */
mb: number;
/** memory max used (bytes) */
/** memory max used (GB) */
mm?: number;
/** memory zfs arc (bytes) */
/** memory zfs arc (GB) */
mz?: number;
/** total swap (bytes) */
/** total swap (GB) */
s: number;
/** swap used (bytes) */
/** swap used (GB) */
su: number;
/** total disk (bytes) */
/** total disk (GB) */
d: number;
/** disk used (bytes) */
/** disk used (GB) */
du: number;
/** disk usage (%) */
dp: number;
/** disk read (bytes/s) */
/** disk read (MB/s) */
dr: number;
/** disk write (bytes/s) */
/** disk write (MB/s) */
dw: number;
/** disk read max (bytes/s) */
/** disk read max (MB/s) */
drm?: number;
/** disk write max (bytes/s) */
/** disk write max (MB/s) */
dwm?: number;
/** disk IOPS [read, write] */
dio?: [number, number];
@@ -236,13 +236,14 @@ export interface BeszelContainer {
* One entry per container in a BeszelContainerStatsRecord.
*
* Network fields: b (bandwidth) is preferred when available; fall back to ns/nr.
* Units: m = MB, c = %, b/ns/nr = bytes/s
*/
export interface BeszelContainerStats {
/** container name */
n: string;
/** CPU usage (%) */
c: number;
/** memory usage (bytes) */
/** memory usage (MB) */
m: number;
/** network sent (bytes/s) — legacy, use b when available */
ns?: number;
+18 -14
View File
@@ -10,7 +10,6 @@ import type {
} from "../../beszel/beszel-types";
const GB = 1024 * 1024 * 1024;
const MB = 1024 * 1024;
const KB = 1024;
interface MockSystemDef {
@@ -144,26 +143,27 @@ function generateSystemStatsSeries(count: number, sys: MockSystemDef): BeszelSys
const netSendWalk = smoothWalk(500, 200, count, 10, 8000);
const netRecvWalk = smoothWalk(800, 300, count, 10, 12000);
const memTotal = sys.mem;
const diskTotal = rand(200, 2000) * GB;
return cpuWalk.map((cpu, i) => {
const memPct = memPctWalk[i] ?? sys.baseMem;
const diskPct = diskPctWalk[i] ?? sys.baseDisk;
const memTotalGB = sys.mem / GB;
const diskTotalGB = diskTotal / GB;
return {
cpu,
la: [cpu / 20 + rand(0, 0.5), cpu / 25 + rand(0, 0.3), cpu / 30 + rand(0, 0.2)] as [number, number, number],
m: memTotal,
mu: (memTotal * memPct) / 100,
m: memTotalGB,
mu: memTotalGB * memPct / 100,
mp: memPct,
mb: rand(0.5, 4) * GB,
s: 4 * GB,
su: rand(0, 2) * GB,
d: diskTotal,
du: (diskTotal * diskPct) / 100,
mb: rand(0.5, 4),
s: 4,
su: rand(0, 2),
d: diskTotalGB,
du: diskTotalGB * diskPct / 100,
dp: diskPct,
dr: (diskReadWalk[i] ?? 5) * MB,
dw: (diskWriteWalk[i] ?? 3) * MB,
dr: (diskReadWalk[i] ?? 5),
dw: (diskWriteWalk[i] ?? 3),
ns: (netSendWalk[i] ?? 500) * KB,
nr: (netRecvWalk[i] ?? 800) * KB,
b: [(netSendWalk[i] ?? 500) * KB * 0.7, (netRecvWalk[i] ?? 800) * KB * 0.7] as [number, number],
@@ -183,7 +183,7 @@ function generateContainerStatsSeries(count: number, profiles: typeof containerP
walks.map(({ profile, cpuWalk, memWalk, netWalk }) => ({
n: profile.name,
c: cpuWalk[i] ?? profile.cpuBase,
m: (memWalk[i] ?? profile.memMB) * MB,
m: memWalk[i] ?? profile.memMB,
b: [(netWalk[i] ?? profile.netKB) * KB * 0.4, (netWalk[i] ?? profile.netKB) * KB * 0.6] as [number, number],
})),
);
@@ -264,13 +264,15 @@ export class BeszelMockService {
const sys = resolveSystem(systemId);
const intervalMinutes = typeToMinutes[type] ?? 1;
const statsSeries = generateSystemStatsSeries(perPage, sys);
return buildTimeSeries(
const result = buildTimeSeries(
systemId,
perPage,
intervalMinutes,
type,
statsSeries.map((stats) => ({ stats })),
);
return result;
}
public async getContainerStatsAsync(
@@ -284,13 +286,15 @@ export class BeszelMockService {
.map((i) => containerProfiles[i])
.filter(Boolean) as typeof containerProfiles;
const series = generateContainerStatsSeries(perPage, selectedProfiles);
return buildTimeSeries(
const result = buildTimeSeries(
systemId,
perPage,
intervalMinutes,
type,
series.map((stats) => ({ stats })),
);
return result;
}
public async getAlertsAsync(): Promise<BeszelAlert[]> {
@@ -63,23 +63,23 @@ export default function BeszelSystemStatsWidget({
{ staleTime: 30 * 1000 },
);
// BeszelSystemStats fields are documented in beszel-types.ts.
// mu/du = bytes, dr/dw = bytes/s, b = public bandwidth [sent,recv] bytes/s, ns/nr = all-interface bytes/s
const BYTES_TO_GB = 1024 * 1024 * 1024;
// BeszelSystemStats fields: storage values (m, mu, mb, d, du, s, su) are already in GB.
// Disk I/O (dr, dw) is in MB/s — scale to bytes/s for display formatters.
// Network (ns, nr, b) is in bytes/s. Container memory (m) is in MB.
const cpuMap = useCallback((s: { cpu: number }) => ({ [t("chart.cpu.series")]: s.cpu }), [t]);
const memoryMap = useCallback(
(s: { mu: number; mb: number }) => ({
[t("chart.memory.series")]: s.mu / BYTES_TO_GB,
[t("chart.memory.cache")]: (s.mb ?? 0) / BYTES_TO_GB,
[t("chart.memory.series")]: s.mu,
[t("chart.memory.cache")]: s.mb ?? 0,
}),
[t],
);
const diskMap = useCallback((s: { du: number }) => ({ [t("chart.disk.series")]: s.du / BYTES_TO_GB }), [t]);
const diskMap = useCallback((s: { du: number }) => ({ [t("chart.disk.series")]: s.du }), [t]);
const diskIOMap = useCallback(
(s: { dr?: number; dw?: number }) => ({
[t("chart.diskIO.read")]: s.dr ?? 0,
[t("chart.diskIO.write")]: s.dw ?? 0,
[t("chart.diskIO.read")]: (s.dr ?? 0) * (1024 * 1024),
[t("chart.diskIO.write")]: (s.dw ?? 0) * (1024 * 1024),
}),
[t],
);
@@ -46,7 +46,8 @@ export const BeszelAreaChart = ({ yAxisFormatter, yAxisDomain, yAxisProps, style
withDots={false}
withXAxis
withYAxis
style={{ minWidth: 0, ...style }}
w="100%"
style={{ minWidth: 0, minHeight: 0, ...style }}
yAxisProps={{
...yAxisBase,
...(yAxisDomain ? { domain: yAxisDomain } : {}),
@@ -83,10 +84,12 @@ export const useContainerNames = (containerStats: BeszelContainerStatsRecord[] |
type ContainerExtractor = (container: BeszelContainerStatsRecord["stats"][number] | undefined) => number;
// c = CPU (%), m = memory (bytes), b = bandwidth [sent,recv] (bytes/s), ns/nr = legacy net (bytes/s)
// c = CPU (%), m = memory (MB), b = bandwidth [sent,recv] (bytes/s), ns/nr = legacy net (bytes/s)
const MB = 1024 * 1024;
const defaultContainerExtractors: Record<string, ContainerExtractor> = {
cpu: (c) => c?.c ?? 0,
memory: (c) => c?.m ?? 0,
memory: (c) => (c?.m ?? 0) * MB,
network: (c) => (c?.b ? c.b[0] + c.b[1] : (c?.ns ?? 0) + (c?.nr ?? 0)),
};