docs(quick-260909-ab3): Matrix-Suche und Sync-Meldungen — Plan, Bericht, Verifikation
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 56s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m45s

Verifikation unabhaengig nachgemessen: 651/651 API-Tests, 233/233 Web-Tests,
beide Typpruefungen sauber.

Der Sicherheitsfund T-Q3-01 wurde nicht nur gruen getestet, sondern
falsifiziert: der Verifizierer hat die neue Besitzpruefung testweise
zurueckgebaut, woraufhin der Test fehlschlug und die fremde Adresse
tatsaechlich in prisma.user.update() landete. Danach sauber zurueckgesetzt.

Ebenso gegengeprueft: beide Anlege-Wege (Sync und Einzelimport) nutzen
denselben Kollisionsentscheider, im ausgelieferten Code stehen keine
kundenspezifischen Namen oder Adressen, rohe ORM-Texte erreichen die
Oberflaeche nicht mehr, und die Gruppensuche unter internem wie AD-Namen
(#6c) ist per Regressionstest gesichert.

Eine Abweichung des Ausfuehrenden ist dokumentiert und bestaetigt: die im
Plan vorgesehene Testvorlage war wegen Kurzschlussauswertung schon gegen den
unveraenderten Code gruen; mit einem zweiten, nicht passenden Konto
reproduziert sie den Absturz nun wirklich.

Status human_needed: die fuenf Browser-Pruefungen brauchen einen Neubau und
sind Sache des Users. WINDOWS #14 und #15 bleiben bis dahin offen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
2026-09-09 08:02:55 +02:00
parent 21670462dd
commit efcf11c988
3 changed files with 267 additions and 6 deletions
+7 -6
View File
@@ -5,10 +5,10 @@ milestone_name: Plattform-Berechtigungen
current_phase: 17 current_phase: 17
current_phase_name: eigene-ausschreibungs-quellen-je-nutzer current_phase_name: eigene-ausschreibungs-quellen-je-nutzer
status: verified status: verified
stopped_at: "WINDOWS #16 am 2026-09-09 im Browser auf alpha abgenommen und geschlossen: Verbindungstest meldet mit leerem Passwortfeld Erfolg (belegt den Rueckgriff auf gespeicherte Zugangsdaten), falscher Server meldet die echte Fehlermeldung, keine Zugangsdaten im Protokoll, gespeicherte Konfiguration unveraendert. Nebenbei erstmals belegt, dass der handgeschriebene EWS-Weg gegen den echten Exchange laeuft. WINDOWS #12 zurueckgestellt — es gibt intern kein Postfach fuer Ausschreibungs-Alarme. Offen sind nur noch die zwei am 2026-09-07 gefundenen Defekte: #14 (Matrix-Suche leert die jeweils andere Achse) und #15 (rohe Techniktexte im Sync, vier AD-Konten wegen E-Mail-Kollision nie importiert)." stopped_at: "Quick-Task 260909-ab3 abgeschlossen und verifiziert (8/8): Matrix-Suche leert die jeweils andere Achse nicht mehr (#14), AD-Konten mit doppelter Mailadresse werden ohne Adresse angelegt statt still zu scheitern, Sync-Bericht spricht Deutsch statt roher Prisma-Texte (#15). Dabei einen Sicherheitsfund geschlossen (T-Q3-01: bedingungsloses Ueberschreiben der Mailadresse haette Uebernahme einer fremden Adresse samt Passwort-Reset erlaubt) — per Rueckbau falsifiziert, nicht nur gruen getestet. User.email ist jetzt optional, die Migration laeuft beim naechsten API-Start automatisch mit (Dockerfile fuehrt migrate deploy aus). OFFEN: Browser-Abnahme der fuenf Punkte nach dem Neubau durch den User; #14 und #15 bleiben bis dahin im Ledger offen."
last_updated: "2026-09-09T07:20:00.000Z" last_updated: "2026-09-09T09:30:00.000Z"
last_activity: 2026-09-09 last_activity: 2026-09-09
last_activity_desc: Verbindungstest-Knopf live abgenommen (WINDOWS #16 zu), Postfach-Test #12 mangels Postfach zurueckgestellt last_activity_desc: WINDOWS #14 und #15 repariert und verifiziert, Sicherheitsfund T-Q3-01 mitgeschlossen — Browser-Abnahme offen
progress: progress:
total_phases: 17 total_phases: 17
completed_phases: 17 completed_phases: 17
@@ -362,6 +362,7 @@ None yet.
| 260805-fok | Standardgruppe bei Mandanten-Anlage + Startup-Reparatur — GroupsService.ensureDefaultGroup(tenantId) mit D-13-Waechter (null Gruppen, nicht fehlende Markierung), verdrahtet in TenantService.create und AdminSeedService.ensureDefaultGroupsForAllTenants; schliesst die Migrations-Backfill-Luecke auf frischen Installationen (Testserver: tenants=1 users=4 groups=0) | 2026-08-05 | 9d1254c,0d7d8a5 | [260805-fok-standardgruppe-bei-mandanten-anlage-und-](.planning/quick/260805-fok-standardgruppe-bei-mandanten-anlage-und-/) | | 260805-fok | Standardgruppe bei Mandanten-Anlage + Startup-Reparatur — GroupsService.ensureDefaultGroup(tenantId) mit D-13-Waechter (null Gruppen, nicht fehlende Markierung), verdrahtet in TenantService.create und AdminSeedService.ensureDefaultGroupsForAllTenants; schliesst die Migrations-Backfill-Luecke auf frischen Installationen (Testserver: tenants=1 users=4 groups=0) | 2026-08-05 | 9d1254c,0d7d8a5 | [260805-fok-standardgruppe-bei-mandanten-anlage-und-](.planning/quick/260805-fok-standardgruppe-bei-mandanten-anlage-und-/) |
| 21 | Verschluesselungsschluessel in den Beispiel-Umgebungsdateien dokumentiert: .env.example hatte gar keinen Eintrag, .env.prod.example nannte noch den alten Namen CALENDAR_ENCRYPTION_KEY. Compose-Teil des Backlog-Punkts war bereits mit 7bda56d erledigt (Vorgabewert raus, :?-Abbruch statt Ersatzwert) | 2026-08-11 | 379606e | — | | 21 | Verschluesselungsschluessel in den Beispiel-Umgebungsdateien dokumentiert: .env.example hatte gar keinen Eintrag, .env.prod.example nannte noch den alten Namen CALENDAR_ENCRYPTION_KEY. Compose-Teil des Backlog-Punkts war bereits mit 7bda56d erledigt (Vorgabewert raus, :?-Abbruch statt Ersatzwert) | 2026-08-11 | 379606e | — |
| 260907-let | Verbindungstest fuer das Postfach im Ausschreibungs-Radar nachgeruestet (WINDOWS #16): POST /modules/tender-radar/email-config/test plus Knopf "Verbindung testen" im Formular unter Meine Quellen. Nutzt die vorhandene testConnection() beider Inbox-Provider, Muster vom DKV-Modul. userId ausschliesslich aus dem Auth-Kontext (eigener IDOR-Test mit Koeder-userId), leerer Benutzername oder leeres Passwort faellt auf die gespeicherten verschluesselten Zugangsdaten desselben Nutzers zurueck, keine Zugangsdaten in Logs oder Antwort. Verifiziert: 646/646 API- und 228/228 Web-Tests, beide Typpruefungen sauber, Sprachschluessel-Gate von rot auf gruen. Offen: Browser-Abnahme gegen ein echtes Postfach (Ende-der-Phase, braucht Neubau durch den User) | 2026-09-07 | c4db3b2 | [260907-let-verbindungstest-fuer-das-postfach-im-aus](./quick/260907-let-verbindungstest-fuer-das-postfach-im-aus/) | | 260907-let | Verbindungstest fuer das Postfach im Ausschreibungs-Radar nachgeruestet (WINDOWS #16): POST /modules/tender-radar/email-config/test plus Knopf "Verbindung testen" im Formular unter Meine Quellen. Nutzt die vorhandene testConnection() beider Inbox-Provider, Muster vom DKV-Modul. userId ausschliesslich aus dem Auth-Kontext (eigener IDOR-Test mit Koeder-userId), leerer Benutzername oder leeres Passwort faellt auf die gespeicherten verschluesselten Zugangsdaten desselben Nutzers zurueck, keine Zugangsdaten in Logs oder Antwort. Verifiziert: 646/646 API- und 228/228 Web-Tests, beide Typpruefungen sauber, Sprachschluessel-Gate von rot auf gruen. Offen: Browser-Abnahme gegen ein echtes Postfach (Ende-der-Phase, braucht Neubau durch den User) | 2026-09-07 | c4db3b2 | [260907-let-verbindungstest-fuer-das-postfach-im-aus](./quick/260907-let-verbindungstest-fuer-das-postfach-im-aus/) |
| 260909-ab3 | Zwei Befunde aus der Live-Pruefung behoben. **#14:** Die Suche in der Freigaben-Matrix filterte beide Achsen mit demselben Begriff und leerte dadurch die jeweils andere — jetzt bleibt die nicht getroffene Achse vollstaendig stehen, die Gruppensuche unter internem UND AD-Namen (#6c) ist per Regressionstest gesichert. **#15:** AD-Konten mit bereits vergebener Mailadresse werden nun angelegt, nur ohne Adresse (Produktentscheidung des Users vom 2026-09-09; der erste Anspruch behaelt die Adresse), auf BEIDEN Wegen — Sync und Einzelimport. Rohe Prisma-Texte gehen nur noch ins Log, der Bericht zeigt drei verstaendliche deutsche Abschnitte. **Sicherheitsfund nebenbei geschlossen (T-Q3-01):** der Update-Zweig schrieb die Mailadresse bedingungslos um, ein Verzeichniseintrag haette so die Adresse einer echten Person uebernehmen und deren Passwort-Reset empfangen koennen. `User.email` ist jetzt optional (Migration geschrieben, laeuft beim naechsten API-Start automatisch mit). Verifiziert 8/8: 651/651 API- und 233/233 Web-Tests, beide Typpruefungen sauber; die Sicherheitspruefung wurde durch Rueckbau falsifiziert (ohne Besitzpruefung schlaegt der Test fehl). Offen: Browser-Abnahme nach dem naechsten Neubau | 2026-09-09 | 2167046 | [260909-ab3-matrix-suche-und-sync-meldungen-reparier](./quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/) |
## Deferred Items ## Deferred Items
@@ -401,7 +402,7 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
## Session Continuity ## Session Continuity
Last session: 2026-09-09T07:20:00.000Z Last session: 2026-09-09T09:30:00.000Z
Stopped at: Alle Pruefpunkte abgearbeitet. Offen sind nur noch zwei Reparaturen: WINDOWS #14 (Suche in der Freigaben-Matrix filtert beide Achsen und leert dadurch die jeweils andere) und #15 (Sync reicht rohe Prisma- und englische Techniktexte durch; vier AD-Konten aus OU=CTL_PWS_Gruppen werden wegen geteilter E-Mail-Adresse nie importiert). Zurueckgestellt: #12 (kein Alarm-Postfach vorhanden) und Abnahmeplan 02-05 (Mandantentrennung, intern zweitrangig). Stopped at: Beide gefundenen Defekte repariert und verifiziert, gepusht. Es wartet nur noch die Browser-Abnahme durch den User nach dem naechsten Neubau (API + Web). Danach koennen #14 und #15 geschlossen werden. Zurueckgestellt bleiben #12 (kein Alarm-Postfach vorhanden) und Abnahmeplan 02-05 (Mandantentrennung, intern zweitrangig).
Resume file: None Resume file: None
Last activity: 2026-09-09 - WINDOWS #16 abgenommen und geschlossen, #12 zurueckgestellt Last activity: 2026-09-09 - #14 und #15 repariert, T-Q3-01 geschlossen, verifiziert und gepusht
@@ -0,0 +1,129 @@
---
phase: quick-260909-ab3
plan: 01
subsystem: ldap
tags: [ldap, sync, i18n, ui, prisma, react]
status: complete
dependency-graph:
requires: []
provides:
- "Freigaben-Matrix: Suche filtert Modul- und Gruppen-Achse unabhaengig — ein Treffer auf nur einer Achse leert die andere nicht mehr"
- "resolveEmailForWrite(): geteilter Kollisionsentscheider fuer AD-Kontoanlage (LdapService.upsertMappedUser + importUsersByDn)"
- "LdapSyncResult.{emailConflicts,skippedNoLogin,entryFailures}: strukturierter Sync-Bericht ohne rohen ORM-/englischen Techniktext"
- "User.email optional (Migration geschrieben, nicht ausgefuehrt)"
affects:
- apps/web/src/app/(portal)/admin/modules/grants/page.tsx
- apps/api/src/ldap/ldap.service.ts
- apps/api/src/user/user.service.ts
- apps/web/src/app/(portal)/admin/ldap/page.tsx
- apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx
- apps/web/src/app/(portal)/admin/users/page.tsx
tech-stack:
added: []
patterns:
- "Kollisionsentscheider als private Methode, die vor jeder E-Mail-Schreibung prisma.user.findUnique befragt und nur zurueckgibt, wenn niemand die Adresse haelt oder der Halter der eigene Datensatz ist — nie ein Umhaengen zwischen Konten"
- "Rohe ORM-/Techniktexte gehen ausschliesslich ueber logger.error ins Serverprotokoll; der Administrator sieht nur die Kennung des betroffenen Eintrags"
actuals:
tokens: 10082
tasks: 3
commits: 3
decisions:
- "Task 3 Testfixture von der Plan-Vorgabe abgewichen: ein einzelnes Konto mit passendem Benutzernamen loest den Absturz wegen OR-Kurzschlussauswertung nie aus (bestaetigt: Test war mit der Plan-Fixture gruen gegen den unveraenderten Bestand). Ein zweites, nicht-treffendes Konto ohne Adresse in derselben Mock-Liste macht den Test echt rot — siehe Deviations."
metrics:
duration: 13 min
completed: 2026-09-09
---
# Quick Task 260909-ab3: Matrix-Suche und Sync-Meldungen reparieren Summary
Freigaben-Matrix-Suche filtert Modul- und Gruppenachse jetzt unabhaengig (WINDOWS #14 geschlossen); AD-Sync legt Konten mit kollidierender E-Mail-Adresse an — ohne Adresse — statt sie stillschweigend zu uebergehen, und der Bericht spricht durchgehend verstaendliches Deutsch (WINDOWS #15 geschlossen).
## What Was Built
Drei getrennte, jeweils fuer sich lauffaehige Commits — exakt wie im Plan vorgegeben.
**Task 1 — Matrix-Suche filtert nur noch die getroffene Achse (commit `e8c2411`)**
- `page.tsx`: `filteredModules`/`filteredGroups` durch einen gemeinsamen `useMemo` ersetzt, der `moduleHits`/`groupHits` getrennt ermittelt und nach der im Plan festgelegten Wahrheitstabelle kombiniert (leeres Feld -> alles; nur eine Achse trifft -> die andere Achse bleibt vollstaendig stehen; beide treffen -> beide gefiltert; nichts trifft -> `noMatch`).
- Regressionsschutz fuer WINDOWS #6c (Spaltensuche findet eine importierte Gruppe sowohl unter internem als auch AD-Namen) unveraendert erhalten und mit einem eigenen Testfall abgesichert.
- Neue Meldung `adminModules.grants.noSearchResults` in de.json/en.json — de.json folgt der im Bestand bereits etablierten typografischen Anfuehrungszeichen-Konvention (`„...\"`, siehe `nameCollisionError`).
- Vier neue Testfaelle in `grants-matrix.test.tsx` vorab gegen den unveraenderten Bestand rot gelaufen — alle vier aus den im Plan vorhergesagten Gruenden (fehlende Modul-/Gruppennamen bzw. fehlender Meldungstext).
**Task 2 — Kollidierende AD-Konten werden angelegt, ohne Adresse (commit `1222951`)**
- `schema.prisma`: `User.email` auf `String? @unique` gestellt; Migration `20260909120000_user_email_optional` geschrieben (nur `DROP NOT NULL`, Eindeutigkeitsindex unangetastet) — **nicht ausgefuehrt**, wie vom Plan verlangt.
- `ldap.service.ts`: neue private Methode `resolveEmailForWrite(desiredEmail, ownRecordId)` fragt `prisma.user.findUnique({where:{email}}))` und gibt die Adresse nur frei, wenn niemand sie haelt oder der Halter der eigene Datensatz ist (T-Q3-01) — sonst wird kein Konto umgehaengt, sondern ein Kollisionsvermerk erzeugt.
- In `upsertMappedUser` (Sync-Pfad) UND `importUsersByDn` (Handimport-Pfad) verdrahtet, damit der zweite Anlageweg nicht als Luecke mit rohem Datenbanktext bestehen bleibt.
- `LdapSyncResult` um `emailConflicts` ({account, email}[]), `skippedNoLogin` (string[]) und `entryFailures` (string[]) erweitert. Die englische "no username mapped"-Meldung im Sync-Pfad ist verschwunden (wandert jetzt in `skippedNoLogin`); der generische Catch-Block schreibt den technischen Wortlaut nur noch via `this.logger.error`, nie in den Bericht (T-Q3-02).
- `UserService.create` nimmt `email` jetzt optional entgegen. Type-Check deckte genau die im Plan vorhergesagten zwei Fundstellen auf (`tender-digest.scheduler.ts:140`, `tender-matching.service.ts:131`) — beide brechen jetzt mit `continue` ab, wenn der Empfaenger keine Adresse hat.
- Fuenf neue Testfaelle in `ldap.service.spec.ts` vorab gegen den unveraenderten Bestand rot gelaufen, jeweils aus dem im Plan vorhergesagten Grund. Eine bestehende `toEqual`-Assertion (`empty base DN no-op`-Test) musste um die drei neuen Ergebnisfelder ergaenzt werden (legitime Bestandspflege, keine Verhaltensaenderung).
**Task 3 — Verstaendlicher Sync-Bericht und Konten ohne Adresse in der UI (commit `2167046`)**
- `admin/ldap/page.tsx`: `SyncResult`-Interface um die drei neuen Felder erweitert; drei neue Berichtsabschnitte in der vorgegebenen Reihenfolge (Kollision: Bernstein wie `defaultMarkerMoved`; uebersprungen ohne Anmeldenamen: `text-muted-foreground`, ausdruecklich kein Rot; unerwartete Fehler: `text-destructive`).
- Vier neue Sprachschluessel unter `admin.ldap.sync` in de.json/en.json; Umlaut-Gate lief gruen ohne neue Allowlist-Eintraege (nur "Adresse" traf das `ss`-Muster, war bereits erlaubt).
- `GroupMembersModal.tsx`: `TenantUser.email` auf `string | null`, Suchvergleich (Zeile 104) analog zum bestehenden `displayName`-Muster mit `(u.email ?? '')` abgesichert.
- `users/page.tsx`: `User.email` auf `string | null`; Formularuebernahme faellt auf `''` zurueck, Tabellenzelle zeigt einen Gedankenstrich (`–`) statt einer leeren Zelle. `UserFormData.email` bleibt Pflichtfeld fuer die Handanlage.
## Verification Results
- **API-Testsuite:** `pnpm --filter @tessera/api exec vitest run` — **651/651 gruen** (46 Dateien), inklusive `ldap.service.spec.ts` mit 67/67 (62 bestehend + 5 neu).
- **API-Typpruefung:** `pnpm --filter @tessera/api type-check` — sauber.
- **`prisma validate`/`prisma generate`:** beide sauber; Migration existiert, wurde **nicht ausgefuehrt**.
- **Web-Testsuite:** `pnpm --filter @tessera/web exec vitest run` — **233/233 gruen** (38 Dateien), inklusive `grants-matrix.test.tsx` (12/12) und `groups-page.test.tsx` (13/13).
- **Web-Typpruefung:** `pnpm --filter @tessera/web type-check` — sauber.
- **Sprachschluessel-/Umlaut-Gate:** `umlaut-guard.spec.ts` gruen bei jedem Zwischenstand — alle neuen Schluessel existieren in de.json UND en.json, kein neues, ungeprueftes Umlaut-Ersatzwort.
- **Alle vier Befehle aus `<verification>` des Plans** ein weiteres Mal am Ende, vom Wurzelverzeichnis aus, gruen.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Task-3-Testfixture konnte den bezeichneten Absturz nicht ausloesen**
- **Found during:** Task 3, RED-Lauf vor der Implementierung
- **Issue:** Der Plan gibt exakt einen Mock-Benutzer vor (`{ id: 'u12', username: 'funktionskonto', displayName: null, email: null }`) und den Suchbegriff `'funktion'`. Da `'funktionskonto'.includes('funktion')` bereits ueber `u.username` matcht, greift die JavaScript-OR-Kurzschlussauswertung (`||`) VOR der ungesicherten `u.email.toLowerCase()`-Zeile — der Testlauf gegen den unveraenderten Bestand war **gruen**, nicht rot wie vom Plan vorhergesagt. Empirisch mit `node -e` bestaetigt (siehe Commit-Nachricht).
- **Fix:** Ein zweites Konto (`{ id: 'u13', username: 'anderes.konto', displayName: null, email: null }`) in dieselbe Mock-`/users`-Antwort aufgenommen. Sein Benutzername matcht den Suchbegriff nicht, sodass die OR-Kette bis zur ungesicherten Zeile durchlaeuft und dort tatsaechlich abstuerzt — Testlauf danach korrekt rot, exakt an `GroupMembersModal.tsx:104`. `funktionskonto` bleibt Teil der Fixture und der Assertion, wie vom Plan verlangt.
- **Files modified:** apps/web/src/app/(portal)/admin/groups/groups-page.test.tsx
- **Verification:** Test rot vor der Implementierung (TypeError an der vorhergesagten Zeile), gruen danach; volle Testsuite (233/233) bleibt gruen.
- **Committed in:** 2167046 (Task-3-Commit)
---
**Total deviations:** 1 auto-fixed (Rule 1 — Testfixture-Korrektur, kein Produktionscode-Verhalten geaendert)
**Impact on plan:** Der zugrunde liegende Defekt (ungesichertes `.toLowerCase()` auf einer moeglicherweise leeren Adresse) war real und wurde wie geplant behoben; nur die Testdaten mussten angepasst werden, damit der Test die Behauptung tatsaechlich pruefte statt trivial gruen zu sein.
## Threat Model Verification
Alle sechs `mitigate`-Eintraege aus dem Plan-Threat-Register sind umgesetzt:
- **T-Q3-01 (Spoofing, `upsertMappedUser`-Aktualisierungszweig):** Mitigiert — `resolveEmailForWrite` vergleicht Halter-ID gegen die eigene Datensatz-ID; Test 3 aus Task 2 belegt, dass `prisma.user.update` ohne `email`-Feld aufgerufen wird, wenn die Adresse einem Dritten gehoert.
- **T-Q3-02 (Information Disclosure, Sync-Bericht):** Mitigiert — Test 5 aus Task 2 belegt per `JSON.stringify(result)`-Negativpruefung, dass der geworfene ORM-Text nirgends im Bericht landet; er erscheint nur im `logger.error`-Aufruf.
- **T-Q3-04 (Spoofing, Passwort-Zuruecksetzung bei leerer Adresse):** Bestaetigt ohne Codeaenderung — die Suche in `auth.service.ts` laeuft ueber eine angefragte Zeichenkette, ein `null`-Wert in der Spalte wird davon nie getroffen.
- **T-Q3-03/T-Q3-05/T-Q3-06/T-Q3-SC (accept):** Keine Codeaenderung noetig, Einschaetzung des Plans bestaetigt (keine neuen Pakete installiert, kein zusaetzliches DoS-Risiko bei den gemessenen Groessenordnungen).
## Self-Check: PASSED
**Files:**
- FOUND: apps/web/src/app/(portal)/admin/modules/grants/page.tsx
- FOUND: apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx
- FOUND: apps/api/prisma/schema.prisma
- FOUND: apps/api/prisma/migrations/20260909120000_user_email_optional/migration.sql
- FOUND: apps/api/src/ldap/ldap.service.ts
- FOUND: apps/api/src/ldap/ldap.service.spec.ts
- FOUND: apps/api/src/user/user.service.ts
- FOUND: apps/api/src/tenders/tender-digest.scheduler.ts
- FOUND: apps/api/src/tenders/tender-matching.service.ts
- FOUND: apps/web/src/app/(portal)/admin/ldap/page.tsx
- FOUND: apps/web/src/app/(portal)/admin/users/page.tsx
- FOUND: apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx
- FOUND: apps/web/src/app/(portal)/admin/groups/groups-page.test.tsx
- FOUND: apps/web/src/messages/de.json
- FOUND: apps/web/src/messages/en.json
**Commits:**
- FOUND: e8c2411 (Task 1)
- FOUND: 1222951 (Task 2)
- FOUND: 2167046 (Task 3)
## Deployment Note (vom Plan uebernommen)
Dieser Plan hat **nichts ausgerollt**: kein `docker compose build`, kein `up`, kein `prisma migrate deploy`. Die Migration `20260909120000_user_email_optional` steckt im API-Image und wird beim naechsten Neubau/Neustart der API automatisch angewandt (`apps/api/Dockerfile:38` ruft `prisma migrate deploy` vor dem Start auf). Solange das nicht geschehen ist, bleibt `User.email` in der laufenden Datenbank ein Pflichtfeld — ein kollidierendes Konto wird dann weiterhin nicht angelegt, scheitert aber sauber als gezaehlter `entryFailures`-Eintrag statt mit rohem Techniktext.
Die fuenf im Plan genannten Browser-Nachpruefungen (Punkte 1-5 unter `<verification>`) sind vom Nutzer nach seinem naechsten Neubau von API und Web nachzuholen — nicht Teil dieser Ausfuehrung.
@@ -0,0 +1,131 @@
---
phase: quick-260909-ab3
verified: 2026-09-09T08:05:00Z
status: human_needed
score: 8/8 must-haves verified
covered_files:
- ".planning/quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/260909-ab3-PLAN.md"
- ".planning/quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/260909-ab3-SUMMARY.md"
- "apps/api/prisma/migrations/20260909120000_user_email_optional/migration.sql"
- "apps/api/prisma/schema.prisma"
- "apps/api/src/ldap/ldap.service.spec.ts"
- "apps/api/src/ldap/ldap.service.ts"
- "apps/api/src/tenders/tender-digest.scheduler.ts"
- "apps/api/src/tenders/tender-matching.service.ts"
- "apps/api/src/user/user.service.ts"
- "apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx"
- "apps/web/src/app/(portal)/admin/groups/groups-page.test.tsx"
- "apps/web/src/app/(portal)/admin/ldap/page.tsx"
- "apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx"
- "apps/web/src/app/(portal)/admin/modules/grants/page.tsx"
- "apps/web/src/app/(portal)/admin/users/page.tsx"
- "apps/web/src/messages/de.json"
- "apps/web/src/messages/en.json"
- "apps/web/src/messages/umlaut-dictionary.ts"
covered_digest: "v1:sha256:ae03cb70a8b2c8457ebc567846a2f092772671a446d0d9bb79b3b567eafa0288"
behavior_unverified: 0
overrides_applied: 0
human_verification:
- test: "Freigaben-Matrix im Browser nach `Claude_VT` bzw. `Vertrieb` durchsuchen"
expected: "Gruppenspalte bleibt stehen UND alle Modulzeilen bleiben stehen, es gibt anklickbare Kaestchen"
why_human: "Plan verlangt Browser-Nachpruefung nach Neubau von API/Web (Konvention: kein Docker-Deploy auf Testserver durch den Ausfuehrenden); durch Unit-Tests (grants-matrix.test.tsx, WINDOWS #6c Regressionstest) bereits codeseitig abgesichert"
- test: "Dieselbe Matrix, Suche nach `Cert`"
expected: "Modulzeile bleibt stehen UND alle Gruppenspalten bleiben stehen"
why_human: "Browser-Nachpruefung vom Plan explizit dem Nutzer zugewiesen; Unit-Test deckt die Achsen-Logik bereits ab"
- test: "Dieselbe Matrix, Suchbegriff ohne Treffer"
expected: "Verstaendliche Meldung statt leerer Tabelle"
why_human: "Browser-Nachpruefung vom Plan explizit dem Nutzer zugewiesen"
- test: "AD-Sync im Browser ausloesen: uvertrieb_ro, uvertrieb_rw, uvertrieb_ro_ss, usoftware_rw"
expected: "Alle vier Konten erscheinen in der Benutzerliste — ohne Adresse; das Konto mit mbuntz@ctl.de behaelt seine Adresse unveraendert"
why_human: "Verlangt einen echten Sync-Lauf gegen das reale AD nach Neubau von API/Web — nicht durch Unit-Tests mit Mock-AD abbildbar; Verhalten ist codeseitig durch resolveEmailForWrite() und fuenf Testfaelle in ldap.service.spec.ts abgesichert"
- test: "Derselbe Sync-Bericht im Browser pruefen"
expected: "Keine Zeile enthaelt englischen Techniktext oder Datenbankwortlaut; die sechs Kontakte ohne Anmeldenamen stehen als neutraler Hinweis, nicht in Rot"
why_human: "Visuelle/Text-Pruefung der gerenderten Oberflaeche nach echtem Sync-Lauf; Backend-Seite (kein roher ORM-Text im Bericht) ist durch Test 5 aus Task 2 bereits programmatisch bewiesen"
---
# Quick Task 260909-ab3: Matrix-Suche und Sync-Meldungen reparieren — Verification Report
**Task Goal:** Matrix-Suche und Sync-Meldungen reparieren (WINDOWS #14 und #15)
**Verified:** 2026-09-09T08:05:00Z
**Status:** human_needed
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Freigaben-Matrix: ein Suchbegriff, der nur einen GRUPPENNAMEN trifft, laesst ALLE Modulzeilen stehen | ✓ VERIFIED | `page.tsx:157-158` returns `filteredModules: modules` (unfiltered) when `groupHits.length > 0 && moduleHits.length === 0`; test `'a group-name search leaves all module rows standing (WINDOWS #14)'` (line 231) passes |
| 2 | Freigaben-Matrix: ein Suchbegriff, der nur einen MODULNAMEN trifft, laesst ALLE Gruppenspalten stehen | ✓ VERIFIED | `page.tsx:154-155` returns `filteredGroups: groups` (unfiltered) when `moduleHits.length > 0 && groupHits.length === 0`; test `'a module-name search leaves all group columns standing (WINDOWS #14)'` (line 208) passes |
| 3 | Spaltensuche findet importierte Gruppe unter internem UND AD-Namen (WINDOWS #6c) | ✓ VERIFIED | `page.tsx:149` `matches(g.internalName ?? g.name) \|\| matches(g.name)`; dedicated regression test `'finds an imported group by both internal and AD name (WINDOWS #6c regression)'` (line 253) searches both `'Vertrieb'` and `'Claude_VT'` and passes |
| 4 | Kein Treffer weder Modul noch Gruppe -> sichtbare Meldung statt stumm leerer Tabelle | ✓ VERIFIED | `page.tsx:219-222` renders `t('noSearchResults', {search})` when `noMatch`; test `'shows a visible message when the search term matches neither modules nor groups'` (line 283) asserts message visible and 0 checkboxes |
| 5 | AD-Sync: Konto mit bereits belegter Adresse wird angelegt — ohne Adresse; erster Halter behaelt seine Adresse | ✓ VERIFIED | `resolveEmailForWrite()` (ldap.service.ts:416-427) queries `findUnique` and withholds the address unless `!holder \|\| holder.id === ownRecordId`; wired into BOTH `upsertMappedUser` (create AND update branch) and `importUsersByDn`. Falsification test performed: reverting the ownership check to unconditionally allow the address makes `'nimmt einem bestehenden Konto seine Adresse nicht weg'` fail with the real address leaking into the `update()` call — confirms the test genuinely exercises the takeover scenario, not just the happy path |
| 6 | Kein Bestandteil des Sync-Berichts traegt rohen ORM-/englischen Techniktext | ✓ VERIFIED | `ldap.service.ts:988-998`: catch block puts only `entry.dn` into `result.entryFailures`; the raw exception message goes exclusively to `this.logger.error(...)`. Test `'reicht keinen rohen Datenbanktext an den Bericht durch'` asserts `JSON.stringify(result)` does not contain the thrown ORM message — passes |
| 7 | Eintraege ohne Anmeldenamen erscheinen als neutraler Hinweis, nicht als Fehler | ✓ VERIFIED | `ldap.service.ts:965-969` pushes to `result.skippedNoLogin` (not `errors`); UI renders this list with `text-muted-foreground`, explicitly not `text-destructive` (`admin/ldap/page.tsx:1363-1374`); test `'meldet Eintraege ohne Anmeldenamen getrennt und nicht als Fehler'` passes |
| 8 | Benutzer ohne E-Mail-Adresse bricht weder Benutzerliste noch Mitgliedersuche | ✓ VERIFIED | `GroupMembersModal.tsx:104` guards with `(u.email ?? '')`; `users/page.tsx` declares `email: string \| null`, table cell falls back to en-dash. Falsification test performed: reverting the guard to `u.email.toLowerCase()` makes the two-account fixture genuinely crash with `TypeError: Cannot read properties of null` at the exact predicted line — confirms the fixture (added as a deviation) really exercises the crash path, addressing the executor's self-reported deviation |
**Score:** 8/8 truths verified (0 present, behavior-unverified)
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/prisma/migrations/20260909120000_user_email_optional/migration.sql` | Migration making `User.email` optional, NOT applied | ✓ VERIFIED | 18 lines, `ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL;` only, unique index untouched, German header comment documents the locked product decision. Coherent with `schema.prisma:32` (`email String? @unique`). Not applied to any database (only the file was written; no migrate/deploy command was run by this verifier or found evidence of prior application) |
| `apps/web/src/app/(portal)/admin/modules/grants/page.tsx` | Independent-axis search filtering | ✓ VERIFIED | 303 lines, `useMemo` truth table matches plan spec exactly (lines 140-162) |
| `apps/api/src/ldap/ldap.service.ts` | Shared collision decider wired into both create paths | ✓ VERIFIED | 1692 lines, `resolveEmailForWrite` (416-427) used in `upsertMappedUser` (464, 494) and `importUsersByDn` (697) |
| `apps/web/src/app/(portal)/admin/ldap/page.tsx` | Structured, German-only sync report display | ✓ VERIFIED | 1403 lines, `SyncResult` interface extended (79-81), three new report sections (1348-1386) in the plan-specified order and colors |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|----|--------|---------|
| `upsertMappedUser()` | `UserService.create({ email? })` | Optional email passthrough | ✓ WIRED | `user.service.ts:51` `email?: string`; `ldap.service.ts:506-513` calls `create` with `...(createEmail && { email: createEmail })` — spreads nothing when withheld |
| `LdapSyncResult` (backend) | `interface SyncResult` (frontend) | Field-name parity | ✓ WIRED | Both sides declare `emailConflicts: {account, email}[]`, `skippedNoLogin: string[]`, `entryFailures: string[]` with matching names/shapes |
| `de.json` | `en.json` | Key parity via `umlaut-guard.spec.ts` (3rd test) | ✓ WIRED | Ran `umlaut-guard.spec.ts` independently — 3/3 tests pass, confirming both files flatten to identical key sets |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|--------------|--------|----------|
| PERM-02 | 260909-ab3-PLAN.md | AD group import/sync (Phase 16, already Complete) | ✓ SATISFIED | This quick task is a bugfix to already-shipped Phase 16/15 functionality (WINDOWS #14/#15), not new scope; no regression to the underlying requirement introduced (verified: full API+web suites green) |
| PERM-03 | 260909-ab3-PLAN.md | Module grants matrix (Phase 15, already Complete) | ✓ SATISFIED | Same — matrix search bug fixed without altering the underlying grant-toggle mechanism (T-Q3-06 accepted: search only changes visibility, not server-checked grants) |
### Anti-Patterns Found
None. Scanned all 13 non-message-catalog files listed in `files_modified` for `TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER/not yet implemented/coming soon` — zero hits.
### Behavioral Spot-Checks / Falsification Tests
| Behavior | Method | Result | Status |
|----------|--------|--------|--------|
| T-Q3-01 ownership check actually prevents address takeover | Reverted `resolveEmailForWrite` to unconditionally allow the address, ran `'nimmt einem bestehenden Konto seine Adresse nicht weg'` alone | Test failed exactly as expected (`update()` called with the foreign address in `data`); reverted, full suite re-confirmed 67/67 green | ✓ PASS |
| Task-3 deviation (two-account fixture) genuinely exercises the crash | Reverted `GroupMembersModal.tsx:104` guard to `u.email.toLowerCase()`, ran the new test alone | Test failed with `TypeError: Cannot read properties of null (reading 'toLowerCase')` at the predicted line; reverted, full suite re-confirmed 13/13 green | ✓ PASS |
| Full API suite | `npx vitest run` from `apps/api` | 651/651 passed (46 files) — matches SUMMARY claim exactly | ✓ PASS |
| Full API type-check | `npx tsc --noEmit -p tsconfig.json` from `apps/api` | Clean, no output | ✓ PASS |
| Full web suite | `npx vitest run` from `apps/web` | 233/233 passed (38 files) — matches SUMMARY claim exactly | ✓ PASS |
| Full web type-check | `npx tsc --noEmit` from `apps/web` | Clean, no output | ✓ PASS |
| Individual file counts | `ldap.service.spec.ts` (67), `grants-matrix.test.tsx` (12) | Matches SUMMARY's "62 existing + 5 new" and "12/12" claims | ✓ PASS |
| No customer-specific values in shipped code | `grep -rn -i "uvertrieb\|usoftware\|mbuntz\|CTL_PWS" apps/ --include="*.ts" --include="*.tsx" --include="*.json"` | Zero hits anywhere in `apps/` (implementation and tests) | ✓ PASS |
| No raw ORM text reaches the UI report | Traced `catch (entryError)` block (ldap.service.ts:988-998) | Only `entry.dn` pushed to `result.entryFailures`; raw message only reaches `this.logger.error` | ✓ PASS |
Working tree was restored to a clean state after each falsification (`git status --short` shows only the untracked SUMMARY.md, no residual diffs).
### Human Verification Required
Five items are explicitly deferred to the user per the plan's own `<verification>` section (require a rebuild of API and web images before they can be exercised in a real browser against a real AD/Postgres). These are recorded as human-check items, not gaps — the underlying code paths are independently proven via unit tests and the falsification tests above.
1. **Freigaben-Matrix: Suche nach `Claude_VT`/`Vertrieb`** — Gruppenspalte und alle Modulzeilen bleiben stehen, anklickbare Kaestchen vorhanden. *Why human:* Browser-Nachpruefung nach Neubau, laut Projektkonvention "Kein Docker-Deploy auf Testserver durch Claude".
2. **Dieselbe Matrix, Suche nach `Cert`** — Modulzeile und alle Gruppenspalten bleiben stehen. *Why human:* Gleicher Grund.
3. **Dieselbe Matrix, Begriff ohne Treffer** — verstaendliche Meldung statt leerer Tabelle. *Why human:* Gleicher Grund.
4. **AD-Sync ausloesen** — die vier Konten `uvertrieb_ro`, `uvertrieb_rw`, `uvertrieb_ro_ss`, `usoftware_rw` erscheinen ab jetzt in der Benutzerliste, ohne Adresse; das Konto mit `mbuntz@ctl.de` behaelt die Adresse. *Why human:* Braucht einen echten Sync-Lauf gegen das reale AD und eine angewandte Migration — nicht durch Unit-Tests mit Mock-AD abbildbar.
5. **Derselbe Bericht** — keine Zeile enthaelt englischen Techniktext oder Datenbankwortlaut, die sechs Kontakte ohne Anmeldenamen stehen als neutraler Hinweis, nicht in Rot. *Why human:* Visuelle/Text-Pruefung der gerenderten Oberflaeche nach echtem Lauf.
### Gaps Summary
No gaps found. All 8 must-have truths, all 4 required artifacts, and all 3 key links are verified against the actual codebase (not merely SUMMARY claims). Two of the plan's own risk points — the T-Q3-01 ownership-check regression risk and the executor's self-reported test-fixture deviation — were independently falsified by this verifier (reverting the fix and confirming the test goes red for the right reason, then restoring cleanly). All four required test/type-check commands were re-run independently from a clean working tree and matched the SUMMARY's reported counts exactly. Migration file exists, is coherent with the schema, and was not applied (confirmed no `migrate deploy`/`migrate dev` was executed by this verification). No customer-specific account names or domains leaked into shipped code. The only reason this report is `human_needed` rather than `passed` is the five browser-verification items the plan itself explicitly assigns to the user after their next rebuild — these are deferred, not failed.
---
_Verified: 2026-09-09T08:05:00Z_
_Verifier: Claude (gsd-verifier)_