feat: per-nest snip time-lapse on the dashboard (#166 phase 3, feature 1)#190
Merged
Conversation
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>
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.
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_detectionsis 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
NestSnipTimelineResponse(reusesNestSnip)GET /detections/timeline— all captures for one(module, beeType, nestIndex), deduped by source filename, oldest-first. Extracted shared_row_to_dict.GET /api/modules/:id/snips/:beeType/:nestIndex/timelineproxy; validatesbeeTypeenum + positive-intnestIndexbefore hitting upstream. Refactored the existing/snipsvalidation+mapping into sharedisValidDetection/toNestSnips(behaviour-preserving).api.getSnipTimeline(), newSnipTimelapseModal(native range slider, opens on newest frame, Escape/backdrop/close perImageLightbox), clickableNestSnipGridcells, EN+DE i18n.image-service/demo_snips/+ matchingnest_detectionsseed for Garten 12's leafcutter nest 1; image-service copies them into the shared volume on boot, gated onSEED_DATA. Wired into dev + UI-test compose; explicitSEED_DATA=falsein prod compose.Demo-data note
Real uploads never run in dev/CI, so
nest_detectionsis seeded there. A cross-service contract test pins that every seededsnip_filenamehas 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
make check-citationsclean; prettier + ruff applied.tests/ui/tests/snip-timelapse.spec.tsasserts 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 viamake test-ui. Seed verified end-to-end via a smoke test against the real route (5 ordered frames).Reviewed
Ran through the
senior-reviewergate: mergeable, no P0/P1. Its two actionable P2s (cross-service filename sync test; explicit prodSEED_DATA=false) are addressed in the second commit.🤖 Generated with Claude Code
Generated by Claude Code