feat(beszel): wire live SSE streaming for 1-minute time period

Add "Live" option to time period selector that uses the existing
subscribeSystemStats tRPC subscription (PocketBase SSE) instead of
REST polling. A useLiveStats hook accumulates incoming data points
in a 60-entry rolling buffer. Charts use HH:mm:ss timestamps in
live mode.
This commit is contained in:
Thomas Camlong committed 2026-06-15 15:09:44 +02:00
1 parent 680cbc69d8
commit c5266a7a42
3 files changed
+62 -13

No files matched your search

@@ -2,7 +2,7 @@
import { useCallback, useMemo } from "react";
import { ActionIcon, Center, Menu, ScrollArea, Select, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconServer, IconServerQuestion } from "@tabler/icons-react";
import { IconServer, IconQuestionMark } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
@@ -26,6 +26,7 @@ import {
formatStorageBytes,
} from "../beszel/_shared/format";
import { makeTooltipProps } from "../beszel/_shared/tooltip";
import { useLiveStats } from "../beszel/_shared/use-live-stats";
const tooltipPercent = makeTooltipProps(formatPercent);
const tooltipGB = makeTooltipProps(formatGB, true);
@@ -63,6 +64,7 @@ export default function BeszelSystemStatsWidget({
);
const showDocker = options.showDockerCpu || options.showDockerMemory || options.showDockerNetwork;
const isLive = options.timePeriod === "1m";
const { data: statsResult } = clientApi.widget.beszel.getSystemStats.useQuery(
{
@@ -71,9 +73,13 @@ export default function BeszelSystemStatsWidget({
timePeriod: options.timePeriod as "1h" | "12h" | "24h" | "1w" | "30d",
includeDocker: showDocker,
},
{ staleTime: 30 * 1000, enabled: systemExists && selectedSystem !== "" },
{ staleTime: 30 * 1000, enabled: !isLive && systemExists && selectedSystem !== "" },
);
const liveData = useLiveStats(integrationIds, selectedSystem, showDocker, isLive && systemExists);
const activeStats = isLive ? liveData : statsResult;
const cpuMap = useCallback((s: { cpu: number }) => ({ [t("chart.cpu.series")]: s.cpu }), [t]);
const memoryMap = useCallback(
(s: { mu: number; mb: number }) => ({
@@ -98,16 +104,16 @@ export default function BeszelSystemStatsWidget({
[t],
);
const cpuData = useSystemChartData(statsResult?.systemStats, cpuMap);
const memoryData = useSystemChartData(statsResult?.systemStats, memoryMap);
const diskData = useSystemChartData(statsResult?.systemStats, diskMap);
const diskIOData = useSystemChartData(statsResult?.systemStats, diskIOMap);
const networkData = useSystemChartData(statsResult?.systemStats, networkMap);
const cpuData = useSystemChartData(activeStats?.systemStats, cpuMap, isLive);
const memoryData = useSystemChartData(activeStats?.systemStats, memoryMap, isLive);
const diskData = useSystemChartData(activeStats?.systemStats, diskMap, isLive);
const diskIOData = useSystemChartData(activeStats?.systemStats, diskIOMap, isLive);
const networkData = useSystemChartData(activeStats?.systemStats, networkMap, isLive);
const containerNames = useContainerNames(statsResult?.containerStats);
const dockerCpuData = useDockerChartData(statsResult?.containerStats, containerNames, "cpu");
const dockerMemData = useDockerChartData(statsResult?.containerStats, containerNames, "memory");
const dockerNetData = useDockerChartData(statsResult?.containerStats, containerNames, "network");
const containerNames = useContainerNames(activeStats?.containerStats);
const dockerCpuData = useDockerChartData(activeStats?.containerStats, containerNames, "cpu", isLive);
const dockerMemData = useDockerChartData(activeStats?.containerStats, containerNames, "memory", isLive);
const dockerNetData = useDockerChartData(activeStats?.containerStats, containerNames, "network", isLive);
const containerSeries = containerNames.map((name, i) => ({
name,
@@ -120,7 +126,7 @@ export default function BeszelSystemStatsWidget({
return (
<Center h="100%">
<Stack align="center" gap="sm" p="md">
<IconServerQuestion size={40} />
<IconQuestionMark size={40} />
<Text size="sm" c="dimmed" ta="center">
{t("error.systemNotFound")}
</Text>
@@ -179,7 +185,7 @@ export default function BeszelSystemStatsWidget({
</Menu>
)}
{statsResult && (
{activeStats && (
<SimpleGrid cols={cols} spacing="md">
{options.showCpu && cpuData.length > 0 && (
<ChartPanel title={t("chart.cpu.title")} subtitle={t("chart.cpu.subtitle")}>
@@ -4,6 +4,7 @@ import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
const timePeriodOptions = [
{ value: "1m", label: "Live" },
{ value: "1h", label: "1 Hour" },
{ value: "12h", label: "12 Hours" },
{ value: "24h", label: "24 Hours" },
@@ -0,0 +1,42 @@
"use client";
import { useCallback, useRef, useState } from "react";
import { clientApi } from "@homarr/api/client";
import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types";
const MAX_BUFFER = 60;
interface LiveStatsData {
systemStats: BeszelSystemStatsRecord[];
containerStats: BeszelContainerStatsRecord[];
}
export const useLiveStats = (
integrationIds: string[],
systemId: string,
includeDocker: boolean,
enabled: boolean,
) => {
const bufferRef = useRef<LiveStatsData>({ systemStats: [], containerStats: [] });
const [data, setData] = useState<LiveStatsData | null>(null);
const onData = useCallback(
(incoming: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }) => {
const buf = bufferRef.current;
buf.systemStats = [...buf.systemStats, incoming.stats].slice(-MAX_BUFFER);
if (incoming.containerStats) {
buf.containerStats = [...buf.containerStats, incoming.containerStats].slice(-MAX_BUFFER);
}
setData({ ...buf });
},
[],
);
clientApi.widget.beszel.subscribeSystemStats.useSubscription(
{ integrationIds, systemId, includeDocker },
{ enabled: enabled && systemId !== "", onData },
);
return data;
};