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
The connect() function creates a WebSocket but never stores the
reference. The useEffect cleanup only closes the RTCPeerConnection
via pcRef, leaving the WebSocket open.
Each time the component re-renders with changed deps (camera switch,
playback toggle, microphone toggle), a new WebSocket is created
without closing the previous one. This leaks connections until the
browser garbage-collects them or the server times out.
Store the WebSocket in a ref and close it in the cleanup function.
When an existing tracked object's label or stationary status changes
(e.g. sub_label assignment from face recognition), the update handler
declares a new const newObjects that shadows the outer let newObjects.
The label and stationary mutations apply to the inner copy, but
handleSetObjects on line 148 reads the outer variable which was never
mutated. The update is silently discarded.
Remove the inner declaration so mutations apply to the outer variable
that gets passed to handleSetObjects.
The motionVisible condition gated the external dot (via onActiveMotionChange
callback) on liveReady, causing the dot to stay hidden for cameras in
continuous mode (showStillWithoutActivity=false) while the stream is loading
or reconnecting. Since the parent (DraggableGridLayout) renders the dot
outside the stream viewport, it should reflect actual motion state without
depending on stream load status.
Simplify the callback-path effect to use !!(autoLive && !offline &&
activeMotion) so the dot appears in the grid card whenever motion is active.
The full condition (including liveReady) is still used for the inline dot
rendered inside LivePlayer when no callback is provided.
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
getStats was always passed showStats (false in grid view), so underlying
players never collected stats data. Now uses showStats || !!onStatsUpdate
so players collect stats whenever the external callback is present.
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
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
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.
* fix double scrollbar in debug replay
* always hide ffmpeg cpu warnings for replay cameras
* add slovenian
* fix motion previews on safari and ios
match the logic used in ScrubbablePreview for manually stepping currentTime at the correct rate
* prevent motion recalibration when opening motion tuner
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
* add shm frame lifetime calculation and update UI for shared memory metrics
* consistent sizing on activity indicator in save buttons
* fix offline overlay overflowing on mobile when in grid mode
* refactor websockets to remove react-tracked
react 19 removed useReducer eager bailout, which broke react-tracked.
react-tracked works by wrapping state in a JavaScript Proxy. When a component reads state.someField, the proxy records that access. On the next state update, it compares only the fields each component actually touched and skips re-renders if those fields are unchanged. Under the hood, this relies on useReducer — and in React 18, useReducer had an "eager bail-out" that short-circuited rendering when the new state was === to the old state. React 19 removed that optimization, so every dispatch now schedules a render regardless, and the proxy comparison runs too late to prevent it.
useSyncExternalStore is a React primitive (added in 18, stable in 19) designed for exactly this pattern: subscribing to an external store:
useSyncExternalStore(
subscribe, // (listener) => unsubscribe — called when the store changes
getSnapshot // () => value — returns the current value for this subscriber
)
React calls getSnapshot during render and compares the result with Object.is. If the value is the same reference, the component bails out — no re-render. The key difference from react-tracked is that this bail-out is built into React's reconciler, not bolted on via proxy tricks and useReducer.
The per-topic subscription model makes this efficient. Instead of one global store where every subscriber has to check if their fields changed, each useWs("some/topic", ...) call subscribes only to that topic's listener set. When a message arrives for front_door/detect/state, only components subscribed to that exact topic get their listener fired → React calls their getSnapshot → Object.is compares the value → bail-out if unchanged. Components watching back_yard/detect/state are never even notified.
* remove react-tracked and react-use-websocket
* refactor usePolygonStates to use ws topic subscription
* fix TimeAgo refresh interval always returning 1s due to unit mismatch (seconds vs milliseconds)
older events now correctly refresh every minute/hour instead of every second
* simplify
* clean up
* don't resend onconnect
* clean up
* remove patch
* optimize recordings/summary endpoint db query
replace strftime with integer arithmetic. increases speed by about 6x, especially noticeable for installs with long retention days
* optimize calendar rendering with Set lookups and remove unnecessary remount key
The old code built Date[] arrays with a TZDate object for every day in recording history (365+ timezone-aware date constructions). react-day-picker then did O(visible × history) date comparisons to match each of the displayed days against these arrays. Now we build Set<string> from the raw keys (zero date construction), and pass matcher functions that do O(1) Set.has() lookups. react-day-picker only calls these for visible days
* clean up