2026-02-27 18:55:36 +03:00
|
|
|
/**
|
|
|
|
|
* Special case handling for config sections with schema/default issues.
|
|
|
|
|
*
|
|
|
|
|
* Some sections have schema patterns that cause false "Modified" indicators
|
|
|
|
|
* when navigating to them due to how defaults are applied. This utility
|
|
|
|
|
* centralizes the logic for detecting and handling these cases.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { RJSFSchema } from "@rjsf/utils";
|
|
|
|
|
import { applySchemaDefaults } from "@/lib/config-schema";
|
|
|
|
|
import { isJsonObject } from "@/lib/utils";
|
2026-05-19 16:12:18 +03:00
|
|
|
import { HiddenFieldContext, JsonObject, JsonValue } from "@/types/configForm";
|
2026-05-19 16:22:35 +03:00
|
|
|
import { getEffectiveAttributeLabels } from "@/utils/configUtil";
|
2026-02-27 18:55:36 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sections that require special handling at the global level.
|
|
|
|
|
* Add new section paths here as needed.
|
|
|
|
|
*/
|
UI fixes (#23127)
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* 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.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
2026-05-07 16:53:07 +03:00
|
|
|
const SPECIAL_CASE_SECTIONS = ["motion", "detectors", "genai"] as const;
|
2026-02-27 18:55:36 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a section requires special case handling.
|
|
|
|
|
*/
|
|
|
|
|
export function isSpecialCaseSection(
|
|
|
|
|
sectionPath: string,
|
|
|
|
|
level: string,
|
|
|
|
|
): boolean {
|
|
|
|
|
return (
|
|
|
|
|
level === "global" &&
|
|
|
|
|
SPECIAL_CASE_SECTIONS.includes(
|
|
|
|
|
sectionPath as (typeof SPECIAL_CASE_SECTIONS)[number],
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Modify schema for sections that need defaults stripped or other modifications.
|
|
|
|
|
*
|
|
|
|
|
* - detectors: Strip the "default" field to prevent RJSF from merging the
|
|
|
|
|
* default {"cpu": {"type": "cpu"}} with stored detector keys.
|
2026-05-19 16:12:18 +03:00
|
|
|
* - genai: Inject a default provider value on the additionalProperties shape.
|
|
|
|
|
* - objects: Promote tracked attribute labels (face, license_plate, courier
|
|
|
|
|
* logos) from `filters.additionalProperties` to explicit
|
|
|
|
|
* `filters.properties.<attr>` entries with a restricted FilterConfig
|
|
|
|
|
* shape, so RJSF renders just that one field for
|
|
|
|
|
* attribute filters. Non-attribute tracked labels (person, car, …)
|
|
|
|
|
* keep flowing through the unmodified `additionalProperties` and render
|
|
|
|
|
* the full FilterConfig form.
|
2026-02-27 18:55:36 +03:00
|
|
|
*/
|
|
|
|
|
export function modifySchemaForSection(
|
|
|
|
|
sectionPath: string,
|
|
|
|
|
level: string,
|
|
|
|
|
schema: RJSFSchema | undefined,
|
2026-05-19 16:12:18 +03:00
|
|
|
ctx?: HiddenFieldContext,
|
2026-02-27 18:55:36 +03:00
|
|
|
): RJSFSchema | undefined {
|
2026-05-19 16:12:18 +03:00
|
|
|
if (!schema) return schema;
|
|
|
|
|
|
|
|
|
|
if (sectionPath === "objects") {
|
|
|
|
|
return modifyObjectsSchema(schema, ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isSpecialCaseSection(sectionPath, level)) {
|
2026-02-27 18:55:36 +03:00
|
|
|
return schema;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// detectors: Remove default to prevent merging with stored keys
|
|
|
|
|
if (sectionPath === "detectors" && "default" in schema) {
|
|
|
|
|
const { default: _, ...schemaWithoutDefault } = schema;
|
|
|
|
|
return schemaWithoutDefault;
|
|
|
|
|
}
|
|
|
|
|
|
UI fixes (#23127)
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* 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.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
2026-05-07 16:53:07 +03:00
|
|
|
if (sectionPath === "genai") {
|
|
|
|
|
const additional = schema.additionalProperties;
|
|
|
|
|
if (
|
|
|
|
|
additional &&
|
|
|
|
|
typeof additional === "object" &&
|
|
|
|
|
!Array.isArray(additional)
|
|
|
|
|
) {
|
|
|
|
|
const props = (additional as RJSFSchema).properties;
|
|
|
|
|
if (props && typeof props.provider === "object") {
|
|
|
|
|
return {
|
|
|
|
|
...schema,
|
|
|
|
|
additionalProperties: {
|
|
|
|
|
...additional,
|
|
|
|
|
properties: {
|
|
|
|
|
...props,
|
|
|
|
|
provider: { ...(props.provider as object), default: "openai" },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 18:55:36 +03:00
|
|
|
return schema;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 16:12:18 +03:00
|
|
|
/**
|
|
|
|
|
* Build a stripped FilterConfig schema for tracked attribute filters
|
|
|
|
|
* (face, license_plate, etc.). Keeps only the fields meaningful for
|
|
|
|
|
* attribute detections — `min_score`, `min_area`, `max_area`. `threshold`
|
|
|
|
|
* and the ratio fields aren't exposed: attributes don't flow through
|
|
|
|
|
* `_is_false_positive` (no median-of-history check), and aspect-ratio
|
|
|
|
|
* filtering isn't a typical attribute-tuning knob.
|
|
|
|
|
*
|
|
|
|
|
* `min_area` and `max_area` are `Union[int, float]` in Pydantic which
|
|
|
|
|
* emits as `anyOf` in JSON schema; we flatten to a plain `number` so RJSF
|
|
|
|
|
* doesn't render the int/float type-selector dropdown for each attribute
|
|
|
|
|
* filter. The backend still accepts either int (pixels) or float
|
|
|
|
|
* (percentage) since the underlying FilterConfig union is unchanged.
|
|
|
|
|
*/
|
|
|
|
|
function buildAttributeFilterSchema(
|
|
|
|
|
filterConfigSchema: RJSFSchema,
|
|
|
|
|
attributeLabel: string,
|
|
|
|
|
): RJSFSchema {
|
|
|
|
|
const props = isJsonObject(
|
|
|
|
|
(filterConfigSchema as { properties?: unknown }).properties,
|
|
|
|
|
)
|
|
|
|
|
? (filterConfigSchema as { properties: Record<string, RJSFSchema> })
|
|
|
|
|
.properties
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const minScoreSchema =
|
|
|
|
|
props && props.min_score ? props.min_score : { type: "number" };
|
|
|
|
|
|
|
|
|
|
const flattenToNumber = (src: RJSFSchema | undefined): RJSFSchema => {
|
|
|
|
|
if (!src) return { type: "number" };
|
|
|
|
|
const { anyOf: _anyOf, ...rest } = src as {
|
|
|
|
|
anyOf?: unknown;
|
|
|
|
|
[k: string]: unknown;
|
|
|
|
|
};
|
|
|
|
|
return { ...rest, type: "number" } as RJSFSchema;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: "object",
|
|
|
|
|
title: attributeLabel,
|
|
|
|
|
properties: {
|
|
|
|
|
min_score: minScoreSchema,
|
|
|
|
|
min_area: flattenToNumber(props && props.min_area),
|
|
|
|
|
max_area: flattenToNumber(props && props.max_area),
|
|
|
|
|
},
|
|
|
|
|
additionalProperties: false,
|
|
|
|
|
} as RJSFSchema;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function modifyObjectsSchema(
|
|
|
|
|
schema: RJSFSchema,
|
|
|
|
|
ctx: HiddenFieldContext | undefined,
|
|
|
|
|
): RJSFSchema {
|
|
|
|
|
if (!ctx) return schema;
|
|
|
|
|
|
2026-05-19 16:22:35 +03:00
|
|
|
const allAttributes = getEffectiveAttributeLabels(
|
|
|
|
|
ctx.fullConfig,
|
|
|
|
|
ctx.fullCameraConfig,
|
|
|
|
|
ctx.level,
|
|
|
|
|
);
|
2026-05-19 16:12:18 +03:00
|
|
|
|
|
|
|
|
// Resolve effective track at this scope, falling back through camera
|
|
|
|
|
// config then global config (matches hideAttributeFilters in objects.ts).
|
|
|
|
|
const trackFromForm = Array.isArray(
|
|
|
|
|
(ctx.formData as { track?: unknown } | undefined)?.track,
|
|
|
|
|
)
|
|
|
|
|
? (ctx.formData as { track: string[] }).track
|
|
|
|
|
: undefined;
|
|
|
|
|
const track =
|
|
|
|
|
trackFromForm ??
|
|
|
|
|
(ctx.level !== "global"
|
|
|
|
|
? ctx.fullCameraConfig?.objects?.track
|
|
|
|
|
: undefined) ??
|
|
|
|
|
ctx.fullConfig.objects?.track ??
|
|
|
|
|
[];
|
|
|
|
|
|
|
|
|
|
if (track.length === 0) return schema;
|
|
|
|
|
|
|
|
|
|
const schemaProperties = isJsonObject(
|
|
|
|
|
(schema as { properties?: unknown }).properties,
|
|
|
|
|
)
|
|
|
|
|
? (schema as { properties: Record<string, RJSFSchema> }).properties
|
|
|
|
|
: undefined;
|
|
|
|
|
const filtersSchema =
|
|
|
|
|
schemaProperties && schemaProperties.filters
|
|
|
|
|
? schemaProperties.filters
|
|
|
|
|
: undefined;
|
|
|
|
|
if (!filtersSchema) return schema;
|
|
|
|
|
|
|
|
|
|
const filterEntrySchema = isJsonObject(
|
|
|
|
|
(filtersSchema as { additionalProperties?: unknown }).additionalProperties,
|
|
|
|
|
)
|
|
|
|
|
? (filtersSchema as { additionalProperties: RJSFSchema })
|
|
|
|
|
.additionalProperties
|
|
|
|
|
: undefined;
|
|
|
|
|
if (!filterEntrySchema) return schema;
|
|
|
|
|
|
|
|
|
|
const attributeSet = new Set(allAttributes);
|
|
|
|
|
const existingProperties = isJsonObject(
|
|
|
|
|
(filtersSchema as { properties?: unknown }).properties,
|
|
|
|
|
)
|
|
|
|
|
? (filtersSchema as { properties: Record<string, RJSFSchema> }).properties
|
|
|
|
|
: {};
|
|
|
|
|
|
|
|
|
|
// Promote every tracked label to an explicit property entry so RJSF
|
|
|
|
|
// renders it as a normal collapsible (no additionalProperties key/value
|
|
|
|
|
// editor UI). Attribute labels get a restricted shape with only
|
|
|
|
|
// `min_score`; non-attribute labels get the full FilterConfig. Sorted
|
|
|
|
|
// alphabetically so the filter collapsibles match the order of the
|
|
|
|
|
// sibling `track` switches.
|
|
|
|
|
const sortedTrackedLabels = track
|
|
|
|
|
.filter((label): label is string => typeof label === "string")
|
|
|
|
|
.slice()
|
|
|
|
|
.sort((a, b) => a.localeCompare(b));
|
|
|
|
|
const updatedFilterProperties: Record<string, RJSFSchema> = {
|
|
|
|
|
...existingProperties,
|
|
|
|
|
};
|
|
|
|
|
for (const label of sortedTrackedLabels) {
|
|
|
|
|
if (attributeSet.has(label)) {
|
|
|
|
|
updatedFilterProperties[label] = buildAttributeFilterSchema(
|
|
|
|
|
filterEntrySchema,
|
|
|
|
|
label,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
updatedFilterProperties[label] = {
|
|
|
|
|
...filterEntrySchema,
|
|
|
|
|
title: label,
|
|
|
|
|
} as RJSFSchema;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedFiltersSchema: RJSFSchema = {
|
|
|
|
|
...filtersSchema,
|
|
|
|
|
properties: updatedFilterProperties,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...schema,
|
|
|
|
|
properties: {
|
|
|
|
|
...schemaProperties,
|
|
|
|
|
filters: updatedFiltersSchema,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 18:55:36 +03:00
|
|
|
/**
|
|
|
|
|
* Get effective defaults for sections with special schema patterns.
|
|
|
|
|
*
|
|
|
|
|
* - motion: Has anyOf schema with [null, MotionConfig]. When stored value is
|
|
|
|
|
* null, derive defaults from the non-null anyOf branch to avoid showing
|
|
|
|
|
* changes when navigating to the page.
|
|
|
|
|
* - detectors: Return empty object since the schema default would add unwanted
|
|
|
|
|
* keys to the stored configuration.
|
|
|
|
|
*/
|
|
|
|
|
export function getEffectiveDefaultsForSection(
|
|
|
|
|
sectionPath: string,
|
|
|
|
|
level: string,
|
|
|
|
|
schema: RJSFSchema | undefined,
|
|
|
|
|
schemaDefaults: unknown,
|
|
|
|
|
): unknown {
|
|
|
|
|
if (!isSpecialCaseSection(sectionPath, level) || !schema) {
|
|
|
|
|
return schemaDefaults;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// motion: Derive defaults from non-null anyOf branch
|
|
|
|
|
if (sectionPath === "motion") {
|
|
|
|
|
const anyOfSchemas = (schema as { anyOf?: unknown[] }).anyOf;
|
|
|
|
|
if (!anyOfSchemas || !Array.isArray(anyOfSchemas)) {
|
|
|
|
|
return schemaDefaults;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the non-null motion config schema
|
|
|
|
|
const motionSchema = anyOfSchemas.find(
|
|
|
|
|
(s) =>
|
|
|
|
|
typeof s === "object" &&
|
|
|
|
|
s !== null &&
|
|
|
|
|
(s as { type?: string }).type !== "null",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!motionSchema) {
|
|
|
|
|
return schemaDefaults;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return applySchemaDefaults(motionSchema as RJSFSchema, {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// detectors: Return empty object to avoid adding default keys
|
|
|
|
|
if (sectionPath === "detectors") {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return schemaDefaults;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 18:06:38 +03:00
|
|
|
// Sections whose `filters` dict is keyed by a sibling list field. The backend
|
|
|
|
|
// auto-populates these filters at config init but doesn't re-run after profile
|
|
|
|
|
// merges, so we synthesize the missing entries on the frontend.
|
|
|
|
|
const FILTER_SECTIONS: Record<string, { listField: string }> = {
|
|
|
|
|
objects: { listField: "track" },
|
|
|
|
|
audio: { listField: "listen" },
|
|
|
|
|
};
|
|
|
|
|
|
UI fixes (#23127)
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* 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.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
2026-05-07 16:53:07 +03:00
|
|
|
/**
|
2026-05-15 18:06:38 +03:00
|
|
|
* Add default filter entries for any label in the section's list field
|
|
|
|
|
* (e.g. `objects.track`, `audio.listen`) that isn't already in `filters`, so
|
|
|
|
|
* each label gets a collapsible. The backend only auto-populates filters at
|
|
|
|
|
* config init, not after profile merges.
|
UI fixes (#23127)
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* 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.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
2026-05-07 16:53:07 +03:00
|
|
|
*/
|
2026-05-15 18:06:38 +03:00
|
|
|
export function synthesizeMissingFilters(
|
UI fixes (#23127)
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* 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.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
2026-05-07 16:53:07 +03:00
|
|
|
sectionPath: string,
|
|
|
|
|
data: unknown,
|
|
|
|
|
sectionSchema: RJSFSchema | undefined,
|
|
|
|
|
): unknown {
|
2026-05-15 18:06:38 +03:00
|
|
|
const sectionConfig = FILTER_SECTIONS[sectionPath];
|
|
|
|
|
if (!sectionConfig) return data;
|
UI fixes (#23127)
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* 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.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
2026-05-07 16:53:07 +03:00
|
|
|
if (!isJsonObject(data)) return data;
|
|
|
|
|
|
2026-05-15 18:06:38 +03:00
|
|
|
const listValue = (data as JsonObject)[sectionConfig.listField];
|
|
|
|
|
if (!Array.isArray(listValue) || listValue.length === 0) return data;
|
UI fixes (#23127)
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* 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.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
2026-05-07 16:53:07 +03:00
|
|
|
|
|
|
|
|
const properties = (sectionSchema as { properties?: Record<string, unknown> })
|
|
|
|
|
?.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;
|
2026-05-15 18:06:38 +03:00
|
|
|
for (const label of listValue) {
|
UI fixes (#23127)
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* 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.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
2026-05-07 16:53:07 +03:00
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 18:55:36 +03:00
|
|
|
/**
|
|
|
|
|
* Sanitize overrides payloads for section-specific quirks.
|
|
|
|
|
*/
|
|
|
|
|
export function sanitizeOverridesForSection(
|
|
|
|
|
sectionPath: string,
|
|
|
|
|
level: string,
|
|
|
|
|
overrides: unknown,
|
|
|
|
|
): unknown {
|
|
|
|
|
if (!overrides || !isJsonObject(overrides)) {
|
|
|
|
|
return overrides;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionPath === "ffmpeg" && level === "camera") {
|
|
|
|
|
const overridesObj = overrides as JsonObject;
|
|
|
|
|
const inputs = overridesObj.inputs;
|
|
|
|
|
if (!Array.isArray(inputs)) {
|
|
|
|
|
return overrides;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cleanedInputs = inputs.map((input) => {
|
|
|
|
|
if (!isJsonObject(input)) {
|
|
|
|
|
return input;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cleanedInput = { ...input } as JsonObject;
|
|
|
|
|
["global_args", "hwaccel_args", "input_args"].forEach((key) => {
|
|
|
|
|
const value = cleanedInput[key];
|
|
|
|
|
if (Array.isArray(value) && value.length === 0) {
|
|
|
|
|
delete cleanedInput[key];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return cleanedInput;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...overridesObj,
|
|
|
|
|
inputs: cleanedInputs,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const flattenRecordWithDots = (
|
|
|
|
|
value: JsonObject,
|
|
|
|
|
prefix: string = "",
|
|
|
|
|
): JsonObject => {
|
|
|
|
|
const flattened: JsonObject = {};
|
|
|
|
|
Object.entries(value).forEach(([key, entry]) => {
|
|
|
|
|
const nextKey = prefix ? `${prefix}.${key}` : key;
|
|
|
|
|
if (isJsonObject(entry)) {
|
|
|
|
|
Object.assign(flattened, flattenRecordWithDots(entry, nextKey));
|
|
|
|
|
} else {
|
|
|
|
|
flattened[nextKey] = entry as JsonValue;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return flattened;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// detectors: Strip readonly model fields that are generated on startup
|
|
|
|
|
// and should never be persisted back to the config file.
|
|
|
|
|
if (sectionPath === "detectors") {
|
|
|
|
|
const overridesObj = overrides as JsonObject;
|
|
|
|
|
const cleaned: JsonObject = {};
|
|
|
|
|
|
|
|
|
|
Object.entries(overridesObj).forEach(([key, value]) => {
|
|
|
|
|
if (!isJsonObject(value)) {
|
|
|
|
|
cleaned[key] = value;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cleanedValue = { ...value } as JsonObject;
|
|
|
|
|
delete cleanedValue.model;
|
|
|
|
|
delete cleanedValue.model_path;
|
|
|
|
|
cleaned[key] = cleanedValue;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return cleaned;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionPath === "logger") {
|
|
|
|
|
const overridesObj = overrides as JsonObject;
|
|
|
|
|
const logs = overridesObj.logs;
|
|
|
|
|
if (isJsonObject(logs)) {
|
|
|
|
|
return {
|
|
|
|
|
...overridesObj,
|
|
|
|
|
logs: flattenRecordWithDots(logs),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionPath === "environment_vars") {
|
|
|
|
|
const overridesObj = overrides as JsonObject;
|
|
|
|
|
return flattenRecordWithDots(overridesObj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return overrides;
|
|
|
|
|
}
|