3ac408aca9bbed46592d474278ceb68646827cdf
280 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2b93710eaa |
chore(deps): update dependency vite to v8.0.14 (#202)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [vite](https://vite.dev) ([source](https://github.com/vitejs/vite/tree/HEAD/packages/vite)) | devDependencies | patch | [`8.0.13` -> `8.0.14`](https://renovatebot.com/diffs/npm/vite/8.0.13/8.0.14) | --- ### Release Notes <details> <summary>vitejs/vite (vite)</summary> ### [`v8.0.14`](https://github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small-8014-2026-05-21-small) [Compare Source](https://github.com/vitejs/vite/compare/v8.0.13...v8.0.14) ##### Features - update rolldown to 1.0.2 ([#​22484](https://github.com/vitejs/vite/issues/22484)) ([96efc88](https://github.com/vitejs/vite/commit/96efc88570b6a6ddf1a910f106920cbac07b3cf0)) ##### Bug Fixes - **deps:** update all non-major dependencies ([#​22471](https://github.com/vitejs/vite/issues/22471)) ([98b8163](https://github.com/vitejs/vite/commit/98b81632139d51820f82036e58d6fbbf122b77b3)) - **dev:** handle errors when sending messages to vite server ([#​22450](https://github.com/vitejs/vite/issues/22450)) ([e8e9a34](https://github.com/vitejs/vite/commit/e8e9a34dcf2540139de558a10187630884d10217)) - **html:** handle trailing slash paths in transformIndexHtml ([#​22480](https://github.com/vitejs/vite/issues/22480)) ([5d94d1b](https://github.com/vitejs/vite/commit/5d94d1bffdb2a15de9341194d89baec86ce1f693)) - **optimizer:** pass oxc jsx options to transformSync in dependency scan ([#​22342](https://github.com/vitejs/vite/issues/22342)) ([b3132da](https://github.com/vitejs/vite/commit/b3132dacea9c6e0cf526cd9f0f09d850f577c262)) ##### Miscellaneous Chores - **deps:** update rolldown-related dependencies ([#​22470](https://github.com/vitejs/vite/issues/22470)) ([7cb728e](https://github.com/vitejs/vite/commit/7cb728eb629cc677661f1bc52a044ffc0b87fc7f)) - remove irrelevant commits from changelog ([2c69495](https://github.com/vitejs/vite/commit/2c69495f250edf01132d4a20128de19dbe836086)) ##### Code Refactoring - **glob:** do not rewrite import path for absolute base ([#​22310](https://github.com/vitejs/vite/issues/22310)) ([0ae2844](https://github.com/vitejs/vite/commit/0ae2844ab6d6d1ccf78a2975b8132769fc35b302)) ##### Tests - **css:** sass does not use main field ([#​22449](https://github.com/vitejs/vite/issues/22449)) ([ebf39a0](https://github.com/vitejs/vite/commit/ebf39a04329ddc6ba765e006a5d463680a952270)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/202 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
ef5073de8a |
docs(adr-0025): authorization model — privileges × roles × scopes (#201)
## Summary
Proposes [ADR-0025](docs/decisions/0025-authorization-model-privileges-roles-scopes.md) — the portal's general-purpose authorization model. **Status: `proposed`.** No code in this PR; the goal is to lock the model and the v1 catalogues before the implementation chantier opens.
The model rejects stargate's linear hierarchy (`Admin ⊃ Directeur ⊃ RH ⊃ Collaborateur`) and adopts **three orthogonal axes**:
| Axis | What it carries | Source of truth |
|---|---|---|
| **Privileges** | Portal-level capabilities (`Portal.Admin`, future `Portal.Auditor`, …) | Entra app roles, `roles` claim |
| **Functional roles** | What someone does in APF (`rh`, `directeur-etablissement`, `elu-cd-tresorier`, …) | Entra security groups, `groups` claim → curated slug catalogue |
| **Scopes** | Where a role applies (`etablissement:0330800013`, `delegation:33`, `unrestricted`, …) | apf_portal-side `user_scopes` table (v1) ; future Pléiades feed |
The three axes compose at sign-in into a session-resident `Principal`. The portal's guards consume the structured shape; a deterministic projector flattens it to the `roles[]` list that `apf-ai-service` expects per [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md).
## What lands
- `docs/decisions/0025-authorization-model-privileges-roles-scopes.md` — full MADR with context, decision drivers, four considered options, exhaustive v1 catalogues, Entra-side configuration, `Principal` shape, AI-service projection, guard surface, ten test-tenant personas, consequences, confirmation criteria, pros/cons per option, ABAC migration path, related ADRs, and proposed follow-up ADRs.
- `docs/decisions/README.md` — index row for ADR-0025 (`proposed`, tags `security, backend, data`, 2026-05-20).
No `CLAUDE.md` update — ADR stays in `proposed` until reviewed; the accepted-ADRs roll-up at the top of `CLAUDE.md` stays at 0001 → 0024. Promotion to `accepted` lands in the same PR that ships the implementation skeleton (`libs/feature/auth` extension with `Principal` builder + catalogues).
## Highlights worth review focus
- **Privilege catalogue is intentionally minimal**: only `Portal.Admin` in v1. Anticipated future entries (`Portal.Auditor`, `Portal.SecurityOfficer`, `Portal.DPO`) are mentioned in the ADR but not formalised — each one rides an amendment ADR.
- **Functional-role catalogue is closed-set, 22 entries** grouped into workforce (15), governance (6), volunteer (2), external (1, placeholder). Adding a new slug requires an ADR amendment. The CI drift gate (proposed in §"Confirmation") asserts no orphan `@RequireRole('x')` literal in code.
- **Scope kinds are also closed-set**: `self`, `etablissement:<finess>`, `delegation:<dept>`, `region:<insee>`, `siege`, `unrestricted`. The `value` carriers are documented (FINESS code rather than internal `etablissement.id` because FINESS is stable across reorgs).
- **`Principal` shape** is the contract for everything downstream. Documented field-by-field. Built once at sign-in, persisted in the Redis session, refreshed on every authenticated request.
- **`PrincipalProjector` for the AI service** is mechanical: union of privileges + roles + scope-strings, no inclusive expansion. The projector is the only seam that knows about the flat shape; the rest of the portal never touches it.
- **Closed-vs-open catalogue trade-off** spelled out: the friction of "every new role rides an ADR amendment" is the price of "every slug in code is one a human approved". The drift CI gate enforces the discipline.
- **ABAC migration path** documented so a future contributor does not feel they must rewrite authorization to introduce a single Cedar/OPA-shaped rule.
## Test-tenant personas
The ADR proposes ten test users covering every interesting combination of the three axes. Table in §"Test-tenant personas" of the ADR; here's the summary the user can act on:
| Login | Privileges | Functional roles | Scopes |
|---|---|---|---|
| `admin@<tenant>` | `Portal.Admin` | `collaborateur`, `rh` | `unrestricted` |
| `directeur-bordeaux@<tenant>` | — | `directeur-etablissement`, `collaborateur` | `etablissement:0330800013` |
| `directeur-complexe@<tenant>` | — | `directeur-etablissement`, `collaborateur` | two `etablissement:*` scopes |
| `rh-aquitaine@<tenant>` | — | `rh`, `collaborateur` | `delegation:33` |
| `rh-siege@<tenant>` | — | `rh`, `responsable-paie`, `collaborateur` | `unrestricted` |
| `collab-simple@<tenant>` | — | `collaborateur` | `self` |
| `tresorier-bordeaux@<tenant>` | — | `elu-cd-tresorier`, `elu-cd` | `delegation:33` |
| `dpo@<tenant>` | — | `dpo`, `collaborateur` | `unrestricted` |
| `it@<tenant>` | — | `it`, `collaborateur` | `unrestricted` |
| `benevole-aquitaine@<tenant>` | — | `benevole`, `benevole-responsable` | `delegation:33` |
**Entra test-tenant setup the user needs to provision after acceptance**:
1. One app role: `Portal.Admin` (likely already there from the existing dev tenant; document the GUID in `apps/portal-bff/.env.test`).
2. **22 security groups**, one per functional-role slug in the catalogue: `apf-role-collaborateur`, `apf-role-chef-equipe`, `apf-role-chef-service`, `apf-role-directeur-etablissement`, `apf-role-directeur-territorial`, `apf-role-rh`, `apf-role-responsable-paie`, `apf-role-comptable`, `apf-role-juriste`, `apf-role-dpo`, `apf-role-rssi`, `apf-role-it`, `apf-role-formation`, `apf-role-qualite`, `apf-role-communication`, `apf-role-elu-ca`, `apf-role-elu-cd`, `apf-role-elu-cd-president`, `apf-role-elu-cd-tresorier`, `apf-role-elu-cd-secretaire`, `apf-role-delegue`, `apf-role-benevole`, `apf-role-benevole-responsable`, `apf-role-partenaire`.
3. The ten test users with the membership matrix above.
4. App registration manifest tweak: `groupMembershipClaims: 'SecurityGroup'` + `optionalClaims.idToken: [{ name: 'groups' }]` so the BFF sees the memberships in the ID token.
GUIDs and credentials stay in the operator's hands (out of git). When the user has provisioned the tenant, drop the GUIDs into `infra/test-tenant.entra.json` (gitignored) and the implementation PR wires `libs/feature/auth/src/lib/entra-group-to-role.ts` against them.
## Notes for the reviewer
- **Why not just extend stargate's `RoleMapper`.** The mapper's inclusive expansion (`Admin → [admin, directeur, rh, collaborateur]`) bakes in the wrong assumption — that roles form a chain. Reusing it would force every new role into the chain too. The three-axis model has no such forcing function.
- **Why `Person` is conspicuously absent here.** Authorization is keyed on the portal-side `User` account (Entra OID, session). The proposed `Person` + `User` split lands in a sibling ADR (proposed: ADR-0026) because the two decisions have different audiences — auth model is a backend/security concern; golden record is a data/domain concern. They will land in coordinated PRs.
- **Why FINESS rather than internal UUID for `etablissement:*` scopes.** FINESS codes are the canonical APF identifier for an établissement, stable across the internal-database churn (etablissement merges, reorgs, system migrations). Using the FINESS as the scope value means scope strings stay readable, debuggable, and stable when an établissement gets a new internal `id` after a Prisma migration.
- **Why no time-bound roles in v1.** APF does have interim assignments (acting Directeur for two months while the permanent one is on leave). The `user_scopes` table already has `expiresAt` to lay the groundwork; extending the *role* axis with time bounds is a future ADR amendment when a concrete use case lands.
- **Coordination with apf-ai-service.** The PrincipalProjector spec here matches exactly what `apf-ai-service`'s RBAC matrix tests expect (each chunk's ACL is a string-match against `Principal.roles[]`). The ADR explicitly notes that the projector is the only place that knows about the flat shape — keeping the AI-side contract honoured without polluting the portal-side guards.
## Test plan
- [x] `prettier --check docs/decisions/0025-authorization-model-privileges-roles-scopes.md` — passes (hook ran on commit).
- [x] Markdown links inside the ADR resolve (`0008`, `0009`, `0010`, `0011`, `0013`, `0020`, `0021`, `0024`, plus the proposed ADR-0026 / ADR-0027 placeholders).
- [x] Index row in `docs/decisions/README.md` follows the table's existing format.
- [x] No tag-vocabulary additions required — `security`, `backend`, `data` are all in the existing vocab.
- [ ] **Review focus** — the v1 catalogues (privileges + 22 functional roles + 6 scope kinds), the `Principal` shape, the projection contract for the AI service, and the ten test personas. Catalogue closures are deliberate; raising the lid requires an amendment so the v1 list deserves a careful pass.
## What's next (once accepted)
The implementation phasing recorded in the ADR's §"More Information":
1. **PR — types + Principal builder + Entra mapping skeleton**. Lands `libs/feature/auth/src/lib/authorization.types.ts` (catalogue constants), `entra-group-to-role.ts` (slug map), and the OIDC callback hook that extends `req.session.user` with `privileges` / `roles` / `scopes`. No new guards yet.
2. **PR — `@RequireRole` + `@RequireScope` decorators + guard tests**. Stub principal in unit tests; real session in e2e.
3. **PR — drift CI gate**. ESLint custom rule or `pnpm run` script: every `@RequireRole('...')` / `@RequirePrivilege('...')` / scope literal must exist in the catalogue constants.
4. **PR — test-tenant seed**. `prisma/seed.ts` populating the ten personas' `user_scopes` rows. Depends on the `Person` + `User` schema PR landing first.
In parallel, the user provisions the test tenant per the §"Test-tenant personas" instructions above.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #201
|
||
|
|
e389567a3c |
fix(ci): move grpc-tools to optionalDependencies (revert NODE_OPTIONS attempt) (#200)
## Summary The `NODE_OPTIONS=--use-system-ca` workaround merged in #199 did not clear the CI install failure — the runner still rejects the TLS chain to `node-precompiled-binaries.grpc.io`. Either the runner's OS CA bundle is missing the same intermediate Node's bundled set is missing, or `--use-system-ca` does not propagate down to the `node-fetch@2` that `node-pre-gyp` uses. Without runner shell access the exact reason is not worth chasing. Switch angle: **the protoc binary `grpc-tools` downloads is never used in CI**. The generated TypeScript stubs in `apps/portal-bff/src/grpc/gen/` are committed per [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md) §"Sub-decision 3 — vendored protos", so CI only needs to type-check them; protoc only runs when a developer regenerates from `apps/portal-bff/src/grpc/proto/apf-ai/*.proto`. This PR moves `grpc-tools` from `devDependencies` to `optionalDependencies`. Per pnpm semantics, when an optional dep's install (including postinstall) fails, pnpm logs a warning and the overall install completes. CI's `pnpm install --frozen-lockfile` therefore succeeds even when the binary cannot be fetched; developer machines (where TLS validates) install grpc-tools normally and `pnpm grpc:codegen` works unchanged. ## What lands - `package.json` — `grpc-tools: "^1.13.0"` moves out of `devDependencies` and into a new `optionalDependencies` block. The entry in `pnpm.onlyBuiltDependencies` stays — it controls whether pnpm *attempts* the postinstall, not whether failure is fatal. Local installs (where TLS works) still run the postinstall and download the binary. - `pnpm-lock.yaml` — refreshed; the lockfile reflects the new optional-dep classification. No version changes to anything else. - `.gitea/workflows/ci.yml` — revert the workflow-level `env: NODE_OPTIONS: --use-system-ca` block added in #199. Did not help; left in place it would be a misleading "this is supposed to fix CI TLS" signpost. - `.gitea/workflows/docs-site.yml` — same revert. ## Notes for the reviewer - **Why `optionalDependencies` is the right primitive.** pnpm 10 documents the contract: a package listed in `optionalDependencies` is *attempted*; if the install fails for any reason (platform mismatch, postinstall script error, network failure), the failure is logged and the overall command continues. That maps exactly to what this PR needs: try in CI, fail gracefully, succeed locally. - **Why not remove `grpc-tools` from `pnpm.onlyBuiltDependencies` instead.** Removing it from the allowlist tells pnpm not to even *try* the postinstall, which would also fix CI. But it would also break the developer workflow — locally, the protoc binary would never download, and `pnpm grpc:codegen` would fail. The dev would have to manually trigger the build (`pnpm approve-builds` then `pnpm rebuild grpc-tools`), or worse, devs would commit accidental `package.json` mutations from `approve-builds`. The `optionalDependencies` route keeps the local DX identical. - **Why revert the workflow `env:` blocks.** They do not help here, and leaving them in place implies that `--use-system-ca` is the canonical fix for this class of failure — which it is *not*, at least not for this runner / this CDN. If a future native dep hits a similar wall and `--use-system-ca` *does* fix it for that case, the env var lands in a focused PR with a real validation. Carrying it now as a "maybe useful later" workaround is noise. - **Codegen workflow on developer machines.** Unchanged. `pnpm install` runs the postinstall (TLS validates locally), the protoc binary lands in `node_modules/`, `pnpm grpc:codegen` regenerates stubs. The only visible difference is a one-line `WARN GET_RESOLVED_FROM_REGISTRY ...` if a contributor ever encounters the same TLS failure locally (e.g., on a corporate-proxied machine) — pnpm will mark grpc-tools as failed-optional and the rest of the install proceeds; the dev can then debug their own network without blocking the whole repo. - **Idempotence with CI's `--frozen-lockfile`.** The lockfile change is small (re-classifies grpc-tools from `dev` to `optional`) and lands in this PR. After merge, CI runs against the new lockfile; no further coordination needed. ## Test plan - [x] `pnpm install` locally — clean, grpc-tools postinstall runs successfully (TLS validates), protoc binary present. - [x] `pnpm grpc:codegen` — regenerates the TypeScript stubs identically (no diff in `apps/portal-bff/src/grpc/gen/`). - [x] `pnpm nx run-many -t lint test build -p portal-shell,portal-admin,portal-bff,shared-ui,shared-charts` — all five projects green. - [ ] **CI green on this PR's first run.** The validation that matters: `pnpm install --frozen-lockfile` completes despite grpc-tools' postinstall failure; `ci:check` runs to completion. - [ ] Spot-check of post-merge CI runs over the next few days to confirm the warning is recurring (expected) and the install never fails (the contract). ## What's next - If the CI behaviour is what this PR predicts (warning instead of failure), no further action required. - If `optionalDependencies` does not work as documented (highly unlikely, but possible if the pnpm version has a regression), the fallback is to drop `grpc-tools` from `pnpm.onlyBuiltDependencies` and add a small dev-onboarding note. One-line change away. - Long-term cleanup: if a future PR migrates the codegen step to a Docker-based tool (`buf`, system protoc) the optional-dep entry can be removed entirely. Out of scope here. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #200 |
||
|
|
219b7a2143 |
fix(ci): pass NODE_OPTIONS=--use-system-ca to clear grpc-tools tls install (#199)
## Summary The CI runner fails `pnpm install --frozen-lockfile` since the AI-relay chantier added `grpc-tools` to the dep tree. The package's postinstall downloads a precompiled `protoc` archive from `node-precompiled-binaries.grpc.io`, and Node 24's bundled CA set cannot verify the TLS chain on the runner network path. The fix follows the Node team's own remediation, surfaced verbatim in the error message: > unable to verify the first certificate; if the root CA is installed locally, try running Node.js with `--use-system-ca` Setting `NODE_OPTIONS=--use-system-ca` at the workflow `env:` block makes Node consult the OS CA store in addition to its bundled set. The runner image (`catthehacker/ubuntu:act-22.04` per [ADR-0015](docs/decisions/0015-cicd-gitea-actions.md)) carries the standard Ubuntu `ca-certificates` bundle, which validates the chain. ## What lands `.gitea/workflows/ci.yml`: ```yaml env: NODE_OPTIONS: --use-system-ca ``` `.gitea/workflows/docs-site.yml`: same one-line block. Both placed at workflow scope so every Node process (pnpm itself + every postinstall it spawns) inherits the option without per-step duplication. No package.json change. No code change. No runner-image change. ## Notes for the reviewer - **Why not skip the grpc-tools postinstall in CI instead.** The protoc binary downloaded here is never invoked in CI — the generated TypeScript stubs in `apps/portal-bff/src/grpc/gen/` are committed (per ADR-0024 §"Sub-decision 3 — vendored protos"), so CI only needs to type-check them. Removing `grpc-tools` from `pnpm.onlyBuiltDependencies` would also clear the failure and is arguably more minimal in CI. The trade-off: developers who update protos would have to opt back into the postinstall (`pnpm approve-builds grpc-tools && pnpm rebuild grpc-tools`) before running `pnpm grpc:codegen`. `--use-system-ca` keeps the developer workflow identical and fixes the underlying TLS-chain issue for any future native dep that hits the same wall (a real risk as the dep tree grows). The cost is a single workflow env var. - **Why workflow-scope `env:` rather than per-step `env:`.** Five separate `pnpm install` steps run across `ci.yml`; setting it five times would invite drift. The `env:` block at workflow scope applies to every step in every job — clean, single source of truth. - **Node 24 compatibility.** `--use-system-ca` is documented since Node 22; Node 24 (the workspace LTS per `.nvmrc`) carries it. No `.nvmrc` change required. - **Local-dev impact.** None. Local developers' Node already validates the chain (the issue is specific to the runner's network path); the env var is a no-op locally. - **Forward compatibility.** If a future native dep fetches binaries from a different CDN with a similar chain issue, this fix covers it without further changes. ## Test plan This PR cannot be locally validated in a way that reproduces the failure — the CI runner's network path is the only environment where `node-precompiled-binaries.grpc.io` cannot be reached with Node's bundled CA. The validation is the next CI run. - [ ] **CI green** on this PR's first run — `pnpm install --frozen-lockfile` completes; `pnpm ci:check` runs to completion. - [ ] If CI still fails: the runner's Ubuntu CA bundle does not contain the missing intermediate either. Fallback path: drop `grpc-tools` from `pnpm.onlyBuiltDependencies` so the postinstall is skipped entirely in CI (and document the dev-side `pnpm approve-builds` step). Trivial follow-up if needed. ## What's next If `--use-system-ca` fixes the CI install and no other Node TLS issues surface, no further action is required. The env var stays as a forward-looking guard. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #199 |
||
|
|
0d5ce1e153 |
fix(deps): update dependency @azure/msal-node to v5.2.2 (#198)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@azure/msal-node](https://github.com/AzureAD/microsoft-authentication-library-for-js) | dependencies | patch | [`5.2.1` -> `5.2.2`](https://renovatebot.com/diffs/npm/@azure%2fmsal-node/5.2.1/5.2.2) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #198 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
2772a918c2 |
feat(portal-shell): chatbot widget — SSE-bridged AI assistant with full a11y uplift (#197)
## Summary Final piece of the AI relay chantier opened with [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md): a chatbot widget on `portal-shell` that consumes the BFF's `POST /api/ai/chat` SSE endpoint (shipped in #196). Built fresh against the WCAG 2.2 AA + targeted AAA bar set by [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md) rather than transcribing the stargate POC's React widget — the POC's drag UX, absent `aria-live`, and minimal screen-reader contract did not meet that bar. The widget renders as a floating launcher bottom-right; clicking opens a dialog panel that can be toggled into fullscreen; sending a message streams the assistant's reply token-by-token; citations render as inline footnote chips with an extracted side panel for the snippet/source/score detail. ## What lands ### Files ``` apps/portal-shell/src/app/features/chatbot/ ├── chatbot-host.ts Standalone component, mounted in app.html via @defer ├── chatbot-host.html Launcher + panel + suggestions + messages + form ├── chatbot-host.scss 7.6 KB compiled — under the new 8 KB budget ├── chatbot-host.spec.ts 12 cases: launcher / panel / log / input / citations ├── chatbot-citation-panel.ts Extracted sibling component so the host stays under budget ├── chatbot-citation-panel.html dialog with metadata + snippet, role=dialog aria-modal=false ├── chatbot-citation-panel.scss 2.6 KB ├── chatbot.service.ts Signals-based state (view / messages / streaming / citation) ├── chatbot.service.spec.ts 10 cases: view, send/stop, citations, errors ├── chatbot-api.service.ts fetch + ReadableStream + CSRF cookie + AbortSignal ├── chatbot-api.service.spec.ts 5 cases: request shape, parsing, errors ├── sse-parser.ts Pure ReadableStream<Uint8Array> → AsyncIterable<{event,data}> ├── sse-parser.spec.ts 8 cases: LF / CRLF / split chunks / trailing / JSON / passthrough └── chatbot.types.ts UI types decoupled from the proto-derived BFF types ``` Plus the shell-side glue: - `apps/portal-shell/src/app/app.html` — `<app-chatbot-host />` mounted as a sibling of `<app-footer />`, wrapped in `@defer (on idle)` so the widget bundle (~27 KB lazy chunk) does not weigh on the initial paint. - `apps/portal-shell/src/app/app.ts` — `ChatbotHost` added to `imports`. - `apps/portal-shell/src/app/app.spec.ts` — `AUTH_CSRF_COOKIE_NAME` provider added to keep the shell smoke test compiling now that the SPA renders the chatbot. - `apps/portal-shell/src/locale/messages.fr.xlf` — 19 new `@@chatbot.*` trans-units covering the trigger / title / fullscreen toggle / suggestions / input / streaming / errors / citation panel. - `apps/portal-shell/project.json` — `anyComponentStyle` budget raised from `5/6 KB` to `6/8 KB` to match `portal-admin`'s posture (the audit page hit the same wall). - `libs/shared/ui/src/lib/icon/icon.ts` — 6 new icons in the registry: `maximize-2`, `message-circle`, `minimize-2`, `send`, `square`, `x`. ### Accessibility decisions (per ADR-0016) | Decision | Why | |---|---| | **No drag**. Fixed bottom-right launcher + fullscreen toggle. | Stargate's mouse-only drag had no keyboard equivalent. Keyboard parity is the project's bar; drag is the wrong primitive to inherit. | | **`role="dialog"` + `aria-modal="false"`** (non-modal). | The page chrome stays operable behind the panel; no focus trap, no `inert` toggle on `<main>`. Closing returns focus to the launcher via a `viewChild` + `effect`. | | **`role="log"` + `aria-live="polite"` + `aria-relevant="additions"`** on the message container. | Screen readers announce the assistant's incoming tokens without re-reading the whole history. Used a `<div>` + `<article>` children — `role="log"` is not allowed on `<ol>` / `<ul>` per ARIA. | | **Typing dots `aria-hidden="true"`**, paired with a `<span class="sr-only">{{ streamingLabel }}</span>`. | The visual signal is decorative; the SR signal is textual. Animation gated by `@media (prefers-reduced-motion: reduce)`. | | **Stop button visible during streaming**. | Wired to `AbortController` → `ChatClient` → upstream LLM cancel (the cancel chain shipped in #195). | | **Inline `role="alert"`** on per-message error banners. | Errors are part of the conversation log, not page-level interruptions; assertive announcement keeps them perceivable without yanking focus. | | **44 × 44 px touch targets everywhere**. | Launcher (3.5 rem), header chrome buttons, send / stop, citation chips, suggestion buttons. ADR-0016 baseline. | | **Citations as inline footnotes** (`[1]`, `[2]`, …) + side panel with `source` / `score` / `snippet`. | Validated in the design check before implementation. Side panel extracted into its own component so the host stays under the SCSS budget. | | **`prefers-reduced-motion` gating** on launcher transitions, suggestion hover, action button transitions, typing-dots animation. | Standard motion-preferences contract. | | **i18n via `$localize`** with the `@@key` catalogue convention per [ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md). | FR strings tagged at source; translations added to `messages.fr.xlf` (build fails otherwise — `i18nMissingTranslation: error`). | ### Streaming + cancellation The browser-side flow: 1. SPA submits a prompt → `ChatbotService.send(prompt)`. 2. Service appends a user message + empty assistant placeholder, sets `isStreaming = true`. 3. `ChatbotApiService.openChatStream(...)` opens a `POST` with `Accept: text/event-stream`, `credentials: 'include'`, `X-CSRF-Token` from the `__Host-portal_csrf` cookie, and an `AbortSignal`. 4. The response body's `ReadableStream<Uint8Array>` is parsed frame-by-frame by `sse-parser.ts` and yielded as `AsyncIterable<{event, data}>`. 5. The state service routes each frame: `token` appends to the assistant message, `citation` accumulates with a 1-based index, `error` marks the message as failed, `done` terminates the stream. 6. Cancellation: clicking Stop, navigating away, or any unhandled error fires `AbortController.abort()` → fetch terminates → upstream gRPC call is cancelled → AI service stops the LLM. Persistence: **session-ephemeral** by design. A SPA reload starts a fresh conversation. Matches the AI service's v1 posture (no per-user conversation table) and avoids the GDPR question of storing free-form transcripts before a consent flow lands. ### Native `fetch` + manual CSRF `HttpClient` buffers responses; native `fetch().body` is the only way to consume the stream incrementally. As a consequence, the project's `bffCredentialsInterceptor` + `csrfInterceptor` do not run on this call. The service handles both concerns manually: - `credentials: 'include'` is set explicitly so `__Host-portal_session` travels. - The `__Host-portal_csrf` cookie is read via `document.cookie` (it is intentionally not `HttpOnly` per [ADR-0009](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md) §"CSRF defense") and echoed in the `X-CSRF-Token` header. The cookie name + BFF base URL come from the same `AUTH_*` injection tokens the interceptors use, so the wire contract stays single-sourced. ## Notes for the reviewer - **Why ChatbotCitationPanel is its own component.** Initial draft put the side panel inside `chatbot-host.scss`, which crossed the `anyComponentStyle` 6 KB error budget. Two paths to fix: raise the budget, or extract. Did both — the panel is genuinely its own dialog with its own ARIA contract, and the host stays under budget. The budget bump (5/6 → 6/8 KB) brings portal-shell in line with portal-admin where the same wall was hit on the audit page. - **Why `@defer (on idle)` rather than `@defer (on viewport)` or eager.** Eager pushed the initial main bundle from ~282 KB to ~315 KB — past the 300 KB budget. The widget is not critical path on first paint, so deferring is correct on its own terms. `on idle` ensures it loads as soon as the browser is idle, so the launcher is interactive within a second on a fresh load. `on viewport` would have required the widget to be in the initial viewport, which the floating launcher is not consistently. - **Why `<article>` children rather than `<li>` inside `role="log"`.** axe-linter (and the underlying ARIA spec) reject `role="log"` on `<ol>` / `<ul>`. Switched to `<div role="log">` with `<article>` children; semantically each chat turn is a discrete piece of content, which is exactly what `<article>` is for. - **Why ChatbotService doesn't extend / re-use the existing `AuthService` patterns more directly.** Conversation state is ephemeral and not security-sensitive; the auth service's discriminated-union state machine is overkill for the toggle + buffer pattern the chatbot needs. The two services share the same `signal` / `computed` / `effect` idiom; that's enough consistency. - **Why hard-coded suggestions instead of pulling from a server.** v1 ships with four French suggestions tagged for translation; server-driven suggestions are a v2 step that requires a new endpoint and a personalisation question that v1 doesn't need to answer. The current shape moves to server-driven by replacing the array literal with an effect that fetches — single point of change. - **Tool-call event handling.** The SSE writer in #196 emits `event: tool-call` frames; the SPA parses them but does not render anything. v1 ships with an empty tool registry on the BFF side, so the AI service never emits them. When the first tool lands, the rendering UI is a follow-up PR. - **stargate-a11y-uplift memory.** The memory note `feedback_stargate_a11y_uplift.md` codifies the rule used here for future migrations from stargate: adapt, don't transcribe. The PR text under "Accessibility decisions" is the case study. ## Test plan - [x] `pnpm nx test portal-shell` — **85 specs pass** (was 58, +27 new across SSE parser / API service / state service / host component). - [x] `pnpm nx test shared-ui` — 10 specs pass (icon registry exhaustive-key spec picks up the 6 new icons). - [x] `pnpm nx lint portal-shell` — 0 errors. 5 pre-existing non-null assertion warnings in the new spec files are documented limits of vitest's mock typing. - [x] `pnpm nx build portal-shell` — clean. Main bundle 297.49 KB raw (under 300 KB budget). Chatbot lazy chunk: 27.61 KB raw / 6.49 KB transfer. SCSS: host 7.6 KB / citation panel 2.6 KB, both under the new 8 KB error budget. - [x] `pnpm nx run-many -t lint test build -p portal-shell,portal-admin,portal-bff,shared-ui,shared-charts` — five projects all green. - [ ] **Manual smoke** (requires the BFF wired to `apf-ai-service` per #196's plan): 1. `cd ../apf-ai-service && docker compose -f infra/docker-compose.yml up` to bring up the AI service. 2. `AI_SERVICE_GRPC_ENDPOINT=localhost:8080` in `apps/portal-bff/.env`, then `pnpm nx serve portal-bff` + `pnpm nx serve portal-shell`. 3. Sign in to the SPA, click the floating launcher bottom-right → panel opens, focus lands on the close button. 4. Pick a suggestion → user message appears right-aligned, assistant message streams in left-aligned with the typing dots animating (unless `prefers-reduced-motion` is on). 5. Click Stop mid-stream → the AI service log shows the gRPC call cancelled. 6. Press Escape with focus inside the panel → panel closes, focus returns to the launcher. 7. Toggle fullscreen → panel expands to `inset: 1rem`, ARIA contract unchanged. 8. Toggle dark mode → all themed surfaces switch via the CSS-variable swap in `chatbot-host.scss`; AA contrast still holds against the brand tokens. 9. Hit `/fr` and `/en` builds independently; suggestion labels swap between locales. ## What's next The AI relay chantier closes here. Pending follow-ups stay as written in #196: 1. **PR (post-v1)** — proto-drift CI gate diffing the BFF's vendored `proto/apf-ai/` against an upstream tag of `apf-ai-service`. 2. **Coordinated amendment** — when the first production deployment is in scope, both repos record the same prod-hardening choice (signed `Principal` envelope or mTLS) on the same date. 3. **Tool-call rendering** — UI surface for the `tool-call` SSE frame, once the BFF gains its first tool descriptor. 4. **Server-driven suggestions** — replace the four hard-coded prompts with an effect that fetches per-user suggestions from a future endpoint. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #197 |
||
|
|
883c5151de |
feat(portal-bff): ai-bridge controller — SSE chat + JSON rag/models (#196)
## Summary Step 3 of the AI-relay chantier (after #194 ADR and #195 client skeleton). Wires the BFF-side **live surface** that the SPA's future chatbot widget will consume. [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md) is promoted from `proposed` to `accepted` in the same change. Three end-user routes under `/api/ai/*`, gated by the active portal session (no `@RequireAdmin` — AI is a regular-user surface): | Route | Verb | Wire | Maps to | |---|---|---|---| | `/api/ai/chat` | `POST` | `text/event-stream` | `apf.ai.v1.ChatService.Chat` (server-stream) | | `/api/ai/rag/search` | `GET` | `application/json` | `apf.ai.v1.RagService.Search` (unary) | | `/api/ai/models` | `GET` | `application/json` | `apf.ai.v1.ModelsService.ListModels` (unary) | CSRF and session validation are delegated to the global middleware mounted in `main.ts` (per [ADR-0009](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md) and [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md)); the controller asserts `req.session.user` and emits 401 if absent. ## What lands ### `apps/portal-bff/src/grpc/ai-bridge/` ``` ai-bridge/ ├── ai-bridge.module.ts imports AiClientModule, exports the controller ├── ai-bridge.controller.ts 3 routes — POST chat (SSE), GET rag/search, GET models ├── sse.writer.ts ChatEvent oneof → SSE frame translator ├── sse.writer.spec.ts unit tests for the codec ├── ai-bridge.controller.spec.ts end-to-end against an in-process fake gRPC server └── dto/ ├── chat-request.dto.ts class-validator body shape (POST /chat) └── rag-search-query.dto.ts class-validator query shape (GET /rag/search) ``` ### SSE codec (`sse.writer.ts`) Each `ChatEvent` oneof case becomes one SSE frame with a kebab-case `event:` name and a JSON-encoded `data:` payload: ``` event: token data: {"token":"…","value":"…"} event: agent-step data: {"agent":"…","step":"…","stepId":"…"} event: tool-call data: {"callId":"…","name":"…","args":{…}} event: done data: {"stats":{"tokensIn":…,"tokensOut":…,"chunksRetrieved":…}} ``` A helper `relayErrorFrame(code, message, retriable)` synthesises a relay-side `event: error` frame that matches the AI service's own `ErrorEvent` shape — the SPA's renderer needs no second code path for relay-level failures vs upstream model errors. gRPC status codes map into the `urn:apf-ai:*` namespace (`UNAVAILABLE` → `urn:apf-ai:unavailable`, `DEADLINE_EXCEEDED` → `urn:apf-ai:timeout`, `PERMISSION_DENIED` → `urn:apf-ai:permission_denied`, `RESOURCE_EXHAUSTED` → `urn:apf-ai:rate_limited`, `INVALID_ARGUMENT` → `urn:apf-ai:invalid_argument`, anything else → `urn:apf-ai:relay_error`). The terminal `done` frame closes the stream — no `[DONE]` sentinel, per ADR-0024. ### Controller (`ai-bridge.controller.ts`) - `POST /api/ai/chat` — builds an `apf.ai.v1.ChatRequest` from the validated DTO + session-derived Principal, calls `ChatClient.chat()`, drains the `ClientReadableStream<ChatEvent>` into SSE frames written on the raw Express `Response`. `req.on('close', …)` propagates browser disconnect through an `AbortController` into `call.cancel()` so the upstream LLM stops (per `apf-ai-service/docs/streaming.md`). - `GET /api/ai/rag/search` — unary RAG call. `topK` defaults to 0 (server picks the default). `source` and `documentId` query params surface the same filter fields the upstream RPC accepts. - `GET /api/ai/models` — unary lookup of the provider catalogue. The SSE writes happen on the raw Express response (manual `setHeader` + `flushHeaders` + `write` + `end`) rather than through NestJS's `@Sse()` decorator, because `@Sse()` is GET-only and the chat endpoint is POST (the SPA carries the conversation history in the body). ### Lifecycle hooks `AiClientModule` now implements `OnApplicationShutdown` and closes the four gRPC stubs (Chat / Rag / Ingestion / Models). The four stubs share the same HTTP/2 channel (gRPC-js dedups on `endpoint + credentials`), so the `close()` calls are cheap, but kept explicit so adding a fifth stub later is an obvious one-line addition. `main.ts` now calls `app.enableShutdownHooks()` so `SIGTERM` / `SIGINT` / `SIGHUP` actually route through the lifecycle interface. ### DTOs `ChatRequestDto` constrains: - `messages` — 1 to 64 entries; each has `role ∈ {user, assistant, system}` (no `tool` — tool messages are constructed BFF-side per ADR-0024 §"Tool-dispatch contract") and `content` ≤ 16 KB. - `conversationId`, `model`, `provider` — optional, ≤ 64 / 128 chars. `RagSearchQueryDto`: - `query` — required, non-empty. - `topK` — optional, integer in `[1, 50]` (the AI service has its own cap; the BFF rejects out-of-range values early). - `source` / `documentId` — optional pass-through filters. ### Documentation - ADR-0024 frontmatter: `status: proposed` → `accepted`. - `docs/decisions/README.md` index reflects the new status. - `CLAUDE.md` Architecture section grows an "AI service relay" bullet; the roll-up line moves from "ADRs 0001 → 0023" to "0001 → 0024"; the shipped-on-main list grows an "AI relay surface" entry. - `apps/portal-bff/.env.example` documents `AI_SERVICE_GRPC_ENDPOINT` / `AI_SERVICE_CLIENT_ID` / `AI_SERVICE_GRPC_TLS` and points operators at `apf-ai-service`'s own docker-compose for the runtime dependency. ## Notes for the reviewer - **No live AI service in this PR's local-dev stack.** `apf-ai-service` runs from its own repo (`/home/jgautier/Works/apf-ai-service`) with its own `infra/docker-compose.yml`. The BFF dials `localhost:8080` by default — the host-published port of the AI service's container. This is option (a) from ADR-0024 §"Open question — Compose orchestration": two independent stacks, dial across via host networking. Merging the compose files into one would couple two release cadences without operational payoff. - **Tests run against an in-process fake `grpc.Server`.** All five spec cases on the controller wire it up against a fake `ChatService` + `RagService` + `ModelsService` server bound to `127.0.0.1:0` (random port). No mocks — the controller's gRPC client makes a real connection, real serialisation, real cancellation propagation. Cost: ~0.5 s overhead from the gRPC server setup. - **CSRF + session middleware are unchanged.** The new POST endpoint is protected by the existing double-submit CSRF middleware mounted in `main.ts` (per [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md)). The SPA's fetch call needs to send the `X-CSRF-Token` header matching the `__Host-portal_csrf` cookie — same protocol as every other POST in the BFF. No per-controller wiring required. - **Manual session check rather than a guard.** Three reasons: (1) matches the existing pattern in `me.controller.ts`; (2) the session check is the only authorization gate (no roles to evaluate) — a guard would add ceremony without payoff; (3) the SSE controller already takes control of the response object (`@Res()`), which `UseGuards` interacts with awkwardly. Throwing `UnauthorizedException` lets `StructuredErrorFilter` produce the 401 envelope before any header is flushed. - **Why the controller does NOT use `@Sse()`.** NestJS's `@Sse()` decorator is GET-only and emits frames from `Observable<MessageEvent>`. The chat endpoint is POST (the SPA sends conversation history in the body) and the source is a Node `Readable` stream from `@grpc/grpc-js`. Manual response handling is simpler than adapting to / from `Observable` for a single consumer. - **Cancellation contract.** When the SPA aborts the fetch, the browser closes the TCP connection, Express emits `'close'` on the request, the controller's `AbortController.abort()` triggers, `ChatClient` calls `.cancel()` on the gRPC stream, the AI service's `ServerCallContext.CancellationToken` cancels the upstream LLM. The spec covers the `'close'` → server-side `cancelled` event end-to-end. - **No ingestion route in the BFF.** Per ADR-0024 §"Out of scope", v1 admin ingestion uses the `apf-ai-service/tools/Apf.Ai.Ingest/` CLI. A future PR adds the BFF endpoint when the admin "manage AI corpus" surface ships. `IngestionClient` remains in `AiClientModule` so that future PR is one new file, not a new module plus a new client. - **No bundle-size or perf surprise.** The BFF is a Node process, not a SPA chunk — bundle budgets don't apply. The gRPC channel is opened lazily on first call; idle BFFs incur no upstream TCP cost. ## Test plan - [x] `pnpm nx test portal-bff` — **461 specs pass** (was 443; +13 new: 8 SSE writer cases + 5 controller end-to-end cases against the in-process fake server). Worker-exit-leak warning persists from the gRPC server's slow shutdown — pre-existing pattern from PR #195; harmless. - [x] `pnpm nx lint portal-bff` — 6 pre-existing warnings, no new ones from the diff. - [x] `pnpm nx build portal-bff` — clean webpack compile. - [x] Module wiring: `AppModule` imports `AiBridgeModule`, which imports `AiClientModule`. Resolves cleanly through DI; the audit-side `HashUserIdService` is satisfied by `AiClientModule`'s local provider (per the rationale recorded in PR #195's `AiClientModule` docstring). - [ ] **Manual smoke** — bring up `apf-ai-service` from its own repo (`cd ../apf-ai-service && docker compose -f infra/docker-compose.yml up`), set `AI_SERVICE_GRPC_ENDPOINT=localhost:8080` in `apps/portal-bff/.env`, run `pnpm nx serve portal-bff`. Sign in to `portal-shell`, then in a terminal: ```bash curl --cookie-jar /tmp/portal-session http://localhost:3000/api/auth/login # follow Entra… curl -N \ -H 'Content-Type: application/json' \ -H 'X-CSRF-Token: <copied from cookie>' \ --cookie /tmp/portal-session \ -d '{"messages":[{"role":"user","content":"hello"}]}' \ http://localhost:3000/api/ai/chat ``` Expect a streamed SSE response terminated by an `event: done` frame. Verify `GET /api/ai/rag/search?query=test` returns a JSON response. Verify `GET /api/ai/models` lists the configured providers. ## What's next 1. **PR (frontend chantier)** — chatbot widget on `portal-shell` consuming the SSE endpoint. Will use `fetch` + `ReadableStream` parsing (not native `EventSource`, since POST is needed). Drag / fullscreen / suggestion UX carries forward from the stargate POC's `ChatbotWidget.tsx`. 2. **PR (post-v1)** — proto-drift CI gate that diffs `proto/apf-ai/` against an upstream tag of `apf-ai-service`. 3. **Coordinated amendment** — when the first production deployment is in scope, both repos record the same prod-hardening choice (signed `Principal` envelope vs mTLS) on the same date. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #196 |
||
|
|
9b7d16601d |
feat(portal-bff): ai-client skeleton — vendored protos + grpc client + Principal mapper (#195)
## Summary Step 2 of the AI-relay chantier (after [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md) merged in #194). Lands the BFF-side **skeleton** that talks to `apf-ai-service` over gRPC: vendored protos, generated TypeScript stubs, typed wrapper clients, Principal mapper, and metadata builder — all tested against an in-process fake gRPC server. **No HTTP route is exposed in this PR**; the SSE bridge (`POST /api/ai/chat`, `GET /api/ai/rag/search`, `GET /api/ai/models`) ships in the next PR. The skeleton is self-contained: `AiClientModule` is built but is NOT imported in `AppModule` yet. The BFF runtime is byte-for-byte unchanged. Everything below exists for the next PR to wire into a controller. ## What lands ### Proto vendoring + codegen - `apps/portal-bff/src/grpc/proto/apf-ai/` — mirror of `apf-ai-service/contract/proto/` (common, chat, rag, ingestion, models). Both the `.proto` files and the regenerated `ts-proto` output under `grpc/gen/` are committed for hermetic builds and reviewable diffs (per ADR-0024 §"Sub-decision 3"). - `pnpm run grpc:codegen` — regenerates the stubs via `grpc-tools`' bundled `protoc` and the `ts-proto` plugin (`outputServices=grpc-js, esModuleInterop, forceLong=long, useOptionals=messages, exportCommonSymbols=false`). - `pnpm run grpc:sync` — copies the vendored `.proto` files from the sibling `apf-ai-service` working tree (`../apf-ai-service/contract/proto/`); developer convenience, never invoked from CI. Errors with an actionable message when the sibling tree is not where it expects. - Generated tree (`grpc/gen/**`) excluded from Prettier (`.prettierignore`) and ESLint (`eslint.config.mjs` ignores). Hand-rules apply to wrappers under `ai-client/`, not to codegen output. ### Dependencies - `@grpc/grpc-js@^1.13.0` — runtime gRPC client. - `@bufbuild/protobuf@^2.10.2` — wire codec used by `ts-proto`'s emitted code. - `long@^5.2.3` — int64 representation for proto Long fields (`forceLong=long`). - `ts-proto@^2.7.0` — devDep, TypeScript codegen plugin. - `grpc-tools@^1.13.0` — devDep, ships `protoc` + the gRPC plugin; added to `pnpm.onlyBuiltDependencies` so the postinstall binary download runs. ### Env validator `apps/portal-bff/src/config/check-ai-service-config.ts` follows the same posture as the other validators (per ADR-0018 §"BFF env-var loading"): small, per-key, runs at module init, throws with an actionable message on misconfiguration. Three vars: | Var | Mandatory | Purpose | |---|---|---| | `AI_SERVICE_GRPC_ENDPOINT` | yes | `host:port` reachable from the BFF — `apf-ai-service:8080` in dev Compose, service DNS + 443 in prod | | `AI_SERVICE_CLIENT_ID` | yes | Deployment slug propagated as the `x-client-id` metadata. Convention: `apf-portal-<env>` | | `AI_SERVICE_GRPC_TLS` | no (default `true`) | `false` for h2c in dev, `true` for h2 + TLS in prod | 7 spec cases lock the validation contract end-to-end. ### `AiClientModule` `apps/portal-bff/src/grpc/ai-client/` houses: - **`tokens.ts`** — DI tokens (`AI_CONFIG`, `AI_CREDENTIALS`, one per generated stub). - **`principal.mapper.ts`** — `PrincipalMapper.fromInputs({oid, tid, roles, extraAttributes})` returns the proto `Principal`. `subject` is hashed via `HashUserIdService` (the **same** salt + algorithm the audit writer uses) so `Principal.subject` matches `audit.events.actor_id_hash` byte-for-byte. `roles` passes through verbatim — inclusive expansion lands with a future role-hierarchy ADR; the wire contract on the AI side is unchanged. - **`grpc-metadata.builder.ts`** — stamps every outbound call with `x-client-id` (from config) and `x-correlation-id` (active OTel span's trace-id when present, else explicit override, else fresh UUID). - **`chat.client.ts`** — server-stream wrapper around `ChatServiceClient`. Returns the raw `ClientReadableStream<ChatEvent>` (Node `Readable` is async-iterable so the SSE bridge consumes with `for await`). Optional `AbortSignal` propagates browser disconnect to `call.cancel()`. - **`rag.client.ts`**, **`models.client.ts`**, **`ingestion.client.ts`** — unary promisified wrappers. `IngestionClient` is unused in v1 (the admin "manage AI corpus" surface lands later) but wired now so the future controller is one new file, not a new module. - **`ai-client.module.ts`** — NestJS module wiring the providers. `HashUserIdService` is declared locally rather than imported via `AuditModule` (the global module) to keep the module unit-testable in isolation; runtime equivalence is guaranteed by the service being a pure function of `LOG_USER_ID_SALT`. ### Tests 5 new spec files, 18 new test cases. All run against in-process fake gRPC servers; no network, no live `apf-ai-service`: - `check-ai-service-config.spec.ts` — 7 cases (happy path + 6 rejection branches). - `principal.mapper.spec.ts` — 7 cases including the cross-service hash-stability invariant and the `tenantId` reserved-key contract. - `grpc-metadata.builder.spec.ts` — 5 cases covering all three correlation-id resolution paths and metadata immutability. - `chat.client.spec.ts` — 4 cases: happy-path stream, metadata propagation, mid-stream cancel via AbortSignal, pre-aborted signal. - `rag.client.spec.ts` — 2 cases: unary happy path, `ServiceError` propagation. - `ai-client.module.spec.ts` — 4 cases: module bootstrap, all four wrappers resolved, env-driven `AI_CONFIG`, shared credentials across stubs. **Total BFF spec suite: 443 → 461 (after merge accounting), all passing.** ## Notes for the reviewer - **Why both `.proto` and generated `.ts` are committed.** Hermetic builds. Reviewers see both sides of a contract change in one diff. CI never runs codegen — drift between proto and stub would otherwise hide behind a successful build. The drift gate that compares the vendored copy against an upstream tag of `apf-ai-service` is the post-v1 follow-up listed in ADR-0024's "What's next". - **`HashUserIdService` declared locally in `AiClientModule`.** Two instances of the service exist when both `AuditModule` (global) and `AiClientModule` are wired into `AppModule`. The cost is one extra constructor call at bootstrap; the value is full test isolation of `AiClientModule` and a self-contained Principal-mapping boundary. The cross-service hash join invariant from ADR-0013 still holds because the service is a pure function of the `LOG_USER_ID_SALT` env var. - **`AiClientModule` is NOT imported by `AppModule`.** Deliberately. The skeleton compiles, type-checks, and unit-tests cleanly with no runtime side effects. The next PR (SSE bridge controller) adds the `imports: [AiClientModule]` line + the controller, in one focused change. - **`ts-proto` flat-oneof emission.** `ChatEvent` is generated with optional siblings (`token?`, `citation?`, `done?`, …) rather than a discriminated union (`$case: 'token'`). The flatter shape composes more naturally with the SSE writer the next PR will introduce (`event:` field name maps directly to the populated sibling). - **Cancellation test deliberately relaxed.** The "AbortSignal already aborted before call dial" test asserts the client-side outcome (no payload, error or clean end) but not server-side observation. gRPC-js may or may not propagate a cancel frame depending on whether the call had time to dial — both outcomes are correct per the contract; only the absence of payload matters. - **Lifecycle (`onApplicationShutdown`) deferred.** The module does not close the gRPC channel on shutdown today. Process termination closes the sockets via OS-level descriptor reclaim — sufficient for dev/preprod. The next PR wires the module into `AppModule` and adds an explicit Nest lifecycle hook in the same change (paired with `app.enableShutdownHooks()` in `main.ts`). ## Test plan - [x] `pnpm run grpc:codegen` — clean regeneration. Generated tree byte-identical to what's committed. - [x] `pnpm nx test portal-bff` — **443 specs pass** (was 425). - [x] `pnpm nx lint portal-bff` — clean. The eslint ignore for `grpc/gen/**` covers ts-proto's relaxed style; hand-written `ai-client/` files pass the project's full rule set. - [x] `pnpm nx build portal-bff` — clean, webpack-compiled. No bundle-size delta on the runtime artefact yet (the module is unimported). - [x] `pnpm install` — lockfile reconciled; `grpc-tools` postinstall fetches `protoc` from the precompiled-binaries mirror without errors on Linux x64. - [ ] **Manual smoke (next PR)** — once the SSE bridge ships, point `AI_SERVICE_GRPC_ENDPOINT` at the local `apf-ai-service` Compose service and run an end-to-end chat against the canned stub responses on the AI side. Not in scope for this PR. ## What's next 1. **PR — SSE bridge controller.** Wires `AiClientModule` into `AppModule`, adds `POST /api/ai/chat` (SSE), `GET /api/ai/rag/search`, `GET /api/ai/models`. Adds the `OnApplicationShutdown` hook + `enableShutdownHooks()`. Adds `apf-ai-service` to `infra/local/dev.compose.yml`. Promotes ADR-0024 from `proposed` to `accepted` and updates `CLAUDE.md`'s ADR roll-up. 2. **PR (frontend chantier)** — chatbot widget on `portal-shell` consuming the SSE endpoint. 3. **PR (post-v1)** — proto-drift CI gate diffing the vendored `proto/apf-ai/` against the upstream tag. 4. **Coordinated amendment** — when the first production deployment is in scope, both repos record the same prod-hardening choice (signed envelope or mTLS) on the same date. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #195 |
||
|
|
3f3f47317b |
docs(adr-0024): ai service relay — gRPC dial + SSE bridge + POC principal (#194)
## Summary
Proposes [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md) — the integration contract between `apf_portal`'s BFF and the sibling `apf-ai-service` repository. The ADR bundles four tightly-coupled sub-choices: the wire transport between BFF and AI service, the wire transport between BFF and SPA for chat streaming, how the protos reach the BFF, and how user identity travels across the boundary in v1. **Status: `proposed`.** No code lands in this PR — the goal is to lock the contract before the implementation chantier starts.
The chosen design:
| Boundary | Choice |
|---|---|
| BFF ↔ AI service | Native **gRPC HTTP/2** via `@grpc/grpc-js`, h2c in dev / h2 + TLS in prod |
| BFF ↔ SPA (chat) | **`text/event-stream`** — one SSE frame per `ChatEvent` oneof case |
| BFF ↔ SPA (unary) | Plain JSON endpoints for `RagService.Search` + `ModelsService.ListModels` |
| Proto distribution | **Vendored** into `apps/portal-bff/src/grpc/proto/apf-ai/`, `ts-proto` codegen on demand, both `.proto` + generated `.ts` committed |
| Identity (POC) | **Unsigned `Principal { subject, roles[], attributes{} }`** in the proto body — mirrors `apf-ai-service`'s ADR-0010 |
| Production hardening | Choice between signed envelope and mTLS — **explicitly deferred** until first production deployment is in scope |
## What lands
- `docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md` — new MADR-formatted ADR with the four sub-choices, decision drivers, considered options, consequences, confirmation criteria, open production-hardening question, and the related-ADRs map.
- `docs/decisions/README.md` — one new index row for ADR-0024 (`proposed`, tags `backend, security, observability`, 2026-05-19).
No source-code changes. No `CLAUDE.md` update — the ADR stays in `proposed` until reviewed, so the accepted-ADRs roll-up at the top of `CLAUDE.md` stays at 0001 → 0023. Promotion to `accepted` lands in the same PR that ships the first implementation chantier (proto vendor + `AiClientModule`), at which point `CLAUDE.md` gets the "0024 accepted" line.
## Notes for the reviewer
- **Why bundle four sub-choices in one ADR rather than four.** They couple tightly: the SPA-facing transport choice depends on the BFF-facing transport choice (gRPC-Web from the browser would dissolve the bridge layer entirely); the auth posture depends on having identity travel in the proto body (vendoring a different contract would change that); the proto-distribution choice depends on the contract being stable enough to vendor (a churning OpenAPI spec would push toward an SDK package). Splitting would force cross-ADR coordination on every revision. The ADR keeps a separate "Sub-choice" section per topic so each one stays reviewable on its own.
- **Out of scope deliberately.** The chatbot UI lives in a future frontend chantier; the role mapper (Entra groups → inclusive-expanded `roles[]`) is a separate proposed ADR; the ingestion-through-BFF path waits for the admin app's "manage AI corpus" surface; tool dispatch is wired but exercised against an empty registry in v1.
- **Hash-salt coordination is the one operational gotcha.** The same `HashUserIdService` salt has to land in both repos' deployment config so `apf-ai-service.audit_log.actor_id_hash` and `apf_portal.audit.events.actor_id_hash` produce identical values. Recorded as an open item in the ADR's "More Information" section; the deployment doc that distributes the secret is a v1-launch deliverable.
- **`apf-ai-service` cross-reference**. The ADR references `apf-ai-service/docs/adr/ADR-0010` (POC unsigned principal) and `apf-ai-service/docs/adr/ADR-0011` (mono-transport gRPC) as upstream anchors. Both are already accepted on the AI side. The "production hardening" decision will be a coordinated amendment in both repos on the same date.
- **No `DownstreamApiClient` (ADR-0014) reuse.** The OBO pattern in ADR-0014 targets *Entra-protected* downstreams that validate the user's access token. `apf-ai-service` is not Entra-protected — it accepts an unsigned Principal proto. The ADR explicitly calls this out so the reader does not expect symmetry with the Entra-protected downstream path.
- **Phasing recorded in the ADR's "More Information" section.** This PR is step (1) "ADR accepted". Steps 2–5 are separate PRs in order: client skeleton → bridge controller → frontend chatbot → proto-drift CI gate.
## Test plan
- [x] `pnpm run --silent prettier --check docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md` — passes (hook ran on commit).
- [x] Markdown links inside the ADR resolve to existing files (`0005`, `0009`, `0010`, `0012`, `0013`, `0014`, `0017`, plus `CLAUDE.md`).
- [x] Index row in `docs/decisions/README.md` follows the table's existing format (column count, tag vocabulary, date format).
- [x] No tag-vocabulary additions required — `backend`, `security`, `observability` are all in the existing vocab.
- [ ] **Review focus** — the four sub-choices and the production-hardening deferral. Code chantier is gated on this PR's acceptance.
## What's next (once accepted)
1. **PR — proto vendor + codegen + `AiClientModule` skeleton** — vendors the protos, wires `ts-proto` codegen, sets up the NestJS module with the metadata interceptor and the Principal mapper, all tested against an in-process fake gRPC server. No live endpoint yet.
2. **PR — `ai-bridge` controller** — `POST /api/ai/chat` (SSE), `GET /api/ai/rag/search`, `GET /api/ai/models`, live against `apf-ai-service` in the dev Compose stack.
3. **PR (frontend chantier)** — the chatbot widget on `portal-shell` consuming the SSE endpoint.
4. **PR (post-v1)** — proto-drift CI gate that diffs the vendored copy against the upstream tag.
5. **Coordinated amendment** — when the first production deployment is in scope, both repos record the same prod-hardening choice (signed envelope or mTLS) on the same date.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #194
|
||
|
|
40d4f7d290 |
chore(deps): update dependency cytoscape to v3.33.4 (#193)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [cytoscape](http://js.cytoscape.org) ([source](https://github.com/cytoscape/cytoscape.js)) | devDependencies | patch | [`3.33.3` -> `3.33.4`](https://renovatebot.com/diffs/npm/cytoscape/3.33.3/3.33.4) | --- ### Release Notes <details> <summary>cytoscape/cytoscape.js (cytoscape)</summary> ### [`v3.33.4`](https://github.com/cytoscape/cytoscape.js/releases/tag/v3.33.4) [Compare Source](https://github.com/cytoscape/cytoscape.js/compare/v3.33.3...v3.33.4) Release version v3.33.4 </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #193 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
8bdfe0a273 |
chore(deps): update dependency ts-jest to v29.4.10 (#192)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ts-jest](https://kulshekhar.github.io/ts-jest) ([source](https://github.com/kulshekhar/ts-jest)) | devDependencies | patch | [`29.4.9` -> `29.4.10`](https://renovatebot.com/diffs/npm/ts-jest/29.4.9/29.4.10) | --- ### Release Notes <details> <summary>kulshekhar/ts-jest (ts-jest)</summary> ### [`v29.4.10`](https://github.com/kulshekhar/ts-jest/blob/HEAD/CHANGELOG.md#29410-2026-05-18) [Compare Source](https://github.com/kulshekhar/ts-jest/compare/v29.4.9...v29.4.10) ##### Bug Fixes - pass `resolutionMode` to `ts.resolveModuleName` for hybrid module support ([b557a85](https://github.com/kulshekhar/ts-jest/commit/b557a85f85c3fd34523ec3a15293afbdc9dea83c)) - rebuild `Program` when consecutive compiles need different module kinds ([a82a2b3](https://github.com/kulshekhar/ts-jest/commit/a82a2b32c4987a5249fd5284283117dd2fa3be47)), closes [#​4774](https://github.com/kulshekhar/ts-jest/issues/4774) - respect tsconfig `moduleResolution` instead of forcing `Node10` ([1bffffc](https://github.com/kulshekhar/ts-jest/commit/1bffffc667557c173ae0c1f93dd436920775dac4)) - **transformer:** transpile `mjs` files from `node_modules` for CJS mode ([96d025d](https://github.com/kulshekhar/ts-jest/commit/96d025dd912ea2bceb18b67d2d509ada7a756d9d)) - **transformer:** use a consistent comparator in hoist-jest sortStatements ([8a8fd2f](https://github.com/kulshekhar/ts-jest/commit/8a8fd2fb8446655bba18367db9306a1089490e62)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/192 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
90504dd32b |
chore(deps): update dependency postcss to v8.5.15 (#191)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [postcss](https://postcss.org/) ([source](https://github.com/postcss/postcss)) | devDependencies | patch | [`8.5.14` -> `8.5.15`](https://renovatebot.com/diffs/npm/postcss/8.5.14/8.5.15) | --- ### Release Notes <details> <summary>postcss/postcss (postcss)</summary> ### [`v8.5.15`](https://github.com/postcss/postcss/blob/HEAD/CHANGELOG.md#8515) [Compare Source](https://github.com/postcss/postcss/compare/8.5.14...8.5.15) - Fixed declaration parsing performance (by [@​homanp](https://github.com/homanp)). </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #191 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
8136695fa8 |
chore(deps): bump brace-expansion + ws overrides to clear audit (#190)
## Summary Clears the two moderate GHSA advisories that started failing `ci:audit`: - **`brace-expansion`** `>=5.0.0 <5.0.6` — DoS via large numeric range that bypasses the documented `max` protection ([GHSA-jxxr-4gwj-5jf2](https://github.com/advisories/GHSA-jxxr-4gwj-5jf2)). The existing override floor was at `5.0.5`; bumped one patch to `5.0.6`. - **`ws`** `>=8.0.0 <8.20.1` — uninitialized memory disclosure ([GHSA-58qx-3vcg-4xpx](https://github.com/advisories/GHSA-58qx-3vcg-4xpx)). No prior override; added one scoped to the 8.x advisory window only. Both are reached transitively through Nx; nothing in this repo's own dependency surface is vulnerable directly. ## What lands `package.json` (`pnpm.overrides` block): ```diff - "brace-expansion@<5.0.5": ">=5.0.5", + "brace-expansion@<5.0.6": ">=5.0.6", + "ws@>=8.0.0 <8.20.1": ">=8.20.1", ``` `pnpm-lock.yaml` reconciled with `pnpm install`. ## Notes for the reviewer - **Why the `ws` override is range-scoped, not a blanket `ws@<8.20.1`.** Lighthouse pulls `ws@7.5.10` via its own tree; the 7.x line sits entirely outside the advisory window (`>=8.0.0 <8.20.1`). A blanket `<8.20.1` override would force-bump that transitive across a major version boundary and risk Lighthouse breakage with no security gain. The range form `>=8.0.0 <8.20.1 → >=8.20.1` patches exactly the vulnerable consumers (`@module-federation/dts-plugin`, etc.) and leaves Lighthouse's pin untouched. - **Why an override and not waiting for Nx to bump.** Nx 22.7.2 is already on `main` (commits `bd94bb4` / `6b20c34`) but its sub-chain still resolves `ws@8.18.0` and `brace-expansion@5.0.5`. Upstream pickup will land eventually via Renovate; in the meantime the audit gate blocks CI on every PR. Overrides are the standard pnpm escape hatch for this exact situation. The pattern is already used in this file (`axios`, `ajv`, `esbuild`, `follow-redirects`, `ip-address`, `protobufjs`, `tmp`, `yaml`). - **Pruning policy.** Once Nx ships a release whose `pnpm-lock.yaml` resolves both packages at or above the patched versions on its own, both override entries can be removed. The convention in this file's existing entries is to leave overrides in place even when redundant (cheap insurance against silent regressions); pruning is a separate sweep, not part of this fix. - **No application code touched.** Both packages live deep in the Nx/Module Federation build tooling — `brace-expansion` inside `minimatch`'s glob expansion, `ws` inside Module Federation's HMR dev socket. Neither surfaces in the BFF or SPA runtime bundles. Build + test + lint were re-run across `portal-shell`, `portal-admin`, `portal-bff`, `shared-charts`, `shared-ui` as a sanity check; all green. ## Test plan - [x] `pnpm install` — lockfile reconciled, no extraneous package churn. - [x] `pnpm audit --audit-level=moderate` — "No known vulnerabilities found" (replaces the two-row failure that started this PR). - [x] `pnpm nx run-many -t build test lint -p portal-shell,portal-admin,portal-bff,shared-charts,shared-ui` — all green. Module Federation's HMR socket (the surface that *uses* `ws`) is exercised implicitly by every Angular build via `@nx/angular`'s webpack pipeline. - [ ] **Manual smoke** — `pnpm nx serve portal-shell` + `pnpm nx serve portal-admin`: dev servers come up, HMR reloads on a trivial edit, no warnings or stack traces about `ws` or `brace-expansion` resolution. ## What's next - Renovate will eventually pick up an Nx release whose own sub-chain ships `ws@>=8.20.1` and `brace-expansion@>=5.0.6` natively. At that point, the two override entries here are dead weight and can be pruned as a follow-up sweep alongside any other stale overrides. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #190 |
||
|
|
aa61ea0e02 |
feat(portal-shell): ghost-style Sign-in button with log-in icon (#189)
## Summary The anonymous "Sign in" CTA in `portal-shell`'s header was a filled brand-primary block sitting next to two round icon-only buttons (Notifications, Help). The contrast was off — the filled rectangle visually dominated the row even though it's the *third* action in the strip. This PR turns it into a ghost-style button (no fill, no border) with a `log-in` icon ahead of the label, matching the quiet posture of its neighbours while still reading as the primary CTA for unauthenticated users. ## What lands `apps/portal-shell/src/app/components/header/header.html` — the anonymous-state button: | Aspect | Before | After | |---|---|---| | Fill | `bg-brand-primary-500` (filled) | `bg-transparent` (ghost) | | Text colour | `text-white` | `text-brand-primary-500` (`-300` in dark) | | Border | none | none — pure text-only style | | Hover | darker fill | light `bg-brand-primary-50` tint + `text-brand-primary-600` | | Icon | (none) | `<lib-icon name="log-in" [size]="16" aria-hidden="true" />` ahead of the label | | Padding / gap | `px-4 gap-2` | `px-3 gap-1.5` (slightly tighter, makes room for the icon without growing the chrome) | | Height | `h-11` | `h-11` (unchanged — 44 × 44 touch target per [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md) holds) | `libs/shared/ui/src/lib/icon/icon.ts` — adds the missing pair of the existing `log-out` icon: - Imports `LogIn` from `lucide-angular`. - Registers `'log-in': LogIn,` in the alphabetical registry between `'layout-dashboard'` and `'log-out'`. ## Notes for the reviewer - **Ghost vs. outline vs. filled — why ghost.** Tried two intermediate iterations during the design pass (outline with brand border, then a more compact outline). The user preferred the ghost rendering once we removed the border — the header strip is the right surface for an "always-quiet, surfaces on hover" CTA, since the user typically scans for the search bar first, not the auth state. Filled buttons are the right call inside content where the CTA *is* the focal point (forms, modals). - **Touch-target stays at 44 × 44.** `h-11` is kept on purpose. The CI a11y gate from ADR-0016 (`touch-target check (44×44 min)`) is non-negotiable for interactive controls; visually shrinking horizontal padding + reducing visual weight is the right way to "compact" a button without breaking the target rule. - **`aria-hidden="true"` on the icon.** The adjacent `<span>` carries the localised label, and the screen-reader contract is "the button announces 'Sign in', not 'log-in icon Sign in'". The icon is decorative reinforcement. - **Label still wrapped in `<span i18n="@@header.signIn">` rather than directly on the button.** Required because the button now contains both an icon child and the text — Angular's `i18n` on the button itself would extract the icon's rendered SVG into the translation unit, which is not what translators want to see. Wrapping the text isolates the translation unit cleanly. - **`log-in` belongs in `shared-ui`, not portal-shell.** Even though portal-shell is the only consumer today, the icon registry is by contract the single point of truth for both apps — `portal-admin`'s eventual sign-in surface will use the same icon, so registering it once in the shared lib is the right boundary. ## Test plan - [x] `pnpm nx test portal-shell` — green. The existing spec asserts `btn.textContent.trim() === 'Sign in'`; the `<lib-icon>` renders to an SVG (no text content) and the label is now inside a `<span>`, so the text-trim check still holds. - [x] `pnpm nx test shared-ui` — green. Icon registry's exhaustive-key spec picks up the new entry automatically (it iterates `Object.keys(ICON_REGISTRY)`). - [x] `pnpm nx build portal-shell` — clean, no bundle-size deltas worth flagging (`log-in` is tree-shaken alongside the rest of lucide-angular). - [x] `pnpm nx lint portal-shell shared-ui` — clean. - [ ] **Manual smoke** — `pnpm nx serve portal-shell`, signed-out, header visible: - Anonymous state: ghost "Sign in" button with the log-in icon. Hover surfaces a faint fill; focus shows the brand outline ring. - Switch to authenticated: button is replaced by `<lib-user-menu>` (unchanged). - `error` state: amber "Can't reach the server" badge (unchanged). - Toggle dark mode: text shifts to `brand-primary-300`, hover surfaces a `gray-800` fill; still readable. - Tab from the address bar into the header — focus order: search input → bell → help → Sign in. Focus ring on the ghost button matches the other icon buttons (`outline-brand-primary-500 outline-offset-2`). ## What's next - `portal-admin` will get the same `log-in` icon for its own (still skeleton) sign-in surface once that wiring lands — single shared registry means no further change here. - If the marketing folks ever ask the sign-in CTA to come back forward visually (festival days, post-incident push), the ghost class block can flip to a filled variant locally without touching the icon or i18n contract. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #189 |
||
|
|
3e120da668 |
chore(deps): update typescript tooling to v8.59.4 (#188)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@typescript-eslint/utils](https://typescript-eslint.io/packages/utils) ([source](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils)) | devDependencies | patch | [`8.59.3` -> `8.59.4`](https://renovatebot.com/diffs/npm/@typescript-eslint%2futils/8.59.3/8.59.4) | | [typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint) ([source](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)) | devDependencies | patch | [`8.59.3` -> `8.59.4`](https://renovatebot.com/diffs/npm/typescript-eslint/8.59.3/8.59.4) | --- ### Release Notes <details> <summary>typescript-eslint/typescript-eslint (@​typescript-eslint/utils)</summary> ### [`v8.59.4`](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/utils/CHANGELOG.md#8594-2026-05-18) [Compare Source](https://github.com/typescript-eslint/typescript-eslint/compare/v8.59.3...v8.59.4) This was a version bump only for utils to align it with other projects, there were no code changes. See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.59.4) for more information. You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website. </details> <details> <summary>typescript-eslint/typescript-eslint (typescript-eslint)</summary> ### [`v8.59.4`](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/typescript-eslint/CHANGELOG.md#8594-2026-05-18) [Compare Source](https://github.com/typescript-eslint/typescript-eslint/compare/v8.59.3...v8.59.4) ##### 🩹 Fixes - **typescript-eslint:** export Compatible\* types from typescript-eslint to resolve pnpm TS error ([#​12340](https://github.com/typescript-eslint/typescript-eslint/pull/12340)) ##### ❤️ Thank You - Kirk Waiblinger [@​kirkwaiblinger](https://github.com/kirkwaiblinger) See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.59.4) for more information. You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website. </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #188 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
5ef1e3d9bb |
chore(deps): lock file maintenance (#187)
This PR contains the following updates: | Update | Change | |---|---| | lockFileMaintenance | All locks refreshed | 🔧 This Pull Request updates lock files to use the latest dependency versions. --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #187 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
0e206d4f89 |
chore(deps): lock file maintenance (#186)
This PR contains the following updates: | Update | Change | |---|---| | lockFileMaintenance | All locks refreshed | 🔧 This Pull Request updates lock files to use the latest dependency versions. --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #186 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
93cf30679e |
fix(deps): update opentelemetry-js-contrib monorepo (#184)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@opentelemetry/instrumentation-document-load](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-document-load#readme) ([source](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-document-load)) | dependencies | minor | [`^0.62.0` -> `^0.63.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-document-load/0.62.0/0.63.0) | | [@opentelemetry/instrumentation-express](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-express#readme) ([source](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-express)) | dependencies | minor | [`^0.65.0` -> `^0.66.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-express/0.65.0/0.66.0) | | [@opentelemetry/instrumentation-ioredis](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-ioredis#readme) ([source](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-ioredis)) | dependencies | minor | [`^0.65.0` -> `^0.66.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-ioredis/0.65.0/0.66.0) | | [@opentelemetry/instrumentation-nestjs-core](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-nestjs-core#readme) ([source](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-nestjs-core)) | dependencies | minor | [`^0.63.0` -> `^0.64.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-nestjs-core/0.63.0/0.64.0) | | [@opentelemetry/instrumentation-pg](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-pg#readme) ([source](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-pg)) | dependencies | minor | [`^0.69.0` -> `^0.70.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-pg/0.69.0/0.70.0) | | [@opentelemetry/instrumentation-pino](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-pino#readme) ([source](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-pino)) | dependencies | minor | [`^0.63.0` -> `^0.64.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-pino/0.63.0/0.64.0) | | [@opentelemetry/instrumentation-user-interaction](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-user-interaction#readme) ([source](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-user-interaction)) | dependencies | minor | [`^0.61.0` -> `^0.62.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-user-interaction/0.61.0/0.62.0) | --- ### Release Notes <details> <summary>open-telemetry/opentelemetry-js-contrib (@​opentelemetry/instrumentation-document-load)</summary> ### [`v0.63.0`](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-document-load/CHANGELOG.md#0630-2026-05-13) [Compare Source](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/b68fb6dcc0649631ebecab7bde81879486f74f0b...15ef7506553f631ea4181391e0c5725a56f0d082) ##### Features - **deps:** update deps matching '@​opentelemetry/\*' ([#​3523](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3523)) ([e26a90a](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/e26a90af6e2fb4666b22388b770add7a60140c9b)) </details> <details> <summary>open-telemetry/opentelemetry-js-contrib (@​opentelemetry/instrumentation-express)</summary> ### [`v0.66.0`](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-express/CHANGELOG.md#0660-2026-05-13) [Compare Source](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/b68fb6dcc0649631ebecab7bde81879486f74f0b...15ef7506553f631ea4181391e0c5725a56f0d082) ##### Features - **deps:** update deps matching '@​opentelemetry/\*' ([#​3523](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3523)) ([e26a90a](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/e26a90af6e2fb4666b22388b770add7a60140c9b)) ##### Dependencies - The following workspace dependencies were updated - devDependencies - [@​opentelemetry/contrib-test-utils](https://github.com/opentelemetry/contrib-test-utils) bumped from ^0.64.0 to ^0.65.0 </details> <details> <summary>open-telemetry/opentelemetry-js-contrib (@​opentelemetry/instrumentation-ioredis)</summary> ### [`v0.66.0`](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-ioredis/CHANGELOG.md#0660-2026-05-13) [Compare Source](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/b68fb6dcc0649631ebecab7bde81879486f74f0b...15ef7506553f631ea4181391e0c5725a56f0d082) ##### Features - **deps:** update deps matching '@​opentelemetry/\*' ([#​3523](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3523)) ([e26a90a](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/e26a90af6e2fb4666b22388b770add7a60140c9b)) ##### Dependencies - The following workspace dependencies were updated - devDependencies - [@​opentelemetry/contrib-test-utils](https://github.com/opentelemetry/contrib-test-utils) bumped from ^0.64.0 to ^0.65.0 </details> <details> <summary>open-telemetry/opentelemetry-js-contrib (@​opentelemetry/instrumentation-nestjs-core)</summary> ### [`v0.64.0`](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-nestjs-core/CHANGELOG.md#0640-2026-05-13) [Compare Source](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/b68fb6dcc0649631ebecab7bde81879486f74f0b...15ef7506553f631ea4181391e0c5725a56f0d082) ##### Features - **deps:** update deps matching '@​opentelemetry/\*' ([#​3523](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3523)) ([e26a90a](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/e26a90af6e2fb4666b22388b770add7a60140c9b)) </details> <details> <summary>open-telemetry/opentelemetry-js-contrib (@​opentelemetry/instrumentation-pg)</summary> ### [`v0.70.0`](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-pg/CHANGELOG.md#0700-2026-05-13) [Compare Source](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/b68fb6dcc0649631ebecab7bde81879486f74f0b...15ef7506553f631ea4181391e0c5725a56f0d082) ##### Features - **deps:** update deps matching '@​opentelemetry/\*' ([#​3523](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3523)) ([e26a90a](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/e26a90af6e2fb4666b22388b770add7a60140c9b)) ##### Dependencies - The following workspace dependencies were updated - devDependencies - [@​opentelemetry/contrib-test-utils](https://github.com/opentelemetry/contrib-test-utils) bumped from ^0.64.0 to ^0.65.0 </details> <details> <summary>open-telemetry/opentelemetry-js-contrib (@​opentelemetry/instrumentation-pino)</summary> ### [`v0.64.0`](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-pino/CHANGELOG.md#0640-2026-05-13) [Compare Source](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/b68fb6dcc0649631ebecab7bde81879486f74f0b...15ef7506553f631ea4181391e0c5725a56f0d082) ##### Features - **deps:** update deps matching '@​opentelemetry/\*' ([#​3523](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3523)) ([e26a90a](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/e26a90af6e2fb4666b22388b770add7a60140c9b)) ##### Dependencies - The following workspace dependencies were updated - devDependencies - [@​opentelemetry/contrib-test-utils](https://github.com/opentelemetry/contrib-test-utils) bumped from ^0.64.0 to ^0.65.0 </details> <details> <summary>open-telemetry/opentelemetry-js-contrib (@​opentelemetry/instrumentation-user-interaction)</summary> ### [`v0.62.0`](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-user-interaction/CHANGELOG.md#0620-2026-05-13) [Compare Source](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/b68fb6dcc0649631ebecab7bde81879486f74f0b...15ef7506553f631ea4181391e0c5725a56f0d082) ##### Features - **deps:** update deps matching '@​opentelemetry/\*' ([#​3523](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3523)) ([e26a90a](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/e26a90af6e2fb4666b22388b770add7a60140c9b)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/184 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
ee59fd900c |
fix(deps): update opentelemetry-js monorepo (#183)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@opentelemetry/exporter-trace-otlp-http](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-http) ([source](https://github.com/open-telemetry/opentelemetry-js)) | dependencies | minor | [`^0.217.0` -> `^0.218.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fexporter-trace-otlp-http/0.217.0/0.218.0) | | [@opentelemetry/exporter-trace-otlp-proto](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-proto) ([source](https://github.com/open-telemetry/opentelemetry-js)) | dependencies | minor | [`^0.217.0` -> `^0.218.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fexporter-trace-otlp-proto/0.217.0/0.218.0) | | [@opentelemetry/instrumentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation) ([source](https://github.com/open-telemetry/opentelemetry-js)) | dependencies | minor | [`^0.217.0` -> `^0.218.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation/0.217.0/0.218.0) | | [@opentelemetry/instrumentation-fetch](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch) ([source](https://github.com/open-telemetry/opentelemetry-js)) | dependencies | minor | [`^0.217.0` -> `^0.218.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-fetch/0.217.0/0.218.0) | | [@opentelemetry/instrumentation-http](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http) ([source](https://github.com/open-telemetry/opentelemetry-js)) | dependencies | minor | [`^0.217.0` -> `^0.218.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-http/0.217.0/0.218.0) | | [@opentelemetry/sdk-node](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node) ([source](https://github.com/open-telemetry/opentelemetry-js)) | dependencies | minor | [`^0.217.0` -> `^0.218.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsdk-node/0.217.0/0.218.0) | | [@opentelemetry/semantic-conventions](https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions) ([source](https://github.com/open-telemetry/opentelemetry-js)) | dependencies | minor | [`1.40.0` -> `1.41.1`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsemantic-conventions/1.40.0/1.41.1) | --- ### Release Notes <details> <summary>open-telemetry/opentelemetry-js (@​opentelemetry/exporter-trace-otlp-http)</summary> ### [`v0.218.0`](https://github.com/open-telemetry/opentelemetry-js/compare/74cde1b674508ccc0ed2601ac43a80ff2d35114c...06ad0eaaecbd49f5ead871325f852cc2a3454079) [Compare Source](https://github.com/open-telemetry/opentelemetry-js/compare/74cde1b674508ccc0ed2601ac43a80ff2d35114c...06ad0eaaecbd49f5ead871325f852cc2a3454079) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #183 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
868cbb6747 |
chore(deps): update dependency @playwright/test to v1.60.0 (#182)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@playwright/test](https://playwright.dev) ([source](https://github.com/microsoft/playwright)) | devDependencies | minor | [`1.59.1` -> `1.60.0`](https://renovatebot.com/diffs/npm/@playwright%2ftest/1.59.1/1.60.0) | --- ### Release Notes <details> <summary>microsoft/playwright (@​playwright/test)</summary> ### [`v1.60.0`](https://github.com/microsoft/playwright/releases/tag/v1.60.0) [Compare Source](https://github.com/microsoft/playwright/compare/v1.59.1...v1.60.0) #### 🌐 HAR recording on Tracing [tracing.startHar()](https://playwright.dev/docs/api/class-tracing#tracing-start-har) / [tracing.stopHar()](https://playwright.dev/docs/api/class-tracing#tracing-stop-har) expose HAR recording as a first-class tracing API, with the same `content`, `mode` and `urlFilter` options as `recordHar`. The returned [Disposable](https://playwright.dev/docs/api/class-disposable) makes it easy to scope a recording with `await using`: ```js await using har = await context.tracing.startHar('trace.har'); const page = await context.newPage(); await page.goto('https://playwright.dev'); // HAR is finalized when `har` goes out of scope. ``` #### 🪝 Drop API New [locator.drop()](https://playwright.dev/docs/api/class-locator#locator-drop) simulates an external drag-and-drop of files or clipboard-like data onto an element. Playwright dispatches `dragenter`, `dragover`, and `drop` with a synthetic \[DataTransfer] in the page context — works cross-browser and is great for testing upload zones: ```js await page.locator('#dropzone').drop({ files: { name: 'note.txt', mimeType: 'text/plain', buffer: Buffer.from('hello') }, }); await page.locator('#dropzone').drop({ data: { 'text/plain': 'hello world', 'text/uri-list': 'https://example.com', }, }); ``` #### 🎯 Aria snapshots - [expect(page).toMatchAriaSnapshot()](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-match-aria-snapshot) now works on a [Page](https://playwright.dev/docs/api/class-page), in addition to a [Locator](https://playwright.dev/docs/api/class-locator) — equivalent to asserting against `page.locator('body')`. - New `boxes` option on [locator.ariaSnapshot()](https://playwright.dev/docs/api/class-locator#locator-aria-snapshot) / [page.ariaSnapshot()](https://playwright.dev/docs/api/class-page#page-aria-snapshot) appends each element's bounding box as `[box=x,y,width,height]`, useful for AI consumption. #### 🛑 test.abort() New [test.abort()](https://playwright.dev/docs/api/class-test#test-abort) aborts the currently running test from a fixture, hook, or route handler with an optional message. Use it when you have detected an unrecoverable misuse and want to fail the test right away: ```js test('does not publish to the shared page', async ({ page }) => { await page.route('**/publish', route => { test.abort('Tests must not publish to the shared page. Use the `clone` option.'); return route.abort(); }); // ... }); ``` #### New APIs ##### Browser, Context and Page - Event [browser.on('context')](https://playwright.dev/docs/api/class-browser#browser-event-context) — fired when a new context is created on the browser. - [BrowserContext](https://playwright.dev/docs/api/class-browsercontext) now mirrors lifecycle events from its pages: [browserContext.on('download')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-download), [browserContext.on('frameattached')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-attached), [browserContext.on('framedetached')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-detached), [browserContext.on('framenavigated')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-navigated), [browserContext.on('pageclose')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-close), [browserContext.on('pageload')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-load). ##### Locators and Assertions - New option `description` in [page.getByRole()](https://playwright.dev/docs/api/class-page#page-get-by-role) / [locator.getByRole()](https://playwright.dev/docs/api/class-locator#locator-get-by-role) / [frame.getByRole()](https://playwright.dev/docs/api/class-frame#frame-get-by-role) / [frameLocator.getByRole()](https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-role) for matching the [accessible description](https://www.w3.org/TR/wai-aria-1.2/#dfn-accessible-description). - New option `pseudo` in [expect(locator).toHaveCSS()](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-css) reads computed styles from `::before` or `::after`. - New option `style` in [locator.highlight()](https://playwright.dev/docs/api/class-locator#locator-highlight) applies extra inline CSS to the highlight overlay, plus new [page.hideHighlight()](https://playwright.dev/docs/api/class-page#page-hide-highlight) to clear all highlights. ##### Network - [webSocketRoute.protocols()](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-protocols) returns the WebSocket subprotocols requested by the page. - New option `noDefaults` in [browserType.connectOverCDP()](https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp) disables Playwright's default overrides on the default context (download behavior, focus emulation, media emulation), so attaching to a user's daily-driver browser doesn't disturb its state. ##### Errors and Reporting - New [webError.location()](https://playwright.dev/docs/api/class-weberror#web-error-location) mirrors [consoleMessage.location()](https://playwright.dev/docs/api/class-consolemessage#console-message-location). - [consoleMessage.location()](https://playwright.dev/docs/api/class-consolemessage#console-message-location) now exposes `line` / `column` properties (`lineNumber` / `columnNumber` are deprecated). - New [testInfoError.errorContext](https://playwright.dev/docs/api/class-testinfoerror#test-info-error-error-context) surfaces additional diagnostic context, such as the aria snapshot of the receiver at the time of an `expect(...)` matcher failure. - [reporter.onError()](https://playwright.dev/docs/api/class-reporter#reporter-on-error) now receives a `workerInfo` argument with details about the worker for fixture teardown errors. ##### Test runner - New `{testFileBaseName}` token in [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template) — file name without extension. - Test runner now errors when a config tries to override a non-option fixture, and rejects `workers: 0` or negative values. #### 🛠️ Other improvements - HTML reporter: - `npx playwright show-report` accepts `.zip` files directly — no need to unzip first. - Steps that contain attachments inside nested children show an indicator on the parent step. - The `repeatEachIndex` is shown in the test header when non-zero. - Trace Viewer adds a pretty-print toggle for JSON / form request and response bodies in the network details panel. #### Breaking Changes ⚠️ - Removed long-deprecated APIs: - `Locator.ariaRef()` — use the standard [locator.ariaSnapshot()](https://playwright.dev/docs/api/class-locator#locator-aria-snapshot) pipeline. - `handle` option on `BrowserContext.exposeBinding` and `Page.exposeBinding`. - `logger` option on `BrowserType.connect` and `BrowserType.connectOverCDP` — use [tracing](https://playwright.dev/docs/trace-viewer) instead. - Context options `videosPath` / `videoSize` — use `recordVideo` instead. #### Browser Versions - Chromium 148.0.7778.96 - Mozilla Firefox 150.0.2 - WebKit 26.4 This version was also tested against the following stable channels: - Google Chrome 147 - Microsoft Edge 147 </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #182 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
e376ea1295 |
chore(deps): update dependency @oxc-project/runtime to ^0.131.0 (#181)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@oxc-project/runtime](https://oxc.rs) ([source](https://github.com/oxc-project/oxc/tree/HEAD/npm/runtime)) | devDependencies | minor | [`^0.129.0` -> `^0.131.0`](https://renovatebot.com/diffs/npm/@oxc-project%2fruntime/0.129.0/0.131.0) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #181 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
f2360b9db3 |
chore(shared-charts): soften the curated palette (#185)
## Summary Tune the curated chart palette to a softer, lower-saturation set. The values shipped in #175 were pulled straight from Tailwind's `-600 / -700` ramp; on real audit-log data the donut's three slices and the bar-chart's blue read as too punchy when they share a tile, especially in dark mode. Same five intents, same a11y posture — just less visual fight. ## What lands `libs/shared/charts/src/lib/_internal/palette.ts`: | Constant | Before | After | | --------------------------------- | -------- | -------- | | `DEFAULT_BAR_FILL` | `#1d4ed8` | `#4075e7` | | `semanticStatusColors.info` | `#2563eb` | `#4075e7` | | `semanticStatusColors.success` | `#16a34a` | `#46ac6b` | | `semanticStatusColors.warning` | `#ea580c` | `#f38043` | | `semanticStatusColors.error` | `#dc2626` | `#eb5252` | | `semanticStatusColors.neutral` | `#6b7280` | `#6b7280` (unchanged) | `info` and `DEFAULT_BAR_FILL` collapse to the same hex — bars and "informational" donut slices are *meant* to read as the same semantic class (no special status), so unifying them at the constant level removes a future drift hazard. Docstrings updated alongside — the previous comments name-checked Tailwind shades (`green-600`, `tailwind blue-700`) that no longer correspond to the values; the new comments describe the palette by intent (`muted green`, `muted orange`, ...) and call out that the softening is deliberate. ## Notes for the reviewer - **A11y posture unchanged.** The lib's contract is "AA contrast on white surfaces, deuteranopia/protanopia distinguishability via lightness deltas, not just hue". All four chromatic entries clear the same bar: each lightness sits in a distinct band (≈ 67 % for warning, ≈ 60 % for success, ≈ 60 % for error, ≈ 56 % for info), so colour-blind viewers still distinguish them by brightness even if the hue collapses. - **Why not derive these from `libs/shared/tokens/brand-tokens.css`?** Brand-primary is the dark teal `#12546c` and brand-accent is `#f7a919`. Neither reads correctly as "success" or "neutral chart fill"; the charts need a categorical palette tuned for *legibility on dense surfaces*, not for chrome and CTAs. Keeping the chart palette in its own lib stays consistent with ADR-0023's "lib owns the palette" stance. - **Bar fill default + `info` semantic alias to the same value on purpose.** A bar with no per-bar encoding is semantically "informational quantity over time" — the same intent as a donut slice tagged `info`. Future consumer that wants to flag a single "info" bar inside a stacked chart will read the colour as consistent. - **No code changes outside this file.** Consumers (`<lib-bar-chart>`, `<lib-donut-chart>`, the audit page) import these constants by name; the swap is purely a value change. ## Test plan - [x] `pnpm nx test shared-charts` — 15 specs pass (the donut `colorMap` spec asserts the *consumer-provided* hexes, not the lib defaults, so the change is transparent there; the bar single-fill spec checks uniqueness, not the specific value). - [x] `pnpm nx test portal-admin` — 62 specs pass. - [x] `pnpm nx run-many -t test build lint -p shared-charts,portal-admin` — clean (same three pre-existing lint warnings unrelated to this PR). - [ ] **Manual smoke** — `pnpm nx serve portal-admin`, sign in with `Portal.Admin`, navigate to `/admin/audit`, switch to Charts: - Daily-volume bars render in the new muted blue. - Outcome donut slices: green (success), red (failure), orange (denied) — softer than before, semantic mapping intact. - Dark-mode toggle — palette still legible against the dark surface. - Side-by-side comparison vs `main` — the new shades feel calmer, especially when multiple charts share the viewport. ## What's next Nothing pending on the palette front. If a future chart needs a sixth intent (e.g. `pending` for in-flight states), add it here with a contrast / colour-blind check and update the typed `SemanticStatus` union in the same PR. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #185 |
||
|
|
8a02ca86a2 |
fix(deps): update vitest to v4.1.6 (#180)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@vitest/coverage-v8](https://vitest.dev/guide/coverage) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8)) | devDependencies | patch | [`4.1.5` -> `4.1.6`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-v8/4.1.5/4.1.6) | | [@vitest/ui](https://vitest.dev/guide/ui) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui)) | devDependencies | patch | [`4.1.5` -> `4.1.6`](https://renovatebot.com/diffs/npm/@vitest%2fui/4.1.5/4.1.6) | | [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | devDependencies | patch | [`4.1.5` -> `4.1.6`](https://renovatebot.com/diffs/npm/vitest/4.1.5/4.1.6) | | [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | dependencies | patch | [`4.1.5` -> `4.1.6`](https://renovatebot.com/diffs/npm/vitest/4.1.5/4.1.6) | --- ### Release Notes <details> <summary>vitest-dev/vitest (@​vitest/coverage-v8)</summary> ### [`v4.1.6`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.6) [Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.5...v4.1.6) ##### 🐞 Bug Fixes - **browser**: Provide project reference in `ToMatchScreenshotResolvePath` - by [@​macarie](https://github.com/macarie) and [@​sheremet-va](https://github.com/sheremet-va) in https://github.com/vitest-dev/vitest/issues/10138 [<samp>(31882)</samp>](https://github.com/vitest-dev/vitest/commit/31882607c) - Global `sequence.concurrent: true` with top-level `test(..., { concurrent: false })` + depreacte `sequential` test API and options - by [@​hi-ogawa](https://github.com/hi-ogawa), **Codex** and [@​sheremet-va](https://github.com/sheremet-va) in https://github.com/vitest-dev/vitest/issues/10196 [<samp>(2847d)</samp>](https://github.com/vitest-dev/vitest/commit/2847dfa2a) - **browser**: Simplify orchestrator otel carrier - by [@​hi-ogawa](https://github.com/hi-ogawa) in https://github.com/vitest-dev/vitest/issues/10285 [<samp>(18af9)</samp>](https://github.com/vitest-dev/vitest/commit/18af98cee) ##### 🏎 Performance - Stringify diff objects only once - by [@​sheremet-va](https://github.com/sheremet-va) in https://github.com/vitest-dev/vitest/issues/10276 [<samp>(9f7b1)</samp>](https://github.com/vitest-dev/vitest/commit/9f7b1528c) ##### [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.5...v4.1.6) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/180 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
bd94bb4ffe |
fix(deps): update nx to v22.7.2 (#179)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@nx/devkit](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/devkit)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fdevkit/22.7.1/22.7.2) | | [@nx/eslint-plugin](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/eslint-plugin)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2feslint-plugin/22.7.1/22.7.2) | | [@nx/jest](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/jest)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fjest/22.7.1/22.7.2) | | [@nx/js](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/js)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fjs/22.7.1/22.7.2) | | [@nx/node](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/node)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fnode/22.7.1/22.7.2) | | [@nx/playwright](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/playwright)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fplaywright/22.7.1/22.7.2) | | [@nx/vite](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/vite)) | dependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fvite/22.7.1/22.7.2) | | [@nx/vitest](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/vitest)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fvitest/22.7.1/22.7.2) | | [@nx/web](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/web)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fweb/22.7.1/22.7.2) | | [@nx/webpack](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/webpack)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fwebpack/22.7.1/22.7.2) | | [nx](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/nx)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/nx/22.7.1/22.7.2) | --- ### Release Notes <details> <summary>nrwl/nx (@​nx/devkit)</summary> ### [`v22.7.2`](https://github.com/nrwl/nx/releases/tag/22.7.2) [Compare Source](https://github.com/nrwl/nx/compare/22.7.1...22.7.2) ##### 22.7.2 (2026-05-14) ##### 🚀 Features - **gradle:** stream batch task results to nx as they finish ([#​35487](https://github.com/nrwl/nx/pull/35487)) - **nx-dev:** track docs analytics for code copy, LLM prompt, YouTube ([#​35526](https://github.com/nrwl/nx/pull/35526)) - **testing:** add migration for Jest 30 snapshot guide link ([#​35629](https://github.com/nrwl/nx/pull/35629)) ##### 🩹 Fixes - **angular:** disable vitest watch by default ([#​35493](https://github.com/nrwl/nx/pull/35493)) - **angular-rspack:** keep root-scoped assets out of per-locale i18n emit ([#​35621](https://github.com/nrwl/nx/pull/35621)) - **bundling:** include tsconfig solution input for rollup ([#​35476](https://github.com/nrwl/nx/pull/35476)) - **bundling:** include tsconfig solution input for webpack ([#​35477](https://github.com/nrwl/nx/pull/35477), [#​35476](https://github.com/nrwl/nx/issues/35476)) - **core:** bump axios to 1.16.0 for all packages ([#​35568](https://github.com/nrwl/nx/pull/35568)) - **core:** add provenance check in nx console status path ([#​35485](https://github.com/nrwl/nx/pull/35485)) - **core:** remove access control header from graph app ([#​35494](https://github.com/nrwl/nx/pull/35494)) - **core:** ensure verbose logs go to stderr and daemon logs are properly decorated ([#​34358](https://github.com/nrwl/nx/pull/34358)) - **core:** show flaky-task count in run summary ([#​35491](https://github.com/nrwl/nx/pull/35491)) - **core:** unique telemetry user\_id; expose workspace\_id dimension ([#​35553](https://github.com/nrwl/nx/pull/35553)) - **core:** update minimatch to 10.2.5 ([#​35569](https://github.com/nrwl/nx/pull/35569), [#​34660](https://github.com/nrwl/nx/issues/34660)) - **core:** restore use-legacy-versioning shim for [@​nx/js](https://github.com/nx/js)[@​21](https://github.com/21) ensurePackage path ([#​35574](https://github.com/nrwl/nx/pull/35574)) - **core:** isolate NX\_PARALLEL env var in parallel-related specs ([#​35579](https://github.com/nrwl/nx/pull/35579)) - **core:** skip handleimport miss path when nx key packages are absent ([#​35596](https://github.com/nrwl/nx/pull/35596)) - **core:** use gethostuuid(3) instead of ioreg on macOS ([#​35599](https://github.com/nrwl/nx/pull/35599)) - **core:** isolate cache env vars in splitArgs spec ([#​35584](https://github.com/nrwl/nx/pull/35584)) - **core:** enable node's native v8 compile cache support ([#​35415](https://github.com/nrwl/nx/pull/35415), [#​20454](https://github.com/nrwl/nx/issues/20454)) - **core:** support skipped batch tasks end-to-end and fix TUI double logs ([#​35617](https://github.com/nrwl/nx/pull/35617)) - **core:** keep TUI task selection on the in-progress section ([#​35640](https://github.com/nrwl/nx/pull/35640)) - **core:** allow `nx mcp` to run outside of an Nx workspace ([#​35655](https://github.com/nrwl/nx/pull/35655)) - **core:** cast perf entries to PerformanceMeasure for detail access ([43c0c821ba](https://github.com/nrwl/nx/commit/43c0c821ba)) - **devkit:** exclude dist from jest module path scan ([#​35615](https://github.com/nrwl/nx/pull/35615)) - **devkit:** expand @​nx/devkit/internal re-exports for cherry-picked v23 deep-import migration ([#​35541](https://github.com/nrwl/nx/issues/35541)) - **dotnet:** correct output paths for Web SDK and centralized dist setups ([#​35398](https://github.com/nrwl/nx/pull/35398)) - **gradle:** exclude batch-runner from jest haste-map crawl ([#​35501](https://github.com/nrwl/nx/pull/35501)) - **gradle:** exclude project-graph from jest module path scan ([#​35609](https://github.com/nrwl/nx/pull/35609)) - **gradle:** support Windows file paths ([#​35184](https://github.com/nrwl/nx/pull/35184), [#​34987](https://github.com/nrwl/nx/issues/34987)) - **js:** strip glob from inferred outputs before resolving as path ([#​35463](https://github.com/nrwl/nx/pull/35463), [#​35452](https://github.com/nrwl/nx/issues/35452)) - **js:** reference vitest.config in eslint dep-checks for vitest libs ([#​35460](https://github.com/nrwl/nx/pull/35460), [#​33670](https://github.com/nrwl/nx/issues/33670), [#​35450](https://github.com/nrwl/nx/issues/35450)) - **js:** include transitive workspace deps in pruned pnpm lockfile ([#​35532](https://github.com/nrwl/nx/pull/35532), [#​35347](https://github.com/nrwl/nx/issues/35347), [#​34655](https://github.com/nrwl/nx/issues/34655)) - **linter:** prevent ENOENT crash in getRelativeImportPath for unresolvable paths ([#​35007](https://github.com/nrwl/nx/pull/35007), [#​13872](https://github.com/nrwl/nx/issues/13872), [#​34066](https://github.com/nrwl/nx/issues/34066), [#​30491](https://github.com/nrwl/nx/issues/30491), [#​16716](https://github.com/nrwl/nx/issues/16716), [#​35006](https://github.com/nrwl/nx/issues/35006), [#​21889](https://github.com/nrwl/nx/issues/21889), [#​32190](https://github.com/nrwl/nx/issues/32190)) - **maven:** skip attached artifacts that fail to materialize in batch record ([#​35473](https://github.com/nrwl/nx/pull/35473)) - **maven:** serialize Maven 4 build state recording ([#​35555](https://github.com/nrwl/nx/pull/35555)) - **maven:** widen runCLI timeout for --no-batch maven.test.ts cases ([#​35589](https://github.com/nrwl/nx/pull/35589)) - **nx-dev:** document nested CLI subcommands beyond two levels ([#​35519](https://github.com/nrwl/nx/pull/35519)) - **nx-dev:** short-circuit bot probes in framer rewrite edge function ([#​35527](https://github.com/nrwl/nx/pull/35527)) - **react:** withSvgr migration preserves other properties ([#​35484](https://github.com/nrwl/nx/pull/35484)) - **repo:** clear NX\_INVOCATION\_ROOT\_PID in run-native-target to avoid recursion false-positive ([443dee0b22](https://github.com/nrwl/nx/commit/443dee0b22)) - **repo:** revert deep-import rewrites that targeted v23-only @​nx/devkit/internal entry ([ac8187963d](https://github.com/nrwl/nx/commit/ac8187963d)) - **repo:** unblock 22.7.x cargo tests and nx-build e2e ([#​34285](https://github.com/nrwl/nx/issues/34285)) - **repo:** expand "..." spread token in graph typecheck inputs ([#​34285](https://github.com/nrwl/nx/issues/34285), [#​35458](https://github.com/nrwl/nx/issues/35458)) - **testing:** pin jest to ~30.3.0 to avoid jest-runtime 30.4 RN incompat ([#​35618](https://github.com/nrwl/nx/pull/35618)) - **testing:** handle absolute cypress screenshotsFolder/videosFolder paths ([#​35624](https://github.com/nrwl/nx/pull/35624)) - **testing:** exclude dist and out-tsc from default jest module path scan ([#​35619](https://github.com/nrwl/nx/pull/35619)) - **testing:** update remaining snapshot guide links missed by migration ([cd350c1140](https://github.com/nrwl/nx/commit/cd350c1140)) ##### ❤️ Thank You - Adam Keenan [@​adamk33n3r](https://github.com/adamk33n3r) - AgentEnder [@​AgentEnder](https://github.com/AgentEnder) - beeman - Claude - Craigory Coppola [@​AgentEnder](https://github.com/AgentEnder) - FrozenPandaz [@​FrozenPandaz](https://github.com/FrozenPandaz) - Jack Hsu [@​jaysoo](https://github.com/jaysoo) - Jason Jean [@​FrozenPandaz](https://github.com/FrozenPandaz) - Leosvel Pérez Espinosa [@​leosvelperez](https://github.com/leosvelperez) - Max Kless - MaxKless [@​MaxKless](https://github.com/MaxKless) - Optischa [@​Optischa](https://github.com/Optischa) - Sharon Lougheed </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled because a matching PR was automerged previously. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/179 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
6b20c3413b |
fix(deps): update nx to v22.7.2 (#178)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@nx/angular](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/angular)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fangular/22.7.1/22.7.2) | | [@nx/devkit](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/devkit)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fdevkit/22.7.1/22.7.2) | | [@nx/eslint](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/eslint)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2feslint/22.7.1/22.7.2) | | [@nx/eslint-plugin](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/eslint-plugin)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2feslint-plugin/22.7.1/22.7.2) | | [@nx/jest](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/jest)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fjest/22.7.1/22.7.2) | | [@nx/js](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/js)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fjs/22.7.1/22.7.2) | | [@nx/nest](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/nest)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fnest/22.7.1/22.7.2) | | [@nx/node](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/node)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fnode/22.7.1/22.7.2) | | [@nx/playwright](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/playwright)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fplaywright/22.7.1/22.7.2) | | [@nx/vite](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/vite)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fvite/22.7.1/22.7.2) | | [@nx/vite](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/vite)) | dependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fvite/22.7.1/22.7.2) | | [@nx/vitest](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/vitest)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fvitest/22.7.1/22.7.2) | | [@nx/web](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/web)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fweb/22.7.1/22.7.2) | | [@nx/webpack](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/webpack)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/@nx%2fwebpack/22.7.1/22.7.2) | | [nx](https://nx.dev) ([source](https://github.com/nrwl/nx/tree/HEAD/packages/nx)) | devDependencies | patch | [`22.7.1` -> `22.7.2`](https://renovatebot.com/diffs/npm/nx/22.7.1/22.7.2) | --- ### Release Notes <details> <summary>nrwl/nx (@​nx/angular)</summary> ### [`v22.7.2`](https://github.com/nrwl/nx/releases/tag/22.7.2) [Compare Source](https://github.com/nrwl/nx/compare/22.7.1...22.7.2) ##### 22.7.2 (2026-05-14) ##### 🚀 Features - **gradle:** stream batch task results to nx as they finish ([#​35487](https://github.com/nrwl/nx/pull/35487)) - **nx-dev:** track docs analytics for code copy, LLM prompt, YouTube ([#​35526](https://github.com/nrwl/nx/pull/35526)) - **testing:** add migration for Jest 30 snapshot guide link ([#​35629](https://github.com/nrwl/nx/pull/35629)) ##### 🩹 Fixes - **angular:** disable vitest watch by default ([#​35493](https://github.com/nrwl/nx/pull/35493)) - **angular-rspack:** keep root-scoped assets out of per-locale i18n emit ([#​35621](https://github.com/nrwl/nx/pull/35621)) - **bundling:** include tsconfig solution input for rollup ([#​35476](https://github.com/nrwl/nx/pull/35476)) - **bundling:** include tsconfig solution input for webpack ([#​35477](https://github.com/nrwl/nx/pull/35477), [#​35476](https://github.com/nrwl/nx/issues/35476)) - **core:** bump axios to 1.16.0 for all packages ([#​35568](https://github.com/nrwl/nx/pull/35568)) - **core:** add provenance check in nx console status path ([#​35485](https://github.com/nrwl/nx/pull/35485)) - **core:** remove access control header from graph app ([#​35494](https://github.com/nrwl/nx/pull/35494)) - **core:** ensure verbose logs go to stderr and daemon logs are properly decorated ([#​34358](https://github.com/nrwl/nx/pull/34358)) - **core:** show flaky-task count in run summary ([#​35491](https://github.com/nrwl/nx/pull/35491)) - **core:** unique telemetry user\_id; expose workspace\_id dimension ([#​35553](https://github.com/nrwl/nx/pull/35553)) - **core:** update minimatch to 10.2.5 ([#​35569](https://github.com/nrwl/nx/pull/35569), [#​34660](https://github.com/nrwl/nx/issues/34660)) - **core:** restore use-legacy-versioning shim for [@​nx/js](https://github.com/nx/js)[@​21](https://github.com/21) ensurePackage path ([#​35574](https://github.com/nrwl/nx/pull/35574)) - **core:** isolate NX\_PARALLEL env var in parallel-related specs ([#​35579](https://github.com/nrwl/nx/pull/35579)) - **core:** skip handleimport miss path when nx key packages are absent ([#​35596](https://github.com/nrwl/nx/pull/35596)) - **core:** use gethostuuid(3) instead of ioreg on macOS ([#​35599](https://github.com/nrwl/nx/pull/35599)) - **core:** isolate cache env vars in splitArgs spec ([#​35584](https://github.com/nrwl/nx/pull/35584)) - **core:** enable node's native v8 compile cache support ([#​35415](https://github.com/nrwl/nx/pull/35415), [#​20454](https://github.com/nrwl/nx/issues/20454)) - **core:** support skipped batch tasks end-to-end and fix TUI double logs ([#​35617](https://github.com/nrwl/nx/pull/35617)) - **core:** keep TUI task selection on the in-progress section ([#​35640](https://github.com/nrwl/nx/pull/35640)) - **core:** allow `nx mcp` to run outside of an Nx workspace ([#​35655](https://github.com/nrwl/nx/pull/35655)) - **core:** cast perf entries to PerformanceMeasure for detail access ([43c0c821ba](https://github.com/nrwl/nx/commit/43c0c821ba)) - **devkit:** exclude dist from jest module path scan ([#​35615](https://github.com/nrwl/nx/pull/35615)) - **devkit:** expand @​nx/devkit/internal re-exports for cherry-picked v23 deep-import migration ([#​35541](https://github.com/nrwl/nx/issues/35541)) - **dotnet:** correct output paths for Web SDK and centralized dist setups ([#​35398](https://github.com/nrwl/nx/pull/35398)) - **gradle:** exclude batch-runner from jest haste-map crawl ([#​35501](https://github.com/nrwl/nx/pull/35501)) - **gradle:** exclude project-graph from jest module path scan ([#​35609](https://github.com/nrwl/nx/pull/35609)) - **gradle:** support Windows file paths ([#​35184](https://github.com/nrwl/nx/pull/35184), [#​34987](https://github.com/nrwl/nx/issues/34987)) - **js:** strip glob from inferred outputs before resolving as path ([#​35463](https://github.com/nrwl/nx/pull/35463), [#​35452](https://github.com/nrwl/nx/issues/35452)) - **js:** reference vitest.config in eslint dep-checks for vitest libs ([#​35460](https://github.com/nrwl/nx/pull/35460), [#​33670](https://github.com/nrwl/nx/issues/33670), [#​35450](https://github.com/nrwl/nx/issues/35450)) - **js:** include transitive workspace deps in pruned pnpm lockfile ([#​35532](https://github.com/nrwl/nx/pull/35532), [#​35347](https://github.com/nrwl/nx/issues/35347), [#​34655](https://github.com/nrwl/nx/issues/34655)) - **linter:** prevent ENOENT crash in getRelativeImportPath for unresolvable paths ([#​35007](https://github.com/nrwl/nx/pull/35007), [#​13872](https://github.com/nrwl/nx/issues/13872), [#​34066](https://github.com/nrwl/nx/issues/34066), [#​30491](https://github.com/nrwl/nx/issues/30491), [#​16716](https://github.com/nrwl/nx/issues/16716), [#​35006](https://github.com/nrwl/nx/issues/35006), [#​21889](https://github.com/nrwl/nx/issues/21889), [#​32190](https://github.com/nrwl/nx/issues/32190)) - **maven:** skip attached artifacts that fail to materialize in batch record ([#​35473](https://github.com/nrwl/nx/pull/35473)) - **maven:** serialize Maven 4 build state recording ([#​35555](https://github.com/nrwl/nx/pull/35555)) - **maven:** widen runCLI timeout for --no-batch maven.test.ts cases ([#​35589](https://github.com/nrwl/nx/pull/35589)) - **nx-dev:** document nested CLI subcommands beyond two levels ([#​35519](https://github.com/nrwl/nx/pull/35519)) - **nx-dev:** short-circuit bot probes in framer rewrite edge function ([#​35527](https://github.com/nrwl/nx/pull/35527)) - **react:** withSvgr migration preserves other properties ([#​35484](https://github.com/nrwl/nx/pull/35484)) - **repo:** clear NX\_INVOCATION\_ROOT\_PID in run-native-target to avoid recursion false-positive ([443dee0b22](https://github.com/nrwl/nx/commit/443dee0b22)) - **repo:** revert deep-import rewrites that targeted v23-only @​nx/devkit/internal entry ([ac8187963d](https://github.com/nrwl/nx/commit/ac8187963d)) - **repo:** unblock 22.7.x cargo tests and nx-build e2e ([#​34285](https://github.com/nrwl/nx/issues/34285)) - **repo:** expand "..." spread token in graph typecheck inputs ([#​34285](https://github.com/nrwl/nx/issues/34285), [#​35458](https://github.com/nrwl/nx/issues/35458)) - **testing:** pin jest to ~30.3.0 to avoid jest-runtime 30.4 RN incompat ([#​35618](https://github.com/nrwl/nx/pull/35618)) - **testing:** handle absolute cypress screenshotsFolder/videosFolder paths ([#​35624](https://github.com/nrwl/nx/pull/35624)) - **testing:** exclude dist and out-tsc from default jest module path scan ([#​35619](https://github.com/nrwl/nx/pull/35619)) - **testing:** update remaining snapshot guide links missed by migration ([cd350c1140](https://github.com/nrwl/nx/commit/cd350c1140)) ##### ❤️ Thank You - Adam Keenan [@​adamk33n3r](https://github.com/adamk33n3r) - AgentEnder [@​AgentEnder](https://github.com/AgentEnder) - beeman - Claude - Craigory Coppola [@​AgentEnder](https://github.com/AgentEnder) - FrozenPandaz [@​FrozenPandaz](https://github.com/FrozenPandaz) - Jack Hsu [@​jaysoo](https://github.com/jaysoo) - Jason Jean [@​FrozenPandaz](https://github.com/FrozenPandaz) - Leosvel Pérez Espinosa [@​leosvelperez](https://github.com/leosvelperez) - Max Kless - MaxKless [@​MaxKless](https://github.com/MaxKless) - Optischa [@​Optischa](https://github.com/Optischa) - Sharon Lougheed </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/178 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
b9daaa5f58 |
fix(portal-shell): same html/body overflow-y hidden shield as admin (#177)
## Summary Mirror of #176 onto `portal-shell`. Same one-line shield, same rationale: the two apps share an identical `:host { height: 100vh }` + `<main> overflow-y: auto` layout, so the same defensive `html, body { overflow-y: hidden }` rule belongs on both surfaces. Brings the public-facing shell to the same posture as the admin one — any future layout escape stops at the shell boundary rather than producing a phantom body scrollbar plus an empty band below the footer. ## What lands `apps/portal-shell/src/styles.css`: ```css html, body { overflow-y: hidden; } ``` Same block as #176 with a comment that explicitly cross-references the admin shield so future contributors don't accidentally diverge the two apps. ## Notes for the reviewer - **Why now, rather than waiting for portal-shell to demonstrate the symptom?** #176's reviewer note said this would land "if/when a layout escape shows up there." The user asked for parity immediately, on the reasoning that the shell contract is the *same* on both apps — the shield is defensive and one-line, so coupling the two posture changes is cheaper than tracking a "TODO once we see it in shell". - **No new tests.** Same justification as #176 — the change is at the global stylesheet level, has no behavioural surface, and the manual repro path already exists (force a chart or wide element past the viewport; pre-shield → body scrollbar + footer gap; post-shield → clipped at the shell root, `<main>` still scrolls). - **Element-level scrolling on `<main>` is unaffected.** The skip-link, sidebar, and footer all keep their pinned positions; long routes (the user list, future content pages) scroll inside `<main>` as designed. ## Test plan - [x] `pnpm nx build portal-shell` — clean. - [x] `pnpm nx test portal-shell` — green. - [x] `pnpm nx lint portal-shell` — clean. - [x] `pnpm nx run-many -t build test lint -p portal-shell,portal-admin` — green (admin and shell share the build cache; touching only one file invalidates only the shell target). - [ ] **Manual smoke** — `pnpm nx serve portal-shell`: - Open `/fr` and `/en`, scroll long pages — `<main>` scrolls, body doesn't. - Resize across the breakpoint where the sidebar collapses — body still doesn't scroll; sidebar/footer pin correctly. - Toggle dark mode — no visual regression. ## What's next Nothing pending on this shell-shield front. The two apps are now symmetric; if a third app appears (it won't in v1) the same pattern is documented in both `styles.css` headers. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #177 |
||
|
|
67e50be1dc |
fix(portal-admin): html/body overflow-y hidden as a shell shield (#176)
## Summary Tiny follow-up to #175 — the bar / donut / stacked-bar charts on the audit-log Charts tab still surfaced a *phantom* body scrollbar plus an empty band below the footer in some viewport widths. The lib-side overflow constraints from #175 hold for the cases tested, but the symptom can re-appear from any future layout escape (a wider downstream component, a third-party iframe, an unforeseen flex bug). This PR adds a `html, body { overflow-y: hidden }` shield at the global stylesheet so any vertical overflow at the document level — wherever it comes from — stops at the shell boundary instead of producing a phantom scrollbar. Element-level scrolling on `<main>` (the only surface that *should* scroll) is unaffected. ## What lands `apps/portal-admin/src/styles.css`: ```css html, body { overflow-y: hidden; } ``` That's the whole change. The admin shell already commits to the "fills the viewport, never scrolls the body" layout — `<app-root>` is locked at `height: 100vh` and `<main>` owns its own `overflow-y: auto`. Anything that escapes that contract is, by design, a bug to fix at the source. The shield is a safety net, not a load-bearing layout rule. ## Notes for the reviewer - **Why only portal-admin?** `portal-shell`'s app.scss carries the exact same `height: 100vh` + `<main> overflow-y: auto` shape, so the same shield would make sense there too. Holding it back to a separate PR because portal-shell hasn't actually demonstrated the symptom and the audit-log chantier is the immediate motivation — a one-line shield to the public-facing app deserves its own minute of consideration. Trivial to extend if/when we want symmetry. - **Why not just delete `height: 100vh` and let the document scroll naturally?** The admin shell deliberately keeps the header, sidebar, and footer pinned while only the content area scrolls — that's a deliberate UX choice for a dense admin surface (long audit-log tables, future CMS editors), not an accident. Keeping the 100vh contract and adding the shield preserves the intent. - **Manual reproduction of the original symptom** (now fixed): pre-shield, switching to the Charts tab on a 1280×720 viewport produced a body scrollbar with ~12 px of empty space below the footer, even though every visible element was inside `<main>`. Post-shield, the body scrollbar is gone; `<main>`'s internal scrollbar still works for the table page below the fold. ## Test plan - [x] `pnpm nx build portal-admin` — clean. - [x] `pnpm nx test portal-admin` — 62 specs pass (no behavioural change). - [x] `pnpm nx lint portal-admin` — same three pre-existing warnings, no new ones. - [ ] **Manual smoke** — `pnpm nx serve portal-admin`, sign in with `Portal.Admin`: - Navigate to `/admin/audit`, switch to Charts — no body scrollbar, no gap under the footer, charts still render at the column width. - Resize the viewport across the `(max-width: 800px)` breakpoint — body still doesn't scroll; `<main>` still does where it should. - Open the user-list page (long table) — `<main>` scrolls internally as expected; the shield does *not* prevent legitimate content scrolling. - Toggle dark mode — no visual regression. ## What's next - Mirror the same shield into `apps/portal-shell/src/styles.css` if/when a layout escape shows up there. Tracked in `docs/decisions/0020-portal-admin-app.md` follow-ups. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #176 |
||
|
|
9e7eb4af15 |
fix(audit): chart colours + Charts-tab layout regressions (#175)
## Summary Follow-up polish on the audit-log Charts tab shipped in #174. Three small but visible regressions surfaced once real data was loaded: 1. **"Events per day" bar chart cycled a categorical palette across the X axis** — every day got a different colour, which read as "categorical meaning per day" when the X axis is just a time bucket. Switched to a single fixed fill (curated blue from the lib palette). 2. **"Outcome breakdown" donut painted slices in Set2 order** — `success` came out teal, `denied` came out orange. With orange = "danger" in most readers' mental model, this is the wrong way around. Added a `colorMap` input on `<lib-donut-chart>` and a curated `semanticStatusColors` export so the audit page can map `success → green`, `denied → orange`, `failure → red`. 3. **Charts tab caused a body scrollbar + empty space below the footer.** Plot renders into a hidden `[hidden]` panel on first switch, so `canvas.clientWidth` is 0 and Plot falls back to its 600/720 default — the resulting fixed-width SVG was wider than the grid column, dragged horizontal overflow up the tree, and (because the grid item defaults to `min-width: auto`) wouldn't shrink. The shell layout broke. Fixed by constraining the chart envelope and the grid items so the SVG can scale down to the actual column width. ## What lands ### Bar chart — single fill, lib-owned default - `Plot.barY` mark no longer encodes colour from `xKey`. The `fill` parameter is now a literal-string colour, applied uniformly to every bar. - New `fillColor` input on `<lib-bar-chart>` for the rare case a consumer needs a different shade; defaults to `DEFAULT_BAR_FILL = '#1d4ed8'` (≈ Tailwind blue-700, picked for AA contrast against both light and dark surfaces, colour-blind-safe). - Removed the now-unused `color: { type: 'ordinal', range: palette }` scale and the `colorScheme` input on the bar chart — the categorical palette only made sense when `fill: xKey` was the default behaviour. ### Donut chart — optional semantic mapping - New `colorMap?: Readonly<Record<string, string>>` input on `<lib-donut-chart>`. When provided, slice fill resolves to `colorMap[category]` first, falling back to the categorical palette for any unmapped category. Lib still owns the palette per ADR-0023; the map only constrains *which colour the consumer picks for which category*, not what colours exist. - New `semanticStatusColors` export from `shared-charts` — a curated 5-entry map (`success / warning / error / info / neutral`) tuned for AA contrast and deuteranopia/protanopia distinguishability. Consumers stay inside the lib's palette without authoring their own hex codes. - The audit page now ships `outcomeColorMap = { success: green, failure: red, denied: orange }` and passes it to the donut. The donut's `description` (screen-reader fallback) and the legend chip semantics now line up. ### Chart envelope — overflow containment `libs/shared/charts/src/lib/_internal/chart-envelope.scss`: - Force `display: block` + `min-width: 0` on every chart host element (`lib-bar-chart`, `lib-donut-chart`, `lib-stacked-bar-chart`). Angular custom-element hosts default to `display: inline`, which lets the inner `<figure>` escape parent sizing constraints — the root cause of the body-scrollbar symptom. - `.chart-canvas` gets `min-width: 0` and constrains every inner `figure { max-width: 100% }` + `svg { max-width: 100%; height: auto }`. The SVG keeps its viewBox aspect ratio while scaling down to the actual column width. `apps/portal-admin/src/app/pages/audit/audit.scss`: - `.chart-tile { min-width: 0; }` — grid items default to `min-width: auto`, which prevents shrinking below the intrinsic content width. Without it, Plot's fixed-width SVG could still push the grid column past `1fr` even with the lib-side fixes. ## Notes for the reviewer - **Why a hardcoded hex (`#1d4ed8`) for `DEFAULT_BAR_FILL` rather than a brand token?** The chart lib is intentionally decoupled from `libs/shared/tokens` — it has no SCSS/CSS-vars dependency and Plot writes the fill as an SVG attribute, not a CSS value, so a CSS variable wouldn't apply anyway. The hex stays inside the lib, marked as the canonical default, with the docstring promising AA contrast + colour-blind safety. If the brand wants to take it over later, swap the constant in one place. - **`semanticStatusColors` is a curated map, not an open extension point.** Five entries (`success / warning / error / info / neutral`) covers the audit module and any future status-bearing donut (user list, integrations health, etc.). Adding a sixth entry needs a small PR + an a11y-contrast check, which is the right friction. - **The `aria-label="bar"` selector in the new bar-chart spec.** Plot tags its bar-mark `<g>` with `aria-label="bar"` (and similarly `"rule"` for `Plot.ruleY`). Selecting on it scopes the fill-uniqueness check to actual bars, not axes / ticks / labels. If Plot changes that label upstream the spec breaks loudly — preferable to a fragile geometric selector. - **Lazy fetch policy and the empty-state edge case** from #174 are unchanged. The donut still receives `colorMap` even when `data` is empty; the lib's `arcs.forEach` short-circuits on zero arcs so no colour lookup happens. - **`audit.scss` is now 7.5 KB**, still over the 6 KB component-style warning (untouched from #174) and well under the 8 KB error. The new `.chart-tile { min-width: 0 }` rule is two lines. ## Test plan - [x] `pnpm nx test shared-charts` — **15 specs pass** (was 14: +1 donut `colorMap` test, +1 bar single-fill test, -1 obsolete bar palette assumption). - [x] `pnpm nx test portal-admin` — **62 specs pass** (unchanged from #174 — semantic mapping is a template-only change, behavioural tests still hold). - [x] `pnpm nx run-many -t lint test build -p portal-shell,portal-admin,portal-bff,shared-charts` — clean. Same three pre-existing lint warnings, no new ones; bundle sizes unchanged within ±0.1 KB. - [ ] **Manual smoke** — `pnpm nx serve portal-admin` + `pnpm nx serve portal-bff`, sign in with `Portal.Admin`, navigate to `/admin/audit`: - Switch to **Charts** tab — daily-volume bars all render the same blue. - Donut centre matches `s.total`, the green slice is `success`, the orange slice is `denied`, the red slice is `failure`. Hover each slice — `<title>` shows `<category>: <count>` (untouched a11y contract from the lib). - Body scrollbar stays absent; footer stays anchored at the viewport bottom with no white-space gap. - Resize the window through the `(max-width: 800px)` breakpoint — grid collapses to one column, charts re-flow without horizontal overflow. - Dark mode toggle — colours stay readable, no contrast regressions. ## What's next Nothing in this chantier. The audit dashboard is now visually coherent and layout-stable. Two background items to track separately when the CMS / user-list pages land their own dashboards: - Tab-state URL persistence (carried forward from #174's "what's next"). - Promote the WAI-ARIA tab pattern out of `audit.html` into `libs/shared/ui/tabs/` once a second consumer appears. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #175 |
||
|
|
9f5106b805 |
feat(portal-admin): audit log tabs (Table / Charts) + server-side stats consumption (#174)
## Summary PR 2 of the tabs + full-result-charts chantier — closes the loop opened in #173. The audit log page now splits into a **Table** tab (existing behaviour) and a **Charts** tab fed by the server-side stats endpoint shipped in PR 1. ``` PR 1 (#173, merged) — BFF GET /api/admin/audit/stats + Redis 5min cache + admin.audit.stats.query audit event + ADR-0013 amendment. PR 2 (this one) — SPA Tabs UX (Table / Charts) + consume the stats endpoint, replacing the per-page client-side aggregations from #172. ``` ## What lands ### Tabs UX — WAI-ARIA tab pattern The two tabs sit between the filter card and the content area. Visual treatment is intentionally minimal: thin brand-coloured underline on the active tab, focus rings on `:focus-visible`, no surrounding chrome. The panel below inherits the page surface so each tab swap reads as a content-only change. ARIA wiring: - `<div role="tablist">` with two `<button role="tab">` children. - `aria-selected` mirrors the active tab; `aria-controls` points each tab at its panel id; roving `tabindex` (active = `0`, inactive = `-1`) keeps Tab linear. - Arrow-key navigation between tabs is bound on the individual buttons (not the tablist div) — focusable elements only, satisfies the `template/click-events-have-key-events` lint without an artificial `tabindex="-1"` on the container. - Two `<section role="tabpanel">` with `[hidden]` binding, `aria-labelledby` pointing back at the tab id. ### Stats consumption — replaces the per-page computeds `AuditEventsService` grows a `stats(filters)` method that calls `GET /api/admin/audit/stats` and returns `AdminAuditStats` (mirror of the BFF DTO). The audit page replaces the four per-page `computed()`s — `totalOnPage`, `dailyVolume`, `outcomeBreakdown`, `dailyByEventType` — with four signals: ```ts readonly stats = signal<AdminAuditStats | null>(null); readonly statsLoading = signal(false); readonly statsError = signal<string | null>(null); readonly hasChartData = computed(() => (this.stats()?.total ?? 0) > 0); ``` The chart-tile components on the Charts panel consume `stats()?.dailyVolume`, `stats()?.outcomeBreakdown`, `stats()?.eventTypeByDay`, and `stats()?.total` unchanged — same shape as the old per-page projections, just sourced server-side. ### Lazy fetch policy | Interaction | Action on `stats` | | ------------------------ | ---------------------------------------------- | | Page load (Table active) | No call — default tab is Table, stats untouched | | Click Charts (first time) | Fetch | | Click Table → Charts | Fetch only if cleared by a filter change | | Apply / clear filters | Clear `stats`, re-fetch **only if Charts active** | | Filter by row's actor | Clear `stats`, re-fetch if Charts active | | Next / previous page | Do **not** invalidate stats (pagination is presentation, the aggregated set is unchanged) | The Redis cache in PR 1 absorbs repeated identical fetches at ~5 ms; the policy here just makes sure we don't fire a stats call when nobody's looking at it. ### Honest panel copy The Charts panel's note now reads: > Aggregations are computed across the full filtered set (server-side), not just the events on the current page. Results are cached for 5 minutes per filter combination. Replaces the previous "this only reflects the current page" disclaimer that #172 carried as a temporary truth. ## Notes for the reviewer - **Why a hand-coded tablist instead of a shared `libs/shared/ui/tabs` primitive?** Two tabs, one consumer, ~30 LOC of template + ~25 LOC of SCSS. The two-occurrences-is-duplication rule says wait until a second consumer appears (cf. CMS module or user-list module slated for the next chantier), then extract with the shape both consumers have actually demanded. - **Why is `setTab()` `async`?** It triggers `fetchStats()` on the first switch to Charts, and the spec exercises that flow with `await`. Keeping it `async` lets the test sequence assertions deterministically without `tick()`/fakeAsync. - **Why does `search()` clear `stats` *before* the fetch even though `fetchStats()` clears it again at the top?** So the empty-state never flickers through a "stale 1000-event" total while the new fetch is in flight on a slow link. The double-clear is intentional. - **Bundle impact.** The audit chunk grew from ~82 KB to ~84.5 KB gzip (two new signals, a stats-mapping HTTP call, the tablist template + SCSS). Well under the lazy-chunk 100 KB ceiling from [ADR-0017](docs/decisions/0017-performance-budgets-lighthouse-ci.md). - **`audit.scss` is 7.5 KB** — over the 6 KB component-style warning, under the 8 KB error. The new `.tablist` / `.tab` rules account for the bump. If a third consumer of the tab pattern lands, the SCSS extraction comes with it. ## Test plan - [x] `pnpm nx test portal-admin` — **62 specs pass** (was 58: removed 3 obsolete "per-page chart" cases, added 7 new tabs/stats cases). - [x] `pnpm nx run portal-admin:lint` — clean (the two pre-existing `use-lifecycle-interface` warnings on `audit.ts` / `users.ts` and the spec non-null assertion are untouched, not regressions). - [x] `pnpm nx build portal-admin` — clean, audit chunk 84.5 KB gzip. - [x] `pnpm nx run-many -t lint test build -p portal-shell,portal-admin,portal-bff,shared-charts` — clean. - [ ] **Manual smoke** — `pnpm nx serve portal-admin` + `pnpm nx serve portal-bff`, sign in with `Portal.Admin`, navigate to `/admin/audit`: - Page loads on the Table tab — DevTools Network shows `GET /api/admin/audit` only, **no** `/audit/stats` call. - Click **Charts** → spinner state, then three chart tiles render with server-side totals. - Apply a filter (e.g. `eventType=auth.sign_in`) while on Charts → tiles re-render with the filtered aggregates; donut centre matches the server `total`. - Switch back to Table, page through results → no new `/audit/stats` calls (pagination doesn't invalidate). - Arrow-Right / Arrow-Left between the two tabs with keyboard — focus moves, ARIA selection follows, `Tab` skips past the inactive tab to the panel content. - Toggle dark mode → tablist + active underline + tab focus rings all read correctly in both modes. ## What's next - Persist the active tab in the URL (`?tab=charts`) so deep-linking and Back/Forward survive the swap. Out of scope for this chantier — once the user-list module lands and we have a second tab consumer, persistence becomes a shared concern. - Extract `libs/shared/ui/tabs` when a second consumer materialises (CMS or user-list module). - Surface cache observability — the stats endpoint Redis cache is silent in v1. A future ADR-0024 follow-up could add a `X-Audit-Stats-Cache: hit|miss` header for ops or an OTel attribute on the BFF span. Out of scope here, but worth flagging while the design is fresh. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #174 |
||
|
|
2cdeb74341 |
feat(portal-bff): audit-stats endpoint — server-side aggregations with redis cache (#173)
## Summary
PR 1 of the tabs + full-result-charts chantier. New BFF endpoint `GET /api/admin/audit/stats` that computes the three chart aggregations server-side over the **full filtered set** (not the paginated slice the SPA currently feeds the charts with).
```
PR 1 (this one) — BFF endpoint + Redis cache + audit event + ADR-0013 amendment.
PR 2 — SPA: Tabs UX (Table / Charts) + replace the per-page computeds
with calls to this endpoint.
```
## What lands
### New route — `GET /api/admin/audit/stats`
```ts
GET /api/admin/audit/stats?eventType=...&audience=...&outcome=...
&subjectPrefix=...&createdAtFrom=...&createdAtTo=...
&actorIdHash=...
→ {
dailyVolume: [{ day: 'YYYY-MM-DD', count }],
outcomeBreakdown: [{ outcome, count }],
eventTypeByDay: [{ day, eventType, count }],
total // sum of dailyVolume.count, drives the donut centre
}
```
Same filter shape as the existing `GET /api/admin/audit` minus pagination — the stats endpoint always aggregates the whole filtered set. `@RequireAdmin` gated (per [ADR-0020](docs/decisions/0020-portal-admin-app.md)). Time bound respects the filters strictly per the chantier brief: no filter → aggregates across the full audit retention (365 days per [ADR-0013](docs/decisions/0013-audit-trail-separated-postgres-append-only.md)). The Redis cache below absorbs repeated heavy queries.
### New service — [`AuditStatsReader`](apps/portal-bff/src/admin/audit-stats.service.ts)
Mirrors `AuditReader`'s posture:
- Every query inside a transaction that opens with `SET LOCAL ROLE audit_reader`. SELECT-only on `audit.events` even if the BFF's connection is otherwise privileged.
- Parameterised SQL only. Filter values flow through positional parameters, never concatenated.
- Three `GROUP BY` queries scoped by the same `WHERE` clause:
- `date_trunc('day', created_at)::date AS day, COUNT(*) GROUP BY day`
- `outcome::text, COUNT(*) GROUP BY outcome`
- `date_trunc('day', created_at)::date AS day, event_type, COUNT(*) GROUP BY day, event_type`
### Redis cache — 5-minute TTL per filter-hash
- Cache key: `audit:stats:<sha256(canonical-JSON of filters), 16 hex chars>`. Sorted-keys canonicalisation so the same filters in different argument orders map to the same key.
- TTL: 300 s. Audit rows are append-only so past aggregations are stable; new events are continuously inserted, so admins see at most 5-minute-stale aggregations — acceptable for "approximate dashboard" usage, not for "did the last event just land" debugging (use the list endpoint for that).
- Cache writes are best-effort — a Redis-write failure does not fail the response. The DB read already happened; the next call rebuilds the cache.
- The cache *write* path is covered by spec; the cache-hit shortcut path is covered too (skips the DB transaction entirely).
### New audit event — `admin.audit.stats.query`
Mirrors `admin.audit.query` in posture (every admin read is auditable per ADR-0020 §"Read actions ... to deter fishing expeditions") with two differences:
- Distinct `event_type` so an auditor can spot "scanned aggregations" vs "paged through rows" — different observation signals (the stats endpoint can sweep millions of rows in one call; the list endpoint is bounded by `MAX_LIMIT=200`).
- Payload carries `total` (size of the aggregated set) instead of `resultCount` — stats responses don't paginate, the value carries more "size of scan" signal.
### Light amendment — [ADR-0013](docs/decisions/0013-audit-trail-separated-postgres-append-only.md)
Two additions:
- New **"Reader endpoints"** subsection that enumerates the two-endpoint reader surface (list + stats), documents the Redis-cache caveat, and points at the new `admin.audit.stats.query` event family.
- The "events emitted in v1" table grows four rows it was previously missing on `main`: `admin.access_denied`, `admin.audit.query`, `admin.audit.stats.query`, `admin.users.query`.
No supersession, no new ADR. The decision shape (server-side aggregation + Redis cache + new audit event family) was settled in chat via `AskUserQuestion` before the implementation started; recording it here keeps the ADR honest without spawning a full ADR-0024 for what's essentially an extension of ADR-0013's reader surface.
## Notes for the reviewer
- **Why not factor `buildWhere` into a shared helper between `AuditReader` and `AuditStatsReader`?** Considered. The two readers' shapes diverge in non-trivial ways: `AuditReader` adds `LIMIT/OFFSET` parameters appended to the same parameter array, `AuditStatsReader` runs three queries that all share the same `WHERE` (no further params). A shared helper would have to either expose both shapes or hand back the raw clauses + params for callers to assemble — at which point the abstraction earns its weight back. Two ~50 LOC copies today, extraction when a third reader lands or when the shape diverges further.
- **Why not cap the time window when no filter is provided?** Honest disclosure beats clever defaults. The list endpoint also returns "everything matching the filters" with no protective cap; the stats endpoint follows the same posture. The Redis cache absorbs the cost when the same heavy query lands repeatedly; an admin running unfiltered queries at high rate will see flat latency after the first call. If we later observe a real perf issue, a `windowDays` parameter is a smaller change than retrofitting one across the API.
- **Why a `text` cast on `outcome` in the SQL?** Prisma's Postgres enum types come back as JS strings already, but the `outcome` column carries a Postgres enum (`audit.AuditOutcome`). The explicit `::text` is defensive — `$queryRawUnsafe`'s typing isn't enum-aware, and the cast keeps the projection unambiguous regardless of the driver's row-shape inference.
- **Why does the date round-trip through `Date.toISOString().slice(0, 10)`?** `date_trunc('day', ...)::date` returns a Postgres `date` that node-postgres surfaces as a JS `Date` at UTC midnight. The default `toJSON` serialises the full ISO timestamp with the timezone offset — which is not what the chart x-axis wants. Slicing to `YYYY-MM-DD` matches the SPA's chart bucket convention exactly.
- **No mention of the `actorIdHash` audit row for the stats endpoint?** It's the same hash flow as `adminAuditQuery` — the `actor.oid` from the session goes through `HashUserIdService` per ADR-0012's salt-based pseudonymisation. The same flow is exercised by the existing `adminAuditQuery` tests; the new `adminAuditStatsQuery` method just routes to `recordEvent` with a different `eventType`.
## Test plan
- [x] `pnpm nx test portal-bff` — **414 specs pass** (was 401; +13 new: 8 `AuditStatsReader` service + 5 controller `stats` endpoint).
- [x] `pnpm nx run portal-bff:lint` — clean.
- [x] `pnpm nx build portal-bff` — clean (webpack).
- [ ] **Manual smoke** — `pnpm nx serve portal-bff`, sign in to portal-admin with `Portal.Admin`:
- `curl http://localhost:3000/api/admin/audit/stats --cookie-jar /tmp/admin` returns the three projections.
- Verify the `admin.audit.stats.query` row in `audit.events` after the call (`SELECT * FROM audit.events WHERE event_type = 'admin.audit.stats.query' ORDER BY occurred_at DESC LIMIT 1`).
- Hit the endpoint twice in quick succession with the same filters → second call shows < 5 ms latency (cache hit, no DB transaction).
- Hit it with different filters → first call hits DB, second cache, third with same filters → cache hit.
- Stop Redis (`./infra/local/dev.sh stop redis`), hit the endpoint → still succeeds (cache miss + write swallowed), comes back live from DB.
## What's next
PR 2 — SPA Tabs UX (Table / Charts) + replace `dailyVolume() / outcomeBreakdown() / dailyByEventType()` (currently computed from `page().items`) with calls to this endpoint. The three computeds become signals filled by the HTTP call; the chart components on the Charts tab consume them unchanged.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #173
|
||
|
|
209f44d667 |
feat(portal-admin): audit dashboard — bar / donut / stacked-bar above the table (#172)
## Summary PR 3 (final) of the charts chantier — closes the loop on [ADR-0023](docs/decisions/0023-charts-d3-observable-plot.md) by wiring the three starter components shipped in #171 onto the `/audit` page. ``` PR 1 ✅ — ADR-0023 (decision + a11y contract + bundle plan). PR 2 ✅ — libs/shared/charts/ foundations + bar / donut / stacked-bar. PR 3 (this one) — /audit page integration: three charts above the existing table. ``` ## What lands ### Three computed aggregations on `AuditPage` ```ts dailyVolume() // (day: 'YYYY-MM-DD', count: number)[] outcomeBreakdown() // (outcome: string, count: number)[] dailyByEventType() // (day, eventType, count)[] — flat, the chart pre-pivots totalOnPage() // number for the donut centre label hasChartData() // gate the section out when the page is empty ``` All four derive from the **current page only** (`page()?.items`). No new BFF endpoint — server-side aggregations across the full filter set would need a `/api/admin/audit/stats` resource that's worth its own ADR + chantier when the use case appears. ### Section markup — [`audit.html`](apps/portal-admin/src/app/pages/audit/audit.html) A new `<section class="charts">` between the status bar and the existing table, gated on `hasChartData()`: ``` [At a glance — current page] [charts-note: "Aggregations are computed from the events currently loaded on this page only…"] ┌─ Events per day (bar) ─┬─ Outcome breakdown (donut) ─┐ ├─ Events per day, by event type (stacked bar) — wide ┤ └──────────────────────────────────────────────────────┘ ``` The stacked-bar tile gets `.chart-tile--wide` (spans both grid columns) since its legend benefits from the horizontal space. The two-column grid collapses to one column under 800 px viewports. ### SCSS — [`audit.scss`](apps/portal-admin/src/app/pages/audit/audit.scss) `.charts` shares the same surface tokens as `.filters` + `.table-wrap` (white / gray-800 background, 1 px border, 0.5 rem radius) so the page reads as one stack of related blocks. `.charts-grid` is `display: grid; grid-template-columns: repeat(2, 1fr)` with a `@media (max-width: 800px)` fallback to single-column. ### Project budget — [`apps/portal-admin/project.json`](apps/portal-admin/project.json) `anyComponentStyle` budget bumped from 5/6 KB warn/error to **6/8 KB**. `audit.scss` lands at ~6.5 KB after the charts grid additions, comfortably under the new ceiling. The old 5/6 KB threshold predated the charts row; this is a one-time accommodation, not a global relaxation. ### Tsconfig wiring — [`apps/portal-admin/tsconfig.app.json`](apps/portal-admin/tsconfig.app.json) `nx sync` added the new project reference to `libs/shared/charts/tsconfig.lib.json` after the `import { BarChart, … } from 'shared-charts'` in `audit.ts`. Standard plumbing. ### Spec — [`audit.spec.ts`](apps/portal-admin/src/app/pages/audit/audit.spec.ts) Three new assertions under a `charts` describe block: 1. The three `<lib-*-chart>` elements render when the page has data. 2. `<section class="charts">` is **absent** when the page is empty (no half-rendered donut on `{ total: 0, items: [] }`). 3. The donut's `.donut-center-label` reads the page's item count — verifies the `[centerLabel]` binding wired correctly. ## Notes for the reviewer - **Why charts only on the current page, not on the full result set?** Per the "Don't add features beyond what the task requires" rule from CLAUDE.md. The user's brief was "tester les composants sur la page Audit log", not "design a full analytics dashboard". The `.charts-note` paragraph explicitly says "computed from the events currently loaded on this page only" so the limitation is **disclosed**, not hidden. A future server-side `/api/admin/audit/stats` (with `audit_reader`-scoped queries + caching) is the natural follow-up if real investigative use exposes the per-page scope as a friction point. - **Why `<section>` with its own `<h2>` rather than just inline tiles?** A11y. Per [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md), document structure is a first-class concern. The charts are a meaningful sub-region of the page; landmark + heading help screen-reader users navigate. - **Why no i18n marks on the captions?** Portal-admin chrome stays in source locale per ADR-0020. The audit page has zero i18n markers today (filter labels, table headers, error messages are all plain English); the chart captions land in the same posture for consistency. If portal-admin grows i18n later, the captions get marked in the same sweep as the rest of the page. - **Bundle impact reality-check**: the chart-bearing lazy `audit` chunk grew from ~4.4 KB gzip to **84 KB gzip** with the chart deps loaded. ADR-0023 estimated ~65 KB; the actual is higher because Plot pulls in more `d3-scale-chromatic` interpolators than I projected. Still well under the 100 KB cap from [ADR-0017](docs/decisions/0017-performance-budgets-lighthouse-ci.md), but worth noting if a fourth chart type with its own d3 submodule lands (line / scatter / heatmap will push it further). - **No anonymous-state regression on the table**: the existing trace-link + actor-pivot behaviours from #163 are unaffected — the charts section sits between the status bar and the table, doesn't share any markup. ## Test plan - [x] `pnpm nx test portal-admin` — **57 specs pass** (was 54; +3 for the new charts assertions). - [x] `pnpm nx run-many -t lint test build --projects=portal-shell,portal-admin,shared-charts` — 9/9 tasks green. - [x] Audit lazy chunk: **84 KB gzip** (was 4.4 KB before the chart deps loaded); under the 100 KB lazy-chunk budget. - [ ] **Manual visual smoke** — sign in to portal-admin with `Portal.Admin` → navigate `/audit`: - The three charts render above the filter form, in a 2-column grid (stacked-bar spanning both). - The donut centre label reads the current page's event count. - Apply a filter that narrows the page to ~3 events → charts re-render with the narrowed dataset. - Click the `<details>` "Data table" disclosure under each chart → the tabular fallback expands with the exact rows. - Toggle dark mode in the footer → axis text + chart envelope swap; the donut's Cividis-equivalent palette kicks in on the categorical chart too (palette already swapped by `resolveTheme()` in the lib). - Filter to an empty result set → the `.charts` section disappears, the "No audit events match the current filters" empty-state stays. ## What's next Chantier closed. Three light follow-ups stay open but optional: - **Server-side aggregations** if the per-page scope becomes a real friction point. New BFF endpoint + likely a fourth `time-range` selector on the SPA. - **More chart types** (line / scatter / heatmap) when business modules ask for them. The lib's `_internal/` carries the a11y plumbing already; each new component is the ~50 LOC Plot wrapper + spec. - **Promote shared chart styling** to a fourth shared concern in `libs/shared/ui/` if a third app ever joins. Not urgent. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #172 |
||
|
|
eb8b65c7bc |
feat(shared-charts): foundations + bar / donut / stacked-bar components (#171)
## Summary Implementation of [ADR-0023](docs/decisions/0023-charts-d3-observable-plot.md) — the foundations of the workspace's chart library. PR 2 of the chantier: | PR | Périmètre | | --- | --- | | PR 1 ✅ | ADR-0023 — decision + a11y contract + bundle plan. | | **PR 2 (this one)** | `libs/shared/charts/` foundations + `<lib-bar-chart>`, `<lib-donut-chart>`, `<lib-stacked-bar-chart>`. | | PR 3 | Integration on the `/audit` page — daily-volume bar + outcome-breakdown donut + event-type-over-time stacked bar. | ## What lands ### Workspace deps ``` d3 — top-level toolkit, types via @types/d3 d3-shape — used directly by <lib-donut-chart> d3-scale-chromatic — colour-blind-safe palette source @observablehq/plot — declarative layer over D3, used by bar + stacked-bar ``` All four (+ matching `@types/*`) land in the workspace root `devDependencies`. Tree-shaken at build time per ADR-0023's bundle plan. ### New lib `libs/shared/charts/` ``` libs/shared/charts/src/lib/ ├── _internal/ ← single source of truth for the a11y contract │ ├── a11y.ts ← chartId, findChartSvg, injectSvgTitleDesc, prefersReducedMotion, resolveTheme │ ├── chart-envelope.scss ← shared figure / caption / fallback / dark-mode rules │ ├── chart-types.ts ← `ChartBaseInputs<T>` extended by each component │ └── palette.ts ← Viridis / Cividis (sequential) + ColorBrewer Set2 (categorical) ├── bar-chart/ ← Plot.barY ├── donut-chart/ ← raw d3-shape (pie + arc); Plot has no donut mark └── stacked-bar-chart/ ← Plot.barY with `fill: <seriesKey>` (auto-stacked, legend on) ``` ### A11y contract baked in for v1 Per ADR-0023's six commitments, every chart component produces (and unit-tests for): 1. `<figure role="img" aria-labelledby aria-describedby>` wrapping the SVG. 2. SVG `<title>` + `<desc>` as the **first two children** — injected post-render via `injectSvgTitleDesc` because Plot doesn't emit them itself. `findChartSvg` handles both Plot output shapes (bare SVG, or `<figure>` wrapping a legend + SVG for `legend: true` configs). 3. A `<details>` disclosure with a `<table>` rendering every data point — the keyboard-navigable / screen-reader-friendly fallback for non-visual users. 4. Palette from `_internal/palette.ts` only — Viridis / Cividis for sequential, ColorBrewer Set2 for categorical. Both colour-blind-safe. 5. AA-contrast axis text via `:where(.dark)` flips in `_internal/chart-envelope.scss`. 6. `prefers-reduced-motion` → `data-no-transitions` marker on the SVG, CSS strips animations + transitions. A custom ESLint rule in [`libs/shared/charts/eslint.config.mjs`](libs/shared/charts/eslint.config.mjs) bans direct imports of `d3-scale-chromatic` outside `_internal/palette.ts` so a future contributor can't bypass the colour-blind-safe contract. ### Component contract Every `<lib-*-chart>` exposes the same Signal-based shape per ADR-0023: ```ts [data]: readonly T[]; [caption]: string; [description]: string; [ariaLabel]: string; [colorScheme]?: 'sequential' | 'categorical'; // + chart-specific keys (xKey, yKey, categoryKey, valueKey, seriesKey, …) ``` Re-renders triggered by Angular's `effect()` on input changes; the previous SVG is `replaceChildren`-d out so there's no DOM accumulation across data updates. ## Notes for the reviewer - **Why three SCSS files importing one shared envelope?** Extracted at the third consumer per CLAUDE.md's "three similar lines is better than a premature abstraction" rule — bar + donut + stacked-bar share ~70 LOC of figure / caption / fallback / dark-mode chrome. `_internal/chart-envelope.scss` is the consolidation; each chart's `.scss` is now 4-20 LOC of chart-specific tweaks. - **Why is `<lib-donut-chart>` raw D3 rather than Plot?** Plot's design philosophy explicitly excludes pie/donut marks ("a bar chart is almost always more legible"). The audit-log outcome breakdown reads naturally as a donut (the centre carries the total). Raw `d3-shape` is the lower-level fallback ADR-0023 reserves precisely for this kind of case; the component's API is identical to the Plot-backed siblings. - **Why does the donut also stamp per-slice `<title>`?** Belt-and-suspenders. The top-level SVG `<title>` reads the caption; per-slice `<title>` reads "category: value" on hover (the SVG-native tooltip convention) for keyboard / screen-reader users who land on a specific slice. - **Why no `pnpm.overrides` adjustment for `d3-*` transitives?** None of the new deps brought a vulnerability in this install. The `pnpm audit --audit-level=moderate` gate from #161 stays green. - **What's deliberately deferred to PR 3?** The `/audit`-page integration — data aggregation from the current page (`AdminAuditPage` rows already loaded), the actual `<lib-*-chart>` placements above the existing table, the i18n strings for the captions / descriptions / aria-labels. No mock SAMPLE data shipped in this PR — every test uses local fixtures so the lib stays decoupled from any specific consumer. ## Test plan - [x] `pnpm nx test shared-charts` — **13 specs pass** across the three components. - [x] `pnpm nx lint shared-charts` — clean, including the custom `no-restricted-imports` guard on `d3-scale-chromatic`. - [x] `pnpm nx build shared-charts` — clean (TS strict + ng-packagr). - [x] `pnpm nx run-many -t lint test build --projects=shared-charts,portal-shell,portal-admin` — 12/12 tasks green. - [x] `pnpm nx build portal-shell --configuration=production` — i18n-strict prod build clean. The lib ships no i18n marks (axis labels are caller-supplied per ADR-0023's i18n posture), so no xlf entry change here. - [ ] **Visual smoke (deferred to PR 3 when the components are placed on `/audit`)** — `<figure>` + caption visible, SVG `<title>` exposed by VoiceOver / NVDA, `<details>` fallback expands to a table, dark-mode toggle flips axis text + Cividis palette, `prefers-reduced-motion` strips Plot's fade-in. ## What's next PR 3 wires the three components onto the `/audit` page: aggregates `AdminAuditPage.items` client-side into the three required shapes (daily totals, outcome counts, daily-by-event-type pivots), places them above the existing filter form, ships the i18n strings for the captions and descriptions in `messages.fr.xlf`. Bundle impact on the lazy `audit` chunk gets verified there (the ~65 KB gzip plan from the ADR). --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #171 |
||
|
|
7ee7b2dadf |
docs(adr-0023): charts and dashboards — d3 + observable plot (#170)
## Summary
Records the decision to use **D3 + Observable Plot**, wrapped in a new `libs/shared/charts/`, as the chart toolkit shared by `portal-shell` and `portal-admin`. ADR-only — implementation lands as the next chantier(s).
This is staged as a 3-PR chantier per the agreed plan:
| PR | Périmètre |
| --- | --- |
| **PR 1 (this one)** | ADR-0023 — decision + a11y contract + bundle plan. |
| PR 2 | `libs/shared/charts/` foundations + 3 starter components (`<lib-bar-chart>`, `<lib-donut-chart>`, `<lib-stacked-bar-chart>`). |
| PR 3 | Integration on the `/audit` page — daily-volume bar + outcome-breakdown donut + event-type-over-time stacked bar. |
## What lands
### [`docs/decisions/0023-charts-d3-observable-plot.md`](docs/decisions/0023-charts-d3-observable-plot.md)
Full MADR 4.0.0 record. Highlights:
- **Choice**: D3 + Observable Plot, both from Mike Bostock / Observable Inc., both MIT, both past 1.0. Plot covers ~80 % of standard charts in declarative one-liners; D3 stays the escape hatch for bespoke viz (heatmap, sankey, …) inside the same lib.
- **Why not D3 alone**: ~250 LOC per chart × 4-5 types × a11y discipline = sustained code investment before the first dashboard ships.
- **Why not ECharts / Chart.js**: 600 KB minified + canvas-rendered + an `aria` plugin afterthought (ECharts), or narrower vocabulary + brittle dark-mode (Chart.js). Both furthest from the Angular-Signals-zoneless idiom the rest of the workspace runs on.
- **A11y contract** is baked into `_internal/` (palette, tabular fallback, SVG `<title>` / `<desc>` builders) so every chart inherits WCAG 2.2 AA + AAA-targeted compliance from the lib, not from contributor discipline. Six commitments, each unit-tested per chart component.
- **Bundle plan**: ~65 KB gzip added to a chart-bearing lazy chunk (d3 modules tree-shaken + Plot + thin wrapper) — well under [ADR-0017](docs/decisions/0017-performance-budgets-lighthouse-ci.md)'s 100 KB cap.
- **Component contract**: every `<lib-*-chart>` exposes the same Signal-based input shape (`[data]`, `[caption]`, `[description]`, `[ariaLabel]`, `[colorScheme]`) regardless of whether Plot or raw D3 powers the rendering.
### [`docs/decisions/README.md`](docs/decisions/README.md)
ADR-0023 added to the index table.
### [`CLAUDE.md`](CLAUDE.md)
- "Architecture (recorded in ADRs)" gains a "Charts + dashboards" bullet describing the lib + a11y baseline + bundle posture.
- "Repository status" bumps the ADR range to `0001 → 0023`.
- "Still on the roadmap" gains the charts implementation entry pointing at this ADR.
## Notes for the reviewer
- **Why honour the user's D3 preference rather than recommend pure ECharts?** D3 (and by extension Plot) is the closest match to the project's tech bar ("stable, recognized, battle-tested") for data-viz on the web; it's also the user's stated preference, and Plot's higher-level layer eliminates the "250 LOC per chart" cost that would otherwise push us toward an alternative. The ADR explicitly walks through ECharts + Chart.js as runners-up so future challengers see the trade-offs we chose against.
- **Why a single shared lib rather than per-app charts?** Both SPAs (portal-shell + portal-admin) will host dashboards. The chart vocabulary, a11y contract, palette, and theme integration are identical between the two — duplicating into app-local code would invite drift. The lib stays at `libs/shared/charts/` next to `libs/shared/ui/`.
- **Why the `_internal/` folder for cross-cutting code?** Single source of truth for the colour palette and the a11y plumbing. A lint rule (added in PR 2) will ban consumers from importing `d3-scale-chromatic` directly so the colour-blind-safe palette stays the only path.
- **Why no ADR amendment to ADR-0016 / ADR-0017?** Both are binding constraints, not superseded. The new ADR operationalises both for the chart surface; cross-references in the "Related ADRs" section make that explicit.
## Test plan
- [x] ADR validates as MADR 4.0.0 (frontmatter, section order, tag vocabulary).
- [x] No code touched — lint / test / build matrix unaffected.
- [x] `docs/decisions/README.md` index updated in the same change per the [ADR conventions](docs/decisions/README.md#conventions).
- [ ] Review for trade-off accuracy: are the bundle estimates fair? Is the "Plot covers ~80 % of standard charts" framing defensible against the user's mental model of D3?
- [ ] Implementation chantier (PR 2) lands directly behind this if accepted: `pnpm add -w d3 @observablehq/plot @types/d3`, `libs/shared/charts/` scaffold via `pnpm nx g @nx/angular:library --name=shared-charts --directory=libs/shared/charts --standalone=true --unitTestRunner=vitest-analog --tags="scope:shared,type:shared" --no-interactive`, then the 3 starter components.
## What's next
If accepted as-is, PR 2 (lib foundations + 3 starter components) follows. If a reviewer wants to push back on D3-vs-ECharts or on the a11y contract's strictness, this is the right PR to surface that — no implementation has started.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #170
|
||
|
|
6c42d4c232 |
fix(deps): update nestjs to v11.1.21 (#169)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@nestjs/common](https://nestjs.com) ([source](https://github.com/nestjs/nest/tree/HEAD/packages/common)) | dependencies | patch | [`11.1.19` -> `11.1.21`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.1.19/11.1.21) | | [@nestjs/core](https://nestjs.com) ([source](https://github.com/nestjs/nest/tree/HEAD/packages/core)) | dependencies | patch | [`11.1.19` -> `11.1.21`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.1.19/11.1.21) | | [@nestjs/platform-express](https://nestjs.com) ([source](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express)) | dependencies | patch | [`11.1.19` -> `11.1.21`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-express/11.1.19/11.1.21) | | [@nestjs/testing](https://nestjs.com) ([source](https://github.com/nestjs/nest/tree/HEAD/packages/testing)) | devDependencies | patch | [`11.1.19` -> `11.1.21`](https://renovatebot.com/diffs/npm/@nestjs%2ftesting/11.1.19/11.1.21) | --- ### Release Notes <details> <summary>nestjs/nest (@​nestjs/common)</summary> ### [`v11.1.21`](https://github.com/nestjs/nest/releases/tag/v11.1.21) [Compare Source](https://github.com/nestjs/nest/compare/v11.1.20...v11.1.21) ##### v11.1.21 (2026-05-14) ##### Bug fixes - `core` - [#​16948](https://github.com/nestjs/nest/pull/16948) fix(core): settle skipped provider initialization ([@​yudin-s](https://github.com/yudin-s)) ##### Committers: 1 - Serge Yudin ([@​yudin-s](https://github.com/yudin-s)) ### [`v11.1.20`](https://github.com/nestjs/nest/releases/tag/v11.1.20) [Compare Source](https://github.com/nestjs/nest/compare/v11.1.19...v11.1.20) ##### v11.1.20 (2026-05-13) ##### Bug fixes - `core`, `testing` - [#​16939](https://github.com/nestjs/nest/pull/16939) fix(core): fix deeply nested transient providers resolution ([@​kamilmysliwiec](https://github.com/kamilmysliwiec)) - `core` - [#​16861](https://github.com/nestjs/nest/pull/16861) fix(core): fix [@​Sse](https://github.com/Sse) losing events on complete ([@​MatthiasBrehmer](https://github.com/MatthiasBrehmer)) - [#​16753](https://github.com/nestjs/nest/pull/16753) fix(core): defer sse writehead until after lifecycle completes ([@​jkalberer](https://github.com/jkalberer)) - [#​16782](https://github.com/nestjs/nest/pull/16782) fix(core): use strict null check for SSE message id ([@​burhanharoon](https://github.com/burhanharoon)) - `microservices` - [#​16850](https://github.com/nestjs/nest/pull/16850) fix(microservices): ServerRMQ crashes at boot when [@​MessagePattern](https://github.com/MessagePattern)(undefined) is combined with wildcards: true ([@​lavieennoir](https://github.com/lavieennoir)) - `common` - [#​16845](https://github.com/nestjs/nest/pull/16845) fix(common): accept zero timestamp in parse date pipe ([@​Mysh3ll](https://github.com/Mysh3ll)) - `platform-socket.io` - [#​16742](https://github.com/nestjs/nest/pull/16742) fix(socket.io): Deduplicate disconnect listener in bindMessageHandlers ([@​fru1tworld](https://github.com/fru1tworld)) ##### Enhancements - `microservices` - [#​16676](https://github.com/nestjs/nest/pull/16676) feat(microservices): add return buffers option for binary data ([@​Forceres](https://github.com/Forceres)) - [#​16826](https://github.com/nestjs/nest/pull/16826) feat(microservices): handle rmq blocked/unblocked connection events ([@​thisalihassan](https://github.com/thisalihassan)) - `common` - [#​16902](https://github.com/nestjs/nest/pull/16902) fix(common): filetype validator buffer message ([@​QusaiAlbonni](https://github.com/QusaiAlbonni)) - `platform-express` - [#​16844](https://github.com/nestjs/nest/pull/16844) feat(platform-express): add defParamCharset to MulterOptions ([@​starnayuta](https://github.com/starnayuta)) ##### Dependencies - `platform-ws` - [#​16941](https://github.com/nestjs/nest/pull/16941) chore(deps): bump ws from 8.20.0 to 8.20.1 ([@​dependabot\[bot\]](https://github.com/apps/dependabot)) ##### Committers: 13 - Ali Hassan ([@​thisalihassan](https://github.com/thisalihassan)) - Burhan Haroon⚡ ([@​burhanharoon](https://github.com/burhanharoon)) - Dmytro Khyzhniak ([@​lavieennoir](https://github.com/lavieennoir)) - Harsh Rathod ([@​harshrathod50](https://github.com/harshrathod50)) - IlyaCredo ([@​Forceres](https://github.com/Forceres)) - Kamil Mysliwiec ([@​kamilmysliwiec](https://github.com/kamilmysliwiec)) - Mysh3ll ([@​Mysh3ll](https://github.com/Mysh3ll)) - [@​MatthiasBrehmer](https://github.com/MatthiasBrehmer) - [@​QusaiAlbonni](https://github.com/QusaiAlbonni) - [@​jkalberer](https://github.com/jkalberer) - [@​pazaderey](https://github.com/pazaderey) - fru1tworld ([@​fru1tworld](https://github.com/fru1tworld)) - starnayuta ([@​starnayuta](https://github.com/starnayuta)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #169 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
1edf154b67 |
fix(deps): update dependency express-rate-limit to v8.5.2 (#168)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | dependencies | patch | [`8.5.1` -> `8.5.2`](https://renovatebot.com/diffs/npm/express-rate-limit/8.5.1/8.5.2) | --- ### Release Notes <details> <summary>express-rate-limit/express-rate-limit (express-rate-limit)</summary> ### [`v8.5.2`](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v8.5.2) [Compare Source](https://github.com/express-rate-limit/express-rate-limit/compare/v8.5.1...v8.5.2) You can view the changelog [here](https://express-rate-limit.mintlify.app/reference/changelog). </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #168 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
b61e550d59 |
chore(deps): update dependency lint-staged to v17.0.5 (#167)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [lint-staged](https://github.com/lint-staged/lint-staged) | devDependencies | patch | [`17.0.4` -> `17.0.5`](https://renovatebot.com/diffs/npm/lint-staged/17.0.4/17.0.5) | --- ### Release Notes <details> <summary>lint-staged/lint-staged (lint-staged)</summary> ### [`v17.0.5`](https://github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1705) [Compare Source](https://github.com/lint-staged/lint-staged/compare/v17.0.4...v17.0.5) ##### Patch Changes - [#​1792](https://github.com/lint-staged/lint-staged/pull/1792) [`1f67271`](https://github.com/lint-staged/lint-staged/commit/1f672718b6fa67e0f00aafe107cb9f084f4d9102) - Correctly set the `--max-arg-length` default value based on the running platform. This controls how very long lists of staged files are split into multiple chunks. </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #167 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
0435fec10a |
feat(portal-admin): jaeger deep link on trace_id + actor-pivot on actor_id_hash (#166)
## Summary
Turns the audit-log table's `trace_id` and `actor_id_hash` columns from inert text into the two pivots an investigator actually needs:
- **trace_id** → Jaeger deep link (opens in a new tab). Closes the "join audit + traces by trace_id" loop from [ADR-0012](docs/decisions/0012-observability-pino-opentelemetry.md) / [ADR-0013](docs/decisions/0013-audit-trail-separated-postgres-append-only.md) without any new BFF surface.
- **actor_id_hash** → click to refilter the table on that single actor. "Show me everything else this user did" stays in the page; no copy-paste loop.
## What lands
### Trace-id deep link to Jaeger
[`audit.html`](apps/portal-admin/src/app/pages/audit/audit.html#L162-L173) — the cell becomes an `<a target="_blank" rel="noopener noreferrer">` pointing at `${environment.jaegerBaseUrl}/trace/<traceId>`. Anonymous events (`traceId === null`) keep the dash placeholder.
[`environment.ts`](apps/portal-admin/src/environments/environment.ts) gains `jaegerBaseUrl`. Dev defaults to `http://localhost:16686` (matches the compose `observability` profile from `infra/local/dev.compose.yml`). Per-env replacement picks up whatever trace backend the future infrastructure ADR settles on — Tempo, Grafana Cloud, on-prem Jaeger; the SPA-side wiring doesn't care.
### Actor-pivot click
[`audit.html`](apps/portal-admin/src/app/pages/audit/audit.html#L146-L160) — non-null `actorIdHash` becomes a `<button>` styled to read inline like the hash text (`.actor-hash--clickable`: button reset + dotted-underline hover + brand-colored focus ring). Click → `filterByActor(hash)` sets the existing `actorIdHash` filter signal, resets offset to 0, and re-runs the query. Each pivot still emits its own `admin.audit.query` audit row server-side (per [ADR-0020](docs/decisions/0020-portal-admin-app.md)) so the drill is itself auditable.
Anonymous rows keep the `(anonymous)` plain-text rendering — there's no useful filter value to pivot on.
## Why not inline-expand Pino log lines under the row
Considered, deferred. The BFF's Pino output goes to **stdout only** today; standing up a queryable log aggregator (Loki, OpenSearch, …) is a separate infrastructure chantier with its own ADR. The Jaeger jump-off carries ~99 % of the investigator's needs anyway — the trace already contains span attributes (`db.statement`, `http.status_code`, exception events) for the same request scope; Pino lines on top of that would be redundant for most investigations.
When the log aggregator does land, the inline-expand model can come back as a follow-up: `GET /api/admin/logs?traceId=<id>` + an expand affordance on the same row. The current Jaeger anchor and the future inline-logs would naturally coexist (different drills, both surfaced on the same `trace_id`).
## Notes for the reviewer
- **Why a `<button>` for the actor cell rather than an `<a>`?** The action is an in-page filter change, not a navigation. Buttons keep keyboard activation (Enter / Space), don't pollute browser history, and screen readers announce "Filter the table on hash(jane), button" rather than a misleading link role.
- **Why the dotted-underline hover for the actor, but solid-underline for trace?** Different affordances. The trace anchor is a permanent link to an external resource (Jaeger UI), so the solid underline matches the universal "link" convention. The actor button is an inline pivot that *mutates state* — the dotted underline + hover-fill conveys "this does something subtle within the page" without screaming "link".
- **CSS guardrails preserved**: focus rings on both elements, brand-color tokens (light + dark), tap targets meet the AAA 44×44 minimum (the button reset preserves the line-height + the `cell-actor` padding ≥ 12 px on each side).
- **No new i18n strings.** `title` attributes are hover hints, not screen-reader-essential — the underlying hash + traceId are the actual semantic content. The "(anonymous)" string and the dash placeholder were already in the template.
- **No BFF change.** This whole PR is SPA-side only. The audit endpoint already returns `traceId` and `actorIdHash` in every row.
## Test plan
- [x] `pnpm nx run-many -t lint test build --projects=portal-admin` — green.
- [x] **54 portal-admin specs pass** (was 50; +4 for the four new behaviours).
- [x] Lazy `audit` chunk: 18.26 → ~18.5 KB raw / 4.44 KB gzip — comfortably under the per-chunk budget.
- [ ] **Manual smoke**:
- Sign in to portal-admin with `Portal.Admin` → open `/audit`.
- Click any trace_id → new tab opens at `http://localhost:16686/trace/<id>` (assuming `./infra/local/dev.sh up observability` is running so Jaeger is up).
- Anonymous rows show `—` for trace, `(anonymous)` (plain text, not clickable) for actor.
- Click any non-anonymous actor hash → the table refreshes filtered on that hash, the "Actor id hash" filter input above shows the same value, page jumps to offset 0.
- Tab through a row: timestamp / event are plain text; outcome badge skipped (not interactive); actor button gets focus ring; trace link gets focus ring; payload `<details>` summary gets focus ring.
## Follow-ups (optional)
- When the log aggregator ADR lands, extend the trace cell to also offer an inline-expand of Pino lines for that trace. Jaeger anchor stays as the primary affordance.
- A similar treatment on the `/users` page (clicking a row's `oid` to "show me this user's audit trail") is the natural sibling. Defer until there's an investigator workflow that asks for it — premature otherwise.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #166
|
||
|
|
6f26bcdd65 |
feat(shared-ui): move the role label from the portal-shell sidebar into the user menu (#165)
## Summary
Moves the "Role: …" widget from the bottom of the `portal-shell` sidebar into the user-menu panel header. Result: the role chip appears only when the reader is authenticated (the user menu only renders in that state), removing the noise of "Role: Anonymous" on signed-out traffic and de-duplicating the surface that already carries displayName / username / capability-gated actions.
```
Before: sidebar bottom → "Role: Anonymous" (always rendered, even signed-out)
After: user-menu panel → • <role chip> (only when authenticated)
```
## What lands
### Shared component — [`UserMenu`](libs/shared/ui/src/lib/user-menu/) gets an optional `role` input
When set, a small brand-tinted pill (`data-testid="user-menu-role"`) renders inside the panel header below the username. When undefined, the entire chip is omitted — keeps backwards compat with any future caller that doesn't carry a role concept.
```ts
readonly role = input<string | undefined>(undefined);
```
Visually echoes the `.role-chip` already used on the admin `/profile` page (rounded brand-tinted pill), tightened for the menu header density and `align-self: flex-start` so the pill doesn't stretch.
### Portal-shell
- **Header** ([header.ts](apps/portal-shell/src/app/components/header/header.ts)) gains a `roleLabel` computed that derives `Administrator` / `User` from `CapabilitiesService.canAccessAdmin` — the same logic that previously lived in the sidebar. The template binds it as `[role]="roleLabel()"` on `<lib-user-menu>`.
- **Sidebar** ([sidebar.ts](apps/portal-shell/src/app/components/sidebar/sidebar.ts), [sidebar.html](apps/portal-shell/src/app/components/sidebar/sidebar.html)) loses the role widget entirely: HTML block, `roleLabel` + `roleAriaLabel` computeds, and the `AuthService` + `CapabilitiesService` injections (the sidebar no longer needs them).
- **Sidebar spec** drops its three "role label" tests, grows a guard test asserting `data-testid="sidebar-role"` no longer exists, and reverts to the pre-#151 setup (no Http testing infrastructure — the sidebar no longer fires `/auth/me` or `/me/capabilities`).
- **Header spec** gains two assertions for the role chip inside the open menu panel (`Administrator` when `canAccessAdmin: true`, `User` otherwise).
### Portal-admin
- **Header** ([header.ts](apps/portal-admin/src/app/components/header/header.ts)) passes a **hardcoded `'Administrator'`** through `[role]`. Every reader who reaches the admin app already carries `Portal.Admin` (it's the `AdminRoleGuard` precondition for `/api/admin/*`); no need to re-derive. No i18n marks per ADR-0020's source-locale-only chrome.
### i18n
Five translation units gone from [`messages.fr.xlf`](apps/portal-shell/src/locale/messages.fr.xlf):
```
sidebar.role.anonymous
sidebar.role.administrator
sidebar.role.user
sidebar.role.aria
sidebar.role.label
```
Replaced by two new units under a generic prefix (the new owners are the user menu in either app, not the sidebar):
```
common.role.administrator
common.role.user
```
The `Anonymous` string is gone too — the chip is hidden on anonymous traffic, the label served no purpose without the user menu rendering it.
## Notes for the reviewer
- **Why a generic `common.role.*` prefix rather than `userMenu.role.*`?** The strings are reusable in any context that needs a curated role display (profile page header in a future PR, an audit log "actor role" column, …). Generic prefix avoids the next rename when a second consumer lands.
- **Why hardcode "Administrator" for portal-admin rather than reading `roles`?** The admin SPA's `CurrentUser.roles` carries the raw Entra role string (`Portal.Admin`). Displaying that in the menu header would leak the internal nomenclature into the UI — fine for the `/profile` page (it's explicit context there), wrong for the casual menu glance. The hardcoded label keeps the menu consistent with portal-shell ("Administrator" in both surfaces). If a non-admin role ever reaches portal-admin (the guard rejects it today, but the surface could evolve), this is the one place to revisit.
- **No CapabilitiesService dependency on portal-admin.** The admin app doesn't import the service — its session already exposes `roles` on `/api/admin/auth/me`, and the chip is unconditional. Keeps the admin bundle untouched by the user-portal capabilities plumbing.
- **A11y check.** The role chip is plain text inside the menu panel; the menu panel itself carries `role="menu"` + `aria-label`. The chip doesn't need an extra label — it reads "Administrator" / "User" in context after the displayName and username, which conveys the meaning. No focus state needed (not interactive).
## Test plan
- [x] `pnpm nx run-many -t lint test build --projects=shared-ui,portal-shell,portal-admin` — green.
- `shared-ui` — **10 specs pass** (was 8; +2 for the role chip).
- `portal-shell` — **43 specs pass** (header +2, sidebar -3 + 1 guard = -2 net, balanced to +0 on the total → 43 unchanged).
- `portal-admin` — **50 specs pass** (no spec edits; the `[role]` binding is exercised by the existing user-menu spec via the shared component).
- [x] `pnpm nx build portal-shell --configuration=production` — i18n-strict prod build passes; confirms the xlf cleanup + new `common.role.*` keys are consistent with usage.
- [ ] **Manual smoke**:
- **portal-shell anonymous**: sidebar bottom shows only the collapse toggle (no role line). Click "Sign in" → land back signed in, sidebar bottom unchanged.
- **portal-shell signed in (non-admin)**: avatar → open menu → header shows `Signed in as / displayName / username / [User]` chip.
- **portal-shell signed in (admin)**: same, chip reads `Administrator`, and the menu carries the `Open Portal Admin` row above Sign out.
- **portal-admin signed in**: avatar → open menu → `[Administrator]` chip regardless of which admin user signed in.
- Tabbing across the sidebar bottom no longer pauses on a non-interactive `<p>` element — directly hits the collapse toggle button.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #165
|
||
|
|
10c80f189d |
feat(portal-shell): align theme switcher trigger with locale switcher shape (#164)
## Summary Aligns the theme-switcher trigger on the same chip shape as the locale-switcher. Both controls live side-by-side in the footer's device-prefs cluster (#162); the visual mismatch (round icon button vs. chip) made them read as two unrelated widgets rather than one family. ``` Before: [☀] (round icon button, 44×44) After: [☀ System ▾] (chip — icon + label + chevron) ``` The leading icon stays driven by `currentIcon()` so a **sun / moon / monitor** glyph still flips with the selected mode — that's the visual feedback the original icon-only design existed for, and it's preserved. ## What lands ### [`theme-switcher.html`](apps/portal-shell/src/app/components/theme-switcher/theme-switcher.html) The round Tailwind-utility icon button becomes a `.theme-switcher__trigger` chip with three children — current-mode icon (size 14), localised mode label, `chevron-down` (size 12). Same children layout as `.locale-switcher__trigger`. ### [`theme-switcher.scss`](apps/portal-shell/src/app/components/theme-switcher/theme-switcher.scss) Adds `.theme-switcher__trigger` mirroring `.locale-switcher__trigger` rule-for-rule: same chip metrics (min-height 2.75rem for the AAA 44×44 tap target via vertical-padding overflow inside the thin footer), same hover/focus tokens, same dark-mode swap. Two copies rather than a shared `_chip-trigger.scss` partial — two switchers in one app is below the "three similar things" threshold CLAUDE.md sets for extraction. Promotes when a third switcher lands. ## Notes for the reviewer - **No spec changes**. The existing assertions check `button[aria-haspopup="menu"]` + the aria-label content (`"Theme: <current> (open menu)"`). Both preserved — the trigger button still has `aria-haspopup="menu"` from `cdkMenuTriggerFor`, and `triggerAriaLabel()` is unchanged. - **No new i18n strings**. The chip text reuses `currentLabel()`, which already returns `Light` / `Dark` / `System` from the existing `theme.mode.{light,dark,system}` translation units (they were used in the dropdown menu items before this PR; now they also drive the trigger label). Prod i18n-strict build passes. - **Why mirror, not refactor into a shared partial?** Two switchers, identical chip shape — extracting now would be premature abstraction per [CLAUDE.md](CLAUDE.md). When a third switcher lands (an accessibility-panel toggle is the most likely candidate per [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md)'s preferences-panel plan), `_chip-trigger.scss` or a `<lib-chip-trigger>` component starts to pay off. The two copies today are mechanical and live next door — easy to keep in sync. - **No `portal-admin` impact.** No theme switcher there (admin chrome is brand-primary-600 hardcoded for now); no change needed. ## Test plan - [x] `pnpm nx run-many -t lint test build --projects=portal-shell` — green; **43 specs pass** (unchanged from main). - [x] `pnpm nx build portal-shell --configuration=production` — i18n-strict prod build passes. - [ ] **Manual visual smoke** — `pnpm nx serve portal-shell`, open the home page in a browser, confirm: - The theme switcher in the bottom-right of the footer is now a chip with the current mode icon + label + chevron, matching the locale chip beside it. - Hovering both switchers shows the same brand-primary hover color (light + dark mode). - Clicking the theme chip still opens the menu with three options (Light / Dark / System), the current option still carries a `✓` check. - Selecting Dark → trigger icon flips to moon + label flips to "Dark" + page goes dark. Switching to System: trigger icon flips to monitor + label flips to "System". - Tabbing across the footer hits accessibility link → locale → theme in that order, focus rings are identical between the two switchers. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #164 |
||
|
|
bba1703622 |
fix(deps): scope the vite < 7 override to vitepress so @angular/build keeps vite 8 (#163)
## Summary Hotfix on the override added in [#161](#161). The unconditional `"vite": ">=6.4.2 <7"` was too broad — it downgraded **every** vite consumer in the workspace to vite 6.4.2, including `@angular/build@21.2.11`. Angular's dev-server plugin (`angular-memory-plugin.js > loadViteClientCode`) monkey-patches Vite's client error-overlay code; the patch targets vite 8.x's internal layout, fails against vite 6, and the home page blank-screens with: ``` [vite] Internal server error: Failed to update Vite client error overlay text. at loadViteClientCode (angular-memory-plugin.js:140:31) ``` …on the very first request to `pnpm nx serve portal-shell`. ## Fix One-line change in [`package.json`](package.json) → `pnpm.overrides`: ```diff - "vite": ">=6.4.2 <7", + "vitepress>vite": ">=6.4.2 <7", ``` pnpm's `parent>child` selector syntax scopes the override to vitepress's transitive resolution only. Other vite consumers (Angular, Nx, Vitest, the analog plugin) follow their own peer constraints and resolve to vite 8.0.13 again. ## Resolution after the fix | Consumer | vite version | Why | | --- | --- | --- | | `vitepress 1.6.4` | **6.4.2** | Override target — keeps VitePress 1.x off rolldown-vite | | `@angular/build 21.2.11` | **8.0.13** | Restored; its dev-server plugin needs vite 8's client layout | | `@nx/vite`, `@analogjs/vite-plugin-angular`, Vitest | **8.0.13** | Restored | | `@vitejs/plugin-basic-ssl` (analog transitive) | 7.3.2 | Its own peer range; HTTPS-cert helper only, not the dev-server host. Doesn't affect us. | ## Why this regression didn't surface in #161's CI The `docs-site.yml` workflow and `pnpm exec nx run-many -t lint test build` exercise the **production build** paths. Vite 6 ↔ Angular-build 21.2's monkey-patch incompatibility is a **dev-server-only** failure (the prod Rollup pipeline doesn't go through `loadViteClientCode`). CI couldn't have caught it; manual `nx serve portal-shell` is the only repro path. Lesson logged. ## Test plan - [x] `pnpm install` — clean. Lockfile diff confirms: vite 6.4.2 only under vitepress, vite 8.0.13 restored everywhere else. - [x] `pnpm audit --audit-level=moderate` — no known vulnerabilities. - [x] `pnpm docs:build` — ~9.5 s, Mermaid still renders in ADR-0009's HTML (regression fence passes). - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell,portal-admin,portal-bff,shared-ui,shared-state,feature-auth` — green. - [x] **`pnpm nx serve portal-shell`** — boots cleanly, `curl http://localhost:4200/` returns HTTP 200 with the SPA shell, no `loadViteClientCode` error in the dev log. (This is the path that broke.) - [ ] **Manual smoke (the user's repro)** — `pnpm nx serve portal-shell`, open the home page in a browser, confirm: - No "Failed to update Vite client error overlay text" in the browser DevTools console. - Page renders; navigating to `/accessibility`, `/profile` works. - Theme switcher in the footer toggles light / dark / system; locale switcher likewise. ## Notes for the reviewer - **Lesson on override scoping**: the version-selector form (`"package@<vuln-range>": "patched"`) is the safest default — it only kicks in for the vulnerable range. The unconditional form (`"package": "<range>"`) is a sledgehammer and should be reserved for cases where the version-selector form genuinely can't express the intent (as was the case in #161 where the resolved version was already past the vulnerable range, so the selector was a no-op). When the unconditional form is needed, **always scope to a parent** (`"parent>package": …`) to avoid the workspace-wide blast radius. - **No documentation change in this PR**: development.md's "Transitive vulnerabilities — `pnpm.overrides`" subsection (from #160) is still accurate. A follow-up edit could add a "scope to a parent when possible" sentence; not blocking. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #163 |
||
|
|
076cfb67a6 |
feat(portal-shell): move theme switcher to footer, drop redundant settings icon (#162)
## Summary Three coupled chrome adjustments in `portal-shell`'s shell layout, all motivated by the same realisation that the UserMenu introduced in #149 made the header's standalone Settings icon redundant and surfaced an inconsistency in the theme switcher's placement (header for everyone, soon to be split between header and user-menu depending on auth state). ## What lands ### 1. Drop the standalone Settings button from the header The UserMenu already carries a "Settings (Soon)" row for authenticated users, and Settings has no meaning for anonymous traffic. The header icon pointed nowhere; keeping it doubled the surface and forced readers to guess which control to use. `header.action.settings` is removed from [`messages.fr.xlf`](apps/portal-shell/src/locale/messages.fr.xlf) in the same step — the i18n-strict prod build is now back in sync with the source. ### 2. Move `<app-theme-switcher>` from the header to the footer The switcher now lives beside `<app-locale-switcher>` in a single **device-prefs cluster** on the right side of the footer. Two reasons: - **Consistency across auth states.** The alternative — keep it in the header for anonymous, move it into the user-menu once authenticated — was the original temptation. Rejected: changing the location of an identical control depending on whether the user is signed in violates Jakob's law and breaks muscle memory. Especially bad for an a11y-first platform per [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md): a reader who relies on dark mode shouldn't have to expand a menu they don't yet recognise on first visit to escape a flashing white background. - **Precedent.** [ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md) already established the footer as the home for ambient device preferences (locale switcher). Theme is the obvious sibling — same family (per-device, non-identity). ### 3. Reshape the footer into two semantic clusters ``` [© APF France handicap · Accessibility statement] [locale • theme] ←——— info / legal ———→ ←—— device prefs ——→ ``` The Accessibility statement link moves from the right cluster (where it sat alongside the locale switcher) to the left, joining the copyright with a typographic separator (`·`). Left = static info / legal anchor; right = interactive prefs the reader controls. Keyboard order naturally follows: a Tab-traversal from the main content hits the informational anchor before the interactive controls — a small a11y win. ## What I left alone - **`portal-admin`'s header / footer.** No theme switcher there in v1 (the admin chrome is brand-primary-600 hardcoded); no settings icon either. ADR-0020 explicitly trims that admin chrome relative to the user shell — nothing to align right now. - **The user-menu's "Settings (Soon)" row.** That stays. PR 2 of the auth chantier ([#150](#150)) wired the row at the request of the chantier's staging; the Settings page itself lands in a future chantier and the row's pre-existing "Soon" badge keeps the affordance honest. ## Notes for the reviewer - **Why not also a "Theme: <current>" hint inside the user-menu?** Considered, deferred. The current Hint would be ornament without a corresponding *target*; once the Settings page exists and the theme has a settings sub-section, a "Theme: System" line in the user-menu makes sense as a deep-link affordance. Not before. - **Why a `·` text separator rather than a CSS border?** Border would force vertical alignment math (height, dark-mode color) for one line of footer text; a typographic mid-dot inherits text color, scales with the line, and is `aria-hidden` so screen readers don't read it as "middle dot". Smaller surface for one less moving piece. - **A11y check**: the left cluster's `<nav aria-label="Legal">` is preserved verbatim (just moved into the left `<div>` rather than the right). The accessibility statement keeps the same focus styles, keyboard behavior, and routerLink target. ## Test plan - [x] `pnpm nx test portal-shell` — **43 specs pass** (was 40; +3 net: dropped a Settings-button assertion + the "embeds theme switcher" header assertion swapped to its inverse, added two footer assertions for the theme switcher's new home + the cluster grouping). - [x] `pnpm nx run-many -t lint test build --projects=portal-shell` — green. - [x] `pnpm nx build portal-shell --configuration=production` — i18n-strict prod build passes, confirming the `header.action.settings` xlf removal didn't leave a dangling source-locale reference. - [ ] **Manual visual smoke**: open `pnpm nx serve portal-shell` in a browser, confirm: - The Settings cog is gone from the header. - The theme-switcher (sun/moon chip) now lives at the bottom-right of the footer, beside the locale chip. - Bottom-left of the footer reads `© 2026 APF France handicap · Accessibility statement`, the dot being decorative (no link). - Tabbing from the main content lands first on the accessibility link, then on the locale switcher, then on the theme switcher. - Toggling the theme still works as before, and toggling it persists across navigations and reloads (the underlying ThemeService is untouched). ## Follow-ups (optional) - The header still carries the global search form + notifications + help buttons. None of those are wired to live data yet — when they get real consumers, the equivalent "double-surface" check applies (is there a duplicate path in the user-menu? a redundant trigger?). For now the placeholder shapes are useful to anchor the layout. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #162 |
||
|
|
2dbf2d8ce4 |
fix(docs): cap vite below 7 (rolldown-vite) and pin mermaid transitives (#161)
## Summary `pnpm docs:dev` failed after #159's transitive-vuln fix landed. Two distinct symptoms in the same log : 1. **VitePress refused to boot** — `VitePress v1 is not compatible with rolldown-vite. Use VitePress v2 instead.` The override added in #159 (`vite@<6.4.2 → >=6.4.2`) had no upper bound; pnpm resolved vitepress's `vite ^5.0.0` constraint up to **vite 7.3.2**, which uses the new rolldown bundler. VitePress 1.x explicitly rejects rolldown-vite. 2. **Resolution-warning flood** — Even on a fallback port, the console showed `Failed to resolve dependency: dayjs / debug / @braintree/sanitize-url / cytoscape / cytoscape-cose-bilkent` from `optimizeDeps.include`. The vitepress-plugin-mermaid wrapper injects those into the optimizer's include list, but under pnpm's strict isolation they aren't reachable from the workspace root (transitives of mermaid, never hoisted). ## What lands ### 1. Vite override is now an **unconditional** `>=6.4.2 <7` range [`package.json`](package.json): ```diff - "vite@<6.4.2": ">=6.4.2", + "vite": ">=6.4.2 <7", ``` The selector form (`vite@<6.4.2`) was a no-op once vite had resolved into 7.x — the override only activates when the resolved version is _in_ the vulnerable range. Vite 7 is ≥ 6.4.2, so the override stayed dormant and rolldown-vite slipped through. The unconditional form forces a downgrade across every consumer (vitepress, `@nx/vite`, `@analogjs/vite-plugin-angular`, Vitest, etc.). All six top-level projects still lint, test, and build under vite 6.4.2. **Trade-off acknowledged**: we're now pinning the whole workspace to vite 6.x to keep VitePress 1.x happy. The day VitePress 2 ships a 1.0 (currently in beta), we revisit and let vite advance again. Tracked as a soft follow-up. ### 2. `optimizeDeps.include` simplified to `['mermaid']` [`docs/.vitepress/config.mts`](docs/.vitepress/config.mts): ```diff - include: ['mermaid', 'dayjs', 'debug', '@braintree/sanitize-url'], + include: ['mermaid'], ``` Vite 6's dep optimizer walks Mermaid's transitives automatically once Mermaid itself is in `include`. The explicit child list from #157 was carried forward in the rolldown attempt and tripped on vite's stricter resolver — collapsing it now both removes the noise and matches what the plugin's docs recommend for vite 6. ### 3. Mermaid transitives pinned as top-level devDeps [`package.json`](package.json): ```diff + "@braintree/sanitize-url": "^7.1.2", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "dayjs": "^1.11.20", + "debug": "^4.4.3", ``` These are already in `node_modules` (pulled in by mermaid). Declaring them at the workspace root makes them reachable from `optimizeDeps.include` under pnpm's strict isolation, which silences the five "Failed to resolve dependency" warnings the plugin's wrapper produced. Cost: five extra devDep lines in `package.json` whose only purpose is to make the optimizer happy. Acceptable — they don't influence the resolved tree, just the resolver's reachability rules. ## Notes for the reviewer - **Why not bump VitePress 1 → 2?** VitePress 2 is still beta. Per [CLAUDE.md](CLAUDE.md) §"Project rules": pre-1.0 dependencies and one-maintainer projects are rejected unless an ADR justifies the exception. ADR-0022 already records VitePress 1.6.4 as the chosen baseline; switching to a beta on the very first follow-up PR would burn the rationale. - **Why an unconditional vite range, not a tighter selector?** The selector form (`vite@vulnerable-range → patched-range`) is the standard pattern when the parent dep's own range *includes* a patched version — pnpm picks it naturally and the override never fires. Here vite's 5.x branch was never patched (5.4.21 stayed vulnerable; vite team moved on to 6.x), so we need to force the downgrade from 7.x to 6.x regardless of the previous resolution. An unconditional override is the cleanest expression of that intent. - **Why not extract the mermaid-transitive pins into the ADR-0022 trail?** They're plumbing for the plugin wrapper, not an architectural decision worth recording. If the plugin ships a fix that removes the include list, these can be removed without consequence. Pinning them is reversible. ## Test plan - [x] `pnpm install` clean; lockfile changes reflect vite 6.4.2 across all consumers. - [x] `pnpm audit --audit-level=moderate` — **No known vulnerabilities found**. - [x] `pnpm docs:dev` — server boots cleanly on `:5173`, no warnings, home + ADR-0009 page return 200. - [x] `pnpm docs:build` — clean build in ~9 s (back to Rollup-based timings; the rolldown 3.87 s we saw briefly was the incompatible path). - [x] `pnpm exec nx run-many -t lint test --projects=portal-shell,portal-admin,portal-bff,shared-ui,shared-state,feature-auth` — 12/12 tasks green under the vite 6 downgrade. - [x] `pnpm exec nx build portal-shell` — clean Angular production build; no Vite 6 incompatibility surfaced. - [ ] **Manual smoke (visual)** — `pnpm docs:dev`, open `http://localhost:5173`, navigate to `/decisions/0009-…` and `/architecture`, confirm Mermaid diagrams render inline (this is the actual UX the user opened the issue on). Dark mode toggle still flips diagrams. ## Follow-ups (optional) - When VitePress 2 reaches 1.0 (`vue/vitepress > releases`), revisit this override and let vite resume its mainline cadence. - If the next Renovate cycle proposes a `cytoscape` / `dayjs` / `debug` major bump that VitePress 1.x can't keep up with, the pins above act as the safety net — Renovate will open a PR rather than silently break the dev server. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #161 |
||
|
|
e3a495ab46 |
docs(development): refresh after phase-3a + add topology / trace diagrams (#160)
## Summary `docs/development.md` drifted as `portal-admin` shipped, the docs site landed, Prisma stayed pinned at 6.x, and a fair chunk of the phase-2 "to come" roadmap quietly turned into "shipped". Surgical refresh of the affected sections + two Mermaid diagrams where prose alone wasn't carrying the cognitive load. ## What changed ### Section 1 — Repo layout - `apps/portal-admin/` + `apps/portal-admin-e2e/` added (both exist on `main` since #134, were never reflected in the tree). - `prisma.config.ts` removed (phantom — Prisma 6.x doesn't ship one). The "Prisma 7" tag corrected to "Prisma 6.x" with the [ADR-0006](docs/decisions/0006-persistence-postgresql-prisma.md) pin reference. - `docs/index.md`, `docs/architecture.md`, `docs/.vitepress/` added. - New workflows listed: `docs-site.yml`, `renovate.yml`. ### Section 3 — Initial setup, new diagram Mermaid `flowchart` of the local-dev topology. Host-side dev servers (`portal-shell:4200`, `portal-admin:4300`, `portal-bff:3000`, `docs:5173`) ↔ Compose containers (Postgres, Redis, OTel) ↔ viewer profiles (Jaeger, pgweb). Replaces a ports/services context that was scattered across §3 and §5. ### Section 4 — Daily commands - `portal-admin` added to serve / test / generate examples. - Single-test-file recipe corrected: Nx's vitest executor rejects `--testFile` (we hit this empirically while debugging the sidebar spec for #151). The new wording recommends positional path for Vitest, `--testPathPattern=…` for Jest. - **New "Documentation site" subsection** with the three commands (`docs:dev`, `docs:build`, `docs:preview`), plus the recipe for adding an ADR. ### Section 5 — Observability, new diagram Mermaid `sequenceDiagram` showing how a click becomes a trace: browser `user_interaction` span → `traceparent` header → BFF span → Pino log line with matching `trace_id` → OTLP batch → Jaeger UI. Anchors the prose's "trace_id is the correlation point" rule with a visual. ### Section 6 — Renovate New subsection **"Transitive vulnerabilities — `pnpm.overrides`"**. Documents the pattern from #159 so the next contributor hitting a transitive vuln (Renovate silent, dashboard empty) has the playbook on hand without re-discovering it. ### Section 9 — Sections to come Restructured into two groups: - **Code shipped — doc to write** (Auth dev-loop, session inspection, MFA step-up debugging, admin surface walkthroughs, audit-log workflow, downstream API recipe, OpenAPI/Scalar workflow, capabilities-driven SPA UX). The code is on `main`; only the prose is missing. Each entry now cites the PR(s) that landed the implementation. - **Not yet** (component patterns library, a11y testing workflow, perf debugging, release workflow, GitLab migration runbook). Both code and doc still pending. ## Notes for the reviewer - **Why diagrams now, not at the next chantier?** Two pieces of context were genuinely easier to grasp visually than as prose lists: the local-dev topology (which port goes where), and the trace-correlation flow. The other sections (Renovate, conventional commits, CI gates) already read well as tables — adding diagrams there would be ornament, not signal. - **Why two diagrams rather than ten?** Per the user instruction "use diagrams when useful" — restraint applies. The two added are the ones that *replace* explanatory prose rather than add to it. - **Why didn't I rewrite the "to come" roadmap from scratch?** The phase mapping was useful intent — kept the same structure, just moved entries between the two columns as code-shipping unlocked them. Future re-shuffles stay cheap. - **The doc still has phase-1 framing in places** (e.g. the "this doc starts as a phase-1 reference" paragraph in §9). I left it — promoting the doc to "phase-3a reference" is a larger editorial pass than this refresh deserves; the new diagrams + section-9 split already do the practical work. - **Open questions deferred to a future pass**: the §2 "Prerequisites" table doesn't mention the docs site (`pnpm docs:dev` adds nothing to the prereqs list — just Node + pnpm, both already required). Leaving the table as is. ## Test plan - [x] `rm -rf docs/.vitepress/{cache,dist} && pnpm docs:build` — clean, 6.3 s. - [x] Mermaid renders inside `docs/.vitepress/dist/development.html` — both new diagrams produce `class="mermaid"` markers. `grep -c 'class="mermaid"\|<svg' development.html` → **2**. - [ ] Visual smoke: `pnpm docs:dev`, navigate to `/development`, confirm both diagrams render (topology with port labels readable, sequence diagram with the trace flow). Toggle dark mode, confirm diagrams flip theme. - [ ] Spot-check the "Code shipped — doc to write" table — for any contributor reading this PR, do the PR-number citations match their memory of when each chantier landed? (`auth ≈ ADR-0009 series`, `admin ≈ #127, #128, #134, #136, #140–142`, `downstream ≈ #137–139`, `OpenAPI ≈ #143`, `capabilities ≈ #151`). ## What's next The two largest "doc to write" entries (Auth dev-loop, Audit-log inspection workflow) are good candidates for the next docs chantier — both have shipped code, RSSI-relevant content, and would benefit from a guided walkthrough. Not blocking anything; pick when the team has bandwidth. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #160 |
||
|
|
a8f027f546 |
fix(deps): update dependency axios to v1.16.1 (#158)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [axios](https://axios-http.com) ([source](https://github.com/axios/axios)) | dependencies | patch | [`1.16.0` -> `1.16.1`](https://renovatebot.com/diffs/npm/axios/1.16.0/1.16.1) | --- ### Release Notes <details> <summary>axios/axios (axios)</summary> ### [`v1.16.1`](https://github.com/axios/axios/releases/tag/v1.16.1) [Compare Source](https://github.com/axios/axios/compare/v1.16.0...v1.16.1) #### v1.16.1 — May 13, 2026 This release ships a defence-in-depth fix for prototype pollution in `formDataToJSON`, hardens proxy and CI workflows, restores Webpack 4 compatibility for the fetch adapter, and includes several small bug fixes and maintenance improvements. #### 🔒 Security Fixes - **Prototype Pollution Defence-in-Depth:** Hardened `formDataToJSON` against already-polluted `Object.prototype` by walking own properties only, so attacker-controlled keys inherited from a poisoned prototype cannot propagate through deserialization. (**[#​7413](https://github.com/axios/axios/issues/7413)**) - **Proxy Cleartext Leak:** Fixed an issue where HTTPS request data could be transmitted in cleartext to an HTTP proxy under certain configurations. (**[#​10858](https://github.com/axios/axios/issues/10858)**) - **CI Cache Removal:** Removed all GitHub Actions caches as a defence-in-depth measure against cache poisoning vectors in the build pipeline. (**[#​10882](https://github.com/axios/axios/issues/10882)**) #### 🐛 Bug Fixes - **Data URI Parsing:** Updated the `fromDataURI` regex to match RFC 2397 more strictly, fixing edge cases in `data:` URL handling. (**[#​10829](https://github.com/axios/axios/issues/10829)**) - **Unicode Headers:** Preserved Unicode header values when running through request interceptors, so non-ASCII header content is no longer corrupted before dispatch. (**[#​10850](https://github.com/axios/axios/issues/10850)**) - **XHR Upload Progress:** Guarded against malformed `ProgressEvent` payloads emitted by some environments during XHR upload, preventing crashes when `loaded` / `total` are missing or invalid. (**[#​10868](https://github.com/axios/axios/issues/10868)**) - **Webpack 4 Fetch Adapter:** Fixed an "unexpected token" error caused by syntax in the fetch adapter that Webpack 4 could not parse, restoring compatibility for legacy bundler users. (**[#​10864](https://github.com/axios/axios/issues/10864)**) - **Type Definitions:** Made `parseReviver` `context.source` optional in the type definitions to align with the ES2023 specification. (**[#​10837](https://github.com/axios/axios/issues/10837)**) - **URL Object Support Reverted:** Reverted the change that allowed passing a `URL` object as `config.url` (originally **[#​10866](https://github.com/axios/axios/issues/10866)**) due to regressions; this support will be reintroduced in a later release once the underlying issues are addressed. (**[#​10874](https://github.com/axios/axios/issues/10874)**) #### 🔧 Maintenance & Chores - **Cycle Detection Refactor:** Replaced the array-based cycle tracker in `toJSONObject` with a `WeakSet`, improving performance and memory behaviour on large nested structures. (**[#​10832](https://github.com/axios/axios/issues/10832)**) - **composeSignals Cleanup:** Refactored `composeSignals` to use a clearer early-return structure, simplifying the cancellation/abort composition path. (**[#​10844](https://github.com/axios/axios/issues/10844)**) - **AI Readiness & Repo Docs:** Added `AGENTS.md` and related contributor-guide updates for both human and AI agents, plus post-release documentation improvements. (**[#​10835](https://github.com/axios/axios/issues/10835)**, **[#​10841](https://github.com/axios/axios/issues/10841)**) - **Docs Improvements:** Clarified the GET request example, fixed the interceptor `eject` example to reference the correct instance, and corrected the Buzzoid sponsor description in the README. (**[#​10836](https://github.com/axios/axios/issues/10836)**, **[#​10853](https://github.com/axios/axios/issues/10853)**, **[#​10856](https://github.com/axios/axios/issues/10856)**) - **Sponsorship Tooling:** Fixed empty sponsor arrays in the sponsor processing script, added the ability to inject additional sponsors, updated the sponsorship link, and added a Twicsy advertisement entry. (**[#​10843](https://github.com/axios/axios/issues/10843)**, **[#​10859](https://github.com/axios/axios/issues/10859)**, **[#​10869](https://github.com/axios/axios/issues/10869)**) - **Dependencies:** Bumped `@commitlint/cli` from 20.5.0 to 20.5.2. (**[#​10846](https://github.com/axios/axios/issues/10846)**) #### 🌟 New Contributors We are thrilled to welcome our new contributors. Thank you for helping improve axios: - **[@​hpinmetaverse](https://github.com/hpinmetaverse)** (**[#​10836](https://github.com/axios/axios/issues/10836)**) - **[@​tommyhgunz14](https://github.com/tommyhgunz14)** (**[#​7413](https://github.com/axios/axios/issues/7413)**) - **[@​abhu85](https://github.com/abhu85)** (**[#​10829](https://github.com/axios/axios/issues/10829)**) - **[@​divyanshuraj1095](https://github.com/divyanshuraj1095)** (**[#​10853](https://github.com/axios/axios/issues/10853)**) - **[@​sagodi97](https://github.com/sagodi97)** (**[#​10856](https://github.com/axios/axios/issues/10856)**) - **[@​rkdfx](https://github.com/rkdfx)** (**[#​10868](https://github.com/axios/axios/issues/10868)**) - **[@​Liuwei1125](https://github.com/Liuwei1125)** (**[#​10866](https://github.com/axios/axios/issues/10866)**) [Full Changelog](https://github.com/axios/axios/compare/v1.16.0...v1.16.1) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/158 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
779061660b |
chore(security): override vite + esbuild past vitepress's vulnerable transitives (#159)
## Summary Unblocks `pnpm ci:audit`. Two moderate vulnerabilities surfaced after the docs-site chantier (#154): | Advisory | Package | Vulnerable | Patched | Path | | --- | --- | --- | --- | --- | | [GHSA-67mh-4wv8-2f99](https://github.com/advisories/GHSA-67mh-4wv8-2f99) | esbuild | ≤ 0.24.2 | ≥ 0.25.0 | `. > vitepress > vite > esbuild` | | [GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9) | vite | ≤ 6.4.1 | ≥ 6.4.2 | `. > vitepress > vite` | Both come from VitePress 1.6.4's pinned dep tree (`vite@5.4.21 → esbuild@0.21.5`). The rest of the workspace was already on vite 8.0.13 + esbuild 0.27.3 — only the VitePress branch was stuck on the vulnerable line. ## What lands Two new entries in `package.json`'s existing `pnpm.overrides` block: ```json "esbuild@<0.25.0": ">=0.25.0", "vite@<6.4.2": ">=6.4.2", ``` Same version-selector pattern as the other overrides already in the file (axios, follow-redirects, ip-address, …). The override only kicks in when the resolved version is in the vulnerable range, so it becomes a no-op the day the underlying dep ships a clean version of its own. After `pnpm install`, the resolver picks **vite 7.3.2** + **esbuild 0.27.3** for the VitePress branch (the workspace's other vite consumers stay on 8.0.13, deduped on esbuild). ## Why couldn't we pin a patched vite 5.x? The vite team did **not** backport the security fix to the 5.x line. `vite@5.4.22` is not published — the latest 5.x stays at 5.4.21, which is vulnerable. The only path forward is to let pnpm pick a patched 6.x or 7.x major. Verified that VitePress 1.6.4 still: - builds cleanly (`pnpm docs:build` succeeds in ~4 s, down from 9 s on the older vite); - renders Mermaid (regression fence in `.gitea/workflows/docs-site.yml` still grep-matches `class="mermaid"` / `<svg>` in ADR-0009's HTML); - runs the dev server (`pnpm docs:dev` boots, the dayjs CJS-interop fix from #156 still applies for the same reason — Mermaid's CJS deps need pre-bundling regardless of vite major). ## Why didn't Renovate propose this PR itself? The user reported that Renovate wasn't creating PRs for these advisories, not even listing them on the Dependency Dashboard. **Root cause: both vite and esbuild are transitive dependencies** — declared by vitepress, not by us. Renovate's `vulnerabilityAlerts` flow handles **direct** package.json deps. For pnpm transitives, the remediation would have to land in `pnpm.overrides`, which the renovatebot/renovate:40 image doesn't write automatically. Adjacent points: - The Renovate workflow runs on a daily 03:00 UTC cron only (plus manual dispatch). If the user wants an immediate dashboard refresh now, the workflow accepts `workflow_dispatch` — fire it once from the Gitea Actions UI. - After this PR merges, Renovate's dashboard should also stop flagging these advisories (the overrides count as remediation). - Renovate config itself is unchanged — no `ignorePaths`, no exclude of vite/esbuild/vitepress. The silence was purely about the transitive-remediation gap, not a config bug. ## Test plan - [x] `pnpm install` — clean, no peer warnings beyond the pre-existing `nestjs-prisma → chokidar` one. - [x] `pnpm audit --audit-level=moderate` — **"No known vulnerabilities found"**. - [x] `pnpm docs:build` — clean build in ~4 s. - [x] Mermaid regression fence — `grep 'class="mermaid"' docs/.vitepress/dist/decisions/0009-…html` matches. - [ ] `pnpm ci:audit` on CI — should now pass (the goal of this PR). - [ ] Manual smoke: `pnpm docs:dev`, navigate to `/decisions/0009-…`, confirm the OIDC sequence diagram renders. Dark-mode toggle still flips theme. ## Follow-ups (optional) - Trigger the Renovate workflow manually (Gitea Actions → Renovate → Run workflow) so the Dependency Dashboard refreshes against this overridden state. - If we hit this transitive-remediation gap again, consider raising a `renovateConfig.transitiveRemediation` story or switching the workflow to `renovate/renovate:latest` — newer point releases sometimes ship better pnpm-overrides authoring. Not urgent. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #159 |
||
|
|
ed477aeb26 |
fix(deps): update dependency @scalar/nestjs-api-reference to v1.1.16 (#156)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@scalar/nestjs-api-reference](https://github.com/scalar/scalar) ([source](https://github.com/scalar/scalar/tree/HEAD/integrations/nestjs)) | dependencies | patch | [`1.1.14` -> `1.1.16`](https://renovatebot.com/diffs/npm/@scalar%2fnestjs-api-reference/1.1.14/1.1.16) | --- ### Release Notes <details> <summary>scalar/scalar (@​scalar/nestjs-api-reference)</summary> ### [`v1.1.16`](https://github.com/scalar/scalar/blob/HEAD/integrations/nestjs/CHANGELOG.md#1116) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #156 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
d94df427a1 |
fix(docs): pre-bundle mermaid deps so the dev server resolves dayjs/cytoscape (#157)
## Summary `pnpm docs:dev` rendered a blank page with this thrown by the browser on the first navigation: ``` Uncaught SyntaxError: The requested module '/@fs/.../node_modules/.pnpm/dayjs@1.11.20/node_modules/dayjs/dayjs.min.js?v=…' does not provide an export named 'default' (at chunk-AGHRB4JF.mjs?v=…:9:8) ``` Root cause: Mermaid 11 (transitive dep of `vitepress-plugin-mermaid` shipped in #154) pulls in `dayjs`, `cytoscape`, `debug`, `@braintree/sanitize-url` — each ships a CommonJS `main` field with no `default` ESM export. Vite's dev server resolves these modules eagerly as ESM and the browser blows up before the home page renders. The **production build was unaffected** — Rollup's plugin pipeline already wraps CJS deps for ESM consumers. `docs:build` shipped clean HTML in #154 and the CI Mermaid-fence (`grep class="mermaid"` in ADR-0009's HTML) passed precisely because it inspects the prod bundle, not the dev-server output. ## What lands [`docs/.vitepress/config.mts`](docs/.vitepress/config.mts) — adds a single `vite.optimizeDeps.include` block: ```ts vite: { optimizeDeps: { include: ['mermaid', 'dayjs', 'debug', '@braintree/sanitize-url'], }, }, ``` `optimizeDeps.include` tells Vite to pre-bundle those modules through esbuild at dev-server boot, applying the same CJS→ESM interop wrapper Rollup uses in prod. The dev server now serves a working `localhost:5173` and Mermaid diagrams render. ## Notes for the reviewer - **Why these four names specifically?** They are the Mermaid deps that ship CJS-only entrypoints. Adding `'mermaid'` alone is sometimes enough because Vite walks the dep tree, but the plugin's own README + a handful of upstream issue threads recommend listing the leaf CJS modules explicitly so the optimizer doesn't miss them on a cold cache. Cheap, defensive. - **Why didn't the CI gate catch this?** The `Assert Mermaid renders` step in `.gitea/workflows/docs-site.yml` greps `docs/.vitepress/dist/` — the production-build output. The bug only manifests on the dev server's runtime-pre-bundling path. Catching it in CI would require booting `docs:dev` headlessly and curling the page; not worth the workflow weight for a once-per-major-upgrade class of issue. Manual `pnpm docs:dev` smoke is the right gate for now. - **Why not bump dayjs / mermaid?** Dayjs's package shape is a long-standing upstream quirk (the `main` points at the UMD/CJS bundle); fixing it upstream would be a breaking change for non-bundler consumers. Mermaid upstream is aware; their fix has historically been "tell your bundler to pre-bundle us", which is exactly what `optimizeDeps.include` does. ## Test plan - [x] `rm -rf docs/.vitepress/cache && pnpm docs:dev` — server boots, `curl http://localhost:5173/` returns 200. - [x] `rm -rf docs/.vitepress/{cache,dist} && pnpm docs:build` — clean prod build in ~10 s, Mermaid SVG still present in ADR-0009's HTML (regression fence passes). - [ ] Manual smoke: with the fix applied, navigate `localhost:5173` → home → `/decisions/0009-…` → confirm the OIDC sequence diagram renders inline, no console errors. Toggle dark mode, confirm diagrams flip theme. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #157 |
||
|
|
abdfb8e1f4 |
fix(deps): update angular (#155)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@angular-devkit/core](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.10` -> `21.2.11`](https://renovatebot.com/diffs/npm/@angular-devkit%2fcore/21.2.10/21.2.11) | | [@angular-devkit/schematics](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.10` -> `21.2.11`](https://renovatebot.com/diffs/npm/@angular-devkit%2fschematics/21.2.10/21.2.11) | | [@angular/build](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.10` -> `21.2.11`](https://renovatebot.com/diffs/npm/@angular%2fbuild/21.2.10/21.2.11) | | [@angular/cdk](https://github.com/angular/components) | dependencies | patch | [`21.2.10` -> `21.2.11`](https://renovatebot.com/diffs/npm/@angular%2fcdk/21.2.10/21.2.11) | | [@angular/cli](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.10` -> `21.2.11`](https://renovatebot.com/diffs/npm/@angular%2fcli/21.2.10/21.2.11) | | [@angular/common](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/common)) | dependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fcommon/21.2.12/21.2.13) | | [@angular/compiler](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/compiler)) | dependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fcompiler/21.2.12/21.2.13) | | [@angular/compiler-cli](https://github.com/angular/angular/tree/main/packages/compiler-cli) ([source](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli)) | devDependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fcompiler-cli/21.2.12/21.2.13) | | [@angular/core](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/core)) | dependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fcore/21.2.12/21.2.13) | | [@angular/forms](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/forms)) | dependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fforms/21.2.12/21.2.13) | | [@angular/language-service](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/language-service)) | devDependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2flanguage-service/21.2.12/21.2.13) | | [@angular/localize](https://github.com/angular/angular) | dependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2flocalize/21.2.12/21.2.13) | | [@angular/platform-browser](https://github.com/angular/angular) ([source](https://github.com/angular/angular/tree/HEAD/packages/platform-browser)) | dependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser/21.2.12/21.2.13) | | [@angular/router](https://github.com/angular/angular/tree/main/packages/router) ([source](https://github.com/angular/angular/tree/HEAD/packages/router)) | dependencies | patch | [`21.2.12` -> `21.2.13`](https://renovatebot.com/diffs/npm/@angular%2frouter/21.2.12/21.2.13) | | [@schematics/angular](https://github.com/angular/angular-cli) | devDependencies | patch | [`21.2.10` -> `21.2.11`](https://renovatebot.com/diffs/npm/@schematics%2fangular/21.2.10/21.2.11) | | [angular-eslint](https://github.com/angular-eslint/angular-eslint) ([source](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/angular-eslint)) | devDependencies | minor | [`21.3.1` -> `21.4.0`](https://renovatebot.com/diffs/npm/angular-eslint/21.3.1/21.4.0) | --- ### Release Notes <details> <summary>angular/angular-cli (@​angular-devkit/core)</summary> ### [`v21.2.11`](https://github.com/angular/angular-cli/blob/HEAD/CHANGELOG.md#21211-2026-05-13) [Compare Source](https://github.com/angular/angular-cli/compare/v21.2.10...v21.2.11) ##### [@​angular/cli](https://github.com/angular/cli) | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------- | | [bbd63b7a5](https://github.com/angular/angular-cli/commit/bbd63b7a5a1049bc56b9ddf6edf6563a1f2d9ace) | fix | robustly parse npm manifest from array | ##### [@​angular/ssr](https://github.com/angular/ssr) | Commit | Type | Description | | --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- | | [eafe1a719](https://github.com/angular/angular-cli/commit/eafe1a719fd3fecd5263e0a8371200b4b1ff4bb9) | fix | allow all hosts in common engine rendering options to prevent validation errors | | [7a116a80d](https://github.com/angular/angular-cli/commit/7a116a80d7e6db341fd003737285d1a9db10ba6c) | fix | remove stateful flag from URL\_PARAMETER\_REGEXP | <!-- CHANGELOG SPLIT MARKER --> </details> <details> <summary>angular/components (@​angular/cdk)</summary> ### [`v21.2.11`](https://github.com/angular/components/blob/HEAD/CHANGELOG.md#21211-crystal-ball-2026-05-13) [Compare Source](https://github.com/angular/components/compare/v21.2.10...v21.2.11) No user facing changes in this release <!-- CHANGELOG SPLIT MARKER --> </details> <details> <summary>angular/angular (@​angular/common)</summary> ### [`v21.2.13`](https://github.com/angular/angular/blob/HEAD/CHANGELOG.md#21213-2026-05-13) [Compare Source](https://github.com/angular/angular/compare/v21.2.12...v21.2.13) ##### core | Commit | Type | Description | | -- | -- | -- | | [1c6553e97d](https://github.com/angular/angular/commit/1c6553e97d9655d8c48fbf625987fae86f9cd947) | fix | disallow event attribute bindings in host bindings unconditionally | ##### platform-server | Commit | Type | Description | | -- | -- | -- | | [629905d537](https://github.com/angular/angular/commit/629905d537f59dc3c264c49f6347e3599dea0215) | fix | add `allowedHosts` option to `renderModule` and `renderApplication` | | [0b7192f441](https://github.com/angular/angular/commit/0b7192f4410d055191ac9b15bff57d1d0b9a644f) | fix | forward BEFORE\_APP\_SERIALIZED errors to ErrorHandler | <!-- CHANGELOG SPLIT MARKER --> </details> <details> <summary>angular-eslint/angular-eslint (angular-eslint)</summary> ### [`v21.4.0`](https://github.com/angular-eslint/angular-eslint/blob/HEAD/packages/angular-eslint/CHANGELOG.md#2140-2026-05-13) [Compare Source](https://github.com/angular-eslint/angular-eslint/compare/v21.3.1...v21.4.0) This was a version bump only for angular-eslint to align it with other projects, there were no code changes. </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19--> Reviewed-on: #155 Co-authored-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> Co-committed-by: APF Portal Bot <jgautier.webdev+apf-portal-bot@gmail.com> |
||
|
|
7579b25dfe |
feat(docs): vitepress site for docs/, mermaid rendering, ci build workflow (#154)
## Summary
Implementation of [ADR-0022](docs/decisions/0022-docs-site-vitepress.md). Stands up the static documentation site that renders `docs/**/*.md` (architecture diagrams, daily-dev guide, ADRs, onboarding) via **VitePress + `vitepress-plugin-mermaid`**, behind a Gitea Actions build gate.
Local dev: `pnpm docs:dev`. Full build: `pnpm docs:build` (~9 s, output in `docs/.vitepress/dist/`).
## What lands
### Dependencies
`vitepress 1.6.4`, `vitepress-plugin-mermaid 2.0.17`, `mermaid 11.15.0` — workspace devDependencies. No runtime impact on `portal-shell` / `portal-admin` / `portal-bff`.
### [`docs/.vitepress/config.mts`](docs/.vitepress/config.mts)
The single source of truth for the site. Highlights:
- **`srcExclude`** drops `docs/README.md` (git/IDE-only index per ADR-0022's option A) and `docs/decisions/template.md` (authoring scaffold).
- **`rewrites`** maps `decisions/README.md` → `decisions/index.md` so `/decisions/` resolves to the curated tag-grouped landing while the source filename stays git-conventional.
- **`ignoreDeadLinks`** skips:
- `localhost:*` URLs (Jaeger, OTLP — only resolve in a live dev session),
- cross-repo references (`../CLAUDE`, `../../apps/**`, `../../infra/**`, `../../notes/**`) — intentional from git/IDE consumers; not the site's job to render them,
- excluded targets (`./template`, `./README`) — file exists in the repo, just not in the site.
- **Auto-sidebar for `/decisions/`** — `adrSidebarItems()` walks `docs/decisions/00*-*.md` and emits sorted `ADR-NNNN — title` entries. Adding an ADR is a single-file change, no `config.mts` edit.
- **Hand-curated top-level nav** (Development, Architecture, Decisions, Onboarding).
- **Mermaid via `withMermaid()`** with `securityLevel: 'strict'` so diagrams can't inject arbitrary HTML.
### [`docs/index.md`](docs/index.md)
VitePress Hero landing with four feature cards (Architecture, Decisions, Development, Onboarding).
### [`docs/development.md`](docs/development.md) — two surgical fixes
- Line ~5: `[setup/](setup/)` → `[setup/01-wsl-terminal-setup.md](setup/01-wsl-terminal-setup.md)`. Folder-style links don't resolve cleanly under `cleanUrls: true`; pointing at the first onboarding page is both correct and useful.
- Line 330: wrap `${{ github.* }}` in `<code v-pre>…</code>`. VitePress runs every Markdown file through the Vue template compiler, which sees the inline `{{ … }}` as an interpolation. `v-pre` keeps the literal text intact. The rest of the source is unaffected.
### [`package.json`](package.json)
Three new scripts:
```
docs:dev → vitepress dev docs
docs:build → vitepress build docs
docs:preview → vitepress preview docs
```
Pure pnpm scripts, no Nx project — the site has no cross-project dependency graph to track.
### [`.gitea/workflows/docs-site.yml`](.gitea/workflows/docs-site.yml)
Triggers on push to `main` and on PR, scoped by `paths:` to `docs/**`, `package.json`, `pnpm-lock.yaml`, and the workflow itself. Three steps:
1. `pnpm install --frozen-lockfile`
2. `pnpm docs:build`
3. Regression fence: `grep` ADR-0009's rendered HTML for `class="mermaid"` or `<svg>` so a silent Mermaid-plugin breakage on a major upgrade fails the workflow rather than ship a site with raw code blocks where diagrams should be.
4. On push only: upload `docs/.vitepress/dist/` as a `docs-site` artifact (30-day retention). The actual rsync to the static host lands when the future infrastructure ADR locks the deployment target.
### [`.gitignore`](.gitignore)
Excludes `docs/.vitepress/{cache,dist}/` so local builds don't leak into commits.
## Notes for the reviewer
- **Why `config.mts` and not `config.ts`?** VitePress is ESM-only, and `vitepress-plugin-mermaid` follows. Vite loads `.ts` config files via its CJS bundler in this workspace's setup and chokes on the ESM imports. `.mts` flips the loader to ESM and the build succeeds. Same pattern is used elsewhere in the workspace (`jest.config.cts`, app `vite.config.mts`).
- **Why no Nx project (`docs/project.json`)?** The doc site has no Nx-trackable dependencies (it consumes `.md` files, not TypeScript projects). Putting it in the Nx graph adds ceremony with no caching benefit — VitePress's incremental rebuilds are sub-second already, and the site never has cross-project `affected` semantics. Pure pnpm scripts keep the surface small.
- **Why the regression fence on Mermaid?** ADR-0022 §"Confirmation" promises it. The plugin is a community dep (sub-1.0 wrapper around the official Mermaid renderer); a major upgrade or a Mermaid runtime change could leave fenced ` ```mermaid ` blocks rendered as raw code without anyone noticing — until an RSSI clicks ADR-0009 and sees no diagram. Cheap grep gate, real signal.
- **Why upload as artifact, not deploy?** Per [ADR-0022](docs/decisions/0022-docs-site-vitepress.md) §"Deployment & CI": the host (`docs.portal.apf.fr` or a sub-path) is provisional. Locking an rsync target now would couple this PR to a not-yet-made infra decision. Artifact upload is the staging mechanism — manual drop on the host until the infrastructure ADR formalises the target.
- **Why `ignoreDeadLinks` rather than fixing every cross-repo reference?** The cross-repo links are genuinely useful from a git/IDE perspective (where the docs/ markdown is browsed alongside the rest of the codebase). Rewriting them to `https://git.unespace.com/julien/apf_portal/src/branch/main/…` would make them work on the site but lose the IDE quick-jump. Skipping at site-build time is the right trade-off — the site reader gets a graceful "link doesn't exist here" if they click, the IDE reader gets a working jump.
## Test plan
- [x] `pnpm docs:build` succeeds in ~9 s. Output at `docs/.vitepress/dist/` contains an `index.html`, every ADR, the development guide, the architecture diagrams, and the three setup pages.
- [x] Mermaid renders: `grep 'class="mermaid"' docs/.vitepress/dist/decisions/0009-…html` returns a match.
- [x] `pnpm exec nx run-many -t format:check lint test build` for the 6 main projects — 18/18 tasks green, no Nx regression from the new top-level config.
- [ ] **Manual smoke**: `pnpm docs:dev`, open `http://localhost:5173`, walk through:
- Landing renders Hero + 4 feature cards.
- Search box returns hits for "audit", "MFA", "OBO".
- `/decisions/0009-…` renders the OIDC sequence diagram (Mermaid SVG, not raw text).
- `/decisions/0010-…` ERD or `/architecture` C4 diagrams likewise.
- Dark-mode toggle flips diagrams to dark theme without page reload.
- Sidebar shows the 22 ADRs auto-listed under `/decisions/`.
- The "Decisions" curated index at `/decisions/` lists ADRs by tag (no regression on the source markdown).
## What's next
Once the deployment target is fixed (future infra ADR), wire the rsync step into the workflow — that lands as a small follow-up PR. Until then the artifact carries the bundle.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #154
|
||
|
|
aa8ad97feb |
fix(portal-admin): adr refs point at gitea, not the madr template repo (#153)
## 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 |