refactor: move the portal home into its own overzicht context (RD-03)

The dashboard page lived inside registratie, a context with no permitted
outbound edges. Two of its sections do cross-context navigation: action
links to herregistratie/intake/brief/concepts, and an admin links section
that imported ADMIN_LINKS directly from the app shell.

Move the page and these two navigation sections into a new overzicht
context. Declare overzicht: ['registratie'] as the one sanctioned edge, so
the four data sections that stay in registratie/ui/dashboard/ remain
reachable. The route stays at /dashboard: it is a user-visible URL and four
e2e specs assert it, so only the import path and class name change.

beheer-links.section.ts now injects HEADER_ADMIN_LINKS, the same token the
site header uses, instead of importing the app shell's nav config directly.

Used npm run gen:context and corrected its three known-wrong outputs: the
overzicht: [] edge, the scaffolded overzicht.page.ts (the real page is the
moved dashboard.page.ts), and the scaffolded path: 'overzicht' route.

Verified the boundary is enforced, not decorative: a temporary import of a
herregistratie file into overzicht.page.ts makes dep:check fail, as
expected, then reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-04 16:26:11 +02:00
co-authored by Claude Sonnet 5
parent b9d572cfdc
commit b9fd411283
8 changed files with 187 additions and 13 deletions
+1
View File
@@ -2,6 +2,7 @@
// scaffolding one (see `gen:context`, WP-44).
module.exports = require('./.dependency-cruiser.base.js')(
{
overzicht: ['registratie'],
auth: [],
registratie: [],
herregistratie: ['registratie'], // the one sanctioned cross-feature edge
+3 -1
View File
@@ -14,9 +14,11 @@ export const routes: Routes = [
loadComponent: () => import('@auth/ui/login.page').then((m) => m.LoginPage),
},
{
// Path stays 'dashboard' on purpose: it is a user-visible URL and four e2e
// specs assert it. The context is `overzicht`; only the path string differs.
path: 'dashboard',
canActivate: [authGuard],
loadComponent: () => import('@registratie/ui/dashboard.page').then((m) => m.DashboardPage),
loadComponent: () => import('@overzicht/ui/overzicht.page').then((m) => m.OverzichtPage),
},
{
path: 'registratie',
@@ -3,7 +3,7 @@ import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
import { AccessStore } from '@shared/application/access.store';
import { ADMIN_LINKS } from '../../../shell/nav.config';
import { HEADER_ADMIN_LINKS } from '@shared/layout/site-header/nav-config';
/** Section: "Beheer" the admin pages the current principal may reach, capability-
gated (never role-derived), the same source + filter the site header uses.
@@ -31,5 +31,6 @@ import { ADMIN_LINKS } from '../../../shell/nav.config';
})
export class BeheerLinksSection {
private access = inject(AccessStore);
protected adminLinks = computed(() => ADMIN_LINKS.filter((l) => this.access.can(l.cap)));
private rawAdminLinks = inject(HEADER_ADMIN_LINKS);
protected adminLinks = computed(() => this.rawAdminLinks.filter((l) => this.access.can(l.cap)));
}
@@ -1,17 +1,18 @@
import { Component } from '@angular/core';
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
import { MijnAanvragenSection } from './dashboard/mijn-aanvragen.section';
import { WatMoetIkRegelenSection } from './dashboard/wat-moet-ik-regelen.section';
import { MijnRegistratieSection } from './dashboard/mijn-registratie.section';
import { SpecialismenSection } from './dashboard/specialismen.section';
import { WatWiltUDoenSection } from './dashboard/wat-wilt-u-doen.section';
import { BeheerLinksSection } from './dashboard/beheer-links.section';
import { MijnAanvragenSection } from '@registratie/ui/dashboard/mijn-aanvragen.section';
import { WatMoetIkRegelenSection } from '@registratie/ui/dashboard/wat-moet-ik-regelen.section';
import { MijnRegistratieSection } from '@registratie/ui/dashboard/mijn-registratie.section';
import { SpecialismenSection } from '@registratie/ui/dashboard/specialismen.section';
import { WatWiltUDoenSection } from './wat-wilt-u-doen.section';
import { BeheerLinksSection } from './beheer-links.section';
/** Page: "Mijn overzicht" the portal home, following the NL Design System "Mijn
omgeving" pattern. Composition only: each section below answers its own data
question (own store, own async state) see `ui/dashboard/*.section.ts`. */
question (own store, own async state) four sections stay in `registratie/ui/dashboard/`
(they render registratie data), two live here (cross-context navigation). */
@Component({
selector: 'app-dashboard-page',
selector: 'app-overzicht-page',
imports: [
PageShellComponent,
MijnAanvragenSection,
@@ -39,4 +40,4 @@ import { BeheerLinksSection } from './dashboard/beheer-links.section';
</app-page-shell>
`,
})
export class DashboardPage {}
export class OverzichtPage {}
+1
View File
@@ -6,6 +6,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@overzicht/*": ["src/app/overzicht/*"],
"@shared/*": ["../../libs/shared/src/*"],
"@beheer/*": ["../../libs/beheer/src/*"],
"@auth/*": ["src/app/auth/*"],
@@ -0,0 +1,168 @@
# RD-03 — Move the portal home into its own `overzicht` context
Status: done
Source: PLAN.md Phase 0
## Why
The dashboard is the portal home, but it lives inside `registratie` — a context that
`.dependency-cruiser.ssp.js` declares as `registratie: []`, permitted to import no other
context. Three concrete symptoms:
- Its six sections span four concerns: registratie data (3), aanvragen (1), cross-context
action links to `/herregistratie` `/intake` `/brief` `/concepts` (1), and admin links to
`/beheer/*` (1).
- The cross-context coupling is **invisible to the linter**, because
`wat-wilt-u-doen.section.ts` links by route _string_. `npm run dep:check` passes and gives
false assurance on exactly this file.
- `beheer-links.section.ts:6` imports `ADMIN_LINKS` from `../../../shell/nav.config` — a
context reaching into the app frame.
Splitting the dashboard into sections is what makes this fixable at all. That is the
refactor's first real payoff.
## Read first
- `apps/ssp/src/app/registratie/ui/dashboard.page.ts` (42 lines, pure composition)
- `apps/ssp/src/app/registratie/ui/dashboard/` — the six sections
- `.dependency-cruiser.ssp.js` — the `CONTEXT_ALLOWED` map (5 lines)
- `libs/shared/src/layout/site-header/nav-config.ts` — the `HEADER_ADMIN_LINKS` token
- `libs/shared/src/layout/site-header/site-header.component.ts:99` — how to inject it
- `apps/ssp/src/app/app.config.ts:72` — where `ADMIN_LINKS` is already provided to the token
- `plopfile.mjs`, the `context` generator
- CLAUDE.md decision 1 (DDD contexts and layers)
## Decisions (pre-made, don't relitigate)
1. **New context name: `overzicht`.** Dutch, per CLAUDE.md ("domain contexts are Dutch"), and
it matches the page's own title, "Mijn overzicht".
2. **Only the page and the two _navigation_ sections move.** These three:
- `dashboard.page.ts``overzicht/ui/overzicht.page.ts` (class `OverzichtPage`, selector
`app-overzicht-page`)
- `dashboard/wat-wilt-u-doen.section.ts``overzicht/ui/wat-wilt-u-doen.section.ts`
- `dashboard/beheer-links.section.ts``overzicht/ui/beheer-links.section.ts`
3. **The four data sections STAY in `registratie/ui/dashboard/`**`mijn-aanvragen`,
`wat-moet-ik-regelen`, `mijn-registratie`, `specialismen`. They render registratie data and
belong beside `BigProfileStore`/`AanvragenStore`. Do not move them, and do not move their
three story files.
4. **Declare `overzicht: ['registratie']`** in `.dependency-cruiser.ssp.js` — the second
sanctioned cross-feature edge, mirroring `herregistratie: ['registratie']`. The generator
inserts `overzicht: []`; you must change it.
5. **`beheer-links.section.ts` injects `HEADER_ADMIN_LINKS`**, exactly as
`site-header.component.ts:99` does. Drop the `shell/nav.config` import entirely.
`app.config.ts:72` already provides `ADMIN_LINKS` to that token, and the `AdminLink` doc
comment already says it is "Consumed by the site header's admin nav AND (per app) a
dashboard's own Beheer section". The direct import was always the anomaly.
6. **KEEP the route at `/dashboard`.** Four e2e specs assert it (`e2e/smoke.spec.ts:18`,
`e2e/brief-v2.spec.ts:33`, `e2e/error-state.spec.ts:16,18`), and it is a user-visible URL.
Only the `loadComponent` import path and the class name change. Add a one-line comment in
`app.routes.ts` saying the path stays `dashboard` deliberately while the context is
`overzicht`, so the mismatch reads as a decision and not an oversight.
7. **Use `npm run gen:context`, then correct three things.** The generator gets the folders,
the `@overzicht/*` tsconfig alias, and the dep-cruiser entry right. It gets three things
wrong for this case:
- it writes `overzicht: []` → change to `['registratie']` (decision 4)
- it scaffolds a **new** `overzicht.page.ts` from a template → delete it, the real page is
the moved `dashboard.page.ts`
- it adds a **new** `path: 'overzicht'` route → delete that block, and instead repoint the
existing `dashboard` route (decision 6)
8. **Delete the empty layer folders the generator creates.** `overzicht` has no `domain/`,
`application/`, `infrastructure/` or `contracts/` — it is composition only. Remove those
four `.gitkeep` directories rather than keeping scaffolding for work that does not exist.
9. **The page keeps its 8 imports.** CLAUDE.md has no import-count rule; one `import` per
rendered section is exactly right. Do not introduce an `OVERZICHT_SECTIONS` const to get
the number down — that trades a self-documenting array for an indirection, and this repo
deliberately has no barrels.
## Files
Move (use `git mv` so the rename is visible in review):
| From | To |
| ---------------------------------------------------------------------- | ---------------------------------------------------------- |
| `apps/ssp/src/app/registratie/ui/dashboard.page.ts` | `apps/ssp/src/app/overzicht/ui/overzicht.page.ts` |
| `apps/ssp/src/app/registratie/ui/dashboard/wat-wilt-u-doen.section.ts` | `apps/ssp/src/app/overzicht/ui/wat-wilt-u-doen.section.ts` |
| `apps/ssp/src/app/registratie/ui/dashboard/beheer-links.section.ts` | `apps/ssp/src/app/overzicht/ui/beheer-links.section.ts` |
Edit: `apps/ssp/tsconfig.json` (alias), `.dependency-cruiser.ssp.js` (edge),
`apps/ssp/src/app/app.routes.ts` (import path + class + comment),
`apps/ssp/src/app/overzicht/ui/beheer-links.section.ts` (token),
`apps/ssp/src/app/overzicht/ui/overzicht.page.ts` (class, selector, section import paths).
## Steps
1. Run `npm run gen:context` and answer `overzicht`.
2. Delete the generated `overzicht/ui/overzicht.page.ts` and the four empty layer folders.
3. Delete the generated `path: 'overzicht'` route block from `app.routes.ts`.
4. Change the generated `overzicht: []` to `overzicht: ['registratie']`.
5. `git mv` the three files per the table above.
6. Rename the page's class to `OverzichtPage` and its selector to `app-overzicht-page`; fix
its six section import paths (four now via `@registratie/ui/dashboard/…`, two local).
7. Repoint the existing `dashboard` route to `@overzicht/ui/overzicht.page` / `OverzichtPage`
and add the comment from decision 6.
8. Rewrite `beheer-links.section.ts` to inject `HEADER_ADMIN_LINKS` per decision 5.
9. Update this ticket's `Status:` to `done` and the README's RD-03 row to `done`.
10. Commit all of it together.
## Acceptance criteria
```bash
npm run dep:check # exits 0 — proves overzicht: ['registratie'] is accepted
npm run ci # exits 0
```
Then prove the boundary is real, not decorative:
```bash
# Add a temporary import of a herregistratie file to overzicht/ui/overzicht.page.ts.
# `npm run dep:check` MUST fail (overzicht may reach registratie only). Then revert.
```
And prove the section still renders for an admin:
```bash
grep -n "HEADER_ADMIN_LINKS" apps/ssp/src/app/overzicht/ui/beheer-links.section.ts # present
grep -rn "shell/nav.config" apps/ssp/src/app/overzicht/ # no match
```
## Verification
`npm run ci`. No story or `.mdx` file moves in this ticket — the three section stories stay
in `registratie` — so `--full` is not strictly required. Run it once anyway, because this is
a structural move and Storybook globs the app's stories by path.
Manual check, since e2e is not in the local GREEN one-liner: `npm start`, open
`http://localhost:4200/dashboard`, confirm all six sections render, then `?role=admin` and
confirm the Beheer section appears (and is absent without it).
## Out of scope
- Renaming the route to `/overzicht`. It needs a redirect and touches four e2e specs.
Recorded in PLAN.md as deliberately out of scope.
- Moving the four data sections (decision 3).
- Story titles — that is RD-04.
- Turning `wat-wilt-u-doen`'s action list into a token beside `NAV_ITEMS`. PLAN.md Phase 0
step 6 lists it as a "consider"; leave it for now, since route strings on a landing page
are legitimate once the page lives in a context whose job is cross-context linking.
## Risks
- **The generator's three wrong outputs** (decision 7). If you skip the corrections,
`dep:check` still passes with the boundary silently missing — the exact failure mode
`plopfile.mjs`'s own comment warns about for `create-frontend.mjs`.
- **`messages.xlf` / `messages.en.xlf` carry `<context context-type="sourcefile">` paths**
that these moves make stale. Nothing checks them and `$localize` ids are the identity, so
`ng build --localize` stays green. Do **not** hand-edit the xlf files to chase paths.
- **Do not change any `$localize` id.** Every id in the three moved files stays byte
identical; only the file's location changes. A changed id needs a new English `<target>` or
the localize gate fails.
- **`git mv`, not delete-and-create**, or the review loses the rename and the diff looks like
300 new lines.
+1 -1
View File
@@ -97,7 +97,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
| ----- | ---------------------------------------------------------------------------- | ---------- | --------- | ------ |
| RD-01 | Scaffold this backlog: README, PLAN, ticket template | — | | done |
| RD-02 | `max-lines` rule + `reportUnusedDisableDirectives` + 7 disables | 01 | | done |
| RD-03 | `overzicht` context: page + 2 nav sections, boundary edge, admin-links token | 02 | yes | todo |
| RD-03 | `overzicht` context: page + 2 nav sections, boundary edge, admin-links token | 02 | yes | done |
| RD-04 | Story titles to `Domein/<Context>/<Name>`; add the missing stories | 03 | yes | todo |
| RD-05 | `createStore` gains the effect map + specs | 02 | | todo |
| RD-06 | **Bug fix:** 2 single-step forms to the effect map + retry affordance | 05 | yes | todo |