feat: polish release v2 experience (#6800)
This commit is contained in:
1 parent
925b0fb801
commit
a4a903d7c3
295 files changed
+10627
-7115
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { isRecord } from "@homarr/common";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
@@ -66,7 +67,7 @@ export const normalizeRealtimeSnapshot = (
|
||||
const id = `realtime-${receivedAt.getTime()}`;
|
||||
const events: LiveStatsEvent[] = [];
|
||||
|
||||
if (payload.stats && typeof payload.stats === "object" && !Array.isArray(payload.stats)) {
|
||||
if (isRecord(payload.stats)) {
|
||||
events.push({
|
||||
type: "system_stats",
|
||||
record: { id, system: systemId, stats: payload.stats, type: "1m", created, updated: created },
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface BeszelSystemInfo {
|
||||
dp: number;
|
||||
/** battery [level%, charging state] */
|
||||
bat?: [number, number];
|
||||
/** bandwidth — public interface (Mbps, legacy). Use bb when available */
|
||||
/** bandwidth — public interface (MiB/s, legacy). Use bb when available */
|
||||
b?: number;
|
||||
/** bandwidth — public interface (bytes/s, newer format). Preferred over b */
|
||||
bb?: number;
|
||||
@@ -94,9 +94,9 @@ export interface BeszelSystemDetails {
|
||||
*
|
||||
* Units:
|
||||
* - CPU/GPU/memory/disk percentages: 0–100
|
||||
* - 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)
|
||||
* - Memory/disk absolute values (mu, du, m, d, etc.): GiB
|
||||
* - Disk I/O (dr, dw): MiB/s
|
||||
* - Legacy network (ns, nr): MiB/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
|
||||
*/
|
||||
export interface BeszelSystemStats {
|
||||
@@ -110,49 +110,49 @@ export interface BeszelSystemStats {
|
||||
cpus?: number[];
|
||||
/** load average [1m, 5m, 15m] */
|
||||
la?: [number, number, number];
|
||||
/** total memory (GB) */
|
||||
/** total memory (GiB) */
|
||||
m: number;
|
||||
/** memory used (GB) */
|
||||
/** memory used (GiB) */
|
||||
mu: number;
|
||||
/** memory usage (%) */
|
||||
mp: number;
|
||||
/** memory buffer/cache (GB) */
|
||||
/** memory buffer/cache (GiB) */
|
||||
mb: number;
|
||||
/** memory max used (GB) */
|
||||
/** memory max used (GiB) */
|
||||
mm?: number;
|
||||
/** memory zfs arc (GB) */
|
||||
/** memory zfs arc (GiB) */
|
||||
mz?: number;
|
||||
/** total swap (GB) */
|
||||
/** total swap (GiB) */
|
||||
s: number;
|
||||
/** swap used (GB) */
|
||||
/** swap used (GiB) */
|
||||
su: number;
|
||||
/** total disk (GB) */
|
||||
/** total disk (GiB) */
|
||||
d: number;
|
||||
/** disk used (GB) */
|
||||
/** disk used (GiB) */
|
||||
du: number;
|
||||
/** disk usage (%) */
|
||||
dp: number;
|
||||
/** disk read (MB/s) */
|
||||
/** disk read (MiB/s) */
|
||||
dr?: number;
|
||||
/** disk write (MB/s) */
|
||||
/** disk write (MiB/s) */
|
||||
dw?: number;
|
||||
/** disk read max (MB/s) */
|
||||
/** disk read max (MiB/s) */
|
||||
drm?: number;
|
||||
/** disk write max (MB/s) */
|
||||
/** disk write max (MiB/s) */
|
||||
dwm?: number;
|
||||
/** disk IOPS [read, write] */
|
||||
/** disk throughput [read bytes/s, write bytes/s]. Prefer over dr/dw */
|
||||
dio?: [number, number];
|
||||
/** disk IOPS max [read, write] */
|
||||
/** disk throughput max [read bytes/s, write bytes/s] */
|
||||
diom?: [number, number];
|
||||
/** network sent — all interfaces (bytes/s) */
|
||||
/** legacy network sent — all interfaces (MiB/s) */
|
||||
ns?: number;
|
||||
/** network received — all interfaces (bytes/s) */
|
||||
/** legacy network received — all interfaces (MiB/s) */
|
||||
nr?: number;
|
||||
/** bandwidth — public interfaces only (bytes/s) [sent, recv]. Prefer over ns/nr */
|
||||
b?: [number, number];
|
||||
/** network sent max (bytes/s) */
|
||||
/** legacy network sent max (MiB/s) */
|
||||
nsm?: number;
|
||||
/** network received max (bytes/s) */
|
||||
/** legacy network received max (MiB/s) */
|
||||
nrm?: number;
|
||||
/** bandwidth max [sent, recv] (bytes/s) */
|
||||
bm?: [number, number];
|
||||
@@ -173,21 +173,21 @@ export interface BeszelExtraFsStats {
|
||||
d: number;
|
||||
/** disk used (GiB) */
|
||||
du: number;
|
||||
/** read (bytes/s) */
|
||||
/** legacy disk read (MiB/s) */
|
||||
r: number;
|
||||
/** write (bytes/s) */
|
||||
/** legacy disk write (MiB/s) */
|
||||
w: number;
|
||||
/** read max (bytes/s) */
|
||||
/** legacy disk read max (MiB/s) */
|
||||
rm?: number;
|
||||
/** write max (bytes/s) */
|
||||
/** legacy disk write max (MiB/s) */
|
||||
wm?: number;
|
||||
/** read (IOPS) */
|
||||
/** disk read (bytes/s) */
|
||||
rb?: number;
|
||||
/** write (IOPS) */
|
||||
/** disk write (bytes/s) */
|
||||
wb?: number;
|
||||
/** read max (IOPS) */
|
||||
/** disk read max (bytes/s) */
|
||||
rbm?: number;
|
||||
/** write max (IOPS) */
|
||||
/** disk write max (bytes/s) */
|
||||
wbm?: number;
|
||||
}
|
||||
|
||||
@@ -236,18 +236,18 @@ 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
|
||||
* Units: m/ns/nr = MiB or MiB/s, c = %, b = bytes/s
|
||||
*/
|
||||
export interface BeszelContainerStats {
|
||||
/** container name */
|
||||
n: string;
|
||||
/** CPU usage (%) */
|
||||
c: number;
|
||||
/** memory usage (MB) */
|
||||
/** memory usage (MiB) */
|
||||
m: number;
|
||||
/** network sent (bytes/s) — legacy, use b when available */
|
||||
/** network sent (MiB/s) — legacy, use b when available */
|
||||
ns?: number;
|
||||
/** network received (bytes/s) — legacy, use b when available */
|
||||
/** network received (MiB/s) — legacy, use b when available */
|
||||
nr?: number;
|
||||
/** network bandwidth [sent, recv] (bytes/s) — preferred over ns/nr */
|
||||
b?: [number, number];
|
||||
@@ -388,7 +388,7 @@ export interface BeszelSystemRow {
|
||||
gpu: number;
|
||||
/** load average [1m, 5m, 15m] */
|
||||
loadAvg: [number, number, number] | null;
|
||||
/** public interface bandwidth (bytes/s). From info.bb or info.b * 1M */
|
||||
/** public interface bandwidth (bytes/s). From info.bb or legacy info.b normalized from MiB/s */
|
||||
netBytes: number;
|
||||
/** temperature (°C) */
|
||||
temp: number | null;
|
||||
|
||||
@@ -37,7 +37,8 @@ export class SabnzbdIntegration extends Integration implements IDownloadClientIn
|
||||
const { history } = historyResult;
|
||||
const status: DownloadClientStatus = {
|
||||
paused: queue.paused,
|
||||
rates: { down: Math.floor(Number(queue.kbpersec) * 1024) }, //Actually rounded kiBps ()
|
||||
// SABnzbd's legacy kbpersec field is kibibytes per second; normalize it to bytes per second.
|
||||
rates: { down: Math.floor(Number(queue.kbpersec) * 1024) },
|
||||
types: [type],
|
||||
};
|
||||
const items = queue.slots
|
||||
|
||||
@@ -10,8 +10,9 @@ import type {
|
||||
LiveStatsEvent,
|
||||
} from "../../beszel/beszel-types";
|
||||
|
||||
const GB = 1024 * 1024 * 1024;
|
||||
const KB = 1024;
|
||||
const KIBIBYTE = 1024;
|
||||
const MEBIBYTE = 1024 * KIBIBYTE;
|
||||
const GIBIBYTE = 1024 * MEBIBYTE;
|
||||
|
||||
interface MockSystemDef {
|
||||
name: string;
|
||||
@@ -34,7 +35,7 @@ const mockSystems: MockSystemDef[] = [
|
||||
os: "Ubuntu 24.04",
|
||||
cpu: "AMD Ryzen 9 7950X",
|
||||
cores: 16,
|
||||
mem: 64 * GB,
|
||||
mem: 64 * GIBIBYTE,
|
||||
arch: "amd64",
|
||||
baseCpu: 35,
|
||||
baseMem: 55,
|
||||
@@ -47,7 +48,7 @@ const mockSystems: MockSystemDef[] = [
|
||||
os: "Debian 12",
|
||||
cpu: "Intel Xeon E-2388G",
|
||||
cores: 8,
|
||||
mem: 32 * GB,
|
||||
mem: 32 * GIBIBYTE,
|
||||
arch: "amd64",
|
||||
baseCpu: 12,
|
||||
baseMem: 70,
|
||||
@@ -60,7 +61,7 @@ const mockSystems: MockSystemDef[] = [
|
||||
os: "Raspberry Pi OS",
|
||||
cpu: "ARM Cortex-A76",
|
||||
cores: 4,
|
||||
mem: 8 * GB,
|
||||
mem: 8 * GIBIBYTE,
|
||||
arch: "aarch64",
|
||||
baseCpu: 45,
|
||||
baseMem: 82,
|
||||
@@ -73,7 +74,7 @@ const mockSystems: MockSystemDef[] = [
|
||||
os: "Proxmox VE 8.2",
|
||||
cpu: "Intel i7-13700K",
|
||||
cores: 16,
|
||||
mem: 128 * GB,
|
||||
mem: 128 * GIBIBYTE,
|
||||
arch: "amd64",
|
||||
baseCpu: 22,
|
||||
baseMem: 48,
|
||||
@@ -86,7 +87,7 @@ const mockSystems: MockSystemDef[] = [
|
||||
os: "TrueNAS SCALE",
|
||||
cpu: "Intel Xeon E5-2680",
|
||||
cores: 12,
|
||||
mem: 48 * GB,
|
||||
mem: 48 * GIBIBYTE,
|
||||
arch: "amd64",
|
||||
baseCpu: 8,
|
||||
baseMem: 30,
|
||||
@@ -95,15 +96,15 @@ const mockSystems: MockSystemDef[] = [
|
||||
},
|
||||
];
|
||||
|
||||
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 containerProfiles: { name: string; cpuBase: number; memMiB: number; netKiB: number }[] = [
|
||||
{ name: "nginx", cpuBase: 2, memMiB: 120, netKiB: 800 },
|
||||
{ name: "postgres", cpuBase: 8, memMiB: 450, netKiB: 200 },
|
||||
{ name: "redis", cpuBase: 3, memMiB: 180, netKiB: 500 },
|
||||
{ name: "grafana", cpuBase: 5, memMiB: 280, netKiB: 150 },
|
||||
{ name: "prometheus", cpuBase: 6, memMiB: 350, netKiB: 300 },
|
||||
{ name: "traefik", cpuBase: 4, memMiB: 90, netKiB: 1200 },
|
||||
{ name: "minio", cpuBase: 3, memMiB: 200, netKiB: 600 },
|
||||
{ name: "gitea", cpuBase: 4, memMiB: 250, netKiB: 100 },
|
||||
];
|
||||
|
||||
const rand = (min: number, max: number) => min + Math.random() * (max - min);
|
||||
@@ -144,30 +145,30 @@ function generateSystemStatsSeries(count: number, sys: MockSystemDef): BeszelSys
|
||||
const netSendWalk = smoothWalk(500, 200, count, 10, 8000);
|
||||
const netRecvWalk = smoothWalk(800, 300, count, 10, 12000);
|
||||
|
||||
const diskTotal = rand(200, 2000) * GB;
|
||||
const diskTotal = rand(200, 2000) * GIBIBYTE;
|
||||
|
||||
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;
|
||||
const memTotalGiB = sys.mem / GIBIBYTE;
|
||||
const diskTotalGiB = diskTotal / GIBIBYTE;
|
||||
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: memTotalGB,
|
||||
mu: (memTotalGB * memPct) / 100,
|
||||
m: memTotalGiB,
|
||||
mu: (memTotalGiB * memPct) / 100,
|
||||
mp: memPct,
|
||||
mb: rand(0.5, 4),
|
||||
s: 4,
|
||||
su: rand(0, 2),
|
||||
d: diskTotalGB,
|
||||
du: (diskTotalGB * diskPct) / 100,
|
||||
d: diskTotalGiB,
|
||||
du: (diskTotalGiB * diskPct) / 100,
|
||||
dp: diskPct,
|
||||
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],
|
||||
ns: ((netSendWalk[i] ?? 500) * KIBIBYTE) / MEBIBYTE,
|
||||
nr: ((netRecvWalk[i] ?? 800) * KIBIBYTE) / MEBIBYTE,
|
||||
b: [(netSendWalk[i] ?? 500) * KIBIBYTE * 0.7, (netRecvWalk[i] ?? 800) * KIBIBYTE * 0.7] as [number, number],
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -176,16 +177,19 @@ function generateContainerStatsSeries(count: number, profiles: typeof containerP
|
||||
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),
|
||||
memWalk: smoothWalk(p.memMiB, p.memMiB * 0.1, count, p.memMiB * 0.5, p.memMiB * 2),
|
||||
netWalk: smoothWalk(p.netKiB, p.netKiB * 0.3, count, 0, p.netKiB * 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,
|
||||
b: [(netWalk[i] ?? profile.netKB) * KB * 0.4, (netWalk[i] ?? profile.netKB) * KB * 0.6] as [number, number],
|
||||
m: memWalk[i] ?? profile.memMiB,
|
||||
b: [(netWalk[i] ?? profile.netKiB) * KIBIBYTE * 0.4, (netWalk[i] ?? profile.netKiB) * KIBIBYTE * 0.6] as [
|
||||
number,
|
||||
number,
|
||||
],
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -227,7 +231,7 @@ export class BeszelMockService {
|
||||
mp: clamp(sys.baseMem + rand(-5, 5), 0, 100),
|
||||
dp: clamp(sys.baseDisk + rand(-2, 2), 0, 100),
|
||||
efs: i === 0 ? { "/mnt/sda": 86.9, "/mnt/sdb": 70.5, "/mnt/sdc": 42.7 } : undefined,
|
||||
bb: rand(200, 3000) * KB,
|
||||
bb: rand(200, 3000) * KIBIBYTE,
|
||||
v: "0.8.2",
|
||||
g: i === 3 ? rand(15, 45) : 0,
|
||||
dt: rand(32, 58),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isRecord } from "@homarr/common";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
|
||||
@@ -38,7 +39,7 @@ export class PiHoleIntegrationV5 extends Integration implements DnsHoleSummaryIn
|
||||
const data = await response.json();
|
||||
|
||||
// Pi-hole v5 returned an empty array if the API key is wrong
|
||||
if (typeof data !== "object" || Array.isArray(data)) {
|
||||
if (!isRecord(data)) {
|
||||
return TestConnectionError.UnauthorizedResult(401);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { SynologyDiskRecord, SynologyVolumeRecord } from "./synology-types"
|
||||
import {
|
||||
AUTH_API_NAME,
|
||||
INFO_API_NAME,
|
||||
parseSynologyNumber,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
SESSION_ERROR_CODES,
|
||||
synologyApiInfoResponseSchema,
|
||||
@@ -174,8 +175,8 @@ export class SynologyClient {
|
||||
status: string | undefined,
|
||||
displayName?: string,
|
||||
): SynologyVolumeRecord | null {
|
||||
const usedBytes = parseNumericValue(usedValue);
|
||||
const totalBytes = parseNumericValue(totalValue);
|
||||
const usedBytes = parseSynologyNumber(usedValue);
|
||||
const totalBytes = parseSynologyNumber(totalValue);
|
||||
if (usedBytes === null || totalBytes === null || totalBytes <= 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -194,7 +195,7 @@ export class SynologyClient {
|
||||
identifier: disk.id ?? disk.name ?? disk.display_name ?? "unknown-disk",
|
||||
name: disk.display_name ?? disk.name ?? disk.id ?? "unknown-disk",
|
||||
status: disk.smart_status ?? disk.status,
|
||||
temperature: parseOptionalNumericValue(disk.temp ?? disk.temperature),
|
||||
temperature: parseSynologyNumber(disk.temp ?? disk.temperature),
|
||||
volumeName: disk.volume_id ?? disk.vol_path,
|
||||
}));
|
||||
}
|
||||
@@ -205,7 +206,7 @@ export class SynologyClient {
|
||||
identifier: disk.disk_id ?? disk.id ?? disk.name ?? "unknown-disk",
|
||||
name: disk.name ?? disk.disk_id ?? disk.id ?? "unknown-disk",
|
||||
status: disk.overall_status ?? disk.status,
|
||||
temperature: parseOptionalNumericValue(disk.temp ?? disk.temperature),
|
||||
temperature: parseSynologyNumber(disk.temp ?? disk.temperature),
|
||||
volumeName: undefined,
|
||||
}));
|
||||
}
|
||||
@@ -394,16 +395,3 @@ function extractCookieHeader(headers: Headers): string | null {
|
||||
|
||||
return cookies.map((cookie) => cookie.split(";")[0]).join("; ");
|
||||
}
|
||||
|
||||
function parseNumericValue(value: number | string | undefined): number | null {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const parsed = typeof value === "number" ? value : Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseOptionalNumericValue(value: number | string | undefined): number | null {
|
||||
const parsed = parseNumericValue(value);
|
||||
return parsed === null ? null : parsed;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type { TestingResult } from "../base/test-connection/test-connection-serv
|
||||
import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration";
|
||||
import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types";
|
||||
import { SynologyClient } from "./synology-client";
|
||||
import { parseSynologyNumber } from "./synology-types";
|
||||
import type { SynologyDiskRecord, SynologyVolumeRecord } from "./synology-types";
|
||||
import type { synologySystemInfoDataSchema, synologyUtilizationDataSchema } from "./synology-types";
|
||||
|
||||
@@ -125,16 +126,16 @@ function mapVersion(systemInfo: z.infer<typeof synologySystemInfoDataSchema>): s
|
||||
}
|
||||
|
||||
function mapCpuUtilization(utilization: z.infer<typeof synologyUtilizationDataSchema>): number {
|
||||
const userLoad = parseNumericValue(utilization.cpu?.user_load) ?? 0;
|
||||
const systemLoad = parseNumericValue(utilization.cpu?.system_load) ?? 0;
|
||||
const userLoad = parseSynologyNumber(utilization.cpu?.user_load) ?? 0;
|
||||
const systemLoad = parseSynologyNumber(utilization.cpu?.system_load) ?? 0;
|
||||
const totalLoad = userLoad + systemLoad;
|
||||
return Math.min(totalLoad, 100);
|
||||
}
|
||||
|
||||
function mapMemoryBytes(utilization: z.infer<typeof synologyUtilizationDataSchema>) {
|
||||
const memory = utilization.memory;
|
||||
const totalRealKib = parseNumericValue(memory?.total_real);
|
||||
const realUsagePercent = parseNumericValue(memory?.real_usage);
|
||||
const totalRealKib = parseSynologyNumber(memory?.total_real);
|
||||
const realUsagePercent = parseSynologyNumber(memory?.real_usage);
|
||||
|
||||
// DSM utilization API reports memory sizes in KiB, not bytes.
|
||||
if (totalRealKib !== null && realUsagePercent !== null) {
|
||||
@@ -146,8 +147,8 @@ function mapMemoryBytes(utilization: z.infer<typeof synologyUtilizationDataSchem
|
||||
};
|
||||
}
|
||||
|
||||
const availableKib = parseNumericValue(memory?.avail_real) ?? 0;
|
||||
const cachedKib = parseNumericValue(memory?.cached) ?? 0;
|
||||
const availableKib = parseSynologyNumber(memory?.avail_real) ?? 0;
|
||||
const cachedKib = parseSynologyNumber(memory?.cached) ?? 0;
|
||||
const totalKib = totalRealKib ?? availableKib + cachedKib;
|
||||
const usedKib = Math.max(totalKib - availableKib, 0);
|
||||
|
||||
@@ -163,8 +164,8 @@ function mapNetworkTraffic(utilization: z.infer<typeof synologyUtilizationDataSc
|
||||
return null;
|
||||
}
|
||||
|
||||
const received = parseNumericValue(totalNetwork.rx);
|
||||
const transmitted = parseNumericValue(totalNetwork.tx);
|
||||
const received = parseSynologyNumber(totalNetwork.rx);
|
||||
const transmitted = parseSynologyNumber(totalNetwork.tx);
|
||||
if (received === null || transmitted === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -176,9 +177,9 @@ function mapNetworkTraffic(utilization: z.infer<typeof synologyUtilizationDataSc
|
||||
}
|
||||
|
||||
function mapLoadAverage(utilization: z.infer<typeof synologyUtilizationDataSchema>) {
|
||||
const oneMinute = parseNumericValue(utilization.cpu?.["1min_load"]);
|
||||
const fiveMinute = parseNumericValue(utilization.cpu?.["5min_load"]);
|
||||
const fifteenMinute = parseNumericValue(utilization.cpu?.["15min_load"]);
|
||||
const oneMinute = parseSynologyNumber(utilization.cpu?.["1min_load"]);
|
||||
const fiveMinute = parseSynologyNumber(utilization.cpu?.["5min_load"]);
|
||||
const fifteenMinute = parseSynologyNumber(utilization.cpu?.["15min_load"]);
|
||||
|
||||
if (oneMinute === null && fiveMinute === null && fifteenMinute === null) {
|
||||
return null;
|
||||
@@ -210,7 +211,7 @@ function mapUptimeSeconds(systemInfo: z.infer<typeof synologySystemInfoDataSchem
|
||||
}
|
||||
|
||||
function mapCpuTemperature(systemInfo: z.infer<typeof synologySystemInfoDataSchema>): number | undefined {
|
||||
const temperature = parseNumericValue(systemInfo.sys_temp ?? systemInfo.temperature);
|
||||
const temperature = parseSynologyNumber(systemInfo.sys_temp ?? systemInfo.temperature);
|
||||
return temperature === null ? undefined : temperature;
|
||||
}
|
||||
|
||||
@@ -313,11 +314,3 @@ function isHealthyStatus(status: string | undefined): boolean {
|
||||
normalizedStatus.includes(unhealthyToken),
|
||||
);
|
||||
}
|
||||
|
||||
function parseNumericValue(value: number | string | undefined): number | null {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const parsed = typeof value === "number" ? value : Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
@@ -36,6 +36,14 @@ export const synologyAuthResponseSchema = synologyEnvelopeSchema.extend({
|
||||
|
||||
const numericString = z.union([z.number(), z.string()]);
|
||||
|
||||
export function parseSynologyNumber(value: number | string | undefined): number | null {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
const parsed = typeof value === "number" ? value : Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
export const synologySystemInfoDataSchema = z
|
||||
.object({
|
||||
up_time: z.string().optional(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { RawData } from "ws";
|
||||
import { WebSocket } from "ws";
|
||||
import z from "zod";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import { createId, isRecord } from "@homarr/common";
|
||||
import { matchErrorCode, RequestError, ResponseError } from "@homarr/common/server";
|
||||
import {
|
||||
getAllTrustedCertificatesAsync,
|
||||
@@ -306,9 +306,7 @@ const resolveCertificateOptionsAsync = async (): Promise<CertificateOptions> =>
|
||||
const parseMessage = (raw: RawData): Record<string, unknown> | undefined => {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw.toString());
|
||||
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, unknown>)
|
||||
: undefined;
|
||||
return isRecord(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { isRecord } from "@homarr/common";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
@@ -39,8 +40,8 @@ const pad = (number: number) => String(number).padStart(2, "0");
|
||||
|
||||
const extractDataArray = (json: unknown): unknown[] => {
|
||||
if (Array.isArray(json)) return json;
|
||||
if (json && typeof json === "object" && "data" in json && Array.isArray((json as { data: unknown }).data)) {
|
||||
return (json as { data: unknown[] }).data;
|
||||
if (isRecord(json) && Array.isArray(json.data)) {
|
||||
return json.data;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user