feat(legal): add GDPR-compliant cookie consent banner

Add CookieBanner component with useCookieConsent hook, translations in EN/PL, and integration into root layout

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
nxtkofi 2026-05-02 16:02:13 +02:00
parent b2652db613
commit d41d4687ee
15 changed files with 1156 additions and 0 deletions

41
.sisyphus/boulder.json Normal file
View file

@ -0,0 +1,41 @@
{
"active_plan": "/home/nxtkofi/dev/templates/convex-next-saas/.sisyphus/plans/app-shell-and-route-fallbacks.md",
"started_at": "2026-04-21T20:19:23.457Z",
"session_ids": [
"ses_24eb26a4bffeSxb1coIit8a4Dv"
],
"plan_name": "app-shell-and-route-fallbacks",
"agent": "atlas",
"task_sessions": {
"todo:1": {
"task_key": "todo:1",
"task_label": "1",
"task_title": "Establish shell and fallback translation contract",
"session_id": "ses_24e43150cffe5Wc6MKMRzAzPi5",
"agent": "Sisyphus-Junior",
"category": "quick",
"updated_at": "2026-04-21T20:40:03.597Z"
},
"todo:2": {
"task_key": "todo:2",
"task_label": "2",
"task_title": "Build shared shell primitives and refactor ThemeChanger for header use",
"session_id": "ses_24e395669ffe5qSSbdGO6z1tAh",
"agent": "Sisyphus-Junior",
"category": "visual-engineering",
"updated_at": "2026-04-21T20:41:37.491Z"
},
"final-wave:f1": {
"task_key": "final-wave:f1",
"task_label": "F1",
"task_title": "Plan Compliance Audit — oracle",
"session_id": "ses_24e202cbbffe7XpuKb4O1csa5w",
"agent": "Sisyphus-Junior",
"category": "unspecified-high",
"updated_at": "2026-04-21T21:12:31.731Z"
}
},
"session_origins": {
"ses_24eb26a4bffeSxb1coIit8a4Dv": "direct"
}
}

View file

@ -0,0 +1,33 @@
# Draft: Convex Coolify Better Auth Debug
## Requirements (confirmed)
- research how this should work with Coolify, Convex, and Let's Encrypt
- explain whether current sign-up flow is correct
- determine likely causes of Convex errors like `No available server` and TLS failures
- account for local Next.js (`SITE_URL=http://localhost:3000`) talking to Convex hosted on Coolify/VPS
## Technical Decisions
- investigate repo auth flow before drawing conclusions
- compare repo implementation against authoritative Better Auth + Convex guidance
- include infrastructure-side TLS/proxy hypotheses, not just app-code explanations
## Research Findings
- local app calls `authClient.signUp.email(...)` from `src/app/sign-up/page.tsx`
- Next route proxies Better Auth via `src/app/api/auth/[...all]/route.ts`
- server-side Better Auth bridge is configured in `src/lib/auth-server.ts`
- authoritative Better Auth + Convex guidance confirms sign-up/sign-in must happen from the client; `authClient.signUp.email(...)` is the canonical flow
- auth requests go browser -> Next `/api/auth/...` -> server-side fetch to Convex site URL; TLS is evaluated on that server-to-server hop, not in the browser
- Better Auth with Convex writes to auth component tables like `user`, `account`, `session`, `verification`; custom app user syncing requires additional trigger-style logic
- Coolify/Traefik can present a self-signed fallback cert when ACME/Let's Encrypt or routing is wrong, even if the public browser path appears healthy
- `No available server` aligns more with Coolify/Traefik upstream health/routing issues than with incorrect Better Auth API usage
- user confirmed local `SITE_URL` is `http://localhost:3000` and Convex runs remotely on Coolify with distinct backend, dashboard, and backend-site hostnames
- user's Coolify env exposes `SERVICE_URL_BACKEND=https://convex-backend.mentat.ovh` and `SERVICE_URL_BACKEND_SITE=https://backend-site-olnjg91x5ervt6j6owwgnlha.mentat.ovh`; these hostnames must not be conflated with the dashboard URL
## Open Questions
- which exact host is currently configured in `NEXT_PUBLIC_CONVEX_SITE_URL`
- whether the Next runtime reaches a different internal/proxied hostname than the browser does
- whether Coolify proxy health/port/DNS/IPv6 configuration is intermittently breaking Convex upstream availability
## Scope Boundaries
- INCLUDE: repo auth flow, Better Auth/Convex expectations, Coolify/Let's Encrypt TLS behavior
- EXCLUDE: implementing fixes in source files during research

View file

@ -0,0 +1,7 @@
Runtime schema validation summary
Verified with `npx tsx` against exported createChangePasswordSchema(...):
- mismatched confirmPassword -> parse failed on path ["confirmPassword"] with message `no-match`
- currentPassword === newPassword -> parse failed on path ["newPassword"] with message `must-differ`
This confirms the client-side schema blocks both required validation scenarios before submission.

View file

@ -0,0 +1,18 @@
QA summary for dashboard/settings password change flow
Verified:
- GET /settings unauthenticated -> 307 redirect to /sign-in?callbackURL=%2Fsettings
- GET /dashboard contains CTA href="/settings"
- Authenticated GET /settings renders currentPassword/newPassword/confirmPassword inputs
- POST /api/auth/change-password with wrong currentPassword -> 400 INVALID_PASSWORD
- Session remains valid after wrong currentPassword attempt
- POST /api/auth/change-password with valid currentPassword -> 200 OK
- Old password sign-in fails after change
- New password sign-in succeeds after change
- Second active session becomes unauthenticated after password rotation with revokeOtherSessions=true
- Accessing /settings with revoked session -> 307 redirect to sign-in
Environment note:
- Browser MCP could not run because Chrome was unavailable and could not be installed without sudo.
- Browser-level QA was replaced with HTTP/session-level verification plus runtime schema checks.
- Sign-in callback propagation verified after fix: POST /api/auth/sign-in/email with callbackURL=/settings returns {"url":"/settings"}

View file

@ -0,0 +1,7 @@
## 2026-04-21 Scope fidelity check
- Scope check verdict is REJECT.
- `src/components/auth/AuthForm.tsx` hard-codes `/dashboard`, `/sign-in`, `/sign-up`, and `/forgot-password` instead of using `routes` constants.
- `src/app/not-found.tsx` contains hard-coded English copy (`Page not found`, `Go back home`) and hard-coded `/`, so root-level fallback copy is not translated through locale messages and does not use route constants.
- `src/app/[locale]/layout.tsx` does not gate auth server-side, and shell auth awareness is client-hydrated through `authClient.useSession()` in `src/components/core/AuthNavActions.tsx`.
- No avatar dropdown, profile menu, locale switcher, breadcrumbs, sidebar, mobile drawer, `app/global-error.tsx`, or page-specific skeleton/loading systems were found.

View file

@ -0,0 +1,373 @@
# App Shell and Route Fallbacks
## TL;DR
> **Summary**: Add a minimal locale-aware application shell with auth-aware navigation, then add deterministic not-found, loading, and route-error UX for the App Router without expanding into broader product UI.
> **Deliverables**:
> - Shared shell in `src/app/[locale]/layout.tsx`
> - Header-safe theme switcher and auth-aware nav actions
> - Localized 404 handling plus root 404 fallback
> - Locale-scoped `loading.tsx` and `error.tsx`
> - Stable selectors and deterministic QA hooks for nav/fallback states
> - EN/PL translations for new shell and fallback copy
> **Effort**: Medium
> **Parallel**: YES - 2 waves
> **Critical Path**: 1 → 2 → 3 → 4/5/6
## Context
### Original Request
Plan implementation of items 1 and 2 from the previously proposed quick wins: error boundaries + loading states, and not-found page + global layout navigation.
### Interview Summary
- Scope is intentionally limited to shared shell/navigation and App Router fallbacks.
- Stripe, uploads, admin, and other template upgrades are explicitly out of scope for this slice.
- Locale routing already exists and must be preserved.
### Metis Review (gaps addressed)
- Avoid root-layout auth checks in the shared layout because they would force unnecessary dynamic rendering and conflict with existing page-level protection.
- Do not rely on `[locale]/not-found.tsx` alone for unknown localized URLs; add an explicit catch-all route.
- Do not assume `error.tsx` handles root-layout/provider failures; keep this slice scoped to locale-route runtime errors only.
- Make loading and error QA deterministic with stable selectors and an internal non-linked route error trigger.
## Work Objectives
### Core Objective
Provide a minimal, auth-aware app shell across localized routes and add deterministic localized UX for loading, 404, and route-segment runtime errors.
### Deliverables
- Shared shell wrapper in `src/app/[locale]/layout.tsx`
- New shell components under `src/components/core/`
- Refactored `ThemeChanger` suitable for header usage
- Localized route copy in `messages/en.json` and `messages/pl.json`
- `src/app/[locale]/not-found.tsx`
- `src/app/[locale]/[...rest]/page.tsx`
- `src/app/not-found.tsx`
- `src/app/[locale]/loading.tsx`
- `src/app/[locale]/error.tsx`
- Internal non-linked QA trigger routes for loading and route-error validation
### Definition of Done (verifiable conditions with commands)
- `pnpm lint` completes successfully
- `pnpm build` completes successfully
- Anonymous users see shell navigation with home/sign-in/sign-up actions and do not see dashboard/settings/sign-out controls
- Authenticated users see shell navigation with dashboard/settings/sign-out controls and do not see sign-in/sign-up controls
- Navigating to `/en/does-not-exist` renders the localized 404 UI
- Navigating to a non-localized unmatched URL renders the root 404 UI
- Navigating between locale routes can surface a deterministic loading fallback with `[data-testid="route-loading"]`
- Triggering the internal QA error route renders the locale error boundary with a retry control
### Must Have
- Shell lives at `[locale]` scope, not root scope
- Auth-awareness in nav is client-hydrated via Better Auth session hook
- Existing page-level redirect guards remain untouched as the only access-control source of truth
- Theme switcher remains available from the shared header
- All shell/fallback copy is translated in both locales
- Stable selectors/test IDs exist for nav, auth action groups, loading, 404, and retry controls
### Must NOT Have (guardrails, AI slop patterns, scope boundaries)
- No avatar dropdown, profile menu, locale switcher, breadcrumbs, sidebar, or mobile drawer in this slice
- No root-layout auth gating with `isAuthenticated()` in `src/app/[locale]/layout.tsx`
- No `app/global-error.tsx` or provider-failure handling in this slice
- No page-specific skeleton systems for dashboard/settings/auth pages
- No new test framework setup
- No hard-coded paths when route constants already exist
## Verification Strategy
> ZERO HUMAN INTERVENTION - all verification is agent-executed.
- Test decision: tests-after using agent-executed browser QA plus lint/build
- QA policy: every task includes explicit selectors, routes, and expected visibility states
- Evidence: `.sisyphus/evidence/task-{N}-{slug}.{ext}`
## Execution Strategy
### Parallel Execution Waves
> Target: 5-8 tasks per wave. <3 per wave (except final) = under-splitting.
> Extract shared dependencies as Wave-1 tasks for max parallelism.
Wave 1: 1) translation contract and selectors, 2) shell primitives + theme control, 3) auth-aware shell integration
Wave 2: 4) localized/root 404 handling, 5) route loading fallback, 6) locale error boundary + QA trigger
### Dependency Matrix (full, all tasks)
- 1 blocks 2, 3, 4, 5, 6
- 2 blocks 3, 4, 5, 6
- 3 blocks 4, 5, 6 because shell selectors and layout structure become QA baseline
- 4, 5, and 6 can proceed in parallel after 1-3
- 6 blocks final verification wave because retry/error QA depends on its internal trigger route
### Agent Dispatch Summary (wave → task count → categories)
- Wave 1 → 3 tasks → quick, visual-engineering, unspecified-high
- Wave 2 → 3 tasks → unspecified-high, quick, unspecified-high
## TODOs
> Implementation + Test = ONE task. Never separate.
> EVERY task MUST have: Agent Profile + Parallelization + QA Scenarios.
- [x] 1. Establish shell and fallback translation contract
**What to do**: Add new translation namespaces for shell navigation and fallback UX in both locale files before touching UI. Define exact labels for brand/home, dashboard, settings, sign-in, sign-up, sign-out, loading title/description, localized 404 title/description/actions, and route-error title/description/retry. Add a small selector contract in the plan implementation itself by reserving stable `data-testid` names that later tasks must use verbatim: `app-shell`, `app-nav`, `nav-public-links`, `nav-private-links`, `nav-auth-actions`, `theme-switcher`, `route-loading`, `localized-not-found`, `root-not-found`, `route-error`, `route-error-retry`, `sign-out-button`.
**Must NOT do**: Do not create a third locale. Do not move existing namespaces. Do not hardcode copy directly into components.
**Recommended Agent Profile**:
- Category: `quick` - Reason: bounded i18n and selector contract work across two JSON files
- Skills: `[]` - Existing repo patterns are sufficient
- Omitted: `writing` - This is product-copy extension, not long-form documentation
**Parallelization**: Can Parallel: YES | Wave 1 | Blocks: 2, 3, 4, 5, 6 | Blocked By: none
**References** (executor has NO interview context - be exhaustive):
- Translation pattern: `messages/en.json:1-58` - existing namespace structure to extend instead of flattening
- Translation mirror: `messages/pl.json` - keep key parity with `messages/en.json`
- Existing auth copy: `src/components/auth/AuthForm.tsx:62-225` - current `AuthPage` key usage pattern
- Existing route labels: `src/app/[locale]/dashboard/page.tsx:26-40` - server-side translation usage for page copy
**Acceptance Criteria** (agent-executable only):
- [ ] `messages/en.json` and `messages/pl.json` contain matching shell and fallback namespaces/keys
- [ ] Reserved `data-testid` names are documented in code comments-free implementation choices and used consistently in later tasks
- [ ] `pnpm build` succeeds after translation additions
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Translation key parity check
Tool: Bash
Steps: Run a Node script that loads `messages/en.json` and `messages/pl.json`, extracts the new shell/fallback namespaces, and compares key sets recursively.
Expected: Script exits 0 and prints no missing-key differences.
Evidence: .sisyphus/evidence/task-1-translation-parity.txt
Scenario: Existing auth copy not regressed
Tool: Playwright
Steps: Start app; open `/en/sign-in`; assert the sign-in title and forgot-password link still render.
Expected: Existing auth form renders with non-empty text and no raw i18n keys.
Evidence: .sisyphus/evidence/task-1-auth-copy-regression.png
```
**Commit**: YES | Message: `feat(i18n): add shell and fallback copy` | Files: `messages/en.json`, `messages/pl.json`
- [x] 2. Build shared shell primitives and refactor ThemeChanger for header use
**What to do**: Introduce the minimal shell primitives under `src/components/core/` needed for the shared layout: a server-friendly shell wrapper (`AppShell`), a presentational nav/header component (`AppNav`), and a compact theme control by refactoring `ThemeChanger` into a header-sized control that still uses `@wrksz/themes`. Keep the shell visual language aligned with existing card/button primitives: simple top border/header, constrained content width, no dropdowns, no drawer, no avatar. Ensure every interactive control exposes the reserved `data-testid` values from Task 1.
**Must NOT do**: Do not fetch auth state here. Do not add mobile menu logic, locale switcher, breadcrumbs, or profile UI. Do not leave the old verbose “The current theme is...” wording in the header.
**Recommended Agent Profile**:
- Category: `visual-engineering` - Reason: shared shell UX and header ergonomics must feel intentional, not placeholder
- Skills: `[]` - Existing UI primitives cover the work
- Omitted: `frontend-ui-ux` - The UI is deliberately minimal and tightly anchored to repo primitives
**Parallelization**: Can Parallel: NO | Wave 1 | Blocks: 3, 4, 5, 6 | Blocked By: 1
**References** (executor has NO interview context - be exhaustive):
- Root provider context: `src/app/layout.tsx:26-38` - shell components will render under these providers
- Locale insertion point: `src/app/[locale]/layout.tsx:10-25` - current wrapper that later task will extend
- Theme control baseline: `src/components/core/ThemeChanger.tsx:5-28` - refactor this component instead of creating duplicate theme logic
- Button styling: `src/components/ui/button.tsx:7-67` - use existing variants/sizes
- Card styling baseline: `src/components/ui/card.tsx:5-103` - follow existing spacing, radius, and muted sections if cardized sub-sections are needed
**Acceptance Criteria** (agent-executable only):
- [ ] New shell primitives exist under `src/components/core/` and compile without auth coupling
- [ ] `ThemeChanger` becomes header-compatible and no longer renders debug-like explanatory text
- [ ] Shell primitives expose stable selectors: `app-shell`, `app-nav`, and `theme-switcher`
- [ ] `pnpm lint` passes after component creation/refactor
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Header primitives render on a locale route
Tool: Playwright
Steps: Open `/en`; inspect the DOM for `[data-testid="app-shell"]`, `[data-testid="app-nav"]`, and `[data-testid="theme-switcher"]`.
Expected: All three selectors exist exactly once and are visible in the viewport.
Evidence: .sisyphus/evidence/task-2-shell-primitives.png
Scenario: Theme control is compact and interactive
Tool: Playwright
Steps: On `/en`, interact with the header theme control and toggle from the default theme to dark.
Expected: The control remains in the header, toggles theme successfully, and does not render the old debug sentence.
Evidence: .sisyphus/evidence/task-2-theme-toggle.png
```
**Commit**: YES | Message: `feat(shell): add shared app shell primitives` | Files: `src/components/core/AppShell.tsx`, `src/components/core/AppNav.tsx`, `src/components/core/ThemeChanger.tsx`, any minimal supporting files
- [x] 3. Integrate an auth-aware shell into the locale layout without changing route protection semantics
**What to do**: Extend `src/app/[locale]/layout.tsx` to render the shared shell around all locale routes. Keep locale validation and `setRequestLocale(locale)` exactly as the first logic in the layout. Implement auth-aware nav actions in a client component (for example `AuthNavActions`) that uses `authClient.useSession()` and `authClient.signOut(...)` with a success redirect to `routes.public.home`. Show home plus auth links (`sign-in`, `sign-up`) when unauthenticated; show home plus private links (`dashboard`, `settings`) and a sign-out button when authenticated. Keep access control in the pages themselves; the shell only changes visibility of actions.
**Must NOT do**: Do not call `isAuthenticated()` in `[locale]/layout.tsx`. Do not redirect from the shell. Do not hide the shell from public auth pages. Do not hardcode `/sign-in`, `/dashboard`, or `/settings`.
**Recommended Agent Profile**:
- Category: `unspecified-high` - Reason: mixed server/client composition with auth session hydration and layout integration
- Skills: `[]` - Existing auth client and layout patterns are sufficient
- Omitted: `playwright` - QA uses Playwright, implementation should stay focused on code
**Parallelization**: Can Parallel: NO | Wave 1 | Blocks: 4, 5, 6 | Blocked By: 1, 2
**References** (executor has NO interview context - be exhaustive):
- Locale layout anchor: `src/app/[locale]/layout.tsx:10-25` - preserve locale guard and wrap children with shell
- Route contract: `src/lib/routes.ts:1-14` - use these constants for every nav link and sign-out redirect target
- Client auth client: `src/lib/auth-client.ts:1-6` - Better Auth client instance for `useSession()` and `signOut()`
- Better Auth sign-out pattern: `https://github.com/better-auth/better-auth/blob/main/docs/content/docs/authentication/email-password.mdx` - `authClient.signOut({ fetchOptions: { onSuccess: ... } })`
- Existing protected-route semantics: `src/app/[locale]/dashboard/page.tsx:16-24` - keep these page-level guards intact
- Existing auth link styling: `src/components/auth/AuthForm.tsx:109-220` - link/button patterns already used in auth UI
**Acceptance Criteria** (agent-executable only):
- [ ] `[locale]/layout.tsx` renders the shell around all locale routes while preserving locale validation and `setRequestLocale(locale)`
- [ ] Anonymous state shows `[data-testid="nav-public-links"]` and hides `[data-testid="nav-private-links"]` and `[data-testid="sign-out-button"]`
- [ ] Authenticated state shows `[data-testid="nav-private-links"]` and `[data-testid="sign-out-button"]` and hides public auth links
- [ ] Clicking sign-out returns the user to `routes.public.home` and removes private nav actions
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Anonymous shell state on public route
Tool: Playwright
Steps: Open a fresh browser context; visit `/en/sign-in`; assert `[data-testid="app-nav"]` is visible; inspect public/private action groups.
Expected: `[data-testid="nav-public-links"]` is visible with sign-in/sign-up links; `[data-testid="nav-private-links"]` and `[data-testid="sign-out-button"]` are absent.
Evidence: .sisyphus/evidence/task-3-anonymous-shell.png
Scenario: Authenticated shell state and sign-out
Tool: Playwright
Steps: Create a fresh QA user via `/en/sign-up` (for example `shell-nav@example.com` / `TemplatePass123!`); after auto-sign-in lands on the authenticated flow, visit `/en/dashboard`; assert dashboard/settings/sign-out controls; click `[data-testid="sign-out-button"]`.
Expected: User lands on `/en` or `/`; private controls disappear; sign-in/sign-up controls are visible again.
Evidence: .sisyphus/evidence/task-3-authenticated-shell.png
```
**Commit**: YES | Message: `feat(shell): integrate auth-aware nav into locale layout` | Files: `src/app/[locale]/layout.tsx`, `src/components/core/AuthNavActions.tsx`, related shell imports
- [x] 4. Add localized and root not-found handling with explicit catch-all coverage
**What to do**: Add three coordinated pieces: `src/app/[locale]/not-found.tsx` for localized `notFound()` rendering inside the locale segment, `src/app/[locale]/[...rest]/page.tsx` that immediately calls `notFound()` to catch unknown localized URLs such as `/en/unknown`, and `src/app/not-found.tsx` as a root fallback for unmatched non-localized requests that bypass locale routing. The localized 404 must render inside the shared shell and use localized copy. The root 404 must be intentionally minimal, must not depend on locale context, and must not attempt to render the locale shell.
**Must NOT do**: Do not enable experimental `global-not-found`. Do not rely on `[locale]/not-found.tsx` alone for unmatched localized URLs. Do not duplicate the full shell in `src/app/not-found.tsx`.
**Recommended Agent Profile**:
- Category: `unspecified-high` - Reason: Next.js file-convention work across locale and root routing layers
- Skills: `[]` - Existing routing patterns plus docs references are enough
- Omitted: `ultrabrain` - this is nuanced but not research-heavy anymore
**Parallelization**: Can Parallel: YES | Wave 2 | Blocks: Final verification wave | Blocked By: 1, 2, 3
**References** (executor has NO interview context - be exhaustive):
- Locale layout invalid-locale behavior: `src/app/[locale]/layout.tsx:17-23` - invalid locales already call `notFound()`
- Route constants: `src/lib/routes.ts:1-14` - use `routes.public.home` for return-home actions where locale context exists
- Next.js not-found docs: `https://nextjs.org/docs/app/api-reference/file-conventions/not-found` - root `app/not-found.tsx` handles global unmatched URLs
- next-intl error-files guide: `https://next-intl.dev/docs/environments/error-files` - localized 404 requires `[locale]/not-found.tsx` plus `[locale]/[...rest]/page.tsx`
- Existing card style: `src/components/ui/card.tsx:23-103` - use for localized 404 presentation
**Acceptance Criteria** (agent-executable only):
- [ ] `/en/does-not-exist` resolves via `[locale]/[...rest]/page.tsx` into the localized 404 UI
- [ ] Invalid locale handling from `[locale]/layout.tsx` still resolves to a 404 outcome instead of crashing
- [ ] A non-localized unmatched request resolves to `src/app/not-found.tsx`
- [ ] Localized 404 shows shell/nav; root 404 does not depend on locale shell
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Unknown localized route shows localized 404 inside shell
Tool: Playwright
Steps: Visit `/en/does-not-exist`; inspect `[data-testid="localized-not-found"]` and `[data-testid="app-nav"]`.
Expected: Localized 404 card is visible; shell navigation is still present; return-home action links to localized home.
Evidence: .sisyphus/evidence/task-4-localized-not-found.png
Scenario: Non-localized unmatched route shows root 404 fallback
Tool: Playwright
Steps: Visit `/totally-missing-route`; inspect `[data-testid="root-not-found"]`.
Expected: Root 404 UI is visible; localized shell selector `[data-testid="app-shell"]` is absent.
Evidence: .sisyphus/evidence/task-4-root-not-found.png
```
**Commit**: YES | Message: `feat(routing): add localized and root not-found handling` | Files: `src/app/[locale]/not-found.tsx`, `src/app/[locale]/[...rest]/page.tsx`, `src/app/not-found.tsx`
- [x] 5. Add a deterministic locale route loading fallback
**What to do**: Add `src/app/[locale]/loading.tsx` as the single shared loading fallback for localized routes. The loading UI must be lightweight, shell-compatible, and selector-stable via `[data-testid="route-loading"]`. Use the existing `Spinner` and current visual primitives. Do not fetch runtime data in the loading file. Ensure the loading UI is designed to render while the shell remains interactive, which means the shell itself must not perform blocking auth work. To make QA deterministic, also add one internal non-linked slow route such as `src/app/[locale]/__qa/slow/page.tsx` that intentionally awaits ~1500ms before rendering success content.
**Must NOT do**: Do not add per-page skeleton files in this slice. Do not put auth/session fetching into `[locale]/layout.tsx`. Do not use vague text-only fallback without the stable selector. Do not expose the slow QA route in navigation or docs user flows.
**Recommended Agent Profile**:
- Category: `quick` - Reason: single-file route fallback plus selector discipline
- Skills: `[]` - Existing spinner/card primitives are enough
- Omitted: `visual-engineering` - this should stay intentionally simple
**Parallelization**: Can Parallel: YES | Wave 2 | Blocks: Final verification wave | Blocked By: 1, 2, 3
**References** (executor has NO interview context - be exhaustive):
- Locale layout caveat: `https://nextjs.org/docs/app/api-reference/file-conventions/loading` - loading fallback will not cover blocking runtime data in the same layout
- Spinner primitive: `src/components/ui/spinner.tsx:5-12` - reuse existing spinner icon and status semantics
- Existing shell insertion: `src/app/[locale]/layout.tsx:10-25` - loading will render beneath this layout
- Existing page spacing: `src/app/[locale]/dashboard/page.tsx:28-41` - use similar content width and spacing rhythm
**Acceptance Criteria** (agent-executable only):
- [ ] `src/app/[locale]/loading.tsx` exists and renders a visible fallback with `[data-testid="route-loading"]`
- [ ] The fallback can render while the shell remains mounted
- [ ] Internal slow QA route deterministically triggers the loading fallback
- [ ] `pnpm build` succeeds with the new loading file in place
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Loading fallback appears during deterministic slow route
Tool: Playwright
Steps: Visit `/en/__qa/slow`; immediately inspect the page before the delayed route resolves.
Expected: `[data-testid="route-loading"]` becomes visible before the success content appears, while `[data-testid="app-nav"]` remains visible.
Evidence: .sisyphus/evidence/task-5-route-loading.png
Scenario: Loading fallback does not replace the shell chrome
Tool: Playwright
Steps: Repeat `/en/__qa/slow` navigation and capture the header region plus loading region simultaneously.
Expected: Header navigation remains mounted; only the route content area swaps to the loading fallback.
Evidence: .sisyphus/evidence/task-5-shell-persistence.png
```
**Commit**: YES | Message: `feat(ux): add locale route loading fallback` | Files: `src/app/[locale]/loading.tsx`, `src/app/[locale]/__qa/slow/page.tsx`
- [x] 6. Add a localized route error boundary with deterministic retry QA
**What to do**: Add `src/app/[locale]/error.tsx` as a client component that uses localized copy, surfaces a stable `[data-testid="route-error"]` wrapper, and exposes a retry control `[data-testid="route-error-retry"]` wired to `unstable_retry()`. To make QA deterministic without adding a full test framework, add one internal non-linked route such as `src/app/[locale]/__qa/route-error/page.tsx` that throws a controlled runtime error for boundary validation. The QA route should continue throwing on retry so the expected post-click state is deterministic. Keep it excluded from navigation and docs user flows; it exists only so agents can validate the boundary reliably.
**Must NOT do**: Do not add `app/global-error.tsx`. Do not leak raw server error details into user-facing copy. Do not put the QA route in nav or route constants.
**Recommended Agent Profile**:
- Category: `unspecified-high` - Reason: file-convention error boundary plus deterministic QA mechanism
- Skills: `[]` - Existing i18n and UI patterns are enough
- Omitted: `writing` - localized copy already comes from Task 1
**Parallelization**: Can Parallel: YES | Wave 2 | Blocks: Final verification wave | Blocked By: 1, 2, 3
**References** (executor has NO interview context - be exhaustive):
- Next.js error docs: `https://nextjs.org/docs/app/api-reference/file-conventions/error` - `error.tsx` must be a client component and should use `unstable_retry()` in v16.2+
- next-intl error-files guide: `https://next-intl.dev/docs/environments/error-files` - error boundary can use translated messages under existing provider context
- Root provider context: `src/app/layout.tsx:32-38` - translated client error boundary can rely on intl provider because scope is `[locale]`, not root
- Existing toast pattern: `src/components/auth/AuthForm.tsx:78-106` - follow repo style for non-blocking error handling if logging/toasts are added
- Button primitive: `src/components/ui/button.tsx:44-64` - use for retry CTA
**Acceptance Criteria** (agent-executable only):
- [ ] `src/app/[locale]/error.tsx` exists, is a client component, and calls `unstable_retry()` from its retry control
- [ ] Error UI is localized and does not expose raw server stack details
- [ ] Internal QA route throws and renders the route error boundary reliably
- [ ] Retry control is selector-stable via `[data-testid="route-error-retry"]`
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Controlled route error renders localized boundary
Tool: Playwright
Steps: Visit `/en/__qa/route-error`; inspect `[data-testid="route-error"]` and retry control.
Expected: Localized route error UI is visible; the page does not hard-crash into a blank screen; retry control is present.
Evidence: .sisyphus/evidence/task-6-route-error.png
Scenario: Retry re-renders the failing boundary deterministically
Tool: Playwright
Steps: On `/en/__qa/route-error`, click `[data-testid="route-error-retry"]` once.
Expected: Retry executes without a white-screen crash and the same localized error fallback re-renders cleanly because the QA route is intentionally still failing.
Evidence: .sisyphus/evidence/task-6-route-error-retry.png
```
**Commit**: YES | Message: `feat(ux): add locale error boundary and retry flow` | Files: `src/app/[locale]/error.tsx`, `src/app/[locale]/__qa/route-error/page.tsx`, any minimal supporting component
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.
> **Do NOT auto-proceed after verification. Wait for user's explicit approval before marking work complete.**
> **Never mark F1-F4 as checked before getting user's okay.** Rejection or user feedback -> fix -> re-run -> present again -> wait for okay.
- [x] F1. Plan Compliance Audit — oracle
- [x] F2. Code Quality Review — unspecified-high
- [x] F3. Real Manual QA — unspecified-high (+ playwright if UI)
- [x] F4. Scope Fidelity Check — deep
## Commit Strategy
- Commit 1: `feat(i18n): add shell and fallback copy`
- Commit 2: `feat(shell): add shared app shell primitives`
- Commit 3: `feat(shell): integrate auth-aware nav into locale layout`
- Commit 4: `feat(routing): add localized and root not-found handling`
- Commit 5: `feat(ux): add locale route loading fallback`
- Commit 6: `feat(ux): add locale error boundary and retry flow`
## Success Criteria
- The template gains one consistent shell across localized routes without changing access-control semantics
- Anonymous and authenticated navigation states are deterministic and testable
- Unknown localized URLs and non-localized unmatched URLs both resolve to intentional 404 experiences
- Route loading and runtime error states are intentionally designed, translated, and recoverable

View file

@ -0,0 +1,361 @@
# Dashboard Password Reset Flow
## TL;DR
> **Summary**: Add an authenticated password-change flow reachable from the dashboard, with the actual form hosted on a protected `/settings` page that follows the repo's existing server-page plus client-component auth pattern.
> **Deliverables**:
> - Dashboard entry affordance to account security settings
> - Protected `/settings` page with localized password-change UI
> - Better Auth `changePassword` integration with explicit success/error handling
> - Agent-executable manual QA evidence for auth protection, validation, and credential rotation
> **Effort**: Medium
> **Parallel**: YES - 2 waves
> **Critical Path**: 1 -> 3 -> 4 -> 5 -> 6
## Context
### Original Request
Implement a user-facing password reset flow from `src/app/dashboard/page.tsx`.
### Interview Summary
- User selected the authenticated in-session flow, not forgot-password by email.
- Dashboard is the entry point, but the actual form should live on `/settings`.
- Manual QA only for now; no test infrastructure setup in this slice.
### Metis Review (gaps addressed)
- Protect `/settings` explicitly instead of assuming auth.
- Keep scope to password/security only; do not expand into general settings architecture.
- Use Better Auth's existing `changePassword` flow instead of inventing custom backend plumbing.
- Define concrete browser QA with fixed credentials and explicit redirect/session assertions.
## Work Objectives
### Core Objective
Provide a secure, authenticated password-change experience that users can reach from the dashboard and complete on a dedicated settings page.
### Deliverables
- Auth-protected `src/app/settings/page.tsx`
- Dashboard entry UI from `src/app/dashboard/page.tsx` to `/settings`
- Localized client password-change component under `src/components/`
- Better Auth client submission flow with `revokeOtherSessions: true`
- Updated translation files for all new strings
- QA evidence captured for happy path and failure cases
### Definition of Done (verifiable conditions with commands)
- `pnpm lint` completes successfully
- `pnpm build` completes successfully
- Visiting `/settings` while unauthenticated redirects to sign-in with a callback back to `/settings`
- A signed-in user can open `/settings`, submit a valid current/new password pair, and subsequently sign in with only the new password
- Wrong current password and mismatched confirmation both fail with clear user feedback
### Must Have
- Protected `/settings` route with explicit redirect behavior
- Dashboard affordance linking to `/settings`
- Fields for `currentPassword`, `newPassword`, and `confirmPassword`
- Client validation for required fields, confirmation match, and rejecting `newPassword === currentPassword`
- Backend-driven password validation through Better Auth's configured policies, including HIBP plugin
- Success/error feedback and loading state consistent with existing auth UI
- New strings added to both `messages/en.json` and `messages/pl.json`
### Must NOT Have (guardrails, AI slop patterns, scope boundaries)
- No forgot-password email flow, token flow, reset page, or mail templates
- No full settings IA, profile editor, preferences, or navigation redesign
- No new test framework, Playwright project, or CI setup in this slice
- No custom Convex mutation/server action for password change unless Better Auth client API proves unusable during execution
- No route-protection behavior left implicit or undocumented
## Verification Strategy
> ZERO HUMAN INTERVENTION - all verification is agent-executed.
- Test decision: none + existing repo has no test framework; use browser-driven QA and build/lint verification
- QA policy: Every task includes agent-executed scenarios with exact credentials, selectors, and expected outcomes
- Evidence: `.sisyphus/evidence/task-{N}-{slug}.{ext}`
## Execution Strategy
### Parallel Execution Waves
> Target: 5-8 tasks per wave. <3 per wave (except final) = under-splitting.
> Extract shared dependencies as Wave-1 tasks for max parallelism.
Wave 1: 1) route protection and settings page scaffold, 2) dashboard entry affordance, 3) settings UI shell + translations
Wave 2: 4) client validation and field UX, 5) Better Auth submission + session behavior, 6) polish and end-to-end manual QA capture
### Dependency Matrix (full, all tasks)
- 1 blocks 4, 5, 6
- 2 is independent after route constants are confirmed; feeds 6 QA coverage
- 3 blocks 4 and 5
- 4 blocks 5 and 6
- 5 blocks 6
- 6 blocks final verification wave
### Agent Dispatch Summary (wave -> task count -> categories)
- Wave 1 -> 3 tasks -> unspecified-high, visual-engineering, visual-engineering
- Wave 2 -> 3 tasks -> quick, unspecified-high, unspecified-high
## TODOs
> Implementation + Test = ONE task. Never separate.
> EVERY task MUST have: Agent Profile + Parallelization + QA Scenarios.
- [ ] 1. Add protected `/settings` page scaffold
**What to do**: Read the relevant Next.js App Router docs under `node_modules/next/dist/docs/` before coding, then create `src/app/settings/page.tsx` as a server page that checks authentication with the existing server auth helpers from `src/lib/auth-server.ts`. If unauthenticated, redirect to `/sign-in?callbackURL=/settings`. If authenticated, render a dedicated client settings/password component and keep the page focused on security only.
**Must NOT do**: Do not implement forgot-password here. Do not place the full form directly in `src/app/settings/page.tsx`. Do not create middleware or a broader settings layout in this slice.
**Recommended Agent Profile**:
- Category: `unspecified-high` - Reason: auth-aware App Router work with redirect behavior and server/client boundary decisions
- Skills: `[]` - No special skill required beyond repo pattern matching
- Omitted: `playwright` - UI automation belongs in QA, not scaffolding
**Parallelization**: Can Parallel: YES | Wave 1 | Blocks: 4, 5, 6 | Blocked By: none
**References** (executor has NO interview context - be exhaustive):
- Pattern: `src/app/sign-in/page.tsx` - thin server page wrapper pattern already used for auth routes
- Pattern: `src/app/sign-up/page.tsx` - same page composition pattern for route-level wrappers
- Auth helper: `src/lib/auth-server.ts` - server-side auth utilities available for checking session/auth state
- Route contract: `src/lib/routes.ts` - `/settings` already exists as a private route constant
- Existing target: `src/app/dashboard/page.tsx` - current dashboard stub that will link into this route
- External: `node_modules/next/dist/docs/` - project instruction requires reading relevant Next.js docs before implementation
**Acceptance Criteria** (agent-executable only):
- [ ] `src/app/settings/page.tsx` exists and remains a server page wrapper rather than a client-heavy file
- [ ] Unauthenticated access to `/settings` redirects to `/sign-in?callbackURL=/settings`
- [ ] Authenticated access to `/settings` renders the dedicated password settings component
- [ ] `pnpm lint` passes after the page scaffold is added
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Unauthenticated redirect from settings
Tool: Playwright
Steps: Open a fresh browser context; visit http://localhost:3000/settings directly.
Expected: Browser lands on `/sign-in` and preserves a callback URL containing `/settings`.
Evidence: .sisyphus/evidence/task-1-settings-redirect.png
Scenario: Authenticated settings page render
Tool: Playwright
Steps: Sign up `changeflow@example.com` with password `OldPass123!`; sign in; visit http://localhost:3000/settings.
Expected: The page renders a password/security card instead of redirecting away.
Evidence: .sisyphus/evidence/task-1-settings-render.png
```
**Commit**: YES | Message: `add protected settings page scaffold and dashboard entry` | Files: `src/app/settings/page.tsx`, `src/lib/routes.ts` (only if needed), related imports
- [ ] 2. Add dashboard entry to account security
**What to do**: Replace the dashboard stub with a minimal, intentional dashboard card or section that exposes a single clear affordance to account security settings. Link to `/settings` via existing route constants. Keep the page intentionally narrow: one CTA, one short description, no full settings UI embedded here.
**Must NOT do**: Do not turn dashboard into a general settings page. Do not add unrelated profile, billing, or preferences UI.
**Recommended Agent Profile**:
- Category: `visual-engineering` - Reason: small UI composition task that must feel deliberate, not boilerplate
- Skills: `[]` - Existing component library provides all primitives needed
- Omitted: `playwright` - verification happens via QA scenario, not implementation
**Parallelization**: Can Parallel: YES | Wave 1 | Blocks: 6 QA coverage only | Blocked By: none
**References** (executor has NO interview context - be exhaustive):
- Target page: `src/app/dashboard/page.tsx` - currently only a stub and should become the entry point
- Route contract: `src/lib/routes.ts` - use the existing private route constant for settings
- UI pattern: `src/components/ui/card.tsx` - use existing card primitives for a compact dashboard section
- UI pattern: `src/components/ui/button.tsx` - use existing button variants for the CTA
**Acceptance Criteria** (agent-executable only):
- [ ] `/dashboard` contains a visible CTA linking to `/settings`
- [ ] The CTA uses route constants rather than a duplicated hard-coded path
- [ ] The dashboard change stays focused on password/security entry only
- [ ] `pnpm lint` passes after the dashboard update
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Dashboard exposes security entry
Tool: Playwright
Steps: Sign in as `changeflow@example.com` with `OldPass123!`; open http://localhost:3000/dashboard; inspect the visible security/settings CTA.
Expected: Clicking the CTA navigates to `/settings`.
Evidence: .sisyphus/evidence/task-2-dashboard-entry.png
Scenario: Dashboard does not embed the password form
Tool: Playwright
Steps: Load http://localhost:3000/dashboard while signed in.
Expected: No `currentPassword`, `newPassword`, or `confirmPassword` input fields are present on the dashboard page itself.
Evidence: .sisyphus/evidence/task-2-dashboard-no-form.png
```
**Commit**: YES | Message: `add protected settings page scaffold and dashboard entry` | Files: `src/app/dashboard/page.tsx`
- [ ] 3. Create the localized password settings component shell
**What to do**: Create a dedicated client component for password change under `src/components/settings/` and wire it into `src/app/settings/page.tsx`. Use the same card, field, button, spinner, and password input-group patterns already used in `src/components/auth/AuthForm.tsx`. Add all new translation keys to both locale files up front so no hard-coded strings remain.
**Must NOT do**: Do not reuse `AuthForm` directly for password change. Do not leave untranslated labels, helper text, button copy, or toast messages.
**Recommended Agent Profile**:
- Category: `visual-engineering` - Reason: new client UI should blend with existing auth UI while staying scoped
- Skills: `[]` - Existing component system is sufficient
- Omitted: `writing` - copy additions are straightforward translation entries, not long-form docs
**Parallelization**: Can Parallel: YES | Wave 1 | Blocks: 4, 5, 6 | Blocked By: 1
**References** (executor has NO interview context - be exhaustive):
- Pattern: `src/components/auth/AuthForm.tsx` - existing client auth form conventions for card layout, field composition, spinner, toast, and password visibility controls
- UI primitives: `src/components/ui/field.tsx`, `src/components/ui/input-group.tsx`, `src/components/ui/button.tsx`, `src/components/ui/spinner.tsx`
- Translation files: `messages/en.json`, `messages/pl.json` - extend with a dedicated settings/security namespace or equivalent flat keys
- Existing i18n usage: `src/components/auth/AuthForm.tsx` - `useTranslations(...)` pattern
**Acceptance Criteria** (agent-executable only):
- [ ] A dedicated client component exists under `src/components/settings/`
- [ ] `/settings` renders a single password/security card with localized heading, description, and button text
- [ ] Both `messages/en.json` and `messages/pl.json` contain all strings needed for the new UI
- [ ] No new hard-coded user-facing strings remain in the component
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Password settings shell renders expected fields
Tool: Playwright
Steps: Sign in; visit http://localhost:3000/settings; inspect the form.
Expected: Inputs named `currentPassword`, `newPassword`, and `confirmPassword` are visible, plus a submit button.
Evidence: .sisyphus/evidence/task-3-settings-shell.png
Scenario: Localized shell does not regress rendering
Tool: Playwright
Steps: Load the page in the default locale; inspect heading, field labels, and submit button.
Expected: All visible strings are rendered from translation data with no raw key names or empty labels.
Evidence: .sisyphus/evidence/task-3-settings-i18n.png
```
**Commit**: YES | Message: `add password change form UI and localization` | Files: `src/components/settings/PasswordChangeCard.tsx`, `src/app/settings/page.tsx`, `messages/en.json`, `messages/pl.json`
- [ ] 4. Implement client-side validation and field UX
**What to do**: Define a dedicated Zod schema for the password-change form that requires `currentPassword`, reuses `defaultPasswordValidator()` for `newPassword`, enforces `confirmPassword === newPassword`, and rejects `newPassword === currentPassword`. Provide inline field errors and disable duplicate submissions with a loading state. Keep password visibility UX consistent with the existing auth form pattern.
**Must NOT do**: Do not add uppercase/number/special-character composition rules back into the client. Do not rely only on toasts for validation errors that belong inline.
**Recommended Agent Profile**:
- Category: `quick` - Reason: bounded form-schema and field-state work inside one component
- Skills: `[]` - Existing validation patterns are enough
- Omitted: `ultrabrain` - no deep algorithmic work needed
**Parallelization**: Can Parallel: NO | Wave 2 | Blocks: 5, 6 | Blocked By: 1, 3
**References** (executor has NO interview context - be exhaustive):
- Validation helper: `src/constants.ts` - current shared minimum-length validator to reuse for `newPassword`
- Pattern: `src/components/auth/AuthForm.tsx` - React Hook Form + Zod resolver usage and inline `FieldError` handling
- UI primitives: `src/components/ui/field.tsx`, `src/components/ui/input-group.tsx`, `src/components/ui/spinner.tsx`
**Acceptance Criteria** (agent-executable only):
- [ ] Submitting mismatched `newPassword` and `confirmPassword` surfaces an inline error on the confirmation field
- [ ] Submitting the same value for current and new password surfaces an inline error before any network request
- [ ] Submitting an empty required field surfaces inline validation without a success toast
- [ ] Submit button disables while the request is pending
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Mismatched confirmation is blocked locally
Tool: Playwright
Steps: Sign in; open `/settings`; fill `currentPassword=OldPass123!`, `newPassword=NewPass123!`, `confirmPassword=Mismatch123!`; submit.
Expected: Inline error appears for `confirmPassword`; no network-driven success state is shown.
Evidence: .sisyphus/evidence/task-4-confirm-mismatch.png
Scenario: Reusing the same password is blocked locally
Tool: Playwright
Steps: Fill `currentPassword=OldPass123!`, `newPassword=OldPass123!`, `confirmPassword=OldPass123!`; submit.
Expected: Inline error indicates the new password must differ from the current password.
Evidence: .sisyphus/evidence/task-4-same-password.png
```
**Commit**: YES | Message: `add password change form UI and localization` | Files: `src/components/settings/PasswordChangeCard.tsx`, related validation helpers only if needed
- [ ] 5. Wire Better Auth password change submission
**What to do**: Connect the settings form to Better Auth's client password-change API using the authenticated session. Submit `currentPassword`, `newPassword`, and `revokeOtherSessions: true`. On success, stay on `/settings`, clear sensitive fields, and show a localized success toast. On failure, surface the returned error cleanly without clearing the current user session.
**Must NOT do**: Do not create a custom Convex mutation for password change unless the Better Auth client call is proven unusable. Do not redirect away from settings after success. Do not silently swallow backend errors.
**Recommended Agent Profile**:
- Category: `unspecified-high` - Reason: auth-sensitive submission flow with session behavior and backend error handling
- Skills: `[]` - Existing Better Auth client is already configured in repo
- Omitted: `writing` - this is behavior wiring, not docs work
**Parallelization**: Can Parallel: NO | Wave 2 | Blocks: 6 | Blocked By: 1, 3, 4
**References** (executor has NO interview context - be exhaustive):
- Client auth entry: `src/lib/auth-client.ts` - Better Auth client already configured with Convex plugin
- Existing auth pattern: `src/components/auth/AuthForm.tsx` - request pending state, toast handling, Better Auth client invocation patterns
- Backend policy: `convex/auth.ts` - Better Auth email/password configuration with HIBP plugin and min/max length
- Settings component: `src/components/settings/PasswordChangeCard.tsx` - task 3/4 output
**Acceptance Criteria** (agent-executable only):
- [ ] Valid submission calls Better Auth password-change API and succeeds for the signed-in user
- [ ] Wrong current password yields a visible error state without logging the user out of the current session
- [ ] Successful submission clears all password inputs and leaves the user on `/settings`
- [ ] `pnpm build` passes after the integration is complete
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Wrong current password is rejected
Tool: Playwright
Steps: Sign in; open `/settings`; fill `currentPassword=WrongPass123!`, `newPassword=NewPass123!`, `confirmPassword=NewPass123!`; submit.
Expected: Error feedback appears; the current session remains usable; revisiting `/dashboard` still works in the same tab.
Evidence: .sisyphus/evidence/task-5-wrong-current-password.png
Scenario: Valid password change succeeds
Tool: Playwright
Steps: Fill `currentPassword=OldPass123!`, `newPassword=NewPass123!`, `confirmPassword=NewPass123!`; submit.
Expected: Success toast/message appears; fields clear; page remains on `/settings`.
Evidence: .sisyphus/evidence/task-5-successful-change.png
```
**Commit**: YES | Message: `wire password change flow and verify session behavior` | Files: `src/components/settings/PasswordChangeCard.tsx`, any minimal auth client touch-ups only if required
- [ ] 6. Finalize credential-rotation behavior and capture manual QA evidence
**What to do**: Validate the full user journey end to end: dashboard -> settings -> change password -> sign out -> sign back in with the new credential. Because `revokeOtherSessions: true` is the default for this slice, also verify in a second browser context that another active session becomes unauthorized after the password change. Capture screenshots/logs into the evidence paths referenced below.
**Must NOT do**: Do not add automated test infrastructure or CI in order to satisfy this task. Do not leave QA as a vague manual checklist.
**Recommended Agent Profile**:
- Category: `unspecified-high` - Reason: auth/stateful browser verification across two sessions
- Skills: [`playwright`] - Use browser automation to validate auth and session transitions precisely
- Omitted: `frontend-ui-ux` - this task is verification-focused, not design-focused
**Parallelization**: Can Parallel: NO | Wave 2 | Blocks: Final verification wave | Blocked By: 1, 2, 3, 4, 5
**References** (executor has NO interview context - be exhaustive):
- Entry page: `src/app/dashboard/page.tsx`
- Protected page: `src/app/settings/page.tsx`
- Form component: `src/components/settings/PasswordChangeCard.tsx`
- Auth UI precedent: `src/components/auth/AuthForm.tsx`
- Command surface: `package.json` - use existing `pnpm dev`, `pnpm lint`, and `pnpm build`
**Acceptance Criteria** (agent-executable only):
- [ ] After password change, signing in with `OldPass123!` fails and signing in with `NewPass123!` succeeds
- [ ] A second active browser context is no longer authorized after the password change
- [ ] Evidence files exist for redirect, validation failure, success state, old-password rejection, and second-session invalidation
- [ ] `pnpm lint` and `pnpm build` both pass on the final implementation
**QA Scenarios** (MANDATORY - task incomplete without these):
```
Scenario: Old password fails and new password succeeds
Tool: Playwright
Steps: Complete the password change; sign out; attempt sign-in with `OldPass123!`; then attempt sign-in with `NewPass123!`.
Expected: Old password sign-in fails; new password sign-in succeeds and returns to an authenticated page.
Evidence: .sisyphus/evidence/task-6-old-vs-new-credential.png
Scenario: Other sessions are revoked
Tool: Playwright
Steps: Open session A and session B signed in as the same user; change the password in session A; in session B navigate to `/dashboard` or refresh.
Expected: Session B is redirected to sign-in or otherwise loses authenticated access.
Evidence: .sisyphus/evidence/task-6-revoke-other-sessions.png
```
**Commit**: YES | Message: `wire password change flow and verify session behavior` | Files: no net-new feature files expected beyond minimal polish; evidence output only
## Final Verification Wave (MANDATORY - after ALL implementation tasks)
> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.
> **Do NOT auto-proceed after verification. Wait for user's explicit approval before marking work complete.**
> **Never mark F1-F4 as checked before getting user's okay.** Rejection or user feedback -> fix -> re-run -> present again -> wait for okay.
- [ ] F1. Plan Compliance Audit - oracle
- [ ] F2. Code Quality Review - unspecified-high
- [ ] F3. Real Manual QA - unspecified-high (+ playwright if UI)
- [ ] F4. Scope Fidelity Check - deep
## Commit Strategy
- Commit 1: `add protected settings page scaffold and dashboard entry`
- Commit 2: `add password change form UI and localization`
- Commit 3: `wire password change flow and verify session behavior`
## Success Criteria
- The feature remains scoped to authenticated password change only
- `/dashboard` clearly leads users into account security settings
- `/settings` is inaccessible without authentication
- Password change succeeds only with the correct current password and valid new password
- QA evidence proves redirect, validation failure, success path, and new-credential sign-in behavior

View file

@ -11,6 +11,7 @@
import type * as auth from "../auth.js"; import type * as auth from "../auth.js";
import type * as hello from "../hello.js"; import type * as hello from "../hello.js";
import type * as http from "../http.js"; import type * as http from "../http.js";
import type * as lib_resend from "../lib/resend.js";
import type { import type {
ApiFromModules, ApiFromModules,
@ -22,6 +23,7 @@ declare const fullApi: ApiFromModules<{
auth: typeof auth; auth: typeof auth;
hello: typeof hello; hello: typeof hello;
http: typeof http; http: typeof http;
"lib/resend": typeof lib_resend;
}>; }>;
/** /**

6
eslint.config.mjs Normal file
View file

@ -0,0 +1,6 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import nextTypeScript from 'eslint-config-next/typescript';
const eslintConfig = [...nextCoreWebVitals, ...nextTypeScript];
export default eslintConfig;

View file

@ -55,6 +55,21 @@
"Submit": "Save new password", "Submit": "Save new password",
"PasswordUpdatedSuccessfully": "Password updated successfully" "PasswordUpdatedSuccessfully": "Password updated successfully"
}, },
"CookieConsent": {
"Title": "We use cookies",
"Description": "We use cookies to enhance your experience. Necessary cookies are always active. You can manage your preferences below.",
"Necessary": "Necessary",
"NecessaryDescription": "Required for authentication, security, and basic site functionality.",
"Analytics": "Analytics",
"AnalyticsDescription": "Helps us understand how visitors interact with our site.",
"Marketing": "Marketing",
"MarketingDescription": "Used to deliver personalized advertisements and measure their effectiveness.",
"AcceptAll": "Accept all",
"AcceptSelected": "Save preferences",
"AcceptNecessary": "Accept necessary only",
"ManagePreferences": "Manage preferences",
"SavePreferences": "Save preferences"
},
"Navigation": { "Navigation": {
"Home": "Home", "Home": "Home",
"Dashboard": "Dashboard", "Dashboard": "Dashboard",

View file

@ -55,6 +55,21 @@
"Submit": "Zapisz nowe hasło", "Submit": "Zapisz nowe hasło",
"PasswordUpdatedSuccessfully": "Hasło zostało zmienione" "PasswordUpdatedSuccessfully": "Hasło zostało zmienione"
}, },
"CookieConsent": {
"Title": "Używamy plików cookie",
"Description": "Używamy plików cookie, aby poprawić Twoje doświadczenie. Niezbędne pliki cookie są zawsze aktywne. Możesz zarządzać preferencjami poniżej.",
"Necessary": "Niezbędne",
"NecessaryDescription": "Wymagane do uwierzytelniania, bezpieczeństwa i podstawowej funkcjonalności strony.",
"Analytics": "Analityczne",
"AnalyticsDescription": "Pomaga nam zrozumieć, jak odwiedzający korzystają z naszej strony.",
"Marketing": "Marketingowe",
"MarketingDescription": "Używane do wyświetlania spersonalizowanych reklam i mierzenia ich skuteczności.",
"AcceptAll": "Akceptuj wszystkie",
"AcceptSelected": "Zapisz preferencje",
"AcceptNecessary": "Akceptuj tylko niezbędne",
"ManagePreferences": "Zarządzaj preferencjami",
"SavePreferences": "Zapisz preferencje"
},
"Navigation": { "Navigation": {
"Home": "Strona główna", "Home": "Strona główna",
"Dashboard": "Panel", "Dashboard": "Panel",

View file

@ -6,6 +6,7 @@ import { ThemeProvider } from '@wrksz/themes/next';
import { Geist_Mono } from 'next/font/google'; import { Geist_Mono } from 'next/font/google';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
import { CookieBanner } from '@/components/core/CookieBanner';
import { TooltipProvider } from '../components/ui/tooltip'; import { TooltipProvider } from '../components/ui/tooltip';
const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-mono' }); const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-mono' });
@ -35,6 +36,7 @@ export default async function RootLayout({
<NextIntlClientProvider> <NextIntlClientProvider>
<main>{children}</main> <main>{children}</main>
<Toaster /> <Toaster />
<CookieBanner />
</NextIntlClientProvider> </NextIntlClientProvider>
</ThemeProvider> </ThemeProvider>
</TooltipProvider> </TooltipProvider>

View file

@ -0,0 +1,150 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { useCookieConsent } from "@/hooks/use-cookie-consent";
import { cn } from "@/lib/utils";
export function CookieBanner() {
const t = useTranslations("CookieConsent");
const {
hasResponded,
analytics,
marketing,
acceptAll,
acceptNecessaryOnly,
savePreferences,
} = useCookieConsent();
const [showPreferences, setShowPreferences] = useState(false);
const [prefAnalytics, setPrefAnalytics] = useState(analytics);
const [prefMarketing, setPrefMarketing] = useState(marketing);
if (hasResponded) {
return null;
}
return (
<div
data-testid="cookie-banner"
className="fixed bottom-0 left-0 right-0 z-[100] p-4"
>
<Card className="mx-auto max-w-3xl shadow-lg">
<CardHeader className="pb-2">
<CardTitle className="text-lg">{t("Title")}</CardTitle>
<CardDescription>{t("Description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{showPreferences && (
<div className="space-y-3 rounded-lg border p-4">
<div className="flex items-start gap-3">
<Checkbox checked disabled id="cookie-necessary" />
<div className="grid gap-1 leading-none">
<label
htmlFor="cookie-necessary"
className="text-sm font-medium"
>
{t("Necessary")}
</label>
<p className="text-xs text-muted-foreground">
{t("NecessaryDescription")}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
id="cookie-analytics"
checked={prefAnalytics}
onCheckedChange={(checked) =>
setPrefAnalytics(checked === true)
}
/>
<div className="grid gap-1 leading-none">
<label
htmlFor="cookie-analytics"
className="text-sm font-medium"
>
{t("Analytics")}
</label>
<p className="text-xs text-muted-foreground">
{t("AnalyticsDescription")}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
id="cookie-marketing"
checked={prefMarketing}
onCheckedChange={(checked) =>
setPrefMarketing(checked === true)
}
/>
<div className="grid gap-1 leading-none">
<label
htmlFor="cookie-marketing"
className="text-sm font-medium"
>
{t("Marketing")}
</label>
<p className="text-xs text-muted-foreground">
{t("MarketingDescription")}
</p>
</div>
</div>
</div>
)}
<div className="flex flex-wrap gap-2">
{showPreferences ? (
<>
<Button
variant="default"
onClick={() =>
savePreferences({
analytics: prefAnalytics,
marketing: prefMarketing,
})
}
>
{t("SavePreferences")}
</Button>
<Button
variant="outline"
onClick={() => setShowPreferences(false)}
>
{t("AcceptNecessary")}
</Button>
</>
) : (
<>
<Button variant="default" onClick={acceptAll}>
{t("AcceptAll")}
</Button>
<Button variant="outline" onClick={acceptNecessaryOnly}>
{t("AcceptNecessary")}
</Button>
<Button
variant="ghost"
onClick={() => setShowPreferences(true)}
>
{t("ManagePreferences")}
</Button>
</>
)}
</div>
</CardContent>
</Card>
</div>
);
}

View file

@ -0,0 +1,30 @@
import { cn } from "@/lib/utils";
interface CheckboxProps {
checked?: boolean;
disabled?: boolean;
onCheckedChange?: (checked: boolean) => void;
id?: string;
}
export function Checkbox({
checked,
disabled,
onCheckedChange,
id,
}: CheckboxProps) {
return (
<input
id={id}
type="checkbox"
checked={checked}
disabled={disabled}
onChange={(e) => onCheckedChange?.(e.target.checked)}
className={cn(
"h-4 w-4 shrink-0 cursor-pointer rounded-sm border border-primary",
"accent-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-50"
)}
/>
);
}

View file

@ -0,0 +1,96 @@
"use client";
import { useCallback, useEffect, useState } from "react";
export type CookieCategory = "necessary" | "analytics" | "marketing";
export interface CookieConsentState {
necessary: boolean;
analytics: boolean;
marketing: boolean;
hasResponded: boolean;
}
const STORAGE_KEY = "cookie-consent";
function getInitialState(): CookieConsentState {
if (typeof window === "undefined") {
return {
necessary: true,
analytics: false,
marketing: false,
hasResponded: false,
};
}
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return JSON.parse(stored) as CookieConsentState;
}
} catch {
// eslint-disable-next-line no-empty
}
return {
necessary: true,
analytics: false,
marketing: false,
hasResponded: false,
};
}
export function useCookieConsent() {
const [state, setState] = useState<CookieConsentState>(getInitialState);
const acceptAll = useCallback(() => {
const newState: CookieConsentState = {
necessary: true,
analytics: true,
marketing: true,
hasResponded: true,
};
setState(newState);
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
}, []);
const acceptNecessaryOnly = useCallback(() => {
const newState: CookieConsentState = {
necessary: true,
analytics: false,
marketing: false,
hasResponded: true,
};
setState(newState);
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
}, []);
const savePreferences = useCallback(
(preferences: Pick<CookieConsentState, "analytics" | "marketing">) => {
const newState: CookieConsentState = {
necessary: true,
analytics: preferences.analytics,
marketing: preferences.marketing,
hasResponded: true,
};
setState(newState);
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
},
[]
);
const isAllowed = useCallback(
(category: CookieCategory) => {
return state[category];
},
[state]
);
return {
...state,
acceptAll,
acceptNecessaryOnly,
savePreferences,
isAllowed,
};
}