# Compliance Review — MovieDice (UI + Filter Batch) Reviewed 2026-05-21. Scope: uncommitted batch on master (cert filter, layout/footer refactor, carousel button conversion, settings refresh, privacy/legal footer). --- ## CRITICAL ### 1. STRICT cert walker silently hides legitimate titles in search `src/lib/tmdb/certification.ts:51-77`, `src/app/api/tmdb/search/route.ts:32-39` — The walker requires `sawRecognizedCert && sawPositiveMatch`. A title is rejected if **no recognized country reports any certification at all** — extremely common for older, foreign-language, and indie titles (TMDB lets contributors leave `certification` empty). `/api/tmdb/search` post-filters with the same walker, so users searching legitimate PG/PG-13-equivalent movies see them silently vanish with no UX signal — invisible failure mode is worse than a permissive filter. **Fix:** Define an explicit policy for "no cert data" titles. Options: (a) allow US-released-but-empty-cert when paired with a non-recent release date; (b) treat known no-rating tokens (`NR`, `""`, `Not Rated`, `Unrated`) as explicitly allowed; (c) fall back to TMDB `adult` flag + popularity threshold when cert data is absent. Document the choice in the file header. **Implementation Risk:** Loosening could let through R-rated indies with missing US cert. Recommend a one-week log of "rejected for no-cert" counts before deciding the policy. --- ### 2. Pre-existing `movies` rows now break `` `src/app/api/tmdb/movie/[id]/route.ts:25-28`, `src/components/dice/list-more-info-modal.tsx` — Any movie added to `public.movies` before cert filtering shipped (or one that flips after a future allowlist tightening) will now fail `isMovieAllowedByCert` when the modal opens. The route correctly 404s to avoid existence leakage, but the user just sees the modal fetch fail on a movie that's already on their list. **Fix:** Add a `?context=in-list` (or similar) opt-in to `/api/tmdb/movie/[id]` that skips the cert gate when the caller is fetching details for a movie already in the user's group (server can verify via session + `movies` lookup before bypassing). Alternative: backfill — sweep `movies`, soft-flag any that no longer pass, surface UI message instead of silent 404. **Implementation Risk:** Context-bypass leaks existence to authenticated members of the owning list only — acceptable. Backfill is destructive and needs admin sign-off. --- ## HIGH ### 3. Admin layout double `min-h-screen` breaks login/dashboard chrome `src/app/admin/layout.tsx:5-9`, `src/app/admin/login/page.tsx:40`, `src/app/admin/page.tsx:13` — The layout now wraps children in `flex min-h-screen flex-col bg-neutral-950` + `` below a `flex-1` slot. Both child pages still self-render their own `min-h-screen` container. Net effect: the child fills 100vh, the footer renders below it, the page now requires scroll by default, and the centered login UX is offset. `bg-neutral-950` is duplicated (harmless). **Fix:** Remove `min-h-screen` from both `admin/login/page.tsx` and `admin/page.tsx`. Drop the now-redundant inner `bg-neutral-950`. Replace login centering with `flex-1 flex items-center justify-center` on an inner container so it still centers within the layout's flex-1 main area. **Implementation Risk:** None if done correctly — Playwright/manual check the centered-login layout after. --- ### 4. List-page back/settings links use raw `` and drop client state `src/app/(app)/list/[id]/page.tsx:46-67, 70-91` — Both navigation chrome elements use `` / `` instead of `` from `next/link`. Each click triggers a full document load: TanStack Query cache evicts, IndexedDB-persisted offline data must rehydrate, `AuthBootstrap` re-runs, optimistic mutation state is lost. The rest of the app uses ``. **Fix:** Convert both anchors to ``. Preserve className, aria-label, svg child. **Implementation Risk:** None; `` accepts arbitrary children including svg. --- ## MEDIUM ### 5. `aria-live="polite"` on the carousel teaser `