From 0bcd268a9af14f0f25b0ca8bb33359e1e5d3d36f Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 1 Sep 2026 17:20:41 +0000 Subject: [PATCH 1/4] docs: rewrite plans around remaining work only Rewrite eleven plans to cover only missing work, trim the bug backlog to four reproducible items, add the PR #11 salvage plan, and delete completed and obsolete plans. Co-Authored-By: Claude Opus 5 --- plans/README.md | 16 +- plans/admin-announcements.md | 137 +++++-- plans/agent-verification-loop.md | 14 +- plans/analytics-instrumentation-handoff.md | 190 +++++++++ plans/animation-polish.md | 89 +++-- plans/auth-email-and-demo-code-sign-in.plan | 156 +++++--- plans/control-honesty.md | 131 ++++--- plans/done/admin-trial-gifts.md | 50 --- plans/done/bugs.txt | 235 ++--------- plans/done/top-users-email-fallback.md | 31 -- plans/model-cost-accounting.md | 293 ++++++-------- plans/pr11-salvage.md | 65 ++++ plans/rate-limit-ui-cleanup.md | 102 ++++- plans/release-checklist.md | 75 ---- plans/search-quality.md | 119 ++++-- plans/smoke-test-checklist.md | 105 ----- plans/source-auto-import.md | 101 ----- plans/sparring-and-streaming.md | 104 ----- plans/streaming-ai.md | 56 --- plans/supabase-staging.md | 411 +++++++++++++------- plans/system-prompt-improvement.md | 152 ++++---- 21 files changed, 1278 insertions(+), 1354 deletions(-) create mode 100644 plans/analytics-instrumentation-handoff.md delete mode 100644 plans/done/admin-trial-gifts.md delete mode 100644 plans/done/top-users-email-fallback.md create mode 100644 plans/pr11-salvage.md delete mode 100644 plans/release-checklist.md delete mode 100644 plans/smoke-test-checklist.md delete mode 100644 plans/source-auto-import.md delete mode 100644 plans/sparring-and-streaming.md delete mode 100644 plans/streaming-ai.md diff --git a/plans/README.md b/plans/README.md index f2e355957..5c278c43e 100644 --- a/plans/README.md +++ b/plans/README.md @@ -5,20 +5,18 @@ This folder holds implementation plans for remaining DebateLab product work. Kee ## Active Plans - [Admin announcements](./admin-announcements.md) +- [Agent verification loop](./agent-verification-loop.md) +- [Analytics instrumentation handoff](./analytics-instrumentation-handoff.md) +- [Animation polish](./animation-polish.md) - [Ask and sitewide search quality](./search-quality.md) +- [Email and demo-code sign-in](./auth-email-and-demo-code-sign-in.plan) - [Control honesty and deterministic toggles](./control-honesty.md) - [Model cost accounting](./model-cost-accounting.md) -- [Prompt evaluation sheet (manual)](./prompt-eval-sheet.md) -- [Quality & Truth release checklist](./release-checklist.md) +- [PR #11 salvage work](./pr11-salvage.md) - [Rate-limit UI cleanup](./rate-limit-ui-cleanup.md) -- [Source auto-import](./source-auto-import.md) -- [Streaming AI](./streaming-ai.md) +- [Supabase staging](./supabase-staging.md) - [System prompt improvement](./system-prompt-improvement.md) -- [Animation polish](./animation-polish.md) -## Done +## Backlog -- [Debate sparring](./sparring-and-streaming.md) - [Bug and polish cleanup](./done/bugs.txt) -- [Admin trial gifts](./done/admin-trial-gifts.md) -- [Top users email fallback](./done/top-users-email-fallback.md) diff --git a/plans/admin-announcements.md b/plans/admin-announcements.md index af743616a..f886754ef 100644 --- a/plans/admin-announcements.md +++ b/plans/admin-announcements.md @@ -1,56 +1,113 @@ -# Admin Announcements Plan +# Admin announcements plan + +**Status as of 2026-08-31:** Not implemented. This plan covers the missing announcement feature only. ## Goal -Allow admins to send a one-time dismissible announcement to all users or selected users. Users should see it on login or next page change. +Let an administrator publish a short, dismissible in-app announcement to every authenticated user or to a bounded list of selected users. A user should see an active announcement after entering the app and after navigating to another app page until they dismiss it. + +## Prerequisites and isolation + +- Use Node 22.x as declared in `package.json`, npm, and the checked-in `package-lock.json`. +- Work in this plan's own isolated T3 Code worktree and feature branch. Before editing, run `pwd`, `git branch --show-current`, `git status --short --untracked-files=all`, and `git worktree list`. Confirm the worktree is not `main` and is not another plan worktree. +- Preserve every pre-existing dirty or untracked change. Do not reset, clean, checkout, or edit files outside this plan's scope. +- Verify the intended Supabase target before staging operations. Production migrations, Edge Function deployment, and publishing announcement content or audiences require Adam's explicit approval. +- Before browser checks, confirm port `18080` is unoccupied. With `CI=1`, the current `playwright.config.ts` starts a fresh Vite server instead of reusing another checkout. + +## Current verified baseline + +- `acaac0d` is the current `main`/`origin/main` head. `src/pages/AdminPage.tsx` has Usage Analytics, User Management, Promo Codes, and Subscribers tabs, but no announcement state, controls, or API calls. +- `supabase/functions/admin/index.ts` authenticates a Bearer token, checks `user_roles.role = 'admin'`, and uses the service-role client for existing admin actions. Its current actions do not include announcements. +- `supabase/migrations/20260204000000_admin_improvements.sql` creates `user_roles` and the existing admin/RLS foundation. No migration or table named `admin_announcements` or `user_announcement_dismissals` exists. +- `src/components/layout/AppLayout.tsx` renders the authenticated shell and `Outlet`, but it does not fetch or render announcements. `src/App.tsx` mounts it for all `/app/*` routes. +- `src/test/pages/AdminPage.test.tsx` covers the existing admin dashboard and grant-trial flow. There is no announcement unit or browser test. + +## Remaining scope + +Implement the feature as an additive migration, admin actions in the existing admin function, RLS-backed user reads/dismissals, an Admin page section, and a small layout/banner component. Use the existing `admin` function only for admin operations. The normal user path should use the authenticated Supabase client under RLS, so it does not turn the admin-only function into a mixed-privilege endpoint. + +Do not add email delivery, push notifications, a second admin function, localStorage-only dismissal state, hard-delete behavior, or unrelated admin dashboard cleanup. + +## Likely files + +- Create: `supabase/migrations/20260831000000_admin_announcements.sql` +- Modify: `supabase/functions/admin/index.ts` +- Modify: `src/pages/AdminPage.tsx` +- Modify: `src/components/layout/AppLayout.tsx` +- Create: `src/hooks/use-announcements.ts` +- Create: `src/components/announcements/AnnouncementBanner.tsx` +- Modify: `src/test/pages/AdminPage.test.tsx` +- Create: `src/test/announcements/use-announcements.test.ts` +- Create: `src/test/components/AnnouncementBanner.test.tsx` +- Create: `e2e/announcements.spec.ts` + +## Data and security design + +1. Add `public.admin_announcements` with UUID primary key, non-empty bounded `title` and `body`, `target_type` (`all` or `users`), `target_user_ids uuid[]` defaulting to an empty array, `starts_at`, nullable `expires_at`, `is_active`, `created_by` referencing `auth.users` with `ON DELETE SET NULL`, and `created_at`/`updated_at` timestamps. Enforce that `all` has no target IDs and `users` has at least one target ID; reject an expiry before the start. +2. Add `public.user_announcement_dismissals` with an announcement foreign key and an `auth.users` foreign key, both cascading on deletion, `dismissed_at`, and primary key `(announcement_id, user_id)`. Add indexes for active announcements and user dismissals. +3. Enable RLS. Authenticated users may select only active, currently scheduled announcements targeted to them and with no matching dismissal. They may insert a dismissal only for their own `user_id`. Do not grant authenticated users update/delete access. The service role retains the admin function's server-side access. +4. In `admin/index.ts`, add a GET action named `announcements` for listing and POST actions named `create-announcement` and `deactivate-announcement`. Re-check the current admin role for each mutation, validate lengths, dates, target type, and target UUIDs, set `created_by` from the JWT user, and use `is_active = false` for reversible deactivation. Never return service credentials or internal policy errors. + +## Sequenced implementation + +### 1. Add the additive schema and RLS policies + +Create the migration above. Keep the policy predicate responsible for audience, schedule, active state, and dismissal filtering. Include comments explaining that the migration is additive and that deactivation is preferred to deletion for auditability. + +### 2. Add admin API actions + +Extend `supabase/functions/admin/index.ts` using its existing auth and response conventions. List active and inactive rows for admins, create a validated announcement, and deactivate by UUID. Return stable JSON fields needed by the Admin page, including target IDs, status, and timestamps. + +### 3. Add user read/dismiss behavior + +Implement `use-announcements.ts` with an authenticated Supabase query and a dismiss mutation. Refresh on authenticated mount and pathname changes so an announcement appears after login and on the next page navigation. Treat a duplicate dismissal as success, and keep a failed network request non-fatal to the rest of the app. + +### 4. Render the announcement in the shell + +Add `AnnouncementBanner.tsx` with accessible title/body, close button, loading/error-safe behavior, and a layout that works on desktop and mobile. Render it from `AppLayout.tsx` without disrupting `Outlet`, navigation, or the existing auth boundary. Do not render stale, expired, inactive, or already dismissed announcements. + +### 5. Add the admin controls + +Add an Announcements tab or section to `AdminPage.tsx`. Reuse the existing authenticated admin request helpers and loaded user list for a bounded target selector. Support composing title/body, all-users versus selected-users targeting, optional start/expiry, listing status and use metadata, and reversible deactivation. Do not display or allow arbitrary SQL, service-role data, or a hard-delete button. -## Data Model +### 6. Add focused coverage and browser proof -Create `admin_announcements`: +Cover schema-facing request and dismissal behavior with the existing Supabase mock style, including audience filtering, schedule, dismissal idempotency, and unauthorized mutation handling. Cover Admin page create/list/deactivate requests and selected-user payloads. Cover banner rendering and dismissal. Add an authenticated Playwright flow that stubs the auth/data calls, verifies the banner on an app route, dismisses it, navigates, and confirms it does not reappear. -- `id uuid primary key` -- `title text not null` -- `body text not null` -- `target_type text check in ('all', 'users')` -- `target_user_ids uuid[] null` -- `starts_at timestamptz default now()` -- `expires_at timestamptz null` -- `deactivated_at timestamptz null` -- `created_by uuid not null` -- `created_at timestamptz default now()` +## Verification -Create `user_announcement_dismissals`: +Run the focused tests first: -- `announcement_id uuid references admin_announcements(id)` -- `user_id uuid not null` -- `dismissed_at timestamptz default now()` -- primary key `(announcement_id, user_id)` +```bash +npm test -- --run src/test/pages/AdminPage.test.tsx src/test/announcements/use-announcements.test.ts src/test/components/AnnouncementBanner.test.tsx +CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e -- e2e/announcements.spec.ts +deno check supabase/functions/admin/index.ts +deno test --allow-read --allow-env=EXA_API_KEY --allow-net=127.0.0.1 supabase/functions/ +``` -## Backend +Then run the repository gates on the implementation branch: -1. Add admin endpoints to existing `admin` edge function: - - create announcement. - - list announcements. - - deactivate an announcement without deleting dismissal history. - - explicitly delete an announcement when permanent removal is intended. -2. Add authenticated non-admin endpoint: - - fetch active announcements for current user. - - dismiss announcement. -3. Enforce admin status server-side using `user_roles`. +```bash +npm run lint +npm run typecheck +npm test +npm run build +CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e +``` -## Frontend +Inspect the migration and final diff. Confirm no policy grants a non-admin write to announcements and no dismissal write can name another user. -1. Add Admin page section for composing announcements. -2. Add user selector for targeted sends. -3. Add global announcement banner/modal in `AppLayout`. -4. Store dismissed state in Supabase, not only localStorage. +## Acceptance criteria -## Tests +- An authenticated admin can create an announcement for all users or a validated selected-user list, and a non-admin receives `403` for every admin action. +- An active, scheduled all-user announcement appears only to authenticated users who have not dismissed it. A targeted announcement appears only to listed users. Before-start and expired announcements do not appear. +- Dismissal persists in `user_announcement_dismissals`; refreshing or changing routes does not show the same announcement again. Repeating the dismissal is harmless. +- Admins can list active/inactive announcements and deactivate one without destroying its record. The user path cannot update or delete announcement rows. +- The UI remains usable at desktop and mobile widths, has an accessible dismiss control, and does not break existing authenticated routes. +- Existing role checks, AI quota enforcement, subscription behavior, and admin actions remain unchanged. -- RLS/admin endpoint tests, including deactivation and dismissal-history retention. -- Admin UI tests for create/list/deactivate/delete. -- Layout test for showing and dismissing an announcement. +## Rollout, rollback, and authorization -## Deployment +Apply the additive migration to staging first, verify policies with an admin and two ordinary test users, deploy the changed admin function if needed, then deploy the frontend. Production migration, function deployment, and public announcement content/audience require Adam's explicit approval for that environment. Do not run a migration against a target project until the linked project and current migration state are confirmed. -Apply migration, deploy admin function, then deploy frontend. +Rollback the feature by reverting the frontend/admin-function release and deactivating any published announcements. Keep the additive tables and rows for auditability. Dropping them or deleting announcement data is out of scope and requires a separate explicit approval after all code references are gone. diff --git a/plans/agent-verification-loop.md b/plans/agent-verification-loop.md index cb1ecc7e1..f7d1b2979 100644 --- a/plans/agent-verification-loop.md +++ b/plans/agent-verification-loop.md @@ -120,7 +120,6 @@ must run all of these gates: npm run lint npm run typecheck npm test -- --coverage -npm run test:edge npm run build CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e ``` @@ -182,8 +181,8 @@ npm run test:e2e:staging The CI staging job may run only from a protected environment on an authorized `main` push or explicitly authorized main-branch dispatch. It must not run on -pull requests or arbitrary branch dispatches. It disables traces, screenshots, -and video and uploads no authenticated browser artifacts. +pull requests or arbitrary branch dispatches. The job should upload the same +failure artifacts without exposing secret-bearing logs. ### 4. Add the verification playbook @@ -210,10 +209,11 @@ npx playwright install --with-deps chromium CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run verify ``` -WebKit is configured as an explicit local and release smoke gate. Install it -with `npx playwright install --with-deps webkit`, then run -`CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e:webkit`. -Chromium remains the credential-free PR gate. The protected staging suite is also +WebKit is conditional. If the implementation adds a `webkit` project to +`playwright.config.ts`, install it and run +`CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npx playwright test --project=webkit` +as an additional local/release smoke gate. Until that project is configured, +Chromium is the credential-free PR gate. The protected staging suite is also required for a release that changes authenticated data or AI flows. Capture the current-head CI result and artifact links. A billing-blocked CI run is reported as an exception, never as a pass. diff --git a/plans/analytics-instrumentation-handoff.md b/plans/analytics-instrumentation-handoff.md new file mode 100644 index 000000000..35053ae7d --- /dev/null +++ b/plans/analytics-instrumentation-handoff.md @@ -0,0 +1,190 @@ +# Handoff prompt: activate analytics and error tracking + +Paste everything below the line into a fresh agent session with the DebateLab +repo as the working directory. + +--- + +You are working in `/home/codex/Projects/DebateLab`, a Vite + React + TypeScript +SPA (shadcn/ui, Tailwind, Zustand, Supabase backend, Capacitor iOS wrapper). + +## The situation + +DebateLab ships twelve tools under `/app` and currently collects **zero product +analytics and zero crash reports in production**. Both libraries are already +installed and wired, but inert: + +- `src/lib/observability.ts` exports `initializeObservability`, + `captureProductEvent`, and `captureException`. All three are gated on + `import.meta.env.VITE_SENTRY_DSN` / `VITE_POSTHOG_KEY` being set. +- `src/main.tsx:26` already calls `initializeObservability()`, so Sentry and + PostHog boot with no code change the moment the env vars exist. +- `captureProductEvent` has **zero callers** in the entire codebase. Verify with + `grep -rn captureProductEvent src/`. +- `captureException` is likewise unused. +- `vercel env ls` returns "No Environment Variables found for + adam-gaborjanis-projects/debate-navigator", so nothing is set in the deployed + environment either. + +So: nobody knows which of the twelve tools are actually used, and production +exceptions vanish silently. + +## Your job, in two parts + +### Part 1: turn the pipes on (no code change) + +The variables are already documented in `.env.example`: + +``` +VITE_SENTRY_DSN= +VITE_POSTHOG_KEY= +VITE_POSTHOG_HOST=https://eu.i.posthog.com +``` + +Do this: + +1. Ask Adam for the Sentry DSN and PostHog project key. **Do not invent + placeholder values, do not print the values back in chat, and do not write + them into any tracked file.** A Sentry DSN is technically public but treat it + as a secret in transit anyway. +2. Add all three to Vercel for the production, preview, and development + environments (`vercel env add VITE_POSTHOG_KEY production`, etc.). Reading + values from stdin avoids them landing in shell history. +3. Confirm with `vercel env ls` that the names are present. That command prints + names only, not values, so its output is safe to show. +4. **Do not trigger a deploy yourself.** Report that a redeploy is required for + the new build-time vars to take effect and let Adam authorize it. + +No CSP work is needed. Both `vercel.json` and `nginx.conf` already allow +`https://*.ingest.sentry.io https://*.sentry.io https://eu.i.posthog.com +https://*.posthog.com` in `connect-src`. If you touch CSP for any reason, the two +files must stay byte-compatible; `src/test/vercel-deployment-config.test.ts` +enforces that they agree. + +### Part 2: instrument the events that answer real questions + +Add roughly a dozen `captureProductEvent` calls. Resist adding more. Every event +should be one you can name the decision it informs. + +**The chokepoint.** `src/lib/ai/client.ts:289` defines +`async function callSupabaseAI(feature, input, config, options)` where `feature` +is `'generate' | 'attack' | 'brainstorm' | 'factoids' | 'ask' | 'sparring' | +'crossfire'`. Every non-streaming AI call in the app funnels through it. It +already computes `requestId`, `startTime`, `provider`, `config.model`, and +`elapsed = Math.round(performance.now() - startTime)`. Instrument here rather +than in each of the eight exported entry points (`generateCase:974`, +`attackCase:999`, `brainstorm:1012`, `factoids:1024`, `askChat:1059`, +`crossfire:543`, `generateSparringTurn:1126`, `judgeSparringSession:1177`). + +Emit: + +- `ai_request_started` with `{ feature, provider, model }` +- `ai_request_succeeded` with `{ feature, provider, model, durationMs }` +- `ai_request_failed` with `{ feature, provider, model, durationMs, errorName, + statusCode }` + +For the failure case, classify using the existing exported predicates rather +than string matching: `isAIAuthError:265`, `isAIRateLimitError:269`, +`isAIProviderError:273`, `isTimeoutError:277`, `isAISubscriptionError:281`, +`isAIModelRestrictionError:285`. All error classes extend `AIError` (line 145) +which carries `statusCode`, `requestId`, and `details`. + +Then route genuinely unexpected failures to Sentry via `captureException`. +Expected states are not bugs: skip auth errors, rate limits, subscription and +model-restriction errors, and user-initiated aborts. Do send provider errors, +`AIResponseError`, and unknown wrapped errors. `RouteErrorBoundary` in +`src/App.tsx:58` currently only calls `console.error` in `componentDidCatch`; +add `captureException` there too. + +**Streaming path.** `askChat` bypasses `callSupabaseAI` when streaming is +available. See `src/lib/ai/streaming.ts` (`isAskStreamingSupported:25`, +`streamAskChat:72`). Instrument that path separately with the same three event +names and `{ feature: 'ask', streaming: true }` so streamed and non-streamed Ask +usage stay comparable. + +**Tool opened.** One event, `tool_opened`, with `{ tool }` derived from the +route. Twelve routes live in `src/App.tsx:118-130`: generate, attack, brainstorm, +factoids, ask, crossfire, history, library, timer, sparring, flowchart, settings, +admin. Prefer a single subscription to router location changes over pasting a +call into twelve page components. + +**Export used.** `content_exported` with `{ format, tool }`. Export helpers live +in `src/lib/export-utils.ts` (docx and Google Docs, lines 39-596) and +`src/lib/pdf-export-utils.ts` (lines 261-653). Both sets return +`Promise`; only count an export when the promise resolves truthy. + +**Sparring.** `sparring_round_completed` with `{ format, roundIndex }` and +`sparring_session_judged`. The store is `src/lib/stores/sparring-store.ts`. + +### Privacy rules, non-negotiable + +- Never put user content in event properties. No motions, no case text, no chat + messages, no source titles, no email addresses. Counts, enum values, booleans, + and durations only. +- PostHog is already configured defensively in `observability.ts`: + `autocapture: false`, `capture_pageview: false`, + `disable_session_recording: true`, `persistence: "memory"`. Do not relax any of + those. +- Sentry is initialized with `sendDefaultPii: false`. Leave it. +- If you add `posthog.identify`, use the Supabase user id only. The precedent is + `setJamUserId` in `src/lib/jam-metadata.ts`, called from + `src/lib/supabase/AuthProvider.tsx:109` and `:129`. Reset identity on sign-out; + the auth listener is at `AuthProvider.tsx:146`. +- The privacy policy at `src/pages/PrivacyPage.tsx` renders from + `src/locales/en.json` under `privacyPage`. Section `p7` currently says only + "We use analytics to improve service quality." Once real product analytics and + error tracking are live, that sentence should name the processors. Flag this to + Adam as a copy change requiring his sign-off; there are three locales + (`en.json`, `hu.json`, `es.json`) and they must stay in sync. Do not silently + rewrite legal copy. + +### Tests + +There is no observability test file yet. Add `src/test/observability.test.ts` +following the conventions of the existing suite (vitest with `globals: true`, +jsdom, setup at `src/test/setup.ts`). Cover at minimum: + +- `captureProductEvent` is a no-op when `VITE_POSTHOG_KEY` is unset, and calls + through when set. +- The error classifier maps each `AIError` subclass to the expected + `errorName` and decides correctly whether it reaches Sentry. +- No event payload contains free-text user input. + +`src/lib/ai/client.ts` is inside the coverage `include` list in +`vitest.config.ts` with thresholds of statements 90, branches 80, functions 89, +lines 90. Your new branches in that file are measured, so cover them or the gate +fails. Existing client tests to model on: `src/test/ai/client.test.ts`, +`src/test/ai/client-branches.test.ts`, `src/test/ai/streaming.test.ts`. + +### Verification + +Run `npm run verify`. It chains lint, typecheck, `vitest run --coverage`, and +build, and it is the only real gate right now. + +**GitHub Actions cannot allocate runners on this repository** because of an +account billing state, so CI will report failure regardless of code quality. Do +not chase it, do not try to fix the workflow, and do not treat a red check as +evidence about your change. Local `npm run verify` passing on a clean tree is the +standard. + +Also confirm the manual behavior: with the env vars absent, the app must build +and run exactly as before, with no console noise and no network calls to Sentry +or PostHog. + +### Boundaries + +- Do not commit, push, open or update a PR, merge, or deploy without Adam's + explicit authorization for that specific action. +- Do not paste secret values into chat, into a file, or into a commit. +- Note that `plans/pr11-salvage.md` is currently staged but uncommitted. Leave it + alone. Stage only files you actually changed. +- If you find something else broken, report it. Do not fix it as part of this + task. + +### Report format + +When done, report in this order: outcome classified as verified / +partially_verified / blocked / not_applicable, then changed files, then checks +run with their results, then remaining risk, then the next action Adam needs to +take (which will be at minimum: authorize the Vercel redeploy, and decide on the +privacy policy wording). diff --git a/plans/animation-polish.md b/plans/animation-polish.md index e9c3e71fc..664f0dff6 100644 --- a/plans/animation-polish.md +++ b/plans/animation-polish.md @@ -1,33 +1,78 @@ -# Animation Polish Plan +# Animation polish plan + +**Status:** Remaining work as of 2026-08-31 on `main` (`acaac0d`). + +## Preconditions + +- Create and use your own isolated T3 Code worktree and feature branch for this plan; do not edit `main` or reuse another plan's worktree. Before edits, verify `pwd`, `git branch --show-current`, `git status --short --branch`, and `git worktree list --porcelain`, and keep this branch/worktree independent through its PR lifecycle. +- Use Node `22.x` from `package.json`, npm, and the committed `package-lock.json`; verify `node --version`, `npm --version`, and `test -f package-lock.json`, then run `npm ci` before the gates. ## Goal -Keep the app feeling responsive and polished without slowing navigation or distracting from dense debate workflows. +Keep DebateLab responsive and readable in dense debate workflows. Motion should explain state changes, never hide content, shift layout unexpectedly, or ignore a user's reduced-motion preference. + +## Current implementation + +The route transition in `src/App.tsx:84-98` is already 160ms, below the old 180ms target, and Framer Motion receives `useReducedMotion()`. Shared motion tokens and loading/reveal classes live in `src/index.css:170-421`; the reduced-motion media block is at `src/index.css:1222-1250`. `AnimatedSection` and `StaggeredContainer` also have reduced-motion handling (`src/components/ui/animated-section.tsx:10-65`). + +The only broad browser proof is the Sparring E2E test (`e2e/sparring.spec.ts:123-140`), which checks mobile overflow and Tailwind `animate-*` elements. Component tests cover class names and timers but do not inspect computed animation or transition behavior. The source still has unscoped `animate-spin`, `animate-pulse`, `animate-bounce`, and custom CSS animations in loading, landing, timer, Ask, and feature-page states. Feature loading pages also nest `motion-swap`/`motion-shimmer` wrappers around `StreamingIndicator`, which has its own motion classes. + +## Remaining work, in order + +### 1. Add a route/state motion audit + +**Files likely to change:** + +- New `e2e/motion.spec.ts` +- `e2e/authed-pages.spec.ts` only if the shared authenticated fixture is the cleanest reuse +- Existing route components only where the audit finds a concrete issue + +Cover the authenticated Home, Generate, Attack, Brainstorm, Factoids, Ask, Crossfire, Sparring, History, Library, Timer, Flowchart, and Settings routes at the configured desktop and mobile widths. Exercise empty, loading, result, and options-open states where the route exposes them. Intercept provider calls with deterministic fixtures. Capture a desktop and mobile screenshot for the dense Generate, Factoids, Ask, and Sparring states during review, and assert no horizontal overflow, hidden primary content, or unexpected scroll jump. + +### 2. Make reduced motion stop every non-essential animation + +**Files likely to change:** + +- `src/index.css:1222-1250` +- `src/components/ui/animated-section.tsx:10-65` +- `src/components/ui/loading-spinner.tsx` +- `src/components/ui/streaming-indicator.tsx` +- `src/App.tsx` +- `src/components/auth/RequireAuth.tsx` +- `src/pages/SharedPage.tsx`, `src/pages/AdminPage.tsx`, `src/pages/TimerPage.tsx` +- `src/components/features/ask/AskChat.tsx` +- `src/components/features/generate/GenerateForm.tsx` +- `src/components/landing/Hero.tsx` +- `src/components/ui/skeleton.tsx` + +Use one clear reduced-motion policy. Persistent utility animations and custom keyframes must resolve to no animation under `prefers-reduced-motion: reduce`; transitions and scroll behavior must not animate. Add explicit `motion-reduce:animate-none` only where a component-specific class is clearer, and avoid selectors that leave an infinite animation active at a 0.001ms duration. Preserve visible loading text and state changes without requiring movement. Ensure Framer Motion and the generated stagger `