Skip to content

v2.4#23

Merged
RobXYZ merged 20 commits into
mainfrom
dev
Jun 18, 2026
Merged

v2.4#23
RobXYZ merged 20 commits into
mainfrom
dev

Conversation

@RobXYZ

@RobXYZ RobXYZ commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Added

Camera Control

A new Camera tab reads the dashcam's current settings and lets you adjust the safely-changeable ones over Wi-Fi — on/off toggles, drop-downs, and a recording indicator — each validated against the camera's own option list and read back to confirm it stuck. Destructive commands (format SD, factory reset, firmware update, delete, reboot) are hard-blocked and never shown, and the few record-only settings auto-pause then resume recording. Settings for 29 Viofo models are mapped from the official app's command database; the A329S is validated on hardware. Contributed by @droomurray (#21).

Three-Camera Support (Telephoto / Interior)

Telephoto (T) and interior/cabin (I) clips are now first-class alongside front and rear. They sync, index, and pair into the same capture group, so a three-camera day shows a third thumbnail in the archive and a third track on the timeline. New exports cover them: Join Tele / Join Interior, plus picture-in-picture with the third camera fullscreen and the front camera as the inset (the front clip stays the audio source, so the microphone track is preserved). The clip viewer's camera key cycles through every camera present at a timestamp. Two-camera setups are visually unchanged. Contributed by @jusii (#18).

Background Thumbnails & Filmstrips

Thumbnails and timeline filmstrips are now produced by a background worker as clips download — and existing clips are backfilled — so the archive and timeline populate as soon as footage arrives instead of after a sync cycle finishes. A new Thumbnails settings section controls it: thumbnail pre-generation is on by default, while the heavier filmstrip pre-generation is opt-in and otherwise falls back to generating on demand the first time a clip is viewed.

Per-Segment Picture-in-Picture in the Editor

The timeline editor's switched-camera cut can now carry a picture-in-picture inset on a per-segment basis. With a segment selected, press the PiP button (or P) to cycle the inset through your other cameras — it skips the segment's own camera — and a green placeholder shows where it will sit. The choice is remembered per segment and composited into the export, in the corner set by the global picture-in-picture position setting. A segment whose chosen camera has no overlapping footage simply exports without the inset.

Skip Downloads

You can now skip clips you don't want to sync. Select them in the download queue and choose Skip from the Actions menu; skipped clips get their own badge and are never downloaded. Clear skip returns them to the queue with a fresh set of retry attempts. Queue selection now spans pending, failed, and skipped clips, so one Actions menu — Download next / Skip / Clear skip / Retry failed — drives the whole list.

Changed

  • On/off controls across the app are now toggle switches, with one tooltip style used app-wide that works on hover, keyboard focus, and tap.
  • The sync pause state is remembered across restarts instead of resetting to running.
  • The download queue's per-action buttons are now a single Actions menu with Apply; selection-based Retry failed replaces the old retry-all button, while Download recent hours next is unchanged.
  • Archive thumbnails animate on hover, scrubbing the clip's filmstrip — the same preview the Export Jobs list already offered. They fall back to the static thumbnail when no filmstrip is available, and respect reduced-motion.

Fixed

  • Locking a clip on the dashcam between sync cycles moves it into the camera's /RO folder; the download queue now refreshes the clip's source path when the camera re-reports it there, instead of exhausting its retry budget against the stale path and never syncing the clip. The dashcam-delete lock guard benefits too, since it keys off the same refreshed /RO path. Contributed by @jusii (Refresh stale source_dir on already-queued rows #17).
  • The Logs view now shows the date alongside the time and stays readable on a phone — long lines wrap rather than being clipped off-screen.

droomurray and others added 20 commits June 16, 2026 09:41
Adds a Camera tab and /api/camera/* endpoints to read the dashcam's current
settings and safely change them over the Novatek netapp HTTP interface
(http://<cam>/?custom=1&cmd=<id>&par=<value>).

- viofosync_lib/_control.py: settings read (cmd=3014) decoded against a
  derived per-model command map; validated writes with read-back verify; a
  hard denylist of destructive commands (format / factory reset / firmware
  update / delete / reboot / restart-wifi / SSD format+delete) refused before
  any request is built; auto stop/resume recording for record-gated settings;
  gentle single-request transport so the camera's single-threaded daemon is
  never overrun.
- web/routers/control.py + web/app.py: session/CSRF-protected REST API.
- web/static: Camera tab UI — toggles for on/off settings, drop-downs for the
  rest, a recording pill, and read-only rows (with reasons) for settings the
  camera can't change now (resolution over Wi-Fi, exposure, lenses not
  attached). Lens-dependent settings re-enable automatically when the lens is
  connected (detected via the live sensor count).
- viofosync_lib/data/command_map.json + scripts/build_command_map.py: a
  derived command/option map. Only the factual API data (command ids, keys,
  option enumerations) is reformatted from the VIOFO app's device-cmd-manager
  .db asset; the .db itself is not redistributed.
- tests/test_control.py: offline unit tests (denylist, validation, support
  classification, read-back verify, record-gated retry).

Validated end-to-end against an A329S; other models are mapped from the app
data but untested.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Shows the Camera control UI on an A329S — toggles, the recording pill,
and the read-only rows (resolution/exposure) the camera won't change
over Wi-Fi.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…lity

The log view rendered time-only timestamps, placed the exception
expand caret at the far left of the row, and truncated long lines with
no way to read them on a narrow screen.

- Timestamps now show date + time (toLocaleDateString + toLocaleTimeString).
- The expand caret moves into its own column immediately right of the
  date/time; the column is always reserved so levels stay aligned
  whether or not a row carries an exception.
- The log list scrolls horizontally (overflow:auto) and rows grow to
  content width, so long lines are reachable everywhere; on screens
  <=720px each entry wraps instead, with the message full-width beneath
  the meta line.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The sync pause state lived only in an in-memory threading.Event, so
pausing sync and restarting the app (or container) silently resumed
it. Persist it in the kv table and restore on worker construction, so
the worker comes back in the state the user last left it.
ENABLE_SCHEDULED_SYNC still governs whether the schedule runs at all.

- Add kv_get/kv_set helpers on Database (the kv table existed but was
  unused).
- SyncWorker restores the flag in __init__ (best-effort: no store =
  nothing to restore) and pause()/resume() persist it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Satisfies the repo's `ruff check` CI step (I001).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add the derive_queue table (one row per clip) and its pure-DB state machine (enqueue/claim/done/transient-failure/reconcile/enqueue-missing) — the durable, restart-safe backbone for off-loop per-clip derivation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
derive_one() runs the idempotent per-clip chain (duration via ffprobe, GPS, thumbnail, filmstrip), gated by the eager-derive settings and classifying failures into requeue/gone/done. The DeriveWorker async task drains derive_queue one clip at a time, sharing the generators' per-loop semaphores so background backfill yields to on-demand requests. Adds a filmstrip failed-recently skip marker; removes the superseded duration sweep.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Index + enqueue each clip as it downloads or imports (so it appears in the archive instantly) and on every scan via enqueue_missing. Start the DeriveWorker in the app lifespan with a one-time boot backfill of the existing archive. Remove the old boot/post-cycle/rescan thumb+duration sweeps (the worker subsumes them) and drop a clip's derive_queue row when retention prunes it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add DERIVE_THUMBS_EAGER (default on) and DERIVE_FILMSTRIPS_EAGER (default off — filmstrips are ~10-25x the ffmpeg work, so opt-in), surfaced in a new Settings > Thumbnails section and projected in the settings GET response (with a regression test pinning that every editable key is projected). Route the worker's clip_derived event through the debounced archive refresh.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…weaks

Post-merge tweaks to the camera-control feature and shared UI:

- Camera tab: cache the last-known info/settings and render them instantly
  with controls disabled, refreshing in the background; controls re-enable
  only once the camera answers (a brief outage keeps the last-known view
  instead of blanking). Refresh button refreshes in place.
- Camera settings rows are now label | status | control: status/marker
  right after the label, controls (toggle/select/value) always flush-right,
  uniform row heights via label padding. Not-adjustable settings show a
  compact ⓘ marker with the reason in a tooltip.
- App-wide `.switch` toggle for on/off checkboxes (Settings + camera rows;
  selection checkboxes stay native), with the settings-pane sizing fixed so
  toggles aren't squashed.
- Reusable `data-tip` tooltip: body-level, fixed-positioned, flips/clamps
  near edges, shown on hover, focus, and tap/click — replacing native
  `title`, which never fired on click/touch.
- Reworded the Thumbnails settings hint.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Timeline export segments gain an optional `pip` channel. The piece builder
carries each piece's absolute start + pip channel; a new overlay-filter
builder and partner-clip resolver let the runner composite the PiP camera
as a corner inset (per the global PIP_POSITION setting), falling back to a
plain scale when no partner footage covers the window. Audio is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a PiP button (and "P" shortcut) that cycles a per-segment inset through
the enabled cameras, skipping the segment's own. A green placeholder
rectangle tracks the letterboxed preview video in the corner set by the
global PIP_POSITION setting; the choice is scoped per segment, persists to
localStorage, and is sent per segment to the export API.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a `skipped` queue state (free-text column, no migration). `skip` moves
pending/failed rows to skipped; `unskip` returns them to pending with a fresh
attempt budget. next_pending only ever picks pending, so skipped files are
never downloaded; reconcile leaves skipped rows alone. The day summary reports
a skipped_count, and CSRF-guarded /api/queue/skip and /api/queue/unskip routes
(mirroring /queue/retry) drive it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Queue selection is broadened from pending-only to pending/failed/skipped, and
skipped rows show a distinct badge + counts. The per-action buttons are
replaced by an Actions dropdown + Apply (Download next / Skip / Clear skip /
Retry failed) that posts the selection to the matching endpoint; the backend
filters by state. "Download recent hours next" is kept; the retry-all button
is removed in favour of selection-based retry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…tive

Show the clip filmstrip animated on hover in the archive view, matching the
export-job thumbnail, and extract the scrub into a reusable, frame-count-
agnostic primitive shared by both call sites.

- Shared .film-scrub CSS class + film-scrub-anim keyframe: background-size is
  driven by a --frames custom property; the steps(N) timing is set inline from
  JS, since CSS steps() can't read a variable.
- Shared JS helpers applyFilmstripScrub() / wireLazyFilmstripScrub() in app.js.
- Export thumb migrated onto the primitive (fixed 10 tiles; behaviour-preserving
  — frames=10 reproduces the previous 1000% / steps(10) values exactly).
- Archive tiles gain a pointer-events:none overlay over the static thumbnail,
  lazily loading each clip's variable-length filmstrip on first hover so opening
  a day doesn't spawn ffmpeg for every tile. Reduced-motion keeps the static
  thumbnail; single-frame and un-renderable (204) clips stay static; the
  existing click-to-open handler is unaffected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
CI runs `ruff check .` and tests/** is linted; this file tripped I001
(an extra blank line after the import block). No behaviour change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…on-tap

- Camera settings rows now use a wrapping flex layout on small screens: the
  select drops to its own full-width line with even spacing while toggles and
  current values stay on the label's line, and empty status cells collapse.
- Stop mobile zoom: 16px touch form controls (kills the iOS focus auto-zoom)
  plus maximum-scale=1,user-scalable=no in the viewport meta.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
GPS stop boundaries land ~50m (STOP_RADIUS_M) inside the real drive, so
the pull-away clip at the start and the pull-in clip at the end fell
outside the raw journey window — the auto-journey was "missing the start"
and "cut short on arrival".

Pad each edge of the /timeline journey window outward, bounded by the
nearest parking-mode clip on that side (the genuine "car is parked"
boundary) and capped at MAX_JOURNEY_BUFFER_S (120s). The cap is the
backstop because the dashcam can be knocked out of parking mode by the
car's electrics waking, so an arbitrary parking->driving switch isn't
trusted — only a parking clip is a hard stop. The padded window is
clamped to actual clip coverage so it never leads into empty timeline.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@RobXYZ RobXYZ merged commit d0d0b60 into main Jun 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants