diff --git a/web/src/components/config-form/section-configs/audio.ts b/web/src/components/config-form/section-configs/audio.ts
new file mode 100644
index 000000000..99e6131a0
--- /dev/null
+++ b/web/src/components/config-form/section-configs/audio.ts
@@ -0,0 +1,27 @@
+import type { SectionConfigOverrides } from "./types";
+
+const audio: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "enabled",
+ "listen",
+ "filters",
+ "min_volume",
+ "max_not_heard",
+ "num_threads",
+ ],
+ fieldGroups: {
+ detection: ["enabled", "listen", "filters"],
+ sensitivity: ["min_volume", "max_not_heard"],
+ },
+ hiddenFields: ["enabled_in_config"],
+ advancedFields: ["min_volume", "max_not_heard", "num_threads"],
+ uiSchema: {
+ listen: {
+ "ui:widget": "audioLabels",
+ },
+ },
+ },
+};
+
+export default audio;
diff --git a/web/src/components/config-form/section-configs/audio_transcription.ts b/web/src/components/config-form/section-configs/audio_transcription.ts
new file mode 100644
index 000000000..f9d822a0d
--- /dev/null
+++ b/web/src/components/config-form/section-configs/audio_transcription.ts
@@ -0,0 +1,16 @@
+import type { SectionConfigOverrides } from "./types";
+
+const audioTranscription: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"],
+ hiddenFields: ["enabled_in_config"],
+ advancedFields: ["language", "device", "model_size"],
+ overrideFields: ["enabled", "live_enabled"],
+ },
+ global: {
+ fieldOrder: ["enabled", "language", "device", "model_size", "live_enabled"],
+ advancedFields: ["language", "device", "model_size"],
+ },
+};
+
+export default audioTranscription;
diff --git a/web/src/components/config-form/section-configs/auth.ts b/web/src/components/config-form/section-configs/auth.ts
new file mode 100644
index 000000000..bc7374c6c
--- /dev/null
+++ b/web/src/components/config-form/section-configs/auth.ts
@@ -0,0 +1,37 @@
+import type { SectionConfigOverrides } from "./types";
+
+const auth: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "enabled",
+ "reset_admin_password",
+ "cookie_name",
+ "cookie_secure",
+ "session_length",
+ "refresh_time",
+ "native_oauth_url",
+ "failed_login_rate_limit",
+ "trusted_proxies",
+ "hash_iterations",
+ "roles",
+ ],
+ hiddenFields: ["admin_first_time_login"],
+ advancedFields: [
+ "cookie_name",
+ "cookie_secure",
+ "session_length",
+ "refresh_time",
+ "failed_login_rate_limit",
+ "trusted_proxies",
+ "hash_iterations",
+ "roles",
+ ],
+ uiSchema: {
+ reset_admin_password: {
+ "ui:widget": "switch",
+ },
+ },
+ },
+};
+
+export default auth;
diff --git a/web/src/components/config-form/section-configs/birdseye.ts b/web/src/components/config-form/section-configs/birdseye.ts
new file mode 100644
index 000000000..9b0fa5ce7
--- /dev/null
+++ b/web/src/components/config-form/section-configs/birdseye.ts
@@ -0,0 +1,26 @@
+import type { SectionConfigOverrides } from "./types";
+
+const birdseye: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["enabled", "mode", "order"],
+ hiddenFields: [],
+ advancedFields: [],
+ overrideFields: ["enabled", "mode"],
+ },
+ global: {
+ fieldOrder: [
+ "enabled",
+ "restream",
+ "width",
+ "height",
+ "quality",
+ "mode",
+ "layout",
+ "inactivity_threshold",
+ "idle_heartbeat_fps",
+ ],
+ advancedFields: ["width", "height", "quality", "inactivity_threshold"],
+ },
+};
+
+export default birdseye;
diff --git a/web/src/components/config-form/section-configs/classification.ts b/web/src/components/config-form/section-configs/classification.ts
new file mode 100644
index 000000000..c76697a4f
--- /dev/null
+++ b/web/src/components/config-form/section-configs/classification.ts
@@ -0,0 +1,10 @@
+import type { SectionConfigOverrides } from "./types";
+
+const classification: SectionConfigOverrides = {
+ base: {
+ hiddenFields: ["custom"],
+ advancedFields: [],
+ },
+};
+
+export default classification;
diff --git a/web/src/components/config-form/section-configs/database.ts b/web/src/components/config-form/section-configs/database.ts
new file mode 100644
index 000000000..6170a157f
--- /dev/null
+++ b/web/src/components/config-form/section-configs/database.ts
@@ -0,0 +1,10 @@
+import type { SectionConfigOverrides } from "./types";
+
+const database: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["path"],
+ advancedFields: [],
+ },
+};
+
+export default database;
diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts
new file mode 100644
index 000000000..29353ab3f
--- /dev/null
+++ b/web/src/components/config-form/section-configs/detect.ts
@@ -0,0 +1,29 @@
+import type { SectionConfigOverrides } from "./types";
+
+const detect: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "enabled",
+ "fps",
+ "width",
+ "height",
+ "min_initialized",
+ "max_disappeared",
+ "annotation_offset",
+ "stationary",
+ ],
+ fieldGroups: {
+ resolution: ["enabled", "width", "height"],
+ tracking: ["min_initialized", "max_disappeared"],
+ },
+ hiddenFields: ["enabled_in_config"],
+ advancedFields: [
+ "min_initialized",
+ "max_disappeared",
+ "annotation_offset",
+ "stationary",
+ ],
+ },
+};
+
+export default detect;
diff --git a/web/src/components/config-form/section-configs/detectors.ts b/web/src/components/config-form/section-configs/detectors.ts
new file mode 100644
index 000000000..a8259015b
--- /dev/null
+++ b/web/src/components/config-form/section-configs/detectors.ts
@@ -0,0 +1,10 @@
+import type { SectionConfigOverrides } from "./types";
+
+const detectors: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [],
+ advancedFields: [],
+ },
+};
+
+export default detectors;
diff --git a/web/src/components/config-form/section-configs/environment_vars.ts b/web/src/components/config-form/section-configs/environment_vars.ts
new file mode 100644
index 000000000..e8bdcad1e
--- /dev/null
+++ b/web/src/components/config-form/section-configs/environment_vars.ts
@@ -0,0 +1,10 @@
+import type { SectionConfigOverrides } from "./types";
+
+const environmentVars: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [],
+ advancedFields: [],
+ },
+};
+
+export default environmentVars;
diff --git a/web/src/components/config-form/section-configs/face_recognition.ts b/web/src/components/config-form/section-configs/face_recognition.ts
new file mode 100644
index 000000000..ab76379c7
--- /dev/null
+++ b/web/src/components/config-form/section-configs/face_recognition.ts
@@ -0,0 +1,36 @@
+import type { SectionConfigOverrides } from "./types";
+
+const faceRecognition: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["enabled", "min_area"],
+ hiddenFields: [],
+ advancedFields: ["min_area"],
+ overrideFields: ["enabled", "min_area"],
+ },
+ global: {
+ fieldOrder: [
+ "enabled",
+ "model_size",
+ "unknown_score",
+ "detection_threshold",
+ "recognition_threshold",
+ "min_area",
+ "min_faces",
+ "save_attempts",
+ "blur_confidence_filter",
+ "device",
+ ],
+ advancedFields: [
+ "unknown_score",
+ "detection_threshold",
+ "recognition_threshold",
+ "min_area",
+ "min_faces",
+ "save_attempts",
+ "blur_confidence_filter",
+ "device",
+ ],
+ },
+};
+
+export default faceRecognition;
diff --git a/web/src/components/config-form/section-configs/ffmpeg.ts b/web/src/components/config-form/section-configs/ffmpeg.ts
new file mode 100644
index 000000000..e725a88c0
--- /dev/null
+++ b/web/src/components/config-form/section-configs/ffmpeg.ts
@@ -0,0 +1,192 @@
+import type { SectionConfigOverrides } from "./types";
+
+const ffmpeg: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "inputs",
+ "path",
+ "global_args",
+ "hwaccel_args",
+ "input_args",
+ "output_args",
+ "retry_interval",
+ "apple_compatibility",
+ "gpu",
+ ],
+ hiddenFields: [],
+ advancedFields: [
+ "global_args",
+ "hwaccel_args",
+ "input_args",
+ "output_args",
+ "retry_interval",
+ "apple_compatibility",
+ "gpu",
+ ],
+ overrideFields: [
+ "path",
+ "global_args",
+ "hwaccel_args",
+ "input_args",
+ "output_args",
+ "retry_interval",
+ "apple_compatibility",
+ "gpu",
+ ],
+ uiSchema: {
+ global_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ hwaccel_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ input_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ output_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ detect: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ record: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ items: {
+ detect: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ record: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ },
+ },
+ inputs: {
+ items: {
+ global_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ hwaccel_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ input_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ output_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ items: {
+ detect: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ record: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ global: {
+ fieldOrder: [
+ "path",
+ "global_args",
+ "hwaccel_args",
+ "input_args",
+ "output_args",
+ "retry_interval",
+ "apple_compatibility",
+ "gpu",
+ ],
+ advancedFields: [
+ "global_args",
+ "hwaccel_args",
+ "input_args",
+ "output_args",
+ "retry_interval",
+ "apple_compatibility",
+ "gpu",
+ ],
+ uiSchema: {
+ global_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ hwaccel_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ input_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ output_args: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ detect: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ record: {
+ "ui:widget": "ArrayAsTextWidget",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ },
+ },
+ },
+};
+
+export default ffmpeg;
diff --git a/web/src/components/config-form/section-configs/genai.ts b/web/src/components/config-form/section-configs/genai.ts
new file mode 100644
index 000000000..d6627fbac
--- /dev/null
+++ b/web/src/components/config-form/section-configs/genai.ts
@@ -0,0 +1,18 @@
+import type { SectionConfigOverrides } from "./types";
+
+const genai: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "provider",
+ "api_key",
+ "base_url",
+ "model",
+ "provider_options",
+ "runtime_options",
+ ],
+ advancedFields: ["base_url", "provider_options", "runtime_options"],
+ hiddenFields: ["genai.enabled_in_config"],
+ },
+};
+
+export default genai;
diff --git a/web/src/components/config-form/section-configs/live.ts b/web/src/components/config-form/section-configs/live.ts
new file mode 100644
index 000000000..bec5c0d7e
--- /dev/null
+++ b/web/src/components/config-form/section-configs/live.ts
@@ -0,0 +1,12 @@
+import type { SectionConfigOverrides } from "./types";
+
+const live: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["stream_name", "height", "quality"],
+ fieldGroups: {},
+ hiddenFields: ["enabled_in_config"],
+ advancedFields: ["quality"],
+ },
+};
+
+export default live;
diff --git a/web/src/components/config-form/section-configs/logger.ts b/web/src/components/config-form/section-configs/logger.ts
new file mode 100644
index 000000000..75322e7b0
--- /dev/null
+++ b/web/src/components/config-form/section-configs/logger.ts
@@ -0,0 +1,10 @@
+import type { SectionConfigOverrides } from "./types";
+
+const logger: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["default", "logs"],
+ advancedFields: ["logs"],
+ },
+};
+
+export default logger;
diff --git a/web/src/components/config-form/section-configs/lpr.ts b/web/src/components/config-form/section-configs/lpr.ts
new file mode 100644
index 000000000..15c5ec7a8
--- /dev/null
+++ b/web/src/components/config-form/section-configs/lpr.ts
@@ -0,0 +1,41 @@
+import type { SectionConfigOverrides } from "./types";
+
+const lpr: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"],
+ hiddenFields: [],
+ advancedFields: ["expire_time", "min_area", "enhancement"],
+ overrideFields: ["enabled", "min_area", "enhancement"],
+ },
+ global: {
+ fieldOrder: [
+ "enabled",
+ "model_size",
+ "detection_threshold",
+ "min_area",
+ "recognition_threshold",
+ "min_plate_length",
+ "format",
+ "match_distance",
+ "known_plates",
+ "enhancement",
+ "debug_save_plates",
+ "device",
+ "replace_rules",
+ ],
+ advancedFields: [
+ "detection_threshold",
+ "recognition_threshold",
+ "min_plate_length",
+ "format",
+ "match_distance",
+ "known_plates",
+ "enhancement",
+ "debug_save_plates",
+ "device",
+ "replace_rules",
+ ],
+ },
+};
+
+export default lpr;
diff --git a/web/src/components/config-form/section-configs/model.ts b/web/src/components/config-form/section-configs/model.ts
new file mode 100644
index 000000000..81867dc0a
--- /dev/null
+++ b/web/src/components/config-form/section-configs/model.ts
@@ -0,0 +1,25 @@
+import type { SectionConfigOverrides } from "./types";
+
+const model: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "path",
+ "labelmap_path",
+ "width",
+ "height",
+ "input_pixel_format",
+ "input_tensor",
+ "input_dtype",
+ "model_type",
+ ],
+ advancedFields: [
+ "input_pixel_format",
+ "input_tensor",
+ "input_dtype",
+ "model_type",
+ ],
+ hiddenFields: ["labelmap", "attributes_map"],
+ },
+};
+
+export default model;
diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts
new file mode 100644
index 000000000..b22bebb0a
--- /dev/null
+++ b/web/src/components/config-form/section-configs/motion.ts
@@ -0,0 +1,32 @@
+import type { SectionConfigOverrides } from "./types";
+
+const motion: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "enabled",
+ "threshold",
+ "lightning_threshold",
+ "improve_contrast",
+ "contour_area",
+ "delta_alpha",
+ "frame_alpha",
+ "frame_height",
+ "mask",
+ "mqtt_off_delay",
+ ],
+ fieldGroups: {
+ sensitivity: ["enabled", "threshold", "contour_area"],
+ algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"],
+ },
+ hiddenFields: ["enabled_in_config", "mask", "raw_mask"],
+ advancedFields: [
+ "lightning_threshold",
+ "delta_alpha",
+ "frame_alpha",
+ "frame_height",
+ "mqtt_off_delay",
+ ],
+ },
+};
+
+export default motion;
diff --git a/web/src/components/config-form/section-configs/mqtt.ts b/web/src/components/config-form/section-configs/mqtt.ts
new file mode 100644
index 000000000..0c9e3c34e
--- /dev/null
+++ b/web/src/components/config-form/section-configs/mqtt.ts
@@ -0,0 +1,52 @@
+import type { SectionConfigOverrides } from "./types";
+
+const mqtt: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "enabled",
+ "timestamp",
+ "bounding_box",
+ "crop",
+ "height",
+ "required_zones",
+ "quality",
+ ],
+ hiddenFields: [],
+ advancedFields: ["height", "quality"],
+ overrideFields: [],
+ uiSchema: {
+ required_zones: {
+ "ui:widget": "zoneNames",
+ },
+ },
+ },
+ global: {
+ fieldOrder: [
+ "enabled",
+ "host",
+ "port",
+ "user",
+ "password",
+ "topic_prefix",
+ "client_id",
+ "stats_interval",
+ "qos",
+ "tls_ca_certs",
+ "tls_client_cert",
+ "tls_client_key",
+ "tls_insecure",
+ ],
+ advancedFields: [
+ "stats_interval",
+ "qos",
+ "tls_ca_certs",
+ "tls_client_cert",
+ "tls_client_key",
+ "tls_insecure",
+ ],
+ liveValidate: true,
+ uiSchema: {},
+ },
+};
+
+export default mqtt;
diff --git a/web/src/components/config-form/section-configs/networking.ts b/web/src/components/config-form/section-configs/networking.ts
new file mode 100644
index 000000000..4abe60451
--- /dev/null
+++ b/web/src/components/config-form/section-configs/networking.ts
@@ -0,0 +1,10 @@
+import type { SectionConfigOverrides } from "./types";
+
+const networking: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [],
+ advancedFields: [],
+ },
+};
+
+export default networking;
diff --git a/web/src/components/config-form/section-configs/notifications.ts b/web/src/components/config-form/section-configs/notifications.ts
new file mode 100644
index 000000000..b0e436cba
--- /dev/null
+++ b/web/src/components/config-form/section-configs/notifications.ts
@@ -0,0 +1,12 @@
+import type { SectionConfigOverrides } from "./types";
+
+const notifications: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["enabled", "email"],
+ fieldGroups: {},
+ hiddenFields: ["enabled_in_config"],
+ advancedFields: [],
+ },
+};
+
+export default notifications;
diff --git a/web/src/components/config-form/section-configs/objects.ts b/web/src/components/config-form/section-configs/objects.ts
new file mode 100644
index 000000000..7652aff22
--- /dev/null
+++ b/web/src/components/config-form/section-configs/objects.ts
@@ -0,0 +1,57 @@
+import type { SectionConfigOverrides } from "./types";
+
+const objects: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["track", "alert", "detect", "filters"],
+ fieldGroups: {
+ tracking: ["track", "alert", "detect"],
+ filtering: ["filters"],
+ },
+ hiddenFields: [
+ "enabled_in_config",
+ "mask",
+ "raw_mask",
+ "genai.enabled_in_config",
+ "filters.*.mask",
+ "filters.*.raw_mask",
+ ],
+ advancedFields: ["filters"],
+ uiSchema: {
+ "filters.*.min_area": {
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ "filters.*.max_area": {
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ track: {
+ "ui:widget": "objectLabels",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ genai: {
+ objects: {
+ "ui:widget": "objectLabels",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ required_zones: {
+ "ui:widget": "zoneNames",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ enabled_in_config: {
+ "ui:widget": "hidden",
+ },
+ },
+ },
+ },
+};
+
+export default objects;
diff --git a/web/src/components/config-form/section-configs/onvif.ts b/web/src/components/config-form/section-configs/onvif.ts
new file mode 100644
index 000000000..973945275
--- /dev/null
+++ b/web/src/components/config-form/section-configs/onvif.ts
@@ -0,0 +1,33 @@
+import type { SectionConfigOverrides } from "./types";
+
+const onvif: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "host",
+ "port",
+ "user",
+ "password",
+ "tls_insecure",
+ "ignore_time_mismatch",
+ "autotracking",
+ ],
+ hiddenFields: [
+ "autotracking.enabled_in_config",
+ "autotracking.movement_weights",
+ ],
+ advancedFields: ["tls_insecure", "ignore_time_mismatch"],
+ overrideFields: [],
+ uiSchema: {
+ autotracking: {
+ required_zones: {
+ "ui:widget": "zoneNames",
+ },
+ track: {
+ "ui:widget": "objectLabels",
+ },
+ },
+ },
+ },
+};
+
+export default onvif;
diff --git a/web/src/components/config-form/section-configs/proxy.ts b/web/src/components/config-form/section-configs/proxy.ts
new file mode 100644
index 000000000..2530f0a98
--- /dev/null
+++ b/web/src/components/config-form/section-configs/proxy.ts
@@ -0,0 +1,17 @@
+import type { SectionConfigOverrides } from "./types";
+
+const proxy: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "header_map",
+ "logout_url",
+ "auth_secret",
+ "default_role",
+ "separator",
+ ],
+ advancedFields: ["header_map", "auth_secret", "separator"],
+ liveValidate: true,
+ },
+};
+
+export default proxy;
diff --git a/web/src/components/config-form/section-configs/record.ts b/web/src/components/config-form/section-configs/record.ts
new file mode 100644
index 000000000..1a15631d6
--- /dev/null
+++ b/web/src/components/config-form/section-configs/record.ts
@@ -0,0 +1,24 @@
+import type { SectionConfigOverrides } from "./types";
+
+const record: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "enabled",
+ "expire_interval",
+ "continuous",
+ "motion",
+ "alerts",
+ "detections",
+ "preview",
+ "export",
+ ],
+ fieldGroups: {
+ retention: ["enabled", "continuous", "motion"],
+ events: ["alerts", "detections"],
+ },
+ hiddenFields: ["enabled_in_config", "sync_recordings"],
+ advancedFields: ["expire_interval", "preview", "export"],
+ },
+};
+
+export default record;
diff --git a/web/src/components/config-form/section-configs/review.ts b/web/src/components/config-form/section-configs/review.ts
new file mode 100644
index 000000000..98301512f
--- /dev/null
+++ b/web/src/components/config-form/section-configs/review.ts
@@ -0,0 +1,31 @@
+import type { SectionConfigOverrides } from "./types";
+
+const review: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["alerts", "detections", "genai"],
+ fieldGroups: {},
+ hiddenFields: [
+ "enabled_in_config",
+ "alerts.labels",
+ "alerts.enabled_in_config",
+ "alerts.required_zones",
+ "detections.labels",
+ "detections.enabled_in_config",
+ "detections.required_zones",
+ "genai.enabled_in_config",
+ ],
+ advancedFields: [],
+ uiSchema: {
+ genai: {
+ additional_concerns: {
+ "ui:widget": "textarea",
+ },
+ activity_context_prompt: {
+ "ui:widget": "textarea",
+ },
+ },
+ },
+ },
+};
+
+export default review;
diff --git a/web/src/components/config-form/section-configs/semantic_search.ts b/web/src/components/config-form/section-configs/semantic_search.ts
new file mode 100644
index 000000000..a85a5bc5b
--- /dev/null
+++ b/web/src/components/config-form/section-configs/semantic_search.ts
@@ -0,0 +1,21 @@
+import type { SectionConfigOverrides } from "./types";
+
+const semanticSearch: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["triggers"],
+ hiddenFields: [],
+ advancedFields: [],
+ overrideFields: [],
+ uiSchema: {
+ enabled: {
+ "ui:after": { render: "SemanticSearchReindex" },
+ },
+ },
+ },
+ global: {
+ fieldOrder: ["enabled", "reindex", "model", "model_size", "device"],
+ advancedFields: ["reindex", "device"],
+ },
+};
+
+export default semanticSearch;
diff --git a/web/src/components/config-form/section-configs/snapshots.ts b/web/src/components/config-form/section-configs/snapshots.ts
new file mode 100644
index 000000000..6b496083b
--- /dev/null
+++ b/web/src/components/config-form/section-configs/snapshots.ts
@@ -0,0 +1,29 @@
+import type { SectionConfigOverrides } from "./types";
+
+const snapshots: SectionConfigOverrides = {
+ base: {
+ fieldOrder: [
+ "enabled",
+ "bounding_box",
+ "crop",
+ "quality",
+ "timestamp",
+ "retain",
+ ],
+ fieldGroups: {
+ display: ["enabled", "bounding_box", "crop", "quality", "timestamp"],
+ },
+ hiddenFields: ["enabled_in_config"],
+ advancedFields: ["quality", "retain"],
+ uiSchema: {
+ required_zones: {
+ "ui:widget": "zoneNames",
+ "ui:options": {
+ suppressMultiSchema: true,
+ },
+ },
+ },
+ },
+};
+
+export default snapshots;
diff --git a/web/src/components/config-form/section-configs/telemetry.ts b/web/src/components/config-form/section-configs/telemetry.ts
new file mode 100644
index 000000000..838258d59
--- /dev/null
+++ b/web/src/components/config-form/section-configs/telemetry.ts
@@ -0,0 +1,10 @@
+import type { SectionConfigOverrides } from "./types";
+
+const telemetry: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["network_interfaces", "stats", "version_check"],
+ advancedFields: [],
+ },
+};
+
+export default telemetry;
diff --git a/web/src/components/config-form/section-configs/timestamp_style.ts b/web/src/components/config-form/section-configs/timestamp_style.ts
new file mode 100644
index 000000000..85d4fd9f5
--- /dev/null
+++ b/web/src/components/config-form/section-configs/timestamp_style.ts
@@ -0,0 +1,11 @@
+import type { SectionConfigOverrides } from "./types";
+
+const timestampStyle: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["position", "format", "color", "thickness"],
+ hiddenFields: ["effect", "enabled_in_config"],
+ advancedFields: [],
+ },
+};
+
+export default timestampStyle;
diff --git a/web/src/components/config-form/section-configs/tls.ts b/web/src/components/config-form/section-configs/tls.ts
new file mode 100644
index 000000000..6f8b64c0b
--- /dev/null
+++ b/web/src/components/config-form/section-configs/tls.ts
@@ -0,0 +1,10 @@
+import type { SectionConfigOverrides } from "./types";
+
+const tls: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["enabled", "cert", "key"],
+ advancedFields: [],
+ },
+};
+
+export default tls;
diff --git a/web/src/components/config-form/section-configs/types.ts b/web/src/components/config-form/section-configs/types.ts
new file mode 100644
index 000000000..600a3ca50
--- /dev/null
+++ b/web/src/components/config-form/section-configs/types.ts
@@ -0,0 +1,7 @@
+import type { SectionConfig } from "../sections/BaseSection";
+
+export type SectionConfigOverrides = {
+ base?: SectionConfig;
+ global?: Partial;
+ camera?: Partial;
+};
diff --git a/web/src/components/config-form/section-configs/ui.ts b/web/src/components/config-form/section-configs/ui.ts
new file mode 100644
index 000000000..4f2e47a98
--- /dev/null
+++ b/web/src/components/config-form/section-configs/ui.ts
@@ -0,0 +1,22 @@
+import type { SectionConfigOverrides } from "./types";
+
+const ui: SectionConfigOverrides = {
+ base: {
+ fieldOrder: ["dashboard", "order"],
+ hiddenFields: [],
+ advancedFields: [],
+ overrideFields: [],
+ },
+ global: {
+ fieldOrder: [
+ "timezone",
+ "time_format",
+ "date_style",
+ "time_style",
+ "unit_system",
+ ],
+ advancedFields: [],
+ },
+};
+
+export default ui;
diff --git a/web/src/components/config-form/sectionConfigs.ts b/web/src/components/config-form/sectionConfigs.ts
index e482089b0..c3a9158a9 100644
--- a/web/src/components/config-form/sectionConfigs.ts
+++ b/web/src/components/config-form/sectionConfigs.ts
@@ -13,830 +13,73 @@
global?: Partial; // overrides for global-level UI
camera?: Partial; // overrides for camera-level UI
}
-
- Merge rules (used by getSectionConfig):
- - `base` is the canonical default and is merged with level-specific overrides.
- - Arrays (e.g., `fieldOrder`, `advancedFields`, etc.) in overrides **replace**
- the `base` arrays (they are not concatenated).
- - `uiSchema` in an override **replaces** the base `uiSchema` rather than deep-merging
- (this keeps widget overrides explicit per level).
- - Other object properties are deep-merged using lodash.mergeWith with custom
- behavior for arrays and `uiSchema` as described.
-
- Example — `ffmpeg`:
- - `base` (camera defaults) may include `inputs` and a `fieldOrder` that shows
- `"inputs"` first.
- - `global` override can replace `fieldOrder` with a different ordering
- (e.g., omit `inputs` and show `path` first). Calling
- `getSectionConfig("ffmpeg", "global")` will return the merged config
- where `fieldOrder` comes from `global` (not concatenated with `base`).
*/
-import mergeWith from "lodash/mergeWith";
-import type { SectionConfig } from "./sections/BaseSection";
+import type { SectionConfigOverrides } from "./section-configs/types";
+import audio from "./section-configs/audio";
+import audioTranscription from "./section-configs/audio_transcription";
+import auth from "./section-configs/auth";
+import birdseye from "./section-configs/birdseye";
+import classification from "./section-configs/classification";
+import database from "./section-configs/database";
+import detect from "./section-configs/detect";
+import detectors from "./section-configs/detectors";
+import environmentVars from "./section-configs/environment_vars";
+import faceRecognition from "./section-configs/face_recognition";
+import ffmpeg from "./section-configs/ffmpeg";
+import genai from "./section-configs/genai";
+import live from "./section-configs/live";
+import logger from "./section-configs/logger";
+import lpr from "./section-configs/lpr";
+import model from "./section-configs/model";
+import motion from "./section-configs/motion";
+import mqtt from "./section-configs/mqtt";
+import networking from "./section-configs/networking";
+import notifications from "./section-configs/notifications";
+import objects from "./section-configs/objects";
+import onvif from "./section-configs/onvif";
+import proxy from "./section-configs/proxy";
+import record from "./section-configs/record";
+import review from "./section-configs/review";
+import semanticSearch from "./section-configs/semantic_search";
+import snapshots from "./section-configs/snapshots";
+import telemetry from "./section-configs/telemetry";
+import timestampStyle from "./section-configs/timestamp_style";
+import tls from "./section-configs/tls";
+import ui from "./section-configs/ui";
-export type SectionConfigOverrides = {
- base?: SectionConfig;
- global?: Partial;
- camera?: Partial;
+export const sectionConfigs: Record = {
+ detect,
+ record,
+ snapshots,
+ motion,
+ objects,
+ review,
+ audio,
+ live,
+ timestamp_style: timestampStyle,
+ notifications,
+ onvif,
+ ffmpeg,
+ audio_transcription: audioTranscription,
+ birdseye,
+ face_recognition: faceRecognition,
+ lpr,
+ semantic_search: semanticSearch,
+ mqtt,
+ ui,
+ database,
+ auth,
+ tls,
+ networking,
+ proxy,
+ logger,
+ environment_vars: environmentVars,
+ telemetry,
+ detectors,
+ model,
+ genai,
+ classification,
};
-const sectionConfigs: Record = {
- detect: {
- base: {
- fieldOrder: [
- "enabled",
- "fps",
- "width",
- "height",
- "min_initialized",
- "max_disappeared",
- "annotation_offset",
- "stationary",
- ],
- fieldGroups: {
- resolution: ["enabled", "width", "height"],
- tracking: ["min_initialized", "max_disappeared"],
- },
- hiddenFields: ["enabled_in_config"],
- advancedFields: [
- "min_initialized",
- "max_disappeared",
- "annotation_offset",
- "stationary",
- ],
- },
- },
- record: {
- base: {
- fieldOrder: [
- "enabled",
- "expire_interval",
- "continuous",
- "motion",
- "alerts",
- "detections",
- "preview",
- "export",
- ],
- fieldGroups: {
- retention: ["enabled", "continuous", "motion"],
- events: ["alerts", "detections"],
- },
- hiddenFields: ["enabled_in_config", "sync_recordings"],
- advancedFields: ["expire_interval", "preview", "export"],
- },
- },
- snapshots: {
- base: {
- fieldOrder: [
- "enabled",
- "bounding_box",
- "crop",
- "quality",
- "timestamp",
- "retain",
- ],
- fieldGroups: {
- display: ["enabled", "bounding_box", "crop", "quality", "timestamp"],
- },
- hiddenFields: ["enabled_in_config"],
- advancedFields: ["quality", "retain"],
- uiSchema: {
- required_zones: {
- "ui:widget": "zoneNames",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- },
- },
- },
- motion: {
- base: {
- fieldOrder: [
- "enabled",
- "threshold",
- "lightning_threshold",
- "improve_contrast",
- "contour_area",
- "delta_alpha",
- "frame_alpha",
- "frame_height",
- "mask",
- "mqtt_off_delay",
- ],
- fieldGroups: {
- sensitivity: ["enabled", "threshold", "contour_area"],
- algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"],
- },
- hiddenFields: ["enabled_in_config", "mask", "raw_mask"],
- advancedFields: [
- "lightning_threshold",
- "delta_alpha",
- "frame_alpha",
- "frame_height",
- "mqtt_off_delay",
- ],
- },
- },
- objects: {
- base: {
- fieldOrder: ["track", "alert", "detect", "filters"],
- fieldGroups: {
- tracking: ["track", "alert", "detect"],
- filtering: ["filters"],
- },
- hiddenFields: [
- "enabled_in_config",
- "mask",
- "raw_mask",
- "genai.enabled_in_config",
- "filters.*.mask",
- "filters.*.raw_mask",
- ],
- advancedFields: ["filters"],
- uiSchema: {
- "filters.*.min_area": {
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- "filters.*.max_area": {
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- track: {
- "ui:widget": "objectLabels",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- genai: {
- objects: {
- "ui:widget": "objectLabels",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- required_zones: {
- "ui:widget": "zoneNames",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- enabled_in_config: {
- "ui:widget": "hidden",
- },
- },
- },
- },
- },
- review: {
- base: {
- fieldOrder: ["alerts", "detections", "genai"],
- fieldGroups: {},
- hiddenFields: [
- "enabled_in_config",
- "alerts.labels",
- "alerts.enabled_in_config",
- "alerts.required_zones",
- "detections.labels",
- "detections.enabled_in_config",
- "detections.required_zones",
- "genai.enabled_in_config",
- ],
- advancedFields: [],
- uiSchema: {
- genai: {
- additional_concerns: {
- "ui:widget": "textarea",
- },
- activity_context_prompt: {
- "ui:widget": "textarea",
- },
- },
- },
- },
- },
- audio: {
- base: {
- fieldOrder: [
- "enabled",
- "listen",
- "filters",
- "min_volume",
- "max_not_heard",
- "num_threads",
- ],
- fieldGroups: {
- detection: ["enabled", "listen", "filters"],
- sensitivity: ["min_volume", "max_not_heard"],
- },
- hiddenFields: ["enabled_in_config"],
- advancedFields: ["min_volume", "max_not_heard", "num_threads"],
- uiSchema: {
- listen: {
- "ui:widget": "audioLabels",
- },
- },
- },
- },
- live: {
- base: {
- fieldOrder: ["stream_name", "height", "quality"],
- fieldGroups: {},
- hiddenFields: ["enabled_in_config"],
- advancedFields: ["quality"],
- },
- },
- timestamp_style: {
- base: {
- fieldOrder: ["position", "format", "color", "thickness"],
- hiddenFields: ["effect", "enabled_in_config"],
- advancedFields: [],
- },
- },
- notifications: {
- base: {
- fieldOrder: ["enabled", "email"],
- fieldGroups: {},
- hiddenFields: ["enabled_in_config"],
- advancedFields: [],
- },
- },
- onvif: {
- base: {
- fieldOrder: [
- "host",
- "port",
- "user",
- "password",
- "tls_insecure",
- "ignore_time_mismatch",
- "autotracking",
- ],
- hiddenFields: [
- "autotracking.enabled_in_config",
- "autotracking.movement_weights",
- ],
- advancedFields: ["tls_insecure", "ignore_time_mismatch"],
- overrideFields: [],
- uiSchema: {
- autotracking: {
- required_zones: {
- "ui:widget": "zoneNames",
- },
- track: {
- "ui:widget": "objectLabels",
- },
- },
- },
- },
- },
- ffmpeg: {
- base: {
- fieldOrder: [
- "inputs",
- "path",
- "global_args",
- "hwaccel_args",
- "input_args",
- "output_args",
- "retry_interval",
- "apple_compatibility",
- "gpu",
- ],
- hiddenFields: [],
- advancedFields: [
- "global_args",
- "hwaccel_args",
- "input_args",
- "output_args",
- "retry_interval",
- "apple_compatibility",
- "gpu",
- ],
- overrideFields: [
- "path",
- "global_args",
- "hwaccel_args",
- "input_args",
- "output_args",
- "retry_interval",
- "apple_compatibility",
- "gpu",
- ],
- uiSchema: {
- global_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- hwaccel_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- input_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- output_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- detect: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- record: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- items: {
- detect: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- record: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- },
- },
- inputs: {
- items: {
- global_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- hwaccel_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- input_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- output_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- items: {
- detect: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- record: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- },
- },
- },
- },
- },
- },
- global: {
- fieldOrder: [
- "path",
- "global_args",
- "hwaccel_args",
- "input_args",
- "output_args",
- "retry_interval",
- "apple_compatibility",
- "gpu",
- ],
- advancedFields: [
- "global_args",
- "hwaccel_args",
- "input_args",
- "output_args",
- "retry_interval",
- "apple_compatibility",
- "gpu",
- ],
- uiSchema: {
- global_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- hwaccel_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- input_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- output_args: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- detect: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- record: {
- "ui:widget": "ArrayAsTextWidget",
- "ui:options": {
- suppressMultiSchema: true,
- },
- },
- },
- },
- },
- },
- audio_transcription: {
- base: {
- fieldOrder: [
- "enabled",
- "language",
- "device",
- "model_size",
- "live_enabled",
- ],
- hiddenFields: ["enabled_in_config"],
- advancedFields: ["language", "device", "model_size"],
- overrideFields: ["enabled", "live_enabled"],
- },
- global: {
- fieldOrder: [
- "enabled",
- "language",
- "device",
- "model_size",
- "live_enabled",
- ],
- advancedFields: ["language", "device", "model_size"],
- },
- },
- birdseye: {
- base: {
- fieldOrder: ["enabled", "mode", "order"],
- hiddenFields: [],
- advancedFields: [],
- overrideFields: ["enabled", "mode"],
- },
- global: {
- fieldOrder: [
- "enabled",
- "restream",
- "width",
- "height",
- "quality",
- "mode",
- "layout",
- "inactivity_threshold",
- "idle_heartbeat_fps",
- ],
- advancedFields: ["width", "height", "quality", "inactivity_threshold"],
- },
- },
- face_recognition: {
- base: {
- fieldOrder: ["enabled", "min_area"],
- hiddenFields: [],
- advancedFields: ["min_area"],
- overrideFields: ["enabled", "min_area"],
- },
- global: {
- fieldOrder: [
- "enabled",
- "model_size",
- "unknown_score",
- "detection_threshold",
- "recognition_threshold",
- "min_area",
- "min_faces",
- "save_attempts",
- "blur_confidence_filter",
- "device",
- ],
- advancedFields: [
- "unknown_score",
- "detection_threshold",
- "recognition_threshold",
- "min_area",
- "min_faces",
- "save_attempts",
- "blur_confidence_filter",
- "device",
- ],
- },
- },
- lpr: {
- base: {
- fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"],
- hiddenFields: [],
- advancedFields: ["expire_time", "min_area", "enhancement"],
- overrideFields: ["enabled", "min_area", "enhancement"],
- },
- global: {
- fieldOrder: [
- "enabled",
- "model_size",
- "detection_threshold",
- "min_area",
- "recognition_threshold",
- "min_plate_length",
- "format",
- "match_distance",
- "known_plates",
- "enhancement",
- "debug_save_plates",
- "device",
- "replace_rules",
- ],
- advancedFields: [
- "detection_threshold",
- "recognition_threshold",
- "min_plate_length",
- "format",
- "match_distance",
- "known_plates",
- "enhancement",
- "debug_save_plates",
- "device",
- "replace_rules",
- ],
- },
- },
- semantic_search: {
- base: {
- fieldOrder: ["triggers"],
- hiddenFields: [],
- advancedFields: [],
- overrideFields: [],
- uiSchema: {
- enabled: {
- "ui:after": { render: "SemanticSearchReindex" },
- },
- },
- },
- global: {
- fieldOrder: ["enabled", "reindex", "model", "model_size", "device"],
- advancedFields: ["reindex", "device"],
- },
- },
- mqtt: {
- base: {
- fieldOrder: [
- "enabled",
- "timestamp",
- "bounding_box",
- "crop",
- "height",
- "required_zones",
- "quality",
- ],
- hiddenFields: [],
- advancedFields: ["height", "quality"],
- overrideFields: [],
- uiSchema: {
- required_zones: {
- "ui:widget": "zoneNames",
- },
- },
- },
- global: {
- fieldOrder: [
- "enabled",
- "host",
- "port",
- "user",
- "password",
- "topic_prefix",
- "client_id",
- "stats_interval",
- "qos",
- "tls_ca_certs",
- "tls_client_cert",
- "tls_client_key",
- "tls_insecure",
- ],
- advancedFields: [
- "stats_interval",
- "qos",
- "tls_ca_certs",
- "tls_client_cert",
- "tls_client_key",
- "tls_insecure",
- ],
- liveValidate: true,
- uiSchema: {},
- },
- },
- ui: {
- base: {
- fieldOrder: ["dashboard", "order"],
- hiddenFields: [],
- advancedFields: [],
- overrideFields: [],
- },
- global: {
- fieldOrder: [
- "timezone",
- "time_format",
- "date_style",
- "time_style",
- "unit_system",
- ],
- advancedFields: [],
- },
- },
- database: {
- base: {
- fieldOrder: ["path"],
- advancedFields: [],
- },
- },
- auth: {
- base: {
- fieldOrder: [
- "enabled",
- "reset_admin_password",
- "cookie_name",
- "cookie_secure",
- "session_length",
- "refresh_time",
- "native_oauth_url",
- "failed_login_rate_limit",
- "trusted_proxies",
- "hash_iterations",
- "roles",
- ],
- hiddenFields: ["admin_first_time_login"],
- advancedFields: [
- "cookie_name",
- "cookie_secure",
- "session_length",
- "refresh_time",
- "failed_login_rate_limit",
- "trusted_proxies",
- "hash_iterations",
- "roles",
- ],
- uiSchema: {
- reset_admin_password: {
- "ui:widget": "switch",
- },
- },
- },
- },
- tls: {
- base: {
- fieldOrder: ["enabled", "cert", "key"],
- advancedFields: [],
- },
- },
- networking: {
- base: {
- fieldOrder: ["ipv6"],
- advancedFields: [],
- },
- },
- proxy: {
- base: {
- fieldOrder: [
- "header_map",
- "logout_url",
- "auth_secret",
- "default_role",
- "separator",
- ],
- advancedFields: ["header_map", "auth_secret", "separator"],
- liveValidate: true,
- },
- },
- logger: {
- base: {
- fieldOrder: ["default", "logs"],
- advancedFields: ["logs"],
- },
- },
- environment_vars: {
- base: {
- fieldOrder: [],
- advancedFields: [],
- },
- },
- telemetry: {
- base: {
- fieldOrder: ["network_interfaces", "stats", "version_check"],
- advancedFields: [],
- },
- },
- detectors: {
- base: {
- fieldOrder: [],
- advancedFields: [],
- },
- },
- model: {
- base: {
- fieldOrder: [
- "path",
- "labelmap_path",
- "width",
- "height",
- "input_pixel_format",
- "input_tensor",
- "input_dtype",
- "model_type",
- ],
- advancedFields: [
- "input_pixel_format",
- "input_tensor",
- "input_dtype",
- "model_type",
- ],
- hiddenFields: ["labelmap", "attributes_map"],
- },
- },
- genai: {
- base: {
- fieldOrder: [
- "provider",
- "api_key",
- "base_url",
- "model",
- "provider_options",
- "runtime_options",
- ],
- advancedFields: ["base_url", "provider_options", "runtime_options"],
- hiddenFields: ["genai.enabled_in_config"],
- },
- },
- classification: {
- base: {
- hiddenFields: ["custom"],
- advancedFields: [],
- },
- },
-};
-
-const mergeSectionConfig = (
- base: SectionConfig | undefined,
- overrides: Partial | undefined,
-): SectionConfig =>
- mergeWith({}, base ?? {}, overrides ?? {}, (objValue, srcValue, key) => {
- if (Array.isArray(objValue) || Array.isArray(srcValue)) {
- return srcValue ?? objValue;
- }
-
- if (key === "uiSchema" && srcValue !== undefined) {
- return srcValue;
- }
-
- return undefined;
- });
-
-export function getSectionConfig(
- sectionKey: string,
- level: "global" | "camera",
-): SectionConfig {
- const entry = sectionConfigs[sectionKey];
- if (!entry) {
- return {};
- }
-
- const overrides = level === "global" ? entry.global : entry.camera;
- return mergeSectionConfig(entry.base, overrides);
-}
+export type { SectionConfigOverrides } from "./section-configs/types";
diff --git a/web/src/components/config-form/sectionConfigsUtils.ts b/web/src/components/config-form/sectionConfigsUtils.ts
new file mode 100644
index 000000000..c8467bf75
--- /dev/null
+++ b/web/src/components/config-form/sectionConfigsUtils.ts
@@ -0,0 +1,32 @@
+import mergeWith from "lodash/mergeWith";
+import type { SectionConfig } from "./sections/BaseSection";
+import { sectionConfigs } from "./sectionConfigs";
+
+const mergeSectionConfig = (
+ base: SectionConfig | undefined,
+ overrides: Partial | undefined,
+): SectionConfig =>
+ mergeWith({}, base ?? {}, overrides ?? {}, (objValue, srcValue, key) => {
+ if (Array.isArray(objValue) || Array.isArray(srcValue)) {
+ return srcValue ?? objValue;
+ }
+
+ if (key === "uiSchema" && srcValue !== undefined) {
+ return srcValue;
+ }
+
+ return undefined;
+ });
+
+export function getSectionConfig(
+ sectionKey: string,
+ level: "global" | "camera",
+): SectionConfig {
+ const entry = sectionConfigs[sectionKey];
+ if (!entry) {
+ return {};
+ }
+
+ const overrides = level === "global" ? entry.global : entry.camera;
+ return mergeSectionConfig(entry.base, overrides);
+}
diff --git a/web/src/components/config-form/sections/ConfigSectionTemplate.tsx b/web/src/components/config-form/sections/ConfigSectionTemplate.tsx
new file mode 100644
index 000000000..18cd58ede
--- /dev/null
+++ b/web/src/components/config-form/sections/ConfigSectionTemplate.tsx
@@ -0,0 +1,40 @@
+import { useMemo } from "react";
+import { createConfigSection } from "./BaseSection";
+import type { BaseSectionProps, SectionConfig } from "./BaseSection";
+import { getSectionConfig } from "@/components/config-form/sectionConfigsUtils";
+
+export type ConfigSectionTemplateProps = BaseSectionProps & {
+ sectionKey: string;
+ sectionConfig?: SectionConfig;
+};
+
+export function ConfigSectionTemplate({
+ sectionKey,
+ level,
+ sectionConfig,
+ ...rest
+}: ConfigSectionTemplateProps) {
+ const defaultConfig = useMemo(
+ () => getSectionConfig(sectionKey, level),
+ [sectionKey, level],
+ );
+
+ const SectionComponent = useMemo(
+ () =>
+ createConfigSection({
+ sectionPath: sectionKey,
+ defaultConfig,
+ }),
+ [sectionKey, defaultConfig],
+ );
+
+ return (
+
+ );
+}
+
+export default ConfigSectionTemplate;
diff --git a/web/src/components/config-form/sections/index.ts b/web/src/components/config-form/sections/index.ts
index ca92c1409..3dce059f0 100644
--- a/web/src/components/config-form/sections/index.ts
+++ b/web/src/components/config-form/sections/index.ts
@@ -7,37 +7,7 @@ export {
type SectionConfig,
type CreateSectionOptions,
} from "./BaseSection";
-
-export { DetectSection } from "./DetectSection";
-export { RecordSection } from "./RecordSection";
-export { SnapshotsSection } from "./SnapshotsSection";
-export { MotionSection } from "./MotionSection";
-export { ObjectsSection } from "./ObjectsSection";
-export { ReviewSection } from "./ReviewSection";
-export { AudioSection } from "./AudioSection";
-export { AudioTranscriptionSection } from "./AudioTranscriptionSection";
-export { BirdseyeSection } from "./BirdseyeSection";
-export { AuthSection } from "./AuthSection";
-export { ClassificationSection } from "./ClassificationSection";
-export { CameraMqttSection } from "./CameraMqttSection";
-export { CameraUiSection } from "./CameraUiSection";
-export { DatabaseSection } from "./DatabaseSection";
-export { DetectorsSection } from "./DetectorsSection";
-export { EnvironmentVarsSection } from "./EnvironmentVarsSection";
-export { FaceRecognitionSection } from "./FaceRecognitionSection";
-export { FfmpegSection } from "./FfmpegSection";
-export { GenaiSection } from "./GenaiSection";
-export { LprSection } from "./LprSection";
-export { LoggerSection } from "./LoggerSection";
-export { NotificationsSection } from "./NotificationsSection";
-export { OnvifSection } from "./OnvifSection";
-export { LiveSection } from "./LiveSection";
-export { ModelSection } from "./ModelSection";
-export { MqttSection } from "./MqttSection";
-export { NetworkingSection } from "./NetworkingSection";
-export { ProxySection } from "./ProxySection";
-export { SemanticSearchSection } from "./SemanticSearchSection";
-export { TelemetrySection } from "./TelemetrySection";
-export { TimestampSection } from "./TimestampSection";
-export { TlsSection } from "./TlsSection";
-export { UiSection } from "./UiSection";
+export {
+ ConfigSectionTemplate,
+ type ConfigSectionTemplateProps,
+} from "./ConfigSectionTemplate";
diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx
index f74cfc817..e76899e9b 100644
--- a/web/src/pages/Settings.tsx
+++ b/web/src/pages/Settings.tsx
@@ -65,10 +65,6 @@ import { cn } from "@/lib/utils";
import Heading from "@/components/ui/heading";
import { LuChevronRight } from "react-icons/lu";
import Logo from "@/components/Logo";
-import {
- CameraMqttSection,
- MqttSection,
-} from "@/components/config-form/sections";
import {
MobilePage,
MobilePageContent,
@@ -100,13 +96,11 @@ type SettingsType = (typeof allSettingsViews)[number];
const MqttSettingsPage = createSingleSectionPage({
sectionKey: "mqtt",
level: "global",
- SectionComponent: MqttSection,
});
const CameraMqttSettingsPage = createSingleSectionPage({
sectionKey: "mqtt",
level: "camera",
- SectionComponent: CameraMqttSection,
showOverrideIndicator: false,
});
diff --git a/web/src/views/settings/CameraConfigView.tsx b/web/src/views/settings/CameraConfigView.tsx
index 51ec0d8bb..d073dacbb 100644
--- a/web/src/views/settings/CameraConfigView.tsx
+++ b/web/src/views/settings/CameraConfigView.tsx
@@ -4,24 +4,7 @@
import { useMemo, useCallback, useState, memo } from "react";
import useSWR from "swr";
import { useTranslation } from "react-i18next";
-import { DetectSection } from "@/components/config-form/sections/DetectSection";
-import { RecordSection } from "@/components/config-form/sections/RecordSection";
-import { SnapshotsSection } from "@/components/config-form/sections/SnapshotsSection";
-import { MotionSection } from "@/components/config-form/sections/MotionSection";
-import { ObjectsSection } from "@/components/config-form/sections/ObjectsSection";
-import { ReviewSection } from "@/components/config-form/sections/ReviewSection";
-import { AudioSection } from "@/components/config-form/sections/AudioSection";
-import { AudioTranscriptionSection } from "@/components/config-form/sections/AudioTranscriptionSection";
-import { BirdseyeSection } from "@/components/config-form/sections/BirdseyeSection";
-import { CameraMqttSection } from "@/components/config-form/sections/CameraMqttSection";
-import { CameraUiSection } from "@/components/config-form/sections/CameraUiSection";
-import { FaceRecognitionSection } from "@/components/config-form/sections/FaceRecognitionSection";
-import { FfmpegSection } from "@/components/config-form/sections/FfmpegSection";
-import { LprSection } from "@/components/config-form/sections/LprSection";
-import { NotificationsSection } from "@/components/config-form/sections/NotificationsSection";
-import { OnvifSection } from "@/components/config-form/sections/OnvifSection";
-import { LiveSection } from "@/components/config-form/sections/LiveSection";
-import { TimestampSection } from "@/components/config-form/sections/TimestampSection";
+import { ConfigSectionTemplate } from "@/components/config-form/sections";
import { useAllCameraOverrides } from "@/hooks/use-config-override";
import type { FrigateConfig } from "@/types/frigateConfig";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -203,83 +186,26 @@ const CameraConfigContent = memo(function CameraConfigContent({
const sections: Array<{
key: string;
- component: typeof DetectSection;
showOverrideIndicator?: boolean;
}> = [
- {
- key: "detect",
- component: DetectSection,
- },
- {
- key: "ffmpeg",
- component: FfmpegSection,
- showOverrideIndicator: true,
- },
- {
- key: "record",
- component: RecordSection,
- },
- {
- key: "snapshots",
- component: SnapshotsSection,
- },
- {
- key: "motion",
- component: MotionSection,
- },
- {
- key: "objects",
- component: ObjectsSection,
- },
- {
- key: "review",
- component: ReviewSection,
- },
- { key: "audio", component: AudioSection },
- {
- key: "audio_transcription",
- component: AudioTranscriptionSection,
- showOverrideIndicator: true,
- },
- {
- key: "notifications",
- component: NotificationsSection,
- },
- { key: "live", component: LiveSection },
- {
- key: "birdseye",
- component: BirdseyeSection,
- showOverrideIndicator: true,
- },
- {
- key: "face_recognition",
- component: FaceRecognitionSection,
- showOverrideIndicator: true,
- },
- {
- key: "lpr",
- component: LprSection,
- showOverrideIndicator: true,
- },
- {
- key: "mqtt",
- component: CameraMqttSection,
- showOverrideIndicator: false,
- },
- {
- key: "onvif",
- component: OnvifSection,
- showOverrideIndicator: false,
- },
- {
- key: "ui",
- component: CameraUiSection,
- showOverrideIndicator: false,
- },
- {
- key: "timestamp_style",
- component: TimestampSection,
- },
+ { key: "detect" },
+ { key: "ffmpeg", showOverrideIndicator: true },
+ { key: "record" },
+ { key: "snapshots" },
+ { key: "motion" },
+ { key: "objects" },
+ { key: "review" },
+ { key: "audio" },
+ { key: "audio_transcription", showOverrideIndicator: true },
+ { key: "notifications" },
+ { key: "live" },
+ { key: "birdseye", showOverrideIndicator: true },
+ { key: "face_recognition", showOverrideIndicator: true },
+ { key: "lpr", showOverrideIndicator: true },
+ { key: "mqtt", showOverrideIndicator: false },
+ { key: "onvif", showOverrideIndicator: false },
+ { key: "ui", showOverrideIndicator: false },
+ { key: "timestamp_style" },
];
return (
@@ -327,23 +253,21 @@ const CameraConfigContent = memo(function CameraConfigContent({
{/* Section Content */}
- {sections.map((section) => {
- const SectionComponent = section.component;
- return (
-
-
-
- );
- })}
+ {sections.map((section) => (
+
+
+
+ ))}
);
diff --git a/web/src/views/settings/GlobalConfigView.tsx b/web/src/views/settings/GlobalConfigView.tsx
index 72b0df7be..d06f928fa 100644
--- a/web/src/views/settings/GlobalConfigView.tsx
+++ b/web/src/views/settings/GlobalConfigView.tsx
@@ -4,81 +4,51 @@
import { useMemo, useCallback, useState } from "react";
import useSWR from "swr";
import { useTranslation } from "react-i18next";
-import { DetectSection } from "@/components/config-form/sections/DetectSection";
-import { RecordSection } from "@/components/config-form/sections/RecordSection";
-import { SnapshotsSection } from "@/components/config-form/sections/SnapshotsSection";
-import { MotionSection } from "@/components/config-form/sections/MotionSection";
-import { ObjectsSection } from "@/components/config-form/sections/ObjectsSection";
-import { ReviewSection } from "@/components/config-form/sections/ReviewSection";
-import { AudioSection } from "@/components/config-form/sections/AudioSection";
-import { AudioTranscriptionSection } from "@/components/config-form/sections/AudioTranscriptionSection";
-import { AuthSection } from "@/components/config-form/sections/AuthSection";
-import { BirdseyeSection } from "@/components/config-form/sections/BirdseyeSection";
-import { ClassificationSection } from "@/components/config-form/sections/ClassificationSection";
-import { DatabaseSection } from "@/components/config-form/sections/DatabaseSection";
-import { DetectorsSection } from "@/components/config-form/sections/DetectorsSection";
-import { EnvironmentVarsSection } from "@/components/config-form/sections/EnvironmentVarsSection";
-import { FaceRecognitionSection } from "@/components/config-form/sections/FaceRecognitionSection";
-import { FfmpegSection } from "@/components/config-form/sections/FfmpegSection";
-import { GenaiSection } from "@/components/config-form/sections/GenaiSection";
-import { LiveSection } from "@/components/config-form/sections/LiveSection";
-import { LoggerSection } from "@/components/config-form/sections/LoggerSection";
-import { LprSection } from "@/components/config-form/sections/LprSection";
-import { ModelSection } from "@/components/config-form/sections/ModelSection";
-import { MqttSection } from "@/components/config-form/sections/MqttSection";
-import { NetworkingSection } from "@/components/config-form/sections/NetworkingSection";
-import { ProxySection } from "@/components/config-form/sections/ProxySection";
-import { SemanticSearchSection } from "@/components/config-form/sections/SemanticSearchSection";
-import { TimestampSection } from "@/components/config-form/sections/TimestampSection";
-import { TelemetrySection } from "@/components/config-form/sections/TelemetrySection";
-import { TlsSection } from "@/components/config-form/sections/TlsSection";
-import { UiSection } from "@/components/config-form/sections/UiSection";
+import { ConfigSectionTemplate } from "@/components/config-form/sections";
import type { FrigateConfig } from "@/types/frigateConfig";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import Heading from "@/components/ui/heading";
import { cn } from "@/lib/utils";
-import { getSectionConfig } from "@/components/config-form/sectionConfigs";
-
// Shared sections that can be overridden at camera level
const sharedSections = [
- { key: "detect", component: DetectSection },
- { key: "record", component: RecordSection },
- { key: "snapshots", component: SnapshotsSection },
- { key: "motion", component: MotionSection },
- { key: "objects", component: ObjectsSection },
- { key: "review", component: ReviewSection },
- { key: "audio", component: AudioSection },
- { key: "live", component: LiveSection },
- { key: "timestamp_style", component: TimestampSection },
+ { key: "detect" },
+ { key: "record" },
+ { key: "snapshots" },
+ { key: "motion" },
+ { key: "objects" },
+ { key: "review" },
+ { key: "audio" },
+ { key: "live" },
+ { key: "timestamp_style" },
];
// System sections (global only)
const systemSections = [
- { key: "database", component: DatabaseSection },
- { key: "tls", component: TlsSection },
- { key: "auth", component: AuthSection },
- { key: "networking", component: NetworkingSection },
- { key: "proxy", component: ProxySection },
- { key: "ui", component: UiSection },
- { key: "logger", component: LoggerSection },
- { key: "environment_vars", component: EnvironmentVarsSection },
- { key: "telemetry", component: TelemetrySection },
- { key: "birdseye", component: BirdseyeSection },
- { key: "ffmpeg", component: FfmpegSection },
- { key: "detectors", component: DetectorsSection },
- { key: "model", component: ModelSection },
+ { key: "database" },
+ { key: "tls" },
+ { key: "auth" },
+ { key: "networking" },
+ { key: "proxy" },
+ { key: "ui" },
+ { key: "logger" },
+ { key: "environment_vars" },
+ { key: "telemetry" },
+ { key: "birdseye" },
+ { key: "ffmpeg" },
+ { key: "detectors" },
+ { key: "model" },
];
// Integration sections (global only)
const integrationSections = [
- { key: "mqtt", component: MqttSection },
- { key: "semantic_search", component: SemanticSearchSection },
- { key: "genai", component: GenaiSection },
- { key: "face_recognition", component: FaceRecognitionSection },
- { key: "lpr", component: LprSection },
- { key: "classification", component: ClassificationSection },
- { key: "audio_transcription", component: AudioTranscriptionSection },
+ { key: "mqtt" },
+ { key: "semantic_search" },
+ { key: "genai" },
+ { key: "face_recognition" },
+ { key: "lpr" },
+ { key: "classification" },
+ { key: "audio_transcription" },
];
export default function GlobalConfigView() {
@@ -201,24 +171,21 @@ export default function GlobalConfigView() {
{/* Section Content */}
- {currentSections.map((section) => {
- const SectionComponent = section.component;
- return (
-
-
-
- );
- })}
+ {currentSections.map((section) => (
+
+
+
+ ))}
diff --git a/web/src/views/settings/SingleSectionPage.tsx b/web/src/views/settings/SingleSectionPage.tsx
index e73b08947..24f9cfbc9 100644
--- a/web/src/views/settings/SingleSectionPage.tsx
+++ b/web/src/views/settings/SingleSectionPage.tsx
@@ -1,9 +1,7 @@
import { useTranslation } from "react-i18next";
import Heading from "@/components/ui/heading";
-import type {
- BaseSectionProps,
- SectionConfig,
-} from "@/components/config-form/sections";
+import type { SectionConfig } from "@/components/config-form/sections";
+import { ConfigSectionTemplate } from "@/components/config-form/sections";
import type { PolygonType } from "@/types/canvas";
export type SettingsPageProps = {
@@ -15,7 +13,6 @@ export type SettingsPageProps = {
export type SingleSectionPageOptions = {
sectionKey: string;
level: "global" | "camera";
- SectionComponent: React.ComponentType;
sectionConfig?: SectionConfig;
requiresRestart?: boolean;
showOverrideIndicator?: boolean;
@@ -24,7 +21,6 @@ export type SingleSectionPageOptions = {
export function createSingleSectionPage({
sectionKey,
level,
- SectionComponent,
sectionConfig,
requiresRestart,
showOverrideIndicator = true,
@@ -63,7 +59,8 @@ export function createSingleSectionPage({
)}
-