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>
162 lines
5.6 KiB
TypeScript
162 lines
5.6 KiB
TypeScript
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;
|
||
}
|