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:
+100
-2
@@ -72,6 +72,12 @@ export const ARTEN = [
|
||||
{ schluessel: "aufgabe_faellig", name: "Aufgabe wird morgen fällig", vorgabe: true },
|
||||
{ schluessel: "aufgabe_ueberfaellig", name: "Aufgabe ist überfällig", vorgabe: true },
|
||||
{ schluessel: "termin_gleich", name: "Termin in einer Stunde", vorgabe: true },
|
||||
/* DER WECKER (07.09.2026). Bewusst eine EIGENE Art neben
|
||||
"termin_gleich": Die eine ist eine Vorgabe des Hauses fuer jeden
|
||||
Termin, der andere ist eine Entscheidung DIESER Person fuer DIESEN
|
||||
Termin. Wer die feste Stunden-Erinnerung abschaltet, will damit
|
||||
nicht seine selbst gesetzten Wecker verlieren. */
|
||||
{ schluessel: "termin_wecker", name: "Selbst gesetzter Wecker", vorgabe: true },
|
||||
{ schluessel: "protokoll_fehlt", name: "Gespräch ohne Protokoll", vorgabe: true },
|
||||
{ schluessel: "followup", name: "Follow-up in der Pipeline fällig", vorgabe: true },
|
||||
/* Chat (06.09.2026). Steht bewusst VOR der Probe: Es ist die Art,
|
||||
@@ -82,8 +88,44 @@ export const ARTEN = [
|
||||
];
|
||||
const ARTEN_SCHLUESSEL = new Set(ARTEN.map((a) => a.schluessel));
|
||||
|
||||
/* Ruhezeit. Nichts zwischen 22 und 7 Uhr. */
|
||||
const RUHE_AB = 22, RUHE_BIS = 7;
|
||||
/** Wie ein Vorlauf heisst, wenn er im Meldungskopf steht.
|
||||
*
|
||||
* In Minuten gerechnet, in Worten gemeldet: "In einer Woche" liest
|
||||
* sich beim Aufwachen besser als "10080 Minuten vorher". Die Staffel
|
||||
* ist dieselbe, die die Oberflaeche anbietet -- kaeme hier eine
|
||||
* ungerade Zahl an, faellt sie auf den allgemeinen Satz zurueck statt
|
||||
* auf einen falschen. */
|
||||
export function vorlauf(minuten) {
|
||||
const m = Number(minuten) || 0;
|
||||
if (m >= 10080) return "Erinnerung: in einer Woche";
|
||||
if (m >= 4320) return "Erinnerung: in drei Tagen";
|
||||
if (m >= 1440) return "Erinnerung: morgen";
|
||||
if (m >= 720) return "Erinnerung: heute";
|
||||
if (m >= 120) return `Erinnerung: in ${Math.round(m / 60)} Stunden`;
|
||||
if (m >= 60) return "Erinnerung: in einer Stunde";
|
||||
if (m > 0) return `Erinnerung: in ${m} Minuten`;
|
||||
return "Erinnerung: jetzt";
|
||||
}
|
||||
|
||||
/* RUHEZEIT. Nichts zwischen 22 und 7 Uhr.
|
||||
|
||||
DIE GRENZEN SIND SEIT DEM 07.09.2026 EINSTELLBAR, und der Grund ist
|
||||
eine Pruefung, die sich selbst ins Knie geschossen hat: Die
|
||||
Weckerpruefung lief um 23:42 und meldete vier Fehler -- der Melder
|
||||
schwieg, weil Ruhezeit war. Sie hat also den Kalender gemessen, nicht
|
||||
die Software, und waere um 10 Uhr gruen gewesen. Genau die Zeitbombe,
|
||||
die in der Projektnotiz steht.
|
||||
|
||||
Eine Pruefung darf ihre Voraussetzung herstellen duerfen. Deshalb
|
||||
liest die Ruhezeit jetzt aus der Umgebung -- Vorgabe unveraendert
|
||||
22 bis 7, und im Betrieb setzt niemand etwas anderes. Die Alternative
|
||||
waere gewesen, in der Pruefung eine Zeit zu faelschen; das faelscht
|
||||
dann irgendwann auch etwas anderes mit.
|
||||
|
||||
Nebennutzen: Wenn das Team eines Tages andere Zeiten will, ist es
|
||||
eine Umgebungsvariable und kein Programmierauftrag. */
|
||||
const RUHE_AB = Number(process.env.PUSH_RUHE_AB ?? 22);
|
||||
const RUHE_BIS = Number(process.env.PUSH_RUHE_BIS ?? 7);
|
||||
export function istRuhezeit(d = new Date()) {
|
||||
const s = d.getHours();
|
||||
return s >= RUHE_AB || s < RUHE_BIS;
|
||||
@@ -252,6 +294,62 @@ export async function laufen() {
|
||||
}
|
||||
}
|
||||
|
||||
/* --- DIE SELBST GESETZTEN WECKER ----------------------------------
|
||||
|
||||
Ein Wecker steht als ABSTAND in der Datenbank (minuten_vorher),
|
||||
nicht als Zeitpunkt. Die Weckzeit wird deshalb hier gerechnet --
|
||||
und ist damit auch nach einer Terminverschiebung richtig, ohne
|
||||
dass irgendjemand etwas nachziehen muss.
|
||||
|
||||
ZWEI GRENZEN, und beide sind noetig:
|
||||
|
||||
faellig die Weckzeit ist erreicht (beginn - abstand <= jetzt)
|
||||
nicht zu spaet der Termin liegt noch VOR uns
|
||||
|
||||
Ohne die zweite wuerde beim ersten Lauf nach einer Auszeit des
|
||||
Dienstes jeder alte Wecker der letzten Wochen nachtraeglich
|
||||
klingeln -- ein Schwall Meldungen zu Terminen, die laengst vorbei
|
||||
sind. Das ist der Fall, den man beim Bauen nie sieht und der beim
|
||||
ersten Neustart nach einem Ausfall sofort auffaellt.
|
||||
|
||||
DIE RUHEZEIT VERSCHLUCKT NICHTS. `benachrichtige` merkt sich erst
|
||||
nach dem Verschicken, dass etwas raus ist -- was zwischen 22 und
|
||||
7 Uhr faellig wird, geht also am Morgen raus, sobald der Termin
|
||||
noch bevorsteht. Ein Wecker, der nachts still ist und dafuer
|
||||
morgens klingelt, ist genau richtig; einer, der verschluckt wird,
|
||||
waere schlimmer als keiner.
|
||||
|
||||
Rechnen auf Textebene geht hier NICHT (Abstand in Minuten), also
|
||||
mit Date -- aber ausschliesslich lokal, nie ueber toISOString. */
|
||||
const alsText = (dt) => {
|
||||
const p2 = (n) => String(n).padStart(2, "0");
|
||||
return `${dt.getFullYear()}-${p2(dt.getMonth() + 1)}-${p2(dt.getDate())}`
|
||||
+ `T${p2(dt.getHours())}:${p2(dt.getMinutes())}`;
|
||||
};
|
||||
for (const w of d.prepare(`
|
||||
SELECT w.termin_id, w.person_id, w.minuten_vorher,
|
||||
t.titel, t.beginn, t.art
|
||||
FROM termin_wecker w
|
||||
JOIN termine t ON t.id = w.termin_id
|
||||
WHERE t.erledigt = 0 AND t.beginn > ?`).all(jetztText)) {
|
||||
const beginn = new Date(String(w.beginn).replace(" ", "T"));
|
||||
if (Number.isNaN(beginn.getTime())) continue;
|
||||
const weckzeit = new Date(beginn.getTime() - w.minuten_vorher * 60_000);
|
||||
if (alsText(weckzeit) > jetztText) continue; // noch nicht so weit
|
||||
|
||||
await senden(w.person_id, "termin_wecker", {
|
||||
titel: vorlauf(w.minuten_vorher),
|
||||
text: `${w.titel} · ${String(w.beginn).slice(8, 10)}.${String(w.beginn).slice(5, 7)}. `
|
||||
+ `um ${String(w.beginn).slice(11, 16)} Uhr`,
|
||||
ziel: w.art === "call" ? "/workspace/calls.html" : "/workspace/kalender.html",
|
||||
/* Der Abstand gehoert ins Merkmal. Ohne ihn wuerde der erste
|
||||
Wecker eines Termins alle weiteren als "schon geschickt"
|
||||
sperren -- und genau das Mehrfach-Wecken, um das es geht,
|
||||
faende nie statt. */
|
||||
merkmal: `wecker:${w.termin_id}:${w.minuten_vorher}`,
|
||||
});
|
||||
}
|
||||
|
||||
/* --- Gespraeche ohne Protokoll ------------------------------------ */
|
||||
for (const t of d.prepare(`
|
||||
SELECT t.id, t.titel, t.erstellt_von FROM termine t
|
||||
|
||||
Reference in New Issue
Block a user