Improve System Health pane (#24188)

* build out system health pane

* tweaks

* fixes

* fix notice link so it opens the correct camera

* tweak language
This commit is contained in:
Josh Hawkins
2026-09-12 07:30:04 -06:00
committed by Nicolas Mowen
parent 6d33b31bc6
commit 70ce193e09
57 changed files with 3965 additions and 464 deletions
+204
View File
@@ -0,0 +1,204 @@
import type { TFunction } from "i18next";
import { sectionConfigs } from "@/components/config-form/sectionConfigs";
import type {
ConditionalMessage,
MessageConditionContext,
} from "@/components/config-form/section-configs/types";
import { settingsLink } from "@/components/config-form/sectionPages";
import type { ConfigSectionData } from "@/types/configForm";
import type { FrigateConfig } from "@/types/frigateConfig";
import type { HealthProblem } from "@/types/health";
import { getSectionConfig } from "@/utils/configUtil";
import { activeCameras } from "@/utils/health";
function healthMessages(
section: string,
level: "global" | "camera",
): ConditionalMessage[] {
const config = getSectionConfig(section, level);
return [...(config.messages ?? []), ...(config.fieldMessages ?? [])].filter(
(message) => message.health,
);
}
function isActive(
message: ConditionalMessage,
ctx: MessageConditionContext,
): boolean {
if (!message.condition(ctx)) {
return false;
}
return typeof message.health === "function" ? message.health(ctx) : true;
}
function toProblem(
message: ConditionalMessage,
section: string,
ctx: MessageConditionContext,
scope: string | undefined,
scopeIsCamera: boolean,
idSuffix: string,
t: TFunction,
): HealthProblem {
return {
id: `config:${section}:${message.key}:${idSuffix}`,
source: "config",
severity: message.severity,
scope,
scopeIsCamera,
text: t(message.messageKey, {
ns: "views/settings",
...(message.values ?? {}),
}),
docLink: message.docLink,
link: settingsLink(section, ctx.level, ctx.cameraName),
};
}
/**
* Evaluate every config message flagged for the Health tab against the saved,
* resolved config. The rules stay in the section configs, so the settings
* form and the Health tab can never disagree.
*/
/**
* Whether a camera section still carries the global section's values. Only
* keys the global block sets count: the resolved global detect leaves width
* and height null while every camera has numbers, so a full JSON comparison
* would never match.
*/
function inheritsGlobal(
cameraSection: Record<string, unknown>,
globalSection: Record<string, unknown>,
): boolean {
return Object.entries(globalSection).every(([key, globalValue]) => {
if (globalValue === null || globalValue === undefined) {
return true;
}
const cameraValue = cameraSection[key];
if (
typeof globalValue === "object" &&
!Array.isArray(globalValue) &&
typeof cameraValue === "object" &&
cameraValue !== null &&
!Array.isArray(cameraValue)
) {
return inheritsGlobal(
cameraValue as Record<string, unknown>,
globalValue as Record<string, unknown>,
);
}
return JSON.stringify(cameraValue) === JSON.stringify(globalValue);
});
}
export function evaluateConfigHealth(
config: FrigateConfig,
t: TFunction,
): HealthProblem[] {
const problems: HealthProblem[] = [];
const firedGlobally = new Set<string>();
const cameras = activeCameras(config);
const record = config as unknown as Record<string, unknown>;
Object.keys(sectionConfigs).forEach((section) => {
const globalMessages = healthMessages(section, "global");
if (globalMessages.length > 0) {
const sectionData = record[section];
// models is a list; every other section is one object
const items: {
formData: ConfigSectionData;
scope?: string;
idSuffix: string;
}[] =
section === "models" && Array.isArray(sectionData)
? sectionData.map((model, index) => ({
formData: model as ConfigSectionData,
scope: t(
`detectionModels.scenes.${(model as { scene?: string }).scene || "all"}`,
{ ns: "views/settings" },
),
idSuffix: `model${index}`,
}))
: [
{
formData: (sectionData ?? {}) as ConfigSectionData,
idSuffix: "global",
},
];
items.forEach(({ formData, scope, idSuffix }) => {
const ctx: MessageConditionContext = {
fullConfig: config,
level: "global",
formData,
};
globalMessages
.filter((message) => isActive(message, ctx))
.forEach((message) => {
const problem = toProblem(
message,
section,
ctx,
scope,
false,
idSuffix,
t,
);
firedGlobally.add(`${section}:${message.key}`);
problems.push(problem);
});
});
}
const cameraMessages = healthMessages(section, "camera");
if (cameraMessages.length > 0) {
const globalSection = (record[section] ?? {}) as Record<string, unknown>;
cameras.forEach((camera) => {
const cameraRecord = camera as unknown as Record<string, unknown>;
const sectionData = cameraRecord[section] ?? {};
// cameras inherit global values, so a problem the global row already
// states would repeat once per camera; a camera that overrides the
// section keeps its own row and link
const inherited = inheritsGlobal(
sectionData as Record<string, unknown>,
globalSection,
);
const ctx: MessageConditionContext = {
fullConfig: config,
fullCameraConfig: camera,
level: "camera",
cameraName: camera.name,
formData: sectionData as ConfigSectionData,
};
cameraMessages
.filter((message) => isActive(message, ctx))
.forEach((message) => {
const problem = toProblem(
message,
section,
ctx,
camera.name,
true,
camera.name,
t,
);
if (
!(inherited && firedGlobally.has(`${section}:${message.key}`))
) {
problems.push(problem);
}
});
});
}
});
return problems;
}