Skip to content

feat: per-nest snip time-lapse on the dashboard (#166 phase 3, feature 1)#190

Merged
cofade merged 3 commits into
mainfrom
claude/next-issue-prioritization-fcdtv2
Jun 26, 2026
Merged

feat: per-nest snip time-lapse on the dashboard (#166 phase 3, feature 1)#190
cofade merged 3 commits into
mainfrom
claude/next-issue-prioritization-fcdtv2

Conversation

@cofade

@cofade cofade commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

What & why

Phase 3, feature 1 of the #154 engagement story, building directly on the just-merged #165 hole-detection snips. Tapping a per-nest snip in the module panel opens a modal that scrubs that same hole across captures with a slider — watching it go empty → sealed over days. Gives returning visitors a reason to come back, using imagery that's already public (the crop is the privacy mechanism).

This addresses feature 1 of #166 only; features 2 ("recently sealed" feed) and 3 (email digest) remain open. It intentionally does not auto-close #166.

The key insight: no schema change

#165 already retains the full snip history — nest_detections is append-only and snip JPEGs are stored per-capture (capture-basename-prefixed filenames never overwrite). So this is a read endpoint + a modal UI, plus demo seed data so the feature is visible in dev and testable in CI.

Changes by layer

Layer Change
contracts NestSnipTimelineResponse (reuses NestSnip)
duckdb-service GET /detections/timeline — all captures for one (module, beeType, nestIndex), deduped by source filename, oldest-first. Extracted shared _row_to_dict.
backend GET /api/modules/:id/snips/:beeType/:nestIndex/timeline proxy; validates beeType enum + positive-int nestIndex before hitting upstream. Refactored the existing /snips validation+mapping into shared isValidDetection/toNestSnips (behaviour-preserving).
homepage api.getSnipTimeline(), new SnipTimelapseModal (native range slider, opens on newest frame, Escape/backdrop/close per ImageLightbox), clickable NestSnipGrid cells, EN+DE i18n.
demo seed 5 generated demo crops (empty→sealed) in image-service/demo_snips/ + matching nest_detections seed for Garten 12's leafcutter nest 1; image-service copies them into the shared volume on boot, gated on SEED_DATA. Wired into dev + UI-test compose; explicit SEED_DATA=false in prod compose.
docs api-reference, api-contracts, building-block (hole-detection-model), runtime-view, docker-compose.

Demo-data note

Real uploads never run in dev/CI, so nest_detections is seeded there. A cross-service contract test pins that every seeded snip_filename has a matching bundled JPEG (and no orphans), so the two sides can't silently drift.

Known limitation (documented in 3 places)

The time-lapse groups by positional (beeType, nestIndex), not a durable physical-tube identity — the detector's per-row hole count can drift ±1 frame-to-frame. Fine for this visualization; a position-tracking pass is the follow-up before this identity underpins anything quantitative.

Testing

  • duckdb-service: 233 (incl. timeline route + seed cross-check) ✅
  • backend: 218 (incl. timeline proxy: enum/format 400s, 502, mapping) ✅
  • homepage: 204 (incl. modal scrub/single-frame/empty/error + grid click) ✅
  • image-service: 97 (incl. demo-snip copy: gated, idempotent, jpg-only) ✅
  • make check-citations clean; prettier + ruff applied.
  • New Playwright spec tests/ui/tests/snip-timelapse.spec.ts asserts the scrubber swaps the real rendered crop (naturalWidth > 0) — the layer jsdom can't cover. Not run here (no Docker in this environment); runs in CI via make test-ui. Seed verified end-to-end via a smoke test against the real route (5 ordered frames).

Reviewed

Ran through the senior-reviewer gate: mergeable, no P0/P1. Its two actionable P2s (cross-service filename sync test; explicit prod SEED_DATA=false) are addressed in the second commit.

🤖 Generated with Claude Code


Generated by Claude Code

claude and others added 3 commits June 26, 2026 19:08
Phase 3 feature 1 of the #154 engagement story, building on the #165
hole-detection snips. Tapping a snip in NestSnipGrid opens SnipTimelapseModal,
which scrubs that one hole across captures with a slider (empty -> sealed).

nest_detections is append-only and snips are stored per-capture, so the history
already existed: this is a read endpoint + modal, no schema change.

- contracts: NestSnipTimelineResponse
- duckdb-service: GET /detections/timeline (all captures for one nest,
  dedup-by-filename, oldest-first) + tests
- backend: GET /api/modules/:id/snips/:beeType/:nestIndex/timeline proxy;
  shared isValidDetection/toNestSnips helpers + tests
- homepage: api.getSnipTimeline, SnipTimelapseModal, clickable grid cells,
  EN+DE i18n; modal + grid tests
- demo seed: generated demo snips + nest_detections seed for Garten 12,
  idempotent SEED_DATA copy in image-service; wired into both compose files
- Playwright spec asserting the scrubber swaps the real rendered crop
- docs: api-reference, api-contracts, building-block, runtime-view, compose

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkuwkjhDAm9W5zi97LYGYV
- duckdb-service: cross-service contract test pinning that every seeded
  nest_detections snip_filename has a matching JPEG in image-service/demo_snips
  (and no orphan demo assets) — the one drift that was invisible until a user
  taps a snip. Fixture purges only db.* modules so it can't disturb the
  log_ring singleton test_logs holds.
- docker-compose.prod.yml: explicit SEED_DATA=false on image-service so the
  service that does the demo-snip copy can't ship demo crops, and the deploy
  docs no longer imply a false that wasn't there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkuwkjhDAm9W5zi97LYGYV
Reshape the #166 phase-3 time-lapse per user feedback: instead of tapping a
single hole to scrub it in a modal, the NestSnipGrid now has one global slider
beneath it that scrubs the whole block across captures at once — dragging it
swaps every hole to the chosen capture's crops and shows that capture's
date+time. The full-module "Latest captures" gallery was removed from the panel.

- duckdb: replace per-nest `GET /detections/timeline` with module-level
  `GET /detections/history` (every nest of every capture, oldest first, deduped
  per (filename, bee_type, nest_index)). detected_at is insert-time — caveat
  documented in the route docstring.
- backend: `GET /api/modules/:id/snips/history` proxy (shared toNestSnips).
- contracts: NestSnipTimelineResponse -> NestSnipHistoryResponse.
- homepage: api.getSnipHistory; NestSnipGrid groups history by capture and
  opens on the newest; SnipTimelapseModal + dead getSnips removed.
- Remove LatestCaptures (component, unit test, e2e spec) and its 6 orphaned
  i18n keys; VITE_ENABLE_DASHBOARD_IMAGES now gates the snip grid. Docs/flags/
  glossary/ch11 updated to match.
- Tests rewritten: backend snips-route, duckdb test_detections, homepage
  NestSnipGrid, Playwright snip-timelapse; module-nest-snips round-trip points
  at /snips/history.

Verified end-to-end on a running stack: /snips/history returns 229 rows across
11 captures for a module fed 10 real ESP captures. Builds on #165/#154.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cofade cofade marked this pull request as ready for review June 26, 2026 22:06
@cofade cofade merged commit aac9c78 into main Jun 26, 2026
9 checks passed
@cofade cofade deleted the claude/next-issue-prioritization-fcdtv2 branch June 26, 2026 22:06
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.

Engagement features on top of nest snips (#154 phase 3)

2 participants