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
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
Two fixes:
1. useCameraActivity: replace broken ternary priority with OR — "OFF"
(truthy string) was silently blocking camera_activity.motion fallback.
Now: motion === true (from camera_activity) OR detectingMotion === "ON".
2. DraggableGridLayout: render CameraMotionDot outside the zoom transform
div so the dot doesn't scale with camera zoom. LivePlayer gets
showMotionDot={false} to avoid duplicate rendering.
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
Remove the external CameraMotionDot component and showMotionDot={false}
override. The ws.ts fix (camera_activity -> motion topic sync) ensures
useCameraActivity gets fresh data, so the built-in dot in LivePlayer works.
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
autoLive ?? globalAutoLive can be undefined when useUserPersistence
hasn't hydrated yet. Change the prop type to optional boolean and
treat undefined as the default-true value (show dot unless explicitly
set to false via no-streaming mode).
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
The previous approach (useEffect → onActiveMotionChange callback →
parent state update) was unreliable: the dot only appeared if motion
was active at the moment of initial mount but did not react to
subsequent WS motion events.
Root cause: the intermediate state chain breaks because React's
useEffect batching and component re-render timing can cause the
parent state to lag behind or miss updates when motion changes after
mount.
Fix: replace the mechanism entirely with a dedicated CameraMotionDot
component that calls useCameraActivity directly. Being a proper React
component it subscribes to the {camera}/motion WS topic via
useSyncExternalStore and re-renders immediately and reliably whenever
motion state changes — no intermediate callbacks or parent state needed.
- Remove onActiveMotionChange prop from LivePlayer; add showMotionDot
boolean prop (default true) to suppress the internal dot in grid view
- Remove cameraMotionStates state and setCameraMotionStates from
DraggableGridLayout
- Add CameraMotionDot component with direct useCameraActivity hook
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
Move PlayerStats, ActivityIndicator and motion dot rendering outside the
zoom transform div in DraggableGridLayout so they are not scaled when
the user zooms with Shift+Wheel.
- Add onStatsUpdate, onLoadingChange, onActiveMotionChange callback props
to LivePlayer; when provided, suppress the internal overlay elements
and bubble state up to the parent instead
- In DraggableGridLayout, maintain per-camera overlay states and render
the three overlays as siblings to the zoom div (inside the clipping
viewport) so they remain at natural size regardless of zoom level
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
Set margin and containerPadding to [0,0] in ResponsiveGridLayout,
removed px-2/my-2/pb-8 from the wrapper div, and updated cellHeight
formula to not account for margins.
https://claude.ai/code/session_01THf2SuS7hLt9NgstxvKdg8
useLayoutEffect with [] deps only ran on the initial render when
gridContainerRef was null (grid div was hidden behind skeleton).
After skeleton disappeared the div mounted but useLayoutEffect never
re-ran, leaving containerWidth=0 and Responsive invisible (blank screen).
A callback ref fires every time the element mounts, so containerWidth
is always set immediately when the grid div first appears.
useResizeObserver reads ref.current during render (before commit), so on
first render ref.current is null, no observation starts, and containerWidth
stays 0 if no subsequent re-render happens (e.g. page refresh with cached
SWR data). useLayoutEffect runs after refs are committed, so ref.current
is always the real DOM element.
This fixes both the right-column overflow (no window.innerWidth fallback
needed — width is always the actual container width) and the black screen
on refresh (containerWidth is reliable before the first paint).
https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
useResizeObserver reads ref.current at render time; on page refresh with
fast SWR cache, no re-render occurs after mount so ref.current remains null
in the effect, observation never starts, and containerWidth stays 0 forever.
Add a useLayoutEffect that measures offsetWidth synchronously before paint
as a seed value (effectiveWidth = containerWidth || initialWidth). Once
ResizeObserver fires normally, containerWidth takes over. The Responsive
grid is gated on effectiveWidth > 0 so it always renders correctly on both
first load and refresh.
https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
Gate <Responsive> rendering on containerWidth > 0 so it only mounts after
ResizeObserver has measured the container. Use availableWidth directly as
the width prop (no window.innerWidth fallback) since the component now only
renders when containerWidth is known. This prevents the grid from rendering
wider than its container (which caused the rightmost column to overflow the
right edge).
https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
availableWidth starts at 0 (not null/undefined) before ResizeObserver fires.
The ?? operator passes 0 through instead of falling back to window.innerWidth,
making cellHeight negative and causing react-grid-layout to render a ~10px
container. The overflow-x-hidden div then becomes an implicit scroll container,
producing the 'cards squeezed in a small rectangle' symptom.
Changing ?? to || makes 0 trigger the window.innerWidth fallback, giving a
reasonable initial rowHeight until the real container width is measured.
https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
Grid tiles explicitly set --frigate-mse-object-fit:fill so video stretches
to fill the card without preserving aspect ratio. The MsePlayer default
is contain, so History preview and all other contexts keep correct proportions.
https://claude.ai/code/session_01EwdaKGsrRLZ74smmCQ1MgW
* fix useImageLoaded hook running on every render
* fix volume not applying for all cameras
* Fix maximum update depth exceeded errors on Review page
- use-overlay-state: use refs for location to keep setter identity
stable across renders, preventing cascading re-render loops when
effects depend on the setter. Add Object.is bail-out guard to skip
redundant navigate calls. Move setPersistedValue after bail-out to
avoid unnecessary IndexedDB writes.
* don't try to fetch previews when motion search dialog is open
* revert unneeded changes
re-rendering was caused by the overlay state hook, not this one
* filter dicts to only use id field in sync recordings
* remove unused RecoilRoot and fix implicit ref callback
Remove the vestigial recoil dependency (zero consumers) and convert
the implicit-return ref callback in SearchView to block form to
prevent React 19 interpreting it as a cleanup function.
* replace react-transition-group with framer-motion in Chip
Replace CSSTransition with framer-motion AnimatePresence + motion.div
for React 19 compatibility (react-transition-group uses findDOMNode).
framer-motion is already a project dependency.
* migrate react-grid-layout v1 to v2
- Replace WidthProvider(Responsive) HOC with useContainerWidth hook
- Update types: Layout (single item) → LayoutItem, Layout[] → Layout
- Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig
- Update EventCallback signature for v2 API
- Remove @types/react-grid-layout (v2 includes its own types)
* upgrade vaul, next-themes, framer-motion, react-zoom-pan-pinch
- vaul: ^0.9.1 → ^1.1.2
- next-themes: ^0.3.0 → ^0.4.6
- framer-motion: ^11.5.4 → ^12.35.0 (React 19 native support)
- react-zoom-pan-pinch: 3.4.4 → latest
* upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5
Core React 19 upgrade with all necessary type fixes:
- Update RefObject types to accept T | null (React 19 refs always nullable)
- Add JSX namespace imports (no longer global in React 19)
- Add initial values to useRef calls (required in React 19)
- Fix ReactElement.props unknown type in config-form components
- Fix IconWrapper interface to use HTMLAttributes instead of index signature
- Add monaco-editor as dev dependency for type declarations
- Upgrade react-konva to v19, eslint-plugin-react-hooks to v5
* upgrade typescript to 5.9.3
* modernize Context.Provider to React 19 shorthand
Replace <Context.Provider value={...}> with <Context value={...}>
across all project-owned context providers. External library contexts
(react-icons IconContext, radix TooltipPrimitive) left unchanged.
* add runtime patches for React 19 compatibility
- Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs
to prevent infinite render loops from unstable ref callbacks
https://github.com/radix-ui/primitives/issues/3799
- Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in
SlotClone instead of inline composeRefs to prevent re-render cycles
https://github.com/radix-ui/primitives/pull/3804
- Patch react-use-websocket@4.8.1: remove flushSync wrappers that
cause "Maximum update depth exceeded" with React 19 auto-batching
https://github.com/facebook/react/issues/27613
- Add npm overrides to ensure single hoisted copies of compose-refs
and react-slot across all Radix packages
- Add postinstall script for patch-package
- Remove leftover react-transition-group dependency
* formatting
* use availableWidth instead of useContainerWidth for grid layout
The useContainerWidth hook from react-grid-layout v2 returns raw
container width without accounting for scrollbar width, causing the
grid to not fill the full available space. Use the existing
availableWidth value from useResizeObserver which already compensates
for scrollbar width, matching the working implementation.
* remove unused carousel component and fix React 19 peer deps
Remove embla-carousel-react and its unused Carousel UI component.
Upgrade sonner v1 → v2 for native React 19 support. Remove
@types/react-icons stub (react-icons bundles its own types).
These changes eliminate all peer dependency conflicts, so
npm install works without --legacy-peer-deps.
* fix React 19 infinite re-render loop on live dashboard
The "Maximum update depth exceeded" error was caused by two issues:
1. useDeferredStreamMetadata returned a new `{}` default on every render
when SWR data was undefined, creating an unstable reference that
triggered the useEffect in useCameraLiveMode on every render cycle.
Fixed by using a stable module-level EMPTY_METADATA constant.
2. useResizeObserver's rest parameter `...refs` created a new array on
every render, causing its useEffect to re-run and re-observe elements
continuously. Fixed by stabilizing refs with useRef and only
reconnecting the observer when actual DOM elements change.
* Implement renaming in model editing dialog
* add transcription faq
* remove incorrect constraint for viewer as username
should be able to change anyone's role other than admin
* Don't save redundant state changes
* prevent crash when a camera doesn't support onvif imaging service required for focus support
* Fine tune behavior
* Stop redundant go2rtc stream metadata requests and defer audio information to allow bandwidth for image requests
* Improve cleanup logic for capture process
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
* feat(player): always show camera names + add UI config toggle
* feat(settings): add toggle for displaying camera names in multi-camera views
* update label and description for camera name setting
* fix i18n keys
* hide disable from context menu for viewers
* Fix auto live check for default dashboard and camera groups
Disabling the Automatic Live View switch in Settings should prevent streaming from occurring. Overriding any settings in a camera group will override the global setting. The check here incorrectly always returned false instead of undefined.
* clarify hardware accelerated enrichments
* clarify
* add note about detect stream to face rec docs
* add note about low end Dahuas for autotracking
* Catch invalid face box / image
* Video tab tweaks
With the changes in https://github.com/blakeblackshear/frigate/pull/18220, the video tab in the Tracked Object Details pane now correctly trims the in-browser HLS video. Because of keyframes and record/detect stream differences, we can manually subtract a couple of seconds from the event start_time to ensure the first few frames aren't cut off from the video
* Clarify
* Don't use Migraphx by default
* Provide better support for running embeddings on GPU
* correctly join cameras
* Adjust blur confidence reduction
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>