docs(quick-260921-jt4): Plan fuer 30 a11y-Befunde und vier Restposten
29 der 30 Fundstellen werden behoben, eine bleibt begruendet stehen. Loesungswege vorab empirisch an Biome 2.5.0 geprueft: der in D-01 als Rueckfall genannte Weg (role+tabIndex) taeuscht -- er tauscht drei Befunde gegen einen neuen useSemanticElements-Befund und wird deshalb nirgends benutzt. Zwei Annahmen der Auftragsbeschreibung beim Nachlesen korrigiert: vier der elf Klick-Befunde sind onError-Handler an img-Elementen, und alle fuenf ARIA-Befunde sind aria-label auf rollenlosen Elementen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
+720
@@ -0,0 +1,720 @@
|
|||||||
|
---
|
||||||
|
phase: quick-260921-jt4
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx
|
||||||
|
- apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx
|
||||||
|
- apps/web/src/components/dashboard/widget-catalog-modal.tsx
|
||||||
|
- apps/web/src/components/dashboard/widget-catalog-modal.test.tsx
|
||||||
|
- apps/web/src/components/settings/calendar-settings-panel.tsx
|
||||||
|
- apps/web/src/components/settings/calendar-settings-panel.test.tsx
|
||||||
|
- apps/web/src/components/dashboard/widgets/note-widget.tsx
|
||||||
|
- apps/web/src/components/dashboard/widgets/note-widget.test.tsx
|
||||||
|
- apps/web/src/components/dashboard/widgets/note-task-list.tsx
|
||||||
|
- apps/web/src/components/dashboard/widgets/note-task-list.test.tsx
|
||||||
|
- apps/web/src/components/dashboard/widgets/calendar-widget.tsx
|
||||||
|
- apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx
|
||||||
|
- apps/web/src/components/dashboard/widgets/calculator-widget.tsx
|
||||||
|
- apps/web/src/components/dashboard/widgets/favorites-widget.tsx
|
||||||
|
- apps/web/src/components/layout/header.tsx
|
||||||
|
- apps/web/src/components/settings/account-settings-form.tsx
|
||||||
|
- apps/web/src/app/(auth)/login/page.tsx
|
||||||
|
- apps/web/src/app/(auth)/reset-password/page.tsx
|
||||||
|
- apps/web/src/app/(auth)/reset-password/[token]/page.tsx
|
||||||
|
- apps/web/src/app/(portal)/change-password/page.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/cert-manager/zip-filename.ts
|
||||||
|
- apps/web/src/app/(portal)/modules/cert-manager/zip-filename.test.ts
|
||||||
|
- apps/web/src/lib/translations-identity.test.tsx
|
||||||
|
- apps/web/src/messages/de.json
|
||||||
|
- apps/web/src/messages/en.json
|
||||||
|
- apps/api/src/tenders/tender-normalizer.service.ts
|
||||||
|
autonomous: true
|
||||||
|
requirements: [D-01, D-02, D-03, D-04, D-05, D-06, D-07]
|
||||||
|
|
||||||
|
estimate:
|
||||||
|
tokens: 150000
|
||||||
|
raw_tokens: 150000
|
||||||
|
tasks: 3
|
||||||
|
confidence: low
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Jede Stelle, die heute nur mit der Maus bedienbar ist, ist danach auch mit der
|
||||||
|
Tastatur bedienbar und wird von einer Vorlesehilfe als Schaltflaeche angesagt —
|
||||||
|
belegt durch Komponententests, die ein Tastaturereignis ausloesen, nicht durch
|
||||||
|
Auszeichnungs-Behauptungen (D-01)."
|
||||||
|
- "Das Erscheinungsbild ist an keiner der 13 Dateien ein anderes als vorher: kein
|
||||||
|
neuer Rahmen, kein neuer Abstand, keine verschobene Kachel (D-04)."
|
||||||
|
- "Biome meldet nach Abschluss genau 399 Befunde, 0 davon der Stufe error, und genau
|
||||||
|
EINEN a11y-Befund — den bewusst stehengelassenen Tastatur-Handler des
|
||||||
|
Taschenrechner-Rahmens. Keine einzige Unterdrueckung per biome-ignore (D-07)."
|
||||||
|
- "Drei Druecke auf Weiter im Kalender-Widget holen die Quellenliste genau einmal
|
||||||
|
statt dreimal, und zwei aufeinanderfolgende Termin-Abrufe tragen nie denselben
|
||||||
|
from/to-Bereich — gemessen im Netzwerkprotokoll des Browsers, nicht per fetch aus
|
||||||
|
der Seite."
|
||||||
|
- "Der ZIP-Name des Zertifikat-Aufteilers kommt aus dem Uebersetzungskatalog und ist
|
||||||
|
auf einer Windows-Freigabe garantiert gueltig, weil eine gepruefte Schutzfunktion
|
||||||
|
jeden unzulaessigen Namen abfaengt — nicht, weil das deutsche Wort zufaellig
|
||||||
|
harmlos ist."
|
||||||
|
- "Jede neue Beschriftung steht in de.json UND en.json, deutsche Oberflaechentexte in
|
||||||
|
der Sie-Form; beide Kataloge haben danach dieselbe Schluesselmenge (D-05)."
|
||||||
|
- "Fuer jede der 30 Fundstellen steht in der SUMMARY, welcher der vier Wege gewaehlt
|
||||||
|
wurde und — bei der Rueckfallvariante oder beim Stehenlassen — warum der gerade
|
||||||
|
Weg dort nicht ging."
|
||||||
|
artifacts:
|
||||||
|
- apps/web/src/app/(portal)/modules/cert-manager/zip-filename.ts
|
||||||
|
- apps/web/src/app/(portal)/modules/cert-manager/zip-filename.test.ts
|
||||||
|
- apps/web/src/components/dashboard/widget-catalog-modal.test.tsx
|
||||||
|
- apps/web/src/components/settings/calendar-settings-panel.test.tsx
|
||||||
|
- apps/web/src/lib/translations-identity.test.tsx
|
||||||
|
key_links:
|
||||||
|
- "Hintergrundflaeche eines Dialogs -> echte Schaltflaeche -> onClose: beim
|
||||||
|
Loeschen-Dialog MUSS dieser Weg abbrechen, niemals bestaetigen."
|
||||||
|
- "note-widget -> previewOptions -> rehypeSanitize: die XSS-Schranke T-IEX-01 muss
|
||||||
|
den Umbau des Kaestchen-Handlers unveraendert ueberleben."
|
||||||
|
- "calendar-widget -> computeFetchWindow -> fetchEvents: der Sperrgriff darf den
|
||||||
|
5-Minuten-Auffrischer nicht mitsperren, sonst friert die Anzeige ein."
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Die 30 zurueckgestellten Barrierefreiheits-Befunde abarbeiten — mit den vom Orchestrator
|
||||||
|
getroffenen Bedienentscheidungen D-01 bis D-04 — und die vier namentlich vermerkten
|
||||||
|
Restposten aus den heutigen Vorgaengen bi2 und gof schliessen.
|
||||||
|
|
||||||
|
Purpose: Heute sind mehrere Bedienelemente ausschliesslich mit der Maus erreichbar. Wer
|
||||||
|
mit der Tastatur oder einer Vorlesehilfe arbeitet, kann sie nicht ausloesen — das ist der
|
||||||
|
eigentliche Schaden hinter diesen Befunden, kein Schoenheitsfehler der Zaehlung.
|
||||||
|
|
||||||
|
Output: 13 Oberflaechendateien mit echten Schaltflaechen statt klickbarer Bereiche, zwei
|
||||||
|
Uebersetzungskataloge im Gleichstand, fuenf neue bzw. erweiterte Testdateien, ein
|
||||||
|
gemessener Rueckgang von 429 auf 399 Befunde und zwei Kalender-Abrufe weniger pro
|
||||||
|
Monatswechsel.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@~/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/STATE.md
|
||||||
|
@CLAUDE.md
|
||||||
|
@.planning/quick/260921-bi2-lint-rueckstand-abbauen-mechanische-fixe/260921-bi2-SUMMARY.md
|
||||||
|
@.planning/quick/260921-gof-effekt-abhaengigkeiten-in-react-21-befun/260921-gof-VERIFICATION.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<measured_baseline>
|
||||||
|
Vom Planer am 2026-09-21 unmittelbar vor dem Schreiben dieses Plans gemessen, nicht
|
||||||
|
uebernommen:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx biome lint . --reporter=json
|
||||||
|
-> total 429 real 350 test 79 errors 0 a11y 30
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Zahl 429 ist die **Gesamtzahl inklusive Testdateien** (350 echter Quelltext + 79
|
||||||
|
Testdateien), nicht der reine Quelltextanteil — die Auftragsbeschreibung bezeichnet sie
|
||||||
|
als "real source only", das ist eine Fehlbeschriftung der sonst korrekten Zahl. Alle 30
|
||||||
|
a11y-Befunde liegen in echtem Quelltext.
|
||||||
|
|
||||||
|
Die 30 Fundstellen, mit Zeilennummer zum Zeitpunkt der Planung (Zeilen verschieben sich
|
||||||
|
beim Umbau — arbeite nach Element, nicht nach Zeilennummer):
|
||||||
|
|
||||||
|
| Datei | Zeile | Regel |
|
||||||
|
|---|---|---|
|
||||||
|
| `app/(auth)/login/page.tsx` | 105 | noAutofocus |
|
||||||
|
| `app/(auth)/reset-password/[token]/page.tsx` | 139 | noAutofocus |
|
||||||
|
| `app/(auth)/reset-password/page.tsx` | 99 | noAutofocus |
|
||||||
|
| `app/(portal)/change-password/page.tsx` | 86 | noAutofocus |
|
||||||
|
| `marketplace/components/MarketplaceCard.tsx` | 116 | noNoninteractiveElementInteractions, noStaticElementInteractions, useKeyWithClickEvents |
|
||||||
|
| `components/dashboard/widget-catalog-modal.tsx` | 56 | noNoninteractiveElementInteractions, noStaticElementInteractions, useKeyWithClickEvents |
|
||||||
|
| `components/dashboard/widget-catalog-modal.tsx` | 64 | noNoninteractiveElementInteractions, useKeyWithClickEvents |
|
||||||
|
| `dashboard/widgets/calculator-widget.tsx` | 323 | noNoninteractiveElementInteractions |
|
||||||
|
| `dashboard/widgets/calculator-widget.tsx` | 345 | useAriaPropsSupportedByRole |
|
||||||
|
| `dashboard/widgets/calendar-widget.tsx` | 253 | noNoninteractiveElementInteractions, noStaticElementInteractions |
|
||||||
|
| `dashboard/widgets/favorites-widget.tsx` | 266 | useAriaPropsSupportedByRole |
|
||||||
|
| `dashboard/widgets/favorites-widget.tsx` | 462 | noNoninteractiveElementInteractions |
|
||||||
|
| `dashboard/widgets/favorites-widget.tsx` | 474 | noNoninteractiveElementInteractions |
|
||||||
|
| `dashboard/widgets/note-widget.tsx` | 184 | noNoninteractiveElementInteractions, noStaticElementInteractions, useKeyWithClickEvents |
|
||||||
|
| `components/layout/header.tsx` | 156 | noNoninteractiveElementInteractions |
|
||||||
|
| `components/settings/account-settings-form.tsx` | 161 | noNoninteractiveElementInteractions |
|
||||||
|
| `components/settings/calendar-settings-panel.tsx` | 159, 182, 199 | useAriaPropsSupportedByRole |
|
||||||
|
| `components/settings/calendar-settings-panel.tsx` | 373 | noNoninteractiveElementInteractions, noStaticElementInteractions, useKeyWithClickEvents |
|
||||||
|
|
||||||
|
**Zwei Befunde der Auftragsbeschreibung sind beim Nachlesen anders als angenommen** — der
|
||||||
|
Plan folgt dem gelesenen Quelltext, nicht der Annahme:
|
||||||
|
|
||||||
|
1. **Vier der elf `noNoninteractiveElementInteractions` sind ueberhaupt keine Klicks**,
|
||||||
|
sondern `onError`-Handler an `<img>`-Elementen (Ersatzweg fuer nicht ladende
|
||||||
|
Profilbilder und Favoriten-Symbole): `header.tsx:156`, `account-settings-form.tsx:161`,
|
||||||
|
`favorites-widget.tsx:462` und `:474`. Ein Ladefehler ist keine Bedienung; hier gibt es
|
||||||
|
nichts in eine Schaltflaeche zu verwandeln.
|
||||||
|
2. **Alle fuenf `useAriaPropsSupportedByRole` haben dieselbe Gestalt**: ein `aria-label`
|
||||||
|
sitzt auf einem schlichten `<div>` bzw. `<span>` ohne Rolle. Solche Elemente haben die
|
||||||
|
Rolle `generic`, die gar keine ARIA-Merkmale traegt — die Beschriftung wird von jeder
|
||||||
|
Vorlesehilfe **stillschweigend verworfen**. Das ist ein echter Mangel, kein Formfehler.
|
||||||
|
</measured_baseline>
|
||||||
|
|
||||||
|
<verified_probes>
|
||||||
|
Die folgenden Loesungswege wurden vom Planer **empirisch an Biome 2.5.0 mit der
|
||||||
|
Projektkonfiguration geprueft** (Probedatei unter `apps/web/src/__probe__/`, danach
|
||||||
|
geloescht). Das ist keine Vermutung — jede Zeile ist gemessen:
|
||||||
|
|
||||||
|
| Probe | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| `<button type="button" className="fixed inset-0" aria-label=… onClick=…/>` | **sauber** — traegt den Dialog-Hintergrund |
|
||||||
|
| `<button … onMouseEnter onMouseLeave onFocus onBlur>` | **sauber** — traegt die Kalender-Tageszelle |
|
||||||
|
| Deckende Schaltflaeche als Geschwister neben der Aktionsschaltflaeche in einer Karte | **sauber** — traegt die Marktplatz-Karte |
|
||||||
|
| `<span role="img" aria-label=…><svg aria-hidden/></span>` | **sauber** — traegt die drei Statussymbole |
|
||||||
|
| `<div role="toolbar" aria-label=…>` ohne Handler | **sauber** — traegt die beiden Schaltflaechenreihen |
|
||||||
|
| `<img alt="" aria-hidden="true" onError=…/>` | a11y-Befund **verschwindet** (nur der vorbestehende `performance/noImgElement` bleibt) |
|
||||||
|
| `<div role="button" tabIndex={0} onClick onKeyDown>` | **loest `useSemanticElements` NEU aus** |
|
||||||
|
| `<div role="group" aria-label=…>` | **loest `useSemanticElements` NEU aus** (Vorschlag: `<fieldset>`) |
|
||||||
|
| `<div role="region" aria-label=…>` | **loest `useSemanticElements` NEU aus** (Vorschlag: `<section>`) |
|
||||||
|
| `<div role="application"/"group"/"toolbar" … onKeyDown>` | Befund bleibt in **allen drei** Varianten |
|
||||||
|
|
||||||
|
**Die wichtigste Erkenntnis daraus:** Der in D-01 als Rueckfall genannte Weg
|
||||||
|
(`role="button"` + `tabIndex={0}` + Tastaturhandler) bringt die Zaehlung NICHT auf null.
|
||||||
|
Er tauscht drei Befunde gegen einen neuen `useSemanticElements`-Befund — eine Regel, die
|
||||||
|
260921-bi2 gerade erst auf 0 gebracht hat. D-07 verbietet, dass die Zahl anderswo waechst.
|
||||||
|
**Der Rueckfall wird deshalb in diesem Vorgang an keiner einzigen Stelle benutzt**; wo
|
||||||
|
eine unmittelbare Umwandlung in `<button>` an der Verschachtelung scheitert (Marktplatz),
|
||||||
|
tritt stattdessen die geprueft saubere deckende Geschwister-Schaltflaeche an ihre Stelle.
|
||||||
|
</verified_probes>
|
||||||
|
|
||||||
|
<decisions_applied>
|
||||||
|
Zuordnung der 30 Fundstellen zu den gesperrten Entscheidungen. Diese Tabelle ist die
|
||||||
|
Vorgabe, nicht ein Vorschlag:
|
||||||
|
|
||||||
|
| # | Fundstelle | Weg | Entscheidung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1-3 | MarketplaceCard.tsx (Karte) | echter Button (deckendes Geschwister) | D-01 |
|
||||||
|
| 4-6 | widget-catalog-modal.tsx (Hintergrund) | echter Button | D-01 |
|
||||||
|
| 7-8 | widget-catalog-modal.tsx (Dialogflaeche) | Handler entfaellt ersatzlos | D-01 |
|
||||||
|
| 9-11 | calendar-settings-panel.tsx (Loeschdialog-Hintergrund) | echter Button | D-01 |
|
||||||
|
| 12-14 | note-widget.tsx (Vorschau) | Kaestchen uebernimmt seinen Handler selbst | D-01 |
|
||||||
|
| 15-16 | calendar-widget.tsx (Tageszelle) | echter Button + Fokus-Handler | D-01 |
|
||||||
|
| 17-20 | vier `noAutofocus`-Stellen | Attribut entfaellt (alle vier sind Seiten, kein Dialog) | D-02 |
|
||||||
|
| 21-23 | calendar-settings-panel.tsx (3 Statussymbole) | `role="img"` + uebersetzte Beschriftung | D-03 |
|
||||||
|
| 24 | calculator-widget.tsx (Speicherzeile) | `role="toolbar"` + uebersetzte Beschriftung | D-03 |
|
||||||
|
| 25 | favorites-widget.tsx (Ansichtsumschalter) | `role="toolbar"` + **richtige** Beschriftung | D-03 |
|
||||||
|
| 26-29 | vier `<img onError>` | `aria-hidden="true"` | D-03 |
|
||||||
|
| 30 | calculator-widget.tsx (Tastatur am Rahmen) | **bleibt stehen, bleibt gezaehlt** | D-07 |
|
||||||
|
|
||||||
|
**Zu D-02:** Keine der vier `autoFocus`-Stellen ist ein Dialog. Es sind vier
|
||||||
|
Seitenformulare (Anmeldung, Passwort-Zuruecksetzen anfordern, Passwort-Zuruecksetzen
|
||||||
|
einloesen, Passwort aendern). D-02 sagt fuer genau diesen Fall: Entfernen ist richtig,
|
||||||
|
weil Fokus-Klauen beim Seitenaufruf das ist, wogegen die Regel existiert. Der
|
||||||
|
ref+Effekt-Zweig von D-02 kommt in diesem Projekt also an keiner Stelle zum Zug — das ist
|
||||||
|
ein Ergebnis, kein Uebersehen. Besonderer Nebennutzen bei `change-password/page.tsx`:
|
||||||
|
unmittelbar ueber dem Formular steht der Hinweisbereich zum erzwungenen Wechsel; heute
|
||||||
|
springt der Fokus daran vorbei, eine Vorlesehilfe liest den Hinweis nie vor.
|
||||||
|
|
||||||
|
**Zu Fundstelle 30 (bleibt stehen):** Der Rahmen des Taschenrechners traegt
|
||||||
|
`role="application"` und einen `onKeyDown`, damit getippte Ziffern ankommen, sobald
|
||||||
|
irgendeine seiner Tasten den Fokus hat. Jede sichtbare Taste ist bereits ein echtes
|
||||||
|
`<button>` und selbst Teil der Tab-Reihenfolge. Der Handler **fuegt einen Tastaturweg
|
||||||
|
hinzu** — er ist das Gegenteil des Schadens, den die Regel beschreibt. Alle drei
|
||||||
|
Rollen-Alternativen wurden gemessen und aendern nichts (siehe Probentabelle).
|
||||||
|
**Ausdruecklich NICHT gewaehlt und auch spaeter nicht nachzuholen:** den Handler per
|
||||||
|
`addEventListener` in einem Effekt anzuhaengen. Das Verhalten waere identisch, nur die
|
||||||
|
Regel saehe ihn nicht mehr — das waere eine geschoente Zahl ohne Gegenwert, und D-07
|
||||||
|
verbietet genau das. Der Befund bleibt sichtbar in der Zaehlung stehen.
|
||||||
|
</decisions_applied>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Aufgabe 1: Aus klickbaren Bereichen echte Schaltflaechen machen (16 Befunde, 5 Dateien)</name>
|
||||||
|
<files>apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx, apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx, apps/web/src/components/dashboard/widget-catalog-modal.tsx, apps/web/src/components/dashboard/widget-catalog-modal.test.tsx, apps/web/src/components/settings/calendar-settings-panel.tsx, apps/web/src/components/settings/calendar-settings-panel.test.tsx, apps/web/src/components/dashboard/widgets/note-widget.tsx, apps/web/src/components/dashboard/widgets/note-task-list.tsx, apps/web/src/components/dashboard/widgets/note-widget.test.tsx, apps/web/src/components/dashboard/widgets/note-task-list.test.tsx, apps/web/src/components/dashboard/widgets/calendar-widget.tsx, apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
|
||||||
|
<read_first>
|
||||||
|
Lies vor dem ersten Eingriff den Abschnitt `verified_probes` dieses Plans und die
|
||||||
|
Pitfall-Notiz aus `260921-bi2-SUMMARY.md` (Abschnitt "tech-stack -> patterns"): ein
|
||||||
|
a11y-Fix kann eine ANDERE Regel neu ausloesen, wenn ein Element seine interaktive
|
||||||
|
Einstufung verliert. Miss nach jedem einzelnen Teilumbau die vollstaendige a11y-Menge,
|
||||||
|
nicht nur die Zielregel.
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- MarketplaceCard: Tastaturbedienung der Karte oeffnet die Moduldetails; die
|
||||||
|
Aktivieren-Schaltflaeche bleibt ein eigener, separat erreichbarer Tab-Stopp und
|
||||||
|
loest beim Ausloesen NICHT zusaetzlich das Oeffnen aus.
|
||||||
|
- MarketplaceCard, gesperrter Zustand: Tastaturbedienung ruft den Gesperrt-Hinweis auf.
|
||||||
|
- MarketplaceCard, nicht aktiviertes Modul: es gibt keine Kartenschaltflaeche.
|
||||||
|
- widget-catalog-modal: Ausloesen der Hintergrundflaeche schliesst; ein Klick im
|
||||||
|
Dialog schliesst nicht; Escape schliesst weiterhin.
|
||||||
|
- calendar-settings-panel, Loeschdialog: Ausloesen der Hintergrundflaeche **bricht ab**
|
||||||
|
(Datensatz bleibt bestehen), Loeschen geschieht ausschliesslich ueber die
|
||||||
|
Loeschen-Schaltflaeche.
|
||||||
|
- note-widget: Ein Aufgabenkaestchen laesst sich mit der Tastatur umschalten und die
|
||||||
|
zugehoerige Markdown-Zeile kippt; im Bearbeitungsmodus passiert nichts.
|
||||||
|
- calendar-widget: Ein Tag MIT Terminen ist mit der Tastatur fokussierbar und zeigt
|
||||||
|
beim Fokussieren dieselbe Termin-Einblendung wie beim Ueberfahren mit der Maus;
|
||||||
|
beim Verlassen verschwindet sie. Ein Tag OHNE Termine ist kein Tab-Stopp.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Fuenf Umbauten, jeder einzeln zu committen. Umsetzung von D-01 ueberall ohne den
|
||||||
|
Rueckfallweg, weil dieser messbar eine andere Regel neu ausloesen wuerde (siehe
|
||||||
|
`verified_probes`). D-04 gilt durchgehend: das Erscheinungsbild bleibt gleich.
|
||||||
|
|
||||||
|
**(1) MarketplaceCard.tsx — die Karte.** Der `onClick` sitzt heute auf der Karten-`<div>`,
|
||||||
|
und die Karte enthaelt im Fuss die Aktivieren/Deaktivieren-Schaltflaeche. Eine unmittelbare
|
||||||
|
Umwandlung der Karte in ein `<button>` ist deshalb unmoeglich (verschachtelte
|
||||||
|
Schaltflaechen sind ungueltige Auszeichnung — genau die Falle, in die 260921-gof bei
|
||||||
|
`DropZone.tsx` schon einmal gelaufen ist). Stattdessen: die Karten-`<div>` verliert ihren
|
||||||
|
`onClick` und bekommt `relative`; als Geschwisterelement kommt eine deckende
|
||||||
|
`<button type="button" className="absolute inset-0 rounded-lg …">` hinzu, die
|
||||||
|
`handleCardClick` traegt und nur gerendert wird, wenn `isActive` gilt (heute haengt der
|
||||||
|
Handler ebenfalls an `isActive`). Die Maus-Zeigerform der deckenden Schaltflaeche
|
||||||
|
uebernimmt die Fallunterscheidung der Karte (`cursor-not-allowed` bei `locked`, sonst
|
||||||
|
`cursor-pointer`), damit sich optisch nichts aendert. Der Fussbereich mit der
|
||||||
|
Aktivieren-Schaltflaeche bekommt `relative`, damit er ueber der deckenden Flaeche liegt und
|
||||||
|
weiterhin unmittelbar getroffen wird; das dortige `e.stopPropagation()` bleibt unangetastet.
|
||||||
|
Die deckende Schaltflaeche braucht einen Namen fuer die Vorlesehilfe — verwende den
|
||||||
|
vorhandenen Modulnamen ueber einen neuen Schluessel `marketplace.openDetail` (D-05).
|
||||||
|
Setze `<h3>`/Beschreibung NICHT in die Schaltflaeche hinein: die Ueberschrift kuerzt per
|
||||||
|
`truncate` (also `overflow-hidden`) und wuerde ein darin liegendes Deckelement beschneiden.
|
||||||
|
|
||||||
|
**(2) widget-catalog-modal.tsx — Hintergrund und Dialogflaeche.** Heute traegt die
|
||||||
|
aeussere Flaeche den `onClick={onClose}`, und die Dialogflaeche haelt mit einem
|
||||||
|
`stopPropagation` dagegen. Kehre das um: die aeussere Flaeche verliert ihren Handler
|
||||||
|
ersatzlos; die bereits vorhandene, bislang rein optische Hintergrund-`<div>`
|
||||||
|
(`fixed inset-0 bg-black/50`) wird zu
|
||||||
|
`<button type="button" className="fixed inset-0 bg-black/50" onClick={onClose}>` und
|
||||||
|
verliert dabei ihr `aria-hidden` — ein fokussierbares Element darf nicht vor der
|
||||||
|
Vorlesehilfe verborgen sein. Sie bekommt stattdessen einen echten Namen ueber einen neuen
|
||||||
|
Schluessel `widgets.catalogClose` (D-05). Weil der schliessende Handler danach kein
|
||||||
|
Vorfahr der Dialogflaeche mehr ist, sondern ihr Geschwister, ist das `stopPropagation`
|
||||||
|
auf der Dialogflaeche **toter Code und wird geloescht** — damit fallen die beiden Befunde
|
||||||
|
an dieser Stelle weg, ohne dass sich irgendein Verhalten aendert. Der Escape-Weg im
|
||||||
|
bestehenden Effekt bleibt unveraendert. Ersetze bei dieser Gelegenheit das fest
|
||||||
|
verdrahtete englische `aria-label="Close"` der Schliessen-Schaltflaeche durch
|
||||||
|
`common.close` (D-05) — es steht in der Datei, die du ohnehin umbaust.
|
||||||
|
|
||||||
|
**(3) calendar-settings-panel.tsx — der Loeschbestaetigungs-Dialog.** Dieselbe Gestalt,
|
||||||
|
andere Datei: die `fixed inset-0 … bg-black/50`-Flaeche ist zugleich Hintergrund UND
|
||||||
|
Zentrierbehaelter und prueft im Handler `e.target === e.currentTarget`. Trenne beides: der
|
||||||
|
Behaelter behaelt `fixed inset-0 z-50 flex items-center justify-center` und verliert jeden
|
||||||
|
Handler; die Hintergrundfarbe wandert auf eine neue
|
||||||
|
`<button type="button" className="fixed inset-0 bg-black/50" onClick={() => setDeletingId(null)}>`;
|
||||||
|
die Dialogkarte bekommt `relative`, damit sie weiterhin ueber dem Hintergrund liegt.
|
||||||
|
Beschriftung ueber einen neuen Schluessel `widgets.calendar.deleteDialogCancel`, dessen
|
||||||
|
Text das Abbrechen benennt — dieser Weg darf niemals loeschen (siehe threat_model
|
||||||
|
T-JT4-04). Ersetze ausserdem das fest verdrahtete englische `aria-label="Confirm deletion"`
|
||||||
|
des `role="alertdialog"` durch einen neuen Schluessel
|
||||||
|
`widgets.calendar.deleteDialogLabel` (D-05).
|
||||||
|
|
||||||
|
**(4) note-widget.tsx + note-task-list.tsx — das Aufgabenkaestchen bedient sich selbst.**
|
||||||
|
Heute faengt der Vorschau-Behaelter die Klicks ab (`handlePreviewClick`) und das von
|
||||||
|
`NoteCheckbox` gerenderte Kaestchen traegt `readOnly`. Dreh das um: `NoteCheckbox`
|
||||||
|
bekommt einen `onChange` und gibt darin sein eigenes DOM-Element an einen von aussen
|
||||||
|
gereichten Rueckruf weiter; `readOnly` entfaellt. Die **Index-Ermittlung bleibt Wort fuer
|
||||||
|
Wort die heutige** (alle Kaestchen im Vorschau-Behaelter einsammeln, `indexOf` auf dem
|
||||||
|
ausloesenden Element) — sie wandert lediglich vom Behaelter-Handler in eine Funktion des
|
||||||
|
Widgets, die den Behaelter ueber ein `ref` statt ueber `event.currentTarget` findet.
|
||||||
|
Fasse den bisher nur module-weit gueltigen `PREVIEW_OPTIONS`-Wert in ein `useMemo`, das
|
||||||
|
den Rueckruf ueber ein `useRef` erreicht, damit das Optionsobjekt **identitaetsstabil**
|
||||||
|
bleibt — der bestehende Kommentar begruendet genau das, und ein pro Tastendruck neu
|
||||||
|
erzeugtes Optionsobjekt liesse react-markdown bei jedem Zeichen neu abgleichen.
|
||||||
|
`rehypePlugins: [[rehypeSanitize]]` muss dabei unveraendert erhalten bleiben (XSS-Schranke
|
||||||
|
T-IEX-01, siehe threat_model T-JT4-03). Der `onClick` am Vorschau-Behaelter entfaellt
|
||||||
|
danach ersatzlos. Die Pruefung auf `isEditing` bleibt erhalten. Nebennutzen, den du in der
|
||||||
|
SUMMARY benennen sollst: `readOnly` war bisher nur da, um Reacts Warnung ueber ein
|
||||||
|
gesteuertes Feld ohne `onChange` zu unterdruecken — das Kaestchen bedient sich jetzt
|
||||||
|
tatsaechlich selbst, statt sich von seinem Behaelter bedienen zu lassen.
|
||||||
|
|
||||||
|
**(5) calendar-widget.tsx — die Tageszelle.** Heute traegt jede der 42 Zellen
|
||||||
|
`onMouseEnter`/`onMouseLeave` fuer die Termin-Einblendung. Wer nicht mit der Maus
|
||||||
|
arbeitet, bekommt die Termine eines Tages **gar nicht** zu sehen — das ist der echte
|
||||||
|
Mangel hinter diesem Befund. Rendere die Zelle als `<button type="button">`, **wenn und
|
||||||
|
nur wenn sie Termine hat** (`hasEvents`), sonst unveraendert als `<div>` ohne Handler.
|
||||||
|
Nur Tage mit Terminen werden so zu Tab-Stopps; ein Widget mit 42 neuen Tab-Stopps waere
|
||||||
|
eine Verschlechterung. Die Schaltflaechen-Variante traegt zusaetzlich `onFocus`/`onBlur`
|
||||||
|
mit demselben Rumpf wie `onMouseEnter`/`onMouseLeave`, damit die Einblendung fuer die
|
||||||
|
Tastatur genauso erscheint und verschwindet. Uebernimm `cellClass` unveraendert und
|
||||||
|
ergaenze nur das, was ein `<button>` braucht, um wie die bisherige `<div>` auszusehen
|
||||||
|
(Textausrichtung, volle Breite, kein geerbter Schaltflaechenrahmen) — `widgetNoDrag` muss
|
||||||
|
erhalten bleiben, sonst reisst react-grid-layout die Kachel beim Klicken mit.
|
||||||
|
Die Schaltflaeche bekommt einen Namen, der Datum und Terminzahl nennt; folge der im
|
||||||
|
Katalog bereits vorhandenen Mehrzahl-Konvention dieses Projekts mit zwei getrennten
|
||||||
|
Schluesseln (`widgets.calendar.dayEventsOne` / `widgets.calendar.dayEventsMany`, Vorbild
|
||||||
|
`configMaxEventsOne`/`configMaxEventsMany`), nicht mit ICU-Plural (D-05).
|
||||||
|
|
||||||
|
**Uebersetzungen:** Jeder neue Schluessel kommt in `de.json` UND `en.json`, deutsche Texte
|
||||||
|
in der Sie-Form (D-05). Keine neuen Pakete, keine Versionsanhebung, keine Umformatierung
|
||||||
|
fremder Dateien (D-06).
|
||||||
|
|
||||||
|
**Tests — Tastaturereignis schlaegt Auszeichnungsbehauptung.** Erweitere bzw. lege an:
|
||||||
|
`MarketplaceCard.test.tsx` (vorhanden), `widget-catalog-modal.test.tsx` (neu),
|
||||||
|
`calendar-settings-panel.test.tsx` (neu), `note-widget.test.tsx` und
|
||||||
|
`note-task-list.test.tsx` (vorhanden), `calendar-widget.test.tsx` (vorhanden).
|
||||||
|
`@testing-library/user-event` ist in `apps/web` verfuegbar (14.6.1) — nutze es, um nach
|
||||||
|
`.focus()` eine echte Tastaturbetaetigung auszuloesen, statt einen Klick zu senden und
|
||||||
|
Tastaturbedienung nur zu behaupten. Fuer das Notiz-Kaestchen ist die Tastaturbetaetigung
|
||||||
|
gerade der Punkt: ein fokussiertes Kaestchen muss sich mit der Leertaste kippen lassen.
|
||||||
|
Fuer den Loeschdialog muss ein Test belegen, dass der Hintergrundweg abbricht und die
|
||||||
|
Quelle **nicht** geloescht wird.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && npx biome lint . --reporter=json 2>/dev/null | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const d=JSON.parse(s).diagnostics||[];const a=d.filter(x=>/a11y\/(noNoninteractiveElementInteractions|noStaticElementInteractions|useKeyWithClickEvents)/.test(x.category));a.forEach(x=>console.log(' ',x.category.replace('lint/a11y/',''),x.location.path+':'+x.location.start.line));console.log('klick-regeln',a.length,'| semantic',d.filter(x=>x.category==='lint/a11y/useSemanticElements').length,'| a11y gesamt',d.filter(x=>x.category.includes('a11y')).length,'| gesamt',d.length,'| errors',d.filter(x=>x.severity==='error').length);})"</automated>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/web exec vitest run 2>&1 | tail -8</automated>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm type-check 2>&1 | tail -4</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Erste Pruefung meldet `klick-regeln 5` — genau die vier `<img onError>`-Stellen
|
||||||
|
(`header.tsx`, `account-settings-form.tsx`, `favorites-widget.tsx` zweimal) und der
|
||||||
|
Taschenrechner-Rahmen; `semantic 0` (die Regel ist NICHT gewachsen); `a11y gesamt 14`;
|
||||||
|
`errors 0`. `apps/web`-Tests gruen mit mindestens 69 Dateien und mindestens 484 Tests
|
||||||
|
(die neuen Faelle kommen obendrauf, die Zahl darf nur steigen). `pnpm type-check` 4/4.
|
||||||
|
Fuer jede der fuenf Umbauten steht fest, welcher Weg gewaehlt wurde; der Rueckfallweg aus
|
||||||
|
D-01 wurde an keiner Stelle benutzt.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Aufgabe 2: Rollen, Beschriftungen, Autofokus — und zwei Restposten (14 Befunde)</name>
|
||||||
|
<files>apps/web/src/app/(auth)/login/page.tsx, apps/web/src/app/(auth)/reset-password/page.tsx, apps/web/src/app/(auth)/reset-password/[token]/page.tsx, apps/web/src/app/(portal)/change-password/page.tsx, apps/web/src/components/settings/calendar-settings-panel.tsx, apps/web/src/components/dashboard/widgets/calculator-widget.tsx, apps/web/src/components/dashboard/widgets/favorites-widget.tsx, apps/web/src/components/layout/header.tsx, apps/web/src/components/settings/account-settings-form.tsx, apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx, apps/web/src/app/(portal)/modules/cert-manager/zip-filename.ts, apps/web/src/app/(portal)/modules/cert-manager/zip-filename.test.ts, apps/api/src/tenders/tender-normalizer.service.ts, apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
|
||||||
|
<action>
|
||||||
|
Attributarbeit ohne Strukturumbau — deutlich geringeres Risiko als Aufgabe 1, deshalb
|
||||||
|
getrennt. Dazu zwei der vier vermerkten Restposten.
|
||||||
|
|
||||||
|
**(1) Vier `autoFocus` entfernen (D-02).** `login/page.tsx`, `reset-password/page.tsx`,
|
||||||
|
`reset-password/[token]/page.tsx`, `change-password/page.tsx`. Der Planer hat alle vier
|
||||||
|
gelesen: es sind Seitenformulare, keine Dialoge — D-02 verlangt hier das Entfernen und
|
||||||
|
ausdruecklich NICHT den Ersatz durch ref+Effekt. Ein Ersatz per Effekt waere dasselbe
|
||||||
|
Verhalten mit stillgelegter Regel. Entferne ausschliesslich das Attribut; `autoComplete`,
|
||||||
|
`required` und alles andere bleibt. Halte in der SUMMARY je Stelle fest, dass es eine
|
||||||
|
Seite und kein Dialog war.
|
||||||
|
|
||||||
|
**(2) Drei Statussymbole in `calendar-settings-panel.tsx` (D-03).** Die drei `<span>` mit
|
||||||
|
`aria-label="Sync error"` / `"Connection OK"` / `"Connection error"` haben die Rolle
|
||||||
|
`generic` und ihre Beschriftung wird stillschweigend verworfen. Die Beschriftung traegt
|
||||||
|
hier echte Bedeutung — sie ist der EINZIGE Text dieser Symbole (das `<svg>` darin ist
|
||||||
|
bereits `aria-hidden`). Also Ursache beheben, nicht Attribut streichen: `role="img"`
|
||||||
|
ergaenzen (geprueft sauber). Die drei englischen Texte in einer deutschen Oberflaeche
|
||||||
|
gehen dabei in den Katalog (D-05): fuer den Erfolgsfall ist der vorhandene Schluessel
|
||||||
|
`widgets.calendar.connectionSuccess` woertlich passend und wird wiederverwendet; fuer die
|
||||||
|
beiden Fehlerfaelle lege kurze eigene Schluessel an (`widgets.calendar.syncErrorLabel`,
|
||||||
|
`widgets.calendar.connectionFailedLabel`) — der vorhandene `connectionError` ist ein
|
||||||
|
ganzer Hinweissatz und als Symbolbeschriftung zu lang. Das `title`-Attribut mit dem
|
||||||
|
Rohfehler bleibt unangetastet.
|
||||||
|
|
||||||
|
**(3) Zwei Schaltflaechenreihen (D-03).** `calculator-widget.tsx`, Speicherzeile
|
||||||
|
(`aria-label="Speicherfunktionen"`) und `favorites-widget.tsx`, Ansichtsumschalter
|
||||||
|
(`aria-label={t('favorites.name')}`): beide sind schlichte `<div>`, die Beschriftung
|
||||||
|
verpufft. Ergaenze `role="toolbar"` (geprueft sauber — `role="group"` und `role="region"`
|
||||||
|
scheiden aus, sie loesen `useSemanticElements` neu aus). Beim Taschenrechner wandert der
|
||||||
|
fest verdrahtete deutsche Text in den Katalog (`widgets.calculator.memoryLabel`). Beim
|
||||||
|
Favoriten-Umschalter ist die heutige Beschriftung sachlich falsch — sie sagt "Favoriten"
|
||||||
|
ueber einem Umschalter zwischen Listen- und Kachelansicht; vergib einen neuen, zutreffenden
|
||||||
|
Schluessel `widgets.favorites.viewModeLabel`. Das ist der Fall, in dem D-03 das Streichen
|
||||||
|
erlauben wuerde (die Beschriftung trug keine echte Bedeutung); eine richtige Beschriftung
|
||||||
|
ist trotzdem besser als gar keine. Uebersetze im Taschenrechner bei dieser Gelegenheit die
|
||||||
|
beiden weiteren fest verdrahteten deutschen Beschriftungen derselben Ansicht (Anzeigefeld,
|
||||||
|
Rueckschritt-Taste) in Katalogschluessel (D-05) — Grenze der Ausweitung: nur
|
||||||
|
Beschriftungen in Dateien, die dieser Vorgang ohnehin aendert.
|
||||||
|
|
||||||
|
**(4) Vier `<img onError>` (D-03).** `header.tsx`, `account-settings-form.tsx` und
|
||||||
|
zweimal `favorites-widget.tsx`. Ergaenze `aria-hidden="true"`. **Sei in der SUMMARY
|
||||||
|
ehrlich darueber, was das leistet und was nicht:** alle vier tragen bereits `alt=""`, sind
|
||||||
|
also schon aus dem Zugaenglichkeitsbaum genommen; `aria-hidden` sagt dasselbe nur
|
||||||
|
ausdruecklich. Es ist richtige Auszeichnung, aber es 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`.
|
||||||
|
|
||||||
|
**(5) Restposten 2 — `tender-normalizer.service.ts`, `noUselessSwitchCase`.** Nachgelesen:
|
||||||
|
die Fallmarke `case 'doe-opendata':` steht unmittelbar ueber `default:` und faellt in
|
||||||
|
denselben Zweig; der Kommentar darunter erklaert, warum der Standardzweig auf dem
|
||||||
|
DOE-Weg bleiben muss. 260921-bi2 hat sie stehen lassen, weil sie Absicht dokumentiert.
|
||||||
|
Diese Absicht laesst sich ohne die ueberfluessige Marke ausdruecken und wird dabei sogar
|
||||||
|
deutlicher: entferne die Fallmarke und erweitere den bestehenden Kommentar so, dass er
|
||||||
|
beide Aussagen traegt — dass die DOE-Quelle hier landet UND dass kuenftige additive
|
||||||
|
Mitglieder der SourceType-Vereinigung ebenfalls hier landen sollen, statt zu scheitern.
|
||||||
|
Kein Verhaltenswechsel: der Zweig, in den `'doe-opendata'` faellt, ist vorher wie nachher
|
||||||
|
derselbe. Belege das mit den vorhandenen `apps/api`-Tests.
|
||||||
|
|
||||||
|
**(6) Restposten 1 — ZIP-Name im Zertifikat-Aufteiler.** `downloadAllAsZip` in
|
||||||
|
`SplitTab.tsx` ist eine Funktion ausserhalb der Komponente und kann den
|
||||||
|
Uebersetzungs-Hook nicht aufrufen; reiche den Namen deshalb als Parameter herein und
|
||||||
|
uebergib an der Aufrufstelle `t('actions.zipFilename')`. Neuer Schluessel unter
|
||||||
|
`certManager.actions` in beiden Katalogen: deutsch `Zertifikate.zip`, englisch
|
||||||
|
`certificates.zip`.
|
||||||
|
|
||||||
|
Der Einwand aus 260921-bi2 war, ein uebersetzter Name koenne Umlaute auf eine
|
||||||
|
Windows-Freigabe tragen. Neuer Befund: das deutsche Wort fuer Zertifikate enthaelt keinen
|
||||||
|
Umlaut und kein von Windows verbotenes Zeichen, der Einwand trifft fuer diesen konkreten
|
||||||
|
Text also nicht zu. **Verlass dich aber nicht darauf, dass das Wort zufaellig harmlos
|
||||||
|
ist** — sonst haengt die Dateisystem-Sicherheit an einer kuenftigen
|
||||||
|
Uebersetzungsentscheidung. Lege `zip-filename.ts` mit einer kleinen, fuer sich pruefbaren
|
||||||
|
Schutzfunktion an, die einen Namen auf das fuer eine Windows-Freigabe Zulaessige
|
||||||
|
zurueckschneidet: die von Windows verbotenen Zeichen und Steuerzeichen ersetzen,
|
||||||
|
Nicht-ASCII ersetzen, abschliessende Punkte und Leerzeichen entfernen, die reservierten
|
||||||
|
Geraetenamen abfangen, bei leerem Ergebnis auf `certificates.zip` zurueckfallen und die
|
||||||
|
Endung sicherstellen. `SplitTab.tsx` schickt den uebersetzten Namen durch diese Funktion,
|
||||||
|
bevor er am Download landet. Decke die Funktion in `zip-filename.test.ts` ab: der
|
||||||
|
deutsche und der englische Katalogwert kommen unveraendert durch, ein Name mit Umlaut und
|
||||||
|
einer mit verbotenem Zeichen werden bereinigt, ein aussichtsloser Name faellt auf den
|
||||||
|
Ersatznamen zurueck. Die Namen der einzelnen Dateien IM Archiv stammen aus der API und
|
||||||
|
bleiben unangetastet.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && npx biome lint . --reporter=json 2>/dev/null | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const d=JSON.parse(s).diagnostics||[];const a=d.filter(x=>x.category.includes('a11y'));a.forEach(x=>console.log(' ',x.category.replace('lint/a11y/',''),x.location.path+':'+x.location.start.line));console.log('a11y',a.length,'| switch',d.filter(x=>x.category==='lint/complexity/noUselessSwitchCase').length,'| gesamt',d.length,'| errors',d.filter(x=>x.severity==='error').length);})"</automated>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && node -e "const f=o=>Object.entries(o).flatMap(([k,v])=>v&&typeof v==='object'?f(v).map(x=>k+'.'+x):[k]);const de=f(require('./apps/web/src/messages/de.json')),en=f(require('./apps/web/src/messages/en.json'));const A=new Set(de),B=new Set(en);console.log('de',de.length,'en',en.length,'nurDe',de.filter(k=>!B.has(k)).join(',')||'-','nurEn',en.filter(k=>!A.has(k)).join(',')||'-');"</automated>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/web exec vitest run zip-filename 2>&1 | tail -6</automated>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/api exec vitest run 2>&1 | tail -6</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Erste Pruefung meldet `a11y 1` und die einzige verbleibende Zeile nennt
|
||||||
|
`noNoninteractiveElementInteractions` in `calculator-widget.tsx`; `switch 0`;
|
||||||
|
`gesamt 399`; `errors 0`. Katalogpruefung: beide Kataloge gleich lang, weder `nurDe` noch
|
||||||
|
`nurEn` nennt einen Schluessel. `zip-filename`-Tests gruen. `apps/api`-Tests gruen mit
|
||||||
|
mindestens 72 Dateien und mindestens 1143 Tests. Weicht `gesamt` von 399 ab, ist das kein
|
||||||
|
stiller Durchlauf: nenne die Abweichung nach Regel aufgeschluesselt und ihre Ursache.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Aufgabe 3: Verschwendete Abrufe — Kalender-Ladefenster und die vier t-Abhaengigkeiten</name>
|
||||||
|
<files>apps/web/src/components/dashboard/widgets/calendar-widget.tsx, apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx, apps/web/src/lib/translations-identity.test.tsx</files>
|
||||||
|
<read_first>
|
||||||
|
`apps/web/src/components/dashboard/widgets/calendar-month.ts`, Funktion
|
||||||
|
`computeFetchWindow` samt Kommentar: das Ladefenster ist die **Vereinigung** aus
|
||||||
|
42-Tage-Raster und Vorschauzeitraum, und alle vier Zwischenwerte sind auf lokale
|
||||||
|
Tagesgrenzen gerundet, damit der Cache-Schluessel des Backends ueber den
|
||||||
|
5-Minuten-Auffrischer stabil bleibt. Diese Eigenschaft darf der Umbau nicht verlieren.
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Beim Aufbau der Kachel wird die Quellenliste genau einmal geholt.
|
||||||
|
- Ein Monatswechsel holt die Quellenliste NICHT erneut.
|
||||||
|
- Ein Monatswechsel, dessen berechnetes Ladefenster mit dem zuletzt geholten
|
||||||
|
uebereinstimmt, loest KEINEN Termin-Abruf aus.
|
||||||
|
- Ein Monatswechsel mit abweichendem Ladefenster loest genau einen Termin-Abruf aus.
|
||||||
|
- Der 5-Minuten-Auffrischer holt weiterhin beides, auch wenn sich nichts geaendert
|
||||||
|
hat — sonst friert die Anzeige ein.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
**(1) Das doppelte Ladefenster (Restposten 3a).** Der Planer hat nachgerechnet, woher die
|
||||||
|
Beobachtung des Orchestrators kommt: `computeFetchWindow` nimmt `from` als den frueheren
|
||||||
|
von Rasteranfang und heutigem Tagesbeginn und `to` als den spaeteren von Rasterende und
|
||||||
|
Vorschauhorizont. Reicht der Vorschauhorizont ueber das Rasterende hinaus, ergeben **zwei
|
||||||
|
benachbarte kuenftige Monate exakt dasselbe Fenster** — bei der Vorschau-Einstellung
|
||||||
|
90 Tage trifft das fuer Oktober und November zu, bei der Voreinstellung 30 Tage nicht.
|
||||||
|
Das erklaert, warum der Effekt nur unter bestimmten Einstellungen sichtbar ist, und es ist
|
||||||
|
der Grund, warum die Browser-Messung unten ausdruecklich auf 90 Tage gestellt werden muss.
|
||||||
|
|
||||||
|
Die Vereinigung selbst ist richtig und bleibt — die Kachel zeigt Monatsraster UND
|
||||||
|
Terminvorschau. Verschwendet wird nur der erneute Abruf eines bereits geholten Bereichs.
|
||||||
|
Merke dir deshalb in einem `ref` das zuletzt tatsaechlich geholte `from`/`to`-Paar (als
|
||||||
|
die beiden ISO-Zeichenketten, die auch an die API gehen) und ueberspringe den
|
||||||
|
Termin-Abruf, wenn das neu berechnete Paar damit uebereinstimmt. Der Sperrgriff gilt
|
||||||
|
**nur fuer den durch den Monatswechsel ausgeloesten Lauf**; der 5-Minuten-Auffrischer
|
||||||
|
muss unbedingt weiter abrufen, auch bei gleichem Fenster, sonst veraltet die Anzeige
|
||||||
|
still. Gib `loadData` dazu einen Parameter, der das Erzwingen ausdrueckt, und uebergib ihn
|
||||||
|
aus dem Intervall. Achte darauf, dass der uebersprungene Lauf den Ladezustand trotzdem
|
||||||
|
sauber beendet und die bereits geladenen Termine nicht leert.
|
||||||
|
|
||||||
|
**(2) Die Quellenliste bei jedem Monatswechsel (Restposten 3b).** `loadData` ruft heute
|
||||||
|
bei jedem Lauf zuerst `fetchSources()` auf, obwohl die Quellenliste nicht vom angezeigten
|
||||||
|
Monat abhaengt. Halte das Ergebnis in einem `ref` fest und hole die Liste nur, wenn sie
|
||||||
|
noch unbekannt ist ODER der Lauf erzwungen wurde (also beim Aufbau und beim
|
||||||
|
5-Minuten-Auffrischer). Damit bemerkt die Kachel eine neu eingerichtete Quelle weiterhin
|
||||||
|
innerhalb von fuenf Minuten — das heutige Verhalten bleibt also erhalten, nur der
|
||||||
|
Monatswechsel hoert auf, unnoetig zu fragen. Der Sonderfall "gar keine Quellen
|
||||||
|
eingerichtet" muss sich genauso verhalten wie heute.
|
||||||
|
|
||||||
|
Beides sind keine Verhaltensdefekte, sondern Verschwendung — aber sie reicht durch die API
|
||||||
|
bis zu einem echten Exchange-Server durch (siehe threat_model T-JT4-02). Aendere nichts an
|
||||||
|
`computeFetchWindow` selbst und nichts an der Tagesgrenzen-Rundung.
|
||||||
|
|
||||||
|
**(3) Tests fuer beides.** `calendar-widget.test.tsx` ist vorhanden und hat die
|
||||||
|
Abruf-Attrappen bereits eingerichtet. Ergaenze Faelle, die die Aufrufe der Attrappen
|
||||||
|
**zaehlen**: Aufbau (je 1), Monatswechsel mit abweichendem Fenster (Termine +1, Quellen
|
||||||
|
+0), Monatswechsel mit identischem Fenster (beide +0), und ein erzwungener Lauf ueber den
|
||||||
|
Zeitgeber (beide +1) — fuer den letzten Fall die Zeitgeber-Attrappe von Vitest nutzen.
|
||||||
|
Das identische Fenster stellst du her, indem du die Kachel mit der Vorschau-Einstellung
|
||||||
|
90 Tage renderst und um einen Monat weiterschaltest.
|
||||||
|
|
||||||
|
**(4) Restposten 4 — die vier `t`-Abhaengigkeiten.** Der Planer hat die Annahme
|
||||||
|
ueberprueft, auf der 260921-gof beruhte ("`t` ist in diesem Projekt bei jedem Render eine
|
||||||
|
frische Funktion"), und sie im Quelltext der eingesetzten Fassung widerlegt: `use-intl`
|
||||||
|
4.13.0 erzeugt `t` in einem `useMemo`, dessen Abhaengigkeiten ausschliesslich aus dem
|
||||||
|
Intl-Kontext stammen, und der Anbieter steht in `app/layout.tsx`, also oberhalb aller
|
||||||
|
betroffenen Komponenten. Ein Zustandswechsel in einer dieser Komponenten rendert den
|
||||||
|
Anbieter nicht neu, also behaelt `t` seine Identitaet, also bleiben die davon abhaengigen
|
||||||
|
Rueckrufe stabil, also laeuft der Effekt nicht erneut. Dazu passt die bereits erbrachte
|
||||||
|
Messung aus dem gof-Nachtrag: der Marktplatz holt `modules/catalog` in 20 Sekunden genau
|
||||||
|
einmal.
|
||||||
|
|
||||||
|
**Belege das, statt es zu behaupten**, und zwar einmal an der Wurzel statt viermal an den
|
||||||
|
Symptomen: lege `translations-identity.test.tsx` an, das eine kleine Testkomponente unter
|
||||||
|
dem echten `NextIntlClientProvider` rendert, einen Zustandswechsel in der Komponente
|
||||||
|
ausloest und festhaelt, dass `t` vor und nach dem erneuten Render dasselbe Objekt ist.
|
||||||
|
Dieser eine Test entscheidet alle vier Stellen auf einmal, weil der Schadensmechanismus
|
||||||
|
ueberall derselbe ist.
|
||||||
|
|
||||||
|
Danach entscheide nach Messlage, nicht nach Gewohnheit:
|
||||||
|
- **Bleibt die Identitaet erhalten** (erwartet), dann aendere an den vier Stellen
|
||||||
|
**nichts**. Halte in der SUMMARY fest, dass die Faustregel aus gof ("`t` gehoert in
|
||||||
|
keine Abhaengigkeitsliste") als Konvention in Ordnung bleibt, die vier verbliebenen
|
||||||
|
Stellen aber nachweislich keinen zusaetzlichen Abruf ausloesen — und korrigiere dabei
|
||||||
|
ausdruecklich die zugrunde liegende Annahme, damit sie nicht ein drittes Mal
|
||||||
|
weitergetragen wird.
|
||||||
|
- **Faellt die Identitaet doch**, dann wende an den beiden Stellen, die in einem Effekt
|
||||||
|
landen (`marketplace/page.tsx`, `admin/users/page.tsx`), die aus gof bekannte Technik an
|
||||||
|
(uebersetzten Text vor dem Rueckruf in eine Konstante ziehen und von der Konstante
|
||||||
|
abhaengen). Die beiden anderen (`calendar-settings-panel.tsx`,
|
||||||
|
`calendar-source-form.tsx`) bleiben in jedem Fall unveraendert: ihre Rueckrufe haengen
|
||||||
|
an KEINEM Effekt, sondern werden als Ereignisbehandlung weitergereicht — eine wechselnde
|
||||||
|
Identitaet kostet dort nichts. Nenne diesen strukturellen Grund in der SUMMARY.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/web exec vitest run calendar-widget translations-identity 2>&1 | tail -10</automated>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && npx biome lint . --reporter=json 2>/dev/null | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const d=JSON.parse(s).diagnostics||[];console.log('gesamt',d.length,'| a11y',d.filter(x=>x.category.includes('a11y')).length,'| exhaustive',d.filter(x=>x.category==='lint/correctness/useExhaustiveDependencies').length,'| errors',d.filter(x=>x.severity==='error').length);})"</automated>
|
||||||
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/web exec vitest run 2>&1 | tail -8 && pnpm type-check 2>&1 | tail -4 && pnpm lint --force 2>&1 | tail -4</automated>
|
||||||
|
<human-check>
|
||||||
|
Browser-Messung durch den Orchestrator (der Ausfuehrende hat keinen Browser). Instrument
|
||||||
|
ist das Netzwerkprotokoll des Browsers ueber Playwright, **niemals ein `fetch` aus der
|
||||||
|
Seite heraus** (siehe Merkposten "Browser-Pruefung: fetch-Falle"). Vorbereitung: Stack aus
|
||||||
|
dem aktuellen Stand bauen, anmelden, Kalender-Kachel aufs Dashboard legen und ihre
|
||||||
|
Vorschau auf **90 Tage** stellen — bei der Voreinstellung 30 Tage tritt das doppelte
|
||||||
|
Ladefenster rechnerisch gar nicht auf, die Messung waere dann nichtssagend.
|
||||||
|
|
||||||
|
Messung: Netzwerkprotokoll leeren, dann dreimal "Weiter" druecken.
|
||||||
|
|
||||||
|
Erwartung nachher:
|
||||||
|
- `calendar/sources`: genau **1** (vorher 3).
|
||||||
|
- `calendar/events`: **weniger als 3**, und unter den abgesetzten Anfragen tragen keine
|
||||||
|
zwei dasselbe `from`/`to`-Paar in der Abfragezeichenkette (vorher 3, darunter ein
|
||||||
|
identisches Paar fuer zwei benachbarte Monate).
|
||||||
|
- Gegenprobe gegen Einfrieren: rund fuenf Minuten ruhen lassen; danach muss je ein
|
||||||
|
weiterer `calendar/events`- und `calendar/sources`-Abruf erscheinen.
|
||||||
|
|
||||||
|
Datenbank und Modul-Aktivierung so hinterlassen, wie sie vorgefunden wurden; Stack danach
|
||||||
|
stoppen.
|
||||||
|
</human-check>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
`calendar-widget`-Tests belegen durch Zaehlung: Aufbau je 1 Abruf, Monatswechsel mit
|
||||||
|
abweichendem Fenster +1 Termine/+0 Quellen, Monatswechsel mit identischem Fenster +0/+0,
|
||||||
|
erzwungener Lauf +1/+1. `translations-identity.test.tsx` ist gruen und haelt die
|
||||||
|
Identitaetsfrage fuer `t` fest. `gesamt 399`, `a11y 1`, `exhaustive 0`, `errors 0`.
|
||||||
|
`apps/web` mindestens 69 Dateien / 484 Tests, `pnpm type-check` 4/4, `pnpm lint --force`
|
||||||
|
5/5. Fuer jede der vier `t`-Stellen steht in der SUMMARY, ob sie geaendert wurde und
|
||||||
|
warum beziehungsweise warum nicht.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Mensch -> Oberflaeche | Bedienung per Tastatur bzw. Vorlesehilfe statt per Maus — die eigentliche Grenze dieses Vorgangs |
|
||||||
|
| Notizinhalt -> Markdown-Darstellung | Vom Nutzer eingegebener Text wird gerendert; `rehypeSanitize` ist die Schranke |
|
||||||
|
| Web -> API -> Exchange | Jeder Termin-Abruf der Kachel erreicht ueber die eigene API einen fremden Exchange-Server |
|
||||||
|
| Katalogtext -> Dateisystem der Gegenstelle | Ein uebersetzter Name landet als Dateiname auf einer Windows-Freigabe |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||||
|
| T-JT4-01 | Denial of Service | alle 21 klickgebundenen Fundstellen | medium | mitigate | Bedienung, die es nur mit der Maus gibt, schliesst Tastatur- und Vorlesehilfe-Nutzung aus. Jede echte Bedienstelle wird ein `<button>`; belegt durch Komponententests, die eine Tastaturbetaetigung ausloesen, nicht durch Auszeichnungs-Behauptungen. |
|
||||||
|
| T-JT4-02 | Denial of Service | `calendar-widget.tsx` -> API -> Exchange | medium | mitigate | Der Sperrgriff darf nur den Monatswechsel sperren, nie den 5-Minuten-Auffrischer, sonst friert die Anzeige ein (Verfuegbarkeitsschaden statt Ersparnis). Vier zaehlende Testfaelle decken beide Richtungen ab; die Tagesgrenzen-Rundung von `computeFetchWindow` bleibt unangetastet, damit der Backend-Cache-Schluessel stabil bleibt. |
|
||||||
|
| T-JT4-03 | Tampering | `note-widget.tsx` -> `previewOptions` -> `rehypeSanitize` | high | mitigate | Der Umbau des Kaestchen-Handlers fasst das Optionsobjekt neu; faellt dabei `rehypePlugins: [[rehypeSanitize]]` heraus, kehrt die XSS-Luecke T-IEX-01 zurueck. Der Eintrag muss woertlich erhalten bleiben; ein Testfall muss belegen, dass eingebettete Auszeichnung im Vorschaumodus weiterhin entschaerft wird. |
|
||||||
|
| T-JT4-04 | Tampering | `calendar-settings-panel.tsx`, Loeschdialog | high | mitigate | Der Hintergrund des Loeschdialogs wird zu einer echten, fokussierbaren Schaltflaeche und rueckt damit in die Tab-Reihenfolge. Sie MUSS abbrechen und darf unter keinen Umstaenden loeschen; ihre Beschriftung benennt das Abbrechen. Ein Testfall belegt, dass nach diesem Weg die Quelle noch existiert. |
|
||||||
|
| T-JT4-05 | Tampering | `zip-filename.ts` -> Windows-Freigabe | low | mitigate | Ein uebersetzter Dateiname kann Zeichen tragen, die Windows verbietet. Eine fuer sich getestete Schutzfunktion schneidet jeden Namen auf das Zulaessige zurueck und faellt notfalls auf einen sicheren Ersatznamen zurueck — die Sicherheit haengt damit nicht an der Wortwahl einer kuenftigen Uebersetzung. |
|
||||||
|
| T-JT4-06 | Information Disclosure | `widget-catalog-modal.tsx` | low | accept | Die Hintergrundflaeche verliert ihr `aria-hidden`, weil ein fokussierbares Element nicht vor der Vorlesehilfe verborgen sein darf. Sie erhaelt dadurch eine angesagte Beschriftung — gewollt, kein Informationsabfluss: der Dialoginhalt war ohnehin sichtbar. |
|
||||||
|
| T-JT4-SC | Tampering | npm/pip/cargo-Installationen | n/a | accept | Dieser Vorgang installiert kein Paket und hebt keine Version an (D-06). Es gibt keine Installationsaufgabe, daher greift das Paket-Echtheitstor nicht; `pnpm-lock.yaml` und alle `package.json` duerfen im Diff nicht vorkommen. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Abschliessend, nach allen drei Aufgaben:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/vicolab/projects/tessera-ctl
|
||||||
|
|
||||||
|
# 1) Befundstand nach Regel
|
||||||
|
npx biome lint . --reporter=json 2>/dev/null | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const d=JSON.parse(s).diagnostics||[];const a=d.filter(x=>x.category.includes('a11y'));a.forEach(x=>console.log(' a11y',x.category.replace('lint/a11y/',''),x.location.path+':'+x.location.start.line));console.log('gesamt',d.length,'| a11y',a.length,'| errors',d.filter(x=>x.severity==='error').length);})"
|
||||||
|
# erwartet: gesamt 399 | a11y 1 | errors 0
|
||||||
|
# die eine a11y-Zeile nennt calculator-widget.tsx
|
||||||
|
|
||||||
|
# 2) Keine Unterdrueckung als Abkuerzung eingeschleust
|
||||||
|
git diff --unified=0 $(git rev-parse HEAD) -- . | grep -c '^+.*biome-ignore' || true
|
||||||
|
# erwartet: 0 (dieser Vorgang fuegt keine einzige neue Unterdrueckung hinzu)
|
||||||
|
|
||||||
|
# 3) Kataloge im Gleichstand
|
||||||
|
node -e "const f=o=>Object.entries(o).flatMap(([k,v])=>v&&typeof v==='object'?f(v).map(x=>k+'.'+x):[k]);const de=f(require('./apps/web/src/messages/de.json')),en=f(require('./apps/web/src/messages/en.json'));const A=new Set(de),B=new Set(en);console.log('de',de.length,'en',en.length,'nurDe',de.filter(k=>!B.has(k)).join(',')||'-','nurEn',en.filter(k=>!A.has(k)).join(',')||'-');"
|
||||||
|
|
||||||
|
# 4) Tore und Testbestand
|
||||||
|
pnpm -C apps/web exec vitest run 2>&1 | tail -6 # >= 69 Dateien, >= 484 Tests
|
||||||
|
pnpm -C apps/api exec vitest run 2>&1 | tail -6 # >= 72 Dateien, >= 1143 Tests
|
||||||
|
pnpm type-check 2>&1 | tail -4 # 4/4
|
||||||
|
pnpm lint --force 2>&1 | tail -4 # 5/5, 0 error
|
||||||
|
|
||||||
|
# 5) Kein Paket, keine Version, kein Fremdumbruch
|
||||||
|
git diff --stat $(git rev-parse HEAD) -- pnpm-lock.yaml '**/package.json'
|
||||||
|
# erwartet: leer
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Zaehlungen stammen ausschliesslich aus dem Feld `category` der Biome-JSON-Ausgabe,
|
||||||
|
niemals aus einem Textgriff in den Quelltext — ein Kommentar, der den Namen einer Regel
|
||||||
|
nennt, wuerde eine Textzaehlung sonst selbst verfaelschen.
|
||||||
|
|
||||||
|
**Schritt des Orchestrators (nicht des Ausfuehrenden):** die Browser-Zaehlung aus dem
|
||||||
|
`human-check` in Aufgabe 3. Der Ausfuehrende hat keinen Browser und darf dieses Ergebnis
|
||||||
|
nicht behaupten.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Biome: 399 Befunde gesamt, 0 der Stufe error, genau 1 a11y-Befund
|
||||||
|
(`calculator-widget.tsx`, begruendet stehengelassen), `useSemanticElements` unveraendert
|
||||||
|
bei 0, `noUselessSwitchCase` bei 0.
|
||||||
|
- Keine einzige neue `biome-ignore`-Zeile im gesamten Diff dieses Vorgangs.
|
||||||
|
- 29 der 30 a11y-Fundstellen behoben, jede mit protokolliertem Weg; fuer die eine
|
||||||
|
stehengelassene ist begruendet, warum sie bleibt und warum die Umgehung per
|
||||||
|
`addEventListener` ausdruecklich nicht gewaehlt wurde.
|
||||||
|
- Tastaturbedienung ist durch Tests mit echten Tastaturereignissen belegt, nicht durch
|
||||||
|
Auszeichnungsbehauptungen.
|
||||||
|
- Kalender-Kachel: Quellenliste 1 statt 3 pro drei Monatswechseln, kein Termin-Abruf mit
|
||||||
|
wiederholtem `from`/`to`-Paar, 5-Minuten-Auffrischer nachweislich unberuehrt.
|
||||||
|
- ZIP-Name uebersetzt UND durch eine getestete Schutzfunktion auf Windows-Freigaben
|
||||||
|
gueltig.
|
||||||
|
- Die vier `t`-Abhaengigkeiten sind durch Messung entschieden, nicht durch Gewohnheit; die
|
||||||
|
zugrunde liegende Annahme aus gof ist ausdruecklich korrigiert oder bestaetigt.
|
||||||
|
- `apps/web` >= 69 Dateien / >= 484 Tests, `apps/api` >= 72 / >= 1143, `pnpm type-check`
|
||||||
|
4/4, `pnpm lint --force` 5/5.
|
||||||
|
- Keine neue Abhaengigkeit, keine Versionsanhebung, kein repo-weiter Umbruch (D-06).
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<commit_hygiene>
|
||||||
|
Ein Commit je Teilumbau, nicht ein Sammelcommit je Aufgabe — das ist der Grund, warum die
|
||||||
|
Umbauten oben einzeln nummeriert sind.
|
||||||
|
|
||||||
|
**Fallstrick, der heute viermal zugeschlagen hat:** Das Write-Werkzeug wandelt Folgen der
|
||||||
|
Form `\uXXXX` im Inhalt still in das tatsaechliche Zeichen um. So sind rohe Steuerzeichen
|
||||||
|
in einen Plan, in Commit-Nachrichten und in eine SUMMARY geraten, woraufhin `git commit`
|
||||||
|
die Annahme verweigert hat. Erzeuge solche Folgen ueber `python3` mit `chr(92)` und pruefe
|
||||||
|
die Rohbytes danach nach, bevor du committest.
|
||||||
|
</commit_hygiene>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Erstelle `.planning/quick/260921-jt4-barrierefreiheit-mit-bedienentscheidunge/260921-jt4-SUMMARY.md`,
|
||||||
|
wenn alle drei Aufgaben abgeschlossen sind. Die SUMMARY muss enthalten:
|
||||||
|
|
||||||
|
1. Eine Tabelle aller 30 a11y-Fundstellen mit gewaehltem Weg (echter Button /
|
||||||
|
role+Tastatur / Fokus-auf-Oeffnen / Rolle korrigiert / stehengelassen) und je einer
|
||||||
|
Zeile Begruendung, wo der gerade Weg nicht ging.
|
||||||
|
2. Je einen Abschnitt zu den vier Restposten mit dem Ergebnis — einschliesslich der
|
||||||
|
ausdruecklichen Aussage, ob der alte Einwand gegen den uebersetzten ZIP-Namen noch
|
||||||
|
traegt, und ob die Annahme "`t` ist bei jedem Render frisch" bestaetigt oder korrigiert
|
||||||
|
wurde.
|
||||||
|
3. Die ehrliche Einordnung der vier `aria-hidden`-Ergaenzungen an den `<img>`-Elementen:
|
||||||
|
richtige Auszeichnung, aber kein Gewinn fuer einen Menschen.
|
||||||
|
4. Die Vorher/Nachher-Zahlen aus der Biome-JSON-Ausgabe, nach Regel aufgeschluesselt.
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user