Commit Graph

750 Commits

Author SHA1 Message Date
schalli 8cbf4c12b8 fix(quick-260909-laa): dritte Eindeutigkeitsverletzung uebersetzen statt sie zu behaupten 2026-09-09 16:12:08 +02:00
schalli df5c5b728b feat(laa-03): binde die Je-Treffer-Haelften der Hintergrunddienste und schliesse die Dokumente
tender-digest.scheduler.ts: die uebergreifende Kandidatenabfrage
(tenderMatch.findMany mit distinct:['userId']) bleibt bewusst ungebunden
und waehlt zusaetzlich das denormalisierte tenantId der Treffer-Zeile mit
aus; innerhalb der Schleife binden tenderNotificationPref.findUnique,
tenderMatch.findMany/updateMany und user.findUnique an den Mandanten
DIESER Kandidatenzeile.

tender-matching.service.ts: die Profilabfrage (tenderSavedSearch.findMany)
und der Lesezugriff auf den plattformweiten Tender-Katalog (D-03) bleiben
ungebunden; innerhalb der Profilschleife bindet die Treffer-Anlage
(tenderMatch.upsert), im nachgelagerten Instant-Dispatch binden
tenderMatch.findMany/updateMany und user.findUnique — je EIN gebundener
Client pro Profil, nicht neu je Treffer.

Beide Dateien tragen Codekommentare, die die uebergreifenden Abfragen
ausdruecklich als Etappe-3-Uebergabe benennen — die Trennlinie zwischen
Etappe 2 und Etappe 3 wird hier gezogen, nicht verwischt.

Alle drei betroffenen Testdateien (inkl. der gemeinsamen
Integrationsdatei) bekommen den Zwei-Client-Nachweis, Tests fuer
Zwei-Mandanten-Laeufe und die lautlose Fehlerform (kein Versand,
notifiedAt bleibt NULL). Falsifiziert: ein probeweiser Rueckbau der
user.findUnique-Bindung im Instant-Dispatch von tender-matching.service.ts
machte genau den erwarteten Test rot, danach zurueckgenommen.

rls-access-inventory.spec.ts gemessen und docs/mandantentrennung-zugriffsklassifikation.md
nachgezogen: Stand der fuenf betroffenen Paare (tender-digest.scheduler.ts/
tenderMatch,tenderNotificationPref,user; tender-matching.service.ts/
tenderMatch,user) auf gemischt bzw. gebunden; Bereichsuebersicht und
Klassen-Verteilung neu gemessen (tenders jetzt 36 ungebunden/26 gebunden);
"Der Hintergrunddienst als Falle" um den Abschluss beider Dateien ergaenzt.

docs/mandantentrennung-etappe2-fehlerrichtung.md: (t4) um den Nachtrag
ergaenzt, dass die Je-Treffer-Haelften geschlossen sind und die
uebergreifenden Haelften an Etappe 3 uebergeben bleiben.

770 Tests gruen, Typpruefung sauber, Wegwerf-Werkzeug 32/32, kein
Schema-/Migrations-/Compose-/Umgebungsdatei-Diff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 16:02:40 +02:00
schalli 3336a6e419 feat(laa-02): binde die fuenf Nutzer-CRUD-Dienste des Bereichs tenders an forTenant()
tender-saved-search.service.ts, tender-triage.service.ts,
tender-notification-pref.service.ts und tender-email-config.service.ts
laufen jetzt vollstaendig ueber forTenant() — vier neue Parameter (list,
update, remove, listForUser, favoriteIds, getForUser, getConfigForApi,
testConnection bekommen tenantId), die anwendungsseitige userId-Filterung
bleibt unveraendert (Befund E: die Policies haben keine Benutzerdimension).

tender-rss-feed.service.ts bindet nur createForUser (Zaehler + Anlage,
beide ausschliesslich auf persoenlichen Zeilen); listForUser, createPlatform
und remove bleiben mit Codekommentar bewusst ungebunden (WINDOWS #19 —
eine gebundene plattformweite Zeile waere unter jedem Mandanten unsichtbar,
ein gebundenes Einfuegen ohne Mandant wuerde abgewiesen).

tender-notification-pref.service.ts und tender-email-config.service.ts
uebersetzen eine P2002-Verletzung auf dem tenantlosen upsert-Schluessel
(Befund F) in eine verstaendliche deutsche Meldung statt eines rohen
Fehlers.

tenders.controller.ts reicht tenantId an den acht betroffenen
Aufrufstellen durch extractTriageContext() durch (kein neuer
Aufloesungsweg); die drei RSS-Aufrufstellen bleiben unveraendert, da ihre
Dienstmethoden nicht binden.

Alle sieben angefassten Testdateien bekommen den Zwei-Client-Nachweis
(__makeBoundClient ueber demselben Speicher) und Bindungstests je
umgestellter Methode; tender-rss-feed.service.spec.ts zusaetzlich den
Gegentest, dass die drei unveraendert bleibenden Pfade forTenant() NICHT
aufrufen. Falsifiziert: ein probeweiser Rueckbau der list()-Bindung in
tender-saved-search.service.ts machte genau den erwarteten Bindungstest
rot, danach zurueckgenommen.

docs/mandantentrennung-zugriffsklassifikation.md: Stand der fuenf Paare
auf gebunden bzw. gemischt nachgezogen; tenderRssFeedSource von
muss-mandantengebunden auf beides umklassifiziert (derselbe Praezedenzfall
wie ldapConfig in 260909-ipc).

761 Tests gruen (743 + 18 neue Bindungsnachweise), Typpruefung sauber,
Wegwerf-Werkzeug 32/32, kein Schema-/Migrations-/Compose-/
Umgebungsdatei-Diff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 15:53:53 +02:00
schalli 349814747e feat(laa-01): messe die drei Sonderfaelle des Bereichs tenders und schreibe die Fehlerrichtung
rls-scratch-check.mjs bekommt einen fuenften Abschnitt (runTendersAreaChecks)
mit den fuenf Policies von TenderEmailConfig/TenderNotificationPref/
TenderRssFeedSource/TenderSavedSearch/TenderTriage, wortgleich aus der
ausgelieferten Migration extrahiert. Neun neue Pruefungen belegen: die
Policies haben keine Benutzerdimension (Befund E), eine plattformweite
RSS-Zeile ist unter jedem Mandantenkontext unsichtbar und ein gebundenes
Einfuegen ohne Mandant wird abgewiesen (WINDOWS #19), und ein gebundenes
upsert auf eine unsichtbare Zeile verletzt die Eindeutigkeitsbedingung,
nicht die Policy (Befund F). Alle 32 Pruefungen (23 bisherige + 9 neue)
bestehen. Nachmessung bestaetigt Befund A: genau eine $transaction in
diesem Bereich, Array-Form auf der plattformweiten Tabelle Tender,
ausserhalb jeder Mandantenbindung — withTenantTransaction() wird hier
nicht gebraucht.

docs/mandantentrennung-etappe2-fehlerrichtung.md bekommt einen
`## Bereich tenders`-Abschnitt mit der beobachteten Messausgabe, einer
Signaltabelle je umzustellendem Pfad und einem eigenen Unterabschnitt zur
lautlosen Fehlerform der beiden Hintergrunddienste (fuenf benannte
Stellen, keine protokolliert etwas).

743 Tests gruen, Typpruefung sauber, kein Schema-/Migrations-/Compose-/
Umgebungsdatei-Diff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 15:43:35 +02:00
schalli b86675b549 docs(quick-260909-laa): Plan fuer Etappe 2 Bereich tenders
Drei Aufgaben: zuerst messen (fuenf Policies wortgleich aus der
ausgelieferten Migration, dazu die drei Sonderfaelle dieses Bereichs —
fehlende Benutzerdimension, nullbarer Mandant bei den RSS-Quellen,
Einfuegen auf eine unsichtbare Zeile), dann die fuenf Nutzerdienste
binden, dann die Je-Treffer-Haelften der beiden Hintergrunddienste und
beide Dokumente schliessen.

Zur Planungszeit gemessen statt zitiert: 743 Tests gruen, Typpruefung
sauber, Wegwerf-Werkzeug 23/23. Von den 62 Rohtreffern ist genau einer
kein Modellzugriff. Der Bereich hat keine mandantengebundene
Transaktion, das Hilfsmittel aus 260909-jts wird hier nicht gebraucht.
Keine der sieben Testdateien kennt die Erweiterung — sie waeren nach
der Umstellung aus dem falschen Grund rot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 15:34:13 +02:00
schalli 4cf7cea01f docs(quick-260909-jts): Etappe 2 Bereich groups abgeschlossen und verifiziert 2026-09-09 15:19:27 +02:00
schalli 604428a91d fix(quick-260909-jts): Lastprobe nachreichen statt sie zu behaupten 2026-09-09 15:18:55 +02:00
schalli abb6c8bea3 feat(jts-03): module-grants.service.ts binden, beide Dokumente schliessen
Alle fuenf Methoden von ModuleGrantsService (assertTargetBelongsToTenant,
grant, revoke, getMatrix, getUserAccess) laufen jetzt ueber forTenant(); bei
den beiden Datenlieferungen teilen sich alle parallel abgesetzten
Teilabfragen denselben gebundenen Client. Die Mandanten-Gegenpruefung vor
jedem Erteilen bleibt ausdruecklich bestehen und bekommt einen Verweis auf
Befund F/T-JTS-03: die Regel auf ModuleGrant prueft nur die
Mandantenkennung der Zeile, nicht die referenzierte Gruppe. Der veraltete
Kommentar ueber der Mitgliedschaftsabfrage im Benutzer-Detail ("kein
forTenant hier") ist durch den neuen Stand ersetzt.

module-grants.service.spec.ts bekommt denselben Bindungsnachweis-Mock wie
groups.service.spec.ts (zwei unterscheidbare Clients ueber demselben
Speicher) und sechs neue Bindungsnachweise; alle 28 Bestandstests bleiben
gruen.

Beide Dokumente geschlossen: die Bereichsuebersicht fuer groups ist neu
gemessen (0 ungebunden, 31 gebunden — ein dokumentierter methodischer
Bodensatz, da die einfache Rohtrefferzaehlung die neun ueber `tx` gebundenen
Zugriffe innerhalb der drei Transaktionen nicht sieht), die
Klassen-Verteilung auf 62 Paare aktualisiert, und der als offen gefuehrte
Befund D aus dem ldap-Abschnitt der Fehlerrichtung ist mit Verweis auf
diesen Durchlauf als erledigt vermerkt (Nachtrag, nicht Neuschrieb). 743
Tests und die Typpruefung gruen; Schema, Migrationen und alle vier
Compose-Dateien unveraendert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 15:07:09 +02:00
schalli 7f08b27eea feat(jts-02): groups.service.ts binden, Transaktionen tragfaehig machen, Absicherung sehend machen
prisma-tenant.extension.ts bekommt withTenantTransaction(prisma, tenantId, fn)
— die interaktive Transaktion auf dem UNgebundenen Client mit set_config als
erster Anweisung direkt auf tx, die in Aufgabe 1 als einzige der drei
gemessenen Formen sowohl die Einzelmessung als auch eine Lastprobe unter
echter Nebenlaeufigkeit bestand (die Array-Form auf dem gebundenen Client
verteilt jede Operation auf eine eigene Teiltransaktion; die interaktive Form
auf dem gebundenen Client brach unter 40 parallelen Aufrufen mit P2028 ab).

rls-access-inventory.spec.ts bekommt eine dritte Erkennung fuer
Modellzugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion
(zwei Formen: direkter Empfaenger.$transaction(async...) und das neue
Hilfsmittel withTenantTransaction(...)) — macht das Paar
(groups.service.ts, tenantModuleActivation) erstmals sichtbar, das bislang
keine Pruefung dieses Projekts je gesehen hat.

groups.service.ts: alle zwoelf Methoden inklusive der drei Transaktionen
(update() isDefault:true, reassignDefaultBeforeDelete(), ensureDefaultGroup())
laufen jetzt ueber den Mandantenkontext. Zaehler und Transaktion in
ensureDefaultGroup() sind gemeinsam gebunden (T-JTS-05). addUserToDefaultGroup()
prueft neu, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02) —
die Regel auf GroupMembership prueft nachweislich nur die Gruppenseite.

groups.service.spec.ts bekommt zwei unterscheidbare Clients ueber demselben
Speicher-Fake (Muster aus 260909-ipc, auf die interaktive Form uebertragen)
und 13 neue Bindungsnachweise; alle 42 Bestandstests bleiben gruen.
Klassifikationsdokument nachgezogen. 737 Tests und die Typpruefung gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 15:01:24 +02:00
schalli fd0b9f7d21 feat(jts-01): messen statt annehmen — groups-Policies und Transaktionsform vor jedem Dienstcode
rls-scratch-check.mjs bekommt einen vierten Abschnitt (Group/GroupMembership/
ModuleGrant/TenantModuleActivation, Policies woertlich aus den ausgelieferten
Migrationen) sowie eine eigene Messung, welche der drei Transaktionsformen
(Array auf gebundenem Client, interaktiv auf gebundenem Client, interaktiv auf
ungebundenem Client mit set_config auf tx) den Mandantenkontext tatsaechlich
auf derselben Verbindung traegt. Ergebnis: Form (i) versagt nachweisbar
(unterschiedliche pg_backend_pid() je Teilschritt); Form (ii) und (iii)
bestehen die Einzelmessung, aber eine zusaetzliche Lastprobe mit 40 parallelen
Aufrufen zeigt, dass Form (ii) unter echter Nebenlaeufigkeit mit P2028
(Transaction API error) abbricht, waehrend Form (iii) 0 Verletzungen zeigt.
Die Kritikschrift bekommt einen eigenen groups-Abschnitt mit den tatsaechlich
beobachteten Werten, der Signaltabelle je Pfad und der Liste der Stellen, die
Leere als Abwesenheit deuten (inkl. der einen Stelle, an der zu wenig Lesen zu
viel Schreiben ausloest). Kein Dienstcode angefasst; 719 Tests und die
Typpruefung bleiben gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 14:48:04 +02:00
schalli b532eaa2ad docs(quick-260909-jts): Plan fuer Etappe 2, Bereich groups 2026-09-09 14:32:42 +02:00
schalli fdca2bc452 docs(quick-260909-ipc): Etappe 2 Bereich ldap abgeschlossen und verifiziert 2026-09-09 14:19:45 +02:00
schalli eec885a9f7 docs(quick-260909-ipc): complete Etappe 2 Bereich ldap plan 2026-09-09 14:13:27 +02:00
schalli e1586a41dd feat(quick-260909-ipc): ldap.service.ts an forTenant() binden, Adress-Kollisionspruefung bewusst uebergreifend belassen
Aufgabe 3 der Etappe 2: elf Abfragen in sechs Methoden (listGroups,
upsertMappedUser, searchUsers, importUsersByDn, importGroupsByDn,
syncUsersForTenant) laufen jetzt ueber forTenant(), teils mit einem neu
erzeugten, teils mit dem in derselben Methode bereits vorhandenen gebundenen
Client. resolveEmailForWrite bleibt ausdruecklich ungebunden (Befund A,
T-IPC-04): email/username sind plattformweit eindeutig, eine Bindung wuerde
einen fremden Halter uebersehen und eine saubere Kollisionsmeldung in einen
P2002-Abbruch verwandeln.

Ein neuer Testblock biegt forTenant() auf ein zweites, unterscheidbares
Client-Objekt um (der bisherige Identitaets-Mock haette die Umstellung nicht
bemerkt, Befund F) und belegt damit, dass die Adressabfrage weiterhin am
ungebundenen und der Rest am gebundenen Client landet. Alle 67 Bestandstests
bleiben unveraendert gruen.

docs/mandantentrennung-zugriffsklassifikation.md ist fuer den Bereich ldap
geschlossen: gemessener Stand je Fundstelle, neu gerechnete Bereichsuebersicht
(gebunden getrennt von ungebunden gezaehlt) und die Uebergabe des
Standardgruppen-Punkts an den Bereich groups vor Etappe 4 dokumentiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 14:10:51 +02:00
schalli 9a57fa79f5 feat(quick-260909-ipc): ldap-config.service.ts an forTenant() binden, Loesch-Fremdzugriff schliessen
Aufgabe 2 der Etappe 2: getConfig/createConfig/updateConfig sowie
addFieldMapping/removeFieldMapping laufen jetzt ueber forTenant(), gebunden
an den aus der Anfrage bekannten Mandanten. removeFieldMapping nimmt den
Mandanten neu als Pflichtparameter entgegen und der Controller holt ihn aus
dem Sitzungsnachweis statt nur die URL-Kennung weiterzureichen (T-IPC-01) --
ein Administrator konnte bisher die Feldzuordnung eines fremden Mandanten
loeschen, wenn er ihre Kennung kannte. getAllActiveConfigs() und die
Start-Nachverschluesselung bleiben bewusst uebergreifend, mit ausgeschriebener
Begruendung im Code (Befund B).

rls-access-inventory.spec.ts erkennt jetzt neben `this.prisma.<Modell>` auch
gebundene `<Name>.<Modell>`-Zugriffe (Befund F/G) und prueft eine neue
Stand-Spalte (gebunden/ungebunden/gemischt) im Klassifikationsdokument gegen
den Quelltext. Das macht zwei bisher unsichtbare, weil schon laenger
gebundene Fundstellen sichtbar (auth.service.ts/passwordResetToken,
ldap.service.ts/groupMembership) und deckt auf, dass
(ldap-config.service.ts, ldapConfig) tatsaechlich "beides" ist, nicht
"muss-mandantengebunden" (Befund B).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 14:01:28 +02:00
schalli a0c9ef070f feat(quick-260909-ipc): Fehlerrichtung des Bereichs ldap messen und schriftlich festhalten
Aufgabe 1 der Etappe 2: erweitert das Wegwerf-Werkzeug rls-scratch-check.mjs
um fuenf Messungen des forTenant()-Musters gegen die echte, aus der
ausgelieferten Migration geschnittene LdapConfig/LdapFieldMapping-Policy
unter einer Rolle ohne BYPASSRLS. Belegt insbesondere, dass ein ungebundener
Zugriff nach dem Scharfschalten 0 Zeilen liefert, nicht alle -- die
Fehlerrichtung dreht sich um. Die neue Kritikschrift
docs/mandantentrennung-etappe2-fehlerrichtung.md haelt das schriftlich fest,
mit Signaltabelle je Pfad und den vier Stellen, die Leere als Abwesenheit
deuten. Kein Dienstcode angefasst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 13:53:56 +02:00
schalli b34500b452 docs(quick-260909-ipc): Plan fuer Etappe 2, Bereich ldap
Drei Aufgaben: Fehlerrichtung schriftlich festhalten und an der
ausgelieferten Policy messen, dann ldap-config.service.ts und
ldap.service.ts auf forTenant() umstellen. Alle Fundstellen zur
Planungszeit einzeln aufgeschlagen statt gezaehlt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-09 13:43:06 +02:00
schalli 9641592a8c docs: Handoff fuer die Fortsetzung der Mandantentrennung
Tessera CI/CD / Lint & Type Check (push) Successful in 46s
Tessera CI/CD / Tests (push) Successful in 52s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
WIP-Uebergabe vor Etappe 2. Der Arbeitsbaum ist sauber, alles gepusht, die CI
gruen — pausiert wird an einer Etappengrenze, nicht mitten in einer Aufgabe.

Die Uebergabe haelt vier Anti-Patterns fest, die alle aus tatsaechlichen
Fehlschlaegen dieser Sitzung stammen. Der wichtigste: auf einem ungeprueften
Fundament bauen. forTenant() war kaputt, und ohne die empirische Probe waere
das erst nach dem Scharfschalten aufgefallen — als stiller Datenverlust, weil
der LDAP-Loeschzweig leere Ergebnisse als "Gruppe verschwunden" deutet.

Ebenfalls festgehalten, weil beides Zeit gekostet hat: eine grep-Zaehlung, die
Kommentare mitzaehlte (36 vermeintliche Aufrufstellen, tatsaechlich 6), und die
falsche Compose-Datei auf dem Server, weil COMPOSE_FILE auf prod.yml zeigt.

Der Einstiegspunkt fuer Etappe 2 ist bewusst nicht der groesste Bereich,
sondern ldap: dort sitzt der gefaehrlichste Loeschzweig, und die Wirkung ist
dort am besten pruefbar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 11:18:12 +02:00
schalli 5228f28cbe docs(quick-260909-eor): Etappe 1 der Mandantentrennung — Bericht und Klassifikation
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 55s
Tessera CI/CD / Build & Publish Images (push) Successful in 29s
Der Kernfund dieser Etappe war ein Defekt im Fundament: forTenant() setzte den
Mandantenkontext auf der Transaktionsverbindung, dispatchte die Abfrage aber
ueber den aeusseren Client. Empirisch reproduziert statt hergeleitet —
set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL.

Damit hat die Mandantentrennung nie funktioniert, auch nicht dort, wo sie
scheinbar benutzt wurde. Nach einem Scharfschalten haette sich das umgekehrt:
die betroffenen Abfragen liefern dann null Zeilen, und der LDAP-Loeschzweig
haette das als "Gruppe im Verzeichnis verschwunden" gedeutet und sie samt
Mitgliedschaften und Modulfreigaben geloescht. Aufgefallen, weil vor dem
Umbau geprueft wurde, ob das Fundament traegt.

Der Anmeldeweg bekam eine bewusst schmale Ausnahme: drei SECURITY-DEFINER-
Funktionen mit fester Spaltenliste, Gleichheitsbedingung und LIMIT 1. Eine
Policy waere hier untauglich — sie ist ein Zeilenpraedikat und haette
zwangslaeufig die ganze Tabelle freigegeben.

Die Klassifikation macht die restliche Arbeit planbar: 227 Zugriffe in 59
Einheiten, davon 31 umzustellen, 9 teilweise, 16 ohne Mandantenbezug und 3
bewusst uebergreifend. Ein Test haelt die Einteilung gegen Abdriften fest.

Browser-Gegenprobe lokal bestanden. 701 Tests gruen. Der Schalter bleibt aus;
#18, #19 und #20 bleiben offen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 11:14:58 +02:00
schalli da0ac04e73 fix(quick-260909-eor): WINDOWS #20 offen halten bis Wirkung nach Scharfschalten belegt ist
Aufgabe 4: die Browser-Gegenprobe der Anmeldung ist bestanden (siehe SUMMARY).
Auf ausdruecklichen Wunsch bleibt WINDOWS #20 (forTenant()-Verbindungsdefekt)
dennoch OPEN statt fixed — die technische Reparatur ist nachgewiesen
(rls-scratch-check.mjs, 8/8 gegen eine Wegwerf-Datenbank mit Rolle ohne
BYPASSRLS), aber die Wirkung unter der echten Anwendungsrolle tessera_app
ist erst nach dem Scharfschalten (#18) beobachtbar. #20 bleibt damit an
dieselbe Bedingung gebunden wie #18 und #19.

Nebenbefund beim Zuruecksetzen: die vorherige Markierung als "fixed" hatte
nur die Markdown-Tabelle veraendert, nicht den massgeblichen JSON-Block am
Dateiende (die eigentliche Quelle der Wahrheit fuer gsd-tools windows status)
— `gsd-tools windows status` scheiterte seitdem mit
"Ledger counts disagree with entries". Behoben durch Neuaufbau aus der
Vorversion via der broken-windows.cjs-Bibliothek (parseLedger/renderLedger),
damit Tabelle und JSON-Block wieder uebereinstimmen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 11:11:28 +02:00
schalli 5f3a39c2c3 docs(quick-260909-eor): alle 227 Datenbankzugriffe klassifiziert und maschinell abgesichert
WINDOWS #18/#20, Aufgabe 3: docs/mandantentrennung-zugriffsklassifikation.md
haelt fuer jede der 227 this.prisma.*-Fundstellen (32 Dateien, zusammengefasst
zu 59 Datei-Modell-Paaren) eine Klasse fest — muss-mandantengebunden (31),
keine-mandantengebundene-tabelle (16), bewusst-uebergreifend (3, mit
ausgeschriebenem Grund) oder beides (9, der Hintergrunddienst-Sonderfall:
uebergreifend lesen, je Zeile mandantengebunden schreiben — betrifft
ldap.service.ts, tender-digest.scheduler.ts, tender-matching.service.ts).

rls-access-inventory.spec.ts ermittelt die Fundstellen bei jedem Testlauf neu
aus dem Quelltext und vergleicht sie gegen die Tabelle im Dokument — Datei und
Modellname als Schluessel, keine Zeilennummer. Scheitert nachweislich, sobald
eine Fundstelle fehlt oder ein Eintrag verwaist (per Testlauf geprueft, danach
zurueckgesetzt).

Zwei belegte Befunde im Dokument festgehalten: req.tenantPrisma wird gesetzt,
aber nirgends gelesen; WINDOWS #19 (nullbares tenantId bei SearchProvider/
TenderRssFeedSource) bleibt benannter Blocker fuer Etappe 3.

docs/mandantentrennung-datenbankrolle.md verweist jetzt auf das neue
Dokument und korrigiert die ueberholte Zahl 182 auf den nachgemessenen Stand
(227/59). WINDOWS.md #18/#19 um Nachtrag auf diesen Plan ergaenzt; #20 (der
in Aufgabe 1 gemessene und behobene forTenant()-Verbindungsdefekt) als
"fixed" markiert.

Deviation (Rule 3, blockierend fuer die Bestandsaufnahme-Pruefung):
auth.service.ts-Kommentar umformuliert, der zuvor woertlich
"this.prisma.user.findUnique" als erklaerenden Text enthielt und dadurch
einen Eigentreffer der grep-basierten Inventur-Pruefung erzeugte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 11:04:29 +02:00
schalli de50297467 feat(quick-260909-eor): schmale SECURITY-DEFINER-Ausnahme fuer den Anmeldeweg
WINDOWS #18/#20, Aufgabe 2: der Anmeldeweg muss den passenden Benutzer
finden, bevor sein Mandant bekannt ist — unter der kuenftigen Rolle ohne
BYPASSRLS (tessera_app) wuerde ein gewoehnlicher SELECT auf "User" sonst
null Zeilen liefern und die Anmeldung waere unmoeglich.

Drei SECURITY-DEFINER-Funktionen (STABLE, fester Suchpfad public/pg_temp,
fester Spaltensatz, LIMIT 1, Ausfuehrungsrecht ausschliesslich fuer
tessera_app) ersetzen die drei pre-tenant Lesezugriffe in auth.service.ts:

- auth_lookup_user_by_username (validateUser)
- auth_lookup_user_by_email (requestPasswordReset)
- auth_lookup_reset_token (resetPassword)

Sobald der Benutzer und damit sein Mandant bekannt sind, laufen alle
Schreibzugriffe (lastLoginAt, passwordHash, Reset-Token) ueber forTenant(),
gebunden an genau diesen Mandanten (Aufgabe 1). getMe/changePassword/
adminResetPassword bleiben bewusst unangetastet — sie kennen den Mandanten
bereits aus dem Sitzungsnachweis und gehoeren in Etappe 2.

rls-scratch-check.mjs um einen zweiten Abschnitt erweitert: spielt die
Migration in die Wegwerf-Datenbank ein und misst live unter der Rolle ohne
BYPASSRLS — Anmeldesuche findet den Benutzer, unbekannter Name liefert
nichts ohne zu werfen, gewoehnlicher SELECT auf "User" liefert null Zeilen.
Alle 8 Pruefungen (5 aus Aufgabe 1 + 3 neue) bestehen gegen die lokale
Datenbank. Volle Testsuite (695 Tests) und type-check bleiben gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:56:41 +02:00
schalli bbf179503c fix(quick-260909-eor): forTenant() bindet Mandantenkontext auf dieselbe Verbindung
WINDOWS #20: set_config() lief auf einer anderen Postgres-Verbindung als die
eigentliche Abfrage, weil die interaktive Callback-Form von $transaction
verwendet wurde. Ersetzt durch die Array-Form, die set_config und Abfrage als
eine Transaktion auf einer Verbindung ausfuehrt (Prismas empfohlenes Muster
fuer RLS-ueber-Extensions). Injektionsfestigkeit (T-02-05) bleibt ueber ein
getaggtes $executeRaw-Template statt $executeRawUnsafe erhalten.

- prisma-tenant.extension.spec.ts: prueft die Form des Aufrufs (Array mit
  zwei Eintraegen, Rueckgabewert ist der zweite Eintrag) ohne laufende
  Datenbank
- rls-scratch-check.mjs: neues Werkzeug, das eine Wegwerf-Datenbank anlegt
  und live misst — gleiche Backend-Verbindung, gesetzter Kontext, keine
  Fremdmandanten-Zeilen, keine Zeilen ohne Kontext. Alle 5 Pruefungen
  bestehen gegen die lokale Datenbank.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:50:37 +02:00
schalli d0393ac4af docs: forTenant setzt den Kontext auf der falschen Verbindung (#20)
Empirisch reproduziert, nicht hergeleitet: set_config landete auf Backend-PID
254999, die eigentliche Abfrage auf 255000, dort war app.current_tenant NULL.
Die Erweiterung setzt den Kontext auf tx, dispatcht die Abfrage aber ueber den
aeusseren Client.

Folge: die Mandantentrennung hat nie funktioniert, auch nicht an den Stellen,
die sie scheinbar nutzen. Heute unsichtbar, weil die Rolle ohnehin BYPASSRLS
hat (#18). Nach dem Scharfschalten kehrt es sich um — die Abfragen liefern
dann null Zeilen, und der LDAP-Loeschzweig deutet das als "Gruppe im
Verzeichnis verschwunden" und loescht sie samt Mitgliedschaften und
Modulfreigaben.

Zusatz: von 36 vermeintlichen forTenant-Vorkommen sind die meisten Kommentare,
die dessen Fehlen erklaeren — echte Aufrufstellen sind 6. Und das von
tenant.middleware.ts:44 und tenant.guard.ts:41 gesetzte req.tenantPrisma liest
niemand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:44:39 +02:00
schalli e777802749 docs(quick-260909-eor): Etappe 1 der Mandantentrennung geplant
Drei Punkte werden in dieser Etappe abschliessend geklaert: der gemessene
forTenant()-Defekt, die schmale Ausnahme fuer den Anmeldeweg und die
maschinell abgesicherte Klassifikation aller 232 Datenbankzugriffe.

Der Befund zu forTenant() wurde vor der Planung empirisch belegt: set_config
laeuft auf Backend 254999, die eigentliche Abfrage auf 255000, der
Mandantenkontext ist dort NULL. Alle heutigen forTenant()-Aufrufe sind damit
stillschweigend ungebunden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:43:24 +02:00
schalli 775a938f32 docs: Dateisicherung auf dem Server wirksam — WINDOWS #17 geschlossen
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 55s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
Beim Nachtragen kam heraus, dass auf dem Server gar nicht docker-compose.yml
gilt: die .env setzt COMPOSE_FILE=docker-compose.prod.yml. Meine erste Angabe
nannte deshalb die falsche Datei — aufgefallen, weil nach dem Eintragen
'docker compose config' weiterhin nur pgdata rendern wollte.

Die drei Zeilen sind jetzt in docker-compose.prod.yml ergaenzt, Sicherung
liegt als docker-compose.prod.yml.bak.20260909-0818 daneben.

Nach dem Neuerstellen durch den User belegt statt behauptet:
- Mount tessera_user-files -> /app/user-files am laufenden Container
- der Ordner gehoert uid 1001, das benannte Volume hat die Eigentuemerschaft
  beim ersten Einhaengen uebernommen (genau der Grund, warum es kein
  Bind-Mount wurde)
- Schreiben als Dienstnutzer funktioniert
- eine Probedatei lag unter /var/lib/docker/volumes/tessera_user-files/_data
  auf dem Host, also ausserhalb des Containers; danach wieder entfernt

Ledger: 16 behoben, 1 zurueckgestellt, offen nur noch #18 und #19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:23:17 +02:00
schalli ea003d44fe docs(quick-260909-dgj): Mandantentrennung vorbereitet — Plan, Bericht, Befunde
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 52s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
Die Arbeit liefert Rolle, Verbindungstrennung, Pruefwerkzeug und Policies fuer
alle 16 offenen Tabellen, schaltet die Trennung aber bewusst NICHT scharf.

Grund, gemessen statt vermutet: im Code stehen 182 Datenbankzugriffe ohne
Mandantenkontext gegen 19 mit. Der Anmeldeweg ist zwingend darunter — er liest
die Benutzerzeile, bevor der Mandant bekannt ist, weil der Mandant erst aus
dieser Zeile kommt. Unter einer Rolle ohne Umgehungsrecht liefert diese Abfrage
nichts, und niemand koennte sich mehr anmelden. Das Scharfschalten ist damit
ein eigener Vorgang, kein Nebeneffekt dieser Arbeit.

Neu im Ledger als #19: SearchProvider und TenderRssFeedSource haben ein
nullable tenantId. Die einfache Policy vergleicht NULL nie gleich, wodurch die
plattformweiten Zeilen nach dem Scharfschalten fuer JEDEN Mandanten
verschwinden wuerden — nicht nur fuer fremde. Heute wirkungslos, beim
Scharfschalten zwingend mitzuloesen. Die richtige Semantik ist eine
Produktentscheidung, deshalb bewusst nicht eigenmaechtig anders geloest.

673/673 Tests gruen, Typpruefung sauber. #18 bleibt offen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:08:59 +02:00
schalli efaabc97a2 feat(quick-260909-dgj): Policies fuer die 16 fehlenden Tabellen (Task 4/4)
- Migration 20260909140000_rls_remaining_tenant_tables ergaenzt ENABLE +
  FORCE ROW LEVEL SECURITY und je eine tenant_isolation_policy fuer alle 16
  noch offenen Tabellen mit tenantId
- Kopfkommentar korrigiert die zu pauschale D-03-Aussage aus
  20260804130918: nur die drei Tender-Tabellen OHNE tenantId sind davon
  betroffen, die sechs MIT tenantId bekommen jetzt eine Policy — die alte
  Migrationsdatei bleibt unveraendert
- rls-coverage.spec.ts misst die Abdeckung aus Schema und Migrationen
  statt Text zu vergleichen (rot mit 16 gemeldeten Luecken vor der
  Migration, jetzt gruen); waechst automatisch mit kuenftigen Modellen und
  erzwingt bei jedem neuen tenantId-losen Modell eine bewusste Entscheidung

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:05:15 +02:00
schalli 44a90e5bfd feat(quick-260909-dgj): Pruefwerkzeug fuer den Nachweis plus Betriebsanleitung (Task 3/4)
- apps/api/scripts/rls-preflight.mjs misst fuenf benannte Eigenschaften
  (rollenrechte, kontext-setzbar, ohne-kontext-leer, mit-kontext-sichtbar,
  schreibrechte) jeweils in einer eigenen Transaktion gegen eine per
  TESSERA_PREFLIGHT_DATABASE_URL angegebene Verbindung; --print-plan
  verbindet nicht, das Werkzeug schreibt in keiner Betriebsart
- 5 Tests in rls-preflight.spec.ts (rot vor dem Werkzeug, jetzt gruen)
- docs/mandantentrennung-datenbankrolle.md: Befund, Sperrgrund (182
  unskalierte Zugriffe, Anmeldeweg), Handgriffe des Betreibers samt
  Kennwortsetzung, Freigabebedingung und Rueckweg; docs/README.md verweist
  darauf

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:03:25 +02:00
schalli a5f99e52e3 feat(quick-260909-dgj): Migrationsverbindung von Laufzeitverbindung trennen (Task 2/4)
- apps/api/scripts/migrate-and-start.sh: TESSERA_MIGRATE_DATABASE_URL fuer
  den Migrationsschritt, DATABASE_URL bleibt unveraendert fuer den
  Laufzeitschritt; leer/nicht gesetzt = bisheriges Verhalten
- Dockerfile kopiert scripts/ und ruft das Skript als CMD auf; exec statt
  &&-Verkettung, damit Signale den Node-Prozess erreichen
- docker-compose.yml/.prod.yml reichen TESSERA_MIGRATE_DATABASE_URL durch
  (leerer Vorgabewert); docker-compose.dev.yml/.ci.yml unveraendert
- .env.example erklaert beide Variablen mit Platzhaltern, Umstellung bleibt
  auskommentiert
- 5 Tests in start-script.spec.ts (rot vor dem Skript, jetzt gruen)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:00:45 +02:00
schalli b3375ae016 feat(quick-260909-dgj): Anwendungsrolle tessera_app ohne RLS-Umgehungsrecht (Task 1/4)
- Migration 20260909130000_rls_app_role legt tessera_app mit NOSUPERUSER
  NOBYPASSRLS wiederholbar an bzw. konvergiert eine vorhandene Rolle darauf
- Kein Kennwort im SQL, Datenbankname und Eigentuemer dynamisch gebildet
- 7 Tests in rls-app-role.spec.ts (rot vor der Migration, jetzt gruen)
- Rolle wird von niemandem benutzt — WINDOWS #18 bleibt offen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 09:59:21 +02:00
schalli 8cb2d43f88 docs(quick-260909-dgj): Plan fuer wirksame Mandantentrennung auf Datenbankebene
WINDOWS #18: RLS ist heute wirkungslos, weil die Anwendungsrolle
Superuser ist und BYPASSRLS traegt. Der Plan folgt der vorgegebenen
Reihenfolge — erst die Rolle ohne Umgehungsrecht, dann der Nachweis,
dann die Ausweitung auf die 16 fehlenden Tabellen.

Gemessen und im Plan festgehalten: 182 Zugriffe im API-Quelltext laufen
ueber den unskalierten Prisma-Client (nur 19 ueber tenantPrisma),
darunter der Anmeldeweg selbst. Ein Umlegen des Schalters wuerde die
Anwendung aussperren; der Plan bereitet die Umstellung deshalb vor und
misst sie, vollzieht sie aber nicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 09:55:59 +02:00
schalli fbb0d09e42 docs: Mandantentrennung greift nicht — Anwendungsrolle umgeht RLS (#18)
Beim Vorbereiten der RLS-Ausweitung gemessen: die API verbindet als Rolle
'tessera' (docker-compose.yml:33), und diese Rolle hat rolsuper=t und
rolbypassrls=t. PostgreSQL wendet Row-Level-Security auf solche Rollen nicht
an; FORCE ROW LEVEL SECURITY hilft nicht, das betrifft nur den
Tabelleneigentuemer.

Praktisch belegt statt hergeleitet: ohne gesetztes app.current_tenant liefert
SELECT count(*) FROM "Group" zwei Zeilen, obwohl die Policy bei NULL-Kontext
null liefern muesste.

Folge: alle sieben bisher mit RLS ausgestatteten Tabellen sind faktisch
ungeschuetzt. Die Trennung haengt allein am manuellen where-tenantId im Code.
Die Migration 20260804130918 beschreibt RLS als 'zweites Sicherheitsnetz' —
dieses Netz existiert derzeit nicht.

Das aendert die Reihenfolge der geplanten Arbeit: erst eine Anwendungsrolle
ohne Superuser- und BYPASSRLS-Recht, dann greifen die vorhandenen Policies,
erst danach lohnt das Ergaenzen fehlender Tabellen. Sonst baut man Regeln, die
nichts tun und Sicherheit vortaeuschen.

Kein akutes Risiko: Tessera laeuft intern mit einem einzigen Mandanten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 09:42:26 +02:00
schalli e6679eb4b1 docs(quick-260909-cx0): Dateisicherung und Versionsangaben — Plan und Bericht
Zwei kleine, unabhaengige Reparaturen in getrennten Commits (dab72eb, c807049).

Bemerkenswert an der Versionskorrektur: sie hat sechs Empfehlungen zutage
gefoerdert, die nie eingebaut wurden — Keycloak als Identitaetsanbieter, Redis,
TanStack Query, shadcn/ui, Playwright als Projektabhaengigkeit und Husky. Die
stehen jetzt in einem eigenen Abschnitt 'Recommended But Not Adopted', damit
niemand sie beim Lesen fuer vorhanden haelt. Ausserdem laeuft Vitest in den
beiden Anwendungen in unterschiedlichen Hauptfassungen (3.2.6 gegen 4.1.9).

Der Technik-Block in CLAUDE.md ist generiert. Eine Korrektur allein dort waere
bei der naechsten Regeneration still zurueckgeholt worden, deshalb zusaetzlich
ein Herkunftsvermerk im Block und eine datierte Hinweiszeile in der
Recherchedatei, deren Zahlen unveraendert bleiben.

Nebenbei zwei Verfaelschungen in STATE.md zurueckgesetzt, die Werkzeugaufrufe
hinterlassen hatten: eine Platzhalterzeile in der Quick-Task-Tabelle und
verfaelschte Fortschrittszahlen (3/82 statt 17/83).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 09:41:17 +02:00
schalli c80704957a docs(claude): Versionsangaben auf den installierten Stand bringen
- CLAUDE.md Technik-Block zeigt jetzt den installierten Stand statt der
  2026-06/07-Empfehlung: Next.js 15.5.19, Prisma 6.19.3, NestJS 11.1.27,
  Express 5.2.1, Node node:24-alpine (neu ergaenzt), Vitest je App
  (3.2.6 / 4.1.9), Docker/Compose als gemessene Wirtseigenschaft
- Authentifizierungs-Zeilen ersetzt: kein Identitaetsanbieter im Einsatz,
  sondern @nestjs/jwt, passport/@nestjs/passport, argon2, ldapts;
  @nestjs/passport-Zweckangabe korrigiert (Anmelde-/JWT-Strategien statt
  Modul-zu-Modul-API-Keys)
- Nie uebernommene Empfehlungen (Keycloak, Redis, TanStack Query, shadcn/ui,
  Playwright, Husky, lint-staged) sowie die beiden nicht aktualisierten
  Hauptversionen (Next.js, Prisma) in eigenem Abschnitt "Recommended But
  Not Adopted" statt in den Ist-Tabellen
- Herkunftsvermerk ergaenzt; Alternatives Considered/Version Pinning
  Strategy/Sources als historische Entscheidungslage gekennzeichnet;
  Multi-Tenancy Strategy verweist auf die tatsaechliche
  prisma-tenant.extension.ts
- .planning/research/STACK.md erhaelt eine Hinweiszeile, Zahlen darin
  unveraendert (datiertes Rechercheergebnis)
- Keine Abhaengigkeit aktualisiert (package.json/pnpm-lock.yaml
  unveraendert, per Gate geprueft)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 09:37:27 +02:00
schalli dab72eb1f9 fix(compose): user-files dauerhaft speichern und Betriebshandbuch nachziehen
- docker-compose.yml und docker-compose.prod.yml mounten /app/user-files
  im Dienst api auf ein neues benanntes Volume user-files (Eigentuemerschaft
  uid 1001 folgt aus dem Image, kein Bind-Mount)
- docker-compose.dev.yml bleibt unveraendert (Compose fuehrt Mount-Listen
  ueber das Ziel zusammen)
- Betriebshandbuch Kapitel 6: Speicher als dauerhaft beschrieben, Mount-Zeile
  woertlich zum Kopieren, Hinweis dass /opt/tessera/docker-compose.yml auf
  dem Server separat gepflegt werden muss (Deploy erreicht sie nicht)
- Betriebshandbuch Kapitel 7: Fehlerzeile zu verschwundenen Avataren/Exporten
  an den reparierten Repository-Stand angepasst

WINDOWS #17 bleibt offen, bis die Serverdatei manuell ergaenzt ist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 09:34:15 +02:00
schalli 7b49747c72 docs(quick-260909-cx0): Plan fuer Dateisicherung und Versionskorrektur
Zwei unabhaengige Aufgaben: user-files als benanntes Volume in beide
Repository-Compose-Dateien plus Betriebshandbuch Kapitel 6/7 (WINDOWS #17),
und die Technik-Tabelle in CLAUDE.md auf den installierten Stand bringen.
Beide Abnahmetore vor der Arbeit als rot gemessen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 09:31:51 +02:00
schalli 3501eb4dd1 docs: Anleitungen fuer Kollegen — Anwender, Administration, Betrieb, Entwicklung
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 54s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
Bisher gab es fuer Kollegen keine Dokumentation: im Projekt lagen nur das
CI/CD-Runbook und die Arbeitsanweisungen fuer die Entwicklung. Diese Luecke
schliessen vier Anleitungen plus eine Einstiegsseite unter docs/.

Alle vier wurden gegen den Quelltext geschrieben, nicht aus der Planung
abgeleitet, und anschliessend unabhaengig gegengeprueft: jede zitierte
Beschriftung ist woertlich aus de.json belegt, jede beschriebene Funktion im
Code nachgewiesen, alle Befehle und Pfade gegen die echten Compose-Dateien,
Dockerfiles und package.json-Skripte verifiziert. Die Gegenpruefung fand keine
falsche Aussage.

Die Einstiegsseite hebt die drei Punkte hervor, die in der Praxis am meisten
Zeit gekostet haben: Anmeldung ueber den Benutzernamen statt der E-Mail,
der Unterschied zwischen aktiviert und freigegeben, und dass ein blosses
'up -d' die laufenden Container nicht ersetzt.

Nebenbefund beim Schreiben des Betriebshandbuchs, als #17 im Ledger erfasst:
user-files/ ist in keiner Compose-Datei als Volume eingebunden — hochgeladene
Profilbilder und DKV-Exporte ueberleben kein --force-recreate. Noch ohne
Schaden, da bisher kein Nutzer ein Profilbild hinterlegt hat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 08:46:33 +02:00
schalli cfb85cd129 docs: Mandanten-Branding zurueckgestellt
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 53s
Tessera CI/CD / Build & Publish Images (push) Successful in 8s
Auf Entscheidung des Users vom 2026-09-09. Der Wunsch bleibt bestehen, hat aber
keine Dringlichkeit und wird nicht mehr als naechster Bau-Kandidat vorgelegt —
erst wieder aufgreifen, wenn der User ihn von sich aus nennt.

Damit ist derzeit kein naechster Schritt vorgemerkt: das Ledger ist leer, und
alle verbliebenen Punkte ruhen bewusst (Postfach-Test mangels Postfach,
Mandantentrennung solange Tessera intern laeuft, Lizenzpruefung bis alle Module
intern laufen, Branding ab jetzt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 08:32:12 +02:00
schalli ceb94e424b test(admin): WINDOWS #14 und #15 abgenommen — Ledger ohne offene Punkte
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 50s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
Abnahme im Browser auf alpha gegen das echte AD, nachdem der User die Container
neu erstellt hatte. Voraussetzungen gemessen statt angenommen: Container
tatsaechlich getauscht, Migration 20260909120000_user_email_optional angewandt,
User.email is_nullable = YES. Die Migration lief beim API-Start automatisch mit.

#15 — der Sync meldet jetzt "Erstellt: 4" statt vier stiller Fehlschlaege. Die
kollidierenden Konten sind angelegt und aktiv, ohne Adresse; mbuntz behaelt als
erster Anspruch seine. Der Bericht zeigt zwei verstaendliche deutsche
Abschnitte statt Prisma-Text und englischer Meldungen. Die beiden Folgestellen
vertragen Konten ohne Adresse: die Benutzerliste zeigt einen Gedankenstrich,
und die Suche im Mitglieder-Dialog liefert die Konten sauber statt
abzustuerzen — das war der eigentliche Fallstrick.

#14 — die Matrix-Suche laesst die nicht getroffene Achse stehen. Alle vier
Faelle geprueft: Modulname behaelt die Gruppenspalten, Gruppenname behaelt die
Modulzeile, der AD-Name findet dieselbe Spalte wie der interne Name
(Regression #6c intakt), und eine Suche ohne Treffer erklaert sich in einem
verstaendlichen Satz.

Am Active Directory wurde nichts veraendert; es wurde nur gelesen. Der fuer die
Regressionsprobe gesetzte interne Name ist wieder geleert.

Ledger danach: 0 offen, 15 behoben, 1 zurueckgestellt (#12 — es gibt intern
kein Postfach fuer Ausschreibungs-Alarme).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 08:26:15 +02:00
schalli efcf11c988 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
2026-09-09 08:02:55 +02:00
schalli 21670462dd feat(quick-260909-ab3): verstaendlicher Sync-Bericht, Konten ohne Adresse in der UI
- admin/ldap/page.tsx: SyncResult um emailConflicts/skippedNoLogin/
  entryFailures erweitert; drei neue Berichtsabschnitte (Bernstein fuer
  Kollisionen, neutral fuer fehlenden Anmeldenamen, destruktiv fuer
  unerwartete Fehler) — kein englischer Techniktext, kein roher
  Datenbank-Wortlaut mehr im Bericht
- GroupMembersModal.tsx: TenantUser.email optional, Suchvergleich gegen
  leere Adresse abgesichert (WINDOWS #15) — die Mitgliedersuche stuerzt
  nicht mehr ab, sobald ein Konto ohne Adresse existiert
- users/page.tsx: User.email optional, Formular faellt auf leere
  Zeichenkette zurueck, Tabellenzelle zeigt einen Gedankenstrich;
  UserFormData.email bleibt bei Handanlage Pflicht
- Neue Sprachschluessel unter admin.ldap.sync in de.json/en.json,
  Umlaut- und Sprachschluessel-Gate bestaetigt gruen (kein neues
  Allowlist-Wort noetig)
- Neuer Testfall in groups-page.test.tsx vorab gegen den unveraenderten
  Bestand rot gelaufen; Abweichung von der Plan-Fixture dokumentiert
  (Kurzfassung: ein einzelnes Konto mit passendem Benutzernamen loest die
  Kollision wegen OR-Kurzschlussauswertung nie aus, ein zweites,
  nicht-treffendes Konto ohne Adresse schon — Vollfassung im SUMMARY)
- 651/651 API- und 233/233 Web-Tests gruen, beide Typpruefungen sauber

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 07:54:32 +02:00
schalli 1222951af6 fix(quick-260909-ab3): kollidierende AD-Konten werden angelegt, nur ohne Adresse
- User.email auf optional gestellt (Migration geschrieben, NICHT
  ausgefuehrt); Eindeutigkeitsindex unangetastet, NULL bleibt in Postgres
  je verschieden
- Neuer Kollisionsentscheider (resolveEmailForWrite) in ldap.service.ts:
  eine bereits vergebene Adresse wird nie umgehaengt (T-Q3-01) — das
  zuerst angelegte Konto behaelt sie, jedes weitere Konto entsteht ohne
  Adresse (gesperrte Nutzerentscheidung 2026-09-09, WINDOWS #15)
- Entscheider in upsertMappedUser (Sync) UND importUsersByDn (Handimport)
  verdrahtet, damit der zweite Anlageweg nicht als Luecke bestehen bleibt
- LdapSyncResult um emailConflicts/skippedNoLogin/entryFailures erweitert;
  rohe ORM-Ausnahmetexte gehen nur noch an logger.error, nie in den
  Bericht (T-Q3-02)
- UserService.create nimmt die Adresse optional entgegen; Tender-Digest
  und Instant-Alert ueberspringen Empfaenger ohne Adresse (continue)
- Fuenf neue Testfaelle vorab gegen den unveraenderten Bestand rot
  gelaufen (erwartete Ursachen bestaetigt); 651/651 API-Tests gruen,
  prisma validate und type-check sauber

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 07:48:37 +02:00
schalli e8c2411e60 fix(quick-260909-ab3): Matrix-Suche filtert nur noch die getroffene Achse
- Gemeinsamer useMemo ermittelt moduleHits/groupHits getrennt; trifft der
  Suchbegriff nur eine Achse, bleibt die andere vollstaendig sichtbar
  (WINDOWS #14) statt leerzulaufen
- Regressionsschutz fuer WINDOWS #6c erhalten: Spaltensuche findet eine
  importierte Gruppe weiterhin unter internem UND AD-Namen
- Neue Meldung adminModules.grants.noSearchResults, wenn ein Begriff weder
  Modul noch Gruppe trifft — Tabelle wird dann nicht gerendert
- Vier neue Testfaelle in grants-matrix.test.tsx vorab gegen den
  unveraenderten Bestand rot gelaufen (erwartete Ursachen bestaetigt)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 07:43:21 +02:00
schalli dbeab8c38a docs(quick-260909-ab3): Plan fuer Matrix-Suche und Sync-Meldungen
WINDOWS #14 (Suche leert die jeweils andere Achse der Freigaben-Matrix) und
WINDOWS #15 (roher Prisma-/englischer Techniktext im AD-Sync, vier Konten
wegen geteilter E-Mail-Adresse nie importiert) als drei getrennte Tasks.

Gesperrte Nutzerentscheidung vom 2026-09-09: kollidierende Konten werden
angelegt, nur ohne Adresse. Dafuer wird User.email optional — Migration wird
geschrieben, nicht ausgefuehrt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 07:38:33 +02:00
schalli f1ecbf542e test(tender-radar): Verbindungstest abgenommen (#16 zu), Postfach-Test zurueckgestellt
Abnahme im Browser auf alpha gegen den echten Exchange owa.ctl.de, nachdem die
Container mit --force-recreate getauscht waren. Der blosse pull genuegte nicht:
die neue Route fehlte im laufenden Container und war im gezogenen Image
vorhanden — hart belegt statt vermutet.

Alle vier Punkte bestanden:
- gespeicherte Konfiguration mit LEEREM Passwortfeld meldet "Verbindung
  erfolgreich" — belegt zugleich den Rueckgriff auf die gespeicherten
  verschluesselten Zugangsdaten
- absichtlich falscher Server meldet "Verbindung fehlgeschlagen: getaddrinfo
  ENOTFOUND owa-gibtsnicht.ctl.de"
- keine Zugangsdaten im API-Protokoll (die Treffer einer Mustersuche waren
  Routennamen wie /auth/reset-password)
- der Test schreibt nichts; die gespeicherte Konfiguration blieb unveraendert

Nebenbefund mit Gewicht: das Protokoll zeigt eine echte Antwort von Exchange
2019 (ServerVersionInfo 15.2) samt aufgeloester FolderId. Der in Plan 14-03 als
fragil markierte, handgeschriebene NTLM/SOAP-Weg ist damit erstmals gegen einen
echten Server gemessen — bisher lag nur gemocktes httpntlm.post vor.

WINDOWS #12 zurueckgestellt (waived): es gibt intern kein Postfach, in das
Ausschreibungs-Alarme hereinkommen. Ein frueheres Missverstaendnis hatte eines
angenommen. Nachzuholen, sobald ein solches Postfach existiert; offen ist dann
allein das Einlesen einer echten Alarm-Mail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 07:21:36 +02:00
schalli d354362a61 docs(quick-260907-let): Verbindungstest fuer das Postfach — Plan, Bericht, Verifikation
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 50s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m29s
Abschluss-Dokumentation zum nachgeruesteten Verbindungstest (WINDOWS #16).

Verifikation unabhaengig nachgemessen, nicht aus dem Ausfuehrungsbericht
uebernommen: 646/646 API-Tests, 228/228 Web-Tests, beide Typpruefungen
sauber, und das Sprachschluessel-Gate ist von rot ("de fehlt testConnection")
auf gruen gewechselt. Sicherheitsseitig geprueft: die DTO traegt kein
Eigentuemer-Feld, der Handler zieht userId ausschliesslich aus dem
Auth-Kontext (eigener IDOR-Test mit Koeder-userId), Rueckfall auf gespeicherte
Zugangsdaten sucht nur ueber denselben userId, und in den geaenderten Dateien
findet sich keine Log- oder Antwortstelle mit Zugangsdaten.

Status bleibt human_needed: die Browser-Abnahme gegen ein echtes Postfach
steht aus und braucht einen Neubau durch den User. WINDOWS #16 bleibt
deshalb offen.

Ausserdem workflow.use_worktrees auf false: origin/HEAD ist in diesem Repo
nicht aufloesbar, der Basis-Check des Workflows verlangt daher selbst
sequenzielle Ausfuehrung ("fork-ref-unknown"). Ein isolierter Arbeitsbaum
wuerde von einem veralteten Stand abzweigen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-07 15:50:49 +02:00
schalli c4db3b2e65 feat(quick-260907-let): Knopf "Verbindung testen" im Postfach-Formular
- testEmailConnection im API-Klienten, POST email-config/test
- EmailAlertConfigForm: Testknopf vor Speichern, Wartezustand, gruene/rote
  Rueckmeldung, Formularaenderung raeumt vorherige Rueckmeldung weg
- Beschreibungsblock der Komponente korrigiert (Knopf existiert jetzt)
- Vier neue Schluessel unter tenderRadar.emailAlerts in de/en
- vi.mock-Fabriken in EmailAlertConfigForm.test.tsx und my-sources.test.tsx
  um testEmailConnection erweitert, zwei neue Testfaelle

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-07 15:43:51 +02:00
schalli 3bf550bc65 feat(quick-260907-let): Verbindungstest fuer Postfach-Endpunkt im API
- TenderEmailConfigService.testConnection(userId, dto) mit Rueckfall auf
  gespeicherte, entschluesselte Zugangsdaten bei leeren Feldern
- TendersController: POST email-config/test, userId aus Auth-Kontext,
  deklariert vor @Get(':id')
- Beide Provider (ImapProvider/ExchangeInboxProvider) optional angehaengt,
  bestehende 2-Arg-Konstruktoraufrufe bleiben typkorrekt
- Reihenfolge-Waechter und IDOR-Testfall ergaenzt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-07 15:40:28 +02:00
Schalli 5da58ad182 docs(quick-260907-let): Plan fuer Postfach-Verbindungstest im Ausschreibungs-Radar
Schliesst WINDOWS #16. Zwei Tasks: Endpunkt POST email-config/test
(Dienst + Route + Reihenfolge-Waechter) und Knopf 'Verbindung testen'
im Formular unter Meine Quellen samt Beschriftungen in beiden Sprachen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-07 15:33:54 +02:00