feat(web): Angular skeleton with PKCE auth, routes and i18n #50
No reviewers
Labels
No labels
in-progress
in-review
ready-for-agent
seam-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
patrick/Polaris!50
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/web-skeleton"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this delivers
PH0.5 (docs/12-implementation-plan.md, Wave 0a) — an Angular 22 workspace (standalone, signals, zoneless) under
frontend/with every frontend dependency from docs/07-architecture.md, a Keycloak PKCE login flow, a complete route table, and the Transloco i18n plumbing.@angular/cdk,@openng/optimus-ui-themes), NgRx SignalStore (@ngrx/signals), MapLibre GL,keycloak-js,@jsverse/transloco,openapi-typescript,angular-eslint.core/auth/):keycloak-jsPKCE (onLoad: 'login-required'), anAuthServiceexposing roles/username/locale as signals, an HTTP interceptor that attaches a bearer token only to Polaris API calls, and aroleGuard()factory applied to every feature route.core/i18n/): aTranslocoLoaderthat resolves each feature's owni18n/{en,nl}.json(docs/07-architecture.md frontend layout) plus the shell scope fromcore/i18n/; locale resolutionlocalStorage→ tokenlocaleclaim → browser →en; a language switcher; a custom ESLint rule (tools/eslint-rules/no-template-literal-text.cjs) that rejects raw template text and literal translatable attributes;tools/i18n-parity.mjschecking every scope has matchingen/nlkeys.app.routes.ts,core/nav.ts): one lazy route per feature in docs/06-ux.md — 13 features total, matching docs/07-architecture.md's feature-directory list (dashboard plan jobs vehicles sites crews today feedback catalogue ingest audit settings admin-crud) — each role-guarded and pointing at a stubNotBuiltPageuntil its own track builds the real page. A spec (app.routes.spec.ts) asserts routes and nav entries never drift apart.core/layout/shell.component): top nav filtered by role, language switcher, and the "hello" panel showing the signed-in user and their realm roles from the token — this is what PH0's "Proves" line refers to (LDAP group → Keycloak role → UI).npm run api:gen(tools/api-gen.mjs) generates the typed client frombackend/openapi.jsoninto the git-ignoredcore/api/generated/.backend/openapi.jsonis delivered by PH1.7, which hasn't merged yet — the script is tolerant of that, printing a note and exiting0rather than failing.frontend/Dockerfile: multi-stagenodebuild →nginx, a self-containeddocker/nginx.conf(SPA fallback,/apiproxy toapi:8000with lazy DNS resolution so the container doesn't crash-loop ifapiisn't up yet), and adocker-entrypoint.dscript that rendersconfig.jsfromPOLARIS_*env vars at container start — one image, configured per environment.frontend/.npmrc:legacy-peer-deps=true.openapi-typescript'stypescript@^5.xpeer range hasn't caught up with Angular 22'stypescript@~6.0.2yet; it only consumes the generated TS AST so this is safe. Without it,npm cifails outright on a peer conflict.Verified locally
npm ci && npm run build— green (production build, ~101 kB gzip initial).npm run lint(including the custom no-literal-text rule) — green.npm test(vitest, 5 files / 12 tests) — green.npm run i18n:parity— green.docker build+docker run— SPA served,config.jscorrectly rendered fromPOLARIS_*env vars, SPA fallback and/apiproxy both verified.Verified live, against a real Keycloak/LDAP
Brought up PR #49's (
feature/infra-compose-base) compose stack in a scratch clone (not merged into this branch —infra/**stays out of this PR), spun it up, drove this PR's actual frontend through a headless-browser PKCE login (Playwright/Chromium) for all five LDAP users, then tore the stack down. This caught and fixed two real bugs thatng build/eslint/vitest never exercised:provideAppInitializer(bootstrapApp())calledbootstrapApp()immediately at module-eval time, before Angular's injector existed —inject(AuthService)threwNG0203on every load, so login never started. Fixed by passing the function reference itself.ScopedTranslocoLoaderassumed Transloco's loader callback gets a bare language code; for a scoped request it's actually"<scope>/<lang>", so every scoped translation fetch 404'd. Fixed by stripping the scope prefix.Also fixed
environment.development.ts's guessed Keycloak port (8081, the nginx gateway) to the real one (8080), now that infra's compose file exists to check against.Results:
Nav counts match docs/02-roles-and-glossary.md's permission matrix exactly.
Acceptance criteria (issue #5)
localStorage→ tokenlocale→ browser →en) and every shell string has bothnlandenkeys; the key-parity script passesapp.routes.tsandcore/nav.tslist every feature from docs/06-ux.md as a lazy route to a placeholdernpm run api:genproduces the client frombackend/openapi.jsoninto a git-ignored folderCloses #5
🤖 Generated with Claude Code
https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX
PH0.5 (docs/12-implementation-plan.md, Wave 0a). Angular 22 workspace (standalone, signals, zoneless) under frontend/ with every frontend dependency from docs/07-architecture.md: Optimus UI (+cdk, themes), NgRx SignalStore, MapLibre GL, keycloak-js, Transloco, openapi-typescript. - core/auth: keycloak-js PKCE login (onLoad: login-required), an AuthService exposing roles/username/locale as signals, an HTTP interceptor that attaches a fresh bearer token to API calls only, and a roleGuard() factory used on every feature route. - core/i18n: a Transloco loader resolving each feature's own i18n/{en,nl}.json (docs/07-architecture.md frontend layout) plus the shell scope in core/i18n; locale resolution localStorage -> token `locale` claim -> browser -> en; a language switcher; a custom ESLint rule (tools/eslint-rules/no-template-literal-text.cjs) that rejects raw text/attributes in templates; tools/i18n-parity.mjs checking every scope has matching en/nl keys. - core/nav.ts + app.routes.ts: one lazy route per feature in docs/06-ux.md (13 features, matching the architecture doc's feature list), each a role-guarded stub NotBuiltPage until its track builds the real page; a parity spec asserts routes and nav never drift. - core/layout/shell.component: top nav filtered by role, language switcher, and the "hello" panel showing the signed-in user's roles from the token (proves LDAP group -> Keycloak role -> UI, per PH0's "Proves" line). - tools/api-gen.mjs (npm run api:gen): generates the typed client from backend/openapi.json into git-ignored core/api/generated/; tolerant of the spec not existing yet (delivered in PH1.7) — prints a note and exits 0 rather than failing the build. - frontend/Dockerfile: multi-stage node build -> nginx, a self-contained nginx.conf (SPA fallback, /api proxy to `api:8000`, lazy DNS resolution so the container doesn't crash-loop if `api` isn't up yet), and a docker-entrypoint.d script that renders config.js from POLARIS_* env vars at container start so one image works in every environment. - frontend/.npmrc: legacy-peer-deps=true, because openapi-typescript's `typescript@^5.x` peer range hasn't caught up with Angular 22's `typescript@~6.0.2` yet; it only reads the generated AST so this is safe. Without it `npm ci` fails outright. Verified locally: npm ci && npm run build/lint/test/i18n:parity all green, and `docker build` + `docker run` serve the SPA with config.js correctly rendered from env vars. Not verified in this sandbox (no Keycloak/LDAP/compose available here): an actual login round-trip against the five LDAP users. The PKCE flow, role extraction and guards are implemented and covered by unit tests against a mocked token; end-to-end login is exercised by PH0.6's compose smoke test per docs/10-dev-workflow.md. Closes #5 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoNrSy7Reyp7evkfcdHeLX