Benachrichtigungen wecken das Telefon wieder: Urgency je Art

Diene hat gemeldet: "Die Benachrichtigungen werden nicht angezeigt,
wenn neue Nachrichten reinkommen. Erst, wenn man die App oeffnet."

Alles Messbare war gruen: Der Service Worker zeigt die Meldung bei
geschlossener App (pruef-push-zu, 13 Pruefungen), jede Anmeldung im
Haus stand auf fehler = 0, der Push-Dienst quittierte mit 201. Nur
den Kopf auf der Leitung hat nie jemand gemessen:

    Urgency: normal

Beide Dienste behandeln das ausdruecklich als "darf warten".
Android/FCM haelt solche Meldungen zurueck, solange das Telefon
doest, und stellt sie zu, wenn es aufwacht -- typischerweise beim
Entsperren oder Oeffnen der App. Apple/APNs nennt es "verzoegert,
gebuendelt oder gedrosselt". Das ist Dienes Satz, Wort fuer Wort,
und es stand als Vorgabe im eigenen Quelltext.

zuletzt_ok konnte das nie zeigen: Es beweist, dass der DIENST
angenommen hat, nicht dass das GERAET etwas angezeigt hat.

Bis hierher hing die Dringlichkeit an der Ruheregel (dringend =
regel === "nie"), mit der Begruendung, "darf das nachts stoeren?"
und "darf das warten?" haetten dieselbe Antwort. Haben sie nicht:
Ein Chat um 3 Uhr soll schweigen, um 14 Uhr aber nicht vierzig
Minuten liegen bleiben. Jetzt zwei Felder -- in DERSELBEN Zeile
derselben Artenliste, damit sie nicht auseinanderlaufen koennen.

10 von 19 Arten sind eilig (Anruf, Chat, Erwaehnung, Support, Hilfe,
Termin, Wecker, zweimal Live, Probe). Die anderen neun duerfen
warten -- waere alles eilig, waere nichts mehr eilig.

DIE FALLE BEIM BEHEBEN: dringend steuerte auch die Haltbarkeit
(TTL 150). Ein einfach auf "dringend" gestellter Chat waere nach
150 s verfallen -- wer sein Telefon drei Minuten aus hat, haette die
Nachricht GAR nicht mehr bekommen. Aus "zu spaet" waere "nie"
geworden. Deshalb sind eilig und kurzlebig getrennt; kurzlebig
bleibt genau beim Anruf und der Probe.

Der alte Name kracht jetzt, statt still "normal" zu liefern.

pruef-push-eilig.mjs (neu, 13 Pruefungen): die Entscheidung je Art
unabhaengig notiert statt aus der Artenliste abgelesen, jede Art
muss eine Entscheidung haben, und der Kopf wird durch
benachrichtige() hindurch an einem nachgebauten Push-Dienst
gemessen. Gegenprobe gelaufen: ohne das Feld meldet sie
"chat_nachricht geht mit Urgency: normal hinaus", 4 Fehler.
Die Uhr ist dort eine Eingabe, keine Annahme -- die Zeitzone wird so
gewaehlt, dass der Lauf immer auf 12 Uhr faellt, sonst waere die
Pruefung nachts rot ohne Befund.

pruef-anruf-klingelt 25 -> 26. Dabei aufgefallen: Eine ihrer
Pruefungen war gruen, obwohl die Zeile aus dem Code verschwunden war
-- sie stand nur noch in einem Kommentar, der die alte Fassung
zitiert. Quelltextpruefungen lesen dort jetzt ohne Kommentare.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-03 12:58:01 +02:00
co-authored by Claude Opus 5
parent db8dd2fbea
commit 73f9b24793
4 changed files with 420 additions and 36 deletions
+50 -14
View File
@@ -63,6 +63,24 @@ import { fileURLToPath } from "node:url";
const REPO = join(dirname(fileURLToPath(import.meta.url)), ".."); const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
const lies = (p) => readFileSync(join(REPO, p), "utf8"); const lies = (p) => readFileSync(join(REPO, p), "utf8");
/* ---- NUR DER CODE, OHNE DIE ERZAEHLUNG DRUMHERUM (03.10.2026) -------
Diese Datei prueft Quelltext mit Mustern. Das ist schnell und
braucht keinen Server -- hat aber eine Luecke, die heute
aufgefallen ist: Die Pruefung "und zwar an derselben Bedingung wie
die Nachtruhe" war gruen, OBWOHL die Zeile aus dem Code
verschwunden war. Sie stand nur noch in einem Kommentar, der die
alte Fassung zitiert -- und ein Muster sieht den Unterschied
nicht.
Das ist die dritte Sorte gruener Haken: nicht uebersprungen, nicht
falsch geprueft, sondern am Beleg vorbei. Deshalb werden
Kommentare vorher entfernt. Was hier geprueft wird, muss laufender
Code sein. */
const nurCode = (t) => t
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:])\/\/.*/g, "$1");
let ok = 0, fehl = 0; let ok = 0, fehl = 0;
const pruef = (name, bedingung, zusatz = "") => { const pruef = (name, bedingung, zusatz = "") => {
if (bedingung) { ok++; console.log(` OK ${name}`); } if (bedingung) { ok++; console.log(` OK ${name}`); }
@@ -104,29 +122,47 @@ pruef("und für alles andere bleibt es ruhig",
/* ===================================================================== /* =====================================================================
2. DER VERSAND — Dringlichkeit und Haltbarkeit 2. DER VERSAND — Dringlichkeit und Haltbarkeit
===================================================================== */ ===================================================================== */
const krypto = lies("server/workspace-push-krypto.js"); const krypto = nurCode(lies("server/workspace-push-krypto.js"));
const versand = lies("server/workspace-push.js"); const versand = nurCode(lies("server/workspace-push.js"));
pruef("der Transport kennt überhaupt eine Dringlichkeit", pruef("der Transport kennt überhaupt eine Dringlichkeit",
/dringend\s*=\s*false\s*\}\s*=\s*\{\}/.test(krypto), /eilig\s*=\s*false/.test(krypto),
"vorher stand Urgency fest auf \"normal\""); "vorher stand Urgency fest auf \"normal\"");
pruef("ein dringender Push geht mit Urgency: high hinaus", pruef("ein eiliger Push geht mit Urgency: high hinaus",
/Urgency:\s*dringend\s*\?\s*"high"\s*:\s*"normal"/.test(krypto)); /Urgency:\s*eilig\s*\?\s*"high"\s*:\s*"normal"/.test(krypto));
pruef("und verfällt, wenn der Anruf vorbei ist", pruef("und verfällt, wenn der Anruf vorbei ist",
/TTL:\s*String\(dringend\s*\?\s*Math\.min\(ttl,\s*\d+\)/.test(krypto), /TTL:\s*String\(kurzlebig\s*\?\s*Math\.min\(ttl,\s*\d+\)/.test(krypto),
"sonst ploppt ein Anruf von vor einer Stunde nachträglich auf"); "sonst ploppt ein Anruf von vor einer Stunde nachträglich auf");
pruef("der Anruf wird als dringend verschickt", pruef("der Anruf wird eilig UND kurzlebig verschickt",
/schicken\(a,\s*inhalt,\s*paar,\s*ABSENDER,\s*\{\s*dringend\s*\}\)/.test(versand)); /schicken\(a,\s*inhalt,\s*paar,\s*ABSENDER,\s*\{\s*eilig,\s*kurzlebig\s*\}\)/.test(versand));
/* DIESELBE BEDINGUNG WIE BEI DER RUHEZEIT. „Darf das nachts stören?" /* ---- ZWEI FRAGEN, ZWEI FELDER (geändert 03.10.2026) -----------------
und „darf das warten?" haben dieselbe Antwort -- zwei getrennte
Bedingungen wären die, die beim nächsten Nachschärfen auseinander Hier stand bis heute das Gegenteil: „Darf das nachts stören?" und
laufen. */ „darf das warten?" hätten dieselbe Antwort, zwei getrennte
pruef("und zwar an derselben Bedingung wie die Nachtruhe", Bedingungen wären die, die auseinanderlaufen. Die Vorhersage war
/const dringend = art === "test" \|\| art === "anruf";/.test(versand)); richtig, nur in die andere Richtung — sie laufen auseinander, WEIL
es zwei Fragen sind:
Chat um 3 Uhr nachts -> nicht stören (Ruhezeit greift)
Chat um 14 Uhr -> nicht warten (eilig)
Genau diese Gleichsetzung hat Dienes Meldung verursacht: Weil der
Chat nicht nachts stören darf, bekam er auch tagsüber
`Urgency: normal` — und Android legt das beiseite, bis jemand die
App öffnet. Das Zusammenlegen war die Ursache, nicht der Schutz.
Auseinanderlaufen können die beiden trotzdem nicht: Sie stehen als
`eilig` und `ruhe` in DERSELBEN Zeile derselben Artenliste. */
pruef("der Anruf ist eilig und kurzlebig, beides in einer Zeile",
/\{\s*schluessel:\s*"anruf",\s*eilig:\s*true,[^}]*ruhe:\s*"nie"/.test(versand));
pruef("und die Dringlichkeit wird aus der Artenliste abgeleitet",
/const eilig = eiligRegel\(art\);/.test(versand),
"nicht aus einer zweiten Aufzählung daneben");
/* ===================================================================== /* =====================================================================
3. DER WEG DORTHIN — wird beim Anruf überhaupt geweckt? 3. DER WEG DORTHIN — wird beim Anruf überhaupt geweckt?
+282
View File
@@ -0,0 +1,282 @@
/* =====================================================================
WECKT DIE MELDUNG DAS TELEFON -- oder darf sie warten?
WARUM ES DIESE PRUEFUNG GIBT (03.10.2026).
Diene hat gemeldet: "Die Benachrichtigungen werden nicht angezeigt,
wenn neue Nachrichten reinkommen. Erst, wenn man die App oeffnet."
Alles, was wir bis dahin messen konnten, war gruen: Der Service
Worker zeigt die Meldung (pruef-push-zu, 13 Pruefungen), jede
Anmeldung im Haus stand auf `fehler = 0`, und der Push-Dienst
quittierte jede Zustellung mit 201. Nur hat niemand den Kopf
gemessen, der auf dem Weg mitgeht:
Urgency: normal
Beide Dienste behandeln das ausdruecklich als "darf warten".
Android/FCM haelt solche Meldungen zurueck, solange das Telefon
doest, und stellt sie zu, wenn es aufwacht -- typischerweise, wenn
jemand entsperrt oder die App oeffnet. Apple/APNs nennt es
"verzoegert, gebuendelt oder gedrosselt". Das ist Dienes Satz, Wort
fuer Wort, und es stand als Vorgabe in unserem eigenen Quelltext.
`zuletzt_ok` konnte das nie zeigen: Es beweist, dass der DIENST
angenommen hat (201), nicht dass das GERAET etwas angezeigt hat.
Zwischen beidem liegt genau dieser Kopf.
WAS HIER GEMESSEN WIRD, und warum in dieser Reihenfolge:
1. Die ENTSCHEIDUNG je Art -- und zwar unabhaengig notiert, nicht
aus der Artenliste abgelesen. Eine Pruefung, die ihre Erwartung
aus derselben Tabelle holt, die sie prueft, ist ein Spiegel:
Sie ist immer gruen, auch wenn jemand die Tabelle falsch
aendert.
2. VOLLSTAENDIGKEIT -- jede Art muss unten eine Entscheidung
haben. Wer eine neue eintraegt, ohne sich zu entscheiden,
bekommt einen Fehler statt stillschweigend "darf warten".
3. Der KOPF AUF DER LEITUNG, durch `benachrichtige` hindurch bis
zu einem nachgebauten Push-Dienst. Nur das beweist, dass die
Entscheidung auch ankommt.
4. Dass Dringlichkeit und HALTBARKEIT getrennt bleiben. Das war
die Falle beim Beheben: Beides hing an einem Schalter, und ein
eiliger Chat haette damit TTL 150 bekommen -- wer sein Telefon
drei Minuten aus hat, haette die Nachricht dann GAR nicht mehr
bekommen. Aus "zu spaet" waere "nie" geworden.
GEGENPROBE: Der alte Name `dringend` muss krachen, und eine nicht
eilige Art muss nachweislich "normal" bekommen -- sonst koennte
diese Pruefung gar nichts anderes als "high" sehen.
DREI AUSGAENGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht nachsehen.
===================================================================== */
/* ---- DIE UHR IST HIER EINE EINGABE, KEINE ANNAHME --------------------
`istRuhezeit` fragt `Date.getHours()`, also die Zeitzone dieses
Prozesses. Liefe die Pruefung um 23 Uhr, wuerde `benachrichtige` zu
Recht nichts verschicken, und alles unten saehe nach Befund aus,
ohne dass etwas kaputt waere -- eine Zeitbombe wie die
Gate-Pruefung vom 06.09. Deshalb wird die Zone so gewaehlt, dass
JETZT auf 12 Uhr mittags faellt, egal wann der Lauf startet. */
const utcStunde = new Date().getUTCHours();
const versatz = ((12 - utcStunde + 12) % 24) - 12; /* -12 .. +11 */
process.env.TZ = versatz === 0 ? "Etc/GMT"
: `Etc/GMT${versatz > 0 ? "-" : "+"}${Math.abs(versatz)}`; /* POSIX: Vorzeichen umgekehrt */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createServer } from "node:http";
import { createECDH, randomBytes } from "node:crypto";
const ordner = mkdtempSync(join(tmpdir(), "ws-eilig-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
import { notbremse } from "./helfer-notbremse.mjs";
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "pruef-push-eilig", 0);
const PORT_DIENST = await eigenerPort(import.meta, "pruef-push-eilig (Push-Dienst)", 1);
process.env.PORT = `${PORT}`;
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
await import("./index.js");
notbremse(120_000, "pruef-push-eilig");
await new Promise((r) => setTimeout(r, 900));
let fehler = 0, geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const abbruch = (grund) => {
console.log(`\n KONNTE NICHT NACHSEHEN: ${grund}`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(2);
};
const push = await import("./workspace-push.js");
const { b64u } = await import("./workspace-push-krypto.js");
const { DatabaseSync } = await import("node:sqlite");
/* =====================================================================
DIE ENTSCHEIDUNG -- von Hand notiert, nicht abgelesen.
Massstab: eilig ist, was durch Warten WERTLOS wird. Nicht eilig
ist, was auch eine halbe Stunde spaeter noch stimmt. Waere alles
eilig, waere nichts mehr eilig -- die Geraete duerfen uns die
Dringlichkeit glauben, sonst drosseln sie uns, und der Akku zahlt.
===================================================================== */
const ERWARTET = {
/* --- weckt das Geraet: jemand wartet, oder es ist gleich vorbei --- */
anruf: true, /* klingelt 120 Sekunden */
test: true, /* jemand steht davor und tippt "Probe" */
chat_nachricht: true, /* jemand wartet auf eine Antwort */
chat_erwaehnung: true, /* jemand meint DICH */
support: true, /* Meldung oder Antwort darauf */
hilfe: true, /* vertrauliche Meldung */
termin_gleich: true, /* in einer Stunde -- danach wertlos */
termin_wecker: true, /* selbst gestellt; ein spaeter Wecker ist keiner */
dogfather_live: true, /* "jetzt" oder gar nicht */
reaktion_live: true, /* dito */
/* --- darf warten: stimmt spaeter auch noch ----------------------- */
aufgabe_faellig: false, /* morgen faellig */
aufgabe_ueberfaellig: false, /* ist es schon */
protokoll_fehlt: false,
followup: false,
bewerbung_neu: false,
bewerbung_antwort: false,
manager_ziele: false,
tagesruf: false, /* Tagesuebersicht */
befinden: false, /* alle zwei Wochen */
};
console.log("\n=== Die Entscheidung je Art ===");
{
const arten = push.ARTEN.map((a) => a.schluessel);
/* Vollstaendigkeit in BEIDE Richtungen -- eine neue Art ohne
Entscheidung ist ein Fehler, und eine Entscheidung ohne Art ist
ein Rest, der niemanden mehr schuetzt. */
const ohneEntscheidung = arten.filter((a) => !(a in ERWARTET));
const ohneArt = Object.keys(ERWARTET).filter((a) => !arten.includes(a));
ok(arten.length > 0 && ohneEntscheidung.length === 0,
ohneEntscheidung.length
? `OHNE ENTSCHEIDUNG: ${ohneEntscheidung.join(", ")} -- eilig oder nicht?`
: `alle ${arten.length} Arten haben eine Entscheidung`);
ok(ohneArt.length === 0,
ohneArt.length ? `Entscheidung ohne Art: ${ohneArt.join(", ")}` : "keine Karteileiche");
let stimmt = 0;
for (const [art, soll] of Object.entries(ERWARTET)) {
if (push.eiligRegel(art) === soll) stimmt++;
else ok(false, `${art}: erwartet ${soll ? "eilig" : "darf warten"}, ist es nicht`);
}
const anzahl = Object.keys(ERWARTET).length;
ok(anzahl > 0 && stimmt === anzahl, `${stimmt} von ${anzahl} Arten wie entschieden`);
const eilige = arten.filter((a) => push.eiligRegel(a)).length;
ok(eilige > 0 && eilige < arten.length,
`${eilige} von ${arten.length} sind eilig -- geteilt, nicht pauschal`);
ok(push.eiligRegel("gibtsnicht") === false,
"eine unbekannte Art ist NICHT eilig (im Zweifel nicht wecken)");
}
/* =====================================================================
Der Kopf auf der Leitung
===================================================================== */
console.log("\n=== Was wirklich beim Push-Dienst ankommt ===");
const empfangen = [];
const dienst = createServer((q, a) => {
q.on("data", () => {});
q.on("end", () => { empfangen.push(q.headers); a.writeHead(201); a.end(); });
});
await new Promise((r) => dienst.listen(PORT_DIENST, "127.0.0.1", r));
const browser = createECDH("prime256v1");
browser.generateKeys();
let d;
try { d = new DatabaseSync(process.env.WORKSPACE_DB); }
catch (e) { abbruch(`die Testdatenbank laesst sich nicht oeffnen: ${e.message}`); }
const jetzt = new Date().toISOString();
/* Der Zugangscode ist hier BEDEUTUNGSLOS -- angemeldet wird sich
nicht, gemessen wird der Weg nach draussen. Die Spalten sind nur
NOT NULL, also stehen dort Zufallsbytes: kein echter Code, kein
erfundener, und keiner, der jemandem Zugang gaebe. */
const fuellung = randomBytes(32).toString("hex");
d.prepare(`INSERT INTO personen
(name, rolle, code_hash, code_salt, code_n, aktiv, erstellt)
VALUES (?,?,?,?,?,1,?)`)
.run("Probeperson", "admin", fuellung, fuellung.slice(0, 32), 32768, jetzt);
const personId = d.prepare("SELECT id FROM personen WHERE name = 'Probeperson'").get().id;
d.prepare(`INSERT INTO push_anmeldungen
(endpunkt, person_id, p256dh, auth, geraet, erstellt, zuletzt_ok, fehler)
VALUES (?,?,?,?,?,?,NULL,0)`)
.run(`http://127.0.0.1:${PORT_DIENST}/push/probe`, personId,
b64u(browser.getPublicKey()), b64u(Buffer.alloc(16, 7)), "Pruefgeraet", jetzt);
d.close();
/** Verschickt EINE Meldung und gibt den Kopf zurueck, der ankam. */
const kopfVon = async (art) => {
const vorher = empfangen.length;
const e = await push.benachrichtige(personId, art, {
titel: "Probe", text: art, ziel: "/workspace/start.html",
});
if (e.verschickt !== 1) return { grund: e.grund };
return { kopf: empfangen[vorher], grund: e.grund };
};
{
/* Je ein Vertreter beider Seiten -- beide mit `ruhe: "immer"`, damit
die Messung nicht an der Ruhezeit haengt (die oben auf Mittag
festgenagelt ist). */
const eilig = await kopfVon("chat_nachricht");
const ruhig = await kopfVon("aufgabe_faellig");
if (!eilig.kopf || !ruhig.kopf) {
abbruch("es ging nichts hinaus "
+ `(chat_nachricht: ${eilig.grund}, aufgabe_faellig: ${ruhig.grund}) `
+ "-- ohne eine zugestellte Meldung ist der Kopf nicht messbar");
}
ok(eilig.kopf.urgency === "high",
`chat_nachricht geht mit Urgency: ${eilig.kopf.urgency} hinaus (muss "high" sein)`);
ok(ruhig.kopf.urgency === "normal",
`aufgabe_faellig geht mit Urgency: ${ruhig.kopf.urgency} hinaus (muss "normal" sein)`);
/* DIE GEGENPROBE: Saehe diese Pruefung grundsaetzlich nur "high",
waere die Zeile darueber wertlos. Dass sich die beiden Koepfe
UNTERSCHEIDEN, ist der Beweis, dass gemessen und nicht geraten
wird. */
ok(eilig.kopf.urgency !== ruhig.kopf.urgency,
"die beiden unterscheiden sich -- es wird wirklich gemessen");
/* HALTBARKEIT: der eigentliche Grund, warum es zwei Felder sind.
Ein eiliger Chat darf NICHT nebenbei kurzlebig werden. */
ok(eilig.kopf.ttl === "3600",
`chat_nachricht bleibt ${eilig.kopf.ttl} s gueltig (muss 3600 sein, nicht 150)`);
ok(ruhig.kopf.ttl === "3600", `aufgabe_faellig: TTL ${ruhig.kopf.ttl}`);
/* Und das, was kurzlebig sein SOLL, ist es noch: ein Anruf von vor
einer Stunde darf nicht nachtraeglich aufploppen. */
const anruf = await kopfVon("anruf");
if (!anruf.kopf) abbruch(`der Anruf ging nicht hinaus (${anruf.grund})`);
ok(anruf.kopf.urgency === "high", `anruf: Urgency ${anruf.kopf.urgency}`);
ok(anruf.kopf.ttl === "150",
`anruf verfaellt nach ${anruf.kopf.ttl} s -- eilig UND kurzlebig, beides zugleich`);
}
/* =====================================================================
Gegenprobe: der alte Name darf nicht still durchrutschen
===================================================================== */
console.log("\n=== Der alte Schalter ist weg -- und zwar laut ===");
{
const { schicken, schluesselErzeugen } = await import("./workspace-push-krypto.js");
let krachte = false;
try {
await schicken({ endpunkt: `http://127.0.0.1:${PORT_DIENST}/alt`,
p256dh: b64u(browser.getPublicKey()), auth: b64u(Buffer.alloc(16, 7)) },
"{}", schluesselErzeugen(), "mailto:[email protected]",
{ dringend: true });
} catch { krachte = true; }
ok(krachte,
"schicken({ dringend: true }) bricht ab -- sonst gaebe es klaglos \"normal\" zurueck");
}
/* ---- SAUBER ZUMACHEN, SONST IST GRUEN TROTZDEM ROT ------------------
`fetch` haelt die Verbindungen zum nachgebauten Dienst offen
(keep-alive). Wer dann `process.exit()` ruft, reisst libuv ein
Handle mitten im Schliessen weg, und Windows antwortet mit
Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)
-- Rueckgabewert 127 NACH "13 Pruefungen, 0 Fehler". Genau das ist
am 05.09. schon einmal passiert. Eine Pruefung, die inhaltlich
besteht und trotzdem rot meldet, ist das Schlimmste von beidem:
Man gewoehnt sich an, das Rot zu uebersehen. Also erst die
Verbindungen kappen, dann zumachen, dann einen Takt warten. */
dienst.closeAllConnections?.();
await new Promise((r) => dienst.close(r));
await new Promise((r) => setTimeout(r, 250));
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(`\n${geprueft} Pruefungen, ${fehler} Fehler`);
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
process.exit(fehler ? 1 : 0);
+19 -3
View File
@@ -223,7 +223,21 @@ export function vapidKopf(endpunkt, paar, absender, stunden = 12) {
* soll NICHT nachtraeglich aufploppen. Deshalb bekommt er eine kurze * soll NICHT nachtraeglich aufploppen. Deshalb bekommt er eine kurze
* TTL -- ist er vorbei, verfaellt auch die Meldung. */ * TTL -- ist er vorbei, verfaellt auch die Meldung. */
export async function schicken(anmeldung, text, paar, absender, export async function schicken(anmeldung, text, paar, absender,
{ ttl = 3600, dringend = false } = {}) { { ttl = 3600, eilig = false, kurzlebig = false,
...rest } = {}) {
/* DER ALTE NAME MUSS KRACHEN, NICHT DURCHRUTSCHEN (03.10.2026).
Bis heute hiess beides zusammen `dringend`. Wer den alten Namen
weitergibt, bekaeme sonst klaglos `Urgency: normal` -- also genau
den Fehler zurueck, der gerade behoben wurde, und niemand saehe
es. Ein unbekannter Schluessel ist hier deshalb ein Absturz. */
const unbekannt = Object.keys(rest);
if (unbekannt.length) {
throw new Error(
`schicken(): unbekannte Angabe ${unbekannt.join(", ")} -- ` +
`aus "dringend" wurden "eilig" (weckt das Geraet) und ` +
`"kurzlebig" (verfaellt nach 150 s).`);
}
try { try {
const koerper = verschluesseln(text, anmeldung.p256dh, anmeldung.auth); const koerper = verschluesseln(text, anmeldung.p256dh, anmeldung.auth);
const kopf = vapidKopf(anmeldung.endpunkt, paar, absender); const kopf = vapidKopf(anmeldung.endpunkt, paar, absender);
@@ -232,8 +246,10 @@ export async function schicken(anmeldung, text, paar, absender,
method: "POST", method: "POST",
headers: { headers: {
...kopf, ...kopf,
TTL: String(dringend ? Math.min(ttl, 150) : ttl), /* Haltbarkeit und Dringlichkeit sind zwei Fragen -- siehe
Urgency: dringend ? "high" : "normal", `eiligRegel` in workspace-push.js. */
TTL: String(kurzlebig ? Math.min(ttl, 150) : ttl),
Urgency: eilig ? "high" : "normal",
}, },
body: koerper, body: koerper,
/* Ein haengender Push-Dienst darf den Lauf nicht aufhalten. */ /* Ein haengender Push-Dienst darf den Lauf nicht aufhalten. */
+69 -19
View File
@@ -113,19 +113,19 @@ const jetzt = () => new Date().toISOString();
export const ARTEN = [ export const ARTEN = [
{ schluessel: "aufgabe_faellig", name: "Aufgabe wird morgen fällig", vorgabe: true }, { schluessel: "aufgabe_faellig", name: "Aufgabe wird morgen fällig", vorgabe: true },
{ schluessel: "aufgabe_ueberfaellig", name: "Aufgabe ist überfällig", vorgabe: true }, { schluessel: "aufgabe_ueberfaellig", name: "Aufgabe ist überfällig", vorgabe: true },
{ schluessel: "termin_gleich", name: "Termin in einer Stunde", vorgabe: true }, { schluessel: "termin_gleich", eilig: true, name: "Termin in einer Stunde", vorgabe: true },
/* DER WECKER (07.09.2026). Bewusst eine EIGENE Art neben /* DER WECKER (07.09.2026). Bewusst eine EIGENE Art neben
"termin_gleich": Die eine ist eine Vorgabe des Hauses fuer jeden "termin_gleich": Die eine ist eine Vorgabe des Hauses fuer jeden
Termin, der andere ist eine Entscheidung DIESER Person fuer DIESEN Termin, der andere ist eine Entscheidung DIESER Person fuer DIESEN
Termin. Wer die feste Stunden-Erinnerung abschaltet, will damit Termin. Wer die feste Stunden-Erinnerung abschaltet, will damit
nicht seine selbst gesetzten Wecker verlieren. */ nicht seine selbst gesetzten Wecker verlieren. */
{ schluessel: "termin_wecker", name: "Selbst gesetzter Wecker", vorgabe: true }, { schluessel: "termin_wecker", eilig: true, name: "Selbst gesetzter Wecker", vorgabe: true },
{ schluessel: "protokoll_fehlt", name: "Gespräch ohne Protokoll", vorgabe: true }, { schluessel: "protokoll_fehlt", name: "Gespräch ohne Protokoll", vorgabe: true },
{ schluessel: "followup", name: "Follow-up in der Pipeline fällig", 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, /* Chat (06.09.2026). Steht bewusst VOR der Probe: Es ist die Art,
die am haeufigsten kommt, und wer die Liste ueberfliegt, sucht die am haeufigsten kommt, und wer die Liste ueberfliegt, sucht
zuerst sie. */ zuerst sie. */
{ schluessel: "chat_nachricht", name: "Neue Nachricht im Chat", vorgabe: true }, { schluessel: "chat_nachricht", eilig: true, name: "Neue Nachricht im Chat", vorgabe: true },
/* ---- ANGESPROCHEN WERDEN IST EINE EIGENE ART (20.09.2026) ------ /* ---- ANGESPROCHEN WERDEN IST EINE EIGENE ART (20.09.2026) ------
Filipe: "ich will das man die leute mit @ markieren kann im chat." Filipe: "ich will das man die leute mit @ markieren kann im chat."
@@ -144,7 +144,7 @@ export const ARTEN = [
KEINE Ausnahme von der Ruhezeit: Ein Anruf wartet auf eine KEINE Ausnahme von der Ruhezeit: Ein Anruf wartet auf eine
Antwort, ein "@Anna" nicht. Wer um halb vier angesprochen wird, Antwort, ein "@Anna" nicht. Wer um halb vier angesprochen wird,
liest es um sieben -- das reicht. */ liest es um sieben -- das reicht. */
{ schluessel: "chat_erwaehnung", name: "Jemand erwähnt dich im Chat (@)", vorgabe: true }, { schluessel: "chat_erwaehnung", eilig: true, name: "Jemand erwähnt dich im Chat (@)", vorgabe: true },
/* ---- BEWERBUNGEN (23.09.2026) ----------------------------------- /* ---- BEWERBUNGEN (23.09.2026) -----------------------------------
@@ -189,7 +189,7 @@ export const ARTEN = [
NICHT DRINGEND: Ein kaputter Knopf weckt niemanden um drei Uhr NICHT DRINGEND: Ein kaputter Knopf weckt niemanden um drei Uhr
nachts; die Ruhezeit gilt. */ nachts; die Ruhezeit gilt. */
{ schluessel: "support", { schluessel: "support", eilig: true,
name: "Support: neue Meldung oder Antwort darauf", vorgabe: true }, name: "Support: neue Meldung oder Antwort darauf", vorgabe: true },
/* ---- MANAGER-ZIELE (02.10.2026) --------------------------------- /* ---- MANAGER-ZIELE (02.10.2026) ---------------------------------
@@ -224,7 +224,7 @@ export const ARTEN = [
Eine eigene Art und nicht "termin_gleich": Das eine ist ein Eine eigene Art und nicht "termin_gleich": Das eine ist ein
Kalendereintrag, das andere ein Stream, der JETZT laeuft. Wer das Kalendereintrag, das andere ein Stream, der JETZT laeuft. Wer das
eine stumm stellt, meint selten das andere. */ eine stumm stellt, meint selten das andere. */
{ schluessel: "dogfather_live", name: "DogFather ist live", vorgabe: true, { schluessel: "dogfather_live", eilig: true, name: "DogFather ist live", vorgabe: true,
/* „spaet", nicht „immer": Ein Stream, der um 22:05 anfaengt, ist um /* „spaet", nicht „immer": Ein Stream, der um 22:05 anfaengt, ist um
22:05 interessant. Mit der alten Regel waere er stumm geblieben, 22:05 interessant. Mit der alten Regel waere er stumm geblieben,
und die Meldung, die Filipe ausdruecklich bestellt hat („die und die Meldung, die Filipe ausdruecklich bestellt hat („die
@@ -257,7 +257,7 @@ export const ARTEN = [
`ruhe: "spaet"` aus demselben Grund wie oben: Eine Reaction um `ruhe: "spaet"` aus demselben Grund wie oben: Eine Reaction um
22:30 ist der Normalfall, keine Ausnahme. */ 22:30 ist der Normalfall, keine Ausnahme. */
{ schluessel: "reaktion_live", { schluessel: "reaktion_live", eilig: true,
name: "Die Reaction geht auf Sendung", vorgabe: true, ruhe: "spaet" }, name: "Die Reaction geht auf Sendung", vorgabe: true, ruhe: "spaet" },
/* ---- ANRUFE SIND EINE EIGENE ART (18.09.2026) ------------------ /* ---- ANRUFE SIND EINE EIGENE ART (18.09.2026) ------------------
@@ -283,7 +283,7 @@ export const ARTEN = [
ist kein Telefon. Wer nachts nicht gestoert werden will, schaltet ist kein Telefon. Wer nachts nicht gestoert werden will, schaltet
jetzt diese Art ab -- eine Entscheidung, die man selbst trifft, jetzt diese Art ab -- eine Entscheidung, die man selbst trifft,
statt einer Regel, die man nicht kennt. */ statt einer Regel, die man nicht kennt. */
{ schluessel: "anruf", name: "Jemand ruft an", vorgabe: true, ruhe: "nie" }, { schluessel: "anruf", eilig: true, name: "Jemand ruft an", vorgabe: true, ruhe: "nie" },
/* DER TAGESRUF (08.09.2026, screen10). Filipe: "eine taetige meldung /* DER TAGESRUF (08.09.2026, screen10). Filipe: "eine taetige meldung
einmal am tag zu aktivieren wenn noch aufgaben auf sind." einmal am tag zu aktivieren wenn noch aufgaben auf sind."
@@ -323,10 +323,10 @@ export const ARTEN = [
IM TEXT STEHT NIE, WORUM ES GEHT. Eine Benachrichtigung erscheint IM TEXT STEHT NIE, WORUM ES GEHT. Eine Benachrichtigung erscheint
auf einem Sperrbildschirm, den auch andere sehen. */ auf einem Sperrbildschirm, den auch andere sehen. */
{ schluessel: "hilfe", name: "Vertrauliche Meldung: neu oder beantwortet", vorgabe: true }, { schluessel: "hilfe", eilig: true, name: "Vertrauliche Meldung: neu oder beantwortet", vorgabe: true },
/* Die Probe beantwortet „kommt ueberhaupt etwas an?" -- und die /* Die Probe beantwortet „kommt ueberhaupt etwas an?" -- und die
stellt man sich auch um 23 Uhr. */ stellt man sich auch um 23 Uhr. */
{ schluessel: "test", name: "Probe-Benachrichtigung", vorgabe: true, ruhe: "nie" }, { schluessel: "test", eilig: true, name: "Probe-Benachrichtigung", vorgabe: true, ruhe: "nie" },
]; ];
const ARTEN_SCHLUESSEL = new Set(ARTEN.map((a) => a.schluessel)); const ARTEN_SCHLUESSEL = new Set(ARTEN.map((a) => a.schluessel));
@@ -382,6 +382,49 @@ export function ruheRegel(art) {
return ARTEN.find((a) => a.schluessel === art)?.ruhe || "immer"; return ARTEN.find((a) => a.schluessel === art)?.ruhe || "immer";
} }
/** Ist diese Art EILIG -- also wertlos, wenn sie zu spaet kommt?
*
* WARUM DAS EINE EIGENE FRAGE IST (03.10.2026, nach Dienes Meldung).
*
* Diene hat gemeldet: "Die Benachrichtigungen werden nicht angezeigt,
* wenn neue Nachrichten reinkommen. Erst, wenn man die App oeffnet."
* Das war kein Fehler an ihrem Geraet und keiner am Service Worker --
* es war der Kopf `Urgency`, den wir mitschicken.
*
* Beide Push-Dienste behandeln "normal" ausdruecklich als "darf
* warten": Android/FCM haelt solche Meldungen zurueck, solange das
* Telefon doest, und stellt sie zu, wenn es aufwacht -- typischerweise,
* wenn jemand das Geraet entsperrt oder die App oeffnet. Apple/APNs
* nennt es "verzoegert, gebuendelt oder gedrosselt". Beschrieben ist
* damit genau Dienes Satz, Wort fuer Wort.
*
* Bis hierher hing die Dringlichkeit an der RUHEREGEL:
*
* const dringend = regel === "nie";
*
* Daneben stand die Begruendung, eine zweite Bedingung sei die, "die
* beim naechsten Nachschaerfen auseinanderlaeuft". Die Vorhersage war
* richtig, nur in die andere Richtung: Die zwei Fragen haben eben
* NICHT dieselbe Antwort.
*
* "Darf das nachts stoeren?" -> Chat: nein.
* "Darf das warten?" -> Chat: auch nein.
*
* Eine Nachricht um drei Uhr nachts soll schweigen -- um zwei Uhr
* nachmittags aber nicht vierzig Minuten liegen bleiben. Deshalb zwei
* Felder, aber in DERSELBEN Liste: auseinanderlaufen koennen sie nur,
* wenn sie an zwei Orten stehen.
*
* MASSSTAB: eilig ist, was durch Warten WERTLOS wird -- ein Anruf, ein
* Wecker, eine Nachricht, auf die jemand wartet, ein "wir sind live".
* Nicht eilig ist, was auch spaeter noch stimmt -- eine Aufgabe, die
* morgen faellig ist, der Tagesruf, eine Bewerbung. Waere alles eilig,
* waere nichts mehr eilig: Die Geraete duerfen uns die Dringlichkeit
* glauben, sonst drosseln sie uns -- und der Akku zahlt es mit. */
export function eiligRegel(art) {
return ARTEN.find((a) => a.schluessel === art)?.eilig === true;
}
/** Ist gerade Ruhezeit -- fuer diese Regel? /** Ist gerade Ruhezeit -- fuer diese Regel?
* *
* `regel` steht hinten und hat eine Vorgabe, damit vorhandene * `regel` steht hinten und hat eine Vorgabe, damit vorhandene
@@ -598,7 +641,21 @@ export async function benachrichtige(personId, art, { titel, text, ziel, merkmal
schlafendes Geraet dafuer aufwacht. Das gilt fuer den Anruf und schlafendes Geraet dafuer aufwacht. Das gilt fuer den Anruf und
die Probe -- nicht fuer eine Einladung: Die soll ankommen, wenn die Probe -- nicht fuer eine Einladung: Die soll ankommen, wenn
das Geraet ohnehin wach wird, und keinen Akku dafuer verbrennen. */ das Geraet ohnehin wach wird, und keinen Akku dafuer verbrennen. */
const dringend = regel === "nie"; const eilig = eiligRegel(art);
/* KURZLEBIG IST NICHT DASSELBE WIE EILIG -- und das Zusammenlegen
waere hier der teure Fehler gewesen (03.10.2026).
Bis heute steuerte EIN Schalter beides: `dringend` hat das Geraet
geweckt UND die Haltbarkeit auf 150 Sekunden gesetzt. Haette ich
den Chat einfach auf `dringend` gestellt, waere aus "kommt vierzig
Minuten zu spaet" ein "kommt gar nicht mehr" geworden: Wer sein
Telefon drei Minuten aus hat, haette die Nachricht dann verloren.
Schlimmer als der Fehler, den ich beheben wollte.
Kurz haltbar bleibt deshalb genau das, was von selbst vorbei ist
-- der Anruf, der 120 Sekunden klingelt, und die Probe, bei der
jemand davorsteht. Das ist unveraendert `ruhe: "nie"`. */
const kurzlebig = regel === "nie";
/* Schon einmal geschickt? */ /* Schon einmal geschickt? */
if (merkmal) { if (merkmal) {
@@ -633,14 +690,7 @@ export async function benachrichtige(personId, art, { titel, text, ziel, merkmal
let raus = 0; let raus = 0;
for (const a of anmeldungen) { for (const a of anmeldungen) {
/* EIN ANRUF IST DRINGEND, ALLES ANDERE NICHT (19.09.2026). const e = await schicken(a, inhalt, paar, ABSENDER, { eilig, kurzlebig });
`dringend` ist hier dasselbe `art === "anruf"`, das zwanzig
Zeilen weiter oben schon ueber die Ruhezeit entscheidet -- die
Frage „darf das nachts stoeren?" und „darf das warten?" haben
dieselbe Antwort. Eine zweite Bedingung waere die, die beim
naechsten Nachschaerfen auseinanderlaeuft. */
const e = await schicken(a, inhalt, paar, ABSENDER, { dringend });
if (e.ok) { if (e.ok) {
raus++; raus++;
db().prepare("UPDATE push_anmeldungen SET zuletzt_ok = ?, fehler = 0 WHERE endpunkt = ?") db().prepare("UPDATE push_anmeldungen SET zuletzt_ok = ?, fehler = 0 WHERE endpunkt = ?")