diff --git a/CLAUDE.md b/CLAUDE.md index 954a524..6ce92e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,13 +50,14 @@ The structural, security, observability, and quality choices are recorded as ADR - **Internationalisation:** `@angular/localize` in build-time mode, two locales (`fr` default served at `/`, `en`), source locale = English (project English-only rule). Path-based URLs always prefixed (`/fr/...`, `/en/...`); `/` smart-redirects via cookie → `Accept-Language` → `fr`. UI strings live in XLIFF (`messages.fr.xlf`); editorial / CMS content is BFF-served already localised (see admin app). Footer hosts the locale switcher; switching writes a `__Host-portal_locale` cookie and hard-refreshes — see [ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md). - **Admin application (`portal-admin`):** dedicated Angular SPA alongside `portal-shell`, sharing the same `portal-bff` via `/api/admin/*` routes guarded by an Entra `Portal.Admin` role + `@RequireMfa({ freshness: 600 })` at entry. Distinct origin / cookie / session from `portal-shell` (`__Host-portal_admin_session`). v1 modules: CMS for static pages (multilingual), menu management, user list (read-only), audit log viewer. Bundle budget relaxed to ≤ 500 KB gzip (vs 300 KB for `portal-shell`); same a11y + dark-mode baseline. Shared UI primitives (`Icon`, `LayoutStateService`, brand tokens) graduate to `libs/shared/*` as both apps need them — see [ADR-0020](docs/decisions/0020-portal-admin-app.md). - **Local quality gates:** Husky + lint-staged + commitlint with Conventional Commits — see [ADR-0007](docs/decisions/0007-pre-commit-hooks-and-conventional-commits.md). +- **Documentation site:** `docs/**/*.md` rendered as a separate static site via **VitePress** (Vite-based, Node-only toolchain, Markdown-first). Mermaid diagrams via `vitepress-plugin-mermaid`. Deployed on its own hostname behind the shared reverse-proxy; CI hook on `docs/` changes rebuilds + publishes. Decoupled from the apps — content lives in `docs/`, no in-app Markdown viewer — see [ADR-0022](docs/decisions/0022-docs-site-vitepress.md). - **Runtime:** Node.js latest LTS major. ## Repository status The Nx workspace is **scaffolded and operational**. The three apps (`portal-shell`, `portal-admin`, `portal-bff`) and the 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. -ADRs 0001 → 0021 are accepted and cover the structural, security, observability, quality, i18n, and admin-app choices. **Shipped on `main`:** +ADRs 0001 → 0022 are accepted and cover the structural, security, observability, quality, i18n, admin-app, and docs-site choices. **Shipped on `main`:** - **Phase-1 foundation** — Nx workspace, Angular `portal-shell`, NestJS `portal-bff`, Prisma + Postgres, Pino + OpenTelemetry, Husky/lint-staged/commitlint, Gitea Actions CI. - **Phase-2 auth + audit + security** — OIDC Auth Code + PKCE via MSAL Node, Redis sessions with AES-256-GCM at rest, idle 30 min sliding + absolute 12 h hard ceiling, RP-initiated logout, double-submit CSRF, `audit.events` append-only schema with role-based grants, helmet + env-driven CORS allowlist + rate limiting + structured error envelope (see [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md)). @@ -66,6 +67,7 @@ ADRs 0001 → 0021 are accepted and cover the structural, security, observabilit - `DownstreamApiClient` + OBO ([ADR-0014](docs/decisions/0014-downstream-api-access-obo-pattern.md)) — no v1 consumer yet; will land when the first business route needs an Entra-protected API. - `@RequireMfa()` / `@RequireAdmin()` guards ([ADR-0011](docs/decisions/0011-mfa-enforcement-entra-conditional-access.md), [ADR-0020](docs/decisions/0020-portal-admin-app.md)) — designed-in, awaiting first consumer route. +- **Docs static-site implementation** ([ADR-0022](docs/decisions/0022-docs-site-vitepress.md)) — ADR accepted, chantier (VitePress install + `.vitepress/config.ts` + `docs-site.yml` workflow) lands next. - **Strategic security baseline ADR** — separate from the implementation-level [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md). Remains **paused** awaiting RSSI input on the OWASP ASVS reference level and adjacent frameworks (HDS, GDPR, possibly NIS 2). When it lands it will either confirm 0021 or supersede pieces of it. ## Commands once the workspace exists diff --git a/apps/portal-admin/src/app/pages/audit/audit.html b/apps/portal-admin/src/app/pages/audit/audit.html index 261f4e0..ed398b8 100644 --- a/apps/portal-admin/src/app/pages/audit/audit.html +++ b/apps/portal-admin/src/app/pages/audit/audit.html @@ -3,9 +3,13 @@

Audit log

Read-only view of audit.events per - ADR-0013. Every query - run on this page emits its own admin.audit.query row — read access is itself - auditable. + ADR-0013. Every query run on this page emits its own admin.audit.query row — read access + is itself auditable.

diff --git a/apps/portal-admin/src/app/pages/users/users.html b/apps/portal-admin/src/app/pages/users/users.html index da662ac..36ff170 100644 --- a/apps/portal-admin/src/app/pages/users/users.html +++ b/apps/portal-admin/src/app/pages/users/users.html @@ -3,9 +3,13 @@

Users

Read-only directory of every identity that has signed in to portal-shell or portal-admin per - ADR-0020. Every query - run on this page emits an admin.users.query audit row — directory access is - itself auditable. + ADR-0020. Every query run on this page emits an admin.users.query audit row — directory + access is itself auditable.

diff --git a/docs/decisions/0022-docs-site-vitepress.md b/docs/decisions/0022-docs-site-vitepress.md new file mode 100644 index 0000000..a224282 --- /dev/null +++ b/docs/decisions/0022-docs-site-vitepress.md @@ -0,0 +1,159 @@ +--- +status: accepted +date: 2026-05-15 +decision-makers: R&D Lead +tags: [process, infrastructure] +--- + +# Documentation site — VitePress + Mermaid plugin, separate static deployment + +## Context and Problem Statement + +The project ships its documentation as Markdown under `docs/` (architecture diagrams, daily-dev guide, onboarding setup, 21 ADRs and growing). Today the only way to consult those files is `git clone` + IDE preview, or browse them directly on Gitea. Two recurring frictions surface from this: + +- Some pages in `portal-admin` reference ADRs in their intro copy; without a public URL for the ADR, the anchors had to either point at Gitea's source-view (raw markdown, ergonomic but no rendering of Mermaid, no search) or — as observed in [PR for `fix(portal-admin): adr refs point at gitea`](../../apps/portal-admin/src/app/pages/audit/audit.html) — at the **MADR template repo by mistake**. +- A future RSSI / auditor / new contributor handed a link to an ADR would land on raw markdown on Gitea, without inter-ADR navigation, without a search box, without rendered Mermaid sequence diagrams (which carry a non-trivial part of the architecture rationale in [ADR-0009](0009-auth-flow-oidc-pkce-msal-node.md) and [architecture.md](../architecture.md)). + +Earlier discussion considered surfacing the `docs/` content **inside `portal-admin`** as a "Documentation" section. That option was rejected: `portal-admin`'s audience per [ADR-0020](0020-portal-admin-app.md) §"Audience is disjoint" is APF internal operators (CMS, audit, user directory) — not architects. Embedding dev-internal ADRs there blurs the surface and adds Markdown-rendering + BFF↔filesystem coupling for no audience win. + +The need is therefore: a **separately-deployed static site** that renders `docs/**/*.md` cleanly, auto-rebuilds on push, and stays decoupled from the runtime apps. + +## Decision Drivers + +- **Toolchain alignment.** The workspace is 100 % Node / pnpm / Vite / TypeScript. Adding a second runtime (Python, Go, Ruby) for the docs site is a permanent maintenance tax in CI. +- **Stability bar.** Per [CLAUDE.md](../../CLAUDE.md) §"Project rules": stable, recognized, battle-tested choices. No pre-1.0 dependencies, no one-maintainer projects. +- **Markdown-first content.** All current and foreseeable content is pure Markdown. MDX / framework-component embedding is not required. +- **Mermaid rendering is a hard requirement.** [ADR-0009](0009-auth-flow-oidc-pkce-msal-node.md) and [architecture.md](../architecture.md) carry diagrams as ` ```mermaid ` fenced blocks; both must render. +- **Audience is internal.** RSSI, future devs, ops. Not a marketing surface; theme polish is secondary, content density is primary. +- **No versioning needed.** ADRs are immutable once accepted; supersession is by inter-ADR reference, not by version snapshot. The site never needs to host `v1.x` and `v2.x` side by side. +- **No i18n needed for the docs themselves.** Per [CLAUDE.md](../../CLAUDE.md): all documentation is English-only. The site is single-locale. +- **Build speed matters.** A CI hook on every `docs/` change should publish in seconds, not minutes — otherwise contributors will lose the "edit a sentence, see it live in 30 s" feedback loop. +- **Cost of failure.** If the docs site is down, no app traffic is affected (it's strictly read-only reference content). Reliability bar is therefore relaxed compared to `portal-shell` / `portal-bff`. + +## Considered Options + +- **VitePress + `vitepress-plugin-mermaid`** (chosen). +- **MkDocs Material** with the Mermaid `pymdownx` extension. +- **Docusaurus 3** (Mermaid built-in since v2). +- **Astro Starlight** (Mermaid via plugin). + +## Decision Outcome + +Chosen: **VitePress** as the static-site generator, **`vitepress-plugin-mermaid`** for the diagram support. + +**Why VitePress wins this matchup.** + +- **Zero new toolchain.** Vite is already in the workspace (`@nx/vite`, `vite-plugin-angular`, Vitest). Adding VitePress is a single dev-dep + a config file; no Python runtime to pin in CI, no Ruby gems, no separate cache layer. +- **Vue ergonomics stay invisible for pure-Markdown content.** We never have to write a Vue component — but if we ever need a small interactive widget (an ADR-status badge, a "supersession chain" navigator), the escape valve exists and is mature. +- **Build is sub-second incremental, ~2 s full** for the current `docs/` size — Vite's HMR carries over to the doc-site dev loop. +- **Battle-tested by the tools we already use:** Vite, Vitest, Pinia, Vue Router all run their docs on VitePress. The project's own tech bar treats "used by the build-tool family we depend on" as a strong signal — it's the same logic that drove the Angular / NestJS / Prisma choices. +- **Markdown-first config.** No MDX ceremony for content that is and will remain pure Markdown. + +### Site structure & source layout + +The site builds from `docs/` (configured as VitePress's `srcDir`). Mapping: + +``` +docs/index.md → / (Hero layout, landing) +docs/development.md → /development +docs/architecture.md → /architecture +docs/decisions/README.md → /decisions/ (curated index — by tag) +docs/decisions/0001-…md → 0021-…md → /decisions/00NN-… +docs/setup/01-…md → 03-…md → /setup/0N-… +``` + +**`docs/index.md` is added in this chantier** with VitePress's `layout: home` frontmatter — Hero + features cards linking the three top-level sections (Architecture, Decisions, Onboarding). It lives alongside the existing **`docs/README.md`**, which stays as the git-side / IDE-preview index that contributors browse via Gitea's source view. The two artefacts serve different audiences and the duplication is light (the IDE index is a flat link list, the web home is a polished landing); `docs/README.md` is excluded from the site via `srcExclude`. + +**Excluded from the published site:** + +- `docs/README.md` — git-side index, redundant with the VitePress home. +- `docs/decisions/template.md` — authoring scaffold, not reader content. + +**Sidebar generation:** + +- Top-level groups (Architecture, Decisions, Onboarding) are hand-curated in `.vitepress/config.ts` — three entries, low churn. +- The Decisions sub-list is **auto-generated** by walking `docs/decisions/00*-*.md` so adding a new ADR is a single-file change (no `config.ts` edit). Numeric filename prefix gives natural ordering. + +The curated `docs/decisions/README.md` index (tag-grouped, supersession-aware) stays as the section's landing page — sidebar lists ADRs by number, README lists them by theme. Both are useful and don't duplicate effort. + +**Empty placeholder sections** of the current [docs/README.md](../README.md) (Operations & runbooks, Security/perf/a11y rationales) are NOT pre-created as empty pages. They appear in the sidebar when real content lands. + +### Mermaid integration + +`vitepress-plugin-mermaid` (community plugin, maintainer `emersonbottero`, ~500★, 2.x release stream, used by mainstream VitePress doc sites). Two lines in `.vitepress/config.ts` + ` ```mermaid ` fenced blocks render. Client-side rendering — acceptable for the document sizes in scope; if diagram count grows enough to warrant build-time SVG pre-rendering, the plugin offers an opt-in for that without changing source documents. + +Mermaid's `theme: 'default'` + plugin config follows the site's light/dark switcher — diagrams adapt to the reader's chosen theme without manual classes. + +### Deployment & CI + +- Build: `pnpm vitepress build docs` produces `docs/.vitepress/dist/`. +- Hosting: served as static files behind the same Caddy reverse-proxy as `portal-shell` / `portal-admin`, on a dedicated hostname (provisional `docs.portal.apf.fr` — the exact host follows the future infrastructure ADR's domain plan; not locked here). +- CI: a new Gitea Actions workflow `docs-site.yml` triggers on `push` to `main` when `docs/**` or `package.json` / `pnpm-lock.yaml` changes. Runs `pnpm vitepress build docs` and rsyncs to the static host. Per [ADR-0015](0015-cicd-gitea-actions.md) the YAML stays thin — orchestration in `package.json` scripts. +- No PR-preview deployments in v1 — contributors render locally via `pnpm vitepress dev docs`. Preview deploys can land later if the cycle becomes painful. + +### Consequences + +- Good, because the broken-ADR-link class of bugs (PR #N pointing at `github.com/adr/madr`) is eliminated at source: anchors become stable site URLs that survive ADR renames. +- Good, because Mermaid sequence diagrams in [ADR-0009](0009-auth-flow-oidc-pkce-msal-node.md) and the C4 diagrams in [architecture.md](../architecture.md) become readable without dev tooling — RSSI / new contributors can absorb them from a browser. +- Good, because the toolchain stays Node-only. Single `pnpm install` boots both the apps and the docs site. +- Good, because the site is decoupled: a docs deploy never threatens app uptime, and an app outage never blocks doc consultation. +- Good, because new ADRs auto-surface in the sidebar with no `config.ts` edit — keeps the "write ADR, push, browse it" loop frictionless. +- Bad, because we add a sub-1.0 dependency to the toolchain: `vitepress-plugin-mermaid` is community-maintained, not first-party. Mitigation: the plugin is a thin wrapper around Mermaid's official renderer; if it ever stalls, replacement with a direct `markdown-it-mermaid` integration is a half-day rewrite, not a regression. +- Bad, because client-side Mermaid rendering produces a brief flash on diagram-heavy pages. Mitigation: in scope today (5-6 diagrams total), revisit with build-time SVG pre-render if it becomes user-visible. +- Neutral, because we ship a tiny dependency on Vue (VitePress's runtime) even though the rest of the workspace is Angular. The Vue runtime ships _inside the docs site bundle only_; no impact on `portal-shell` / `portal-admin` bundles, no impact on `feature-auth` / `shared-*` libs. + +### Confirmation + +- The docs-site Gitea Actions workflow becomes a required check on PRs that touch `docs/**` — a syntactically broken Markdown or sidebar config fails the build before merge. +- The build produces `docs/.vitepress/dist/` deterministically; size is monitored manually for now (no Lighthouse-style budget — site is internal, content density beats bundle size). +- A smoke check in the workflow asserts that the generated HTML for [ADR-0009](0009-auth-flow-oidc-pkce-msal-node.md) and [architecture.md](../architecture.md) contains rendered Mermaid `` (or at minimum the plugin's `
` host) — guards against a future Mermaid plugin upgrade silently breaking the integration. + +## Pros and Cons of the Options + +### VitePress + `vitepress-plugin-mermaid` (chosen) + +- Good, because zero new toolchain (Vite already in the workspace). +- Good, because Markdown-first config, no MDX ceremony. +- Good, because used by the build-tool family we already trust (Vite, Vitest, Pinia, Vue Router). +- Good, because sub-second incremental dev builds via Vite HMR. +- Good, because the sidebar can mix hand-curated top-level groups with filesystem-walking sub-sections, scaling cleanly as ADRs grow. +- Bad, because Mermaid support is a community plugin (sub-1.0 dependency on the wrapper, though Mermaid itself is mature and the plugin is thin). +- Neutral, because we ship a Vue runtime in the doc site bundle. Internal site, no perceptible impact. + +### MkDocs Material + +- Good, because the gold-standard tech-docs theme — best polish out of the box, deepest plugin ecosystem (mermaid2, swagger, social cards, instant prev/next, etc.). +- Good, because Mermaid integration is documented in the Material handbook itself, zero ambiguity on setup. +- Good, because i18n + versioning are first-class if we ever need them. +- Bad, because adds **Python runtime** to CI — pinning version + installing the right `mkdocs-material` + extension stack on every PR. Permanent friction in an otherwise Node-only workspace. +- Bad, because the build is slower than VitePress on incremental changes (rebuilds the whole site on most edits). +- Neutral, because YAML config is fine but is a third config syntax to maintain alongside TypeScript + JSON. + +### Docusaurus 3 + +- Good, because Mermaid is **built-in** since v2 (`docusaurus.config.js`: one boolean flag). +- Good, because versioning + i18n + blog + MDX + search are all first-class. +- Bad, because the full feature set is overkill for the scope. Internal ADR site = pure Markdown, single-locale, no versioning, no blog — paying complexity tax for unused capabilities. +- Bad, because MDX-by-default makes simple Markdown edits subtly fragile (a stray `<` in prose becomes an error). Can be disabled, but at that point we're paying Docusaurus's footprint for VitePress's feature set. +- Bad, because the bundle/runtime ships React, adding a stack the workspace doesn't otherwise use. Unlike Vue-via-VitePress (a sub-dep that only fires during the docs build), React-via-Docusaurus is the docs surface itself — every site visit loads it. + +### Astro Starlight + +- Good, because Markdown-first like VitePress, plus first-class MDX if we ever want it. +- Good, because Astro's "islands" model produces fast, low-JS pages — better than client-rendered VitePress on pure-content sites. +- Good, because the theme is sober and doc-centric. +- Bad, because **young**: Astro 1.0 in 2022, Starlight 1.0 in 2024. The plugin ecosystem is smaller than VitePress/Docusaurus/MkDocs; for less-mainstream integrations we'd be more likely to hit "write your own". +- Bad, because Astro is a fourth framework in the workspace (Angular + React via Storybook later + Vue via VitePress + Astro). The tech-bar's "stable, recognized" lens flags this as worth deferring 12–18 months. +- Neutral, on Mermaid: plugin exists, comparable maturity to VitePress's. + +## More Information + +- Implementation lands in a single chantier directly after this ADR: `docs/.vitepress/config.ts`, `docs/index.md` (Hero layout), `.gitea/workflows/docs-site.yml`, `package.json` scripts (`docs:dev`, `docs:build`). +- The host (`docs.portal.apf.fr` or a sub-path) is **provisional** — the production hosting decision (Caddy config, TLS termination, IP-allowlist gating) follows the future infrastructure ADR. The doc site is internal-only by default; opening it to the public web is a separate decision that can be made later by flipping one Caddy rule. +- Related ADRs: + - [ADR-0001](0001-use-adrs-to-record-architectural-decisions.md) — choice of MADR + folder layout. This ADR builds on it by giving the rendered surface. + - [ADR-0015](0015-cicd-gitea-actions.md) — Gitea Actions as the CI platform; the docs-site workflow follows the same "thin YAML over portable scripts" convention. + - [ADR-0018](0018-environment-configuration-strategy.md) — for the hostname / per-environment config when the future infra ADR lands. +- Revisit triggers: + - If `docs/` content stops being pure Markdown (gain interactive widgets, embedded diagrams beyond Mermaid, …), reconsider whether VitePress's escape valves are enough or whether Astro Starlight's MDX-first model is a better fit. + - If Vue is genuinely a "fourth framework" maintenance cost (we hit a CVE chain that ripples through the doc site), the migration target is most likely MkDocs Material — at which point the Python cost is paid once, accepted as the price of removing Vue. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 999c32d..e12c49e 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -65,3 +65,4 @@ ADRs are listed in numerical order. To slice by topic, filter on the `Tags` colu | [0019](0019-internationalisation-angular-localize.md) | Internationalisation — `@angular/localize`, build-time per-locale bundles, `/fr` + `/en` path-based routing | accepted | `frontend`, `accessibility`, `performance`, `process` | 2026-05-11 | | [0020](0020-portal-admin-app.md) | `portal-admin` — dedicated SPA for portal administration, sharing the existing BFF | accepted | `frontend`, `backend`, `security`, `infrastructure`, `process` | 2026-05-11 | | [0021](0021-phase-2-security-baseline.md) | Phase-2 security baseline — helmet, CORS allowlist, double-submit CSRF, rate limiting, structured error envelope | accepted | `security`, `backend` | 2026-05-13 | +| [0022](0022-docs-site-vitepress.md) | Documentation site — VitePress + Mermaid plugin, separate static deployment | accepted | `process`, `infrastructure` | 2026-05-15 |