|
| 1 | +# Feature Plan 036 – Photo Page URL |
| 2 | + |
| 3 | +_Linked specification:_ `docs/specs/4-architecture/features/036-photo-page-url/spec.md` |
| 4 | +_Status:_ Complete |
| 5 | +_Last updated:_ 2026-04-12 |
| 6 | + |
| 7 | +> Guardrail: Keep this plan traceable back to the governing spec. Reference FR/NFR/Scenario IDs from `spec.md` where relevant. Log any new high- or medium-impact questions in [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md). |
| 8 | +
|
| 9 | +## Vision & Success Criteria |
| 10 | + |
| 11 | +Direct links to photos in large (paginated) albums reliably open the correct photo instead of silently falling back to the album view. Success is measured by S-036-01: visiting `/gallery/:albumId/:photoId?page=N` always renders the photo panel for the correct photo. |
| 12 | + |
| 13 | +## Scope Alignment |
| 14 | + |
| 15 | +- **In scope:** Frontend-only changes to `PhotosState`, `AlbumState`, `photoRoute`, `Album.vue`, and `AlbumPanel.vue`. Model albums and tag albums. |
| 16 | +- **Out of scope:** Backend endpoint to look up the page of a photo by ID (deferred — see Follow-ups). Smart albums, search, tag, and timeline photo views (same pattern can be applied later). Restructuring the photos store as an array of arrays. |
| 17 | + |
| 18 | +## Dependencies & Interfaces |
| 19 | + |
| 20 | +- `AlbumService.getPhotos(album_id, page)` — existing paginated endpoint, no changes needed. |
| 21 | +- `PhotosState` — extended with `photoPageMap`, `prependPhotos`, and page-aware `setPhotos`/`appendPhotos`. |
| 22 | +- `AlbumState` — extended with `photos_min_page`, `prepend` mode in `loadPhotos`, and `startPage` in `load()`. |
| 23 | +- Vue Router query params — `?page=N` is read and written via `route.query.page` / `router.replace()`. |
| 24 | + |
| 25 | +## Assumptions & Risks |
| 26 | + |
| 27 | +- **Assumptions:** The existing `Album::photos` API endpoint supports arbitrary page numbers; out-of-range pages return an empty result without error. |
| 28 | +- **Risks:** Background prepend calls run concurrently with each other. Because `photos_loading` is skipped for prepend calls, the UI guard (`loadMorePhotos` guard) won't block during background prepends — this is intentional and acceptable. |
| 29 | +- **Risks:** The `?page=N` in old/external links that were generated before this feature will be absent. Those links still fail for photos on page > 1; a backend endpoint (deferred) would resolve this. |
| 30 | + |
| 31 | +## Implementation Drift Gate |
| 32 | + |
| 33 | +Run `npm run build` after each increment; confirm no TypeScript or bundle errors. Run the existing PHP test suite (`php artisan test`) to confirm no regressions in backend behaviour. |
| 34 | + |
| 35 | +## Increment Map |
| 36 | + |
| 37 | +1. **I1 – PhotosState: photoPageMap + prependPhotos** |
| 38 | + - _Goal:_ Extend the photos store to track which page each photo was loaded from, and add a `prependPhotos` method for inserting earlier pages before the currently loaded window. |
| 39 | + - _Preconditions:_ None. |
| 40 | + - _Steps:_ |
| 41 | + - Add `photoPageMap: Record<string, number>` state field; clear it in `reset()`. |
| 42 | + - Add `recordPhotoPages(photos, page)` helper. |
| 43 | + - Add `page` parameter to `setPhotos` and `appendPhotos`; call `recordPhotoPages` in each. |
| 44 | + - Implement `prependPhotos(photos, isTimeline, page)` with boundary-link repairs (non-timeline) and timeline group merging (timeline mode). |
| 45 | + - _Commands:_ `npm run build` |
| 46 | + - _Exit:_ Build passes; `photoPageMap` is populated after each load call. |
| 47 | + |
| 48 | +2. **I2 – AlbumState: prepend support + startPage in load()** |
| 49 | + - _Goal:_ Teach `loadPhotos` about a `prepend` mode, and allow `load()` to jump to a specific start page and load earlier pages in background. |
| 50 | + - _Preconditions:_ I1 complete. |
| 51 | + - _Steps:_ |
| 52 | + - Add `photos_min_page: number` to state; reset to 1 in `reset()`. |
| 53 | + - Add `prepend: boolean = false` parameter to `loadPhotos`; route to `prependPhotos` and skip `photos_loading` flag for prepend calls. |
| 54 | + - Change `load()` signature to `load(startPage: number = 1)`. |
| 55 | + - In `load()`, `await loadPhotos(resolvedStart, false)` first, then fire-and-forget prepend calls for pages `resolvedStart-1` down to 1 via `void this.loadPhotos(p, false, true)`. |
| 56 | + - _Commands:_ `npm run build` |
| 57 | + - _Exit:_ Build passes; album with `startPage=3` loads page 3 first, then page 2 and 1 in background. |
| 58 | + |
| 59 | +3. **I3 – photoRoute: include ?page=N in photo URLs** |
| 60 | + - _Goal:_ All photo route objects generated by `photoRoute()` include `?page=N` when the photo's page is known from `photoPageMap`. |
| 61 | + - _Preconditions:_ I1 complete. |
| 62 | + - _Steps:_ |
| 63 | + - Import `usePhotosStore` in `photoRoute.ts`. |
| 64 | + - Look up `photosStore.photoPageMap[photoId]`; add `query: { page: String(page) }` to album and flow-album routes when `page` is defined. |
| 65 | + - _Commands:_ `npm run build` |
| 66 | + - _Exit:_ Clicking a photo in the album view navigates to a URL that includes `?page=N`. |
| 67 | + |
| 68 | +4. **I4 – Album.vue: read ?page from route + reactive photo watcher** |
| 69 | + - _Goal:_ When navigating to an album+photo URL with `?page=N`, pass `N` to `albumStore.load()` so the target page is fetched first. |
| 70 | + - _Preconditions:_ I2 complete. |
| 71 | + - _Steps:_ |
| 72 | + - Add `getStartPage()` helper that parses `route.query.page` (defaults to 1 for absent or invalid values). |
| 73 | + - Pass `getStartPage()` to `albumStore.load()` in the `load()` function. |
| 74 | + - Add a `watch(() => photosStore.photos, ...)` that retries `photoStore.load()` whenever photos change and the photo is not yet loaded — handles background-prepend scenarios where the photo arrives after the initial `load()` call. |
| 75 | + - _Commands:_ `npm run build` |
| 76 | + - _Exit:_ Direct link with `?page=3` opens the photo panel; direct link without `?page` continues to work for page-1 photos. |
| 77 | + |
| 78 | +5. **I5 – AlbumPanel.vue: update URL query after pagination events** |
| 79 | + - _Goal:_ Keep `?page=N` in the URL in sync with the currently visible pagination window so copied links always include the correct page. |
| 80 | + - _Preconditions:_ I3 complete. |
| 81 | + - _Steps:_ |
| 82 | + - Replace `@load-more="albumStore.loadMorePhotos()"` with `@load-more="loadMorePhotosAndUpdateUrl()"`. |
| 83 | + - Implement `loadMorePhotosAndUpdateUrl()`: awaits `albumStore.loadMorePhotos()`, then calls `router.replace()` to update `?page=N`. |
| 84 | + - Change `goToPhotosPage(page)` to also call `router.replace()` after the page load resolves. |
| 85 | + - _Commands:_ `npm run build` |
| 86 | + - _Exit:_ Scrolling to the next page of photos updates the URL to `?page=N`. |
| 87 | + |
| 88 | +## Scenario Tracking |
| 89 | + |
| 90 | +| Scenario ID | Increment / Task reference | Notes | |
| 91 | +|---|---|---| |
| 92 | +| S-036-01 | I2, I4 | `?page=N` → target page loaded first → photo displayed | |
| 93 | +| S-036-02 | I4 | Missing `?page` defaults to page 1; page-1 photos work; others still limited | |
| 94 | +| S-036-03 | I5 | Infinite scroll / load-more updates URL | |
| 95 | +| S-036-04 | I3 | Clicking photo thumb includes `?page=N` from `photoPageMap` | |
| 96 | +| S-036-05 | I3 | Same-page navigation keeps same `?page` value | |
| 97 | +| S-036-06 | I3 | Cross-page navigation updates `?page` | |
| 98 | + |
| 99 | +## Analysis Gate |
| 100 | + |
| 101 | +Analysis complete on 2026-04-12. No open questions were identified as blocking implementation. One deferred item is captured in Follow-ups. |
| 102 | + |
| 103 | +## Exit Criteria |
| 104 | + |
| 105 | +- [x] `npm run build` passes with no TypeScript errors. |
| 106 | +- [x] Visiting `/gallery/:albumId/:photoId?page=N` opens the photo panel for a photo on page N of a large album. |
| 107 | +- [x] Scrolling down in the album view updates the URL `?page` query parameter. |
| 108 | +- [x] Clicking a photo in the album view navigates to a URL with the correct `?page`. |
| 109 | +- [x] All existing PHP tests continue to pass (`php artisan test`). |
| 110 | + |
| 111 | +## Follow-ups / Backlog |
| 112 | + |
| 113 | +- **Backend endpoint for photo page lookup:** A `Album::photoPage` endpoint (given `album_id` + `photo_id` returns `{ page: N }`) would allow old/external links without `?page` to resolve the correct page automatically. Deferred to a future increment. |
| 114 | +- **Same-pattern application to Search, Tag, Timeline views:** Currently only album and flow-album routes include `?page`. The same `photoPageMap` approach can be extended to those views. |
0 commit comments