Conversation
…clips Squashes three minor fixes: - Don't upload clips already in the archive but do import incomplete files - Mark already-on-disk clips done so sync stops re-downloading - Stop re-running ffmpeg on un-thumbable clips every sweep Co-Authored-By: Claude Opus 4.8 <[email protected]>
Opening a day (or the day route) re-parsed every GPX sidecar for that date on each request — ~19s for 536 files on a busy day, and the day view's "Loading…" blocks on it. The result only changes when the day's GPX files change, so cache the aggregated payload keyed by a signature of those files (path + mtime + size), persisted as a JSON sidecar under $RECORDINGS/.route_cache so it survives restarts. Labels are applied on read (not cached) so they stay current as the geocode cache fills. First open still pays the parse; every later open is near-instant until the day's GPX actually changes. Co-Authored-By: Claude Opus 4.8 <[email protected]>
os.access(path, W_OK) is unreliable on NFS — it checks cached owner/mode against the local UID and can report a genuinely writable export as non-writable, while the real write is accepted by the server's own permission mapping. This caused a false "recordings path not writable" error for users on NFS mounts where manual writes succeed. Co-Authored-By: Claude Opus 4.8 <[email protected]>
_connect_and_loop runs its workers in an asyncio.TaskGroup, which reports a task failure as an ExceptionGroup rather than the bare error. A routine broker disconnect (aiomqtt.MqttError, e.g. "Disconnected during message iteration") therefore escaped the `except aiomqtt.MqttError` clause, fell through to `except Exception`, was logged as a fatal "unexpected error" with a full traceback, and set the connection state to ERROR instead of RECONNECTING. Unwrap the ExceptionGroup: propagate cancellation untouched (so shutdown is never swallowed), classify a group of pure MqttErrors as a normal reconnect, and only flag genuinely unexpected errors as ERROR. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Timeline video editor (clip filmstrips, duration probing, scrubbing) plus a move to a Debian/jellyfin-ffmpeg base image to unlock Intel QuickSync, with VAAPI and software fallbacks for join, PiP and switched exports. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the 30s per-tab /api/archive/rescan poll with a clip_indexed WebSocket push, so the archive updates without every open browser tab re-triggering a scan (which was doubling scanner/duration logs). Co-Authored-By: Claude Opus 4.8 <[email protected]>
Play/pause, set start/end, switch camera/view, plus navigation and zoom. Keys are surfaced in button tooltips and a '?' cheat-sheet overlay (role=dialog, aria-modal, raised above the player modal). Co-Authored-By: Claude Opus 4.8 <[email protected]>
A per-job filmstrip sprite, generated eagerly when the job finishes and served from a cache-only endpoint (no request-time ffmpeg, which previously caused a CPU storm when the jobs table re-rendered on every progress tick). The table shows one frame and scrubs the strip on hover; deleting a job cleans up its cached sprite. Built on a timestamp-driven sprite core (generate_sprite_at) refactored out of the clip filmstrip code. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Lift --bg out of the near-black zone where the GPU dithered visible stripes on the export/login backgrounds; rename the export 'Clear' button to 'Clear selected clips' and the 'GPS maps' filter to 'GPS Journey Splits'; persist open archive day(s) across navigation so returning from the timeline doesn't collapse them and lose your place. Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Click a job thumbnail to play the export in the player modal. - Switched exports take one continuous front-camera audio track, muxed at the end, eliminating the audio jump at camera switches. - Silence benign libva init chatter in export logs (LIBVA_MESSAGING_LEVEL=1). - Show a shimmer placeholder while a job's filmstrip generates, swapping to the real strip when ready (export_preview_ready event + has_preview). - Add output Length and Size columns to the jobs table. - Label the switched export type 'Timeline' in the table. Co-Authored-By: Claude Opus 4.8 <[email protected]>
- XSS: add escHtml() and escape clip filenames, geocode place labels, and the kind-badge class at every innerHTML sink (the filename regex allows arbitrary characters in the camera segment, and Nominatim labels are external data). escapeExportText now delegates to escHtml so attribute contexts get quote escaping. - parse_moov: stop the moov child walk on any atom smaller than its own 8-byte header — a truncated power-loss clip used to spin a worker thread forever. - get_dashcam_filenames: pass socket_timeout to urlopen; a half-open dashcam connection used to wedge the sync worker indefinitely. - Event loop: move blocking I/O off the loop — listing reconcile (NAS walk + DB txn), ExportWorker._pop_next, MQTT state_fn/attrs_fn (disk_used walks the archive in quota mode), and the upload route's quota check + chunk writes. - ExportWorker.stop(): SIGCONT + kill the in-flight ffmpeg child (a paused encoder outlived the server) and await the cancelled task so cleanup runs. Also clears pre-existing ruff errors in tests (E702 semicolons, unsorted imports) so `ruff check .` passes again. Co-Authored-By: Claude Fable 5 <[email protected]>
Reliability and correctness batch, each item pinned by regression tests (34 new tests; suite now 613): - WebSocket /api/progress rejects cross-origin handshakes (4403) — the docstring claimed an Origin check that didn't exist. - queue_changed events from threadpool route handlers no longer vanish: Hub.bind_loop() in lifespan + schedule_broadcast falling back to the bound loop (MQTT interceptor mirrors it). - SyncWorker honours runtime ADDRESS / ENABLE_SCHEDULED_SYNC changes (start/stop via settings subscriber) and stop() bounds its waits with STOP_TIMEOUT instead of the dead wait_for branch. - Retention can no longer delete clips a queued/running export will read: exporter.export_protect_ids() threaded through sweep and make_room_for at every call site. - Thumbnails and sprite montages write to .part names renamed only on verified success — a killed ffmpeg no longer poisons the cache with a partial file served forever. ensure_thumb gained a 3-permit semaphore (a 100-clip day view used to spawn 100 ffmpegs). - Download size verification falls back to the listing size when the HEAD probe fails, so truncated transfers aren't archived as done. - Importer staging: recover_staging() re-ingests complete staged clips at boot and before folder ingest (a crash between the two same-volume renames used to cost the only copy — the next run deleted it); uploads stream to .part and are renamed only after size verification; cleanup keeps fresh .part files so a concurrent upload survives a folder ingest. - MQTT reconnect backoff resets after a stable connection; discovery topology state is owned by MqttService.__init__; timed-out ffprobe children are killed and reaped. - ENOSPC is a sticky disk_full sync error that stops the drain and refunds the attempt, instead of burning the whole queue's retry budget against a full disk. - export_preview.preview_path() is pure (no makedirs side effect) — also removes the MagicMock/ repo-root dir leak from tests. - Hub bounds each client send with SEND_TIMEOUT_S so one stalled tab can't starve every other client of events. - Frontend: import modal fetches gained 401-redirect / CSRF-retry semantics (ifetch); WS reconnect uses exponential backoff with jitter and re-fetches list views after reconnecting; archive Leaflet maps are .remove()d before re-render; loadDays uses the stale-response token pattern. Co-Authored-By: Claude Fable 5 <[email protected]>
Security hardening, resource hygiene, and concurrency-safety batch
(25 new tests; suite now 639):
- MQTT_PASSWORD is write-only over the API: GET returns a masking
sentinel, a PUT carrying the sentinel means "unchanged", and the
Test-connection endpoint substitutes the stored secret.
- The unauthenticated /api/setup/test-dashcam probe is confined to
LAN-resolving targets, so it can't be used as a TCP-connect prober
against arbitrary hosts during the setup window. The authenticated
settings probe stays unrestricted (remote-VPN dashcams).
- The import scan endpoint returns a skipped count, not the filenames
of whatever readable directory was scanned.
- The login rate-limiter sweeps stale per-IP buckets so the map can't
grow unbounded; documents the proxy-IP keying trade-off.
- Exports stage to {id}.mp4.part renamed only on verified success;
failures/cancels remove the partial and a startup sweep clears
pre-existing orphans, so .exports no longer accumulates unreferenced
files that count against the recordings quota.
- download_file takes max_attempts/socket_timeout as parameters
instead of download_file_with mutating module globals, so concurrent
downloads can't clobber each other.
- New web/services/tasks.spawn() holds a strong reference to detached
fire-and-forget tasks until done (and logs their exceptions) —
applied to the export/scanner/queue broadcasts, rescan sweeps, and
the app.py settings callbacks.
- Settings subscribe() returns an unsubscribe handle; the lifespan
detaches all callbacks on shutdown so the singleton provider stops
leaking callbacks that pin dead app objects.
- Retry backoff sleeps are interruptible: a pause/stop is honoured in
~100ms instead of after the full 5-50s ladder.
Co-Authored-By: Claude Fable 5 <[email protected]>
The _is_lan_host restriction on the unauthenticated /api/setup/test-dashcam probe is removed. It blocked legitimate remote dashcams — Tailscale uses CGNAT 100.64/10 (not is_private), and public DDNS to a port-forwarded camera resolves to a public IP — while providing little real security: during the setup window the unauthenticated setup form itself allows account takeover, which dwarfs a TCP-connect/port-scan oracle. The README already warns not to expose the container during setup. Kept: the import-scan filename-leak fix (skipped_count, not names), which is authenticated and has no such downside. Test renamed to test_import_scan_no_leak.py. Co-Authored-By: Claude Fable 5 <[email protected]>
…rt type to "timeline" Collapse the timeline editor's export section (Switched/Front/Rear buttons + "Export" label + inline status text) into a single primary "Export" button. Queue feedback now flows through a new app-wide toast() primitive — a success toast with a "View export jobs" action, an error toast on failure/empty — replacing the small aria-live status line. Retire the confusing export-type term "switched" -> "timeline" everywhere on the wire and in code: API validation regex + dispatch, DB type value, worker (enqueue_timeline/_run_timeline/_TIMELINE_PROTECT_MARGIN_S), EXPORT_TYPE_LABELS, and the test suite. The separate switch-point editing concept (+ Switch Camera, build_switch_pieces, .tl-switch*) is intentionally left unchanged. Co-Authored-By: Claude Opus 4.8 <[email protected]>
ffmpeg picks its output muxer from the filename extension. Staging to
{id}.mp4.part gave it a bare '.part' tail it can't infer, so the final
mux/encode failed with 'Unable to choose an output format' for every export
type (join, pip, timeline). Move the .part marker before the extension
({id}.part.mp4), matching the .part.jpg staging already used by
thumbs/filmstrip; _finish strips the infix on promotion. The orphan sweep
already covers both the new and legacy names.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The two POST /api/exports timeline tests passed locally (ffmpeg installed) but failed in CI: enqueue_timeline() calls ffmpeg_available() independently of the encoder check, raising 503 when ffmpeg is absent. Patch it to True like every other test in the file, making them deterministic and letting requires_segments reach the actual segment validation (400) instead of the ffmpeg short-circuit. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Added
Timeline Video Editor
A multi-camera timeline editor for the archive: scrub the front/rear channels on a shared playhead with per-clip filmstrips, set in/out trim points, and cut between cameras to export a single switched-angle video. Includes frame-accurate keyboard shortcuts. Join, picture-in-picture, and switched exports are now hardware-accelerated via Intel QuickSync where available, with VAAPI and software fallbacks.
Export Jobs
Changed
Fixed
A broad reliability and security hardening pass (full per-item detail in
CLAUDE.md):