Compare commits

...
19 Commits
Author SHA1 Message Date
ajnart 268c864801 fix: narrow PR to verified bug fixes 2026-08-22 10:52:15 +02:00
ajnart 3f9941f9c7 Merge remote-tracking branch 'origin/dev' into feat/pr-6510-polish 2026-08-22 10:47:15 +02:00
ajnart 6868e4b8d2 test(auth): assert complete membership set for invalid OIDC claim
Address CodeRabbit: query all memberships instead of findFirst so a
handler that preserves the existing row but adds a membership is caught.
2026-08-08 12:25:11 +02:00
ajnart cc4e11fce4 Merge remote-tracking branch 'origin/dev' into fix/three-xs-fixes
# Conflicts:
#	apps/nextjs/src/components/board/items/widget-context-menu.tsx
2026-08-08 12:07:10 +02:00
ajnart 71b986f249 fix: gate scrollbar resize guard on size, preserve parse causes
Address CodeRabbit review on #6510:
- gridstack: only suppress recalculation when the width delta is
  insignificant AND (non-dynamic OR height unchanged), so dynamic
  sections still rebuild on height changes
- beszel: propagate the response parse failure via { cause } in both
  error wrappers
- auth tests: cover invalid (non-string) OIDC groups claims
2026-08-08 11:35:42 +02:00
ajnart e0ebdd9047 test(auth): add regression test for single-string OIDC groups claim 2026-08-08 11:16:39 +02:00
Thomas Camlong b2a4dcad1c fix: revert ineffective Uptime-Kuma paused fix; harden Unraid CPU divisor
The Uptime-Kuma status-page API (toPublicJSON) does not emit a status
field on monitor objects, so the added status schema field and paused
early-return were dead code and did not resolve #6002. Paused monitors
stop producing heartbeats and the two consumed endpoints expose no
paused signal, so revert the change.

Unraid CPU: divide by cpus.length (the array actually being averaged)
instead of info.cpu.threads, which is more robust if the two ever
disagree, while keeping the SMT over-reporting fix.
2026-08-03 17:41:24 +02:00
ajnart 751dd751e0 style: auto-fix lint and format 2026-08-03 15:31:09 +00:00
Thomas Camlong 7dd7d9280a chore: remove leaked docs sitemap entry from this PR 2026-08-03 17:30:00 +02:00
Thomas Camlong ebd8e00119 fix: scope app right-click bypass to view mode only 2026-08-03 17:21:38 +02:00
ajnart c51dbca9bc style: auto-fix lint and format 2026-08-03 15:17:53 +00:00
Thomas Camlong 60b0f59fe7 chore: remove board hotkeys from this PR (owned by #6393) 2026-08-03 17:16:50 +02:00
Thomas Camlong bc71cb7f16 chore: remove ISSUES.md from PR 2026-08-03 17:13:40 +02:00
Thomas Camlong 6e9136c5a0 chore: remove ISSUES.md from PR, merge BoardHotkeys into hook file 2026-08-03 17:11:04 +02:00
Thomas Camlong ef9ead60ba fix: address CodeRabbit review — guard OIDC group types and CPU division 2026-08-03 17:09:05 +02:00
ajnart ac8e0b1abe style: auto-fix lint and format 2026-08-03 15:04:34 +00:00
Thomas Camlong ce6a637220 fix: right-click apps, board keyboard shortcuts, scrollbar oscillation, Beszel JSON errors
- #6416: Skip widget context menu for app widgets (pass through native right-click)
- #6392: Add board keyboard shortcuts (mod+shift+arrows to cycle, mod+shift+1-9 for direct)
- #6080: Prevent GridStack scrollbar oscillation by thresholding resize events
- #6024: Add try-catch for Beszel response.json() calls in auth and testing paths
2026-08-03 17:03:24 +02:00
ajnart afe5f2b6b4 style: auto-fix lint and format 2026-08-03 14:52:40 +00:00
Thomas Camlong 7de2fc04ec fix: correct Unraid CPU calculation, OIDC single-group sync, and Uptime-Kuma paused status
- **Unraid CPU calculation** (#6082): Use thread count instead of core count
  when normalizing CPU utilization. Each cpus[] entry is one thread, so
  dividing by physical cores (cores) over-reports on SMT systems.

- **OIDC single-group sync** (#5716): Normalize the groups claim to always
  be an array. Some providers (e.g. Jumpcloud) send a single group as a
  plain string instead of an array, which caused the Array.isArray check
  to skip group synchronization entirely.

- **Uptime-Kuma paused status** (#6002): Add the status field to the
  monitor schema and check it before falling back to heartbeat inference.
  When a monitor is paused, heartbeats stop arriving, so the latest
  heartbeat may still show 'up' from before pausing. The status page
  returns status=2 for paused monitors, which this change now respects.
2026-08-03 16:51:21 +02:00
5 changed files with 64 additions and 34 deletions

No files matched your search

+13 -15
View File
@@ -1,30 +1,27 @@
--- ---
title: 'App' title: "App"
description: 'Embeds an app into the board.' description: "Embeds an app into the board."
hide_title: true hide_title: true
--- ---
import { AddingWidget } from '@site/src/components/widgets/adding'; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from '@site/src/components/widgets/configuration'; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from '@site/src/components/widgets/header'; import { WidgetHeader } from "@site/src/components/widgets/header";
import { appWidget } from '.'; import { appWidget } from ".";
<WidgetHeader widget={appWidget} categories={["Links", "Apps"]} />
<WidgetHeader
widget={appWidget}
categories={['Links', 'Apps']}
/>
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. 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. 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 ### Screenshots
import githubApp from './img/github.png'; import githubApp from "./img/github.png";
import jellyseerrApp from './img/jellyseerr-description-tooltip.png'; import jellyseerrApp from "./img/jellyseerr-description-tooltip.png";
import piHoleApp from './img/pi-hole-ping-icon.png'; import piHoleApp from "./img/pi-hole-ping-icon.png";
<div className='flex gap-1'> <div className="flex gap-1">
<img src={githubApp} alt="app github with-status-check" width={128 * 1.5} height={128 * 1.5} /> <img src={githubApp} alt="app github with-status-check" width={128 * 1.5} height={128 * 1.5} />
<img src={piHoleApp} alt="app pi-hole with-status-check status-check-icon" width={128 * 1.5} height={128 * 1.5} /> <img src={piHoleApp} alt="app pi-hole with-status-check status-check-icon" width={128 * 1.5} height={128 * 1.5} />
<img src={jellyseerrApp} alt="app jellyseerr with-description-tooltip" height={128 * 1.5} /> <img src={jellyseerrApp} alt="app jellyseerr with-description-tooltip" height={128 * 1.5} />
@@ -34,4 +31,5 @@ import piHoleApp from './img/pi-hole-ping-icon.png';
<AddingWidget /> <AddingWidget />
### Configuration ### Configuration
<WidgetConfig configuration={appWidget.configuration} /> <WidgetConfig configuration={appWidget.configuration} />
+14 -16
View File
@@ -1,29 +1,26 @@
--- ---
title: 'Bookmarks' title: "Bookmarks"
description: 'Displays multiple app links' description: "Displays multiple app links"
hide_title: true hide_title: true
--- ---
import { AddingWidget } from '@site/src/components/widgets/adding'; import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from '@site/src/components/widgets/configuration'; import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from '@site/src/components/widgets/header'; import { WidgetHeader } from "@site/src/components/widgets/header";
import { bookmarksWidget } from '.'; import { bookmarksWidget } from ".";
<WidgetHeader widget={bookmarksWidget} categories={["Links", "Apps"]} />
<WidgetHeader
widget={bookmarksWidget}
categories={['Links', 'Apps']}
/>
The bookmarks widget allows you to display multiple app links in a single widget. 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. 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 ### Screenshots
import gridIconsOnly from './img/grid-icons-only.png'; import gridIconsOnly from "./img/grid-icons-only.png";
import gridWithTitle from './img/grid-with-title.png'; import gridWithTitle from "./img/grid-with-title.png";
import verticalListAll from './img/vertical-list-all.png'; import verticalListAll from "./img/vertical-list-all.png";
import vericalListSimple from './img/vertical-list-simple.png'; import vericalListSimple from "./img/vertical-list-simple.png";
<div className='flex gap-1'> <div className='flex gap-1'>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
@@ -33,12 +30,13 @@ import vericalListSimple from './img/vertical-list-simple.png';
<img src={gridWithTitle} alt="bookmarks grid with-title" width={256 + 4} height={256 + 4} /> <img src={gridWithTitle} alt="bookmarks grid with-title" width={256 + 4} height={256 + 4} />
<img src={verticalListAll} alt="bookmarks vertical-list all-elements" width={128} height={256 + 4} /> <img src={verticalListAll} alt="bookmarks vertical-list all-elements" width={128} height={256 + 4} />
</div>
</div>
### Adding the widget ### Adding the widget
<AddingWidget /> <AddingWidget />
### Configuration ### Configuration
<WidgetConfig configuration={bookmarksWidget.configuration} /> <WidgetConfig configuration={bookmarksWidget.configuration} />
@@ -44,9 +44,13 @@ interface WidgetContextMenuProps {
export const WidgetContextMenu = ({ item, definition, widgetStateRef, children }: WidgetContextMenuProps) => { export const WidgetContextMenu = ({ item, definition, widgetStateRef, children }: WidgetContextMenuProps) => {
const { data: session } = useSession(); const { data: session } = useSession();
const [isEditMode] = useEditMode();
const settings = useSettings(); const settings = useSettings();
if (!session || !settings.enableRightClickOnWidgets) return <>{children}</>; 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 ( return (
<WidgetContextMenuInner item={item} definition={definition} widgetStateRef={widgetStateRef} settings={settings}> <WidgetContextMenuInner item={item} definition={definition} widgetStateRef={widgetStateRef} settings={settings}>
@@ -301,6 +301,8 @@ interface UseCssVariableConfiguration {
isDynamic: boolean; isDynamic: boolean;
} }
const minimumResizeWidthDelta = 20;
/** /**
* This hook is used to configure the css variables for the gridstack * 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 * Those css variables are used to define the size of the gridstack items
@@ -319,9 +321,30 @@ const useCssVariableConfiguration = ({
columnCount, columnCount,
isDynamic, isDynamic,
}: UseCssVariableConfiguration) => { }: UseCssVariableConfiguration) => {
const prevSizeRef = useRef<{ width: number; height: number } | null>(null);
const onResize = useCallback(() => { const onResize = useCallback(() => {
if (!wrapperRef.current) return; if (!wrapperRef.current) return;
if (!gridRef.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( handleResizeChange(
wrapperRef.current, wrapperRef.current,
gridRef.current, gridRef.current,
@@ -37,9 +37,16 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
const systemInfo = await this.getSystemInformationAsync(); const systemInfo = await this.getSystemInformationAsync();
const cpuUtilization = systemInfo.metrics.cpu.cpus.reduce((acc, val) => acc + val.percentTotal, 0); 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 totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100); const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
const uptime = dayjs(systemInfo.info.os.uptime); const uptime = dayjs(systemInfo.info.os.uptime);
@@ -47,7 +54,7 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
return { return {
version: systemInfo.info.os.release, version: systemInfo.info.os.release,
cpuModelName: systemInfo.info.cpu.brand, cpuModelName: systemInfo.info.cpu.brand,
cpuUtilization: cpuUtilization / cpuCount, cpuUtilization: cpuUtilizationNormalized,
memUsedInBytes: usedMemory, memUsedInBytes: usedMemory,
memAvailableInBytes: totalMemory - usedMemory, memAvailableInBytes: totalMemory - usedMemory,
uptime: dayjs().diff(uptime, "seconds"), uptime: dayjs().diff(uptime, "seconds"),