Files
dogfather-universe/server/pruef-modi-livecheck.mjs
T
DogFatherGitandClaude Opus 5 790b77a73d Bei dir klingelt nichts -- und 49 Pruefungen, die blind waren
1. SIEBEN VON ELF HATTEN KEIN GERAET ANGEMELDET.

Gemessen am echten Server, nicht geschaetzt (die Notiz von heute Nacht
sagte acht -- Tamy hat inzwischen eins). Bei ihnen kommt nichts an,
solange die Seite zu ist: keine Aufgabe, kein Termin, kein Anruf.

Die Glocke sagt das seit jeher korrekt und kennt sogar den
iPhone-Sonderfall ("erst zum Home-Bildschirm"). Nur tippt sie niemand
an -- sie ist ein stiller Schalter in einer Leiste.

Jetzt steht es dort, wo man hinsieht: als Hinweis in "Was ist dran?",
mit Weg auf anruf-probe.html. Als OFFENER PUNKT, nicht als Warnung --
eine Warnung, die bei sieben von elf Leuten dauerhaft oben steht, ist
nach einer Woche unsichtbar, und mit ihr die echten daneben. Er
verschwindet von selbst, sobald ein Geraet da ist (Gegenprobe in
pruef-glocke).

Und ohne die "1" davor: Er zaehlt nichts, er beschreibt einen Zustand.

FUENF DER SIEBEN DURFTEN DIE SEITE GAR NICHT OEFFNEN. anruf-probe.html
stand nur Leitung und Modis offen. Das war die falsche Einschraenkung:
Die Seite prueft die ganze Kette bis zur Geraete-Kennung, und die
traegt auch jede Aufgaben- und Terminerinnerung. Jetzt alle Team-Rollen,
ohne "gast" (die Community bekommt keine Erinnerungen).

2. 49 PRUEFUNGEN KONNTEN "BELEGT" NICHT VON "KAPUTT" UNTERSCHEIDEN.

Aufgefallen beim Nachsehen, warum ein Lauf rot war: Es war mein eigener,
haengengebliebener Lauf, der den Port hielt. Kein Befund.

Dabei gemessen: Von 130 Pruefungen hatten 49 keinen Portwaechter, und
SIEBEN Portnummern sind doppelt vergeben (4186, 4188, 4189, 4193, 4198,
4321, 4345). Bei belegtem Port startet der eigene Server STILL nicht --
und alles Weitere misst gegen einen fremden Stand. Das hat in diesem
Haus schon zweimal Stunden gekostet (27.08. und 05.09.).

Der Waechter existierte seit dem 05.09., er war nur nie ausgerollt.
Jetzt haben ihn alle 130. Gegenprobe gemacht: Port belegt ->
Rueckgabewert 3 mit Begruendung statt stillem Messen.

pruef-content bekommt BEIDE Ports -- sie startet zwei Server, und der
zweite gehoert zur Gegenprobe. Nur den ersten zu schuetzen hiesse,
ausgerechnet den Teil ungeschuetzt zu lassen, dem man glaubt.

Die sieben doppelten Nummern bleiben vorerst. Mit dem Waechter ist eine
Kollision jetzt laut statt still; Umnummerieren ist ein eigener Schritt.

3. pruef-rollen IST ABGESTUERZT, SEIT WANN WEISS NIEMAND.

"Target crashed" bei der fuenften Rolle, nach 234 gruenen Pruefungen.
Kein Fehlschlag -- ein toter Browser.

NACHGEMESSEN GEGEN DEN STAND VOR HEUTE (0b73fbb): dort genauso, an
derselben Stelle. Es lag also nicht an den Aenderungen dieses Tages.
Aufgefallen ist es nie, weil ein abgestuerzter Browser von aussen wie
ein Programmfehler aussieht und die 234 gruenen Zeilen davor sich wie
ein fast bestandener Lauf lesen.

Ursache ist die Menge: fuenf Rollen mal sechzehn Seiten in EINEM
Browserprozess. Die Kontexte wurden je Rolle geschlossen, der Prozess
darunter lief durch. Jetzt bekommt jede Rolle ihren eigenen Browser,
und ein Absturz meldet sich selbst statt als Zeitueberschreitung
irgendwo weiter unten aufzuschlagen.

Ergebnis: 321 Pruefungen, alles in Ordnung -- statt Absturz bei 234.

4. DAS SYMBOL FUER TIKTOK.

assets/img/app-symbole/workspace-1024.png, frisch gerendert statt
hochskaliert (das Huskygesicht lebt von harten Kanten) und als VOLLES
Quadrat ohne Rundung: Die Aufnahme hat keinen Alphakanal, die Ecken
waeren sonst weiss. App-Portale wollen ohnehin das volle Quadrat, die
Rundung macht das Betriebssystem.

Die sechs vorhandenen Groessen sind dabei bitgleich geblieben --
gepruefte Nebenwirkung, nicht gehofft.

GEMESSEN: pruef-glocke 31/0 (neu: 5 zum Hinweis), pruef-rollen 321/0
(vorher Absturz), pruef-rechtetafel 19/0, pruef-struktur 0 Fehler,
pruef-betreuung und pruef-werdegang gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 11:14:18 +02:00

249 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");
/* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026).
Ohne diese Zeile startet der eigene Server bei belegtem Port
STILL nicht -- und alles Folgende misst gegen einen fremden
Stand. Das ist der dritte Ausgang: belegt ist etwas anderes
als kaputt. */
await portMussFreiSein(4367, "pruef-modi-livecheck");
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";
import { portMussFreiSein } from "./helfer-port.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);