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.
This commit is contained in:
Josh Hawkins 2026-03-12 11:20:03 -05:00
parent 091e0b80d2
commit 12e9bb3944
3 changed files with 28 additions and 5 deletions

View File

@ -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,

View File

@ -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]] = {}

View File

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