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
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
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
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
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
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
- 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
JwtStrategy.validate liess mustChangePassword auf dem Weg vom Token zu
request.user fallen; der global registrierte ForcePasswordChangeInterceptor
prueft genau dieses Feld und hat seit seiner Einfuehrung nie etwas
blockiert. validate() reicht das Feld jetzt durch (strenger Vergleich mit
true, Alt-Sitzungen ohne den Anspruch bleiben unveraendert unbetroffen).
Zusaetzlich die Erlaubnisliste des Abfangers von Teilstring-Vergleich auf
exakten Abgleich von Methode UND Pfad umgestellt (Absicherung gegen eine
kuenftige kollidierende Route, heute nicht ausnutzbar).
Nahttest gepinnt, der gegen den alten Quelltext nachweislich scheitert
(6 von 12 neuen Faellen rot vor der Aenderung, gruen danach).
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
with_desktop_marker haengte bisher nur desktop=1 an die erste Navigation;
der Fehler-melden-Knopf konnte deshalb Windows- und Linux-Client nicht
unterscheiden. with_client_marker ist die neue reine Kernfunktion (dv,
dc, dos zusaetzlich zu desktop=1), with_desktop_marker bleibt als Huelle
mit den echten env!-Werten die unveraenderte Aufrufstelle an allen drei
Navigationen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
WebView2 (Windows) sieht im User-Agent aus wie Edge, WebKitGTK (Linux) wie
Safari — im Postfach war eine Client-Meldung von einer Browser-Meldung
nicht zu unterscheiden. Neuer reiner Helfer origin.ts leitet aus vier
optionalen DTO-Feldern (Desktop-App) bzw. dem User-Agent (Browser) ein
Betreff-Kuerzel und eine Zeile "Herkunft: ..." ab; rein informativ,
laengenbegrenzt, nichts wird gespeichert (T-GZA-01). Browser-Pfad ist
damit Ende-zu-Ende fertig.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
Neues Command get_client_info liefert "Tessera-App X.Y.Z · Stand <sha7>"
(ohne Stempel nur die Version); die Setup-Seite blendet die Zeile beim
Erststart und unter "Server-Adresse ändern" dezent ein. So ist ohne
Adressleiste erkennbar, welcher Client-Stand läuft.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Neuer oeffentlicher Endpunkt (vor download/:platform): base-Origin wird
per safeOrigin validiert (nur http/https, kein Pfad/Query/Fragment/
Userinfo, sonst 400) und nur zum Bau der absoluten Download-URL genutzt,
nie serverseitig abgerufen
- 204 ohne Body bei fremder Plattform/Architektur, fehlendem Manifest,
fehlender Signatur oder fehlendem/ungueltigem updateVersion; sonst
{ version, pub_date (nur RFC 3339), url, signature, notes }
- Manifest-Felder signature (je Plattform) und updateVersion optional in
@tessera/shared, Eintragspruefung akzeptiert nur String-Signaturen
- 10 neue Spec-Tests, Test 11 erweitert (23 gesamt); latest/download
unveraendert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- tauri-plugin-updater 2.11 + semver eingebunden; pubkey, installMode passive
und createUpdaterArtifacts in tauri.conf.json (kein endpoints, keine
dangerous*-Schalter)
- Versionspruefung laeuft ueber updater_builder().check() gegen
/api-proxy/desktop/update mit eigenem version_comparator (is_update_newer:
hoehere Basis oder anderer Beta-Stempel beta.g<sha7>; gleiche Basis Live
→ kein Update); 15 s Pruefung, 600 s Download-Timeout
- Tray-Eintrag "Update installieren" laedt mit Fortschritt im Menuetext,
prueft die minisign-Signatur, installiert (Windows NSIS passiv, Linux
AppImage an Ort und Stelle) und startet neu; Fehler → Benachrichtigung
und Rueckfall auf die Einstellungsseite im Browser
- http-Server im Release: Menuetext "Update nur über https möglich", gesperrt
- 15 neue Tests, 2 umgestellt (33 gesamt); gen/schemas vom Bau nachgezogen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 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>
- Tray zeigt erste (gesperrte) Menüzeile "Verbunden mit {host}" und Tooltip "Tessera – {host}" (bzw. "nicht verbunden")
- Neuer Menüpunkt "Server-Adresse ändern…" navigiert zur gebündelten Setup-Seite (lokaler Ursprung, kein Capability-Eintrag nötig)
- setup.html erkennt per neuem Command get_server_url den Änderungsmodus (Feld vorbelegt, "Aktuell verbunden mit: …", Knopf "Abbrechen" → open_server)
- Nach save_server_url aktualisieren apply_server (Tooltip + Menüzeile) und spawn_version_check (aus setup herausgezogen, läuft neu gegen den neuen Server) ohne Neustart; Tray-Klick "update" liest die Adresse jetzt beim Klick aus dem Store
- Reine Helfer server_host, tray_labels, setup_page_url, parse_server_url mit 13 neuen Tests (RED zuerst: 13 Compile-Fehler auf die fehlenden Helfer, dann GREEN); 18 Rust-Tests gesamt, fmt/check/clippy sauber
- capabilities/default.json unverändert (T-JN2-01, kein remote-Block)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- CHANGELOG.md: je ein Stichpunkt unter Neu (Sortierpfeile) und Behoben
(Symbol trotz Zertifikatsfehler/interner Adresse)
- docs/anleitung-anwender.md: Tabellenzeile „Favoriten" nennt die Pfeile
und den Browser-Ersatzweg fuer das Symbol
- docs/mandantentrennung-zugriffsklassifikation.md: Nachtrag zu
favoriteLink — reorder() laeuft ueber withTenantTransaction() ohne
Benutzerdimension in der Sitzung, Stand bleibt gebunden
- prisma-tenant.extension.ts: Kopfkommentar-Nachtrag, favorites.service.ts
(reorder) ist der erste Nutzer-CRUD-Aufrufer von withTenantTransaction()
— nur Kommentartext, Funktionscode unveraendert (prisma-tenant.extension.spec.ts
und rls-access-inventory.spec.ts weiterhin gruen)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- icon-discovery.service.ts: undicis eigenes fetch mit Modul-Singleton
LENIENT_TLS_AGENT (Agent({ connect: { rejectUnauthorized: false } }))
als dispatcher in fetchWithRedirectGuard, der einzigen Ausgangsstelle
fuer HTML-Ermittlung und Icon-Byte-Holen; SSRF-Schutz unveraendert
- undici 7.28.0 (bereits im Lockfile aufgeloest) als direkte Abhaengigkeit
von @tessera/api via pnpm add --offline
- PUT /favorites/order (ReorderFavoritesDto) vor den :id-Routen;
FavoritesService.reorder() setzt position=index fuer die Favoriten
eines Widgets in EINER withTenantTransaction, userId+widgetId in jeder
Bedingung (zweites Netz), eine BadRequestException fuer alle
Abweichungen (T-JDD-06)
- getIcon: X-Content-Type-Options nosniff + restriktive CSP (T-JDD-02)
- 10 neue Tests (3 Dispatcher, 7 reorder); volle API-Suite 68 Dateien/
1101 Tests und type-check gruen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>