7d56802e1d581585ec643f6046d86ef5170f73fc
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7d56802e1d |
feat(portal-admin): audit dashboard — bar / donut / stacked-bar above the table
PR 3 of the charts chantier — closes the loop on ADR-0023 by wiring the three starter components from #171 onto the /audit page. Three computed signals derive aggregations from the *current page's* items (no new BFF endpoint — server-side stats land separately if ever needed): * `dailyVolume` — events grouped by ISO date (YYYY-MM-DD). * `outcomeBreakdown` — counts per outcome. * `dailyByEventType` — flat (day, eventType, count) triples. A new `<section class="charts">` renders above the existing filter form, behind a `hasChartData()` guard so the section disappears entirely on empty pages (no "0 events" donut, no half-rendered bars). Each chart sits in a `.chart-tile`; the stacked-bar gets `.chart-tile--wide` to span both grid columns since its legend benefits from the extra horizontal space. A `.charts-note` paragraph above the grid says explicitly that the aggregations are "computed from the events currently loaded on this page only" — honest disclosure of the per-page scope, no misleading "all-time" framing. Captions / descriptions / aria-labels live in plain English in the template per ADR-0020's source-locale-only chrome posture. No new i18n strings. Bundle impact: the lazy `audit` chunk grows from ~4.4 KB to ~84 KB gzip with the chart deps loaded — comfortable under [ADR-0017]'s 100 KB cap. The estimate in ADR-0023 was ~65 KB; Plot's full mark set + d3-scale-chromatic interpolators add a bit more than projected but still fit the budget. Side tweaks: * `apps/portal-admin/project.json` bumps the `anyComponentStyle` budget from 5/6 KB to 6/8 KB to accommodate the charts grid SCSS (audit.scss lands at ~6.5 KB). * `apps/portal-admin/tsconfig.app.json` references the new `libs/shared/charts/tsconfig.lib.json` (auto-added by `nx sync` after the import in audit.ts). Verification: * 57 portal-admin specs pass (was 54; +3 for the charts section rendering, the empty-page hide guard, and the donut center label binding). * `pnpm nx run-many -t lint test build --projects=portal-shell, portal-admin,shared-charts` — green. * Audit chunk gzip = 84 KB, under the budget. |
||
|
|
6120471b66 |
chore(workspace): tsconfig composite refs + axe-linter false-positive config (#147)
## Summary
Three editor-noise sources flagged by the VS Code TypeScript service + the Deque axe Linter extension, each tamed at the right layer. No runtime behaviour change.
| Source | Fix |
| --- | --- |
| **TS6306** — Referenced project `libs/{shared/ui,shared/state,feature/auth}` must have `composite: true`. Nx 22's lib generator doesn't emit `composite`; modern VS Code TS service flags it. | Add `composite: true` to each lib's `tsconfig.lib.json`, let `nx sync` redirect consumer references in `apps/portal-{shell,admin}/tsconfig.app.json` to point at the `.lib.json` directly. |
| **TS6504** — `moduleResolution: "node"` / `"node10"` deprecated, removed in TS 7.0. Two hits on the BFF tsconfigs. | Add `ignoreDeprecations: "5.0"` on `apps/portal-bff/tsconfig.{app,spec}.json` — the opt-out knob the diagnostic itself suggests. A proper migration to `nodenext`/`node16` is a separate chantier. |
| **axe-core/list (WCAG 1.3.1)** — `<ul>` "must only directly contain `<li>`, `<script>`, or `<template>`" — fires on Angular 17+ `@for` blocks inside lists. Pure static-linter limitation; rendered DOM is fine. | New `.axe-linter.yml` at repo root: `global-disable: [list]`. |
## What lands
### `composite: true` on lib `.lib.json`
[`libs/shared/ui/tsconfig.lib.json`](libs/shared/ui/tsconfig.lib.json), [`libs/shared/state/tsconfig.lib.json`](libs/shared/state/tsconfig.lib.json), [`libs/feature/auth/tsconfig.lib.json`](libs/feature/auth/tsconfig.lib.json) get `composite: true` added. `nx sync` then automatically rewrites consumer references:
```diff
- "path": "../../libs/shared/ui"
+ "path": "../../libs/shared/ui/tsconfig.lib.json"
```
in [`apps/portal-shell/tsconfig.app.json`](apps/portal-shell/tsconfig.app.json) and [`apps/portal-admin/tsconfig.app.json`](apps/portal-admin/tsconfig.app.json). Semantically cleaner — the app references the lib's actual compile config (which produces the `.d.ts` it consumes), not the lib's solution-style root tsconfig.
**Earlier attempt — composite on the solution `tsconfig.json` — silently broke `vitest`**: the Angular Vite plugin chokes on a composite project with `files: []` / `include: []` and falls through, leaving spec files loaded but tests not registered (`"No test suite found in file"`). Moving `composite` to `.lib.json` (the project that actually has inputs) fixes the contract without poking the plugin.
### `ignoreDeprecations: "5.0"`
[`apps/portal-bff/tsconfig.app.json`](apps/portal-bff/tsconfig.app.json) and [`apps/portal-bff/tsconfig.spec.json`](apps/portal-bff/tsconfig.spec.json) — silences `Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0`. The diagnostic suggests `"6.0"` as the value, but TS 5.9 (our pinned version) only accepts `"5.0"`; using `"6.0"` results in `TS5103: Invalid value for '--ignoreDeprecations'` and breaks every spec. `"5.0"` is the current-gen accepted value.
The deprecation is real — TS 7.0 will drop both `"node"` and `"node10"` `moduleResolution` modes. The migration target is `moduleResolution: "nodenext"` paired with matching `module: "nodenext"`, but that interacts non-trivially with Nest's CommonJS pipeline and the BFF's import semantics. Out of scope for a drive-by fix; we'll handle it as a dedicated chantier when TS 7.0 lands on the roadmap.
### `.axe-linter.yml`
New file at repo root:
```yaml
global-disable:
- list
```
The Deque axe Linter VS Code extension reads `.axe-linter.yml` at workspace root. The `list` rule (WCAG 1.3.1) fires false positives on Angular 17+ control-flow syntax — `@for (item of list; ...) { <li>… }` looks like a non-`<li>` child of `<ul>` to a static HTML scanner. The Angular compiler erases those tokens at build time; the rendered DOM is compliant. CI accessibility coverage is provided by `axe-playwright` per [ADR-0016 §"Tooling"](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md) — it runs against the rendered DOM and is unaffected by this disable.
## Notes for the reviewer
- **Why not `composite: true` on every lib?** Per CLAUDE.md "no premature abstractions" — `libs/shared/{tokens,util}` are not currently referenced by any `tsconfig.app.json`, so they don't trigger TS6306. Adding `composite` to them would be future-proofing without a current consumer. When a consumer reference is added, the same one-line fix lands then.
- **Why not migrate `moduleResolution` properly?** The BFF runs on Nest's CommonJS pipeline; `nodenext` brings stricter ESM resolution (`.js` extensions in imports, package `exports` map enforcement) that ripples through. Not a 5-minute change. The `ignoreDeprecations` knob is the textbook defer mechanism for exactly this case.
- **Why disable `list` globally rather than per-file?** The rule's false-positive pattern (`<ul><@for>` / `<ol><@for>`) applies workspace-wide; we use `@for` consistently across `portal-shell` + `portal-admin`. Per-file disables would multiply as new templates land. axe-playwright remains the authoritative check on the rule.
## Test plan
- [x] `pnpm nx run-many -t lint test build --projects=portal-shell,portal-admin,portal-bff,shared-ui,shared-state,feature-auth` — 18/18 tasks pass. **517 specs green** across the affected projects.
- [x] `pnpm nx sync:check` — workspace in sync after the changes; running `sync` again is a no-op.
- [ ] Editor smoke — reopen the workspace in VS Code: the TS6306 errors on lib `tsconfig.json` files should be gone, the two `moduleResolution=node10` deprecation lines on BFF tsconfigs should be silenced, and the `list` rule under `sidebar.html` (`portal-admin`) should no longer surface.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #147
|
||
|
|
40741ce326 |
feat(portal-admin): spa auth wiring + admin shell skeleton (#134)
## Summary Phase-3a step per [ADR-0020](docs/decisions/0020-portal-admin-app.md) §"Confirmation" item 4 (entry route + admin shell). Wires the existing [`feature-auth`](libs/feature/auth) library against the distinct admin OIDC routes (`/api/admin/auth/*`) the BFF exposes since PR #129, ships a lean header/sidebar/footer chrome with an "Admin" badge so an internal user can never mistake the surface for portal-shell, and gives the landing page a self-test panel that confirms the auth chain end-to-end as soon as an `admin` Entra role gets assigned. ## What lands ### Lib change — `AUTH_PATH_PREFIX` injection token [`libs/feature/auth`](libs/feature/auth/src/lib/auth.config.ts) gains one injection token. AuthService composes URLs as `${bffBaseUrl}${pathPrefix}/{me,login,logout}` instead of hard-coding `/auth/...`. Default factory returns `/auth`, so portal-shell-shaped consumers keep working without an explicit provider — no churn on existing call sites. Admin hosts override with `/admin/auth`. The interceptors (`bffCredentialsInterceptor`, `csrfInterceptor`, `bffUnauthorizedInterceptor`) are **unchanged** — they only care about the BFF base URL, not the path prefix. ### portal-admin wiring - [environment.ts](apps/portal-admin/src/environments/environment.ts) — same shape as portal-shell, same BFF base URL (both SPAs talk to one BFF per ADR-0020 §"Where does the admin app live"). Same CSRF cookie name in v1. - [app.config.ts](apps/portal-admin/src/app/app.config.ts) — `HttpClient` with the standard interceptor chain (credentials → csrf → unauthorized), `AUTH_BFF_BASE_URL` from env, `AUTH_PATH_PREFIX = '/admin/auth'`, `AUTH_CSRF_COOKIE_NAME` from env. ### Admin shell - **[AdminHeader](apps/portal-admin/src/app/components/header/header.ts)** — APF wordmark + persistent "Admin" badge + auth widget. No global search / notifications / help cluster: admins land on tabular workloads, not a discovery dashboard (ADR-0020 §"UX style is data-dense"). - **[AdminSidebar](apps/portal-admin/src/app/components/sidebar/sidebar.ts)** — static menu listing the four ADR-0020 v1 modules. Audit log is a live router link (target of the next PR); the others are `aria-disabled` placeholders with a "Soon" badge so the navigation shape is visible even before they ship. - **[AdminFooter](apps/portal-admin/src/app/components/footer/footer.ts)** — copyright + persistent "Admin surface" tag. Stays in view for long workloads where the header has scrolled off. ### Home — auth self-test panel [apps/portal-admin/src/app/pages/home/](apps/portal-admin/src/app/pages/home/): - Signed-in: shows `displayName`, `username`, `tenant`, `oid` in a monospaced detail list. - Anonymous: "No admin session detected" + a "Sign in via Entra" button that delegates to `AuthService.login()` → 302 through `/api/admin/auth/login`. - Error: "Could not reach the BFF" + retry button. - Roadmap list quoting ADR-0020's v1 catalogue. ## Known limitations (v1, documented) - **CSRF cookie shared between surfaces.** Both portals issue `portal_csrf` on session creation. A user with both portals open will see overwrites on the second sign-in; mutating actions on the first surface will then 403 until refresh. Splitting to `portal_admin_csrf` is a follow-up if the pattern becomes common. - **Shell chrome strings are plain English.** The admin app's `$localize` plumbing is wired (matches portal-shell), but adding markers to the shell here would require regenerating `messages.fr.xlf` and `i18nMissingTranslation=error` fails the prod build on every gap. Full admin i18n is its own follow-up. - **`LayoutStateService` not yet consumed.** No collapse toggle in v1. Theme preference still threads through because both apps read the same `localStorage` key — toggle in portal-shell, see it honoured in portal-admin. - **`ci:perf` only runs against portal-shell.** Admin perf budgets are enforced at build time by `apps/portal-admin/project.json` (`maximumError == maximumWarning` per ADR-0020's relaxed thresholds: 500 KB initial JS / 1.5 MB initial total). Adding admin to `pnpm ci:perf`'s gzip + Lighthouse chain is a follow-up. ## Test plan - [x] `pnpm nx test feature-auth` — **29 specs pass** (was 28; +1 for the `AUTH_PATH_PREFIX` override). - [x] `pnpm nx test portal-admin` — **15 specs pass** (was 1; +14: AdminHeader 5, AdminSidebar 3, AdminFooter 2, Home 4, App 1 expanded). - [x] `pnpm exec nx affected -t format:check lint test build --base=origin/main` — clean. - [x] Gzip-budget script run manually against `dist/apps/portal-admin/browser`: 86.65 KB initial / 300 KB budget, 4.46 KB CSS / 150 KB budget, 2.15 KB largest lazy / 100 KB per-chunk budget. Both `en` + `fr` bundles checked thanks to PR #133's multi-locale detection. - [ ] e2e — pending an Entra `admin` role assignment. Once available: `pnpm nx serve portal-admin`, visit `http://localhost:4300`, see "No admin session detected", click "Sign in via Entra", complete the round-trip, land back on `/` with the signed-in payload + `portal_admin_session` cookie set. ## Notes for the reviewer - `AdminHeader` is intentionally narrower than `Header` from portal-shell — no shared base class. The two are likely to evolve in different directions (admin-specific banner with system-status badges, audit-trail link, etc.) and a premature abstraction would be expensive to undo. - `AdminSidebar`'s "Soon" badge is a deliberate signal — without it, an admin who clicked a placeholder link and got a route-not-found would assume the app is broken. The badge sets expectations. - All shell components use `:host-context(.dark)` for dark-mode SCSS instead of `:host(.dark)` — same pattern as portal-shell, since the `.dark` class lives on `<html>` outside the component's view encapsulation. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #134 |
||
|
|
d962be838a |
feat(portal-admin): skeleton app per ADR-0020 (#100)
## Summary
First step of the admin track per ADR-0020. Scaffold the second Angular SPA in the workspace alongside `portal-shell`, with the architectural decisions from the v1 ADRs already wired so subsequent feature PRs can drop straight into modules.
## What lands
- **App generated** via `nx g @nx/angular:application` (with the matching e2e project skeleton). Standalone, zoneless-ready, prefix `app`, scss component styles, css root stylesheet, no SSR.
- **Tags** `scope:portal-admin, type:app` — module-boundary lint now treats `portal-admin` distinctly from `portal-shell` and lets it depend only on `scope:portal-admin` + `scope:shared` libs.
- **Tailwind v4 + brand tokens**: `.postcssrc.json`, `@import 'tailwindcss'`, class-based dark variant, and `@import '../../../libs/shared/tokens/src/brand-tokens.css'` — identical visual baseline to portal-shell.
- **i18n config mirrors portal-shell** (per ADR-0019): sourceLocale `en`, target `fr`, baseHref `/{locale}/` per locale, `@angular/localize/init` polyfill, `localize: ["en", "fr"]` on production, `i18nMissingTranslation: "error"` so CI blocks any PR that adds a marker without translating it. Seed [`messages.fr.xlf`](apps/portal-admin/src/locale/messages.fr.xlf) carries the four marked strings used today.
- **Budgets** relaxed to **500 KB gzip initial** per ADR-0020 §"Performance budgets" (vs 300 KB for portal-shell).
- **Skeleton home page** at `/` — `<h1>` + intro paragraph + "Skeleton — coming soon" chip in the brand-accent palette. All marked for i18n.
- **Skip-link landmark** (WCAG 2.4.1) with the same component-scoped scss as portal-shell.
- **Wildcard catch-all route** → bounces unknown paths to home (same defensive pattern as PR #96 on portal-shell).
- **Dev server on port 4300** (vs 4200 for portal-shell) — both can run in parallel.
- **`serve-static` target without `spa: true`** — locale-prefixed routing in production is handled by the reverse proxy (per ADR-0019), same as portal-shell since PR #92.
## CLAUDE.md
Picked up the second app in the **Naming** entry and the **Commands** snippet (`<app>` is now one of `portal-shell`, `portal-admin`, `portal-bff`).
## Verification
- `pnpm exec nx run-many -t lint test build --projects=portal-shell,portal-admin,shared-ui,shared-state,shared-tokens` — green.
- `portal-admin` test: 1 spec (skip-link + main landmark present).
- Production build of `portal-admin`: **62 KB gzip initial per locale** (vs 500 KB budget — plenty of headroom for the upcoming modules).
- Both `dist/apps/portal-admin/browser/{en,fr}/` emit with the right `<html lang>` and `<base href>`. FR bundle contains `"Administration"` / `"Squelette"` / `"bientôt disponible"` — translations applied.
- portal-shell production build unchanged.
## Out of scope (each its own follow-up PR)
- Admin app shell (header / sidebar / footer with "Admin" badge — likely sharing graduated primitives plus admin-specific bits).
- OpenTelemetry tracing setup (`service.name=portal-admin` per ADR-0012).
- `environment.ts` wiring (per ADR-0018) for admin-specific endpoints.
- BFF `AdminModule` + `AdminRoleGuard` + smoke `/api/admin/me` (per ADR-0020 §"Auth").
- First functional module — CMS / menu / users / audit viewer.
## Test plan
- [x] Lint + test + build green across the workspace.
- [x] Per-locale production build emits both folders with correct metadata.
- [ ] Manual: `pnpm exec nx serve portal-admin` boots on `:4300`, smoke page renders.
- [ ] Manual: prod build + `pnpm exec nx run portal-admin:serve-static` → `http://localhost:4300/en/` and `/fr/` show the placeholder with the matching language.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #100
|