Commit Graph

557 Commits

Author SHA1 Message Date
schalli 9c518238f5 feat(quick-260923-ad9): Datenmodell, Migration und Reiter-Grundlage - Task 1
Neues Modell Dashboard (D-01/D-02/D-09): position statt Standard-Feld,
kein Unique auf (userId, position) - Umsortieren schreibt spaeter alle
Positionen einer Transaktion neu. WidgetInstance/DashboardLayout haengen
jetzt am Reiter statt am Benutzer (DashboardLayout.dashboardId @unique
ersetzt userId @unique).

Migration 20260923120000_dashboard_tabs: Zeilenschutz mit Mandant- UND
Benutzerdimension (Form 20260911120000/20260921120000), Bestands-
uebernahme fuer jeden Benutzer mit Kacheln oder Anordnung VOR den
Fremdschluesseln (D-03) - gemessen: 0 Kacheln/Anordnungen ohne Reiter,
genau 2 Reiter auf Position 0.

dashboard.service.ts: listDashboards() (Transaktionssperre gegen
doppelte Erstanlage, T-AD9-07), Riegel assertOwnedDashboard() (fail-
closed gegen fremde Reiter, T-AD9-01/02/03) - getLayout/saveLayout/
getWidgets/addWidget laufen jetzt ueber dashboardId statt userId.
GET /dashboard/tabs neu; die vier bestehenden Wege reichen die Reiter-
Kennung durch. Verhalten fuer den Benutzer unveraendert (ein Reiter,
wie bisher) - Task 2 ergaenzt Anlegen/Umbenennen/Loeschen/Umsortieren.

dashboard.service.spec.ts: 43 Tests (31 alte unveraendert + 12 neue fuer
Reiter-Anlage, -Reihenfolge und den Fremdreiter-Riegel bei allen vier
Wegen). Zugriffsklassifikation nachgerechnet: 75 Paare (+1), Bereich
dashboard 21->24 gebunden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:56:38 +02:00
schalli d9f2af32d6 fix(quick-260922-vdk): Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus
- Messung an Ref-Rueckruf measureRef gehaengt statt an Effekt mit leerer
  Abhaengigkeitsliste: der Effekt sah den gemessenen Knoten nie, wenn er
  waehrend eines leeren Dashboards einhaengt, weil der fruehe Ruecksprung
  in den Leerzustand das <div> gar nicht rendert
- Synchrone Erstmessung in der Commit-Phase, vor dem ersten Zeichnen
- Fenster-Horcher als Netz, zusaetzlich zum ResizeObserver
- applyWidth verwirft 0 und nicht endliche Werte (T-VDK-01/T-VDK-03)
- Beobachter trennt sich im null-Zweig des Ref-Rueckrufs (T-VDK-02)
- Zwei neue Regressionstests (Test 10/11), zuerst rot nachgewiesen
- FREE_PLACEMENT_COMPACTOR, Konstanten, Leerzustand unveraendert

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 22:45:24 +02:00
schalli 8be0725577 docs(quick-260922-m1h): Hinweis bei gesperrter Kachel, Changelog und Entwicklerdoku
- eine Kachel ohne Bauteil (entfernter Typ oder gesperrtes Modul) zeigt
  statt des rohen Typnamens den Satz `widgets.unavailable`, zentriert und
  grau; Schluessel in de.json und en.json
- Entwicklerdoku: neuer Abschnitt "Eine Kachel zum Modul" im
  Modul-Walkthrough — die drei verbliebenen Stellen und was eine Kachel
  mit moduleSlug automatisch tut
- Changelog unter "Unveroeffentlicht - Geaendert"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:06:25 +02:00
schalli 56c07c3581 refactor(quick-260922-m1h): Widget-Typen an einer Stelle, Katalog aus der Registry, Kachel kennt ihr Modul
Ein neuer Widget-Typ war an sieben Stellen einzutragen; vergass man eine,
fehlte die Kachel im Katalog oder die API lehnte sie mit 400 ab.

- WIDGET_TYPES/WidgetType/WIDGET_MODULE_SLUGS stehen jetzt einmal in
  packages/shared; Registry, Katalog und die @IsIn-Whitelist der API
  leiten davon ab
- neun wireXWidget()-Funktionen durch ein generisches registerWidget()
  ersetzt (idempotent, unbekannter Typ wirft in der Entwicklung)
- der Katalog fuehrt keine zweite Typliste mehr, sondern leitet sie aus
  der Registry ab und filtert nach Modulzugriff (fail-closed, wenn die
  Modulliste unbekannt ist); der Abruf von /modules/active liegt auf der
  Dashboard-Seite, nicht im Dialog
- widget-module-map.ts liest die geteilte Tabelle statt einer Kopie, die
  oeffentliche Funktion bleibt unveraendert

Der Katalogfilter ist Komfort (T-M1H-01) — verbindlich bleibt der
serverseitige Filter in DashboardService.getWidgets.

Abweichung vom Plan: apps/web hing entgegen der Planannahme noch nicht
von @tessera/shared ab; die Abhaengigkeit wurde ergaenzt (Lockfile). Die
Dockerfiles kopieren packages/shared bereits, der Produktionsbau von
Next.js und der nest build laufen unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:03:58 +02:00
schalli 82472ee665 fix(quick-260922-hk4): fehlende Bilddatei aus der alten data-Spalte wiederherstellen
Beim Rundgang aufgefallen: nach dem Umzug zeigt eine Zeile auf eine Datei,
die es auf diesem Server nicht gibt — lokal, weil der Umzug am Host lief und
der Container ein eigenes Volume hat. Derselbe Zustand entsteht im Betrieb,
wenn jemand einen `pg_dump` von vor dem Umzug zurueckspielt: die Bytes
stecken noch in der Spalte `data`, das getrennt gesicherte Volume
`user-files` ist aber leer.

`getBytes` schreibt die Datei in diesem Fall aus `data` neu und liefert sie
aus, statt 404 zu melden. Fehlt beides, bleibt es bei 404. Nach dem
Entfernen der Spalte (eigenes Todo) faellt der Zweig ersatzlos weg.

api-Tests 1186 -> 1188, type-check und lint unveraendert gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 15:33:55 +02:00
schalli 9039cea686 refactor(quick-260922-hk4): Bilderrahmen-Bilder in user-files statt in der Datenbank
- Bytes liegen unter user-files/dashboard-images/<userId>/<id>.<ext>, die
  Zeile haelt nur noch storagePath (Muster User.avatarPath)
- Dateiname immer servergeneriert: UUID der Zeile + Endung aus dem
  ERKANNTEN Mime-Typ, originalName kommt in keinem Pfad vor (T-HK4-01)
- Migration 20260922120000: storagePath dazu, data wird NULLbar, kein DROP
  (zweistufig, T-HK4-03); system_read_policy fuer den Umzug
- onApplicationBootstrap zieht Altbestand automatisch um: systemgebunden
  lesen, je Zeile mandantengebunden schreiben (Muster DKV-Planer)
- Upload nimmt die Zeile bei fehlgeschlagenem Schreiben zurueck, Loeschen
  entfernt die Datei mit, fehlende Datei -> 404 (T-HK4-04)
- 11 neue Dienst-Tests gegen ein echtes Temp-Verzeichnis (kein fs-Mock)
- Zugriffsklassifikation: Stand system-gebunden, Zahlen nachgemessen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 15:22:47 +02:00
schalli cf70a197cd fix(quick-260922-ge2): Rahmenhoehe in der Kachel = Vorschauhoehe, Vorschau ohne Querbalken
Befund aus dem Browser-Rundgang: example.com setzt `margin: 15vh` — mit
3000 px Vorschauhoehe lag die Ueberschrift bei y 450, in der Kachel mit
720 px Rahmenhoehe bei y 108. Der in der Vorschau gewaehlte Ausschnitt
zeigte in der Kachel also etwas anderes. Die Kachel nutzt jetzt dieselbe
Layouthoehe wie die Vorschau (3000), damit vh-relative Seiten identisch
umbrechen; der Rest wird ohnehin weggeschnitten.

Vorschau: `overflow-x-hidden`, die Eckgriffe am rechten Rand erzeugten
einen 4-px-Querbalken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 12:22:37 +02:00
schalli 445b1d3100 feat(quick-260922-ge2): XFrame-Widget - Ausschnitt der Seite waehlen und einpassen, Zoom fuer die ganze Seite, Nur anzeigen
- Resolver: crop (x/y/w/h, geklemmt auf 1280er-Seite, x verschoben statt abgewiesen), zoom (50..150, groesste Stufe <= n), readOnly (nur echtes true)
- xframe-crop.ts: reine Geometrie - computeCropLayout (contain + zentriert, scale 0 bis gemessen), applyCropDrag (Verschieben, vier Ecken, Gegenecke bleibt)
- Kachel: Clip + verschobener, skalierter <iframe> bei fester Layoutbreite 1280, ResizeObserver am Koerper; Zoom-Zweig mit Prozentmassen; 100 % wie bisher; readOnly-Flaeche nur im Ansichtsmodus
- Formular: Checkbox Ausschnitt (ein Aufruf mit crop + readOnly), Vorschau 1280 px breit mit derselben Sandbox und pointer-events none, Rahmen mit vier Griffen (Pointer-Events, Capture-Waechter fuer jsdom), Zahlenfelder, Zoom-Auswahl nur ohne Ausschnitt, Nur anzeigen, dauerhafter Hinweis
- Rahmen als <fieldset> statt div role=group (Biome useSemanticElements, kein biome-ignore)
- Test-Helfer stubResizeObserver, 13 neue Schluessel de/en, Ausschnitt auf der Umlaut-Allowlist; Web-Tests 604 -> 640

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 12:12:18 +02:00
schalli 747a4d432b fix(desktop): Download-Knoepfe in der App reichen an den System-Browser weiter
Befund des Nutzers (22.09.2026): "Herunterladen" unter Einstellungen ->
Desktop-App tut in der App nichts, unter Windows wie Linux. Die Webansicht
hatte keinen Download-Handler; webkit2gtk verwirft Downloads dann still,
WebView2 zeigte ebenfalls nichts.

Das Hauptfenster entsteht jetzt im Code (app.windows in tauri.conf.json
leer), weil nur der Builder `on_download` annimmt. Der Handler bricht den
Download in der App ab und oeffnet die Adresse ueber den Opener im
System-Browser -- mit Fortschritt, Speicherort und Passwortfenster fuer
einen vorgeschalteten Proxy. Masse, Zentrierung, Titel wie bisher; die
Capability "main" gilt unveraendert.

cargo fmt/clippy/test/check gruen (44 Tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:43:27 +02:00
schalli d73aad1ef1 fix(desktop): Update-Eintrag nennt den Grund einer fehlgeschlagenen Pruefung, Klick prueft erneut, Pruefung alle 4 h
Befund 22.09.2026: alpha bietet 1.2.0-beta.gc001a08 an, der Client auf
a6d1a64 zeigt aber nur den grauen Eintrag "Update installieren". Der
Nginx Proxy Manager vor alpha beantwortet die Update-Anfrage mit 401
(Basic-Auth); die Webansicht kann das Passwortfenster beantworten, der
Updater (eigener reqwest-Client) nicht. tauri-plugin-updater verschluckt
einen Nicht-2xx-Status (updater.rs Z. 529-559: nur Log, last_error
leer, Ergebnis Err(ReleaseNotFound)), und spawn_version_check fing das
mit Err(_) => {} stumm ab. Ein fehlgeschlagener Check war damit vom
Zustand "kein Update" nicht unterscheidbar, und ohne Neustart gab es
keinen Weg, erneut zu pruefen.

- Drei Endzustaende, alle anklickbar: "Auf Version/Beta-Stand ...
  aktualisieren", "Kein Update verfuegbar - erneut pruefen",
  "Update-Pruefung fehlgeschlagen (HTTP n | keine Verbindung) - erneut
  pruefen"; waehrend der Pruefung "Suche nach Updates..." (gesperrt),
  http-Server unveraendert gesperrt
- Bei ReleaseNotFound stellt der Client dieselbe Anfrage einmal selbst
  (Platzhalter ersetzt, Timeout 8 s) und liest nur den Statuscode;
  401/403 erklaeren den Proxy-Passwortschutz, Zugangsdaten werden
  bewusst NICHT in den Client eingebaut
- Benachrichtigung je unterschiedlichem Fehlertext einmal
  (LastCheckNotice), Erfolg leert die Entprellung
- Wiederhol-Thread alle 4 h (std::thread, kein neues Crate), liest die
  Adresse frisch, ueberspringt bei bereits abgelegtem Update
- Klick ohne abgelegtes Update prueft erneut; der Browser-Weg bleibt nur
  Rueckfall einer fehlgeschlagenen Installation
- 7 neue Tests (Labels, Diagnose-URL, Konstanten), zuvor rot (E0425)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:26:09 +02:00
schalli 8b45a281be fix(dashboard): Bilderrahmen - "1 Stunde" statt "60 Minuten", Bildanzahl in der Einstellungs-Kopfzeile
Zwei Kosmetik-Punkte nach dem Browser-Rundgang vom 21.09.2026:

- Das laengste Wechselintervall (3600 s) hiess "60 Minuten", beim XFrame
  heisst dieselbe Stufe "Jede Stunde". Neuer Schluessel
  `pictureFrame.intervalHours` (ICU-Plural, de + en).
- Unter Einstellungen -> Dashboard zeigte "XFrame #1 - Board" seinen Titel,
  "Bilderrahmen #1" nichts. Die Kopfzeile nennt jetzt "- 1 Bild" bzw.
  "- N Bilder" (Schluessel `imageCountOne`/`imageCountMany`, zwei Schluessel
  statt ICU, weil die Panel-Tests eine einfache Uebersetzungs-Attrappe nutzen).

Web-Tests 603 -> 604, type-check und lint unveraendert gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 08:21:23 +02:00
schalli d63d9f5563 feat(quick-260921-qd3): XFrame-Widget - Webseite als Rahmen im Dashboard, Sandbox ohne Top-Navigation, Neuladen-Intervall
- xframe-config.ts: Resolver (https-Pruefung via isHttpsUrl des Bilderrahmens,
  Titel bis 100 Zeichen, Neuladen 0/60/300/600/1800/3600 s geklemmt),
  XFRAME_SANDBOX ohne allow-top-navigation und allow-modals; 12 Tests zuerst rot
- xframe-widget.tsx: genau ein <iframe> (sandbox, allow="", no-referrer, lazy),
  Kopfleiste mit Titel oder Ecksymbol "In neuem Tab oeffnen", Neuladen ueber
  key-Wechsel mit Timer-Raeumung, transparente Flaeche im Bearbeitungsmodus
  damit die Kachel Ziehgriff bleibt; 12 Tests zuerst rot
- xframe-config-form.tsx: Adresse/Titel mit Uebernahme bei Blur/Enter, http wird
  mit Meldung abgewiesen und nicht gespeichert, Intervall-Auswahl, dauerhafter
  Hinweis auf verweigertes Einbetten; 8 Tests
- Panel-Zweig samt "— Titel" in der Kopfzeile, Registry (12x12, Fenster-Symbol),
  Katalog, Seite, DTO @IsIn, de/en widgets.xframe (15 Schluessel), Umlaut-Allowlist
  "neuem"; der Server ruft die Adresse nie ab

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 19:18:48 +02:00
schalli 8bf3601a18 fix(quick-260921-pi9): Grossansicht per Portal, "1 Minute" statt "1 Minuten", Kachel-Vorgabe 8x12
Befunde aus dem Browser-Rundgang am 21.09.2026:

- Die Grossansicht lag in einem `react-grid-item` mit CSS-`transform`; ein
  transformierter Vorfahr wird fuer `position: fixed` zum Bezugsrahmen, der
  Dialog war deshalb auf die Kachelflaeche (531x216) beschraenkt statt den
  Viewport zu fuellen. Jetzt per `createPortal` in `document.body`, wie der
  Kalender-Tooltip.
- Wechselintervall "1 Minuten" -> ICU-Plural (`one {# Minute}`), de + en; der
  Formular-Test nutzt dafuer den echten `createTranslator` von next-intl auf
  der echten de.json statt eines `{n}`-Ersatzes.
- Standardgroesse 8x8 (216 px hoch) war zu flach fuer ein Foto -> 8x12 wie die
  Kalender-Vorgabe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 19:10:52 +02:00
schalli c3b45974f5 docs(quick-260921-pi9): Changelog und Anwenderhandbuch - Bilderrahmen-Widget
- CHANGELOG.md: erster Stichpunkt unter Unveroeffentlicht -> Neu
- docs/anleitung-anwender.md: Zeile in der Widget-Tabelle, Absatz zur
  Bildverwaltung unter Dashboard > Widgets
- umlaut-dictionary.ts: „Webadresse“ und „Bildausschnitt“ als korrektes
  Deutsch auf die Erlaubnisliste des Umlaut-Waechters (der volle Web-Testlauf
  hatte die beiden neuen ss-Woerter aus de.json gemeldet)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:59:06 +02:00
schalli c080580459 feat(quick-260921-pi9): Bilderrahmen-Widget - Diashow mit Grossansicht, Bildverwaltung in den Einstellungen
- picture-frame-config.ts: Eintragstyp als Vereinigung (upload | url) in EINER
  geordneten Liste, resolvePictureFrameConfig laesst alles ausser https weg
  (T-PI9-07), Intervall 0 oder 5..3600 s, pickNextIndex (Zufall nie dasselbe)
- dashboard-images-api.ts: Upload als FormData-Feld image ohne eigenen
  Content-Type, 413 -> deutsche Meldung, Proxy-Pfad fuer <img src>
- PictureFrameWidget: Leerzustand, <img referrerPolicy=no-referrer> (Browser
  laedt Fremdbilder, Server nie), object-contain/cover, Unterschrift-Streifen,
  Wechsel per Timer mit Raeumung, kaputte Bilder verlassen den Umlauf,
  Grossansicht nur ausserhalb des Bearbeitungsmodus mit Fokus-Rueckgabe und
  Pause des Wechsels; im Bearbeitungsmodus kein Knopf (Karte bleibt Griff)
- PictureFrameConfigForm im WidgetSettingsPanel: Ausschnitt, Intervall,
  Reihenfolge, Liste mit Vorschau/Unterschrift/Pfeilen/Entfernen (Upload wird
  auch serverseitig geloescht), Datei hochladen, https-Adresse hinzufuegen
- Registry (4x4 min, 8x8 Vorgabe), Katalog, Seite, Uebersetzungen de/en
  (widgets.pictureFrame, 28 Schluessel), bestehende Tests auf acht Typen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:56:02 +02:00
schalli 737974b653 feat(quick-260921-pi9): Bilderrahmen-API - Bilder je Benutzer in der Datenbank, Magic-Byte-Pruefung, 5 MiB / 30 Stueck
- Prisma-Modell DashboardImage (bytea) mit Migration 20260921120000: Tabelle,
  Indizes, RLS ENABLE/FORCE und tenant_isolation_policy mit Benutzerdimension
- dashboard-image-rules.ts: detectImageMime ueber Magic Bytes (PNG/JPEG/GIF/
  WebP), Grenzen 5 MiB je Datei und 30 je Benutzer
- DashboardImagesService: list/upload/getBytes/remove, je Methode
  forTenant(prisma, tenantId, userId); Besitz = Mandant UND Benutzer, sonst 404
- DashboardImagesController unter dashboard/images: GET, POST (FileInterceptor
  image, 5 MiB, eine Datei), GET :id mit Content-Type aus dem erkannten Typ,
  Cache-Control private, nosniff, Content-Disposition inline ohne Dateinamen,
  CSP sandbox; DELETE :id
- CreateWidgetDto kennt 'picture-frame'
- Klassifikationsdokument: neues Paar dashboard-images.service.ts/
  dashboardImage; Bereichs- und Summenzeilen nachgemessen (dashboard 12->18,
  settings 3->4 und bug-reports waren in der Summe nie mitgezaehlt)
- Befund: Prisma-Bytes verlangt Uint8Array<ArrayBuffer>, multers Buffer wird
  ohne Zusicherung abgelehnt - Kopie per new Uint8Array(buffer) statt Cast

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:47:07 +02:00
schalli d0266bf86e fix(quick-260921-oxm): STARTTLS wirklich erzwingen, Anhangs-Dateinamen richtig lesen
B-06: requireTLS durch doSTARTTLS ersetzt. requireTLS kennt imapflow 1.4.3
nicht und verwirft es still; ein als STARTTLS eingerichtetes Postfach konnte
deshalb unbemerkt im Klartext verbinden. Gewollte Folge: so ein Postfach
scheitert jetzt, wenn der Server kein STARTTLS anbietet. Bei ssl-tls ergibt
der Ausdruck false, was die Unvertraeglichkeit secure=true + doSTARTTLS=true
gar nicht erst entstehen laesst.

B-05: Dateiname aus Content-Disposition kommt jetzt aus dem Feld, in dem
imapflow ihn ablegt (dispositionParameters), statt aus .parameters einer
Zeichenkette. Der alte Ausdruck war zur Laufzeit immer undefined, wodurch
Anhaenge als application/octet-stream (typisch Outlook) nicht erkannt wurden.

Die Zusicherung am Ende von buildClient() entfaellt ersatzlos; sie bestand
nur wegen der unbekannten Option. noExplicitAny in apps/api/src faellt damit
von 15 auf 13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 18:03:21 +02:00
schalli 7691d1fd6d test(quick-260921-oxm): rote Tests fuer die beiden IMAP-Befunde
- B-06: prueft die an ImapFlow uebergebenen Optionen fuer starttls und ssl-tls
- B-05: prueft, dass ein octet-stream-Anhang am Dateinamen aus
  Content-Disposition erkannt wird
- Einhaengen des Testdoppels in einen Helfer gezogen; die Umdeutung des
  Konstruktors steht damit nur noch an einer Stelle statt an zwoelf

Gegen den heutigen Stand rot: 3 von 12 Faellen scheitern
(doSTARTTLS undefined, Feld requireTLS vorhanden, Anhang nicht eingesammelt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 18:00:53 +02:00
schalli d8fb9ae07d refactor(quick-260921-m34): Aufgabe 3c - Randschicht beurteilt, drei Befunde gemeldet, 15 bleiben mit Urteil
httpntlm (exchange.provider, exchange-inbox.provider): NtlmOptions und
NtlmResponse beschreiben genau das, was uebergeben und gelesen wird. Die
ueberfluessige Zusicherung (httpntlm as any) faellt weg.

Graph-Rueckrufe (exchange.provider :157/:313): AuthProviderCallback aus dem
SDK selbst statt Handannotation - als import type, also ohne den dynamischen
Import zur Laufzeit zurueckzunehmen.

imapflow: streamToBuffer() nimmt Readable statt NodeJS.ReadableStream (alle
drei Aufrufer reichen client.download().content herein, imapflow deklariert
das als Readable) - damit traegt der Typ destroy() und die Zusicherung
faellt. node.parameters?.name war ebenfalls schon getypt.

nodemailer: ResolvedTransport.options wird SMTPTransport.Options; beide
Zweige bauen reine SMTP-Optionen, createTransport() nimmt sie ohne
Zusicherung.

node-forge: die vier let p7: any werden Captured<PkcsEnvelopedData |
PkcsSignedData> - der MITGELIEFERTE Typ. Die Lesestellen grenzen mit
'certificates' in p7 ein statt zuzusichern; verhaltensgleich, weil der
enveloped-Form das Feld fehlt und beide Schreibweisen dann die leere Liste
liefern. cert.siginfo war bereits getypt.

apps/web/src/test/setup.ts: expect.extend(matchers) traegt ohne Zusicherung
- geprueft im echten Typlauf (setup.ts liegt im include von
apps/web/tsconfig.json, mit einem absichtlichen Fehler nachgewiesen).

BEFUND 4 (D-03, gemeldet, NICHT repariert) imap.provider.ts:78 - der
Ausdruck (node as any).disposition?.parameters?.filename liest .parameters
von einer ZEICHENKETTE: imapflow deklariert disposition als string
(imap-flow.d.ts:448), die Parameter liegen in dispositionParameters (:450).
dispositionFilename ist damit zur Laufzeit immer ''. Folge: Outlook-Anhaenge,
die als application/octet-stream kommen, werden ueber den Dateinamen aus
Content-Disposition NICHT erkannt - nur ueber den aus Content-Type. Umbiegen
waere eine Verhaltensaenderung; die Zusicherung bleibt sichtbar stehen.

BEFUND 5 (D-03, gemeldet, NICHT repariert) imap.provider.ts:402 -
requireTLS kommt in imapflow 1.4.3 NIRGENDS vor, weder in ImapFlowOptions
noch im Laufzeitcode (beides durchsucht). Die Option wird still verworfen;
STARTTLS wird durch sie nicht erzwungen. Genau das } as any hat es
verdeckt. Bleibt stehen, damit der Befund in der Zaehlung sichtbar ist.

BEFUND 6 (D-03, gemeldet, Verhalten unveraendert) httpntlm liefert den
Rumpf als Zeichenkette, nicht als Buffer: httpreq setzt ihn nur bei
gesetzter Option binary auf Buffer (httpreq@1.1.1/lib/httpreq.js:391),
keiner der beiden Aufrufer setzt sie. Der Bestand rief unbesehen
.toString('utf-8') auf - das ging nur gut, weil String.toString() sein
Argument ignoriert. Die Testdoppel reichen dagegen wirklich Buffer herein.
NtlmResponse.body nennt jetzt beide Formen, die Fallunterscheidung liefert
fuer jede exakt dasselbe Ergebnis wie zuvor.

Urteil BLEIBT mit Begruendung im Code an allen 15 verbleibenden Stellen:
3x addCronJob (require-Umweg aus 07-04), 5x node-forge (EC-Zweig und
extensions: any[] sind in @types/node-forge nicht beschrieben, 2x null as
any wo die Typen die Bibliothek nachweislich falsch beschreiben), 2x
imap-Befunde oben, 2x tx: any plus 2x Gefolge (Aufgabe 1), 1x
disposition-Befund.

noExplicitAny in apps/api/src: 31 -> 15 (Ausgang 288, Schranke 45), apps/web
1 -> 0. type-check 4/4, lint 5/5 (0 error), apps/api 72/1143, apps/web
73/531, rls-access-inventory 30/30. noNonNullAssertion 56, as unknown as 33,
ts-expect-error/ts-ignore 0/0, Unterdrueckungsmarker 1. biome.json, alle
package.json und pnpm-lock.yaml unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 17:29:28 +02:00
schalli 3892c5f3c6 refactor(quick-260921-m34): Aufgabe 3b - Prisma-nahe Formen getypt, Erkenner-Falle gemeldet
- auth.service.ts:393: (response as any).cookie war schlicht ueberfluessig.
  response ist in derselben Signatur bereits Response aus express, die
  Schwesterstelle :190 kommt ohne Zusicherung aus. Ersatzlos entfernt.
- calendar.service.ts: das lokal gebaute data-Objekt traegt jetzt
  Prisma.CalendarSourceUncheckedCreateInput bzw. ...UncheckedUpdateInput
  statt Record<string, unknown> plus Zusicherung. Damit fallen beide
  `data as any` weg, ohne dass ein Feld behauptet wird.
- user.service.ts: `let created: any` -> User (die Zuweisung steht im try,
  der catch endet ausnahmslos mit throw). `const updateData: any` wird aus
  der Signatur hergeleitet - Omit<UpdateUserInput, 'password'> plus dem
  daraus berechneten passwordHash; die Parameterform ist dafuer als
  UpdateUserInput benannt und nicht neu erfunden. `const results: any[]`
  wird Pick<User, keyof typeof PLATFORM_USER_SELECT>[], die Spaltenauswahl
  steht als Konstante daneben.
- tenant.controller.ts:69: Elementtyp aus dem hergeleitet, was die Schleife
  hineinlegt (fuenf Tenant-Spalten plus userCount).

BEFUND 3 (D-03, gemeldet, kein Verhalten betroffen) Die naheliegende
Prisma-Schreibweise Prisma.UserGetPayload<{ select: typeof X }> laesst
rls-access-inventory.spec.ts rot werden: der Erkenner zaehlt JEDE
select:-Angabe ausserhalb eines erkannten Modellaufrufs als Verstoss und
unterscheidet Typposition nicht von Aufrufposition. Gemessen beim ersten
Versuch. Der Erkenner ist die Mandantenkontrolle (T-M34-03) und wurde
NICHT aufgeweicht - stattdessen leitet der Zeilentyp ueber Pick<User, ...>
her, was ohne das Wort select auskommt. Begruendung steht am Typ.

noExplicitAny in apps/api/src: 38 -> 31. type-check 4/4, lint 5/5 (0
error), apps/api 72/1143, apps/web 73/531, rls-access-inventory 30/30.
noNonNullAssertion 56, as unknown as 33, Unterdrueckungsmarker 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 17:21:53 +02:00
schalli 32591b6690 refactor(quick-260921-m34): Aufgabe 3a - 18 Fehlerfaenger auf unknown, mit echter Eingrenzung
catch (e: any) in groups, module-grants, ldap, user, admin-seed, calendar
und vier tenders-Diensten auf catch (e: unknown) umgestellt. Die
Eingrenzung passiert an der Verwendungsstelle, nicht per Zusicherung.

Neu: apps/api/src/prisma/prisma-error.ts mit prismaErrorCode() und
prismaErrorTarget(). Bewusst Form-Pruefungen statt instanceof
Prisma.PrismaClientKnownRequestError - gemessen: samtliche Testdoppel in
apps/api werfen new Error(...) mit angehaengtem .code (groups, user, ldap,
tenders, module-grants, admin-seed) und dashboard.service.spec.ts:451 ein
reines { code: 'P2002' }. Ein instanceof-Test haette all diese Werte in den
anderen Zweig geschickt - Verhaltensaenderung, verboten nach D-03/T-M34-06.
Die Helfer bilden err?.code und err?.meta?.target eins zu eins ab.

ldap.service.ts liest zusaetzlich meta.target; prismaErrorTarget() gibt
unknown zurueck, weil der Bestand dort Array UND Zeichenkette getrennt
behandelt - ein engerer Typ waere eine Behauptung.

calendar.service.ts:341 nutzt instanceof Error statt e?.message: gemessen
wirft validateUrlNotPrivate() ausschliesslich ForbiddenException (der
eigene catch dort setzt jeden Fremdfehler in eine um), also trifft
instanceof dieselben Faelle. Ersatzzweig 'URL not allowed' unveraendert.

noExplicitAny in apps/api/src: 56 -> 38. type-check 4/4, lint 5/5 (0
error), apps/api 72/1143, apps/web 73/531, rls-access-inventory 30/30.
noNonNullAssertion 56, as unknown as 33, Unterdrueckungsmarker 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 17:18:12 +02:00
schalli 52668c2c88 refactor(quick-260921-m34): Aufgabe 2c - Hochladewege getypt, 25 Zusicherungen fallen mit
@UploadedFile()/@UploadedFiles() in cert-manager.controller auf
UploadedFileLike. Der Dienst nimmt CertFileLike = Pick<UploadedFileLike,
'buffer' | 'originalname'> - genau die zwei Felder, die er liest; mimetype
und size bleiben draussen, weil kein Zweig sie anfasst.

Belegt statt behauptet: keiner der sechs FileInterceptor/FilesInterceptor-
Aufrufe in apps/api/src setzt eine storage-Option, also gilt multers
memoryStorage, also ist buffer ein Buffer. @types/multer bleibt
uninstalliert (D-04).

Damit fallen 25 Zusicherungen der Form file.buffer as Buffer und
file.originalname as string ersatzlos weg - sie standen nur da, weil file
ein any war. as unknown as bleibt bei 33, noNonNullAssertion bei 56.

noExplicitAny in apps/api/src: 66 -> 56 (Ausgang der Aufgabe: 149,
Schranke des Plans: 75). type-check 4/4, lint 5/5, apps/api 72/1143,
apps/web 73/531.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 17:11:28 +02:00
schalli 7c9d7c1223 refactor(quick-260921-m34): Aufgabe 2b - getypte Anfrage in elf Controllern, zwei Befunde gemeldet
(req as any) und @Req() req: any durch AuthenticatedRequest ersetzt in
dashboard, favorites, calendar, groups, module-grants, module-registry,
tenders, dkv, ldap, settings; @CurrentUser() in user.controller auf AuthUser.
Die abwehrenden Pruefungen ("No tenant context", "No user context") bleiben
lebendig, weil user auf dem Anfragetyp wahlfrei ist - genau das beschreibt
den Zustand auf oeffentlichen Wegen.

Nebengewinn ohne neue Zusicherungen: req.tenantId as string | undefined
(dkv, settings), file.buffer as Buffer und file.mimetype as string
(dkv, user) sind weggefallen, weil der Typ sie jetzt traegt.

BEFUND 1 (D-03, gemeldet) dashboard.controller.ts:74 alt: der Handler las
req.user?.role NACH extractContext und gab sie an getWidgets(role: Role)
weiter, das eine Rolle zwingend verlangt. Die Annahme "hier gibt es immer
einen Aufrufer" stimmt - die Pruefung "No user context" erzwingt sie -, aber
sie stand in einer anderen Methode, wo der Compiler sie nicht sehen konnte.
extractContext gibt die Rolle jetzt mit zurueck: keine neue Pruefung, kein
erfundener Wert, gleiche Reihenfolge, gleiche Meldungen.

BEFUND 2 (D-03, gemeldet) tenders.controller.ts:142: resolveRequestingTenantId
erklaerte string | undefined, liest aber req.tenantId, das TenantGuard fuer
einen SUPER_ADMIN ohne Mandanten auf null setzt. Die Erklaerung war also nie
vollstaendig. Erweitert auf string | null | undefined, und buildTenderWhere
nimmt string | null - beides nur Erklaerung, kein Verhalten: die Funktion
entscheidet seit jeher ueber Wahrheitswert und faellt bei beiden zu
(nur global sichtbare Ausschreibungen).

Fixtures in user.controller.spec.ts ergaenzt (username, mustChangePassword,
originalname, size). Testzahlen unveraendert.

noExplicitAny in apps/api/src: 137 -> 66. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531, tenant.guard.ts unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 17:08:44 +02:00
schalli f2fc39f51c refactor(quick-260921-m34): Aufgabe 2a - gemeinsamer Aufrufer-Typ, aus den Signierstellen abgeleitet
apps/api/src/auth/types/auth-user.ts angelegt: AuthUser, AuthenticatedRequest,
LocalAuthenticatedRequest, LoginUser, JwtPayload, UploadedFileLike. Jedes Feld
traegt seine Herkunft als Kommentar.

tenantId ist string, hergeleitet und nicht gewaehlt: die Spalte User.tenantId
ist in schema.prisma Pflicht, beide Signierstellen schreiben genau sie, und
der Bestand beschreibt dasselbe Objekt in SessionUser schon so. Der
SUPER_ADMIN-Zweig in TenantGuard spricht nicht dagegen - der Waechter liest
AuthUser gar nicht, und dass es den Zweig gibt, steht als null in
AuthenticatedRequest.tenantId weiter im Typsystem. tenant.guard.ts bleibt
unberuehrt.

role ist die Aufzaehlung Role: schema.prisma deklariert die Spalte so, die
SQL-Funktion auth_lookup_user_by_username gibt sie als "Role" zurueck. Die
Handannotation role: string in AuthLookupUserByUsernameRow war eine zweite
Fassung desselben Wertes und faellt damit weg.

SessionUser und UploadedPng in bug-reports.service.ts sind jetzt Pick<> der
neuen Typen statt eigener Beschreibungen.

Fixtures in auth.controller.spec.ts ergaenzt: sie uebergaben einen Aufrufer
ohne username und ohne mustChangePassword - eine Form, die JwtStrategy nie
erzeugt. Testzahlen unveraendert.

noExplicitAny in apps/api/src: 149 -> 137. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 17:03:24 +02:00
schalli b188946e31 refactor(quick-260921-m34): Aufgabe 1 - Mandantenbindung entzaubert, 105 unnoetige any-Zusicherungen entfernt
- prisma-tenant.extension.ts: (prisma as any) und die Handannotation an
  $allOperations in forTenant()/forSystem() entfernt; Kopfkommentar
  unveraendert. .then((results: any[]) => ...) auf unknown[] umgestellt.
- 105 Aufrufstellen `const X = forTenant(...) as any` / `forSystem(...) as
  any` von der Zusicherung befreit, Zuweisungsform woertlich erhalten
  (rls-access-inventory.spec.ts bleibt scharf, 30/30 gruen einzeln
  geprueft).
- withTenantTransaction(): Prisma.TransactionClient fuer tx probiert,
  gemessen verworfen - bricht das Testdoppel in
  prisma-tenant.extension.spec.ts (TS2322 auf einem absichtlich
  unvollstaendigen Fake-Objekt). tx bleibt any, mit Begruendung am Typ.
- Gefolge des jetzt getypten Klienten entfernt: any[]-Annotationen und
  .map((x: any) => ...) in groups.service.ts, module-grants.service.ts,
  dkv.service.ts, ldap-config.service.ts, tenders.controller.ts:270.
- Befund (D-03): tender-matching.service.ts:159 trug eine Handannotation
  (match: { tender: unknown }), die den Wert nur deshalb auf unknown
  verengte, um TS7006 unter dem alten any-Klienten zu vermeiden - mit dem
  getypten Klienten war das falsch. Annotation geloescht, kein Ersatz
  durch Zusicherung.
- Zwei any bleiben gezielt in groups.service.ts (u/a in
  ensureDefaultGroup(), gefolge von tx: any) - Begruendung am Code.

noExplicitAny apps/api/src: 288 -> 149 (Schranke 155). type-check 4/4,
lint 5/5 (0 error). apps/api 72/1143 gruen, apps/web 73/531 gruen,
rls-access-inventory.spec.ts 30/30 gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 16:19:16 +02:00
schalli 9f02fcc190 test(quick-260921-ldf): Test 14/15 halten jeden Commit fest statt nur den Endzustand
Der alte Test 1 fand den Fehler nur durch Zufall: er las den DOM einmal,
nachdem findByRole den Dialog gemeldet hatte, und traf damit mal den
falschen ersten, meist den richtigen zweiten Commit. Ein Fehlschlag auf
rund siebzehn volle Laeufe.

Die beiden neuen Tests beobachten stattdessen per MutationObserver JEDEN
Commit waehrend des Oeffnens und dulden keinen einzigen, in dem das
Vorschaubild sichtbar ist und das Haekchen aus. Test 14 deckt das erste
Oeffnen ab, Test 15 das erneute Oeffnen nach einem Versand mit
abgewaehltem Haekchen und getipptem Text - dort darf weder der alte
Dankestext noch der alte Text noch ein ausgeschaltetes Haekchen in
irgendeinem Commit auftauchen.

Beide sind gegen den Stand vor dem Fix in fuenf von fuenf Laeufen rot,
danach gruen. Kein retry, kein hoeheres Zeitlimit: die Ursache war nie
blosse Zeit, sondern ein falscher Zustand, den es jetzt nicht mehr gibt.
Test 1 bleibt unveraendert in der Sache und prueft weiterhin dieselbe
Zusicherung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 15:38:54 +02:00
schalli de7fdb7377 fix(quick-260921-ldf): Fehler-melden-Dialog stimmt ab dem ersten Commit, nicht erst einen spaeter
Gemessene Ursache des Wackeltests aus CI-Lauf 395, und es ist ein
Produktfehler, kein Testfehler. Ein MutationObserver ueber jeden
DOM-Commit beim Oeffnen protokollierte:

  COMMIT dialog=true img=ja box=AUS   <- falsch, aber festgeschrieben
  COMMIT dialog=true img=ja box=AN

Der erste Zustand entstand bei JEDEM Oeffnen, nicht nur unter Last, und
hielt ohne act() zwei volle Makrotask-Runden - der Browser hat in dieser
Zeit mindestens zwei Gelegenheiten, ihn zu zeichnen. Ein Nutzer sieht
also sein Vorschaubild kurz mit ausgeschaltetem Haekchen. Der Test fiel
nur dann durch, wenn er zufaellig den ersten statt den zweiten Commit
sah; die Last im vollen Lauf war der Ausloeser, nicht die Ursache.

Zwei Bedingungen mussten zusammentreffen. Erstens war der Dialog
dauerhaft eingehaengt und gab bei geschlossenem Zustand nur null zurueck
- useState(screenshot !== null) lief damit ein einziges Mal, beim
allerersten Mount des Knopfs, als noch gar kein Bild da war. Das
Haekchen startete also immer aus. Zweitens zog ein useEffect den
Zustand nach, und passive Effekte laufen erst NACH dem Commit.

Beides ist jetzt weg. Der Dialog wird nur noch eingehaengt, solange er
offen ist, also ist jedes Oeffnen ein frischer Mount mit frischem
Zustand. Und das Haekchen wird beim Rendern aus screenshot abgeleitet
statt per Effekt nachgezogen; attachChoice haelt allein die bewusste
Abwahl des Nutzers. Der Effekt, der Status, Text und Haekchen beim
Oeffnen zuruecksetzte, entfaellt ersatzlos.

Damit verschwindet dieselbe Klasse an einer zweiten Stelle: beim
erneuten Oeffnen nach einem Versand stand bisher zwei Runden lang der
alte Danke-Bildschirm im DOM, bevor das frische Formular erschien.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 15:38:38 +02:00
schalli c0ab5b5584 test(quick-260921-ldf): zwei Konstruktionsfehler im Fehler-melden-Test behoben
Beide Befunde sind nicht die Ursache des Wackeltests aus CI-Lauf 395,
aber beide haetten die Fehlersuche in die Irre fuehren koennen.

Erstens stand ein expect INNERHALB der toPng-Attrappe. Wirft es, landet
der Fehler mitten im await von captureScreenshot, und dessen catch
liefert still null zurueck. Der Test waere dann nicht an der Stelle
durchgefallen, die er prueft, sondern viel spaeter mit der Meldung, es
gebe kein Vorschaubild. Die Attrappe haelt die Beobachtung jetzt nur
noch fest, geprueft wird sie im Testkoerper. Bewiesen wird dasselbe:
zum Zeitpunkt der Aufnahme steht kein role="dialog" im DOM.

Zweitens wurden scrollWidth und scrollHeight von document.body per
Object.defineProperty ueberschrieben und nie zurueckgesetzt. Die eigene
Eigenschaft verdeckt den Getter von Element.prototype, und document.body
ueberlebt cleanup() - alle zwoelf folgenden Tests der Datei sahen
weiterhin 3200x1000. stubBodyGroesse merkt sich das jetzt, afterEach
nimmt es per Reflect.deleteProperty wieder zurueck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 15:38:21 +02:00
schalli f471b785db fix(quick-260921-jt4): verschwendete Kalender-Abrufe abgestellt, t-Identitaet gemessen statt behauptet
Restposten 3a (doppeltes Ladefenster): lastFetchWindowRef merkt sich das
zuletzt TATSAECHLICH geholte from/to-Paar; ein Monatswechsel, dessen
berechnetes Fenster damit uebereinstimmt, ueberspringt fetchEvents. Der
5-Minuten-Auffrischer (force=true) umgeht den Sperrgriff immer, sonst
friert die Anzeige ein. computeFetchWindow und die Tagesgrenzen-Rundung
bleiben unangetastet.

Restposten 3b (Quellenliste je Monatswechsel): hasSourcesRef merkt sich
das Ergebnis; fetchSources laeuft nur beim Aufbau (Merkung leer) oder
erzwungen (Auffrischer) — eine neu eingerichtete Quelle wird weiterhin
binnen fuenf Minuten bemerkt.

Restposten 4 (t-Identitaet): neue translations-identity.test.tsx rendert
eine Testkomponente unter dem ECHTEN NextIntlClientProvider und beweist
per Referenzgleichheit, dass t bei einem lokalen Zustandswechsel
dasselbe Funktionsobjekt bleibt — bestaetigt durch den use-intl-4.13.0-
Quelltext (translate entsteht in einem useMemo, dessen Abhaengigkeiten
ausschliesslich aus dem root-staendigen Intl-Kontext stammen). Die
Faustregel aus 260921-gof ("t gehoert in keine Abhaengigkeitsliste")
bleibt als Konvention in Ordnung; die zugrunde liegende Annahme ("t ist
bei jedem Render frisch") ist damit ausdruecklich WIDERLEGT statt ein
drittes Mal weitergetragen. Die vier verbliebenen Stellen
(marketplace/page.tsx, admin/users/page.tsx,
calendar-settings-panel.tsx, calendar-source-form.tsx) bleiben deshalb
unveraendert.

Vier neue zaehlende Testfaelle in calendar-widget.test.tsx (Aufbau je 1,
abweichendes Fenster +1/+0, identisches Fenster +0/+0, erzwungener Lauf
+1/+1) plus ein Test, dass ein uebersprungener Lauf den Ladezustand
sauber beendet und geladene Termine nicht leert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 15:10:10 +02:00
schalli 6c10c9bbe0 feat(quick-260921-jt4): uebersetzter ZIP-Name im Zertifikat-Aufteiler mit Windows-Schutzfunktion (Restposten 1)
downloadAllAsZip liegt ausserhalb der Komponente und kann den
Uebersetzungs-Hook nicht aufrufen; der Name kommt jetzt als Parameter
herein, Aufrufstelle uebergibt t('actions.zipFilename'). Neuer
Schluessel unter certManager.actions: deutsch "Zertifikate.zip",
englisch "certificates.zip".

Der 260921-bi2-Einwand (ein uebersetzter Name koenne Umlaute auf eine
Windows-Freigabe tragen) trifft fuer diesen konkreten Text nicht zu —
das deutsche Wort enthaelt keinen Umlaut. Die Sicherheit haengt darauf
aber NICHT: neue Datei zip-filename.ts mit einer fuer sich pruefbaren
Schutzfunktion, die Windows-verbotene Zeichen, Steuerzeichen und
Nicht-ASCII ersetzt, abschliessende Punkte/Leerzeichen entfernt,
reservierte Geraetenamen abfaengt, bei leerem Ergebnis auf
certificates.zip zurueckfaellt und die .zip-Endung sicherstellt.
SplitTab.tsx schickt den uebersetzten Namen durch diese Funktion, bevor
er am Download landet. Die Dateinamen IM Archiv bleiben unangetastet.

zip-filename.test.ts deckt beide Katalogwerte (unveraendert), Umlaut,
verbotenes Zeichen, Steuerzeichen, abschliessende Punkte/Leerzeichen,
fehlende/vorhandene Endung, leeres Ergebnis und reservierte
Geraetenamen ab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 15:02:08 +02:00
schalli 5a03b75f1e refactor(quick-260921-jt4): ueberfluessige case-Marke in tender-normalizer.service.ts entfernt (Restposten 2)
Die Fallmarke 'doe-opendata' stand unmittelbar ueber default und fiel
in denselben Zweig — 260921-bi2 liess sie stehen, weil der Kommentar
darunter Absicht dokumentiert. Die Absicht laesst sich ohne die
Fallmarke ausdruecken und wird dabei deutlicher: der erweiterte
Kommentar traegt jetzt beide Aussagen (DÖE-Quelle landet hier UND
kuenftige additive SourceType-Mitglieder sollen ebenfalls hier landen
statt zu scheitern). Kein Verhaltenswechsel — derselbe Zweig wie
vorher. Belegt durch die vorhandenen 18 tender-normalizer-Tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:57:40 +02:00
schalli 69fe706580 fix(quick-260921-jt4): vier <img onError>-Avatarbilder tragen aria-hidden (D-03)
header.tsx, account-settings-form.tsx, favorites-widget.tsx (zweimal):
alle vier tragen bereits alt="", sind also schon aus dem
Zugaenglichkeitsbaum genommen; aria-hidden sagt dasselbe nur
ausdruecklich. Ehrliche Einordnung: richtige Auszeichnung, verbessert
fuer keinen Menschen etwas — der Befund verschwindet, weil die Regel
ein verborgenes Element nicht mehr betrachtet. onError ist ein
Ladefehler, keine Bedienung: hier gab es nie einen Tastaturweg zu
schaffen. Keine Unterdrueckung, kein biome-ignore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:57:01 +02:00
schalli f7b5df4db8 fix(quick-260921-jt4): zwei Schaltflaechenreihen tragen role="toolbar" (D-03)
calculator-widget.tsx Speicherzeile und favorites-widget.tsx
Ansichtsumschalter waren schlichte <div> mit aria-label, das die Rolle
generic stillschweigend verwarf. role="toolbar" ergaenzt (geprueft
sauber; role="group"/"region" loesen useSemanticElements neu aus).

Taschenrechner: fest verdrahteter Text wandert in
calculator.memoryLabel; bei dieser Gelegenheit auch Anzeigefeld
(displayLabel) und Rueckschritt-Taste (backspaceLabel) in den Katalog
gezogen (D-05), da diese Datei ohnehin geaendert wird.

Favoriten: die bisherige Beschriftung ("Favoriten") war sachlich
falsch fuer einen Listen/Kachel-Umschalter — neuer, zutreffender
Schluessel favorites.viewModeLabel statt des wiederverwendeten
favorites.name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:56:02 +02:00
schalli b406a9c5f7 fix(quick-260921-jt4): drei Statussymbole in calendar-settings-panel tragen role="img" (D-03)
Die drei <span> mit aria-label ("Sync error"/"Connection OK"/
"Connection error") hatten die Rolle generic, die keine ARIA-Merkmale
traegt — die Beschriftung wurde stillschweigend verworfen. role="img"
ergaenzt (geprueft sauber, keine useSemanticElements-Neuausloesung).
Die Beschriftung ist hier der einzige Text des Symbols (svg bereits
aria-hidden) — Ursache behoben, nicht das Attribut gestrichen.

Erfolgsfall nutzt den vorhandenen Schluessel calendar.connectionSuccess
woertlich; fuer die beiden Fehlerfaelle neue kurze Schluessel
syncErrorLabel/connectionFailedLabel (der vorhandene connectionError
ist ein ganzer Hinweissatz, als Symbolbeschriftung zu lang). Die drei
fest verdrahteten englischen Texte damit aus der deutschen Oberflaeche.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:54:06 +02:00
schalli 9aa87bd000 fix(quick-260921-jt4): autoFocus von vier Seitenformularen entfernt (D-02)
login/page.tsx, reset-password/page.tsx, reset-password/[token]/page.tsx,
change-password/page.tsx: alle vier sind Seitenformulare, kein Dialog —
D-02 verlangt hier das Entfernen, nicht den ref+Effekt-Ersatz.
Fokus-Klauen beim Seitenaufruf ist genau der Schaden, gegen den
noAutofocus existiert. autoComplete/required unveraendert.

Nebennutzen bei change-password: der Hinweisbereich zum erzwungenen
Passwortwechsel steht unmittelbar ueber dem Formular — bislang sprang
der Fokus daran vorbei, eine Vorlesehilfe las ihn nie vor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:52:40 +02:00
schalli e651c24647 fix(quick-260921-jt4): Kalender-Tageszelle wird echte Schaltflaeche fuer Tage mit Terminen
- Zelle rendert als <button type="button"> NUR wenn hasEvents gilt,
  sonst unveraendert als <div> ohne Handler (D-01) — 42 neue Tab-Stopps
  waeren eine Verschlechterung
- onFocus/onBlur mit demselben Rumpf wie onMouseEnter/onMouseLeave,
  damit die Termin-Einblendung auch per Tastatur erscheint/verschwindet
- cellClass unveraendert uebernommen, nur w-full text-left ergaenzt
  (D-04, kein optischer Unterschied); widgetNoDrag bleibt erhalten
- aria-label nennt Datum und Terminzahl ueber neue Katalogschluessel
  widgets.calendar.dayEventsOne/dayEventsMany (Mehrzahl-Konvention wie
  configMaxEventsOne/Many, D-05)
- Neue Tests: Tag mit Terminen ist <button> und reagiert auf
  Fokus/Weggehen wie auf Maus-Hover; Tag ohne Termine bleibt <div>;
  Einzahl-/Mehrzahl-Beschriftung; echter Tab-Stopp nachgewiesen

Nach diesem Umbau: klick-regeln 5 (nur die vier <img onError> +
Taschenrechner-Rahmen), semantic 0, a11y gesamt 14, errors 0 — Aufgabe
1 des Plans vollstaendig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:51:41 +02:00
schalli b601141bcf fix(quick-260921-jt4): Notiz-Aufgabenkaestchen bedient sich selbst
- NoteCheckbox traegt jetzt ein echtes onChange und gibt sein eigenes
  DOM-Element an onToggle weiter; readOnly entfaellt (D-01) — war bisher
  nur da, um Reacts Warnung ueber ein gesteuertes Feld ohne onChange zu
  unterdruecken
- Index-Ermittlung bleibt wortgleich (alle Kaestchen im Behaelter
  einsammeln, indexOf auf dem ausloesenden Element), wandert aber vom
  Behaelter-onClick in handleCheckboxToggle, das den Behaelter ueber ein
  ref statt event.currentTarget findet
- previewOptions als useMemo mit leerer Abhaengigkeitsliste, Rueckruf
  ueber ein ref erreicht — identitaetsstabil wie die alte Modulkonstante,
  T-JT4-03: rehypePlugins: [[rehypeSanitize]] unveraendert erhalten
- Tests: echte Tastaturbetaetigung (Leertaste auf fokussiertem
  Kaestchen) UND echter Klick loesen onToggle/PATCH aus; neue Tests
  belegen readOnly/disabled entfallen

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:47:45 +02:00
schalli 0c89c13bb2 fix(quick-260921-jt4): calendar-settings-panel Loeschdialog-Hintergrund wird echte Schaltflaeche
- Zentrierbehaelter verliert den e.target===e.currentTarget-Handler,
  die Hintergrundfarbe wandert auf eine eigene benannte <button> (D-01)
- Muss abbrechen, darf niemals loeschen (T-JT4-04): Beschriftung nennt
  ausdruecklich das Abbrechen (widgets.calendar.deleteDialogCancel)
- Dialogkarte bekommt relative, aria-label des alertdialog aus dem
  Katalog statt fest verdrahtetem Englisch (deleteDialogLabel)
- Neue Testdatei belegt per Klick UND echter Tastaturbetaetigung, dass
  der Hintergrundweg abbricht und deleteSource nie aufgerufen wird;
  eigener Test fuer den tatsaechlichen Loeschweg ueber die CTA

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:43:36 +02:00
schalli 3d0bc0bfaa fix(quick-260921-jt4): widget-catalog-modal Hintergrund wird echte Schaltflaeche
- Aeussere Flaeche verliert onClick, die bislang nur optische
  Hintergrund-Flaeche wird eine benannte <button> und traegt onClose
  (D-01); verliert dabei aria-hidden, weil ein fokussierbares Element
  nicht verborgen sein darf
- stopPropagation auf der Dialogflaeche entfaellt als toter Code, weil
  der Hintergrund jetzt Geschwister statt Vorfahr ist
- Fest verdrahtetes englisches aria-label="Close" durch common.close
  ersetzt
- Neuer Katalogschluessel widgets.catalogClose in de.json/en.json
- Neue Testdatei: Hintergrund schliesst (Klick + Tastatur), Dialogklick
  schliesst nicht, Escape weiterhin, Kartenauswahl fuegt Widget hinzu

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:41:54 +02:00
schalli a8531d44df fix(quick-260921-jt4): MarketplaceCard-Klick wird echte deckende Schaltflaeche
- Karten-onClick entfernt, deckende <button> als Geschwister traegt
  handleCardClick nur wenn isActive (D-01, verschachtelte Buttons
  vermieden wie bei DropZone.tsx aus 260921-bi2)
- Aktivieren/Deaktivieren-Schaltflaeche bleibt eigener Tab-Stopp,
  Fussbereich bekommt relative fuer korrekten Stapelkontext
- Neuer Katalogschluessel marketplace.openDetail in de.json/en.json
- Tests: echte Tastaturbetaetigung (Enter auf fokussierter
  Schaltflaeche) statt Klick-Behauptung; kein Overlay im
  nicht-aktivierten Zustand

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:40:18 +02:00
schalli de6986340d fix(quick-260921-iwr): mergeCerts-Waechter loest keinen zusaetzlichen Lint-Fund mehr aus
Die urspruengliche findIndex((c) => c === null)-Pruefung erzeugte einen
neuen lint/complexity/useIndexOf-Fund (info) und hob TOTAL dadurch auf 430
statt der erwarteten 429 an — die Endverifikation des Plans deckte das auf
(D-06: TOTAL darf nirgends anders steigen).

@types/node-forge deklariert Bag.cert als "Certificate | undefined", die
node-forge-Laufzeit setzt bei einem unlesbaren Bag aber "null" (nicht
undefined). Ein blosses indexOf(null) ist deshalb nicht typsicher; die
Pruefung testet jetzt ausdruecklich auf beide Werte, was fuer biome kein
Single-Value-Vergleich mehr ist und keinen useIndexOf-Vorschlag ausloest.

TOTAL steht jetzt bei 429 wie geplant, NONNULL unveraendert bei 6, tsc und
beide Testsuiten weiterhin gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:05:13 +02:00
schalli 27909e4502 refactor(quick-260921-iwr): zwei ueberfluessige Ausrufezeichen in imap.provider.ts entfernt
imapflow typisiert uid in FetchMessageObject als Pflichtfeld (lib/imap-flow.d.ts
Zeile 469, Kommentar "Always included in the response"). Die beiden
Zusicherungen msg.uid! sicherten also einen Wert ab, der ohnehin nicht fehlen
kann. Reine Lesbarkeitsaenderung ohne Verhaltensaenderung, tsc bleibt gruen.

Die drei verbliebenen Zusicherungen (tenders.controller.ts:244,
favorites-widget.tsx:149, sidebar.tsx:80) bleiben unveraendert — jede durch
eine konkrete vorgelagerte Zeile garantiert (Provider-Eintrag, gemeinsame
Herleitung aus favorites, Anlegen des Map-Eintrags direkt davor).

NONNULL 8 -> 6, keines davon mehr in imap.provider.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:03:01 +02:00
schalli b4aaed4b7c fix(quick-260921-iwr): drei bag.cert-Zusicherungen in cert-manager.service.ts zu echten Waechtern gemacht
node-forge setzt bag.cert bei einem wohlgeformten, aber nicht als X.509
lesbaren Zertifikats-Bag auf null (lib/pkcs12.js Zeile 703-709). Die drei
Zusicherungen in parseCert, mergeCerts und convertCert behaupteten also
etwas Falsches, obwohl die Folge bereits behandelt war: alle vier Pfade
antworteten schon vorher mit 400, nie mit 500 (mit einer selbst gebauten
83-Byte-PFX nachgemessen).

Ersetzt die drei Zusicherungen durch ausdrueckliche Pruefungen mit
praeziser BadRequestException. In mergeCerts wird kein Zertifikat mehr
verschluckt: alle Bags werden auf Vollstaendigkeit geprueft, bevor die
Liste ueber ein Typpraedikat zurueckgegeben wird.

RED-Tests zuerst geschrieben und mit den heutigen Meldungen ("Failed to
extract certificate details" / "Failed to create merged certificate
output" / "Failed to convert certificate to pem: serialization error")
rot bestaetigt, dann die Waechter ergaenzt: GREEN.

Verhaltensaenderung ausdruecklich beabsichtigt (D-02): nur der Text der
Fehlermeldung fuer diese eine Eingabeklasse aendert sich, der Statuscode
bleibt in allen vier Pfaden 400.

NONNULL 11 -> 8, davon 3 in cert-manager.service.ts (Zeilen 133/516/661
bleiben unveraendert — durch Hash-Laenge bzw. vorgelagerte Passwort-
Pruefung garantiert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:01:36 +02:00
schalli 8716fa5234 feat(quick-260921-iwr): Rundenliste der Stoppuhr per Test belegt, MergeTab-Entfernen abgesichert
- Wirkungslose eslint-disable-Zeile in stopwatch-widget.tsx ersetzt durch
  Sachhinweis: Zeilen halten keinen Zustand, Rundennummer wird aus Laenge
  und Position berechnet. Keine neue Unterdrueckung, ARRAYKEY bleibt bei 19.
- Neuer Testfall in stopwatch-widget.test.tsx: zwei Runden nacheinander,
  neuere Runde steht oben, Rundennummern 2/1 stimmen zu ihrer eigenen Zeit.
- Neue MergeTab.test.tsx: Entfernen der mittleren Datei laesst genau erste
  und dritte Datei mit eigenem Namen und eigenem Entfernen-Knopf uebrig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 13:55:42 +02:00
schalli b92dd5dda1 fix(api,web): exec-Schleifen ohne Unterdrueckungskommentar umgeschrieben, LDAP-NUL-Maskierung und Sprachcookie-SameSite dokumentiert/gehaertet
Drei while ((m = re.exec(t)) !== null)-Schleifen (dkv-parser.service.ts,
dkv-parser.validate.ts, icon-discovery.service.ts) sind die korrekte
Standardform fuer globale Regexe - kein verrutschtes "=". Umgeschrieben auf
eine verhaltensgleiche for-Schleife, die ohne noAssignInExpressions-
Unterdrueckung auskommt: Zuweisung wandert in Initialisierung und
Fortschaltung der for-Schleife, Bedingung prueft weiterhin auf null.
Abfolge der exec-Aufrufe, lastIndex-Fortschritt und Rumpfinhalte
unveraendert. Neue Spezifikation dkv-parser.service.spec.ts deckt
parseDkvText erstmals eigenstaendig ab (zwei Fahrzeugbloecke, Rechnungsnummer
und -datum aus einer gemockten pdf-parse-Attrappe) - das Rueckfall-Tor fuer
diesen Umbau. dkv-parser.validate.ts bleibt bei 27 Fahrzeugbloecken/66
Transaktionen gegen die reale invoice.pdf identisch.

LdapService.escapeLdapFilterValue bleibt zeichengleich: der NUL-Treffer in
der Regel ist die von RFC 4515 vorgeschriebene \00-Maskierung, kein Fehler.
Ein biome-ignore-Kommentar dokumentiert das, statt die Funktion zu aendern.

locale-switcher.tsx setzt jetzt SameSite=Lax auf dem NEXT_LOCALE-Cookie -
path=/ und max-age waren bereits korrekt, es lag also kein Persistenzdefekt
vor. Ohne SameSite haengt die Uebertragung am Browservorgabewert statt an
einer Festlegung. Neue Spezifikation locale-switcher.test.tsx haelt die
vollstaendige geschriebene Cookie-Zeichenkette fest.

Biome-Warnungen 446 -> 434 (noControlCharactersInRegex/useIterableCallbackReturn/
noGlobalIsNan auf 0, noAssignInExpressions auf 4 und noDocumentCookie auf 17 -
beide Reste ausschliesslich in Testdateien, suppressions/unused auf 0).

Quick-Vorgang 260921-i8x, Task 3/3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 13:32:31 +02:00
schalli 076ca4bf64 fix(web): Number.isNaN statt globalem isNaN in vier Date-Pruefungen, forEach-Rueckgabewert verworfen
Alle vier Stellen (TenderDetail, ResultsList, InvoiceHistoryTable) rufen
isNaN(d.getTime()) auf - Date.prototype.getTime() liefert immer number,
also findet an keiner der vier Stellen tatsaechlich eine Umwandlung statt.
Der Tausch auf Number.isNaN ist reine Haertung: sobald dort einmal ein
String ankaeme, wuerde globales isNaN ihn stillschweigend umwandeln statt
ihn als kaputte Eingabe zu erkennen. Kein Rueckfallwert geaendert.

In cert-manager/actions.ts gibt files.forEach jetzt keinen Wert mehr aus
der Schleifenfunktion heraus - forEach verwirft ihn ohnehin, die Aenderung
ist rein kosmetisch (useIterableCallbackReturn). Reihenfolge der
form.append-Aufrufe unveraendert.

Quick-Vorgang 260921-i8x, Task 2/3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 13:28:07 +02:00
schalli f85c91be01 fix(web): Steuerzeichen in safe-next.ts als Unicode-Escapes statt Rohbytes
FORBIDDEN_CHARS_RE in sanitizeNextPath bestand aus drei rohen Steuerbytes
(NUL, US, DEL) statt Escapes - jeder Editor, Formatierer oder Minifier in
der Kette kann solche Bytes stillschweigend verschlucken. Umgeschrieben auf
Unicode-Escapes fuer den Bereich U+0000 bis U+001F und U+007F. Zeichenmenge
ueber alle 65536 Codepunkte aus U+0000 bis U+FFFF als unveraendert
nachgewiesen (54 abgewiesene Codepunkte, Bitmap-SHA-256
3d58108b87e4641e506602cc701a66d11ec19cf20551937fada1826f50ecbe64 vor und
nach dem Umbau identisch). Kommentar korrigiert: die vorherige Behauptung,
Escapes seien "am Edge" noetig, war falsch - zwischen den beiden
Escape-Schreibweisen gibt es zur Laufzeit keinen Unterschied.

Quick-Vorgang 260921-i8x, Task 1/3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 13:26:25 +02:00
schalli e780b2cc69 fix(web): Absicht/Ballast der letzten sechs useExhaustiveDependencies-Befunde entschieden
Befunde 16-21 (quick-260921-gof), Regel-Gesamtstand jetzt 0/446 (vorher
21/467), pnpm lint 5/5, pnpm type-check 4/4:

- InvoiceHistoryTable.tsx (16) und sidebar.tsx (17): refreshKey /
  sidebarRefreshKey bleiben als begruendete Auffrisch-Ausloeser stehen
  (biome-ignore mit deutschem Grund) - ohne sie zeigt die DKV-Historie
  nach "Jetzt pruefen" bzw. die Seitenleiste nach einer Modul-
  Aktivierung den alten Stand.
- ActivateModuleDialog.tsx (18): moduleId aus der Abhaengigkeitsliste
  entfernt - reiner Ballast, der Effekt holt ohnehin nur die vom Modul
  unabhaengige Gruppenliste und die Elternseite haengt den Dialog je
  Modul frisch ein.
- GroupMembersModal.tsx (19/20): fetchMembers/fetchAllUsers in die
  Liste aufgenommen (echter Defekt) - ohne sie zeigt der Dialog bei
  einem Gruppenwechsel ohne Neuaufbau die Mitglieder der vorigen
  Gruppe. Neue Testdatei nach dem grants-matrix.test.tsx-Muster belegt
  genau diesen Fall.
- grants/page.tsx (21): die Suchabgleich-Hilfsfunktion `matches` in den
  Merkungs-Rumpf verschoben statt im Bauteil-Rumpf zu bleiben - reiner
  Ballast, die bestehenden Suchfaelle bleiben unveraendert gruen.
- sidebar.test.tsx um eine Zaehlprobe erweitert: Bump loest genau einen
  weiteren Abruf aus, erneutes Zeichnen ohne Bump keinen.
- Alle drei begruendeten biome-ignore-Zeilen sind jetzt gesetzt (erste
  Verwendung dieses Mechanismus im Projekt), alle wirkungslosen
  eslint-disable-Zeilen fuer diese Regel sind aus apps/web/src
  verschwunden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 12:34:55 +02:00
schalli e2c508cff5 fix(web): t-Falle in acht Ladeeffekten entschaerft, RssFeedListForm/SavedSearchBar-Ladefunktionen stabilisiert
Befunde 7-15 der Biome-Regel useExhaustiveDependencies (quick-260921-gof):

- VehicleTable, TenderDetail, DigestIntervalForm, SourceConfigForm,
  favorites-widget: Ersatz-Fehlertext aus useTranslations wird jetzt vor
  dem Effekt/Rueckruf in eine Konstante gezogen und diese Konstante in
  die Abhaengigkeitsliste aufgenommen - `t` selbst kommt nirgends mehr
  in eine Liste. In diesem Projekt ist belegt, dass `t` bei jedem
  Durchlauf eine frische Funktion ist (Testattrappen), eine `t`-
  Abhaengigkeit haette den jeweiligen Mount-Abruf zur Schleife gemacht.
- RssFeedListForm.tsx und SavedSearchBar.tsx: die Ladefunktionen waren
  gewoehnliche Funktionen im Rumpf (bei jedem Durchlauf neu) - jetzt in
  einen stabilen Rueckruf mit der Text-Konstante als einziger
  Abhaengigkeit eingepackt.
- ResultsList.tsx: Befund 8 (t) wie oben, Befund 15 (refreshKey) in den
  Effekt verschoben, der `load` aufruft, statt in `load` selbst zu
  stehen - eine begruendete `biome-ignore`-Zeile (erste im Projekt)
  haelt fest, dass der Auffrisch-Ausloeser der Elternseite ohne diese
  Abhaengigkeit wirkungslos waere.
- Sieben Testdateien um eine Zaehlprobe erweitert: erneutes Zeichnen mit
  unveraenderten Props darf keinen weiteren Abruf ausloesen; ResultsList
  zusaetzlich um eine Probe, dass ein refreshKey-Bump genau einen
  weiteren Abruf ausloest.
- DigestIntervalForm hat keine Testdatei - nur am laufenden System auf
  Meine Quellen geprueft (siehe SUMMARY).
- Wirkungslose eslint-disable-Zeilen fuer diese Regel entfallen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 12:29:05 +02:00
schalli b3f0e3cdcd fix(web): Kalender-Ladeeffekt an monthDate statt getTime(), Stoppuhr-Takt liest nur noch Einzelwerte
Befunde 1-6 der Biome-Regel useExhaustiveDependencies (quick-260921-gof):

- calendar-widget.tsx: useMemo um resolveCalendarConfig() entfernt
  (reine Funktion, spart nichts). showToday setzt monthDate jetzt
  identitaetserhaltend, wenn der aktuelle Monat schon angezeigt wird -
  erst danach durfte der Ladeeffekt von monthDate.getTime() auf
  monthDate umgestellt werden, sonst haette jeder Druck auf den
  Monatsknopf im laufenden Monat einen Termin-Abruf bis zum
  Exchange-Server ausgeloest (D-04).
- stopwatch-widget.tsx: neue reine Hilfsfunktion computeElapsedFrom()
  fuer den Takt-Effekt, der jetzt nur noch drei Einzelwerte statt des
  ganzen sw-Objekts liest - eine sw-Abhaengigkeit haette den 100-ms-Takt
  bei jeder aufgezeichneten Runde ab- und wiederaufgebaut.
- Testerweiterungen als Rueckfallsicherungen: 2x weiterblaettern -> 3
  Abrufe, 3x Monatsknopf im laufenden Monat -> kein Zusatzabruf; Runde
  waehrend die Stoppuhr laeuft unterbricht den Takt nicht, genau 1 PATCH
  je Klick.
- Wirkungslose eslint-disable-Zeilen fuer diese Regel entfallen (kein
  ESLint mehr im Projekt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 12:23:35 +02:00