Restposten 3a (doppeltes Ladefenster): lastFetchWindowRef merkt sich das
zuletzt TATSAECHLICH geholte from/to-Paar; ein Monatswechsel, dessen
berechnetes Fenster damit uebereinstimmt, ueberspringt fetchEvents. Der
5-Minuten-Auffrischer (force=true) umgeht den Sperrgriff immer, sonst
friert die Anzeige ein. computeFetchWindow und die Tagesgrenzen-Rundung
bleiben unangetastet.
Restposten 3b (Quellenliste je Monatswechsel): hasSourcesRef merkt sich
das Ergebnis; fetchSources laeuft nur beim Aufbau (Merkung leer) oder
erzwungen (Auffrischer) — eine neu eingerichtete Quelle wird weiterhin
binnen fuenf Minuten bemerkt.
Restposten 4 (t-Identitaet): neue translations-identity.test.tsx rendert
eine Testkomponente unter dem ECHTEN NextIntlClientProvider und beweist
per Referenzgleichheit, dass t bei einem lokalen Zustandswechsel
dasselbe Funktionsobjekt bleibt — bestaetigt durch den use-intl-4.13.0-
Quelltext (translate entsteht in einem useMemo, dessen Abhaengigkeiten
ausschliesslich aus dem root-staendigen Intl-Kontext stammen). Die
Faustregel aus 260921-gof ("t gehoert in keine Abhaengigkeitsliste")
bleibt als Konvention in Ordnung; die zugrunde liegende Annahme ("t ist
bei jedem Render frisch") ist damit ausdruecklich WIDERLEGT statt ein
drittes Mal weitergetragen. Die vier verbliebenen Stellen
(marketplace/page.tsx, admin/users/page.tsx,
calendar-settings-panel.tsx, calendar-source-form.tsx) bleiben deshalb
unveraendert.
Vier neue zaehlende Testfaelle in calendar-widget.test.tsx (Aufbau je 1,
abweichendes Fenster +1/+0, identisches Fenster +0/+0, erzwungener Lauf
+1/+1) plus ein Test, dass ein uebersprungener Lauf den Ladezustand
sauber beendet und geladene Termine nicht leert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
header.tsx, account-settings-form.tsx, favorites-widget.tsx (zweimal):
alle vier tragen bereits alt="", sind also schon aus dem
Zugaenglichkeitsbaum genommen; aria-hidden sagt dasselbe nur
ausdruecklich. Ehrliche Einordnung: richtige Auszeichnung, verbessert
fuer keinen Menschen etwas — der Befund verschwindet, weil die Regel
ein verborgenes Element nicht mehr betrachtet. onError ist ein
Ladefehler, keine Bedienung: hier gab es nie einen Tastaturweg zu
schaffen. Keine Unterdrueckung, kein biome-ignore.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
calculator-widget.tsx Speicherzeile und favorites-widget.tsx
Ansichtsumschalter waren schlichte <div> mit aria-label, das die Rolle
generic stillschweigend verwarf. role="toolbar" ergaenzt (geprueft
sauber; role="group"/"region" loesen useSemanticElements neu aus).
Taschenrechner: fest verdrahteter Text wandert in
calculator.memoryLabel; bei dieser Gelegenheit auch Anzeigefeld
(displayLabel) und Rueckschritt-Taste (backspaceLabel) in den Katalog
gezogen (D-05), da diese Datei ohnehin geaendert wird.
Favoriten: die bisherige Beschriftung ("Favoriten") war sachlich
falsch fuer einen Listen/Kachel-Umschalter — neuer, zutreffender
Schluessel favorites.viewModeLabel statt des wiederverwendeten
favorites.name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- Zelle rendert als <button type="button"> NUR wenn hasEvents gilt,
sonst unveraendert als <div> ohne Handler (D-01) — 42 neue Tab-Stopps
waeren eine Verschlechterung
- onFocus/onBlur mit demselben Rumpf wie onMouseEnter/onMouseLeave,
damit die Termin-Einblendung auch per Tastatur erscheint/verschwindet
- cellClass unveraendert uebernommen, nur w-full text-left ergaenzt
(D-04, kein optischer Unterschied); widgetNoDrag bleibt erhalten
- aria-label nennt Datum und Terminzahl ueber neue Katalogschluessel
widgets.calendar.dayEventsOne/dayEventsMany (Mehrzahl-Konvention wie
configMaxEventsOne/Many, D-05)
- Neue Tests: Tag mit Terminen ist <button> und reagiert auf
Fokus/Weggehen wie auf Maus-Hover; Tag ohne Termine bleibt <div>;
Einzahl-/Mehrzahl-Beschriftung; echter Tab-Stopp nachgewiesen
Nach diesem Umbau: klick-regeln 5 (nur die vier <img onError> +
Taschenrechner-Rahmen), semantic 0, a11y gesamt 14, errors 0 — Aufgabe
1 des Plans vollstaendig.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- NoteCheckbox traegt jetzt ein echtes onChange und gibt sein eigenes
DOM-Element an onToggle weiter; readOnly entfaellt (D-01) — war bisher
nur da, um Reacts Warnung ueber ein gesteuertes Feld ohne onChange zu
unterdruecken
- Index-Ermittlung bleibt wortgleich (alle Kaestchen im Behaelter
einsammeln, indexOf auf dem ausloesenden Element), wandert aber vom
Behaelter-onClick in handleCheckboxToggle, das den Behaelter ueber ein
ref statt event.currentTarget findet
- previewOptions als useMemo mit leerer Abhaengigkeitsliste, Rueckruf
ueber ein ref erreicht — identitaetsstabil wie die alte Modulkonstante,
T-JT4-03: rehypePlugins: [[rehypeSanitize]] unveraendert erhalten
- Tests: echte Tastaturbetaetigung (Leertaste auf fokussiertem
Kaestchen) UND echter Klick loesen onToggle/PATCH aus; neue Tests
belegen readOnly/disabled entfallen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- Aeussere Flaeche verliert onClick, die bislang nur optische
Hintergrund-Flaeche wird eine benannte <button> und traegt onClose
(D-01); verliert dabei aria-hidden, weil ein fokussierbares Element
nicht verborgen sein darf
- stopPropagation auf der Dialogflaeche entfaellt als toter Code, weil
der Hintergrund jetzt Geschwister statt Vorfahr ist
- Fest verdrahtetes englisches aria-label="Close" durch common.close
ersetzt
- Neuer Katalogschluessel widgets.catalogClose in de.json/en.json
- Neue Testdatei: Hintergrund schliesst (Klick + Tastatur), Dialogklick
schliesst nicht, Escape weiterhin, Kartenauswahl fuegt Widget hinzu
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- Wirkungslose eslint-disable-Zeile in stopwatch-widget.tsx ersetzt durch
Sachhinweis: Zeilen halten keinen Zustand, Rundennummer wird aus Laenge
und Position berechnet. Keine neue Unterdrueckung, ARRAYKEY bleibt bei 19.
- Neuer Testfall in stopwatch-widget.test.tsx: zwei Runden nacheinander,
neuere Runde steht oben, Rundennummern 2/1 stimmen zu ihrer eigenen Zeit.
- Neue MergeTab.test.tsx: Entfernen der mittleren Datei laesst genau erste
und dritte Datei mit eigenem Namen und eigenem Entfernen-Knopf uebrig.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
Befunde 7-15 der Biome-Regel useExhaustiveDependencies (quick-260921-gof):
- VehicleTable, TenderDetail, DigestIntervalForm, SourceConfigForm,
favorites-widget: Ersatz-Fehlertext aus useTranslations wird jetzt vor
dem Effekt/Rueckruf in eine Konstante gezogen und diese Konstante in
die Abhaengigkeitsliste aufgenommen - `t` selbst kommt nirgends mehr
in eine Liste. In diesem Projekt ist belegt, dass `t` bei jedem
Durchlauf eine frische Funktion ist (Testattrappen), eine `t`-
Abhaengigkeit haette den jeweiligen Mount-Abruf zur Schleife gemacht.
- RssFeedListForm.tsx und SavedSearchBar.tsx: die Ladefunktionen waren
gewoehnliche Funktionen im Rumpf (bei jedem Durchlauf neu) - jetzt in
einen stabilen Rueckruf mit der Text-Konstante als einziger
Abhaengigkeit eingepackt.
- ResultsList.tsx: Befund 8 (t) wie oben, Befund 15 (refreshKey) in den
Effekt verschoben, der `load` aufruft, statt in `load` selbst zu
stehen - eine begruendete `biome-ignore`-Zeile (erste im Projekt)
haelt fest, dass der Auffrisch-Ausloeser der Elternseite ohne diese
Abhaengigkeit wirkungslos waere.
- Sieben Testdateien um eine Zaehlprobe erweitert: erneutes Zeichnen mit
unveraenderten Props darf keinen weiteren Abruf ausloesen; ResultsList
zusaetzlich um eine Probe, dass ein refreshKey-Bump genau einen
weiteren Abruf ausloest.
- DigestIntervalForm hat keine Testdatei - nur am laufenden System auf
Meine Quellen geprueft (siehe SUMMARY).
- Wirkungslose eslint-disable-Zeilen fuer diese Regel entfallen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
Befunde 1-6 der Biome-Regel useExhaustiveDependencies (quick-260921-gof):
- calendar-widget.tsx: useMemo um resolveCalendarConfig() entfernt
(reine Funktion, spart nichts). showToday setzt monthDate jetzt
identitaetserhaltend, wenn der aktuelle Monat schon angezeigt wird -
erst danach durfte der Ladeeffekt von monthDate.getTime() auf
monthDate umgestellt werden, sonst haette jeder Druck auf den
Monatsknopf im laufenden Monat einen Termin-Abruf bis zum
Exchange-Server ausgeloest (D-04).
- stopwatch-widget.tsx: neue reine Hilfsfunktion computeElapsedFrom()
fuer den Takt-Effekt, der jetzt nur noch drei Einzelwerte statt des
ganzen sw-Objekts liest - eine sw-Abhaengigkeit haette den 100-ms-Takt
bei jeder aufgezeichneten Runde ab- und wiederaufgebaut.
- Testerweiterungen als Rueckfallsicherungen: 2x weiterblaettern -> 3
Abrufe, 3x Monatsknopf im laufenden Monat -> kein Zusatzabruf; Runde
waehrend die Stoppuhr laeuft unterbricht den Takt nicht, genau 1 PATCH
je Klick.
- Wirkungslose eslint-disable-Zeilen fuer diese Regel entfallen (kein
ESLint mehr im Projekt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
Vier der fuenf noch offenen Regeln aus Teillieferung B bereinigt:
- noRedundantRoles (4): ueberfluessige role-Angaben auf button/time/select
entfernt (maschineller --unsafe-Fix, gelesen).
- useAriaPropsForRole (1): entfaellt automatisch mit obigem Fix -- das
<select role="combobox"> in search-widget.tsx verlangte die fehlenden
ARIA-Attribute nur wegen der ueberfluessigen Rolle.
- useSemanticElements (4): admin-sidebar/settings-sidebar tragen role=
"navigation" jetzt am <nav> statt am <aside> (kein doppeltes Landmark
mehr); widget-wrapper.tsx ist jetzt ein echtes <article> statt
div role="article"; DropZone.tsx trennt die Datei-Entfernen-Schaltflaeche
als Geschwister ab, damit die Drop-Flaeche selbst ein echtes <button>
werden kann (ein <button> darf kein zweites <button> verschachteln).
Die Drop-Flaeche traegt darum jetzt Klick- UND Drag-Handler direkt am
<button>, sonst waere sie ein "statisches" Element mit Ereignis-Handlern
geworden (die zurueckgestellten Regeln noStaticElementInteractions /
noNoninteractiveElementInteractions waeren neu angeschlagen -- geprueft,
bleiben bei 5/11).
- noNoninteractiveTabindex (1): calculator-widget.tsx traegt jetzt
tabIndex={-1} statt {0}. Die Zifferntasten sind bereits echte <button>
und damit selbst Teil der Tab-Reihenfolge; Tastendruecke erreichen
handleKeyboard weiterhin per Bubbling, sobald eine Taste fokussiert ist.
Verhalten unveraendert, nur ein wirkungsloser Tab-Stopp auf dem Container
selbst entfaellt.
Verbleibend: noLabelWithoutControl (22), naechster Schritt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
useButtonType: "Widget hinzufuegen"-, Bearbeiten-Modus- und Katalog-Buttons
(kein Formular in diesen Dateien) erhalten type="button".
noSvgWithoutTitle: Widget-Icons in der Katalog-Kachel (immer neben dem
Widget-Namen) und Buttons mit bestehendem aria-label/title werden dekorativ
(aria-hidden="true"); der bislang unbeschriftete Such-Button im Such-Widget
erhaelt aria-label={t('search.searchButton')} (neuer Schluessel, siehe
vorherige i18n-Festschreibung).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- Aufgabe 2: vier sichere Biome-Regeln (useImportType pfadgebunden auf
apps/web+packages, noUselessEscapeInRegex, useConst,
useExponentiationOperator) sowie fuenf ungesicherte Regeln
(useNodejsImportProtocol, useLiteralKeys, useOptionalChain, useTemplate,
useParseIntRadix) angewendet und den gesamten Diff von Hand gelesen
(ldap.service.ts zeichenweise gegen Gross-/Kleinschreibung der
AD-Merkmale, auth.service.ts/jwt.strategy.ts gegen Durchwinken bei
fehlender Sitzung geprueft)
- noUselessSwitchCase bleibt bewusst stehen (tender-normalizer.service.ts:60,
die Fallmarke dokumentiert Absicht)
- Toter Code (D-03): fuenf folgenlose Auffangvariablen entfernt, eine
nicht benutzte Funktion (forSystemQuery, Pruefskript) entfernt, ein
positionsgebundener Dekoratorparameter umbenannt (current-user.decorator.ts),
fuenf Symptomfunde entfernt und als Folgeaufgaben zu melden (siehe unten)
- Sechs weitere, im Plan nicht namentlich gelistete aber
gleich-kategorische Dead-Code-Fundstellen in Testdateien zusaetzlich
bereinigt (groups.service.spec.ts, cert-manager.test.tsx,
ldap.service.spec.ts, prisma-tenant.extension.spec.ts x3) — noetig, um
die vom Plan selbst verlangten Nullstaende bei noUnusedVariables/
noUnusedImports/noUnusedFunctionParameters zu erreichen
Dekoratordaten aus apps/api unveraendert (593 Zeilen, sha256 6e1583f1...).
Endstand 620 Befunde (541 echt, 79 Test) statt der im Plan geschaetzten
621/542 — eine Differenz von 1, weil das Streichen des Namens aus
`catch (e: any)` in calendar.service.ts (Symptom-Fix) den dort ebenfalls
gemeldeten noExplicitAny-Befund miteliminiert; das ist eine erwuenschte
Nebenwirkung, keine Regression. Fehlerstufe 0, beide Testlaeufe
punktgleich gruen (69/1124, 66/459), pnpm type-check 4/4, pnpm lint
--force 5/5.
Folgeaufgaben aus D-03 (nicht in diesem Vorgang behoben):
- force-password-change.interceptor.ts: Freigabeliste prueft nur den Pfad,
nicht die HTTP-Methode
- change-password/page.tsx: nach erzwungenem Wechsel bleibt die Person auf
der Seite stehen (keine Weiterleitung, keine Aktualisierung der
Benutzerablage)
- VehicleTable.tsx: Loeschschaltflaeche hat keinen Besetztzustand, laesst
sich doppelt ausloesen
- SplitTab.tsx: downloadAllAsZip erhielt eine ungenutzte
Uebersetzungsfunktion, Hinweis auf fest verdrahtete Texte im Zip-Pfad
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- Plaketten-Wrapper calendar-day-badges rendert buildDayBadges(day.events) statt der alten Einzel-Plakette
- Ein Kalender: unveraendert eine Plakette in Kalenderfarbe/Akzentfarbe (Tests 3/3b/3c bleiben gruen)
- Zwei/drei Kalender: je ein kleiner Kreis pro Kalender in Startreihenfolge (Test 3d)
- Vier oder mehr Kalender: zwei Kreise plus grauer Restkreis bg-muted-foreground/text-background mit Summe (Test 3e)
- CHANGELOG-Stichpunkt erweitert (mehrere Kalender am selben Tag)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- groupDayBySource gruppiert Tagestermine nach sourceId (nicht Farbe), Reihenfolge = erstes Auftreten, Farbe = Farbe des ersten Termins der Gruppe
- buildDayBadges baut daraus bis zu drei Plaketten-Kreise, ab dem vierten Kalender einen grauen Restkreis mit Summe
- Tests 8/9 in calendar-month.test.ts (TDD: RED bestaetigt vor Implementierung)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- groupEventsByDate sortiert jede Tagesgruppe jetzt nach Start aufsteigend
- Zaehl-Plakette im Monatsraster traegt die Farbe des Kalenders des fruehesten Termins (weisse Schrift), ohne Farbe unveraendert bg-primary
- Tooltip-Zeilen bekommen denselben Farbpunkt wie die Liste "Naechste Termine" (Rueckfall var(--muted-foreground))
- 3 neue Tests: calendar-month Test 2b, calendar-widget Test 3b/3c
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- useTheme (next-themes) + mounted-Guard wie changelog-view.tsx
- data-color-mode am Wurzel-Div folgt resolvedTheme statt fest 'auto'
- zwei neue Tests (dark/light) via vi.hoisted-Themenzustand
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Titel-Span im Tooltip: min-w-0 break-words statt truncate
- Breite (288px) und Rand-Klemmung aus TOOLTIP_WIDTH_PX/TOOLTIP_EDGE_PX
- Test 4c: belegt Klassen und Tooltip-Breite
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- WIDGET_CONSTRAINTS: Minima aus dem Innenaufbau gerechnet (clock 2/2, search 6/2,
calendar 3/3, note 4/4, calculator 3/9, favorites 3/3, link 3/2, stopwatch 4/3),
Vorgaben unveraendert; Test A pinnt alle 32 Werte per toEqual
- dashboard-grid.tsx: effectiveLayouts (useMemo) ueberschreibt minW/minH jedes
gespeicherten Eintrags in jedem Breakpoint aus den Konstanten und hebt zu
kleine w/h auf das Minimum an — RGL 2.2.3 nimmt gespeicherte Eintraege
woertlich (synchronizeLayoutWithChildren) und liest data-grid nur ohne Eintrag
- dashboard-grid.test.tsx: Test 5 angepasst, Test 9 (Ueberschreibung je
Breakpoint, unbekannter Typ unveraendert) und Test 9b (h 8 -> 9, w >= minW bleibt)
- stopwatch-widget.tsx: Bedienleiste px-2 py-1 text-xs, Zeile gap-1 py-1, damit
Stop + Runde + Reset in 4 Spalten passen und die Kachel auf 4x3 schrumpfen kann
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
Ueber "Keine Widgets aktiv" stand noch dasselbe handgezeichnete
Vier-Quadrate-Symbol, das auf der Anmeldeseite bereits durch das Logo ersetzt wurde
— beim Logo-Einbau uebersehen, weil der leere Zustand nur ohne Widgets sichtbar ist.
Jetzt die echte Bildmarke aus der Marken-Komponente, ohne den bisherigen grauen
Rahmen und ohne Kontur: die Marke steht frei auf dem Seitenhintergrund und braucht
keine eigene Flaeche. 226 Web-Tests gruen, im Browser gegengeprueft.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
<img src={fav.iconUrl}> hotlinked the external favicon directly; sites
that send Cross-Origin-Resource-Policy: same-origin (e.g. claude.ai)
get blocked by the browser (net::ERR_BLOCKED_BY_RESPONSE.NotSameOrigin),
leaving only the letter fallback. Points src at the new same-origin
/api-proxy/favorites/:id/icon route instead (same pattern already used
for the user avatar image). Render guard and onError fallback unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- dashboard-grid.test: add noCompactor to react-grid-layout mock
- note-widget.test: enable edit mode before typing (onChange is undefined
when isEditing=false), replace native dispatchEvent with fireEvent.change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CR-01: fix SSRF bypass — isPrivateIpv6 now delegates ::ffff:<ipv4> to
isPrivateIpv4, covering 172.16-31.x and 169.254.x ranges
- CR-02: add ParseUUIDPipe to GET /favorites widgetId param + service guard
so missing widgetId returns 400 instead of leaking all user favorites
- WR-01: link-widget — replace raw 'link.error' key with t('link.error') (4 sites)
- WR-02: favorites-widget — fix load-path error to use t('favorites.error')
- WR-03: widget-catalog-modal — move aria-hidden from outer wrapper to backdrop
- WR-04: calculator — remove duplicate M button (MR clone); MC/MR/M+/M−/MS remain
- WR-05: schema — add FavoriteLink→WidgetInstance FK with onDelete:Cascade
- IN-01: create-widget.dto.ts — update comment from four to eight supported types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fetchFavorites called with instanceId on mount
- Link renders as anchor with target="_blank" rel="noreferrer" (T-08-12)
- Empty + edit mode shows add form; createFavorite called on submit
- Single-link enforcement: add form hidden when link exists (D-06)
- Edit mode: updateFavorite called with id and new title
- View toggle: list default, tile container on gridView click
- Letter fallback: iconUrl null shows first uppercase letter
- favorites-api.ts: FavoriteLink type + fetchFavorites/createFavorite/updateFavorite/deleteFavorite
all use credentials: include and API_URL/favorites
- favorites-widget.tsx: list/grid view, inline add/edit/delete in edit mode,
icon + letter fallback, rel=noreferrer + target=_blank, no dangerouslySetInnerHTML (T-08-07)
- useEffect deps fixed to [instanceId] only — excludes t() to prevent re-fetch on each render
- page.tsx: wireFavoritesWidget(FavoritesWidget) wired
- Full test suite: 81/81 pass (17 test files)
- Web TypeScript: clean
- fetchFavorites called with instanceId (widgetId scope, Pitfall 3)
- covers add/edit/delete, empty state, list/grid toggle, letter fallback
- tests fail: favorites-widget.tsx and favorites-api.ts do not exist yet
- stopwatch-widget.tsx: start/stop/reset/lap controls, setInterval tick (100ms)
- Reload reconstruction: Date.now() - startedAt + elapsed (Pitfall 2 fix)
- State persisted via updateWidgetConfig(instanceId, {...}) on each action
- Single interval cleared on unmount and when not running (T-08-04 mitigated)
- Lap times stored newest-first per RESEARCH recommendation
- No CSS modules — Tailwind only (grep -c module.css = 0)
- page.tsx: added wireStopwatchWidget(StopwatchWidget) import + call
- All 7 stopwatch tests pass (GREEN)
- Tests for start/stop/reset/lap controls
- Reload reconstruction test verifies elapsed from startedAt + stored elapsed
- Tests fail because stopwatch-widget.tsx does not yet exist (expected RED state)
- calendar-api.ts: fetchSources/addSource/updateSource/deleteSource/testSource/fetchEvents with credentials:'include'
- calendar-widget.tsx: upcoming-events list with source color dots, three empty states (no sources/no events/loading)
- widget-registry.tsx: wireCalendarWidget() replaces placeholder with real CalendarWidget
- page.tsx: wires CalendarWidget into registry on mount
- i18n: added calendar.loading key to de.json and en.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Three test cases: event rendering with color dots, no-events empty state, no-sources empty state
- Mocks calendar-api and next-intl following existing test patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SearchWidget: provider dropdown, text input, button; opens search in new tab via window.open (D-14/D-15)
- NoteWidget: MDEditor with compact toolbar, debounced autosave (1500ms), AbortController for in-flight cancellation (D-16/D-17/D-18)
- rehype-sanitize enabled for Markdown XSS prevention (T-05-05)
- Widget registry updated with wireSearchWidget/wireNoteWidget (no more placeholders)
- dashboard-api.ts: added fetchSearchProviders, addSearchProvider, removeSearchProvider, signal support on updateWidgetConfig
- 9 new tests passing (search: 5, note: 4)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install react-grid-layout@2.2.3 and react-resizable
- Create widget-registry.tsx with all 4 widget types, WIDGET_CONSTRAINTS, WidgetProps
- Create dashboard-api.ts with fetch/save layout and widget CRUD functions
- Create dashboard-store.ts (Zustand, NO persist — D-05) with edit mode and auto-save on exit
- Create DashboardGrid with react-grid-layout v2 Responsive, ResizeObserver width
- Create ClockWidget using Intl.DateTimeFormat (no manual UTC offsets)
- Create WidgetWrapper with drag handle and delete button in edit mode
- Create EditModeToggle (pencil/checkmark), WidgetCatalogModal (2x2 grid)
- Rewrite portal page.tsx as dashboard with grid, edit toggle, widget catalog
- Add ResizeObserver polyfill in test setup, CSS mock support in vitest config
- All 5 tests green (dashboard grid + clock widget)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>