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
+138
View File
@@ -935,6 +935,11 @@
a.href = 'aufgaben.html';
rechts.append(a);
} else {
/* DER WECKER (07.09.2026). Er steht bei JEDEM Termin, den man
sieht -- auch bei fremden: Ein Wecker ist eine Verabredung mit
sich selbst, keine Aenderung am Termin. Deshalb steht er auch
VOR "erledigt": Er ist das, was man am haeufigsten will. */
rechts.append(weckerKnopf(e));
rechts.append(tuKnopf(e.erledigt ? 'wieder offen' : 'erledigt', async () => {
const a = await hole('/workspace/api/termine/' + e.id, {
method: 'PATCH',
@@ -1191,6 +1196,12 @@
}
$('tagdialog-zu').addEventListener('click', () => $('tagdialog').close());
$('wecker-zu')?.addEventListener('click', () => $('wecker-dialog').close());
/* Klick auf den Schleier schliesst -- wie beim Tagesfenster. Zwei
Dialoge, die sich verschieden bedienen lassen, sind einer zu viel. */
$('wecker-dialog')?.addEventListener('click', (ev) => {
if (ev.target === $('wecker-dialog')) $('wecker-dialog').close();
});
/* Klick auf den Rand schließt -- erwartet man bei einem Dialog. */
$('tagdialog').addEventListener('mousedown', (ev) => {
if (ev.target === $('tagdialog')) $('tagdialog').close();
@@ -1224,6 +1235,133 @@
* eine Absage bringt; die Sperre steht im Server. */
const darfAendern = (e) => LEITUNG.has(ich.rolle) || e.erstellt_von === ich.id;
/* =====================================================================
DER WECKER -- BEDIENUNG (07.09.2026)
Wunsch Filipe: "wie so ein wecker, den man in den eintraegen
aktivieren oder ausschalten kann ... auch mehrmals informiert,
einmal eine woche vorher, einmal drei tage vorher und einmal am tag
selber. das soll jeder fuer sich selbst einstellen koennen."
EIN KNOPF, DER SEINEN ZUSTAND ZEIGT. Keine Glocke, die man erst
anklicken muss, um zu erfahren, ob sie an ist: Steht eine Zahl
daran, sind so viele Wecker gesetzt. Das beantwortet die haeufigste
Frage ("bekomme ich hier eine Erinnerung?") ohne einen Klick.
Die Auswahl selbst ist eine Liste zum Ankreuzen, kein Menue: Es
sind sechs Moeglichkeiten, mehrere davon gleichzeitig, und beides
kann ein Auswahlmenue nicht.
Gespeichert wird SOFORT beim Ankreuzen. Ein "Speichern"-Knopf
daneben waere ein zweiter Schritt fuer eine Entscheidung, die aus
einem Klick besteht -- und der haeufigste Weg, dass eine
Einstellung doch nicht ankommt. */
const WECKER_NAME = {
10080: 'Eine Woche vorher',
4320: 'Drei Tage vorher',
1440: 'Einen Tag vorher',
720: 'Am selben Tag (12 Std.)',
60: 'Eine Stunde vorher',
10: 'Zehn Minuten vorher',
};
function weckerKnopf(e) {
const knopf = el('button', 'wecker-knopf');
knopf.type = 'button';
knopf.title = 'Erinnerungen für diesen Termin';
const zahl = el('span', 'wecker-knopf__zahl');
knopf.append(weckerZeichen(), zahl);
const stand = (anzahl) => {
knopf.dataset.an = anzahl > 0 ? 'ja' : 'nein';
zahl.textContent = anzahl > 0 ? String(anzahl) : '';
knopf.setAttribute('aria-label',
anzahl > 0 ? `${anzahl} Erinnerung(en) gesetzt` : 'Keine Erinnerung gesetzt');
};
stand(0);
/* Der Stand wird beim Aufbau geholt, nicht erst beim Klick: Sonst
saehe man erst nach dem Oeffnen, ob ueberhaupt etwas gesetzt ist
-- und das ist die Frage, die man von aussen beantwortet haben
will. */
(async () => {
try {
const a = await hole(`/workspace/api/termine/${e.id}/wecker`);
if (a.ok) stand(((await a.json()).meine || []).length);
} catch { /* ohne Stand bleibt der Knopf leise */ }
})();
knopf.addEventListener('click', () => weckerOeffnen(e, stand));
return knopf;
}
function weckerZeichen() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('class', 'wecker-knopf__svg');
svg.setAttribute('aria-hidden', 'true');
svg.innerHTML = '<path class="fuell" d="M12 4.2c3.6 0 6.2 2.7 6.2 6.3v3.3l1.4 2.5H4.4l1.4-2.5v-3.3'
+ 'c0-3.6 2.6-6.3 6.2-6.3z"/>'
+ '<path d="M12 4.2c3.6 0 6.2 2.7 6.2 6.3v3.3l1.4 2.5H4.4l1.4-2.5v-3.3c0-3.6 2.6-6.3 6.2-6.3z"/>'
+ '<path d="M10.2 19.2a1.9 1.9 0 0 0 3.6 0"/>'
+ '<path d="M12 2.2v2M4.8 5.2 3.4 3.9M19.2 5.2l1.4-1.3"/>';
return svg;
}
async function weckerOeffnen(e, stand) {
const dlg = $('wecker-dialog');
if (!dlg) return;
$('wecker-titel').textContent = e.titel;
$('wecker-wann').textContent = `${tagLang(e.tag)}${e.uhr ? ' · ' + e.uhr + ' Uhr' : ''}`;
const liste = $('wecker-liste');
liste.textContent = 'Wird geladen …';
dlg.showModal();
let daten;
try {
const a = await hole(`/workspace/api/termine/${e.id}/wecker`);
if (!a.ok) throw new Error('nicht ok');
daten = await a.json();
} catch {
liste.textContent = 'Die Erinnerungen konnten nicht geladen werden.';
return;
}
const gewaehlt = new Set(daten.meine || []);
liste.textContent = '';
for (const m of daten.stufen || []) {
const zeile = el('label', 'wecker-zeile');
const kasten = document.createElement('input');
kasten.type = 'checkbox';
kasten.checked = gewaehlt.has(m);
kasten.addEventListener('change', async () => {
if (kasten.checked) gewaehlt.add(m); else gewaehlt.delete(m);
/* Die Obergrenze steht im Server; hier wird sie nur gezeigt,
damit niemand ankreuzt, was gleich wieder verschwindet. */
if (gewaehlt.size > (daten.hoechstens || 5)) {
gewaehlt.delete(m); kasten.checked = false;
$('wecker-fehler').textContent =
`Mehr als ${daten.hoechstens} Erinnerungen je Termin gehen nicht.`;
return;
}
$('wecker-fehler').textContent = '';
try {
const a = await hole(`/workspace/api/termine/${e.id}/wecker`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ minuten: [...gewaehlt] }),
});
if (!a.ok) throw new Error('nicht ok');
stand(gewaehlt.size);
} catch {
$('wecker-fehler').textContent = 'Speichern ging nicht.';
}
});
zeile.append(kasten, el('span', 'wecker-zeile__text', WECKER_NAME[m] || `${m} Minuten vorher`));
liste.append(zeile);
}
}
function formularOeffnen(tag, termin) {
/* Vorbelegung: der angeklickte Tag um 18:00 -- ein realistischer
Zeitpunkt für einen Call, spart in der Regel zwei Klicks. Ohne Tag