/* ===================================================================== WERDEN DIE MONATSZIEL-ERINNERUNGEN WIRKLICH ZUGESTELLT? (02.10.2026) ===================================================================== `pruef-manager-ziele` prueft, was `zielRufe()` ZURUECKGIBT -- eine Liste. Eine Liste ist aber keine Benachrichtigung. Zwischen ihr und dem Handy liegen noch: der Fuenf-Minuten-Takt, die Artenliste, der Schalter in der Glocke, die Ruhezeit, die Verschluesselung und das Merkmal gegen Doppelsendungen. Dieser Weg ist genauso nie gelaufen wie der Monatswechsel -- und er laeuft zum ersten Mal am 1. November um 00:05 Uhr, von selbst, fuer alle gleichzeitig. --------------------------------------------------------------------- DIE WICHTIGSTE FRAGE STEHT GLEICH AM ANFANG Am 1. um 00:05 ist RUHEZEIT (Vorgabe 22 bis 7). Die Meldung darf dann nicht herausgehen -- aber sie darf auch nicht VERFALLEN. Das haengt an einer einzigen Zeile in `benachrichtige`: Das Merkmal wird erst geschrieben, NACHDEM wirklich zugestellt wurde. Waere es umgekehrt, bekaeme am 1. November niemand eine Meldung, und zwar fuer immer -- der Takt haette sie als „schon geschickt" abgehakt, waehrend alle schliefen. --------------------------------------------------------------------- GEMESSEN WERDEN ZUSTAENDE, NICHT ZEITPUNKTE Der Server startet seinen eigenen Takt (alle fuenf Minuten, der erste Lauf nach zwanzig Sekunden). Er kann also jederzeit mitten in diese Pruefung hineinlaufen. Deshalb steht hier nirgends „nach meinem Aufruf kamen genau drei dazu", sondern „in push_verschickt steht jetzt genau eine Zeile je Person" -- das stimmt auch dann, wenn der Takt dieselbe Arbeit vorher gemacht hat. Dieselbe Falle wie am 06.09.2026 in RunOne, als zwei Prueflaeufe gleichzeitig liefen und sechzehn Fehler meldeten, von denen keiner aus dem Code kam. ===================================================================== */ /* ---------- Die Uhr ---------------------------------------------- */ const ECHTE_ZEIT = Date; let versatz = 0; globalThis.Date = class extends ECHTE_ZEIT { constructor(...a) { if (a.length === 0) super(ECHTE_ZEIT.now() + versatz); else super(...a); } static now() { return ECHTE_ZEIT.now() + versatz; } }; function uhrAuf(iso) { versatz = 0; versatz = new ECHTE_ZEIT(iso).getTime() - ECHTE_ZEIT.now(); } import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createServer } from "node:http"; import { eigenerPort } from "./helfer-port.mjs"; const PORT = await eigenerPort(import.meta, "pruef-ziel-zustellung"); const DIENST = await eigenerPort(import.meta, "pruef-ziel-zustellung (Push-Dienst)", 1); const ordner = mkdtempSync(join(tmpdir(), "ws-zz-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; /* DIE RUHEZEIT BLEIBT AUF DER VORGABE (22 bis 7). Sie wird hier nicht per Umgebungsvariable verstellt, sondern ueber die Uhr erreicht -- denn genau die Uhrzeit ist der Fall, der am 1. November eintritt. Eine gefaelschte Ruhezeit um neun Uhr morgens wuerde eine andere Frage beantworten als die, die sich stellt. */ /* 1. NOVEMBER, 00:05 -- fuenf Minuten nach dem Monatswechsel, mitten in der Ruhezeit. Genau so wird es wirklich sein. */ uhrAuf("2026-11-01T00:05:00+01:00"); 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 }); }; import { notbremse } from "./helfer-notbremse.mjs"; await import("./index.js"); notbremse(240_000, "pruef-ziel-zustellung"); await new Promise((r) => setTimeout(r, 800)); let fehler = 0, geprueft = 0; const melde = (t) => console.log(t); const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; function braucht(bedingung, was) { if (bedingung) return; console.log(`\nABBRUCH: konnte nicht nachsehen — ${was}\n`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(2); } const { DatabaseSync } = await import("node:sqlite"); const { randomBytes, createECDH } = await import("node:crypto"); const push = await import("./workspace-push.js"); /* ---------- Ein Push-Dienst, der alles annimmt -------------------- */ let zustellungen = 0; const dienst = createServer((q, a) => { q.on("data", () => {}); q.on("end", () => { zustellungen++; a.writeHead(201); a.end(); }); }); await new Promise((r) => dienst.listen(DIENST, "127.0.0.1", r)); /* ---------- Leute mit Geraeten ------------------------------------ */ const ALT = "2025-10-01T08:00:00Z"; const d = new DatabaseSync(process.env.WORKSPACE_DB); const anlegen = (name, rolle) => { d.prepare(`INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,32768,1,?)`) .run(name, rolle, randomBytes(32).toString("hex"), randomBytes(16).toString("hex"), ALT); return d.prepare("SELECT last_insert_rowid() AS id").get().id; }; const idOffen1 = anlegen("Offen Eins", "scout"); const idOffen2 = anlegen("Offen Zwei", "manager"); const idStumm = anlegen("Stumm", "scout"); const idOhneGeraet = anlegen("Ohne Geraet", "scout"); const idCreator = anlegen("Luna", "creator"); /* Echte Schluessel -- ein erfundener wuerde beim Verschluesseln scheitern, und „nicht zugestellt" waere dann die Antwort auf eine ganz andere Frage. */ function geraet(personId) { const k = createECDH("prime256v1"); k.generateKeys(); d.prepare(`INSERT INTO push_anmeldungen (person_id, endpunkt, p256dh, auth, erstellt) VALUES (?,?,?,?,?)`) .run(personId, `http://127.0.0.1:${DIENST}/push/${personId}`, k.getPublicKey().toString("base64url"), randomBytes(16).toString("base64url"), new Date().toISOString()); } for (const id of [idOffen1, idOffen2, idStumm, idCreator]) geraet(id); /* „Stumm" hat die Art abgeschaltet -- das ist der Schalter aus der Glocke, und er muss staerker sein als jede Erinnerung. */ d.prepare(`INSERT INTO push_einstellungen (person_id, art, an) VALUES (?,?,0)`) .run(idStumm, "manager_ziele"); d.close(); const zaehle = (sql, ...w) => { const db2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); const n = db2.prepare(sql).get(...w)?.n ?? 0; db2.close(); return n; }; const mzMerkmale = () => zaehle( "SELECT COUNT(*) n FROM push_verschickt WHERE merkmal LIKE 'mz:%'"); /* ===================================================================== 0 · MISST DIESE PRUEFUNG UEBERHAUPT ETWAS? ===================================================================== „Null zugestellt" ist gleich die erste erwartete Antwort. Ohne diesen Beweis hiesse sie moeglicherweise „der Dienst ist gar nicht angeschlossen" -- und die ganze Datei waere wertlos. Die Art „test" traegt `ruhe: "nie"` und geht deshalb auch nachts durch. ===================================================================== */ melde("\n=== Zuerst: kann hier ueberhaupt etwas ankommen? ==="); const probe = await push.benachrichtige(idOffen1, "test", { titel: "Probe", text: "nur zum Messen", ziel: "/workspace/start.html" }); ok(probe.verschickt === 1, `eine Probemeldung kommt an (${probe.verschickt}) — der Dienst ist angeschlossen`); ok(zustellungen >= 1, `und der Zaehler bewegt sich (${zustellungen})`); /* ===================================================================== 1 · 1. NOVEMBER, 00:05 UHR -- RUHEZEIT ===================================================================== */ melde("\n=== 1. November, 00:05 Uhr — mitten in der Ruhezeit ==="); const vorher = zustellungen; await push.laufen(); ok(zustellungen === vorher, `nachts geht nichts raus (${zustellungen - vorher} zugestellt)`); ok(mzMerkmale() === 0, `UND das Merkmal wurde NICHT gesetzt (${mzMerkmale()})` + " — sonst waere die Meldung fuer immer verfallen, waehrend alle schliefen"); /* ===================================================================== 2 · DERSELBE TAG, 09:00 UHR ===================================================================== */ melde("\n=== Derselbe 1. November, 09:00 Uhr ==="); uhrAuf("2026-11-01T09:00:00+01:00"); await push.laufen(); const db3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); const zeilen = db3.prepare( "SELECT person_id, merkmal FROM push_verschickt WHERE merkmal LIKE 'mz:%'").all(); db3.close(); const wer = new Set(zeilen.map((z) => z.person_id)); ok(zeilen.length === 2, `genau zwei Meldungen sind heraus (${zeilen.length})`); ok(wer.has(idOffen1) && wer.has(idOffen2), "…an die beiden mit offenen Zielen und einem Geraet"); ok(!wer.has(idStumm), "…NICHT an den, der die Art abgeschaltet hat — der Schalter schlaegt die Erinnerung"); ok(!wer.has(idCreator), "…und nicht an einen Creator, der diese Pflichten gar nicht hat"); ok(!wer.has(idOhneGeraet), "…und nicht an den ohne Geraet (es gibt niemanden, der sie annimmt)"); ok(zeilen.every((z) => z.merkmal === "mz:2026-11-01"), `das Merkmal traegt den Tag (${zeilen[0]?.merkmal})`); /* ===================================================================== 3 · UND JETZT LAEUFT DER TAKT 287 WEITERE MALE ===================================================================== Das ist der Fall, der im Betrieb wirklich eintritt: Der Lauf findet alle fuenf Minuten statt. Ohne das Merkmal bekaeme jeder 288 Meldungen an einem Tag -- das waere kein Hinweis mehr, sondern ein Grund, das Handy wegzulegen. ===================================================================== */ melde("\n=== Der Takt laeuft weiter ==="); const nachErstem = zustellungen; await push.laufen(); await push.laufen(); await push.laufen(); ok(zustellungen === nachErstem, `drei weitere Laeufe stellen nichts mehr zu (${zustellungen - nachErstem})`); ok(mzMerkmale() === 2, `und es bleibt bei zwei Merkmalen (${mzMerkmale()})`); /* ===================================================================== 4 · DER 10. -- EIN NEUER TAG, EINE NEUE MELDUNG ===================================================================== */ melde("\n=== Der 10. November ==="); uhrAuf("2026-11-10T09:00:00+01:00"); const vor10 = zustellungen; await push.laufen(); ok(zustellungen - vor10 === 2, `an einem neuen Anlasstag geht wieder etwas raus (${zustellungen - vor10})`); ok(zaehle("SELECT COUNT(*) n FROM push_verschickt WHERE merkmal = ?", "mz:2026-11-10") === 2, "…unter einem eigenen Merkmal fuer diesen Tag"); /* GEGENPROBE: Der 11. ist KEIN Anlasstag. Ginge auch dort etwas heraus, hiesse „es kommt an" in Wahrheit „es kommt jeden Tag", und der ganze Terminplan der Vorlage waere wirkungslos. */ melde("\n=== Der 11. November — kein Anlass ==="); uhrAuf("2026-11-11T09:00:00+01:00"); const vor11 = zustellungen; await push.laufen(); ok(zustellungen === vor11, `am 11. ist Ruhe (${zustellungen - vor11} zugestellt)`); /* ===================================================================== 5 · WER ALLES GESCHAFFT HAT ===================================================================== Vorlage: „Glueckwunsch-Meldung, danach keine Erinnerungen mehr in diesem Monat." ===================================================================== */ melde("\n=== Wer alles geschafft hat ==="); const dbF = new DatabaseSync(process.env.WORKSPACE_DB); dbF.prepare("UPDATE mz_lage SET monat = '2026-11' WHERE eins = 1").run(); /* Alle Ziele dieser Rolle auf 0 -- damit ist „alles erfuellt" wahr, ohne dass Eintraege erfunden werden muessten. Null Ziele heisst aber auch: nichts zu gratulieren. Deshalb bekommt Offen Zwei EIN gefordertes Ziel, das er erfuellt. */ for (const a of ["creator", "meeting", "schulung", "werbung"]) { dbF.prepare(`INSERT INTO mz_ziel (monat, rolle, aufgabe, ziel, gesetzt_am) VALUES ('2026-11','manager',?,?,?) ON CONFLICT(monat, rolle, aufgabe) DO UPDATE SET ziel = excluded.ziel`) .run(a, a === "meeting" ? 1 : 0, new Date().toISOString()); } dbF.prepare(`INSERT INTO mz_eintrag (person_id, von_name, von_rolle, aufgabe, monat, datum, erstellt) VALUES (?,?,?,?,?,?,?)`) .run(idOffen2, "Offen Zwei", "manager", "meeting", "2026-11", "2026-11-05", new Date().toISOString()); dbF.close(); uhrAuf("2026-11-15T09:00:00+01:00"); const vor15 = zustellungen; await push.laufen(); const glueck = zaehle("SELECT COUNT(*) n FROM push_verschickt WHERE merkmal = ?", "mz:2026-11:fertig"); ok(glueck === 1, `der Fertige bekommt genau einen Glueckwunsch (${glueck})`); ok(zustellungen - vor15 === 2, `insgesamt zwei Meldungen an diesem Tag (${zustellungen - vor15})` + " — ein Glueckwunsch und eine Mahnung"); /* UND DANACH IST RUHE FUER IHN. Der 16. ist ein Anlasstag; der Fertige darf dort nichts mehr bekommen, der andere schon. */ melde("\n=== Der 16. — Mahnung ja, Glueckwunsch nein ==="); uhrAuf("2026-11-16T09:00:00+01:00"); const vor16 = zustellungen; await push.laufen(); const am16 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); const empfaenger16 = am16.prepare( "SELECT person_id FROM push_verschickt WHERE merkmal = ?").all("mz:2026-11-16"); am16.close(); ok(zustellungen - vor16 === 1, `nur noch einer bekommt etwas (${zustellungen - vor16})`); ok(empfaenger16.length === 1 && empfaenger16[0].person_id === idOffen1, "…und zwar der, bei dem noch etwas offen ist"); ok(zaehle("SELECT COUNT(*) n FROM push_verschickt WHERE merkmal = ?", "mz:2026-11:fertig") === 1, "der Glueckwunsch bleibt bei einem — er wird nicht wiederholt"); /* ===================================================================== SCHLUSS ===================================================================== */ versatz = 0; dienst.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } melde(`\n${geprueft} Pruefungen, ${fehler} Fehler`); process.exit(fehler ? 1 : 0);