fix(web): Kalender-Ladeeffekt an monthDate statt getTime(), Stoppuhr-Takt liest nur noch Einzelwerte

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
This commit is contained in:
2026-09-21 12:23:35 +02:00
parent 54fdf699dc
commit b3f0e3cdcd
4 changed files with 122 additions and 19 deletions
@@ -73,11 +73,10 @@ export function CalendarWidget({ config }: WidgetProps) {
} | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
const { showMonth, maxEvents, lookaheadDays } = useMemo(
() => resolveCalendarConfig(config),
// eslint-disable-next-line react-hooks/exhaustive-deps
[config.showMonth, config.maxEvents, config.lookaheadDays],
);
// Befund 1/2 (quick-260921-gof): keine Merkung noetig — resolveCalendarConfig
// ist eine reine Funktion, die nur drei einfache Werte liefert; eine
// Merkung um das ganze config-Objekt hat hier nie etwas gespart.
const { showMonth, maxEvents, lookaheadDays } = resolveCalendarConfig(config);
useEffect(() => {
let cancelled = false;
@@ -128,8 +127,15 @@ export function CalendarWidget({ config }: WidgetProps) {
clearInterval(intervalRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [monthDate.getTime(), lookaheadDays]);
// Befund 3/4 (quick-260921-gof): haengt jetzt direkt an monthDate statt an
// monthDate.getTime(). Das ist nur deshalb gefahrlos, weil showToday
// (unten) das Datum identitaetserhaltend setzt, wenn der gewuenschte
// Monat bereits angezeigt wird — sonst wuerde jeder Druck auf den
// Monatsknopf im laufenden Monat einen neuen Termin-Abruf ausloesen, der
// ueber die API bis zum Exchange-Server durchschlaegt (D-04). Das
// Ladefenster bleibt weiterhin auf lokale Tagesgrenzen gerundet, der
// Backend-Cache-Schluessel aendert sich dadurch nicht.
}, [monthDate, lookaheadDays]);
const showPrev = useCallback(() => {
setHover(null);
@@ -141,8 +147,18 @@ export function CalendarWidget({ config }: WidgetProps) {
}, []);
const showToday = useCallback(() => {
setHover(null);
const now = new Date();
setMonthDate(new Date(now.getFullYear(), now.getMonth(), 1));
// Identitaetserhaltend: wird der aktuelle Monat bereits angezeigt, bleibt
// dieselbe monthDate-Referenz stehen, statt ein frisches Date-Objekt zu
// erzeugen. Der Ladeeffekt haengt jetzt direkt an monthDate — ohne diese
// Stabilisierung wuerde jeder Druck auf den Knopf einen neuen
// Termin-Abruf ausloesen (Befund 3/4, D-04).
setMonthDate((d) => {
const now = new Date();
if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth()) {
return d;
}
return new Date(now.getFullYear(), now.getMonth(), 1);
});
}, []);
const eventsByDate = useMemo(() => groupEventsByDate(events), [events]);