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) { @if (visible()) {
{{ snapshot() | json }}
} } `, }) 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; }