Files
atomic-design-poc/src/app/shared/ui/debug-state/debug-state.component.ts
T
ehoandClaude Opus 4.8 1ed4850858
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 27m52s
CI / backend (push) Successful in 1m24s
CI / e2e (push) Successful in 27m24s
CI / semgrep (push) Failing after 30s
CI / api-client-drift (push) Successful in 2m7s
feat(dev): WP-33 — in-app dev switchers for scenario + role
Surface the ?scenario= and ?role= dev stand-ins as dropdowns in the existing
debug-state devtool, so a demo can flip them with a click instead of editing the
URL. scenario.ts/role.ts gain set* setters + exported valid-value lists (reused
by the panel, no duplicated source of truth); scenario becomes tab-sticky like
role so it survives navigation. Applied via location.reload() since both are
read per-request in interceptors. Extends the debug-state eslint exemption to the
ui→infrastructure rule (same devtool precedent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:53:26 +02:00

162 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, Injector, computed, inject, isDevMode, signal } from '@angular/core';
import { JsonPipe } from '@angular/common';
import { SessionStore } from '@auth/application/session.store';
import { Session } from '@auth/domain/session';
import { BigProfileStore } from '@registratie/application/big-profile.store';
import { map } from '@shared/application/remote-data';
import { Role } from '@shared/domain/role';
import { ROLES, currentRole, setRole } from '@shared/infrastructure/role';
import { Scenario, SCENARIOS, currentScenario, setScenario } from '@shared/infrastructure/scenario';
import { maskBsn, redactProfile } from './mask';
// CIBG-GAP EXTENSION: n/a — devtool, no corresponding CIBG concept; deliberately
// off-theme by design (see the ponytail note below), see cibg-gaps.mdx.
/**
* Dev-only "show the current Model" panel (Elm-debugger style, read-only).
* Observes the root singletons and renders them via the json pipe. Never a
* product feature: the whole template is gated by isDevMode(), so it does not
* render and is unreachable in a production build.
*
* ponytail: dev tool — intentionally off-theme (a dark code-editor look, not
* CIBG). Its palette lives as --app-devpanel-* tokens in styles.scss so this file
* stays hex-free (check:tokens has no holes), while still reading as a tool.
*/
@Component({
selector: 'app-debug-state',
imports: [JsonPipe],
styles: [
`
.fab {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 9999;
font: 600 12px/1 monospace;
background: var(--app-devpanel-bg);
color: var(--app-devpanel-accent);
border: var(--rhc-border-width-sm) solid var(--app-devpanel-border);
border-radius: 4px;
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.panel {
position: fixed;
bottom: 3.25rem;
right: 1rem;
z-index: 9999;
width: min(90vw, 28rem);
max-height: 70vh;
overflow: auto;
background: var(--app-devpanel-bg);
color: var(--app-devpanel-fg);
border: var(--rhc-border-width-sm) solid var(--app-devpanel-border);
border-radius: 6px;
box-shadow: 0 6px 24px var(--app-devpanel-shadow);
}
.panel pre {
margin: 0;
padding: 0.75rem;
font: 12px/1.5 monospace;
white-space: pre-wrap;
word-break: break-word;
}
.switchers {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
border-bottom: var(--rhc-border-width-sm) solid var(--app-devpanel-border);
}
.switchers label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font: 11px/1.3 monospace;
color: var(--app-devpanel-accent);
}
.switchers select {
font: 12px/1 monospace;
background: var(--app-devpanel-bg);
color: var(--app-devpanel-fg);
border: var(--rhc-border-width-sm) solid var(--app-devpanel-border);
border-radius: 3px;
padding: 0.25rem;
}
`,
],
template: `
@if (isDev) {
<button type="button" class="fab" (click)="toggle()">
{{ visible() ? '× state' : '⚙ state' }}
</button>
@if (visible()) {
<div class="panel">
<div class="switchers">
<label
>role
<select [value]="role" (change)="switchRole($any($event.target).value)">
@for (r of roles; track r) {
<option [value]="r">{{ r }}</option>
}
</select>
</label>
<label
>scenario
<select [value]="scenario" (change)="switchScenario($any($event.target).value)">
@for (s of scenarios; track s) {
<option [value]="s">{{ s }}</option>
}
</select>
</label>
</div>
<pre>{{ snapshot() | json }}</pre>
</div>
}
}
`,
})
export class DebugStateComponent {
protected readonly isDev = isDevMode();
protected readonly visible = signal(false);
private session = inject(SessionStore);
private injector = inject(Injector);
// Resolved on first open only — BigProfileStore's httpResources fetch eagerly
// on construction, so we must not instantiate it until the dev asks to look.
private profileStore?: BigProfileStore;
// PII is redacted/masked here (see mask.ts): the panel inspects state SHAPE,
// never personal data — a deliberate habit for a PII-handling app.
protected readonly snapshot = computed(() => ({
session: maskSession(this.session.session()),
profile: this.profileStore ? map(this.profileStore.profile(), redactProfile) : undefined,
decisions: this.profileStore?.decisions(),
aantekeningen: this.profileStore?.aantekeningen(),
pendingHerregistratie: this.profileStore?.pendingHerregistratie(),
}));
// Dev switchers (WP-33): flip role/scenario without hand-editing the URL. Both are
// read per-request in interceptors, so a reload re-runs them and re-fetches decisions.
protected readonly roles = ROLES;
protected readonly scenarios = SCENARIOS;
protected readonly role = currentRole();
protected readonly scenario = currentScenario();
switchRole(r: Role): void {
setRole(r);
location.reload();
}
switchScenario(s: Scenario): void {
setScenario(s);
location.reload();
}
toggle(): void {
if (!this.visible()) this.profileStore ??= this.injector.get(BigProfileStore);
this.visible.update((v) => !v);
}
}
function maskSession(s: Session | null): Session | null {
return s ? { ...s, bsn: maskBsn(s.bsn) } : null;
}