Files
dogfather-universe/workspace/assets/js/glocke.js
T
DogFatherGitandClaude Opus 5 f701dae53e screen10: Der Tagesruf -- einmal am Tag, was noch offen ist
Wunsch Filipe: "ich will das neben diesem kreis auch ein kleiner button
ist fuer den wecker von den aufgaben, oder quasi eine taetige meldung
einmal am tag zu aktivieren wenn noch aufgaben auf sind."

Ein Wecker-Knopf unter der Glocke, neben der Uhr. Eingeschaltet meldet
er sich einmal taeglich zur eingestellten Zeit -- aber nur, wenn
wirklich noch etwas offen ist. Der Satz nennt die Zahl und die
Ueberfaelligen: "3 Aufgaben sind noch offen / Davon eine ueberfaellig."

ALS EINZIGE ART MIT `vorgabe: false`, und das ist keine
Nachlaessigkeit. Alle anderen Benachrichtigungen antworten auf ein
Ereignis, das gerade passiert ist. Der Tagesruf kommt ungefragt zur
selben Zeit, ob es etwas Neues gibt oder nicht -- so etwas schaltet man
sich selbst ein, sonst ist es Werbung. Bei null offenen Aufgaben kommt
nichts: Eine taegliche Meldung "du hast nichts zu tun" ist der
schnellste Weg, dass man die naechste nicht mehr liest.

EIN FENSTER VON DREI STUNDEN. Der Takt laeuft alle fuenf Minuten; ein
einfaches "jetzt >= eingestellte Zeit" wuerde nach einem Serverausfall
den Ruf fuer neun Uhr um zwanzig Uhr zustellen. Wer eine Erinnerung an
einen vergangenen Tag bekommt, schaltet sie ab. Faellt der Tag aus, ist
das die ehrlichere Antwort.

VIER DINGE, DIE ERST DAS NACHMESSEN GEZEIGT HAT:

1. DER KNOPF VERSPRACH ETWAS, DAS ER NICHT HALTEN KONNTE. Chromium
   meldet `Notification.permission === 'denied'` -- gemessen, nicht
   vermutet. Der Knopf sah einladend aus ("Einmal am Tag melden…") und
   sagte erst NACH dem Antippen ab. Ein Bedienelement, das den Grund
   erst hinterher nennt, ist die schlechtere Haelfte einer
   Fehlermeldung. Jetzt steht er im Titel, und der Knopf ist gedimmt.

2. EINE UHRZEIT IN DER RUHEZEIT WAERE EIN STILLES NICHTS. Der Server
   laesst zwischen 22 und 7 Uhr nichts durch. Wer 23:00 einstellt,
   bekaeme nie etwas und saehe nur einen Knopf auf "an". Jetzt steht
   der Hinweis dort, wo man es einstellt -- mit den Grenzen VOM SERVER,
   nicht mit hier getippten Zahlen.

3. `wert` UND `an` SIND ZWEI ENTSCHEIDUNGEN. Wer nur den Schalter
   umlegt, schickt kein `wert` -- stumpf `req.body.wert` zu schreiben
   haette bei jedem Aus- und Einschalten die Uhrzeit geloescht, und
   beim naechsten Mal staende wieder neun Uhr da. Ein Datenverlust, den
   niemand meldet, weil er wie eine Vorgabe aussieht. Genau dieser Weg
   wird jetzt geprueft.

4. pruef-css-klassen HATTE ZWEIMAL RECHT. Der Stil lag in heim.css
   (nur Startseite), die Zeichen entstehen aber in glocke.js (18
   Seiten) -- auf 17 davon waere ein nackter Knopf gestanden. Und die
   beiden neuen Schriftgroessen (10 und 11 px) haetten die Grundlinie
   von 43 zu kleinen Stellen still auf 45 gehoben. Beides behoben:
   Stil nach start.css, Schrift auf 12 px.

NEUE PRUEFUNG server/pruef-tagesruf.mjs, drei Schichten getrennt, weil
sie getrennt kaputtgehen: Oberflaeche im Browser, Schalten ueber die
Schnittstelle (aus der SEITE heraus, damit Sitzung und
Herkunftspruefung mitgehen), Zeitentscheidung als reine Rechnung. Die
Entscheidung wurde dafuer aus dem Rundgang herausgeloest -- dazwischen
steckend haette man zum Pruefen Datenbank und Push-Versand aufbauen
muessen, also haette man sie nicht geprueft.

Die Erwartung der ersten Schicht richtet sich nach der GEMESSENEN
Berechtigung statt sie vorauszusetzen: Erlaubt eine kuenftige
Chromium-Fassung Benachrichtigungen von sich aus, waere ein fest
verdrahtetes "muss blockiert sein" ein Fehlalarm ohne Fehler.
Gegenproben sind dabei: "99:99", "7:30" ohne fuehrende Null und ein
Wert an einer Art, die keinen kennt, muessen abgelehnt werden -- sonst
bewiese der Bereichstest nichts.

Der reservierte Platz waechst von 46 auf 104 px, damit der zweite Knopf
die Kachelreihe darunter nicht nach unten schiebt; das Zeitfeld schwebt
statt zu schieben. Beides derselbe Grund wie bei der Glocke: Ein
Sprung ist kein Schoenheitsfehler, sondern der Grund, warum man auf den
falschen Knopf drueckt.

Geprueft: pruef-tagesruf (neu, alles in Ordnung), pruef-push,
pruef-css-klassen, pruef-start-ansicht -- alle in Ordnung.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 23:54:51 +02:00

439 lines
19 KiB
JavaScript

/* =====================================================================
Der Benachrichtigungs-Knopf — auf JEDER Seite, in der Kopfleiste.
Wunsch Filipe (05.09.2026), mit dem Bild eines "Benachrichtigungen
aus"-Knopfes: *"ich will auch sowas und dass es perfekt funktioniert
auf der seite fuer jeden."*
---------------------------------------------------------------------
WARUM IN DER KOPFLEISTE UND NICHT AUF EINER EINSTELLUNGSSEITE
Dasselbe Argument wie bei der Suche, die hier schon so gebaut ist:
Was man nur auf einer Extraseite findet, benutzt niemand. Der Knopf
sagt jederzeit, woran man ist -- Glocke an oder durchgestrichen.
---------------------------------------------------------------------
DER KNOPF SAGT IMMER DIE WAHRHEIT, auch wenn sie unbequem ist
Es gibt mehr Zustaende als "an" und "aus", und jeder braucht einen
anderen Satz. Ein Knopf, der bei einem abgelehnten Recht einfach
nichts tut, ist der sichere Weg zu "das funktioniert nicht":
geht nicht Der Browser kann kein Push (aeltere Fassungen).
iPhone Safari kann es NUR nach "Zum Home-Bildschirm".
Im Reiter gibt es die Berechtigung ueberhaupt nicht.
verboten Schon einmal abgelehnt. Das laesst sich per
JavaScript NICHT zuruecknehmen -- man muss es in den
Browsereinstellungen tun. Also wird genau das gesagt.
aus / an der Normalfall.
===================================================================== */
(() => {
'use strict';
/* Nur im Workspace, und nicht auf der Anmeldeseite -- dort ist
niemand angemeldet, den man benachrichtigen koennte. */
if (!location.pathname.startsWith('/workspace/')) return;
if (/\/workspace\/(index\.html)?$/.test(location.pathname)) return;
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
const kannPush = 'serviceWorker' in navigator && 'PushManager' in window
&& 'Notification' in window;
/* Laeuft die Seite als installierte App? Auf dem iPhone ist das die
Bedingung dafuer, dass es ueberhaupt geht. */
const alsApp = window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone === true;
const istApple = /iPad|iPhone|iPod/.test(navigator.userAgent)
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
let anmeldung = null;
let knopf = null;
const b64uZuBytes = (s) => {
const roh = atob(String(s).replace(/-/g, '+').replace(/_/g, '/')
+ '='.repeat((4 - String(s).length % 4) % 4));
return Uint8Array.from(roh, (c) => c.charCodeAt(0));
};
const bytesZuB64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
/** Ein Name, an dem man sein Geraet in der Liste wiedererkennt. */
function geraetName() {
const u = navigator.userAgent;
const os = /Windows/.test(u) ? 'Windows'
: /Android/.test(u) ? 'Android'
: istApple ? (/iPad/.test(u) ? 'iPad' : 'iPhone')
: /Mac/.test(u) ? 'Mac' : /Linux/.test(u) ? 'Linux' : 'Gerät';
const browser = /Edg\//.test(u) ? 'Edge'
: /Chrome\//.test(u) ? 'Chrome'
: /Firefox\//.test(u) ? 'Firefox'
: /Safari\//.test(u) ? 'Safari' : 'Browser';
return `${os} · ${browser}${alsApp ? ' (App)' : ''}`;
}
/** Was ist gerade der Fall? */
function lage() {
if (!kannPush) return 'geht-nicht';
if (istApple && !alsApp) return 'iphone';
if (Notification.permission === 'denied') return 'verboten';
return anmeldung ? 'an' : 'aus';
}
const TEXTE = {
'geht-nicht': { text: 'Hier nicht möglich', titel: 'Dieser Browser kann keine Benachrichtigungen.' },
iphone: {
text: 'Erst zum Home-Bildschirm',
titel: 'Auf dem iPhone gehen Benachrichtigungen nur, wenn die Seite '
+ 'zum Home-Bildschirm hinzugefügt wurde: Teilen-Knopf → „Zum Home-Bildschirm“. '
+ 'Danach hier noch einmal antippen.',
},
verboten: {
text: 'Vom Browser blockiert',
titel: 'Du hast Benachrichtigungen für diese Seite einmal abgelehnt. Das lässt sich '
+ 'nur in den Browsereinstellungen wieder erlauben — bei Chrome über das Symbol '
+ 'links in der Adresszeile.',
},
aus: { text: 'Benachrichtigungen aus', titel: 'Antippen, um Erinnerungen zu bekommen.' },
an: { text: 'Benachrichtigungen an', titel: 'Antippen, um sie auf diesem Gerät wieder abzuschalten.' },
};
function zeichnen() {
if (!knopf) return;
const l = lage();
const t = TEXTE[l];
knopf.dataset.lage = l;
knopf.title = t.titel;
knopf.setAttribute('aria-label', t.titel);
knopf.querySelector('.glocke__text').textContent = t.text;
/* aria-pressed sagt einem Vorleseprogramm, dass es ein Schalter
ist und wie er gerade steht. */
knopf.setAttribute('aria-pressed', l === 'an' ? 'true' : 'false');
knopf.disabled = l === 'geht-nicht';
}
async function anmelden() {
const recht = await Notification.requestPermission();
if (recht !== 'granted') { zeichnen(); return; }
const reg = await navigator.serviceWorker.register('/workspace/sw.js', { scope: '/workspace/' });
await navigator.serviceWorker.ready;
const a = await fetch('/workspace/api/push/schluessel', { credentials: 'same-origin' });
const { schluessel } = await a.json();
const abo = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: b64uZuBytes(schluessel),
});
const roh = abo.toJSON();
await fetch('/workspace/api/push/anmelden', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
endpunkt: abo.endpoint,
p256dh: roh.keys?.p256dh || bytesZuB64u(abo.getKey('p256dh')),
auth: roh.keys?.auth || bytesZuB64u(abo.getKey('auth')),
geraet: geraetName(),
}),
});
anmeldung = abo;
zeichnen();
/* Sofort eine Probe. Das ist der Unterschied zwischen "sollte
gehen" und "geht": Wer den Knopf drueckt, will WISSEN, dass es
ankommt -- und nicht bis morgen warten. */
await fetch('/workspace/api/push/probe', {
method: 'POST', credentials: 'same-origin',
}).catch(() => {});
}
async function abmelden() {
try {
if (anmeldung) {
await fetch('/workspace/api/push/abmelden', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ endpunkt: anmeldung.endpoint }),
});
await anmeldung.unsubscribe();
}
} catch { /* der Servereintrag ist weg, das genuegt */ }
anmeldung = null;
zeichnen();
}
async function bauen() {
/* WO DIE GLOCKE HINGEHÖRT (07.09.2026, Wunsch Filipe: "der
benachrichtigungs button soll auch von der leiste in diese kachel
gesetzt werden").
Gibt es auf der Seite einen ausgewiesenen Platz, kommt sie
dorthin -- auf der Startseite in die Begrüßungskachel, neben die
Uhr. Das ist der richtige Ort: Benachrichtigungen sind eine
Einstellung zum eigenen Tag, keine Navigation.
Auf allen anderen Seiten gibt es diesen Platz nicht, und dort
bleibt die Leiste. Deshalb ein Rückfall und keine Umstellung:
Eine Glocke, die auf zwölf Seiten verschwindet, weil sie auf
einer umgezogen ist, wäre keine Verbesserung. Und weil die
Kopfleiste am 06.09. schon einmal an einem sechsten Element
zerbrochen ist, wird sie hier nebenbei um eines leichter. */
const platz = document.getElementById('glocke-platz');
const kopf = platz || document.querySelector('.kopfleiste__rechts');
if (!kopf || document.getElementById('glocke')) return;
knopf = el('button', 'glocke');
knopf.type = 'button';
knopf.id = 'glocke';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'glocke__zeichen');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('aria-hidden', 'true');
svg.innerHTML = '<path class="glocke__koerper" d="M12 4a5 5 0 0 0-5 5v3.5L5.5 15h13L17 12.5V9a5 5 0 0 0-5-5z"/>'
/* "kloeppel" ohne Umlaut: Ein ö in einem Klassennamen ist erlaubt,
aber es überlebt nicht jede Datei, die zwischendurch in einer
anderen Kodierung gespeichert wird -- und im ganzen Haus wird
ohnehin "oe"/"ue" geschrieben. */
+ '<path class="glocke__kloeppel" d="M10 17.5a2 2 0 0 0 4 0"/>'
+ '<path class="glocke__strich" d="M4 4l16 16"/>';
knopf.append(svg);
knopf.append(el('span', 'glocke__text', '…'));
/* VOR dem Abmelden-Knopf, nicht dahinter: Abmelden ist die letzte
Handlung auf einer Seite und bleibt ganz rechts. Im eigenen Platz
gibt es nichts, wovor sie stehen müsste. */
const abmeldenKnopf = platz ? null : kopf.querySelector('.abmelden');
if (abmeldenKnopf) kopf.insertBefore(knopf, abmeldenKnopf);
else kopf.append(knopf);
if (platz) knopf.classList.add('glocke--kachel');
knopf.addEventListener('click', async () => {
const l = lage();
if (l === 'geht-nicht') return;
/* Bei 'iphone' und 'verboten' nichts tun ausser erklaeren -- der
Titel steht schon dran, und ein Klick, der nichts bewirkt und
nichts sagt, wirkt kaputt. */
if (l === 'iphone' || l === 'verboten') { window.alert(TEXTE[l].titel); return; }
knopf.disabled = true;
try {
if (l === 'an') await abmelden(); else await anmelden();
} catch (f) {
console.warn('[glocke]', f);
zeichnen();
}
knopf.disabled = false;
});
/* Steht hier schon eine Anmeldung? */
if (kannPush && Notification.permission === 'granted') {
try {
const reg = await navigator.serviceWorker.getRegistration('/workspace/');
anmeldung = reg ? await reg.pushManager.getSubscription() : null;
} catch { anmeldung = null; }
}
zeichnen();
if (platz) await tagesrufBauen(platz);
}
/* =====================================================================
DER TAGESRUF -- der Wecker fuer offene Aufgaben (screen10)
Filipe: "ich will das neben diesem kreis auch ein kleiner button ist
fuer den wecker von den aufgaben, oder quasi eine taetige meldung
einmal am tag zu aktivieren wenn noch aufgaben auf sind."
WARUM IN DIESER DATEI UND NICHT IN EINER EIGENEN. Er braucht genau
dasselbe Vorwissen wie die Glocke: Kann der Browser ueberhaupt
Push? Laeuft die Seite auf einem iPhone ausserhalb der App? Ist
die Berechtigung abgelehnt? Eine zweite Datei muesste das alles
noch einmal feststellen -- und beim naechsten Browser, der etwas
anders macht, wuerde eine der beiden Fassungen nachgezogen und die
andere vergessen. Dieselbe Frage gehoert an eine Stelle.
NUR IN DER KACHEL, nicht in der Kopfleiste. "neben diesem kreis"
war ausdruecklich der Ort -- und die Kopfleiste ist am 06.09.2026
schon einmal an einem sechsten Element zerbrochen. Wo es den Platz
nicht gibt, gibt es den Knopf nicht.
================================================================== */
let rufKnopf = null, rufFeld = null, rufAn = false, rufZeit = '09:00';
let rufRuhe = null; /* { ab, bis } -- die Ruhezeit des Hauses */
/* Faellt die gewaehlte Uhrzeit in die Ruhezeit?
Die Grenzen kommen vom Server (`/stand`), nicht aus einer hier
getippten Zahl: Sie stehen in einer Umgebungsvariablen und koennen
sich aendern. Eine zweite Zahl im Browser waere spaetestens dann
falsch, ohne dass es jemand merkt. */
function inRuhezeit(t) {
if (!rufRuhe || !/^\d{2}:\d{2}$/.test(t)) return false;
const h = Number(t.slice(0, 2));
/* Die Ruhezeit laeuft ueber Mitternacht (22 bis 7): "ab ODER bis",
nicht "ab UND bis". Mit UND waere sie leer. */
return rufRuhe.ab > rufRuhe.bis ? (h >= rufRuhe.ab || h < rufRuhe.bis)
: (h >= rufRuhe.ab && h < rufRuhe.bis);
}
function rufZeichnen() {
if (!rufKnopf) return;
const l = lage();
/* NUR 'an' UND 'aus' SIND BENUTZBARE LAGEN.
Im ersten Anlauf stand hier `l !== 'geht-nicht' && l !== 'iphone'`
-- damit galt 'verboten' als brauchbar, und der Knopf trug den
Satz "Einmal am Tag melden, wenn noch Aufgaben offen sind."
Beim Antippen kam dann die Absage. Gemessen im Browser: Chromium
meldet `Notification.permission === 'denied'`, der Knopf sah
einladend aus und tat nichts.
Ein Bedienelement, das erst NACH dem Klick sagt, dass es nicht
geht, ist die schlechtere Haelfte einer Fehlermeldung. Der Grund
gehoert davor -- in den Titel, den man beim Zeigen liest. */
const brauchbar = l === 'an' || l === 'aus';
rufKnopf.disabled = l === 'geht-nicht';
rufKnopf.dataset.an = rufAn && brauchbar ? 'ja' : 'nein';
rufKnopf.dataset.lage = l;
rufKnopf.setAttribute('aria-pressed', rufAn && brauchbar ? 'true' : 'false');
const satz = l === 'geht-nicht'
? 'Der tägliche Ruf braucht Benachrichtigungen — dieser Browser kann keine.'
: l === 'iphone'
? 'Der tägliche Ruf braucht Benachrichtigungen. Auf dem iPhone gehen die nur, '
+ 'wenn die Seite zum Home-Bildschirm hinzugefügt wurde: Teilen → „Zum Home-Bildschirm“.'
: l === 'verboten'
? 'Der tägliche Ruf braucht Benachrichtigungen — die sind für diese Seite im '
+ 'Browser blockiert. Das lässt sich nur in den Browsereinstellungen wieder erlauben.'
: rufAn
? `Täglich um ${rufZeit}, wenn noch Aufgaben offen sind. Antippen zum Abschalten.`
: 'Einmal am Tag melden, wenn noch Aufgaben offen sind.';
/* EINE UHRZEIT IN DER RUHEZEIT IST EIN STILLES NICHTS.
Der Server laesst zwischen 22 und 7 Uhr nichts durch -- das ist
richtig so. Aber wer 23:00 einstellt, bekaeme nie etwas und
saehe nur einen Knopf, der auf "an" steht. Genau die Sorte
Fehler, die niemand meldet, weil nichts kaputt aussieht.
Also wird es gesagt, an der Stelle, an der man es einstellt. */
const warnt = brauchbar && rufAn && inRuhezeit(rufZeit);
if (rufFeld) {
let hinweis = rufFeld.querySelector('.tagesruf__warnung');
if (warnt && !hinweis) {
hinweis = el('span', 'tagesruf__warnung');
rufFeld.append(hinweis);
}
if (hinweis) {
hinweis.hidden = !warnt;
if (warnt) {
hinweis.textContent = `Zwischen ${String(rufRuhe.ab).padStart(2, '0')}:00 und `
+ `${String(rufRuhe.bis).padStart(2, '0')}:00 ist Ruhezeit — dann kommt nichts an.`;
}
}
}
rufKnopf.title = satz;
rufKnopf.setAttribute('aria-label', satz);
rufKnopf.querySelector('.tagesruf__text').textContent = rufAn && brauchbar ? rufZeit : 'Tagesruf';
/* Das Feld steht im normalen Fluss unter dem Knopf, nicht als
schwebendes Fenster: Ein Aufklappmenue braucht eine Position, ein
Schliessen, einen Klick nach draussen und eine Tastaturfalle --
vier Gelegenheiten fuer Fehler, damit eine Uhrzeit eingestellt
werden kann. */
if (rufFeld) rufFeld.hidden = !(rufAn && brauchbar);
}
async function rufSetzen(an, wert) {
const koerper = { art: 'tagesruf', an };
/* `wert` NUR mitschicken, wenn er sich aendert -- der Server laesst
ihn sonst stehen. Beide Seiten muessen sich darin einig sein,
sonst loescht ein Ein-/Ausschalten die eingestellte Uhrzeit. */
if (wert !== undefined) koerper.wert = wert;
const a = await fetch('/workspace/api/push/art', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(koerper),
});
if (!a.ok) throw new Error('Konnte nicht gespeichert werden.');
const e = await a.json();
rufAn = !!e.an;
if (e.wert) rufZeit = e.wert;
rufZeichnen();
}
async function tagesrufBauen(platz) {
if (document.getElementById('tagesruf')) return;
rufKnopf = el('button', 'tagesruf');
rufKnopf.type = 'button';
rufKnopf.id = 'tagesruf';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'tagesruf__zeichen');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('aria-hidden', 'true');
/* Ein WECKER, keine zweite Glocke: Zwei gleiche Zeichen
nebeneinander sind zwei Knoepfe, von denen man einen falsch
drueckt. Runder Koerper, zwei Fuesse, zwei Ohren, Zeiger auf
kurz vor zwoelf. */
svg.innerHTML = '<circle class="tagesruf__koerper" cx="12" cy="13" r="7"/>'
+ '<path class="tagesruf__ohr" d="M5.5 5.2 3.2 7.5M18.5 5.2l2.3 2.3"/>'
+ '<path class="tagesruf__fuss" d="m7.5 19.4-1.4 1.9M16.5 19.4l1.4 1.9"/>'
+ '<path class="tagesruf__zeiger" d="M12 9.4V13l2.4 1.6"/>';
rufKnopf.append(svg);
rufKnopf.append(el('span', 'tagesruf__text', 'Tagesruf'));
rufFeld = el('label', 'tagesruf__feld');
rufFeld.hidden = true;
rufFeld.append(el('span', 'tagesruf__feld-schild', 'Uhrzeit'));
const eingabe = document.createElement('input');
eingabe.type = 'time';
eingabe.className = 'tagesruf__zeit';
eingabe.step = '300';
rufFeld.append(eingabe);
platz.append(rufKnopf, rufFeld);
rufKnopf.addEventListener('click', async () => {
const l = lage();
if (l === 'geht-nicht') return;
if (l === 'iphone' || l === 'verboten') { window.alert(TEXTE[l].titel); return; }
rufKnopf.disabled = true;
try { await rufSetzen(!rufAn); } catch (f) { console.warn('[tagesruf]', f); }
rufKnopf.disabled = false;
});
/* `change`, nicht `input`: Ein Zeitfeld meldet bei jedem Tastendruck
-- "0", "09", "09:0", "09:00" waeren vier Anfragen fuer eine
Einstellung, und die drei ersten sind ungueltig. */
eingabe.addEventListener('change', async () => {
if (!eingabe.value) return;
try { await rufSetzen(true, eingabe.value); } catch (f) { console.warn('[tagesruf]', f); }
});
/* Den Stand holen. Schlaegt das fehl, bleibt der Knopf auf "aus"
stehen -- er behauptet dann nichts Falsches, und ein Klick
richtet ihn wieder. */
try {
const a = await fetch('/workspace/api/push/stand');
if (a.ok) {
const stand = await a.json();
const art = (stand.arten || []).find((x) => x.schluessel === 'tagesruf');
if (art) { rufAn = !!art.an; if (art.wert) rufZeit = art.wert; }
if (stand.ruhe) rufRuhe = { ab: Number(stand.ruhe.ab), bis: Number(stand.ruhe.bis) };
}
} catch { /* offline -- der Knopf steht auf aus */ }
eingabe.value = rufZeit;
rufZeichnen();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bauen);
} else bauen();
})();