# MO-23 · Welle 0 — Befundaufnahme (Arbeitsstand)

> Session 0380fb6a · begonnen 17.07.2026 ~08:45 CEST · KEIN Code-Fix in W0 (§6.1)
> Repo: /home/aria/work/kadi-v2 (= „~/KADi/backend-nextjs" im Auftrag — Kais' lokaler Pfadname; GitHub: KADiCon/Kadi-v2)
> Supabase: nghepoidagwamsesrcdl (kadi-v2, eu-west-1)

## 0 · Korrekturen an den Auftrags-Kontextangaben (§1 „verifizieren, nicht glauben" — getan)

| Auftrag behauptet | Ist-Befund | Beleg |
|---|---|---|
| „RLS-Rollout läuft, PRs #13–#22" | PRs #13–#22 sind vom 12.05., Intake/LSC-Features (nur #17 = OEE-RLS). Der echte RLS-Umbau: KAR-775…795-Serie, Commits/PRs #209–#223+ (21.–23.06.) + using-true-Tranchen a/b1/b2 | `gh pr list`, git log |
| „25 flache supabase-*.sql im Root, kein CLI-Verzeichnis" | Migrationen liegen heute in `supabase/migrations/` (Namensschema `supabase-migration-*.sql`), Bootstrap in `supabase/bootstrap/`; MIGRATIONS.md ist der Katalog | ls, MIGRATIONS.md |
| „Nur 13 Vitest-Dateien" | 276 Test-Dateien (Stand heute) | find |
| „~75k LOC, 479 Dateien" | 1049 TS/TSX-Dateien | find |
| „Multi-Tenant-Control-Plane" (Modul) | Multi-Tenant wurde 26.05. entfernt, App ist single-tenant (BMW-Pilot) | README, repo-CLAUDE.md |
| „Betrieb: Adesso auf BMW AWS Cloud Room" | Heute: Vercel (app.kaiis.de); Azure/Adesso ist Handover-Ziel (Dockerfile vorhanden) | repo-CLAUDE.md „Deployment targets: today Vercel" |

**Konsequenz:** Der Auftragstext wurde auf einem ~3 Wochen alten Repo-Stand geschrieben. Die Hypothesen H1–H4 bleiben davon unberührt gültig — die Prüfschritte wurden am heutigen Stand ausgeführt.

## 1 · Zeitachse (§1) — BELEGT

- **Getesteter Stand am 24.06.:** Production-Deploy `70195fc` (deployt 23.06. 15:34 UTC; am 24.06. kein Deploy). Letzte Commits davor: KAR-787-Admin-Serie.
- **Das RLS-Hardening lief GENAU im Testfenster:** using-true-write-Tranchen a/b1/b2 (KAR-777, „verified against prod 22.06.2026") verschärften Write-Policies u.a. auf `assignments` (= Kalender-Einsätze, b1), `documents` (= Datenablage, b2, vorher „NO owner write policy, only permissive"), `appointment_types`, `planning_projects`, `supplier_master_data` (a). Commits 22.06.
- **Reaktion nach dem Feedback:** 25.06. 09:11 UTC ging `07e2e5e` (KAR-795, `demo-shared-read-rls`) in Production: additive SELECT-Policies `USING (is_demo = true)` über alle Module — vorher sahen authenticated User ohne Owner-Beziehung die Demo-Seeds NICHT.
- 145 Commits seit 20.06. · Migrations-Apply ist operator-managed (Live-DB `list_migrations` leer) → Apply-Zeitpunkte nur über Live-Zustand belegbar, nicht über Commit-Daten.

## 2 · Hypothesen-Stand

### H1 — Leere Auswahllisten: **DIFFERENZIERT — RLS-Mechanismus real, aber NICHT die Ursache der gemeldeten Dropdowns**
- **Alle 3 DB-gespeisten Dropdown-Punkte (FB-02 Verrechnungsgrund, FB-23 „Sprache", FB-19 KPI-Boards) laufen über EINE Tabelle `master_data_values`** via Hook `lib/master-data/use-master-data.ts:15-46` (Type-Codes BILLING_DENIAL_REASON, CUSTOMER_SAT_LANGUAGE, KPI_BOARD_DE/INTERNATIONAL). Formulare: new-project-form.tsx:889-1035, project-edit-form.tsx analog.
- **RLS als Ursache so gut wie ausgeschlossen:** `mdv_select FOR SELECT TO authenticated USING (true)` existiert seit spätestens 09.05. (bootstrap-from-prod.sql:5115; RLS-Aktivierung schon 23.04. in der April-Serie). Tabelle war am 24.06. lesbar.
- **Wahrscheinlichste Ursache (§5.2-Alternative 1): fehlende/inaktive Stammdaten-Werte.** master_data_types/values haben kein CREATE TABLE im Repo (MIGRATIONS.md: manuell angelegt), Seeds laufen über Admin-UI „Stammdaten". Query filtert is_active=true, is_hidden=false. → Prüffrage: Waren am 24.06. Werte für die 4 Type-Codes gepflegt? (Kais kann das im Admin-UI beantworten; N1-Verify in W1 mit Test-User.)
- **NEU-02 BESTÄTIGT und verschärft:** Der Hook verwirft `error` komplett (kein .catch, nur data destrukturiert), `loading` wird von den Formularen nie gelesen, und ein leeres Ergebnis wird **pro Browser-Session gecacht ohne Retry** (cache.set(typeCode, [])). Lädt/leer/Fehler/RLS sehen identisch aus. Adresse für den Fix liegt vor.
- **Der RLS-Sichtbarkeits-Mechanismus (owner-scoped SELECT + Demo-Daten) ist trotzdem real** — er betraf Projekt-/Entitäts-LISTEN (nicht die Stammdaten-Dropdowns) und wurde für Demo-Zeilen am 25.06. mit `07e2e5e` (KAR-795) gefixt. Wirkt in H3 hinein.
- **FB-54 (ZDSC Campus): kein H6-Fall.** „ZDSC Campus" ist ein Wert im Mehrfachfeld „Auftragstyp(en)" aus Tabelle `project_types` (Admin-pflegbar, kein Hardcode). `zdsc_campus` ist seit 05.06. (f3ab5fd) im Code-Stand angelegt, aber `is_implemented=false` → UI zeigt „Coming Soon". Punkt ist kleiner als gemeldet ODER Apply-Lücke am 24.06.; eigentliche Frage ist Scope („Workflow implementieren?") = Klasse B.
- **Nebenfund FB-23:** Es gibt keine „Kommunikationssprache" — das Feld ist `customer_satisfaction_language` (Registry: „Sprache für den Kundenzufriedenheitsfragebogen"). Zusätzlich: legacy CHECK-Constraint `IN ('Deutsch','English')` (bootstrap:1437) kollidiert mit dynamischer Stammdaten-Pflege — ein dritter Admin-Wert würde beim Speichern scheitern → NEU-Kandidat.
- **Auftrags-PR-Rätsel gelöst:** „RLS-Rollout PRs #13–#22" = die April-RLS-Aktivierungs-Serie aus dem VORGÄNGER-Repo (KADi-backend), dokumentiert in docs/foundation/rls-audit-2026-04-21.md und den supabase/rls/*-Dateien (Header nennen „PR #9…#20"). Nicht die Kadi-v2-GitHub-PRs.

### H2 — Schreiben schlägt fehl: **H2-A für FB-48 BESTÄTIGT (N2) — NICHT behoben**
- `project_notes_own_project`: `FOR ALL USING (project_id IN (SELECT id FROM projects WHERE user_id = auth.uid()))` OHNE separates WITH CHECK (pmo-phase-1.sql:376). FOR-ALL+nur-USING ⇒ USING wirkt als WITH CHECK ⇒ nur der Projekt-OWNER darf Notizen anlegen. MO-23-User auf Demo-/fremden Projekten → exakt „new row violates row-level security policy for table project_notes".
- Der 25.06.-Fix betrifft NUR SELECT auf is_demo-Zeilen — Write-Pfad unverändert. **FB-48 heute mutmaßlich weiter kaputt** (Live-Verify mit User-JWT = W1-Schritt, dann N1).
- Gleiche Policy-Familie „*_own_project" FOR ALL in pmo-phase-1 (pmo_handovers, project_note_attachments, …) → FB-49 (PMO) sehr wahrscheinlich gleiche Ursache. FB-51 (Datenablage): Kandidat tranche b2 (`documents` owner-scoped seit 22.06.) + storage.objects-Policies (notes-storage.sql) — Einzelprüfung offen.
- **H2-B BESTÄTIGT als app-weites Muster (Kern-Fund von W0 = NEU-01):**
  - Kein zentraler Daten-Layer (API_SPEC.md:3-5 dokumentiert das explizit): 66 Client-Komponenten + 15 Server Actions schreiben direkt via supabase.from().
  - ~234 echte update/delete-Aufrufe klassifiziert: ~12% prüfen Trefferzahl (.select()+!data bzw. count:'exact'), ~53% nur `error`, ~34% gar nichts (fire-and-forget). **~88% anfällig für lautlose 0-Zeilen-Fehlschläge. 0 von 100 .delete() prüfen die Trefferzahl.**
  - **Kalender (FB-06/39) exakt erklärt:** alle Schreibpfade (edit-assignment-modal.tsx:204-253, planning-client.tsx:413,464-468,487,598,655) nur-error; „Erfolg" = Dialog schließt + Refetch zeigt den alten Stand kommentarlos. Passt 1:1 zur Meldung „weder Überschreiben noch Löschen, keine Fehlermeldung". Ursachen-Kette: Demo-/Fremd-Einsätze (created_by ≠ auth.uid()) + b1-verschärfte Write-Policies → 0 Zeilen → UI schweigt. Sogar der Offline-Sync (sync-engine.ts:118-124) markiert 0-Zeilen-Updates als 'synced'.
  - **Notes (FB-48):** updateNote nur-error (actions.ts:63-89) + note-editor zeigt „Gespeichert." falsch-positiv; createNote hat dagegen .select().single() — konsistent damit, dass der Melder beim ANLEGEN den sichtbaren RLS-Fehler bekam (H2-A).
  - **PMO (FB-49):** Tabelle ist `project_members` (nicht team_members). update/deleteMember nur-error + UI volloptimistisch (pmo-team-client.tsx:70-97): Mitglied verschwindet lokal, DB unverändert, kein Toast.
  - **Dokumente (FB-50/51):** Upload+Edit sauber abgesichert (.select().single()); Delete/Move komplett ungeprüft + optimistisch (documents-tree.tsx:76-124, project-documents-client.tsx:335-351).
  - **Vorbild-Muster existiert im Repo** (qaf-differences/actions.ts:4416ff KAR-846+, lib/gdpr/deletion.ts:46-61, seeder.ts:662 count:'exact') — bekannt und geübt, nie rückwirkend angewendet, nie auf .delete(). → §8.2-Prognose trifft zu: Der eigentliche Fund ist „Schreibpfade vereinheitlichen" = Architekturfrage = Klasse B (Kais).

### H3 — „Angelegt, aber nicht sichtbar": **DIFFERENZIERT — Ursache ist Cache/Revalidation (Flow Projekt), NICHT RLS-Waisen**
- **Flow A Lieferant (supplier_master_data):** SELECT `USING(true)` (bootstrap:5507/5547) → Policy-Unsichtbarkeit ausgeschlossen; beide In-App-Insert-Pfade setzen created_by UND updaten die UI direkt; kein Dexie. Die §5.4-Waisen-These (Zeile ohne Owner-Spalte → weggefiltert) trifft hier NICHT (SELECT filtert gar nicht).
  - **ABER Zeitachsen-Twist:** Tranche a (22.06., 58537df) droppte `supplier_insert WITH CHECK(true)` → seither dürfen NUR admin/masteradmin Lieferanten anlegen (`smd_write`). UI meldet 42501 sauber als Toast („keine Berechtigung", Handling seit 08.05.). Für FB-15/18 heißt das: Je nach Apply-Datum von Tranche a sahen die Melder am 24.06. entweder (a) Insert ok + Sichtbarkeits-Problem im damaligen Stand oder (b) heute stattdessen einen Berechtigungs-Fehler. **W2-Prüffrage: Sollen Nicht-Admins Lieferanten anlegen dürfen? (Produktfrage, Klasse B-Kandidat)**
  - Nebenfund: API-Route `app/api/v1/suppliers/route.ts:42-46` insertet ohne created_by (Zod-Schema kennt das Feld nicht, kein Default/Trigger) → NULL-created_by-Zeilen möglich (heute ohne Sichtbarkeits-Folge, aber Datenqualität).
- **Flow B Projekt (projects): WAHRSCHEINLICHSTE H3-URSACHE, am Code belegt.** Insert setzt user_id=auth.uid() korrekt (projects_own greift immer für den Ersteller). Aber: client-seitiger Insert OHNE Server Action/revalidatePath, danach `router.push` auf die Edit-Route — Rücksprung zur Liste innerhalb des Router-Cache-Fensters (30s für dynamic segments, dokumentiert in docs/adr/022-caching-strategy.md:81-83) zeigt eine veraltete Liste ohne das neue Projekt. Die ADR beschreibt das kanonische Muster (Server Action + revalidatePath), dieser Flow folgt ihm nicht. → erklärt FB-58-artige Beobachtungen („kurz danach nicht da, später doch") ohne jeden RLS-Bezug.
  - **Starker Nebenfund (NEU-Kandidat, erklärt „Kollege sieht Projekt nicht"):** `projects_dept_visible` verlangt `department_id IS NOT NULL`; das Formularfeld „Abteilung" ist optional und unvalidiert (new-project-form.tsx:858-868, nicht in validate()). Projekt ohne Abteilung = für Abteilungs-Kollegen unsichtbar (Ersteller sieht es via projects_own, Admins via projects_admin). Lautlos, kein Fehler.
- **Flow C Einsatz (assignments):** created_by + is_demo explizit gesetzt; `assignments_read USING(true)`; nach Anlage expliziter Refetch + Dexie-`seedFromServer`; Kalender-Grid rendert reaktiv aus Dexie (useLiveQuery). Für den ERSTELLER kein Loch. **Stale-Fenster ≤5 Min existiert nur für ANDERE offene Tabs/Sessions** (Hintergrund-fullSync-Kadenz, sync-hooks.ts:92-131) → Kandidat-Erklärung für FB-59 („Lieferant nicht synchron", Hagen) und Mehrbenutzer-Beobachtungen.
  - Nebenfund: Der Einsatz-Insert umgeht den dokumentierten Offline-Schreibweg (OFFLINE_ARCHITECTURE.md:100-114, Queue) und schreibt direkt online → offline schlägt Anlage schlicht fehl, kein Queueing.

### H4 — gen_random_bytes: **BESTÄTIGT (N2), Ursache exakt lokalisiert**
- `supabase-migration-intake-v1-public-submission-tokens.sql`: Funktion (SECURITY DEFINER) `SET search_path = public, pg_temp` (Z.76) ruft Z.116 `gen_random_bytes(32)` unqualifiziert.
- Live-DB: pgcrypto installiert in Schema **`extensions`** (nicht public) → Funktion nicht auffindbar → exakt der gemeldete Fehler (Nr. 13/FB-13).
- Bootstrap-Script macht `CREATE EXTENSION IF NOT EXISTS pgcrypto` OHNE `WITH SCHEMA` (prerequisites.sql:30) — auf vanilla PG landet es in public (dort funktionierts), auf Supabase-hosted lag es schon in extensions → Umgebungs-abhängiger Bug. Fix-Optionen §5.5 des Auftrags; Empfehlung folgt im W0-Bericht (Option 1: `extensions.gen_random_bytes(...)` qualifizieren + search_path ergänzen — kleinster Eingriff; Option 2 Node-crypto entkoppelt ganz).

## 3 · Nebenbefunde (Kandidaten NEU-xx / G12)

- **`oee_orphan_reminders_own`**: letzte verbliebene `WITH CHECK (true)`-ALL-Policy (Advisor rls_policy_always_true) — Rest des Tightenings. → NEU-Kandidat (P2), passt zur Tranchen-Serie.
- **22 SECURITY-DEFINER-Funktionen für `anon` EXECUTABLE** (u.a. promote_intake_to_project, reject_intake, move_intake_status). Stichprobe create_intake_submission_token prüft intern Consultant-Status (fail-closed) — Muster „EXECUTE offen, Auth-Check innen" plausibel, aber pro Funktion unverifiziert. → G12-Thema: nur an Kais, NICHT in PR-Texte/Linear. In W1 pro Funktion verifizieren.
- 15 Funktionen mit mutablem search_path (Advisor WARN) — Housekeeping, Liste liegt vor.
- `pg_trgm` in public (Advisor WARN) — Housekeeping.
- Advisor: auth_leaked_password_protection + auth_otp_long_expiry deaktiviert/lang — Auth-Settings-Housekeeping.

## 4 · W0-Bericht nach §9.2 — „Was ist überhaupt wahr?" (Stand 17.07. ~09:45)

**1. Hypothesen bestätigt/widerlegt:**
- **H1 (leere Listen): Mechanismus real, aber für die gemeldeten Dropdowns WIDERLEGT als RLS-Problem.** FB-02/23/19 hängen an `master_data_values` (SELECT USING(true) seit Mai) — Verdacht: Werte fehlten/inaktiv am 24.06. (Admin-UI-Pflege). Der RLS-Sichtbarkeits-Mechanismus traf stattdessen Entitäts-LISTEN und wurde für Demo-Daten am 25.06. gefixt (07e2e5e, KAR-795).
- **H2-A BESTÄTIGT (N2), unbehoben:** project_notes (+ Familie) FOR-ALL-own_project ⇒ nur Projekt-Owner darf schreiben ⇒ gemeldete Fehlermeldung exakt. H2-B BESTÄTIGT app-weit: ~88% der Schreibpfade blind für 0-Zeilen-Fälle (= NEU-01, Architekturfrage).
- **H3: für Lieferant/Einsatz (Ersteller-Sicht) WIDERLEGT, für Projekt-Flow BESTÄTIGT** (kein revalidate + 30s-Router-Cache, ADR-022). Plus dept_visible-Falle (NEU-04) und 5-Min-Sync-Fenster für andere Sessions (Kandidat FB-59).
- **H4 BESTÄTIGT (N2):** pgcrypto in `extensions`, Funktion sucht in `public` — FB-13-Fehler vollständig erklärt.
- H5-H7: nicht W0-Scope (H5 = W1; H6 für FB-54 widerlegt — project_types ist DB-pflegbar; H7 ungeprüft).

**2. Bereits behoben (mit Beleg):** Demo-Daten-Sichtbarkeit (Teil der H1-Fälle auf Listen-Ebene) via 07e2e5e am 25.06. deployt. Einzelpunkt-Bestätigung steht aus (welcher Melder-Fall genau Demo-Listen betraf) → W2-Einzelprüfung. zdsc_campus (FB-54) seit 05.06. im Code-Stand (f3ab5fd) als „Coming Soon".

**3. Nicht reproduzierbar:** Noch keiner final — W0 war Code-/DB-Analyse, N1-Repros folgen in den Wellen. Kandidat: FB-15/18 (Lieferant) — heutiger Stand blockt Nicht-Admin-INSERT sichtbar (Toast), das gemeldete Bild kann so nicht mehr auftreten.

**4. Größer als gemeldet:** FB-06 → NEU-01 (app-weit ~206 blinde Schreibpfade, 0/100 deletes geprüft). FB-02/19/23 → NEU-02 (alle Auswahllisten: laden/leer/Fehler ununterscheidbar + session-langer Leer-Cache). FB-23 zusätzlich: Es gibt gar keine „Kommunikationssprache" — das Feld ist die Fragebogen-Sprache mit CHECK-Constraint (Deutsch/English) der die Admin-Stammdatenpflege konterkariert (NEU-05).

**5. Kleiner als gemeldet:** FB-02: „Grund" ist KEIN Pflichtfeld (Submit sendet ||null) — Projektanlage nicht blockiert, P1→P2-Kandidat. FB-54: Wert existiert, Frage ist nur Workflow-Scope.

**6. Klasse-B-Zählung:** per Aktenlage 24 der 60 Nummern (§6.2) + aus W0 neu: NEU-01-Architekturentscheidung. P0-Paket (FB-36/41/45 + Konflikt Nr.14) unverändert W1.

**Fragen an Kais — BEANTWORTET (TG 8807, 17.07. 09:21):**
(a) **„Waren nicht alle Daten gepflegt"** → H1-Ursache bestätigt: Stammdaten-Lücke + NEU-02. FB-02/19/23-Fix = Pflege (Kais/Admin) + UI-Zustände (Klasse A); W2 prüft heutigen Pflegestand je Type-Code.
(b) **app.kaiis.de, als Berater UND Admin, meist Berater** → passt exakt zu den RLS-Befunden (Berater ohne Owner-Beziehung: project_notes-Block, fremde assignments nicht änderbar).
(c) **Apply-Daten unbekannt** → BEREITS-BEHOBEN-Belege konservativ formulieren („spätestens seit Deploy X ist Zustand Y"), Live-Zustand zählt.
(d) **Nicht-Admin-Lieferanten-Anlage ist GEWOLLT, mit Prüf-Workflow:** Berater reicht ein, „Admin kann es bestätigen unter Stammdaten mit einem Klick." → Produktentscheidung dokumentiert. Konsequenz W2 (FB-15/18): Prüfen ob ein pending/approved-Status existiert; tranche-a-Policy (nur admin schreibt) bildet den gewollten Workflow NICHT ab → Policy präzisieren auf Workflow (Berater-INSERT mit Prüf-Status, Bestätigung admin-only), /cso Pflicht, positiv+negativ-Tests.

**G12-Funde: 2 Stück, separat via TG an Kais gemeldet (nicht in diesem Dokument, §8.1.5).**
