From 9c44a2f662b828dee3d0b5b455dfd72fc5e5c311 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 7 May 2026 09:05:32 -0500 Subject: [PATCH] respect section hiddenFields when detecting config overrides --- web/src/hooks/use-config-override.ts | 52 ++++++++++++++++++++++++---- web/src/utils/configUtil.ts | 5 ++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/web/src/hooks/use-config-override.ts b/web/src/hooks/use-config-override.ts index d4bf624cfd..8e0732e050 100644 --- a/web/src/hooks/use-config-override.ts +++ b/web/src/hooks/use-config-override.ts @@ -1,6 +1,7 @@ // Hook to detect when camera config overrides global defaults import { useMemo } from "react"; import useSWR from "swr"; +import cloneDeep from "lodash/cloneDeep"; import isEqual from "lodash/isEqual"; import get from "lodash/get"; import set from "lodash/set"; @@ -8,7 +9,11 @@ import type { RJSFSchema } from "@rjsf/utils"; import { FrigateConfig } from "@/types/frigateConfig"; import { JsonObject, JsonValue } from "@/types/configForm"; import { isJsonObject } from "@/lib/utils"; -import { getBaseCameraSectionValue } from "@/utils/configUtil"; +import { + getBaseCameraSectionValue, + getEffectiveHiddenFields, + unsetWithWildcard, +} from "@/utils/configUtil"; import { extractSectionSchema } from "@/hooks/use-config-schema"; import { applySchemaDefaults } from "@/lib/config-schema"; @@ -38,6 +43,21 @@ export function normalizeConfigValue(value: unknown): JsonValue { return stripInternalFields(value as JsonValue); } +/** + * Remove hidden-field paths from a value before comparison so fields the + * user can't change in the UI (e.g. motion masks, attribute filters) don't + * trigger override badges. Operates on a clone so the input is unchanged. + */ +function stripHiddenPaths(value: JsonValue, hiddenFields: string[]): JsonValue { + if (hiddenFields.length === 0 || !isJsonObject(value)) return value; + const cloned = cloneDeep(value) as JsonObject; + for (const path of hiddenFields) { + if (!path) continue; + unsetWithWildcard(cloned as Record, path); + } + return cloned; +} + /** * Collapse null and empty-object values for override comparisons so * semantically equivalent shapes match. The schema may default `mask: None` @@ -45,7 +65,7 @@ export function normalizeConfigValue(value: unknown): JsonValue { * masks", so collapsing them here keeps the equality check honest. We * keep this off the public `normalizeConfigValue` so save-flow code paths * (which serialize form data) aren't affected. - */ + **/ function collapseEmpty(value: JsonValue): JsonValue { if (Array.isArray(value)) { return value.map(collapseEmpty); @@ -202,8 +222,21 @@ export function useConfigOverride({ // Collapse empty/null values for comparison so semantically equivalent // shapes (e.g. schema default `mask: null` vs runtime `mask: {}`) match. - const collapsedGlobal = collapseEmpty(normalizedGlobalValue); - const collapsedCamera = collapseEmpty(normalizedCameraValue); + // Also strip hidden-field paths (motion masks, attribute filters, etc.) + // so fields the user can't edit in the UI don't trigger override badges. + const hiddenFields = getEffectiveHiddenFields( + sectionPath, + "camera", + config, + ); + const collapsedGlobal = stripHiddenPaths( + collapseEmpty(normalizedGlobalValue), + hiddenFields, + ); + const collapsedCamera = stripHiddenPaths( + collapseEmpty(normalizedCameraValue), + hiddenFields, + ); const comparisonGlobal = compareFields ? pickFields(collapsedGlobal, compareFields) @@ -328,8 +361,15 @@ export function useAllCameraOverrides( getBaseCameraSectionValue(config, cameraName, key), ); - const collapsedGlobal = collapseEmpty(globalValue); - const collapsedCamera = collapseEmpty(cameraValue); + const hiddenFields = getEffectiveHiddenFields(key, "camera", config); + const collapsedGlobal = stripHiddenPaths( + collapseEmpty(globalValue), + hiddenFields, + ); + const collapsedCamera = stripHiddenPaths( + collapseEmpty(cameraValue), + hiddenFields, + ); const comparisonGlobal = compareFields ? pickFields(collapsedGlobal, compareFields) : collapsedGlobal; diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index e8c682a5bf..ee33e59b7b 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -254,7 +254,10 @@ export function flattenOverrides( // lodash `unset` treats `*` as a literal key. This helper expands wildcard // segments so that e.g. `"filters.*.mask"` unsets `filters..mask`. -function unsetWithWildcard(obj: Record, path: string): void { +export function unsetWithWildcard( + obj: Record, + path: string, +): void { if (!path.includes("*")) { unset(obj, path); return;