From 12e9bb3944af75b3c099f6682945771efb7b0454 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:20:03 -0500 Subject: [PATCH] fix profile config inheritance bug where Pydantic defaults override base values The /config API was dumping profile overrides with model_dump() which included all Pydantic defaults. When the frontend merged these over the camera's base config, explicitly-set base values were lost. Now profile overrides are re-dumped with exclude_unset=True so only user-specified fields are returned. Also fixes the Save All path generating spurious deletion markers for restart-required fields that are hidden during profile editing but not excluded from the raw data sanitization in prepareSectionSavePayload. --- frigate/api/app.py | 16 +++++++++++++--- frigate/config/profile_manager.py | 4 +++- web/src/utils/configUtil.ts | 13 ++++++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/frigate/api/app.py b/frigate/api/app.py index 71b9dbc74..383b76151 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -158,6 +158,18 @@ def config(request: Request): for zone_name, zone in config_obj.cameras[camera_name].zones.items(): camera_dict["zones"][zone_name]["color"] = zone.color + # Re-dump profile overrides with exclude_unset so that only + # explicitly-set fields are returned (not Pydantic defaults). + # Without this, the frontend merges defaults (e.g. threshold=30) + # over the camera's actual base values (e.g. threshold=20). + if camera.profiles: + for profile_name, profile_config in camera.profiles.items(): + camera_dict.setdefault("profiles", {})[profile_name] = ( + profile_config.model_dump( + mode="json", warnings="none", exclude_unset=True + ) + ) + # remove go2rtc stream passwords go2rtc: dict[str, Any] = config_obj.go2rtc.model_dump( mode="json", warnings="none", exclude_none=True @@ -229,9 +241,7 @@ def set_profile(request: Request, body: ProfileSetBody): content={"success": False, "message": err}, status_code=400, ) - request.app.dispatcher.publish( - "profile/state", body.profile or "none", retain=True - ) + request.app.dispatcher.publish("profile/state", body.profile or "none", retain=True) return JSONResponse( content={ "success": True, diff --git a/frigate/config/profile_manager.py b/frigate/config/profile_manager.py index ac07cf54c..d5cd6f921 100644 --- a/frigate/config/profile_manager.py +++ b/frigate/config/profile_manager.py @@ -101,7 +101,9 @@ class ProfileManager: """ if profile_name is not None: if profile_name not in self.config.profiles: - return f"Profile '{profile_name}' is not defined in the profiles section" + return ( + f"Profile '{profile_name}' is not defined in the profiles section" + ) # Track which camera/section pairs get changed for ZMQ publishing changed: dict[str, set[str]] = {} diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index 0be14059b..1707bc720 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -533,10 +533,21 @@ export function prepareSectionSavePayload(opts: { ? {} : rawSectionValue; + // For profile sections, also hide restart-required fields to match + // effectiveHiddenFields in BaseSection (prevents spurious deletion markers + // for fields that are hidden from the form during profile editing). + let hiddenFieldsForSanitize = sectionConfig.hiddenFields; + if (profileInfo.isProfile && sectionConfig.restartRequired?.length) { + const base = sectionConfig.hiddenFields ?? []; + hiddenFieldsForSanitize = [ + ...new Set([...base, ...sectionConfig.restartRequired]), + ]; + } + // Sanitize raw form data const rawData = sanitizeSectionData( rawFormData as ConfigSectionData, - sectionConfig.hiddenFields, + hiddenFieldsForSanitize, ); // Compute schema defaults