PH0.5 — Web skeleton: Angular workspace, PKCE login, shell, complete routes and nav, Transloco #5

Closed
opened 2026-09-11 09:02:07 +00:00 by bart · 3 comments
Collaborator

Parent

Row PH0.5 in docs/12-implementation-plan.md (branch, owned paths and doc section are defined there).

What to build

A user opens the web app, logs in through Keycloak PKCE, sees a shell whose "hello" page shows the roles from their token, can switch the UI between nl and en, and can navigate to every feature listed in docs/06-ux.md — each rendering a "not built yet" placeholder. All frontend dependencies are declared, the OpenAPI client is generated into a git-ignored folder, and a lint rule rejects user-facing string literals.

Acceptance criteria

  • Login as each LDAP user shows that user's role on the hello page; role guards block routes the role may not see — verified live against PR #49's Keycloak/LDAP stack, see PR #50
  • Language switch persists (localStorage → token locale → browser → en) and every shell string has both nl and en keys; the key-parity script passes
  • app.routes.ts and core/nav.ts list every feature from docs/06-ux.md as a lazy route to a placeholder
  • npm run api:gen produces the client from backend/openapi.json into a git-ignored folder
  • eslint (incl. the no-literal rule), vitest and production build are green; frontend Dockerfile builds

Blocked by

  • None (can start immediately)
## Parent Row **PH0.5** in `docs/12-implementation-plan.md` (branch, owned paths and doc section are defined there). ## What to build A user opens the web app, logs in through Keycloak PKCE, sees a shell whose "hello" page shows the roles from their token, can switch the UI between `nl` and `en`, and can navigate to every feature listed in docs/06-ux.md — each rendering a "not built yet" placeholder. All frontend dependencies are declared, the OpenAPI client is generated into a git-ignored folder, and a lint rule rejects user-facing string literals. ## Acceptance criteria - [x] Login as each LDAP user shows that user's role on the hello page; role guards block routes the role may not see — *verified live against PR #49's Keycloak/LDAP stack, see PR #50* - [x] Language switch persists (localStorage → token locale → browser → `en`) and every shell string has both `nl` and `en` keys; the key-parity script passes - [x] `app.routes.ts` and `core/nav.ts` list every feature from docs/06-ux.md as a lazy route to a placeholder - [x] `npm run api:gen` produces the client from `backend/openapi.json` into a git-ignored folder - [x] eslint (incl. the no-literal rule), vitest and production build are green; frontend Dockerfile builds ## Blocked by - None (can start immediately)
Owner

🚧 Started — session working this on branch feature/web-skeleton (owns: frontend/**), in an isolated worktree off main. Will tick acceptance criteria live as they are met and open a PR (Closes #5) when done.

🚧 Started — session working this on branch `feature/web-skeleton` (owns: frontend/**), in an isolated worktree off `main`. Will tick acceptance criteria live as they are met and open a PR (`Closes #5`) when done.
patrick self-assigned this 2026-09-11 09:45:08 +00:00
patrick stopped working 2026-09-11 10:02:55 +00:00
17 minutes 47 seconds
Owner

PR #50 opened: feature/web-skeleton -> main.

Notes for reviewers / things the ticket didn't foresee:

  • Live login not exercised: this session had no Keycloak/LDAP/compose stack available (frontend-only sandbox, infra owned by #3/#4 running in parallel), so the first acceptance criterion (login as each LDAP user) is implemented and unit-tested against a mocked token/AuthService, but not run end-to-end. Left unchecked on purpose; PH0.6's compose smoke test is the right place to close the loop.
  • openapi-typescript peer conflict: it still declares typescript@^5.x while Angular 22 needs typescript@~6.0.2, so a plain npm ci fails without help. Added frontend/.npmrc with legacy-peer-deps=true (comment in the file explains why) rather than pin an older TypeScript. Worth revisiting once openapi-typescript widens its peer range.
  • nginx /api upstream: the self-contained docker/nginx.conf resolves the api upstream lazily (Docker embedded DNS + resolver) instead of at config-load time, so the web container doesn't crash-loop if api isn't up yet or if this image is run standalone.
  • 13 features, not 11: docs/06-ux.md's compact "Navigation" tree lists 11 top-level entries; docs/07-architecture.md's frontend-layout feature list has 13 (feedback and admin-crud are reachable from within other pages there, not top-nav items). Since the row says routes+nav must cover every feature, core/nav.ts includes all 13 with a comment explaining the discrepancy.
  • Ran docker build/docker run locally to confirm the Dockerfile actually serves the SPA and renders config.js from POLARIS_* env vars correctly (not just that it builds).
PR #50 opened: feature/web-skeleton -> main. Notes for reviewers / things the ticket didn't foresee: - **Live login not exercised**: this session had no Keycloak/LDAP/compose stack available (frontend-only sandbox, infra owned by #3/#4 running in parallel), so the first acceptance criterion (login as each LDAP user) is implemented and unit-tested against a mocked token/AuthService, but not run end-to-end. Left unchecked on purpose; PH0.6's compose smoke test is the right place to close the loop. - **`openapi-typescript` peer conflict**: it still declares `typescript@^5.x` while Angular 22 needs `typescript@~6.0.2`, so a plain `npm ci` fails without help. Added `frontend/.npmrc` with `legacy-peer-deps=true` (comment in the file explains why) rather than pin an older TypeScript. Worth revisiting once openapi-typescript widens its peer range. - **nginx `/api` upstream**: the self-contained `docker/nginx.conf` resolves the `api` upstream lazily (Docker embedded DNS + `resolver`) instead of at config-load time, so the `web` container doesn't crash-loop if `api` isn't up yet or if this image is run standalone. - **13 features, not 11**: docs/06-ux.md's compact "Navigation" tree lists 11 top-level entries; docs/07-architecture.md's frontend-layout feature list has 13 (`feedback` and `admin-crud` are reachable from within other pages there, not top-nav items). Since the row says routes+nav must cover every feature, `core/nav.ts` includes all 13 with a comment explaining the discrepancy. - Ran `docker build`/`docker run` locally to confirm the Dockerfile actually serves the SPA and renders `config.js` from `POLARIS_*` env vars correctly (not just that it builds).
patrick stopped working 2026-09-11 10:04:50 +00:00
1 minute 40 seconds
Owner

Live login now verified, per request. Brought up the real Keycloak/LDAP/compose stack from PR #49 (feature/infra-compose-base) in a scratch clone (not merged into this PR/branch — infra/** stays #3's, this PR touches only files already listed above), spun it up with docker compose -f infra/compose/docker-compose.yml up, torn down afterwards. Drove PH0.5's actual frontend (ng serve) through a headless-browser PKCE login (Playwright/Chromium) for all five LDAP users.

Two real bugs found and fixed (neither ng build, eslint, nor vitest caught either — only an actual browser hitting a real Keycloak did):

  1. provideAppInitializer(bootstrapApp()) called bootstrapApp() immediately at module-eval time (before Angular's injector exists), instead of passing the function reference for Angular to call inside its own injection context — inject(AuthService) threw NG0203 on every load, so login never started.
  2. ScopedTranslocoLoader assumed Transloco's getTranslation(lang, data) first argument was a bare language code; for a scoped load it's actually "<scope>/<lang>" (confirmed against the library source). Every scoped fetch 404'd (core/i18n/shell/en.json instead of core/i18n/en.json), so the shell rendered with empty text.

Also fixed environment.development.ts's Keycloak URL — guessed at localhost:8081 before infra's compose file existed; the real one exposes Keycloak on 8080 (8081 is the unrelated nginx gateway).

Results (scripted PKCE token check + real-browser login):

OK   admin        preferred_username='admin'      roles=['admin']      aud=polaris-api
OK   planner      preferred_username='planner'    roles=['planner']    aud=polaris-api
OK   technician   preferred_username='technician' roles=['technician'] aud=polaris-api
OK   viewer       preferred_username='viewer'     roles=['viewer']     aud=polaris-api
OK   ingest       preferred_username='ingest'     roles=['ingest']     aud=polaris-api

OK   admin        hello='Hello, admin'      roleChip='Admin'      nav=12 entries
OK   planner      hello='Hello, planner'    roleChip='Planner'    nav=8 entries
OK   technician   hello='Hello, technician' roleChip='Technician' nav=1 entry
OK   viewer       hello='Hello, viewer'     roleChip='Viewer'     nav=2 entries
OK   ingest       hello='Hello, ingest'     roleChip='Ingest'     nav=0 entries

OK   viewer -> /settings  redirected to /forbidden (admin-only)
OK   viewer -> /jobs      redirected to /forbidden (planner/admin-only)
OK   viewer -> /plan      allowed (viewer has read access)

OK   language switch: nav label 'Vehicles' -> 'Voertuigen' immediately, persists as 'nl' across a full page reload via localStorage['polaris.locale']

Nav counts match the permission matrix exactly (docs/02-roles-and-glossary.md): admin sees everything except today (technician-only) = 12; planner sees dashboard/plan/jobs/vehicles/sites/crews/feedback/audit = 8; technician sees only today = 1; viewer sees dashboard/plan = 2; ingest (a machine/service role with no UI pages) sees 0.

Pushed as a follow-up commit on feature/web-skeleton (PR #50): fix(web): fix PKCE bootstrap and scoped i18n loader. npm run build/lint/test all still green after the fixes. Ticked off the first acceptance criterion above.

**Live login now verified**, per request. Brought up the real Keycloak/LDAP/compose stack from PR #49 (`feature/infra-compose-base`) in a scratch clone (not merged into this PR/branch — `infra/**` stays #3's, this PR touches only files already listed above), spun it up with `docker compose -f infra/compose/docker-compose.yml up`, torn down afterwards. Drove PH0.5's actual frontend (`ng serve`) through a headless-browser PKCE login (Playwright/Chromium) for all five LDAP users. **Two real bugs found and fixed** (neither `ng build`, eslint, nor vitest caught either — only an actual browser hitting a real Keycloak did): 1. `provideAppInitializer(bootstrapApp())` called `bootstrapApp()` immediately at module-eval time (before Angular's injector exists), instead of passing the function reference for Angular to call inside its own injection context — `inject(AuthService)` threw `NG0203` on every load, so login never started. 2. `ScopedTranslocoLoader` assumed Transloco's `getTranslation(lang, data)` first argument was a bare language code; for a scoped load it's actually `"<scope>/<lang>"` (confirmed against the library source). Every scoped fetch 404'd (`core/i18n/shell/en.json` instead of `core/i18n/en.json`), so the shell rendered with empty text. Also fixed `environment.development.ts`'s Keycloak URL — guessed at `localhost:8081` before infra's compose file existed; the real one exposes Keycloak on `8080` (`8081` is the unrelated nginx gateway). **Results** (scripted PKCE token check + real-browser login): ``` OK admin preferred_username='admin' roles=['admin'] aud=polaris-api OK planner preferred_username='planner' roles=['planner'] aud=polaris-api OK technician preferred_username='technician' roles=['technician'] aud=polaris-api OK viewer preferred_username='viewer' roles=['viewer'] aud=polaris-api OK ingest preferred_username='ingest' roles=['ingest'] aud=polaris-api OK admin hello='Hello, admin' roleChip='Admin' nav=12 entries OK planner hello='Hello, planner' roleChip='Planner' nav=8 entries OK technician hello='Hello, technician' roleChip='Technician' nav=1 entry OK viewer hello='Hello, viewer' roleChip='Viewer' nav=2 entries OK ingest hello='Hello, ingest' roleChip='Ingest' nav=0 entries OK viewer -> /settings redirected to /forbidden (admin-only) OK viewer -> /jobs redirected to /forbidden (planner/admin-only) OK viewer -> /plan allowed (viewer has read access) OK language switch: nav label 'Vehicles' -> 'Voertuigen' immediately, persists as 'nl' across a full page reload via localStorage['polaris.locale'] ``` Nav counts match the permission matrix exactly (docs/02-roles-and-glossary.md): admin sees everything except `today` (technician-only) = 12; planner sees dashboard/plan/jobs/vehicles/sites/crews/feedback/audit = 8; technician sees only `today` = 1; viewer sees dashboard/plan = 2; ingest (a machine/service role with no UI pages) sees 0. Pushed as a follow-up commit on `feature/web-skeleton` (PR #50): `fix(web): fix PKCE bootstrap and scoped i18n loader`. `npm run build/lint/test` all still green after the fixes. Ticked off the first acceptance criterion above.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Total time spent: 19 minutes 27 seconds
patrick
19 minutes 27 seconds
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
patrick/Polaris#5
No description provided.