diff --git a/server/mess-regie.mjs b/server/mess-regie.mjs new file mode 100644 index 00000000..f6c5d526 --- /dev/null +++ b/server/mess-regie.mjs @@ -0,0 +1,501 @@ +/* ===================================================================== + DER PLATZ HINTER DEM PULT (08.10.2026) + + Zwei Dinge, die Filipe am 08.10.2026 gemeldet hat, und beide lassen + sich NUR in einem echten Browser messen: + + 1. „in der regie will ich auch video hinzufuegen koennen fuer + andere tage und uhrzeiten, gerade geht das nicht. wie so ein + kalender wo vanvan und dogfather videos eintragen und + vorbereiten koennen." + + 2. „wenn ich texte eingebe oder videos und kurz was anderes mache + und es nicht gespeichert hab loescht es sich von selbst. es + soll bleiben bis ich fertig bin und speichern druecke. oder + aus der seite gehe aber nicht solange wie ich noch da bin." + + --------------------------------------------------------------------- + WARUM DER ZWEITE PUNKT HIER UND NICHT IN EINER PRUEFUNG STEHT + + Er handelt von einem Zustand, den es auf dem Server gar nicht gibt: + getippter, noch nicht abgeschickter Text. Eine Schnittstellenpruefung + kann ihn nicht sehen. Gemessen wird deshalb der Vorgang selbst -- + tippen, ein Neuzeichnen ausloesen, nachsehen, ob es noch dasteht. + + DAS NEUZEICHNEN WIRD ECHT AUSGELOEST und nicht nachgeahmt: Die + Messung schickt eine Aenderung von einer ZWEITEN Sitzung (VanVan), + wie es an einem Sendeabend auch passiert. Ein `zeichnen()` von Hand + aufzurufen wuerde beweisen, dass die Funktion funktioniert -- nicht, + dass der Fall eintritt. + + NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes + Zertifikat -- und am Ende ist alles weg. + + DREI AUSGAENGE: 0 in Ordnung · 1 ein Befund · 3 konnte nicht nachsehen + + Aufrufen mit: node server/mess-regie.mjs + ===================================================================== */ +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { request } from "node:http"; +import { createServer as httpsServer } from "node:https"; +import { zertifikatBauen } from "./helfer-openssl.mjs"; +import { notbremse } from "./helfer-notbremse.mjs"; +import { eigenerPort } from "./helfer-port.mjs"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const ordner = mkdtempSync(join(tmpdir(), "mess-regie-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +const PORT = await eigenerPort(import.meta); +const HTTPS_PORT = await eigenerPort(import.meta, "mess-regie (HTTPS)", 1); +process.env.PORT = `${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 }); }; +await import("./index.js"); +notbremse(240_000, "mess-regie"); +await new Promise((r) => setTimeout(r, 1000)); + +const CREW = "crew.dogfather-universe.com"; + +/* ---------- Leute ------------------------------------------------------- */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const kenn = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", kenn, new Date().toISOString()); +const ALT = new Date(Date.now() - 200 * 86400000).toISOString(); +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", kenn).update(code).digest("hex"), ALT); +} +const CODES = { admin: "CODE-M00-000001", hand: "CODE-M01-000001" }; +anlegen("DogFather", "admin", CODES.admin); +anlegen("VanVan", "hand", CODES.hand); +d.close(); + +function roh(pfad, { method = "GET", keks = "", rumpf = null } = {}) { + return new Promise((fertig, schief) => { + const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf)); + const a = request({ + host: "127.0.0.1", port: PORT, path: pfad, method, + headers: { + Host: CREW, Origin: `https://${CREW}`, + ...(keks ? { Cookie: keks } : {}), + ...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}), + }, + }, (antwort) => { + const teile = []; + antwort.on("data", (s) => teile.push(s)); + antwort.on("end", () => { + const text = Buffer.concat(teile).toString("utf8"); + let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ } + fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers }); + }); + }); + a.on("error", schief); + if (daten) a.write(daten); + a.end(); + }); +} +async function anmelden(wer) { + const a = await roh("/workspace/api/anmelden", { + method: "POST", rumpf: { rolle: wer, code: CODES[wer], alter_ok: true }, + }); + return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); +} +const k = { admin: await anmelden("admin"), hand: await anmelden("hand") }; + +/* ---------- Zaehlwerk --------------------------------------------------- */ +let fehler = 0; +let geprueft = 0; +const ok = (b, t) => { + geprueft++; + console.log((b ? " ok " : " FEHL ") + t); + if (!b) fehler++; +}; +function nichtNachsehbar(grund) { + console.log("\n -- NICHT NACHSEHBAR"); + console.log(` ${grund}`); + console.log(" Das ist KEIN Befund am Code.\n"); + try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + process.exit(3); +} + +/* ---------- Der Vorbau -------------------------------------------------- */ +const schluesselDatei = join(ordner, "p.key"); +const zertDatei = join(ordner, "p.crt"); +try { zertifikatBauen(schluesselDatei, zertDatei, CREW); } +catch (f) { nichtNachsehbar(`Kein Zertifikat zu bauen: ${f?.message}`); } +const vorbau = httpsServer({ + key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei), +}, (von, nach) => { + const weiter = request({ + host: "127.0.0.1", port: PORT, path: von.url, method: von.method, + headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` }, + }, (antwort) => { + nach.writeHead(antwort.statusCode || 502, antwort.headers); + antwort.pipe(nach); + }); + weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } }); + von.pipe(weiter); +}); +await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r)); +const BASIS = `https://${CREW}:${HTTPS_PORT}`; + +let pw = null; +try { + pw = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +} catch (f) { nichtNachsehbar(`Playwright ist nicht da: ${f?.message}`); } +const browser = await pw.chromium.launch({ + args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors", + "--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"], +}); + +async function alsWer(wer, breite = 1440, hoehe = 980) { + const kontext = await browser.newContext({ + viewport: { width: breite, height: hoehe }, + hasTouch: breite <= 860, + ignoreHTTPSErrors: true, + permissions: ["camera", "microphone"], + }); + await kontext.addCookies((k[wer] || "").split("; ").filter(Boolean).map((z) => { + const [n, ...r] = z.split("="); + return { name: n, value: r.join("="), domain: CREW, path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("pageerror", (f) => konsole.push(String(f?.message || f))); + seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); + await seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + return { kontext, seite, konsole }; +} + +/** Die Regie aufklappen und auf ein Register gehen. */ +async function regieAuf(seite, tafel) { + await seite.evaluate(() => { + const auf = document.getElementById("reiter-auf"); + if (auf && auf.getAttribute("aria-expanded") !== "true") auf.click(); + }); + await seite.waitForTimeout(150); + await seite.click(`#reiter-${tafel}`); + await seite.waitForTimeout(250); +} + +async function bild(seite, name) { + await seite.screenshot({ path: join(HIER, `mess-regie-${name}.png`) }); + console.log(` Bild: server/mess-regie-${name}.png`); +} + +/* ===================================================================== + 1. DER KALENDER + ===================================================================== */ +console.log("\n=== 1. Der Kalender ==="); + +/* Zwei Videos mit Termin, eines ohne -- beide Faelle muessen sich im + Raster unterscheiden lassen. Die Termine werden RELATIV gerechnet: + Ein festes Datum waere eine Zeitbombe, die stimmt, bis der Kalender + sie einholt. */ +const INZWEITAGEN = new Date(); +INZWEITAGEN.setDate(INZWEITAGEN.getDate() + 2); +INZWEITAGEN.setHours(20, 30, 0, 0); + +{ + const a = await roh("/workspace/api/reaktion/liste", { + method: "POST", keks: k.admin, + rumpf: { video: "https://youtu.be/9bZkp7q19f0", wann: INZWEITAGEN.toISOString() }, + }); + ok(a.code === 200, `ein Video laesst sich GLEICH mit Termin anhaengen (${a.code})`); + const z = (a.j?.liste || []).find((x) => x.video === "9bZkp7q19f0"); + ok(!!z && !!z.wann, `und der Termin steht daran (${z?.wann || "leer"})`); + + const zweit = new Date(INZWEITAGEN); + zweit.setHours(22, 0, 0, 0); + await roh("/workspace/api/reaktion/liste", { + method: "POST", keks: k.admin, + rumpf: { video: "https://www.youtube.com/watch?v=kJQP7kiw5Fk", wann: zweit.toISOString() }, + }); + await roh("/workspace/api/reaktion/liste", { + method: "POST", keks: k.admin, + rumpf: { video: "https://www.youtube.com/watch?v=aqz-KE-bpKQ" }, + }); + + /* UND EIN UNSINNIGES DATUM WIRD ABGEWIESEN -- mit einem eigenen + Wort, nicht mit „video_unklar". */ + const b = await roh("/workspace/api/reaktion/liste", { + method: "POST", keks: k.admin, + rumpf: { video: "https://youtu.be/dQw4w9WgXcQ", wann: "irgendwann mal" }, + }); + ok(b.code === 400 && b.j?.fehler === "termin_unklar", + `ein unsinniges Datum wird abgewiesen (${b.code} ${b.j?.fehler})`); +} + +{ + const chef = await alsWer("admin"); + await regieAuf(chef.seite, "liste"); + + /* ---- Die Umschaltung ---- */ + const vorher = await chef.seite.evaluate(() => ({ + liste: !document.getElementById("warteliste").hidden, + kalender: !document.getElementById("plan-kalender").hidden, + })); + ok(vorher.liste && !vorher.kalender, "zuerst steht die Reihenfolge da"); + + await chef.seite.click("#plan-sicht-kalender"); + await chef.seite.waitForTimeout(250); + const nachher = await chef.seite.evaluate(() => ({ + liste: !document.getElementById("warteliste").hidden, + kalender: !document.getElementById("plan-kalender").hidden, + monat: document.getElementById("plan-monat").textContent.trim(), + zellen: document.querySelectorAll("#plan-raster .plan-zelle:not(.plan-zelle--leer)").length, + voll: [...document.querySelectorAll("#plan-raster .plan-zelle[data-voll=\"ja\"]")] + .map((z) => z.dataset.tag), + heute: document.querySelectorAll("#plan-raster .plan-zelle[data-heute=\"ja\"]").length, + })); + ok(!nachher.liste && nachher.kalender, "der Knopf schaltet auf den Kalender"); + ok(nachher.zellen >= 28 && nachher.zellen <= 31, + `der Monat hat so viele Tage, wie er hat (${nachher.zellen})`); + ok(/^[A-ZÄÖÜ][a-zäöü]+ \d{4}$/.test(nachher.monat), + `und er steht ausgeschrieben da (${nachher.monat})`); + ok(nachher.heute === 1, `heute ist genau einmal markiert (${nachher.heute})`); + + /* ==== BEIDE TERMINE LIEGEN AUF DEMSELBEN TAG ================== + Deshalb darf GENAU EINE Zelle gefuellt sein -- zwei waeren ein + Zeichen dafuer, dass in UTC statt in Ortszeit gruppiert wird, + und genau dieser Fehler verschiebt eine Sendung um 00:30 auf + den Vortag. */ + ok(nachher.voll.length === 1, + `zwei Termine am selben Tag ergeben EINE volle Zelle (${nachher.voll.length})`); + + const erwartet = `${INZWEITAGEN.getFullYear()}-` + + `${String(INZWEITAGEN.getMonth() + 1).padStart(2, "0")}-` + + `${String(INZWEITAGEN.getDate()).padStart(2, "0")}`; + ok(nachher.voll[0] === erwartet, + `und zwar die richtige (${nachher.voll[0]} statt ${erwartet})`); + + const anzahl = await chef.seite.textContent( + `#plan-raster .plan-zelle[data-tag="${erwartet}"] .plan-zelle__anzahl`); + ok(anzahl?.trim() === "2", `die Zelle zeigt die ANZAHL und nicht nur einen Punkt (${anzahl})`); + await bild(chef.seite, "1-kalender"); + + /* ---- Einen Tag antippen ---- */ + await chef.seite.click(`#plan-raster .plan-zelle[data-tag="${erwartet}"]`); + await chef.seite.waitForTimeout(200); + const tag = await chef.seite.evaluate(() => ({ + kopf: document.querySelector("#plan-tag .plan-tag__kopf")?.textContent.trim() || "", + eintraege: document.querySelectorAll("#plan-tag .plan-eintrag").length, + zeiten: [...document.querySelectorAll("#plan-tag .plan-eintrag__zeit")] + .map((z) => z.textContent.trim()), + })); + ok(tag.eintraege === 2, `der Tag zeigt beide Videos (${tag.eintraege})`); + ok(tag.zeiten[0] === "20:30 Uhr" && tag.zeiten[1] === "22:00 Uhr", + `nach Uhrzeit sortiert (${tag.zeiten.join(" / ")})`); + /* Vor dem Bild in die Schublade rollen: Sie ist hoeher als das + Fenster, und ein Bildschirmfoto vom oberen Rand zeigt genau das + nicht, worum es hier geht. */ + await chef.seite.evaluate(() => { + document.getElementById("plan-tag")?.scrollIntoView({ block: "center" }); + }); + await chef.seite.waitForTimeout(250); + await bild(chef.seite, "2-tag"); + + /* ---- „Fuer diesen Tag anhaengen" fuellt das Feld ---- */ + await chef.seite.click(".plan-tag__anhaengen"); + await chef.seite.waitForTimeout(150); + const vorbelegt = await chef.seite.inputValue("#l-wann"); + ok(vorbelegt === `${erwartet}T20:00`, + `der Knopf schlaegt den Abend vor, statt ihn zu erzwingen (${vorbelegt})`); + const wo = await chef.seite.evaluate(() => document.activeElement?.id); + ok(wo === "l-neu", `und setzt den Finger in die Adresse (${wo})`); + + /* ---- Und ein Monat weiter ---- */ + const jetztMonat = nachher.monat; + await chef.seite.click("#plan-vor"); + await chef.seite.waitForTimeout(200); + const weiter = await chef.seite.textContent("#plan-monat"); + ok(weiter?.trim() !== jetztMonat, `ein Monat vor wechselt (${jetztMonat} -> ${weiter?.trim()})`); + await chef.seite.click("#plan-heute"); + await chef.seite.waitForTimeout(200); + const zurueck = await chef.seite.textContent("#plan-monat"); + ok(zurueck?.trim() === jetztMonat, `und „Heute" bringt zurueck (${zurueck?.trim()})`); + + ok(chef.konsole.length === 0, "nichts in der Konsole" + + (chef.konsole.length ? " -> " + chef.konsole.slice(0, 3).join(" | ") : "")); + await chef.kontext.close(); +} + +/* ===================================================================== + 2. GETIPPTES ÜBERLEBT DAS NEUZEICHNEN + + Der eigentliche Befund. Gemessen wird der Vorgang, den Filipe + beschrieben hat: tippen, den Finger woanders hin, etwas passiert -- + und nachsehen, ob der Text noch dasteht. + ===================================================================== */ +console.log("\n=== 2. Getipptes ueberlebt das Neuzeichnen ==="); +{ + const chef = await alsWer("admin"); + await regieAuf(chef.seite, "sendung"); + + await chef.seite.fill("#p-titel", "Mein halber Satz"); + /* ==== UND DANN WOANDERS HIN ================================ + Genau das ist Filipes Fall: „und kurz was anderes mache". Der + alte Schutz hing am Fokus und griff ab hier nicht mehr. */ + await chef.seite.click("#p-video"); + await chef.seite.waitForTimeout(100); + + const markiert = await chef.seite.getAttribute("#p-titel", "data-ungespeichert"); + ok(markiert === "ja", `das Feld sagt, dass es ungespeichert ist (${markiert})`); + + /* ==== DAS NEUZEICHNEN WIRD ECHT AUSGELOEST ================== + VanVan aendert etwas; der Rundruf kommt beim Host an und zeichnet + die Regie neu. Ein `zeichnen()` von Hand wuerde beweisen, dass + die Funktion laeuft -- nicht, dass der Fall eintritt. */ + await roh("/workspace/api/reaktion/liste", { + method: "POST", keks: k.hand, rumpf: { video: "https://youtu.be/ZZ5LpwO-An4" }, + }); + + /* ==== AUF DAS EREIGNIS WARTEN, NICHT AUF DIE UHR =============== + Der erste Entwurf wartete 1200 ms und zaehlte dann. Die Zahl + stand auf 3 statt 4 -- das Neuzeichnen war also noch gar nicht + passiert, und die Zeile darunter („der Text steht noch da") war + damit wertlos: Er stand da, weil NICHTS ihn angefasst hatte. + + Das ist der gefaehrlichste Messfehler ueberhaupt, weil er GRUEN + wird. Jetzt wird gewartet, bis VanVans Video wirklich + angekommen ist -- und erst dann gefragt, ob der getippte Text + ueberlebt hat. Scheitert das Warten, ist es ein Fehler und kein + Ueberspringen. */ + let gezeichnet = true; + try { + await chef.seite.waitForFunction( + () => document.querySelectorAll("#warteliste .warte-zeile").length === 4, + null, { timeout: 10000 }); + } catch { gezeichnet = false; } + ok(gezeichnet, + "VanVans Video kommt an -- es wurde also wirklich neu gezeichnet" + + (gezeichnet ? "" : " (nach 10 s nicht)")); + + const steht = await chef.seite.inputValue("#p-titel"); + ok(steht === "Mein halber Satz", + `und der getippte Text steht immer noch da ("${steht}")`); + + /* ---- Speichern macht es wieder sauber ---- */ + await chef.seite.click("#p-sichern"); + await chef.seite.waitForTimeout(900); + const sauber = await chef.seite.getAttribute("#p-titel", "data-ungespeichert"); + ok(sauber === "nein", `nach dem Speichern ist die Marke weg (${sauber})`); + const nachSpeichern = await chef.seite.inputValue("#p-titel"); + ok(nachSpeichern === "Mein halber Satz", `und der Text steht weiter da ("${nachSpeichern}")`); + await bild(chef.seite, "3-getippt"); + + /* ---- Und in einer LISTE, die bei jedem Takt neu gebaut wird ---- + Das ist der haertere Fall: Die Zeile selbst entsteht neu, das + Feld ist danach ein anderes Element. */ + await regieAuf(chef.seite, "liste"); + await chef.seite.evaluate(() => { + document.getElementById("plan-sicht-reihe").click(); + }); + await chef.seite.waitForTimeout(200); + const felder = await chef.seite.evaluate(() => + document.querySelectorAll("#warteliste .warte-zeile__wann").length); + ok(felder >= 3, `die Warteschlange hat Terminfelder (${felder})`); + + await chef.seite.evaluate(() => { + const f = document.querySelectorAll("#warteliste .warte-zeile__wann")[2]; + f.value = "2027-03-05T19:45"; + f.dispatchEvent(new Event("input", { bubbles: true })); + }); + await chef.seite.waitForTimeout(100); + + await roh("/workspace/api/reaktion/liste", { + method: "POST", keks: k.hand, rumpf: { video: "https://youtu.be/L_jWHffIx5E" }, + }); + await chef.seite.waitForTimeout(1200); + + const inListe = await chef.seite.evaluate(() => { + const f = [...document.querySelectorAll("#warteliste .warte-zeile__wann")] + .find((x) => x.dataset.ungespeichert === "ja"); + return f ? { wert: f.value, schluessel: f.dataset.schluessel } : null; + }); + ok(inListe?.wert === "2027-03-05T19:45", + `auch in einer neu gebauten Liste bleibt es stehen (${inListe?.wert})`); + ok(/^warte-wann-\d+$/.test(inListe?.schluessel || ""), + `und haengt an der Kennung der Zeile, nicht an ihrer Nummer (${inListe?.schluessel})`); + + ok(chef.konsole.length === 0, "nichts in der Konsole" + + (chef.konsole.length ? " -> " + chef.konsole.slice(0, 3).join(" | ") : "")); + await chef.kontext.close(); +} + +/* ===================================================================== + 3. GEGENPROBEN + ===================================================================== */ +console.log("\n=== 3. Gegenproben ==="); +{ + const chef = await alsWer("admin"); + await regieAuf(chef.seite, "sendung"); + + /* ==== OHNE DAS GEDAECHTNIS MUSS DER TEXT VERSCHWINDEN ========= + + Sonst bewiese Abschnitt 2 nur, dass irgendetwas stehen bleibt. + + DER ERSTE ENTWURF HAT DAS GEGENTEIL GEMESSEN: Er machte + `Getippt.binde` zu einer LEEREN Funktion -- damit schreibt + niemand mehr in das Feld, und der Text bleibt erst recht + stehen. „Ohne das Gedaechtnis" heisst nicht „ohne Schreiben", + sondern: wieder so wie frueher, naemlich stumpf + `feld.value = wert`. Genau das wird hier eingesetzt. */ + await chef.seite.evaluate(() => { + window.Getippt.binde = (feld, schluessel, vomServer) => { + feld.value = vomServer === null || vomServer === undefined ? "" : String(vomServer); + }; + window.Getippt.vergissAlles(); + }); + await chef.seite.fill("#p-titel", "Dieser Satz geht verloren"); + await chef.seite.click("#p-video"); + await roh("/workspace/api/reaktion", { + method: "PUT", keks: k.hand, rumpf: { titel: "Von VanVan" }, + }); + let kam = true; + try { + await chef.seite.waitForFunction( + () => document.getElementById("p-titel").value === "Von VanVan", + null, { timeout: 10000 }); + } catch { kam = false; } + const weg = await chef.seite.inputValue("#p-titel"); + ok(kam && weg === "Von VanVan", + `Gegenprobe: nach der alten Bauart ist der getippte Satz weg ("${weg}")`); + + await chef.kontext.close(); +} + +/* ===================================================================== + SCHLUSS + ===================================================================== */ +await browser.close(); +await new Promise((r) => vorbau.close(r)); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + +console.log(`\n${geprueft} Messungen, ${fehler} Fehler`); +if (geprueft < 20) { + console.log(`NICHT NACHSEHBAR: nur ${geprueft} Messungen gelaufen, erwartet mindestens 20.`); + process.exit(3); +} +if (fehler) { console.log("NICHT IN ORDNUNG"); process.exit(1); } +console.log("ALLES IN ORDNUNG"); +process.exit(0); diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs index 89419683..6078a4c9 100644 --- a/server/pruef-reaktion.mjs +++ b/server/pruef-reaktion.mjs @@ -3691,6 +3691,140 @@ melde("=== 21. Das Regie-Tor ==="); } } +/* ======================================================================= + 22. DER SENDEPLAN (08.10.2026) + + Filipe: „in der regie will ich auch video hinzufuegen koennen fuer + andere tage und uhrzeiten, gerade geht das nicht. wie so ein kalender + wo vanvan und dogfather videos eintragen und vorbereiten koennen." + + WAS HIER GEPRUEFT WIRD UND WAS NICHT: Hier steht, was die + Schnittstelle antwortet. Ob der Kalender im Browser richtig + gruppiert -- und ob getippter Text ein Neuzeichnen ueberlebt -- + misst `mess-regie.mjs`; das laesst sich ohne Browser gar nicht + fragen. + ======================================================================= */ +melde(""); +melde("=== 22. Der Sendeplan ==="); +{ + const dbP = new DatabaseSync(process.env.WORKSPACE_DB); + dbP.prepare("DELETE FROM reaktion_liste").run(); + + /* RELATIV GERECHNET und nicht mit einem festen Datum: Ein fester + Termin stimmt, bis der Kalender ihn einholt -- und dann wird die + Pruefung rot, ohne dass am Programm etwas ist. */ + const inDreiTagen = new Date(Date.now() + 3 * 86400000); + inDreiTagen.setHours(20, 30, 0, 0); + + { + const a = await roh(CREW, "/workspace/api/reaktion/liste", { + method: "POST", keks: k.admin, + rumpf: { video: "https://youtu.be/9bZkp7q19f0", wann: inDreiTagen.toISOString() }, + }); + ok(a.code === 200, `ein Video laesst sich GLEICH mit Termin anhaengen (${a.code})`); + const z = (a.j?.liste || []).find((x) => x.video === "9bZkp7q19f0"); + ok(!!z?.wann, `der Termin steht sofort daran (${z?.wann || "leer"})`); + ok(z?.wann === inDreiTagen.toISOString(), + "und zwar auf die Minute der, der geschickt wurde"); + } + + /* OHNE TERMIN BLEIBT DER NORMALFALL. Ein Pflichtfeld haette aus + einer Vorbereitung eine Verabredung gemacht. */ + { + const a = await roh(CREW, "/workspace/api/reaktion/liste", { + method: "POST", keks: k.admin, + rumpf: { video: "https://www.youtube.com/watch?v=kJQP7kiw5Fk" }, + }); + const z = (a.j?.liste || []).find((x) => x.video === "kJQP7kiw5Fk"); + ok(a.code === 200 && z?.wann === "", + `ohne Termin geht weiterhin (${a.code}, wann="${z?.wann}")`); + } + + /* EIN UNSINNIGES DATUM BEKOMMT SEIN EIGENES WORT und nicht + „video_unklar" -- sonst sucht jemand den Fehler in der Adresse. */ + { + const a = await roh(CREW, "/workspace/api/reaktion/liste", { + method: "POST", keks: k.admin, + rumpf: { video: "https://youtu.be/aqz-KE-bpKQ", wann: "naechsten Donnerstag" }, + }); + ok(a.code === 400 && a.j?.fehler === "termin_unklar", + `ein unsinniges Datum wird abgewiesen (${a.code} ${a.j?.fehler})`); + const n = dbP.prepare("SELECT COUNT(*) AS n FROM reaktion_liste WHERE video = ?") + .get("aqz-KE-bpKQ").n; + ok(n === 0, `und das Video landet GAR NICHT in der Liste (${n})`); + } + + /* DIESELBE PRUEFUNG AUF BEIDEN WEGEN. Haette nur das Anhaengen sie + und das nachtraegliche Aendern nicht, kaeme derselbe Unsinn ueber + die zweite Tuer doch herein. */ + { + const liste = (await roh(CREW, "/workspace/api/reaktion", { keks: k.admin })).j?.liste || []; + const id = liste[0]?.id; + const a = await roh(CREW, `/workspace/api/reaktion/liste/${id}/wann`, { + method: "PUT", keks: k.admin, rumpf: { wann: "irgendwann" }, + }); + ok(a.code === 400 && a.j?.fehler === "termin_unklar", + `auch beim nachtraeglichen Aendern (${a.code} ${a.j?.fehler})`); + const b = await roh(CREW, `/workspace/api/reaktion/liste/${id}/wann`, { + method: "PUT", keks: k.admin, rumpf: { wann: "" }, + }); + ok(b.code === 200 && (b.j?.liste || []).find((x) => x.id === id)?.wann === "", + "Gegenprobe: leer nimmt den Termin weg, statt abgelehnt zu werden"); + } + + /* WER NICHT FUEHRT, TRAEGT AUCH KEINEN TERMIN EIN. */ + { + const a = await roh(CREW, "/workspace/api/reaktion/liste", { + method: "POST", keks: k.modi, + rumpf: { video: "https://youtu.be/L_jWHffIx5E", wann: inDreiTagen.toISOString() }, + }); + ok(a.code === 403, `ein Modi kann nichts einplanen (${a.code})`); + } + + /* ---- Was auf der Seite steht ---- */ + { + const html = readFileSync(join(HIER, "..", "workspace", "reaktion.html"), "utf8"); + const js = readFileSync( + join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8"); + const getippt = readFileSync( + join(HIER, "..", "workspace", "assets", "js", "getippt.js"), "utf8"); + ok(/id="l-wann"/.test(html), "das Anhaengen hat ein Terminfeld"); + ok(/id="plan-kalender"/.test(html) && /id="plan-raster"/.test(html), + "und es gibt ein Monatsraster"); + ok(/data-sicht="reihe"/.test(html) && /data-sicht="kalender"/.test(html), + "zwei Sichten auf DIESELBE Liste, keine zweite Liste"); + /* ==== GEMESSEN WIRD DER EINHAENGER, NICHT DER FLIESSTEXT ===== + Der erste Entwurf verglich `html.indexOf("getippt.js")` mit + `html.indexOf("reaktion.js")` -- und wurde rot, obwohl die + Reihenfolge stimmt: Direkt ueber dem Einhaenger steht ein + Kommentar, in dem das Wort „reaktion.js" vorkommt, und den fand + `indexOf` zuerst. Eine Pruefung, die Prosa liest, misst Prosa. */ + const einhaenger = (datei) => html.indexOf(` + - - - - - - + + + + + + diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index d939995f..c1c4299d 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +