/* ===================================================================== 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. */ const PORT = await eigenerPort(import.meta, "pruef-modi-livecheck"); process.env.PORT = `${PORT}`; 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 { eigenerPort } 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:${PORT}`; 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("modi", "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);