docs
This commit is contained in:
@@ -4,6 +4,542 @@ Scope: `apps/website/**` aligned against `docs/architecture/website/**`, with th
|
||||
|
||||
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)).
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
### 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 `DriverService.getDriverProfile()` (and any similar methods) to return Page DTO only when used from server paths.
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
@@ -81,6 +617,273 @@ This report lists violations as: rule ⇒ evidence ⇒ impact ⇒ fix direction.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
### 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)
|
||||
@@ -100,4 +903,3 @@ This report lists violations as: rule ⇒ evidence ⇒ impact ⇒ fix direction.
|
||||
|
||||
- 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user