diff --git a/server/pruef-push-eilig.mjs b/server/pruef-push-eilig.mjs index d6272945..28780f93 100644 --- a/server/pruef-push-eilig.mjs +++ b/server/pruef-push-eilig.mjs @@ -244,6 +244,54 @@ const kopfVon = async (art) => { `anruf verfaellt nach ${anruf.kopf.ttl} s -- eilig UND kurzlebig, beides zugleich`); } +/* ===================================================================== + Das Sendeprotokoll -- damit "ich bekomme nichts" beantwortbar wird + ===================================================================== */ +console.log("\n=== Was hinausging, steht hinterher da ==="); +{ + const p = new DatabaseSync(process.env.WORKSPACE_DB); + const zaehle = (wo = "1=1", ...w) => + p.prepare(`SELECT COUNT(*) AS n FROM push_versand WHERE ${wo}`).get(...w).n; + + /* Drei Meldungen sind oben wirklich hinausgegangen. */ + const geschickt = zaehle("grund = 'ok'"); + ok(geschickt >= 3, `${geschickt} zugestellte Meldungen stehen im Protokoll (mindestens 3)`); + + const mitGeraet = zaehle("grund = 'ok' AND geraete >= 1 AND zugestellt >= 1"); + ok(mitGeraet === geschickt, + `bei allen ${mitGeraet} steht, an wie viele Geraete (nicht nur DASS)`); + + const arten = p.prepare( + "SELECT DISTINCT art FROM push_versand ORDER BY art").all().map((r) => r.art); + ok(arten.includes("chat_nachricht") && arten.includes("anruf"), + `die Art steht dabei: ${arten.join(", ")}`); + + /* DER EIGENTLICHE ZWECK: auch das NICHT-Senden muss dastehen. + "Abgeschaltet", "Ruhezeit", "kein Geraet" sind die haeufigsten + Antworten auf "ich bekomme nichts" -- ein Protokoll, das nur + Erfolge kennt, kann die Frage nicht beantworten, fuer die es + angelegt wurde. */ + const vorher = zaehle(); + const nichts = await push.benachrichtige(personId, "gibtsnicht", { + titel: "x", text: "x", ziel: "/workspace/start.html", + }); + const nachher = zaehle(); + ok(nichts.verschickt === 0 && nichts.grund === "unbekannte_art", + `es ging nichts hinaus (${nichts.grund})`); + ok(nachher === vorher + 1, + `und genau DAS steht jetzt auch da (${vorher} -> ${nachher})`); + ok(zaehle("grund = 'unbekannte_art'") === 1, + "mit dem Grund, nicht nur als leere Zeile"); + + /* Gegenprobe: Koennte diese Messung ueberhaupt "nein" sagen? Eine + Art, die es nicht gibt, darf nicht als zugestellt gelten -- sonst + waere jede Zeile oben wertlos. */ + ok(zaehle("grund = 'ok' AND art = 'gibtsnicht'") === 0, + "Gegenprobe: der Fehlschlag steht NICHT als zugestellt drin"); + + p.close(); +} + /* ===================================================================== Gegenprobe: der alte Name darf nicht still durchrutschen ===================================================================== */ diff --git a/server/workspace-push.js b/server/workspace-push.js index 573ca6fe..b154e6a9 100644 --- a/server/workspace-push.js +++ b/server/workspace-push.js @@ -614,7 +614,48 @@ export function hausFuerMeldung(personId, ziel) { } } -export async function benachrichtige(personId, art, { titel, text, ziel, merkmal, zahl }) { +/** Verschickt eine Meldung -- und schreibt auf, was dabei herauskam. + * + * WARUM DAS PROTOKOLL AUSSEN SITZT (03.10.2026). + * + * `versenden` hat sieben Ausgaenge: unbekannte_art, abgeschaltet, + * ruhezeit, schon_geschickt, keine_geraete, nicht_zugestellt, ok. + * Haette jeder davon seine eigene Protokollzeile, waere das eine + * Liste zum Pflegen -- und der achte Ausgang, den jemand naechstes + * Jahr einbaut, wuerde sie stillschweigend umgehen. Genau so sind + * die Spalten beim Tabellenumbau am 11.09. verlorengegangen. + * + * Hier wird deshalb nichts aufgezaehlt: Was `versenden` auch immer + * zurueckgibt, wird festgehalten. Ein neuer Ausgang steht ohne + * Zutun mit drin. + * + * UND WARUM UEBERHAUPT: Als Diene meldete, sie bekomme nichts, + * konnte niemand nachsehen, ob je etwas hinausging. Die Frage + * "warum kommt es nicht an?" setzt voraus, dass "wurde es + * abgeschickt?" schon beantwortet ist -- im Shop hat das Ueberspringen + * dieser Reihenfolge am 06.09. zwei Stunden gekostet. + * + * Das Schreiben darf den Versand nie aufhalten: Eine Meldung, die + * ankommt, ist wichtiger als die Notiz darueber. Deshalb steht es + * in einem try -- aber mit Ausgabe, denn ein Protokoll, das + * heimlich nichts schreibt, ist schlimmer als keins. */ +export async function benachrichtige(personId, art, sache) { + const e = await versenden(personId, art, sache); + try { + db().prepare( + "INSERT INTO push_versand (zeit, person_id, art, grund, geraete, zugestellt) " + + "VALUES (?,?,?,?,?,?)") + .run(jetzt(), personId, art, e.grund || "unbekannt", + e.geraete || 0, e.verschickt || 0); + } catch (fehler) { + console.error("[push] Versand nicht protokolliert:", fehler?.message || fehler); + } + return e; +} + +/* Der eigentliche Versand. Umhuellt von `benachrichtige` -- siehe + dort, warum das Protokoll aussen sitzt und nicht hier drin. */ +async function versenden(personId, art, { titel, text, ziel, merkmal, zahl }) { if (!ARTEN_SCHLUESSEL.has(art)) return { verschickt: 0, grund: "unbekannte_art" }; if (!willHaben(personId, art)) return { verschickt: 0, grund: "abgeschaltet" }; @@ -715,7 +756,7 @@ export async function benachrichtige(personId, art, { titel, text, ziel, merkmal Nachricht aufmachen zu muessen -- derselbe Gedanke wie beim Ziel. */ return { verschickt: raus, grund: raus ? "ok" : "nicht_zugestellt", - ziel: zielEcht, haus }; + geraete: anmeldungen.length, ziel: zielEcht, haus }; } /* --------------------------------------------------------------------- @@ -1057,6 +1098,21 @@ export async function laufen() { mehr, und die Tabelle soll nicht ewig wachsen. */ const grenze = new Date(Date.now() - 60 * 86400_000).toISOString(); d.prepare("DELETE FROM push_verschickt WHERE zeit < ?").run(grenze); + + /* Das Sendeprotokoll ebenso -- 30 Tage. + + Die Frist ist kuerzer als oben, weil die beiden Tabellen + verschiedene Fragen beantworten: push_verschickt verhindert + Doppelmeldungen und muss deshalb so lange halten wie die Sache + selbst. Das Protokoll beantwortet "kam letzte Woche etwas an?" + -- wer danach noch fragt, fragt nach etwas anderem. + + Gemessen statt geschaetzt: rund 16 Chat-Nachrichten am Tag an + bis zu acht angemeldete Geraete, also hoechstens ein paar + tausend Zeilen im Monat. Die Tabelle bleibt klein genug, dass + niemand sie je bemerkt. */ + d.prepare("DELETE FROM push_versand WHERE zeit < ?") + .run(new Date(Date.now() - 30 * 86400_000).toISOString()); } catch (fehler) { console.error("[push] Lauf:", fehler?.message); } diff --git a/server/workspace.js b/server/workspace.js index 5a5ab76c..fb9d809f 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -5216,6 +5216,46 @@ export function db() { ); CREATE INDEX IF NOT EXISTS idx_push_verschickt_zeit ON push_verschickt (zeit); + /* WAS HINAUSGING -- das Sendeprotokoll (03.10.2026). + + ACHTUNG: In diesem Block duerfen keine schraegen Anfuehrungs- + striche stehen, er liegt in einem Template-Literal. Zweimal + hat genau das den Serverstart zerlegt. + + WARUM ES DAS BRAUCHT. Diene meldete "ich bekomme nichts", und + niemand konnte nachsehen, OB etwas hinausging. Es gab nur + push_angemeldet und push_abgemeldet im Protokoll -- kein + Wort darueber, ob je eine Meldung verschickt wurde, an wie + viele Geraete, und was der Dienst geantwortet hat. + + Das ist dieselbe Falle wie am 06.09. in VanVans Shop: Zwei + Stunden Suche nach dem Grund, warum eine Mail nicht ankommt, + bevor jemand fragte, ob sie ueberhaupt abgeschickt wurde. Sie + war es, alle, nachweisbar. Eine einzige Abfrage haette am + Anfang stehen muessen statt am Ende. + + zuletzt_ok in push_anmeldungen reicht dafuer NICHT: Es sagt + nur, wann zuletzt IRGENDETWAS ankam, nicht was, nicht an wen + sonst, und vor allem nichts ueber die Faelle, in denen gar + nicht erst gesendet wurde -- abgeschaltet, Ruhezeit, kein + Geraet. Genau die sind die haeufigste Antwort auf "ich + bekomme nichts". + + Deshalb wird JEDER Ausgang festgehalten, auch der, bei dem + nichts hinausging. Ein Protokoll, das nur Erfolge kennt, kann + die Frage nicht beantworten, fuer die es angelegt wurde. */ + CREATE TABLE IF NOT EXISTS push_versand ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + zeit TEXT NOT NULL, + person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, + art TEXT NOT NULL, + grund TEXT NOT NULL, + geraete INTEGER NOT NULL DEFAULT 0, + zugestellt INTEGER NOT NULL DEFAULT 0 + ); + CREATE INDEX IF NOT EXISTS idx_push_versand_zeit ON push_versand (zeit); + CREATE INDEX IF NOT EXISTS idx_push_versand_person ON push_versand (person_id, zeit); + /* Rueckmeldungen zu einer Aufgabe (Konzept: "Aufgaben & Feedback"). Ohne sie endet jede Rueckfrage ausserhalb des Systems -- in WhatsApp, und damit ausserhalb dessen, was spaeter noch