fix(reminders): keine Dauermeldung ohne localStorage, Mail nach Zeitaenderung, null-Pruefung

- Merkliste zusaetzlich im Arbeitsspeicher (sonst alle 10 s dieselbe Meldung)
- Aendern der Faelligkeit atomar gegen gleichzeitiges Faelligwerden, setzt Mail-Spur zurueck
- UpdateReminderDto lehnt null ab (400 statt 500)
- keine Mails an deaktivierte Benutzer
- Spaeter erinnern beschriftet heute/morgen nach dem berechneten Zeitpunkt
- Bearbeiten schickt dueAt nur bei geaenderter Zeit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 03:20:16 +02:00
parent be1e0035e0
commit c2e4467dd8
14 changed files with 442 additions and 78 deletions
+23 -9
View File
@@ -30,12 +30,7 @@ export const CATCH_UP_WINDOW_MS = 24 * 60 * 60 * 1000;
/** Eintraege der lokalen Merkliste, die aelter sind, werden entfernt. */
const NOTIFIED_TTL_MS = 7 * 24 * 60 * 60 * 1000;
export type BrowserPermissionState =
| 'desktop'
| 'granted'
| 'default'
| 'denied'
| 'unsupported';
export type BrowserPermissionState = 'desktop' | 'granted' | 'default' | 'denied' | 'unsupported';
type TauriInvoke = (command: string, args?: Record<string, unknown>) => Promise<unknown>;
@@ -135,6 +130,19 @@ function readNotified(): Record<string, number> {
}
}
/**
* Merkliste im Arbeitsspeicher dieses Tabs. Sie greift IMMER, auch wenn der
* localStorage gesperrt, voll oder im privaten Modus nicht nutzbar ist — sonst
* meldete der Melder dieselbe faellige Erinnerung 24 Stunden lang bei jeder
* Pruefung erneut. Der localStorage bleibt fuer die Abstimmung zwischen Tabs.
*/
const notifiedInMemory = new Map<string, number>();
/** Nur fuer Tests: leert die Merkliste im Arbeitsspeicher. */
export function resetNotifiedMemoryForTests(): void {
notifiedInMemory.clear();
}
/**
* Merkt sich `key` (`${id}|${dueAt}`) lokal. `true` nur beim ERSTEN Mal — so
* meldet jeder Client jede Faelligkeit hoechstens einmal, auch ueber mehrere
@@ -143,16 +151,22 @@ function readNotified(): Record<string, number> {
* aelter als 7 Tage werden dabei entfernt.
*/
export function claimNotification(key: string, nowMs: number): boolean {
for (const [k, at] of notifiedInMemory) {
if (nowMs - at > NOTIFIED_TTL_MS) notifiedInMemory.delete(k);
}
const record = readNotified();
for (const [k, at] of Object.entries(record)) {
if (nowMs - at > NOTIFIED_TTL_MS) delete record[k];
}
const first = !(key in record);
if (first) record[key] = nowMs;
const first = !(key in record) && !notifiedInMemory.has(key);
if (first) {
record[key] = nowMs;
notifiedInMemory.set(key, nowMs);
}
try {
window.localStorage.setItem(NOTIFIED_KEY, JSON.stringify(record));
} catch {
/* Speicher voll oder gesperrt: dann eben ohne Merkliste */
/* Speicher voll oder gesperrt: dann greift nur die Merkliste im Arbeitsspeicher */
}
return first;
}