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
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
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
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
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
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
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
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
- 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
- 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
- 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
- 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
- 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
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
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
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
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
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
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
VehicleTable.confirmDelete liess das Beschaeftigt-Kennzeichen zwar setzen,
aber nie lesen (const [, setIsDeleting]); Dialogschaltflaechen blieben
waehrend der laufenden Loeschanfrage bedienbar. Kennzeichen jetzt lesbar
gebunden, Dialog reicht den Zustand an Bestaetigen/Abbrechen weiter
(disabled + Sperr-Klassen), und confirmDelete bricht bei bereits laufender
Loeschung selbst ab.
Alle sieben fest verdrahteten Texte und sechs Vorlesehilfen der Tabelle
jetzt ueber next-intl (sieben neue Schluessel im Bereich dkvFleet, gleicher
Schluesselsatz in de.json und en.json).
Nebenbefund beim Testen: `load` haette mit `t` als Abhaengigkeit bei einem
instabilen Uebersetzer-Mock einen Abruf-bei-jedem-Render-Zyklus ausgeloest
— bewusst mit leerem Abhaengigkeitsfeld gelassen. Ausserdem
`vi.restoreAllMocks()` im Testabbau ersetzt: es leerte die Aufrufzaehlung
der reinen vi.fn()-Mocks nicht, wodurch der neue Doppelklick-Test falsche
Aufrufzahlen sah.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
noLabelWithoutControl (22) auf 0: jede Beschriftung ueber htmlFor/id an ihr
Feld gebunden, in Formularen mit wiederholten Zeilen ueber praefixierte,
seitenweit eindeutige Kennungen (z.B. ldap-*, user-*, tenant-*).
Sonderfall calendar-source-form.tsx: die Farbauswahl beschriftet eine ganze
Gruppe von Farb-Schaltflaechen, kein einzelnes Feld. Dafuer fieldset/legend
statt htmlFor/id (Rand/Abstand zurueckgesetzt, damit sich am Erscheinungsbild
nichts aendert) -- eine Umwandlung in <span> haette die Assoziation entfernt
statt sie herzustellen, darum nicht gewaehlt.
Damit steht der gesamte Lint-Rueckstand bei 465 (386 echt, 79 Test),
Fehlerstufe 0 -- Zielwert dieses Vorgangs erreicht. Die fuenf zurueck-
gestellten Regeln (noNoninteractiveElementInteractions, useKeyWithClick-
Events, noStaticElementInteractions, useAriaPropsSupportedByRole,
noAutofocus) stehen unveraendert bei 11/5/5/5/4.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
Vier der fuenf noch offenen Regeln aus Teillieferung B bereinigt:
- noRedundantRoles (4): ueberfluessige role-Angaben auf button/time/select
entfernt (maschineller --unsafe-Fix, gelesen).
- useAriaPropsForRole (1): entfaellt automatisch mit obigem Fix -- das
<select role="combobox"> in search-widget.tsx verlangte die fehlenden
ARIA-Attribute nur wegen der ueberfluessigen Rolle.
- useSemanticElements (4): admin-sidebar/settings-sidebar tragen role=
"navigation" jetzt am <nav> statt am <aside> (kein doppeltes Landmark
mehr); widget-wrapper.tsx ist jetzt ein echtes <article> statt
div role="article"; DropZone.tsx trennt die Datei-Entfernen-Schaltflaeche
als Geschwister ab, damit die Drop-Flaeche selbst ein echtes <button>
werden kann (ein <button> darf kein zweites <button> verschachteln).
Die Drop-Flaeche traegt darum jetzt Klick- UND Drag-Handler direkt am
<button>, sonst waere sie ein "statisches" Element mit Ereignis-Handlern
geworden (die zurueckgestellten Regeln noStaticElementInteractions /
noNoninteractiveElementInteractions waeren neu angeschlagen -- geprueft,
bleiben bei 5/11).
- noNoninteractiveTabindex (1): calculator-widget.tsx traegt jetzt
tabIndex={-1} statt {0}. Die Zifferntasten sind bereits echte <button>
und damit selbst Teil der Tab-Reihenfolge; Tastendruecke erreichen
handleKeyboard weiterhin per Bubbling, sobald eine Taste fokussiert ist.
Verhalten unveraendert, nur ein wirkungsloser Tab-Stopp auf dem Container
selbst entfaellt.
Verbleibend: noLabelWithoutControl (22), naechster Schritt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
apps/web/src/app/icon.svg (Next.js-Favicon, keine next-intl-Anbindung
moeglich): <title>Tessera</title> als einzige Fundstelle, die die ganze
Bedeutung allein traegt.
apps/desktop/src/setup.html: Bildmarke steht direkt vor der Ueberschrift
"Tessera" und wird dekorativ (aria-hidden="true").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
useButtonType: Hamburger-, Profil-, Kategorie-, Ein-/Ausklapp- und Logout-
Buttons (kein Formular in diesen Dateien) erhalten type="button".
noSvgWithoutTitle: Icons neben sichtbarem Text werden dekorativ
(aria-hidden="true"). Zwei bislang unbenannte interaktive Elemente erhalten
zusaetzlich ein aria-label, weil ihr Text im eingeklappten Sidebar-Zustand
verschwindet bzw. ganz fehlte: die Dashboard-/Marketplace-Links und der
"Einstellungen"-Button in der Seitenleiste (t('dashboard')/t('marketplace')/
t('settings'), alle bereits vorhandene Schluessel) sowie der mobile
Schliessen-Button (tCommon('close')).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
useButtonType: "Widget hinzufuegen"-, Bearbeiten-Modus- und Katalog-Buttons
(kein Formular in diesen Dateien) erhalten type="button".
noSvgWithoutTitle: Widget-Icons in der Katalog-Kachel (immer neben dem
Widget-Namen) und Buttons mit bestehendem aria-label/title werden dekorativ
(aria-hidden="true"); der bislang unbeschriftete Such-Button im Such-Widget
erhaelt aria-label={t('search.searchButton')} (neuer Schluessel, siehe
vorherige i18n-Festschreibung).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
useButtonType: alle Aktions-Buttons in Cert-Manager-Tabs (kein Formular in
diesen Dateien) erhalten type="button".
noSvgWithoutTitle: Modul-Icons neben dem Modulnamen, Leer- und Nicht-
gefunden-Zustaende neben ihrer Ueberschrift werden dekorativ
(aria-hidden="true"); die Augen-Icons im Passwortfeld sind bereits ueber das
aria-label des umschliessenden Buttons benannt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
useButtonType: alle Filter-, Karten- und Dialog-Schaltflaechen (keine davon
in einem Formular) erhalten type="button".
noSvgWithoutTitle: Modul-Icons neben dem Modulnamen und die Erfolgs-/Fehler-
Symbole neben der Toast-Nachricht werden dekorativ (aria-hidden="true"); der
bislang unbeschriftete Toast-Schliessen-Button erhaelt aria-label={t('close')}
aus dem bereits vorhandenen common.close-Schluessel, sein Icon wird dekorativ.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
useButtonType: alle Schaltflaechen ausserhalb der drei echten Formulare
(Mandanten/Benutzer/LDAP) erhalten type="button"; die drei tatsaechlichen
Absende-Buttons behalten type="submit" (Zahl bleibt 1/1/2, siehe <verify>).
noSvgWithoutTitle: Symbole neben sichtbarem Text (Zurueck-Pfeil, Navigations-
Icons in der Admin-Seitenleiste) werden dekorativ (aria-hidden="true"); das
Schloss-Symbol der LDAP-Standardzuordnung traegt jetzt einen eigenen Titel
(admin.ldap.fieldMapping.defaultIcon), weil es ohne begleitenden Text pro
Tabellenzeile die ganze Bedeutung allein traegt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
useButtonType (Absenden-Button behaelt type="submit", keine weiteren Buttons
in diesen Dateien) und noSvgWithoutTitle (Lade-Spinner im Absenden-Button ist
rein dekorativ, da er den sichtbaren Beschriftungstext waehrend des Ladens
ersetzt -> aria-hidden="true").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- Aufgabe 2: vier sichere Biome-Regeln (useImportType pfadgebunden auf
apps/web+packages, noUselessEscapeInRegex, useConst,
useExponentiationOperator) sowie fuenf ungesicherte Regeln
(useNodejsImportProtocol, useLiteralKeys, useOptionalChain, useTemplate,
useParseIntRadix) angewendet und den gesamten Diff von Hand gelesen
(ldap.service.ts zeichenweise gegen Gross-/Kleinschreibung der
AD-Merkmale, auth.service.ts/jwt.strategy.ts gegen Durchwinken bei
fehlender Sitzung geprueft)
- noUselessSwitchCase bleibt bewusst stehen (tender-normalizer.service.ts:60,
die Fallmarke dokumentiert Absicht)
- Toter Code (D-03): fuenf folgenlose Auffangvariablen entfernt, eine
nicht benutzte Funktion (forSystemQuery, Pruefskript) entfernt, ein
positionsgebundener Dekoratorparameter umbenannt (current-user.decorator.ts),
fuenf Symptomfunde entfernt und als Folgeaufgaben zu melden (siehe unten)
- Sechs weitere, im Plan nicht namentlich gelistete aber
gleich-kategorische Dead-Code-Fundstellen in Testdateien zusaetzlich
bereinigt (groups.service.spec.ts, cert-manager.test.tsx,
ldap.service.spec.ts, prisma-tenant.extension.spec.ts x3) — noetig, um
die vom Plan selbst verlangten Nullstaende bei noUnusedVariables/
noUnusedImports/noUnusedFunctionParameters zu erreichen
Dekoratordaten aus apps/api unveraendert (593 Zeilen, sha256 6e1583f1...).
Endstand 620 Befunde (541 echt, 79 Test) statt der im Plan geschaetzten
621/542 — eine Differenz von 1, weil das Streichen des Namens aus
`catch (e: any)` in calendar.service.ts (Symptom-Fix) den dort ebenfalls
gemeldeten noExplicitAny-Befund miteliminiert; das ist eine erwuenschte
Nebenwirkung, keine Regression. Fehlerstufe 0, beide Testlaeufe
punktgleich gruen (69/1124, 66/459), pnpm type-check 4/4, pnpm lint
--force 5/5.
Folgeaufgaben aus D-03 (nicht in diesem Vorgang behoben):
- force-password-change.interceptor.ts: Freigabeliste prueft nur den Pfad,
nicht die HTTP-Methode
- change-password/page.tsx: nach erzwungenem Wechsel bleibt die Person auf
der Seite stehen (keine Weiterleitung, keine Aktualisierung der
Benutzerablage)
- VehicleTable.tsx: Loeschschaltflaeche hat keinen Besetztzustand, laesst
sich doppelt ausloesen
- SplitTab.tsx: downloadAllAsZip erhielt eine ungenutzte
Uebersetzungsfunktion, Hinweis auf fest verdrahtete Texte im Zip-Pfad
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
WINDOWS #36, Aufgabe 3/3: canManageRow spiegelt den Zielrollen-Riegel aus
apps/api/src/user/user.controller.ts (update/remove, WINDOWS #29) rein
ergonomisch — die Serverpruefung bleibt unveraendert und ist die einzige
wirksame Grenze. Bearbeiten und Loeschen entfallen jetzt in der Zeile
eines SUPER_ADMIN, wenn die angemeldete Person selbst keiner ist;
Details bleibt in jeder Zeile. Die Sperre gegen Selbstloeschung bleibt
unveraendert. Gesamtbestand apps/web: 66 Dateien / 459 Tests gruen,
type-check Exit 0, lint 5/5 erfolgreich, apps/api unangetastet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
WINDOWS #36, Aufgabe 2/3: handleSubmit und fetchUsers verschluckten
abgewiesene Antworten und Verbindungsfehler ebenso wie der Loeschweg aus
Aufgabe 1. formError zeigt jetzt den Servertext oder eine Ersatzmeldung
im offenen Formulardialog; loadError verhindert die irrefuehrende
Meldung "Keine Benutzer gefunden", wenn das Laden selbst gescheitert
ist. Beide Zustaende werden beim Oeffnen eines neuen Dialogs
zurueckgesetzt, damit eine alte Meldung nicht in den naechsten Aufruf
hinueberwandert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
WINDOWS #36, Aufgabe 1/3: handleDelete verschluckte einen 403 bisher
komplett (nur res.ok geprueft, Fang-Zweig ohne Wirkung). readApiMessage
liest jetzt gezielt das Feld message aus dem Antwortrumpf; der
Loeschdialog zeigt den Servertext, eine uebersetzte Ersatzmeldung ohne
verwertbaren Rumpf oder bei Verbindungsfehler — und bleibt in allen drei
Faellen offen. Neue Texte unter admin.users.errors in de.json/en.json,
Umlaut-Waechter gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
Die Middleware liest jetzt zusaetzlich dv/dc/dos aus der Anfrage und legt
daraus das Cookie tessera_desktop_client an (bereinigt per Muster, nur
wenn alle drei Werte gueltig sind); desktop-client.ts liest es zurueck.
Der Fehler-melden-Dialog fuellt daraus vier neue Nutzlastfelder
(clientKind/clientOs/clientVersion/clientCommit), damit die API die
Herkunft der Meldung ausweisen kann. Ohne das zweite Cookie (alter
Client) bleibt es bei "Desktop-App (unbekannt)".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
- DesktopAppSettings rendert im Desktop-Client (useIsDesktopClient) einen Block "Verbunden mit: {origin}" plus Hinweis auf "Server-Adresse ändern…" im Infobereich-Menü; im Browser bleibt der Block weg
- Neue i18n-Schlüssel settings.desktop.connectedTo/changeHint in de.json und en.json
- Test 4 (Cookie tessera_desktop=1) und Test 5 (ohne Cookie) ergänzt; RED zuerst (Test 4 schlug auf der Zielbehauptung fehl), dann GREEN
- Alle 431 Web-Tests und type-check grün
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- BRAND_OLIVE_MIX (54 %, #363636) und BRAND_OLIVE_FILL in brand.ts, Kalibrierung kommentiert
- vier achsenparallele Kacheln in tessera-logo.tsx: Inline-Style BRAND_OLIVE_FILL, festes Oliv-Attribut als Rückfall
- brand.test.ts (neu): rechnet Kalibrierung, Neutralität und Grenzfälle aus denselben Konstanten nach
- tessera-logo.test.tsx: Kacheltest auf Token-Fuellung (1x) und abgeleitete Fuellung (4x) umgestellt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- withDesktopCookie in middleware.ts setzt tessera_desktop=1 auf JEDER
Antwort (Fruehausstieg, Redirects, next()), wenn ?desktop=1 anliegt
- desktop-client.ts: isDesktopClient() liest das Cookie, useIsDesktopClient()
kapselt es hydration-sicher per useEffect
- DesktopDownloadLinks fragt /desktop/latest im Desktop-Client gar nicht
erst an und rendert nichts
- DesktopContextMenuGuard unterdrueckt das WebView2-Kontextmenue ausserhalb
von Eingabefeldern/contenteditable, in layout.tsx eingebunden
- middleware.test.ts (neu), desktop-client.test.ts (neu),
desktop-context-menu-guard.test.tsx (neu), Test 4 in
desktop-download-links.test.tsx — alle 417 Web-Tests und type-check gruen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- auth-actions.ts: fetchSessionState() unterscheidet tote Sitzung (401/403/leere 200-Antwort, Cookie wird geloescht) von API-Ausfall (5xx/Netzwerkfehler/Nicht-JSON, unavailable ohne Redirect); fetchCurrentUser bleibt unveraendert
- header.tsx: Waechter im useEffect leitet bei toter Sitzung per Vollnavigation auf /login?next=… um, bleibt bei API-Ausfall still
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- safe-next.ts: buildNextParam()/sanitizeNextPath() als reine, getestete Funktionen (Open-Redirect-Schutz)
- middleware.ts: haengt next-Parameter an beide Login-Umleitungen (fehlendes Cookie, ungueltige Signatur)
- login/page.tsx: springt nach erfolgreicher Anmeldung auf den bereinigten next-Wert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- LogoMark: gedrehte Signalkachel per Inline-Style fill: var(--primary, BRAND_YELLOW) statt festem fill-Attribut
- folgt damit der persoenlichen Akzentfarbe (applyAccentColor in auth-store.ts) in Kopfzeile, Seitenleiste und leerem Dashboard; ohne Nutzer (Anmeldeseite) gilt der CSS-Standard Markengelb
- brand.ts: Kommentar zu BRAND_YELLOW als Rueckfallwert ergaenzt
- tessera-logo.test.tsx: Kacheltest auf Inline-Style-Fuellung umgeschrieben (jsdom haelt var() in style.fill)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Neue Route /settings/general/desktop (DesktopSettingsPage)
- DesktopAppSettings: Version, zwei Primaerknoepfe mit Plattform-Symbol,
Dateiname/Groesse je Knopf, Beta-Kanal-Hinweis, vier erklaerende Saetze,
Hinweistext wenn keine Pakete hinterlegt sind
- Seitenleiste: Eintrag "Desktop-App" unter "Allgemein"
- de/en: settings.categoryDesktopApp, settings.desktop.*
- umlaut-dictionary.ts: "neuere" zur Allowlist ergaenzt (Rule 1 —
Waechter-Test brach an einer bereits korrekten Umlautschreibung, die
zufaellig die Buchstabenfolge "ue" enthaelt)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- apps/web/src/lib/desktop.ts: loadDesktopLatest (memoisiert, still bei
Fehler), desktopDownloadUrl, formatFileSize (lokalisierte MB-Werte)
- DesktopDownloadLinks: unauffaelliger Link-Block, rendert nichts ohne
Daten; Windows fuehrt, Linux als Kurzlink wenn beide Pakete vorliegen
- Anmeldeseite bindet den Block nach dem Formular ein (D-12)
- de/en: auth.desktopDownload.* ergaenzt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Plaketten-Wrapper calendar-day-badges rendert buildDayBadges(day.events) statt der alten Einzel-Plakette
- Ein Kalender: unveraendert eine Plakette in Kalenderfarbe/Akzentfarbe (Tests 3/3b/3c bleiben gruen)
- Zwei/drei Kalender: je ein kleiner Kreis pro Kalender in Startreihenfolge (Test 3d)
- Vier oder mehr Kalender: zwei Kreise plus grauer Restkreis bg-muted-foreground/text-background mit Summe (Test 3e)
- CHANGELOG-Stichpunkt erweitert (mehrere Kalender am selben Tag)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- groupDayBySource gruppiert Tagestermine nach sourceId (nicht Farbe), Reihenfolge = erstes Auftreten, Farbe = Farbe des ersten Termins der Gruppe
- buildDayBadges baut daraus bis zu drei Plaketten-Kreise, ab dem vierten Kalender einen grauen Restkreis mit Summe
- Tests 8/9 in calendar-month.test.ts (TDD: RED bestaetigt vor Implementierung)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>