Present one movement across multiple cameras as a single journey #146

Closed
opened 2026-09-05 05:54:27 +00:00 by agent-pi · 3 comments
Collaborator

Today motion events are grouped per camera. A single real-world movement — a vehicle or person coming through the gate and travelling up one of the driveways to the carpark — shows up in the day view as two or three unrelated event tiles.

On this low-traffic, single-access site those per-camera events are almost always the same object moving through the property, and the day view fails to tell that story.

What I want:

  • A movement spanning several cameras appears as one event (a "journey") in the day view, with its per-camera segments in order.
  • A way to watch a journey's snapshots as one continuous sequence that hops between cameras as the movement progresses.
  • A vehicle that comes through the gate, loops the property, and leaves through the gate again appears as one journey when the hops are close enough in time.

Scope agreed during brainstorming: browsing only — no notifications or alerts for the foreseeable future. Timestamps only — no object classification and no camera topology config in v1 (the design keeps the door open for direction labels later).

Today motion events are grouped per camera. A single real-world movement — a vehicle or person coming through the gate and travelling up one of the driveways to the carpark — shows up in the day view as two or three unrelated event tiles. On this low-traffic, single-access site those per-camera events are almost always the same object moving through the property, and the day view fails to tell that story. What I want: - A movement spanning several cameras appears as **one** event (a "journey") in the day view, with its per-camera segments in order. - A way to watch a journey's snapshots as one continuous sequence that hops between cameras as the movement progresses. - A vehicle that comes through the gate, loops the property, and leaves through the gate again appears as one journey when the hops are close enough in time. Scope agreed during brainstorming: browsing only — no notifications or alerts for the foreseeable future. Timestamps only — no object classification and no camera topology config in v1 (the design keeps the door open for direction labels later).
Author
Collaborator

Findings from exploration

  • events.py already derives per-camera events at query time (group_events_per_camera, 60 s gap hardcoded as a function default); nothing is persisted.
  • Rows are aiosqlite.Row at runtime (db.row_factory) but the grouping functions are duck-typed; tests use MagicMock/dict fakes.
  • The scrubber is single-camera scoped (camera query param required, falls back to the alphabetically-first camera) — a cross-camera view is new surface.
  • /snapshots/{snapshot_id}/img already serves images by primary key — the journey viewer can reuse it directly.
  • The calendar's event_count is derived from per-camera events.
  • config.py has [server] / [[camera]] / [web] sections; no [events] section yet.

Spec (approved): .superpowers/specs/2026-09-05-cross-camera-journeys-design.md

Plan of attack

  1. events.py: add a Snapshot structural protocol (typing only; also re-annotate the two existing functions) and group_events_into_journeys — greedy chronological chaining of per-camera events with an inclusive per-hop join window, attaching to the most-recently-ended eligible journey. Plus small journey helpers (journey_start, journey_end, journey_anchor_id). TDD.
  2. config.py: new [events] section with journey_gap_seconds (default 180) — EventsConfig + _parse_events + AppConfig.events.
  3. web/routes/days.py + day.html: derive journeys in the day view; single-segment journeys render as today's tiles, multi-segment ones as journey cards with ordered segment chips linking to the viewer; calendar counts journeys.
  4. New web/routes/journeys.py + journey.html: stitched viewer at /days/{day}/journeys/{anchor_id} (anchor = first snapshot id of the journey's first segment), 404 on unknown/purged anchors, images via the existing by-id route.
  5. Docs: docs/config.md, docs/web-ui.md, README bullet, Release-Notes entry.

Everything derives at request time in cangjian; cangcang and the DB schema stay untouched.

**Findings from exploration** - `events.py` already derives per-camera events at query time (`group_events_per_camera`, 60 s gap hardcoded as a function default); nothing is persisted. - Rows are `aiosqlite.Row` at runtime (`db.row_factory`) but the grouping functions are duck-typed; tests use `MagicMock`/dict fakes. - The scrubber is single-camera scoped (camera query param required, falls back to the alphabetically-first camera) — a cross-camera view is new surface. - `/snapshots/{snapshot_id}/img` already serves images by primary key — the journey viewer can reuse it directly. - The calendar's `event_count` is derived from per-camera events. - `config.py` has `[server]` / `[[camera]]` / `[web]` sections; no `[events]` section yet. **Spec** (approved): `.superpowers/specs/2026-09-05-cross-camera-journeys-design.md` **Plan of attack** 1. `events.py`: add a `Snapshot` structural protocol (typing only; also re-annotate the two existing functions) and `group_events_into_journeys` — greedy chronological chaining of per-camera events with an inclusive per-hop join window, attaching to the most-recently-ended eligible journey. Plus small journey helpers (`journey_start`, `journey_end`, `journey_anchor_id`). TDD. 2. `config.py`: new `[events]` section with `journey_gap_seconds` (default 180) — `EventsConfig` + `_parse_events` + `AppConfig.events`. 3. `web/routes/days.py` + `day.html`: derive journeys in the day view; single-segment journeys render as today's tiles, multi-segment ones as journey cards with ordered segment chips linking to the viewer; calendar counts journeys. 4. New `web/routes/journeys.py` + `journey.html`: stitched viewer at `/days/{day}/journeys/{anchor_id}` (anchor = first snapshot id of the journey's first segment), 404 on unknown/purged anchors, images via the existing by-id route. 5. Docs: `docs/config.md`, `docs/web-ui.md`, README bullet, Release-Notes entry. Everything derives at request time in cangjian; cangcang and the DB schema stay untouched.
Author
Collaborator

Follow-up from prod-data testing (branch feat/issue-146-cross-camera-journeys, PR #147): the separate journey viewer diverged from the event viewer — different image sizing, plain-text prev/next links instead of the scrubber controls, no play button.

Decision: there is now ONE viewer. Journeys become a filter mode of the snapshot scrubber (GET /days/{day}/snapshots?journey=<anchor_id>); the separate /days/{day}/journeys/{anchor_id} route and journey.html are deleted. Parity with the event viewer is guaranteed by construction — same template, same JS (play/pause, speed, filmstrip, slider, keyboard), camera selector hidden in journey mode, per-frame camera label in the header (back-drive · 10:04:47 · 3 / 12), blue camera-change ticks on the scrub track alongside the orange motion markers.

Spec updated: .superpowers/specs/2026-09-05-cross-camera-journeys-design.md (Unified journey mode in the scrubber).

**Follow-up from prod-data testing** (branch `feat/issue-146-cross-camera-journeys`, PR #147): the separate journey viewer diverged from the event viewer — different image sizing, plain-text prev/next links instead of the scrubber controls, no play button. **Decision:** there is now ONE viewer. Journeys become a filter mode of the snapshot scrubber (`GET /days/{day}/snapshots?journey=<anchor_id>`); the separate `/days/{day}/journeys/{anchor_id}` route and `journey.html` are deleted. Parity with the event viewer is guaranteed by construction — same template, same JS (play/pause, speed, filmstrip, slider, keyboard), camera selector hidden in journey mode, per-frame camera label in the header (`back-drive · 10:04:47 · 3 / 12`), blue camera-change ticks on the scrub track alongside the orange motion markers. Spec updated: `.superpowers/specs/2026-09-05-cross-camera-journeys-design.md` (Unified journey mode in the scrubber).
Author
Collaborator

Closed via PR #147 (merged into main).

Closed via PR #147 (merged into `main`).
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marvin8/cang#146
No description provided.