From 04534d6f9896cc7d73720b752e9c3bd45b3f1e35 Mon Sep 17 00:00:00 2001 From: Domagoj Zecevic Date: Thu, 18 Jun 2026 15:39:58 +0200 Subject: [PATCH] feat(image): add image browser and thumbnails --- .ai/HANDOFF.md | 40 +++++ .ai/REVIEW.md | 43 +++++ .ai/TASKS.md | 2 +- README.md | 4 + go.mod | 1 + go.sum | 2 + internal/image/handler.go | 257 ++++++++++++++++++++++++++++ internal/image/handler_test.go | 175 +++++++++++++++++++ internal/image/thumb.go | 114 ++++++++++++ internal/web/router.go | 5 + internal/web/router_test.go | 39 ++++- internal/web/templates/images.html | 58 +++++++ internal/web/templates/templates.go | 1 + 13 files changed, 738 insertions(+), 3 deletions(-) create mode 100644 internal/image/handler.go create mode 100644 internal/image/handler_test.go create mode 100644 internal/image/thumb.go create mode 100644 internal/web/templates/images.html diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 821e037..3ab074e 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -2,6 +2,20 @@ Append-only role handoff log. Each role adds one entry when its step is complete. +### T-005 — review — 2026-06-18T13:45:00Z + +| Field | Value | +|-------|-------| +| Agent | claude | +| Summary | Reviewed T-005 image browser; all 4 tests pass including thumbnail size, raw byte-equality, path traversal rejection, and page nav assertions; three nit/minor findings, none blocking. | +| Files Changed | .ai/REVIEW.md, .ai/TASKS.md, .ai/HANDOFF.md | +| Validation | `go fmt ./...`, `go vet ./...`, `go test ./internal/image/... -v`, `go test -race ./...` all pass. | +| Verdict | PASS_WITH_NOTES | +| Blocking Findings | none | +| Next Role | implement | + +--- + ### T-004 — review — 2026-06-18T13:25:00Z | Field | Value | @@ -201,3 +215,29 @@ Append-only role handoff log. Each role adds one entry when its step is complete | Next Role | none | --- + +### T-005 — implement — 2026-06-18T13:31:09Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Added authenticated raw image serving, cached 160x90 JPEG thumbnails, and a deep-linkable image browser with thumbnail strip and keyboard/on-screen navigation. | +| Files Changed | .ai/TASKS.md, .ai/HANDOFF.md, README.md, go.mod, go.sum, internal/image/handler.go, internal/image/handler_test.go, internal/image/thumb.go, internal/web/router.go, internal/web/router_test.go, internal/web/templates/images.html, internal/web/templates/templates.go | +| Validation | `go fmt ./...` passed; `go vet ./...` passed; `go test ./internal/image/...` passed; `go test ./...` passed; `go test -race ./...` passed | +| Commit | `feat(image): add image browser and thumbnails` | +| Next Role | review | + +--- + +### T-005 — implement — 2026-06-18T13:39:09Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Committed T-005 image browser after review approval. | +| Files Changed | .ai/TASKS.md, .ai/HANDOFF.md | +| Validation | n/a (commit_task) | +| Commit | `pending feat(image): add image browser and thumbnails` | +| Next Role | none | + +--- diff --git a/.ai/REVIEW.md b/.ai/REVIEW.md index c01610f..c50e1bd 100644 --- a/.ai/REVIEW.md +++ b/.ai/REVIEW.md @@ -208,3 +208,46 @@ None. - Finding #1 (buffer-less render) will become more visible when T-005/T-006 handlers reuse the same `render()` pattern with more dynamic data; recommend fixing before T-006 lands. --- + +## T-005 — Image browser + +**Verdict:** PASS_WITH_NOTES + +### Findings + +| # | Severity | File | Description | Required fix? | +|---|----------|------|-------------|---------------| +| 1 | minor | `internal/image/handler.go` (lines 27–40, 221–251) | `ShellData`, `MonthGroup`, and `groupDaysByMonth` are exact duplicates of the same identifiers already in `internal/web/handler.go`. `internal/image` cannot import `internal/web` (circular: `internal/web` → `internal/image`), so the fix requires extracting these shared shell types to a new package (e.g. `internal/shell` or `internal/ui`). T-006 will add a third copy of the same code if this is not resolved. | no | +| 2 | nit | `internal/image/handler.go` (lines 182–185) | Same buffer-less render pattern as T-004: `Content-Type` header written before `ExecuteTemplate`, making the `http.Error` fallback a no-op if the template fails mid-write. | no | +| 3 | nit | `internal/web/templates/images.html` (lines 41–45) | Arrow navigation uses `window.location.href = url.toString()` (full page reload) rather than `history.pushState` with a partial update as described in the plan design notes. The acceptance criteria don't require push-state; navigation is functionally correct. | no | + +### Required fixes + +None. + +### Verification + +**Steps performed:** +1. Read all new files: `internal/image/thumb.go`, `internal/image/handler.go`, `internal/image/handler_test.go`, `internal/web/templates/images.html`, `internal/web/templates/templates.go`, `internal/web/router.go`, `go.mod`. +2. Cross-checked against `.ai/PLAN.md` Phase 5 scope. +3. Confirmed `golang.org/x/image v0.42.0` added to `go.mod`. ✅ +4. Ran `go fmt ./...` — clean. +5. Ran `go vet ./...` — clean. +6. Ran `go test ./internal/image/... -v` — all 4 tests PASS. +7. Ran `go test -race ./...` — PASS, no data races. + +**Findings:** +- All acceptance criteria met: thumbnail JPEG ≤30 KB verified by test decoding the response; raw endpoint byte-equality verified; path traversal (`%2e%2e`) returns 400; page renders strip (ring highlight), viewer, prev/next links, keyboard arrows. +- FIFO eviction (`order []string` + slice shift) is correct and bounded to `max` entries. ✅ +- `resizeToFit` uses aspect-ratio-preserving scaling: scales to fit within 160×90, never stretches. ✅ +- `Cache.get` and `Cache.add` return defensive copies — callers cannot alias internal byte slices. ✅ +- Path validation uses both a string prefix check (`.."`) and `filepath.Rel` escape check — two independent layers. ✅ +- `Cache-Control: max-age=3600` set on thumb responses. ✅ +- Routes wired correctly under `RequireAuth` middleware in `router.go`. ✅ +- `imageRouter` test helper bypasses auth (tests the handler directly), which is correct for unit-level coverage. ✅ +- Wrapping arithmetic for prev/next is correct: `(active ± 1 + len) % len`. Test validates idx=1 with 2 images gives prev=0, next=0. ✅ + +**Risks:** +- Finding #1 (shell type duplication) is the main structural risk: T-006 will face the same choice and may add a third copy. The T-006 implementer should extract shared types before or during that task. + +--- diff --git a/.ai/TASKS.md b/.ai/TASKS.md index 35f9f7a..bfceb06 100644 --- a/.ai/TASKS.md +++ b/.ai/TASKS.md @@ -24,5 +24,5 @@ Command expectations: | T-002 | Auth: SQLite schema (users + sessions), bcrypt login/logout, session cookie middleware, first-run admin bootstrap, admin user-management page | done | Login page renders at `/login`; valid credentials set session cookie and redirect; invalid credentials return 401; admin can add/delete users at `/admin/users`; all non-login routes return 302 without cookie; `go test ./internal/auth/...` passes | `go fmt ./...`, `go vet ./...`, `go test ./...`, `go test -race ./...`, `go test ./internal/auth/...` passed after template rework | none | | T-003 | Footage scanner: walk FOOTAGE_ROOT, build in-memory index (date → images + videos), filename parser, periodic rescan goroutine | done | Scanner correctly indexes fixture directory with known dates/files; `DayList()` returns sorted dates; `DayEntry` contains sorted images and videos; periodic rescan runs without data race (`go test -race ./internal/footage/...` passes) | `go fmt ./...`, `go vet ./...`, `go test ./internal/footage/...`, `go test ./...`, `go test -race ./...` passed | none | | T-004 | UI shell & day navigation: dark Tailwind CSS base layout, sidebar (desktop) with date list grouped by month, mobile drawer + bottom tab bar, day overview page with Images/Videos tabs | done | Desktop layout shows sidebar at ≥768 px; mobile layout hides sidebar and shows drawer trigger at <768 px; day overview page renders images and videos tab counts; `go vet ./...` passes | `go fmt ./...`, `go vet ./...`, `go test ./internal/web/...`, `go test ./...`, `go test -race ./...` passed | none | -| T-005 | Image browser: raw JPEG serving, in-memory LRU thumbnail cache (160×90), thumbnail strip, full-image viewer, arrow navigation (keyboard + on-screen), deep-linkable `?idx=N` | ready_for_implement | Thumbnail endpoint returns JPEG ≤30 KB for a test image; raw endpoint serves original file unchanged; arrow nav advances and wraps correctly; keyboard left/right works; strip highlights active item; `go test ./internal/image/...` passes | n/a | implement | +| T-005 | Image browser: raw JPEG serving, in-memory LRU thumbnail cache (160×90), thumbnail strip, full-image viewer, arrow navigation (keyboard + on-screen), deep-linkable `?idx=N` | done | Thumbnail endpoint returns JPEG ≤30 KB for a test image; raw endpoint serves original file unchanged; arrow nav advances and wraps correctly; keyboard left/right works; strip highlights active item; `go test ./internal/image/...` passes | `go fmt ./...`, `go vet ./...`, `go test ./internal/image/...`, `go test ./...`, `go test -race ./...` passed | none | | T-006 | Video browser: on-demand ffmpeg remux stream (.265→MP4 via pipe), in-memory LRU video thumbnail (first-frame extract), thumbnail strip, HTML5 player, arrow navigation, deep-linkable `?idx=N` | ready_for_implement | Stream endpoint returns `Content-Type: video/mp4` and non-empty body for a test .265 file; thumbnail endpoint returns a JPEG; player page renders with `