From 26d12d9fad4f3701cdb16d3ab77b872a3e0e3ccc Mon Sep 17 00:00:00 2001 From: Bergasha Date: Thu, 27 Aug 2026 09:07:05 +1000 Subject: [PATCH 1/2] Fix system resources widget overflow and initial chart flicker The chart card and its outer widget shell both had overflow:visible, letting chart lines bleed past their rounded borders into neighboring widgets. Compact-mode charts also stretched to fill the widget's full resized height instead of staying compact like other widgets. Additionally, the history used to build the chart needs 2+ points to render a line - it started empty and only got its first real point on the next poll (up to 30s later), so the chart would flash between a single-value fallback and the actual graph. Both the initial render and the first state update now seed with the current value duplicated, so a flat line renders immediately with no flicker. --- .../system-resources/chart/common-chart.tsx | 2 +- .../src/system-resources/component.tsx | 33 +++++++++++-------- packages/widgets/src/widget-card-shell.tsx | 1 - 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/widgets/src/system-resources/chart/common-chart.tsx b/packages/widgets/src/system-resources/chart/common-chart.tsx index 121cd1182..cc396f8e5 100644 --- a/packages/widgets/src/system-resources/chart/common-chart.tsx +++ b/packages/widgets/src/system-resources/chart/common-chart.tsx @@ -53,7 +53,7 @@ export const CommonChart = ({ ref={ref} h={"100%"} pos={"relative"} - style={{ overflow: "visible", border: `1px solid ${borderColor}` }} + style={{ overflow: "hidden", border: `1px solid ${borderColor}` }} p={0} bg={backgroundColor} radius={board.itemRadius} diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index f17b1cfab..6ef2f6419 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -110,14 +110,13 @@ export default function SystemResources({ lastUpdatedAt.current = dataUpdatedAt; setHistoryByIntegration((previous) => Object.fromEntries( - data.map((entry) => [ - entry.integrationId, - appendBoundedHistory( - previous[entry.integrationId] ?? [], - toChartItem(entry.healthInfo), - ADVANCED_HISTORY_SIZE, - ), - ]), + data.map((entry) => { + const currentItem = toChartItem(entry.healthInfo); + // Seed with the current item when there's no history yet, so the first real + // update lands at 2 points instead of dropping back to the single-value fallback. + const seed = previous[entry.integrationId] ?? [currentItem]; + return [entry.integrationId, appendBoundedHistory(seed, currentItem, ADVANCED_HISTORY_SIZE)]; + }), ), ); }, [dataUpdatedAt, data]); @@ -126,7 +125,9 @@ export default function SystemResources({ const panelWidth = width / panelColumns; const renderCharts = (entry: (typeof data)[number], availableWidth: number, availableHeight: number) => { const currentItem = toChartItem(entry.healthInfo); - const fullHistory = historyByIntegration[entry.integrationId] ?? [currentItem]; + // Duplicate the first point so the chart has two points to draw a line from + // immediately, instead of showing the single-value fallback until the next poll. + const fullHistory = historyByIntegration[entry.integrationId] ?? [currentItem, currentItem]; const history = isAdvanced ? fullHistory : fullHistory.slice(-COMPACT_HISTORY_SIZE); return ( @@ -225,20 +226,24 @@ const SystemCharts = ({ const showNetwork = visibleCharts.includes("network"); const labelDisplayMode = isAdvanced ? "textWithIcon" : options.labelDisplayMode; const chartCount = visibleCharts.length; - const chartColumns = isAdvanced && width >= 560 ? 2 : 1; + const chartColumns = isAdvanced ? (width >= 1100 ? 4 : width >= 560 ? 2 : 1) : 1; const compactChartGap = 8; + const compactRowHeight = 56; const chartHeight = isAdvanced - ? 180 - : `calc((100% - ${Math.max(0, chartCount - 1) * compactChartGap}px) / ${Math.max(1, chartCount)})`; + ? 110 + : Math.max( + 28, + Math.min(compactRowHeight, (height - Math.max(0, chartCount - 1) * compactChartGap) / Math.max(1, chartCount)), + ); return ( - + {showTitle && ( {integrationName} )} - + {chartCount === 0 && (
diff --git a/packages/widgets/src/widget-card-shell.tsx b/packages/widgets/src/widget-card-shell.tsx index a8843c5c4..c5b9f93f4 100644 --- a/packages/widgets/src/widget-card-shell.tsx +++ b/packages/widgets/src/widget-card-shell.tsx @@ -11,7 +11,6 @@ import type { BoardItemAdvancedOptions } from "@homarr/validation/shared"; import classes from "./widget-card-shell.module.css"; const getOverflow = (kind: WidgetKind, hasCustomCssClasses: boolean): CSSProperties => { - if (kind === "systemResources") return { overflowX: "visible", overflowY: "visible" }; if (kind === "iframe" || kind === "assistant") return { overflowX: "hidden", overflowY: "hidden" }; if (hasCustomCssClasses) return {}; return { overflowX: "hidden", overflowY: "auto" }; From 73de83b48f8e840b9670ca69e3e862e9bafba63e Mon Sep 17 00:00:00 2001 From: Bergasha Date: Thu, 27 Aug 2026 10:45:54 +1000 Subject: [PATCH 2/2] Address CodeRabbit review on system resources widget PR - Render the chart tooltip via a portal so it isn't clipped by the card's overflow:hidden (needed to stop the chart line itself bleeding past its rounded corners). - Move chartColumns' width breakpoints into a lookup table and memoize the computed value, instead of an inline ternary. --- .../src/system-resources/chart/common-chart.tsx | 7 ++++++- .../widgets/src/system-resources/component.tsx | 14 +++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/widgets/src/system-resources/chart/common-chart.tsx b/packages/widgets/src/system-resources/chart/common-chart.tsx index cc396f8e5..e839f4423 100644 --- a/packages/widgets/src/system-resources/chart/common-chart.tsx +++ b/packages/widgets/src/system-resources/chart/common-chart.tsx @@ -127,7 +127,12 @@ export const CommonChart = ({ }, }} tooltipAnimationDuration={200} - tooltipProps={tooltipProps} + tooltipProps={{ + // Render via portal so the tooltip isn't clipped by the card's overflow:hidden + // (needed to stop the chart line itself bleeding past its rounded corners). + portal: typeof document !== "undefined" ? document.body : undefined, + ...tooltipProps, + }} withTooltip={height >= 64} yAxisProps={yAxisProps} fillOpacity={chartType === "area" ? 0.3 : undefined} diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index 6ef2f6419..f077659bd 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -19,6 +19,15 @@ const COMPACT_HISTORY_SIZE = 15; const ADVANCED_HISTORY_SIZE = 60; const ALL_SYSTEM_CHARTS = ["cpu", "memory", "gpu", "network"] as const; +const ADVANCED_CHART_COLUMN_BREAKPOINTS = [ + { minWidth: 1100, columns: 4 }, + { minWidth: 560, columns: 2 }, + { minWidth: 0, columns: 1 }, +] as const; + +const getAdvancedChartColumns = (width: number): number => + ADVANCED_CHART_COLUMN_BREAKPOINTS.find(({ minWidth }) => width >= minWidth)?.columns ?? 1; + type SystemChart = WidgetComponentProps<"systemResources">["options"]["visibleCharts"][number]; type ChartItem = { @@ -226,7 +235,10 @@ const SystemCharts = ({ const showNetwork = visibleCharts.includes("network"); const labelDisplayMode = isAdvanced ? "textWithIcon" : options.labelDisplayMode; const chartCount = visibleCharts.length; - const chartColumns = isAdvanced ? (width >= 1100 ? 4 : width >= 560 ? 2 : 1) : 1; + const chartColumns = useMemo( + () => (isAdvanced ? getAdvancedChartColumns(width) : 1), + [isAdvanced, width], + ); const compactChartGap = 8; const compactRowHeight = 56; const chartHeight = isAdvanced