In-memory cameraZoomStates was not reset when switching groups,
causing hydration to skip cameras already in state and keep
zoom from the previous group. Resetting on group change lets
hydration reload correct per-group zoom from localStorage.
https://claude.ai/code/session_01WidMYGkyBCFf4L9PnFEiZ5
Each camera now stores zoom state under a group-scoped key
(live:grid-card:zoom:<group>:<camera>), so different groups
can have independent zoom levels for the same camera.
https://claude.ai/code/session_01WidMYGkyBCFf4L9PnFEiZ5
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