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