fix(quick-260921-jt4): verschwendete Kalender-Abrufe abgestellt, t-Identitaet gemessen statt behauptet
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
This commit is contained in:
@@ -73,6 +73,18 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
} | null>(null);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
|
||||
|
||||
// Restposten 3a/3b (quick-260921-jt4): zwei Merkungen ausserhalb des
|
||||
// Render-Zustands, damit ein Monatswechsel, der weder die Quellenliste noch
|
||||
// das Termin-Ladefenster tatsaechlich aendert, keinen erneuten Abruf
|
||||
// ausloest — der 5-Minuten-Auffrischer (siehe `force` unten) ruft
|
||||
// unabhaengig davon immer beide ab, sonst veraltet die Anzeige still.
|
||||
// hasSourcesRef: `null` heisst "noch nie erfolgreich geholt".
|
||||
const hasSourcesRef = useRef<boolean | null>(null);
|
||||
// lastFetchWindowRef: die beiden ISO-Zeichenketten, die zuletzt
|
||||
// TATSAECHLICH an fetchEvents gingen (nicht nur berechnet) — derselbe
|
||||
// Vergleichswert, den auch die API sieht.
|
||||
const lastFetchWindowRef = useRef<{ from: string; to: string } | null>(null);
|
||||
|
||||
// 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.
|
||||
@@ -81,13 +93,23 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadData() {
|
||||
// `force`: true nur fuer den 5-Minuten-Auffrischer. Ein durch
|
||||
// Monatswechsel ausgeloester Lauf (force=false) darf beide Sperrgriffe
|
||||
// unten greifen lassen; der Auffrischer muss sie UNBEDINGT umgehen,
|
||||
// sonst friert die Anzeige ein.
|
||||
async function loadData(force: boolean) {
|
||||
try {
|
||||
// Check if user has any sources configured
|
||||
const sources = await fetchSources();
|
||||
if (cancelled) return;
|
||||
// Restposten 3b: die Quellenliste haengt nicht vom angezeigten Monat
|
||||
// ab — nur holen, wenn sie noch unbekannt ist ODER der Lauf
|
||||
// erzwungen wurde (Aufbau: hasSourcesRef ist null, greift also
|
||||
// ohnehin; Auffrischer: force=true).
|
||||
if (hasSourcesRef.current === null || force) {
|
||||
const sources = await fetchSources();
|
||||
if (cancelled) return;
|
||||
hasSourcesRef.current = sources.length > 0;
|
||||
}
|
||||
|
||||
if (sources.length === 0) {
|
||||
if (!hasSourcesRef.current) {
|
||||
setHasSources(false);
|
||||
setEvents([]);
|
||||
setIsLoading(false);
|
||||
@@ -99,8 +121,26 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
// Fetch aggregated events for the current grid/lookahead window —
|
||||
// ALWAYS with both day-boundary ISO strings (T-HTC-03).
|
||||
const { from, to } = computeFetchWindow(monthDate, lookaheadDays);
|
||||
const eventData = await fetchEvents(from.toISOString(), to.toISOString());
|
||||
const fromIso = from.toISOString();
|
||||
const toIso = to.toISOString();
|
||||
|
||||
// Restposten 3a: das neu berechnete Fenster deckt sich bei einem
|
||||
// Monatswechsel manchmal mit dem zuletzt tatsaechlich geholten
|
||||
// (siehe Kopfkommentar computeFetchWindow/SUMMARY) — dann ist der
|
||||
// Abruf verschwendet und faellt bis zum naechsten Server durch.
|
||||
const windowUnchanged =
|
||||
!force &&
|
||||
lastFetchWindowRef.current !== null &&
|
||||
lastFetchWindowRef.current.from === fromIso &&
|
||||
lastFetchWindowRef.current.to === toIso;
|
||||
if (windowUnchanged) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventData = await fetchEvents(fromIso, toIso);
|
||||
if (cancelled) return;
|
||||
lastFetchWindowRef.current = { from: fromIso, to: toIso };
|
||||
setEvents(eventData);
|
||||
} catch {
|
||||
// Silent fail — show empty state
|
||||
@@ -114,11 +154,16 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
// Aufbau bzw. Monatswechsel: nicht erzwungen — die beiden Sperrgriffe
|
||||
// oben entscheiden selbst, ob wirklich etwas Neues zu holen ist (beim
|
||||
// allerersten Aufbau sind beide Merkungen leer, holen also ohnehin).
|
||||
loadData(false);
|
||||
|
||||
// Refresh every 5 minutes (300000ms) to match backend cache TTL
|
||||
// Refresh every 5 minutes (300000ms) to match backend cache TTL — IMMER
|
||||
// erzwungen, sonst bemerkt die Kachel weder eine neue Quelle noch neue
|
||||
// Termine innerhalb des Fensters.
|
||||
intervalRef.current = setInterval(() => {
|
||||
loadData();
|
||||
loadData(true);
|
||||
}, 300_000);
|
||||
|
||||
return () => {
|
||||
|
||||
Reference in New Issue
Block a user