* resolve saved credential sentinel to the stored api_key in the GenAI probe
* add profile faq
* center the multi-camera export time range on the current playback position
* add faq about preview restart cache
* clarify exports bulk download
* Catch faces that become empty after cropping
* don't drop batched camera add/remove config updates
TrackedObjectProcessor drained all pending camera config updates at once but handled them in a mutually exclusive if/elif on enabled/add/remove, so only one topic was processed per drain. When an add arrived in the same batch as an enabled update, the add was skipped and the new camera never got a camera state. Adding a camera reliably produced that batch: config_set now re-applies runtime overrides, which republishes an enabled update for every previously toggled camera immediately before the add, in the same request. The dashboard and camera capture still saw the camera (the maintainer does not subscribe to enabled, so it got a clean add-only batch), but object_processing did not, and disabling the camera then crashed with a KeyError on the unguarded camera_states lookup.
Handle add and remove independently instead of as exclusive branches so a batched add is no longer dropped, and guard the remove lookup so a missing state is skipped rather than raising. Drop the enabled branch entirely: it only ever set prev_enabled when it was None, but prev_enabled is seeded to a bool at camera state creation and is never None (mypy flags the body as unreachable), and the actual enable/disable transition is already driven by the disabled-state loop from config.enabled.
* Don't stay on motion search page when user cancels flow
* fix notification test button being blocked by websocket auth
* fix overflowing model names in settings genai widget
* add note about auth debugging
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
/api/config/set and camera deletion re-parse yaml into a fresh FrigateConfig and swap it in, then re-layered the persisted runtime toggle overrides so a camera the user turned off wouldn't come back on. That re-layer ran apply_runtime_state, which replays each override through the command handlers, so every save re-published a ZMQ config update, a retained MQTT state message, and a runtime-state disk write for every camera with a stored toggle. All of it was redundant: the worker processes were never swapped and still hold the live toggle values, so only the in-process config object the API and dispatcher read was out of date. The extra traffic churned the retained MQTT topics, amplified disk writes, and co-drained enabled updates with other topics on the config socket.
Add Dispatcher.reapply_runtime_state_to_config, which corrects only the swapped-in config object, mirroring the field mutations and gates of the _on_*_command handlers with no ZMQ, MQTT, or disk writes. swap_runtime_config now calls it instead of apply_runtime_state; apply_runtime_state is unchanged and still used at startup, where the workers genuinely must be told.
* preserve runtime camera toggles across config saves
Runtime toggles (camera on/off, detect, recordings, snapshots, audio) mutate the in-memory config and persist an override to .runtime_state.json. /api/config/set re-parses yaml into a fresh FrigateConfig and swaps it in, re-applying the yaml and profile layers but dropping the runtime layer, so a camera turned off from the dashboard came back on when an unrelated camera was saved. The workers were never notified, so it only appeared to come back: the UI streamed go2rtc while ffmpeg stayed stopped.
Extract the startup replay into Dispatcher.apply_runtime_state() and call it from config_set after the swap, re-layering the overrides and republishing them so workers and the UI reconverge.
Remove the broad clear_runtime_state() from ProfileManager.update_config, which is only ever reached from config_set: with a profile active, every save wiped every camera's overrides from disk. The broad wipe stays in activate_profile, where a real profile switch does invalidate the steady state. Saves still clear the keys they rewrote via clear_runtime_state_for_yaml_keys, so yaml wins where the two disagree.
* sync runtime config on camera delete and prune its overrides
Deleting a camera re-parsed yaml into a fresh FrigateConfig but only rebound app.frigate_config and genai_manager, never dispatcher.config (nor profile_manager, stats_emitter, or the runtime overrides). The API and the dispatcher then drifted onto different config objects until the next config save re-synced them, so the API reported surviving cameras with their yaml enabled state while the dispatcher still acted on their real runtime state.
Extract the config swap that config_set already does into a shared swap_runtime_config helper and call it from both sites, so every collaborator is rebound and the surviving cameras' runtime toggles are re-layered. Also drop the deleted camera's persisted overrides via a new clear_camera so a camera later added under the same name does not inherit them.
* Cleanup llama.cpp and use api key when configured
* don't report auto-populated object and audio filters as camera overrides
* derive stale replay cameras from bounded directory listings to avoid scanning all clips at startup
* fix tests
* add -vaapi_device to the birdseye vaapi encode preset so hwupload can initialize on ffmpeg 8
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
The recognized_license_plate event filter passed attacker-controlled patterns to re.search on the single serialized SQLite queue thread, letting any authenticated user freeze the whole application with a catastrophic regex. This swaps stdlib re for the regex module with a per-evaluation timeout so a pathological pattern is aborted instead of stalling every database operation.
* honor enabled flag for custom classification models
for both startup and dynamically, even though the UI doesn't currently have a way to toggle dynamically
* add test
* Pin ruff
* Add python upgrade fixes
This enables python upgrade checks in ruff to look for deprecated types and patterns. This namely fixes:
- usage of deprecated `Typing` which is now built in
- some specific exceptions which are caught and have new aliases
Some specific UP checks were also ignored as they are stylistic / unimportant and likely to cause bugs
* Remove async blocking calls
Use asyncio.to_thread on two remaining blocking calls to fix hanging event thread loop. Enable this specific rule to block it in the future.
* Use proper logging mechanism
* Correctly format logs
* Raise with context
When raising an exception include the from context to improve debugging
* Cleanup
* fix per-camera notification MQTT topics never being registered
register notifications/set and notifications/suspend callbacks for each camera, and gate the global notifications topics on per-camera config as well as global (matching WebPushClient creation in app.py). Unregistered topics were silently dropped by paho since only registered callbacks receive messages.
* add tests
Deleting the last entry of a mapping or sequence via config/set orphaned ruamel's comment tokens, which were then emitted above a flow-style {} / [] at column 0, which is unparseable yaml that failed validation and silently rolled the change back. The fix is to clear the emptied collection's stale comment metadata (and the parent's entry for it) so the dump stays valid. Non-empty collections are left untouched so sibling comments are preserved. This covers both emptied maps and emptied lists.
* Handle back seeking going to previous clip
* scope /recordings/unavailable query to the caller's allowed cameras
* listen for config updates in activity manager
* don't set search after awaited request
Intentionally do NOT setSearch() to mark the open event submitted. This runs after the awaited request, by which point the user may have closed the dialog; re-setting the parent's selected event would resurrect it and the force-open effect would reopen it (see #23599). The local "submitted" state covers the open card, and mutate() updates the events cache so the grid and any future open reflect the result.
* fix ruff
#23201 removed pathlib import but for some reason it's just now causing ruff to fail
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
Resolve conflicts in the export pipeline where dev's job-queue refactor
met master's chapter-metadata and security work.
- Unify chapter support under ChaptersEnum (none / recording_segments /
review_items); the realtime stream-copy export selects the per-segment
or per-review-item builder by the camera's configured mode. Thread
chapters through ExportRecordingsBody -> _build_export_job -> ExportJob
-> RecordingExporter.
- Keep master's creation_time/comment export metadata and fix a
video_path duplication the textual merge introduced in the preview
command.
- Move the chapters request field to ExportRecordingsBody (the single
export endpoint) where it is actually honored.
Restore security fixes the automatic merge would have reverted:
- frigate/util/services.py: restore the #23493 rename to the public
is_go2rtc_arbitrary_exec_allowed so create_config.py's dynamic-source
exec guard imports and runs (the merge otherwise left a broken import).
- Preserve the export image-path ".." traversal check inside
_sanitize_existing_image, applied to single/custom/batch exports.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(track): avoid numpy reductions on tiny box lists in position smoothing
update_position runs per tracked object per frame. While a position has
fewer than 10 samples it calls np.percentile four times, and average_boxes
(per stationary object per frame) calls np.mean four times - all on lists of
at most 10 ints, where numpy's per-call dispatch/validation overhead
dominates the actual work.
Replace them with pure-Python equivalents:
- average_boxes: sum()/len() instead of np.mean (bit-identical output)
- interpolated_percentile(): linear-interpolated percentile matching
numpy.percentile (including its lerp branch at frac>=0.5) for the small
lists used here, in place of np.percentile
Measured in the release image (numpy 1.26.4) on a 10-element list:
np.percentile 18735 ns -> 191 ns/call (98x); np.mean-based average_boxes
7480 ns -> 591 ns (12.7x); ~74 us saved per object-frame in update_position.
A live py-spy --gil profile of a camera process_frames worker showed
np.percentile (update_position) and np.mean (average_boxes) among the top
Frigate-owned on-CPU frames.
Output is unchanged: added tests assert both helpers are bit-identical to
numpy over randomized small inputs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Drop interpolated_percentile, keep only average_boxes
Per review: reimplementing np.percentile hurts readability and risks
divergence from numpy (e.g. numpy 2.x). Revert update_position to
np.percentile and remove the helper; keep only the average_boxes change
(sum()/len() instead of np.mean), which stays bit-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two small per-object-per-frame improvements in the tracker hot path
(match_and_update), both bit-identical:
- get_stationary_threshold returned a freshly constructed StationaryThresholds
(a dataclass plus a list) on every call for any label not in the three
known lists - i.e. for common labels like person/dog. The default thresholds
are constant and never mutated, so return a shared module-level singleton,
as the other three cases already do.
- untracked_object_boxes membership used `box not in [list of boxes]` (O(n));
build a set of box tuples for O(1) membership. Boxes are hashable as tuples
and output is unchanged.
get_stationary_threshold appeared in a live py-spy --gil profile of a camera
process_frames worker. Adds tests for the threshold lookups.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
video/detect.py runs these for every frame:
- get_cluster_candidates: used_boxes was a list with `in` membership tests
inside the nested loop (O(n) per check). It is only ever membership-tested,
so switching it to a set (O(1)) leaves output unchanged.
- get_consolidated_object_detections: area(current_box) was recomputed on
every inner-loop iteration though it is loop-invariant; hoist it to one
call per outer detection.
Both are bit-identical (verified against the previous implementations over
randomized inputs). Measured in the release image, get_cluster_candidates on
a frame of 30 detection boxes: 59.2 us -> 42.1 us (1.4x); the gain scales
with the number of boxes per frame.
Adds a partition-invariant test (every box index lands in exactly one
cluster).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A record-enabled camera whose record stream produces no cache segments
never appears in grouped_recordings, so the per-camera prune in
RecordingMaintainer.move_files() never runs for it. Its
object_recordings_info and audio_recordings_info buffers then grow
without bound until the recording process is OOM-killed (discussion
#23451).
Run a prune every move_files() cycle for cameras absent from
grouped_recordings, dropping entries older than the longest a segment
could still wait in cache before being matched
(MAX_SEGMENTS_IN_CACHE * MAX_SEGMENT_DURATION * 2). Cameras present in
grouped_recordings are left untouched and keep their existing prune.
Add a regression test asserting that an absent camera's stale entries
are dropped (recent ones kept) while a present camera's entries are
left intact.
Co-authored-by: John Pescatore <johnpescatore@claude.internal.johnpescatore.com>
* perf(util): use monotonic clock and bounded deque in EventsPerSecond
EventsPerSecond is updated on every captured frame, every detection and
every processed frame across all cameras and detectors. The previous
implementation derived timestamps from datetime.now().timestamp() (wall
clock), so an NTP or manual clock adjustment could skew the rolling-window
expiry; it also stored timestamps in a list and expired them with
del self._timestamps[0] (O(n) per removal) plus a periodic slice-copy to
cap growth.
Switch to time.monotonic() for the interval math (correct by construction
and immune to wall-clock jumps) and a collections.deque(maxlen=...) so
expiry is O(1) (popleft) and retention is bounded automatically. This
mirrors the deque-based expiry already used in video/ffmpeg.py and
watchdog.py. Observable output is unchanged.
Adds frigate/test/test_builtin.py covering rate calculation, window
expiry and the memory bound.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: drop test_timestamps_are_memory_bounded
It only asserted that deque(maxlen=) caps length, which is stdlib behavior
rather than something this change needs to verify.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* display zone names consistently using friendly_name or raw id without transformation
* enforce camera-level access on go2rtc live stream websocket endpoints
* republish MQTT switch states when a profile is activated or deactivated
* fix object mask default name when created from Explore tracking details
* tweak annotation offset max in UI
* optimize recordings/unavailable gap detection and drop empty motion activity buckets
* add tests
* refactor motion search
* cleanup dead code and tests
* tweaks
* fix multi-day seeking
* start playback a few seconds before the change so the motion is in view
* add prop to disable id field
* disable id field when editing profile mask/zone
also, disable if the zone name already exists in required_zones or the base config is being edited and the id already exists on a profile
* add backend validation to reject profile-omly masks/zones
* add tests
* update docs
* tweak
* unlink shm frames when camera is removed
* drop stale shm cache refs when cached segment is too small for requested shape
* skip new-object frame cache write when current_frame is unavailable
* add tests
* use setdefault when adding a new camera
Multiple subscribers in the same process each unpickle the ZMQ payload independently and would otherwise write divergent Python objects to the shared cameras dict — leaving long-lived references (e.g. CameraState.camera_config) pointing at a copy that subsequent in-place mutations like apply_section_update can never reach. setdefault collapses everyone onto the first writer's object so attribute mutations propagate to every consumer in this process.
* rebuild ffmpeg commands on detect update
Rebuild the cached ffmpeg cmd so the next process spawn picks up new resolution/fps. Running cameras keep their existing cmd (ffmpeg_cmds is only read at process startup); replay cameras are recycled by CameraMaintainer to pick up the rebuilt cmd
* drop stale shm cache refs when cached segment size doesn't match requested shape
The cached SharedMemoryFrameManager reference can point at a segment whose
size no longer matches the requested shape — the segment was unlinked and
recreated at a different size in a camera add/remove cycle. This catches
both a resolution increase (cached too small) and a decrease (cached too
large, pointing at an orphaned inode whose stale bytes would otherwise be
misinterpreted at the new shape, producing distorted/miscolored YUV frames).
After reopening, if the OS-level segment still doesn't match the requested
shape we're in a transient mid-recreate state — either the maintainer
hasn't allocated the new segment yet (size too small) or we opened a
pre-recycle segment (size too big). Either way, skip the frame and don't
cache the mismatched ref.
* recycle replay camera on detect update
* discard tracked-object state when detect resolution changes mid-session
When detect resolution changes mid-session every tracked object we hold
was localized against the old pixel grid. Their boxes no longer
correspond to anything in the new frame, and the `end` callback that
fires when their IDs disappear from the new detect process's detections
publishes those stale boxes to consumers (LPR, snapshot crop) that slice
the new frame and crash on empty arrays. Drop the tracked-object state
on a shape change so no stale boxes ever cross the CameraState boundary.
Belt-and-suspenders: also drop any incoming batch whose boxes exceed the
current detect resolution. These are in-flight queue entries from the
pre-recycle detect process that beat the new detect process to the
queue; processing them would re-introduce stale-resolution tracked
objects we just dropped above. The per-camera detect process clamps
legitimate boxes to detect.width-1 / detect.height-1, so any coord
beyond that is unambiguously stale.
* rebuild motion and object filter masks on detect resolution change
Apply the detect update first so frame_shape reflects the new resolution
before we rebuild dependents.
Motion's rasterized_mask is sized to frame_shape at construction. When
detect resolution changes we must rebuild RuntimeMotionConfig so the
mask matches the new frame size; otherwise consumers like the LPR
processor and motion detector hit a shape mismatch when they index
frames with the stale mask.
Same story for per-object filter masks — rebuild RuntimeFilterConfig at
the new frame_shape so the merged global+per-object masks they hold
match what they'll be indexed against.
* republish motion and objects on in-memory detect resize
A detect resolution change also invalidates the rasterized masks on
motion and per-object filters. apply_section_update has rebuilt them at
the new frame_shape; publish them too so other processes replace their
old values.
* add test
* frontend
* add refresh topic for camera maintainer recycle action
The maintainer's recycle branch is doing an action (recycle the camera)
in response to a section-level signal. Introduce a
CameraConfigUpdateEnum.refresh case as an explicit action signal — the
maintainer subscribes to refresh instead of detect, parallel with add
and remove. Publishers fire refresh alongside detect when a recycle is
needed; section-level subscribers keep their existing topic.
Since no main-process subscriber listens for detect anymore, the
refresh handler calls recreate_ffmpeg_cmds() explicitly so the shared
CameraConfig's ffmpeg_cmds is rebuilt before the new subprocesses
spawn.
* factor stale-resolution state drop into a CameraState method
* ensure motion masks from source camera are copied to replay
* stop polling debug_replay/status after live_ready
* use vod for constructing replay clips
* render orphaned filter entries as collapsibles instead of the Key/Value editor
* Symlink for various AI files
* change replay confg dialog to platform aware sheet
* change agents title
* fix test
* tweak collapsible
* remove camera ui section in settings
no point to having it anymore with profiles and camera management settings
* fix admin response cache leak to non-admin users via nginx proxy_cache
* add model fetcher endpoint for genai config ui
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
* filter outbound ws broadcasts by per-recipient camera access
* fan out config updates to comms
* tests
* mypy
* allow viewers to use jobstate
* update agent instructions
* remove vitest
* preserve user-set min_score on attribute filters instead of bumping any 0.5 value
use model_fields_set to distinguish "user explicitly set min_score" from "Pydantic applied the generic FilterConfig default of 0.5"
* add config test for attributes
* fix attributes frontend type
* add expanded hidden field context
* extend schema modification
* special case for attributes
* i18n for attributes
* handle dedicated lpr mode
* strip unrendered FilterConfig fields from attribute filter form data to fix validation errors
* start audio transcription post processor when enabled on any camera
* Fetch embed key whenever an error occurs in case the llama server was restarted
* mypy
* add tooltips for colored dots in settings menu
* add ability to reorder cameras from management pane
* add ability to reorder birdseye
* add reordering save text to camera management view
* Include NPU in latency performance hint
* Implement turbo for NPU on object detection
* hide order fields
* drop auto-derived field paths from camera value when unset globally
* use correct field type for export hwaccel args
* add debug replay to detail actions menu
* clarify debug replay in docs
* guard get_current_frame_time against missing camera state
* Implement debug reply from export
* Refactor debug replay to use sources for dynamic playback
* Mypy
* fix debug export replay source timestamp handling
* skip replay cameras in stats immediately
* broadcast debug replay state over ws and buffer pre-OPEN sends
- push debug replay session state over the job_state ws topic so the status bar reacts instantly to start/stop without polling
- fix child-effect-before-parent-effect race in WsProvider that silently dropped initial snapshot requests on cold load
* fix debug replay test hang
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
* use stable empty object reference for swr metadata default
* version bump
* Refactor get_min_region_size for dimension normalization
Refactor get_min_region_size to normalize dimensions for smaller models and ensure minimum region size is 320 for larger models.
* reject restricted go2rtc stream sources when added via api
* add env var check function
* fix typing
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
* add embedded mode to BaseSection so parents can host the save action
* add optional action slot to current Frigate+ model summary
* add w-full to action slot flex wrapper for explicit width contract
* i18n
* merged detectors and model settings view
* fix document title
* Embed detector form in merged settings view
* add detection model card with tabs and custom model embed
* add Frigate+ model selector with filter popover to merged page
* Add mismatch banner and gate save on detector and model compatibility
* Wire atomic save, restart toast, and undo on detectors and model page
* Clear child pending data on undo
* route merged detectors and model view in settings
* trim Frigate+ page to account-only and remove old detection model view
* basic e2e
* Fix unsaved-changes guard, custom path leak, and post-failure cache resync
* Rename to Detectors and model, float Modified badge, use ConfigMessageBanner for mismatch
* Hide Plus/Custom tabs when Frigate+ is not enabled
* Detect active Plus model via model.plus.id instead of path prefix
* Sync state back to snapshot when child form un-modifies and remount on undo
* Always require restart on save since model changes also need one
* Wrap Frigate+ model selector in SplitCardRow with label and description
* rename tab
* update docs
* sync top-level model with default detector's resolved model
when the user doesn't define a top-level `model:` block, `FrigateConfig.model` stayed at pydantic field defaults (320×320, /labelmap.txt) while the per-detector model picked up `DEFAULT_MODEL` for openvino on cpu (300×300, coco_91cl_bkgr.txt introduced in #23127), causing `RemoteObjectDetector` to fail with "buffer is too small for requested array" because the SHM was sized from the per-detector model but mapped using the top-level one. After the detector loop, copy the first detector's resolved model up to `self.model` so both sides agree on dimensions and labelmap
* revert to cpu detector by default
use openvino cpu for new configs only
* add defaults
* sync filter entries with track and listen labels
- Auto-populate `audio.filters` from `audio.listen` instead of the full audio labelmap, matching how `objects.filters` is keyed by `track` (no longer need to populate the full audio labelmap, which was added in #22630)
- Synthesize the matching filter entries in the settings form on load so each track/listen label shows its collapsible after a profile is selected, since the backend's auto-populate only runs at config init
* translate main label for lifecycle description with attribute
* reject restricted go2rtc stream sources when added via api
* add env var check function
* hide camera overrides badge from system sections
* show empty card on camera metrics page when no cameras are defined
* fix enabled camera state switch after adding via wizard
Cameras added mid-session have no WS state until the dispatcher publishes camera_activity (which only happens on a fresh onConnect). Fall back to the config's enabled value so the switch reflects reality immediately after the wizard closes.
* guard camera enabled access
console would throw errors after adding via camera wizard
* fix useOptimisticState dropping debounced setState under StrictMode
* use openvino on cpu as default model
- faster than tflite on cpu
- add to default generated config
* use an enum for model_size
the frontend will then render this as a select dropdown because of the changes in the json schema
* i18n
* sync object filter entries with tracked labels in camera config form
Filter sub-collapsibles in the camera Objects section are driven by `filters` dict keys, but profile merges and live track-switch edits don't add matching entries, so newly tracked labels (like from a profile override) had no collapsible. Synthesize default filter entries from `track` in the form data so every tracked label renders a collapsible; baseline data also gets the synthesized entries, so save payloads are unchanged.
* revalidate raw paths cache after config save so CameraPathWidget shows fresh credentials
* fix test
* restore masked ffmpeg credentials when persisting camera config
* formatting
* rebuild ffmpeg commands when enabling recording for the first time
Toggling record.enabled from the config UI updated the in-memory config but left ffmpeg running with its original command, so the record output args were never wired in and nothing landed in the cache for the maintainer to move. The record config update now rebuilds ffmpeg_cmds when enabled_in_config transitions, and the camera watchdog restarts ffmpeg on a false to true transition so the record output gets wired in. MQTT toggles, which only flip record.enabled at runtime, are unaffected and continue to work via the maintainer's drop/keep gate.
* keep record toggle switch in single camera view disabled until enabled in config
* fix override detection for sections unset in the global config
Override badges and the blue dot now compare against schema defaults for sections like motion that the API serializes as null when omitted from the global YAML, instead of treating any populated camera config as an override
* add support for config-aware patterns in section hiddenFields
Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable
* siimplify object filters handling
live updating was getting very messy. users will just need to save once they enable a new object in order to see filters for that object
* tweaks
* update docs for new detector default
* make genai provider required and add special case for UI
prevent validation errors from appearing on initial creation of genai provider by setting the first option in the select dropdown as default
* Rewrite intel GPU stats to use file descriptors instead of intel_gpu_top, leading to significantly better API for interaction and more accurate results
* Update tests
* Update docs
* Adjust approach
* Update strings
* use ReplayState enum
* extract shared ffmpeg progress helper
* make start call non-blocking with worker thread
* expose replay state on status endpoint and return 202 from start
* cancel in-flight ffmpeg when stop is called during preparation
* add replay i18n strings for preparing and error states
* show status in replay UI
* navigate immediately on 202 from debug replay menus and dialog
* remove unused
* simplify to use Job infrastructure
* tests
* cleanup and tweaks
* fetch schema
* update api spec
* formatting
* fix e2e test
* mypy
* clean up
* formatting
* fix
* fix test
* don't try to show camera image until status reports ready
* simplify loading logic
* fix race in latest_frame on debug replay shutdown
* remove toast when successfully stopping
it gets hidden almost immediately
* lpr fixes
- remove duplicate code
- fix min_area check for non frigate+ code path
- move log outside of non frigate+ code path
* only show chat link when a genai provider is configured with the chat role
* respect ui.timezone when generating fallback export names
* reapply radix pointer events fix to call sites that use navigate()
* formatting
* fall back to prior preview frame for short export thumbnails
* fix typing
* fix e2e test for chat navigation
* batch annotation offset to seek atomically and throttle slider drag
* add debug replay loading toast for explore actions
* Improve handling of webpush missing shortSummary
---------
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>