feat(WP-67): merge behandelportal into this repo as a monorepo

Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.

- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
  environments/, the Storybook docs/*.mdx, and styles.scss into
  libs/shared + libs/beheer (all confirmed identical between the two
  repos before merging). auth stays deliberately duplicated per
  ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
  and Storybook into .storybook-ssp/.storybook-behandelportal - both
  forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
  HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
  its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
  WP-67 backlog entry documents the full decision trail.

npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.

The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-02 21:01:57 +02:00
co-authored by Claude Sonnet 5
parent d3f3b13345
commit e7156c5132
403 changed files with 7103 additions and 60917 deletions
@@ -0,0 +1,32 @@
import { Component, computed, input, output } from '@angular/core';
import { ButtonComponent } from '@shared/ui/button/button.component';
/**
* Atom: a possibly-masked sensitive value (BSN, BIG-nummer, …) with an optional, audited
* reveal affordance (WP-40). The value arrives masked from the server (data-minimisation)
* and is swapped for the full value on reveal; the reveal button shows only when the value
* is still masked AND the caller says the principal may reveal it. Centralises the
* masked-detection that consumers used to sniff inline. The atom only emits `reveal`; the
* caller owns the step-up gesture + the audited fetch (see behandel-scherm).
*
* ponytail: masked-detection is the mask character (`*`) — a POC heuristic. A server-sent
* `masked` boolean would remove the sniff; wire it here without touching consumers.
*/
@Component({
selector: 'app-masked-value',
imports: [ButtonComponent],
template: `
<span class="value">{{ value() }}</span>
@if (canReveal() && masked()) {
<app-button variant="subtle" (click)="reveal.emit()">{{ revealLabel() }}</app-button>
}
`,
})
export class MaskedValueComponent {
value = input.required<string>();
canReveal = input(false);
revealLabel = input($localize`:@@maskedValue.reveal:Tonen`);
reveal = output<void>();
protected masked = computed(() => this.value().includes('*'));
}
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { MaskedValueComponent } from './masked-value.component';
const meta: Meta<MaskedValueComponent> = {
title: 'Design System/Atoms/Masked Value',
component: MaskedValueComponent,
};
export default meta;
type Story = StoryObj<MaskedValueComponent>;
/** Masked + the principal may reveal → the reveal button shows. */
export const RevealableMasked: Story = {
args: { value: '******601', canReveal: true, revealLabel: 'Toon BIG-nummer' },
};
/** Masked but no reveal right → just the masked value, no affordance. */
export const MaskedNoReveal: Story = {
args: { value: '******601', canReveal: false },
};
/** Already revealed (no mask character) → no reveal button even with the right. */
export const Revealed: Story = {
args: { value: '990000000012', canReveal: true },
};