diff --git a/CLAUDE.md b/CLAUDE.md index 6d911f3..66896d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,32 +21,32 @@ These constraints were set by the project lead at kickoff. They apply to every c ## Architectural Decision Records (ADRs) -- Format: **MADR 4.0.0** (https://adr.github.io/, https://github.com/adr/madr). Template at [decisions/template.md](decisions/template.md). -- Location: flat folder [decisions/](decisions/), indexed by [decisions/README.md](decisions/README.md). +- Format: **MADR 4.0.0** (https://adr.github.io/, https://github.com/adr/madr). Template at [docs/decisions/template.md](docs/decisions/template.md). +- Location: flat folder [docs/decisions/](docs/decisions/), indexed by [docs/decisions/README.md](docs/decisions/README.md). - Filename convention: `NNNN-kebab-title.md` with **globally sequential** 4-digit numbers. Numbers are never reset and never reused — even when an ADR is superseded or deprecated. -- Categorization: via the `tags:` array in the MADR frontmatter (e.g. `[frontend, security]`). The canonical tag vocabulary lives in `decisions/README.md`; never invent ad-hoc tags inline. +- Categorization: via the `tags:` array in the MADR frontmatter (e.g. `[frontend, security]`). The canonical tag vocabulary lives in `docs/decisions/README.md`; never invent ad-hoc tags inline. - **Proactivity.** Any non-trivial development decision (tool/library choice, framework pattern, security control, perf budget, a11y target, naming convention, deprecation, breaking change) warrants proposing an ADR before implementation. Don't wait to be asked. Update the index in the same change. ## Architecture (recorded in ADRs) The structural, security, observability, and quality choices are recorded as ADRs and summarized below. Any change to these requires updating the corresponding ADR. -- **Workspace:** Nx monorepo with the `apps` preset, managed by pnpm — see [ADR-0002](decisions/0002-adopt-nx-monorepo-apps-preset.md). -- **Naming:** workspace `apf-portal`; apps `portal-shell` (frontend) and `portal-bff` (backend); libs `feature-` and `shared-` — see [ADR-0003](decisions/0003-workspace-and-app-naming-convention.md). -- **Frontend (`portal-shell`):** Angular at the latest LTS major — standalone APIs, zoneless change detection, Signals, **CSR only (no SSR)**, Vitest, SCSS — see [ADR-0004](decisions/0004-frontend-stack-angular-csr-zoneless-signals.md). -- **Backend (`portal-bff`):** NestJS at the latest stable major, mounted on the Express adapter (Fastify adapter swappable later) — see [ADR-0005](decisions/0005-backend-stack-nestjs.md). -- **Persistence:** PostgreSQL (latest stable major) via Prisma — see [ADR-0006](decisions/0006-persistence-postgresql-prisma.md). -- **Sessions:** opaque session id in `__Host-portal_session`, payload in self-hosted Redis (Sentinel HA in prod, single node in dev), tokens encrypted at rest with AES-256-GCM, idle 30 min sliding + absolute 12 h — see [ADR-0010](decisions/0010-session-management-redis.md). -- **MFA:** enforced by Entra ID Conditional Access (org-side policy, P1 licensing required); BFF sanity-checks the `amr` claim at session creation; `@RequireMfa()` decorator and freshness-based step-up are designed-in for future sensitive routes (no v1 consumer) — see [ADR-0011](decisions/0011-mfa-enforcement-entra-conditional-access.md). -- **Identity:** multi-tenant Microsoft Entra ID with B2B invitation for workforce in v1, dual-audience design ready for future External ID activation — see [ADR-0008](decisions/0008-identity-model-entra-workforce-dual-audience.md). -- **Authentication flow:** OIDC Authorization Code + PKCE via `@azure/msal-node`, executed entirely on the BFF; SPA never holds tokens; `__Host-` prefixed cookies, double-submit CSRF, RP-initiated logout — see [ADR-0009](decisions/0009-auth-flow-oidc-pkce-msal-node.md). -- **Observability:** Pino + `nestjs-pino` for structured JSON logs, OpenTelemetry SDK + auto-instrumentations for traces, W3C Trace Context propagation across SPA → BFF → DB → Redis, `nestjs-cls` for request-scoped context (`trace_id`, `session_id`, `user_id_hash`, `audience`), 100 % sampling at the app with tail sampling deferred to the OTel Collector, stdout + OTLP shipping — see [ADR-0012](decisions/0012-observability-pino-opentelemetry.md). -- **Audit trail:** dedicated `audit.events` schema in the same Postgres instance, append-only by Postgres role grants (`audit_writer` INSERT, `audit_reader` SELECT, `audit_archiver` DELETE older than retention; no `UPDATE`/`TRUNCATE` to anyone); 365-day retention default; cross-referenced with app logs via `trace_id` and `actor_id_hash` (same salt); blocking writes (no audit ⇒ no action) — see [ADR-0013](decisions/0013-audit-trail-separated-postgres-append-only.md). -- **Downstream API access:** unified `DownstreamApiClient` (`@nestjs/axios` + `cockatiel`), per-service `DownstreamApiConfig`; default auth strategy is **OBO via MSAL Node** for Entra-protected APIs (downstream-scoped tokens cached in Redis with AES-256-GCM under a dedicated key); fallback strategy is service credential + signed `X-User-Assertion` JWT (BFF JWKS at `/.well-known/jwks.json`); per-call audience pre-check; no `axios`/`fetch` outside `src/downstream/` — see [ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md). -- **CI/CD:** **Gitea Actions** (level-2 implementation; will be superseded by a GitLab migration ADR within 6-18 months). Trunk-based with squash-merge, branch protection on `main`, all CI gates blocking. Thin YAML — orchestration logic lives in `package.json` scripts (`ci:check`, `ci:scan`, `ci:commits`) and Nx targets, runnable locally. Gates: format / lint / type-check / test / build / audit / secret-scan / commit-lint, plus `a11y` (per ADR-0016) and future `perf`. Self-hosted `act_runner` on-prem. Conventional Commits validated locally (hook) and in CI (defense in depth). Required reviewer count = 0 in v1, raised to ≥1 once a second contributor joins. Signed commits recommended, revisited at GitLab migration — see [ADR-0015](decisions/0015-cicd-gitea-actions.md). -- **Accessibility:** **WCAG 2.2 AA baseline + targeted AAA** on criteria with high impact for APF's user base (1.4.6 Contrast Enhanced, 2.2.3 No Timing, 2.3.3 Animation, 3.1.5 Reading Level, 1.4.8 Visual Presentation, 2.4.9 Link Purpose, 3.3.5 Help). RGAA 4.1 alignment for French audit. UI stack: **Angular CDK + spartan-ng + Tailwind** (no Angular Material; no React libs). User-preferences panel (contrast / text size / motion / spacing / cognitive simplification / reading focus) persisted in session. Tooling: `@angular-eslint/template/*` lint, `@axe-core/playwright` e2e (blocking on critical/serious), token-contrast CI check, touch-target check (44×44 min). Manual testing cadence with APF's internal user panel before each major release. Public accessibility statement page at `/accessibility` and `/accessibilite` — see [ADR-0016](decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md). -- **Performance budgets:** Core Web Vitals at Google "Good" thresholds (LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1, TBT ≤ 200 ms, TTFB ≤ 800 ms), Lighthouse Performance ≥ 90 on critical routes. **Lighthouse CI** (`@lhci/cli`) runs in CI with median-of-3 mitigation, blocking on threshold breach. Angular bundle `budgets` (`type: "error"`): initial ≤ 300 KB gzip, lazy chunks ≤ 100 KB gzip. BFF p95/p99 SLOs per endpoint family observed via OTel (advisory in CI, alerting in prod). Weekly scheduled Lighthouse run on prod env. **a11y wins over perf** when they conflict — see [ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md). -- **Local quality gates:** Husky + lint-staged + commitlint with Conventional Commits — see [ADR-0007](decisions/0007-pre-commit-hooks-and-conventional-commits.md). +- **Workspace:** Nx monorepo with the `apps` preset, managed by pnpm — see [ADR-0002](docs/decisions/0002-adopt-nx-monorepo-apps-preset.md). +- **Naming:** workspace `apf-portal`; apps `portal-shell` (frontend) and `portal-bff` (backend); libs `feature-` and `shared-` — see [ADR-0003](docs/decisions/0003-workspace-and-app-naming-convention.md). +- **Frontend (`portal-shell`):** Angular at the latest LTS major — standalone APIs, zoneless change detection, Signals, **CSR only (no SSR)**, Vitest, SCSS — see [ADR-0004](docs/decisions/0004-frontend-stack-angular-csr-zoneless-signals.md). +- **Backend (`portal-bff`):** NestJS at the latest stable major, mounted on the Express adapter (Fastify adapter swappable later) — see [ADR-0005](docs/decisions/0005-backend-stack-nestjs.md). +- **Persistence:** PostgreSQL (latest stable major) via Prisma — see [ADR-0006](docs/decisions/0006-persistence-postgresql-prisma.md). +- **Sessions:** opaque session id in `__Host-portal_session`, payload in self-hosted Redis (Sentinel HA in prod, single node in dev), tokens encrypted at rest with AES-256-GCM, idle 30 min sliding + absolute 12 h — see [ADR-0010](docs/decisions/0010-session-management-redis.md). +- **MFA:** enforced by Entra ID Conditional Access (org-side policy, P1 licensing required); BFF sanity-checks the `amr` claim at session creation; `@RequireMfa()` decorator and freshness-based step-up are designed-in for future sensitive routes (no v1 consumer) — see [ADR-0011](docs/decisions/0011-mfa-enforcement-entra-conditional-access.md). +- **Identity:** multi-tenant Microsoft Entra ID with B2B invitation for workforce in v1, dual-audience design ready for future External ID activation — see [ADR-0008](docs/decisions/0008-identity-model-entra-workforce-dual-audience.md). +- **Authentication flow:** OIDC Authorization Code + PKCE via `@azure/msal-node`, executed entirely on the BFF; SPA never holds tokens; `__Host-` prefixed cookies, double-submit CSRF, RP-initiated logout — see [ADR-0009](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md). +- **Observability:** Pino + `nestjs-pino` for structured JSON logs, OpenTelemetry SDK + auto-instrumentations for traces, W3C Trace Context propagation across SPA → BFF → DB → Redis, `nestjs-cls` for request-scoped context (`trace_id`, `session_id`, `user_id_hash`, `audience`), 100 % sampling at the app with tail sampling deferred to the OTel Collector, stdout + OTLP shipping — see [ADR-0012](docs/decisions/0012-observability-pino-opentelemetry.md). +- **Audit trail:** dedicated `audit.events` schema in the same Postgres instance, append-only by Postgres role grants (`audit_writer` INSERT, `audit_reader` SELECT, `audit_archiver` DELETE older than retention; no `UPDATE`/`TRUNCATE` to anyone); 365-day retention default; cross-referenced with app logs via `trace_id` and `actor_id_hash` (same salt); blocking writes (no audit ⇒ no action) — see [ADR-0013](docs/decisions/0013-audit-trail-separated-postgres-append-only.md). +- **Downstream API access:** unified `DownstreamApiClient` (`@nestjs/axios` + `cockatiel`), per-service `DownstreamApiConfig`; default auth strategy is **OBO via MSAL Node** for Entra-protected APIs (downstream-scoped tokens cached in Redis with AES-256-GCM under a dedicated key); fallback strategy is service credential + signed `X-User-Assertion` JWT (BFF JWKS at `/.well-known/jwks.json`); per-call audience pre-check; no `axios`/`fetch` outside `src/downstream/` — see [ADR-0014](docs/decisions/0014-downstream-api-access-obo-pattern.md). +- **CI/CD:** **Gitea Actions** (level-2 implementation; will be superseded by a GitLab migration ADR within 6-18 months). Trunk-based with squash-merge, branch protection on `main`, all CI gates blocking. Thin YAML — orchestration logic lives in `package.json` scripts (`ci:check`, `ci:scan`, `ci:commits`) and Nx targets, runnable locally. Gates: format / lint / type-check / test / build / audit / secret-scan / commit-lint, plus `a11y` (per ADR-0016) and future `perf`. Self-hosted `act_runner` on-prem. Conventional Commits validated locally (hook) and in CI (defense in depth). Required reviewer count = 0 in v1, raised to ≥1 once a second contributor joins. Signed commits recommended, revisited at GitLab migration — see [ADR-0015](docs/decisions/0015-cicd-gitea-actions.md). +- **Accessibility:** **WCAG 2.2 AA baseline + targeted AAA** on criteria with high impact for APF's user base (1.4.6 Contrast Enhanced, 2.2.3 No Timing, 2.3.3 Animation, 3.1.5 Reading Level, 1.4.8 Visual Presentation, 2.4.9 Link Purpose, 3.3.5 Help). RGAA 4.1 alignment for French audit. UI stack: **Angular CDK + spartan-ng + Tailwind** (no Angular Material; no React libs). User-preferences panel (contrast / text size / motion / spacing / cognitive simplification / reading focus) persisted in session. Tooling: `@angular-eslint/template/*` lint, `@axe-core/playwright` e2e (blocking on critical/serious), token-contrast CI check, touch-target check (44×44 min). Manual testing cadence with APF's internal user panel before each major release. Public accessibility statement page at `/accessibility` and `/accessibilite` — see [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md). +- **Performance budgets:** Core Web Vitals at Google "Good" thresholds (LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1, TBT ≤ 200 ms, TTFB ≤ 800 ms), Lighthouse Performance ≥ 90 on critical routes. **Lighthouse CI** (`@lhci/cli`) runs in CI with median-of-3 mitigation, blocking on threshold breach. Angular bundle `budgets` (`type: "error"`): initial ≤ 300 KB gzip, lazy chunks ≤ 100 KB gzip. BFF p95/p99 SLOs per endpoint family observed via OTel (advisory in CI, alerting in prod). Weekly scheduled Lighthouse run on prod env. **a11y wins over perf** when they conflict — see [ADR-0017](docs/decisions/0017-performance-budgets-lighthouse-ci.md). +- **Local quality gates:** Husky + lint-staged + commitlint with Conventional Commits — see [ADR-0007](docs/decisions/0007-pre-commit-hooks-and-conventional-commits.md). - **Runtime:** Node.js latest LTS major. ## Repository status diff --git a/decisions/0001-use-adrs-to-record-architectural-decisions.md b/decisions/0001-use-adrs-to-record-architectural-decisions.md deleted file mode 100644 index a1c6fe0..0000000 --- a/decisions/0001-use-adrs-to-record-architectural-decisions.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -status: accepted -date: 2026-04-29 -decision-makers: R&D Lead -tags: [process] ---- - -# Use ADRs to record architectural decisions - -## Context and Problem Statement - -Architecturally-significant decisions will be taken throughout the lifecycle of `apf-portal`. Without a written, durable trace of *why* a decision was made — context, drivers, alternatives, trade-offs — that knowledge fades within months. New contributors re-litigate settled debates, the rationale of constraints (security, performance, accessibility) gets lost, and reversing a choice later becomes risky because nobody remembers what it was protecting against. - -How do we record decisions in a way that is light enough to be sustained, durable enough to outlive contributor turnover, and integrated into the development workflow? - -## Decision Drivers - -* Preserve the *why* of each decision so future contributors can understand or revisit it without re-deriving it. -* Make architecture reviewable — both at the time of a decision and during retrospective audits. -* Keep the format light enough that writing an ADR isn't a bureaucratic deterrent. -* Live alongside the code in version control, so decisions evolve with what they govern. -* Match the project's stated values: high technical bar, no bricolage, security/performance/accessibility as first-class concerns whose rationale must be traceable. - -## Considered Options - -* No formal records — rely on commit messages and tribal knowledge. -* External wiki / Confluence — decisions live outside the codebase. -* ADRs in markdown using the [MADR 4.0.0](https://github.com/adr/madr) format, in-repo. -* ADRs in another format (Nygard, Y-Statement, Cockburn). - -## Decision Outcome - -Chosen option: **ADRs in markdown using MADR 4.0.0**, stored in `decisions/` at the repository root. - -Conventions are defined in [README.md](README.md): -- flat folder layout (no nested category folders); -- globally sequential 4-digit numeric prefix (`NNNN-kebab-title.md`); -- categorization via the `tags:` field in the MADR frontmatter, drawn from a canonical vocabulary; -- status lifecycle: `proposed` → `accepted` → optionally `deprecated` / `superseded by ADR-NNNN`; -- index maintained in [README.md](README.md) alongside any ADR change. - -### Consequences - -* Good, because the *why* of every architecturally-significant decision is captured at the moment it is made and travels with the code. -* Good, because ADRs are reviewed via the normal pull-request flow — architecture becomes an explicit team conversation. -* Good, because MADR provides enough structure (drivers, options, trade-offs) without being heavy. -* Good, because flat-folder + tags scales better than nested category folders as the count grows. -* Bad, because writing an ADR adds friction; this must be balanced against the cost of *not* writing one. -* Bad, because the index is maintained manually until tooling or a CI check is added. - -### Confirmation - -* Every change introducing or transitioning the status of an ADR updates the index table in [README.md](README.md) in the same commit. -* PR review explicitly checks for ADR proposal whenever a non-trivial decision is being introduced. -* (Future) A CI lint job validates filename pattern, frontmatter shape, and tag vocabulary. - -## More Information - -* MADR 4.0.0 specification: https://github.com/adr/madr -* General ADR resources: https://adr.github.io/ -* Template: [template.md](template.md) -* Tag vocabulary: see "Tag vocabulary" in [README.md](README.md). diff --git a/decisions/0003-workspace-and-app-naming-convention.md b/decisions/0003-workspace-and-app-naming-convention.md deleted file mode 100644 index 869cba2..0000000 --- a/decisions/0003-workspace-and-app-naming-convention.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -status: accepted -date: 2026-04-29 -decision-makers: R&D Lead -tags: [process] ---- - -# Workspace and app naming convention - -## Context and Problem Statement - -Default Nx scaffolding offers placeholder names like `my-workspace` and `web`, which are unacceptable for a long-lived enterprise project. Naming has to survive two foreseeable changes: the product's marketing name may evolve (the working title `apf_portal` is provisional), and the project may sit alongside other R&D projects in the same Gitea organization, where collisions on generic names like `web` or `api` would cause confusion. - -What naming convention do we adopt for the workspace, the apps, and future libraries so names are explicit, function-anchored, and stable against rebranding? - -## Decision Drivers - -* The product's marketing name is provisional — names anchored on it become stale at rebrand. -* Names should reflect *function*, not *brand*. -* Multiple R&D projects may share the org — generic names (`web`, `api`, `frontend`) cause friction. -* Compliance with npm package naming rules (lowercase, kebab-case). -* Readability inside Nx's project graph and CLI output. - -## Considered Options - -* **Defaults** — `my-workspace`, `web`. (Rejected up-front.) -* **Brand-anchored** — `apf-front`, `apf-back`. Fragile to further rebranding. -* **Function-prefixed** — `portal-shell`, `portal-bff`. (Chosen.) -* **Generic** — `shell`, `bff`. Risk of org-wide collision; reduced readability. - -## Decision Outcome - -Chosen option: **function-prefixed naming**. - -| Scope | Name | Rationale | -| --- | --- | --- | -| Workspace / repo / npm package root | `apf-portal` | matches the Gitea repository name; the only place a brand-ish name lives, so a rebrand is a one-line change | -| Frontend app | `portal-shell` | "shell" describes the function — the Angular host that loads features | -| Backend app | `portal-bff` | explicit role: backend-for-frontend | -| Feature libraries | `feature-` | e.g. `feature-auth`, `feature-billing` | -| Shared libraries | `shared-` | e.g. `shared-ui`, `shared-data-access`, `shared-util` | - -The `portal-` prefix encodes the *function* (a portal). The host organisation's brand can change without invalidating "portal" — the project was originally proposed as `adastra_portal` and renamed to `apf_portal` on 2026-04-30 once the host organisation (APF France Handicap) was confirmed; the apps `portal-shell` / `portal-bff` were unaffected by the rename, validating this convention. The brand-ish token `apf` is confined to the workspace root and a tiny number of metadata files (root `package.json`, repo URL). - -### Consequences - -* Good, because names survive a brand change with minimal churn. -* Good, because `portal-shell` / `portal-bff` are unambiguous in CLI output and in the Nx project graph. -* Good, because the prefix scales when more apps appear (e.g. `portal-admin`, `portal-jobs`). -* Bad, because the prefix adds verbosity (`pnpm nx serve portal-shell` vs `pnpm nx serve web`). Acceptable. -* Neutral, because if "portal" itself ceases to be accurate (e.g. the system pivots to something other than a portal), the prefix would also need updating — but that would be a deeper architectural pivot anyway. - -### Confirmation - -* Nx project names match the convention in `nx.json` and in each project's `project.json`. -* `@nx/eslint/enforce-module-boundaries` is configured with tags aligned to the convention (`scope:portal-shell`, `scope:portal-bff`, `type:feature`, `type:shared`). -* PR review rejects new project names that don't match. - -## More Information - -* Related ADRs: [ADR-0002](0002-adopt-nx-monorepo-apps-preset.md), and the "Project name" rule in [CLAUDE.md](../CLAUDE.md). diff --git a/decisions/0004-frontend-stack-angular-csr-zoneless-signals.md b/decisions/0004-frontend-stack-angular-csr-zoneless-signals.md deleted file mode 100644 index 977c968..0000000 --- a/decisions/0004-frontend-stack-angular-csr-zoneless-signals.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -status: accepted -date: 2026-04-29 -decision-makers: R&D Lead -tags: [frontend] ---- - -# Frontend stack — Angular (latest LTS), standalone, zoneless, Signals, CSR-only, Vitest - -## Context and Problem Statement - -The portal's frontend (`portal-shell`) is a single-page application that aggregates access to existing applications and progressively integrates re-developed features. It sits fully behind authentication: no public content. We need to fix the framework, the rendering mode, the change-detection model, and the test runner now, so subsequent ADRs (auth flow, observability, accessibility, performance budgets) can build on a stable foundation. - -What frontend stack maximizes alignment with our backend (NestJS), gives us first-class accessibility and performance hooks, and is enterprise-stable for a long-lived project? - -## Decision Drivers - -* Architectural alignment with NestJS (DI, decorators, modules, RxJS) — minimizes cognitive distance for the team. -* Performance and accessibility as first-class concerns from day one. -* Long-term enterprise stability (no pre-1.0, no exotic stack). -* End-to-end type safety. -* No public surface — all content gated by authentication, which removes the usual SSR drivers (SEO, anonymous LCP). - -## Considered Options - -* **Angular (latest LTS) — standalone, zoneless, Signals, CSR, Vitest.** (Chosen.) -* Angular with SSR (`@angular/ssr`). -* React + Next.js (or Vite + React Router). -* Vue + Nuxt. -* Svelte + SvelteKit. - -## Decision Outcome - -Chosen option: **Angular at the latest LTS major**, with the following modern defaults: - -* **Standalone APIs** — no `NgModule`s. Configuration is composed via providers; routing is functional. -* **Zoneless change detection** — no `Zone.js`. Change detection is driven by Signals and explicit `markForCheck` where needed. -* **Signals** — primary reactive primitive for component state. RxJS retained for async streams (HTTP, events). -* **Vitest** — unit and component test runner (Karma is deprecated upstream). -* **CSR only — no SSR** for v1. Reconsider only if a measured performance metric requires it. -* **SCSS** for styles. Design-token strategy to be defined in a future ADR. -* **Strict TypeScript** across the workspace (`strict: true`, `noUncheckedIndexedAccess: true`). - -### Consequences - -* Good, because the team works with a single mental model across front and back (DI, decorators, RxJS). -* Good, because zoneless + Signals removes Zone.js overhead and gives finer-grained, more predictable reactivity. -* Good, because Vitest is significantly faster than Karma and shares the Vite toolchain Nx already uses. -* Good, because CSR keeps the BFF free of SSR concerns (no auth-aware rendering, no double-fetch, no hydration debugging) — simpler operational surface. -* Bad, because Angular's release cadence is brisk; the project commits to staying on LTS rather than skipping versions. -* Bad, because zoneless and Signals are recent — though stable, the broader Angular ecosystem still includes Zone-dependent libraries that must be vetted before adoption. -* Bad, because CSR yields a minimal initial HTML payload; this must be paired with strong performance budgets (covered by ADR-0015, future). - -### Confirmation - -* `package.json` pins Angular to the current LTS major. -* `app.config.ts` registers `provideZonelessChangeDetection()` (or its current stable equivalent) and standalone routing. -* `tsconfig.base.json` sets `strict: true` and `noUncheckedIndexedAccess: true`. -* CI runs `pnpm nx test portal-shell` (Vitest). -* No `import 'zone.js'` anywhere in the codebase. -* `@angular/ssr` is not installed. - -## Pros and Cons of the Options - -### Angular CSR (chosen) - -* Good, because alignment with NestJS minimizes context-switching for the team. -* Good, because mature, enterprise-supported (Google + community). -* Good, because Signals + zoneless make Angular's reactivity competitive with React/Vue. -* Bad, because verbosity is higher than minimalist frameworks. - -### Angular SSR - -* Good, because better TTFB and accessibility on first paint for unauthenticated content. -* Bad, because we have no unauthenticated content — the gain is essentially zero. -* Bad, because SSR-with-auth introduces non-trivial complexity (cookie-aware fetch, hydration with secured data). - -### React + Next.js - -* Good, because large ecosystem, fast iteration. -* Bad, because no architectural alignment with NestJS — different DI/state paradigms. -* Bad, because Next.js's defaults push toward server components and edge rendering, which conflict with our on-prem, all-authenticated profile. - -### Vue + Nuxt / Svelte + SvelteKit - -* Good, because lighter syntax, strong DX. -* Bad, because no DI alignment with NestJS. -* Bad, because smaller enterprise-grade community than Angular or React. - -## More Information - -* Angular zoneless change detection: https://angular.dev/guide/experimental/zoneless -* Angular Signals: https://angular.dev/guide/signals -* Vitest with Nx: https://nx.dev/nx-api/vite -* Related ADRs: [ADR-0005](0005-backend-stack-nestjs.md), ADR-0008 (auth flow, future), ADR-0014 (accessibility, future), ADR-0015 (performance budgets, future). diff --git a/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md b/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md deleted file mode 100644 index f211ad4..0000000 --- a/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md +++ /dev/null @@ -1,244 +0,0 @@ ---- -status: accepted -date: 2026-04-30 -decision-makers: R&D Lead -tags: [accessibility, frontend, process] ---- - -# Accessibility baseline — WCAG 2.2 AA + targeted AAA, Angular CDK + spartan-ng + Tailwind, APF panel testing - -## Context and Problem Statement - -The host organisation is **APF France Handicap**. Accessibility is the organisation's core mission, not a compliance checkbox. A meaningful proportion of the portal's users will be in situations of disability — visual, motor, cognitive, hearing — and will rely on assistive technologies (screen readers, switch controls, eye tracking, voice control, head pointers, mouth sticks). The portal's accessibility quality is therefore a **product attribute**, not a downstream concern. - -The default WCAG 2.2 AA baseline applicable to enterprise portals would *under-serve* this user base. We need to fix: -- the conformance target (WCAG level + targeted enhancements); -- the implementation stack (component library, primitives, styling); -- the user-preferences surface (contrast, text size, motion, etc.) the portal must offer; -- the testing strategy, including manual testing with users from APF's internal network; -- the CI gates that prevent regression; -- the legal-disclosure surface (the EU Accessibility Act–required accessibility statement). - -This ADR fixes the framework. Concrete component-by-component implementation lands as the scaffold and features arrive. - -## Decision Drivers - -* APF's mission elevates accessibility from "compliance" to "product". -* European Accessibility Act (EAA, in application since June 2025) makes WCAG 2.2 AA a legal floor in the EU; the portal will publish an accessibility statement and accept regression as a defect, not as a backlog item. -* RGAA 4.1 (French national standard, derived from WCAG) is the operational reference for accessibility audits in France; alignment is necessary to be auditable by French experts (including APF's own). -* The user base extends beyond screen-reader users: switch controls, eye tracking, voice control, cognitive load tolerance — all must be considered. -* The technical stack is locked on Angular ([ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md)). Component-library choice must therefore be Angular-native or framework-agnostic. -* The dual-audience design ([ADR-0008](0008-identity-model-entra-workforce-dual-audience.md)) means a11y must be uniform across audiences — workforce and (future) customer users alike. -* Anti-bricolage: no hand-rolled a11y where a maintained library exists. - -## Considered Options - -### Conformance target -* WCAG 2.2 **AA** baseline (EU EAA legal floor). -* WCAG 2.2 **AA + targeted AAA** on criteria with high impact for APF's user base. (Chosen.) -* WCAG 2.2 **AAA** uniform. - -### UI component stack -* Angular Material (Angular CDK + Material visual layer). -* **Angular CDK + spartan-ng + Tailwind.** (Chosen — see also `notes/argumentaire-stack-ui-spartan-cdk-tailwind.md` for the team-internal rationale.) -* Custom-on-Angular-CDK only (no spartan-ng). -* React-ecosystem libs (shadcn/ui, dice-ui, animate-ui). - -### User-preferences scope -* None (rely on browser/OS prefs). -* `prefers-reduced-motion` only. -* **Full preferences panel** — contrast modes, text size, motion, spacing, simplified UI. (Chosen.) - -### Testing strategy -* Automated only (axe-core + lint). -* Automated + external annual audit. -* **Automated + manual screen-reader/keyboard/switch testing + APF user panel cadence + annual internal audit.** (Chosen.) - -### Documentation surface -* Internal docs only. -* **Public accessibility statement page (legal EAA requirement) + internal patterns library.** (Chosen.) - -## Decision Outcome - -### Conformance target - -**WCAG 2.2 AA** as the universal baseline. **AAA** applied on the criteria that materially affect APF's user base: - -| AAA criterion | What it adds vs AA | Why APF | -| --- | --- | --- | -| **1.4.6 Contrast (Enhanced)** | 7:1 normal text / 4.5:1 large text (vs 4.5:1 / 3:1 at AA) | Low-vision users are a primary audience | -| **2.2.3 No Timing** | No time limits except where essential | Cognitive load and motor speed vary widely | -| **2.3.3 Animation from Interactions** | All interaction-driven animation can be disabled | Vestibular and cognitive disorders | -| **3.1.5 Reading Level** | Content readable at lower secondary level *or* a simpler alternative is provided | Cognitive accessibility | -| **1.4.8 Visual Presentation** | User control over text formatting (line spacing, paragraph spacing, line length) | Dyslexia, low vision | -| **2.4.9 Link Purpose (Link Only)** | Each link's purpose understandable from its text alone (no surrounding context) | Screen-reader navigation by link list | -| **3.3.5 Help** | Context-sensitive help available where needed | Cognitive accessibility | - -The remaining AAA criteria (e.g. 1.2.6 Sign Language, 1.2.8 Media Alternative) are **not** uniformly required — they apply where the corresponding content type appears, and are addressed case by case. **RGAA 4.1** alignment is maintained as the audit reference for French experts (APF, CNIL audits, public-sector partners). - -### User-preferences panel - -The portal exposes a **first-class user-preferences panel** accessible from any page (keyboard-discoverable, screen-reader-announced, persisted in the user's session per [ADR-0010](0010-session-management-redis.md)): - -| Preference | Options | Default | -| --- | --- | --- | -| Contrast mode | Standard (AA-compliant) / Enhanced (AAA, 7:1) / Maximum (high-contrast OS-style, monochrome) | Standard | -| Text size | 100 % / 125 % / 150 % / 175 % / 200 % | 100 % | -| Motion | Full / Reduced / None | inherited from `prefers-reduced-motion` | -| Text spacing | Default / Generous (line-spacing 1.8, paragraph-spacing 2×, letter-spacing 0.12em) | Default | -| Cognitive simplification | Off / On (reduces secondary chrome, larger primary actions, easier-to-read summaries when authored) | Off | -| Reading focus | Off / Line-by-line | Off | - -Preferences are saved per user; an unauthenticated default is read from browser preferences (`prefers-reduced-motion`, `prefers-contrast`, etc.). Changing a preference takes effect immediately, no page reload. - -### Component stack - -**Angular CDK + spartan-ng + TailwindCSS**. See [`notes/argumentaire-stack-ui-spartan-cdk-tailwind.md`](../notes/argumentaire-stack-ui-spartan-cdk-tailwind.md) for the full rationale produced for the dev team. Summary: - -- **Angular CDK** (`@angular/cdk`) — primitives accessibility layer maintained by Google's Angular Material team. Equivalent role to Radix UI in React. Provides `Overlay`, `FocusTrap`, `FocusMonitor`, `LiveAnnouncer`, `A11yModule`, `ListKeyManager`, `Drag and Drop`, etc. Battle-tested since 2017. -- **spartan-ng** (https://www.spartan.ng/) — copy-paste component layer built on Angular CDK, styled with Tailwind. Components are checked into our repo (`libs/shared/ui/`) and modified freely. Equivalent role to shadcn/ui in React. -- **Tailwind CSS** — utility-first styling. Tokens for colour, spacing, contrast tiers live in `libs/shared/tokens/`. The contrast tiers map directly to the user preference modes above. - -Angular Material is **rejected** because its visual design system (Material Design) is opinionated in ways that clash with both APF branding and the multi-tier contrast requirements. React-ecosystem libraries (shadcn/ui, dice-ui, animate-ui) are rejected as incompatible with the Angular stack ([ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md)). - -### Concrete patterns to enforce - -| Concern | Rule | -| --- | --- | -| Sémantique HTML | `
`, `
`, `