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

@@ -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({