From 5fe43eb36329e0db20879cb1ea85264beb35f904 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 14 Jun 2026 18:21:29 +0200 Subject: [PATCH] fix(beszel): correct unit scaling for system stats and container charts (#5958) --- .../items/widget-hover-overlay.module.css | 10 ++++-- .../board/items/widget-hover-overlay.tsx | 34 +++---------------- .../src/docs/homarr-docs-sitemap.ts | 10 +++++- .../integrations/src/beszel/beszel-types.ts | 33 +++++++++--------- packages/integrations/src/mock/data/beszel.ts | 32 +++++++++-------- packages/widgets/src/app/app.module.css | 2 ++ .../src/beszel-system-stats/component.tsx | 16 ++++----- packages/widgets/src/beszel/_shared/chart.tsx | 9 +++-- 8 files changed, 72 insertions(+), 74 deletions(-) diff --git a/apps/nextjs/src/components/board/items/widget-hover-overlay.module.css b/apps/nextjs/src/components/board/items/widget-hover-overlay.module.css index 32201049f..89b3ba93e 100644 --- a/apps/nextjs/src/components/board/items/widget-hover-overlay.module.css +++ b/apps/nextjs/src/components/board/items/widget-hover-overlay.module.css @@ -5,7 +5,7 @@ bottom: calc(100% - 10px); margin-bottom: 1px; z-index: 200; - pointer-events: auto; + pointer-events: none; opacity: 0; transform: translateY(6px); transition: @@ -13,8 +13,10 @@ transform 150ms ease; } -:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):hover) .wrapper, -:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):focus-within) .wrapper { +:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):hover) + .wrapper, +:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):focus-within) + .wrapper { opacity: 1; transform: translateY(0); } @@ -77,6 +79,8 @@ gap: 4px; min-width: 0; margin-left: auto; + flex-wrap: nowrap; + overflow: hidden; } .metadataItem { diff --git a/apps/nextjs/src/components/board/items/widget-hover-overlay.tsx b/apps/nextjs/src/components/board/items/widget-hover-overlay.tsx index 40c64d3a1..6fcdc4a93 100644 --- a/apps/nextjs/src/components/board/items/widget-hover-overlay.tsx +++ b/apps/nextjs/src/components/board/items/widget-hover-overlay.tsx @@ -1,17 +1,14 @@ "use client"; -import { Avatar, Group, Text, Tooltip } from "@mantine/core"; +import { Avatar, Text, Tooltip } from "@mantine/core"; import type { RouterOutputs } from "@homarr/api"; import { useEditMode } from "@homarr/boards/edit-mode"; import { getIconUrl } from "@homarr/definitions"; -import { useSettings } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; import { widgetImports } from "@homarr/widgets"; import type { SectionItem } from "~/app/[locale]/boards/_types"; -import type { Translator } from "./widget-hover-metadata"; -import { collectWidgetHoverMetadata, resolveWidgetDisplayName } from "./widget-hover-metadata"; import classes from "./widget-hover-overlay.module.css"; interface WidgetHoverOverlayProps { @@ -21,27 +18,17 @@ interface WidgetHoverOverlayProps { export const WidgetHoverOverlay = ({ item, integrations }: WidgetHoverOverlayProps) => { const [isEditMode] = useEditMode(); - const settings = useSettings(); const t = useI18n(); const { definition } = widgetImports[item.kind]; const WidgetIcon = definition.icon; - const translate = ((key: string) => t(key as never)) as Translator; - const displayName = resolveWidgetDisplayName(item, settings, translate); + const displayName = item.advancedOptions.title?.trim() || t(`widget.${item.kind}.name`); const connectedIntegrations = (integrations ?? []).filter((integration) => item.integrationIds.includes(integration.id), ); - const metadataItems = collectWidgetHoverMetadata( - item, - settings, - connectedIntegrations.map((integration) => integration.kind), - translate, - ); - const hasIntegrations = connectedIntegrations.length > 0; - const hasMetadata = metadataItems.length > 0; - if (isEditMode) { + if (!isEditMode) { return null; } @@ -55,13 +42,13 @@ export const WidgetHoverOverlay = ({ item, integrations }: WidgetHoverOverlayPro - {hasIntegrations && ( + {connectedIntegrations.length > 0 && ( {connectedIntegrations.map((integration) => ( @@ -69,17 +56,6 @@ export const WidgetHoverOverlay = ({ item, integrations }: WidgetHoverOverlayPro ))} )} - - {hasMetadata && ( - - {metadataItems.map((metadataItem) => ( -
- {metadataItem.label} - {metadataItem.value} -
- ))} -
- )} ); diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 89ed05435..39ee81c08 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -2,6 +2,7 @@ // it uses the sitemap.xml to generate the HomarrDocumentationPath type export type HomarrDocumentationPath = | "/about-us" + | "/api-reference" | "/blog" | "/blog/2023/01/11/version0.11" | "/blog/2023/04/16/version0.12-more-widgets" @@ -42,9 +43,11 @@ export type HomarrDocumentationPath = | "/docs/tags/active-directory" | "/docs/tags/administration" | "/docs/tags/advanced" + | "/docs/tags/ai" | "/docs/tags/analytics" | "/docs/tags/api" | "/docs/tags/apps" + | "/docs/tags/automation" | "/docs/tags/background" | "/docs/tags/backup" | "/docs/tags/boards" @@ -55,9 +58,10 @@ export type HomarrDocumentationPath = | "/docs/tags/community" | "/docs/tags/configuration" | "/docs/tags/connections" - | "/docs/tags/custom" + | "/docs/tags/custom-widgets" | "/docs/tags/customization" | "/docs/tags/data-sources" + | "/docs/tags/data" | "/docs/tags/database" | "/docs/tags/developer" | "/docs/tags/development" @@ -78,9 +82,11 @@ export type HomarrDocumentationPath = | "/docs/tags/integrations" | "/docs/tags/interface" | "/docs/tags/jobs" + | "/docs/tags/jsx" | "/docs/tags/layout" | "/docs/tags/ldap" | "/docs/tags/management" + | "/docs/tags/mcp" | "/docs/tags/media" | "/docs/tags/oidc" | "/docs/tags/open-collective" @@ -217,10 +223,12 @@ export type HomarrDocumentationPath = | "/docs/integrations/uptime-kuma" | "/docs/management/api" | "/docs/management/apps" + | "/docs/management/backup" | "/docs/management/boards" | "/docs/management/certificates" | "/docs/management/custom-widgets" | "/docs/management/integrations" + | "/docs/management/mcp" | "/docs/management/media" | "/docs/management/search-engines" | "/docs/management/settings" diff --git a/packages/integrations/src/beszel/beszel-types.ts b/packages/integrations/src/beszel/beszel-types.ts index 52643c701..e5c6566a5 100644 --- a/packages/integrations/src/beszel/beszel-types.ts +++ b/packages/integrations/src/beszel/beszel-types.ts @@ -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; diff --git a/packages/integrations/src/mock/data/beszel.ts b/packages/integrations/src/mock/data/beszel.ts index 8788cf136..40acc78be 100644 --- a/packages/integrations/src/mock/data/beszel.ts +++ b/packages/integrations/src/mock/data/beszel.ts @@ -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 { diff --git a/packages/widgets/src/app/app.module.css b/packages/widgets/src/app/app.module.css index 94c41a1e0..e1477f431 100644 --- a/packages/widgets/src/app/app.module.css +++ b/packages/widgets/src/app/app.module.css @@ -6,6 +6,8 @@ object-fit: contain; scale: 0.8; transition: scale 0.2s ease-in-out; + user-select: none; + -webkit-touch-callout: none; } .appWithUrl:hover > .appIcon { diff --git a/packages/widgets/src/beszel-system-stats/component.tsx b/packages/widgets/src/beszel-system-stats/component.tsx index 73715839e..2612ee81a 100644 --- a/packages/widgets/src/beszel-system-stats/component.tsx +++ b/packages/widgets/src/beszel-system-stats/component.tsx @@ -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], ); diff --git a/packages/widgets/src/beszel/_shared/chart.tsx b/packages/widgets/src/beszel/_shared/chart.tsx index 6100a7d37..f7b735255 100644 --- a/packages/widgets/src/beszel/_shared/chart.tsx +++ b/packages/widgets/src/beszel/_shared/chart.tsx @@ -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 = { 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)), };