From 3730a78e5ae2f07c646b17a8b552bd9eb2a770c7 Mon Sep 17 00:00:00 2001 From: henrygd Date: Wed, 16 Jul 2025 21:24:42 -0400 Subject: [PATCH] update load avg display and include it in longer records --- beszel/internal/records/records.go | 7 +- .../src/components/alerts/alert-button.tsx | 3 +- .../components/charts/load-average-chart.tsx | 100 ++++++---------- beszel/site/src/components/routes/system.tsx | 24 ++-- .../systems-table/systems-table.tsx | 109 ++++++++---------- beszel/site/src/lib/utils.ts | 24 ---- 6 files changed, 99 insertions(+), 168 deletions(-) diff --git a/beszel/internal/records/records.go b/beszel/internal/records/records.go index d263ba7..d468881 100644 --- a/beszel/internal/records/records.go +++ b/beszel/internal/records/records.go @@ -203,6 +203,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) * sum.DiskWritePs += stats.DiskWritePs sum.NetworkSent += stats.NetworkSent sum.NetworkRecv += stats.NetworkRecv + sum.LoadAvg1 += stats.LoadAvg1 + sum.LoadAvg5 += stats.LoadAvg5 + sum.LoadAvg15 += stats.LoadAvg15 // Set peak values sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu) sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent) @@ -278,7 +281,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) * sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count) sum.NetworkSent = twoDecimals(sum.NetworkSent / count) sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count) - + sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count) + sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count) + sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count) // Average temperatures if sum.Temperatures != nil && tempCount > 0 { for key := range sum.Temperatures { diff --git a/beszel/site/src/components/alerts/alert-button.tsx b/beszel/site/src/components/alerts/alert-button.tsx index 4ec27d0..b06eef5 100644 --- a/beszel/site/src/components/alerts/alert-button.tsx +++ b/beszel/site/src/components/alerts/alert-button.tsx @@ -11,14 +11,13 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" -import { BellIcon, GlobeIcon, ServerIcon, HourglassIcon } from "lucide-react" +import { BellIcon, GlobeIcon, ServerIcon } from "lucide-react" import { alertInfo, cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { AlertRecord, SystemRecord } from "@/types" import { $router, Link } from "../router" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Checkbox } from "../ui/checkbox" -import { Collapsible } from "../ui/collapsible" import { SystemAlert, SystemAlertGlobal } from "./alerts-system" import { getPagePath } from "@nanostores/router" diff --git a/beszel/site/src/components/charts/load-average-chart.tsx b/beszel/site/src/components/charts/load-average-chart.tsx index 0607bcd..273891e 100644 --- a/beszel/site/src/components/charts/load-average-chart.tsx +++ b/beszel/site/src/components/charts/load-average-chart.tsx @@ -8,16 +8,9 @@ import { ChartTooltipContent, xAxis, } from "@/components/ui/chart" -import { - useYAxisWidth, - cn, - formatShortDate, - toFixedWithoutTrailingZeros, - decimalString, - chartMargin, -} from "@/lib/utils" +import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils" import { ChartData } from "@/types" -import { memo, useMemo } from "react" +import { memo } from "react" import { t } from "@lingui/core/macro" export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) { @@ -27,46 +20,20 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD return null } - /** Format load average data for chart */ - const newChartData = useMemo(() => { - const newChartData = { data: [], colors: {} } as { - data: Record[] - colors: Record - } - - // Define colors for the three load average lines - const colors = { - "1m": "hsl(25, 95%, 53%)", // Orange for 1-minute - "5m": "hsl(217, 91%, 60%)", // Blue for 5-minute - "15m": "hsl(271, 81%, 56%)", // Purple for 15-minute - } - - for (let data of chartData.systemStats) { - let newData = { created: data.created } as Record - - // Add load average values if they exist and stats is not null - if (data.stats && data.stats.l1 !== undefined) { - newData["1m"] = data.stats.l1 - } - if (data.stats && data.stats.l5 !== undefined) { - newData["5m"] = data.stats.l5 - } - if (data.stats && data.stats.l15 !== undefined) { - newData["15m"] = data.stats.l15 - } - - newChartData.data.push(newData) - } - - newChartData.colors = colors - return newChartData - }, [chartData]) - - const loadKeys = ["1m", "5m", "15m"].filter(key => - newChartData.data.some(data => data[key] !== undefined) - ) - - // console.log('rendered at', new Date()) + const keys = { + l1: { + color: "hsl(271, 81%, 60%)", // Purple + label: t`1 min`, + }, + l5: { + color: "hsl(217, 91%, 60%)", // Blue + label: t`5 min`, + }, + l15: { + color: "hsl(25, 95%, 53%)", // Orange + label: t`15 min`, + }, + } return (
@@ -75,7 +42,7 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD "opacity-100": yAxisWidth, })} > - + { - const val = toFixedWithoutTrailingZeros(value, 2) - return updateYAxisWidth(val) + return updateYAxisWidth(String(toFixedFloat(value, 2))) }} tickLine={false} axisLine={false} @@ -95,7 +61,7 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD animationEasing="ease-out" animationDuration={150} // @ts-ignore - itemSorter={(a, b) => b.value - a.value} + // itemSorter={(a, b) => b.value - a.value} content={ formatShortDate(data[0].payload.created)} @@ -103,21 +69,23 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD /> } /> - {loadKeys.map((key) => ( - - ))} + {Object.entries(keys).map(([key, value]: [string, { color: string; label: string }]) => { + return ( + + ) + })} } />
) -}) \ No newline at end of file +}) diff --git a/beszel/site/src/components/routes/system.tsx b/beszel/site/src/components/routes/system.tsx index f4c8b35..44d51bd 100644 --- a/beszel/site/src/components/routes/system.tsx +++ b/beszel/site/src/components/routes/system.tsx @@ -484,18 +484,6 @@ export default function SystemDetail({ name }: { name: string }) { /> - {/* Load Average chart */} - {(systemStats.at(-1)?.stats.l1 !== undefined || systemStats.at(-1)?.stats.l5 !== undefined || systemStats.at(-1)?.stats.l15 !== undefined) && ( - - - - )} - {containerFilterBar && ( )} + {/* Load Average chart */} + {system.info.l1 !== undefined && ( + + + + )} + {/* Temperature chart */} {systemStats.at(-1)?.stats.t && ( copyToClipboard(info.getValue() as string)} > {info.getValue() as string} - + ), header: sortableHeader, }, { - accessorFn: (originalRow) => originalRow.info.cpu, + accessorFn: ({ info }) => decimalString(info.cpu, info.cpu >= 10 ? 1 : 2), id: "cpu", name: () => t`CPU`, cell: CellFormatter, @@ -222,6 +221,44 @@ export default function SystemsTable() { Icon: GpuIcon, header: sortableHeader, }, + { + id: "loadAverage", + accessorFn: ({ info }) => { + const { l1 = 0, l5 = 0, l15 = 0 } = info + return l1 + l5 + l15 + }, + name: () => t({ message: "Load Avg", comment: "Short label for load average" }), + size: 0, + Icon: HourglassIcon, + header: sortableHeader, + cell(info: CellContext) { + const { info: sysInfo, status } = info.row.original + if (sysInfo.l1 == undefined) { + return null + } + + const { l1 = 0, l5 = 0, l15 = 0, t: cpuThreads = 1 } = sysInfo + const loadAverages = [l1, l5, l15] + + function getDotColor() { + const max = Math.max(...loadAverages) + const normalized = max / cpuThreads + if (status !== "up") return "bg-primary/30" + if (normalized < 0.7) return "bg-green-500" + if (normalized < 1.0) return "bg-yellow-500" + return "bg-red-600" + } + + return ( +
+ + {loadAverages.map((la, i) => ( + {decimalString(la, la >= 10 ? 1 : 2)} + ))} +
+ ) + }, + }, { accessorFn: (originalRow) => originalRow.info.b || 0, id: "net", @@ -230,8 +267,12 @@ export default function SystemsTable() { Icon: EthernetIcon, header: sortableHeader, cell(info) { + if (info.row.original.status !== "up") { + return null + } + const val = info.getValue() as number const userSettings = useStore($userSettings) - const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, true) + const { value, unit } = formatBytes(val, true, userSettings.unitNet, true) return ( {decimalString(value, value >= 100 ? 1 : 2)} {unit} @@ -239,64 +280,6 @@ export default function SystemsTable() { ) }, }, - { - id: "loadAverage", - name: () => t`Load Average`, - size: 0, - hideSort: true, - Icon: HourglassIcon, - header: sortableHeader, - cell(info: CellContext) { - const system = info.row.original; - const l1 = system.info?.l1; - const l5 = system.info?.l5; - const l15 = system.info?.l15; - const cores = system.info?.c || 1; - - // If no load average data, return null - if (!l1 && !l5 && !l15) return null; - - const loadAverages = [ - { name: "1m", value: l1 }, - { name: "5m", value: l5 }, - { name: "15m", value: l15 } - ].filter(la => la.value !== undefined); - - if (!loadAverages.length) return null; - - function getDotColor(value: number) { - const normalized = value / cores; - if (normalized < 0.7) return "bg-green-500"; - if (normalized < 1.0) return "bg-orange-500"; - return "bg-red-600"; - } - - return ( -
- {loadAverages.map((la, idx) => ( - - - - - - - {decimalString(la.value || 0, 2)} - - {idx < loadAverages.length - 1 && /} - - - -
-
{t`${la.name}`}
-
-
-
-
- ))} -
- ); - }, - }, { accessorFn: (originalRow) => originalRow.info.dt, id: "temp", @@ -565,7 +548,7 @@ function SystemsTableHead({ table, colLength }: { table: TableType {headerGroup.headers.map((header) => { return ( - + {flexRender(header.column.columnDef.header, header.getContext())} ) diff --git a/beszel/site/src/lib/utils.ts b/beszel/site/src/lib/utils.ts index bb8cd2f..7020d78 100644 --- a/beszel/site/src/lib/utils.ts +++ b/beszel/site/src/lib/utils.ts @@ -455,27 +455,3 @@ export const getHubURL = () => BESZEL?.HUB_URL || window.location.origin /** Map of system IDs to their corresponding tokens (used to avoid fetching in add-system dialog) */ export const tokenMap = new Map() - -/** - * Calculate load average percentage relative to CPU cores - * @param loadAverage - The load average value (1m, 5m, or 15m) - * @param cores - Number of CPU cores - * @returns Percentage (0-100) representing CPU utilization - */ -export const calculateLoadAveragePercent = (loadAverage: number, cores: number): number => { - if (!loadAverage || !cores) return 0 - return Math.min((loadAverage / cores) * 100, 100) -} - -/** - * Get load average opacity based on utilization relative to cores - * @param loadAverage - The load average value - * @param cores - Number of CPU cores - * @returns Opacity value (0.6, 0.8, or 1.0) - */ -export const getLoadAverageOpacity = (loadAverage: number, cores: number): number => { - if (!loadAverage || !cores) return 0.6 - if (loadAverage < cores * 0.5) return 0.6 - if (loadAverage < cores) return 0.8 - return 1.0 -} \ No newline at end of file