`pruef-manager-ziele` prueft, was `zielRufe()` ZURUECKGIBT. Eine Liste
ist aber keine Benachrichtigung. Dazwischen liegen noch: der
Fuenf-Minuten-Takt, die Artenliste, der Schalter in der Glocke, die
Ruhezeit, die Verschluesselung und das Merkmal gegen Doppelsendungen.
Dieser Weg war 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 STAND 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: nachts null zugestellt UND null Merkmale, um
09:00 dann zwei. Die Zeile haelt.
GEPRUEFT WIRD DIE ECHTE UHRZEIT, NICHT EINE GEFAELSCHTE RUHEZEIT. Das
Haus kann die Ruhezeit per Umgebungsvariable verstellen; das waere
hier die falsche Frage gewesen. Gefragt ist, was am 1. November um
fuenf nach null passiert -- also wird die Uhr dorthin gestellt.
WAS SONST NOCH BEWIESEN IST (20 Pruefungen, 0 Fehler)
* Der Takt laeuft 288-mal am Tag. Drei weitere Laeufe direkt
hintereinander stellen NICHTS mehr zu -- ohne das Merkmal
bekaeme jeder 288 Meldungen und legte das Handy weg.
* Der Schalter in der Glocke schlaegt die Erinnerung: Wer die Art
abgeschaltet hat, bekommt nichts, obwohl bei ihm etwas offen ist.
* Ein Creator bekommt nichts (er hat diese Pflichten nicht), und
wer kein Geraet angemeldet hat, erzeugt keine Geisterzustellung.
* Am 11. ist Ruhe. Ohne diese Gegenprobe hiesse „es kommt an"
moeglicherweise „es kommt jeden Tag", und der ganze Terminplan
der Vorlage waere wirkungslos.
* Der Glueckwunsch kommt genau einmal, und danach ist fuer den
Fertigen Ruhe -- waehrend der andere am 16. weiter gemahnt wird.
GEMESSEN WERDEN ZUSTAENDE, NICHT ZEITPUNKTE. Der Server startet seinen
eigenen Takt und kann jederzeit in die Pruefung hineinlaufen. Deshalb
steht nirgends „nach meinem Aufruf kamen genau drei dazu", sondern „in
push_verschickt steht jetzt genau eine Zeile je Person".
UND DIE ERSTE PRUEFUNG DER DATEI PRUEFT DIE PRUEFUNG. „Null
zugestellt" ist gleich die erste erwartete Antwort; ohne den Beweis,
dass ueberhaupt etwas ankommen KANN, hiesse sie moeglicherweise „der
Dienst ist gar nicht angeschlossen".
NUR DIESE EINE DATEI IST DRIN. Der erste Anlauf dieses Commits hat
mit `git add -A` drei Dateien mitgenommen, die ich nie angefasst
habe -- sie wurden waehrenddessen von einer anderen Sitzung im selben
Verzeichnis bearbeitet (Zeitstempel: Sekunden alt). Zurueckgenommen,
bevor etwas gepusht wurde; ihre Arbeit liegt unveraendert im
Arbeitsverzeichnis.
Co-Authored-By: Claude Opus 5 <[email protected]>
310 lines
14 KiB
JavaScript
310 lines
14 KiB
JavaScript
/* =====================================================================
|
|
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);
|