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:
@@ -0,0 +1,358 @@
|
||||
/* DER WECKER — eigene Erinnerungen zu einem Termin.
|
||||
|
||||
Wunsch Filipe (07.09.2026): *"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."*
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM DAS EINE EIGENE PRUEFUNG BRAUCHT
|
||||
|
||||
Ein Wecker ist die einzige Stelle im Haus, an der die Anwendung von
|
||||
sich aus etwas TUT, ohne dass jemand davorsitzt. Alles andere
|
||||
passiert auf Klick und wird sofort gesehen. Ein Wecker, der nicht
|
||||
klingelt, faellt niemandem auf -- man verpasst den Termin und haelt
|
||||
sich selbst fuer schuld. Und einer, der zu oft klingelt, wird nach
|
||||
drei Tagen stummgeschaltet, womit auch die richtigen Meldungen weg
|
||||
sind.
|
||||
|
||||
DIE VIER FEHLER, die dabei moeglich sind, und alle vier sind hier
|
||||
nachgestellt:
|
||||
|
||||
KLINGELT NICHT die Weckzeit ist erreicht, es kommt nichts
|
||||
KLINGELT DOPPELT dieselbe Erinnerung zweimal
|
||||
KLINGELT NUR EINMAL von drei gesetzten Weckern kommt einer, weil
|
||||
die Doppelsperre den ganzen Termin sperrt statt
|
||||
des einzelnen Abstands
|
||||
KLINGELT ZU SPAET nach einem Ausfall des Dienstes klingeln alle
|
||||
alten Wecker der letzten Wochen nach
|
||||
|
||||
Der letzte ist der teuerste: Er tritt genau dann auf, wenn ohnehin
|
||||
etwas kaputt war, und schuettet den Nutzer mit Meldungen zu Terminen
|
||||
zu, die laengst vorbei sind.
|
||||
|
||||
GEMESSEN WIRD AM ECHTEN SERVER, nicht an der Rechnung im Kopf: Die
|
||||
Wecker werden ueber die Schnittstelle gesetzt, der Weckerlauf wird
|
||||
wirklich aufgerufen, und nachgesehen wird in `push_verschickt` --
|
||||
der Tabelle, die der Melder fuehrt.
|
||||
|
||||
Aufruf: node server/pruef-wecker.mjs */
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-wecker-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
||||
await portMussFreiSein(4357, "die Weckerpruefung");
|
||||
|
||||
process.env.PORT = "4357";
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
/* KEINE RUHEZEIT WAEHREND DER PRUEFUNG.
|
||||
|
||||
Beim ersten Lauf um 23:42 meldete diese Datei vier Fehler -- und
|
||||
keiner davon war echt: Der Melder schweigt zwischen 22 und 7 Uhr.
|
||||
Sie haette also den Kalender gemessen statt die Software und waere am
|
||||
naechsten Vormittag gruen gewesen. Ein Test, der die Wanduhr als
|
||||
Annahme benutzt, misst irgendwann das Gegenteil.
|
||||
|
||||
Dass die Gegenprobe MITGEFALLEN ist, war dabei die eigentliche
|
||||
Auskunft: Waeren nur die Grenzen falsch gewesen, haette sie gehalten.
|
||||
Sie fiel mit -- also feuerte der Mechanismus ueberhaupt nicht, und
|
||||
die Ursache lag ausserhalb der geprueften Regel.
|
||||
|
||||
`ab 24` und `bis 0` heisst: nie. Die Ruhezeit selbst wird davon nicht
|
||||
ungeprueft -- dafuer gibt es pruef-push. */
|
||||
process.env.PUSH_RUHE_AB = "24";
|
||||
process.env.PUSH_RUHE_BIS = "0";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||||
await import("./index.js");
|
||||
notbremse(300_000, "pruef-wecker");
|
||||
await new Promise((r) => setTimeout(r, 900));
|
||||
const BASIS = "http://127.0.0.1:4357";
|
||||
|
||||
let fehler = 0, gemacht = 0;
|
||||
const ok = (b, t) => { gemacht++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
||||
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
||||
}
|
||||
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const jetztIso = new Date().toISOString();
|
||||
function anlegen(name, rolle, code) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768, jetztIso);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
||||
const idNora = anlegen("Nora", "creator", "CODE-CREA-0002");
|
||||
|
||||
/* EIN ECHTER EMPFAENGER, kein erfundener.
|
||||
|
||||
Erster Anlauf: Adressen auf `example.invalid`, in der Annahme, der
|
||||
Melder trage den Versand nach dem VERSUCH ein. Falsch -- er traegt
|
||||
ihn nach der erfolgreichen ZUSTELLUNG ein (siehe benachrichtige:
|
||||
`if (raus && merkmal)`), und das ist richtig so: Was nicht ankam,
|
||||
darf nicht als geschickt gelten, sonst schweigt der Wecker beim
|
||||
naechsten Lauf endgueltig.
|
||||
|
||||
Meine Annahme war also falsch, nicht der Code. Deshalb steht hier
|
||||
jetzt ein winziger Empfaenger, der wie ein Push-Dienst antwortet
|
||||
(201). Nebennutzen: Damit laeuft in dieser Pruefung auch der echte
|
||||
Versandweg mit -- Verschluesselung, VAPID-Kopf, HTTP -- statt nur
|
||||
der Buchhaltung darum herum. */
|
||||
const { createServer } = await import("node:http");
|
||||
let angekommen = 0;
|
||||
const empfaenger = createServer((req, res) => { angekommen++; res.writeHead(201); res.end(); });
|
||||
await new Promise((r) => empfaenger.listen(0, "127.0.0.1", r));
|
||||
const pushPort = empfaenger.address().port;
|
||||
empfaenger.unref?.();
|
||||
|
||||
const geraet = d.prepare(`INSERT INTO push_anmeldungen
|
||||
(person_id, endpunkt, p256dh, auth, erstellt) VALUES (?,?,?,?,?)`);
|
||||
/* Gueltige Schluessel, keine Platzhalter: Die Verschluesselung rechnet
|
||||
wirklich damit, und ein Fantasieschluessel liesse sie scheitern --
|
||||
die Pruefung waere rot, ohne dass am Wecker etwas falsch waere. */
|
||||
const { createECDH, randomBytes: zufall } = await import("node:crypto");
|
||||
const b64u = (b) => Buffer.from(b).toString("base64")
|
||||
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
for (const pid of [idDogi, idLuna, idNora]) {
|
||||
const kurve = createECDH("prime256v1"); kurve.generateKeys();
|
||||
geraet.run(pid, `http://127.0.0.1:${pushPort}/push/${pid}`,
|
||||
b64u(kurve.getPublicKey()), b64u(zufall(16)), jetztIso);
|
||||
}
|
||||
|
||||
/* Ortszeit, nie toISOString -- ein Termin "in zwei Stunden" darf nicht
|
||||
nachts auf gestern rutschen. */
|
||||
const alsText = (dt) => {
|
||||
const p = (n) => String(n).padStart(2, "0");
|
||||
return `${dt.getFullYear()}-${p(dt.getMonth() + 1)}-${p(dt.getDate())}`
|
||||
+ `T${p(dt.getHours())}:${p(dt.getMinutes())}`;
|
||||
};
|
||||
const inMinuten = (m) => alsText(new Date(Date.now() + m * 60_000));
|
||||
|
||||
const terminAnlegen = (titel, beginn, wer) => {
|
||||
d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, erstellt, erstellt_von)
|
||||
VALUES (?, 'call', ?, 30, ?, ?)`).run(titel, beginn, jetztIso, wer);
|
||||
const id = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
d.prepare("INSERT INTO termin_teilnehmer (termin_id, person_id) VALUES (?,?)").run(id, wer);
|
||||
return id;
|
||||
};
|
||||
|
||||
/* Vier Termine, jeder fuer einen der vier Fehler oben. */
|
||||
const idBald = terminAnlegen("Call in 90 Minuten", inMinuten(90), idDogi);
|
||||
const idFern = terminAnlegen("Call in zehn Tagen", inMinuten(60 * 24 * 10), idDogi);
|
||||
const idVorbei = terminAnlegen("Call von gestern", inMinuten(-60 * 26), idDogi);
|
||||
const idFremd = terminAnlegen("Noras eigener Call", inMinuten(120), idNora);
|
||||
d.close();
|
||||
|
||||
async function anmelden(rolle, code) {
|
||||
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle, code }) });
|
||||
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||||
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
||||
return keks;
|
||||
}
|
||||
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
|
||||
const kLuna = await anmelden("creator", "CODE-CREA-0001");
|
||||
|
||||
const ruf = async (art, pfad, rumpf, keks) => {
|
||||
const a = await fetch(BASIS + pfad, {
|
||||
method: art,
|
||||
headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS },
|
||||
body: rumpf === undefined ? undefined : JSON.stringify(rumpf),
|
||||
});
|
||||
let daten = null; try { daten = await a.json(); } catch { /* leer erlaubt */ }
|
||||
return { status: a.status, daten };
|
||||
};
|
||||
|
||||
/* =======================================================================
|
||||
1. Setzen, lesen, abwaehlen
|
||||
======================================================================= */
|
||||
console.log("\n=== Eigene Wecker setzen ===");
|
||||
{
|
||||
const leer = await ruf("GET", `/workspace/api/termine/${idBald}/wecker`, undefined, kDogi);
|
||||
ok(leer.status === 200, `die Wecker sind abrufbar (${leer.status})`);
|
||||
ok((leer.daten?.meine || []).length === 0, "am Anfang ist keiner gesetzt");
|
||||
ok((leer.daten?.stufen || []).includes(10080) && (leer.daten?.stufen || []).includes(1440),
|
||||
`die Staffel enthaelt Woche und Tag (${(leer.daten?.stufen || []).join(", ")})`);
|
||||
|
||||
const gesetzt = await ruf("PUT", `/workspace/api/termine/${idBald}/wecker`,
|
||||
{ minuten: [10080, 1440, 60] }, kDogi);
|
||||
ok(gesetzt.status === 200, `drei Wecker gesetzt (${gesetzt.status})`);
|
||||
ok(JSON.stringify(gesetzt.daten?.meine) === JSON.stringify([10080, 1440, 60]),
|
||||
`und zwar absteigend sortiert (${JSON.stringify(gesetzt.daten?.meine)})`);
|
||||
|
||||
const wieder = await ruf("GET", `/workspace/api/termine/${idBald}/wecker`, undefined, kDogi);
|
||||
ok(JSON.stringify(wieder.daten?.meine) === JSON.stringify([10080, 1440, 60]),
|
||||
"sie stehen beim naechsten Abruf noch da");
|
||||
|
||||
/* ABWAEHLEN MUSS WIRKEN. Wer nur einfuegt statt vorher zu raeumen,
|
||||
sammelt bei jedem Speichern eine Zeile mehr an -- und der
|
||||
abgewaehlte Wecker klingelt trotzdem. */
|
||||
const weniger = await ruf("PUT", `/workspace/api/termine/${idBald}/wecker`,
|
||||
{ minuten: [1440] }, kDogi);
|
||||
ok(JSON.stringify(weniger.daten?.meine) === JSON.stringify([1440]),
|
||||
`abwaehlen entfernt die anderen wirklich (${JSON.stringify(weniger.daten?.meine)})`);
|
||||
await ruf("PUT", `/workspace/api/termine/${idBald}/wecker`,
|
||||
{ minuten: [10080, 1440, 60] }, kDogi);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
2. Was NICHT geht
|
||||
======================================================================= */
|
||||
console.log("\n=== Die Grenzen ===");
|
||||
{
|
||||
const quatsch = await ruf("PUT", `/workspace/api/termine/${idFern}/wecker`,
|
||||
{ minuten: [7, 999999, 1440, 1440] }, kDogi);
|
||||
ok(JSON.stringify(quatsch.daten?.meine) === JSON.stringify([1440]),
|
||||
`unbekannte Stufen und Dubletten fallen weg, der Rest bleibt (${JSON.stringify(quatsch.daten?.meine)})`);
|
||||
|
||||
const zuviele = await ruf("PUT", `/workspace/api/termine/${idFern}/wecker`,
|
||||
{ minuten: [10080, 4320, 1440, 720, 60, 10] }, kDogi);
|
||||
ok((zuviele.daten?.meine || []).length === 5,
|
||||
`hoechstens fuenf je Termin (${(zuviele.daten?.meine || []).length})`);
|
||||
|
||||
/* Der fremde Termin: Luna sieht Noras Call nicht -- also darf sie
|
||||
sich auch nicht zu ihm wecken lassen. Sonst liesse sich aus dem
|
||||
Meldungstext Titel und Uhrzeit eines fremden Termins lesen. */
|
||||
const fremd = await ruf("PUT", `/workspace/api/termine/${idFremd}/wecker`,
|
||||
{ minuten: [60] }, kLuna);
|
||||
ok(fremd.status === 404, `an einem fremden Termin geht es nicht (${fremd.status})`);
|
||||
|
||||
/* GEGENPROBE: An IHREM eigenen ginge es sehr wohl -- sonst bewiese
|
||||
die Zeile darueber nur, dass gar nichts geht. */
|
||||
const eigener = await ruf("PUT", `/workspace/api/termine/${idBald}/wecker`,
|
||||
{ minuten: [60] }, kDogi);
|
||||
ok(eigener.status === 200, "am eigenen dagegen schon");
|
||||
await ruf("PUT", `/workspace/api/termine/${idBald}/wecker`,
|
||||
{ minuten: [10080, 1440, 60] }, kDogi);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. JEDER FUER SICH
|
||||
======================================================================= */
|
||||
console.log("\n=== Jeder stellt seine eigenen ein ===");
|
||||
{
|
||||
d2Auf();
|
||||
/* Luna wird Teilnehmerin an DogFathers Termin -- damit sieht sie ihn
|
||||
und kann sich eigene Wecker setzen. Genau der Fall aus Filipes
|
||||
Satz: derselbe Termin, zwei Personen, verschiedene Wecker. */
|
||||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
dd.prepare("INSERT OR IGNORE INTO termin_teilnehmer (termin_id, person_id) VALUES (?,?)")
|
||||
.run(idBald, idLuna);
|
||||
dd.close();
|
||||
|
||||
const lunas = await ruf("PUT", `/workspace/api/termine/${idBald}/wecker`,
|
||||
{ minuten: [10] }, kLuna);
|
||||
ok(lunas.status === 200 && JSON.stringify(lunas.daten?.meine) === JSON.stringify([10]),
|
||||
`Luna setzt ihren eigenen (${JSON.stringify(lunas.daten?.meine)})`);
|
||||
|
||||
const dogis = await ruf("GET", `/workspace/api/termine/${idBald}/wecker`, undefined, kDogi);
|
||||
ok(JSON.stringify(dogis.daten?.meine) === JSON.stringify([10080, 1440, 60]),
|
||||
`DogFathers bleiben davon unberuehrt (${JSON.stringify(dogis.daten?.meine)})`);
|
||||
}
|
||||
function d2Auf() { /* nur zur Lesbarkeit oben */ }
|
||||
|
||||
/* =======================================================================
|
||||
4. KLINGELT ES AUCH? Der Weckerlauf am echten Melder
|
||||
======================================================================= */
|
||||
console.log("\n=== Der Lauf ===");
|
||||
{
|
||||
const { laufen } = await import("./workspace-push.js");
|
||||
await laufen();
|
||||
|
||||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const merkmale = dd.prepare(
|
||||
"SELECT person_id, merkmal FROM push_verschickt WHERE merkmal LIKE 'wecker:%'").all();
|
||||
dd.close();
|
||||
const alsSatz = merkmale.map((m) => `${m.person_id}/${m.merkmal}`).sort();
|
||||
|
||||
/* Der Termin ist in 90 Minuten. Faellig sind damit die Wecker fuer
|
||||
eine Woche (10080) und einen Tag (1440) -- beide Zeitpunkte liegen
|
||||
in der Vergangenheit. Der Stunden-Wecker (60) noch NICHT: Er
|
||||
klingelt erst in 30 Minuten. */
|
||||
ok(alsSatz.includes(`${idDogi}/wecker:${idBald}:10080`),
|
||||
`der Wochen-Wecker ist raus (${alsSatz.join(" | ") || "nichts"})`);
|
||||
ok(alsSatz.includes(`${idDogi}/wecker:${idBald}:1440`),
|
||||
"der Tages-Wecker ebenfalls -- ZWEI aus einem Termin, nicht einer");
|
||||
ok(!alsSatz.includes(`${idDogi}/wecker:${idBald}:60`),
|
||||
"der Stunden-Wecker noch nicht -- seine Zeit ist noch nicht da");
|
||||
ok(alsSatz.includes(`${idLuna}/wecker:${idBald}:10`) === false,
|
||||
"Lunas Zehn-Minuten-Wecker auch nicht");
|
||||
|
||||
/* KEINE ALTLASTEN. Der Termin von gestern hat einen Wecker, dessen
|
||||
Zeitpunkt laengst vorbei ist -- er darf NICHT nachtraeglich
|
||||
klingeln. Das ist der Fall nach einem Ausfall des Dienstes, und er
|
||||
faellt sonst erst im Ernstfall auf. */
|
||||
const dd2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
dd2.prepare(`INSERT OR IGNORE INTO termin_wecker (termin_id, person_id, minuten_vorher, gesetzt)
|
||||
VALUES (?,?,?,?)`).run(idVorbei, idDogi, 1440, jetztIso);
|
||||
dd2.close();
|
||||
await laufen();
|
||||
const dd3 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const spaet = dd3.prepare(
|
||||
"SELECT 1 FROM push_verschickt WHERE merkmal = ?").get(`wecker:${idVorbei}:1440`);
|
||||
dd3.close();
|
||||
ok(!spaet, "ein Wecker zu einem vergangenen Termin klingelt NICHT nach");
|
||||
|
||||
/* KEINE DOPPELUNG. Ein zweiter Lauf darf nichts Neues erzeugen --
|
||||
sonst klingelt es alle fuenf Minuten erneut, bis der Termin da ist. */
|
||||
const vorher = alsSatz.length;
|
||||
await laufen();
|
||||
const dd4 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const nachher = dd4.prepare(
|
||||
"SELECT COUNT(*) AS n FROM push_verschickt WHERE merkmal LIKE 'wecker:%'").get().n;
|
||||
dd4.close();
|
||||
ok(nachher === vorher,
|
||||
`ein zweiter Lauf schickt nichts noch einmal (${vorher} -> ${nachher})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
5. GEGENPROBE: erkennt diese Pruefung ueberhaupt ein Klingeln?
|
||||
======================================================================= */
|
||||
console.log("\n=== Gegenprobe ===");
|
||||
{
|
||||
/* Ohne sie bewiesen alle "klingelt nicht"-Zeilen oben nur, dass der
|
||||
Melder gar nichts tut. Ein Wecker, dessen Zeit JETZT gekommen ist,
|
||||
muss ankommen. */
|
||||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
dd.prepare(`INSERT OR IGNORE INTO termin_wecker (termin_id, person_id, minuten_vorher, gesetzt)
|
||||
VALUES (?,?,?,?)`).run(idFremd, idNora, 720, jetztIso);
|
||||
dd.close();
|
||||
|
||||
const { laufen } = await import("./workspace-push.js");
|
||||
await laufen();
|
||||
|
||||
const dd2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const da = dd2.prepare("SELECT 1 FROM push_verschickt WHERE person_id = ? AND merkmal = ?")
|
||||
.get(idNora, `wecker:${idFremd}:720`);
|
||||
dd2.close();
|
||||
ok(!!da, "ein faelliger Wecker kommt wirklich an -- die Messung erkennt beides");
|
||||
}
|
||||
|
||||
console.log(`\n${gemacht} Pruefungen`);
|
||||
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
process.exit(fehler ? 1 : 0);
|
||||
@@ -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);
|
||||
|
||||
+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
|
||||
|
||||
@@ -1181,6 +1181,49 @@ export function db() {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_termin_teilnehmer ON termin_teilnehmer (person_id);
|
||||
|
||||
/* =================================================================
|
||||
DER WECKER (07.09.2026)
|
||||
|
||||
Wunsch 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 jeder fuer sich selbst einstellen koennen."
|
||||
|
||||
EINE ZEILE IST EIN WECKER. Nicht ein Feld am Termin mit einer
|
||||
Liste darin: Mehrere Vorlaufzeiten je Termin sind der ganze
|
||||
Zweck, und "jeder fuer sich" heisst, dass zwei Personen am
|
||||
SELBEN Termin verschiedene Wecker haben. Beides zusammen ist
|
||||
eine klassische n:m-Beziehung, und die gehoert in eine eigene
|
||||
Tabelle. Ein Feld mit kommagetrennten Zahlen waere beim ersten
|
||||
"zeig mir alle faelligen Wecker" nicht mehr abfragbar.
|
||||
|
||||
Die Spalte minuten_vorher statt eines Zeitpunkts: 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, egal wie oft der
|
||||
Termin wandert.
|
||||
|
||||
ON DELETE CASCADE an beiden Seiten: Ein Wecker ohne Termin
|
||||
weckt niemanden, und ein Wecker ohne Person auch nicht.
|
||||
Zusammengesetzter Primaerschluessel: Dieselbe Person kann
|
||||
denselben Abstand nicht zweimal setzen -- sonst klingelt es
|
||||
doppelt, und das merkt man erst nachts.
|
||||
|
||||
Recherche (Google Calendar, Outlook, Morgen): Fuenf Wecker je
|
||||
Termin sind das uebliche Maximum, und die verbreitete Empfehlung
|
||||
fuer Wichtiges lautet "eine Woche, ein Tag, am Tag selbst".
|
||||
Genau diese Staffel bietet die Oberflaeche an. */
|
||||
CREATE TABLE IF NOT EXISTS termin_wecker (
|
||||
termin_id INTEGER NOT NULL REFERENCES termine(id) ON DELETE CASCADE,
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
minuten_vorher INTEGER NOT NULL,
|
||||
gesetzt TEXT NOT NULL,
|
||||
PRIMARY KEY (termin_id, person_id, minuten_vorher)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_termin_wecker_person
|
||||
ON termin_wecker (person_id);
|
||||
|
||||
/* Dasselbe fuer die Wiederholungen. Eine Serie ist eine Regel --
|
||||
die Teilnehmer gehoeren zur Regel, nicht zur einzelnen
|
||||
Auspraegung, sonst muesste man sie jede Woche neu eintragen.
|
||||
|
||||
@@ -938,3 +938,66 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.k-abo__kopf, .k-abo__kopf::after { transition: none; }
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DER WECKER (07.09.2026)
|
||||
|
||||
Filipe: "wie so ein wecker, den man in den eintraegen aktivieren oder
|
||||
ausschalten kann ... auch mehrmals informiert."
|
||||
|
||||
DER KNOPF ZEIGT SEINEN ZUSTAND, ohne dass man ihn anfassen muss:
|
||||
grau und leer heisst "keine Erinnerung", golden mit einer Zahl heisst
|
||||
"so viele". Das beantwortet die haeufigste Frage von aussen. Gold
|
||||
und nicht Rot: Ein Wecker ist kein Alarm, sondern ein Dienst.
|
||||
===================================================================== */
|
||||
.wecker-knopf {
|
||||
--w-ton: #e3b341;
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
min-height: 30px; padding: 3px 9px 3px 7px;
|
||||
border: 1px solid var(--rand); border-radius: 999px;
|
||||
background: transparent; color: var(--text-still);
|
||||
font: inherit; font-size: .78rem; cursor: pointer;
|
||||
transition: color var(--tempo), border-color var(--tempo), background var(--tempo);
|
||||
}
|
||||
.wecker-knopf:hover { color: var(--text); border-color: var(--rand-hell); }
|
||||
.wecker-knopf__svg {
|
||||
width: 16px; height: 16px; flex: none;
|
||||
fill: none; stroke: currentColor; stroke-width: 1.6;
|
||||
stroke-linecap: round; stroke-linejoin: round;
|
||||
}
|
||||
.wecker-knopf__svg .fuell { stroke: none; fill: transparent; }
|
||||
.wecker-knopf__zahl { font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* Gesetzt: der Wecker bekommt Farbe und Fuellung -- dieselbe Sprache
|
||||
wie die Rollensymbole, wo die Flaeche den Zustand traegt. */
|
||||
.wecker-knopf[data-an="ja"] {
|
||||
color: var(--w-ton);
|
||||
border-color: color-mix(in srgb, var(--w-ton) 45%, transparent);
|
||||
background: color-mix(in srgb, var(--w-ton) 12%, transparent);
|
||||
}
|
||||
.wecker-knopf[data-an="ja"] .wecker-knopf__svg .fuell {
|
||||
fill: color-mix(in srgb, var(--w-ton) 42%, transparent);
|
||||
}
|
||||
|
||||
.wecker-dialog { max-width: 420px; }
|
||||
.wecker-hinweis { margin: 0 0 12px; }
|
||||
.wecker-liste { display: grid; gap: 2px; }
|
||||
.wecker-zeile {
|
||||
display: flex; align-items: center; gap: 11px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background var(--tempo);
|
||||
}
|
||||
.wecker-zeile:hover { background: rgba(255, 255, 255, .05); }
|
||||
/* Das Kaestchen gross genug fuer den Daumen -- eine Auswahl, die man
|
||||
dreimal antippen muss, ist keine. */
|
||||
.wecker-zeile input { width: 20px; height: 20px; flex: none; accent-color: #e3b341; }
|
||||
.wecker-zeile__text { font-size: .92rem; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wecker-knopf, .wecker-zeile { transition: none; }
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.wecker-knopf[data-an="ja"] { border: 2px solid CanvasText; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -212,10 +212,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -135,10 +135,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-14
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -144,14 +144,14 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -77,11 +77,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,17 +9,17 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
|
||||
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
|
||||
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
|
||||
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
|
||||
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
|
||||
Dialog zu sehen bekam. -->
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -155,10 +155,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609072322"></script>
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322"></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322"></script>
|
||||
<script src="assets/js/chat.js?v=202609072322"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344"></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344"></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344"></script>
|
||||
<script src="assets/js/chat.js?v=202609072344"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -167,12 +167,12 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/content.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/content.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -96,10 +96,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="gate">
|
||||
@@ -219,6 +219,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/gate.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/gate.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+41
-11
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -256,13 +256,43 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- ====================================================================
|
||||
DER WECKER (07.09.2026)
|
||||
|
||||
Filipe: "wie so ein wecker, den man in den eintraegen aktivieren
|
||||
oder ausschalten kann ... auch mehrmals informiert."
|
||||
|
||||
Ein eigener Dialog und kein Aufklapp-Feld im Tagesfenster: Der
|
||||
Wecker gehoert zu EINEM Termin, und im Tagesfenster stehen mehrere.
|
||||
Ein Feld, das sich dort aufklappt, schiebt alles darunter weg --
|
||||
man verliert die Zeile, die man gerade angesehen hat.
|
||||
|
||||
<dialog> statt eines selbstgebauten Kastens: Es bringt das
|
||||
Wichtigste mit, was man sonst vergisst -- Escape schliesst, der
|
||||
Tastaturfokus bleibt darin gefangen, und der Rest der Seite ist
|
||||
fuer Vorleseprogramme abgemeldet. -->
|
||||
<dialog class="tagdialog wecker-dialog" id="wecker-dialog" aria-labelledby="wecker-titel">
|
||||
<div class="tagdialog__kopf">
|
||||
<div>
|
||||
<h2 class="titel" id="wecker-titel">Erinnerung</h2>
|
||||
<p class="leise" id="wecker-wann"></p>
|
||||
</div>
|
||||
<button type="button" class="knopf-still" id="wecker-zu" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
<p class="leise wecker-hinweis">
|
||||
Nur für dich. Andere Teilnehmer stellen ihre Erinnerungen selbst ein.
|
||||
</p>
|
||||
<div class="wecker-liste" id="wecker-liste"></div>
|
||||
<p class="fehler" id="wecker-fehler" role="alert" aria-live="polite"></p>
|
||||
</dialog>
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kalender-abo.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kalender-abo.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -212,10 +212,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322"></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322"></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322"></script>
|
||||
<script src="assets/js/leistung.js?v=202609072322"></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344"></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344"></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344"></script>
|
||||
<script src="assets/js/leistung.js?v=202609072344"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -157,10 +157,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -202,11 +202,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -94,11 +94,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/report.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/report.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -127,10 +127,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
|
||||
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
|
||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -320,10 +320,10 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/start.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/start.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -73,11 +73,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -114,10 +114,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,13 +9,13 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
|
||||
Dialog "Creator anlegen". -->
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -137,11 +137,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -9,11 +9,11 @@
|
||||
<meta name="theme-color" content="#0674b9" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072322" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609072344" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609072344" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -178,10 +178,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609072322" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609072344" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609072344" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user