From 73f9b247939b5dfae176bb12154817169b051689 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sat, 3 Oct 2026 12:58:01 +0200 Subject: [PATCH] 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 --- server/pruef-anruf-klingelt.mjs | 64 ++++++-- server/pruef-push-eilig.mjs | 282 ++++++++++++++++++++++++++++++++ server/workspace-push-krypto.js | 22 ++- server/workspace-push.js | 88 +++++++--- 4 files changed, 420 insertions(+), 36 deletions(-) create mode 100644 server/pruef-push-eilig.mjs diff --git a/server/pruef-anruf-klingelt.mjs b/server/pruef-anruf-klingelt.mjs index 8e129e6e..9a362f2c 100644 --- a/server/pruef-anruf-klingelt.mjs +++ b/server/pruef-anruf-klingelt.mjs @@ -63,6 +63,24 @@ import { fileURLToPath } from "node:url"; const REPO = join(dirname(fileURLToPath(import.meta.url)), ".."); 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; const pruef = (name, bedingung, zusatz = "") => { 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 ===================================================================== */ -const krypto = lies("server/workspace-push-krypto.js"); -const versand = lies("server/workspace-push.js"); +const krypto = nurCode(lies("server/workspace-push-krypto.js")); +const versand = nurCode(lies("server/workspace-push.js")); 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\""); -pruef("ein dringender Push geht mit Urgency: high hinaus", - /Urgency:\s*dringend\s*\?\s*"high"\s*:\s*"normal"/.test(krypto)); +pruef("ein eiliger Push geht mit Urgency: high hinaus", + /Urgency:\s*eilig\s*\?\s*"high"\s*:\s*"normal"/.test(krypto)); 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"); -pruef("der Anruf wird als dringend verschickt", - /schicken\(a,\s*inhalt,\s*paar,\s*ABSENDER,\s*\{\s*dringend\s*\}\)/.test(versand)); +pruef("der Anruf wird eilig UND kurzlebig verschickt", + /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?" - und „darf das warten?" haben dieselbe Antwort -- zwei getrennte - Bedingungen wären die, die beim nächsten Nachschärfen auseinander - laufen. */ -pruef("und zwar an derselben Bedingung wie die Nachtruhe", - /const dringend = art === "test" \|\| art === "anruf";/.test(versand)); +/* ---- ZWEI FRAGEN, ZWEI FELDER (geändert 03.10.2026) ----------------- + + Hier stand bis heute das Gegenteil: „Darf das nachts stören?" und + „darf das warten?" hätten dieselbe Antwort, zwei getrennte + Bedingungen wären die, die auseinanderlaufen. Die Vorhersage war + 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? diff --git a/server/pruef-push-eilig.mjs b/server/pruef-push-eilig.mjs new file mode 100644 index 00000000..d6272945 --- /dev/null +++ b/server/pruef-push-eilig.mjs @@ -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:test@dogfather-universe.com", + { 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); diff --git a/server/workspace-push-krypto.js b/server/workspace-push-krypto.js index dc99b4fe..15a1d00d 100644 --- a/server/workspace-push-krypto.js +++ b/server/workspace-push-krypto.js @@ -223,7 +223,21 @@ export function vapidKopf(endpunkt, paar, absender, stunden = 12) { * soll NICHT nachtraeglich aufploppen. Deshalb bekommt er eine kurze * TTL -- ist er vorbei, verfaellt auch die Meldung. */ 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 { const koerper = verschluesseln(text, anmeldung.p256dh, anmeldung.auth); const kopf = vapidKopf(anmeldung.endpunkt, paar, absender); @@ -232,8 +246,10 @@ export async function schicken(anmeldung, text, paar, absender, method: "POST", headers: { ...kopf, - TTL: String(dringend ? Math.min(ttl, 150) : ttl), - Urgency: dringend ? "high" : "normal", + /* Haltbarkeit und Dringlichkeit sind zwei Fragen -- siehe + `eiligRegel` in workspace-push.js. */ + TTL: String(kurzlebig ? Math.min(ttl, 150) : ttl), + Urgency: eilig ? "high" : "normal", }, body: koerper, /* Ein haengender Push-Dienst darf den Lauf nicht aufhalten. */ diff --git a/server/workspace-push.js b/server/workspace-push.js index 9493ae8f..573ca6fe 100644 --- a/server/workspace-push.js +++ b/server/workspace-push.js @@ -113,19 +113,19 @@ const jetzt = () => new Date().toISOString(); 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 }, + { schluessel: "termin_gleich", eilig: true, 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: "termin_wecker", eilig: true, 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, die am haeufigsten kommt, und wer die Liste ueberfliegt, sucht 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) ------ 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 Antwort, ein "@Anna" nicht. Wer um halb vier angesprochen wird, 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) ----------------------------------- @@ -189,7 +189,7 @@ export const ARTEN = [ NICHT DRINGEND: Ein kaputter Knopf weckt niemanden um drei Uhr nachts; die Ruhezeit gilt. */ - { schluessel: "support", + { schluessel: "support", eilig: true, name: "Support: neue Meldung oder Antwort darauf", vorgabe: true }, /* ---- MANAGER-ZIELE (02.10.2026) --------------------------------- @@ -224,7 +224,7 @@ export const ARTEN = [ Eine eigene Art und nicht "termin_gleich": Das eine ist ein Kalendereintrag, das andere ein Stream, der JETZT laeuft. Wer das 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 22:05 interessant. Mit der alten Regel waere er stumm geblieben, 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 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" }, /* ---- 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 jetzt diese Art ab -- eine Entscheidung, die man selbst trifft, 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 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 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 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)); @@ -382,6 +382,49 @@ export function ruheRegel(art) { 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? * * `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 die Probe -- nicht fuer eine Einladung: Die soll ankommen, wenn 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? */ if (merkmal) { @@ -633,14 +690,7 @@ export async function benachrichtige(personId, art, { titel, text, ziel, merkmal let raus = 0; for (const a of anmeldungen) { - /* EIN ANRUF IST DRINGEND, ALLES ANDERE NICHT (19.09.2026). - - `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 }); + const e = await schicken(a, inhalt, paar, ABSENDER, { eilig, kurzlebig }); if (e.ok) { raus++; db().prepare("UPDATE push_anmeldungen SET zuletzt_ok = ?, fehler = 0 WHERE endpunkt = ?")