2025-10-13 19:52:08 +03:00
|
|
|
import Heading from "@/components/ui/heading";
|
2026-05-19 06:52:40 +03:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
2026-02-27 18:55:36 +03:00
|
|
|
import {
|
|
|
|
|
CONTROL_COLUMN_CLASS_NAME,
|
|
|
|
|
SettingsGroupCard,
|
|
|
|
|
SPLIT_ROW_CLASS_NAME,
|
|
|
|
|
} from "@/components/card/SettingsGroupCard";
|
|
|
|
|
import { toast } from "sonner";
|
2026-01-20 18:17:58 +03:00
|
|
|
import { Toaster } from "@/components/ui/sonner";
|
2025-10-13 19:52:08 +03:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import useSWR from "swr";
|
|
|
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import CameraWizardDialog from "@/components/settings/CameraWizardDialog";
|
2026-03-09 01:23:48 +03:00
|
|
|
import DeleteCameraDialog from "@/components/overlay/dialog/DeleteCameraDialog";
|
2026-05-19 06:52:40 +03:00
|
|
|
import {
|
|
|
|
|
LuCheck,
|
|
|
|
|
LuExternalLink,
|
|
|
|
|
LuGripVertical,
|
|
|
|
|
LuPencil,
|
|
|
|
|
LuPlus,
|
2026-05-24 23:59:56 +03:00
|
|
|
LuRefreshCcw,
|
2026-05-19 06:52:40 +03:00
|
|
|
LuTrash2,
|
|
|
|
|
} from "react-icons/lu";
|
|
|
|
|
import { Reorder, useDragControls } from "framer-motion";
|
2026-05-06 19:01:50 +03:00
|
|
|
import { Link } from "react-router-dom";
|
|
|
|
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
2025-11-07 17:02:06 +03:00
|
|
|
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
2025-10-13 19:52:08 +03:00
|
|
|
import { Trans } from "react-i18next";
|
2026-02-27 18:55:36 +03:00
|
|
|
import { useEnabledState, useRestart } from "@/api/ws";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import axios from "axios";
|
|
|
|
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
|
|
|
|
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
|
|
|
|
|
import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator";
|
2026-05-22 16:52:01 +03:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
2026-04-27 01:09:35 +03:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "@/components/ui/tooltip";
|
Camera profile support (#22482)
* add CameraProfileConfig model for named config overrides
* add profiles field to CameraConfig
* add active_profile field to FrigateConfig
Runtime-only field excluded from YAML serialization, tracks which
profile is currently active.
* add ProfileManager for profile activation and persistence
Handles snapshotting base configs, applying profile overrides via
deep_merge + apply_section_update, publishing ZMQ updates, and
persisting active profile to /config/.active_profile.
* add profile API endpoints (GET /profiles, GET/PUT /profile)
* add MQTT and dispatcher integration for profiles
- Subscribe to frigate/profile/set MQTT topic
- Publish profile/state and profiles/available on connect
- Add _on_profile_command handler to dispatcher
- Broadcast active profile state on WebSocket connect
* wire ProfileManager into app startup and FastAPI
- Create ProfileManager after dispatcher init
- Restore persisted profile on startup
- Pass dispatcher and profile_manager to FastAPI app
* add tests for invalid profile values and keys
Tests that Pydantic rejects: invalid field values (fps: "not_a_number"),
unknown section keys (ffmpeg in profile), invalid nested values, and
invalid profiles in full config parsing.
* formatting
* fix CameraLiveConfig JSON serialization error on profile activation
refactor _publish_updates to only publish ZMQ updates for
sections that actually changed, not all sections on affected cameras.
* consolidate
* add enabled field to camera profiles for enabling/disabling cameras
* add zones support to camera profiles
* add frontend profile types, color utility, and config save support
* add profile state management and save preview support
* add profileName prop to BaseSection for profile-aware config editing
* add profile section dropdown and wire into camera settings pages
* add per-profile camera enable/disable to Camera Management view
* add profiles summary page with card-based layout and fix backend zone comparison bug
* add active profile badge to settings toolbar
* i18n
* add red dot for any pending changes including profiles
* profile support for mask and zone editor
* fix hidden field validation errors caused by lodash wildcard and schema gaps
lodash unset does not support wildcard (*) segments, so hidden fields like
filters.*.mask were never stripped from form data, leaving null raw_coordinates
that fail RJSF anyOf validation. Add unsetWithWildcard helper and also strip
hidden fields from the JSON schema itself as defense-in-depth.
* add face_recognition and lpr to profile-eligible sections
* move profile dropdown from section panes to settings header
* add profiles enable toggle and improve empty state
* formatting
* tweaks
* tweak colors and switch
* fix profile save diff, masksAndZones delete, and config sync
* ui tweaks
* ensure profile manager gets updated config
* rename profile settings to ui settings
* refactor profilesview and add dots/border colors when overridden
* implement an update_config method for profile manager
* fix mask deletion
* more unique colors
* add top-level profiles config section with friendly names
* implement profile friendly names and improve profile UI
- Add ProfileDefinitionConfig type and profiles field to FrigateConfig
- Use ProfilesApiResponse type with friendly_name support throughout
- Replace Record<string, unknown> with proper JsonObject/JsonValue types
- Add profile creation form matching zone pattern (Zod + NameAndIdFields)
- Add pencil icon for renaming profile friendly names in ProfilesView
- Move Profiles menu item to first under Camera Configuration
- Add activity indicators on save/rename/delete buttons
- Display friendly names in CameraManagementView profile selector
- Fix duplicate colored dots in management profile dropdown
- Fix i18n namespace for overridden base config tooltips
- Move profile override deletion from dropdown trash icon to footer
button with confirmation dialog, matching Reset to Global pattern
- Remove Add Profile from section header dropdown to prevent saving
camera overrides before top-level profile definition exists
- Clean up newProfiles state after API profile deletion
- Refresh profiles SWR cache after saving profile definitions
* remove profile badge in settings and add profiles to main menu
* use icon only on mobile
* change color order
* docs
* show activity indicator on trash icon while deleting a profile
* tweak language
* immediately create profiles on backend instead of deferring to Save All
* hide restart-required fields when editing a profile section
fields that require a restart cannot take effect via profile switching,
so they are merged into hiddenFields when profileName is set
* show active profile indicator in desktop status bar
* fix profile config inheritance bug where Pydantic defaults override base values
The /config API was dumping profile overrides with model_dump() which included
all Pydantic defaults. When the frontend merged these over
the camera's base config, explicitly-set base values were
lost. Now profile overrides are re-dumped with exclude_unset=True so only
user-specified fields are returned.
Also fixes the Save All path generating spurious deletion markers for
restart-required fields that are hidden during profile
editing but not excluded from the raw data sanitization in
prepareSectionSavePayload.
* docs tweaks
* docs tweak
* formatting
* formatting
* fix typing
* fix test pollution
test_maintainer was injecting MagicMock() into sys.modules["frigate.config.camera.updater"] at module load time and never restoring it. When the profile tests later imported CameraConfigUpdateEnum and CameraConfigUpdateTopic from that module, they got mock objects instead of the real dataclass/enum, so equality comparisons always failed
* remove
* fix settings showing profile-merged values when editing base config
When a profile is active, the in-memory config contains effective
(profile-merged) values. The settings UI was displaying these merged
values even when the "Base Config" view was selected.
Backend: snapshot pre-profile base configs in ProfileManager and expose
them via a `base_config` key in the /api/config camera response when a
profile is active. The top-level sections continue to reflect the
effective running config.
Frontend: read from `base_config` when available in BaseSection,
useConfigOverride, useAllCameraOverrides, and prepareSectionSavePayload.
Include formData labels in Object/Audio switches widgets so that labels
added only by a profile override remain visible when editing that profile.
* use rasterized_mask as field
makes it easier to exclude from the schema with exclude=True
prevents leaking of the field when using model_dump for profiles
* fix zones
- Fix zone colors not matching across profiles by falling back to base zone color when profile zone data lacks a color field
- Use base_config for base-layer values in masks/zones view so profile-merged values don't pollute the base config editing view
- Handle zones separately in profile manager snapshot/restore since ZoneConfig requires special serialization (color as private attr, contour generation)
- Inherit base zone color and generate contours for profile zone overrides in profile manager
* formatting
* don't require restart for camera enabled change for profiles
* publish camera state when changing profiles
* formatting
* remove available profiles from mqtt
* improve typing
2026-03-19 17:47:57 +03:00
|
|
|
import type { ProfileState } from "@/types/profile";
|
|
|
|
|
import { getProfileColor } from "@/utils/profileColors";
|
2026-05-21 17:12:53 +03:00
|
|
|
import { isReplayCamera } from "@/utils/cameraUtil";
|
Camera profile support (#22482)
* add CameraProfileConfig model for named config overrides
* add profiles field to CameraConfig
* add active_profile field to FrigateConfig
Runtime-only field excluded from YAML serialization, tracks which
profile is currently active.
* add ProfileManager for profile activation and persistence
Handles snapshotting base configs, applying profile overrides via
deep_merge + apply_section_update, publishing ZMQ updates, and
persisting active profile to /config/.active_profile.
* add profile API endpoints (GET /profiles, GET/PUT /profile)
* add MQTT and dispatcher integration for profiles
- Subscribe to frigate/profile/set MQTT topic
- Publish profile/state and profiles/available on connect
- Add _on_profile_command handler to dispatcher
- Broadcast active profile state on WebSocket connect
* wire ProfileManager into app startup and FastAPI
- Create ProfileManager after dispatcher init
- Restore persisted profile on startup
- Pass dispatcher and profile_manager to FastAPI app
* add tests for invalid profile values and keys
Tests that Pydantic rejects: invalid field values (fps: "not_a_number"),
unknown section keys (ffmpeg in profile), invalid nested values, and
invalid profiles in full config parsing.
* formatting
* fix CameraLiveConfig JSON serialization error on profile activation
refactor _publish_updates to only publish ZMQ updates for
sections that actually changed, not all sections on affected cameras.
* consolidate
* add enabled field to camera profiles for enabling/disabling cameras
* add zones support to camera profiles
* add frontend profile types, color utility, and config save support
* add profile state management and save preview support
* add profileName prop to BaseSection for profile-aware config editing
* add profile section dropdown and wire into camera settings pages
* add per-profile camera enable/disable to Camera Management view
* add profiles summary page with card-based layout and fix backend zone comparison bug
* add active profile badge to settings toolbar
* i18n
* add red dot for any pending changes including profiles
* profile support for mask and zone editor
* fix hidden field validation errors caused by lodash wildcard and schema gaps
lodash unset does not support wildcard (*) segments, so hidden fields like
filters.*.mask were never stripped from form data, leaving null raw_coordinates
that fail RJSF anyOf validation. Add unsetWithWildcard helper and also strip
hidden fields from the JSON schema itself as defense-in-depth.
* add face_recognition and lpr to profile-eligible sections
* move profile dropdown from section panes to settings header
* add profiles enable toggle and improve empty state
* formatting
* tweaks
* tweak colors and switch
* fix profile save diff, masksAndZones delete, and config sync
* ui tweaks
* ensure profile manager gets updated config
* rename profile settings to ui settings
* refactor profilesview and add dots/border colors when overridden
* implement an update_config method for profile manager
* fix mask deletion
* more unique colors
* add top-level profiles config section with friendly names
* implement profile friendly names and improve profile UI
- Add ProfileDefinitionConfig type and profiles field to FrigateConfig
- Use ProfilesApiResponse type with friendly_name support throughout
- Replace Record<string, unknown> with proper JsonObject/JsonValue types
- Add profile creation form matching zone pattern (Zod + NameAndIdFields)
- Add pencil icon for renaming profile friendly names in ProfilesView
- Move Profiles menu item to first under Camera Configuration
- Add activity indicators on save/rename/delete buttons
- Display friendly names in CameraManagementView profile selector
- Fix duplicate colored dots in management profile dropdown
- Fix i18n namespace for overridden base config tooltips
- Move profile override deletion from dropdown trash icon to footer
button with confirmation dialog, matching Reset to Global pattern
- Remove Add Profile from section header dropdown to prevent saving
camera overrides before top-level profile definition exists
- Clean up newProfiles state after API profile deletion
- Refresh profiles SWR cache after saving profile definitions
* remove profile badge in settings and add profiles to main menu
* use icon only on mobile
* change color order
* docs
* show activity indicator on trash icon while deleting a profile
* tweak language
* immediately create profiles on backend instead of deferring to Save All
* hide restart-required fields when editing a profile section
fields that require a restart cannot take effect via profile switching,
so they are merged into hiddenFields when profileName is set
* show active profile indicator in desktop status bar
* fix profile config inheritance bug where Pydantic defaults override base values
The /config API was dumping profile overrides with model_dump() which included
all Pydantic defaults. When the frontend merged these over
the camera's base config, explicitly-set base values were
lost. Now profile overrides are re-dumped with exclude_unset=True so only
user-specified fields are returned.
Also fixes the Save All path generating spurious deletion markers for
restart-required fields that are hidden during profile
editing but not excluded from the raw data sanitization in
prepareSectionSavePayload.
* docs tweaks
* docs tweak
* formatting
* formatting
* fix typing
* fix test pollution
test_maintainer was injecting MagicMock() into sys.modules["frigate.config.camera.updater"] at module load time and never restoring it. When the profile tests later imported CameraConfigUpdateEnum and CameraConfigUpdateTopic from that module, they got mock objects instead of the real dataclass/enum, so equality comparisons always failed
* remove
* fix settings showing profile-merged values when editing base config
When a profile is active, the in-memory config contains effective
(profile-merged) values. The settings UI was displaying these merged
values even when the "Base Config" view was selected.
Backend: snapshot pre-profile base configs in ProfileManager and expose
them via a `base_config` key in the /api/config camera response when a
profile is active. The top-level sections continue to reflect the
effective running config.
Frontend: read from `base_config` when available in BaseSection,
useConfigOverride, useAllCameraOverrides, and prepareSectionSavePayload.
Include formData labels in Object/Audio switches widgets so that labels
added only by a profile override remain visible when editing that profile.
* use rasterized_mask as field
makes it easier to exclude from the schema with exclude=True
prevents leaking of the field when using model_dump for profiles
* fix zones
- Fix zone colors not matching across profiles by falling back to base zone color when profile zone data lacks a color field
- Use base_config for base-layer values in masks/zones view so profile-merged values don't pollute the base config editing view
- Handle zones separately in profile manager snapshot/restore since ZoneConfig requires special serialization (color as private attr, contour generation)
- Inherit base zone color and generate contours for profile zone overrides in profile manager
* formatting
* don't require restart for camera enabled change for profiles
* publish camera state when changing profiles
* formatting
* remove available profiles from mqtt
* improve typing
2026-03-19 17:47:57 +03:00
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
2026-05-22 16:52:01 +03:00
|
|
|
import {
|
|
|
|
|
Form,
|
|
|
|
|
FormControl,
|
|
|
|
|
FormField,
|
|
|
|
|
FormItem,
|
|
|
|
|
FormLabel,
|
|
|
|
|
FormMessage,
|
|
|
|
|
} from "@/components/ui/form";
|
|
|
|
|
import { useForm } from "react-hook-form";
|
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
|
import { z } from "zod";
|
2025-10-13 19:52:08 +03:00
|
|
|
|
2026-05-19 06:52:40 +03:00
|
|
|
const REORDER_SAVED_INDICATOR_MS = 1500;
|
|
|
|
|
|
|
|
|
|
type ReorderSaveStatus = "idle" | "saving" | "saved";
|
|
|
|
|
|
2025-10-13 19:52:08 +03:00
|
|
|
type CameraManagementViewProps = {
|
Camera profile support (#22482)
* add CameraProfileConfig model for named config overrides
* add profiles field to CameraConfig
* add active_profile field to FrigateConfig
Runtime-only field excluded from YAML serialization, tracks which
profile is currently active.
* add ProfileManager for profile activation and persistence
Handles snapshotting base configs, applying profile overrides via
deep_merge + apply_section_update, publishing ZMQ updates, and
persisting active profile to /config/.active_profile.
* add profile API endpoints (GET /profiles, GET/PUT /profile)
* add MQTT and dispatcher integration for profiles
- Subscribe to frigate/profile/set MQTT topic
- Publish profile/state and profiles/available on connect
- Add _on_profile_command handler to dispatcher
- Broadcast active profile state on WebSocket connect
* wire ProfileManager into app startup and FastAPI
- Create ProfileManager after dispatcher init
- Restore persisted profile on startup
- Pass dispatcher and profile_manager to FastAPI app
* add tests for invalid profile values and keys
Tests that Pydantic rejects: invalid field values (fps: "not_a_number"),
unknown section keys (ffmpeg in profile), invalid nested values, and
invalid profiles in full config parsing.
* formatting
* fix CameraLiveConfig JSON serialization error on profile activation
refactor _publish_updates to only publish ZMQ updates for
sections that actually changed, not all sections on affected cameras.
* consolidate
* add enabled field to camera profiles for enabling/disabling cameras
* add zones support to camera profiles
* add frontend profile types, color utility, and config save support
* add profile state management and save preview support
* add profileName prop to BaseSection for profile-aware config editing
* add profile section dropdown and wire into camera settings pages
* add per-profile camera enable/disable to Camera Management view
* add profiles summary page with card-based layout and fix backend zone comparison bug
* add active profile badge to settings toolbar
* i18n
* add red dot for any pending changes including profiles
* profile support for mask and zone editor
* fix hidden field validation errors caused by lodash wildcard and schema gaps
lodash unset does not support wildcard (*) segments, so hidden fields like
filters.*.mask were never stripped from form data, leaving null raw_coordinates
that fail RJSF anyOf validation. Add unsetWithWildcard helper and also strip
hidden fields from the JSON schema itself as defense-in-depth.
* add face_recognition and lpr to profile-eligible sections
* move profile dropdown from section panes to settings header
* add profiles enable toggle and improve empty state
* formatting
* tweaks
* tweak colors and switch
* fix profile save diff, masksAndZones delete, and config sync
* ui tweaks
* ensure profile manager gets updated config
* rename profile settings to ui settings
* refactor profilesview and add dots/border colors when overridden
* implement an update_config method for profile manager
* fix mask deletion
* more unique colors
* add top-level profiles config section with friendly names
* implement profile friendly names and improve profile UI
- Add ProfileDefinitionConfig type and profiles field to FrigateConfig
- Use ProfilesApiResponse type with friendly_name support throughout
- Replace Record<string, unknown> with proper JsonObject/JsonValue types
- Add profile creation form matching zone pattern (Zod + NameAndIdFields)
- Add pencil icon for renaming profile friendly names in ProfilesView
- Move Profiles menu item to first under Camera Configuration
- Add activity indicators on save/rename/delete buttons
- Display friendly names in CameraManagementView profile selector
- Fix duplicate colored dots in management profile dropdown
- Fix i18n namespace for overridden base config tooltips
- Move profile override deletion from dropdown trash icon to footer
button with confirmation dialog, matching Reset to Global pattern
- Remove Add Profile from section header dropdown to prevent saving
camera overrides before top-level profile definition exists
- Clean up newProfiles state after API profile deletion
- Refresh profiles SWR cache after saving profile definitions
* remove profile badge in settings and add profiles to main menu
* use icon only on mobile
* change color order
* docs
* show activity indicator on trash icon while deleting a profile
* tweak language
* immediately create profiles on backend instead of deferring to Save All
* hide restart-required fields when editing a profile section
fields that require a restart cannot take effect via profile switching,
so they are merged into hiddenFields when profileName is set
* show active profile indicator in desktop status bar
* fix profile config inheritance bug where Pydantic defaults override base values
The /config API was dumping profile overrides with model_dump() which included
all Pydantic defaults. When the frontend merged these over
the camera's base config, explicitly-set base values were
lost. Now profile overrides are re-dumped with exclude_unset=True so only
user-specified fields are returned.
Also fixes the Save All path generating spurious deletion markers for
restart-required fields that are hidden during profile
editing but not excluded from the raw data sanitization in
prepareSectionSavePayload.
* docs tweaks
* docs tweak
* formatting
* formatting
* fix typing
* fix test pollution
test_maintainer was injecting MagicMock() into sys.modules["frigate.config.camera.updater"] at module load time and never restoring it. When the profile tests later imported CameraConfigUpdateEnum and CameraConfigUpdateTopic from that module, they got mock objects instead of the real dataclass/enum, so equality comparisons always failed
* remove
* fix settings showing profile-merged values when editing base config
When a profile is active, the in-memory config contains effective
(profile-merged) values. The settings UI was displaying these merged
values even when the "Base Config" view was selected.
Backend: snapshot pre-profile base configs in ProfileManager and expose
them via a `base_config` key in the /api/config camera response when a
profile is active. The top-level sections continue to reflect the
effective running config.
Frontend: read from `base_config` when available in BaseSection,
useConfigOverride, useAllCameraOverrides, and prepareSectionSavePayload.
Include formData labels in Object/Audio switches widgets so that labels
added only by a profile override remain visible when editing that profile.
* use rasterized_mask as field
makes it easier to exclude from the schema with exclude=True
prevents leaking of the field when using model_dump for profiles
* fix zones
- Fix zone colors not matching across profiles by falling back to base zone color when profile zone data lacks a color field
- Use base_config for base-layer values in masks/zones view so profile-merged values don't pollute the base config editing view
- Handle zones separately in profile manager snapshot/restore since ZoneConfig requires special serialization (color as private attr, contour generation)
- Inherit base zone color and generate contours for profile zone overrides in profile manager
* formatting
* don't require restart for camera enabled change for profiles
* publish camera state when changing profiles
* formatting
* remove available profiles from mqtt
* improve typing
2026-03-19 17:47:57 +03:00
|
|
|
profileState?: ProfileState;
|
2025-10-13 19:52:08 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function CameraManagementView({
|
Camera profile support (#22482)
* add CameraProfileConfig model for named config overrides
* add profiles field to CameraConfig
* add active_profile field to FrigateConfig
Runtime-only field excluded from YAML serialization, tracks which
profile is currently active.
* add ProfileManager for profile activation and persistence
Handles snapshotting base configs, applying profile overrides via
deep_merge + apply_section_update, publishing ZMQ updates, and
persisting active profile to /config/.active_profile.
* add profile API endpoints (GET /profiles, GET/PUT /profile)
* add MQTT and dispatcher integration for profiles
- Subscribe to frigate/profile/set MQTT topic
- Publish profile/state and profiles/available on connect
- Add _on_profile_command handler to dispatcher
- Broadcast active profile state on WebSocket connect
* wire ProfileManager into app startup and FastAPI
- Create ProfileManager after dispatcher init
- Restore persisted profile on startup
- Pass dispatcher and profile_manager to FastAPI app
* add tests for invalid profile values and keys
Tests that Pydantic rejects: invalid field values (fps: "not_a_number"),
unknown section keys (ffmpeg in profile), invalid nested values, and
invalid profiles in full config parsing.
* formatting
* fix CameraLiveConfig JSON serialization error on profile activation
refactor _publish_updates to only publish ZMQ updates for
sections that actually changed, not all sections on affected cameras.
* consolidate
* add enabled field to camera profiles for enabling/disabling cameras
* add zones support to camera profiles
* add frontend profile types, color utility, and config save support
* add profile state management and save preview support
* add profileName prop to BaseSection for profile-aware config editing
* add profile section dropdown and wire into camera settings pages
* add per-profile camera enable/disable to Camera Management view
* add profiles summary page with card-based layout and fix backend zone comparison bug
* add active profile badge to settings toolbar
* i18n
* add red dot for any pending changes including profiles
* profile support for mask and zone editor
* fix hidden field validation errors caused by lodash wildcard and schema gaps
lodash unset does not support wildcard (*) segments, so hidden fields like
filters.*.mask were never stripped from form data, leaving null raw_coordinates
that fail RJSF anyOf validation. Add unsetWithWildcard helper and also strip
hidden fields from the JSON schema itself as defense-in-depth.
* add face_recognition and lpr to profile-eligible sections
* move profile dropdown from section panes to settings header
* add profiles enable toggle and improve empty state
* formatting
* tweaks
* tweak colors and switch
* fix profile save diff, masksAndZones delete, and config sync
* ui tweaks
* ensure profile manager gets updated config
* rename profile settings to ui settings
* refactor profilesview and add dots/border colors when overridden
* implement an update_config method for profile manager
* fix mask deletion
* more unique colors
* add top-level profiles config section with friendly names
* implement profile friendly names and improve profile UI
- Add ProfileDefinitionConfig type and profiles field to FrigateConfig
- Use ProfilesApiResponse type with friendly_name support throughout
- Replace Record<string, unknown> with proper JsonObject/JsonValue types
- Add profile creation form matching zone pattern (Zod + NameAndIdFields)
- Add pencil icon for renaming profile friendly names in ProfilesView
- Move Profiles menu item to first under Camera Configuration
- Add activity indicators on save/rename/delete buttons
- Display friendly names in CameraManagementView profile selector
- Fix duplicate colored dots in management profile dropdown
- Fix i18n namespace for overridden base config tooltips
- Move profile override deletion from dropdown trash icon to footer
button with confirmation dialog, matching Reset to Global pattern
- Remove Add Profile from section header dropdown to prevent saving
camera overrides before top-level profile definition exists
- Clean up newProfiles state after API profile deletion
- Refresh profiles SWR cache after saving profile definitions
* remove profile badge in settings and add profiles to main menu
* use icon only on mobile
* change color order
* docs
* show activity indicator on trash icon while deleting a profile
* tweak language
* immediately create profiles on backend instead of deferring to Save All
* hide restart-required fields when editing a profile section
fields that require a restart cannot take effect via profile switching,
so they are merged into hiddenFields when profileName is set
* show active profile indicator in desktop status bar
* fix profile config inheritance bug where Pydantic defaults override base values
The /config API was dumping profile overrides with model_dump() which included
all Pydantic defaults. When the frontend merged these over
the camera's base config, explicitly-set base values were
lost. Now profile overrides are re-dumped with exclude_unset=True so only
user-specified fields are returned.
Also fixes the Save All path generating spurious deletion markers for
restart-required fields that are hidden during profile
editing but not excluded from the raw data sanitization in
prepareSectionSavePayload.
* docs tweaks
* docs tweak
* formatting
* formatting
* fix typing
* fix test pollution
test_maintainer was injecting MagicMock() into sys.modules["frigate.config.camera.updater"] at module load time and never restoring it. When the profile tests later imported CameraConfigUpdateEnum and CameraConfigUpdateTopic from that module, they got mock objects instead of the real dataclass/enum, so equality comparisons always failed
* remove
* fix settings showing profile-merged values when editing base config
When a profile is active, the in-memory config contains effective
(profile-merged) values. The settings UI was displaying these merged
values even when the "Base Config" view was selected.
Backend: snapshot pre-profile base configs in ProfileManager and expose
them via a `base_config` key in the /api/config camera response when a
profile is active. The top-level sections continue to reflect the
effective running config.
Frontend: read from `base_config` when available in BaseSection,
useConfigOverride, useAllCameraOverrides, and prepareSectionSavePayload.
Include formData labels in Object/Audio switches widgets so that labels
added only by a profile override remain visible when editing that profile.
* use rasterized_mask as field
makes it easier to exclude from the schema with exclude=True
prevents leaking of the field when using model_dump for profiles
* fix zones
- Fix zone colors not matching across profiles by falling back to base zone color when profile zone data lacks a color field
- Use base_config for base-layer values in masks/zones view so profile-merged values don't pollute the base config editing view
- Handle zones separately in profile manager snapshot/restore since ZoneConfig requires special serialization (color as private attr, contour generation)
- Inherit base zone color and generate contours for profile zone overrides in profile manager
* formatting
* don't require restart for camera enabled change for profiles
* publish camera state when changing profiles
* formatting
* remove available profiles from mqtt
* improve typing
2026-03-19 17:47:57 +03:00
|
|
|
profileState,
|
2025-10-13 19:52:08 +03:00
|
|
|
}: CameraManagementViewProps) {
|
2026-05-19 06:52:40 +03:00
|
|
|
const { t } = useTranslation(["views/settings", "common"]);
|
2025-10-13 19:52:08 +03:00
|
|
|
|
|
|
|
|
const { data: config, mutate: updateConfig } =
|
|
|
|
|
useSWR<FrigateConfig>("config");
|
|
|
|
|
|
|
|
|
|
const [showWizard, setShowWizard] = useState(false);
|
2026-03-09 01:23:48 +03:00
|
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
2025-10-13 19:52:08 +03:00
|
|
|
|
2026-02-27 18:55:36 +03:00
|
|
|
// State for restart dialog when enabling a disabled camera
|
|
|
|
|
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
|
|
|
|
const { send: sendRestart } = useRestart();
|
|
|
|
|
|
|
|
|
|
const enabledCameras = useMemo(() => {
|
|
|
|
|
if (config) {
|
|
|
|
|
return Object.keys(config.cameras)
|
2026-05-21 17:12:53 +03:00
|
|
|
.filter(
|
|
|
|
|
(camera) =>
|
|
|
|
|
config.cameras[camera].enabled_in_config && !isReplayCamera(camera),
|
|
|
|
|
)
|
2026-05-19 06:52:40 +03:00
|
|
|
.sort((a, b) => {
|
|
|
|
|
const orderA = config.cameras[a].ui?.order ?? 0;
|
|
|
|
|
const orderB = config.cameras[b].ui?.order ?? 0;
|
|
|
|
|
if (orderA !== orderB) return orderA - orderB;
|
|
|
|
|
return a.localeCompare(b);
|
|
|
|
|
});
|
2026-02-27 18:55:36 +03:00
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}, [config]);
|
|
|
|
|
|
2026-05-19 06:52:40 +03:00
|
|
|
// Diverges from config during a drag and while the save is in flight.
|
|
|
|
|
const [orderedCameras, setOrderedCameras] =
|
|
|
|
|
useState<string[]>(enabledCameras);
|
|
|
|
|
const orderedCamerasRef = useRef(orderedCameras);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
orderedCamerasRef.current = orderedCameras;
|
|
|
|
|
}, [orderedCameras]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setOrderedCameras((prev) => {
|
|
|
|
|
if (
|
|
|
|
|
prev.length === enabledCameras.length &&
|
|
|
|
|
prev.every((cam, i) => cam === enabledCameras[i])
|
|
|
|
|
) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
return enabledCameras;
|
|
|
|
|
});
|
|
|
|
|
}, [enabledCameras]);
|
|
|
|
|
|
|
|
|
|
const [reorderSaveStatus, setReorderSaveStatus] =
|
|
|
|
|
useState<ReorderSaveStatus>("idle");
|
|
|
|
|
const reorderSavedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (reorderSavedTimerRef.current) {
|
|
|
|
|
clearTimeout(reorderSavedTimerRef.current);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleReorderDragEnd = useCallback(async () => {
|
|
|
|
|
const current = orderedCamerasRef.current;
|
|
|
|
|
if (
|
|
|
|
|
current.length === enabledCameras.length &&
|
|
|
|
|
current.every((cam, i) => cam === enabledCameras[i])
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cameraUpdates: Record<string, { ui: { order: number } }> = {};
|
|
|
|
|
current.forEach((cam, i) => {
|
|
|
|
|
cameraUpdates[cam] = { ui: { order: i * 10 } };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (reorderSavedTimerRef.current) {
|
|
|
|
|
clearTimeout(reorderSavedTimerRef.current);
|
|
|
|
|
reorderSavedTimerRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
setReorderSaveStatus("saving");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await axios.put("config/set", {
|
|
|
|
|
requires_restart: 0,
|
|
|
|
|
config_data: { cameras: cameraUpdates },
|
|
|
|
|
});
|
|
|
|
|
await updateConfig();
|
|
|
|
|
setReorderSaveStatus("saved");
|
|
|
|
|
reorderSavedTimerRef.current = setTimeout(() => {
|
|
|
|
|
setReorderSaveStatus("idle");
|
|
|
|
|
reorderSavedTimerRef.current = null;
|
|
|
|
|
}, REORDER_SAVED_INDICATOR_MS);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setOrderedCameras(enabledCameras);
|
|
|
|
|
setReorderSaveStatus("idle");
|
|
|
|
|
const errorMessage =
|
|
|
|
|
axios.isAxiosError(error) &&
|
|
|
|
|
(error.response?.data?.message || error.response?.data?.detail)
|
|
|
|
|
? error.response?.data?.message || error.response?.data?.detail
|
|
|
|
|
: t("toast.save.error.noMessage", { ns: "common" });
|
|
|
|
|
|
|
|
|
|
toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [enabledCameras, updateConfig, t]);
|
|
|
|
|
|
2026-02-27 18:55:36 +03:00
|
|
|
const disabledCameras = useMemo(() => {
|
2025-10-13 19:52:08 +03:00
|
|
|
if (config) {
|
2026-02-27 18:55:36 +03:00
|
|
|
return Object.keys(config.cameras)
|
2026-05-21 17:12:53 +03:00
|
|
|
.filter(
|
|
|
|
|
(camera) =>
|
|
|
|
|
!config.cameras[camera].enabled_in_config &&
|
|
|
|
|
!isReplayCamera(camera),
|
|
|
|
|
)
|
2026-02-27 18:55:36 +03:00
|
|
|
.sort();
|
2025-10-13 19:52:08 +03:00
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}, [config]);
|
|
|
|
|
|
2026-05-06 19:01:50 +03:00
|
|
|
const allCameras = useMemo(() => {
|
|
|
|
|
if (config) {
|
2026-05-21 17:12:53 +03:00
|
|
|
return Object.keys(config.cameras)
|
|
|
|
|
.filter((camera) => !isReplayCamera(camera))
|
|
|
|
|
.sort();
|
2026-05-06 19:01:50 +03:00
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}, [config]);
|
|
|
|
|
|
2025-10-13 19:52:08 +03:00
|
|
|
useEffect(() => {
|
|
|
|
|
document.title = t("documentTitle.cameraManagement");
|
|
|
|
|
}, [t]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2025-10-23 16:34:52 +03:00
|
|
|
<Toaster
|
|
|
|
|
richColors
|
|
|
|
|
className="z-[1000]"
|
|
|
|
|
position="top-center"
|
|
|
|
|
closeButton
|
|
|
|
|
/>
|
2026-02-27 18:55:36 +03:00
|
|
|
<div className="flex size-full space-y-6">
|
|
|
|
|
<div className="scrollbar-container flex-1 overflow-y-auto pb-2">
|
2026-05-24 23:59:56 +03:00
|
|
|
<Heading as="h4" className="mb-2">
|
|
|
|
|
{t("cameraManagement.title")}
|
|
|
|
|
</Heading>
|
|
|
|
|
<p className="mb-6 max-w-5xl text-sm text-muted-foreground">
|
|
|
|
|
{t("cameraManagement.description")}
|
|
|
|
|
</p>
|
2025-10-13 19:52:08 +03:00
|
|
|
|
2026-05-24 23:59:56 +03:00
|
|
|
<div className="w-full max-w-5xl space-y-6">
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="select"
|
|
|
|
|
onClick={() => setShowWizard(true)}
|
|
|
|
|
className="mb-2 flex max-w-48 items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<LuPlus className="h-4 w-4" />
|
|
|
|
|
{t("cameraManagement.addCamera")}
|
|
|
|
|
</Button>
|
|
|
|
|
{enabledCameras.length + disabledCameras.length > 0 && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={() => setShowDeleteDialog(true)}
|
|
|
|
|
className="mb-2 flex max-w-48 items-center gap-2 text-white"
|
|
|
|
|
>
|
|
|
|
|
<LuTrash2 className="h-4 w-4" />
|
|
|
|
|
{t("cameraManagement.deleteCamera")}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{(enabledCameras.length > 0 || disabledCameras.length > 0) && (
|
|
|
|
|
<SettingsGroupCard
|
|
|
|
|
title={
|
|
|
|
|
<Trans ns="views/settings">
|
|
|
|
|
cameraManagement.streams.title
|
|
|
|
|
</Trans>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<div className={SPLIT_ROW_CLASS_NAME}>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label>{t("cameraManagement.streams.label")}</Label>
|
|
|
|
|
<p className="hidden text-sm text-muted-foreground md:block">
|
2026-02-27 18:55:36 +03:00
|
|
|
<Trans ns="views/settings">
|
2026-05-24 23:59:56 +03:00
|
|
|
cameraManagement.streams.description
|
2026-02-27 18:55:36 +03:00
|
|
|
</Trans>
|
2026-05-24 23:59:56 +03:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="max-w-md space-y-1.5">
|
|
|
|
|
<div className="space-y-3 rounded-lg bg-secondary p-4">
|
|
|
|
|
{orderedCameras.length > 0 && (
|
2026-05-19 06:52:40 +03:00
|
|
|
<Reorder.Group
|
|
|
|
|
as="div"
|
|
|
|
|
axis="y"
|
|
|
|
|
values={orderedCameras}
|
|
|
|
|
onReorder={setOrderedCameras}
|
2026-05-24 23:59:56 +03:00
|
|
|
className="space-y-2"
|
2026-05-19 06:52:40 +03:00
|
|
|
>
|
|
|
|
|
{orderedCameras.map((camera) => (
|
2026-05-24 23:59:56 +03:00
|
|
|
<ActiveCameraRow
|
2026-05-19 06:52:40 +03:00
|
|
|
key={camera}
|
|
|
|
|
camera={camera}
|
|
|
|
|
onConfigChanged={updateConfig}
|
|
|
|
|
onDragEnd={handleReorderDragEnd}
|
2026-05-24 23:59:56 +03:00
|
|
|
setRestartDialogOpen={setRestartDialogOpen}
|
2026-05-19 06:52:40 +03:00
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</Reorder.Group>
|
2026-05-24 23:59:56 +03:00
|
|
|
)}
|
|
|
|
|
{orderedCameras.length > 0 &&
|
|
|
|
|
disabledCameras.length > 0 && (
|
|
|
|
|
<div className="border-t border-border/40" />
|
|
|
|
|
)}
|
|
|
|
|
{disabledCameras.length > 0 && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
{t("cameraManagement.streams.disabledSubheading")}
|
2026-02-27 18:55:36 +03:00
|
|
|
</p>
|
2026-05-24 23:59:56 +03:00
|
|
|
{disabledCameras.map((camera) => (
|
|
|
|
|
<DisabledCameraRow
|
|
|
|
|
key={camera}
|
|
|
|
|
camera={camera}
|
|
|
|
|
onConfigChanged={updateConfig}
|
|
|
|
|
setRestartDialogOpen={setRestartDialogOpen}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
2026-02-27 18:55:36 +03:00
|
|
|
</div>
|
2026-05-24 23:59:56 +03:00
|
|
|
)}
|
2025-10-13 19:52:08 +03:00
|
|
|
</div>
|
2026-05-24 23:59:56 +03:00
|
|
|
<ReorderSaveStatusIndicator status={reorderSaveStatus} />
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm text-muted-foreground md:hidden">
|
|
|
|
|
<Trans ns="views/settings">
|
|
|
|
|
cameraManagement.streams.description
|
|
|
|
|
</Trans>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</SettingsGroupCard>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{profileState &&
|
|
|
|
|
profileState.allProfileNames.length > 0 &&
|
|
|
|
|
enabledCameras.length > 0 && (
|
|
|
|
|
<ProfileCameraEnableSection
|
|
|
|
|
profileState={profileState}
|
|
|
|
|
cameras={enabledCameras}
|
|
|
|
|
config={config}
|
|
|
|
|
onConfigChanged={updateConfig}
|
2025-10-13 19:52:08 +03:00
|
|
|
/>
|
2026-05-24 23:59:56 +03:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{config?.lpr?.enabled && allCameras.length > 0 && (
|
|
|
|
|
<CameraTypeSection
|
|
|
|
|
cameras={allCameras}
|
|
|
|
|
config={config}
|
|
|
|
|
onConfigChanged={updateConfig}
|
|
|
|
|
setRestartDialogOpen={setRestartDialogOpen}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-13 19:52:08 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<CameraWizardDialog
|
|
|
|
|
open={showWizard}
|
|
|
|
|
onClose={() => setShowWizard(false)}
|
|
|
|
|
/>
|
2026-03-09 01:23:48 +03:00
|
|
|
<DeleteCameraDialog
|
|
|
|
|
show={showDeleteDialog}
|
|
|
|
|
cameras={[...enabledCameras, ...disabledCameras]}
|
|
|
|
|
onClose={() => setShowDeleteDialog(false)}
|
|
|
|
|
onDeleted={() => {
|
|
|
|
|
setShowDeleteDialog(false);
|
|
|
|
|
updateConfig();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-02-27 18:55:36 +03:00
|
|
|
<RestartDialog
|
|
|
|
|
isOpen={restartDialogOpen}
|
|
|
|
|
onClose={() => setRestartDialogOpen(false)}
|
|
|
|
|
onRestart={() => sendRestart("restart")}
|
|
|
|
|
/>
|
2025-10-13 19:52:08 +03:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 06:52:40 +03:00
|
|
|
type ReorderSaveStatusIndicatorProps = {
|
|
|
|
|
status: ReorderSaveStatus;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function ReorderSaveStatusIndicator({
|
|
|
|
|
status,
|
|
|
|
|
}: ReorderSaveStatusIndicatorProps) {
|
|
|
|
|
const { t } = useTranslation(["views/settings"]);
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
aria-live="polite"
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex h-4 items-center justify-start gap-1 text-xs transition-opacity duration-200",
|
|
|
|
|
status === "idle" ? "opacity-0" : "opacity-100",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{status === "saving" && (
|
|
|
|
|
<span className="text-muted-foreground">
|
|
|
|
|
{t("cameraManagement.streams.saving")}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{status === "saved" && (
|
|
|
|
|
<span className="flex items-center gap-1 text-success">
|
|
|
|
|
<LuCheck className="size-3.5" />
|
|
|
|
|
{t("cameraManagement.streams.saved")}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 23:59:56 +03:00
|
|
|
type ActiveCameraRowProps = {
|
2026-05-19 06:52:40 +03:00
|
|
|
camera: string;
|
|
|
|
|
onConfigChanged: () => Promise<unknown>;
|
|
|
|
|
onDragEnd: () => void;
|
2026-05-24 23:59:56 +03:00
|
|
|
setRestartDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
2026-05-19 06:52:40 +03:00
|
|
|
};
|
|
|
|
|
|
2026-05-24 23:59:56 +03:00
|
|
|
function ActiveCameraRow({
|
2026-05-19 06:52:40 +03:00
|
|
|
camera,
|
|
|
|
|
onConfigChanged,
|
|
|
|
|
onDragEnd,
|
2026-05-24 23:59:56 +03:00
|
|
|
setRestartDialogOpen,
|
|
|
|
|
}: ActiveCameraRowProps) {
|
2026-05-19 06:52:40 +03:00
|
|
|
const { t } = useTranslation(["views/settings"]);
|
|
|
|
|
const controls = useDragControls();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Reorder.Item
|
|
|
|
|
as="div"
|
|
|
|
|
value={camera}
|
|
|
|
|
dragListener={false}
|
|
|
|
|
dragControls={controls}
|
|
|
|
|
onDragEnd={onDragEnd}
|
|
|
|
|
className="flex flex-row items-center justify-between"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onPointerDown={(e) => controls.start(e)}
|
|
|
|
|
className="-ml-1 cursor-grab touch-none rounded p-1 text-muted-foreground hover:text-primary active:cursor-grabbing"
|
|
|
|
|
aria-label={t("cameraManagement.streams.reorderHandle")}
|
|
|
|
|
>
|
|
|
|
|
<LuGripVertical className="size-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<CameraNameLabel camera={camera} />
|
2026-05-22 16:52:01 +03:00
|
|
|
<CameraDetailsEditor
|
2026-05-19 06:52:40 +03:00
|
|
|
cameraName={camera}
|
|
|
|
|
onConfigChanged={onConfigChanged}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-05-24 23:59:56 +03:00
|
|
|
<CameraStatusSelect
|
|
|
|
|
cameraName={camera}
|
|
|
|
|
isDisabledInConfig={false}
|
|
|
|
|
onConfigChanged={onConfigChanged}
|
|
|
|
|
setRestartDialogOpen={setRestartDialogOpen}
|
|
|
|
|
/>
|
2026-05-19 06:52:40 +03:00
|
|
|
</Reorder.Item>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 23:59:56 +03:00
|
|
|
type DisabledCameraRowProps = {
|
|
|
|
|
camera: string;
|
|
|
|
|
onConfigChanged: () => Promise<unknown>;
|
|
|
|
|
setRestartDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function DisabledCameraRow({
|
|
|
|
|
camera,
|
|
|
|
|
onConfigChanged,
|
|
|
|
|
setRestartDialogOpen,
|
|
|
|
|
}: DisabledCameraRowProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-row items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<CameraNameLabel camera={camera} className="text-muted-foreground" />
|
|
|
|
|
<CameraDetailsEditor
|
|
|
|
|
cameraName={camera}
|
|
|
|
|
onConfigChanged={onConfigChanged}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<CameraStatusSelect
|
|
|
|
|
cameraName={camera}
|
|
|
|
|
isDisabledInConfig={true}
|
|
|
|
|
onConfigChanged={onConfigChanged}
|
|
|
|
|
setRestartDialogOpen={setRestartDialogOpen}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CameraStatus = "on" | "off" | "disabled";
|
|
|
|
|
|
|
|
|
|
type CameraStatusSelectProps = {
|
2025-10-13 19:52:08 +03:00
|
|
|
cameraName: string;
|
2026-05-24 23:59:56 +03:00
|
|
|
isDisabledInConfig: boolean;
|
|
|
|
|
onConfigChanged: () => Promise<unknown>;
|
|
|
|
|
setRestartDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
2025-10-13 19:52:08 +03:00
|
|
|
};
|
|
|
|
|
|
2026-05-24 23:59:56 +03:00
|
|
|
function CameraStatusSelect({
|
|
|
|
|
cameraName,
|
|
|
|
|
isDisabledInConfig,
|
|
|
|
|
onConfigChanged,
|
|
|
|
|
setRestartDialogOpen,
|
|
|
|
|
}: CameraStatusSelectProps) {
|
|
|
|
|
const { t } = useTranslation([
|
|
|
|
|
"views/settings",
|
|
|
|
|
"components/dialog",
|
|
|
|
|
"common",
|
|
|
|
|
]);
|
2025-10-13 19:52:08 +03:00
|
|
|
const { payload: enabledState, send: sendEnabled } =
|
|
|
|
|
useEnabledState(cameraName);
|
2026-05-24 23:59:56 +03:00
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
const currentStatus: CameraStatus = isDisabledInConfig
|
|
|
|
|
? "disabled"
|
|
|
|
|
: enabledState === "OFF"
|
|
|
|
|
? "off"
|
|
|
|
|
: "on";
|
|
|
|
|
|
|
|
|
|
const restartLabel = t("configForm.restartRequiredField", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
defaultValue: "Restart required",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleChange = useCallback(
|
|
|
|
|
async (newStatus: string) => {
|
|
|
|
|
if (newStatus === currentStatus || isSaving) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newStatus === "on" && !isDisabledInConfig) {
|
|
|
|
|
sendEnabled("ON");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newStatus === "off" && !isDisabledInConfig) {
|
|
|
|
|
sendEnabled("OFF");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newStatus === "on" && isDisabledInConfig) {
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
await axios.put("config/set", {
|
|
|
|
|
requires_restart: 1,
|
|
|
|
|
config_data: {
|
|
|
|
|
cameras: { [cameraName]: { enabled: true } },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
await onConfigChanged();
|
|
|
|
|
toast.success(
|
|
|
|
|
t("cameraManagement.streams.enableSuccess", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
cameraName,
|
|
|
|
|
}),
|
|
|
|
|
{
|
|
|
|
|
position: "top-center",
|
|
|
|
|
action: (
|
|
|
|
|
<a onClick={() => setRestartDialogOpen(true)}>
|
|
|
|
|
<Button>
|
|
|
|
|
{t("restart.button", { ns: "components/dialog" })}
|
|
|
|
|
</Button>
|
|
|
|
|
</a>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
axios.isAxiosError(error) &&
|
|
|
|
|
(error.response?.data?.message || error.response?.data?.detail)
|
|
|
|
|
? error.response?.data?.message || error.response?.data?.detail
|
|
|
|
|
: t("toast.save.error.noMessage", { ns: "common" });
|
|
|
|
|
toast.error(
|
|
|
|
|
t("toast.save.error.title", { errorMessage, ns: "common" }),
|
|
|
|
|
{ position: "top-center" },
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newStatus === "disabled" && !isDisabledInConfig) {
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
// Stop runtime processing immediately before persisting the
|
|
|
|
|
// disable so the camera stops working without waiting for
|
|
|
|
|
// a restart. The config write below makes the change durable.
|
|
|
|
|
sendEnabled("OFF");
|
|
|
|
|
await axios.put("config/set", {
|
|
|
|
|
requires_restart: 0,
|
|
|
|
|
config_data: {
|
|
|
|
|
cameras: { [cameraName]: { enabled: false } },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
await onConfigChanged();
|
|
|
|
|
toast.success(
|
|
|
|
|
t("cameraManagement.streams.disableSuccess", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
cameraName,
|
|
|
|
|
}),
|
|
|
|
|
{ position: "top-center" },
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
axios.isAxiosError(error) &&
|
|
|
|
|
(error.response?.data?.message || error.response?.data?.detail)
|
|
|
|
|
? error.response?.data?.message || error.response?.data?.detail
|
|
|
|
|
: t("toast.save.error.noMessage", { ns: "common" });
|
|
|
|
|
toast.error(
|
|
|
|
|
t("toast.save.error.title", { errorMessage, ns: "common" }),
|
|
|
|
|
{ position: "top-center" },
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[
|
|
|
|
|
cameraName,
|
|
|
|
|
currentStatus,
|
|
|
|
|
isDisabledInConfig,
|
|
|
|
|
isSaving,
|
|
|
|
|
onConfigChanged,
|
|
|
|
|
sendEnabled,
|
|
|
|
|
setRestartDialogOpen,
|
|
|
|
|
t,
|
|
|
|
|
],
|
|
|
|
|
);
|
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-24 23:59:56 +03:00
|
|
|
if (isSaving) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-7 w-[110px] flex-row items-center justify-end">
|
|
|
|
|
<ActivityIndicator className="size-4" size={16} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-13 19:52:08 +03:00
|
|
|
|
|
|
|
|
return (
|
2026-05-24 23:59:56 +03:00
|
|
|
<Select value={currentStatus} onValueChange={handleChange}>
|
|
|
|
|
<SelectTrigger className="h-7 w-[110px] text-xs">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="on">
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
{t("cameraManagement.streams.status.on")}
|
|
|
|
|
{isDisabledInConfig && (
|
|
|
|
|
<LuRefreshCcw
|
|
|
|
|
className="size-3 text-muted-foreground"
|
|
|
|
|
aria-label={restartLabel}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</SelectItem>
|
|
|
|
|
{!isDisabledInConfig && (
|
|
|
|
|
<SelectItem value="off">
|
|
|
|
|
{t("cameraManagement.streams.status.off")}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
)}
|
|
|
|
|
<SelectItem value="disabled">
|
|
|
|
|
{t("cameraManagement.streams.status.disabled")}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-10-13 19:52:08 +03:00
|
|
|
);
|
|
|
|
|
}
|
2026-02-27 18:55:36 +03:00
|
|
|
|
2026-05-22 16:52:01 +03:00
|
|
|
type CameraDetailsEditorProps = {
|
2026-04-27 01:09:35 +03:00
|
|
|
cameraName: string;
|
|
|
|
|
onConfigChanged: () => Promise<unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-22 16:52:01 +03:00
|
|
|
type CameraDetailsFormValues = {
|
|
|
|
|
friendlyName: string;
|
|
|
|
|
webuiUrl: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function CameraDetailsEditor({
|
2026-04-27 01:09:35 +03:00
|
|
|
cameraName,
|
|
|
|
|
onConfigChanged,
|
2026-05-22 16:52:01 +03:00
|
|
|
}: CameraDetailsEditorProps) {
|
2026-04-27 01:09:35 +03:00
|
|
|
const { t } = useTranslation(["views/settings", "common"]);
|
|
|
|
|
const { data: config } = useSWR<FrigateConfig>("config");
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
const currentFriendlyName = config?.cameras?.[cameraName]?.friendly_name;
|
2026-05-22 16:52:01 +03:00
|
|
|
const currentWebuiUrl = config?.cameras?.[cameraName]?.webui_url;
|
|
|
|
|
|
|
|
|
|
const formSchema = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
z.object({
|
|
|
|
|
friendlyName: z.string(),
|
|
|
|
|
webuiUrl: z.string().refine(
|
|
|
|
|
(val) => {
|
|
|
|
|
const trimmed = val.trim();
|
|
|
|
|
if (!trimmed) return true;
|
|
|
|
|
try {
|
|
|
|
|
new URL(trimmed);
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
message: t("cameraManagement.streams.details.webuiUrlInvalid", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
}),
|
|
|
|
|
[t],
|
|
|
|
|
);
|
2026-04-27 01:09:35 +03:00
|
|
|
|
2026-05-22 16:52:01 +03:00
|
|
|
const form = useForm<CameraDetailsFormValues>({
|
|
|
|
|
resolver: zodResolver(formSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
friendlyName: currentFriendlyName ?? "",
|
|
|
|
|
webuiUrl: currentWebuiUrl ?? "",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Reset form values from config whenever the dialog is opened.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) {
|
|
|
|
|
form.reset({
|
|
|
|
|
friendlyName: currentFriendlyName ?? "",
|
|
|
|
|
webuiUrl: currentWebuiUrl ?? "",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [open, currentFriendlyName, currentWebuiUrl, form]);
|
|
|
|
|
|
|
|
|
|
const onSubmit = useCallback(
|
|
|
|
|
async (values: CameraDetailsFormValues) => {
|
2026-04-27 01:09:35 +03:00
|
|
|
if (isSaving) return;
|
2026-05-22 16:52:01 +03:00
|
|
|
|
|
|
|
|
// only send fields the user actually changed
|
|
|
|
|
const newFriendly = values.friendlyName.trim() || null;
|
|
|
|
|
const newWebui = values.webuiUrl.trim() || null;
|
|
|
|
|
const cameraUpdate: Record<string, string | null> = {};
|
|
|
|
|
if (newFriendly !== (currentFriendlyName ?? null)) {
|
|
|
|
|
cameraUpdate.friendly_name = newFriendly;
|
|
|
|
|
}
|
|
|
|
|
if (newWebui !== (currentWebuiUrl ?? null)) {
|
|
|
|
|
cameraUpdate.webui_url = newWebui;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Object.keys(cameraUpdate).length === 0) {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 01:09:35 +03:00
|
|
|
setIsSaving(true);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await axios.put("config/set", {
|
|
|
|
|
requires_restart: 0,
|
|
|
|
|
config_data: {
|
|
|
|
|
cameras: {
|
2026-05-22 16:52:01 +03:00
|
|
|
[cameraName]: cameraUpdate,
|
2026-04-27 01:09:35 +03:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await onConfigChanged();
|
|
|
|
|
setOpen(false);
|
|
|
|
|
|
|
|
|
|
toast.success(t("toast.save.success", { ns: "common" }), {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
axios.isAxiosError(error) &&
|
|
|
|
|
(error.response?.data?.message || error.response?.data?.detail)
|
|
|
|
|
? error.response?.data?.message || error.response?.data?.detail
|
|
|
|
|
: t("toast.save.error.noMessage", { ns: "common" });
|
|
|
|
|
|
|
|
|
|
toast.error(
|
|
|
|
|
t("toast.save.error.title", { errorMessage, ns: "common" }),
|
|
|
|
|
{ position: "top-center" },
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-05-22 16:52:01 +03:00
|
|
|
[
|
|
|
|
|
cameraName,
|
|
|
|
|
currentFriendlyName,
|
|
|
|
|
currentWebuiUrl,
|
|
|
|
|
isSaving,
|
|
|
|
|
onConfigChanged,
|
|
|
|
|
t,
|
|
|
|
|
],
|
2026-04-27 01:09:35 +03:00
|
|
|
);
|
|
|
|
|
|
2026-05-22 16:52:01 +03:00
|
|
|
const editLabel = t("cameraManagement.streams.details.edit", {
|
2026-04-27 01:09:35 +03:00
|
|
|
ns: "views/settings",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="size-7"
|
2026-05-22 16:52:01 +03:00
|
|
|
aria-label={editLabel}
|
2026-04-27 01:09:35 +03:00
|
|
|
onClick={() => setOpen(true)}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
>
|
|
|
|
|
<LuPencil className="size-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
2026-05-22 16:52:01 +03:00
|
|
|
<TooltipContent>{editLabel}</TooltipContent>
|
2026-04-27 01:09:35 +03:00
|
|
|
</Tooltip>
|
2026-05-22 16:52:01 +03:00
|
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>
|
|
|
|
|
{t("cameraManagement.streams.details.title", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
{t("cameraManagement.streams.details.description", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<Form {...form}>
|
|
|
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="friendlyName"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>
|
|
|
|
|
{t("cameraManagement.streams.details.friendlyNameLabel", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Input
|
|
|
|
|
{...field}
|
|
|
|
|
placeholder={cameraName}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{t("cameraManagement.streams.details.friendlyNameHelp", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="webuiUrl"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>
|
|
|
|
|
{t("cameraManagement.streams.details.webuiUrlLabel", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Input
|
|
|
|
|
{...field}
|
|
|
|
|
placeholder="https://"
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
/>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{t("cameraManagement.streams.details.webuiUrlHelp", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<DialogFooter className="pt-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
onClick={() => setOpen(false)}
|
|
|
|
|
>
|
|
|
|
|
{t("button.cancel", { ns: "common" })}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="select" type="submit" disabled={isSaving}>
|
|
|
|
|
{isSaving ? (
|
|
|
|
|
<div className="flex flex-row items-center gap-2">
|
|
|
|
|
<ActivityIndicator className="size-4" />
|
|
|
|
|
<span>{t("button.saving", { ns: "common" })}</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
t("button.save", { ns: "common" })
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</form>
|
|
|
|
|
</Form>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2026-04-27 01:09:35 +03:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 19:01:50 +03:00
|
|
|
type CameraTypeSectionProps = {
|
|
|
|
|
cameras: string[];
|
|
|
|
|
config: FrigateConfig | undefined;
|
|
|
|
|
onConfigChanged: () => Promise<unknown>;
|
|
|
|
|
setRestartDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function CameraTypeSection({
|
|
|
|
|
cameras,
|
|
|
|
|
config,
|
|
|
|
|
onConfigChanged,
|
|
|
|
|
setRestartDialogOpen,
|
|
|
|
|
}: CameraTypeSectionProps) {
|
|
|
|
|
const { t } = useTranslation([
|
|
|
|
|
"views/settings",
|
|
|
|
|
"common",
|
|
|
|
|
"components/dialog",
|
|
|
|
|
]);
|
|
|
|
|
const { getLocaleDocUrl } = useDocDomain();
|
|
|
|
|
const [savingCamera, setSavingCamera] = useState<string | null>(null);
|
|
|
|
|
// Optimistic local state: the parsed config API doesn't reflect type
|
|
|
|
|
// changes until Frigate restarts, so we track saved values locally.
|
|
|
|
|
const [localOverrides, setLocalOverrides] = useState<Record<string, string>>(
|
|
|
|
|
{},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleTypeChange = useCallback(
|
|
|
|
|
async (camera: string, value: string) => {
|
|
|
|
|
setSavingCamera(camera);
|
|
|
|
|
try {
|
|
|
|
|
const typeValue = value === "lpr" ? "lpr" : null;
|
|
|
|
|
await axios.put("config/set", {
|
|
|
|
|
requires_restart: 1,
|
|
|
|
|
config_data: {
|
|
|
|
|
cameras: {
|
|
|
|
|
[camera]: {
|
|
|
|
|
type: typeValue,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
await onConfigChanged();
|
|
|
|
|
|
|
|
|
|
setLocalOverrides((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[camera]: value,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
toast.success(
|
|
|
|
|
t("cameraManagement.cameraType.saveSuccess", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
cameraName: camera,
|
|
|
|
|
}),
|
|
|
|
|
{
|
|
|
|
|
position: "top-center",
|
|
|
|
|
action: (
|
|
|
|
|
<a onClick={() => setRestartDialogOpen(true)}>
|
|
|
|
|
<Button>
|
|
|
|
|
{t("restart.button", { ns: "components/dialog" })}
|
|
|
|
|
</Button>
|
|
|
|
|
</a>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
axios.isAxiosError(error) &&
|
|
|
|
|
(error.response?.data?.message || error.response?.data?.detail)
|
|
|
|
|
? error.response?.data?.message || error.response?.data?.detail
|
|
|
|
|
: t("toast.save.error.noMessage", { ns: "common" });
|
|
|
|
|
|
|
|
|
|
toast.error(
|
|
|
|
|
t("toast.save.error.title", { errorMessage, ns: "common" }),
|
|
|
|
|
{ position: "top-center" },
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
setSavingCamera(null);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[onConfigChanged, setRestartDialogOpen, t],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getCameraType = useCallback(
|
|
|
|
|
(camera: string): string => {
|
|
|
|
|
const localValue = localOverrides[camera];
|
|
|
|
|
if (localValue) return localValue;
|
|
|
|
|
|
|
|
|
|
const type = config?.cameras?.[camera]?.type;
|
|
|
|
|
return type === "lpr" ? "lpr" : "normal";
|
|
|
|
|
},
|
|
|
|
|
[config, localOverrides],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SettingsGroupCard
|
|
|
|
|
title={t("cameraManagement.cameraType.title", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<div className={SPLIT_ROW_CLASS_NAME}>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label>
|
|
|
|
|
{t("cameraManagement.cameraType.label", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
<RestartRequiredIndicator className="ml-1" />
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="hidden text-sm text-muted-foreground md:block">
|
|
|
|
|
{t("cameraManagement.cameraType.description", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="hidden items-center text-sm text-primary md:flex">
|
|
|
|
|
<Link
|
|
|
|
|
to={getLocaleDocUrl(
|
|
|
|
|
"configuration/license_plate_recognition#dedicated-lpr-cameras",
|
|
|
|
|
)}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="inline"
|
|
|
|
|
>
|
|
|
|
|
{t("readTheDocumentation", { ns: "common" })}
|
|
|
|
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`${CONTROL_COLUMN_CLASS_NAME} space-y-1.5`}>
|
|
|
|
|
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
|
|
|
|
|
{cameras.map((camera) => {
|
|
|
|
|
const currentType = getCameraType(camera);
|
|
|
|
|
const isSaving = savingCamera === camera;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={camera}
|
|
|
|
|
className="flex flex-row items-center justify-between"
|
|
|
|
|
>
|
|
|
|
|
<CameraNameLabel camera={camera} />
|
|
|
|
|
{isSaving ? (
|
|
|
|
|
<ActivityIndicator className="h-5 w-20" size={16} />
|
|
|
|
|
) : (
|
|
|
|
|
<Select
|
|
|
|
|
value={currentType}
|
|
|
|
|
onValueChange={(v) => handleTypeChange(camera, v)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-7 w-full max-w-[140px] text-xs">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="normal">
|
|
|
|
|
{t("cameraManagement.cameraType.normal", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="lpr">
|
|
|
|
|
{t("cameraManagement.cameraType.dedicatedLpr", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm text-muted-foreground md:hidden">
|
|
|
|
|
{t("cameraManagement.cameraType.description", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex items-center text-sm text-primary md:hidden">
|
|
|
|
|
<Link
|
|
|
|
|
to={getLocaleDocUrl(
|
|
|
|
|
"configuration/license_plate_recognition#dedicated-lpr-cameras",
|
|
|
|
|
)}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="inline"
|
|
|
|
|
>
|
|
|
|
|
{t("readTheDocumentation", { ns: "common" })}
|
|
|
|
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</SettingsGroupCard>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
Camera profile support (#22482)
* add CameraProfileConfig model for named config overrides
* add profiles field to CameraConfig
* add active_profile field to FrigateConfig
Runtime-only field excluded from YAML serialization, tracks which
profile is currently active.
* add ProfileManager for profile activation and persistence
Handles snapshotting base configs, applying profile overrides via
deep_merge + apply_section_update, publishing ZMQ updates, and
persisting active profile to /config/.active_profile.
* add profile API endpoints (GET /profiles, GET/PUT /profile)
* add MQTT and dispatcher integration for profiles
- Subscribe to frigate/profile/set MQTT topic
- Publish profile/state and profiles/available on connect
- Add _on_profile_command handler to dispatcher
- Broadcast active profile state on WebSocket connect
* wire ProfileManager into app startup and FastAPI
- Create ProfileManager after dispatcher init
- Restore persisted profile on startup
- Pass dispatcher and profile_manager to FastAPI app
* add tests for invalid profile values and keys
Tests that Pydantic rejects: invalid field values (fps: "not_a_number"),
unknown section keys (ffmpeg in profile), invalid nested values, and
invalid profiles in full config parsing.
* formatting
* fix CameraLiveConfig JSON serialization error on profile activation
refactor _publish_updates to only publish ZMQ updates for
sections that actually changed, not all sections on affected cameras.
* consolidate
* add enabled field to camera profiles for enabling/disabling cameras
* add zones support to camera profiles
* add frontend profile types, color utility, and config save support
* add profile state management and save preview support
* add profileName prop to BaseSection for profile-aware config editing
* add profile section dropdown and wire into camera settings pages
* add per-profile camera enable/disable to Camera Management view
* add profiles summary page with card-based layout and fix backend zone comparison bug
* add active profile badge to settings toolbar
* i18n
* add red dot for any pending changes including profiles
* profile support for mask and zone editor
* fix hidden field validation errors caused by lodash wildcard and schema gaps
lodash unset does not support wildcard (*) segments, so hidden fields like
filters.*.mask were never stripped from form data, leaving null raw_coordinates
that fail RJSF anyOf validation. Add unsetWithWildcard helper and also strip
hidden fields from the JSON schema itself as defense-in-depth.
* add face_recognition and lpr to profile-eligible sections
* move profile dropdown from section panes to settings header
* add profiles enable toggle and improve empty state
* formatting
* tweaks
* tweak colors and switch
* fix profile save diff, masksAndZones delete, and config sync
* ui tweaks
* ensure profile manager gets updated config
* rename profile settings to ui settings
* refactor profilesview and add dots/border colors when overridden
* implement an update_config method for profile manager
* fix mask deletion
* more unique colors
* add top-level profiles config section with friendly names
* implement profile friendly names and improve profile UI
- Add ProfileDefinitionConfig type and profiles field to FrigateConfig
- Use ProfilesApiResponse type with friendly_name support throughout
- Replace Record<string, unknown> with proper JsonObject/JsonValue types
- Add profile creation form matching zone pattern (Zod + NameAndIdFields)
- Add pencil icon for renaming profile friendly names in ProfilesView
- Move Profiles menu item to first under Camera Configuration
- Add activity indicators on save/rename/delete buttons
- Display friendly names in CameraManagementView profile selector
- Fix duplicate colored dots in management profile dropdown
- Fix i18n namespace for overridden base config tooltips
- Move profile override deletion from dropdown trash icon to footer
button with confirmation dialog, matching Reset to Global pattern
- Remove Add Profile from section header dropdown to prevent saving
camera overrides before top-level profile definition exists
- Clean up newProfiles state after API profile deletion
- Refresh profiles SWR cache after saving profile definitions
* remove profile badge in settings and add profiles to main menu
* use icon only on mobile
* change color order
* docs
* show activity indicator on trash icon while deleting a profile
* tweak language
* immediately create profiles on backend instead of deferring to Save All
* hide restart-required fields when editing a profile section
fields that require a restart cannot take effect via profile switching,
so they are merged into hiddenFields when profileName is set
* show active profile indicator in desktop status bar
* fix profile config inheritance bug where Pydantic defaults override base values
The /config API was dumping profile overrides with model_dump() which included
all Pydantic defaults. When the frontend merged these over
the camera's base config, explicitly-set base values were
lost. Now profile overrides are re-dumped with exclude_unset=True so only
user-specified fields are returned.
Also fixes the Save All path generating spurious deletion markers for
restart-required fields that are hidden during profile
editing but not excluded from the raw data sanitization in
prepareSectionSavePayload.
* docs tweaks
* docs tweak
* formatting
* formatting
* fix typing
* fix test pollution
test_maintainer was injecting MagicMock() into sys.modules["frigate.config.camera.updater"] at module load time and never restoring it. When the profile tests later imported CameraConfigUpdateEnum and CameraConfigUpdateTopic from that module, they got mock objects instead of the real dataclass/enum, so equality comparisons always failed
* remove
* fix settings showing profile-merged values when editing base config
When a profile is active, the in-memory config contains effective
(profile-merged) values. The settings UI was displaying these merged
values even when the "Base Config" view was selected.
Backend: snapshot pre-profile base configs in ProfileManager and expose
them via a `base_config` key in the /api/config camera response when a
profile is active. The top-level sections continue to reflect the
effective running config.
Frontend: read from `base_config` when available in BaseSection,
useConfigOverride, useAllCameraOverrides, and prepareSectionSavePayload.
Include formData labels in Object/Audio switches widgets so that labels
added only by a profile override remain visible when editing that profile.
* use rasterized_mask as field
makes it easier to exclude from the schema with exclude=True
prevents leaking of the field when using model_dump for profiles
* fix zones
- Fix zone colors not matching across profiles by falling back to base zone color when profile zone data lacks a color field
- Use base_config for base-layer values in masks/zones view so profile-merged values don't pollute the base config editing view
- Handle zones separately in profile manager snapshot/restore since ZoneConfig requires special serialization (color as private attr, contour generation)
- Inherit base zone color and generate contours for profile zone overrides in profile manager
* formatting
* don't require restart for camera enabled change for profiles
* publish camera state when changing profiles
* formatting
* remove available profiles from mqtt
* improve typing
2026-03-19 17:47:57 +03:00
|
|
|
type ProfileCameraEnableSectionProps = {
|
|
|
|
|
profileState: ProfileState;
|
|
|
|
|
cameras: string[];
|
|
|
|
|
config: FrigateConfig | undefined;
|
|
|
|
|
onConfigChanged: () => Promise<unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function ProfileCameraEnableSection({
|
|
|
|
|
profileState,
|
|
|
|
|
cameras,
|
|
|
|
|
config,
|
|
|
|
|
onConfigChanged,
|
|
|
|
|
}: ProfileCameraEnableSectionProps) {
|
|
|
|
|
const { t } = useTranslation(["views/settings", "common"]);
|
|
|
|
|
const [selectedProfile, setSelectedProfile] = useState<string>(
|
|
|
|
|
profileState.allProfileNames[0] ?? "",
|
|
|
|
|
);
|
|
|
|
|
const [savingCamera, setSavingCamera] = useState<string | null>(null);
|
|
|
|
|
// Optimistic local state: the parsed config API doesn't reflect profile
|
|
|
|
|
// enabled changes until Frigate restarts, so we track saved values locally.
|
|
|
|
|
const [localOverrides, setLocalOverrides] = useState<
|
|
|
|
|
Record<string, Record<string, string>>
|
|
|
|
|
>({});
|
|
|
|
|
|
|
|
|
|
const handleEnabledChange = useCallback(
|
|
|
|
|
async (camera: string, value: string) => {
|
|
|
|
|
setSavingCamera(camera);
|
|
|
|
|
try {
|
|
|
|
|
const enabledValue =
|
|
|
|
|
value === "enabled" ? true : value === "disabled" ? false : null;
|
|
|
|
|
const configData =
|
|
|
|
|
enabledValue === null
|
|
|
|
|
? {
|
|
|
|
|
cameras: {
|
|
|
|
|
[camera]: {
|
|
|
|
|
profiles: { [selectedProfile]: { enabled: "" } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
cameras: {
|
|
|
|
|
[camera]: {
|
|
|
|
|
profiles: { [selectedProfile]: { enabled: enabledValue } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await axios.put("config/set", {
|
|
|
|
|
requires_restart: 0,
|
|
|
|
|
config_data: configData,
|
|
|
|
|
});
|
|
|
|
|
await onConfigChanged();
|
|
|
|
|
|
|
|
|
|
setLocalOverrides((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[selectedProfile]: {
|
|
|
|
|
...prev[selectedProfile],
|
|
|
|
|
[camera]: value,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
toast.success(t("toast.save.success", { ns: "common" }), {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
toast.error(t("toast.save.error.title", { ns: "common" }), {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setSavingCamera(null);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[selectedProfile, onConfigChanged, t],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getEnabledState = useCallback(
|
|
|
|
|
(camera: string): string => {
|
|
|
|
|
// Check optimistic local state first
|
|
|
|
|
const localValue = localOverrides[selectedProfile]?.[camera];
|
|
|
|
|
if (localValue) return localValue;
|
|
|
|
|
|
|
|
|
|
const profileData =
|
|
|
|
|
config?.cameras?.[camera]?.profiles?.[selectedProfile];
|
|
|
|
|
if (!profileData || profileData.enabled === undefined) return "inherit";
|
|
|
|
|
return profileData.enabled ? "enabled" : "disabled";
|
|
|
|
|
},
|
|
|
|
|
[config, selectedProfile, localOverrides],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!selectedProfile) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SettingsGroupCard
|
|
|
|
|
title={t("cameraManagement.profiles.title", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<div className={SPLIT_ROW_CLASS_NAME}>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label>
|
|
|
|
|
{t("cameraManagement.profiles.selectLabel", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{t("cameraManagement.profiles.description", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`${CONTROL_COLUMN_CLASS_NAME} space-y-4`}>
|
|
|
|
|
<Select value={selectedProfile} onValueChange={setSelectedProfile}>
|
|
|
|
|
<SelectTrigger className="w-full max-w-[200px]">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{profileState.allProfileNames.map((profile) => {
|
|
|
|
|
const color = getProfileColor(
|
|
|
|
|
profile,
|
|
|
|
|
profileState.allProfileNames,
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<SelectItem key={profile} value={profile}>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
"h-2 w-2 shrink-0 rounded-full",
|
|
|
|
|
color.dot,
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
{profileState.profileFriendlyNames.get(profile) ??
|
|
|
|
|
profile}
|
|
|
|
|
</div>
|
|
|
|
|
</SelectItem>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
|
|
|
|
|
{cameras.map((camera) => {
|
|
|
|
|
const state = getEnabledState(camera);
|
|
|
|
|
const isSaving = savingCamera === camera;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={camera}
|
|
|
|
|
className="flex flex-row items-center justify-between"
|
|
|
|
|
>
|
|
|
|
|
<CameraNameLabel camera={camera} />
|
|
|
|
|
{isSaving ? (
|
|
|
|
|
<ActivityIndicator className="h-5 w-20" size={16} />
|
|
|
|
|
) : (
|
|
|
|
|
<Select
|
|
|
|
|
value={state}
|
|
|
|
|
onValueChange={(v) => handleEnabledChange(camera, v)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-7 w-[120px] text-xs">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="inherit">
|
|
|
|
|
{t("cameraManagement.profiles.inherit", {
|
|
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="enabled">
|
2026-05-24 23:59:56 +03:00
|
|
|
{t("cameraManagement.profiles.on", {
|
Camera profile support (#22482)
* add CameraProfileConfig model for named config overrides
* add profiles field to CameraConfig
* add active_profile field to FrigateConfig
Runtime-only field excluded from YAML serialization, tracks which
profile is currently active.
* add ProfileManager for profile activation and persistence
Handles snapshotting base configs, applying profile overrides via
deep_merge + apply_section_update, publishing ZMQ updates, and
persisting active profile to /config/.active_profile.
* add profile API endpoints (GET /profiles, GET/PUT /profile)
* add MQTT and dispatcher integration for profiles
- Subscribe to frigate/profile/set MQTT topic
- Publish profile/state and profiles/available on connect
- Add _on_profile_command handler to dispatcher
- Broadcast active profile state on WebSocket connect
* wire ProfileManager into app startup and FastAPI
- Create ProfileManager after dispatcher init
- Restore persisted profile on startup
- Pass dispatcher and profile_manager to FastAPI app
* add tests for invalid profile values and keys
Tests that Pydantic rejects: invalid field values (fps: "not_a_number"),
unknown section keys (ffmpeg in profile), invalid nested values, and
invalid profiles in full config parsing.
* formatting
* fix CameraLiveConfig JSON serialization error on profile activation
refactor _publish_updates to only publish ZMQ updates for
sections that actually changed, not all sections on affected cameras.
* consolidate
* add enabled field to camera profiles for enabling/disabling cameras
* add zones support to camera profiles
* add frontend profile types, color utility, and config save support
* add profile state management and save preview support
* add profileName prop to BaseSection for profile-aware config editing
* add profile section dropdown and wire into camera settings pages
* add per-profile camera enable/disable to Camera Management view
* add profiles summary page with card-based layout and fix backend zone comparison bug
* add active profile badge to settings toolbar
* i18n
* add red dot for any pending changes including profiles
* profile support for mask and zone editor
* fix hidden field validation errors caused by lodash wildcard and schema gaps
lodash unset does not support wildcard (*) segments, so hidden fields like
filters.*.mask were never stripped from form data, leaving null raw_coordinates
that fail RJSF anyOf validation. Add unsetWithWildcard helper and also strip
hidden fields from the JSON schema itself as defense-in-depth.
* add face_recognition and lpr to profile-eligible sections
* move profile dropdown from section panes to settings header
* add profiles enable toggle and improve empty state
* formatting
* tweaks
* tweak colors and switch
* fix profile save diff, masksAndZones delete, and config sync
* ui tweaks
* ensure profile manager gets updated config
* rename profile settings to ui settings
* refactor profilesview and add dots/border colors when overridden
* implement an update_config method for profile manager
* fix mask deletion
* more unique colors
* add top-level profiles config section with friendly names
* implement profile friendly names and improve profile UI
- Add ProfileDefinitionConfig type and profiles field to FrigateConfig
- Use ProfilesApiResponse type with friendly_name support throughout
- Replace Record<string, unknown> with proper JsonObject/JsonValue types
- Add profile creation form matching zone pattern (Zod + NameAndIdFields)
- Add pencil icon for renaming profile friendly names in ProfilesView
- Move Profiles menu item to first under Camera Configuration
- Add activity indicators on save/rename/delete buttons
- Display friendly names in CameraManagementView profile selector
- Fix duplicate colored dots in management profile dropdown
- Fix i18n namespace for overridden base config tooltips
- Move profile override deletion from dropdown trash icon to footer
button with confirmation dialog, matching Reset to Global pattern
- Remove Add Profile from section header dropdown to prevent saving
camera overrides before top-level profile definition exists
- Clean up newProfiles state after API profile deletion
- Refresh profiles SWR cache after saving profile definitions
* remove profile badge in settings and add profiles to main menu
* use icon only on mobile
* change color order
* docs
* show activity indicator on trash icon while deleting a profile
* tweak language
* immediately create profiles on backend instead of deferring to Save All
* hide restart-required fields when editing a profile section
fields that require a restart cannot take effect via profile switching,
so they are merged into hiddenFields when profileName is set
* show active profile indicator in desktop status bar
* fix profile config inheritance bug where Pydantic defaults override base values
The /config API was dumping profile overrides with model_dump() which included
all Pydantic defaults. When the frontend merged these over
the camera's base config, explicitly-set base values were
lost. Now profile overrides are re-dumped with exclude_unset=True so only
user-specified fields are returned.
Also fixes the Save All path generating spurious deletion markers for
restart-required fields that are hidden during profile
editing but not excluded from the raw data sanitization in
prepareSectionSavePayload.
* docs tweaks
* docs tweak
* formatting
* formatting
* fix typing
* fix test pollution
test_maintainer was injecting MagicMock() into sys.modules["frigate.config.camera.updater"] at module load time and never restoring it. When the profile tests later imported CameraConfigUpdateEnum and CameraConfigUpdateTopic from that module, they got mock objects instead of the real dataclass/enum, so equality comparisons always failed
* remove
* fix settings showing profile-merged values when editing base config
When a profile is active, the in-memory config contains effective
(profile-merged) values. The settings UI was displaying these merged
values even when the "Base Config" view was selected.
Backend: snapshot pre-profile base configs in ProfileManager and expose
them via a `base_config` key in the /api/config camera response when a
profile is active. The top-level sections continue to reflect the
effective running config.
Frontend: read from `base_config` when available in BaseSection,
useConfigOverride, useAllCameraOverrides, and prepareSectionSavePayload.
Include formData labels in Object/Audio switches widgets so that labels
added only by a profile override remain visible when editing that profile.
* use rasterized_mask as field
makes it easier to exclude from the schema with exclude=True
prevents leaking of the field when using model_dump for profiles
* fix zones
- Fix zone colors not matching across profiles by falling back to base zone color when profile zone data lacks a color field
- Use base_config for base-layer values in masks/zones view so profile-merged values don't pollute the base config editing view
- Handle zones separately in profile manager snapshot/restore since ZoneConfig requires special serialization (color as private attr, contour generation)
- Inherit base zone color and generate contours for profile zone overrides in profile manager
* formatting
* don't require restart for camera enabled change for profiles
* publish camera state when changing profiles
* formatting
* remove available profiles from mqtt
* improve typing
2026-03-19 17:47:57 +03:00
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="disabled">
|
2026-05-24 23:59:56 +03:00
|
|
|
{t("cameraManagement.profiles.off", {
|
Camera profile support (#22482)
* add CameraProfileConfig model for named config overrides
* add profiles field to CameraConfig
* add active_profile field to FrigateConfig
Runtime-only field excluded from YAML serialization, tracks which
profile is currently active.
* add ProfileManager for profile activation and persistence
Handles snapshotting base configs, applying profile overrides via
deep_merge + apply_section_update, publishing ZMQ updates, and
persisting active profile to /config/.active_profile.
* add profile API endpoints (GET /profiles, GET/PUT /profile)
* add MQTT and dispatcher integration for profiles
- Subscribe to frigate/profile/set MQTT topic
- Publish profile/state and profiles/available on connect
- Add _on_profile_command handler to dispatcher
- Broadcast active profile state on WebSocket connect
* wire ProfileManager into app startup and FastAPI
- Create ProfileManager after dispatcher init
- Restore persisted profile on startup
- Pass dispatcher and profile_manager to FastAPI app
* add tests for invalid profile values and keys
Tests that Pydantic rejects: invalid field values (fps: "not_a_number"),
unknown section keys (ffmpeg in profile), invalid nested values, and
invalid profiles in full config parsing.
* formatting
* fix CameraLiveConfig JSON serialization error on profile activation
refactor _publish_updates to only publish ZMQ updates for
sections that actually changed, not all sections on affected cameras.
* consolidate
* add enabled field to camera profiles for enabling/disabling cameras
* add zones support to camera profiles
* add frontend profile types, color utility, and config save support
* add profile state management and save preview support
* add profileName prop to BaseSection for profile-aware config editing
* add profile section dropdown and wire into camera settings pages
* add per-profile camera enable/disable to Camera Management view
* add profiles summary page with card-based layout and fix backend zone comparison bug
* add active profile badge to settings toolbar
* i18n
* add red dot for any pending changes including profiles
* profile support for mask and zone editor
* fix hidden field validation errors caused by lodash wildcard and schema gaps
lodash unset does not support wildcard (*) segments, so hidden fields like
filters.*.mask were never stripped from form data, leaving null raw_coordinates
that fail RJSF anyOf validation. Add unsetWithWildcard helper and also strip
hidden fields from the JSON schema itself as defense-in-depth.
* add face_recognition and lpr to profile-eligible sections
* move profile dropdown from section panes to settings header
* add profiles enable toggle and improve empty state
* formatting
* tweaks
* tweak colors and switch
* fix profile save diff, masksAndZones delete, and config sync
* ui tweaks
* ensure profile manager gets updated config
* rename profile settings to ui settings
* refactor profilesview and add dots/border colors when overridden
* implement an update_config method for profile manager
* fix mask deletion
* more unique colors
* add top-level profiles config section with friendly names
* implement profile friendly names and improve profile UI
- Add ProfileDefinitionConfig type and profiles field to FrigateConfig
- Use ProfilesApiResponse type with friendly_name support throughout
- Replace Record<string, unknown> with proper JsonObject/JsonValue types
- Add profile creation form matching zone pattern (Zod + NameAndIdFields)
- Add pencil icon for renaming profile friendly names in ProfilesView
- Move Profiles menu item to first under Camera Configuration
- Add activity indicators on save/rename/delete buttons
- Display friendly names in CameraManagementView profile selector
- Fix duplicate colored dots in management profile dropdown
- Fix i18n namespace for overridden base config tooltips
- Move profile override deletion from dropdown trash icon to footer
button with confirmation dialog, matching Reset to Global pattern
- Remove Add Profile from section header dropdown to prevent saving
camera overrides before top-level profile definition exists
- Clean up newProfiles state after API profile deletion
- Refresh profiles SWR cache after saving profile definitions
* remove profile badge in settings and add profiles to main menu
* use icon only on mobile
* change color order
* docs
* show activity indicator on trash icon while deleting a profile
* tweak language
* immediately create profiles on backend instead of deferring to Save All
* hide restart-required fields when editing a profile section
fields that require a restart cannot take effect via profile switching,
so they are merged into hiddenFields when profileName is set
* show active profile indicator in desktop status bar
* fix profile config inheritance bug where Pydantic defaults override base values
The /config API was dumping profile overrides with model_dump() which included
all Pydantic defaults. When the frontend merged these over
the camera's base config, explicitly-set base values were
lost. Now profile overrides are re-dumped with exclude_unset=True so only
user-specified fields are returned.
Also fixes the Save All path generating spurious deletion markers for
restart-required fields that are hidden during profile
editing but not excluded from the raw data sanitization in
prepareSectionSavePayload.
* docs tweaks
* docs tweak
* formatting
* formatting
* fix typing
* fix test pollution
test_maintainer was injecting MagicMock() into sys.modules["frigate.config.camera.updater"] at module load time and never restoring it. When the profile tests later imported CameraConfigUpdateEnum and CameraConfigUpdateTopic from that module, they got mock objects instead of the real dataclass/enum, so equality comparisons always failed
* remove
* fix settings showing profile-merged values when editing base config
When a profile is active, the in-memory config contains effective
(profile-merged) values. The settings UI was displaying these merged
values even when the "Base Config" view was selected.
Backend: snapshot pre-profile base configs in ProfileManager and expose
them via a `base_config` key in the /api/config camera response when a
profile is active. The top-level sections continue to reflect the
effective running config.
Frontend: read from `base_config` when available in BaseSection,
useConfigOverride, useAllCameraOverrides, and prepareSectionSavePayload.
Include formData labels in Object/Audio switches widgets so that labels
added only by a profile override remain visible when editing that profile.
* use rasterized_mask as field
makes it easier to exclude from the schema with exclude=True
prevents leaking of the field when using model_dump for profiles
* fix zones
- Fix zone colors not matching across profiles by falling back to base zone color when profile zone data lacks a color field
- Use base_config for base-layer values in masks/zones view so profile-merged values don't pollute the base config editing view
- Handle zones separately in profile manager snapshot/restore since ZoneConfig requires special serialization (color as private attr, contour generation)
- Inherit base zone color and generate contours for profile zone overrides in profile manager
* formatting
* don't require restart for camera enabled change for profiles
* publish camera state when changing profiles
* formatting
* remove available profiles from mqtt
* improve typing
2026-03-19 17:47:57 +03:00
|
|
|
ns: "views/settings",
|
|
|
|
|
})}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</SettingsGroupCard>
|
|
|
|
|
);
|
|
|
|
|
}
|