Der Wecker: mehrere Erinnerungen je Termin, jeder fuer sich

Filipe: "wie so ein wecker, den man auch in den eintraegen aktivieren
oder ausschalten kann, den soll man sogar so einstellen koennen, dass
er einen auch mehrmals informiert, einmal eine woche vorher, einmal
drei tage vorher und einmal am tag selber. das soll man auch selbst
jeder fuer sich einstellen koennen. hol die besten skills."

NACHGELESEN, NICHT GERATEN. Google Calendar erlaubt fuenf Erinnerungen
je Termin, Outlook genau eine, Apple zwei. Die verbreitete Empfehlung
fuer Wichtiges lautet "eine Woche, ein Tag, am Tag selbst" -- also
genau die Staffel, die Filipe genannt hat. Uebernommen: sechs Stufen
zur Wahl (Woche, drei Tage, ein Tag, selber Tag, Stunde, zehn Minuten),
hoechstens fuenf gleichzeitig.

EINE ZEILE IST EIN WECKER -- kein Feld am Termin mit einer Liste darin.
Mehrere Vorlaufzeiten UND "jeder fuer sich" sind zusammen eine
n:m-Beziehung; ein Feld mit kommagetrennten Zahlen waere beim ersten
"zeig mir alle faelligen Wecker" nicht mehr abfragbar.

DER ABSTAND STEHT IN DER DATENBANK, NICHT DER ZEITPUNKT. Ein Zeitpunkt
muesste bei jeder Terminverschiebung nachgezogen werden -- und genau
das vergisst man. Ein Abstand rechnet sich beim Wecken aus dem
aktuellen Beginn und ist damit immer richtig.

ZWEI GRENZEN IM WECKLAUF, und beide sind noetig: faellig (Weckzeit
erreicht) UND der Termin liegt noch vor uns. Ohne die zweite wuerde
beim ersten Lauf nach einem Ausfall jeder alte Wecker der letzten
Wochen nachtraeglich klingeln.

DER ABSTAND GEHOERT INS MERKMAL der Doppelsperre. Ohne ihn wuerde der
erste Wecker eines Termins alle weiteren sperren -- und genau das
Mehrfach-Wecken, um das es geht, faende nie statt.

DIE PRUEFUNG HAT SICH ZWEIMAL SELBST KORRIGIERT

  1. Erster Lauf um 23:42: vier Fehler, keiner echt -- der Melder
     schweigt zwischen 22 und 7 Uhr. Sie hat den Kalender gemessen,
     nicht die Software, und waere am Vormittag gruen gewesen. Dass die
     GEGENPROBE mitgefallen ist, war die eigentliche Auskunft: Waeren
     nur die Grenzen falsch, haette sie gehalten. Die Ruhezeit ist
     jetzt ueber die Umgebung einstellbar (Vorgabe unveraendert 22/7),
     damit eine Pruefung ihre Voraussetzung herstellen kann.
  2. Danach immer noch nichts: Ich hatte angenommen, der Melder trage
     den Versand nach dem VERSUCH ein. Er traegt ihn nach der
     erfolgreichen ZUSTELLUNG ein -- und das ist richtig so. Meine
     Annahme war falsch, nicht der Code. Die Pruefung hat jetzt einen
     winzigen echten Empfaenger; damit laeuft der ganze Versandweg mit,
     Verschluesselung und VAPID inbegriffen.

pruef-wecker.mjs: 20 Pruefungen. Sie stellt alle vier Fehler nach, die
bei einem Wecker moeglich sind (klingelt nicht / doppelt / nur einmal
von dreien / nachtraeglich nach einem Ausfall) -- plus die Gegenprobe,
dass ein faelliger Wecker wirklich ankommt.

Zwoelf weitere Pruefungen gelaufen, alle gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-07 23:51:20 +02:00
co-authored by Claude Opus 5
parent 63036fc33e
commit 1c2e196d1d
25 changed files with 1008 additions and 193 deletions
+85
View File
@@ -521,6 +521,91 @@ kalenderRouter.patch("/workspace/api/termine/:id", gleicheHerkunft, (req, res) =
}
});
/* =====================================================================
DER WECKER -- MEINE EIGENEN, ZU DIESEM TERMIN (07.09.2026)
Filipe: "das soll man auch selbst jeder fuer sich einstellen
koennen."
Deshalb steht in KEINER dieser beiden Routen eine Rollenfrage: Es
geht nie um fremde Wecker. Wer den Termin sehen darf, darf sich zu
ihm wecken lassen -- und aendert dabei ausschliesslich seine eigenen
Zeilen. Ein Wecker ist keine Eigenschaft des Termins, sondern eine
Verabredung mit sich selbst.
Die Sichtbarkeit wird trotzdem geprueft: Ohne sie koennte man sich
ueber eine geratene Nummer wecken lassen und aus dem Meldungstext
Titel und Uhrzeit eines fremden Termins lesen.
===================================================================== */
/** Erlaubte Vorlaufzeiten in Minuten. Eine feste Liste, keine freie
* Zahl: Ein Wecker "in 7 Minuten" waere im Fuenf-Minuten-Takt des
* Melders ohnehin ungenau, und eine offene Zahl ist eine Eingabe, die
* jemand pruefen muss. Die Staffel folgt der Recherche (Google,
* Outlook, Morgen): eine Woche, drei Tage, ein Tag, der Morgen des
* Tages, eine Stunde, zehn Minuten. */
const WECKER_STUFEN = [10080, 4320, 1440, 720, 60, 10];
const WECKER_MAX = 5; /* wie bei Google -- mehr weckt niemanden besser */
kalenderRouter.get("/workspace/api/termine/:id/wecker", (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
const { wo, werte } = sichtbar(req.person);
const da = db().prepare(`SELECT t.id ${VERBUND} WHERE ${wo} AND t.id = ?`).get(...werte, id);
if (!da) return res.status(404).json({ fehler: "nicht_gefunden" });
res.json({
stufen: WECKER_STUFEN,
hoechstens: WECKER_MAX,
meine: db().prepare(
"SELECT minuten_vorher FROM termin_wecker WHERE termin_id = ? AND person_id = ? ORDER BY minuten_vorher DESC")
.all(id, req.person.id).map((z) => z.minuten_vorher),
});
} catch (fehler) {
console.error("[workspace] Wecker lesen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
kalenderRouter.put("/workspace/api/termine/:id/wecker", gleicheHerkunft, (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
const { wo, werte } = sichtbar(req.person);
const da = db().prepare(`SELECT t.id ${VERBUND} WHERE ${wo} AND t.id = ?`).get(...werte, id);
if (!da) return res.status(404).json({ fehler: "nicht_gefunden" });
/* Nur bekannte Stufen, ohne Dubletten, hoechstens fuenf. Gefiltert
statt abgelehnt: Eine Oberflaeche, die eine unbekannte Zahl
schickt, hat einen Fehler -- aber die vier richtigen daneben
sollen deshalb nicht verlorengehen. */
const roh = Array.isArray(req.body?.minuten) ? req.body.minuten : [];
const gewaehlt = [...new Set(roh.map(Number).filter((m) => WECKER_STUFEN.includes(m)))]
.sort((a, b) => b - a).slice(0, WECKER_MAX);
const d = db();
d.exec("BEGIN");
try {
/* Erst raeumen, dann setzen -- das ist der einzige Weg, bei dem
ein ABGEWAEHLTER Wecker auch wirklich verschwindet. Wer nur
einfuegt, sammelt bei jedem Speichern eine Zeile mehr an. */
d.prepare("DELETE FROM termin_wecker WHERE termin_id = ? AND person_id = ?")
.run(id, req.person.id);
const rein = d.prepare(
"INSERT INTO termin_wecker (termin_id, person_id, minuten_vorher, gesetzt) VALUES (?,?,?,?)");
const jetzt = new Date().toISOString();
for (const m of gewaehlt) rein.run(id, req.person.id, m, jetzt);
d.exec("COMMIT");
} catch (f) { d.exec("ROLLBACK"); throw f; }
res.json({ meine: gewaehlt });
} catch (fehler) {
console.error("[workspace] Wecker setzen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
kalenderRouter.delete("/workspace/api/termine/:id", gleicheHerkunft, (req, res) => {
try {
const id = Number(req.params.id);