# Website Architecture Violations Report

Scope: `apps/website/**` aligned against `docs/architecture/website/**`, with the authoritative contract at [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).

This report lists violations as: rule ⇒ evidence ⇒ impact ⇒ fix direction.

This version also includes a concrete remediation plan with file-by-file actions.

---

## 0) Target architecture (what good looks like)

Per-route structure required by [`WEBSITE_RSC_PRESENTATION.md`](docs/architecture/website/WEBSITE_RSC_PRESENTATION.md:50):

```text
app/<route>/page.tsx                server: composition only
lib/page-queries/<Route>...         server: fetch + assemble Page DTO + return PageQueryResult
app/<route>/<Route>PageClient.tsx   client: ViewData creation + client state
templates/<Route>Template.tsx       client: pure UI, ViewData-only, no computation
lib/view-models/**                  client-only: ViewModels + Presenters (pure)
lib/display-objects/**              deterministic formatting helpers (no locale APIs)
```

Hard boundaries to enforce:

- No server code imports from `lib/view-models/**` (guardrail: [`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:9)).
- Templates import neither `lib/view-models/**` nor `lib/display-objects/**` (guardrail: [`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:16)).
- No locale APIs (`Intl.*`, `toLocale*`) in any formatting path (guardrails: [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:41), [`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:13)).
- All writes enter via Server Actions (contract: [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:18)).

### 0.1 Naming: stop calling everything “dto” (clarify the model taxonomy)

This repo currently uses `dto` as a generic variable name, which collapses three different concepts.

We will use the contract’s model names and enforce naming conventions to prevent category errors.

Authoritative types:

- **API Transport DTO**: returned by `apps/api` over HTTP. In this repo, often generated from OpenAPI.
  - Canonical placement: [`apps/website/lib/types/**`](apps/website/lib/types/League.ts:1) per [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:41).
  - Naming rule:
    - Type names end with `ApiDto` (preferred) or `DTO` (existing generated types).
    - Variable names: `apiDto`, `apiResponse`.

- **Page DTO**: website-owned server-to-client payload assembled by PageQueries.
  - Canonical placement: `apps/website/lib/page-queries/**` per [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:54).
  - Naming rule:
    - Type names end with `PageDto`.
    - Variable names: `pageDto`.

- **ViewData**: the only allowed input to Templates.
  - Canonical placement: `apps/website/templates/**` per [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:86).
  - Naming rule:
    - Type names end with `ViewData`.
    - Variable names: `viewData`.

Other model names we should keep explicit:

- **ViewModel**: client-only class instance.
  - Naming rule: `*ViewModel` types/classes, variables `viewModel`.

- **Command DTO** (write intent): the payload sent to Server Actions / API for mutations.
  - Naming rule: `*CommandDto`, variables `commandDto`.

Non-negotiable rule:

- Never use a bare `dto` variable name. Use `apiDto`, `pageDto`, `viewData`, or `commandDto`.

This is a Clean Architecture guardrail: when names are precise, wrong-layer dependencies become obvious during review.

### 0.2 Enforce the taxonomy with required abstractions (not lint)

You asked for “proper abstractions that must be implemented” (interfaces and/or abstract classes), rather than naming-only or ESLint enforcement.

The goal is that the *compiler* (and the module boundaries) makes it hard to mix:

- API Transport DTO vs Page DTO vs ViewData
- server-only vs client-only
- orchestration vs presentation mapping

Proposed required abstractions (website-owned):

1. **API Client contracts** (transport boundary)
   - Interface per API area, returns **ApiDto/DTO only**.
   - Example: `LeaguesApiClient` already exists; enforce that it returns `*DTO` types only.
   - Prohibit returning ViewModels.

2. **Service contracts** (orchestration boundary, server-safe)
   - Interface per feature: `*Service` returns **ApiDto** or **PageDto** only.
   - Must be stateless.
   - Must not import `lib/view-models/**`.

3. **PageQuery contract** (server composition boundary)
   - A generic interface:
     - `execute(params) -> PageQueryResult<PageDto>`
   - PageQueries must only depend on API clients + services (manual wiring).
   - Must not import `lib/view-models/**`.

4. **Presenter contract** (pure mapping boundary, client-only)
   - Presenter is a pure mapper:
     - `PageDto -> ViewData` OR `PageDto -> ViewModel` OR `ViewModel -> ViewData`
   - No HTTP, no side effects.

5. **ViewModel base** (client-only)
   - Optional but useful to enforce non-serializability:
     - e.g. `abstract class ViewModel { abstract toViewData(): ViewData }`
   - No `toDTO()` method that resembles server DTOs (it tempts server usage).

6. **DisplayObject base** (deterministic formatting)
   - Abstract base or interface to enforce “no locale APIs” policy by convention and review.
   - Encourages `new MoneyDisplay(amount).formatted()` style, but only used from Presenters/ViewModels.

Implementation note:

- These abstractions live in `apps/website/lib/contracts/**` so every feature must conform.
- This is complementary to guardrails: guardrails prevent forbidden imports; contracts ensure the correct “shape” of each layer.

---

## 1) Remediation plan (explicit actions)

This is the **minimum actionable work** to align the codebase. It is ordered so earlier steps unblock later ones.

### 1.1 Block server-side container usage (DI contract)

**Goal:** zero usages of [`ContainerManager.getInstance().getContainer()`](apps/website/lib/di/container.ts:67) in server execution paths.

Actions (final standard):

1. Ban DI from server execution paths entirely.
   - `apps/website/app/**/page.tsx` and `apps/website/lib/page-queries/**` MUST NOT use DI.
   - PageQueries MUST do manual wiring (construct API clients + services explicitly).

2. Delete or rewrite any helper that makes server DI easy.
   - Replace any server usage of [`PageDataFetcher.fetch()`](apps/website/lib/page/PageDataFetcher.ts:15) with manual wiring.
   - Stop calling [`ContainerManager.getInstance().getContainer()`](apps/website/lib/page/PageDataFetcher.ts:21) from server.

3. Enforcement:
   - Add a guardrail test/ESLint rule that forbids importing [`ContainerManager`](apps/website/lib/di/container.ts:61) from:
     - `apps/website/app/**/page.tsx`
     - `apps/website/lib/page-queries/**`

Deliverable for this section:

- A CI-failing rule that prevents future regressions.
- A grep for `getContainer()` in server modules returns zero hits.

Rationale: why DI is banned on the server side

This is a deliberate safety simplification.

1. Next.js server execution is concurrent; DI containers make it easy to accidentally share state across requests.
   - The contract explicitly warns about this risk ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:227), [`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:9)).
   - The singleton container is explicitly called out as unsafe ([`ContainerManager`](apps/website/lib/di/container.ts:61), [`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:33)).

2. The codebase already contains stateful “service” patterns (e.g. blockers stored as instance fields), which are harmless in a per-request graph but dangerous in a shared container.
   - Example: [`LeagueService`](apps/website/lib/services/leagues/LeagueService.ts:95) stores [`submitBlocker`](apps/website/lib/services/leagues/LeagueService.ts:96).

3. Clean Architecture intent: server modules are composition roots; manual wiring makes dependencies explicit and reviewable.
   - This reduces hidden coupling and prevents accidental imports of client-only types like `lib/view-models/**`.

Note: the strict contract technically allows request-scoped server DI ([`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:25)), but we choose the stronger rule (manual wiring only) to eliminate an entire class of cross-request bugs and enforcement ambiguity.

### 1.2 Standardize PageQuery contract (one discriminated union)

**Goal:** all PageQueries return the exact contract described in [`WEBSITE_PAGE_QUERIES.md`](docs/architecture/website/WEBSITE_PAGE_QUERIES.md:17).

Actions:

1. Define a single `PageQueryResult` type in a shared place under `apps/website/lib/page-queries/**`.
2. Update all PageQueries to return:
   - `ok` with `{ dto }`
   - `notFound`
   - `redirect` with `{ to }`
   - `error` with `{ errorId }` (do not return raw Error objects)

Example violation:

- Current local type uses `data`/`destination`: [`PageQueryResult<TData>`](apps/website/lib/page-queries/DashboardPageQuery.ts:11)

Deliverable:

- All pages use `switch(result.status)` and call `notFound()` / `redirect()` only from `page.tsx`.

### 1.3 Fix Template purity (ViewData-only, no imports, no compute)

**Goal:** Templates become “dumb” renderers: no `useMemo`, no filtering/sorting, no ViewModel or DisplayObject imports.

Actions:

1. Remove ViewModel props from Templates.
   - Example: replace [`DriverRankingsTemplateProps.drivers: DriverLeaderboardItemViewModel[]`](apps/website/templates/DriverRankingsTemplate.tsx:19)
     with `drivers: DriverRankingsViewData[]` (a ViewData type).

2. Remove compute from Templates.
   - Example: move [`drivers.filter()`](apps/website/templates/DriverRankingsTemplate.tsx:73) and [`sort(...)`](apps/website/templates/DriverRankingsTemplate.tsx:81)
     into:
     - a ViewModel (client) OR
     - a Presenter (client) OR
     - the PageClient container (client) if it is driven by UI state.

3. Remove DisplayObject usage from Templates.
   - Example violation: [`LeagueRoleDisplay`](apps/website/templates/LeagueDetailTemplate.tsx:13)
   - Replace with primitive values in ViewData (badge label, badge classes).

Deliverable:

- Guardrail tests (or ESLint) that fail if any `apps/website/templates/**` imports from:
  - `apps/website/lib/view-models/**`
  - `apps/website/lib/display-objects/**`

### 1.4 Eliminate locale APIs from all formatting paths

**Goal:** zero usage of `Intl.*` and `toLocale*` in:

- `apps/website/templates/**`
- `apps/website/app/**/page.tsx`
- `apps/website/lib/view-models/**`
- `apps/website/lib/display-objects/**`
- shared formatting helpers like [`time.ts`](apps/website/lib/utilities/time.ts:1)

Actions:

1. Replace locale formatting in Templates.
   - Example: [`toLocaleDateString()`](apps/website/templates/RacesTemplate.tsx:148)
   - Replace with: (a) deterministic formatter in a Display Object, or (b) API-provided display labels.

2. Replace locale formatting in ViewModels.
   - Example: [`BillingViewModel.InvoiceViewModel.formattedTotalAmount`](apps/website/lib/view-models/BillingViewModel.ts:85)
   - Replace with deterministic formatting helpers (no runtime locale).

3. Remove locale formatting helpers in `lib/utilities`.
   - Example: [`formatDate()`](apps/website/lib/utilities/time.ts:48)
   - Replace with deterministic formatters.

Deliverable:

- The search pattern `\\bIntl\\.|toLocale` returns zero results for production code.

### 1.5 Enforce write boundary: Server Actions only

**Goal:** no client-initiated writes (no `fetch` POST/PUT/PATCH/DELETE from client components).

Actions:

1. Replace client logout POST with Server Action.
   - Example violation: [`fetch('/api/auth/logout', { method: 'POST' })`](apps/website/components/profile/UserPill.tsx:212)
   - Replace with a Server Action and use `<form action={logoutAction}>`.
   - Ensure the action does the mutation and then triggers navigation and/or revalidation.

Deliverable:

- Search for `fetch(` with write methods in client components returns zero hits.

### 1.6 Remove UX Blockers from services (state leakage risk)

**Goal:** services remain stateless and safe regardless of DI scope.

Actions:

1. Remove or relocate blockers from [`LeagueService`](apps/website/lib/services/leagues/LeagueService.ts:95).
   - Evidence: [`submitBlocker`](apps/website/lib/services/leagues/LeagueService.ts:96) and [`throttle`](apps/website/lib/services/leagues/LeagueService.ts:97)
2. Re-introduce blockers at the client boundary (component/hook) where they belong.

Deliverable:

- No stateful blockers stored on service instances.

### 1.7 Consolidate hooks into the canonical folder

**Goal:** one place for React-only helpers: `apps/website/lib/hooks/**`.

Contract:

- Canonical placement is `apps/website/lib/hooks/**` ([`WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:32), [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:217)).

Actions:

1. Move `apps/website/hooks/**` → `apps/website/lib/hooks/**`.
2. Update imports across `apps/website/**`.
3. Remove `hooks/` from TS include list in [`tsconfig.json`](apps/website/tsconfig.json:70).
4. Add a guardrail test to fail if `apps/website/hooks/**` reappears.

### 1.8 Tighten the Service boundary (server-safe only)

**Goal:** services become safe to call from server modules (Page Queries) with zero ambiguity.

New non-negotiable rule:

- `apps/website/lib/services/**` returns **API DTOs or Page DTOs only** (JSON-serializable). It MUST NOT import or instantiate anything from `apps/website/lib/view-models/**`.

Why (Clean Architecture + SOLID):

- SRP: services orchestrate IO and composition; they do not prepare UI.
- DIP: server modules depend on service interfaces/DTO contracts, not client-only classes.
- Eliminates the current “maybe server, maybe client” ambiguity.

Actions:

1. Refactor any service returning ViewModels to return DTOs/Page DTOs instead.
2. Move mapping/formatting responsibilities into Presenters colocated with ViewModels.
3. Add a boundary test: forbid `lib/services/**` importing `lib/view-models/**`.

### 1.9 Remove `AdminViewModelService` (Presenter misclassified as Service)

**Goal:** all mapping lives in Presenters, not in services.

Action:

- Replace [`AdminViewModelService`](apps/website/lib/services/AdminViewModelService.ts:10) with a Presenter colocated with its ViewModel(s), then delete the service + test.

### 1.10 Fix the confirmed “PageQuery constructs ViewModel” violation

Evidence:

- [`ProfilePageQuery.execute()`](apps/website/lib/page-queries/ProfilePageQuery.ts:34) resolves [`DriverService`](apps/website/lib/page-queries/ProfilePageQuery.ts:3) and then calls `viewModel.toDTO()` ([`ProfilePageQuery`](apps/website/lib/page-queries/ProfilePageQuery.ts:54)).

Rule violated:

- Page Queries MUST NOT instantiate ViewModels ([`WEBSITE_PAGE_QUERIES.md`](docs/architecture/website/WEBSITE_PAGE_QUERIES.md:31)).

Action:

- Refactor any service method used by a PageQuery that currently returns a ViewModel to return a Page DTO instead.

---

## 12) Generic integrity rules for untrusted transport data (no case studies)

This is the durable architectural rule behind the “`as` looks vulnerable” concern.

### 12.1 Rule: treat API Transport DTO values as untrusted input

Even with OpenAPI generation, runtime values can drift (backend bug, contract mismatch, migrations, older clients).

Therefore:

- Never use `as SomeClosedUnion` on fields coming from an API response.
- Never assume string enums are safe.

### 12.2 Where validation/coercion belongs

- **API Transport DTO** remains raw (what the API sent).
- **Page DTO** can remain raw but should be structurally stable.
- **Presenter/ViewModel** is the correct place to normalize/coerce *for UI resilience*.

This keeps the website as a delivery layer: we’re not enforcing business rules; we’re preventing UI crashes.

### 12.3 Required pattern: parsers for string unions

Define small pure parsers (in a Presenter-adjacent module) for every “closed set” field:

- `parseSocialPlatform(value: unknown): SocialPlatform | 'unknown'`
- `parseAchievementIcon(value: unknown): AchievementIcon | 'unknown'`
- `parseAchievementRarity(value: unknown): AchievementRarity | 'unknown'`

Policy (agreed):

- ViewModel keeps `'unknown'` for debugging/telemetry.
- ViewData omits unknown items (UI stays clean).

This keeps code safe without turning the website into a second source of truth (the API still owns validation).

---

## 2) Route-by-route refactor recipes (concrete file actions)

This section tells you **exactly what to change** for the already-identified hot spots.

### 2.1 `profile/leagues` route

Current violations:

- DI container used on server: [`ContainerManager.getInstance().getContainer()`](apps/website/app/profile/leagues/page.tsx:45)
- Inline server “template” inside `page.tsx`: [`ProfileLeaguesTemplate()`](apps/website/app/profile/leagues/page.tsx:82)
- Server code manipulates ViewModels: [`LeagueSummaryViewModel`](apps/website/app/profile/leagues/page.tsx:13)

Required changes:

1. Create a PageQuery: `apps/website/lib/page-queries/ProfileLeaguesPageQuery.ts`.
   - It should:
     - Read session (server-side) and determine `currentDriverId`.
     - Call API clients directly (manual wiring).
     - Assemble a **Page DTO** like:
       - `ownedLeagues: Array<{ leagueId; name; description; membershipRole }>`
       - `memberLeagues: Array<{ leagueId; name; description; membershipRole }>`
     - Return `PageQueryResult`.

2. Replace `apps/website/app/profile/leagues/page.tsx` with composition:
   - Call PageQuery.
   - Switch on result.
   - Render `<ProfileLeaguesPageClient dto={dto} />`.

3. Create `apps/website/app/profile/leagues/ProfileLeaguesPageClient.tsx` (`'use client'`).
   - Convert Page DTO into ViewData (via Presenter function in `lib/view-models/**`).
   - Render a pure Template.

4. Create `apps/website/templates/ProfileLeaguesTemplate.tsx`.
   - Props: ViewData only.
   - No API calls. No `useMemo`. No filtering/sorting.

### 2.2 `teams` routes

Current violations:

- Server route does sorting/filtering: [`computeDerivedData()`](apps/website/app/teams/page.tsx:12)
- Server route imports ViewModels: [`TeamSummaryViewModel`](apps/website/app/teams/page.tsx:10)
- Team detail server route passes ViewModels into Template: [`TeamDetailTemplateWrapper()`](apps/website/app/teams/[id]/page.tsx:20)
- Placeholder “currentDriverId” on server: [`const currentDriverId = ''`](apps/website/app/teams/[id]/page.tsx:63)

Required changes:

1. Introduce PageQueries:
   - `TeamsPageQuery` returns Page DTO `{ teams: Array<{ id; name; rating; ...raw fields }> }`.
   - `TeamDetailPageQuery` returns Page DTO `{ team; memberships; currentDriverId; }` (raw serializable).

2. Move derived computations out of server route.
   - `teamsByLevel`, `topTeams`, etc. belong in client ViewModel or PageClient.

3. Add `TeamsPageClient.tsx` and `TeamDetailPageClient.tsx`.
   - Instantiate ViewModels client-side.
   - Produce ViewData for Templates.

4. Change Templates to take ViewData.
   - Stop passing ViewModels into [`TeamDetailTemplate`](apps/website/app/teams/[id]/page.tsx:22).

### 2.3 `dashboard` PageQuery

Current violations:

- Server PageQuery uses singleton DI container: [`ContainerManager.getInstance().getContainer()`](apps/website/lib/page-queries/DashboardPageQuery.ts:110)
- PageQuery imports from `lib/view-models/**`: [`DashboardOverviewViewModelData`](apps/website/lib/page-queries/DashboardPageQuery.ts:6)

Required changes:

1. Move the DTO-like shape out of view-models and into `lib/page-queries/**`.
2. Replace DI with manual wiring.
3. Return `PageQueryResult` as spec.
4. Ensure the result payload is a Page DTO (raw values only).

### 2.4 Logout write boundary

Current violation:

- Client-side POST request: [`fetch('/api/auth/logout', { method: 'POST' })`](apps/website/components/profile/UserPill.tsx:212)

Required changes:

1. Create a server action in an appropriate server-only module.
2. Replace the demo-user logout handler with a `<form action={logoutAction}>` submission.
3. Ensure post-logout navigation uses server-side redirect.

---

## 3) What to add to make this enforceable (guardrails)

Minimum guardrails to add:

1. ESLint or test guard: fail on `toLocale*` / `Intl.*` under `apps/website/**` formatting paths.
2. ESLint or test guard: fail if `apps/website/templates/**` imports ViewModels or Display Objects.
3. ESLint or test guard: fail if `apps/website/app/**/page.tsx` imports from `lib/view-models/**`.
4. ESLint or test guard: fail if server code imports [`ContainerManager`](apps/website/lib/di/container.ts:61).

---

## 4) Missing pieces found in the second scan (add to plan)

This section is the “coverage pass”: it enumerates additional concrete hotspots that must be refactored using the recipes above.

### 4.1 Remaining server-side singleton container usage (must be removed)

1. Home SSR data fetch uses container directly
   - Evidence: [`ContainerManager.getInstance().getContainer()`](apps/website/lib/services/home/getHomeData.ts:9)
   - Action: rewrite [`getHomeData()`](apps/website/lib/services/home/getHomeData.ts:8) to use explicit wiring (manual construction) or move the logic into a PageQuery that does manual wiring.

2. Profile page query dynamically imports container and uses singleton
   - Evidence: `(await import('@/lib/di/container')).ContainerManager.getInstance().getContainer()` in [`ProfilePageQuery`](apps/website/lib/page-queries/ProfilePageQuery.ts:43)
   - Action: ban all container usage in [`apps/website/lib/page-queries/**`](apps/website/lib/page-queries/DashboardPageQuery.ts:1); refactor to manual wiring.

Deliverable:

- Add these files to the “no server DI” guardrail allowlist/banlist:
  - [`getHomeData.ts`](apps/website/lib/services/home/getHomeData.ts:1)
  - [`ProfilePageQuery.ts`](apps/website/lib/page-queries/ProfilePageQuery.ts:1)

### 4.2 Routes still importing ViewModels from server modules (must be converted to Page DTO + PageClient)

The following are **server route modules** (App Router) that import ViewModels and therefore violate the server/client boundary.

Actions for each: create a PageQuery returning Page DTO, and a `*PageClient.tsx` that builds ViewData and renders the Template.

1. Leaderboards entry route
   - Evidence: ViewModel types imported in [`leaderboards/page.tsx`](apps/website/app/leaderboards/page.tsx:8)
   - Action:
     - Replace `*PageWrapper` patterns with a `*PageClient` container.
     - Ensure the template receives ViewData only (no ViewModels).
     - Remove ViewModel imports from:
       - [`leaderboards/page.tsx`](apps/website/app/leaderboards/page.tsx:8)
       - [`LeaderboardsPageWrapper.tsx`](apps/website/app/leaderboards/LeaderboardsPageWrapper.tsx:4)

2. Driver leaderboards route
   - Evidence: ViewModel import in [`leaderboards/drivers/page.tsx`](apps/website/app/leaderboards/drivers/page.tsx:7)
   - Action: same as above; rename wrapper to `DriverRankingsPageClient.tsx` and pass ViewData to Templates.

3. Team leaderboard route
   - Evidence: ViewModel import in [`teams/leaderboard/page.tsx`](apps/website/app/teams/leaderboard/page.tsx:7) and wrapper import in [`TeamLeaderboardPageWrapper.tsx`](apps/website/app/teams/leaderboard/TeamLeaderboardPageWrapper.tsx:5)
   - Action: same as above.

4. Leagues routes
   - Evidence:
     - [`LeagueDetailPageViewModel`](apps/website/app/leagues/[id]/page.tsx:13)
     - [`LeagueDetailPageViewModel`](apps/website/app/leagues/[id]/rulebook/page.tsx:13)
     - [`LeagueScheduleViewModel`](apps/website/app/leagues/[id]/schedule/page.tsx:13)
     - [`LeagueStandingsViewModel`](apps/website/app/leagues/[id]/standings/page.tsx:13) and [`DriverViewModel`](apps/website/app/leagues/[id]/standings/page.tsx:14)
   - Action:
     - Introduce PageQueries per route: `LeagueDetailPageQuery`, `LeagueRulebookPageQuery`, `LeagueSchedulePageQuery`, `LeagueStandingsPageQuery`.
     - PageQueries return Page DTO only.
     - Client creates ViewModels + ViewData.

5. Sponsor routes
   - Evidence: server route imports ViewModel and performs reduce/filter on ViewModel instance: [`AvailableLeaguesViewModel`](apps/website/app/sponsor/leagues/page.tsx:6)
   - Action:
     - Convert to PageQuery returning Page DTO.
     - Move summary computations (counts/averages) to Presenter/ViewModel in client.

6. Races + stewarding routes
   - Evidence:
     - ViewModel imports in [`races/[id]/page.tsx`](apps/website/app/races/[id]/page.tsx:7)
     - Non-canonical hooks usage and ViewModel transformer import in [`races/[id]/results/page.tsx`](apps/website/app/races/[id]/results/page.tsx:5)
     - ViewModel import in [`races/[id]/stewarding/page.tsx`](apps/website/app/races/[id]/stewarding/page.tsx:10)
   - Action:
     - Convert each to PageQuery + PageClient.
     - Move hooks from top-level `apps/website/hooks/**` into `apps/website/lib/hooks/**` (canonical per [`WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:47)).
     - Rename the “transformer” pattern to a Presenter under `lib/view-models/**` and ensure it stays deterministic.

7. Profile routes (beyond `profile/leagues`)
   - Evidence: ViewModel import and locale formatting in [`profile/page.tsx`](apps/website/app/profile/page.tsx:16) and `toLocaleDateString` usage (e.g. [`profile/page.tsx`](apps/website/app/profile/page.tsx:430))
   - Action: convert to PageQuery + ProfilePageClient + ViewData.

8. Races listing route doing server-side filtering
   - Evidence: [`races/all/page.tsx`](apps/website/app/races/all/page.tsx:39) uses `.filter` in route module
   - Action: same route split; if filtering is canonical (not UX-only), push it into API; if UX-only, do it client-side in a ViewModel.

### 4.3 “Templates” living under `app/**` (structural and guardrail gap)

The architecture assumes Templates are under `apps/website/templates/**`.

- Evidence: wallet UI is a “Template” in a route folder: [`WalletTemplate.tsx`](apps/website/app/leagues/[id]/wallet/WalletTemplate.tsx:1) and it imports a ViewModel: [`LeagueWalletViewModel`](apps/website/app/leagues/[id]/wallet/WalletTemplate.tsx:7)

Actions:

1. Move this file to `apps/website/templates/LeagueWalletTemplate.tsx`.
2. Ensure it takes ViewData only.
3. Add a guardrail: forbid `*Template.tsx` under `apps/website/app/**`.

### 4.4 Remaining write-boundary violations (client-side POST)

Additional client-side logout write:

- Evidence: [`fetch('/api/auth/logout', { method: 'POST' })`](apps/website/app/sponsor/settings/page.tsx:178)

Actions:

1. Replace with Server Action flow.
2. Ensure any other write HTTP calls from client code are removed (search pattern already exists).

### 4.5 Locale formatting still present outside Templates (components + lib)

The strict contract forbids locale APIs in formatting paths; the codebase currently uses `toLocale*` heavily in:

- Components (examples):
  - [`DriverTopThreePodium.tsx`](apps/website/components/DriverTopThreePodium.tsx:75)
  - [`TeamAdmin.tsx`](apps/website/components/teams/TeamAdmin.tsx:192)
  - [`UserPill.tsx`](apps/website/components/profile/UserPill.tsx:212)
- Infrastructure (example):
  - [`ErrorReplay.ts`](apps/website/lib/infrastructure/ErrorReplay.ts:214)
- ViewModels (examples):
  - [`BillingViewModel`](apps/website/lib/view-models/BillingViewModel.ts:86)
  - [`SponsorshipViewModel`](apps/website/lib/view-models/SponsorshipViewModel.ts:48)
  - [`LeagueMemberViewModel`](apps/website/lib/view-models/LeagueMemberViewModel.ts:22)
  - [`PaymentViewModel`](apps/website/lib/view-models/PaymentViewModel.ts:44)
  - [`utilities/time.ts`](apps/website/lib/utilities/time.ts:41)

Actions (make it explicit):

1. Introduce deterministic formatting primitives under `apps/website/lib/display-objects/**` (or a dedicated deterministic formatter module) and route **all** formatting through them.
2. Replace locale formatting calls in ViewModels first (largest fan-out).
3. Replace locale formatting in components by ensuring components consume ViewData strings/numbers that are already formatted deterministically.
4. Add guardrails for `apps/website/components/**` too (not only Templates), otherwise the same issue migrates.

---

## 5) Third scan: contract checks against the questions

This section explicitly answers:

- do we have any filtering, sorting, formatting or any business logic in pages or ui components?
- do we have clear separation of concerns?
- did you consider correct DI usage?
- did you consider server actions?

### 5.1 Filtering, sorting, formatting, business logic in `app/**`

Contract expectations:

- `app/**/page.tsx` should be server composition only ([`WEBSITE_RSC_PRESENTATION.md`](docs/architecture/website/WEBSITE_RSC_PRESENTATION.md:50), [`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:9)).
- Formatting must be deterministic and must not use `Intl.*` or `toLocale*` ([`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:41)).

Observed violations (representative, not exhaustive):

| Category | Evidence in `app/**` | Why this violates the contract | Required remediation |
|---|---|---|---|
| Filtering/sorting in `page.tsx` | `reduce/filter/sort` in [`computeDerivedData()`](apps/website/app/teams/page.tsx:14) | Server route does presentation shaping (guardrail violation). | Convert to PageQuery + `TeamsPageClient.tsx`; keep `page.tsx` orchestration only. |
| Filtering inside route module | Filtering races in [`races/all/page.tsx`](apps/website/app/races/all/page.tsx:40) | Filtering belongs in API (canonical) or in client ViewModel/PageClient (UX-only), not in route module. | Split route into PageQuery + PageClient. |
| “Authorization-like” decisions in route modules | `isAdmin` derived from role in [`races/[id]/results/page.tsx`](apps/website/app/races/[id]/results/page.tsx:80) and [`races/[id]/stewarding/page.tsx`](apps/website/app/races/[id]/stewarding/page.tsx:65) | Website may hide UI for UX but must not drift into enforcing security truth ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:186)). These checks are mixed with data/load/render responsibilities. | Ensure these are strictly UI flags derived from API truth and produced in client ViewModels/Presenters. |
| Locale formatting in route modules | `toLocaleDateString/toLocaleString` in [`profile/page.tsx`](apps/website/app/profile/page.tsx:430) and [`sponsor/campaigns/page.tsx`](apps/website/app/sponsor/campaigns/page.tsx:210) | Forbidden locale APIs in formatting paths. | Replace with deterministic formatting, passed via ViewData. |
| ViewModel instantiation in routes | `new DriverViewModel(...)` in [`leagues/[id]/standings/page.tsx`](apps/website/app/leagues/[id]/standings/page.tsx:66) | ViewModels are client-only ([`VIEW_MODELS.md`](docs/architecture/website/VIEW_MODELS.md:71)). | Replace with Page DTO; create ViewModels client-side only. |

### 5.2 Filtering, sorting, formatting, business logic in `components/**`

Observed violations (high signal):

| Category | Evidence in `components/**` | Why this violates the contract | Required remediation |
|---|---|---|---|
| Locale formatting in components | `toLocale*` in [`TeamAdmin.tsx`](apps/website/components/teams/TeamAdmin.tsx:192), [`DriverTopThreePodium.tsx`](apps/website/components/DriverTopThreePodium.tsx:75), [`NotificationCenter.tsx`](apps/website/components/notifications/NotificationCenter.tsx:84) | Determinism/hydration risk; forbidden formatting path. | Ensure components consume deterministic ViewData strings/numbers produced by Presenters/Display Objects. |
| Client-initiated write calls | `fetch('/api/auth/logout', { method: 'POST' })` in [`UserPill.tsx`](apps/website/components/profile/UserPill.tsx:212) and [`AdminLayout.tsx`](apps/website/components/admin/AdminLayout.tsx:67) | Violates Server Action-only write boundary ([`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:18)). | Replace with Server Action invoked via `<form action={...}>`. |

### 5.3 Separation of concerns

Answer: **No**; boundaries are blurred.

Concrete evidence:

1. Templates import ViewModels and compute.
   - Import example: [`DriverLeaderboardItemViewModel`](apps/website/templates/DriverRankingsTemplate.tsx:7)
   - Compute example: [`drivers.filter()`](apps/website/templates/DriverRankingsTemplate.tsx:73) and [`sort(...)`](apps/website/templates/DriverRankingsTemplate.tsx:81)

2. Templates exist under `app/**`.
   - Example: [`WalletTemplate.tsx`](apps/website/app/leagues/[id]/wallet/WalletTemplate.tsx:1)

3. Services construct ViewModels.
   - Example: `return new RaceDetailViewModel(...)` in [`RaceService`](apps/website/lib/services/races/RaceService.ts:24)
   - Example: `return ...map(... => new TeamSummaryViewModel(...))` in [`TeamService`](apps/website/lib/services/teams/TeamService.ts:35)

Remediation:

- Server: PageQueries return Page DTO.
- Client: Presenters/ViewModels produce ViewData.
- Templates: ViewData-only, no compute.

### 5.4 DI usage

Answer: **Not correct today**; server uses the singleton container.

Confirmed violations:

- Server route DI: [`ContainerManager.getInstance().getContainer()`](apps/website/app/profile/leagues/page.tsx:45)
- Server PageQuery DI: [`ContainerManager.getInstance().getContainer()`](apps/website/lib/page-queries/DashboardPageQuery.ts:110)
- Server helper enabling DI: [`PageDataFetcher.fetch()`](apps/website/lib/page/PageDataFetcher.ts:21)
- Server service function DI: [`getHomeData()`](apps/website/lib/services/home/getHomeData.ts:9)
- Dynamic import DI in PageQuery: [`ProfilePageQuery`](apps/website/lib/page-queries/ProfilePageQuery.ts:43)

Remediation:

1. Add guardrail: forbid server imports of [`ContainerManager`](apps/website/lib/di/container.ts:61).
2. Refactor these call sites to manual wiring or request-scoped container.

### 5.5 Server Actions

Answer: **Not fully enforced**; client-side POSTs exist.

Confirmed violations:

- Client POST logout in [`UserPill.tsx`](apps/website/components/profile/UserPill.tsx:212)
- Client POST logout in [`AdminLayout.tsx`](apps/website/components/admin/AdminLayout.tsx:67)
- Client POST logout in [`sponsor/settings/page.tsx`](apps/website/app/sponsor/settings/page.tsx:178)

Remediation:

1. Implement a Server Action for logout.
2. Replace all client-side POSTs with `<form action={logoutAction}>`.
3. Add guardrail to forbid `fetch` write methods in client code.

---

## A) DI contract violations (server-side singleton container usage)

| Rule | Evidence | Why this is a violation | Fix direction |
|---|---|---|---|
| Server `app/**/page.tsx` MUST NOT access the DI container ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:231), [`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:15)) | [`ContainerManager.getInstance().getContainer()`](apps/website/app/profile/leagues/page.tsx:45) | Server route is directly resolving from a singleton container; violates the client-first DI contract and request-safety. | Replace with a PageQuery + manual per-request wiring, or a request-scoped container factory. |
| `ContainerManager.getContainer()` is client-only ([`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:35)) | [`PageDataFetcher.fetch()`](apps/website/lib/page/PageDataFetcher.ts:15) calls [`ContainerManager.getInstance().getContainer()`](apps/website/lib/page/PageDataFetcher.ts:21) | This helper enables forbidden server access to the singleton container. | Ban server use of [`PageDataFetcher.fetch()`](apps/website/lib/page/PageDataFetcher.ts:15). Use explicit construction in PageQueries. |
| PageQueries may use DI only if request-scoped and stateless ([`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:20)) | [`DashboardPageQuery.execute()`](apps/website/lib/page-queries/DashboardPageQuery.ts:108) uses [`ContainerManager.getInstance().getContainer()`](apps/website/lib/page-queries/DashboardPageQuery.ts:110) | PageQuery runs on the server; the singleton container is explicitly unsafe for concurrent server requests. | Convert to manual wiring (construct API client + service per call), or create a fresh container per request. |

---

## B) RSC route structure violations (page.tsx doing more than composition)

| Rule | Evidence | Why this is a violation | Fix direction |
|---|---|---|---|
| `page.tsx` does composition only ([`WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:26), [`WEBSITE_RSC_PRESENTATION.md`](docs/architecture/website/WEBSITE_RSC_PRESENTATION.md:50)) | Data fetch + categorization + inline template in a server route module: [`fetchProfileLeaguesData()`](apps/website/app/profile/leagues/page.tsx:22) and [`ProfileLeaguesTemplate()`](apps/website/app/profile/leagues/page.tsx:82) | Responsibilities are mixed: server data fetching, decision logic, and rendering composition in one module. | Split into per-route shape: `page.tsx` calls a PageQuery and passes Page DTO; a `*PageClient.tsx` builds ViewData; Template renders ViewData only. |
| PageQueries MUST return the documented discriminated union ([`WEBSITE_PAGE_QUERIES.md`](docs/architecture/website/WEBSITE_PAGE_QUERIES.md:17)) | Local type differs from spec: [`PageQueryResult<TData>`](apps/website/lib/page-queries/DashboardPageQuery.ts:11) uses `data` and `destination` | Breaks the documented contract (`ok` with `dto`, `redirect` with `to`). | Standardize a single `PageQueryResult` type per [`WEBSITE_PAGE_QUERIES.md`](docs/architecture/website/WEBSITE_PAGE_QUERIES.md:17) and enforce across queries. |

---

## C) ViewModel boundary violations (ViewModels used/created on server and/or passed into Templates)

| Rule | Evidence | Why this is a violation | Fix direction |
|---|---|---|---|
| ViewModels are client-only and never serialized ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:70), [`VIEW_MODELS.md`](docs/architecture/website/VIEW_MODELS.md:71)) | Server route data types include ViewModels: [`TeamDetailData.team: TeamDetailsViewModel`](apps/website/app/teams/[id]/page.tsx:15) and are passed into Template wrapper: [`TeamDetailTemplateWrapper()`](apps/website/app/teams/[id]/page.tsx:20) | Implies ViewModels cross the server→client boundary, contradicting the “client-only class” rule and risking serialization/hydration issues. | Use Page DTOs for server-to-client, and instantiate ViewModels only in `'use client'` modules. |
| Templates accept ViewData only ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:86), [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:3)) | Template prop uses ViewModel instances: [`DriverRankingsTemplateProps.drivers: DriverLeaderboardItemViewModel[]`](apps/website/templates/DriverRankingsTemplate.tsx:19) | Template is no longer receiving ViewData (JSON-serializable primitive shapes). | Introduce Presenters that map Page DTO → ViewData and pass ViewData into Templates. |
| PageQueries must not instantiate client-only types / ViewModels ([`WEBSITE_PAGE_QUERIES.md`](docs/architecture/website/WEBSITE_PAGE_QUERIES.md:31)) | PageQuery imports from `lib/view-models/**`: [`DashboardOverviewViewModelData`](apps/website/lib/page-queries/DashboardPageQuery.ts:6) | `lib/view-models/**` is the client-only boundary (also where Presenters live per [`PRESENTERS.md`](docs/architecture/website/PRESENTERS.md:50)). | Move Page DTO types under `lib/page-queries/**` and keep view-model types under `lib/view-models/**`. |

---

## D) Template purity violations (Templates importing forbidden layers and doing computation)

| Rule | Evidence | Why this is a violation | Fix direction |
|---|---|---|---|
| Templates MUST NOT import ViewModels ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:16)) | Example: [`DriverLeaderboardItemViewModel`](apps/website/templates/DriverRankingsTemplate.tsx:7) | Template depends on client-only classes, drifting into Presenter/ViewModel responsibilities. | Replace ViewModel props with ViewData props. |
| Templates MUST NOT import Display Objects ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:16)) | Example: [`LeagueRoleDisplay`](apps/website/templates/LeagueDetailTemplate.tsx:13) | Display Objects are forbidden inside Templates. | Use Presenters/ViewModels to emit primitive strings into ViewData. |
| Templates MUST NOT compute derived values ([`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:33)) | Filtering/sorting occurs inside Template: [`drivers.filter()`](apps/website/templates/DriverRankingsTemplate.tsx:73) and [`sort(...)`](apps/website/templates/DriverRankingsTemplate.tsx:81) | Template performs orchestration and non-trivial computation rather than rendering prepared ViewData. | Move to ViewModels (client) and/or Presenters; Templates render only. |
| “Templates are pure” ([`WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:27)) | Hooks in templates: [`useMemo()`](apps/website/templates/RacesTemplate.tsx:3) used for filtering/grouping | Pure-template contract is violated by stateful/reactive computations. | Create a `*PageClient.tsx` container for state/computation; keep template as pure function over ViewData. |

---

## E) Determinism violations: forbidden locale/time formatting paths (`Intl.*`, `toLocale*`)

| Rule | Evidence | Why this is a violation | Fix direction |
|---|---|---|---|
| Templates must not call locale APIs ([`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:41), [`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:16)) | Example: [`toLocaleDateString()`](apps/website/templates/RacesTemplate.tsx:148) | Locale/timezone-dependent formatting can differ between SSR and browser, risking hydration mismatches and non-determinism. | Replace with deterministic formatting via Display Objects, or API-provided labels, passed through Presenters into ViewData. |
| Formatting code paths must not use locale APIs ([`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:63), [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:37)) | Example: [`toLocaleString()`](apps/website/lib/view-models/BillingViewModel.ts:86) | Even though ViewModels may format, this repo’s strict determinism rules forbid runtime-locale APIs. | Replace with deterministic numeric/date formatting helpers (no `Intl.*`, no `toLocale*`). |
| Utility helpers used in presentation must not use locale APIs ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:7)) | [`formatDate()`](apps/website/lib/utilities/time.ts:48) calls [`toLocaleDateString()`](apps/website/lib/utilities/time.ts:50) | Utilities become hidden sources of non-determinism across the app. | Replace these helpers with deterministic formatters and enforce via guardrail tests/ESLint. |

---

## F) Page.tsx guardrail violations (sorting/filtering in server routes and forbidden imports)

| Rule | Evidence | Why this is a violation | Fix direction |
|---|---|---|---|
| RSC `page.tsx` must not perform sorting/filtering beyond trivial checks ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:9)) | Server route computes derived values via reduce/filter/sort: [`computeDerivedData()`](apps/website/app/teams/page.tsx:12) | Server route contains presentation shaping. | Move shaping into PageQuery output (as Page DTO only) and/or into client ViewModel/Presenter. |
| RSC `page.tsx` must not import ViewModels ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:9)) | Example: [`TeamSummaryViewModel`](apps/website/app/teams/page.tsx:10) | ViewModels are client-only. | Replace with Page DTO types; construct ViewModels client-side. |

---

## G) Write-flow violations (writes not entering through Server Actions)

| Rule | Evidence | Why this is a violation | Fix direction |
|---|---|---|---|
| All writes MUST enter through Server Actions ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:171), [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:18)) | Client component performs POST: [`fetch('/api/auth/logout', { method: 'POST' })`](apps/website/components/profile/UserPill.tsx:212) | Client-initiated writes are explicitly forbidden. | Replace with a Server Action invoked via `<form action={...}>` or button action, then revalidate/navigate. |

---

## H) UX-only blockers embedded in services (state leakage risk)

| Rule | Evidence | Why this is a violation | Fix direction |
|---|---|---|---|
| Blockers are UX-only and should be local + reversible ([`BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:7), [`CLIENT_STATE.md`](docs/architecture/website/CLIENT_STATE.md:7)) | Stateful blockers inside a service: [`submitBlocker`](apps/website/lib/services/leagues/LeagueService.ts:96) and [`throttle`](apps/website/lib/services/leagues/LeagueService.ts:97) | If the service is DI-singleton or reused across requests, this state can leak across users/requests; also mixes UI concerns into a service boundary. | Move blockers into client UI boundary (hook/component) or ensure strict client-only, per-instance usage. Services should remain stateless. |

---

## 6) Display Objects, Command Models, Blockers (fourth scan)

This section answers:

1) Are Display Objects used as intended?
2) Where should we introduce Display Objects instead of ad-hoc formatting/mapping?
3) Are Command Models used as intended?
4) Where should we introduce Command Models instead of ad-hoc form state/validation?
5) Are Blockers used as intended?
6) Where should we introduce Blockers instead of ad-hoc UX prevention?

### 6.1 Display Objects: current state vs contract

Contract excerpts:

- Display Objects are deterministic UI-only formatting/mapping helpers ([`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:1)).
- Placement rule: `apps/website/lib/display-objects/**` ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:141)).
- Must be class-based, immutable, deterministic ([`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:46)).
- Must NOT call `Intl.*` or `toLocale*` ([`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:63)).
- Templates must not import Display Objects (guardrail: [`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:16)).

#### 6.1.1 Violations: Display Objects imported from Templates

| Violation | Evidence | Why it matters | Required remediation |
|---|---|---|---|
| Template imports a Display Object | [`LeagueRoleDisplay`](apps/website/templates/LeagueDetailTemplate.tsx:13) | Templates are required to be ViewData-only and must not import display objects. | Move role badge derivation into Presenter/ViewModel and pass `{ text, badgeClasses }` as ViewData. |

#### 6.1.2 Structural drift: “Display Objects” implemented as plain functions/records

The following files live under `lib/display-objects/**`, but they are not class-based value objects (they are exported maps/functions):

- [`DashboardDisplay.ts`](apps/website/lib/display-objects/DashboardDisplay.ts:1)
- [`ProfileDisplay.ts`](apps/website/lib/display-objects/ProfileDisplay.ts:1)
- [`LeagueRoleDisplay.ts`](apps/website/lib/display-objects/LeagueRoleDisplay.ts:1) (partly mitigated via wrapper class, but still map-backed)

This conflicts with the strict “class-based” rule in [`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:46). If we keep them as functions, we should either:

Clean-code direction (no exceptions):

- Refactor these modules into small, immutable classes with explicit APIs.
- Keep them deterministic and free of locale APIs.

#### 6.1.3 Where Display Objects should be introduced (replace scattered formatting/mapping)

The repo currently performs formatting/mapping directly in Templates, route modules, components, and ViewModels. Those are prime candidates for Display Objects.

High-signal candidates found:

| Pattern | Evidence | Display Object to introduce | What the Display Object should output |
|---|---|---|---|
| Date formatting (`toLocaleDateString`, `toLocaleString`) | Many templates, e.g. [`RaceDetailTemplate.tsx`](apps/website/templates/RaceDetailTemplate.tsx:172), and route code e.g. [`profile/page.tsx`](apps/website/app/profile/page.tsx:430) | `DeterministicDateDisplay` (class) | `{ shortDate, longDate, shortTime, dateTime }` as strings based on ISO inputs, deterministic timezone policy.
| Number formatting (`toLocaleString`) | Templates like [`DriverRankingsTemplate.tsx`](apps/website/templates/DriverRankingsTemplate.tsx:206) and components like [`MetricCard.tsx`](apps/website/components/sponsors/MetricCard.tsx:49) | `DeterministicNumberDisplay` (class) | `{ compact, integer, fixed2 }` or explicit formatting helpers; avoid runtime locale.
| Currency formatting | ViewModels like [`BillingViewModel.ts`](apps/website/lib/view-models/BillingViewModel.ts:85) | `MoneyDisplay` (class) | `formatted` string(s) given `{ currencyCode, minorUnits }` or `{ currency, amount }`.
| Role badge mapping duplicated outside display objects | Switch in [`MembershipStatus.tsx`](apps/website/components/leagues/MembershipStatus.tsx:33) and also display object usage elsewhere ([`StandingsTable.tsx`](apps/website/components/leagues/StandingsTable.tsx:9)) | Consolidate as `LeagueRoleBadgeDisplay` | `{ text, badgeClasses }` (or `{ text, bg, border, textColor }`), deterministic.
| Sponsorship status + time remaining UI mapping | Derived UI flags in [`sponsor/campaigns/page.tsx`](apps/website/app/sponsor/campaigns/page.tsx:138) | `SponsorshipStatusDisplay` | `{ statusText, statusClasses, isExpiringSoon, daysRemainingLabel }` (pure).

**Important boundary rule:** Display Objects must be used by Presenters/ViewModels, not by Templates. Templates should only see the primitive outputs.

### 6.2 Command Models: current state vs intended use

Contract excerpts:

- Command Models are for transient form state; UX validation only; never derived from ViewModels; never reused from read models ([`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:61)).
- Canonical placement: `apps/website/lib/command-models/**` ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:219)).

#### 6.2.1 Good usage already present

- `League wizard` uses a command model: [`LeagueWizardCommandModel`](apps/website/lib/command-models/leagues/LeagueWizardCommandModel.ts:79) (consumed in [`CreateLeagueWizard.tsx`](apps/website/components/leagues/CreateLeagueWizard.tsx:27)).
- `Auth` command models exist: [`LoginCommandModel`](apps/website/lib/command-models/auth/LoginCommandModel.ts:17) and [`SignupCommandModel`](apps/website/lib/command-models/auth/SignupCommandModel.ts:21).
- `Protest` write intent uses a command model: [`ProtestDecisionCommandModel`](apps/website/lib/command-models/protests/ProtestDecisionCommandModel.ts:10) (constructed in [`protests/[protestId]/page.tsx`](apps/website/app/leagues/[id]/stewarding/protests/[protestId]/page.tsx:171)).

#### 6.2.2 Violations / drift: Command-model-like logic living in components

Example: onboarding wizard duplicates field validation inline:

- Validation logic in [`OnboardingWizard.tsx`](apps/website/components/onboarding/OnboardingWizard.tsx:202)

Plan action:

1. Create `apps/website/lib/command-models/onboarding/OnboardingCommandModel.ts`.
2. Move field-level validation + errors to the command model.
3. Keep the component responsible only for UI state (step navigation, show/hide) and invoking the server action.

Similarly, any other form pages that implement repeated validation strings/constraints should be moved into command models.

#### 6.2.3 Server Actions integration: command models should be consumed at the boundary

Contract requires writes to enter via Server Actions.

Plan action (repeatable pattern):

1. UI collects primitives → CommandModel instance (client-only) validates.
2. Submit creates **Command DTO** (plain object) and calls a Server Action.
3. Server Action performs UX validation (not business rules), calls API, redirects/revalidates.

### 6.3 Blockers: current state vs intended use

Contract excerpts:

- Blockers are UX-only, reversible helpers ([`BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:7)).
- Client state is UI-only; blockers are not security ([`CLIENT_STATE.md`](docs/architecture/website/CLIENT_STATE.md:45)).
- Canonical placement: `apps/website/lib/blockers/**` ([`BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:48)).

#### 6.3.1 Violations: Blockers embedded in services (state leakage risk)

- [`LeagueService`](apps/website/lib/services/leagues/LeagueService.ts:95) stores blockers as instance fields: [`submitBlocker`](apps/website/lib/services/leagues/LeagueService.ts:96) and [`throttle`](apps/website/lib/services/leagues/LeagueService.ts:97).
- [`LeagueWalletService`](apps/website/lib/services/leagues/LeagueWalletService.ts:12) does the same: [`submitBlocker`](apps/website/lib/services/leagues/LeagueWalletService.ts:13) and [`throttle`](apps/website/lib/services/leagues/LeagueWalletService.ts:14).

Why this matters:

- If any service instance is shared (DI singleton, caching, module singletons), blockers become cross-user/cross-request state.

Plan action:

1. Remove blockers from service instance state.
2. Reintroduce blockers at the client boundary:
   - component local state
   - a `useSubmitBlocker()` hook under `apps/website/lib/hooks/**`
   - or a per-interaction blocker instance created inside a client function.

#### 6.3.2 Where blockers should be used instead of ad-hoc UX prevention

Candidates:

| UX need | Evidence | Recommended blocker |
|---|---|---|
| Prevent multiple logout clicks | Logout actions exist and currently do client POSTs: [`UserPill.tsx`](apps/website/components/profile/UserPill.tsx:212) and [`AdminLayout.tsx`](apps/website/components/admin/AdminLayout.tsx:67) | `SubmitBlocker` at the UI boundary (button/form) |
| Throttle rapid filter/search updates | Multiple pages render lists with client filtering/sorting, e.g. [`DriverRankingsTemplate.tsx`](apps/website/templates/DriverRankingsTemplate.tsx:72) | `ThrottleBlocker` inside a client container or hook (not in service) |

---

## 7) Hooks folder split (`apps/website/hooks` vs `apps/website/lib/hooks`)

Contract:

- React-only utilities MUST live under `apps/website/lib/hooks/**` ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:217), [`WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:32)).

Observation:

- Repo contains **two** hooks locations:
  - Top-level: [`apps/website/hooks/useCapability.ts`](apps/website/hooks/useCapability.ts:1) (and many more)
  - Canonical: [`apps/website/lib/hooks/useEnhancedForm.ts`](apps/website/lib/hooks/useEnhancedForm.ts:1) (and others)

Violation:

| Violation | Evidence | Why it matters | Required remediation |
|---|---|---|---|
| Hooks implemented outside canonical `lib/hooks` | Many hooks under [`apps/website/hooks/**`](apps/website/hooks/useEffectiveDriverId.ts:1) despite contract naming `apps/website/lib/hooks/**` | Confuses imports/boundaries; increases chance hooks depend on route-level concerns or mix server/client incorrectly. Also violates the strict file-structure contract. | Move `apps/website/hooks/**` into `apps/website/lib/hooks/**`, update all imports, remove the top-level folder, and add a guardrail to prevent regression. |

Notes:

- The current `tsconfig` explicitly includes top-level `hooks/` ([`tsconfig.json`](apps/website/tsconfig.json:70)), which suggests this drift is intentional-but-undocumented.

---

## 8) `AdminViewModelService` violates ViewModel instantiation + Presenter placement rules

Contract:

- ViewModels are client-only classes; they are instantiated only in `'use client'` modules ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:76)).
- Presenters (pure mappings) are colocated with ViewModels in `apps/website/lib/view-models/**` ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:124)).
- Services are for orchestration, not mapping API DTOs into ViewModels (implied by data-flow contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:145)).

Evidence:

- [`AdminViewModelService`](apps/website/lib/services/AdminViewModelService.ts:5) is a `lib/services/**` module that performs mapping of API DTOs to ViewModels:
  - imports API DTO types from [`AdminApiClient`](apps/website/lib/services/AdminViewModelService.ts:1)
  - instantiates `AdminUserViewModel` et al. ([`AdminViewModelService.mapUser()`](apps/website/lib/services/AdminViewModelService.ts:14))

Violations:

| Violation | Evidence | Why it matters | Required remediation |
|---|---|---|---|
| ViewModels instantiated in a non-client module | [`new AdminUserViewModel(dto)`](apps/website/lib/services/AdminViewModelService.ts:15) in a service file with no `'use client'` boundary | If imported by server modules (Page Queries, RSC pages), it breaks the rule that ViewModels are client-only, and risks serializing or executing client-only assumptions on the server. | Move mapping into a Presenter colocated with the ViewModel (e.g. [`AdminUserViewModelPresenter`](apps/website/lib/view-models/AdminUserViewModel.ts:1) or a sibling file) and ensure ViewModel creation happens only in client modules. |
| Presenter logic misclassified as a Service | All methods are pure mapping functions ([`AdminViewModelService.mapUsers()`](apps/website/lib/services/AdminViewModelService.ts:21)) | Encourages importing “services” in server paths, causing architecture erosion (services become dumping grounds). | Rename/rehome as Presenter. Services should call API clients + return Page DTOs, not construct ViewModels. |

---

## 9) Alignment plan (approved direction: align code to contract)
## 9) Alignment plan (Clean Architecture / SOLID, no gaps)

Non-negotiable target:

- `app/**/page.tsx`: server composition only (no business logic, no formatting, no filtering).
- `lib/page-queries/**`: server composition + IO only; returns Page DTO; **no ViewModels**; **no DI singleton**.
- Server DI policy: **no DI at all** in PageQueries or `page.tsx` (manual wiring only).
- `lib/services/**`: server-safe orchestration only; returns API DTO or Page DTO; **no ViewModels**; **no blockers**.
- `lib/view-models/**` + Presenters: client-only; pure mapping to ViewData.
- `templates/**`: dumb renderer; ViewData only.

Naming enforcement (applies across all refactors):

- API response shapes are `ApiDto`/`DTO` and stored/handled as `apiDto`.
- PageQuery output is always named `pageDto` and typed `*PageDto`.
- Template props are always `*ViewData` and passed as `viewData`.

Abstractions enforcement (applies across all refactors):

- Every PageQuery implements a common `PageQuery<PageDto>` contract (in `lib/contracts/page-queries/**`).
- Every service implements a `Service` interface that returns `ApiDto`/`PageDto` only (in `lib/contracts/services/**`).
- Every Presenter implements `Presenter<Input, Output>` with `Output` being `ViewData` or `ViewModel` (in `lib/contracts/presenters/**`).
- ViewModels extend a shared base (in `lib/contracts/view-models/**`) to discourage accidental server serialization patterns.

### 9.1 Guardrails first (prevent regression)

Add CI-failing checks (tests or ESLint rules). These come first to enforce Clean Architecture boundaries while refactors are in flight:

1. `app/**/page.tsx` MUST NOT import from `apps/website/lib/view-models/**`.
2. `templates/**` MUST NOT import from `apps/website/lib/view-models/**` nor `apps/website/lib/display-objects/**`.
3. `lib/page-queries/**` MUST NOT import from `apps/website/lib/view-models/**`.
4. `lib/services/**` MUST NOT import from `apps/website/lib/view-models/**`.
5. Forbid server imports of [`ContainerManager`](apps/website/lib/di/container.ts:61).
6. Forbid `Intl.*` and `toLocale*` in all presentation paths.
7. Forbid `*Template.tsx` under `apps/website/app/**`.

Order of implementation (guardrails):

1. Server DI ban: forbid server imports of [`ContainerManager`](apps/website/lib/di/container.ts:61) and forbid use of [`PageDataFetcher.fetch()`](apps/website/lib/page/PageDataFetcher.ts:15) from server modules.
2. Server/client boundary: forbid any imports of `apps/website/lib/view-models/**` from `apps/website/app/**/page.tsx` and `apps/website/lib/page-queries/**`.
3. Template boundary: forbid imports of view-models and display-objects from `apps/website/templates/**`.
4. Service boundary: forbid imports of view-models from `apps/website/lib/services/**`.
5. Determinism: forbid `Intl.*` and `toLocale*` in all presentation paths.
6. Write boundary: forbid client-side `fetch` with write methods in client modules.

These align cleanly with SRP (single reason to change per layer) and remove ambiguity.

### 9.2 Structural cleanups

1. Move all hooks to `apps/website/lib/hooks/**` (see §7 and §1.7).
2. Convert Display Objects to class-based, immutable modules (see §6.1.2).
3. Delete [`AdminViewModelService`](apps/website/lib/services/AdminViewModelService.ts:10) and replace with a Presenter.

### 9.3 Service boundary refactor (DTO-only)

1. For each service currently returning ViewModels (examples: [`LeagueService`](apps/website/lib/services/leagues/LeagueService.ts:95), [`DashboardService`](apps/website/lib/services/dashboard/DashboardService.ts:12), [`MediaService`](apps/website/lib/services/media/MediaService.ts:15)):
   - Change outputs to API DTOs or Page DTOs.
   - Move any ViewModel instantiation into client Presenters.
   - Remove blockers/state from services.

2. Fix the confirmed server/ViewModel leak:
   - [`ProfilePageQuery`](apps/website/lib/page-queries/ProfilePageQuery.ts:34) must not call `viewModel.toDTO()`.
   - It must call a service returning Page DTO and return that directly.

### 9.4 Route refactor recipe (repeatable)

For every route currently doing logic in `page.tsx`:

1. `page.tsx`: fetches via PageQuery, switches on result, renders a `*PageClient`.
2. PageQuery: calls services + assembles Page DTO only.
3. `*PageClient.tsx`: instantiates ViewModels (client-only) and produces ViewData via Presenters.
4. Template: renders ViewData only.

Write path (per-route):

1. `app/<route>/actions.ts`: Server Actions for that route only (mutations, UX validation, redirect/revalidate).
2. `*PageClient.tsx` (or a client component) uses `<form action={...}>` to invoke the action.
3. Services called by actions remain DTO-only and do not contain UI logic.

This is the mechanism that keeps pages free from business logic and leaves no seams for drift.

### 9.5 ViewModels: allowed, but only behind the client boundary

You chose: keep ViewModels, enforce client-only.

Non-negotiable rules (to avoid gaps):

1. No server module imports from `apps/website/lib/view-models/**`.
2. No `apps/website/lib/services/**` imports from `apps/website/lib/view-models/**`.
3. A ViewModel instance is never passed into a Template.
4. A ViewModel instance is never serialized.

Allowed flows:

- Page DTO → Presenter → ViewData → Template
- Page DTO → Presenter → ViewModel → Presenter → ViewData → Template

This keeps presentation state cohesive (ViewModels) while preserving a clean dependency direction (server depends on DTOs, client depends on ViewModels).

---

## High-signal file sets (pattern-based indicators)

### Templates importing ViewModels and or Display Objects (forbidden)

- [`DriverRankingsTemplate.tsx`](apps/website/templates/DriverRankingsTemplate.tsx:7)
- [`LeagueDetailTemplate.tsx`](apps/website/templates/LeagueDetailTemplate.tsx:13)
- [`LeagueStandingsTemplate.tsx`](apps/website/templates/LeagueStandingsTemplate.tsx:6)
- [`TeamDetailTemplate.tsx`](apps/website/templates/TeamDetailTemplate.tsx:14)

### Server routes importing ViewModels and or doing formatting/filtering (forbidden/discouraged)

- [`teams/page.tsx`](apps/website/app/teams/page.tsx:12)
- [`teams/[id]/page.tsx`](apps/website/app/teams/[id]/page.tsx:9)
- [`profile/leagues/page.tsx`](apps/website/app/profile/leagues/page.tsx:7)

### Structural drift vs canonical `lib/*` layout

- API client exists at root `lib/` instead of `lib/api/`: [`apiClient.ts`](apps/website/lib/apiClient.ts:1)
- Non-canonical `lib/page/` exists: [`PageDataFetcher.ts`](apps/website/lib/page/PageDataFetcher.ts:1)
