docs(quick-260914-m97): Fehler-melden-Knopf abgeschlossen, verifiziert 9/9 und im Browser bewiesen — Zusammenfassung, Verifikation, Aktenstand, Ledger #38 fixed
Tessera CI/CD / Build & Publish Images (push) Successful in 2m42s
Tessera CI/CD / Lint & Type Check (push) Successful in 46s
Tessera CI/CD / Tests (push) Successful in 53s

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-14 17:18:28 +02:00
parent 77117de3d0
commit e5098603b4
4 changed files with 570 additions and 10 deletions
+8 -7
View File
@@ -4,11 +4,11 @@ milestone: v1.2
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: "2026-09-14: Quick 260914-ku1 abgeschlossen (3 Commits cdb571c/9731501/ea6aa99 gepusht, CI-Lauf 297 success, :beta-Abbilder mit ea6aa99 beta); offen: Fehler-melden-Knopf, danach Erstfreigabe v1.0.0 (Zweig live + Tag), Server-Handgriffe durch den User" stopped_at: "2026-09-14: Quick 260914-m97 Fehler-melden-Knopf ausgefuehrt (4 Commits 54121c1/60b0ee8/b41be21/77117de gepusht, CI-Lauf 299 nach Rerun success, :beta-Abbilder mit 77117de beta); offen: Browser-Check mit mailhog durch den Verifizierer, danach Erstfreigabe v1.0.0"
last_updated: "2026-09-14T13:53:23.837Z" last_updated: "2026-09-14T15:08:28.980Z"
last_activity: 2026-09-14 last_activity: 2026-09-14
last_activity_desc: Quick 260910-jab — drei zu kurz greifende RLS-Regeln geschlossen (GroupMembership beide Seiten, ModuleGrant beide Ziele, TenderRssFeedSource Lese-/Schreibsplit), listForUser gebunden, Aktenstand kohaerent last_activity_desc: Quick 260910-jab — drei zu kurz greifende RLS-Regeln geschlossen (GroupMembership beide Seiten, ModuleGrant beide Ziele, TenderRssFeedSource Lese-/Schreibsplit), listForUser gebunden, Aktenstand kohaerent
state_head: ea6aa995b27de1dfa9b557c06df9fa3bbcdd8ea3 state_head: 77117de3d0f1bb82df7b659f46fe26244ca6f164
progress: progress:
total_phases: 17 total_phases: 17
completed_phases: 15 completed_phases: 15
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
Phase: 17 (eigene-ausschreibungs-quellen-je-nutzer) — VERIFIED / passed Phase: 17 (eigene-ausschreibungs-quellen-je-nutzer) — VERIFIED / passed
Plan: 3 of 3 Plan: 3 of 3
Status: Phase abgeschlossen und im Browser gegengeprueft — bereit fuer /gsd-ship Status: Phase abgeschlossen und im Browser gegengeprueft — bereit fuer /gsd-ship
Last activity: 2026-09-14 - Zwei Auslieferungskanaele + Versionsstempel (260914-ku1, verifiziert 8/8); davor Etappe 3c (260914-eym) und WINDOWS #29 (260914-ebg). Naechster Schritt: Fehler-melden-Knopf, danach Zweig live + Tag v1.0.0 Last activity: 2026-09-14 - Fehler-melden-Knopf (260914-m97, verifiziert 9/9 + Browser), Zwei Kanaele + Versionsstempel (260914-ku1, 8/8), Etappe 3c (260914-eym, 9/9), WINDOWS #29 (260914-ebg, 6/6). Naechster Schritt: Zweig live + Tag v1.0.0
Progress: [██████████] 100% Progress: [██████████] 100%
@@ -403,6 +403,7 @@ None yet.
| 260914-ebg | **WINDOWS #29 geschlossen — Zielrollen-Riegel in `UserController.update()`/`remove()`.** Ein ADMIN kann den SUPER_ADMIN seines Mandanten nicht mehr aendern (Kennwort, isActive, Rolle, Anmeldename, E-Mail) oder loeschen; Riegel nach der Mandantengrenze, vor der Rollenzuweisungs-Pruefung (Vorlage `AuthService.adminResetPassword`, T-FH9-04). Acht neue Spec-Tests (8 -> 16), Baseline 1020 -> 1028 Tests / 62 Dateien, Falsifizierung durch Rueckbau `Tests 2 failed | 14 passed (16)` (Test 9/13), unabhaengig vom Verifizierer wiederholt. Kopfkommentar `adminResetPassword` nachgezogen (T-FH9-05 nicht mehr offen). Ledger 16 offen / 1 zurueckgestellt / 19 geschlossen / 36 gesamt: #29 fixed, NEU #35 (Biome-Konfiguration im Bestand nicht lauffaehig, `pnpm lint` Leerlauf) und #36 (Admin-Frontend verschluckt 403 still). Verifiziert 6/6, gepusht. | 2026-09-14 | 759ea3b,63f9df0,70d007b | [260914-ebg-windows-29-schliessen-rechteausweitung-a](./quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/) | | 260914-ebg | **WINDOWS #29 geschlossen — Zielrollen-Riegel in `UserController.update()`/`remove()`.** Ein ADMIN kann den SUPER_ADMIN seines Mandanten nicht mehr aendern (Kennwort, isActive, Rolle, Anmeldename, E-Mail) oder loeschen; Riegel nach der Mandantengrenze, vor der Rollenzuweisungs-Pruefung (Vorlage `AuthService.adminResetPassword`, T-FH9-04). Acht neue Spec-Tests (8 -> 16), Baseline 1020 -> 1028 Tests / 62 Dateien, Falsifizierung durch Rueckbau `Tests 2 failed | 14 passed (16)` (Test 9/13), unabhaengig vom Verifizierer wiederholt. Kopfkommentar `adminResetPassword` nachgezogen (T-FH9-05 nicht mehr offen). Ledger 16 offen / 1 zurueckgestellt / 19 geschlossen / 36 gesamt: #29 fixed, NEU #35 (Biome-Konfiguration im Bestand nicht lauffaehig, `pnpm lint` Leerlauf) und #36 (Admin-Frontend verschluckt 403 still). Verifiziert 6/6, gepusht. | 2026-09-14 | 759ea3b,63f9df0,70d007b | [260914-ebg-windows-29-schliessen-rechteausweitung-a](./quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/) |
| 260914-eym | **Etappe 3c — Systemkontext fuer die Hintergrunddienste.** Migration `20260914120000_rls_system_context_read`: `is_system_context()`, fuenf permissive `system_read_policy ... FOR SELECT` (DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch); `forSystem(prisma)` in Array-Form mit ausdruecklichem Zuruecksetzen von Mandant/Benutzer, `forTenant()` setzt `app.system_context` zurueck (kein Erben, gemessen). Sechs Faelle: DKV-Planer einmal-abfragen-viele-bedienen (Auftrag je Mandant, WINDOWS #21 fixed); Mail-Transport je Versand aus der SmtpConfig des Empfaenger-Mandanten mit unveraenderter Umgebungs-Rueckfallkette, Startpfad und Mailer-Fabrik entfallen (WINDOWS #30 fixed, SmtpConfig ohne Systemregel); ldap `getAllActiveConfigs()` und Boot-Nachverschluesselung lesen ueber Systemkontext, schreiben je Mandant gebunden; tender-digest Kandidaten und tender-matching Suchprofile ueber Systemkontext, Schleifen gebunden; admin-seed nur dokumentiert (Tenant ohne Regel). Detektor mit fuenfter Erkennungsform `forSystem(` und exakter Erlaubnisliste (falsifiziert: Fremddatei 1 rot, Zweitaufruf 2 rot). Werkzeug 203 -> 253 (`runSystemContextChecks`: ungebunden 0 / System beide Mandanten / Schreiben abgewiesen 42501 bzw. count 0 / kein Erben / pg_policies 34, 5x SELECT). Rueckbau (a) 5 rot mit gelungenem Insert, (b) 1 rot, (c1) 253 gruen + (c2) 5 rot, (d) 2/3 rot. Tests 1028 -> 1054 / 62 -> 64 Dateien, tsc 0, 29 Dateien gegen 5e0e408, Schalter AUS (Compose/.env/Schema/Lockfile unveraendert). Klassifikation 61/179/5, 72 Paare, sechs Zeilen `system-gebunden`; Kritikschrift (y1)-(y5); Auftrag 3c Erledigt. Ledger 15 offen / 1 zurueckgestellt / 21 geschlossen / 37 gesamt; NEU #37 (prozessweiter Single-Flight-Riegel `processInbox`). Verifiziert 9/9, gepusht. | 2026-09-14 | 3d64567,6e2a641,939c812 | [260914-eym-mandantentrennung-etappe-3c-systemkontex](./quick/260914-eym-mandantentrennung-etappe-3c-systemkontex/) | | 260914-eym | **Etappe 3c — Systemkontext fuer die Hintergrunddienste.** Migration `20260914120000_rls_system_context_read`: `is_system_context()`, fuenf permissive `system_read_policy ... FOR SELECT` (DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch); `forSystem(prisma)` in Array-Form mit ausdruecklichem Zuruecksetzen von Mandant/Benutzer, `forTenant()` setzt `app.system_context` zurueck (kein Erben, gemessen). Sechs Faelle: DKV-Planer einmal-abfragen-viele-bedienen (Auftrag je Mandant, WINDOWS #21 fixed); Mail-Transport je Versand aus der SmtpConfig des Empfaenger-Mandanten mit unveraenderter Umgebungs-Rueckfallkette, Startpfad und Mailer-Fabrik entfallen (WINDOWS #30 fixed, SmtpConfig ohne Systemregel); ldap `getAllActiveConfigs()` und Boot-Nachverschluesselung lesen ueber Systemkontext, schreiben je Mandant gebunden; tender-digest Kandidaten und tender-matching Suchprofile ueber Systemkontext, Schleifen gebunden; admin-seed nur dokumentiert (Tenant ohne Regel). Detektor mit fuenfter Erkennungsform `forSystem(` und exakter Erlaubnisliste (falsifiziert: Fremddatei 1 rot, Zweitaufruf 2 rot). Werkzeug 203 -> 253 (`runSystemContextChecks`: ungebunden 0 / System beide Mandanten / Schreiben abgewiesen 42501 bzw. count 0 / kein Erben / pg_policies 34, 5x SELECT). Rueckbau (a) 5 rot mit gelungenem Insert, (b) 1 rot, (c1) 253 gruen + (c2) 5 rot, (d) 2/3 rot. Tests 1028 -> 1054 / 62 -> 64 Dateien, tsc 0, 29 Dateien gegen 5e0e408, Schalter AUS (Compose/.env/Schema/Lockfile unveraendert). Klassifikation 61/179/5, 72 Paare, sechs Zeilen `system-gebunden`; Kritikschrift (y1)-(y5); Auftrag 3c Erledigt. Ledger 15 offen / 1 zurueckgestellt / 21 geschlossen / 37 gesamt; NEU #37 (prozessweiter Single-Flight-Riegel `processInbox`). Verifiziert 9/9, gepusht. | 2026-09-14 | 3d64567,6e2a641,939c812 | [260914-eym-mandantentrennung-etappe-3c-systemkontex](./quick/260914-eym-mandantentrennung-etappe-3c-systemkontex/) |
| 260914-ku1 | **Zwei Auslieferungskanaele und Versionsstempel.** `main` = Beta (Etiketten `beta` + `latest`), Tag `vX.Y.Z` = Live (Etiketten `live` + `vX.Y.Z`), Zweig `live` ohne Tag nur geprueft — Entscheidung in `.gitea/scripts/publish-images.sh` (`--print-plan`), CI-Trigger `branches: [main, live]` + `tags: [v*]`, `fetch-depth: 0`. Versionsstempel `APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME` als Build-Args in beide Dockerfiles (web zur Bauzeit als `NEXT_PUBLIC_APP_*`, api als Laufzeit-ENV; Vorgabe `dev`). `GET /health/version` liefert name/version/channel/commit/buildTime, Startlog `Tessera API vX (channel) commit`. Web: `app-version.ts`, `AppVersionBadge` in `sidebar.tsx` (sidebar-footer.tsx ist seit ba02b25 toter Code). `docker-compose.prod.yml`: `image: ...:${IMAGE_TAG:-beta}`. Betriebshandbuch Kapitel 9 (Zwei Kanaele, Freigabe, Hotfix ohne Datenbankaenderung, neuer Live-Server), ci-cd-setup.md auf gemessenen Stand. Falsifiziert: Build mit `v9.9.9-test live` -> Stempel in dist und Web-Bundle, ohne Args `dev`. Echter CI-Lauf 297 gruen (5:18 min), Abbilder `beta`/`latest` tragen `ea6aa99 beta`. Tests API 1054 -> 1060 / 64 -> 65 Dateien, Web 233 -> 243 / 38 -> 40, tsc 0, 20 Dateien gegen 6c19451. Offen: Zweig `live` + Tag `v1.0.0` nach dem Fehler-melden-Knopf anlegen; Handgriffe fuer den User (IMAGE_TAG je Server) im SUMMARY. Verifiziert 8/8, gepusht. | 2026-09-14 | cdb571c,9731501,ea6aa99 | [260914-ku1-zwei-auslieferungskanaele-beta-auf-main-](./quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/) | | 260914-ku1 | **Zwei Auslieferungskanaele und Versionsstempel.** `main` = Beta (Etiketten `beta` + `latest`), Tag `vX.Y.Z` = Live (Etiketten `live` + `vX.Y.Z`), Zweig `live` ohne Tag nur geprueft — Entscheidung in `.gitea/scripts/publish-images.sh` (`--print-plan`), CI-Trigger `branches: [main, live]` + `tags: [v*]`, `fetch-depth: 0`. Versionsstempel `APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME` als Build-Args in beide Dockerfiles (web zur Bauzeit als `NEXT_PUBLIC_APP_*`, api als Laufzeit-ENV; Vorgabe `dev`). `GET /health/version` liefert name/version/channel/commit/buildTime, Startlog `Tessera API vX (channel) commit`. Web: `app-version.ts`, `AppVersionBadge` in `sidebar.tsx` (sidebar-footer.tsx ist seit ba02b25 toter Code). `docker-compose.prod.yml`: `image: ...:${IMAGE_TAG:-beta}`. Betriebshandbuch Kapitel 9 (Zwei Kanaele, Freigabe, Hotfix ohne Datenbankaenderung, neuer Live-Server), ci-cd-setup.md auf gemessenen Stand. Falsifiziert: Build mit `v9.9.9-test live` -> Stempel in dist und Web-Bundle, ohne Args `dev`. Echter CI-Lauf 297 gruen (5:18 min), Abbilder `beta`/`latest` tragen `ea6aa99 beta`. Tests API 1054 -> 1060 / 64 -> 65 Dateien, Web 233 -> 243 / 38 -> 40, tsc 0, 20 Dateien gegen 6c19451. Offen: Zweig `live` + Tag `v1.0.0` nach dem Fehler-melden-Knopf anlegen; Handgriffe fuer den User (IMAGE_TAG je Server) im SUMMARY. Verifiziert 8/8, gepusht. | 2026-09-14 | cdb571c,9731501,ea6aa99 | [260914-ku1-zwei-auslieferungskanaele-beta-auf-main-](./quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/) |
| 260914-m97 | **Fehler-melden-Knopf.** Kaefer-Knopf in der Kopfzeile: Bildschirmfoto VOR dem Dialog (`html-to-image` 1.11.13, laengste Kante 1600 px, `computeCaptureSize`), Dialog mit Vorschau, Haekchen und "Was ist passiert?"; Fehlerpuffer (Ringpuffer 20: window.onerror, unhandledrejection, console.error, fehlgeschlagene fetch-Antworten — keine Ruempfe/Cookies/Tokens); `POST /bug-reports` als Multipart (FileInterceptor 4 MiB -> 413, PNG-Signatur -> 400, kein Empfaenger -> 409, Drossel 5/10 min -> 429, Versandfehler -> 502; Mandant/Benutzer nur aus der Sitzung); E-Mail mit PNG-Anhang und Kontext (URL, Web-/API-Version+Kanal+Commit, Browser, Fenster, Zeitpunkt, Benutzer, letzte Fehler) ueber `MailService.sendBugReport` (Anhaenge; Kennwort-Reset bleibt verschluckend). Empfaenger: neue nullable Spalte `SmtpConfig.bugReportRecipient` (Migration `20260914170000`), Feld "Fehlermeldungen an" unter Administrator -> SMTP, Rueckfall `TESSERA_BUGREPORT_TO` (docker-compose.prod.yml). Handbuecher Anwender/Administration/Betrieb. Tests API 1060 -> 1076 / 67 Dateien, Web 243 -> 260 / 43 Dateien, tsc 0, `--frozen-lockfile` 0, 35 Dateien gegen 5c42c55, vier Commits. CI-Lauf 299 gruen (zweiter Versuch, erster scheiterte an Gitea-DB). Browser-Beweis durch den Orchestrator: E-Mail mit 85-KB-PNG (ohne Dialog, OKLCH korrekt) in mailhog, 409-Pfad im Dialog. Ledger #38 (Rule-1-Fix `@Expose()`) als fixed. Verifiziert 9/9 + Browser, gepusht. | 2026-09-14 | 54121c1,60b0ee8,b41be21,77117de | [260914-m97-fehler-melden-knopf-bildschirmfoto-der-a](./quick/260914-m97-fehler-melden-knopf-bildschirmfoto-der-a/) |
## Deferred Items ## Deferred Items
@@ -444,8 +445,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
## Session Continuity ## Session Continuity
Last session: 2026-09-14T13:53:23.147Z Last session: 2026-09-14T15:08:28.744Z
Resumed: 2026-09-14 — Sitzung ueber /gsd-resume-work fortgesetzt; #29 und 3c als /gsd-quick --validate mit voller Kette durchgefuehrt. Resumed: 2026-09-14 — Sitzung ueber /gsd-resume-work fortgesetzt; #29 und 3c als /gsd-quick --validate mit voller Kette durchgefuehrt.
Stopped at: 2026-09-14: Quick 260914-ku1 abgeschlossen (3 Commits cdb571c/9731501/ea6aa99 gepusht, CI-Lauf 297 success, :beta-Abbilder mit ea6aa99 beta); offen: Fehler-melden-Knopf, danach Erstfreigabe v1.0.0 (Zweig live + Tag), Server-Handgriffe durch den User Stopped at: 2026-09-14: Quick 260914-m97 Fehler-melden-Knopf ausgefuehrt (4 Commits 54121c1/60b0ee8/b41be21/77117de gepusht, CI-Lauf 299 nach Rerun success, :beta-Abbilder mit 77117de beta); offen: Browser-Check mit mailhog durch den Verifizierer, danach Erstfreigabe v1.0.0
Resume file: None Resume file: None
Last activity: 2026-09-14 - Completed quick task 260914-ku1: Zwei Auslieferungskanaele (beta/live), Versionsstempel, Versionsanzeige, Betriebshandbuch Kapitel 9 Last activity: 2026-09-14 - Completed quick task 260914-m97: Fehler-melden-Knopf mit Bildschirmfoto per E-Mail, Empfaenger unter Administrator -> SMTP
+17 -3
View File
@@ -2,9 +2,9 @@
schema_version: 1 schema_version: 1
open_count: 15 open_count: 15
waived_count: 1 waived_count: 1
fixed_count: 21 fixed_count: 22
total_count: 37 total_count: 38
last_updated: 2026-09-14T09:51:24.295Z last_updated: 2026-09-14T15:17:38.808Z
--- ---
# Broken Windows Ledger # Broken Windows Ledger
@@ -52,6 +52,7 @@ last_updated: 2026-09-14T09:51:24.295Z
| 35 | quick-260914-ebg | deviation | biome.json | | Biome ist im Bestand nicht lauffaehig: biome.json traegt den in Biome 2.5.0 unbekannten Schluessel organizeImports (gehoert unter assist), Biome bricht bei jedem Aufruf mit Konfigurationsfehler ab; zusaetzlich fehlt javascript.parser.unsafeParameterDecoratorsEnabled, ohne den jeder NestJS-Parameter-Dekorator ein Parse-Fehler ist (17 allein in user.controller.ts). Der CI-Schritt Lint ruft pnpm lint = turbo lint, keine App hat ein lint-Skript - der Schritt ist ein Leerlauf, der gruen meldet. CLAUDE.md und docs/anleitung-entwicklung.md beschreiben Biome als aktives Werkzeug. Gemessen 260914-ebg; das dortige Gate lief mit einer Ersatzkonfiguration im Scratchpad, relativ zur Baseline (0 Fehler, Warnungen je Datei 22/25/20, alle noExplicitAny-Familie; biome format ebenfalls unsauber, Anfuehrungszeichen-Stil). Zu entscheiden: biome.json reparieren (organizeImports nach assist, Parser-Schalter, quoteStyle single) und ein lint-Skript je App anlegen, dann die Warnungen in einem eigenen Durchlauf abbauen oder als Regelabschaltung begruenden. | open | | 2026-09-14T08:38:04.079Z | | | 35 | quick-260914-ebg | deviation | biome.json | | Biome ist im Bestand nicht lauffaehig: biome.json traegt den in Biome 2.5.0 unbekannten Schluessel organizeImports (gehoert unter assist), Biome bricht bei jedem Aufruf mit Konfigurationsfehler ab; zusaetzlich fehlt javascript.parser.unsafeParameterDecoratorsEnabled, ohne den jeder NestJS-Parameter-Dekorator ein Parse-Fehler ist (17 allein in user.controller.ts). Der CI-Schritt Lint ruft pnpm lint = turbo lint, keine App hat ein lint-Skript - der Schritt ist ein Leerlauf, der gruen meldet. CLAUDE.md und docs/anleitung-entwicklung.md beschreiben Biome als aktives Werkzeug. Gemessen 260914-ebg; das dortige Gate lief mit einer Ersatzkonfiguration im Scratchpad, relativ zur Baseline (0 Fehler, Warnungen je Datei 22/25/20, alle noExplicitAny-Familie; biome format ebenfalls unsauber, Anfuehrungszeichen-Stil). Zu entscheiden: biome.json reparieren (organizeImports nach assist, Parser-Schalter, quoteStyle single) und ein lint-Skript je App anlegen, dann die Warnungen in einem eigenen Durchlauf abbauen oder als Regelabschaltung begruenden. | open | | 2026-09-14T08:38:04.079Z | |
| 36 | quick-260914-ebg | deviation | apps/web/src/app/(portal)/admin/users/page.tsx | | handleSubmit und handleDelete pruefen nur res.ok ohne else-Zweig und fangen mit leerem catch - ein 403 der API fuehrt zu keiner sichtbaren Reaktion (Formular bleibt offen, Loeschdialog bleibt stehen, keine Meldung). Bestehendes Verhalten fuer alle 403-Wege (fremder Mandant, Selbstloeschung); seit 260914-ebg (WINDOWS #29) ist der Fall fuer einen ADMIN im Alltag erreichbar, weil die SUPER_ADMIN-Zeile in der eigenen Benutzerliste steht und Aendern/Loeschen darauf jetzt 403 liefert. Familie der still verschluckten Antworten (#28, #32). Frontend von 260914-ebg NICHT geaendert (ausserhalb der Erlaubnisliste). Zu schliessen: Fehlermeldung aus dem Antwortrumpf anzeigen und die Aktionsknoepfe fuer SUPER_ADMIN-Zeilen einem ADMIN gar nicht erst anbieten. | open | | 2026-09-14T08:38:12.619Z | | | 36 | quick-260914-ebg | deviation | apps/web/src/app/(portal)/admin/users/page.tsx | | handleSubmit und handleDelete pruefen nur res.ok ohne else-Zweig und fangen mit leerem catch - ein 403 der API fuehrt zu keiner sichtbaren Reaktion (Formular bleibt offen, Loeschdialog bleibt stehen, keine Meldung). Bestehendes Verhalten fuer alle 403-Wege (fremder Mandant, Selbstloeschung); seit 260914-ebg (WINDOWS #29) ist der Fall fuer einen ADMIN im Alltag erreichbar, weil die SUPER_ADMIN-Zeile in der eigenen Benutzerliste steht und Aendern/Loeschen darauf jetzt 403 liefert. Familie der still verschluckten Antworten (#28, #32). Frontend von 260914-ebg NICHT geaendert (ausserhalb der Erlaubnisliste). Zu schliessen: Fehlermeldung aus dem Antwortrumpf anzeigen und die Aktionsknoepfe fuer SUPER_ADMIN-Zeilen einem ADMIN gar nicht erst anbieten. | open | | 2026-09-14T08:38:12.619Z | |
| 37 | quick-260914-eym | deviation | apps/api/src/dkv/dkv.service.ts | | Der Single-Flight-Riegel processing in DkvService.processInbox ist EIN prozessweites Boolean, nicht je Mandant. Seit 260914-eym laeuft je aktivem Mandanten ein eigener Cron-Auftrag (dkv-inbox-poll:<tenantId>); ueberschneiden sich zwei Ticks verschiedener Mandanten, bricht der zweite still ab (Warnzeile 'already processing') und der Mandant wartet bis zum naechsten Intervall - kein Datenverlust, Verzoegerung; mit EINEM Mandanten unveraendert. Der Tick blieb in 3c laut Auftrag unangetastet (T-EYM-09, accept mit Aufzeichnung). Zu schliessen: Riegel je Mandant (Set<tenantId>) mit Test 'zwei Mandanten gleichzeitig, beide werden bedient'. | open | | 2026-09-14T09:51:24.295Z | | | 37 | quick-260914-eym | deviation | apps/api/src/dkv/dkv.service.ts | | Der Single-Flight-Riegel processing in DkvService.processInbox ist EIN prozessweites Boolean, nicht je Mandant. Seit 260914-eym laeuft je aktivem Mandanten ein eigener Cron-Auftrag (dkv-inbox-poll:<tenantId>); ueberschneiden sich zwei Ticks verschiedener Mandanten, bricht der zweite still ab (Warnzeile 'already processing') und der Mandant wartet bis zum naechsten Intervall - kein Datenverlust, Verzoegerung; mit EINEM Mandanten unveraendert. Der Tick blieb in 3c laut Auftrag unangetastet (T-EYM-09, accept mit Aufzeichnung). Zu schliessen: Riegel je Mandant (Set<tenantId>) mit Test 'zwei Mandanten gleichzeitig, beide werden bedient'. | open | | 2026-09-14T09:51:24.295Z | |
| 38 | quick-260914-m97 | deviation | apps/api/src/bug-reports/dto/bug-report.dto.ts | 71 | Rule 1: @Expose() auf errors ergaenzt, damit die @Transform-Normalisierung auch bei ganz fehlendem Multipart-Feld greift (class-transformer transformiert nur vorhandene Schluessel) | fixed | | 2026-09-14T15:04:31.846Z | 2026-09-14T15:17:38.808Z |
````json ````json
[ [
@@ -501,6 +502,19 @@ last_updated: 2026-09-14T09:51:24.295Z
"recorded_at": "2026-09-14T09:51:24.295Z", "recorded_at": "2026-09-14T09:51:24.295Z",
"resolved_at": null, "resolved_at": null,
"milestone": "v1.2" "milestone": "v1.2"
},
{
"id": 38,
"kind": "deviation",
"phase": "quick-260914-m97",
"file": "apps/api/src/bug-reports/dto/bug-report.dto.ts",
"line": 71,
"description": "Rule 1: @Expose() auf errors ergaenzt, damit die @Transform-Normalisierung auch bei ganz fehlendem Multipart-Feld greift (class-transformer transformiert nur vorhandene Schluessel)",
"status": "fixed",
"reason": "",
"recorded_at": "2026-09-14T15:04:31.846Z",
"resolved_at": "2026-09-14T15:17:38.808Z",
"milestone": "v1.2"
} }
] ]
```` ````
@@ -0,0 +1,366 @@
---
phase: quick-260914-m97
plan: 01
subsystem: api, ui, mail
tags: [bug-report, html-to-image, multipart, multer, nodemailer, prisma, smtp, next-intl, vitest]
requires:
- phase: quick-260914-ku1
provides: Versionsstempel appVersion (Web) und formatAppVersionLine (API), CI-Skript mit Build-Args, Kanaele beta/live
- phase: quick-260914-eym
provides: MailService mit Transport je Versand nach Mandant (resolveTransport)
provides:
- "POST /bug-reports (Multipart, 4 MiB je Route, alle angemeldeten Rollen, Drossel 5/10 min, PNG-Signatur, 409/413/429/502) mit E-Mail und PNG-Anhang ueber den Transport des Sitzungs-Mandanten"
- "SmtpConfig.bugReportRecipient (additive Migration 20260914170000), Feld Fehlermeldungen an unter Administrator -> SMTP, Rueckfall TESSERA_BUGREPORT_TO in docker-compose.prod.yml"
- "Fehler-melden-Knopf in der Kopfzeile: Bild VOR dem Dialog (html-to-image 1.11.13), Dialog mit Vorschau, Fehlerpuffer im Browser (error-buffer.ts)"
- "Handbuecher Anwender/Administration/Betrieb mit Ablauf, Datenschutz-Hinweis, Feld und Rueckfall"
affects: [erstfreigabe-v1.0.0, smtp, mail, handbuecher]
actuals:
tokens: 206676
tasks: 3
commits: 4
plan_head_before: 17a7e5ef9b77b9e6bd4cf5cb337e691b215bbabb
tech-stack:
added: [html-to-image 1.11.13 (apps/web, exakt gepinnt, 0 Abhaengigkeiten)]
patterns:
- "Multipart je Route mit FileInterceptor-Limit statt globalem JSON-Limit (main.ts unangetastet)"
- "MailService: Versandkern deliver wirft, Mantel sendViaTenantTransport verschluckt (T-02-12 nur fuer Kennwort-Reset/Willkommen)"
- "Multipart-Wiederholfelder im DTO per @Expose() + @Transform normalisieren (String/undefined/Array -> Array)"
- "Browser-Fehlerpuffer: fetch-Wrapper notiert nur !ok, nie Anfrage-Rumpf/Suchteil/Kopfzeilen (T-M97-02)"
- "Komponententest liest Texte aus der echten de.json (next-intl-Mock mit Punktpfad-Lookup)"
key-files:
created:
- apps/api/prisma/migrations/20260914170000_smtp_config_bug_report_recipient/migration.sql
- apps/api/src/bug-reports/bug-reports.module.ts
- apps/api/src/bug-reports/bug-reports.controller.ts
- apps/api/src/bug-reports/bug-reports.service.ts
- apps/api/src/bug-reports/dto/bug-report.dto.ts
- apps/api/src/bug-reports/bug-reports.service.spec.ts
- apps/api/src/bug-reports/bug-reports.controller.spec.ts
- apps/web/src/lib/error-buffer.ts
- apps/web/src/lib/error-buffer.test.ts
- apps/web/src/lib/bug-report-api.ts
- apps/web/src/components/bug-report/bug-report-button.tsx
- apps/web/src/components/bug-report/bug-report-dialog.tsx
- apps/web/src/components/bug-report/bug-report-button.test.tsx
- apps/web/src/components/settings/smtp-settings-form.test.tsx
modified:
- apps/api/prisma/schema.prisma
- apps/api/src/settings/settings.service.ts
- apps/api/src/settings/settings.service.spec.ts
- apps/api/src/settings/dto/smtp-config.dto.ts
- apps/api/src/mail/mail.service.ts
- apps/api/src/mail/mail.service.spec.ts
- apps/api/src/app.module.ts
- docs/mandantentrennung-zugriffsklassifikation.md
- docker-compose.prod.yml
- apps/web/package.json
- pnpm-lock.yaml
- apps/web/src/components/layout/header.tsx
- apps/web/src/components/layout/app-shell.tsx
- apps/web/src/lib/settings-api.ts
- apps/web/src/components/settings/smtp-settings-form.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
- apps/web/src/messages/umlaut-dictionary.ts
- docs/anleitung-anwender.md
- docs/anleitung-administration.md
- docs/anleitung-betrieb.md
key-decisions:
- "Multipart statt JSON+Base64: Limit nur auf POST /bug-reports (FileInterceptor 4 MiB), main.ts unangetastet, kein globales Body-Limit fuer /auth/login"
- "Keine GET /bug-reports/status-Route: 409 beim Senden reicht, kein Aufruf je Seitenladung"
- "Empfaenger lebt in SmtpConfig (Feld Fehlermeldungen an), Rueckfall TESSERA_BUGREPORT_TO nur ueber die Prod-Compose; Leerstring zaehlt als ungesetzt"
- "sendBugReport laesst Transportfehler durch (502), sendPasswordResetEmail verschluckt weiter (T-02-12) — Regressionstest im selben Spec"
- "Commits auf main (branching_strategy none, quick_branch_template null, wie 260914-ku1); CI-Lauf ueber Rerun-API wiederholt, weil die Ursache ein Gitea-Ausfall war, kein Code"
patterns-established:
- "Multipart-Wiederholfeld errors: @Expose() + @Transform im DTO, Pipe-Test mit metatype"
- "Bild-vor-Dialog per toPng-Mock gepinnt (queryByRole('dialog') ist null waehrend der Aufnahme)"
requirements-completed: [QUICK-260914-M97]
coverage:
- id: D1
description: "POST /bug-reports: Drossel, PNG-Signatur, Empfaenger-Aufloesung, Mandant aus der Sitzung, E-Mail mit PNG-Anhang, 502 bei Versandfehler"
requirement: QUICK-260914-M97
verification:
- kind: unit
ref: "apps/api/src/bug-reports/bug-reports.service.spec.ts#Test 1-8 (Falsifizierungen a-d)"
status: pass
- kind: unit
ref: "apps/api/src/bug-reports/bug-reports.controller.spec.ts#Test 1-3 (whitelist, Grenzen, kein @Roles)"
status: pass
- kind: unit
ref: "apps/api/src/mail/mail.service.spec.ts#Test 5-6 (Anhaenge, Fehler durch vs. verschluckt)"
status: pass
human_judgment: false
- id: D2
description: "SmtpConfig.bugReportRecipient mit Migration, DTO, SAFE_SELECT, getBugReportRecipient; Feld Fehlermeldungen an im SMTP-Formular"
requirement: QUICK-260914-M97
verification:
- kind: unit
ref: "apps/api/src/settings/settings.service.spec.ts#Test A-C"
status: pass
- kind: unit
ref: "apps/web/src/components/settings/smtp-settings-form.test.tsx#Test 1-2"
status: pass
- kind: other
ref: "prisma migrate deploy / migrate status / migrate diff gegen tessera-ctl-db-1"
status: pass
human_judgment: false
- id: D3
description: "Fehler-melden-Knopf: Bild VOR dem Dialog, Dialog mit Vorschau/Haekchen/Beschreibung, Multipart-Versand, Meldungen je Status, Fehlerpuffer ohne Kennwoerter"
requirement: QUICK-260914-M97
verification:
- kind: unit
ref: "apps/web/src/components/bug-report/bug-report-button.test.tsx#Test 1-11"
status: pass
- kind: unit
ref: "apps/web/src/lib/error-buffer.test.ts#Test 1-4"
status: pass
human_judgment: true
rationale: "html-to-image rastert nur im echten Browser (jsdom: HTMLVideoElement is not defined, kein Canvas); dass das Bild den Dialog NICHT zeigt und OKLCH-Farben stimmen, und dass die E-Mail mit PNG-Anhang in mailhog ankommt, muss der Browser-Check zeigen (siehe Fuer den Verifizierer)"
- id: D4
description: "Handbuecher Anwender/Administration/Betrieb"
requirement: QUICK-260914-M97
verification:
- kind: other
ref: "grep-Gates Task 3 (1 / 1 / 3 / 2 / 1)"
status: pass
human_judgment: true
rationale: "Alltagssprache und Verstaendlichkeit fuer Nicht-Programmierer kann nur ein Mensch beurteilen"
duration: "27 min (14:37Z bis 15:04Z, davon ca. 3,5 min Suiten-Laeufe und 2 x 4 min CI-Beobachtung)"
completed: "2026-09-14"
status: complete
---
# Quick 260914-m97 Plan 01: Fehler-melden-Knopf — Bildschirmfoto der aktuellen Seite per E-Mail mit PNG-Anhang — Summary
Ein Klick auf den Kaefer-Knopf rechts in der Kopfzeile nimmt zuerst ein Bild der Seite auf (html-to-image 1.11.13, laengste Kante 1600 px) und oeffnet erst danach den Dialog mit Vorschau, Haekchen und Feld „Was ist passiert?“; „Senden“ schickt Bild, Beschreibung, Seite, Web-/API-Version mit Kanal und Commit, Browser, Fenstergroesse, Zeitpunkt, angemeldeten Benutzer und die letzten 20 Browser-Fehler als Multipart an `POST /bug-reports`, das daraus eine E-Mail mit PNG-Anhang ueber den Transport des Sitzungs-Mandanten an `SmtpConfig.bugReportRecipient` (neues Feld „Fehlermeldungen an“ unter Administrator -> SMTP) oder den Rueckfall `TESSERA_BUGREPORT_TO` schickt. Drossel 5 je Benutzer je 10 Minuten (429), PNG-Signatur (400), kein Postfach (409), Versandfehler (502) — der Anwender erfaehrt immer, ob sein Bericht ankam. Vier Commits auf `main`, gepusht, CI-Lauf 299 nach einem Gitea-Datenbank-Ausfall im ersten Versuch per Rerun `success`; `:beta`-Abbilder tragen `77117de beta`.
## Ausgangslage und Bezugspunkt
Alle Gates gegen `5c42c55` (Code unangetastet seit Planung; HEAD bei Start `17a7e5e`, Arbeitsbaum sauber, `main == origin/main`). Vorbedingung Task 1: `docker ps | grep -c ^tessera-ctl-db-1$` -> `1`, `prisma migrate status` -> `36 migrations found` / `Database schema is up to date!` (IP `172.19.0.2`). Vorbedingung Task 3: `curl localhost:3002/api/v1/version` -> `{"version":"1.26.2"}`, `gitea-runner` -> `1`.
Baseline vor jeder Aenderung (erneut gemessen, identisch mit der Planung):
| Suite | Test Files | Tests |
|---|---|---|
| API (`pnpm -C apps/api exec vitest run`) | `65 passed (65)` | `1060 passed (1060)` |
| Web (`pnpm -C apps/web exec vitest run`) | `40 passed (40)` | `243 passed (243)` |
Konfiguration: `branching_strategy: none`, `quick_branch_template: null`, `auto_advance: false`, `human_verify_mode: end-of-phase`, `commit_docs: true`. Alle Commits liegen deshalb — wie die Plan-Commits dieses Auftrags und 260914-ku1 heute — auf `main`; `git.allow_default_branch_commits` ist nicht gesetzt, die Projektkonfiguration und der Plan (Push auf `main`, CI-Beobachtung des `main`-Laufs) verlangen es aber ausdruecklich.
## Task 1 — API (Commit `54121c1`, 16 Dateien)
**Schritt A, RED** (`pnpm -C apps/api exec vitest run src/bug-reports src/mail/mail.service.spec.ts src/settings/settings.service.spec.ts`):
```
FAIL src/bug-reports/bug-reports.controller.spec.ts — Error: Cannot find module './bug-reports.controller'
FAIL src/bug-reports/bug-reports.service.spec.ts — Error: Cannot find module './bug-reports.service'
FAIL mail.service.spec.ts > Test 5 / Test 6 — TypeError: service.sendBugReport is not a function
FAIL settings.service.spec.ts > Test A — TypeError: service.getBugReportRecipient is not a function
FAIL settings.service.spec.ts > Test B / Test C — AssertionError: expected undefined to be 'fehler@a.example.invalid'
Test Files 4 failed (4)
Tests 5 failed | 20 passed (25)
```
**Schritt B, Schema und Migration, [BLOCKING] `migrate deploy`** (`DATABASE_URL` nur in der Shell-Zeile, keine `.env` angefasst):
```
The following migration(s) have been applied:
migrations/
└─ 20260914170000_smtp_config_bug_report_recipient/
└─ migration.sql
All migrations have been successfully applied.
--- migrate status: 37 migrations found in prisma/migrations / Database schema is up to date!
--- migrate diff: -- This is an empty migration.
--- prisma generate: (ohne Fehler; nur der Accelerate-Tipp)
```
Migrationsordner-Zaehlung `ls apps/api/prisma/migrations | grep -c ""` -> `38` (36 + `migration_lock.toml` + 1 neu).
**Schritte C-E:** `SmtpConfigDto.bugReportRecipient` (`@IsOptional() @IsEmail()`, `string | null`), `SMTP_SAFE_SELECT` um das Feld, `saveSmtpConfig` mit bedingtem Spreading (fehlend = bewahren, `null`/leer = loeschen), neue Methode `getBugReportRecipient` (ein gebundener Klient, `findUnique` mit schmalem `select`). `MailService`: exportierte Typen `OutgoingAttachment`/`OutgoingMail`/`BugReportMail`, Versandkern `deliver` (wirft, `close()` im `finally`, Anhaenge/HTML nur wenn gesetzt), `sendViaTenantTransport` als verschluckender Mantel, `sendBugReport` ruft `deliver` direkt. Modul `bug-reports` mit DTO (Grenzen 4000/2000/100/20/64/1000/50/50, `errors` 30 x 1000), Dienst (Drossel-Map, PNG-Signatur, Empfaenger-Kette, gebundene Benutzerzeile `const tenantPrisma = forTenant(`, Betreff/Text/Anhang, 502-Uebersetzung, eine Protokollzeile), Controller (`@Controller('bug-reports')`, `@Post()`, `FileInterceptor('screenshot', { limits: { fileSize: 4 * 1024 * 1024, files: 1 } })`, kein `@Roles`), Modul in `app.module.ts` hinter `TendersModule`. Doku-Zeilen in `docs/mandantentrennung-zugriffsklassifikation.md` (Bestandsaufnahme alphabetisch hinter `auth/`, Bereichs-Tabelle `bug-reports | 0 | 1 | 0`). `docker-compose.prod.yml`: `TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-}` hinter `TESSERA_SMTP_FROM` mit englischem Kommentar.
**Schritt F, GREEN** (Zielspecs): `Test Files 5 passed (5)` / `Tests 66 passed (66)` (rls-inventory 30, mail 6, bug-reports.service 8, settings 19, bug-reports.controller 3 = 50 bisherige + 16 neue). Volle Suite: `Test Files 67 passed (67)` / `Tests 1076 passed (1076)`. `tsc --noEmit` api: `TSC_api=0`.
**Automatisierter Verify-Block Task 1** (Ausgabe in Planreihenfolge): `5 passed (5)` / `66 passed (66)`; `Database schema is up to date!`; `-- This is an empty migration.`; `38`; `1` (Schema); `1` (Migration); `1` (FileInterceptor); `1` (4-MiB-Limit); `0` (kein `@Roles`); `0` (kein `tenantId` im DTO); `1` (Zuweisungsform); `2` (`sendBugReport` in mail.service.ts); `2` (Import + Eintrag); `1` (Doku-Zeile); **`0` (Compose-Grep — siehe Befund unten)**; `M_EXIT=0`; `M_EMPTY=0`; `TSC_api=0`.
**Befund Compose-Grep (Messinstrument, nicht Datei):** das Muster des Plans `grep -c 'TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-}' docker-compose.prod.yml` liefert `0` — dasselbe Muster liefert fuer die BESTEHENDE Zeile `TESSERA_SMTP_HOST: ${TESSERA_SMTP_HOST:-}` ebenfalls `0`. Mit festem Text `grep -c -F 'TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-}'` -> `1`, mit `\$` -> `1`; `sed -n 52p | cat -A` zeigt exakt ` TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-}$`. Die Datei traegt die vorgeschriebene Zeile; das Regex-Muster (`$` vor `{`) trifft sie nicht. Gate nicht angepasst, beide Zahlen hier festgehalten.
## Task 2 — Web (Commit 2a `60b0ee8`, 13 Dateien; Commit 2b `b41be21`, 5 Dateien)
**Schritt A, Abhaengigkeit:** `"html-to-image": "1.11.13"` alphabetisch unter `dependencies`; `pnpm install` -> `Done in 5.5s` (Lockfile +8 Zeilen: `html-to-image@1.11.13` als Importer-Eintrag, Paket und Snapshot; die Warnung `nunjucks 3.2.4 unmet peer chokidar` ist Bestand). `pnpm install --frozen-lockfile` -> `FROZEN=0`. `apps/web/node_modules/html-to-image/lib/index.d.ts` vorhanden. `git diff --stat 5c42c55 -- apps/api/package.json packages/shared/package.json package.json` leer (`U_EMPTY=0`). Folge fuer die CI: die deps-Stufe beider Dockerfiles laeuft wegen des Lockfiles neu (gemessen: Lauf 299 baute 3 min 37 s bzw. 3 min 58 s statt 5 min 18 s bei 297 — der Bau war sogar schneller, weil kein Base-Image nachzuladen war).
**Schritt B, RED Teil 2a** (`pnpm -C apps/web exec vitest run src/lib/error-buffer.test.ts src/components/bug-report`):
```
FAIL src/lib/error-buffer.test.ts — Error: Failed to resolve import "./error-buffer"
FAIL src/components/bug-report/bug-report-button.test.tsx — Error: Failed to resolve import "@/lib/error-buffer"
Test Files 2 failed (2)
Tests no tests
```
**Schritte C-F:** `error-buffer.ts` (Ringpuffer 20, `MAX_MESSAGE` 1000, `BODY_EXCERPT` 200, Guard `__tesseraErrorBufferInstalled`, `error`/`unhandledrejection`/`console.error`/`fetch`-Wrapper, `uninstallErrorBuffer` nur fuer Tests), `installErrorBuffer()` in `app-shell.tsx` als eigener `useEffect`; `bug-report-api.ts` (`computeCaptureSize`, `captureScreenshot` mit dynamischem Import und `filter` auf `data-bug-report-ignore`, `dataUrlToBlob`, `sendBugReport` ohne `headers`); i18n `bugReport` (20 Schluessel) in `de.json`/`en.json` je an Position 10 hinter `theme`; Dialog (`role="dialog"`, `aria-modal`, `aria-labelledby`, Escape ausser waehrend `sending`, Fokus auf das Textfeld, Zustaende `ready | sending | sent | failed`, Meldung je Status, Admin-Link `next/link` auf `/admin/smtp`), Knopf (Kaefer-Symbol nach lucide `bug`, `captureScreenshot()` VOR `setOpen(true)`), `<BugReportButton />` unmittelbar vor `<ThemeToggle />` in `header.tsx`.
**Umlaut-Waechter:** nach dem Eintrag des Namensraums flaggte `umlaut-guard.spec.ts` GENAU EIN Wort: `bugReport.descriptionLabel: "passiert" is a new word not on UMLAUT_ALLOWLIST` -> `'passiert'` mit Kommentar `// 260914-m97` in `UMLAUT_ALLOWLIST` eingetragen; `geschehen`, `aktuellen` wurden nicht verlangt. Danach `src/messages`: `Test Files 2 passed (2)` / `Tests 6 passed (6)`.
**Schritt F2, Gate 2a:** `pnpm -C apps/web exec vitest run src/lib/error-buffer.test.ts src/components/bug-report src/messages` -> `Test Files 4 passed (4)` / `Tests 21 passed (21)` (error-buffer 4, bug-report-button 11, Waechter 6); `TSC_web=0`. Commit 2a mit genau 13 Dateien (`git show --stat HEAD` -> `13 files changed, 968 insertions(+)`).
**Schritt G, RED Teil 2b** (`pnpm -C apps/web exec vitest run src/components/settings/smtp-settings-form.test.tsx`):
```
× Test 1: das Feld ist aus GET /settings/smtp vorbelegt
× Test 2: PUT-Payload traegt den Wert; leeres Feld -> null
Error: It looks like undefined was passed instead of a matcher. Did you do something like getByText(undefined)?
Test Files 1 failed (1)
Tests 2 failed (2)
```
(rot, weil `de.settings.smtp.bugReportRecipient` noch nicht existierte — das Label war `undefined`.)
**Schritt G/H, GREEN Teil 2b:** `settings.smtp.bugReportRecipient` / `bugReportRecipientHelp` hinter `testFailed` in beiden Dateien (jetzt 20 Schluessel unter `settings.smtp`), `settings-api.ts` (`bugReportRecipient: string | null` in `SmtpConfig`, optional in `SaveSmtpPayload`), Formular (`FormState.bugReportRecipient`, Vorbelegung `config.bugReportRecipient ?? ''`, `payload.bugReportRecipient = form.bugReportRecipient.trim() || null`, Eingabefeld `id="smtp-bug-report-recipient"` `type="email"` zwischen Absenderadresse und „Test-E-Mail an“). Zielspecs `Test Files 3 passed (3)` / `Tests 8 passed (8)`; volle Suite `Test Files 43 passed (43)` / `Tests 260 passed (260)`; `TSC_web=0`.
**Automatisierter Verify-Block Task 2:** `FROZEN=0`; `h2i=1.11.13`; `Test Files 5 passed (5)` / `Tests 23 passed (23)` (6 Waechter + 17 neue); `1`; `ORDER=ok`; `1`; `1`; `1`; `2` (`smtp-bug-report-recipient`, `htmlFor` + `id`); `Fehler melden | Was ist passiert? | Fehlermeldungen an | Report a problem | true`; `U_EMPTY=0`; `TSC_web=0`. Commit 2b mit genau 5 Dateien (`5 files changed, 115 insertions(+), 2 deletions(-)`).
## Task 3 — Handbuecher, Abschluss-Gates, Push, CI (Commit `77117de`, 3 Dateien)
`docs/anleitung-anwender.md`: Inhaltsverzeichnis 8. „Einen Fehler melden“, 9. „Haeufige Stolpersteine“; Kopfleiste „drei Bedienelemente“ mit dem Knopf als erstem Punkt; neuer Abschnitt (fuenf Absaetze: Ablauf, Beschreibung, Haekchen mit fettem Datenschutz-Satz, was mitgeschickt wird und dass nichts in Tessera gespeichert wird, Rueckmeldungen); neuer Stolperstein „kein Postfach“. `docs/anleitung-administration.md`: Kapitel 6 nennt das Feld in der Feldaufzaehlung und in einem eigenen Absatz (Wirkung, Betreff, Anhang, immer sichtbar, Drossel, 4 MB, dieselben Zugangsdaten, Rueckfall, Datenschutz); zwei neue Zeilen in der Fehlersuche-Tabelle. `docs/anleitung-betrieb.md`: `TESSERA_BUGREPORT_TO` in der Konfigurationstabelle nach der `TESSERA_SMTP_*`-Zeile (Serverdatei von Hand, wie `IMAGE_TAG`), neue Zeile in der Symptomtabelle Kapitel 7.
**Grep-Gates:** `^## Einen Fehler melden` -> `1`; `drei Bedienelemente` -> `1`; `Fehlermeldungen an` in administration -> `3` (erste Messung `2`, weil `grep -c` Zeilen zaehlt und Absatz plus Tabellenzeile zwei Zeilen sind — die Feldaufzaehlung im ersten Absatz von Kapitel 6 hat das Feld dann sachlich richtig als dritte Nennung bekommen; `grep -o | wc -l` -> `3`); `TESSERA_BUGREPORT_TO` in betrieb -> `2`; in administration -> `1`.
**Abschluss-Gates (alle nach dem letzten Code-Commit gemessen):**
| Gate | Ergebnis |
|---|---|
| API volle Suite | `Test Files 67 passed (67)` / `Tests 1076 passed (1076)` |
| Web volle Suite | `Test Files 43 passed (43)` / `Tests 260 passed (260)` |
| `tsc --noEmit` | `TSC_packages/shared=0`, `TSC_apps/api=0`, `TSC_apps/web=0` |
| `pnpm install --frozen-lockfile` | `FROZEN=0` |
| `git diff --stat 5c42c55 -- . ':!.planning'` | `GIT_EXIT=0`, ` 35 files changed, 2026 insertions(+), 17 deletions(-)` |
| Unangetastet-Stichprobe (`main.ts`, `biome.json`, `.env*`, `apps/api/package.json`, `packages/shared`, `package.json`, Migration `20260914120000`) | `U_EXIT=0`, `U_EMPTY=0` |
| `migrate status` (nach allem) | `Database schema is up to date!` |
| `git status -sb` nach `git fetch` | `## main...origin/main` (kein `[ahead`) |
**Push:** `git push` um 14:53:40Z -> `5c42c55..77117de main -> main` (die zwei Plan-Commits des Orchestrators gingen mit).
### CI-Lauf nach dem Push
| Feld | Versuch 1 | Versuch 2 |
|---|---|---|
| Lauf-ID | 299 (event `push`, ref `main`, `head_sha` `77117de`) | 299 (Rerun per `POST .../actions/runs/299/rerun`, HTTP 201, 14:59:08Z) |
| status / conclusion | `completed` / **`failure`** | `completed` / **`success`** |
| started_at / completed_at | 16:53:44 / 16:57:21 (+02:00) — 3 min 37 s | 16:59:10 / 17:03:08 (+02:00) — 3 min 58 s |
| Jobs | Lint & Type Check `success`, Tests `success`, Build & Publish `failure` im Schritt „Versionsstempel berechnen, Abbilder bauen und veroeffentlichen“ | alle drei `success` |
| Ursache Versuch 1 | Beide Abbilder wurden fertig gebaut (Web-Bundle inkl. `/login`-Route, `naming to localhost:3002/schalli/tessera-ctl/web:beta done`); der anschliessende `docker push` scheiterte sofort mit `error from registry: unauthorized`, obwohl `Login Succeeded` vorher stand. Gitea-Serverlog 16:57:09-16:57:19: `dial tcp: lookup db on 127.0.0.11:53: no such host` — Gitea konnte seine eigene Datenbank (`gitea-db`, laut `docker ps` in dieser Minute neu gestartet, nicht durch mich) zehn Sekunden lang nicht erreichen, jede authentifizierte Anfrage antwortete 401 (auch mein Poll 11 um 14:57:17Z: `GET .../actions/runs 401` -> `not-found`, und die Registry-`HEAD /v2/.../blobs` -> 401). Kein Zusammenhang mit den 35 Dateien; kein `fix`-Commit moeglich oder noetig. | — |
| `api:beta` node `APP_VERSION APP_CHANNEL APP_COMMIT` | — | `77117de beta 77117de` (`git rev-parse --short HEAD` = `77117de`) |
| `docker image inspect Created` web:beta / api:beta | — | `2026-09-14T17:01:00+02:00` / `2026-09-14T17:02:07+02:00` (nach dem Rerun-Start) |
| `web:beta` html-to-image, Probe des Plans (`ls /app/apps/web/node_modules/html-to-image/package.json \|\| ls /app/node_modules/.pnpm \| grep -c html-to-image`) | — | **`0`** — Messinstrument-Befund: das Standalone-Abbild traegt nur die vom Server benoetigten Pakete (`/app/node_modules/.pnpm` hat 25 Eintraege, kein `html*`); `html-to-image` wird ausschliesslich im Browser dynamisch importiert und liegt deshalb im Client-Bundle |
| `web:beta` html-to-image, korrigierte Probe | — | Chunk `/app/apps/web/.next/static/chunks/3717.5ecd3f65b9b8111a.js` (12.547 Bytes) enthaelt `cacheBust` und `skipFonts` (die Optionen des Aufrufs, in der minifizierten Bibliothek erhalten); 4 Chunks mit `foreignObject`, 1 Chunk mit `bug-report-ignore` -> Bibliothek und Knopf sind im Abbild |
## Falsifizierungen (a)-(d) — rot/gruen
| Spec | RED (vor Produktionscode) | GREEN |
|---|---|---|
| (a) `bug-reports.service.spec.ts` Test 3: fuenf Berichte durch, der sechste -> `HttpException` mit `getStatus() === 429`, `sendBugReport` genau fuenfmal; `u2` gleichzeitig frei; `vi.advanceTimersByTime(600001)` -> `u1` wieder `{ sent: true }` | `Cannot find module './bug-reports.service'` | `✓ 8 tests` in `bug-reports.service.spec.ts` |
| (b) Test 4: `Buffer.from('nicht png, aber lang genug')` -> `BadRequestException`; die ersten 7 PNG-Bytes -> `BadRequestException`; `sendBugReport` nie gerufen | wie oben | ✓ |
| (c) Test 5: Settings `null` + Variable `undefined` -> `ConflictException` mit `Fehlermeldungen an` in der Meldung; Variable `''` -> ebenfalls `ConflictException`; nie versendet | wie oben | ✓ |
| (d) Test 8: DTO mit `tenantId: 'fremd'`, `userId: 'u-fremd'` -> `getBugReportRecipient('t1')`, jeder `forTenant`-Aufruf mit `'t1'`, `sendBugReport` mit `'t1'`; Text enthaelt `Anna Muster (anna)`, nicht `Fremde Anna`/`Eindringling`/`fremd@x.invalid`. Controller-Spec Test 1: `ValidationPipe({ whitelist: true, transform: true })` entfernt `tenantId`, `errors: 'einzeln'` -> `['einzeln']`, fehlend -> `[]`, Array bleibt | Service: wie oben; Controller: `Cannot find module './bug-reports.controller'`; nach dem ersten GREEN-Lauf war Test 1 noch rot (`BadRequestException` bei ganz fehlendem `errors`) — siehe Abweichung 1 | ✓ 3 tests |
| Reihenfolge Bild-vor-Dialog: `bug-report-button.test.tsx` Test 1 (`toPng`-Mock prueft `queryByRole('dialog')` ist `null`, `canvasWidth: 1600`, `canvasHeight: 500` bei 3200x1000) | `Failed to resolve import "@/lib/error-buffer"` | ✓ 11 tests |
| Kennwort-Reset bleibt verschluckend: `mail.service.spec.ts` Test 6 (`sendBugReport` -> `rejects.toThrow('ECONNREFUSED')`, `sendPasswordResetEmail` -> `resolves.toBeUndefined()`, `close()` zweimal) | `service.sendBugReport is not a function` | ✓ 6 tests |
| Fehlerpuffer ohne Geheimnisse: `error-buffer.test.ts` Test 2 (`POST /api/x -> 500`, enthaelt `kaputt`, NICHT `geheim`, NICHT `password`, NICHT `token=`; `res.json()` weiter lesbar; 200 nicht notiert) | `Failed to resolve import "./error-buffer"` | ✓ 4 tests |
## Task Commits
1. **Task 1: API** — `54121c1` (feat) — 16 Dateien
2. **Task 2a: Web Knopf/Dialog/Puffer** — `60b0ee8` (feat) — 13 Dateien
3. **Task 2b: SMTP-Formular** — `b41be21` (feat) — 5 Dateien
4. **Task 3: Handbuecher** — `77117de` (docs) — 3 Dateien
`commits: 4` gemessen aus `git rev-list --count 17a7e5e..HEAD` (Ledger `plan_head_before` = `17a7e5ef9b77b9e6bd4cf5cb337e691b215bbabb`). `actuals.tokens` = 826.706 Zeichen ueber die 35 geaenderten Dateien / 4 = 206.676 (Methode wie 260914-ku1; der reine Diff waere 121.245 Zeichen = 30.311). Der Plan schaetzte 150.000 bei `confidence: low`.
`git log --oneline 17a7e5e..HEAD` (vor dem SUMMARY):
```
77117de docs(quick-260914-m97): Handbuecher — Einen Fehler melden (Anwender), Feld Fehlermeldungen an (Administration), TESSERA_BUGREPORT_TO als Rueckfall (Betrieb)
b41be21 feat(quick-260914-m97): Feld Fehlermeldungen an im SMTP-Formular — settings-api, Formular, i18n settings.smtp
60b0ee8 feat(quick-260914-m97): Fehler-melden-Knopf in der Kopfzeile — Bildschirmfoto vor dem Dialog (html-to-image 1.11.13), Fehlerpuffer, Dialog mit Vorschau, i18n bugReport
54121c1 feat(quick-260914-m97): Fehlermeldungen per E-Mail — Empfaenger in SmtpConfig (Migration), MailService-Anhaenge, Modul bug-reports mit Drossel, PNG-Pruefung und Mandant aus der Sitzung
```
`git status --porcelain` (vor dem SUMMARY): nur ` M .planning/WINDOWS.md` (Ledger-Eintrag der Abweichung 1, siehe unten) — kein Code ungeschrieben, kein Code uncommittet.
## Entscheidungen
- **Multipart statt JSON+Base64** (Planungsmessung uebernommen und im Bau bestaetigt): `FileInterceptor('screenshot', { limits: { fileSize: 4 MiB, files: 1 } })` begrenzt nur diese Route; `main.ts` unangetastet (Gate `M_EMPTY=0`). Ein globales `app.useBodyParser('json', { limit })` haette jede JSON-Route inkl. `/auth/login` geoeffnet.
- **Keine `GET /bug-reports/status`-Route:** der Knopf ist immer sichtbar, 409 beim Senden traegt die Information (mit Admin-Link); keine zusaetzliche Anfrage je Seitenladung.
- **`@Expose()` im DTO** (Abweichung 1): ohne `@Expose()` ruft class-transformer `@Transform` fuer einen im Rumpf GANZ fehlenden Schluessel nicht auf; die Normalisierung `undefined -> []` haette nur auf dem Papier gestanden.
- **Rerun statt `fix`-Commit** fuer den CI-Lauf: die Ursache lag in Gitea (Datenbank kurz nicht erreichbar), nicht im Code; ein Leer-Commit haette nur die Historie verschmutzt.
- **Commits auf `main`:** siehe Ausgangslage — Projektkonfiguration `branching_strategy: none`, Plan verlangt Push auf `main` und CI-Beobachtung; identisch mit 260914-ku1 heute.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] `@Expose()` auf `errors`, damit die `@Transform`-Normalisierung auch bei ganz fehlendem Feld greift**
- **Found during:** Task 1, Schritt F (erster GREEN-Lauf, Controller-Spec Test 1 rot: `BadRequestException` bei `pipe.transform({ ...baseBody }, meta)` ohne `errors`)
- **Issue:** Das Rezept des Plans (`@Transform` gefolgt von `@IsArray()`) deckt den Fall „Feld fehlt im Multipart-Rumpf“ nicht: class-transformer laeuft `@Transform` nur fuer Schluessel, die im Quellobjekt vorhanden sind; `errors` blieb `undefined`, `@IsArray()` schlug fehl — ein Anwender ohne Browserfehler haette 400 bekommen.
- **Fix:** `@Expose()` (aus `class-transformer`, bereits installiert) vor `@Transform`; Kommentar im DTO nennt die Messung.
- **Files modified:** `apps/api/src/bug-reports/dto/bug-report.dto.ts`
- **Verification:** `bug-reports.controller.spec.ts` Test 1 gruen (String -> `['einzeln']`, fehlend -> `[]`, Array bleibt), Test 2 (Grenzen) unveraendert gruen
- **Committed in:** `54121c1` (Teil des Task-1-Commits)
**2. [Rule 2 - Missing content] Dritte Nennung von „Fehlermeldungen an“ in der Feldaufzaehlung von Kapitel 6**
- **Found during:** Task 3, Grep-Gate (`grep -c "Fehlermeldungen an" docs/anleitung-administration.md` -> `2`, Plan: mindestens `3`)
- **Issue:** `grep -c` zaehlt Zeilen; Absatz und Tabellenzeile sind zwei Zeilen. Inhaltlich fehlte das neue Feld in der Aufzaehlung der SMTP-Felder im ersten Absatz von Kapitel 6.
- **Fix:** Aufzaehlung ergaenzt („… die Absenderadresse und optional das Feld „Fehlermeldungen an“ (siehe unten)“). Kein Gate angepasst.
- **Files modified:** `docs/anleitung-administration.md`
- **Verification:** `grep -c` -> `3`, `grep -o | wc -l` -> `3`
- **Committed in:** `77117de`
---
**Total deviations:** 2 auto-fixed (1 x Rule 1, 1 x Rule 2). **Impact on plan:** keine Erweiterung der 35 Dateien, keine Gate-Anpassung; beide Korrekturen sind fuer Korrektheit bzw. Vollstaendigkeit noetig.
Ledger: `gsd_run windows append --kind deviation` fuer Abweichung 1 ist geschrieben (`.planning/WINDOWS.md`, `ok: true`) — die Datei liegt uncommittet fuer den Docs-Commit des Orchestrators.
## Issues Encountered
1. **CI-Lauf 299, Versuch 1 `failure`** — Ursache Gitea-Datenbank-Ausfall 16:57:09-16:57:19 (siehe Tabelle „CI-Lauf nach dem Push“), Registry antwortete 401 beim Push. Behoben durch Rerun ueber die API (Versuch 2 `success`). Kein Code-Commit.
2. **Zwei Messinstrumente des Plans trafen die Wirklichkeit nicht:** Compose-Grep-Muster (`0` fuer die neue UND fuer die bestehende SMTP-Zeile; `grep -F` -> `1`) und Abbild-Probe fuer `html-to-image` (`0` in den Server-`node_modules` des Standalone-Abbilds; korrigierte Probe im Client-Chunk positiv). Beide Male ist die Datei/das Abbild wie vorgeschrieben; beide Zahlen stehen oben nebeneinander.
3. **Web-Test-Baseline unveraendert 40/243, API 65/1060** — keine Abweichung, hier nur als Kontrolle: die Zielzahlen 67/1076 und 43/260 wurden exakt erreicht (API +2 Dateien, +16 Tests; Web +3 Dateien, +17 Tests).
## Was bewusst offen bleibt
- **Browser-Beweis** (Bild ohne Dialog, OKLCH-Farben, E-Mail mit PNG-Anhang in mailhog, Groesse des Anhangs): nicht im Executor moeglich (jsdom rastert nicht) — Human-Check `end-of-phase`, Anleitung unten.
- **Serverdatei `/opt/tessera/docker-compose.prod.yml`** bekommt die Zeile `TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-}` nur von Hand (wie `IMAGE_TAG`, Betriebshandbuch Kapitel 3/9). Ohne sie gilt auf dem Server ausschliesslich das UI-Feld — was fuer den Live-Betrieb reicht.
- **Basis-/Dev-Compose** reichen `TESSERA_BUGREPORT_TO` nicht durch (Plan: nur `docker-compose.prod.yml`); lokal ist der Empfaenger ueber das UI-Feld zu setzen.
- **Drossel im Prozessspeicher:** je API-Prozess, geht bei Neustart verloren und gilt je Instanz — fuer eine Instanz korrekt, bei mehreren Instanzen waere die Grenze n x 5.
- **Erstfreigabe v1.0.0** (Zweig `live` + Tag) ist nicht Teil dieses Plans.
## Fuer den Verifizierer (Browser-Check mit mailhog)
1. Mail-Senke starten: `docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mailhog` (Ports 1025 SMTP, 8025 Web-Oberflaeche: `http://localhost:8025`). Dann `docker compose up -d --build api web` (die `:latest`-Abbilder sind lokal warm; `--build` ist Pflicht, `up` allein baut nicht neu).
2. Im Browser anmelden, **Administrator -> SMTP**: Host `mailhog`, Port `1025`, Verschluesselung „Keine“, Benutzername/Passwort leer, Absender `tessera@tessera.local`, **Fehlermeldungen an** `fehler@example.invalid`, speichern. (Env-Variante nur bei Bedarf: `TESSERA_BUGREPORT_TO` ist in der Basis-Compose NICHT durchgereicht — dafuer muesste man sie lokal, uncommittet, in den `environment`-Block von `api` in `docker-compose.dev.yml` eintragen; das UI-Feld ist der vorgesehene Weg.)
3. Auf einer Seite mit Inhalt (z. B. Benutzerverwaltung, dunkles Erscheinungsbild) den Kaefer-Knopf rechts oben klicken: Dialog mit Vorschau — die Vorschau darf den Dialog NICHT zeigen und muss die Farben der Seite wiedergeben. Beschreibung eintragen, „Senden“ -> „Vielen Dank, die Meldung wurde gesendet.“
4. `http://localhost:8025`: E-Mail mit Betreff `[Tessera Fehlermeldung] dev dev - /admin/users` (lokal ohne Build-Args: Version/Kanal `dev`), Text mit allen Kontextzeilen, Anhang `fehlermeldung-<yyyymmdd-hhmm>.png` — Groesse notieren (Erwartung unter 2 MB).
5. Zweite Probe: Feld „Fehlermeldungen an“ leeren und speichern -> Senden zeigt „Fuer Fehlermeldungen ist noch kein Postfach eingerichtet.“ plus Admin-Hinweis mit Link „Zu den SMTP-Einstellungen“ (als ADMIN/SUPER_ADMIN).
6. Dritte Probe: sechs Meldungen hintereinander -> die sechste zeigt „Zu viele Meldungen in kurzer Zeit …“.
7. `docker compose logs api | grep "Bug report"` -> je gesendeter Meldung eine Zeile `Bug report from <user> (tenant <id>) sent to <adresse> — page <pfad>, screenshot <n> bytes`, ohne Beschreibung und ohne Bild.
## Handgriffe fuer den User
- **Wo der Empfaenger eingestellt wird:** In Tessera als Administrator oben rechts **Administrator -> SMTP**, Feld **Fehlermeldungen an** (unter der Absenderadresse), Adresse eintragen, „Einstellungen speichern“. Ab dann gehen alle Meldungen der Anwender dieses Mandanten mit Bild dorthin. Feld leeren und speichern schaltet den Versand wieder ab (der Knopf bleibt sichtbar und erklaert dann, dass kein Postfach eingerichtet ist).
- **Rueckfall ueber die Umgebung** (nur fuer Installationen ohne gespeicherte SMTP-Einstellungen): `TESSERA_BUGREPORT_TO=<adresse>` in der `.env` des Servers UND die Zeile `TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-}` im `environment`-Block von `api` in `/opt/tessera/docker-compose.prod.yml` (von Hand, wie beim `IMAGE_TAG`), danach `api` neu erstellen. Das UI-Feld gewinnt immer, wenn beides gesetzt ist.
- **Datenbank:** Die neue Spalte kommt beim naechsten Deploy automatisch mit (`migrate deploy` beim API-Start, additive Migration, nichts zu tun).
## Self-Check: PASSED
- Dateien: alle 14 neu angelegten Dateien vorhanden (`bug-reports/*` 7, `error-buffer.ts/.test.ts`, `bug-report-api.ts`, `bug-report/*` 3, `smtp-settings-form.test.tsx`, Migration).
- Commits: `54121c1`, `60b0ee8`, `b41be21`, `77117de` in `git log --oneline --all` gefunden; `main == origin/main`.
- `commits: 4` = `git rev-list --count 17a7e5e..HEAD`.
@@ -0,0 +1,179 @@
---
phase: quick-260914-m97
verified: 2026-09-14T15:15:19Z
status: passed
score: 9/9 must-haves verified (Code/Tests/CI) + Browser-Beweis durch den Orchestrator am 2026-09-14 15:16Z bestanden (siehe Nachtrag unten)
behavior_unverified: 0
overrides_applied: 0
human_verification:
- test: "Browser-Check mit mailhog (SUMMARY-Abschnitt 'Fuer den Verifizierer')"
expected: "Kaefer-Knopf nimmt Bild VOR dem Dialog auf (Vorschau zeigt NICHT den Dialog, OKLCH-Farben korrekt); Senden erzeugt E-Mail in mailhog mit Betreff '[Tessera Fehlermeldung] ...' und PNG-Anhang < 2 MB; leeres Empfaenger-Feld -> 409-Text mit Admin-Link; sechste Meldung -> 429-Text; API-Log zeigt eine Zeile je Meldung ohne Bild/Beschreibung"
why_human: "jsdom rastert nicht (HTMLVideoElement is not defined, kein Canvas-Backend) - html-to-image kann nur im echten Browser beobachtet werden; dies ist laut Auftrag ausdruecklich Aufgabe des Orchestrators, NICHT dieses Verifizierers"
---
# Quick 260914-m97: Fehler-melden-Knopf — Verifikationsbericht
**Auftrag:** Fehler-melden-Knopf in der Kopfzeile — Bildschirmfoto VOR dem Dialog (html-to-image 1.11.13), Dialog mit Vorschau/Beschreibung/Haekchen, `POST /bug-reports` (Multipart, nur angemeldet, Mandant/Benutzer nur aus der Sitzung, 4 MiB -> 413, PNG-Signatur -> 400, fehlender Empfaenger -> 409, Drossel 5/10 min -> 429, Versandfehler -> 502), E-Mail mit PNG-Anhang und Kontext, Empfaenger als neue Spalte `SmtpConfig.bugReportRecipient`, Rueckfall `TESSERA_BUGREPORT_TO`, Handbuecher, gepusht, CI gruen.
**Verifiziert:** 2026-09-14T15:15Z
**Status:** human_needed (Code/Tests/Migration/CI vollstaendig verifiziert; einziger offener Punkt ist der Browser-Beweis, der laut Auftrag dem Orchestrator obliegt)
Umgebungshinweis: Zu Beginn dieser Verifikation war die Festplatte `/` kurzzeitig zu 100% voll, wodurch drei parallel gestartete `npx tsc`-Aufrufe mit `ENOSPC` fehlschlugen (npx wollte Cache-Metadaten schreiben, auch fuer bereits lokal vorhandene Binaries). Ich habe daraufhin `node node_modules/typescript/bin/tsc --noEmit` direkt aufgerufen (umgeht den npx-Cache) — alle drei Pakete meldeten danach Exit 0. Kein Projektartefakt betroffen; df zeigte kurz danach wieder 8,4 GiB frei (90% belegt), vermutlich ein voruebergehender Cache-Peak eines Fremdprozesses auf der Maschine.
## 1. Git-Historie und Datei-Umfang
| Pruefung | Befehl | Ergebnis | Status |
|---|---|---|---|
| Vier Commits | `git log --oneline 17a7e5e..HEAD` | `77117de`, `b41be21`, `60b0ee8`, `54121c1` — exakt die vier erwarteten | OK |
| Datei-Umfang | `git diff --stat 5c42c55 -- . ':!.planning'` | `35 files changed, 2026 insertions(+), 17 deletions(-)` | OK |
| Sensible Dateien unangetastet | `git diff --name-only 5c42c55 -- '.env*' apps/api/src/main.ts biome.json` | leer | OK |
| Bestehende Migrationen unangetastet | `git diff --name-only 5c42c55 -- apps/api/prisma/migrations \| grep -v 20260914170000` | leer (grep exit 1) | OK |
| Commit `60b0ee8` Datei-Zeilen | `git show --stat 60b0ee8 \| grep -c '\|'` | `13` | OK, passt zu 13 Dateien |
| Commit `b41be21` Datei-Zeilen | `git show --stat b41be21 \| grep -c '\|'` | `7` — bei Pruefung: Commit-Botschaft selbst enthaelt zwei `\|`-Zeichen (`string \| null`, `trim() \|\| null`); tatsaechliche Datei-Zeilen im Diffstat sind **5** (`smtp-settings-form.test.tsx`, `smtp-settings-form.tsx`, `settings-api.ts`, `de.json`, `en.json`) — passt zu den 5 erwarteten Dateien | OK (Messmuster liefert falsches Positiv, Datei-Zaehlung selbst stimmt) |
| Nur die zwei erwarteten Paket-Dateien geaendert | `git diff --name-only 5c42c55 -- apps/api/package.json packages/shared/package.json package.json apps/web/package.json pnpm-lock.yaml` | nur `apps/web/package.json`, `pnpm-lock.yaml` | OK |
## 2. Testsuiten und Typprüfung
| Pruefung | Befehl | Ergebnis | Status |
|---|---|---|---|
| API-Suite | `cd apps/api && npx vitest run` | `Test Files 67 passed (67)` / `Tests 1076 passed (1076)` | OK, passt exakt |
| Web-Suite | `cd apps/web && node node_modules/vitest/vitest.mjs run` (npx scheiterte an ENOSPC, siehe Umgebungshinweis) | `Test Files 43 passed (43)` / `Tests 260 passed (260)` | OK, passt exakt |
| `tsc --noEmit` api | `node node_modules/typescript/bin/tsc --noEmit` | Exit 0 | OK |
| `tsc --noEmit` web | `node node_modules/typescript/bin/tsc --noEmit` | Exit 0 | OK |
| `tsc --noEmit` shared | `node node_modules/typescript/bin/tsc --noEmit` | Exit 0 | OK |
| `pnpm install --frozen-lockfile` | `pnpm install --frozen-lockfile` | Exit 0, `Lockfile is up to date, resolution step is skipped`; `git status --porcelain -- pnpm-lock.yaml apps/web/package.json` danach leer | OK |
| `rls-access-inventory.spec.ts` | `node node_modules/vitest/vitest.mjs run src/prisma/rls-access-inventory.spec.ts` | `30 passed (30)` | OK |
| `umlaut-guard.spec.ts` + `tenderRadar-parity.spec.ts` | `node node_modules/vitest/vitest.mjs run src/messages` | `Test Files 2 passed (2)` / `Tests 6 passed (6)` | OK |
## 3. Migration und Datenbank
| Pruefung | Befehl | Ergebnis | Status |
|---|---|---|---|
| Migrationsstatus | `prisma migrate status` (DATABASE_URL gegen `172.19.0.2`, nicht in `.env` geschrieben) | `37 migrations found` / `Database schema is up to date!` | OK |
| Migrations-SQL additiv | `cat .../20260914170000_.../migration.sql` | genau EINE Anweisung `ALTER TABLE "SmtpConfig" ADD COLUMN "bugReportRecipient" TEXT;` (nullbar, keine weiteren Statements), davor nur Kommentarzeilen | OK |
| Spalte in lokaler DB | `docker exec ... psql -c '\d "SmtpConfig"'` | `bugReportRecipient \| text \| \| \|` (nullable) | OK |
## 4. API-Modul `bug-reports`
Gelesen: `bug-reports.module.ts`, `bug-reports.controller.ts`, `bug-reports.service.ts`, `dto/bug-report.dto.ts`, `mail.service.ts`.
| Anforderung | Befund | Status |
|---|---|---|
| Kein `@Roles`, offen fuer alle angemeldeten Rollen | `@Controller('bug-reports')` / `@Post()` ohne Rollen-Dekorator; `ROLES_KEY`-Metadatum ist laut `bug-reports.controller.spec.ts` Test 3 `undefined` | OK |
| `@CurrentUser()` einzige Quelle fuer Mandant/Benutzer | `submit(@CurrentUser() user, @Body() dto, @UploadedFile() file)`; DTO hat keine `tenantId`/`userId`-Felder | OK |
| `FileInterceptor` 4 MiB | `FileInterceptor('screenshot', { limits: { fileSize: 4 * 1024 * 1024, files: 1 } })` | OK |
| PNG-Signatur | `PNG_SIGNATURE = Buffer.from([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a])`, Pruefung vor Versand, `BadRequestException` bei Fehlschlag | OK |
| Drossel 5/10min -> 429 | `WINDOW_MS = 10*60*1000`, `MAX_PER_WINDOW = 5`, `HttpException(..., 429)`; **unabhaengig falsifiziert** (siehe unten) | OK |
| 409 bei fehlendem Empfaenger | `getBugReportRecipient(tenantId) \|\| TESSERA_BUGREPORT_TO.trim() \|\| null`, sonst `ConflictException` mit Text „Fehlermeldungen an" | OK |
| 502 bei Versandfehler | `try { sendBugReport(...) } catch { throw new BadGatewayException(...) }` | OK |
| Genau eine Protokollzeile, nie Bild/Beschreibung | `this.logger.log('Bug report from ... sent to ... page ..., screenshot ... bytes')` | OK |
| `mail.service.ts`: `sendBugReport` wirft, `sendPasswordResetEmail` verschluckt weiter | `deliver()` wirft, `sendViaTenantTransport` faengt (T-02-12 unveraendert), `sendBugReport` ruft `deliver` direkt; `mail.service.spec.ts` Test 5/6 pinnt genau das | OK |
**Unabhaengige Falsifizierung (Punkt 4 der Vorgabe):** `MAX_PER_WINDOW` in `bug-reports.service.ts` temporaer von `5` auf `6` geaendert, `bug-reports.service.spec.ts` erneut gelaufen -> Test 3 ("Falsifizierung a") wird ROT (`expected undefined to be an instance of HttpException`), die uebrigen 7 Tests bleiben gruen. Danach `git checkout -- apps/api/src/bug-reports/bug-reports.service.ts`; `grep MAX_PER_WINDOW` zeigt wieder `= 5`; `git status --porcelain -- apps/` ist leer — Ruecksetzung bewiesen.
## 5. Web: Fehlerpuffer, Bildaufnahme, Knopf, Dialog
| Anforderung | Befund | Status |
|---|---|---|
| Ringpuffer 20, `error`/`unhandledrejection`/`console.error`/`fetch` | `error-buffer.ts`: `MAX_ENTRIES = 20`, alle vier Quellen registriert | OK |
| Fetch-Wrapper notiert nur `!response.ok`, nie Anfrage-Rumpf/Cookies/Suchteil | Wrapper prueft `if (!response.ok)`, nutzt `pathOf()` (nur `pathname`, kein `search`), liest nur `response.clone().text()` (Antwort, nicht Anfrage); Test 2 pinnt „enthaelt NICHT geheim/password" | OK |
| SSR-sicher, idempotent | `if (typeof window === 'undefined') return;`, Guard `__tesseraErrorBufferInstalled` | OK |
| `computeCaptureSize` exportiert und rein | `apps/web/src/lib/bug-report-api.ts`, reine Funktion, Test 11 (eigener describe-Block) prueft 5 Faelle direkt | OK |
| `toPng` mit `pixelRatio: 1, skipFonts: true, cacheBust: true, canvasWidth/canvasHeight` | `captureScreenshot()` genau so implementiert | OK |
| Bild VOR Dialog | `bug-report-button.tsx`: `const shot = await captureScreenshot(); setScreenshot(shot); setOpen(true);` — Reihenfolge im Code UND in Test 1 (`toPng`-Mock prueft `queryByRole('dialog')` ist `null` waehrend seines eigenen Aufrufs) gepinnt | OK |
| Knopf vor ThemeToggle in `header.tsx` | Zeile 113/114: `<BugReportButton />` unmittelbar vor `<ThemeToggle />` | OK |
| Dialog: Escape, Fokus, Zustaende, 409/413/429/502/allgemein | `bug-report-dialog.tsx`: `role="dialog" aria-modal`, Escape-Handler (nicht waehrend `sending`), Fokus auf Textarea beim Oeffnen, `errorKey`-Zuordnung 409/429/413/502/sonst; 11 Tests in `bug-report-button.test.tsx` (echte `it(...)`-Zeilen gezaehlt: 11, eine weitere Fundstelle war ein Kommentar/Helper, kein Test) decken Tests 7-10 fuer 413/429/502/allgemein mit Texten aus `de.json` | OK |
| `installErrorBuffer()` in `app-shell.tsx` | `useEffect(() => { installErrorBuffer(); }, [])` vorhanden | OK |
| `html-to-image` exakt `1.11.13` | `apps/web/package.json` Zeile 15 | OK |
## 6. i18n
| Pruefung | Ergebnis | Status |
|---|---|---|
| `bugReport`-Namensraum in de.json/en.json identisch strukturiert | 20 Schluessel in beiden Dateien, gleiche Schluesselmenge (Python-Vergleich) | OK |
| `settings.smtp.bugReportRecipient`/`bugReportRecipientHelp` in beiden Sprachen | vorhanden, Sie-Form in de | OK |
| `umlaut-dictionary.ts` um `passiert` erweitert | Zeile 178, Kommentar `// 260914-m97` | OK |
| `umlaut-guard.spec.ts` / `tenderRadar-parity.spec.ts` gruen | siehe Abschnitt 2 | OK |
## 7. Einstellungen (SMTP-Formular)
| Pruefung | Ergebnis | Status |
|---|---|---|
| `SMTP_SAFE_SELECT` enthaelt `bugReportRecipient` | `settings.service.ts` Zeile 22 | OK |
| DTO `@IsOptional() @IsEmail()` | `smtp-config.dto.ts` Zeile 59-61 | OK |
| `PUT /settings/smtp` weiterhin nur ADMIN/SUPER_ADMIN | `@Put('smtp') @Roles(Role.ADMIN, Role.SUPER_ADMIN)` | OK |
| Web-Formular: Feld „Fehlermeldungen an" mit Hinweistext | `smtp-settings-form.tsx` Zeile 308-325, `id="smtp-bug-report-recipient"`, `type="email"` | OK |
## 8. `docker-compose.prod.yml`
`grep TESSERA_BUGREPORT_TO docker-compose.prod.yml` -> `TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-}` (Zeile 52, im `api.environment`-Block). `.env*` unveraendert (siehe Abschnitt 1).
## 9. Handbuecher
| Datei | Pruefung | Ergebnis |
|---|---|---|
| `docs/anleitung-anwender.md` | Abschnitt „Einen Fehler melden", Datenschutz-Satz, „drei Bedienelemente" | vorhanden (Zeilen 43, 160, 166) |
| `docs/anleitung-administration.md` | Feld „Fehlermeldungen an" unter Administrator -> SMTP, Fehlersuche-Zeile | vorhanden (Zeilen 204, 208, 241) |
| `docs/anleitung-betrieb.md` | `TESSERA_BUGREPORT_TO` in Konfigurationstabelle | vorhanden (Zeile 161, 340) |
| `docs/mandantentrennung-zugriffsklassifikation.md` | neue Zeile fuer den gebundenen `user`-Lesezugriff in `bug-reports.service.ts` | vorhanden (Zeile 664) plus Bereichs-Tabellen-Zeile (Zeile 176) |
## 10. CI und Container-Abbilder
| Pruefung | Befehl/Quelle | Ergebnis | Status |
|---|---|---|---|
| CI-Lauf fuer `77117de` | Gitea-API `.../actions/tasks?limit=5` (Token nur aus `git remote get-url --push origin` gelesen, nie ausgegeben) | Run 299/215: `Lint & Type Check`, `Tests`, `Build & Publish Images` je `status: success` fuer `head_sha 77117de3d0f1bb82df7b659f46fe26244ca6f164` | OK |
| `api:beta`-Abbild traegt den richtigen Commit | `docker run --rm --entrypoint node localhost:3002/schalli/tessera-ctl/api:beta -e "console.log(process.env.APP_VERSION, process.env.APP_CHANNEL)"` | `77117de beta` | OK |
| `web:beta`-Abbild enthaelt html-to-image im Client-Bundle | `docker run --rm --entrypoint sh localhost:3002/.../web:beta -c 'grep -rl "toPng\|html-to-image" apps/web/.next/static'` | Treffer in `chunks/3717.5ecd3f65b9b8111a.js` und `chunks/app/(portal)/layout-....js` | OK |
## 11. Push-Status
`git fetch -q && git status -sb | head -1` -> `## main...origin/main` (kein `[ahead`). OK.
## 12. Ledger `.planning/WINDOWS.md` #38
Eintrag #38 (`status: open`) beschreibt die Rule-1-Selbstkorrektur des Executors: `@Expose()` wurde auf `errors` im DTO ergaenzt, weil `class-transformer` `@Transform` sonst nur fuer im Rumpf VORHANDENE Schluessel aufruft (ohne die Korrektur haette ein Anwender ohne Browserfehler 400 statt 200 erhalten). Diese Korrektur ist bereits im selben Commit (`54121c1`) enthalten, verifiziert (`bug-reports.controller.spec.ts` Test 1 gruen, siehe Abschnitt 2) und im Code vorhanden (`bug-report.dto.ts` Zeile 71 `@Expose()`).
**Meine Einschaetzung:** Dies ist eine reine Protokollzeile eines bereits erledigten, verifizierten In-Scope-Fixes — kein offener technischer Mangel im Code. Der `status: open` bedeutet hier lediglich, dass niemand `gsd-tools windows fixed 38` ausgefuehrt hat, nicht dass am Code noch etwas fehlt. Zum Vergleich: Eintrag #37 (Single-Flight-Riegel prozessweit statt je Mandant) beschreibt eine tatsaechlich noch bestehende Einschraenkung im laufenden Code — #38 ist damit nicht vergleichbar und sollte administrativ geschlossen werden, ohne dass ein Folgeauftrag noetig ist.
## Vom Orchestrator im Browser zu pruefen
Dieser Verifizierer hat KEINE Container gestartet/gestoppt (Vorgabe). Folgende Schritte aus dem SUMMARY-Abschnitt „Fuer den Verifizierer" bleiben fuer den Orchestrator:
1. `docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mailhog`, danach `docker compose up -d --build api web`.
2. Anmelden, Administrator -> SMTP: Host `mailhog`, Port `1025`, Verschluesselung „Keine", Absender `tessera@tessera.local`, „Fehlermeldungen an" `fehler@example.invalid`, speichern.
3. Auf einer Seite mit Inhalt (dunkles Erscheinungsbild) Kaefer-Knopf klicken: Vorschau darf den Dialog NICHT zeigen, Farben muessen stimmen; Beschreibung eintragen, „Senden" -> „Vielen Dank, die Meldung wurde gesendet."
4. `http://localhost:8025`: E-Mail mit Betreff `[Tessera Fehlermeldung] dev dev - /admin/users`, Anhang `fehlermeldung-<yyyymmdd-hhmm>.png` unter 2 MB, alle Kontextzeilen im Text.
5. Feld leeren, speichern, senden -> 409-Text mit Admin-Link.
6. Sechs Meldungen hintereinander -> sechste zeigt 429-Text.
7. `docker compose logs api | grep "Bug report"` -> je Meldung eine Zeile ohne Bild/Beschreibung.
## Angenommene Risiken
- **Drossel im Prozessspeicher:** gilt je API-Instanz, geht bei Neustart verloren. Fuer den Livestart mit einer Instanz korrekt; bei mehreren Instanzen waere die effektive Grenze `n x 5`. So im Plan akzeptiert, nicht Teil dieses Auftrags zu loesen.
- **`TESSERA_BUGREPORT_TO` auf dem Produktivserver:** die Zeile in `/opt/tessera/docker-compose.prod.yml` muss von Hand ergaenzt werden (wie `IMAGE_TAG`) — ohne diesen manuellen Schritt gilt dort ausschliesslich das UI-Feld, was fuer den Livebetrieb morgen ausreicht.
- **Ledger #38:** administrative Restarbeit (Eintrag als `fixed` markieren), kein Code-Risiko — siehe Abschnitt 12.
- **Commit-`b41be21`-Messmuster:** das im Auftrag vorgegebene `grep -c '|'` liefert wegen Pipe-Zeichen in der Commit-Botschaft selbst `7` statt `5`; die tatsaechliche Datei-Zaehlung (5) stimmt nachweislich mit den erwarteten Dateien ueberein — kein Befund, nur eine Schwaeche des Messinstruments (bereits im Plan als "Messinstrument, nicht Datei"-Muster fuer den Compose-Grep dokumentiert, hier dasselbe Phaenomen bei einem anderen Grep).
- **Browser-Beweis steht aus** (siehe `human_verification` oben) — laut Auftragstext ausdruecklich Aufgabe des Orchestrators nach dieser Verifikation, nicht dieses Verifizierers.
---
_Verifiziert: 2026-09-14T15:15Z_
_Verifizierer: Claude (gsd-verifier)_
## Nachtrag des Orchestrators — Browser-Check durchgefuehrt (2026-09-14, 15:15-15:17Z)
Umgebung: lokale Container aus `main` (`77117de`) frisch gebaut (`docker compose up -d --build api web`), `mailhog` aus `docker-compose.dev.yml`, Playwright MCP (Chromium 153). Vorher musste die Platte freigeraeumt werden (`docker builder prune` 9,25 GB, `docker image prune` 4,85 GB; der erste Bau scheiterte mit `no space left on device`).
| Schritt | Beobachtung |
|---|---|
| Administrator -> SMTP | Feld "Fehlermeldungen an" mit Hinweistext vorhanden; Host `mailhog`, Port 1025, Keine Verschluesselung, Absender `tessera@tessera.local`, Empfaenger `fehler@example.invalid` gespeichert (DB: `bugReportRecipient = fehler@example.invalid`) |
| Kopfzeile | Knopf "Fehler melden" (aria-label) links neben dem Erscheinungsbild-Knopf; Seitenleiste unten: Abzeichen `dev · Entwicklung`, Tooltip `API dev (dev)` |
| Klick auf /admin/users | Dialog mit Hinweistext, Vorschau, Haekchen (an), Feld "Was ist passiert?", Abbrechen/Senden — die Vorschau zeigt die Seite OHNE den Dialog |
| Senden mit Beschreibung | "Vielen Dank, die Meldung wurde gesendet." |
| mailhog `GET /api/v2/messages` | 1 Nachricht, Von `tessera@tessera.local`, An `fehler@example.invalid`, Betreff `[Tessera Fehlermeldung] dev dev - /admin/users`; Text mit Beschreibung, Seite, Zeitpunkt (Server/Browser), Benutzer `admin`, Rolle, Mandant, Web `dev (dev)`, API `Tessera API dev (dev)`, Browser, Fenster `1920x949`, "Letzte Fehlermeldungen im Browser (0)" |
| Anhang | `fehlermeldung-20260914-1516.png`, 85619 Bytes, PNG-Signatur korrekt; Bild 1600 px breit, Farben der Seite korrekt (OKLCH), ohne Dialog |
| API-Protokoll | `Bug report email sent to fehler@example.invalid (transport: tenant)` und `Bug report from admin (tenant ...) sent to ... — page /admin/users, screenshot 85619 bytes` — ohne Beschreibung, ohne Bild |
| Gegenprobe ohne Empfaenger | `bugReportRecipient` auf NULL gesetzt, Senden -> "Fuer Fehlermeldungen ist noch kein Postfach eingerichtet." + Admin-Hinweis mit Link "Zu den SMTP-Einstellungen" (409-Pfad) |
| Drossel (429) | im Browser NICHT wiederholt (sechs Mails); durch die Spec gedeckt, die der Verifizierer unabhaengig falsifiziert hat (MAX_PER_WINDOW 5 -> 6 macht Test 3 rot) |
Nach der Probe: Empfaenger in der lokalen DB wieder gesetzt, Playwright-Artefakte entfernt, Arbeitsbaum unveraendert bis auf die Akten dieses Quick-Tasks.