Commit Graph

24 Commits

Author SHA1 Message Date
Julien Gautier aa87db4362 docs(adr-0023): charts and dashboards — d3 + observable plot
CI / scan (pull_request) Successful in 3m30s
CI / commits (pull_request) Successful in 3m48s
CI / check (pull_request) Successful in 4m1s
CI / a11y (pull_request) Successful in 3m7s
Docs site / build (pull_request) Successful in 3m24s
CI / perf (pull_request) Successful in 8m13s
Records the decision to use D3 + Observable Plot, wrapped in a new
`libs/shared/charts/` lib, as the chart toolkit shared by
`portal-shell` and `portal-admin`. Implementation lands as a
separate chantier (`pnpm add` + `libs/shared/charts/` foundations +
3 starter components + audit-page integration).

The user's original ask cited D3 directly. The ADR honours that
preference but pairs D3 with Observable Plot — the same team's
higher-level layer (Mike Bostock / Observable Inc.). Plot turns
"standard charts" (bar / donut / line / stacked-bar / scatter) into
~50 LOC per type instead of the ~250 LOC each that raw D3 would
take. The shared lib's component contract stays uniform whether the
underlying technique is Plot or raw D3 — `<lib-bar-chart>`,
`<lib-heatmap>`, all expose the same `[data] / [caption] /
[description] / [ariaLabel] / [colorScheme]` signal-based shape.

Accessibility is enforced at the lib level (not bolted on per
chart): SVG `<title>` + `<desc>`, `<details>`-based tabular
fallback, colour-blind-safe palettes (Viridis / Cividis for
sequential, ColorBrewer Set2 for categorical), AA-contrast text,
`prefers-reduced-motion` gating. Six commitments, each unit-tested
so a sloppy contributor can't drop one silently.

Bundle impact (~65 KB gzip for the v1 vocabulary loaded on a
chart-bearing lazy chunk) stays well under ADR-0017's 100 KB lazy-
chunk cap via per-`d3-*` module imports.

Considered + rejected:
* D3 alone — 250 LOC per chart × 4-5 charts × consistent a11y
  posture across them all = sustained code investment before the
  first dashboard ships.
* Apache ECharts — 600 KB minified + canvas rendering with an
  `aria` plugin afterthought + JSON-config idiom is the furthest
  from the Angular-Signals direction the workspace runs on.
* Chart.js — narrower vocabulary than Plot, canvas-rendered with
  weaker out-of-the-box a11y, dark-mode integration brittle.

CLAUDE.md and `docs/decisions/README.md` updated in the same
change. ADR-0017 (perf budgets) and ADR-0016 (a11y baseline) are
referenced as the binding constraints; this ADR doesn't supersede
either, it operationalises both for the chart surface.
2026-05-16 21:22:41 +02:00
julien aa8ad97feb fix(portal-admin): adr refs point at gitea, not the madr template repo (#153)
CI / check (push) Successful in 1m47s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m48s
CI / a11y (push) Successful in 2m1s
CI / perf (push) Successful in 3m33s
## Summary

Records the decision to render `docs/**/*.md` as a separately-deployed static site using **VitePress + `vitepress-plugin-mermaid`**.

This is **ADR-only** — the implementation (install + `.vitepress/config.ts` + `docs/index.md` + Gitea Actions workflow) lands as the next chantier. Splitting them keeps the decision review focused on the *why* before the *how*.

## What lands

### [`docs/decisions/0022-docs-site-vitepress.md`](docs/decisions/0022-docs-site-vitepress.md)

Full MADR 4.0.0 ADR. Decision drivers, 4 considered options (VitePress, MkDocs Material, Docusaurus 3, Astro Starlight), site-structure mapping, Mermaid integration, deployment + CI plan, consequences, revisit triggers. Tags: `process`, `infrastructure`.

**Key choices captured:**

- VitePress wins on toolchain alignment (Vite already in the workspace via `@nx/vite`, `vite-plugin-angular`, Vitest). MkDocs Material was the strong runner-up; the Python runtime tax in Gitea Actions tipped the balance.
- `vitepress-plugin-mermaid` for ```` ```mermaid ```` blocks ([ADR-0009](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md) sequence + [architecture.md](docs/architecture.md) C4 are hard requirements).
- Site sources entirely from `docs/`. Mapping:
  - `docs/index.md` (new, Hero layout) → `/`
  - `docs/development.md` → `/development`
  - `docs/architecture.md` → `/architecture`
  - `docs/decisions/README.md` → `/decisions/` (curated index, kept as section landing)
  - `docs/decisions/00NN-….md` → auto-listed in the sidebar by numeric prefix
  - `docs/setup/0N-….md` → `/setup/0N-…`
- **Excluded** via `srcExclude`: `docs/README.md` (stays as the git-side / IDE-preview index — option A from the prior discussion) and `docs/decisions/template.md` (authoring scaffold).
- Empty placeholder sections in `docs/README.md` (Operations runbooks, Security/perf/a11y rationales) are NOT pre-created as empty pages — they appear in the sidebar when real content lands.
- Deployment: dedicated hostname (provisional `docs.portal.apf.fr`) behind the same Caddy reverse-proxy as the apps, fed by a new `.gitea/workflows/docs-site.yml` triggered on `docs/**` push. Exact hostname follows the future infrastructure ADR; not locked here.

### [`docs/decisions/README.md`](docs/decisions/README.md)

ADR-0022 added to the index table.

### [`CLAUDE.md`](CLAUDE.md)

Bumps the ADR range from `0001 → 0021` to `0001 → 0022`. New bullet in the "Architecture (recorded in ADRs)" section describing the docs-site choice in one paragraph. Implementation tracked in "Still on the roadmap" until the next PR lands it.

## Notes for the reviewer

- **Why ADR before implementation?** The choice between VitePress / MkDocs Material / Docusaurus / Astro Starlight is exactly the "stable + recognized + innovative" trade-off [CLAUDE.md](CLAUDE.md) asks to document. Reviewing the rationale on its own (without dragging through the install diff) keeps the discussion focused.
- **Why not surface ADRs inside `portal-admin`?** Audience mismatch — [ADR-0020](docs/decisions/0020-portal-admin-app.md) §"Audience is disjoint" frames `portal-admin` around APF internal operators (CMS, audit, user directory), not architects. The full reasoning is in ADR-0022 §"Context and Problem Statement".
- **Why two index artefacts (`docs/README.md` + future `docs/index.md`)?** Option A from the structure discussion. Each serves a distinct audience: `README.md` is the flat link list that renders well in IDEs / Gitea source view; `index.md` will be the VitePress Hero landing for the web audience. Light duplication, no maintenance pressure (the IDE one only needs updating when sections appear/disappear).
- **Why `vitepress-plugin-mermaid` rather than Docusaurus's built-in Mermaid?** The community plugin is a sub-1.0 dependency on the wrapper (Mermaid itself is mature), but Mermaid is so mainstream that switching it out is a half-day rewrite if the plugin stalls. Trading that risk against Docusaurus's MDX-by-default footprint + React runtime is a net win.
- **Why `process` + `infrastructure` tags?** Mirrors [ADR-0015](docs/decisions/0015-cicd-gitea-actions.md) (also a CI / deploy decision with content authoring implications) and is consistent with the [tag vocabulary](docs/decisions/README.md#tag-vocabulary). No new tag invented.

## Test plan

- [x] `docs/decisions/0022-docs-site-vitepress.md` validates as MADR 4.0.0 (frontmatter, section order). Index in [`docs/decisions/README.md`](docs/decisions/README.md) updated in the same PR per [ADR conventions](docs/decisions/README.md#conventions).
- [x] No code touched — `lint / test / build` matrix unaffected.
- [ ] Review for trade-off accuracy: did I get MkDocs Material's strengths right? Is Astro Starlight's maturity argument fair?
- [ ] Implementation chantier (next PR): `pnpm add -D vitepress vitepress-plugin-mermaid mermaid`, `docs/.vitepress/config.ts`, `docs/index.md`, `.gitea/workflows/docs-site.yml`, `package.json` scripts. Will land within the same week assuming this ADR holds.

## What's next

If accepted as-is, the immediate follow-up is:

1. Install VitePress + the Mermaid plugin.
2. Author `docs/.vitepress/config.ts` with the sidebar shape spelled out in this ADR (auto-generated sub-sidebar for `/decisions`, hand-curated top-level).
3. Author `docs/index.md` (Hero layout).
4. Add the `docs-site` Gitea Actions workflow.
5. Wire the dev script (`pnpm docs:dev`) into `package.json` so contributors can preview locally.

If reviewers want to push back on the toolchain choice (MkDocs Material in particular has a strong case for the theme polish), this is the right PR to surface that — implementation hasn't started.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #153
2026-05-15 18:48:07 +02:00
julien 2480d0dd6d fix(portal-bff): align admin entra role name with Portal.Admin (#145)
CI / check (push) Successful in 2m47s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m47s
CI / a11y (push) Successful in 1m30s
CI / perf (push) Successful in 3m57s
## Summary

The [`AdminRoleGuard`](apps/portal-bff/src/admin/admin-role.guard.ts) was matching on the literal `'admin'`, but the Entra app registration declares the admin app role with `value: "Portal.Admin"`. End result: an authenticated user with the role assigned in Entra still landed with `roles: []` in their session (claim simply not present in the id token), and every request to `/api/admin/audit` and `/api/admin/users` returned a **403**.

Caught manually in the portal-admin SPA: login succeeded, sidebar links to "Audit log" / "User list" returned 403. The [`/api/admin/auth/me`](apps/portal-bff/src/admin/admin-auth.controller.ts) self-test confirmed the missing claim was the cause.

## What lands

### Constant value — single source of truth

[`apps/portal-bff/src/admin/admin-role.guard.ts:18`](apps/portal-bff/src/admin/admin-role.guard.ts#L18):

```diff
-export const ADMIN_ROLE = 'admin';
+export const ADMIN_ROLE = 'Portal.Admin';
```

[`admin-role.guard.spec.ts`](apps/portal-bff/src/admin/admin-role.guard.spec.ts) already imports `ADMIN_ROLE` from the source rather than hardcoding the literal, so the guard contract spec rolls through unchanged. The fixtures elsewhere ([`auth.service.spec.ts`](apps/portal-bff/src/auth/auth.service.spec.ts), [`admin.controller.spec.ts`](apps/portal-bff/src/admin/admin.controller.spec.ts), [`admin-auth.controller.spec.ts`](apps/portal-bff/src/admin/admin-auth.controller.spec.ts), [`require-mfa.guard.spec.ts`](apps/portal-bff/src/auth/require-mfa.guard.spec.ts)) keep `roles: ['admin']` as fixture data — those tests exercise the extraction / serialization pipeline, which is role-value-agnostic; touching them would be incidental cleanup with no behaviour signal.

### Doc-comment refresh

Inline references to the role name updated so future readers don't grep `'admin'` and find a phantom value:

- [`admin-role.guard.ts`](apps/portal-bff/src/admin/admin-role.guard.ts) — class doc-block (3 mentions).
- [`admin.controller.ts`](apps/portal-bff/src/admin/admin.controller.ts) — class doc-block + inline guard-contract comment.
- [`audit.service.ts`](apps/portal-bff/src/audit/audit.service.ts) — `adminAccessDenied` doc-block (2 mentions).

### Documentation

- [`docs/decisions/0020-portal-admin-app.md`](docs/decisions/0020-portal-admin-app.md) — 5 references to the role across §"How is admin access enforced", §"Auth — same Entra ID …", and the Consequences §.
- [`docs/architecture.md`](docs/architecture.md) — note next to the C4 container diagram describing the admin entry gate.
- [`CLAUDE.md`](CLAUDE.md) — "Admin application" project rule.

## Notes for the reviewer

- **Why `Portal.Admin` rather than `admin`?** Operator's call on the Entra side. The `<Application>.<Role>` namespace is the conventional Entra App Role pattern when the directory may host roles for multiple applications, and `admin` alone is ambiguous in a directory shared across products.
- **Why no migration / backfill?** The role value lives only in two places: Entra app-registration manifest (operator-managed) and the BFF constant (this PR). Existing Redis sessions captured `roles: []` (claim absent) — they'll naturally pick up the correct value on next sign-in. No persisted data references the old value.
- **No ADR.** ADR-0020 §"How is admin access enforced" already commits to "Entra ID role claim + BFF guard"; the literal role string is an implementation detail the ADR happened to spell. Updated the ADR's prose to the new value to keep the doc honest, but the decision is unchanged.

## Test plan

- [x] `pnpm nx test portal-bff` — **396 specs pass**, unchanged from `main`. The `AdminRoleGuard` contract spec (covers 401-on-no-session, 403-on-missing-role + audit emission, pass-through-on-role-present) imports `ADMIN_ROLE` and re-exercises with the new value.
- [x] `pnpm exec nx affected -t format:check lint test build --base=origin/main` — clean.
- [ ] Manual verification — pending Entra-side App Role declaration with `value: "Portal.Admin"` + assignment to the test user. Once both exist: sign out + sign in on portal-admin, hit `/api/admin/auth/me` and confirm `roles: ["Portal.Admin"]`, then click "Audit log" + "User list" and confirm both render. An `admin.access_denied` row in `audit.events` is the negative-test signal (still emitted for any user without the role).

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #145
2026-05-15 10:33:54 +02:00
julien 6a6bf90e7f docs: refresh CLAUDE.md "Repository status" section (#125)
CI / check (push) Successful in 2m18s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 2m19s
CI / a11y (push) Successful in 1m5s
CI / perf (push) Successful in 3m56s
## Summary

The "Repository status" section in [CLAUDE.md](CLAUDE.md) was frozen at the pre-bootstrap state. It claimed the Nx workspace was "not yet bootstrapped", referenced ADRs only up to 0020, told future contributors that "the next step is to scaffold the workspace", and instructed agents to verify workspace existence before running build/test/run commands — all obsolete since phase-1 was shipped.

This PR rewrites the section to match reality:

- The workspace is **operational**: three apps (`portal-shell`, `portal-admin`, `portal-bff`) and four lib roots (`libs/feature/`, `libs/shared/state`, `libs/shared/tokens`, `libs/shared/ui`, `libs/shared/util`) are in place. CI runs `format:check / lint / test / build` on every PR.
- Lists what's **shipped on `main`** — phase-1 foundation, phase-2 auth + audit + security (with the right ADR citations).
- Lists what's **still on the roadmap** — `DownstreamApiClient` + OBO (ADR-0014, no v1 consumer yet), `@RequireMfa()` / `@RequireAdmin()` guards (designed-in but no consumer).
- **Disambiguates** the two distinct security ADRs: [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md) is the implementation-level baseline (helmet, CORS, CSRF, rate-limit, error envelope) — **accepted**. The strategic security baseline ADR (OWASP ASVS reference level, HDS / GDPR / NIS 2 framing, RSSI sign-off) remains **paused** — when it lands it will either confirm 0021 or supersede pieces of it.

No code or behavior changes. Documentation only.

## Why this is its own PR

The stale paragraph has been there since ADR-0021 was being written; both ADR-0021 (in its PR description) and `notes/handoff.md` flagged it for a dedicated docs PR rather than piggybacking on a code change. Keeping it isolated makes the diff trivially reviewable and avoids mixing documentation refreshes with implementation changes.

## Test plan

- [x] `pnpm exec prettier --check CLAUDE.md` — clean.
- [x] Section content cross-referenced with `ls apps/`, `ls libs/feature libs/shared`, `ls docs/decisions/` — workspace structure, lib layout, and ADR count match the rewritten claims.
- [x] All ADR links in the rewritten section resolve (0011, 0014, 0020, 0021).
- [x] Conventional Commits — `docs:` type.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #125
2026-05-14 00:12:46 +02:00
julien d962be838a feat(portal-admin): skeleton app per ADR-0020 (#100)
CI / check (push) Successful in 2m49s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 2m32s
CI / a11y (push) Successful in 1m26s
CI / perf (push) Successful in 4m17s
## Summary

First step of the admin track per ADR-0020. Scaffold the second Angular SPA in the workspace alongside `portal-shell`, with the architectural decisions from the v1 ADRs already wired so subsequent feature PRs can drop straight into modules.

## What lands

- **App generated** via `nx g @nx/angular:application` (with the matching e2e project skeleton). Standalone, zoneless-ready, prefix `app`, scss component styles, css root stylesheet, no SSR.
- **Tags** `scope:portal-admin, type:app` — module-boundary lint now treats `portal-admin` distinctly from `portal-shell` and lets it depend only on `scope:portal-admin` + `scope:shared` libs.
- **Tailwind v4 + brand tokens**: `.postcssrc.json`, `@import 'tailwindcss'`, class-based dark variant, and `@import '../../../libs/shared/tokens/src/brand-tokens.css'` — identical visual baseline to portal-shell.
- **i18n config mirrors portal-shell** (per ADR-0019): sourceLocale `en`, target `fr`, baseHref `/{locale}/` per locale, `@angular/localize/init` polyfill, `localize: ["en", "fr"]` on production, `i18nMissingTranslation: "error"` so CI blocks any PR that adds a marker without translating it. Seed [`messages.fr.xlf`](apps/portal-admin/src/locale/messages.fr.xlf) carries the four marked strings used today.
- **Budgets** relaxed to **500 KB gzip initial** per ADR-0020 §"Performance budgets" (vs 300 KB for portal-shell).
- **Skeleton home page** at `/` — `<h1>` + intro paragraph + "Skeleton — coming soon" chip in the brand-accent palette. All marked for i18n.
- **Skip-link landmark** (WCAG 2.4.1) with the same component-scoped scss as portal-shell.
- **Wildcard catch-all route** → bounces unknown paths to home (same defensive pattern as PR #96 on portal-shell).
- **Dev server on port 4300** (vs 4200 for portal-shell) — both can run in parallel.
- **`serve-static` target without `spa: true`** — locale-prefixed routing in production is handled by the reverse proxy (per ADR-0019), same as portal-shell since PR #92.

## CLAUDE.md

Picked up the second app in the **Naming** entry and the **Commands** snippet (`<app>` is now one of `portal-shell`, `portal-admin`, `portal-bff`).

## Verification

- `pnpm exec nx run-many -t lint test build --projects=portal-shell,portal-admin,shared-ui,shared-state,shared-tokens` — green.
- `portal-admin` test: 1 spec (skip-link + main landmark present).
- Production build of `portal-admin`: **62 KB gzip initial per locale** (vs 500 KB budget — plenty of headroom for the upcoming modules).
- Both `dist/apps/portal-admin/browser/{en,fr}/` emit with the right `<html lang>` and `<base href>`. FR bundle contains `"Administration"` / `"Squelette"` / `"bientôt disponible"` — translations applied.
- portal-shell production build unchanged.

## Out of scope (each its own follow-up PR)

- Admin app shell (header / sidebar / footer with "Admin" badge — likely sharing graduated primitives plus admin-specific bits).
- OpenTelemetry tracing setup (`service.name=portal-admin` per ADR-0012).
- `environment.ts` wiring (per ADR-0018) for admin-specific endpoints.
- BFF `AdminModule` + `AdminRoleGuard` + smoke `/api/admin/me` (per ADR-0020 §"Auth").
- First functional module — CMS / menu / users / audit viewer.

## Test plan

- [x] Lint + test + build green across the workspace.
- [x] Per-locale production build emits both folders with correct metadata.
- [ ] Manual: `pnpm exec nx serve portal-admin` boots on `:4300`, smoke page renders.
- [ ] Manual: prod build + `pnpm exec nx run portal-admin:serve-static` → `http://localhost:4300/en/` and `/fr/` show the placeholder with the matching language.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #100
2026-05-12 01:49:19 +02:00
julien c5e91f240b docs(decisions): add ADR-0019 i18n + ADR-0020 portal-admin (#89)
CI / commits (push) Has been skipped
CI / scan (push) Successful in 2m22s
CI / check (push) Successful in 2m36s
CI / a11y (push) Successful in 53s
CI / perf (push) Successful in 3m25s
## Summary

Pure documentation PR. Two ADRs that answer the two strategic questions raised after the footer chantier:

- **[ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md)** — how the portal handles multiple languages.
- **[ADR-0020](docs/decisions/0020-portal-admin-app.md)** — where portal administration lives.

Implementation will land across follow-up feature PRs, each consumable on its own.

## ADR-0019 — Internationalisation

**Decision:** `@angular/localize` in **build-time** mode, two locales (`fr` default served at `/`, `en` source). Path-based URLs always prefixed (`/fr/...`, `/en/...`); `/` smart-redirects via cookie → `Accept-Language` → `fr`. The locale switcher in the footer writes a `__Host-portal_locale` cookie and hard-refreshes to the matching bundle.

**Considered and rejected:**

- `@angular/localize` runtime mode (single bundle, higher LCP / payload cost).
- `@ngx-translate` / `transloco` (community libraries; tech-bar prefers Angular first-party for foundational primitives).
- Query-param URL strategy (fragile, weaker SEO, `<html lang>` becomes harder).
- Subdomain URL strategy (breaks `__Host-` cookie scoping from ADR-0010).

**Scope boundary:** UI strings owned by developers (templates + `$localize` in code). Editorial content (CMS-managed pages, news, etc.) is BFF-served already localised — that's the admin-app pipeline (ADR-0020), not `@angular/localize`.

**First sweep consequence:** the duplicate `/accessibility` + `/accessibilite` routes collapse to one Angular route with locale-translated paths.

## ADR-0020 — `portal-admin`

**Decision:** new Angular SPA `portal-admin` alongside `portal-shell`, sharing the existing `portal-bff` via `/api/admin/*` routes guarded by an Entra `admin` role plus `@RequireMfa({ freshness: 600 })` at the entry route. Distinct origin + cookie + session (`__Host-portal_admin_session`).

**v1 modules** (all four selected):

1. Editorial pages CMS (multilingual content, fed back to `portal-shell` via the BFF).
2. Sidebar menu management (activates the `requiredPermissions` field already on `MenuItem`).
3. User list (read-only).
4. Audit log viewer (consumes the `audit.events` table per ADR-0013, via the `audit_reader` role).

**Out of v1:** B2B invitations (stay in Entra Admin Center), feature flags (no substrate yet), CMS workflow / approval flows, theme customisation, live preview.

**Considered and rejected:**

- `/admin/*` lazy-loaded inside `portal-shell` (admin code in the same origin → weaker defense in depth, admin URL not IP-restrictable independently).
- Two SPAs **and** two BFFs (doubles infra at our scale — bricolage).
- Off-the-shelf admin tooling (Retool, etc. — escapes our security baseline).

**Performance budget for admin:** ≤ 500 KB gzip initial (vs 300 KB for `portal-shell`, per ADR-0017). Lighthouse Performance ≥ 85 on critical admin routes (vs ≥ 90 on `portal-shell`). Same a11y baseline (ADR-0016), same dark-mode support.

**Shared-libs graduation:** `Icon`, `LayoutStateService`, brand tokens, dark-mode SCSS helpers move from `portal-shell` to `libs/shared/{ui,state}` when both apps need them. Mechanical refactor; tracked as the first implementation PR.

## Implementation roadmap (out of scope of this PR)

ADR-0019:

1. Install `@angular/localize`, wire build target.
2. Mark every existing UI string in `portal-shell` with `i18n` + `@@id`; produce `messages.fr.xlf`.
3. Locale switcher in footer + `/api/preferences/locale` BFF route + smart redirect at `/`.
4. Collapse the duplicate accessibility routes into a localised single route, with 301s.
5. CI gate: `nx build portal-shell --localize` is added to `ci:check` and fails on missing translation.

ADR-0020:

1. `nx g @nx/angular:app portal-admin` skeleton.
2. Shared-libs extraction (`libs/shared/ui`, `libs/shared/state`).
3. BFF `AdminModule` + `AdminRoleGuard` + smoke `GET /api/admin/me`.
4. Admin shell (header / sidebar / footer with an "Admin" badge).
5. One PR per v1 module — suggested order: CMS pages → menu management → audit viewer → user list.

## Test plan

- [x] Both ADRs follow MADR 4.0.0 (frontmatter, sections, tags from the canonical vocabulary).
- [x] `docs/decisions/README.md` index updated in the same commit.
- [x] `CLAUDE.md` architecture summary picks up entries for both decisions and bumps the ADR coverage line to 0020.
- [ ] Read-through review — invite the project lead to push back on any decision before locking implementation.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #89
2026-05-11 12:29:54 +02:00
julien 4476cbb518 docs(decisions): add ADR-0018 — environment configuration strategy (#80)
CI / check (push) Successful in 1m36s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 2m2s
CI / a11y (push) Successful in 1m36s
CI / perf (push) Successful in 3m33s
## Summary

Records the per-environment config strategy for both apps. ADR-only — no code changes; the implementation is anchored in §Confirmation against the next feature work that touches per-environment values.

## What lands

[**ADR-0018 — Environment configuration**](docs/decisions/0018-environment-configuration-strategy.md):

- **SPA**: Angular `environment.ts` + project.json `fileReplacements`. Build-time substitution; no runtime config fetch (rejected for the LCP/TTFB cost it would add and the deploy-time HTML rewrite that the alternatives need). Concretely cleans up the hard-coded URLs we left in `observability/tracing.ts` and `home-status.service.ts` ("hard-coded for v1 — env-config when it lands" comments).
- **BFF**: keep `process.env` + small per-key boot-time validators (the shape `apps/portal-bff/src/config/check-database-url.ts` already follows). `@nestjs/config` rejected as too heavy for the current key count; `zod` not justified yet.
- **Audit log connection**: formalises the `AUDIT_DATABASE_URL` split that ADR-0013 §"Wired as features land" already pointed at. When set, the `AuditModule` instantiates a second Prisma client with `audit_writer`-only credentials (defense in depth); when unset, the dev fallback (shared pool + `SET LOCAL ROLE audit_writer` per transaction) keeps working. A boot-time `UPDATE`-rejection self-test runs against the dedicated pool when configured — refuses to start if the pool can mutate the audit table.

The §Confirmation block cross-references the env-var-as-boot-gate items already pre-figured in ADR-0009 / ADR-0010 / ADR-0012 / ADR-0013 / ADR-0014, so the validator pattern is the single landing place when those features ship.

## What does NOT change in this PR

- No `apps/portal-shell/src/environments/*.ts` files yet — landed alongside the next feature that actually needs per-environment values.
- No `AUDIT_DATABASE_URL` validator in BFF — same; lands when the second pool is wired.
- No CLAUDE.md restructuring; just one extra bullet under the Architecture summary referencing ADR-0018.

## Doc updates

- `docs/decisions/README.md` index — new row for ADR-0018.
- `CLAUDE.md` Architecture summary — one-line reference to ADR-0018 between the perf-budget and local-quality-gates entries.

## Test plan

- [ ] CI green on this PR (`format:check`).
- [ ] ADR-0018 renders in the doc index with the right tags (`frontend`, `backend`, `infrastructure`, `process`) and 2026-05-10 date.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #80
2026-05-10 04:58:02 +02:00
Julien Gautier 25a89bc8b0 docs: align CLAUDE.md UI stack note with the ADR-0016 spartan-ng deferral
Follow-up to the previous commit (ADR-0016 amendment). The CLAUDE.md
'Architecture' section's Accessibility line still mentioned spartan-ng
as part of the chosen UI stack; align it with the deferral note now in
ADR-0016. Phrasing matches: 'Angular CDK + TailwindCSS' as the v1
stack, with the spartan-ng philosophy applied in-house and the lib
itself revisited at 1.0.0.
2026-04-30 18:59:49 +02:00
Julien Gautier 0e58e32d29 chore: relocate ADRs from decisions/ to docs/decisions/ to consolidate documentation
Move the ADR folder under docs/ alongside the rest of the project
documentation. Convention (flat folder, globally-sequential 4-digit
numbering, tags-based categorization, MADR 4.0.0 format) is unchanged
- only the path moved.

- git mv decisions docs/decisions preserves history for all 18 ADRs +
  README + template (19 files renamed in this commit).
- ADR-0001 amended in-place with a dated note documenting the
  relocation. Status remains 'accepted' - the location detail
  changed, the decision did not.
- All cross-references updated:
  - CLAUDE.md (~17 ADR links + 3 mentions of decisions/ in the Project
    rules section)
  - docs/README.md (now references decisions/ as a sibling under docs/)
  - docs/setup/03-angular-nx-monorepo.md (paths shortened from
    ../../decisions/ to ../decisions/, since setup/ and decisions/ are
    now both inside docs/)
  - docs/decisions/0003 ../CLAUDE.md adjusted to ../../CLAUDE.md
    (one extra level of nesting)
  - docs/decisions/template.md mention of the README path
  - notes/asvs-level-decision-briefing-rssi.md mention of the index

Sanity verified: every ADR link in CLAUDE.md, docs/setup/03, and
docs/decisions/0001 resolves to an existing file. pnpm nx run-many
-t lint passes on 8 projects.
2026-04-30 18:57:59 +02:00
Julien Gautier d797becc2b chore: track Nx 22 AI tooling artefacts injected by generators
Nx 22 generators inject AI agent tooling into the repo via marked
sections and side files. Rather than re-deleting them after every
generator run, track the workspace-level ones and document why.

- CLAUDE.md gains a 'General Guidelines for working with Nx' section
  between Nx-managed markers; future Nx versions will update this
  section automatically without touching our project rules above.
  Pre-existing prettier-formatted blank lines added before code blocks
  are kept.
- .claude/settings.json (228 bytes) enables the nx-claude-plugins
  marketplace from nrwl/nx-ai-agents-config; tracked for contributor
  consistency. The personal .claude/settings.local.json stays
  gitignored.
- .github/ is the Nx AI skills/prompts/agents catalog. Kept despite
  being GitHub-named; it does not conflict with our Gitea workflows
  which will live under .gitea/workflows/ (per ADR-0015).
- .gitignore picks up Nx-managed transient dirs (.nx/polygraph,
  .claude/worktrees) and a trailing newline fix.

AGENTS.md is removed: it duplicated only the Nx auto-injected guidance
that CLAUDE.md already carries (CLAUDE.md is the strictly broader file
- project rules + Nx section). One source of truth for AI-agent
guidance.
2026-04-30 17:46:04 +02:00
Julien Gautier a88fc1e8a1 docs: align CLAUDE.md narrative with phase-2 and phase-3a ADRs
The Architecture section title and the Repository status paragraph
still referred to 'phase-1 ADRs' as if that were the current state.
Update both to reflect that ADRs 0001-0017 (phase 1 + 2 + 3a) are
recorded, that the security baseline ADR is paused awaiting RSSI
input, and that docs/setup/03 has already been rewritten to align
with the ADRs.
2026-04-30 15:46:42 +02:00
Julien Gautier ea852b3239 docs: add ADR-0017 for performance budgets (Core Web Vitals + Lighthouse CI + Angular budgets + BFF SLOs)
Pin the perf framework. Front-end metrics tracked at Google Core Web
Vitals 'Good' thresholds (LCP <= 2.5s, INP <= 200ms, CLS <= 0.1, plus
TBT <= 200ms, TTFB <= 800ms) and Lighthouse Performance >= 90 on
critical routes. Lighthouse CI (@lhci/cli) enforces them in CI with
median-of-3 runs to mitigate runner variance, on a curated
critical-routes list (login, home, accessibility statement, flagship
features as they land). Wires into the perf gate slot from ADR-0015.

Bundle budgets enforced at nx build via Angular's project.json budgets
array, type 'error': initial <= 300 KB gzip, lazy chunks <= 100 KB
gzip, per-component CSS <= 6 KB. source-map-explorer wired as an Nx
analyze target for diagnosis on budget breaches.

Back-end SLOs documented per endpoint family (p95/p99) and observed
via the OpenTelemetry spans already shipped by ADR-0012 - advisory in
CI (load profile unrepresentative), alerting in production. Quarterly
review tightens budgets when achievable.

Scheduled weekly Lighthouse run on the prod env via the existing
security-scheduled.yml workflow extends coverage beyond PR-time.

Explicit a11y/perf trade-off rule: when they conflict, a11y wins (per
APF's mission, ADR-0016). The perf budget is then re-evaluated at the
next quarterly review.

No browser-side RUM SDK in v1 - the OTel browser tracing from ADR-0012
plus scheduled prod Lighthouse runs cover the gap. RUM revisited in
v2 if a real incident escapes the existing signal.

decisions/README.md index updated. CLAUDE.md gains an explicit
'Performance budgets' line pointing to ADR-0017.
2026-04-30 14:22:36 +02:00
Julien Gautier 98a8c78a31 docs: add ADR-0016 for accessibility baseline (WCAG 2.2 AA + targeted AAA, spartan-ng + CDK + Tailwind, APF panel)
Pin the a11y baseline given the host organisation context (APF France
Handicap, where accessibility is the core mission, not a compliance
checkbox):

- Conformance: WCAG 2.2 AA universal + AAA on criteria with high
  impact for the user base (1.4.6 Enhanced Contrast 7:1, 2.2.3 No
  Timing, 2.3.3 Animation from Interactions, 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 context.

- User-preferences panel as a first-class feature: contrast (3 tiers),
  text size (up to 200%), motion, text spacing, cognitive
  simplification, reading focus. Persisted in session (ADR-0010).

- UI stack: Angular CDK + spartan-ng + TailwindCSS (not Angular
  Material; not React libs). Components copy-pasted into
  libs/shared/ui under our control. Design tokens with
  contrast-verified colour pairs in libs/shared/tokens.

- Tooling and CI gates: @angular-eslint/template/* a11y rules
  (blocking), @axe-core/playwright e2e blocking on critical/serious
  violations, design-token contrast verifier (blocking), touch-target
  size check (blocking on 44x44 min, default 48x48). Wires into the
  a11y gate slot from ADR-0015.

- Manual testing: keyboard and screen reader required on every UI PR
  via PR-template checklist; APF user-panel session before every major
  release covering visual / motor / cognitive / hearing categories.

- Documentation: public accessibility statement at /accessibility (EN)
  and /accessibilite (FR) - EAA legal requirement, generated from
  source-of-truth in the repo. Internal patterns library in
  docs/accessibility/.

decisions/README.md index updated. CLAUDE.md gains an explicit
'Accessibility' line pointing to ADR-0016 and the CI/CD line is
adjusted to mark the a11y gate as locked-in (no longer 'future').
2026-04-30 13:36:00 +02:00
Julien Gautier 880c7ded6b chore: rename project from adastra_portal to apf_portal
The host organisation - APF France Handicap - was confirmed on
2026-04-30. Update all in-repo references to use apf_portal
(snake_case in prose) and apf-portal (kebab-case workspace name and
repo URL). Touched: CLAUDE.md, ADRs 0001/0002/0003/0015, and the Nx
bootstrap setup guide.

The historical name is preserved as a single sentence in ADR-0003 as
a self-validating example of the function-prefixed naming convention
designed exactly for this scenario - the apps (portal-shell,
portal-bff) and the lib conventions (feature-<name>, shared-<scope>)
were unaffected by the rename, which was the explicit point of
ADR-0003.

Memory state aligned out-of-band: project_adastra.md retired,
project_apf_portal.md created with the expanded APF context (host
org, health + financial data scope, ASVS L3 pending RSSI input, UI
stack decision spartan-ng + CDK + Tailwind, expanded phase-3 status).

Pending follow-ups (user-side, not in this commit):
- rename the Gitea repo julien/adastra_portal -> julien/apf_portal
- git remote set-url origin gitea@git.unespace.com:julien/apf_portal.git
- optionally rename the local working directory ~/Works/adastra_portal/
  -> ~/Works/apf_portal/
2026-04-30 13:31:38 +02:00
Julien Gautier 49712d0bbf docs: add ADR-0015 for the CI/CD pipeline (Gitea Actions, trunk-based + squash-merge, thin YAML)
Pin the CI/CD shape with an explicit two-level structure, anticipating
the GitLab migration on the 6-18-month horizon:

Level 1 - vendor-neutral, survives the migration:
- Trunk-based with short-lived feature branches; squash-merge only.
- Branch protection on main: no direct push, no force push, linear
  history, required green CI, required reviewers = 0 in v1 (raised to
  >=1 when a second contributor joins), branch deletion after merge.
- Required CI gates (all blocking): format, lint, type-check, test,
  build (nx affected), audit (pnpm + Trivy), secret-scan (gitleaks),
  commit-lint (Conventional Commits on the PR commit range). Future
  a11y and perf gates land with their respective ADRs.
- Conventional Commits validated locally (hook from ADR-0007) and in
  CI as defense in depth.
- Signed commits recommended but not required in v1; revisited at the
  GitLab migration.
- Thin YAML: all orchestration logic lives in package.json scripts
  (ci:check, ci:scan, ci:commits) and Nx targets. Workflow files only
  do checkout, setup, cache, and call one script per job. The migration
  rewrites the YAML wrappers, never the gates.
- Secrets convention SCOPE_PURPOSE; gitleaks enforces no-secrets-in-code.
- Container images / deploy explicitly out of scope (deferred to the
  on-prem infrastructure ADR).

Level 2 - Gitea Actions specific, will be superseded by a GitLab
migration ADR:
- Engine: Gitea Actions (built-in, GitHub Actions-compatible syntax,
  partial portability hedge if the org pivots to GitHub).
- Runners: >=3 self-hosted act_runner instances on-prem, Debian image
  pinned by SHA, weekly rebuild via security-scheduled.yml.
- Three workflow files: ci.yml (PR + push to main), release.yml
  (stub for tag, populated by the deploy ADR), security-scheduled.yml
  (weekly Trivy + gitleaks full-tree scan + Renovate trigger).

Migration weight estimated at 3-5 days dev/ops when GitLab arrives:
the YAML is rewritten, the gates and scripts are unchanged. A future
ADR will explicitly supersede only the level-2 sections.

decisions/README.md index updated. CLAUDE.md gains an explicit 'CI/CD'
line pointing to ADR-0015 and noting the future GitLab supersession.
2026-04-30 11:21:30 +02:00
Julien Gautier a20330a474 docs: add ADR-0014 for downstream API access (OBO pattern + DownstreamApiClient framework)
Pin the framework for calls from the BFF to integrated downstream APIs.
The concrete list of downstream services is not yet known, but the
framework must exist so that the day a developer adds an integration the
answer is 'use the standard client', not 'invent something'.

A DownstreamApisModule exposes a DownstreamApiClientFactory that produces
typed clients from per-service DownstreamApiConfig blocks. Each config
declares the auth strategy, base URL, timeout, retry, circuit breaker,
bulkhead, and audienceConstraint.

Default auth strategy for Entra-protected downstreams is On-Behalf-Of
(MSAL Node acquireTokenOnBehalfOf). Downstream-scoped tokens are cached
in Redis under obo:{user_id_hash}:{resource}, encrypted with AES-256-GCM
using a dedicated key (OBO_CACHE_ENCRYPTION_KEY) distinct from the
session-encryption key so a cache compromise doesn't cascade into a
session compromise.

Fallback strategy for non-Entra downstreams is service credential +
signed user-assertion header (X-User-Assertion JWT signed by the BFF's
private key, verified by downstreams against the BFF JWKS at
/.well-known/jwks.json). Token relay is rejected as a default;
per-user credential mapping is rejected outright.

Resilience composes via cockatiel: timeout outermost, then retry (only
on idempotent verbs and retriable error classes), circuit breaker per
service, bulkhead per service. Each call opens a downstream.<service>
OpenTelemetry span; auth failures emit audit events. Downstream errors
are translated at the client boundary - never bubbled with raw payload.

Audience pre-check is enforced at the call site (not at controller
entry) - even a missing authorization guard upstream cannot bypass the
audience constraint.

The framework is forward-looking; concrete integrations land per-service
in code config (no per-integration ADR unless the integration deviates
non-trivially from the defaults). Strategy code is exercised by
mock-driven tests until the first real integration ships.

decisions/README.md index updated. CLAUDE.md gains an explicit
'Downstream API access' line pointing to ADR-0014.
2026-04-29 23:58:49 +02:00
Julien Gautier b35bf2b3de docs: add ADR-0013 for the audit trail (dedicated append-only Postgres schema)
Pin the audit-trail architecture: a dedicated 'audit' schema in the same
Postgres instance as business data, with three Postgres roles enforcing
append-only at the database layer - audit_writer (INSERT only),
audit_reader (SELECT only), audit_archiver (DELETE only on rows past the
retention threshold). No role anywhere holds UPDATE or TRUNCATE; the BFF
verifies this at startup with a deliberate failing UPDATE probe.

The audit stream is decoupled from the application logs (different sink,
different access controls, different retention) but cross-referenced via
trace_id (ADR-0012) and actor_id_hash, which uses the same salt as the
app logs so an investigator joins the two streams without re-hashing.

Events captured in v1 cover the auth and session lifecycle: sign_in
(success/failure), sign_out, session.expired, session.revoked,
token.validation.failed, mfa.assertion.failed, authz.deny. Hooks for
admin actions and sensitive data access are designed-in but inert until
v1+ features call them - kept alive by tests so they don't drift.

Failure semantics are blocking - if the audit INSERT fails, the
in-flight operation fails with 503. Trade-off acknowledged: the audit DB
is part of the trust path. Mitigation is HA Postgres in prod, deferred
to the infrastructure ADR.

Retention defaults to 365 days, env-overridable, enforced by a daily
purge running under audit_archiver. The retention default is engineering
prudence, not legal advice - the org-side legal review of the actual
applicable retention regime is explicitly owed and noted in the ADR.

Cryptographic chaining and WORM storage are deferred unless a compliance
regime demands them.

decisions/README.md index updated. CLAUDE.md gains an explicit
'Audit trail' line pointing to ADR-0013.
2026-04-29 23:32:35 +02:00
Julien Gautier fe3bb2dd7d docs: add ADR-0012 for observability (Pino + OpenTelemetry, W3C Trace Context, stdout + collector)
Pin the observability foundation. Two signals are in scope: structured
logs and distributed traces.

Logs: pino + nestjs-pino, JSON line-delimited on stdout, with a fixed
envelope (level, time, service, version, env, trace_id, span_id,
session_id, user_id_hash, audience, msg, ...). Pino redact strips a
reviewable allowlist of sensitive paths (Authorization/Cookie headers,
*.password, *.access_token, *.refresh_token, ...). user_id_hash uses a
per-environment salt (LOG_USER_ID_SALT) so the same userId is not
correlatable across environments.

Tracing: OpenTelemetry SDK for Node + auto-instrumentations (HTTP,
Express, NestJS, pg, ioredis, Prisma). The SPA also runs OTel-Web with
an HTTP interceptor propagating traceparent on outbound calls; the same
trace_id is the correlation identifier from the user click to the DB
query. No separate X-Correlation-ID.

Request-scoped context lives in nestjs-cls; the Pino formatter and the
future audit-log writer pull from CLS - no per-call threading.

Sampling is 100% at the application; tail sampling is performed at the
local OpenTelemetry Collector (deferred to the phase-3 infrastructure
ADR, where the on-prem backend - Grafana stack, ELK, or other - will be
chosen). Output: stdout for logs, OTLP/HTTP for traces, both consumed
by the local collector. The application stays vendor-neutral.

Audit logs are explicitly out of scope of this ADR - they share the
trace_id but use a separate writer and a separate sink (next ADR).

decisions/README.md index updated. CLAUDE.md gains an explicit
'Observability' summary pointing to ADR-0012.
2026-04-29 23:25:36 +02:00
Julien Gautier 065d50247f docs: add ADR-0011 for MFA enforcement (Entra Conditional Access + step-up hooks)
Pin the MFA policy: enforcement lives in Entra ID Conditional Access at
the tenant level (org IT responsibility) - the application code does not
implement MFA mechanics. The BFF performs a defense-in-depth sanity-check
on the id_token amr claim at session creation; sessions without evidence
of multi-factor authentication are rejected. The accepted amr values are
maintained in a small in-source list and reviewed on cadence.

Step-up MFA is designed-in for v1 but dormant: a @RequireMfa() decorator
and a RequireMfaGuard ship in the codebase, the session payload carries
mfaVerifiedAt, and the SPA HTTP interceptor handles the 401 + claims
challenge round trip. No v1 route is annotated, since v1 has no admin UI
or other operations sensitive enough to require fresh MFA. The hooks are
kept alive by automated tests so they don't drift.

Authentication Context Classes (ACR-based step-up) are not used in v1;
they remain a future option if specific operations later demand them.
Service-account / app-only flows are out of scope.

decisions/README.md index updated. CLAUDE.md gains an explicit 'MFA' line
pointing to ADR-0011.
2026-04-29 23:16:51 +02:00
Julien Gautier b6e43565c3 docs: add ADR-0010 for session management (opaque ID + Redis + AES-GCM)
Pin the session-state architecture: the browser carries only an opaque
crypto-random session id in __Host-portal_session, signed with
SESSION_SECRET. The payload (userId, audience, curated claims, encrypted
tokens, timestamps) lives in self-hosted Redis, accessed via the standard
express-session + connect-redis pair under the NestJS Express adapter.

The id_token / access_token / refresh_token tuple is encrypted with
AES-256-GCM before being stored - per-record IV, GCM auth tag - using
SESSION_ENCRYPTION_KEY. A Redis snapshot or memory dump alone is not
enough to forge a working session; the encryption key must also be
compromised. Tampered or wrong-key records are rejected and audited.

TTL policy: idle 30 min sliding (TTL refreshed on each request) +
absolute 12 h (checked in a global interceptor, triggers DEL on expiry).

Topology: Redis Sentinel (3+ nodes) in prod with TLS and ACL; single node
in dev. Operational specifics deferred to a phase-3 infrastructure ADR.

Revocation is immediate (DEL session:{id}). A secondary index
user_sessions:{userId} supports per-user listing and force-logout. No
PostgreSQL mirror; historical trace lives in the future audit-log ADR.

decisions/README.md index updated. CLAUDE.md gains an explicit 'Sessions'
line pointing to ADR-0010.
2026-04-29 23:09:34 +02:00
Julien Gautier 90bca95fce docs: add ADR-0009 for the authentication flow (OIDC Auth Code + PKCE via MSAL Node)
Pin the BFF authentication mechanics: OAuth 2.0 Authorization Code Flow
with PKCE, executed server-side via @azure/msal-node's
ConfidentialClientApplication. Tokens are held in the BFF session and
never reach the browser; the SPA only ever sees the opaque
__Host-portal_session cookie.

Token validation enforces the tenant allowlist from ADR-0008 (iss check)
and maps the audience claim to our Audience enum at the validation step.
Refresh-token rotation is enabled via MSAL acquireTokenSilent. Cookies
use the __Host- prefix (forces Secure/Path=/, no Domain) with
HttpOnly/SameSite=Lax. CSRF uses the double-submit pattern with a
matching X-CSRF-Token header on every state-changing request, enforced
by a NestJS interceptor and injected client-side by an Angular HTTP
interceptor. Logout is RP-initiated against Entra's end_session_endpoint.

Routes are pinned: GET /auth/login, GET /auth/callback, POST
/auth/logout, GET /auth/me. AuthGuard is registered globally - public
routes must be explicitly opted in.

Local dev runs over HTTPS via mkcert to keep cookie behaviour identical
to prod. All Entra-specific values come from environment variables; the
BFF refuses to start without them.

decisions/README.md index updated. CLAUDE.md gains an explicit
'Authentication flow' line pointing to ADR-0009.
2026-04-29 22:59:33 +02:00
Julien Gautier f5e8e6ef61 docs: add ADR-0008 for the identity model (multi-tenant Entra workforce + dual-audience design)
Capture the v1 identity model: Microsoft Entra ID, multi-tenant app with
B2B guest invitation for partner-org employees, workforce-only
authentication in v1. Code and data are architected for dual audience
from day one (Audience enum, audience claim in sessions, audience column
+ RLS policies on shared tables, claims-based authz) so that adding Entra
External ID for customers later is a switch-flip rather than a refactor.

The dev environment uses a Microsoft 365 Developer Program tenant (free,
renewable) to unblock work while the prod tenant is being provisioned by
the org IT contact. Production requires Entra ID P1 licensing - flagged
here so it can be planned, not surprised.

decisions/README.md index updated. CLAUDE.md 'Identity' line now points
to ADR-0008.
2026-04-29 22:53:17 +02:00
Julien Gautier 084ff5c3bf docs: add ADR-0007 for pre-commit hooks and align documentation references
- decisions/0007-pre-commit-hooks-and-conventional-commits.md formalizes
  Husky + lint-staged + commitlint with Conventional Commits as the local
  quality-gate baseline.
- decisions/README.md index updated.
- docs/setup/03 section 8 rewritten to reference the ADR and document the
  full hook setup (pre-commit, commit-msg, commitlint config).
- docs/setup/03 future-work table 'ADR(s)' column removed; future ADR
  numbers are now assigned at the moment each ADR is written, not
  pre-reserved.
- CLAUDE.md aligned: pre-allocated phase-2 ADR numbers replaced by phase
  references; a pointer to ADR-0007 added under 'Local quality gates'.
2026-04-29 21:01:49 +02:00
Julien Gautier 79eee77594 chore: initialize repository with project rules, docs, and phase-1 ADRs
Set up the foundation for the adastra-portal project:

- CLAUDE.md captures durable project rules (quality bar, security/perf/a11y
  as first-class, language, commit conventions, ADR proactivity).
- docs/ and decisions/ scaffolding with maintained indexes (docs/README.md
  and decisions/README.md), MADR 4.0.0 template, and tag vocabulary.
- Phase-1 ADRs (0001-0006) lock structural choices: ADR usage, Nx monorepo
  with the apps preset, naming convention (adastra-portal / portal-shell /
  portal-bff), Angular CSR/zoneless/Signals/Vitest, NestJS over Express,
  PostgreSQL with Prisma.
- docs/setup/ guides translated to English.
- .gitignore covers Node/Nx artifacts and the personal notes/ scratchpad.

The Nx workspace itself is not yet bootstrapped; that step is gated on a
revised setup guide aligned with the ADRs.
2026-04-29 20:43:00 +02:00