Keys 1-9 switch to camera groups 1-9 (sorted by order), key 0 switches
to the 10th group. Shortcuts are ignored when viewing a single camera
(selectedCameraName is set) and do not interfere with text input fields
(handled by useKeyboardListener's built-in focus detection).
https://claude.ai/code/session_018vH9fGSi5McLLa47GEiZJC
Replace cameras array dependency in the reset useEffect with a stable
cameraKey string (sorted camera names joined by comma). Since cameras
is recreated on every parent render, the previous effect fired on each
re-render, immediately clearing the persisted order. The key changes
only when the actual set of cameras changes.
https://claude.ai/code/session_018sRNpvMwxLgt5Un8PCjfRU
Replace in-memory fitLayoutOverride (useState) with fitCameraOrder
(useUserPersistence<string[]>) keyed as `${cameraGroup}-fit-camera-order`.
The fit layout now restores the saved camera order on load and recomputes
positions from current fitGridParams, so the order survives page reloads
and window resizes without storing screen-size-dependent coordinates.
https://claude.ai/code/session_018sRNpvMwxLgt5Un8PCjfRU
fitGridParams is recreated on every useMemo run, causing the useEffect to
fire and clear fitLayoutOverride after each swap. Switch deps to
fitGridParams?.gridUnitsPerCam and fitGridParams?.colsPerRow (primitives)
so the reset only triggers when the grid geometry actually changes.
Also remove all debug console.log added during investigation.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
Replace getBoundingClientRect+clientX/Y with newItem.x/y from react-grid-layout.
With noCompactor, newItem reports the free grid position where the element was
dropped — reliable across all rows without pixel math or scroll issues.
Also remove handleFitDrag/fitDragRef (no longer needed) and generate a full
snapBack layout so displaced items snap back correctly on no-op drops.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
In fit-to-screen mode, pass noCompactor to <Responsive> so elements
follow the mouse freely without vertical compaction pushing them down.
The swap-on-drop logic (mouse coordinates in onDragStop) stays unchanged.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
All LayoutItem args are nullable (LayoutItem | null) and event is Event
not MouseEvent — matching the actual react-grid-layout EventCallback type.
fitDragRef simplified to string | null. Cast event to MouseEvent inside
the handler to access clientX/Y for pixel-accurate slot detection.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
rgl with compactType=vertical doesn't move items horizontally, so
layoutItem.x in onDragStop stays near its origin, making horizontal
slot detection wrong.
Switch to tracking event.clientX/Y from onDrag (fitDragRef), then in
onDragStop translate the final mouse position against the .grid-layout
element's bounding rect to get pixel-accurate targetCol/targetRow.
This makes horizontal, vertical, and long-distance swaps all reliable.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
Previous approach sorted react-grid-layout's post-drag positions to infer
order, which broke for non-adjacent and horizontal moves because rgl pushes
items down instead of swapping them.
New approach:
- onDrag records which item is being dragged (draggedItemRef)
- onDragStop uses the dragged item's final x/y to compute the target slot
in our own grid, then performs a clean swap in the ordered name array
- Layout is always fully regenerated from our order array, ignoring rgl's
position arithmetic entirely
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
handleFitDragStop now sorts dragged items by position to determine new
order, then recalculates all x/y coords into a strict dense grid instead
of spreading react-grid-layout's arbitrary y values — prevents cards from
being pushed off-screen after a drag.
Also replaces LuMaximize with LuScanBarcode for the fit-to-screen button.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
In fitToScreen mode, drag is now enabled so users can reorder cameras
while in edit mode. A fitLayoutOverride state captures the new order
after each drag, normalizing w/h back to gridUnitsPerCam to prevent
size changes. The override resets automatically when the camera list or
grid parameters change.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
- PreviewPlayer: add rotate prop, pass through to PreviewVideoPlayer and
PreviewFramesPlayer
- PreviewVideoPlayer: add rotate prop + ResizeObserver; wrap <img> and
<video> in width/height-swap container with rotate(90deg) transform;
add h-full when rotate to fix height chain
- PreviewFramesPlayer: same pattern for <img> frame previews
- DynamicVideoPlayer: pass rotate={rotate} to PreviewPlayer
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
Adds a toggle button to the Live view toolbar that automatically arranges
all cameras to fit within the viewport without scrolling. Uses a brute-force
algorithm to find the optimal number of columns that maximizes camera size
while keeping all cameras visible. State persists via IndexedDB.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
TransformComponent's contentStyle had height: undefined on desktop,
so the rotate container's ResizeObserver measured height=0, causing
the inner div to get width=0 and the video to be invisible.
Adding || rotate to the height condition ensures the height chain is
intact when rotation is active, matching the isMobile path that already
set height: "100%".
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
- HlsVideoPlayer: add rotate prop; when true, wraps <video> in a
ResizeObserver-tracked container that swaps width/height and applies
rotate(90deg) transform, mirroring the MsePlayer grid-rotation logic
- DynamicVideoPlayer: thread rotate prop through to HlsVideoPlayer
- RecordingView: invert getCameraAspect ratio (1/ratio) for cameras
with ui.rotate so the outer container gets portrait proportions;
pass rotate={camera.ui?.rotate} to DynamicVideoPlayer
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
Root cause: LivePlayer's outer div has no explicit height (only w-full),
so when MsePlayer reads containerSize.height via ResizeObserver it gets 0.
With isRotatedGrid=true, MsePlayer sets the inner div width:
containerSize.height → width: 0 → video invisible.
Fix:
- Add size-full to LivePlayer className when camera.ui?.rotate, ensuring
height: 100% propagates through the chain so MsePlayer gets real dims
- Re-add cameraAspectRatio inversion (1/ratio) for portrait container
layout; now that the height chain is intact this works correctly:
portrait container → LivePlayer size-full → MsePlayer real dims → swap+rotate
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
The previous commit caused a double dimension swap for rotated cameras:
- LiveCameraView was inverting the aspect ratio (1/ratio) → portrait container
- MsePlayer was then swapping width/height again internally when
isRotatedGrid=true → video got zero/invalid dimensions, nothing visible
The MsePlayer already handles the full rotation internally via CSS variables
(transform + width/height swap). The container in LiveCameraView should
keep the original (landscape) aspect ratio, matching the grid cell behavior
in DraggableGridLayout where this works correctly.
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
- Invert cameraAspectRatio when camera.ui?.rotate is true so the
container dimensions match the rotated video (width↔height swap)
- Pass CSS variables --frigate-mse-grid-rotated and
--frigate-mse-grid-rotation to LivePlayer, enabling the existing
MsePlayer rotation/swap logic for single-camera view
- Fullscreen orientation lock works automatically: an inverted ratio
< 1 causes portrait lock for a normally-landscape camera
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
When clicking the History button on a specific camera's Live view,
append `?cameras=<camera_name>` to the review URL so the camera
filter is pre-set to that camera instead of showing "All Cameras".
The Events (Review) page already supports reading the `cameras` URL
parameter via useSearchEffect - no changes needed there.
Fixes: #12776, #16987https://claude.ai/code/session_01PnMA1HcuKsEXcvVLaXRgF1
When scrubbing in RecordingView, tall cameras passed size-full (width:100% + height:100%)
to PreviewPlayer, causing the browser to ignore aspect-ratio. Replacing size-full with
w-full lets height be computed from width + aspect-ratio, preserving correct proportions.
https://claude.ai/code/session_019sUH2h6HoVswdtD7EbhAJa
Replace size-full (100%×100%) on img/video elements with max-h-full max-w-full
so portrait (9:16) and 4:3 cameras maintain their natural proportions during
scrubbing in RecordingView. Add items-center to flex containers so content
stays vertically centered within the available space.
https://claude.ai/code/session_01H1uowWMpsNm1U8HdcSP8AA
* fix genai settings ui
- add roles widget to select roles for genai providers
- add dropdown in semantic search to allow selection of embeddings genai provider
* tweak grouping to prioritize fieldOrder before groups
previously, groups were always rendered first. now fieldOrder is respected, and any fields in a group will cause the group and all the fields in that group to be rendered in order. this allows moving the enabled switches to the top of the section
* mobile tweaks
stack buttons, add more space on profiles pane, and move the overridden badge beneath the description
* language consistency
* prevent camera config sections from being regenerated for profiles
* conditionally import axengine module
to match other detectors
* i18n
* update vscode launch.json for new integrated browser
* formatting
- MsePlayer: change default object-fit fallback from fill to contain
(grid layout keeps fill via --frigate-mse-object-fit:fill CSS variable)
- PreviewPlayer: add object-contain class to video element
- HlsVideoPlayer: add object-contain class to video element
Recordings view and timeline preview now preserve aspect ratio,
while live grid continues to stretch corridor cameras as before.
Add optional `roles` field to camera groups config to control which user
roles can see each group. Groups without roles are visible only to admins.
Admin users always see all groups. Backend filters groups in GET /config
based on remote-role header. Frontend adds roles multiselect in group
editor (admin only).
https://claude.ai/code/session_011sp9kHQfM39JvVxKHFh1Xq
* Add go2rtc settings section
- create separate settings section for all go2rtc streams
- extract credentials mask code into util
- create ffmpeg module utility
- i18n
* add camera config updater topic for live section
to support adding go2rtc streams after configuring a new one via the UI
* clean up
* tweak delete button color for consistency
* tweaks
* 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
Add optional `users` field to camera groups config allowing groups to be
restricted to specific users. Groups without users remain visible to all
(backward compatible). Admin users always see all groups. Backend filters
groups in GET /config based on authenticated user. Frontend adds a users
multi-select toggle in the group editor (admin only).
https://claude.ai/code/session_01PooiYnugPWqdCYDq4TU7ti