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.
This commit is contained in:
Thomas Camlong committed 2026-08-03 16:51:21 +02:00
1 parent 05292ae1f5
commit 7de2fc04ec
6 files changed
+277 -9

No files matched your search

+11 -5
View File
@@ -32,15 +32,21 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE;
// Groups from oidc provider are provided from the profile, it's not typed.
// Some providers send a single group as a string instead of an array — normalize it.
const oidcGroups = profile && groupsKey in profile ? profile[groupsKey] : undefined;
const normalizedOidcGroups =
oidcGroups !== undefined
? Array.isArray(oidcGroups)
? oidcGroups
: [oidcGroups]
: undefined;
if (
!env.AUTH_OIDC_GROUPS_LOCAL_MANAGEMENT &&
dbUser.provider === "oidc" &&
profile &&
groupsKey in profile &&
Array.isArray(profile[groupsKey])
normalizedOidcGroups !== undefined
) {
logger.debug(`Using profile groups (${groupsKey}): ${JSON.stringify(profile[groupsKey])}`);
await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]);
logger.debug(`Using profile groups (${groupsKey}): ${JSON.stringify(normalizedOidcGroups)}`);
await synchronizeGroupsWithExternalForUserAsync(db, user.id, normalizedOidcGroups as string[]);
}
// In ldap-authroization we return the groups from ldap, it's not typed.
@@ -119,6 +119,7 @@ export type HomarrDocumentationPath =
| "/docs/tags/variables"
| "/docs/tags/widgets"
| "/docs/advanced/command-line"
| "/docs/advanced/command-line/development"
| "/docs/advanced/command-line/fix-usernames"
| "/docs/advanced/command-line/integrations"
| "/docs/advanced/command-line/password-recovery"
@@ -37,9 +37,10 @@ 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.info.cpu.threads;
// We use "info" object instead of the stats since this is the exact amount the kernel sees, which is what Unraid displays.
// We use the thread count (not physical cores) because each logical CPU entry in cpus[]
// represents a thread. Dividing by cores would over-report utilization on SMT systems.
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);
@@ -139,7 +140,7 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
},
memory {
layout {
size
size
}
}
}
@@ -62,9 +62,24 @@ export class UptimeKumaIntegration extends Integration {
}
private mapMonitor(
monitor: { id: number; name: string },
monitor: { id: number; name: string; status?: number },
heartbeat: { heartbeatList: Record<string, { status: number }[]>; uptimeList: Record<string, number> },
): UptimeKumaMonitor {
// If the status page reports a monitor status directly, prefer it over heartbeat inference.
// Uptime-Kuma status page returns status=2 for paused monitors, and once paused,
// heartbeats stop arriving so the latest heartbeat may still show "up" from before pausing.
if (monitor.status !== undefined) {
const statusFromPage = uptimeKumaHeartbeatCategoryMap[monitor.status];
if (statusFromPage === "paused") {
return {
id: monitor.id,
name: monitor.name,
status: "paused",
uptimePercent24h: null,
};
}
}
const heartbeats = heartbeat.heartbeatList[String(monitor.id)] ?? [];
const latestHeartbeat = heartbeats.at(-1);
const uptimeKey = `${monitor.id}_24`;
@@ -19,6 +19,7 @@ export const uptimeKumaHeartbeatCategoryMap: Record<number, UptimeKumaMonitorCat
export const uptimeKumaStatusPageMonitorSchema = z.object({
id: z.number().int(),
name: z.string(),
status: z.number().int().optional(),
});
export const uptimeKumaStatusPageGroupSchema = z.object({