Improve UI zone operations (#24376)
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s

* improve zone renaming

Zone rename now saves as one JSON body via config/set, moving required_zones and profile overrides instead of leaving stale references

* fixes
This commit is contained in:
Josh Hawkins
2026-09-16 12:07:05 -06:00
committed by GitHub
parent 64d6366ac4
commit 10a0d5ea37
6 changed files with 709 additions and 368 deletions
+88 -73
View File
@@ -1,78 +1,93 @@
// Build a config/set query fragment that removes `name` from a
// required_zones list on the given camera section (e.g. "snapshots",
// "mqtt", "objects.genai", "onvif.autotracking"), rebuilding the
// remaining entries. When removing the name empties the list, the
// required_zones key itself is deleted so the field reverts to its
// default instead of retaining the now-stale zone name. Returns an empty
// string when `name` is not present so unrelated sections are untouched.
export const removeRequiredZoneQuery = (
import get from "lodash/get";
import setWith from "lodash/setWith";
import { CameraConfig } from "@/types/frigateConfig";
// Camera sections whose required_zones lists name zones
const REQUIRED_ZONES_SECTIONS = [
"review.alerts",
"review.detections",
"objects.genai",
"snapshots",
"mqtt",
"onvif.autotracking",
];
// The subset a profile can override
const PROFILE_REQUIRED_ZONES_SECTIONS = [
"review.alerts",
"review.detections",
"objects.genai",
"snapshots",
];
/**
* Build the camera-level config_data that follows a zone rename or delete.
*
* With newName, every required_zones list and profile zone override moves to
* the new name. Without it, they drop the zone. Lists are written whole, so a
* list the camera inherits from the global config gets a camera-level copy.
* An empty result means nothing else names the zone.
*/
export const zoneReferenceUpdates = (
camera: CameraConfig,
name: string,
camera: string,
section: string,
zones: string[],
) => {
const remaining = new Set<string>(zones || []);
newName?: string,
): Record<string, unknown> => {
const updates: Record<string, unknown> = {};
if (!remaining.has(name)) {
return "";
// Object paths keep a numeric profile name from becoming an array index
const put = (path: string[], value: unknown) =>
setWith(updates, path, value, Object);
const moveInLists = (
source: unknown,
prefix: string[],
sections: string[],
) => {
for (const section of sections) {
const path = section.split(".");
const zones: string[] | undefined = get(source, [
...path,
"required_zones",
]);
if (!zones?.includes(name)) {
continue;
}
const renamed = zones.flatMap((zone) =>
zone !== name ? [zone] : newName ? [newName] : [],
);
put([...prefix, ...path, "required_zones"], [...new Set(renamed)]);
}
};
// An active profile merges into the top-level sections, so read the base
moveInLists(
{ ...camera, ...camera.base_config },
[],
REQUIRED_ZONES_SECTIONS,
);
for (const [profile, override] of Object.entries(camera.profiles ?? {})) {
moveInLists(
override,
["profiles", profile],
PROFILE_REQUIRED_ZONES_SECTIONS,
);
const zone = override?.zones?.[name];
if (zone === undefined) {
continue;
}
put(["profiles", profile, "zones", name], null);
if (newName) {
put(["profiles", profile, "zones", newName], zone);
}
}
remaining.delete(name);
const key = `cameras.${camera}.${section}.required_zones`;
if (remaining.size === 0) {
return `&${key}`;
}
return [...remaining].map((zone) => `&${key}=${zone}`).join("");
};
export const reviewQueries = (
name: string,
review_alerts: boolean,
review_detections: boolean,
camera: string,
alertsZones: string[],
detectionsZones: string[],
) => {
let same_alerts = false;
let same_detections = false;
const alerts = new Set<string>(alertsZones || []);
if (review_alerts) {
alerts.add(name);
} else {
same_alerts = !alerts.has(name);
alerts.delete(name);
}
let alertQueries = [...alerts]
.map((zone) => `&cameras.${camera}.review.alerts.required_zones=${zone}`)
.join("");
const detections = new Set<string>(detectionsZones || []);
if (review_detections) {
detections.add(name);
} else {
same_detections = !detections.has(name);
detections.delete(name);
}
let detectionQueries = [...detections]
.map(
(zone) => `&cameras.${camera}.review.detections.required_zones=${zone}`,
)
.join("");
if (!alertQueries && !same_alerts) {
alertQueries = `&cameras.${camera}.review.alerts`;
}
if (!detectionQueries && !same_detections) {
detectionQueries = `&cameras.${camera}.review.detections`;
}
return { alertQueries, detectionQueries };
return updates;
};