|
|
@@ -266,10 +266,11 @@ Button layout:
|
|
|
|
|
|
```
|
|
|
SHIPPED (2026-05-06): src/app/(app)/list/[id]/page.tsx
|
|
|
-- Centered vertical stack (both mobile and desktop)
|
|
|
-- List name: text-2xl sm:text-3xl font-bold
|
|
|
-- Uppercase tracked "JOIN CODE" eyebrow with mono chip (bg-foreground/10) showing WORD-WORD code
|
|
|
-- Settings cog below the join code
|
|
|
+- 3-column grid layout: back arrow (→ /home) left, center block, settings cog right
|
|
|
+- Center block: list name (text-2xl sm:text-3xl font-bold), uppercase tracked "JOIN CODE" eyebrow,
|
|
|
+ mono chip (bg-foreground/10) showing WORD-WORD invite code
|
|
|
+- /create-group page mirrors back-arrow + centered-title pattern; button text changed to
|
|
|
+ "Create List" and centered (2026-05-06)
|
|
|
```
|
|
|
|
|
|
### List Admin Actions
|
|
|
@@ -455,7 +456,7 @@ RLS must be enabled on ALL tables with explicit policies — no permissive catch
|
|
|
- **users**: Users can read/update only their own row
|
|
|
- **groups**: Readable by members of the group only
|
|
|
- **group_members**: Readable by members of the same group; joining is a server-side operation via service role key (not client INSERT); deletable by admins or self (leave); UPDATE must prevent role escalation (member cannot set own role to admin)
|
|
|
-- **movies**: Full CRUD for members of the owning group only; INSERT `WITH CHECK` must enforce `added_by = auth.uid()` (prevent attribution spoofing); UPDATE must prevent changing `added_by`
|
|
|
+- **movies**: Full CRUD for members of the owning group only; INSERT `WITH CHECK` must enforce `added_by = auth.uid()` (prevent attribution spoofing); UPDATE must prevent changing `added_by` — enforced by `movies_added_by_immutable` BEFORE UPDATE trigger (migration 00005) rather than a self-referencing WITH CHECK subquery (which causes Postgres error 42P17 infinite recursion)
|
|
|
- **landing_reel_posters**: Readable by anyone (public); writable only by service role (cron job)
|
|
|
|
|
|
Supabase Realtime also respects RLS — subscriptions are authorized by the same policies.
|
|
|
@@ -529,6 +530,9 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
|
|
|
|
|
|
- [ ] 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`
|
|
|
+
|
|
|
+ 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.
|
|
|
@@ -538,13 +542,14 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
|
|
|
|
|
|
- [ ] 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.
|
|
|
|
|
|
### 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.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.
|
|
|
+- [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.
|
|
|
|
|
|
@@ -570,6 +575,9 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
|
|
|
- [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.
|
|
|
+
|
|
|
- [ ] 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
|