Use useRef to store onStatsUpdate/onLoadingChange/onActiveMotionChange
callbacks so useEffect deps don't include the callback references.
Inline arrow functions in .map() change identity every render, causing
the previous useEffect([stats, onCallback]) to re-fire on each parent
re-render, triggering another setState → re-render → infinite loop.
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
When reduce_storage_consumption() encountered a FileNotFoundError
(file deleted outside Frigate), it silently skipped the recording
without removing it from the database. Over time this caused the DB
to accumulate stale entries, making "Frigate recordings tracked" in
/system#storage dramatically overstate actual disk usage.
The bug also affected cleanup behaviour: stale entries don't count
toward freed-space accounting, so Phase 2 (force-delete retained
recordings) could trigger prematurely when most old entries were stale.
Fix: always append the recording to deleted_recordings regardless of
whether the file existed, so the DB entry is removed. freed-space
accounting is unchanged — FileNotFoundError still does not increment
deleted_segments_size since no actual disk space was recovered.
Applied to both Phase 1 (non-retained) and Phase 2 (retained) loops
inside reduce_storage_consumption().
https://claude.ai/code/session_01DMdSSQhQfTuXmzPtRvJmLB
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
VideoPreview's <video> had aspect-video + size-full, but size-full overrides
the aspect-ratio constraint, leaving object-fit at the default fill.
Adding object-contain preserves the video's natural aspect ratio in event cards.
https://claude.ai/code/session_01EwdaKGsrRLZ74smmCQ1MgW
PreviewVideoPlayer's <video> had no explicit object-fit, so browsers
applied the CSS default (fill), stretching the video when the container
aspect ratio (detect resolution) didn't match the actual preview video.
Adding object-contain preserves aspect ratio in the recording/history view.
https://claude.ai/code/session_01EwdaKGsrRLZ74smmCQ1MgW
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
The MSE player default was set to 'fill' which stretches video in all contexts.
Only the draggable grid should use 'cover' (via --frigate-mse-object-fit:cover).
Changing the fallback to 'contain' restores aspect-ratio-preserving behaviour
everywhere else (History preview, etc.) while keeping the grid fill intact.
https://claude.ai/code/session_01EwdaKGsrRLZ74smmCQ1MgW
When cameras are configured with recording paths outside /media/frigate
(e.g. /video1, /video2), preview mp4 files generated there had no
corresponding nginx location block — requests returned 404.
At nginx startup, get_nginx_settings.py now extracts unique recording
roots outside /media/frigate from the Frigate config. The nginx run
script uses a new extra_recordings.gotmpl template to generate
location blocks (e.g. /video1/preview/) with alias directives for
each such root, included via extra_recordings.conf.
The API already returns correct src URLs for these paths (the existing
replace(BASE_DIR, "") leaves non-media paths unchanged), so no API
changes are needed.
https://claude.ai/code/session_016bxjbVpx8DqpjysnGYmXdx
When reduce_storage_consumption deletes old recording segments to free
disk space, it now also deletes preview files that overlap the same
time range. Without this, preview mp4 files on the same disk continued
to consume space, causing the storage maintainer to delete progressively
newer recordings while old previews accumulated — resulting in archives
where older periods had previews but no video.
This is particularly impactful for multi-path setups where each camera's
preview directory shares a disk with its recordings.
https://claude.ai/code/session_016bxjbVpx8DqpjysnGYmXdx
- write_frame_to_cache() now returns bool; callers only append the
timestamp to output_frames when cv2.imwrite() actually succeeded,
preventing dangling timestamps that cause ffmpeg "Impossible to open"
errors when the cache disk is full
- FFMpegConverter removes the partial output mp4 on ffmpeg failure so
stale partial files don't accumulate on the recording disk
https://claude.ai/code/session_016bxjbVpx8DqpjysnGYmXdx