|
@@ -29,28 +29,28 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
|
|
|
|
|
|
|
|
| Feature | Description | Priority |
|
|
| Feature | Description | Priority |
|
|
|
|---------|-------------|----------|
|
|
|---------|-------------|----------|
|
|
|
-| Landing page | Centered logo, splash text, slot-machine reel animation on Roll the Dice (3 reels spinning through ~20 automatically fetched posters from TMDB popular/top-rated, replaced on each periodic refresh), Genre Roll against TMDB (no login required), Login button, scrolling About section, 3-step how-it-works demo with alternating left-right-left alignment | Must Have |
|
|
|
|
|
-| Anonymous auth with display name | User picks a display name and optional avatar color; a UUID-based account is created and persisted on device | Must Have |
|
|
|
|
|
-| Recovery code | A longer alphanumeric code shown once after account creation that lets users reclaim their identity on a new device | Must Have |
|
|
|
|
|
-| Group creation with invite code | Creator gets a short human-readable code (e.g., WOLF-42) to share; creator becomes List Admin | Must Have |
|
|
|
|
|
-| Group join via invite code | Enter code to join a group and access its shared list; regular member role assigned | Must Have |
|
|
|
|
|
|
|
+| Landing page | Centered logo, splash text, slot-machine reel animation on Roll the Dice (3 reels spinning through ~20 automatically fetched posters from TMDB popular/top-rated, replaced on each periodic refresh), Genre Roll against TMDB (no login required), Login button, scrolling About section, 3-step how-it-works demo with alternating left-right-left alignment, TMDB attribution footer, privacy policy link | Must Have |
|
|
|
|
|
+| Anonymous auth via Supabase | User picks a display name and optional avatar color; account created via `supabase.auth.signInAnonymously()` which issues a JWT for RLS-compatible sessions; persisted on device | Must Have |
|
|
|
|
|
+| Recovery code | A 24-character alphanumeric code (128-bit entropy) shown once after account creation that lets users reclaim their identity on a new device; hashed with Argon2id before storage; single-use (invalidated after successful claim); claim endpoint rate-limited (5 failed attempts per IP per 15-minute window) | Must Have |
|
|
|
|
|
+| Group creation with invite code | Creator gets a short human-readable code in WORD-WORD format (e.g., WOLF-MOON) to share; creator becomes List Admin | Must Have |
|
|
|
|
|
+| Group join via invite code | Enter code to join a group and access its shared list; regular member role assigned; join endpoint rate-limited (5-10 failed attempts per IP per window) | Must Have |
|
|
|
| List Admin permissions | Creator can rename the list, initiate list deletion or ownership transfer (on self-removal), remove members, and regenerate the invite code | Must Have |
|
|
| List Admin permissions | Creator can rename the list, initiate list deletion or ownership transfer (on self-removal), remove members, and regenerate the invite code | Must Have |
|
|
|
| Regular user permissions | Members can add/remove movies, mark movies as watched, and leave the list | Must Have |
|
|
| Regular user permissions | Members can add/remove movies, mark movies as watched, and leave the list | Must Have |
|
|
|
-| Movie search (TMDB integration) | Search bar queries TMDB with debounce (~300ms); results show below a separator from in-list results | Must Have |
|
|
|
|
|
|
|
+| Movie search (TMDB integration) | Search bar queries TMDB via server-side API proxy (`/api/tmdb/*`) with debounce (~300ms); results show below a separator from in-list results | Must Have |
|
|
|
| Add/remove movie | Tap a TMDB result to add it; poster, genres, title, year, and trailer URL auto-populate (trailer URL stored in DB and refreshed periodically); added-by attribution stored | Must Have |
|
|
| Add/remove movie | Tap a TMDB result to add it; poster, genres, title, year, and trailer URL auto-populate (trailer URL stored in DB and refreshed periodically); added-by attribution stored | Must Have |
|
|
|
-| Poster-forward grid view | 2-column evenly-scaling grid; each card shows movie poster (full bleed) with title below; added-by avatar overlaid top-right; binoculars emoji overlaid top-left when watched; infinite scroll loading 12 movies initially | Must Have |
|
|
|
|
|
|
|
+| Poster-forward grid view | 2-column evenly-scaling grid; each card shows movie poster (full bleed, using TMDB native sized URLs) with title below; added-by avatar overlaid top-right; binoculars emoji overlaid top-left when watched; infinite scroll loading 12 movies initially | Must Have |
|
|
|
| Expanded movie card (inline panel) | Tapping a poster expands a full-page-width panel downward, inserted below that row in the grid — not a modal or popup. Panel order (top to bottom): full-size poster → title → "Added by [username]" → genre tags → Watched It + Trailer (side by side) → Delete (centered below). Delete uses two-tap shake-and-confirm. Watched It toggles watched state. Trailer opens in new tab. Panel collapses on tap outside. | Must Have |
|
|
| Expanded movie card (inline panel) | Tapping a poster expands a full-page-width panel downward, inserted below that row in the grid — not a modal or popup. Panel order (top to bottom): full-size poster → title → "Added by [username]" → genre tags → Watched It + Trailer (side by side) → Delete (centered below). Delete uses two-tap shake-and-confirm. Watched It toggles watched state. Trailer opens in new tab. Panel collapses on tap outside. | Must Have |
|
|
|
| Genre filter | Tapping a genre tag in the expanded panel filters the grid to that genre | Must Have |
|
|
| Genre filter | Tapping a genre tag in the expanded panel filters the grid to that genre | Must Have |
|
|
|
| Roll the Dice | Large button pinned above the list; triggers an animated randomizer that lands on one unwatched movie from the group list | Must Have |
|
|
| Roll the Dice | Large button pinned above the list; triggers an animated randomizer that lands on one unwatched movie from the group list | Must Have |
|
|
|
| Re-roll | Tapping Roll again re-rolls from the same eligible pool | Must Have |
|
|
| Re-roll | Tapping Roll again re-rolls from the same eligible pool | Must Have |
|
|
|
| Genre + Emotion Roll | Secondary button accepting comma-separated genres and/or emotion keywords; maps emotions to genre IDs, filters pool, then rolls | Must Have |
|
|
| Genre + Emotion Roll | Secondary button accepting comma-separated genres and/or emotion keywords; maps emotions to genre IDs, filters pool, then rolls | Must Have |
|
|
|
| Watched state (per group, toggle) | Marking a movie watched moves it to a collapsed "Watched" section; marking again moves it back. Binoculars overlay and button color update in real time across all members. | Must Have |
|
|
| Watched state (per group, toggle) | Marking a movie watched moves it to a collapsed "Watched" section; marking again moves it back. Binoculars overlay and button color update in real time across all members. | Must Have |
|
|
|
-| Real-time list sync | Add, remove, and watched-status changes appear live on all connected group members' screens (Supabase real-time) | Must Have |
|
|
|
|
|
|
|
+| Real-time list sync | Add, remove, and watched-status changes appear live on all connected group members' screens (Supabase real-time); subscribe only to the currently-viewed list, unsubscribe on navigation away | Must Have |
|
|
|
| Logged-in home page | Upon login or return visit (stored user ID detected), user lands on a home page that mirrors the landing page layout but shows their lists as cards and replaces Login with Create List; Roll the Dice and Genre Roll roll across all user lists combined and display the result as a standalone teaser card on the home page (no navigation into a specific list) | Must Have |
|
|
| Logged-in home page | Upon login or return visit (stored user ID detected), user lands on a home page that mirrors the landing page layout but shows their lists as cards and replaces Login with Create List; Roll the Dice and Genre Roll roll across all user lists combined and display the result as a standalone teaser card on the home page (no navigation into a specific list) | Must Have |
|
|
|
| Multi-group support | A user can belong to more than one group; all their lists appear as cards on the home page | Should Have |
|
|
| Multi-group support | A user can belong to more than one group; all their lists appear as cards on the home page | Should Have |
|
|
|
| Invite code rotation | List Admin can regenerate the invite code to revoke access for anyone with the old code | Should Have |
|
|
| Invite code rotation | List Admin can regenerate the invite code to revoke access for anyone with the old code | Should Have |
|
|
|
-| Trailer URL periodic refresh | Background job on a bi-weekly cadence refreshes stored trailer URLs only for movies where trailer_url is currently null. Note: this behavior should be reassessed post-launch to also refresh stale URLs after a certain age. | Should Have |
|
|
|
|
|
-| Master Admin panel | Site-owner-only admin page with TOTP 2FA; can search and delete any list or user; credentials set via environment variables | Must Have |
|
|
|
|
|
|
|
+| Trailer URL periodic refresh | Background job via Supabase pg_cron on a bi-weekly cadence refreshes stored trailer URLs only for movies where trailer_url is currently null. Note: this behavior should be reassessed post-launch to also refresh stale URLs after a certain age. | Should Have |
|
|
|
|
|
+| Master Admin panel | Site-owner-only admin page with TOTP 2FA; can search and delete any list or user; credentials set via environment variables; session managed via iron-session (HttpOnly, Secure, SameSite=Strict cookie, 8-hour expiry) | Must Have |
|
|
|
|
|
|
|
|
### Out of Scope (Future)
|
|
### Out of Scope (Future)
|
|
|
|
|
|
|
@@ -87,50 +87,55 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
|
|
|
- Step 1: left-aligned
|
|
- Step 1: left-aligned
|
|
|
- Step 2: right-aligned
|
|
- Step 2: right-aligned
|
|
|
- Step 3: left-aligned (zigzag visual rhythm)
|
|
- Step 3: left-aligned (zigzag visual rhythm)
|
|
|
|
|
+8. Footer on all pages: TMDB attribution (logo + link + disclaimer), privacy policy link
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
### Onboarding (New User)
|
|
### Onboarding (New User)
|
|
|
```
|
|
```
|
|
|
1. User taps "Login / Get Started" on landing page
|
|
1. User taps "Login / Get Started" on landing page
|
|
|
2. User enters display name, optionally picks an avatar color
|
|
2. User enters display name, optionally picks an avatar color
|
|
|
-3. UUID-based account created; persisted in local storage
|
|
|
|
|
-4. Recovery code shown once — user prompted to save it
|
|
|
|
|
|
|
+3. Account created via Supabase Anonymous Sign-In (supabase.auth.signInAnonymously());
|
|
|
|
|
+ JWT issued and managed by Supabase GoTrue; session persisted automatically
|
|
|
|
|
+4. Recovery code (24 alphanumeric characters, 128-bit entropy) shown once — user prompted to save it
|
|
|
5. User selects: "Create a Group" or "Join with a Code"
|
|
5. User selects: "Create a Group" or "Join with a Code"
|
|
|
- A. Create → enter group name → group created, invite code shown (e.g., WOLF-42)
|
|
|
|
|
|
|
+ A. Create → enter group name → group created, invite code shown (e.g., WOLF-MOON)
|
|
|
→ creator assigned List Admin role → lands on home page (with their new list card shown)
|
|
→ creator assigned List Admin role → lands on home page (with their new list card shown)
|
|
|
- B. Join → enter invite code → validated → member role assigned → lands on home page
|
|
|
|
|
|
|
+ B. Join → enter invite code → validated (rate-limited) → member role assigned → lands on home page
|
|
|
(with the joined list card shown)
|
|
(with the joined list card shown)
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
### Logged-In Home Page (Returning or Newly Onboarded User)
|
|
### Logged-In Home Page (Returning or Newly Onboarded User)
|
|
|
```
|
|
```
|
|
|
-1. App reads stored user ID from browser (local storage or cookie)
|
|
|
|
|
-2. If user ID found → navigate directly to the home page (skip landing page)
|
|
|
|
|
-3. If user ID not found → show landing page (pre-login)
|
|
|
|
|
|
|
+1. App checks for valid Supabase Auth session
|
|
|
|
|
+2. If valid session found → navigate directly to the home page (skip landing page)
|
|
|
|
|
+3. If no session → show landing page (pre-login)
|
|
|
4. Home page layout mirrors the landing page layout, with these differences:
|
|
4. Home page layout mirrors the landing page layout, with these differences:
|
|
|
a. The Login button is replaced by a "Create List" button
|
|
a. The Login button is replaced by a "Create List" button
|
|
|
b. The About / how-it-works section is replaced by the user's list cards
|
|
b. The About / how-it-works section is replaced by the user's list cards
|
|
|
5. Each list card shows:
|
|
5. Each list card shows:
|
|
|
- List name (left-aligned)
|
|
- List name (left-aligned)
|
|
|
- - Number of movies with a 🎬 film emoji (right-aligned)
|
|
|
|
|
|
|
+ - Number of movies with a film emoji (right-aligned)
|
|
|
- Below: "Created by: [username]"
|
|
- Below: "Created by: [username]"
|
|
|
6. Tapping a list card navigates into that list view
|
|
6. Tapping a list card navigates into that list view
|
|
|
7. "Roll the Dice" and "Genre Roll" buttons at the top roll across ALL unwatched movies
|
|
7. "Roll the Dice" and "Genre Roll" buttons at the top roll across ALL unwatched movies
|
|
|
from ALL of the user's lists combined (cross-list roll)
|
|
from ALL of the user's lists combined (cross-list roll)
|
|
|
8. The roll result is displayed as a standalone teaser card directly on the home page.
|
|
8. The roll result is displayed as a standalone teaser card directly on the home page.
|
|
|
The result does NOT navigate the user into any specific list.
|
|
The result does NOT navigate the user into any specific list.
|
|
|
-9. If user ID not found (new device) → prompt for recovery code or start fresh
|
|
|
|
|
|
|
+9. If no session (new device) → prompt for recovery code or start fresh
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
### Adding a Movie
|
|
### Adding a Movie
|
|
|
```
|
|
```
|
|
|
1. User taps search bar at top of list view
|
|
1. User taps search bar at top of list view
|
|
|
-2. User types a movie title; TMDB is queried with ~300ms debounce
|
|
|
|
|
|
|
+2. User types a movie title; TMDB is queried via server-side proxy (/api/tmdb/search)
|
|
|
|
|
+ with ~300ms debounce
|
|
|
3. Results appear in two sections:
|
|
3. Results appear in two sections:
|
|
|
- Top: movies already in the group's list (labeled "In Your List")
|
|
- Top: movies already in the group's list (labeled "In Your List")
|
|
|
- Below separator: TMDB search results
|
|
- Below separator: TMDB search results
|
|
|
4. User taps a TMDB result → movie inserted into DB with poster, genres, title, year,
|
|
4. User taps a TMDB result → movie inserted into DB with poster, genres, title, year,
|
|
|
- trailer URL (fetched from TMDB and stored at add-time), and added-by attribution
|
|
|
|
|
|
|
+ trailer URL (fetched from TMDB via server proxy and stored at add-time;
|
|
|
|
|
+ validated against allowlist: youtube.com, themoviedb.org, imdb.com),
|
|
|
|
|
+ and added-by attribution
|
|
|
5. All group members see the new movie appear in real time
|
|
5. All group members see the new movie appear in real time
|
|
|
```
|
|
```
|
|
|
|
|
|
|
@@ -138,12 +143,13 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
|
|
|
```
|
|
```
|
|
|
1. Default view: 2-column evenly-scaling poster grid (3-4 columns on tablet/desktop)
|
|
1. Default view: 2-column evenly-scaling poster grid (3-4 columns on tablet/desktop)
|
|
|
2. Each card shows:
|
|
2. Each card shows:
|
|
|
- - Movie poster (full bleed)
|
|
|
|
|
|
|
+ - Movie poster (full bleed, using TMDB native sized URL: w342 for mobile grid)
|
|
|
- Movie title below the poster
|
|
- Movie title below the poster
|
|
|
- Added-by user avatar overlaid top-right corner
|
|
- Added-by user avatar overlaid top-right corner
|
|
|
- Binoculars emoji overlaid top-left corner — only when movie is watched
|
|
- Binoculars emoji overlaid top-left corner — only when movie is watched
|
|
|
3. Grid loads 12 movies initially; additional movies load automatically on scroll to bottom
|
|
3. Grid loads 12 movies initially; additional movies load automatically on scroll to bottom
|
|
|
4. No action buttons on collapsed grid cards — cards are tap-only
|
|
4. No action buttons on collapsed grid cards — cards are tap-only
|
|
|
|
|
+5. All poster images use native loading="lazy" attribute
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
### Expanded Movie Card (Inline Panel)
|
|
### Expanded Movie Card (Inline Panel)
|
|
@@ -152,7 +158,7 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
|
|
|
2. A full-page-width panel expands downward, inserted inline below that row in the grid
|
|
2. A full-page-width panel expands downward, inserted inline below that row in the grid
|
|
|
(mirrors Google Image Search inline expansion — not a modal, popup, or slide-up sheet)
|
|
(mirrors Google Image Search inline expansion — not a modal, popup, or slide-up sheet)
|
|
|
3. Panel contents, top to bottom:
|
|
3. Panel contents, top to bottom:
|
|
|
- a. Full-size movie poster
|
|
|
|
|
|
|
+ a. Full-size movie poster (TMDB native sized URL: w500)
|
|
|
b. Movie title
|
|
b. Movie title
|
|
|
c. "Added by [username]"
|
|
c. "Added by [username]"
|
|
|
d. Genre tags — tappable; each filters the grid to that genre
|
|
d. Genre tags — tappable; each filters the grid to that genre
|
|
@@ -167,7 +173,7 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
|
|
|
poster (top-left), movie moves to "Watched" section
|
|
poster (top-left), movie moves to "Watched" section
|
|
|
- If watched: button color reverts, binoculars overlay removed, movie returns to main list
|
|
- If watched: button color reverts, binoculars overlay removed, movie returns to main list
|
|
|
- Both indicators update simultaneously on all group members' screens
|
|
- Both indicators update simultaneously on all group members' screens
|
|
|
-6. Trailer: opens stored trailer URL in a new tab
|
|
|
|
|
|
|
+6. Trailer: opens stored trailer URL in a new tab (with rel="noopener noreferrer")
|
|
|
7. Tapping outside the panel or a close affordance collapses it
|
|
7. Tapping outside the panel or a close affordance collapses it
|
|
|
```
|
|
```
|
|
|
|
|
|
|
@@ -176,6 +182,7 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
|
|
|
1. User taps "Roll the Dice!" (pinned above the movie grid or on the home page)
|
|
1. User taps "Roll the Dice!" (pinned above the movie grid or on the home page)
|
|
|
2. Animated randomizer plays — scatter/flip/spin elimination sequence, 2-3 seconds
|
|
2. Animated randomizer plays — scatter/flip/spin elimination sequence, 2-3 seconds
|
|
|
(this is distinct from the landing page slot-machine reel animation)
|
|
(this is distinct from the landing page slot-machine reel animation)
|
|
|
|
|
+ Respects prefers-reduced-motion: if enabled, use a simple fade-in on the winner instead
|
|
|
3. Animation settles on one random unwatched movie from the eligible pool
|
|
3. Animation settles on one random unwatched movie from the eligible pool
|
|
|
- On a list page: pool is that list's unwatched movies
|
|
- On a list page: pool is that list's unwatched movies
|
|
|
- On the home page: pool is all unwatched movies across all of the user's lists combined
|
|
- On the home page: pool is all unwatched movies across all of the user's lists combined
|
|
@@ -226,12 +233,14 @@ List Deletion flow (separate from self-removal):
|
|
|
1. Master Admin navigates to /admin
|
|
1. Master Admin navigates to /admin
|
|
|
2. Login prompt: username + TOTP authenticator code (no password-only fallback)
|
|
2. Login prompt: username + TOTP authenticator code (no password-only fallback)
|
|
|
3. Credentials (username and TOTP secret) are set via environment variables — no first-run UI
|
|
3. Credentials (username and TOTP secret) are set via environment variables — no first-run UI
|
|
|
-4. On successful auth → Master Admin dashboard
|
|
|
|
|
|
|
+4. On successful auth → iron-session issues encrypted HttpOnly cookie (8-hour expiry)
|
|
|
|
|
+ → Master Admin dashboard
|
|
|
5. Available tools:
|
|
5. Available tools:
|
|
|
- Search any list by name or ID → view details → delete (with confirmation)
|
|
- Search any list by name or ID → view details → delete (with confirmation)
|
|
|
- Search any user by display name or ID → view details → delete (with confirmation)
|
|
- Search any user by display name or ID → view details → delete (with confirmation)
|
|
|
6. Master Admin session is separate from regular user sessions
|
|
6. Master Admin session is separate from regular user sessions
|
|
|
7. All /admin routes redirect to login if no valid admin session
|
|
7. All /admin routes redirect to login if no valid admin session
|
|
|
|
|
+8. TOTP secret rotation requires redeployment (documented operational constraint)
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
## 6. Usability Concerns
|
|
## 6. Usability Concerns
|
|
@@ -239,13 +248,15 @@ List Deletion flow (separate from self-removal):
|
|
|
- **Mobile-first**: All layouts designed for 375px+ screens first; desktop is a bonus. Grid expands to 3-4 columns on tablet/desktop.
|
|
- **Mobile-first**: All layouts designed for 375px+ screens first; desktop is a bonus. Grid expands to 3-4 columns on tablet/desktop.
|
|
|
- **PWA**: App should be installable to home screen and behave like a native app in browser.
|
|
- **PWA**: App should be installable to home screen and behave like a native app in browser.
|
|
|
- **Inline expansion**: The expanded movie panel is a full-page-width inline expansion inserted below the tapped card's row — never a modal, popup, slide-up sheet, or centered dialogue. Mirrors Google Image Search inline expansion behavior.
|
|
- **Inline expansion**: The expanded movie panel is a full-page-width inline expansion inserted below the tapped card's row — never a modal, popup, slide-up sheet, or centered dialogue. Mirrors Google Image Search inline expansion behavior.
|
|
|
|
|
+- **Inline panel keyboard navigation**: Panel must be operable via keyboard: Enter to open, Escape to close, focus moves into panel on open and returns to trigger on close. Use `aria-expanded` on trigger and `role="region"` on panel.
|
|
|
- **Two distinct roll animations**: The landing page uses a slot-machine reel animation (3 spinning reels of poster images). The in-app roll uses a scatter/flip/spin elimination animation. These must feel visually distinct — the landing animation is attention-grabbing and cinematic; the in-app animation is fast and satisfying but not distracting from repeat use.
|
|
- **Two distinct roll animations**: The landing page uses a slot-machine reel animation (3 spinning reels of poster images). The in-app roll uses a scatter/flip/spin elimination animation. These must feel visually distinct — the landing animation is attention-grabbing and cinematic; the in-app animation is fast and satisfying but not distracting from repeat use.
|
|
|
|
|
+- **Reduced motion**: Both animations must respect `prefers-reduced-motion`. Landing page reel: instant reveal or simple fade-in. In-app roll: fade to winner instead of scatter animation. Full animation remains the default.
|
|
|
- **Roll animation (in-app)**: Target 2-3 seconds max — visually impactful but not so long it frustrates repeat use.
|
|
- **Roll animation (in-app)**: Target 2-3 seconds max — visually impactful but not so long it frustrates repeat use.
|
|
|
- **Search responsiveness**: Debounce TMDB queries at ~300ms; show loading state during fetch.
|
|
- **Search responsiveness**: Debounce TMDB queries at ~300ms; show loading state during fetch.
|
|
|
- **Real-time updates**: New movies and watched status changes appear without a page refresh via Supabase subscriptions.
|
|
- **Real-time updates**: New movies and watched status changes appear without a page refresh via Supabase subscriptions.
|
|
|
-- **Offline tolerance**: If connection drops, the app should degrade gracefully — show cached list, disable write actions with an explanatory message.
|
|
|
|
|
|
|
+- **Offline tolerance**: If connection drops, the app should degrade gracefully — show cached list (via TanStack Query persistence), disable write actions with an explanatory message. Do not queue offline writes.
|
|
|
- **Accessibility**: Sufficient color contrast on poster overlays; tap targets minimum 44x44px; screen reader labels on icon buttons.
|
|
- **Accessibility**: Sufficient color contrast on poster overlays; tap targets minimum 44x44px; screen reader labels on icon buttons.
|
|
|
-- **TMDB API**: Cache recent search results client-side to reduce API calls and stay within free tier rate limits.
|
|
|
|
|
|
|
+- **TMDB API**: All TMDB calls routed through server-side proxy (`/api/tmdb/*`). Cache recent search results via TanStack Query with explicit `staleTime` configuration.
|
|
|
|
|
|
|
|
## 7. Technical Considerations
|
|
## 7. Technical Considerations
|
|
|
|
|
|
|
@@ -253,28 +264,55 @@ List Deletion flow (separate from self-removal):
|
|
|
|
|
|
|
|
| Layer | Choice | Notes |
|
|
| Layer | Choice | Notes |
|
|
|
|-------|--------|-------|
|
|
|-------|--------|-------|
|
|
|
-| Frontend | Next.js (React, App Router) | PWA support, Vercel deployment |
|
|
|
|
|
|
|
+| Frontend | Next.js (React, App Router) | PWA support; `output: 'standalone'` for Docker |
|
|
|
| Styling | Tailwind CSS | Mobile-first, fast iteration |
|
|
| Styling | Tailwind CSS | Mobile-first, fast iteration |
|
|
|
-| Backend / Database | Supabase | Postgres + real-time subscriptions |
|
|
|
|
|
-| Movie Data | TMDB API (free tier) | Posters, genres, metadata, trailer URLs |
|
|
|
|
|
-| State Management | TanStack Query (React Query) | Server state sync, caching, loading states |
|
|
|
|
|
|
|
+| Backend / Database | Supabase (self-hosted) | Postgres + real-time subscriptions + GoTrue auth; full Docker stack |
|
|
|
|
|
+| Auth | Supabase Anonymous Sign-In | `signInAnonymously()` — no email, instant account, JWT for RLS |
|
|
|
|
|
+| Movie Data | TMDB API | Posters, genres, metadata, trailer URLs; all calls via server-side proxy |
|
|
|
|
|
+| State Management | TanStack Query (React Query) | Server state sync, caching with explicit `staleTime`, loading states |
|
|
|
|
|
+| Admin Sessions | iron-session | Encrypted HttpOnly cookie for Master Admin TOTP sessions |
|
|
|
| 2FA (Master Admin) | TOTP via otplib (or equivalent) | Authenticator-app compatible; TOTP secret never exposed client-side |
|
|
| 2FA (Master Admin) | TOTP via otplib (or equivalent) | Authenticator-app compatible; TOTP secret never exposed client-side |
|
|
|
-| Deployment | Vercel (frontend) + Supabase (hosted) | Both free tiers sufficient for MVP scale |
|
|
|
|
|
|
|
+| PWA | @serwist/next | App Router compatible, Workbox 7, actively maintained |
|
|
|
|
|
+| Image Optimization | sharp (local assets only) | TMDB posters use native sized URLs from TMDB CDN directly (not next/image) |
|
|
|
|
|
+| Background Jobs | Supabase pg_cron + Edge Functions | Runs on self-hosted Postgres; replaces Vercel Cron |
|
|
|
|
|
+| Reverse Proxy | Caddy | HTTPS termination (required for PWA, wss://, secure cookies) |
|
|
|
|
|
+| Linting / Formatting | ESLint + Prettier + TypeScript strict | next/core-web-vitals + next/typescript presets; husky + lint-staged pre-commit |
|
|
|
|
|
+| Testing | Vitest (unit) + Playwright (E2E) | Unit tests for pure logic; E2E for critical paths |
|
|
|
|
|
+| Env Validation | zod (or t3-env) | Validate all required env vars at startup |
|
|
|
|
|
+| Runtime | Node.js 22 LTS | node:22-slim Docker base image |
|
|
|
|
|
+
|
|
|
|
|
+### TMDB Image Strategy
|
|
|
|
|
+
|
|
|
|
|
+TMDB posters are served directly from TMDB's CDN using native sized URLs — not processed through `next/image` optimization (which would run `sharp` in-container and create CPU/memory pressure in Docker). Use these TMDB size variants:
|
|
|
|
|
+- `w342` — grid thumbnails (mobile)
|
|
|
|
|
+- `w185` — reel animation posters
|
|
|
|
|
+- `w500` — expanded inline panel
|
|
|
|
|
+- Add `loading="lazy"` attribute on all poster `<img>` tags
|
|
|
|
|
+- Reserve `next/image` for locally-served assets only (logo, icons)
|
|
|
|
|
+- Install `sharp` as an explicit production dependency for local asset optimization
|
|
|
|
|
+
|
|
|
|
|
+### TMDB Attribution
|
|
|
|
|
+
|
|
|
|
|
+TMDB Terms of Service require visible attribution on every page: the TMDB logo, a link to themoviedb.org, and the disclaimer "This product uses the TMDB API but is not endorsed or certified by TMDB." Implement as a site-wide footer component. Non-compliance risks API key revocation.
|
|
|
|
|
+
|
|
|
|
|
+### TMDB API Proxy
|
|
|
|
|
+
|
|
|
|
|
+All TMDB API calls must be routed through Next.js API Route Handlers (`/api/tmdb/*`). The `TMDB_API_KEY` environment variable must NEVER use the `NEXT_PUBLIC_` prefix — it must remain server-side only. This is a TMDB Terms of Service requirement. The proxy also enables server-side response caching via `Cache-Control` headers.
|
|
|
|
|
|
|
|
### Data Model
|
|
### Data Model
|
|
|
|
|
|
|
|
**users**
|
|
**users**
|
|
|
-- id (UUID, primary key)
|
|
|
|
|
-- display_name (text)
|
|
|
|
|
|
|
+- id (UUID, primary key — maps to Supabase Auth UID from `signInAnonymously()`)
|
|
|
|
|
+- display_name (text, CHECK: 1-30 characters, no HTML angle brackets or control characters, Unicode letters allowed)
|
|
|
- avatar_color (text, hex)
|
|
- avatar_color (text, hex)
|
|
|
-- recovery_code (text, hashed)
|
|
|
|
|
-- is_master_admin (boolean, default false)
|
|
|
|
|
|
|
+- recovery_code (text, hashed with Argon2id; 24 alphanumeric characters / 128-bit entropy; single-use)
|
|
|
|
|
+- last_active_at (timestamp — updated on login/activity; used for 12-month retention policy)
|
|
|
- created_at
|
|
- created_at
|
|
|
|
|
|
|
|
**groups**
|
|
**groups**
|
|
|
- id (UUID, primary key)
|
|
- id (UUID, primary key)
|
|
|
-- name (text)
|
|
|
|
|
-- invite_code (text, unique, short human-readable)
|
|
|
|
|
|
|
+- name (text, CHECK: 1-50 characters, no HTML angle brackets or control characters, Unicode letters allowed)
|
|
|
|
|
+- invite_code (text, unique, WORD-WORD human-readable format)
|
|
|
- created_by (FK → users.id)
|
|
- created_by (FK → users.id)
|
|
|
- created_at
|
|
- created_at
|
|
|
|
|
|
|
@@ -290,9 +328,9 @@ List Deletion flow (separate from self-removal):
|
|
|
- tmdb_id (integer)
|
|
- tmdb_id (integer)
|
|
|
- title (text)
|
|
- title (text)
|
|
|
- year (integer)
|
|
- year (integer)
|
|
|
-- poster_path (text, TMDB relative path — full URL constructed at render time)
|
|
|
|
|
|
|
+- poster_path (text, TMDB relative path — full URL constructed at render time using TMDB native sizes)
|
|
|
- genres (text[], TMDB genre labels)
|
|
- genres (text[], TMDB genre labels)
|
|
|
-- trailer_url (text, nullable — fetched from TMDB at add-time and stored; background job refreshes only null entries on a bi-weekly cadence)
|
|
|
|
|
|
|
+- trailer_url (text, nullable — fetched from TMDB at add-time via server proxy and stored; validated against domain allowlist: youtube.com, themoviedb.org, imdb.com; background job refreshes only null entries on a bi-weekly cadence)
|
|
|
- trailer_url_refreshed_at (timestamp — tracks when the trailer URL was last fetched, used by the refresh job)
|
|
- trailer_url_refreshed_at (timestamp — tracks when the trailer URL was last fetched, used by the refresh job)
|
|
|
- added_by (FK → users.id)
|
|
- added_by (FK → users.id)
|
|
|
- watched (boolean, default false)
|
|
- watched (boolean, default false)
|
|
@@ -304,30 +342,67 @@ List Deletion flow (separate from self-removal):
|
|
|
- tmdb_id (integer)
|
|
- tmdb_id (integer)
|
|
|
- poster_path (text)
|
|
- poster_path (text)
|
|
|
- title (text)
|
|
- title (text)
|
|
|
-- refreshed_at (timestamp — set by the periodic reel refresh job)
|
|
|
|
|
|
|
+- refreshed_at (timestamp — set by the periodic reel refresh job via pg_cron)
|
|
|
- (Table holds ~20 rows; entire set replaced on each refresh from TMDB popular/top-rated)
|
|
- (Table holds ~20 rows; entire set replaced on each refresh from TMDB popular/top-rated)
|
|
|
|
|
|
|
|
**admin_sessions**
|
|
**admin_sessions**
|
|
|
-- Secure server-side token store for Master Admin TOTP-authenticated sessions; separate from regular user sessions
|
|
|
|
|
|
|
+- Managed via iron-session encrypted HttpOnly cookies; no database table required. Session expiry: 8 hours. Cookies set with Secure and SameSite=Strict flags.
|
|
|
|
|
+
|
|
|
|
|
+### Row Level Security (RLS)
|
|
|
|
|
+
|
|
|
|
|
+RLS must be enabled on ALL tables with explicit policies — no permissive catch-all. Supabase's anon key is public by design; RLS is the authorization mechanism. Policies use `auth.uid()` from the JWT issued by Supabase Anonymous Sign-In. Define policies alongside the schema in Phase 1.2. Key rules:
|
|
|
|
|
+- **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; insertable via valid invite code flow; deletable by admins or self (leave)
|
|
|
|
|
+- **movies**: Full CRUD for members of the owning group only
|
|
|
|
|
+- **landing_reel_posters**: Readable by anyone (public); writable only by service role (pg_cron job)
|
|
|
|
|
+
|
|
|
|
|
+Supabase Realtime also respects RLS — subscriptions are authorized by the same policies.
|
|
|
|
|
+
|
|
|
|
|
+### Database Migrations
|
|
|
|
|
+
|
|
|
|
|
+Use `supabase migration new` via the Supabase CLI. All migrations stored in version control. Migrations must be the sole mechanism for schema changes — no ad-hoc SQL in production.
|
|
|
|
|
|
|
|
### Privacy
|
|
### Privacy
|
|
|
- No email addresses collected in MVP
|
|
- No email addresses collected in MVP
|
|
|
- Display names only — no real identity data
|
|
- Display names only — no real identity data
|
|
|
-- Recovery codes hashed before storage
|
|
|
|
|
-- Invite codes are the sole access control mechanism for regular users
|
|
|
|
|
|
|
+- Recovery codes hashed with Argon2id before storage
|
|
|
|
|
+- Invite codes are the primary access control mechanism for regular users; join endpoint rate-limited
|
|
|
- Master Admin credentials (username) and TOTP secret are stored as environment variables server-side only; never exposed client-side
|
|
- Master Admin credentials (username) and TOTP secret are stored as environment variables server-side only; never exposed client-side
|
|
|
- TMDB data is public — no privacy concern
|
|
- TMDB data is public — no privacy concern
|
|
|
|
|
+- Privacy policy page required: factual description of what data is stored (anonymous UUID, display name, group membership, movie preferences, server logs with IPs), how it is used, and user rights
|
|
|
|
|
+- **Data retention**: Inactive accounts (no activity for 12 months) are automatically deleted. Users cannot be notified before deletion due to anonymous auth (no email). This is documented in the privacy policy.
|
|
|
|
|
+- TMDB attribution displayed on all pages per Terms of Service
|
|
|
|
|
+
|
|
|
|
|
+### Security Headers
|
|
|
|
|
+
|
|
|
|
|
+Configure HTTP security headers in `next.config.js` or at the Caddy reverse proxy level:
|
|
|
|
|
+- `Content-Security-Policy` — restrict `img-src` to `image.tmdb.org`, `connect-src` to Supabase `wss://`, etc.
|
|
|
|
|
+- `X-Frame-Options: DENY`
|
|
|
|
|
+- `X-Content-Type-Options: nosniff`
|
|
|
|
|
+- `Referrer-Policy: strict-origin-when-cross-origin`
|
|
|
|
|
+- `Strict-Transport-Security` (HSTS)
|
|
|
|
|
+- `Permissions-Policy`
|
|
|
|
|
+- Use `Content-Security-Policy-Report-Only` during development to identify violations without blocking
|
|
|
|
|
|
|
|
### Deployment
|
|
### Deployment
|
|
|
-- Vercel auto-deploys from main branch
|
|
|
|
|
-- Supabase on free tier; upgrade path available if scale demands
|
|
|
|
|
|
|
+- **Self-hosted Docker deployment** orchestrated via docker-compose
|
|
|
|
|
+- **Next.js app container**: multi-stage Dockerfile with `node:22-slim`, `output: 'standalone'`, non-root user, `tini` for PID 1 signal handling, `.dockerignore`
|
|
|
|
|
+- **Supabase self-hosted**: full Docker stack (Postgres, GoTrue, Realtime, PostgREST, Kong, Studio) using Supabase's official docker-compose configuration adapted for this project
|
|
|
|
|
+- **Reverse proxy**: Caddy for HTTPS termination (required for PWA service workers, `wss://` Supabase Realtime, and secure cookies)
|
|
|
|
|
+- **Health check**: `/api/health` endpoint checking Supabase connectivity; used by Docker `HEALTHCHECK`
|
|
|
|
|
+- **Background jobs**: Supabase pg_cron triggering Edge Functions (local Deno runtime in Docker) for landing reel refresh and trailer URL refresh
|
|
|
- Environment variables required:
|
|
- Environment variables required:
|
|
|
- - `TMDB_API_KEY`
|
|
|
|
|
|
|
+ - `TMDB_API_KEY` (server-side only — never `NEXT_PUBLIC_`)
|
|
|
- `SUPABASE_URL`
|
|
- `SUPABASE_URL`
|
|
|
- `SUPABASE_ANON_KEY`
|
|
- `SUPABASE_ANON_KEY`
|
|
|
|
|
+ - `SUPABASE_SERVICE_ROLE_KEY` (for server-side admin operations)
|
|
|
- `MASTER_ADMIN_USERNAME` — the master admin login username
|
|
- `MASTER_ADMIN_USERNAME` — the master admin login username
|
|
|
- `MASTER_ADMIN_TOTP_SECRET` — the TOTP secret (base32); configure this in your authenticator app (e.g., Google Authenticator, Authy) before first use
|
|
- `MASTER_ADMIN_TOTP_SECRET` — the TOTP secret (base32); configure this in your authenticator app (e.g., Google Authenticator, Authy) before first use
|
|
|
|
|
+ - `IRON_SESSION_SECRET` — 32+ character secret for iron-session cookie encryption
|
|
|
|
|
+- All environment variables validated at startup via zod (or t3-env); missing or malformed variables produce a clear startup failure
|
|
|
- No first-run setup UI exists; the master admin account is fully configured via environment variables before deployment
|
|
- No first-run setup UI exists; the master admin account is fully configured via environment variables before deployment
|
|
|
|
|
+- **CI**: Linting, type-checking, and build validation enforced via husky pre-push hook (no external CI platform required)
|
|
|
|
|
|
|
|
## 8. Implementation Plan
|
|
## 8. Implementation Plan
|
|
|
|
|
|
|
@@ -336,64 +411,66 @@ List Deletion flow (separate from self-removal):
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### Phase 1: Foundation (April 5-9, 2026) — MVP
|
|
|
|
|
-- [ ] 1.1 — Initialize Next.js project with Tailwind CSS and App Router
|
|
|
|
|
-- [ ] 1.2 — Set up Supabase project; create schema (users, groups, group_members, movies, landing_reel_posters tables)
|
|
|
|
|
-- [ ] 1.3 — Configure Supabase client in Next.js with environment variables
|
|
|
|
|
-- [ ] 1.4 — Implement anonymous user creation: display name input, UUID generation, local storage persistence
|
|
|
|
|
-- [ ] 1.5 — Implement recovery code generation and show-once display screen
|
|
|
|
|
-- [ ] 1.6 — Implement recovery code claim flow (enter code → restore identity on new device)
|
|
|
|
|
-- [ ] 1.7 — Deploy skeleton to Vercel; confirm Supabase connection works in production
|
|
|
|
|
-
|
|
|
|
|
-### Phase 2: Groups and Permissions (April 9-13, 2026) — MVP
|
|
|
|
|
-- [ ] 2.1 — Build "Create a Group" flow: name input, invite code generation (short human-readable format, e.g., WOLF-42), store in DB, assign creator as List Admin
|
|
|
|
|
-- [ ] 2.2 — Build "Join with a Code" flow: code entry, validation, group_members record with role: 'member'
|
|
|
|
|
-- [ ] 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 + 🎬 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)
|
|
|
|
|
|
|
+### 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
|
|
|
|
|
+- [ ] 1.2 — Set up self-hosted Supabase Docker stack; 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 (see RLS section); initialize Supabase CLI migrations workflow — all schema changes via `supabase migration new`
|
|
|
|
|
+- [ ] 1.3 — Configure Supabase client in Next.js with environment variables; implement env var validation at startup via zod (or t3-env); create TMDB API proxy route (`/api/tmdb/*`) to keep API key server-side
|
|
|
|
|
+- [ ] 1.4 — Implement anonymous auth via `supabase.auth.signInAnonymously()`; display name input and optional avatar color picker; session managed by Supabase GoTrue (JWT issued automatically)
|
|
|
|
|
+- [ ] 1.5 — Implement recovery code generation (24 alphanumeric characters, 128-bit entropy), Argon2id hashing, and show-once display screen
|
|
|
|
|
+- [ ] 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
|
|
|
|
|
+- [ ] 1.7 — Build Docker infrastructure: multi-stage Dockerfile (node:22-slim, non-root user, tini), docker-compose.yml orchestrating Next.js app + self-hosted Supabase stack + Caddy reverse proxy, .dockerignore, /api/health endpoint; deploy and confirm Supabase connection works in production
|
|
|
|
|
+- [ ] 1.8 — Add Sentry error monitoring (free tier) for production error tracking from day one
|
|
|
|
|
+
|
|
|
|
|
+### 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), store in DB, assign creator as List Admin; document API routes in markdown
|
|
|
|
|
+- [ ] 2.2 — Build "Join with a Code" flow: code entry, validation, group_members record with role: 'member'; rate-limit join endpoint (5-10 failed attempts per IP per 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
|
|
|
- [ ] 2.4 — Implement List Admin settings: rename list, regenerate invite code, display + copy invite code, view + remove members; implement "Leave this list" self-removal for admin — if other members exist, show "Transfer Ownership" popup (member list + Cancel button); on confirm, transfer ownership and remove original admin from the list; if admin is last member, delete the list with confirmation
|
|
- [ ] 2.4 — Implement List Admin settings: rename list, regenerate invite code, display + copy invite code, view + remove members; implement "Leave this list" self-removal for admin — if other members exist, show "Transfer Ownership" popup (member list + Cancel button); on confirm, transfer ownership and remove original admin from the list; if admin is last member, delete the list with confirmation
|
|
|
- [ ] 2.5 — Implement List Admin direct deletion: "Delete the list" action in settings shows a standard confirmation prompt and permanently deletes the list; does NOT trigger the Transfer Ownership popup
|
|
- [ ] 2.5 — Implement List Admin direct deletion: "Delete the list" action in settings shows a standard confirmation prompt and permanently deletes the list; does NOT trigger the Transfer Ownership popup
|
|
|
- [ ] 2.6 — Implement regular user settings: leave list option only
|
|
- [ ] 2.6 — Implement regular user settings: leave list option only
|
|
|
|
|
|
|
|
-### Phase 3: Movie List Core (April 13-19, 2026) — MVP
|
|
|
|
|
-- [ ] 3.1 — Integrate TMDB API: search endpoint, poster URL construction, trailer URL fetch at add-time; implement client-side result caching to stay within free tier rate limits
|
|
|
|
|
|
|
+### Phase 3: Movie List Core (April 14-20, 2026) — MVP
|
|
|
|
|
+- [ ] 3.1 — Integrate TMDB API via server-side proxy (`/api/tmdb/*`): search endpoint, poster URL construction using TMDB native sizes (w342 grid, w185 reel, w500 expanded), 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.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 at add-time), trailer_url_refreshed_at, and added_by attribution
|
|
|
|
|
-- [ ] 3.4 — Build 2-column poster grid: full-bleed poster, title below, added-by avatar overlaid top-right, binoculars emoji overlaid top-left (watched only), no action buttons on cards, tap-only interaction
|
|
|
|
|
|
|
+- [ ] 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
|
|
|
|
|
+- [ ] 3.4 — Build 2-column poster grid: full-bleed poster using TMDB native sized URLs (w342) with `loading="lazy"`, title below, added-by avatar overlaid top-right, binoculars emoji overlaid top-left (watched only), no action buttons on cards, tap-only interaction
|
|
|
- [ ] 3.5 — Implement infinite scroll: load 12 movies initially, fetch and append next batch automatically on scroll to bottom
|
|
- [ ] 3.5 — Implement infinite scroll: load 12 movies initially, fetch and append next batch automatically on scroll to bottom
|
|
|
-- [ ] 3.6 — Build expanded movie inline panel: full-page-width expansion inserted below the tapped row (not a modal); fixed element order (poster → title → Added by → genre tags → Watched It + Trailer side-by-side → Delete centered below); delete two-tap shake-and-confirm; Watched It toggle; Trailer opens stored trailer_url in new tab; collapse on tap outside
|
|
|
|
|
|
|
+- [ ] 3.6 — Build expanded movie inline panel: full-page-width expansion inserted below the tapped row (not a modal); fixed element order (poster at w500 → title → Added by → genre tags → Watched It + Trailer side-by-side → Delete centered below); delete two-tap shake-and-confirm; Watched It toggle; Trailer opens stored trailer_url in new tab with rel="noopener noreferrer"; collapse on tap outside
|
|
|
- [ ] 3.7 — Implement genre filter: tapping a genre tag in the inline panel filters the grid to that genre
|
|
- [ ] 3.7 — Implement genre filter: tapping a genre tag in the inline panel filters the grid to that genre
|
|
|
- [ ] 3.8 — Implement Watched state: toggle marks/unmarks movie as watched for the group; watched movies move to collapsed "Watched" section; binoculars overlay and button color update simultaneously across all members
|
|
- [ ] 3.8 — Implement Watched state: toggle marks/unmarks movie as watched for the group; watched movies move to collapsed "Watched" section; binoculars overlay and button color update simultaneously across all members
|
|
|
-- [ ] 3.9 — Enable Supabase real-time subscriptions on movies table for live add, remove, and watched-status updates
|
|
|
|
|
|
|
+- [ ] 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 19-22, 2026) — MVP
|
|
|
|
|
|
|
+### Phase 4: Randomizer (April 20-23, 2026) — MVP
|
|
|
- [ ] 4.1 — Build "Roll the Dice!" and "Genre Roll!" button layout pinned above the movie grid
|
|
- [ ] 4.1 — Build "Roll the Dice!" and "Genre Roll!" button layout pinned above the movie grid
|
|
|
- [ ] 4.2 — Implement randomizer logic: select a random unwatched movie from the eligible pool (single list when in a list view; all user lists combined when on the home page); on the home page, render result as a standalone teaser card in place — do not navigate into any list
|
|
- [ ] 4.2 — Implement randomizer logic: select a random unwatched movie from the eligible pool (single list when in a list view; all user lists combined when on the home page); on the home page, render result as a standalone teaser card in place — do not navigate into any list
|
|
|
-- [ ] 4.3 — Build in-app roll animation: scatter/flip/spin elimination sequence landing on winner (target 2-3 seconds); test performance on low-end mobile devices
|
|
|
|
|
|
|
+- [ ] 4.3 — Build in-app roll animation: scatter/flip/spin elimination sequence landing on winner (target 2-3 seconds); test performance on low-end mobile devices; respect `prefers-reduced-motion` (use simple fade-in on winner when enabled)
|
|
|
- [ ] 4.4 — Implement re-roll on second tap of Roll button
|
|
- [ ] 4.4 — Implement re-roll on second tap of Roll button
|
|
|
- [ ] 4.5 — Build Genre Roll text input UI
|
|
- [ ] 4.5 — Build Genre Roll text input UI
|
|
|
-- [ ] 4.6 — Implement emotion-to-genre static mapping (see Section 10 reference table)
|
|
|
|
|
|
|
+- [ ] 4.6 — Implement emotion-to-genre mapping as a static TypeScript constant (see Section 10 reference table); translate genre labels to TMDB numeric genre IDs
|
|
|
- [ ] 4.7 — Implement genre + emotion filter logic: normalize input (lowercase, tokenize on spaces/commas), map emotions to TMDB genre IDs, filter unwatched pool; if no matches, show "No matches — showing full list" and proceed unfiltered
|
|
- [ ] 4.7 — Implement genre + emotion filter logic: normalize input (lowercase, tokenize on spaces/commas), map emotions to TMDB genre IDs, filter unwatched pool; if no matches, show "No matches — showing full list" and proceed unfiltered
|
|
|
-- [ ] 4.8 — Apply roll animation to genre-filtered results
|
|
|
|
|
|
|
+- [ ] 4.8 — Apply roll animation to genre-filtered results; document API routes in markdown
|
|
|
|
|
|
|
|
-### Phase 5: Landing Page and MVP Polish (April 22-26, 2026) — MVP CUTOFF
|
|
|
|
|
-- [ ] 5.1 — Build landing page structure: centered logo, splash text, Roll + Genre Roll buttons, Login button, scrolling About section, 3-step how-it-works demo (Step 1 left, Step 2 right, Step 3 left)
|
|
|
|
|
-- [ ] 5.2 — Build and seed landing_reel_posters table: implement periodic refresh job (suggested cadence: bi-weekly) that automatically fetches ~20 posters from TMDB popular or top-rated endpoints (no manual curation) and replaces the full set in the DB on each run
|
|
|
|
|
-- [ ] 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, decelerate, and land on a single TMDB movie result (the final result is fetched live from TMDB — not constrained to the reel poster set)
|
|
|
|
|
-- [ ] 5.4 — Wire landing page Roll the Dice and Genre Roll to TMDB directly (no login required); display result as a static teaser card showing poster, title, and genres — no link, no tap action
|
|
|
|
|
|
|
+### Phase 5: Landing Page and MVP Polish (April 23-26, 2026) — MVP CUTOFF
|
|
|
|
|
+- [ ] 5.1 — Build landing page structure: centered logo, splash text, Roll + Genre Roll buttons, Login button, scrolling About section, 3-step how-it-works demo (Step 1 left, Step 2 right, Step 3 left); add site-wide footer with TMDB attribution (logo + link + disclaimer) and privacy policy link; build privacy policy page
|
|
|
|
|
+- [ ] 5.2 — Build and seed landing_reel_posters table: implement periodic refresh job via Supabase pg_cron + Edge Function (local Deno runtime) on bi-weekly cadence that automatically fetches ~20 posters from TMDB popular or top-rated endpoints (no manual curation) and replaces the full set in the DB on each run
|
|
|
|
|
+- [ ] 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), decelerate, and land on a single TMDB movie result (the final result is fetched live from TMDB via server proxy — not constrained to the reel poster set); respect `prefers-reduced-motion` (instant reveal or simple fade-in when enabled)
|
|
|
|
|
+- [ ] 5.4 — Wire landing page Roll the Dice and Genre Roll to TMDB via server proxy (no login required); display result as a static teaser card showing poster, 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)
|
|
- [ ] 5.5 — Loading and empty states for all major views (empty list, no search results, no genre matches, empty home page for new users)
|
|
|
- [ ] 5.6 — Error handling: invalid invite code, TMDB API failure, network errors
|
|
- [ ] 5.6 — Error handling: invalid invite code, TMDB API failure, network errors
|
|
|
-- [ ] 5.7 — Final MVP smoke test and Vercel production deployment
|
|
|
|
|
|
|
+- [ ] 5.7 — Configure HTTP security headers (CSP, X-Frame-Options, HSTS, etc.) in next.config.js or Caddy; use Report-Only mode during testing
|
|
|
|
|
+- [ ] 5.8 — Final MVP smoke test and Docker production deployment
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
### Phase 6: Trailer URL Refresh (April 27 - May 3, 2026) — Post-MVP
|
|
### Phase 6: Trailer URL Refresh (April 27 - May 3, 2026) — Post-MVP
|
|
|
-- [ ] 6.1 — Implement background trailer URL refresh job: query movies where trailer_url IS NULL (bi-weekly cadence); fetch trailer URL from TMDB for each; update trailer_url and trailer_url_refreshed_at in DB. NOTE: this scope (null-only) should be reassessed post-launch to also refresh stale/aging URLs after a certain age threshold.
|
|
|
|
|
-- [ ] 6.2 — Set up job scheduling (Vercel Cron or Supabase scheduled function); confirm job runs reliably in production
|
|
|
|
|
|
|
+- [ ] 6.1 — Implement background trailer URL refresh job: query movies where trailer_url IS NULL (bi-weekly cadence); fetch trailer URL from TMDB via server proxy for each; validate against domain allowlist; update trailer_url and trailer_url_refreshed_at in DB. NOTE: this scope (null-only) should be reassessed post-launch to also refresh stale/aging URLs after a certain age threshold.
|
|
|
|
|
+- [ ] 6.2 — Set up job scheduling via Supabase pg_cron + Edge Function (local Deno runtime); confirm job runs reliably in production
|
|
|
- [ ] 6.3 — Add monitoring/logging for refresh failures (e.g., TMDB returned no trailer) so missing URLs can be investigated
|
|
- [ ] 6.3 — Add monitoring/logging for refresh failures (e.g., TMDB returned no trailer) so missing URLs can be investigated
|
|
|
|
|
|
|
|
### Phase 7: Master Admin (May 4-17, 2026) — Post-MVP
|
|
### Phase 7: Master Admin (May 4-17, 2026) — Post-MVP
|
|
|
-- [ ] 7.1 — Document Master Admin setup: MASTER_ADMIN_USERNAME and MASTER_ADMIN_TOTP_SECRET must be set as environment variables before first use; provide instructions for generating and enrolling the TOTP secret in an authenticator app
|
|
|
|
|
|
|
+- [ ] 7.1 — Document Master Admin setup: MASTER_ADMIN_USERNAME and MASTER_ADMIN_TOTP_SECRET must be set as environment variables before first use; provide instructions for generating and enrolling the TOTP secret in an authenticator app; document that TOTP secret rotation requires redeployment
|
|
|
- [ ] 7.2 — Build /admin login route: username + TOTP code input, no password-only fallback; reads credentials from environment variables
|
|
- [ ] 7.2 — Build /admin login route: username + TOTP code input, no password-only fallback; reads credentials from environment variables
|
|
|
-- [ ] 7.3 — Implement server-side TOTP verification (otplib); issue admin session token on success
|
|
|
|
|
|
|
+- [ ] 7.3 — Implement server-side TOTP verification (otplib); issue admin session via iron-session (encrypted HttpOnly cookie, Secure, SameSite=Strict, 8-hour expiry)
|
|
|
- [ ] 7.4 — Build Master Admin dashboard UI
|
|
- [ ] 7.4 — Build Master Admin dashboard UI
|
|
|
- [ ] 7.5 — Implement list search and deletion (by name or ID, with confirmation)
|
|
- [ ] 7.5 — Implement list search and deletion (by name or ID, with confirmation)
|
|
|
- [ ] 7.6 — Implement user search and deletion (by display name or ID, with confirmation)
|
|
- [ ] 7.6 — Implement user search and deletion (by display name or ID, with confirmation)
|
|
@@ -401,29 +478,34 @@ List Deletion flow (separate from self-removal):
|
|
|
- [ ] 7.8 — End-to-end TOTP test with a real authenticator app (Google Authenticator, Authy)
|
|
- [ ] 7.8 — End-to-end TOTP test with a real authenticator app (Google Authenticator, Authy)
|
|
|
|
|
|
|
|
### Phase 8: PWA and Accessibility (May 18-31, 2026) — Post-MVP
|
|
### Phase 8: PWA and Accessibility (May 18-31, 2026) — Post-MVP
|
|
|
-- [ ] 8.1 — Add PWA manifest and service worker for home screen installation
|
|
|
|
|
-- [ ] 8.2 — Implement offline graceful degradation (show cached list, disable write actions with message)
|
|
|
|
|
-- [ ] 8.3 — Accessibility pass: contrast ratios, tap target sizes (min 44x44px), aria labels on icon buttons
|
|
|
|
|
-- [ ] 8.4 — Performance tuning: poster lazy loading, infinite scroll, search debounce, animation on low-end devices
|
|
|
|
|
|
|
+- [ ] 8.1 — Add PWA manifest and service worker via `@serwist/next` for home screen installation
|
|
|
|
|
+- [ ] 8.2 — Implement offline graceful degradation: cached list data via TanStack Query `persistQueryClient` (IndexedDB), disable write actions with message; do not queue offline writes
|
|
|
|
|
+- [ ] 8.3 — Accessibility pass: contrast ratios, tap target sizes (min 44x44px), aria labels on icon buttons; inline panel keyboard navigation and focus management (Enter to open, Escape to close, focus trap on open, focus return on close, `aria-expanded` on trigger, `role="region"` on panel)
|
|
|
|
|
+- [ ] 8.4 — Performance tuning: poster lazy loading verification, infinite scroll performance, search debounce, animation on low-end devices; consider virtualized scrolling (`@tanstack/react-virtual`) if grid performance degrades with large lists
|
|
|
|
|
|
|
|
### Phase 9: QA and Cross-Device Testing (June 1-21, 2026) — Post-MVP
|
|
### Phase 9: QA and Cross-Device Testing (June 1-21, 2026) — Post-MVP
|
|
|
- [ ] 9.1 — Cross-device testing: iOS Safari, Android Chrome, desktop Chrome/Firefox
|
|
- [ ] 9.1 — Cross-device testing: iOS Safari, Android Chrome, desktop Chrome/Firefox
|
|
|
- [ ] 9.2 — Group flow end-to-end: create group, join from second device, add movies, roll, mark watched
|
|
- [ ] 9.2 — Group flow end-to-end: create group, join from second device, add movies, roll, mark watched
|
|
|
-- [ ] 9.3 — Real-time sync test: verify live updates across two active sessions
|
|
|
|
|
-- [ ] 9.4 — Recovery code test: create account, simulate new device, recover identity
|
|
|
|
|
-- [ ] 9.5 — List Admin permissions test: confirm admin-only actions are blocked for regular members
|
|
|
|
|
|
|
+- [ ] 9.3 — Real-time sync test: verify live updates across two active sessions; verify subscription cleanup on navigation
|
|
|
|
|
+- [ ] 9.4 — Recovery code test: create account, simulate new device, recover identity; verify rate limiting on claim endpoint; verify code invalidation after use
|
|
|
|
|
+- [ ] 9.5 — List Admin permissions test: confirm admin-only actions are blocked for regular members via RLS
|
|
|
- [ ] 9.6 — Admin self-removal test: trigger "Leave this list" as admin with other members present; confirm Transfer Ownership popup appears with member list and Cancel; confirm original admin is removed (not just demoted) after transfer; confirm list is NOT deleted; confirm that "Delete the list" action does NOT show the Transfer Ownership popup
|
|
- [ ] 9.6 — Admin self-removal test: trigger "Leave this list" as admin with other members present; confirm Transfer Ownership popup appears with member list and Cancel; confirm original admin is removed (not just demoted) after transfer; confirm list is NOT deleted; confirm that "Delete the list" action does NOT show the Transfer Ownership popup
|
|
|
-- [ ] 9.7 — Master Admin test: TOTP login, list/user search and deletion, session protection
|
|
|
|
|
-- [ ] 9.8 — TMDB rate limit check: confirm debounce and client-side caching stay within free tier limits
|
|
|
|
|
-- [ ] 9.9 — Landing page test: slot-machine reel animation plays using automatically fetched TMDB posters and lands on a valid movie result; result displays as a static teaser card (poster, title, genres) with no tap/link action; roll buttons work without login; 3-step demo alignment renders correctly on all screen sizes
|
|
|
|
|
|
|
+- [ ] 9.7 — Master Admin test: TOTP login, iron-session cookie validation, list/user search and deletion, session protection and expiry
|
|
|
|
|
+- [ ] 9.8 — TMDB rate limit check: confirm debounce and TanStack Query caching stay within rate limits; verify TMDB API key is not exposed in client-side code or network requests
|
|
|
|
|
+- [ ] 9.9 — Landing page test: slot-machine reel animation plays using automatically fetched TMDB posters and lands on a valid movie result; result displays as a static teaser card (poster, title, genres) with no tap/link action; roll buttons work without login; 3-step demo alignment renders correctly on all screen sizes; TMDB attribution footer visible; privacy policy accessible
|
|
|
- [ ] 9.10 — Home page test: returning user lands on home page (not landing page); list cards display correctly; cross-list roll works and result appears as a standalone teaser card on the home page (no navigation into a list); new user with no lists sees empty state
|
|
- [ ] 9.10 — Home page test: returning user lands on home page (not landing page); list cards display correctly; cross-list roll works and result appears as a standalone teaser card on the home page (no navigation into a list); new user with no lists sees empty state
|
|
|
-- [ ] 9.11 — Inline panel test: expands below correct row, collapses cleanly, delete two-tap flow works, binoculars overlay and Watched It button update in real time
|
|
|
|
|
-- [ ] 9.12 — Trailer URL refresh test: confirm background job processes only movies with null trailer_url and updates timestamps correctly; verify movies with existing trailer URLs are not re-processed
|
|
|
|
|
|
|
+- [ ] 9.11 — Inline panel test: expands below correct row, collapses cleanly, delete two-tap flow works, binoculars overlay and Watched It button update in real time; keyboard navigation works (Enter/Escape, focus management)
|
|
|
|
|
+- [ ] 9.12 — Trailer URL refresh test: confirm pg_cron job processes only movies with null trailer_url and updates timestamps correctly; verify movies with existing trailer URLs are not re-processed; verify URL domain validation
|
|
|
|
|
+- [ ] 9.13 — Security headers test: verify CSP, HSTS, X-Frame-Options are correctly applied on all routes
|
|
|
|
|
+- [ ] 9.14 — RLS test: verify unauthorized access attempts are blocked at the database level (attempt direct Supabase queries outside group membership)
|
|
|
|
|
+- [ ] 9.15 — Invite code rate limiting test: verify brute-force protection on join endpoint
|
|
|
|
|
+- [ ] 9.16 — Run Playwright E2E tests for critical paths: onboarding flow, add movie + real-time sync, roll the dice
|
|
|
|
|
+- [ ] 9.17 — Data retention test: verify inactive account cleanup job correctly identifies and deletes accounts inactive for 12+ months
|
|
|
|
|
|
|
|
### Phase 10: Launch (June 22 - July 5, 2026) — Full Feature Complete
|
|
### Phase 10: Launch (June 22 - July 5, 2026) — Full Feature Complete
|
|
|
-- [ ] 10.1 — Final Vercel production deployment and full smoke test
|
|
|
|
|
|
|
+- [ ] 10.1 — Final Docker production deployment and full smoke test
|
|
|
- [ ] 10.2 — Confirm PWA install flows work on iOS and Android
|
|
- [ ] 10.2 — Confirm PWA install flows work on iOS and Android
|
|
|
-- [ ] 10.3 — Set up basic error monitoring (Vercel logs + Sentry free tier)
|
|
|
|
|
|
|
+- [ ] 10.3 — Verify Sentry error monitoring is capturing errors correctly; review and resolve any outstanding issues
|
|
|
- [ ] 10.4 — Soft launch: share with initial test group, gather feedback
|
|
- [ ] 10.4 — Soft launch: share with initial test group, gather feedback
|
|
|
- [ ] 10.5 — Address any launch-blocking bugs found during soft launch
|
|
- [ ] 10.5 — Address any launch-blocking bugs found during soft launch
|
|
|
- [ ] 10.6 — Full feature complete sign-off by July 5, 2026
|
|
- [ ] 10.6 — Full feature complete sign-off by July 5, 2026
|
|
@@ -435,23 +517,33 @@ List Deletion flow (separate from self-removal):
|
|
|
- Real-time list updates appear on all connected devices within 3 seconds
|
|
- Real-time list updates appear on all connected devices within 3 seconds
|
|
|
- Genre Roll correctly filters by at least 8 of 10 emotion categories
|
|
- Genre Roll correctly filters by at least 8 of 10 emotion categories
|
|
|
- Recovery code flow successfully restores a user's identity on a new device
|
|
- Recovery code flow successfully restores a user's identity on a new device
|
|
|
-- Returning users land on their home page (not the landing page) when their user ID is in the browser
|
|
|
|
|
|
|
+- Recovery code claim endpoint blocks after 5 failed attempts per IP
|
|
|
|
|
+- Returning users land on their home page (not the landing page) when a valid Supabase session exists
|
|
|
- Logged-in home page correctly shows all of the user's lists as cards with accurate movie counts
|
|
- Logged-in home page correctly shows all of the user's lists as cards with accurate movie counts
|
|
|
- Cross-list roll on the home page draws from all user lists combined and displays the result as a standalone teaser card on the home page without navigating into any list
|
|
- Cross-list roll on the home page draws from all user lists combined and displays the result as a standalone teaser card on the home page without navigating into any list
|
|
|
- Admin "Leave this list" with other members present triggers the Transfer Ownership popup; original admin is removed after transfer; list is not deleted; direct "Delete the list" action does not trigger the transfer popup
|
|
- Admin "Leave this list" with other members present triggers the Transfer Ownership popup; original admin is removed after transfer; list is not deleted; direct "Delete the list" action does not trigger the transfer popup
|
|
|
-- Master Admin can log in with TOTP (credentials from environment variables) and delete any list or user
|
|
|
|
|
-- Trailer URL refresh job processes only movies where trailer_url is null (bi-weekly cadence)
|
|
|
|
|
|
|
+- Master Admin can log in with TOTP (credentials from environment variables) and delete any list or user; session expires after 8 hours
|
|
|
|
|
+- Trailer URL refresh job processes only movies where trailer_url is null (bi-weekly cadence via pg_cron)
|
|
|
- Landing page slot-machine reel animation uses automatically fetched TMDB posters and lands on a valid movie result displayed as a static teaser card (poster, title, genres) with no tap/link action
|
|
- Landing page slot-machine reel animation uses automatically fetched TMDB posters and lands on a valid movie result displayed as a static teaser card (poster, title, genres) with no tap/link action
|
|
|
- Landing page roll buttons work without any login or account creation
|
|
- Landing page roll buttons work without any login or account creation
|
|
|
- App installs to mobile home screen and functions as a PWA
|
|
- App installs to mobile home screen and functions as a PWA
|
|
|
- Inline panel expands below the correct grid row with no layout shift
|
|
- Inline panel expands below the correct grid row with no layout shift
|
|
|
|
|
+- Inline panel is fully operable via keyboard (Enter/Escape, focus management)
|
|
|
- Delete two-tap confirmation does not trigger on a single tap
|
|
- Delete two-tap confirmation does not trigger on a single tap
|
|
|
- Watched state toggle (binoculars overlay + button color) updates in real time across all group members' screens
|
|
- Watched state toggle (binoculars overlay + button color) updates in real time across all group members' screens
|
|
|
- No personal data beyond display name is stored or transmitted
|
|
- No personal data beyond display name is stored or transmitted
|
|
|
|
|
+- TMDB API key is not exposed in any client-side code or network request
|
|
|
|
|
+- TMDB attribution is visible on all pages
|
|
|
|
|
+- RLS policies prevent unauthorized data access at the database level
|
|
|
|
|
+- All animations respect `prefers-reduced-motion` preference
|
|
|
|
|
+- Inactive accounts are automatically deleted after 12 months
|
|
|
|
|
+- Security headers (CSP, HSTS, X-Frame-Options) are correctly applied
|
|
|
|
|
+- Invite code join endpoint is rate-limited against brute force
|
|
|
|
|
+- Docker deployment runs with non-root user and health check
|
|
|
|
|
|
|
|
## 10. Reference: Emotion-to-Genre Mapping
|
|
## 10. Reference: Emotion-to-Genre Mapping
|
|
|
|
|
|
|
|
-Static mapping used by Genre Roll to translate emotion keywords into TMDB genre IDs. Normalize input to lowercase and tokenize on spaces and commas before matching.
|
|
|
|
|
|
|
+Static mapping used by Genre Roll to translate emotion keywords into TMDB genre IDs. Implemented as a static TypeScript constant. Normalize input to lowercase and tokenize on spaces and commas before matching.
|
|
|
|
|
|
|
|
| Emotion Keywords | Primary Genres | Secondary Genres |
|
|
| Emotion Keywords | Primary Genres | Secondary Genres |
|
|
|
|-----------------|----------------|-----------------|
|
|
|-----------------|----------------|-----------------|
|
|
@@ -474,3 +566,4 @@ _Features added after initial scope. Complete current Implementation Plan progre
|
|
|
| Feature | Description | Added On | Rationale |
|
|
| Feature | Description | Added On | Rationale |
|
|
|
|---------|-------------|----------|-----------|
|
|
|---------|-------------|----------|-----------|
|
|
|
| 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 |
|
|
| 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, ownership transfer for administered groups, and anonymization of added_by references in movies | 2026-04-06 | GDPR Article 17 (Right to Erasure); MVP relies on Master Admin deletion on request as interim |
|