From b317f6b8ad54a700f12e4ced6b890b7534db2aa2 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 6 May 2026 14:08:33 -0500 Subject: [PATCH] sync object filter entries with tracked labels in camera config form Filter sub-collapsibles in the camera Objects section are driven by `filters` dict keys, but profile merges and live track-switch edits don't add matching entries, so newly tracked labels (like from a profile override) had no collapsible. Synthesize default filter entries from `track` in the form data so every tracked label renders a collapsible; baseline data also gets the synthesized entries, so save payloads are unchanged. --- .../config-form/sections/BaseSection.tsx | 21 ++++++--- .../sections/section-special-cases.ts | 45 +++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index df248d271..1f7f5b324 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -22,6 +22,7 @@ import { modifySchemaForSection, getEffectiveDefaultsForSection, sanitizeOverridesForSection, + synthesizeMissingObjectFilters, } from "./section-special-cases"; import { getSectionValidation } from "../section-validations"; import { useConfigOverride } from "@/hooks/use-config-override"; @@ -357,15 +358,19 @@ export function ConfigSection({ return get(config, sectionPath); }, [config, cameraName, sectionPath, effectiveLevel, profileName]); - const rawFormData = useMemo(() => { + const rawFormData = useMemo(() => { if (!config) return {}; if (rawSectionValue === undefined || rawSectionValue === null) { return {}; } - return rawSectionValue; - }, [config, rawSectionValue]); + return synthesizeMissingObjectFilters( + sectionPath, + rawSectionValue, + modifiedSchema ?? undefined, + ) as ConfigSectionData; + }, [config, rawSectionValue, sectionPath, modifiedSchema]); // When editing a profile, hide fields that require a restart since they // cannot take effect via profile switching alone. @@ -387,7 +392,7 @@ export function ConfigSection({ const baseData = modifiedSchema ? applySchemaDefaults(modifiedSchema, rawFormData) : rawFormData; - return sanitizeSectionData(baseData); + return sanitizeSectionData(baseData as ConfigSectionData); }, [rawFormData, modifiedSchema, sanitizeSectionData]); const baselineSnapshot = useMemo(() => { @@ -506,7 +511,11 @@ export function ConfigSection({ setPendingOverrides(undefined); return; } - const sanitizedData = sanitizeSectionData(data as ConfigSectionData); + const sanitizedData = synthesizeMissingObjectFilters( + sectionPath, + sanitizeSectionData(data as ConfigSectionData), + modifiedSchema ?? undefined, + ) as ConfigSectionData; const nextBaselineFormData = baselineSnapshot; const overrides = buildOverrides( sanitizedData, @@ -546,6 +555,8 @@ export function ConfigSection({ setPendingOverrides, setDirtyOverrides, baselineSnapshot, + sectionPath, + modifiedSchema, ], ); diff --git a/web/src/components/config-form/sections/section-special-cases.ts b/web/src/components/config-form/sections/section-special-cases.ts index 94771644f..df835c571 100644 --- a/web/src/components/config-form/sections/section-special-cases.ts +++ b/web/src/components/config-form/sections/section-special-cases.ts @@ -105,6 +105,51 @@ export function getEffectiveDefaultsForSection( return schemaDefaults; } +/** + * Add default filter entries for any label in `objects.track` that isn't + * already in `objects.filters`, so each tracked label gets a collapsible. + * The backend only auto-populates filters at config init, not after profile + * merges or live track edits. + */ +export function synthesizeMissingObjectFilters( + sectionPath: string, + data: unknown, + sectionSchema: RJSFSchema | undefined, +): unknown { + if (sectionPath !== "objects") return data; + if (!isJsonObject(data)) return data; + + const trackValue = (data as JsonObject).track; + if (!Array.isArray(trackValue) || trackValue.length === 0) return data; + + const properties = (sectionSchema as { properties?: Record }) + ?.properties; + const filtersSchema = isJsonObject(properties) + ? (properties.filters as { additionalProperties?: unknown } | undefined) + : undefined; + const filterEntrySchema = isJsonObject(filtersSchema?.additionalProperties) + ? (filtersSchema.additionalProperties as RJSFSchema) + : undefined; + + const existingFilters = isJsonObject((data as JsonObject).filters) + ? ((data as JsonObject).filters as JsonObject) + : {}; + + const newFilters: JsonObject = { ...existingFilters }; + let added = false; + for (const label of trackValue) { + if (typeof label !== "string") continue; + if (Object.prototype.hasOwnProperty.call(newFilters, label)) continue; + newFilters[label] = ( + filterEntrySchema ? applySchemaDefaults(filterEntrySchema, {}) : {} + ) as JsonValue; + added = true; + } + + if (!added) return data; + return { ...(data as JsonObject), filters: newFilters }; +} + /** * Sanitize overrides payloads for section-specific quirks. */