From 9b5f1d08d43a71105d776bf71367d2e9654a6a4b Mon Sep 17 00:00:00 2001 From: Domagoj Zecevic Date: Thu, 18 Jun 2026 15:04:16 +0200 Subject: [PATCH] feat(footage): index camera footage by day --- .ai/HANDOFF.md | 40 ++++ .ai/REVIEW.md | 42 ++++ .ai/TASKS.md | 2 +- README.md | 6 +- cmd/server/main.go | 6 +- internal/footage/index.go | 104 +++++++++ internal/footage/index_test.go | 96 ++++++++ internal/footage/scanner.go | 212 ++++++++++++++++++ internal/footage/scanner_test.go | 89 ++++++++ internal/web/router.go | 3 +- .../20260101/images/A26010112000001.jpg | 1 + .../20260101/images/A26010112050001.jpg | 1 + .../20260101/record/A260101_120000_120015.265 | 1 + .../20260101/record/A260101_120500_120530.265 | 1 + .../20260102/images/A26010213000001.jpg | 1 + 15 files changed, 600 insertions(+), 5 deletions(-) create mode 100644 internal/footage/index.go create mode 100644 internal/footage/index_test.go create mode 100644 internal/footage/scanner.go create mode 100644 internal/footage/scanner_test.go create mode 100644 testdata/footage/20260101/images/A26010112000001.jpg create mode 100644 testdata/footage/20260101/images/A26010112050001.jpg create mode 100644 testdata/footage/20260101/record/A260101_120000_120015.265 create mode 100644 testdata/footage/20260101/record/A260101_120500_120530.265 create mode 100644 testdata/footage/20260102/images/A26010213000001.jpg diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 7e483e3..9377c58 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -4,6 +4,20 @@ Append-only role handoff log. Each role adds one entry when its step is complete --- +### T-003 — review — 2026-06-18T11:05:00Z + +| Field | Value | +|-------|-------| +| Agent | claude | +| Summary | Reviewed T-003 footage scanner; all 7 tests pass including race detector; two nit-level findings only — no blockers. | +| Files Changed | .ai/REVIEW.md, .ai/TASKS.md, .ai/HANDOFF.md | +| Validation | `go fmt ./...`, `go vet ./...`, `go test ./internal/footage/... -v`, `go test -race ./...` all pass. | +| Verdict | PASS_WITH_NOTES | +| Blocking Findings | none | +| Next Role | implement | + +--- + ### T-002 — review — 2026-06-18T10:50:00Z | Field | Value | @@ -45,6 +59,32 @@ Append-only role handoff log. Each role adds one entry when its step is complete --- +### T-003 — implement — 2026-06-18T09:04:41Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Added the footage scanner, in-memory day index, startup wiring, fixture coverage, and README scanner documentation. | +| Files Changed | .ai/TASKS.md, .ai/HANDOFF.md, README.md, cmd/server/main.go, internal/web/router.go, internal/footage/scanner.go, internal/footage/index.go, internal/footage/scanner_test.go, internal/footage/index_test.go, testdata/footage/20260101/images/A26010112000001.jpg, testdata/footage/20260101/images/A26010112050001.jpg, testdata/footage/20260101/record/A260101_120000_120015.265, testdata/footage/20260101/record/A260101_120500_120530.265, testdata/footage/20260102/images/A26010213000001.jpg | +| Validation | `go fmt ./...` passed; `go vet ./...` passed; `go test ./internal/footage/...` passed; `go test ./...` passed; `go test -race ./...` passed | +| Commit | `feat(footage): index camera footage by day` | +| Next Role | review | + +--- + +### T-003 — implement — 2026-06-18T13:03:48Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Committed T-003 footage scanner after review approval. | +| Files Changed | .ai/TASKS.md, .ai/HANDOFF.md | +| Validation | n/a (commit_task) | +| Commit | `pending feat(footage): index camera footage by day` | +| Next Role | none | + +--- + ### T-001 — review — 2026-06-18T10:20:00Z | Field | Value | diff --git a/.ai/REVIEW.md b/.ai/REVIEW.md index 3518170..dc7cb0e 100644 --- a/.ai/REVIEW.md +++ b/.ai/REVIEW.md @@ -124,3 +124,45 @@ None. - When T-004 adds `base.html` to the embed glob, template parse order matters; the implementer should parse base first or use `template.ParseFS(..., "base.html", "admin_users.html")` to ensure the base is available before extension templates. --- + +## T-003 — Footage scanner + +**Verdict:** PASS_WITH_NOTES + +### Findings + +| # | Severity | File | Description | Required fix? | +|---|----------|------|-------------|---------------| +| 1 | nit | `internal/footage/index.go` (line 83) | `rescan()` silently discards `Scan()` errors — the index silently retains stale data with no log output. Hard to diagnose filesystem issues in production. | no | +| 2 | nit | `internal/web/router.go` (line 14) | `_ *footage.Index` parameter accepted but still ignored. Correct for T-003 scope; will be wired in T-004. | no | + +### Required fixes + +None. + +### Verification + +**Steps performed:** +1. Read all new/changed files: `internal/footage/scanner.go`, `internal/footage/index.go`, `internal/footage/scanner_test.go`, `internal/footage/index_test.go`, `cmd/server/main.go`, `internal/web/router.go`. +2. Cross-checked against `.ai/PLAN.md` Phase 3 scope — all required types, functions, and behaviours implemented. +3. Inspected fixture files in `testdata/footage/` — 5 files across 2 day directories, correct naming pattern, 8 bytes each. +4. Checked regex patterns against fixture filenames: image `^A(\d{6})(\d{6})\d+\.jpg$` and video `^A(\d{6})_(\d{6})_(\d{6})\.265$` both match. +5. Verified time parsing format `"060102150405"` (YYMMDD + HHMMSS) produces correct timestamps for fixture filenames. +6. Ran `go fmt ./...` — clean. +7. Ran `go vet ./...` — clean. +8. Ran `go test ./internal/footage/... -v` — all 7 tests PASS. +9. Ran `go test -race ./...` — PASS, no data races (cached + fresh run). + +**Findings:** +- All acceptance criteria met: fixture directory indexed correctly (2 days, correct image/video counts), `DayList()` returns newest-first sorted dates, `DayEntry` contains timestamp-sorted images and start-time-sorted videos, periodic rescan confirmed by `TestIndexPeriodicRescan`, race detector clean on `TestIndexConcurrentAccessDuringRescan` (8 goroutines × 100 iterations). +- `copyDayEntry` defensively copies slices on every `Day()` call — callers cannot mutate internal index state. ✅ +- `Close()` uses `sync.Once` around `close(done)` — safe to call multiple times. ✅ +- `NewIndex` does an initial synchronous `rescan()` before starting the goroutine — index is always non-empty on first use if footage exists. ✅ +- Midnight-spanning clips handled: `if end.Before(start) { end = end.Add(24 * time.Hour) }`. ✅ +- Missing `images/` or `record/` subdirectory returns `nil, nil` (not an error) — scanner gracefully handles partial day directories. ✅ +- `main.go` wires `footage.NewIndex` and defers `Close()` correctly. ✅ + +**Risks:** +- Silent error discard in `rescan()` (finding #1) is low risk for correctness but could delay diagnosis of a footage mount problem in production. + +--- diff --git a/.ai/TASKS.md b/.ai/TASKS.md index 20c8dbf..ceb90be 100644 --- a/.ai/TASKS.md +++ b/.ai/TASKS.md @@ -22,7 +22,7 @@ Command expectations: | --- | --- | --- | --- | --- | --- | | T-001 | Project scaffold: Go module, chi router, health endpoint, multi-stage Dockerfile (debian:slim + ffmpeg), docker-compose.yml, .env.example, README.md | done | `docker compose build` succeeds; `GET /health` returns 200; `go vet ./...` passes | `go fmt ./...`, `go vet ./...`, `go test ./...`, `go test -race ./...`, `docker compose build`, `curl -i http://127.0.0.1:18080/health` passed | none | | 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 | ready_for_implement | 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) | n/a | implement | +| 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 | ready_for_implement | 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 | 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` | 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-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 `