# Foundation Follow-ups

Known gaps and explicit Phase 2 entry points. Authoritative status is in
`governance/scorecard/risk-register.md` (the IDs below cross-reference it).

## Active risks

| ID                | Item | Phase 2 action |
| ----------------- | ---- | -------------- |
| ~~R-2026-04-19-01~~ | ~~BMW strings in product core~~                                 | **Closed** 2026-04-19 — see risk register Resolved row. |
| ~~R-2026-04-19-02~~ | ~~CSP allows `'unsafe-inline'` and `'unsafe-eval'` on script-src~~ | **Closed** 2026-04-19 — per-request nonce + `'strict-dynamic'` wired via `proxy.ts` and `lib/security/csp.ts`. `'unsafe-inline'` removed from script-src. `'unsafe-eval'` retained in dev only (HMR). `style-src 'unsafe-inline'` remains as a documented residual (inline `style={{...}}` attributes — CSP nonces do not cover style attributes). |
| R-2026-04-19-03   | `service_role_key` rotation cadence not formalized                | **Partial close 2026-04-19 (R-03).** Runbook published at `docs/security/service-role-key-rotation.md`; env-loading centralized in `lib/supabase/privileged-env.ts`; `npm run check:secrets` gate + `npm run preflight:rotation` helper added. Remaining: (a) operator rotation dry-run in staging, (b) secrets-manager integration once infra team picks one, (c) rotation-bot with its own scoped tokens (never handles the key it rotates). |
| ~~R-2026-04-19-04~~ | ~~console.* calls remain in server code~~                      | **Closed** 2026-04-19 — 45 call sites migrated to `logger.*` (server + client). ESLint `no-console: error` now enforced across `app/`, `components/`, `lib/`, `hooks/`, `config/`. Allowed zones: the logger transport itself, tests, and `scripts/`. See risk register Resolved row. |
| R-2026-04-19-05   | 3 oversized components remain (`project-edit-form` 1437 lines, `vsm-editor` 1192, `oee-calculator` ~640 after split) | **Partial close 2026-04-19 (R-05).** `project-edit-form.tsx` 1437 → 1340 (3 sibling modules extracted: helpers, payload, conflict-modal). `vsm-editor.tsx` 1192 → 1033 (7 sibling modules: config, geometry, metrics, excel, 3 modals). 48 characterization tests added locking extracted logic. Remaining: VSM canvas pan/zoom/drag state (needs real-browser test harness first); other oversized files (`project-tabs.tsx` 928, `berichte-client.tsx` 825, `planning-client.tsx` ~1100). |
| ~~R-2026-04-19-06~~ | ~~Boundaries lint runs in warn~~                                | **Closed** 2026-04-19 — relocated `components/ui/master-data-combobox.tsx` → `components/master-data/`, `lib/i18n/assessment-lang.ts` → `lib/assessment-lang.ts`, promoted to error, added `scripts/check-boundaries.mjs` and `npm run check:boundaries`. |
| R-2026-04-19-07   | `bmw.ts` profile has no real BMW infra wired (placeholders only)  | Phase 9 plan: add Connect-IT / WebEAM / Q-number adapters under `lib/customers/bmw/` |

## What was deferred from Phase 1

- **Component split**: only `oee-calculator.tsx` was split this session
  (827 → 639 lines, with `oee-form-constants.ts` and `oee-form-fields.tsx`
  extracted). `project-edit-form.tsx` and `vsm-editor.tsx` remain — both
  high-risk to refactor without a behavior-test net.
- **Logger migration**: scoped to a representative set of high-leverage
  server files (email notifications, owner/admin/tenant API routes). Tracked
  as R-2026-04-19-04.
- **Adapter contract tests**: Phase 2 scope per ADR 010. The portability
  matrix runs schema/lint/typecheck per profile today; full per-profile test
  suite is a Phase 2 add.

## Phase 2 status (2026-04-19)

R-01 (customer isolation), R-06 (boundary enforcement), and the interface
hardening / OpenAPI v1 foundation block are all closed:

- `lib/customers/bmw/` is the canonical BMW adapter zone. It owns the design
  system + palette today; Phase 9 adapters (`identity-webeam`,
  `otel-dynatrace`, mail gateway, ITSM, BMW Postgres) slot in alongside.
- The generic `IntegrationsProfileSchema` no longer names BMW vendors. New
  customer adapters select via the `"custom"` enum slot.
- `CHECK_FORBIDDEN_LEVEL=error` is wired in `portability-matrix.yml`. The
  forbidden-strings check returns 0 hits in product-core paths.
- `boundaries/element-types` is **error** in `eslint.config.mjs`. The
  dedicated `npm run check:boundaries` script gates CI; `check:portability`
  now also includes it.
- Domain widgets and domain i18n helpers were relocated out of shared:
  `components/ui/master-data-combobox.tsx` → `components/master-data/`,
  `lib/i18n/assessment-lang.ts` → `lib/assessment-lang.ts`.
- Public-surface barrels added for the highest-leverage modules:
  `lib/api`, `lib/control-plane`, `lib/entitlement`, `lib/oee`,
  `lib/owner-audit`, `lib/provisioning`, `lib/support`, `lib/tenant`.
  See [`api-surface.md`](./api-surface.md) for the inventory.
- Initial OpenAPI v1 foundation lives at `openapi/v1/openapi.json`; the
  contract rules are in [`api-contract.md`](./api-contract.md). Validated by
  `npm run check:openapi` (bundled into `check:portability`).
- Three v1 routes (health, master-data, suppliers) demonstrate the new
  envelope helper convention from `@/lib/api`. Other v1 routes still inline
  `NextResponse.json`; migrate opportunistically.

## Phase 2 remaining entry points

1. ~~R-02: adopt the Next.js nonce CSP strategy; drop `'unsafe-inline'` /
   `'unsafe-eval'`.~~ **Closed 2026-04-19** — per-request nonce + `'strict-dynamic'`
   now on `script-src`; CSP moved out of `next.config.mjs` into `proxy.ts`.
   ~~R-02-B: stand up a CSP report endpoint and low-noise review path.~~
   **Closed 2026-04-19** — `app/api/csp-report/route.ts` accepts both
   legacy and Reporting API payload shapes, normalizes via
   `lib/security/csp-report.ts`, logs structured `csp.violation.*` events,
   and is excluded from the proxy matcher so unauthenticated reports
   survive. `report-uri` + `report-to` are wired, a paired
   `Reporting-Endpoints` response header is emitted, and `check:csp`
   asserts the wiring. Staging rollout playbook + readiness gate documented
   in `csp-strategy.md`.
   Next open items on this axis:
   - 7-day staging observation window before flipping `CSP_ENFORCE=1`
     (sign-off on risk register required).
   - Refactor inline `style={{...}}` attributes to a single nonced
     `<style>` tag in `<head>` so `style-src 'unsafe-inline'` can also be
     dropped. 5 files involved, highest-leverage is `app/layout.tsx`
     (tenant branding CSS variables).
2. ~~R-04: bulk-migrate remaining `console.*` calls in server code to `logger.*`
   and add a lint rule.~~ **Closed 2026-04-19** — 45 call sites migrated
   across server + client code; `no-console: error` is active. Browser-side
   logger transport (ship client-side logs to a collector) stays deferred as
   a later observability phase — the facade interface is stable, only the
   `emit()` function swaps.
3. ~~R-05: continue the oversized-component split sprint
   (`project-edit-form.tsx` 1437, `vsm-editor.tsx` 1192).~~ **Partial close
   2026-04-19** — both top offenders reduced. `project-edit-form.tsx`
   1437 → 1340 via `project-edit-form.helpers.ts` (64 lines, pure date/ISO-week
   helpers), `project-edit-form.payload.ts` (226 lines, the full `projects`
   Supabase update contract: `buildProjectUpdatePayload` + `buildConsultantEntries`
   + `buildTypeAssignmentRows`), and `project-edit-form.conflict-modal.tsx`
   (67 lines, `VisitConflictModal`). `vsm-editor.tsx` 1192 → 1033 via
   `vsm-config.ts` (NODE_CONFIG), `vsm-geometry.ts` (NODE_W/getNodeH/port
   positions/buildPath SVG cubic), `vsm-metrics.ts` (findBottleneckId +
   computeTimeline), `vsm-excel.ts` (mapExcelRowsToPreview), and three modal
   components (stoppuhr picker, excel import, LSC import). 48 new tests in
   `__tests__/project-edit-form-helpers.test.ts` (15), `__tests__/project-edit-form-payload.test.ts`
   (13), `__tests__/vsm-editor-logic.test.ts` (20). Remaining open items:
   - VSM canvas interaction state (pan/zoom refs + global mousemove/mouseup
     + SVG world transform) is intentionally NOT split — needs a real-browser
     test harness (jsdom or Playwright) before it is safe to decompose.
   - `project-tabs.tsx` (928 lines), `berichte-client.tsx` (825 lines),
     `planning-client.tsx` (~1100 lines) not yet addressed.
4. Migrate `eslint-plugin-boundaries` to v6 selector syntax (currently runs
   on legacy `boundaries/element-types`; v6 prefers `boundaries/dependencies`
   with object selectors). When migrating, add an `internal/` selector so the
   public-surface rule (only `index.ts` imports allowed) is enforced in code,
   not just convention.
5. ~~Extend the portability matrix to run the full Vitest suite per profile.~~ **Closed 2026-04-19** — Vitest (239 tests, env:node, no network) now runs as a hard gate in `portability-matrix.yml` after `check:openapi` and before `Full lint`, in both matrix profiles (default, bmw) and on the nightly schedule.
6. ~~Migrate the remaining v1 routes to envelope helpers.~~ **Closed
   2026-04-19** — `projects`, `projects/[id]`, `oee`, `assignments` all use
   `@/lib/api`. `DELETE /projects/{id}` preserves its legacy `{ success: true }`
   shape; this is documented in the OpenAPI spec. Note: `GET /health` also
   intentionally keeps a flat body (not an envelope) because external
   liveness probes read `response.status` at the top level. Documented in
   scorecard "Top remaining structural cleanups" #3.
7. Migrate the remaining tenant/entitlement/owner-audit/control-plane deep
   imports to the new module barrels. **2026-04-19 progress:** all 22 owner
   routes migrated (+ 9 cross-module lib/ files, + 2 layouts). Remaining
   scattered deep imports live in `app/api/admin/*`, `app/api/wertstrom/*`,
   `app/api/repository/*`, `app/api/planning/*`, `app/api/qaf-template/*`,
   `app/api/projects/next-id/*`, `app/api/holidays/*`, `app/api/demo/*`, and
   a handful of `app/owner/**` page files. Migrate opportunistically; these
   routes change rarely and don't block the Phase 2 close.
8. OpenAPI schema tightening. **2026-04-19 progress:** `Supplier` and
   `SupplierCreate` are now closed (`additionalProperties: false`) and
   enumerate every column in `supplier_master_data`. `Project`, `OeeRecord`,
   and `Assignment` stay open (`additionalProperties: true`) with
   descriptions that call out why:
     - `Project` — PATCH/POST return the full row including 25+ MO-24
       columns whose surface is not yet stable.
     - `OeeRecord` — route handler references `week_number`; DB column is
       `calendar_week`. Schema tracks the route, not the DB, until the route
       is fixed.
     - `Assignment` — route uses `start_date`/`end_date` and joins
       `planning_projects` via `planning_project_id`; DB has a single `date`
       column and no such FK. Schema tracks the route.
   Next step belongs to the MO-OEE / MO-23 modules: reconcile the SQL, then
   tighten the spec. See `api-surface.md` "Known schema-vs-route mismatches".
9. ~~Pre-existing typecheck noise: `__tests__/*.test.ts` mock-cast
   errors~~ **Closed 2026-04-19.** All 61 TS2352 errors across 7 test
   files resolved by routing the cast through `unknown` (the
   remediation TypeScript itself suggests and the precedent already
   used in `__tests__/seeder.test.ts`). No product code touched, no
   test behavior changed, 239/239 tests pass. Typecheck step in
   `portability-matrix.yml` was promoted from `continue-on-error: true`
   back to a hard CI gate in the same PR.
10. Pre-existing lint noise: 47 errors / 115 warnings in `npm run lint`
    (React-19 `set-state-in-effect`, `no-explicit-any` in export-service).
    Fix or move them to `--fix` / boundaries-style focused gates.
11. Consider a `lib/branding/` barrel. Only two external consumers today
    (`invalidateBrandingCache` in `app/api/owner/tenants/[tenantId]/branding`,
    `createControlPlaneReadonlyClient` inside the engine). Add a barrel when
    the third consumer shows up; not worth the churn today.
12. **Module-gated layouts return redirects under anon-fallback CP client** —
    after PR `fix/control-plane-soft-fallback-and-layout-dynamic`, every
    `requireTenantAndModule`-guarded layout (fabrikanalyse, planning,
    assessment, repository, oee, kalender, lsc-workshop) redirects to
    `/projektanlage?blocked=...` because `cp_*` tables have service_role-only
    RLS and the fallback anon client reads empty. Resolution options:
    (a) set `CONTROL_PLANE_READONLY_KEY` in Vercel env (requires non-anon
    read access to cp_*), or (b) extend cp_* RLS policies to allow
    authenticated SELECT for single-instance deployments (architecturally
    cleaner). Decide based on multi-tenant timeline.

    **RESOLVED 2026-04-20** via single-instance RLS relaxation
    (option b, with a correction): a first attempt added
    `authenticated`-only SELECT policies on 7 cp_* tables
    (cp_tenants, cp_plans, cp_features,
    cp_plan_feature_entitlements, cp_tenant_feature_overrides,
    cp_branding_profiles, cp_release_rollouts), but
    `createControlPlaneReadonlyClient` runs without a user session
    so queries execute under the `anon` role — the policies
    silently filtered to empty. The effective fix widened the role
    target to `anon, authenticated` via
    `supabase/rls/supabase-rls-cp-tables-public-read.sql`. 6 cp_* tables remain
    service_role-only (cp_tenant_environments, cp_audit_events,
    cp_provisioning_jobs, cp_provisioning_job_steps,
    cp_support_access_sessions, cp_owner_users). Multi-tenant
    activation requires a tenant-scoped rewrite — see item #15
    below.
13. **Apply cp_subscriptions migration and extend RLS for tenant-
    runtime reads** — `supabase/migrations/supabase-migration-platform-v2.sql` defines
    the commercial-lifecycle table `cp_subscriptions` but was never
    applied to production. `checkTenantStatus()` had its subscription
    embed removed in PR #8 to restore functionality after the
    control-plane anon-fallback hit the missing FK. Restore the embed
    (revert the change in `lib/tenant/status.ts:26-30` and re-add the
    subscription check) when (a) the migration is applied in
    production, (b) the new table's RLS is extended to authenticated-
    read for single-instance deployments (same pattern as #12), and
    (c) the seed INSERT at `supabase/migrations/supabase-migration-platform-v2.sql:323-330`
    has been verified to have run. Blocks enforcement of trial / grace
    / cancelled access gating.
14. **eslint-plugin-boundaries legacy selector syntax warning** —
    CI emits `Detected legacy selector syntax in 3 rule(s) at
    indices: 0, 1, 2` during `npm run check:boundaries` (observed
    on PR #9). Non-blocking; lint still reports clean. Migrate to
    object-based selectors per
    https://www.jsboundaries.dev/docs/releases/migration-guides/v5-to-v6/.
    Low priority; no behavioural impact. Related to existing
    entry #4 (Phase 2 remaining entry points) which already
    contemplates the v6 migration — this entry is the operational
    reminder, #4 is the architectural shape.
15. **Multi-tenant activation requires cp_* RLS rewrite** — the
    2026-04-20 single-instance fix (item #12) grants SELECT to
    anon+authenticated on every row of 7 cp_* tables (cp_tenants,
    cp_plans, cp_features, cp_plan_feature_entitlements,
    cp_tenant_feature_overrides, cp_branding_profiles,
    cp_release_rollouts). Acceptable for single-tenant, unsafe
    for multi-tenant: tenant A would read tenant B's branding,
    plan assignment, entitlements, and the full tenant directory.
    Before provisioning a second live tenant, replace these
    policies with tenant-scoped variants — options include (a) a
    JWT tenant claim enforced via `auth.jwt() ->> 'tenant_id'`,
    (b) a `cp_tenant_users` mapping table used via `EXISTS (...)`,
    or (c) standing up a signed-JWT non-anon readonly channel
    that makes `authenticated`-only policies viable again.
    Promote to an ADR when multi-tenant is on the near-term
    roadmap.
16. **Complete migration to new publishable/secret API keys** —
    2026-04-21: Vercel `SUPABASE_SERVICE_ROLE_KEY` rotated from
    Legacy HS256 JWT to `sb_secret_...` (new asymmetric key
    system) in response to the April 2026 Vercel security
    incident. Legacy JWTs remain in parallel so iOS (which
    embeds the legacy `anon` JWT in
    `SupplierDev/SupplierDev/Utilities/Constants.swift`,
    expires 2036) continues working. Next steps: (a) replace
    iOS-embedded legacy anon with `sb_publishable_...`,
    rebuild, App Store release; (b) after iOS is live, revoke
    Legacy HS256 JWT secret in Supabase → Settings → JWT Keys
    → Previous Key → Revoke; (c) then disable legacy
    anon/service_role API keys entirely in Supabase → Settings
    → API Keys → "Disable JWT-based API keys". Until all three
    are done, the Legacy-anon JWT remains a long-lived
    credential embedded in every installed iOS binary —
    attackers with that key plus the Supabase URL get anon-role
    DB access, limited by RLS.
17. **Supabase log retention too short for production forensics**
    — Free Tier limits Logs & Analytics to ~1 hour retention.
    When investigating the April 2026 Vercel security incident,
    no forensic record of the 19–21 April window existed to
    confirm or refute unauthorized service-role-key usage.
    Rotation was done as a precaution, not as evidence-based
    response. If KADi continues on this Supabase project for
    production, upgrade to a plan with reasonable log retention
    (Pro Tier has 7-day logs). Alternatively, configure Log
    Drains to an external sink for longer-term retention.
18. **RLS audit backlog tracked in dedicated document** — the
    18 tables with rls_enabled=false are inventoried, risk-rated,
    and sequenced in docs/foundation/rls-audit-2026-04-21.md.
    Future fix PRs should reference that document, follow its
    proposed sequence (PR A through PR I), and update the doc
    inline as each target is fixed.

19. **RLS activation of `public.master_data_audit_log` pending
    INSERT policy design** — audit on 2026-04-22 (PR #18) revised
    the table's complexity from Simple to Moderate. Live-DB policy
    state: two SELECT policies only (`audit_select` USING (true),
    `mdal_admin` admin-gated via EXISTS on user_profiles JOIN
    roles). No INSERT / UPDATE / DELETE policy exists. Code audit
    found three client-side INSERT paths that would break on naive
    activation:
    `components/repository/supplier-master-data-client.tsx:599,611`
    (deactivate + hard-delete supplier master data);
    `components/repository/department-master-data-client.tsx:441,451`
    (deactivate + hard-delete department master data);
    `lib/duplicates/merge.ts:65` (duplicate-merge audit). Before
    activation, resolve three open questions: (a) style consistency
    — should the new INSERT policy use the `current_user_role()`
    helper (as in `holiday_entries_write`, `holiday_calendars_write`,
    `mdt_admin`) or the explicit EXISTS form (as in the existing
    `mdal_admin` SELECT policy)? (b) confirm that all three
    client-side paths are only invokable by admin/masteradmin users
    in the UI, or if a broader INSERT policy is required;
    (c) check for server-side INSERT paths beyond the *.ts/*.tsx
    grep — `MO-25/repository-phase4.sql` references triggers;
    verify whether any are still active on the live schema. Once
    answered, the activation PR should add a CREATE POLICY for
    INSERT gated to admin/masteradmin BEFORE calling
    `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`, in the same
    migration SQL file.

    *RESOLVED 2026-04-27 (PR #22). Activation strategy:
    CREATE POLICY mdal_insert (helper-form, admin-gated)
    BEFORE ENABLE RLS, in one transaction. UPDATE/DELETE
    intentionally remain blocked (append-only design).
    Three Phase-0-discovered risks documented as
    follow-ups #21, #22, #23 — non-blocking for activation,
    tracked separately. A fourth (#24) tracks a dead
    component carrying an audit-log INSERT contract.*
20. **PR D simple-batch assumption invalidated; subset 2
    requires policy design** — the audit doc's PR D was
    drafted as a "repeat PR C pattern" for six tables. A
    2026-04-23 live-DB check on the six revealed two distinct
    sub-patterns:
    (a) `master_data_values` and `department_master_data`
    have a clean duplicate-collapse opportunity (one
    offensively-lax SELECT policy plus two redundant
    admin-gated write policies — one using
    `current_user_role()` helper, one using explicit EXISTS).
    Activation requires (i) dropping the lax SELECT
    duplicate, (ii) consolidating the two redundant
    admin-gated write policies into a single canonical one
    (style-consistency decision: helper vs. EXISTS — see
    follow-up #19 for the same question on
    `master_data_audit_log`).
    (b) `appointment_types`, `document_departments`,
    `document_metadata`, `document_suppliers` have FOUR or
    FIVE permissive policies each, ALL with `USING (true)`
    or `WITH CHECK (true)` for INSERT/UPDATE/DELETE. There
    is NO admin-restriction. Activation as-is would
    enforce the lax policies, producing zero security
    benefit. These tables require POLICY REWRITES (not
    collapse), and the rewrite needs a product decision:
    who is allowed to mutate `appointment_types` /
    `document_*`? Only admins? Specific roles? The audit
    doc's complexity rating "Moderate" was optimistic for
    this subset — closer to "Complex" or "Architectural".
    Defer to a separate PR (or batch of PRs) once the
    product decision is made.

21. **Auth-mechanism outlier: SuppliersPage gates on
    `consultants.role` while the rest of the app uses
    `user_profiles+roles`** — discovered during PR #22
    Phase 0 audit (2026-04-23). The
    `app/repository/masterdata/suppliers/page.tsx` server
    page redirects non-admins by reading
    `consultants.role`, while every other admin gate
    (including the `current_user_role()` helper used by
    the new `mdal_insert` policy and the
    `getUserSession()` server util) reads
    `user_profiles JOIN roles`. Today the two are
    consistent for the only logged-in admin (operator
    masteradmin), but the architectural drift is real —
    a future consultant added to `consultants` with role
    admin/masteradmin but without a corresponding
    `user_profiles` row would (a) pass the SuppliersPage
    gate and reach the supplier-master-data UI but
    (b) fail the new RLS INSERT policy on
    `master_data_audit_log` because
    `current_user_role()` would return NULL for them.
    Result: deactivate / hard-delete actions could
    succeed at the data layer (those tables also have
    lax or no RLS) while the audit-log INSERT 403s,
    leaking writes that are not audited. Recommendation:
    either migrate SuppliersPage to `user_profiles+roles`
    (preferred — one canonical auth mechanism), or
    document why `consultants` is a distinct entity for
    that page and add a defensive check that the
    consultant row also has a `user_profiles` entry. Out
    of scope for the RLS activation; tracked here.

22. **Non-fatal audit-insert in `lib/duplicates/merge.ts`
    creates silent audit-trail gaps under RLS** —
    discovered during PR #22 Phase 0 audit (2026-04-23).
    Lines 77-79 of `lib/duplicates/merge.ts` catch INSERT
    failures into `master_data_audit_log` with a
    `logger.error` call but the surrounding
    `mergeRecords` server action returns
    `{ success: true }` regardless. Pre-RLS, INSERT
    failures were rare (table had no RLS to bounce them).
    Post-RLS, edge cases — JWT-stale, role-cache desync,
    helper-function transient failure — could result in
    a successful merge with no audit record, and the
    user / caller never knows. Recommendation: choose
    one of (a) make the INSERT fatal so a failed audit
    rolls back the merge, or (b) keep best-effort INSERT
    but add a structured retry + alerting path so silent
    drops are at minimum observable. Probably (a) is
    safer: audit logs are compliance evidence; a merge
    without an audit row should not be considered
    successful. Out of scope for the RLS activation;
    tracked here.

23. **Two ghost consultants with `auth_user_id=NULL` and
    `role IN (admin, masteradmin)`** — discovered during
    PR #22 Phase 0 audit (2026-04-23 / 2026-04-24). The
    `consultants` table contains three rows whose role is
    admin or masteradmin; one is the operator's
    masteradmin and is consistent with `user_profiles`,
    the other two have `auth_user_id=NULL` and cannot
    log in (no authentication path produces a NULL
    `auth.uid()`). Origin unclear — possibly legacy
    seeding, staging-environment artifacts mistakenly
    promoted, or migration debris from an earlier
    consultants-table refactor. Recommendation:
    investigate origin (git log on `consultants`
    migrations + supabase/bootstrap/supabase-schema.sql history); if
    legacy/staging, delete; if intentional (e.g.
    placeholder rows for a future SSO flow), document
    the intent inline in the schema comments. No
    immediate security risk because the rows are
    unreachable as logged-in users, but they pollute
    role-count queries and create confusion in audits.
    Out of scope for the RLS activation; tracked here.

    *Update 2026-04-27 (post-investigation):*
    Live-DB query against `consultants` revealed the
    two `auth_user_id=NULL` rows are not separate
    identities but earlier self-onboarding versions of
    the operator's own consultant record (both
    display_name "operator", team_code MO-23,
    created 2026-03-31 within minutes of each other,
    role admin). The operator's canonical consultant
    row is a third record linked to auth_user_id
    d0be602e-ab9f-403f-90e4-9ea8e155ed5f (masteradmin).
    FK-reference check found 13 production rows
    pointing to the ghost IDs: 6 in
    `assignments.consultant_id`, 6 in
    `project_consultants.consultant_id`, 1 in
    `projects.project_lead_id`, 0 in
    `projects.created_by_consultant_id`. A standalone
    DELETE is therefore not safe (would orphan or
    break those rows). Operator clarified intent: the
    development environment contains substantial
    test/dummy data accumulated during build, and a
    holistic pre-handover cleanup is planned before
    customer delivery. The ghost consultants and their
    13 FK references are scoped into that final
    cleanup, not handled as a standalone fix.
    **Status: deferred to final pre-handover cleanup
    phase (see follow-up #28).** Not a permanent
    design decision; the canonical resolution will
    come during the holistic cleanup.

24. **Dead component `DepartmentMasterDataClient` defined
    but not mounted** — discovered during PR #22 Phase 0
    audit (2026-04-23). The file
    `components/repository/department-master-data-client.tsx`
    contains an audit-log INSERT path at lines 441
    (deactivate) and 451 (hard-delete) and exports the
    `DepartmentMasterDataClient` component, but no page
    or layout under `app/` mounts it. Either (a) the
    feature was planned and never finished — mount it
    on `app/repository/masterdata/departments/` (or
    similar) when the product flow is ready; or (b) the
    feature was abandoned — delete the file and its
    audit-log INSERT path. Until decided, the component
    is dead code that still imports the audit-log INSERT
    contract; a future RLS-enforced
    `master_data_audit_log` schema or policy change
    would silently break a code path nobody runs. Out
    of scope for the RLS activation; tracked here.

    *Update 2026-04-27 (post-investigation):*
    Live-codebase audit revealed more nuance than the
    original entry suggested.
    (1) The component is 591 lines and has been
    deliberately split into sub-components
    (`department-table.tsx`, `department-toolbar.tsx`
    — see header comment), indicating non-trivial
    engineering investment, not abandoned scaffolding.
    (2) A route stub does exist at
    `app/repository/masterdata/departments/page.tsx`,
    but it is a 5-line redirect to
    `/repository/masterdata` — i.e., the page exists
    and is intentionally disabled, not absent.
    (3) The data type `department_master_data` is
    alive in 16 files across admin user modals,
    account form, repository-types, document drawer,
    project-documents, master-data-provider,
    upload-dialog, and entity-resolver. Only the
    dedicated management UI is unreachable; the
    underlying domain data is heavily consumed
    elsewhere.
    (4) Operator confirmed (2026-04-27) that this
    code was authored by them, but the original
    product/scheduling rationale for unmounting the
    management UI is not currently retrievable from
    memory.
    **Status: not dead code, not safe to delete;
    intentionally-disabled UI of unknown vintage.**
    Deferred until either (a) operator recovers the
    original rationale (git log / commit history /
    parallel project notes may help), or (b) the
    final pre-handover phase (#28) forces a decision
    (delete the dead UI scaffolding, or finish +
    mount it). Tracked here so the future decision
    is anchored.

25. **`changed_by` is NULL in `master_data_audit_log`
    inserts** — discovered during PR #22 smoke-test
    verification (2026-04-27). The hard-delete path in
    `components/repository/supplier-master-data-client.tsx`
    (around line 611) successfully inserts a row into
    `master_data_audit_log` under the new `mdal_insert`
    policy, but the `changed_by` column is left NULL even
    though the action was triggered by an authenticated
    masteradmin session. The insert appears to omit the
    actor identity. Recommendation: populate `changed_by`
    from the authenticated session (e.g.
    `(await supabase.auth.getUser()).data.user?.id` or
    equivalent) on every audit-log insert. Audit trails
    without the responsible user are significantly less
    valuable for compliance and incident investigation.
    Out of scope for this RLS PR; tracked here.

    *RESOLVED 2026-04-27 (PR #26). Pattern-template from
    components/project/create-supplier-inline.tsx:125-138
    copied verbatim into the 4 reachable write paths in
    supplier-master-data-client.tsx and the audit-insert in
    merge.ts. Live smoke-test confirmed changed_by populated
    on the new insert. Dead department paths (#24) NOT
    touched.*

26. **`created_by` and `updated_by` are NULL in
    `supplier_master_data` rows** — observed in the
    `old_data` snapshot of the audit entry created during
    PR #22 smoke-test (2026-04-27). The deleted Test123
    row had both columns set to NULL despite being created
    and modified by an authenticated masteradmin session.
    Suggests the user-tracking pattern is broken or never
    implemented across the master-data write paths.
    Recommendation: audit all master-data write paths
    (create, update) for missing `created_by` /
    `updated_by` population, and either implement
    consistent population from the authenticated session
    or document why these columns are intentionally
    unused. Related to follow-up #25 — likely the same
    root cause. Out of scope for this RLS PR; tracked
    here.

    *RESOLVED 2026-04-27 (PR #26). Same pattern-template as
    #25. created_by populated on supplier_master_data INSERT
    (line 579), updated_by populated on UPDATE deactivate
    path (line 596). Backfill of 9350 existing NULL rows
    not in scope; deferred to final cleanup phase #28.*

27. **Branch-creation from non-clean main bundled
    unrelated commits in PR #22** — discovered during
    post-merge sync (2026-04-27). The operator committed
    a parallel gstack-installation change (4415164,
    19:35:23) to main during the session, while the
    prompt-engineer was still in Phase 1 of PR #22.
    Phase 2 then ran `git checkout -b
    chore/rls-enable-master-data-audit-log` from main,
    which carried 4415164 forward into the feature
    branch. The squash-merge of PR #22 (b4c6ae0)
    therefore bundled gstack + RLS into one commit, with
    a title scoped to RLS only. Audit-trail single-
    purpose principle violated; technical state correct.
    Convention going forward: before `git checkout -b`
    for any feature branch, verify
    `git log --oneline origin/main..main` returns no
    commits — i.e. local main is in sync with origin/main
    and carries no unmerged operator side-work. If it
    does, either push that work as a separate PR first,
    or stash it before branching. Out of scope to fix
    retroactively (no revert, no force-push); this entry
    documents the convention and the precedent.

28. **Final pre-handover data cleanup phase** —
    operator-stated 2026-04-27. Before the
    application is handed over to the customer,
    the development environment needs a holistic
    cleanup of test/dummy data accumulated during
    build. Currently identified items: the two
    ghost consultant records and their 13 FK
    references (follow-up #23). Likely many more
    dummy entries exist across master-data,
    suppliers, projects, assignments, and other
    domain tables — full inventory not yet taken.
    When this phase starts it will need (a) a
    complete inventory of test/dummy data per
    table (probably driven by simple discriminators
    like display_name patterns, created_at ranges,
    or known-test-supplier numbers), (b) a decision
    tree per row (delete, anonymize, or keep as
    legitimate domain data), (c) a sequenced
    migration plan that respects FK constraints
    (delete child rows before parents, or
    re-assign children to canonical anchors), and
    (d) a verification pass against the cleaned
    database. Out of scope until the cleanup phase
    starts; tracked here so the work is
    discoverable later.

29. **Schema-level user-tracking-column gap across 15
    public tables** — discovered during the Phase 0
    audit for PR #26 (2026-04-27). Information_schema
    inventory revealed user-tracking columns
    (created_by / updated_by / changed_by) on 15 public
    tables: app_settings, assessment_responses,
    assessments, assignments, audit_log, consultants,
    cp_owner_users, cp_tenants, master_data_audit_log,
    master_data_suggestions, master_data_values,
    oee_records, supplier_master_data, user_profiles,
    value_stream_maps. PR #26 fixes only the two called
    out in follow-ups #25/#26 (master_data_audit_log
    and supplier_master_data). The other 13 may exhibit
    the same management-UI gap — none audited yet.
    Recommendation: when each of the other 13 tables
    comes into RLS-activation or feature-work scope,
    audit its write paths against the
    management-UI-vs-inline-flow split discovered for
    supplier_master_data (Phase 0 of PR #26 found that
    inline flows populate correctly, management UIs
    sometimes don't — likely the same root cause where
    the management component lacks an auth identity
    source). Backfill of legacy NULL rows on any of
    these tables, if needed, to be coordinated with
    follow-up #28 (final pre-handover data cleanup).
    Out of scope for PR #26; tracked here as a
    discoverable backlog anchor.

    *Audit pattern (verified pilot 2026-04-28):*
    Per-table audit follows 5 steps before any code
    change is considered:
    (1) Daten-Inventar — count NULL vs populated for
    each tracking column to gauge how broken (or not)
    the pattern is in current production data.
    (2) Schema-Check — confirm tracking columns have
    no DB-level default (e.g. `auth.uid()`); if a
    default exists, the column is auto-populated and
    no code-side fix can be inferred from the data
    alone.
    (3) Code-Audit — `grep -rn "from('<table>')"
    --include="*.ts" --include="*.tsx"` to enumerate
    read and write paths.
    (4) Raw-SQL-Check —
    `grep -rn "INSERT INTO <table>\|UPDATE <table>"
    --include="*.sql" --include="*.ts" --include="*.tsx"
    --include="*.mjs" --include="*.cjs"` to verify
    no migration or seed scripts bypass the JS-client
    write paths.
    (5) Code-Inhalt der Schreib-Pfade — read each
    write path's surrounding code to verify the
    PR-#26 user-source pattern is applied
    (`const { data: { user } } = await
    supabase.auth.getUser()` followed by
    `created_by: user?.id ?? null` or the
    context-appropriate variant such as `updated_by`,
    `changed_by`, or `reviewed_by`).

    *Pilot audit completed 2026-04-28 — master_data_suggestions:*
    Pattern correctly applied at all 3 write paths;
    no code change needed for this table. Findings:
    - Daten-Inventar: 1 row total, `created_by`
      populated, `reviewed_by` NULL but expected NULL
      because the row's status is 'pending' (no review
      has occurred). Daten-Lage inkonklusiv for
      `reviewed_by` from data alone.
    - Schema-Check: `created_by` and `reviewed_by`
      are uuid NULL with no DB default. `created_at`
      has `now()` default; `id` has `gen_random_uuid()`
      default. No `updated_by`/`changed_by` columns
      on this table.
    - Code-Audit: 5 grep hits, 4 files. 2 read paths
      in `app/repository/masterdata/stammdaten/`.
      3 write paths:
      `components/master-data/master-data-combobox.tsx:138`
      (INSERT, suggestion submission),
      `components/repository/stammdaten-detail-client.tsx:191`
      (UPDATE, approve), and
      `components/repository/stammdaten-detail-client.tsx:206`
      (UPDATE, reject).
    - Raw-SQL-Check: 0 raw SQL writes.
    - Code-Inhalt: combobox.tsx:138 uses `getUser()`
      at line 137 + `created_by: user?.id ?? null`
      at line 143 (correct). stammdaten-detail-
      client.tsx:191 uses `user` already in scope
      + `reviewed_by` and `reviewed_at` at lines
      194-195 (correct). stammdaten-detail-
      client.tsx:206 uses `getUser()` at line 205 +
      `reviewed_by` and `reviewed_at` at lines
      208-209 (correct).
    Conclusion: the PR-#26-style management-UI gap
    does not manifest on this table. **Status for
    master_data_suggestions: resolved-no-action-
    needed.** 1 of 13 remaining #29 tables audited;
    12 to go.

    *Pilot audit completed 2026-04-28 — assessments:*
    Pattern correctly applied at all 2 user-tracking-
    relevant write paths; no code change needed for
    this table. Findings:
    - Daten-Inventar: 10 rows total, `created_by`
      populated on all 10 (0 NULL). Larger sample
      than the master_data_suggestions stichprobe;
      strongly indicates the pattern is genuinely
      applied rather than coincidence.
    - Schema-Check: `created_by` is uuid NULL with no
      DB default. `id` has `gen_random_uuid()` default,
      `created_at` and `updated_at` have `now()`
      defaults. `created_by` is the single user-
      tracking column on this table — no
      `updated_by`, `changed_by`, `reviewed_by`, or
      other tracking columns exist.
    - Code-Audit: 11 grep hits in 11 files. ~8 read
      paths across reporting, project analysis,
      fabrikanalyse pages, demo-status route, and
      berichte components. 4 write-side hits, of
      which 2 are user-tracking-relevant:
      `components/project/new-project-form.tsx:318`
      (INSERT, user-facing Fabrikanalyse auto-create)
      and `lib/demo/seeder.ts:386` (UPSERT, demo
      seeder). The other 2 are DELETEs
      (`seeder.ts:441` and
      `app/api/demo/reset/route.ts:73`) where
      tracking-column population is not applicable.
    - Raw-SQL-Check: 0 active raw-SQL writes (1
      commented-out hint at
      `supabase/migrations/supabase-migration-cleanup.sql:23`, inactive).
    - Code-Inhalt: new-project-form.tsx:318 uses
      `created_by: user.id` directly (not
      `user?.id ?? null`); the user variable is
      guaranteed in scope by an earlier guard, so
      the non-null short form is safe and idiomatic.
      seeder.ts:386 uses `created_by: userId` where
      userId is passed by the caller. Both correct.
    Side-observations recorded for context, not
    requiring action:
    - Pattern variant `user.id` (without
      `?? null` fallback) is valid when the user is
      guaranteed to be in scope by a prior guard.
      Functionally equivalent to the canonical
      `user?.id ?? null` form from the PR-#26 template;
      both forms acceptable depending on call site.
    - Demo-seeded assessments inherit the operator's
      auth user id as `created_by`, meaning production
      demo data carries the operator's UUID as
      author. Relevant for follow-up #28 (final pre-
      handover cleanup): demo rows should likely be
      deleted or reassigned to a system user before
      customer handover. No new follow-up created;
      #28 is the existing anchor.
    Conclusion: the PR-#26-style management-UI gap
    does not manifest on this table. **Status for
    assessments: resolved-no-action-needed.** 2 of
    13 remaining #29 tables audited; 11 to go.

    *Pilot audit completed 2026-04-28 — value_stream_maps:*
    Pattern correctly applied at all 2 user-tracking-
    relevant write paths; no code change needed for
    this table. Findings:
    - Daten-Inventar: 2 rows total, `created_by`
      populated on both. Small sample like
      master_data_suggestions; weaker statistical
      signal but consistent with the assessments
      finding (10 rows, 100% populated).
    - Schema-Check: `created_by` is uuid NULL with no
      DB default. `id` has `gen_random_uuid()` default,
      `created_at` and `updated_at` have `now()`
      defaults. `created_by` is the single tracking
      column.
    - Code-Audit: 10 grep hits in 7 files. ~6 read
      paths. 2 user-tracking-relevant writes:
      `app/api/wertstrom/route.ts:45` (POST handler
      INSERT, user-facing) and
      `lib/demo/seeder.ts:482` (UPSERT, demo seeder).
      3 DELETEs where pattern is N/A:
      `app/api/wertstrom/[id]/route.ts:63`,
      `lib/demo/seeder.ts:504`,
      `app/api/demo/reset/route.ts:91`.
    - Raw-SQL-Check: 0 active raw-SQL writes.
    - Code-Inhalt: api/wertstrom/route.ts:45 uses
      `supabase.auth.getClaims()` + 401 guard,
      extracts `userId = claimsData.claims.sub` from
      the JWT, sets `created_by: userId` at insert.
      seeder.ts:482 uses `created_by: userId` where
      userId is caller-passed. Both correct.
    Side-observation recorded for context, not
    requiring action:
    - Third pattern variant for user-source surfaced
      here: `getClaims().claims.sub` in API route
      handlers, alongside `getUser().id` in client
      components and `user.id` (with prior guard) in
      components like new-project-form.tsx (per
      assessments audit). All three forms valid;
      choice depends on the caller's auth context.
    Conclusion: the PR-#26-style management-UI gap
    does not manifest on this table. **Status for
    value_stream_maps: resolved-no-action-needed.** 3
    of 13 remaining #29 tables audited; 10 to go.

    *Pilot audit completed 2026-04-28 — assignments:*
    First non-trivial finding since PR #26. Pattern
    inconsistently applied across manual-INSERT
    paths. Two distinct issues identified.
    - Daten-Inventar: 182 rows total. 87 NULL in
      `created_by` (47.8%) and exactly 87 NULL in
      `updated_by` (47.8%) — same row-set across
      both columns.
    - Diagnostic split by `source`:
        manual: 117 rows, 87 NULL (82 of those
        is_demo=true);
        auto_erstbesuch: 30 rows, 0 NULL;
        auto_folgebesuch: 24 rows, 0 NULL;
        duplicated: 6 rows, 0 NULL;
        copied: 5 rows, 0 NULL.
      The four programmatic sources populate
      consistently. Only `source=manual` exhibits
      NULL.
    - Diagnostic split of source=manual by is_demo:
        is_demo=false: 35 rows, 5 NULL;
        is_demo=true: 82 rows, 82 NULL (100% of
        demo).
      Two distinct buggy producers identified:
        (A) Demo-seeder (lib/demo/seeder.ts:298,
            UPSERT with source='manual') produces
            82 demo rows, all with NULL
            created_by/updated_by. Demo-seed bug,
            affects demo data only.
        (B) Real (non-demo) manual assignments: 5 of
            35 rows (14%) have NULL — inconsistent
            across the manual-INSERT code paths.
            Some populate, some don't. This is a
            real production bug.
    - Schema-Check: `created_by` uuid NULL,
      `updated_by` uuid NULL, both no DB default.
      Substantial domain table with status,
      work_mode, source, is_demo flags.
    - Code-Audit: 33 grep hits in 14+ files —
      substantial surface compared to prior audited
      tables. INSERT paths (6):
      `components/planning/create-assignment-modal.tsx:145`,
      `components/planning/copy-week-modal.tsx:106`,
      `components/planning/planning-client.tsx:362`,
      `:569`, `:622`,
      `lib/planning/create-visit-assignments.ts:132`.
      UPDATE paths (≥5): planning-client.tsx:421,
      :586, edit-assignment-modal.tsx:111, :117,
      bulk-edit-modal.tsx:44. UPSERT (1):
      seeder.ts:298 (demo). DELETEs (7) — pattern
      N/A.
    - Raw-SQL-Check: 0 active (1 commented-out hint
      at supabase/migrations/supabase-migration-cleanup.sql:22).
    - Code-Inhalt: NOT yet read in detail. Phase 0
      stopped at Step 5 because the Daten-Inventar
      already surfaced the inconsistency clearly
      enough to defer to a dedicated code-change
      session with fresh focus.
    Conclusion: PR-#26-style management-UI gap does
    manifest on this table, but selectively rather
    than uniformly. The 82 demo NULL rows fall under
    follow-up #28 (final pre-handover cleanup) as
    dummy-data residue; the 5 real NULL rows plus
    the underlying inconsistent-INSERT-paths bug
    warrant a dedicated code-change PR, tracked as
    new follow-up #31. **Status for assignments:
    fix-needed-deferred (see follow-up #31).** 4 of
    13 remaining #29 tables audited; 9 to go.

    *Pilot audit completed 2026-04-28 — app_settings:*
    Pattern correctly applied at the single user-
    tracking-relevant write path; no code change
    needed for this table. Findings:
    - Daten-Inventar: 2 rows total, `updated_by`
      populated on both. Note specific to updated_by-
      tracked tables: the Daten-Inventar shows only
      the snapshot of the last writer per row, since
      every UPDATE/UPSERT overwrites updated_by. So
      100% population means the most recent write on
      each row applied the pattern; earlier writes
      (if any) cannot be evaluated from data alone.
      For app_settings this caveat is acceptable
      because there is exactly one write path
      covering the whole table.
    - Schema-Check: only 4 columns on this table —
      `key` (text, identifier), `value` (jsonb), and
      the two tracking columns `updated_by` (uuid
      NULL, no DB default) and `updated_at`
      (timestamp NULL, default `now()`). No
      `created_by`, no `created_at`, no UUID `id`
      column. First audited table with this minimal
      key-value-plus-tracking shape. `updated_by` is
      the single tracking column.
    - Code-Audit: 4 grep hits in 3 files. 3 read
      paths (`app/kalender/page.tsx:35`,
      `app/kalender/settings/page.tsx:30`,
      `app/api/planning/settings/route.ts:21`). 1
      user-tracking-relevant write path:
      `app/api/planning/settings/route.ts:52`
      (UPSERT, server-side API route, single point-
      of-write for all settings). Smallest write
      surface among audited tables so far.
    - Raw-SQL-Check: 0 active raw-SQL writes.
    - Code-Inhalt: the PATCH handler in
      route.ts uses `getUserSession()` followed by
      `isAtLeastRole(session, 'admin')` as a role
      guard, then sets `updated_by:
      session.authUserId` and `updated_at: new
      Date().toISOString()` in the UPSERT payload
      with `onConflict: 'key'`. Pattern correct: the
      UPSERT body covers all four schema columns,
      and for both INSERT (new key) and UPDATE
      (existing key) branches, updated_by is
      populated with the admin's authUserId.
    Side-observations recorded for context, not
    requiring action:
    - Fourth user-source pattern variant surfaced:
      `getUserSession()` returning a session object
      with `authUserId` attribute, used after a role
      guard via `isAtLeastRole(session, 'admin')`.
      Adds to the existing three documented variants:
      `getUser().id` (client components),
      `user.id` (with prior guard, e.g.
      new-project-form.tsx), and
      `getClaims().claims.sub` (API route handlers
      per the value_stream_maps audit). Four valid
      auth-resolution patterns now documented;
      choice depends on the call site's auth context
      and any role-gating requirements.
    - Audit-method limitation specific to
      updated_by-tracked tables: the Daten-Inventar
      can only show the last writer per row, since
      updated_by is overwritten on every write.
      Useful for assessing "is the pattern currently
      applied?" but not "has the pattern always been
      applied?" — for app_settings this is
      acceptable because there is exactly one write
      path. For multi-write-path updated_by tables,
      this limitation should be considered when
      interpreting future audit results.
    Conclusion: the PR-#26-style management-UI gap
    does not manifest on this table. **Status for
    app_settings: resolved-no-action-needed.** 5 of
    13 remaining #29 tables audited; 8 to go.

    *Pilot audit completed 2026-04-28 — assessment_responses:*
    Second non-trivial finding under #29 after
    assignments. Pattern inconsistently applied;
    finding is in pure production data with no
    demo contamination, unlike assignments where
    82 of 87 NULL rows were demo. Findings:
    - Daten-Inventar: 131 rows total. 48 NULL on
      `updated_by` (36.6%), 83 populated (63.4%).
    - Diagnostic split by `is_demo`: all 131 rows
      are `is_demo=false`. The full 48-NULL set is
      in real production data — no demo
      contamination at all. Purer production-bug
      signal than the assignments audit.
    - Schema-Check: 10 columns. `id` (uuid PK
      with `gen_random_uuid()` default),
      `assessment_id` (uuid FK), `question_id`
      (uuid FK), `is_relevant` (bool default
      true), `is_flagged` (bool default false),
      `selected_rating` (int NULL), `comment`
      (text NULL), `updated_by` (uuid NULL, no DB
      default), `updated_at` (timestamp NOT NULL,
      `now()` default), `is_demo` (bool default
      false). Important shape detail: no
      `created_by`, no `created_at` — UPDATE-
      zentric table like app_settings. `updated_by`
      is the single tracking column; `updated_at`
      is NOT NULL with a default, so rows always
      carry a write timestamp even when
      `updated_by` is NULL.
    - Code-Audit: not performed. Phase 0 stopped
      at the Daten-Inventar + Schema-Check +
      diagnostic-split stage because the data
      signal was already clear enough to defer.
      Code-audit (5-step Schritte 3-5) belongs in
      the future fix-PR's Phase 0.
    - Raw-SQL-Check: not performed (deferred with
      the rest of the code audit).
    - Possible mechanisms (not yet verified;
      listed for the future fix-PR's audit):
      (a) Multi-path inconsistency: multiple
      UPDATE/UPSERT paths in the assessment-
      execution UI, some populating `updated_by`,
      some not. Analogous to the
      assignments multi-INSERT-path inconsistency
      but on UPDATE paths. (b) Pattern introduced
      later: early responses written before the
      user-tracking pattern, never updated since,
      retain NULL `updated_by`. Plausible because
      `updated_by` overwrites on every write per
      the audit-method limitation note from the
      app_settings audit — an untouched-since-
      creation row would keep its original NULL.
      (c) Specific code path missing: a bulk-save
      or submit-assessment path writing many
      responses at once without setting
      `updated_by`.
    Conclusion: PR-#26-style gap does manifest on
    this table, and unlike assignments this
    finding is in pure production data with no
    demo confound — every NULL row is a real
    production write that omitted user-tracking.
    **Status for assessment_responses: fix-
    needed-deferred (see follow-up #32).** 6 of
    13 remaining #29 tables audited; 7 to go.

30. **Trash-button on supplier management UI lacks
    click-throttling; delete handler doesn't verify
    rows-affected before audit-insert** — discovered
    2026-04-28 during Test #27 cleanup verification
    after PR #26 merge. Operator multi-clicked the
    trash button on 2026-04-27 21:03:39 when the UI
    gave no immediate feedback; the result was 3
    master_data_audit_log rows for a single intended
    delete (record_id
    260f05f8-5602-4135-95c9-28d8238e0335; audit ids 5,
    6, 7, all with action='delete' and changed_by
    populated to the operator's masteradmin UUID, all
    within 0.4 milliseconds of each other). Mechanism:
    the confirmHardDelete handler in
    components/repository/supplier-master-data-client.tsx
    (around line 607-611, post-PR-#26) issues
    `supabase.from('supplier_master_data').delete().eq('id', old.id)`,
    checks only the returned `error` (not the affected-
    row count), and then unconditionally inserts an
    audit_log row. Subsequent clicks therefore issue
    no-op DELETEs (0 affected rows, no error) but
    still write phantom audit rows. The same risk
    pattern likely applies to confirmDeactivate (UPDATE
    path, line 596) and possibly to all management-UI
    write paths surfaced during PR #26's Phase-0 audit.
    Two-part fix when this comes into scope: (a) add
    button-disable / click-throttling on the trash and
    deactivate buttons during in-flight requests; (b)
    check the affected-row count from the supabase
    response (e.g. via .select() and inspecting
    data.length, or via the count returned by
    supabase) before inserting the audit_log entry —
    only insert when an actual change occurred.
    Possibly relates to follow-up #29 (schema-level
    user-tracking gap) — the same management-UI
    surface might exhibit this multi-click pattern
    broadly. Out of scope for today's session; tracked
    here. The 3 phantom audit rows themselves will be
    cleaned up during the final pre-handover phase
    (#28) as dummy-data residue.

31. **`assignments` user-tracking inconsistent across
    manual-INSERT paths** — discovered 2026-04-28
    during the #29 pilot audit on the assignments
    table. Two distinct bugs identified:
    (A) **Real-data bug:** 5 of 35 non-demo manual
    assignments have NULL `created_by` and NULL
    `updated_by` (14% of real manual rows). The other
    30 are populated correctly. This means at least
    one of the 6 INSERT paths in the planning
    subsystem omits user-tracking-column population
    while at least one other does include it —
    inconsistent pattern application similar to PR
    #26's supplier-master-data finding but selective
    rather than uniform. (B) **Demo-seed bug:**
    lib/demo/seeder.ts:298 produces 82 demo
    assignments via UPSERT with source='manual', all
    with NULL created_by/updated_by. The 4
    programmatic sources (auto_erstbesuch,
    auto_folgebesuch, duplicated, copied) populate
    correctly, so the demo-seed gap is specific to
    the manual-source UPSERT path. Code-change scope
    when this is picked up: (1) read each of the 6
    INSERT paths (create-assignment-modal.tsx:145,
    copy-week-modal.tsx:106, planning-client.tsx:362,
    :569, :622, create-visit-assignments.ts:132) and
    identify which omit user-tracking population;
    (2) read the ≥5 UPDATE paths (planning-client.tsx
    :421, :586, edit-assignment-modal.tsx:111, :117,
    bulk-edit-modal.tsx:44) and verify updated_by
    population; (3) apply the PR-#26 pattern
    (`getUser()` / `getClaims()` per context) to
    missing paths; (4) fix seeder.ts:298 to populate
    created_by; (5) smoke-test on a representative
    subset of write paths. The 82 existing demo
    NULL rows are residue, will be cleaned up via
    follow-up #28 (final pre-handover cleanup) — no
    backfill in scope. The 5 real NULL rows: backfill
    not in scope (no reliable historical user
    attribution); pre-fix data stays NULL, post-fix
    writes populate. Possibly also relates to
    follow-up #30 (multi-click trash-button on
    supplier UI) — same broader concern about UI-
    layer write-path consistency in the planning/
    master-data surfaces. Out of scope for the audit
    PR (#32); tracked here as a discoverable
    backlog anchor with concrete code paths.

32. **`assessment_responses` user-tracking
    inconsistent across UPDATE/UPSERT paths** —
    discovered 2026-04-28 during the #29 pilot
    audit on the assessment_responses table. 48
    of 131 rows (36.6%) have NULL `updated_by` in
    pure production data; no demo contamination
    (all 131 rows are `is_demo=false`). Distinct
    from follow-up #31 (assignments) in two ways:
    (a) different subsystem — assessments
    execution UI rather than planning; (b) pure
    production-bug signal, not Demo+Real mix.
    Possible mechanisms to investigate in the fix-
    PR's Phase 0: multi-path inconsistency across
    UPDATE/UPSERT paths in the assessments UI;
    earlier-written responses retaining NULL from
    a pre-pattern era; or a specific bulk-save /
    submit path missing the `updated_by` field
    in its payload. Code-change scope when this
    is picked up: (1) full Phase 0 5-step audit
    on the assessment_responses write surface
    (grep `from('assessment_responses')` paths,
    enumerate INSERT/UPSERT/UPDATE handlers,
    verify each sets `updated_by`); (2) apply
    the PR-#26 pattern (`getUser()` /
    `getClaims()` / `getUserSession()` per
    context) to missing paths; (3) optional
    backfill consideration for the 48 existing
    NULL rows (no reliable historical user
    attribution likely available; pre-fix data
    probably stays NULL, post-fix writes
    populate). Smoke-test scope: trigger an
    assessment-response UPDATE through each
    identified write path and verify
    `updated_by` is populated. Cross-reference:
    follow-up #31 is the parallel anchor for
    assignments in the planning subsystem; same
    broader pattern (inconsistent application
    of user-tracking across multiple write
    paths in a feature subsystem). Out of scope
    for this audit PR (#34); tracked here as a
    discoverable backlog anchor.
