diff --git a/apps/docs/docs/widgets/app/index.mdx b/apps/docs/docs/widgets/app/index.mdx index ec62d862e..39b12e64a 100644 --- a/apps/docs/docs/widgets/app/index.mdx +++ b/apps/docs/docs/widgets/app/index.mdx @@ -1,30 +1,27 @@ --- -title: 'App' -description: 'Embeds an app into the board.' +title: "App" +description: "Embeds an app into the board." hide_title: true --- -import { AddingWidget } from '@site/src/components/widgets/adding'; -import { WidgetConfig } from '@site/src/components/widgets/configuration'; -import { WidgetHeader } from '@site/src/components/widgets/header'; -import { appWidget } from '.'; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { appWidget } from "."; - - + The app widget allows you to add a link to an application or a website that you want to access quickly from your Homarr board. It also supports a status check to indicate whether Homarr can reach the application or website. +In view mode, right-clicking uses the browser's link menu. Homarr's widget menu remains available in edit mode. ### Screenshots -import githubApp from './img/github.png'; -import jellyseerrApp from './img/jellyseerr-description-tooltip.png'; -import piHoleApp from './img/pi-hole-ping-icon.png'; +import githubApp from "./img/github.png"; +import jellyseerrApp from "./img/jellyseerr-description-tooltip.png"; +import piHoleApp from "./img/pi-hole-ping-icon.png"; -
+
app github with-status-check app pi-hole with-status-check status-check-icon app jellyseerr with-description-tooltip @@ -34,4 +31,5 @@ import piHoleApp from './img/pi-hole-ping-icon.png'; ### Configuration + diff --git a/apps/docs/docs/widgets/bookmarks/index.mdx b/apps/docs/docs/widgets/bookmarks/index.mdx index c3104ad5b..c79629113 100644 --- a/apps/docs/docs/widgets/bookmarks/index.mdx +++ b/apps/docs/docs/widgets/bookmarks/index.mdx @@ -1,29 +1,26 @@ --- -title: 'Bookmarks' -description: 'Displays multiple app links' +title: "Bookmarks" +description: "Displays multiple app links" hide_title: true --- -import { AddingWidget } from '@site/src/components/widgets/adding'; -import { WidgetConfig } from '@site/src/components/widgets/configuration'; -import { WidgetHeader } from '@site/src/components/widgets/header'; -import { bookmarksWidget } from '.'; +import { AddingWidget } from "@site/src/components/widgets/adding"; +import { WidgetConfig } from "@site/src/components/widgets/configuration"; +import { WidgetHeader } from "@site/src/components/widgets/header"; +import { bookmarksWidget } from "."; - - + The bookmarks widget allows you to display multiple app links in a single widget. You can add links to your favorite applications, websites, or any other resources you want quick access to. +In view mode, right-clicking uses the browser's link menu. Homarr's widget menu remains available in edit mode. ### Screenshots -import gridIconsOnly from './img/grid-icons-only.png'; -import gridWithTitle from './img/grid-with-title.png'; -import verticalListAll from './img/vertical-list-all.png'; -import vericalListSimple from './img/vertical-list-simple.png'; +import gridIconsOnly from "./img/grid-icons-only.png"; +import gridWithTitle from "./img/grid-with-title.png"; +import verticalListAll from "./img/vertical-list-all.png"; +import vericalListSimple from "./img/vertical-list-simple.png";
@@ -33,12 +30,13 @@ import vericalListSimple from './img/vertical-list-simple.png'; bookmarks grid with-title bookmarks vertical-list all-elements -
+
### Adding the widget ### Configuration + diff --git a/apps/nextjs/src/components/board/items/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx index 3b844874f..9f3e19da7 100644 --- a/apps/nextjs/src/components/board/items/widget-context-menu.tsx +++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx @@ -44,9 +44,13 @@ interface WidgetContextMenuProps { export const WidgetContextMenu = ({ item, definition, widgetStateRef, children }: WidgetContextMenuProps) => { const { data: session } = useSession(); + const [isEditMode] = useEditMode(); const settings = useSettings(); if (!session || !settings.enableRightClickOnWidgets) return <>{children}; + const isLinkWidget = item.kind === "app" || item.kind === "bookmarks"; + // Keep the Homarr menu in edit mode so these widgets can still be edited or removed. + if (isLinkWidget && !isEditMode) return <>{children}; return ( diff --git a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts index b5e468de6..38f5fd596 100644 --- a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts +++ b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts @@ -301,6 +301,8 @@ interface UseCssVariableConfiguration { isDynamic: boolean; } +const minimumResizeWidthDelta = 20; + /** * This hook is used to configure the css variables for the gridstack * Those css variables are used to define the size of the gridstack items @@ -319,9 +321,30 @@ const useCssVariableConfiguration = ({ columnCount, isDynamic, }: UseCssVariableConfiguration) => { + const prevSizeRef = useRef<{ width: number; height: number } | null>(null); + const onResize = useCallback(() => { if (!wrapperRef.current) return; if (!gridRef.current) return; + + const currentWidth = wrapperRef.current.clientWidth; + const currentHeight = wrapperRef.current.clientHeight; + const prevSize = prevSizeRef.current; + + // Only skip recalculation when the width change is insignificant + // and, for dynamic sections, the height is unchanged. This prevents infinite + // feedback loops when the vertical scrollbar appears/disappears, causing + // clientWidth to oscillate by ~15px, while still reacting to height changes. + if ( + prevSize !== null && + Math.abs(currentWidth - prevSize.width) < minimumResizeWidthDelta && + (!isDynamic || currentHeight === prevSize.height) + ) { + return; + } + + prevSizeRef.current = { width: currentWidth, height: currentHeight }; + handleResizeChange( wrapperRef.current, gridRef.current, diff --git a/packages/integrations/src/unraid/unraid-integration.ts b/packages/integrations/src/unraid/unraid-integration.ts index c10aad89e..006282f3b 100644 --- a/packages/integrations/src/unraid/unraid-integration.ts +++ b/packages/integrations/src/unraid/unraid-integration.ts @@ -37,9 +37,16 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit const systemInfo = await this.getSystemInformationAsync(); const cpuUtilization = systemInfo.metrics.cpu.cpus.reduce((acc, val) => acc + val.percentTotal, 0); - const cpuCount = systemInfo.info.cpu.cores; + const cpuCount = systemInfo.metrics.cpu.cpus.length; + + // Each entry in cpus[] is one logical CPU (thread). Dividing the summed per-thread + // utilization by the thread count (not physical cores) gives the correct average and + // avoids over-reporting on SMT systems. Guard against zero to avoid Infinity/NaN. + let cpuUtilizationNormalized = 0; + if (cpuCount > 0) { + cpuUtilizationNormalized = cpuUtilization / cpuCount; + } - // We use "info" object instead of the stats since this is the exact amount the kernel sees, which is what Unraid displays. const totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0); const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100); const uptime = dayjs(systemInfo.info.os.uptime); @@ -47,7 +54,7 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit return { version: systemInfo.info.os.release, cpuModelName: systemInfo.info.cpu.brand, - cpuUtilization: cpuUtilization / cpuCount, + cpuUtilization: cpuUtilizationNormalized, memUsedInBytes: usedMemory, memAvailableInBytes: totalMemory - usedMemory, uptime: dayjs().diff(uptime, "seconds"), @@ -139,7 +146,7 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit }, memory { layout { - size + size } } }