Files
dogfather-universe/server/pruef-eskalation.mjs
T
DogFatherGitandClaude Opus 5 12d4d48975 pruef-struktur: von zehn Fehlern auf null -- vier davon waren keine
Die dritte und letzte rote Pruefung. Sie meldete zehn Fehler, und
sechs davon waren Fehlalarm.

1. SECHS SEITEN "VON NIRGENDWO VERLINKT"

Gemeldet wurden crew-index, entwicklung, rechte, talente, teamlage und
treff-moderation. Keine davon war verwaist -- man kommt auf jede, indem
man eine KACHEL antippt. Die Kacheln stehen im Server (`ziel:
"talente.html"`), und der wurde nie durchsucht. Der blinde Fleck lag in
der Pruefung.

Abgeleitet statt nachgetragen: Statt die sechs von Hand auszunehmen,
kommen workspace.js und crew-adresse.js als Quellen dazu. Wer morgen
eine Kachel anlegt, ist damit automatisch abgedeckt.

2. FALSCHE ZEILENNUMMERN

Die Ortszeit-Pruefung schnitt Blockkommentare heraus und ersetzte sie
durch EIN Leerzeichen -- ab da zaehlte split("\n") falsch. Gemeldet
wurde "pruef-eskalation.mjs:43", dort steht eine Zeile ueber Kekse. Wer
dem nachgeht, findet nichts, haelt die Pruefung fuer kaputt und sieht
beim naechsten Mal nicht mehr nach.

Mit richtigen Zeilen waren sechs der sieben Funde echt: Sie bauen ihr
Tagesdatum aus toISOString(), also aus UTC. Zwischen Mitternacht und
zwei Uhr liefert das den Vortag -- die Pruefungen waeren tagsueber
gruen und nachts rot gewesen.

Der siebte (pruef-eskalation.mjs:70) rechnet von einem FESTEN Mittag
aus und ist damit sicher; das Muster kann es nur nicht unterscheiden.
Auch umgestellt, damit es nicht beim naechsten Lesen wieder auffaellt.

Neu: server/helfer-tag.mjs -- heuteLokal, tagLokal, tagVon. Nicht aus
workspace.js importiert, weil eine Pruefung, die nur ein Datum
braucht, dafuer keinen Server hochfahren soll. pruef-backstage-import
hatte die Rechnung sogar schon richtig stehen -- und benutzte sie 1270
Zeilen weiter unten trotzdem nicht.

3. TOTES CSS

`.rolle__zeichen .nase` war tot. Beim Nachsehen: `.auge` und `.hell`
daneben auch -- sie wurden nur davon verdeckt, dass die Pruefung den
Klassennamen als TEILZEICHENKETTE gegen den Quelltext haelt, und
"auge" steckt in "Auge", "hell" in "hell". Dutzende Treffer im
Fliesstext deutscher Kommentare. `.fuell` bleibt, die wird benutzt.
Die Schwaeche der Suche steht jetzt an der Stelle notiert.

4. start.css MIT 347 KB

Die Grenze soll verhindern, "dass eine Seite unnoetig viel laedt". Auf
der echten Seite nachgemessen:

  start.css        auf der Platte 346 KB   uebertragen 110 KB (br)
  chat.css                         59 KB                16 KB
  entwicklung.css                  33 KB                 8 KB

Caddy packt unterwegs. Von start.css sind ausserdem 227 KB KOMMENTAR
(64 %) -- die Pruefung bestrafte genau das, was dieses Haus absichtlich
tut, und haette eine Datei mit 199 KB dichtem CSS durchgewunken.
Dasselbe Argument steht drei Absaetze hoeher schon fuer gestufte
Bilder.

Jetzt zwei Zahlen, weil es zwei Fragen sind: was der BESUCHER laedt
(brotli Stufe 4 -- bei 4 liefert node 111 KB, Caddy 110, jede andere
Stufe waere eine erfundene Zahl) und was ein MENSCH pflegen muss (ohne
Kommentare). Sonst koennte man die erste Zahl klein halten, indem man
immer mehr Prosa schreibt.

MIT GEGENPROBE, in drei Faellen: 281 KB dichtes CSS faellt durch,
404 KB Kommentar gehen durch, und 246 KB sich WIEDERHOLENDES CSS
faellt ebenfalls durch -- sonst koennte man die erste Grenze mit
Wiederholung unterlaufen.

  pruef-struktur          0 Fehler (vorher 10)
  pruef-eskalation 40, pruef-modi-livecheck 16, pruef-treff-werkzeuge 70,
  pruef-backstage-import 160, pruef-css-klassen, pruef-crew-adresse 132
  -- alle 0 Fehler

Damit sind alle drei roten Pruefungen gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-16 18:03:50 +02:00

309 lines
14 KiB
JavaScript

/* =====================================================================
Wie lange geht das schon so? (15.09.2026)
Stufe 9: „Eskalationsstufe sichtbar“.
---------------------------------------------------------------------
GEPRÜFT WIRD AN DEN GRENZEN, NICHT IN DER MITTE
Eine Stufenfunktion ist überall dort richtig oder falsch, wo sie
umspringt — dazwischen ist sie langweilig. Jede Schwelle wird deshalb
dreimal gemessen: einen Tag davor, genau darauf, einen Tag danach.
Die wichtigste Grenze ist die erste: **am Tag der Frist ist nichts
überfällig.** Wer bis zum 15. Zeit hat, ist am 15. pünktlich. Eine
Eskalation, die einen Tag zu früh anschlägt, verliert genau das
Vertrauen, das sie braucht.
---------------------------------------------------------------------
UND ZWEI DINGE, DIE NICHT ESKALIEREN DÜRFEN
Erledigtes und Abgebrochenes — es liegt niemandem mehr im Weg.
Eine Aufgabe mit Frist in der Zukunft — sie ist eingeplant, nicht
still.
Aufrufen mit: node server/pruef-eskalation.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { portMussFreiSein } from "./helfer-port.mjs";
import { heuteLokal, tagLokal, tagVon } from "./helfer-tag.mjs";
const PORT = await portMussFreiSein(4417, "pruef-eskalation");
const ordner = mkdtempSync(join(tmpdir(), "ws-eskal-"));
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";
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(180_000, "pruef-eskalation");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
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++; };
const HAUS = "workspace.dogfather-universe.com";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const { eskalation, STUFEN, STILL_TAGE } = await import("./workspace-eskalation.js");
const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
/* EIN FESTER ZEITPUNKT, keine Wanderuhr. Eine Pruefung, die "heute"
als Annahme benutzt, misst irgendwann das Gegenteil -- der Kalender
holt sie ein. Hier wird die Zeit uebergeben, also ist sie Eingabe. */
const NUN = Date.parse("2026-09-15T12:00:00+02:00");
const tagVor = (n) => tagVon(NUN, -n);
let idDogi;
{
const d = oeffne();
const schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-DOGI-0001", salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES ('Filipe','admin',?,?,?,?,1,?)")
.run(hash, salt, 32768, createHmac("sha256", schluessel).update("CODE-DOGI-0001").digest("hex"), jetzt);
idDogi = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.close();
}
function ruf(pfad, methode, kopf = {}, koerper = null) {
return new Promise((fertig, schief) => {
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: { Host: HAUS, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf } },
(antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
const anm = await ruf("/workspace/api/anmelden", "POST", {},
{ rolle: "admin", code: "CODE-DOGI-0001" });
const kDogi = [].concat(anm.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
ok(!!kDogi, "DogFather ist angemeldet");
/** Kurzform: eine gedachte Aufgabe einstufen. */
const stufeVon = (a) => eskalation(a, NUN)?.stufe ?? null;
const tageVon = (a) => eskalation(a, NUN)?.tage ?? null;
/* =======================================================================
1. DIE ERSTE GRENZE: AM TAG DER FRIST IST NICHTS ÜBERFÄLLIG
======================================================================= */
melde("");
melde("=== 1. Der Tag der Frist ===");
{
const offen = (frist) => ({ status: "offen", frist });
ok(stufeVon(offen(tagVor(-1))) === null, "eine Frist von morgen eskaliert nicht");
ok(stufeVon(offen(tagVor(0))) === null,
"am Tag der Frist selbst auch nicht — wer bis zum 15. Zeit hat, ist am 15. pünktlich");
ok(stufeVon(offen(tagVor(1))) === "faellig",
`einen Tag danach schon (${stufeVon(offen(tagVor(1)))})`);
ok(tageVon(offen(tagVor(1))) === 1, `und zwar seit ${tageVon(offen(tagVor(1)))} Tag`);
}
/* =======================================================================
2. JEDE SCHWELLE DREIMAL: DAVOR, DARAUF, DANACH
======================================================================= */
melde("");
melde("=== 2. An den Grenzen ===");
{
const bei = (tage) => eskalation({ status: "offen", frist: tagVor(tage) }, NUN);
ok(STUFEN.length === 3, `${STUFEN.length} Stufen im Katalog`);
for (const s of STUFEN) {
const davor = bei(s.ab - 1);
const drauf = bei(s.ab);
const danach = bei(s.ab + 1);
ok(drauf?.stufe === s.schluessel,
`bei ${s.ab} Tagen gilt „${s.schluessel}“ (${drauf?.stufe})`);
ok(danach?.rang >= drauf.rang,
`einen Tag danach nicht milder (${danach?.stufe})`);
/* Die Stufe DAVOR muss eine andere sein -- sonst waere die Schwelle
wirkungslos, und die Zeile darueber trotzdem gruen. */
ok(davor === null || davor.stufe !== s.schluessel,
`und einen Tag davor noch nicht (${davor?.stufe ?? "gar nichts"})`);
}
/* Die Zahl im Wort muss zur Zahl im Feld passen -- sonst liest jemand
"liegt seit 5 Tagen" an einer Karte, die seit 12 liegt. */
const zwoelf = bei(12);
ok(zwoelf.wort.includes(String(zwoelf.tage)),
`das Wort nennt dieselbe Zahl wie das Feld (${zwoelf.wort} / ${zwoelf.tage})`);
ok(zwoelf.stufe === "haengt", `und zwölf Tage sind „${zwoelf.stufe}“`);
}
/* =======================================================================
3. WAS NICHT ESKALIEREN DARF
======================================================================= */
melde("");
melde("=== 3. Was ruhig bleibt ===");
{
ok(stufeVon({ status: "erledigt", frist: tagVor(60) }) === null,
"eine erledigte Aufgabe eskaliert nicht, auch nach 60 Tagen nicht");
ok(stufeVon({ status: "abgebrochen", frist: tagVor(60) }) === null,
"eine abgebrochene ebenso wenig");
ok(stufeVon({ status: "arbeit", frist: tagVor(-30) }) === null,
"und eine mit Frist in der Zukunft ist eingeplant, nicht still");
/* GEGENPROBE -- ohne sie beweisen die drei Zeilen nichts: Dieselbe
Frist bei offenem Status eskaliert sehr wohl. */
ok(stufeVon({ status: "offen", frist: tagVor(60) }) === "haengt",
"dieselbe alte Frist bei einer offenen Aufgabe dagegen schon");
}
/* =======================================================================
4. OHNE FRIST: EINE BEOBACHTUNG, KEIN GEBROCHENES VERSPRECHEN
======================================================================= */
melde("");
melde("=== 4. Ohne Frist ===");
{
const still = (tage) => ({
status: "offen", frist: null,
geaendert: new Date(NUN - tage * 86400000).toISOString(),
});
ok(stufeVon(still(STILL_TAGE - 1)) === null,
`${STILL_TAGE - 1} Tage ohne Bewegung sind noch nichts`);
ok(stufeVon(still(STILL_TAGE)) === "still",
`ab ${STILL_TAGE} Tagen steht es da (${stufeVon(still(STILL_TAGE))})`);
ok(eskalation(still(20), NUN)?.grund === "keine_bewegung",
"und der Grund ist ein anderer als bei einer Frist");
ok(/unberührt/.test(eskalation(still(20), NUN)?.wort || ""),
`das Wort wirft niemandem etwas vor: „${eskalation(still(20), NUN)?.wort}“`);
/* Ohne `geaendert` zaehlt das Anlegedatum -- sonst waere eine nie
angefasste Aufgabe fuer immer unsichtbar. */
const nie = { status: "offen", frist: null, geaendert: null,
erstellt: new Date(NUN - 30 * 86400000).toISOString() };
ok(stufeVon(nie) === "still", "eine nie angefasste Aufgabe zählt ab dem Anlegen");
/* GEGENPROBE: Eine Aufgabe MIT Frist wird nicht zusaetzlich als
"still" gemeldet -- zwei Marken an einer Karte waeren Laerm. */
const beides = { status: "offen", frist: tagVor(-5),
geaendert: new Date(NUN - 40 * 86400000).toISOString() };
ok(stufeVon(beides) === null,
"eine eingeplante Aufgabe wird nicht zusätzlich als still gemeldet");
}
/* =======================================================================
5. DIE STUFE KOMMT MIT DER LISTE — GERECHNET, NICHT IM BROWSER
======================================================================= */
melde("");
melde("=== 5. Über die Leitung ===");
{
const d = oeffne();
const rein = d.prepare(`INSERT INTO aufgaben (titel, status, prioritaet, frist, erstellt, erstellt_von)
VALUES (?,?,'mittel',?,?,?)`);
const heute = heuteLokal();
const vor = (n) => tagLokal(-n);
rein.run("Seit gestern faellig", "offen", vor(1), jetzt, idDogi);
rein.run("Liegt seit fuenf Tagen", "offen", vor(5), jetzt, idDogi);
rein.run("Haengt seit zwanzig", "offen", vor(20), jetzt, idDogi);
rein.run("Erst naechste Woche", "offen", tagLokal(7), jetzt, idDogi);
rein.run("Heute faellig", "offen", heute, jetzt, idDogi);
d.close();
const liste = json(await ruf("/workspace/api/aufgaben", "GET", { cookie: kDogi })).aufgaben || [];
ok(liste.length >= 5, `${liste.length} Aufgaben in der Liste`);
const nach = (t) => liste.find((a) => a.titel === t);
ok(nach("Seit gestern faellig")?.eskalation?.stufe === "faellig",
`„seit gestern“ → ${nach("Seit gestern faellig")?.eskalation?.stufe}`);
ok(nach("Liegt seit fuenf Tagen")?.eskalation?.stufe === "liegt",
`„seit fünf Tagen“ → ${nach("Liegt seit fuenf Tagen")?.eskalation?.stufe}`);
ok(nach("Haengt seit zwanzig")?.eskalation?.stufe === "haengt",
`„seit zwanzig“ → ${nach("Haengt seit zwanzig")?.eskalation?.stufe}`);
ok(nach("Erst naechste Woche")?.eskalation === null,
"„erst nächste Woche“ trägt keine Stufe");
ok(nach("Heute faellig")?.eskalation === null,
"und „heute fällig“ ebenfalls nicht — heute ist nicht zu spät");
/* Jede Stufe bringt ihr WORT mit. Rechnete der Browser es selbst,
waere das die zweite Wahrheit. */
ok(/\d/.test(nach("Haengt seit zwanzig")?.eskalation?.wort || ""),
`das Wort kommt fertig mit: „${nach("Haengt seit zwanzig")?.eskalation?.wort}“`);
}
/* =======================================================================
6. IM BROWSER
======================================================================= */
melde("");
melde("=== 6. Im Browser ===");
{
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
const kontext = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); });
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" });
await seite.click('.rolle[data-rolle="admin"]');
await seite.fill("#code", "CODE-DOGI-0001");
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 20000 });
await seite.goto(`http://127.0.0.1:${PORT}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const stand = await seite.evaluate(() => {
const m = [...document.querySelectorAll(".karte__eskal")];
return {
anzahl: m.length,
stufen: [...new Set(m.map((x) => x.dataset.stufe))].sort(),
texte: m.map((x) => x.textContent.trim()),
/* Steht das Wort "ueberfaellig" noch zweimal an derselben Karte? */
doppelt: [...document.querySelectorAll(".karte")]
.filter((k) => (k.innerText.match(/überfällig/gi) || []).length > 1).length,
};
});
ok(stand.anzahl >= 3, `${stand.anzahl} Karten tragen eine Stufe`);
ok(stand.stufen.length >= 3,
`und es sind verschiedene: ${stand.stufen.join(", ")}`);
ok(stand.texte.some((t) => /\d+ Tagen/.test(t)),
`die Zahl steht im Text: ${stand.texte.join(" | ").slice(0, 90)}`);
/* DIE FARBE IST NICHT DIE AUSKUNFT. Wer Farben schlecht sieht, muss
dasselbe lesen koennen -- also steht in jedem Kaestchen ein Wort. */
ok(stand.texte.every((t) => t.length > 3), "jede Stufe steht als Wort da, nicht nur als Farbe");
ok(stand.doppelt === 0,
`und „überfällig“ steht an keiner Karte zweimal (${stand.doppelt})`);
ok(konsole.length === 0,
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser");
await browser.close();
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);