Files
dogfather-universe/workspace/assets/js/glocke.js
T
DogFatherGitandClaude Opus 5 cccafd2c31 Sechs Wuensche: Hintergruende, Knopfverteilung, Universum, Uhrkoepfe, Knallrot
--- screen3 + screen1: die Hintergruende draengeln nicht mehr ---

"die hintergrunde sollen ueberall so sein dass die sich nicht in den
vordergrund draengeln." / "mach den hintergrund von dieser kachel
dunkler, so dass die die kacheln drin viel mehr auffallen."

`--raster` stand auf .26 Deckkraft -- auf dunkler Flaeche kein Hauch
mehr, sondern ein gezeichnetes Gitter. Im Tagdialog lief es sichtbar
durch die Ueberschrift, in den Sammelkacheln stand es VOR den Karten
darin. Jetzt .09: Man sieht eine Struktur, man zaehlt keine Linien.
Eine Zahl fuer das ganze Haus -- sie steht einmal in module.css und
wird an fuenf Stellen benutzt.

Die Anlasskachel im Kalender lag mit rgba(19,26,38,.88) auf demselben
Helligkeitsniveau wie die Karten darin: keine Vitrine, sondern eine
dritte Flaeche gleicher Lautstaerke. Jetzt fast schwarz. An den Karten
musste dafuer nichts geaendert werden -- der Abstand entsteht von
selbst.

--- screen2: ein Knopf wandert nach links ---

"eins von diesen buttons soll links bei dem anderen kreis sein."

Die GLOCKE geht nach links zum Ring, der WECKER bleibt rechts bei der
Uhr. Das ist nicht ausgewuerfelt: Der Wecker ist eine Uhrzeit. Die
Glocke entscheidet, ob man ueberhaupt etwas ueber sein Team erfaehrt --
und der Ring links zeigt genau das.

Die Kachel ist damit spiegelsymmetrisch belegt: 48 px Knopf + 14 px
Abstand + 248 px Instrument auf beiden Seiten. Genau das rechnet
`--spalte`. Die Zentrierung des Rings ist weggefallen -- sie war noetig,
solange er allein in einer fuer Instrument PLUS Knopf bemessenen Spalte
stand. Der reservierte Platz schrumpft von 104 auf 48 px je Seite; die
Begruendung fuer das Reservieren bleibt: Ein Platz, der erst mit der
Antwort entsteht, laesst die Zeile springen.

--- screen4: zwei Chilis und zwei Huskys dazu ---

"setz in den hintergrund noch 1-2 peperonis und dan das logo von
dogfather, also nur den husky."

DER HUSKY IST EINE MASKE, KEIN BILD. Die Datei ist schwarz-weiss und
freigestellt (nachgemessen: 49 % durchsichtig). Als Hintergrundbild bei
15 % verschwaenden die schwarzen Flaechen im dunklen Grund und uebrig
blieben die hellen -- ein zerrissener Umriss, kein Hund. Als Maske
ueber einer Farbflaeche wird daraus eine geschlossene Silhouette in
DogFathers Babyblau. Im Universum schweben jetzt beide Marken in ihren
beiden Farben: sieben Chilis rot, zwei Huskys blau.

--- screen5: die Punkte sind ersetzt, das letzte Glied leuchtet ---

"ich will dass du die punkte ersetzt und immer der letzte soll mehr
strahlen oder so." / "alles ist mega ausser die stunden muss du noch
perfektionnieren."

DIE DREI UMLAUFENDEN PERLEN SIND WEG -- samt 60 Zeilen Rechnung. Sie
waren ein zweites Ding an einer zweiten Stelle: eigene Uhr ab dem
ersten Takt (weil die volle Unixzeit `rotate(1.07333e+10deg)` ergab),
eigener Startwinkel je Bahn, drei Winkel, die die kleineren Einheiten
anteilig mittragen mussten. All das war noetig, WEIL der Kopf neben der
Reihe herlief statt Teil von ihr zu sein. Genau deshalb trugen sie am
08.09. noch die alten Farben, als die Bahnen getauscht wurden.

Jetzt zeichnet eine zweite SVG-Lage genau EIN Glied heller -- dasselbe,
das die Reihe darunter zuletzt gesetzt hat, aus denselben Zahlen. Sie
kann gar nicht danebenstehen. Heller statt groesser: Waere der Kopf
groesser, waere er ein Fremdkoerper in der Reihe.

DIE STUNDEN WAREN BREITER ALS LANG -- 5,5 lang bei 7 breit, also 0,79:1.
Ein Segment, das breiter ist als lang, liest sich als Klotz quer auf
der Bahn statt als Balken entlang. Und weil die Kantenlage nur 0,4
versetzt ist, lief der 0,9 breite Lichtstrich MITTEN DURCH jeden Balken
statt an seiner Kante. Jetzt 10 lang bei 5,8 breit (1,7:1), und der
Kantenversatz ist nach Bandbreite gestaffelt: (Breite - 0,9) / 2, also
0,75 / 1,65 / 2,05 zusaetzlich zur Gruppe.

--- screen6: die Scout-Pipeline wird knallrot ---

"die farbe von dieser kategorie soll knall rot sein."

Die anderen zwanzig Kachelfarben sind gerechnet (OKLab, groesstmoeglicher
Abstand). Diese eine ist gewuenscht -- und wurde deshalb GEGEN den Satz
geprueft statt eingetragen: Der engste vorhandene Abstand liegt bei
0,0973. #ff1f2e kommt seinem naechsten Nachbarn auf 0,1228 nahe, ist
also weiter entfernt als das engste vorhandene Paar. Kontrast 5,01:1
(noetig 4,5). Von sechs geprueften Rottoenen der mit dem groessten
Abstand UND genug Kontrast.

tools/kachel-farben.mjs weiss jetzt davon: Ein kuenftiger Lauf wuerde
wieder Rosa vorschlagen, und das waere eine stille Ruecknahme einer
ausdruecklichen Entscheidung.

--- Ein Messfehler, der festgehalten gehoert ---

Beim Pruefen von screen4 meldete meine Foto-Methode vier Beschriftungen
unter 4,5:1 -- bei einem Verlust von 0,00 bis 0,15 durch das Universum.
Dass die Ursache nicht das Universum sein konnte, stand damit schon in
den Zahlen. Exakt gerechnet (Vordergrundfarbe gegen die tatsaechliche
Flaeche darunter) liegen dieselben Texte bei 9,13:1 bis 10,76:1.

Die Foto-Methode mittelt ueber alle helleren Bildpunkte, und bei
duenner Grossbuchstabenschrift ist die Haelfte davon halb
ausgeleuchtete Kantenpunkte. Fuer grosse Schrift taugt sie, fuer kleine
Versalien meldet sie systematisch zu wenig. Haette ich ihr geglaubt,
haette ich vier Farben "repariert", die in Ordnung sind.

Geprueft: pruef-start-ansicht, pruef-css-klassen, pruef-kalender,
pruef-buehne, pruef-handy, pruef-workspace-seiten, pruef-tagesruf --
alle in Ordnung. Dazu Ring und Uhr auf 248/248 nachgemessen, die
Kopf-Muster gegen die Uhrzeit nachgerechnet (01:24:59 -> Sekunde bei
281,115, Minute bei 96,76, Stunde bei 16,493) und die Kachelfarbe gegen
alle zwanzig anderen in OKLab geprueft.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 01:33:59 +02:00

457 lines
20 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. */
/* SEIT DEM 09.09.2026 GIBT ES ZWEI PLAETZE (screen2).
Die Glocke geht nach LINKS neben den Ring, der Tagesruf bleibt
rechts neben der Uhr. Welcher wohin, folgt der Bedeutung: Der
Wecker ist eine Uhrzeit und gehoert zur Uhr; die Glocke
entscheidet, ob man ueberhaupt etwas ueber sein Team erfaehrt --
und der Ring links zeigt genau das.
Gibt es den linken Platz nicht (jede Seite ausser der
Startseite), faellt die Glocke wie bisher auf den rechten
zurueck und von dort auf die Kopfleiste. Zwei Rueckfaelle, kein
Sonderfall: Ein Knopf, der auf zwoelf Seiten verschwindet, weil
er auf einer umgezogen ist, waere keine Verbesserung. */
const platzLinks = document.getElementById('glocke-platz-links');
const platz = platzLinks || 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();
/* Der Tagesruf gehoert IMMER an den rechten Platz -- auch wenn die
Glocke links sitzt. `platz` zeigt jetzt auf den linken, deshalb
wird der rechte hier eigens geholt. */
const platzRechts = document.getElementById('glocke-platz');
if (platzRechts) await tagesrufBauen(platzRechts);
}
/* =====================================================================
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();
})();