Ver Fonte

[Scope] Reconcile Phases 1-3 + 5.5 complete; log 3.8/5.6 bugs (now fixed)

Walkthrough on 2026-05-07 confirmed Phases 1-3 and 5.5 are built and
working. Logged 3.8 watched-state real-time UPDATE bug and 5.6
offline-search false-empty as known issues — both subsequently fixed
in 47b4c01. 1.7/5.8 stay open pending production deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User há 1 mês atrás
pai
commit
dc2d4ef447
1 ficheiros alterados com 43 adições e 15 exclusões
  1. 43 15
      PROJECT_SCOPE.md

+ 43 - 15
PROJECT_SCOPE.md

@@ -528,36 +528,47 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 
 ### Phase 1: Foundation (April 6-10, 2026) — MVP
 
-- [ ] 1.1 — Initialize Next.js project with Tailwind CSS, App Router, and `output: 'standalone'` in `next.config.ts`; configure TypeScript strict mode, ESLint (`next/core-web-vitals` + `next/typescript`), Prettier; set up husky + lint-staged (pre-commit: lint/format, pre-push: lint + typecheck + build); install sharp as production dependency; add Vitest for unit testing (scope: pure logic and Client Components only — Vitest cannot render RSC)
-- [ ] 1.2 — Set up self-hosted Supabase Docker stack; replace ALL default secrets before first `docker compose up` (JWT_SECRET → regenerate ANON_KEY + SERVICE_ROLE_KEY together; replace POSTGRES_PASSWORD, DASHBOARD_USERNAME, DASHBOARD_PASSWORD); configure GoTrue: set `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true`, disable email/phone/OAuth auth methods; create schema (users, groups, group_members, movies, landing_reel_posters tables) with CHECK constraints on display_name and group name; define and enable RLS policies on all tables with `WITH CHECK` clauses (see RLS section); initialize Supabase CLI migrations workflow — all schema changes via `supabase migration new`
+- [x] 1.1 — Initialize Next.js project with Tailwind CSS, App Router, and `output: 'standalone'` in `next.config.ts`; configure TypeScript strict mode, ESLint (`next/core-web-vitals` + `next/typescript`), Prettier; set up husky + lint-staged (pre-commit: lint/format, pre-push: lint + typecheck + build); install sharp as production dependency; add Vitest for unit testing (scope: pure logic and Client Components only — Vitest cannot render RSC)
+
+  VERIFIED (2026-05-07): Project initialized, TypeScript strict, ESLint, Prettier, husky pre-commit (lint/format), and Vitest all in place. Lint/typecheck/build run in CI and locally. SUB-TODO: pre-push hook (lint + typecheck + build) is not yet wired — currently only CI and manual runs cover this. Wire husky pre-push before first external collaborator onboards.
+
+- [x] 1.2 — Set up self-hosted Supabase Docker stack; replace ALL default secrets before first `docker compose up` (JWT_SECRET → regenerate ANON_KEY + SERVICE_ROLE_KEY together; replace POSTGRES_PASSWORD, DASHBOARD_USERNAME, DASHBOARD_PASSWORD); configure GoTrue: set `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true`, disable email/phone/OAuth auth methods; create schema (users, groups, group_members, movies, landing_reel_posters tables) with CHECK constraints on display_name and group name; define and enable RLS policies on all tables with `WITH CHECK` clauses (see RLS section); initialize Supabase CLI migrations workflow — all schema changes via `supabase migration new`
 
   RLS HOTFIX SHIPPED (2026-05-06, migration `supabase/migrations/00005_movies_update_recursion_fix.sql`): Symptom — PATCH /api/movies/[id]/watched returned 500 with Postgres error 42P17 ("infinite recursion detected in policy for relation 'movies'"). Root cause — the original `movies_update` RLS policy's WITH CHECK clause referenced `public.movies` in a subquery to enforce `added_by` immutability; selecting from movies while the movies_update policy evaluates triggers RLS recursion. Fix — dropped the self-referencing WITH CHECK; replaced with a `BEFORE UPDATE` trigger `movies_added_by_immutable` that raises exception 23514 if `NEW.added_by IS DISTINCT FROM OLD.added_by`. Group-membership check remains in the policy. NOTE: other RLS policies should be audited for similar self-referencing subqueries — see task 1.9.
 
-- [ ] 1.3 — Configure Supabase client in Next.js using `@supabase/ssr` (`createBrowserClient` for browser, `createServerClient` for server-side with `SUPABASE_INTERNAL_URL`); implement env var validation at startup via t3-env (`@t3-oss/env-nextjs`) with zod — server vars in `server` block, client vars in `client` block; create TMDB API proxy route (`/api/tmdb/*`) to keep API key server-side; set `include_adult=false` on all TMDB calls
-- [ ] 1.4 — Implement anonymous auth via `supabase.auth.signInAnonymously()`; display name input and optional avatar color picker; session managed by Supabase GoTrue via `@supabase/ssr` cookie-based handling (JWT issued automatically); verify GoTrue returns 200 (not 400) — confirm `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true` is set
-- [ ] 1.5 — Implement recovery code generation (24 alphanumeric characters, 128-bit entropy), Argon2id hashing (explicit OWASP parameters: memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes — do not use library defaults), and show-once display screen. NOTE (2026-05-02, commits f71cb76 + a54a5ca): Fixed a post-signup race on the /recovery page where React 19 StrictMode double-mount caused the page to hang on "Generating your recovery code..." indefinitely (reload was required to see the code). Root cause: the original useMutation + useRef guard fired on the first mount instance; the second instance never observed the response. Fixed by replacing useMutation with useQuery (key: `recovery-code-generate`, staleTime/gcTime: Infinity, refetchOnMount: false) — TanStack Query dedupes the request across mount/remount/StrictMode and persists the result in cache. Server route (POST /api/auth/recovery/generate) unchanged.
+- [x] 1.3 — Configure Supabase client in Next.js using `@supabase/ssr` (`createBrowserClient` for browser, `createServerClient` for server-side with `SUPABASE_INTERNAL_URL`); implement env var validation at startup via t3-env (`@t3-oss/env-nextjs`) with zod — server vars in `server` block, client vars in `client` block; create TMDB API proxy route (`/api/tmdb/*`) to keep API key server-side; set `include_adult=false` on all TMDB calls
+- [x] 1.4 — Implement anonymous auth via `supabase.auth.signInAnonymously()`; display name input and optional avatar color picker; session managed by Supabase GoTrue via `@supabase/ssr` cookie-based handling (JWT issued automatically); verify GoTrue returns 200 (not 400) — confirm `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true` is set
+- [x] 1.5 — Implement recovery code generation (24 alphanumeric characters, 128-bit entropy), Argon2id hashing (explicit OWASP parameters: memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes — do not use library defaults), and show-once display screen. NOTE (2026-05-02, commits f71cb76 + a54a5ca): Fixed a post-signup race on the /recovery page where React 19 StrictMode double-mount caused the page to hang on "Generating your recovery code..." indefinitely (reload was required to see the code). Root cause: the original useMutation + useRef guard fired on the first mount instance; the second instance never observed the response. Fixed by replacing useMutation with useQuery (key: `recovery-code-generate`, staleTime/gcTime: Infinity, refetchOnMount: false) — TanStack Query dedupes the request across mount/remount/StrictMode and persists the result in cache. Server route (POST /api/auth/recovery/generate) unchanged.
 - [x] 1.6 — Implement recovery code claim flow (enter code → verify against Argon2id hash → restore identity on new device); rate-limit claim endpoint (5 failed attempts per IP per 15-minute window); invalidate code after successful claim. VERIFIED (2026-05-06): Manual end-to-end browser test passed. Both escalation triggers cleared: (A) `auth.identities` row present and correct — no issue; (B) GoTrue #2013 residual — not triggered with pinned v2.170.0. This was the highest-risk open item in the recovery system: GoTrue accepting our minted JWTs via HS256 signature alone (without a matching `auth.sessions` row) was the critical architectural unknown. Browser verification clears it — the synthetic-identity-at-generate architecture is sound. Architectural risk for the recovery system is resolved.
 
   ARCHITECTURE — Synthetic-identity-at-generate (replaces broken claim-only-verifies-no-session flow): At code-generation time, `/api/auth/recovery/generate` assigns `<uid>@moviedice.invalid` + HKDF-SHA256 derived password (label `"moviedice-recovery-v1"`, salt=uid) to the GoTrue user via admin API. Returns 409 if a code already exists (idempotent — no auto-rotate). Atomic rollback on failure (`admin.deleteUser` + 500). At claim time, `/api/auth/recovery/claim` runs Argon2 scan → `signInWithPassword` with derived synthetic creds → cookies set via `createServerClient` bound to response → `{ ok: true }`. `/recover` page calls `window.location.assign("/")` post-claim (hard nav for cookie propagation). New files: `src/lib/auth/synthetic.ts` (`syntheticEmail()`, `derivePassword()`), `supabase/migrations/00003_synthetic_email_constraint.sql` (CHECK on `auth.users.email` rejecting `@moviedice.invalid` on non-promoted rows), `src/lib/supabase/admin.ts` updated with module-load assertion on `GOTRUE_EXTERNAL_EMAIL_ENABLED`. Tests: 15 passing (synthetic.test.ts ×6, recovery-generate.test.ts ×5, recovery-claim.test.ts ×4) + 1 todo regression scaffold for GoTrue #2013 canary.
 
 - [ ] 1.7 — Build Docker infrastructure: multi-stage Dockerfile (node:22-slim, non-root user, tini; builder stage installs `python3 make g++` for argon2 native build), docker-compose.yml orchestrating Next.js app + self-hosted Supabase stack + Caddy reverse proxy + Node.js cron container + pg_dump backup container (daily, 7-day retention), .dockerignore, /api/health endpoint; network security: Kong/Postgres ports internal only, Studio restricted to 127.0.0.1; Caddy persistent volumes for `/data` and `/config` (TLS certificates); Docker log rotation on all containers (max-size: 10m, max-file: 5); use Let's Encrypt staging for initial testing; deploy and confirm Supabase connection works in production
-- [ ] 1.8 — Add Sentry error monitoring (free tier); configure `beforeSend` to strip UUID path segments from error events; do not call `Sentry.setUser()` with user identifiers
-- [ ] 1.9 — RLS policy audit: review all remaining RLS policies (users, groups, group_members, landing_reel_posters) for WITH CHECK or USING clauses that contain self-referencing subqueries against the same table being evaluated — these cause Postgres error 42P17 (infinite recursion). The movies_update recursion was fixed in migration 00005 via trigger; confirm no other policies contain the same pattern. Document findings.
+
+  NOTE (2026-05-07): Docker infrastructure code (Dockerfile, docker-compose.yml, Caddy config, cron container, health endpoint) is complete and runs locally. OPEN: app has never been deployed to a real server — production deploy and smoke test remain pending (see also 5.8).
+
+- [x] 1.8 — Add Sentry error monitoring (free tier); configure `beforeSend` to strip UUID path segments from error events; do not call `Sentry.setUser()` with user identifiers
+- [x] 1.9 — RLS policy audit: review all remaining RLS policies (users, groups, group_members, landing_reel_posters) for WITH CHECK or USING clauses that contain self-referencing subqueries against the same table being evaluated — these cause Postgres error 42P17 (infinite recursion). The movies_update recursion was fixed in migration 00005 via trigger; confirm no other policies contain the same pattern. Document findings.
+
+  VERIFIED (2026-05-07): Clean migrations confirmed; no other policies contain self-referencing subqueries. CI guard covers future migrations. Audit is sufficient documentation for pre-launch.
 
 ### Phase 2: Groups and Permissions (April 10-14, 2026) — MVP
 
-- [ ] 2.1 — Build "Create a Group" flow: name input (validated: 1-50 chars), invite code generation (WORD-WORD format; word list: 2,000+ words, 3-8 chars, offensive terms filtered, collision check), store in DB, assign creator as List Admin; document API routes in markdown
-- [ ] 2.2 — Build "Join with a Code" flow: code entry, validation via server-side route handler using service role key (not client-side INSERT), group_members record with role: 'member'; rate-limit join endpoint (5-10 failed attempts per IP per 15-minute window); document API routes in markdown
-- [ ] 2.3 — Build logged-in home page: mirrors landing page layout; replaces Login with "Create List" button; shows user's list cards (list name left, movie count + film emoji right, "Created by: [username]" below); tapping a card navigates to that list; Roll the Dice and Genre Roll on the home page roll across all lists combined and display the result as a standalone teaser card on the home page (no navigation into a specific list); use polling (not real-time subscriptions) for home page movie counts
+- [x] 2.1 — Build "Create a Group" flow: name input (validated: 1-50 chars), invite code generation (WORD-WORD format; word list: 2,000+ words, 3-8 chars, offensive terms filtered, collision check), store in DB, assign creator as List Admin; document API routes in markdown
+- [x] 2.2 — Build "Join with a Code" flow: code entry, validation via server-side route handler using service role key (not client-side INSERT), group_members record with role: 'member'; rate-limit join endpoint (5-10 failed attempts per IP per 15-minute window); document API routes in markdown
+- [x] 2.3 — Build logged-in home page: mirrors landing page layout; replaces Login with "Create List" button; shows user's list cards (list name left, movie count + film emoji right, "Created by: [username]" below); tapping a card navigates to that list; Roll the Dice and Genre Roll on the home page roll across all lists combined and display the result as a standalone teaser card on the home page (no navigation into a specific list); use polling (not real-time subscriptions) for home page movie counts
+
+  VERIFIED (2026-05-07): Cross-list roll confirmed working — rolls across all user lists combined, result displays via ListRollCarousel in place with no navigation into any list.
+
 - [x] 2.4 — Implement List Admin settings. SHIPPED (2026-05-06): Settings page `src/app/(app)/list/[id]/settings/page.tsx` now exists (gear icon was previously broken/404); mounts `src/components/groups/settings-panel.tsx`. Join code displayed in list header under list name (`src/app/(app)/list/[id]/page.tsx` selects `invite_code`, renders "Join code: WORD-WORD"). Delete/leave/transfer actions use shake-to-arm pattern (animate-shake, 4s auto-disarm) replacing all `window.confirm()` popups. Admin with other members: second click reveals inline non-admin successor picker; selecting a member runs transfer+leave atomically → router.push("/"). Admin last member: second click permanently deletes → router.push("/"). Settings also supports: rename list, regenerate invite code (copy-to-clipboard), view + remove members. UI POLISH (2026-05-06): List page header redesigned as 3-col grid (back arrow left, centered name + JOIN CODE eyebrow + mono invite chip, settings cog right); `/create-group` page header mirrors back-arrow + centered-title pattern, button text changed to "Create List" and centered.
 - [x] 2.5 — Implement List Admin direct deletion. SHIPPED (2026-05-06): "Delete the list" in SettingsPanel uses shake-to-arm (first click arms + shakes, 4s auto-disarm, second click confirms permanent deletion) → router.push("/"). Does NOT trigger the successor/ownership picker.
 - [x] 2.6 — Implement regular user settings. SHIPPED (2026-05-06): "Leave this list" for non-admin members uses same shake-to-arm pattern for consistency → router.push("/") on confirm.
 
 ### Phase 3: Movie List Core (April 14-20, 2026) — MVP
 
-- [ ] 3.1 — Integrate TMDB API via server-side proxy (`/api/tmdb/*`): search endpoint with `include_adult=false` and server-side `adult` field filtering, poster URL construction using TMDB native sizes (w342 grid, w185 reel, w500 modal), trailer URL fetch at add-time; implement TanStack Query caching with explicit `staleTime` configuration; document API routes in markdown
-- [ ] 3.2 — Build search bar with ~300ms debounce, loading state, and two-section results ("In Your List" above separator, TMDB results below)
-- [ ] 3.3 — Implement add-movie flow: tap result → insert into movies table with TMDB metadata, stored trailer_url (fetched via server proxy at add-time; validated against domain allowlist: youtube.com, themoviedb.org, imdb.com), trailer_url_refreshed_at, and added_by attribution; document API routes in markdown
+- [x] 3.1 — Integrate TMDB API via server-side proxy (`/api/tmdb/*`): search endpoint with `include_adult=false` and server-side `adult` field filtering, poster URL construction using TMDB native sizes (w342 grid, w185 reel, w500 modal), trailer URL fetch at add-time; implement TanStack Query caching with explicit `staleTime` configuration; document API routes in markdown
+- [x] 3.2 — Build search bar with ~300ms debounce, loading state, and two-section results ("In Your List" above separator, TMDB results below)
+- [x] 3.3 — Implement add-movie flow: tap result → insert into movies table with TMDB metadata, stored trailer_url (fetched via server proxy at add-time; validated against domain allowlist: youtube.com, themoviedb.org, imdb.com), trailer_url_refreshed_at, and added_by attribution; document API routes in markdown
 
   BUG FIX SHIPPED (2026-05-06, master — uncommitted): Clicking "+ Add" on a search result flashed the button but silently failed. Root cause: `src/app/api/movies/route.ts` was calling TMDB with `?api_key=${TMDB_API_KEY}` (v3 query-string auth), but `TMDB_API_KEY` is a v4 read-access JWT that TMDB only accepts via `Authorization: Bearer`. Every other TMDB call in the repo (`src/lib/tmdb-fetch.ts`, `src/lib/tmdb/client.ts`) already used Bearer — only this route was wrong. TMDB returned 401 → route returned 404 "Movie not found on TMDB" → `useAddMovie` threw silently (no `onError` handler) → invisible failure to the user. Fix: switched both TMDB fetches in that route to `Authorization: Bearer ${TMDB_API_KEY}` headers; added `console.error` with TMDB response body+status on details-fetch failure. UX fix in the same pass (`src/components/movies/movie-list-client.tsx`): clicking a search result now immediately hides the results panel (`resultsHidden` state, reset on next keystroke); search input text is preserved.
 
@@ -571,13 +582,15 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
   - `src/components/dice/roll-result-card.tsx` — list used it before; list now uses `ListRollCarousel`
 
 - [x] 3.4 — Build 2-column poster grid. SHIPPED (2026-05-06): `PosterCard` is now a full-card button (no ExpandedPanel). Tapping opens `ListMoreInfoModal`. "i" glyph bottom-right is decorative `pointer-events-none`. Watched overlay changed from binoculars emoji to green-circle checkmark top-left. Avatar dot remains top-right. Grid layout unchanged (w342 posters, `loading="lazy"`, meaningful `alt` text, added-by avatar overlaid top-right).
-- [ ] 3.5 — Implement infinite scroll: load 12 movies initially, fetch and append next batch automatically on scroll to bottom
+- [x] 3.5 — Implement infinite scroll: load 12 movies initially, fetch and append next batch automatically on scroll to bottom
 - [x] 3.6 — Build movie info modal. SHIPPED (2026-05-06): `src/components/dice/list-more-info-modal.tsx`. Replaces the planned inline ExpandedPanel entirely. See "Movie Info Modal" flow and feature table entry for full spec. Portal to document.body; TMDB overview fetched per-open; Watched + Delete both use shake-to-arm three-state pattern.
 - [x] 3.7 — Genre filter (scope clarified 2026-05-06). DELIBERATE SCOPE REDUCTION — not a regression. Genre tags are display-only everywhere except Genre Roll. Genre Roll (`RollBar` / `RollSection`) is the sole filter entry point — tappable tag-filters have been explicitly removed from the product design when ExpandedPanel was replaced by `ListMoreInfoModal`. Cleanup in flight: dormant `selectedGenre` prop being removed from `PosterGrid`.
 - [ ] 3.8 — Implement Watched state: toggle marks/unmarks movie as watched for the group; watched movies move to collapsed "Watched" section; green-circle checkmark overlay and button color update simultaneously across all members; announce state change via `aria-live="polite"` region
 
   NOTE (2026-05-06): The PATCH /api/movies/[id]/watched endpoint was 500ing due to Postgres error 42P17 (RLS recursion). Fixed by migration 00005 (see task 1.2 note). The API layer is now unblocked. Remaining work: collapsed "Watched" section UI in the grid, real-time propagation of watched state to all group members (via task 3.9), and `aria-live` announcement.
 
+  KNOWN BUG (2026-05-07): Real-time UPDATE events for watched state do not propagate to other connected clients. The watching user's UI updates correctly (optimistic update), but a second browser window on the same list requires a manual refresh to see the change. INSERT (add movie) and DELETE (remove movie) real-time events propagate correctly — only UPDATE (watched toggle) is affected. Root cause not yet investigated; likely a Supabase Realtime subscription filter or event type mismatch.
+
 - [ ] 3.9 — Enable Supabase real-time subscriptions on movies table for live add, remove, and watched-status updates; subscribe only to the currently-viewed list (subscribe on mount, unsubscribe on unmount via useEffect cleanup); implement exponential backoff on reconnection
 
 ### Phase 4: Randomizer (April 20-23, 2026) — MVP
@@ -598,10 +611,19 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 - [x] 5.3 — Build slot-machine reel animation for the landing page Roll the Dice button: 3 side-by-side reels that spin through the automatically fetched poster set (using w185 size, `aria-hidden` on spinning posters), decelerate, and land on a single TMDB movie result with alt text (the final result is fetched live from TMDB via server proxy — not constrained to the reel poster set); animation is user-triggered only and completes within 5 seconds (WCAG 2.2.2); respect `prefers-reduced-motion` (instant reveal or simple fade-in when enabled). NOTE (2026-05-02): Continuous carousel and slot-machine spin on roll are implemented. KNOWN BUG (U9): modulo wrap missing in spinning branch causes scroll offset to drift on repeated rolls; fix tracked in `research/PHASE5_UI_FIXES_PLAN.md`. NOTE (2026-05-02 dea71d9/0061375): Roll result card now emerges inside the carousel center via animate-emerge keyframe (scale 0→1.08→1, ~500ms, bouncy, skipped under prefers-reduced-motion); snap math positions midpoint of a gap at viewport center so card sits in a symmetric slot; posters spread ±110px on settle. Card stays settled until user re-rolls or refreshes (RESUME_DELAY auto-resume timer removed). TeaserCard reduced to ~1.3x reel poster size. "i" info button on TeaserCard opens <MoreInfoModal> (src/components/landing/more-info-modal.tsx) with title+year, genres, plot, Add to list (→ /login, no save-intent), Watch Trailer (lazy-fetches /api/tmdb/movie/[id]/videos); mirrors GenreRollModal a11y pattern; uses createPortal to escape the transformed ancestor. Gold glow added to TeaserCard in same UI batch as in-app roll (2026-05-06). NOTE: landing MoreInfoModal is distinct from the in-app `ListMoreInfoModal` (src/components/dice/list-more-info-modal.tsx).
 - [x] 5.3a — Wire landing page Roll the Dice to TMDB via `/api/tmdb/popular?page=N` (random page 1–50, pool ~1000 movies). Result displayed as a static teaser card (poster with alt text, title, genres) — no link, no tap action. NOTE: this is a distinct pool from the carousel reel — see Feature Flow section 5 for the two-pool distinction.
 - [x] 5.4 — Wire landing page Genre Roll to TMDB via server proxy (no login required); display result as a static teaser card showing poster (with alt text), title, and genres — no link, no tap action.
-- [ ] 5.5 — Loading and empty states for all major views (empty list, no search results, no genre matches, empty home page for new users)
+- [x] 5.5 — Loading and empty states for all major views (empty list, no search results, no genre matches, empty home page for new users)
+
+  VERIFIED (2026-05-07): User-confirmed adequate empty and loading states across tested views.
+
 - [ ] 5.6 — Error handling: invalid invite code, TMDB API failure, network errors
+
+  PARTIAL (2026-05-07): Most tested error paths are handled correctly. KNOWN GAP: with network set to offline, the add-movie search silently shows "No results" instead of a network-error indicator — a misleading false-empty state. Remaining sub-todo: detect offline/fetch-failure in the search path and show a distinct error message rather than an empty results list.
+
 - [ ] 5.7 — Configure HTTP security headers in Caddyfile: CSP (with self-hosted URLs, not \*.supabase.co), X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS (start with max-age=86400, increase before launch), Permissions-Policy; use Report-Only mode during testing
 - [ ] 5.8 — Final MVP smoke test and Docker production deployment
+
+  NOTE (2026-05-07): App has only run locally — no production server deployment has occurred. This task and 1.7's production deploy step remain fully open.
+
 - [x] 5.9 — Implement user-facing sign-out. SHIPPED (2026-05-02, commits 44ff76c→582a7fd→add8745): `POST /api/auth/signout` calls `supabase.auth.signOut()` server-side and returns `Set-Cookie` clears; `GET/POST /logout` is a redirect-to-`/` alias for manual/debug links. `<SignOutButton />` (`src/components/auth/sign-out-button.tsx`) placed in the `(app)` layout header — no avatar/menu. On click: calls API, clears TanStack Query cache, hard-navigates to `/`. No confirmation dialog (decided: anonymous-account warning is deliberately omitted; click = signed out). Distinct from `/api/admin/logout` (iron-session). No `Clear-Site-Data` header (no service worker yet). persistQueryClient cleanup deferred to Phase 8.
 
   DEV-ENV QUIRK — cookie-name pinning: `SUPABASE_INTERNAL_URL` and `NEXT_PUBLIC_SUPABASE_URL` may differ in dev (different hostnames). The Supabase SDK derives the session cookie name from the URL, so each side silently derived a different name, making `signOut`/`getUser` no-ops. Fixed by pinning `cookieOptions.name` to a value derived from `NEXT_PUBLIC_SUPABASE_URL` in `src/lib/supabase/cookie-name.ts`. Any new `createServerClient` call must include this `cookieOptions.name` — see also CLAUDE.md.
@@ -749,3 +771,9 @@ _Features added after initial scope. Complete current Implementation Plan progre
 | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------- |
 | Compact list/grid toggle      | Toggle between poster grid and a compact list layout (title, year, metadata per row)                                                                                                                                                                                                                        | 2026-04-05 | Deferred from MVP — user unfamiliar with it at scoping; low priority relative to core flows   |
 | Self-service account deletion | User-facing account deletion flow with cascading deletes (wrapped in transaction), ownership transfer for administered groups (auto-transfer to longest-tenured member), anonymization of added_by references (ON DELETE SET NULL), and removal of auth.users record via `supabase.auth.admin.deleteUser()` | 2026-04-06 | GDPR Article 17 (Right to Erasure); MVP relies on Master Admin deletion on request as interim |
+
+## 12. Change Log
+
+| Date       | Summary                                                                                                                                                                                                                                                                                                                                                                                                                                           |
+| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 2026-05-07 | Walkthrough audit: marked complete — 1.1–1.5, 1.8, 1.9, 2.1–2.3, 3.1–3.3, 3.5, 5.5. Kept open — 1.7 (production deploy pending; code done locally), 5.8 (no server deploy yet). Added known bug to 3.8: real-time UPDATE events for watched state don't propagate to other clients. Added partial gap to 5.6: offline search shows false-empty instead of error. Sub-todo on 1.1: pre-push hook not yet wired. Phase 4 and Phases 6–10 unchanged. |