From 5059311c9de262c97f863514dc9d003d0e42e654 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Thu, 2 Apr 2026 09:15:51 -0500
Subject: [PATCH] Mask/zone editor fixes (#22732)
* add guards to reject missing sub commands
* mask/zone bugfixes
- fix websocket crash when creating a new mask or zone before a name is assigned
- fix deleted masks and zones not disappearing from the list until navigating away
- fix deleting profile override not reverting to the base mask in the list
- fix inertia defaulting to nan
* disable save button on invalid form state
* fix validation for speed estimation
* ensure polygon is closed before allowing save
* require all masks and zones to be on the base config
* clarify dialog message and tooltip when removing an override
* clarify docs
---
docs/docs/configuration/profiles.md | 2 +-
frigate/api/camera.py | 9 ++
frigate/comms/dispatcher.py | 11 ++
web/public/locales/en/views/settings.json | 5 +
.../settings/MotionMaskEditPane.tsx | 21 +++-
.../settings/ObjectMaskEditPane.tsx | 18 ++-
web/src/components/settings/PolygonItem.tsx | 110 ++++++++++++++----
web/src/components/settings/ZoneEditPane.tsx | 45 ++++---
web/src/views/settings/MasksAndZonesView.tsx | 100 ++++++++++------
9 files changed, 235 insertions(+), 86 deletions(-)
diff --git a/docs/docs/configuration/profiles.md b/docs/docs/configuration/profiles.md
index c37fca7db..b290d30f7 100644
--- a/docs/docs/configuration/profiles.md
+++ b/docs/docs/configuration/profiles.md
@@ -120,7 +120,7 @@ The following camera configuration sections can be overridden in a profile:
:::note
-Only the fields you explicitly set in a profile override are applied. All other fields retain their base configuration values. For zones, profile zones are merged with the camera's base zones — any zone defined in the profile will override or add to the base zones.
+Only the fields you explicitly set in a profile override are applied. All other fields retain their base configuration values. For masks and zones, profile zones **override** the camera's base masks and zones. If configuring profiles via YAML, you should not define masks or zones in profiles that are not defined in the base config.
:::
diff --git a/frigate/api/camera.py b/frigate/api/camera.py
index 1c03a5b7a..1881bde6d 100644
--- a/frigate/api/camera.py
+++ b/frigate/api/camera.py
@@ -1224,6 +1224,15 @@ def camera_set(
status_code=400,
)
+ if not sub_command and feature in _SUB_COMMAND_FEATURES:
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": f"Feature '{feature}' requires a sub-command (e.g. mask or zone name)",
+ },
+ status_code=400,
+ )
+
if camera_name == "*":
cameras = list(frigate_config.cameras.keys())
elif camera_name not in frigate_config.cameras:
diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py
index 4eeb76396..27d5ef125 100644
--- a/frigate/comms/dispatcher.py
+++ b/frigate/comms/dispatcher.py
@@ -118,10 +118,21 @@ class Dispatcher:
try:
if command_type == "set":
+ # Commands that require a sub-command (mask/zone name)
+ sub_command_required = {
+ "motion_mask",
+ "object_mask",
+ "zone",
+ }
if sub_command:
self._camera_settings_handlers[command](
camera_name, sub_command, payload
)
+ elif command in sub_command_required:
+ logger.error(
+ "Command %s requires a sub-command (mask/zone name)",
+ command,
+ )
else:
self._camera_settings_handlers[command](camera_name, payload)
elif command_type == "ptz":
diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index 18c277cfc..660821c9c 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -539,6 +539,7 @@
},
"restart_required": "Restart required (masks/zones changed)",
"disabledInConfig": "Item is disabled in the config file",
+ "addDisabledProfile": "Add to the base config first, then override in the profile",
"profileBase": "(base)",
"profileOverride": "(override)",
"toast": {
@@ -613,6 +614,10 @@
"desc": "Are you sure you want to delete the {{type}} {{name}}?",
"success": "{{name}} has been deleted."
},
+ "revertOverride": {
+ "title": "Revert to Base Config",
+ "desc": "This will remove the profile override for the {{type}} {{name}} and revert to the base configuration."
+ },
"error": {
"mustBeFinished": "Polygon drawing must be finished before saving."
}
diff --git a/web/src/components/settings/MotionMaskEditPane.tsx b/web/src/components/settings/MotionMaskEditPane.tsx
index 1f4aaf76f..4fa09837c 100644
--- a/web/src/components/settings/MotionMaskEditPane.tsx
+++ b/web/src/components/settings/MotionMaskEditPane.tsx
@@ -74,9 +74,11 @@ export default function MotionMaskEditPane({
}
}, [polygons, activePolygonIndex]);
+ const maskCamera = polygon?.camera || "";
+ const maskName = polygon?.name || "";
const { send: sendMotionMaskState } = useMotionMaskState(
- polygon?.camera || "",
- polygon?.name || "",
+ maskCamera,
+ maskName,
);
const cameraConfig = useMemo(() => {
@@ -154,7 +156,7 @@ export default function MotionMaskEditPane({
message: t("masksAndZones.form.name.error.mustNotBeEmpty"),
}),
enabled: z.boolean(),
- isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
+ isFinished: z.boolean().refine((val) => val === true, {
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
}),
});
@@ -170,6 +172,12 @@ export default function MotionMaskEditPane({
},
});
+ useEffect(() => {
+ if (polygon?.isFinished !== undefined) {
+ form.setValue("isFinished", polygon.isFinished, { shouldValidate: true });
+ }
+ }, [polygon?.isFinished, form]);
+
const saveToConfig = useCallback(
async ({
name: maskId,
@@ -250,8 +258,8 @@ export default function MotionMaskEditPane({
},
);
updateConfig();
- // Only publish WS state for base config
- if (!editingProfile) {
+ // Only publish WS state for base config when mask has a name
+ if (!editingProfile && maskName) {
sendMotionMaskState(enabled ? "ON" : "OFF");
}
} else {
@@ -291,6 +299,7 @@ export default function MotionMaskEditPane({
cameraConfig,
t,
sendMotionMaskState,
+ maskName,
editingProfile,
],
);
@@ -449,7 +458,7 @@ export default function MotionMaskEditPane({