fix(beszel-mock): improve mock data fidelity and demo coverage

- Use correct time intervals per period (not always 1min)
- Return newest-first sort order matching real Beszel API
- Pass systemId into time series records
- Stable container sets per system (deterministic from system def)
- Fix pi-cluster arch to aarch64
- Clamp cpu/mem percentages to [0, 100]
- Add beszelSystemStats widget to demo seed
This commit is contained in:
Thomas Camlong committed 2026-06-08 16:14:22 +02:00
1 parent 52d5c050b6
commit cfa510dbf9
2 files changed
+144 -146

No files matched your search

+3 -2
View File
@@ -412,8 +412,9 @@ const buildDemoWidgets = (appIds: string[]): DemoWidget[] => [
// Row 9: beszelSystemGrid + beszelAlerts = 12
{ kind: "beszelSystemGrid", width: 8, height: 3, needsIntegration: true },
{ kind: "beszelAlerts", width: 4, height: 3, needsIntegration: true },
// Row 10: beszelSystemTable = 12
{ kind: "beszelSystemTable", width: 12, height: 3, needsIntegration: true },
// Row 10: beszelSystemTable + beszelSystemStats = 12
{ kind: "beszelSystemTable", width: 6, height: 3, needsIntegration: true },
{ kind: "beszelSystemStats", width: 6, height: 4, needsIntegration: true },
// Row 11: notebook + dockerContainers + weather = 12
{ kind: "notebook", width: 4, height: 4, needsIntegration: false },
{ kind: "dockerContainers", width: 6, height: 2, needsIntegration: false },
+141 -144
View File
@@ -9,109 +9,133 @@ import type {
BeszelSystemStatsRecord,
} from "../../beszel/beszel-types";
const mockSystems: { name: string; host: string; os: string; cpu: string; cores: number; mem: number }[] = [
{
name: "web-server",
host: "192.168.1.10",
os: "Ubuntu 24.04",
cpu: "AMD Ryzen 9 7950X",
cores: 16,
mem: 64 * 1024 * 1024 * 1024,
},
{
name: "nas-01",
host: "192.168.1.20",
os: "Debian 12",
cpu: "Intel Xeon E-2388G",
cores: 8,
mem: 32 * 1024 * 1024 * 1024,
},
{
name: "pi-cluster-1",
host: "192.168.1.30",
os: "Raspberry Pi OS",
cpu: "ARM Cortex-A76",
cores: 4,
mem: 8 * 1024 * 1024 * 1024,
},
{
name: "media-server",
host: "192.168.1.40",
os: "Proxmox VE 8.2",
cpu: "Intel i7-13700K",
cores: 16,
mem: 128 * 1024 * 1024 * 1024,
},
{
name: "backup-node",
host: "192.168.1.50",
os: "TrueNAS SCALE",
cpu: "Intel Xeon E5-2680",
cores: 12,
mem: 48 * 1024 * 1024 * 1024,
},
const GB = 1024 * 1024 * 1024;
const MB = 1024 * 1024;
const KB = 1024;
interface MockSystemDef {
name: string;
host: string;
os: string;
cpu: string;
cores: number;
mem: number;
arch: string;
baseCpu: number;
baseMem: number;
baseDisk: number;
containers: number[];
}
const mockSystems: MockSystemDef[] = [
{ name: "web-server", host: "192.168.1.10", os: "Ubuntu 24.04", cpu: "AMD Ryzen 9 7950X", cores: 16, mem: 64 * GB, arch: "amd64", baseCpu: 35, baseMem: 55, baseDisk: 42, containers: [0, 1, 2, 3, 5] },
{ name: "nas-01", host: "192.168.1.20", os: "Debian 12", cpu: "Intel Xeon E-2388G", cores: 8, mem: 32 * GB, arch: "amd64", baseCpu: 12, baseMem: 70, baseDisk: 68, containers: [1, 2, 6] },
{ name: "pi-cluster-1", host: "192.168.1.30", os: "Raspberry Pi OS", cpu: "ARM Cortex-A76", cores: 4, mem: 8 * GB, arch: "aarch64", baseCpu: 45, baseMem: 82, baseDisk: 35, containers: [0, 2, 7] },
{ name: "media-server", host: "192.168.1.40", os: "Proxmox VE 8.2", cpu: "Intel i7-13700K", cores: 16, mem: 128 * GB, arch: "amd64", baseCpu: 22, baseMem: 48, baseDisk: 55, containers: [0, 1, 2, 3, 4, 5, 6, 7] },
{ name: "backup-node", host: "192.168.1.50", os: "TrueNAS SCALE", cpu: "Intel Xeon E5-2680", cores: 12, mem: 48 * GB, arch: "amd64", baseCpu: 8, baseMem: 30, baseDisk: 78, containers: [1, 6] },
];
const containerNames = ["nginx", "postgres", "redis", "grafana", "prometheus", "traefik", "minio", "gitea"];
const containerProfiles: { name: string; cpuBase: number; memMB: number; netKB: number }[] = [
{ name: "nginx", cpuBase: 2, memMB: 120, netKB: 800 },
{ name: "postgres", cpuBase: 8, memMB: 450, netKB: 200 },
{ name: "redis", cpuBase: 3, memMB: 180, netKB: 500 },
{ name: "grafana", cpuBase: 5, memMB: 280, netKB: 150 },
{ name: "prometheus", cpuBase: 6, memMB: 350, netKB: 300 },
{ name: "traefik", cpuBase: 4, memMB: 90, netKB: 1200 },
{ name: "minio", cpuBase: 3, memMB: 200, netKB: 600 },
{ name: "gitea", cpuBase: 4, memMB: 250, netKB: 100 },
];
const rand = (min: number, max: number) => min + Math.random() * (max - min);
const randInt = (min: number, max: number) => Math.floor(rand(min, max));
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
const BYTES_GB = 1024 * 1024 * 1024;
const BYTES_MB = 1024 * 1024;
function generateSystemStats(baseLoad: number): BeszelSystemStats {
const cpuJitter = rand(-5, 5);
const memTotal = rand(8, 128) * BYTES_GB;
const memUsed = memTotal * rand(0.3, 0.85);
const diskTotal = rand(200, 2000) * BYTES_GB;
const diskUsed = diskTotal * rand(0.2, 0.7);
return {
cpu: Math.max(0, Math.min(100, baseLoad + cpuJitter)),
la: [rand(0.1, 4), rand(0.1, 3), rand(0.1, 2)],
m: memTotal,
mu: memUsed,
mp: (memUsed / memTotal) * 100,
mb: rand(0.5, 4) * BYTES_GB,
s: 4 * BYTES_GB,
su: rand(0, 2) * BYTES_GB,
d: diskTotal,
du: diskUsed,
dp: (diskUsed / diskTotal) * 100,
dr: rand(0, 50) * BYTES_MB,
dw: rand(0, 30) * BYTES_MB,
ns: rand(100, 5000) * 1024,
nr: rand(100, 8000) * 1024,
b: [rand(50, 3000) * 1024, rand(100, 5000) * 1024],
};
function smoothWalk(base: number, amplitude: number, steps: number, lo: number, hi: number): number[] {
const values: number[] = [];
let current = base + rand(-amplitude * 0.3, amplitude * 0.3);
for (let i = 0; i < steps; i++) {
const spike = Math.random() < 0.08 ? rand(-amplitude * 2, amplitude * 3) : 0;
current += rand(-amplitude, amplitude) + spike;
current = clamp(current, lo, hi);
values.push(current);
}
return values;
}
function generateContainerStats(names: string[]): BeszelContainerStats[] {
return names.map((n) => ({
n,
c: rand(0, 25),
m: rand(50, 500) * BYTES_MB,
b: [rand(0, 500) * 1024, rand(0, 800) * 1024],
function resolveSystem(systemId: string): MockSystemDef {
const idx = Number(systemId.replace("mock-sys-", "")) - 1;
return mockSystems[idx] ?? mockSystems[0] ?? mockSystems.find(Boolean) as MockSystemDef;
}
const typeToMinutes: Record<string, number> = {
"1m": 1, "10m": 10, "20m": 20, "120m": 120, "480m": 480,
};
function generateSystemStatsSeries(count: number, sys: MockSystemDef): BeszelSystemStats[] {
const cpuWalk = smoothWalk(sys.baseCpu, 3, count, 1, 98);
const memPctWalk = smoothWalk(sys.baseMem, 1.5, count, 10, 95);
const diskPctWalk = smoothWalk(sys.baseDisk, 0.2, count, 5, 95);
const diskReadWalk = smoothWalk(5, 3, count, 0, 80);
const diskWriteWalk = smoothWalk(3, 2, count, 0, 50);
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;
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,
mp: memPct,
mb: rand(0.5, 4) * GB,
s: 4 * GB,
su: rand(0, 2) * GB,
d: diskTotal,
du: diskTotal * diskPct / 100,
dp: diskPct,
dr: (diskReadWalk[i] ?? 5) * MB,
dw: (diskWriteWalk[i] ?? 3) * MB,
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],
};
});
}
function generateContainerStatsSeries(count: number, profiles: typeof containerProfiles): BeszelContainerStats[][] {
const walks = profiles.map((p) => ({
profile: p,
cpuWalk: smoothWalk(p.cpuBase, 1.5, count, 0, 50),
memWalk: smoothWalk(p.memMB, p.memMB * 0.1, count, p.memMB * 0.5, p.memMB * 2),
netWalk: smoothWalk(p.netKB, p.netKB * 0.3, count, 0, p.netKB * 4),
}));
return Array.from({ length: count }, (_, i) =>
walks.map(({ profile, cpuWalk, memWalk, netWalk }) => ({
n: profile.name,
c: cpuWalk[i] ?? profile.cpuBase,
m: (memWalk[i] ?? profile.memMB) * MB,
b: [(netWalk[i] ?? profile.netKB) * KB * 0.4, (netWalk[i] ?? profile.netKB) * KB * 0.6] as [number, number],
})),
);
}
function generateTimeSeries<T>(
count: number,
intervalMinutes: number,
generator: () => T,
): (T & { id: string; system: string; type: string; created: string; updated: string })[] {
function buildTimeSeries<T>(systemId: string, count: number, intervalMinutes: number, type: string, dataPoints: T[]): (T & { id: string; system: string; type: string; created: string; updated: string })[] {
const now = Date.now();
return Array.from({ length: count }, (_, i) => ({
id: `mock-stat-${i}`,
system: "mock-sys-1",
type: `${intervalMinutes}m`,
created: new Date(now - (count - i) * intervalMinutes * 60_000).toISOString(),
updated: new Date(now - (count - i) * intervalMinutes * 60_000).toISOString(),
...generator(),
}));
const items = dataPoints.slice(0, count).map((data, i) => {
const ts = new Date(now - (count - i) * intervalMinutes * 60_000).toISOString();
return { id: `mock-stat-${i}`, system: systemId, type, created: ts, updated: ts, ...data };
});
return items.reverse();
}
const statusByIndex: Record<number, "up" | "paused" | "down"> = { 2: "paused", 4: "down" };
export class BeszelMockService {
public async getSystemsAsync(): Promise<BeszelSystem[]> {
return mockSystems.map((sys, i) => ({
@@ -119,23 +143,23 @@ export class BeszelMockService {
name: sys.name,
host: sys.host,
port: "45876",
status: i === 2 ? ("paused" as const) : ("up" as const),
status: statusByIndex[i] ?? "up",
info: {
h: sys.name,
cpu: rand(5, 85),
cpu: clamp(sys.baseCpu + rand(-5, 10), 0, 100),
c: sys.cores,
ct: sys.cores * 2,
m: sys.cpu,
la: [rand(0.1, 4), rand(0.1, 3), rand(0.1, 2)] as [number, number, number],
la: [sys.baseCpu / 20, sys.baseCpu / 25, sys.baseCpu / 30] as [number, number, number],
o: sys.os,
u: randInt(3600, 2_000_000),
mp: rand(20, 85),
dp: rand(15, 70),
bb: rand(100, 5000) * 1024,
u: randInt(86400, 2_000_000),
mp: clamp(sys.baseMem + rand(-5, 5), 0, 100),
dp: clamp(sys.baseDisk + rand(-2, 2), 0, 100),
bb: rand(200, 3000) * KB,
v: "0.8.2",
g: i === 3 ? rand(10, 60) : 0,
dt: rand(30, 65),
sv: [randInt(5, 30), randInt(30, 50)] as [number, number],
g: i === 3 ? rand(15, 45) : 0,
dt: rand(32, 58),
sv: [randInt(8, 25), randInt(25, 40)] as [number, number],
},
users: [],
created: new Date().toISOString(),
@@ -144,8 +168,7 @@ export class BeszelMockService {
}
public async getSystemDetailsAsync(systemId: string): Promise<BeszelSystemDetails> {
const idx = Number(systemId.replace("mock-sys-", "")) - 1;
const sys = mockSystems[idx] ?? mockSystems[0]!;
const sys = resolveSystem(systemId);
return {
id: systemId,
system: systemId,
@@ -156,72 +179,46 @@ export class BeszelMockService {
cpu: sys.cpu,
os: 1,
os_name: sys.os,
arch: "amd64",
arch: sys.arch,
memory: sys.mem,
podman: false,
updated: new Date().toISOString(),
};
}
public async getSystemStatsAsync(
_systemId: string,
_type: string,
perPage: number,
): Promise<BeszelSystemStatsRecord[]> {
const baseLoad = rand(10, 60);
return generateTimeSeries(perPage, 1, () => ({ stats: generateSystemStats(baseLoad) }));
public async getSystemStatsAsync(systemId: string, type: string, perPage: number): Promise<BeszelSystemStatsRecord[]> {
const sys = resolveSystem(systemId);
const intervalMinutes = typeToMinutes[type] ?? 1;
const statsSeries = generateSystemStatsSeries(perPage, sys);
return buildTimeSeries(systemId, perPage, intervalMinutes, type, statsSeries.map((stats) => ({ stats })));
}
public async getContainerStatsAsync(
_systemId: string,
_type: string,
perPage: number,
): Promise<BeszelContainerStatsRecord[]> {
const names = containerNames.slice(0, randInt(3, containerNames.length));
return generateTimeSeries(perPage, 1, () => ({ stats: generateContainerStats(names) }));
public async getContainerStatsAsync(systemId: string, type: string, perPage: number): Promise<BeszelContainerStatsRecord[]> {
const sys = resolveSystem(systemId);
const intervalMinutes = typeToMinutes[type] ?? 1;
const selectedProfiles = sys.containers.map((i) => containerProfiles[i]).filter(Boolean) as typeof containerProfiles;
const series = generateContainerStatsSeries(perPage, selectedProfiles);
return buildTimeSeries(systemId, perPage, intervalMinutes, type, series.map((stats) => ({ stats })));
}
public async getAlertsAsync(): Promise<BeszelAlert[]> {
return [
{ id: "mock-alert-1", user: "mock-user", system: "mock-sys-1", name: "CPU", triggered: true, value: 90, min: 5 },
{
id: "mock-alert-2",
user: "mock-user",
system: "mock-sys-2",
name: "Memory",
triggered: false,
value: 80,
min: 10,
},
{
id: "mock-alert-3",
user: "mock-user",
system: "mock-sys-1",
name: "Disk",
triggered: false,
value: 85,
min: 15,
},
{
id: "mock-alert-4",
user: "mock-user",
system: "mock-sys-4",
name: "Temperature",
triggered: true,
value: 75,
min: 3,
},
{ id: "mock-alert-2", user: "mock-user", system: "mock-sys-2", name: "Memory", triggered: false, value: 80, min: 10 },
{ id: "mock-alert-3", user: "mock-user", system: "mock-sys-1", name: "Disk", triggered: false, value: 85, min: 15 },
{ id: "mock-alert-4", user: "mock-user", system: "mock-sys-4", name: "Temperature", triggered: true, value: 75, min: 3 },
];
}
public async getAlertHistoryAsync(_systemId?: string, perPage = 10): Promise<BeszelAlertHistory[]> {
const now = Date.now();
const names = ["CPU", "Memory", "Disk", "Temperature", "Bandwidth"];
return Array.from({ length: Math.min(perPage, 5) }, (_, i) => ({
id: `mock-history-${i}`,
alert: `mock-alert-${(i % 4) + 1}`,
user: "mock-user",
system: `mock-sys-${(i % 5) + 1}`,
name: ["CPU", "Memory", "Disk", "Temperature", "Bandwidth"][i % 5]!,
name: names[i % names.length] ?? "CPU",
val: rand(60, 95),
created: new Date(now - (i + 1) * 3_600_000).toISOString(),
resolved: i % 2 === 0 ? new Date(now - i * 1_800_000).toISOString() : null,