Files
dogfather-universe/server/pruef-modi-livecheck.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

242 lines
11 KiB
JavaScript

/* =====================================================================
DIE LIVE-CHECKLISTE AM TERMIN (Kapitel 5.7 und 11)
"Wird pro Live-Termin als Checkliste erzeugt und den eingeteilten
Modis zugewiesen."
Filipes Entscheidung dazu (10.09.2026): ein KNOPF statt Automatik.
Vierzehn Aufgaben, die bei jedem Termin von selbst erscheinen,
ueberrumpeln -- und was ungefragt Dinge anlegt, ist schwer wieder
loszuwerden.
DREI STELLEN, AN DENEN SO ETWAS ERFAHRUNGSGEMAESS SCHIEFGEHT:
1. ZWEIMAL DRUECKEN. Der zweite Druck darf nichts verdoppeln --
vierzehn Punkte doppelt auf einem Live-Tag sind unbrauchbar.
2. DAS ZWEITE LIVE. Die Kennung der Vorlage entscheidet, was als
"schon uebernommen" gilt. Traegt sie die Terminnummer nicht,
steht beim zweiten Live alles als erledigt da -- und niemand
bekommt seine Liste.
3. DIE FRIST. Eine Vorbereitung, die nach dem Live faellig wird,
ist keine.
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { tagLokal } from "./helfer-tag.mjs";
const ordner = mkdtempSync(join(tmpdir(), "ws-live-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4367";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
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-modi-livecheck");
await new Promise((r) => setTimeout(r, 800));
const BASIS = "http://127.0.0.1:4367";
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
let offen = 0;
const unklar = (t) => { offen++; console.log(" -- " + t); };
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
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);
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, 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 (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idMan = anlegen("Schulle", "manager", "CODE-MAN-0001");
const idModi = anlegen("Marina", "modi", "CODE-MODI-0001");
d.close();
void idMan;
/* ZWEI TERMINE, damit Punkt 2 oben ueberhaupt pruefbar ist. Der Tag
liegt in der Zukunft und wird RELATIV gerechnet -- ein festes Datum
holt der Kalender irgendwann ein, und dann ist die Pruefung rot,
ohne dass etwas kaputt waere. */
const inTagen = (n) => tagLokal(n);
const TAG1 = inTagen(3);
const TAG2 = inTagen(10);
async function anmelden(rolle, code) {
const a = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle, code }),
});
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
return { status: a.status, keks };
}
async function hole(keks, pfad, aufruf = {}) {
const a = await fetch(BASIS + pfad, {
...aufruf, headers: { ...(aufruf.headers || {}), cookie: keks },
});
return { status: a.status, daten: await a.json().catch(() => null) };
}
const jsonKopf = { "Content-Type": "application/json" };
const dogiAn = await anmelden("admin", "CODE-DOGI-0001");
const modiAn = await anmelden("creator", "CODE-MODI-0001");
const manAn = await anmelden("manager", "CODE-MAN-0001");
/* ---------- Zwei Live-Termine mit dem Modi als Teilnehmer ----------- */
console.log("\n=== 1. Zwei Live-Termine ===\n");
const termine = [];
for (const [tag, titel] of [[TAG1, "Freitags-Live"], [TAG2, "Turnier-Abend"]]) {
const a = await hole(dogiAn.keks, "/workspace/api/termine", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({
titel, art: "bigmatch", beginn: `${tag}T20:00`, dauer_min: 120,
teilnehmer: [idModi],
}),
});
if (a.status === 201 || a.status === 200) termine.push(a.daten?.id);
else unklar(`Termin "${titel}" ging nicht (${a.status}: ${JSON.stringify(a.daten)})`);
}
ok(termine.length === 2 && termine.every(Boolean),
`zwei Termine angelegt (${termine.join(", ")})`);
/* ---------- 2. Der Knopf ist angeboten ------------------------------ */
console.log("\n=== 2. Die Auskunft am Termin ===\n");
{
const a = await hole(modiAn.keks, `/workspace/api/termine?von=${TAG1}&tage=30`);
ok(a.daten?.checkliste_moeglich === true, "dem Modi wird der Knopf angeboten");
const b = await hole(manAn.keks, `/workspace/api/termine?von=${TAG1}&tage=30`);
/* Und dem Manager nicht -- sonst stuende bei ihm ein Knopf, der eine
Absage bringt, und die Absage waere die Auskunft, dass es das gibt. */
ok(b.daten?.checkliste_moeglich === false, "dem Manager nicht");
}
/* ---------- 3. Anlegen ---------------------------------------------- */
console.log("\n=== 3. Die Checkliste zum ersten Live ===\n");
{
const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "katalog_termin", termin_id: termine[0] }),
});
/* VIERZEHN: fuenf vorher, fuenf waehrend, vier nachher -- so steht es
in Kapitel 11. Die Zahl fest hingeschrieben: Faellt ein Punkt aus
dem Katalog, muss das auffallen. */
ok(a.status === 200 && a.daten?.angelegt === 14,
`vierzehn Punkte angelegt (${a.daten?.angelegt})`);
const b = await hole(modiAn.keks, "/workspace/api/aufgaben");
const meine = (b.daten?.aufgaben || []).filter((x) => (x.vorlage || "").endsWith(`#${termine[0]}`));
ok(meine.length === 14, `sie liegen auf seinem Brett (${meine.length})`);
ok(meine.every((x) => x.frist === TAG1),
`alle mit dem Termintag als Frist (${[...new Set(meine.map((x) => x.frist))].join(", ")})`);
ok(meine.every((x) => x.verantwortlich_id === idModi),
"und der eingeteilte Modi ist verantwortlich");
ok(meine.every((x) => x.kategorie), "jede traegt ihre Kategorie");
}
/* ---------- 4. Zweimal druecken -------------------------------------- */
console.log("\n=== 4. Und wenn man zweimal drückt? ===\n");
{
const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "katalog_termin", termin_id: termine[0] }),
});
ok(a.status === 200 && a.daten?.angelegt === 0,
`der zweite Druck legt nichts an (${a.daten?.angelegt})`);
const b = await hole(modiAn.keks, "/workspace/api/aufgaben");
const meine = (b.daten?.aufgaben || []).filter((x) => (x.vorlage || "").endsWith(`#${termine[0]}`));
ok(meine.length === 14, `es sind immer noch vierzehn (${meine.length})`);
}
/* ---------- 5. Das ZWEITE Live --------------------------------------- */
console.log("\n=== 5. Das zweite Live bekommt seine eigene Liste ===\n");
{
/* HIER WAERE DER FEHLER TEUER: Traegt die Kennung der Vorlage die
Terminnummer nicht, gilt beim zweiten Live alles als "schon
uebernommen" -- und niemand bekaeme seine Liste. */
const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "katalog_termin", termin_id: termine[1] }),
});
ok(a.status === 200 && a.daten?.angelegt === 14,
`auch das zweite Live bekommt vierzehn (${a.daten?.angelegt})`);
const b = await hole(modiAn.keks, "/workspace/api/aufgaben");
const zwei = (b.daten?.aufgaben || []).filter((x) => (x.vorlage || "").endsWith(`#${termine[1]}`));
ok(zwei.length === 14 && zwei.every((x) => x.frist === TAG2),
`mit dem eigenen Termintag (${[...new Set(zwei.map((x) => x.frist))].join(", ")})`);
}
/* ---------- 6. Die Zahl am Termin ------------------------------------ */
console.log("\n=== 6. Der Knopf weiß, was schon da ist ===\n");
{
const a = await hole(modiAn.keks, `/workspace/api/termine?von=${TAG1}&tage=30`);
const t1 = (a.daten?.termine || []).find((t) => t.id === termine[0]);
ok(t1?.checkliste_aufgaben === 14,
`am ersten Termin stehen ${t1?.checkliste_aufgaben} Punkte`);
}
/* ---------- 7. Wer es nicht darf ------------------------------------- */
console.log("\n=== 7. Ein Manager versucht es ===\n");
{
const a = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "katalog_termin", termin_id: termine[0] }),
});
const b = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "gibtesnicht" }),
});
ok(a.status === b.status && JSON.stringify(a.daten) === JSON.stringify(b.daten),
`wortgleich mit einer erfundenen Art (${a.status} ${JSON.stringify(a.daten)})`);
}
/* ---------- 8. Gegenprobe -------------------------------------------- */
console.log("\n=== 8. Gegenprobe ===\n");
{
/* Ein Termin, den es nicht gibt. Kaeme dafuer ein 200, wuerden die
Haken oben nichts beweisen -- dann sagt der Server zu allem ja. */
const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "katalog_termin", termin_id: 999999 }),
});
ok(a.status === 404, `ein erfundener Termin wird abgelehnt (${a.status})`);
/* Und ein Termin OHNE eingeteilten Modi: Vierzehn herrenlose
Aufgaben waeren schlimmer als keine. */
const t = await hole(dogiAn.keks, "/workspace/api/termine", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ titel: "Ohne Team", art: "termin", beginn: `${TAG1}T10:00` }),
});
if (!t.daten?.id) {
unklar("Termin ohne Teilnehmer liess sich nicht anlegen");
} else {
const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "katalog_termin", termin_id: t.daten.id }),
});
ok(b.status === 400 && /eingeteilt/i.test(b.daten?.fehler || ""),
`ohne eingeteilten Modi wird gefragt (${b.status} ${b.daten?.fehler || ""})`);
}
}
console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- `
+ `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);