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
@@ -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)_