diff --git a/server/helfer-notbremse.mjs b/server/helfer-notbremse.mjs new file mode 100644 index 00000000..a35920ed --- /dev/null +++ b/server/helfer-notbremse.mjs @@ -0,0 +1,75 @@ +/* DIE NOTBREMSE FÜR PRÜFUNGEN + + --------------------------------------------------------------------- + DER FEHLER, DEN DAS VERHINDERT (06.09.2026, drei Stunden gekostet) + + index.js setzt am Ende bewusst zwei Auffangnetze: + + process.on("uncaughtException", (f) => console.error(…)); + process.on("unhandledRejection", (g) => console.error(…)); + + Für den echten Betrieb ist das genau richtig: Ein Fehler in einer + Nebensache darf die Website nicht offline nehmen. + + Für eine PRÜFUNG ist es das Gegenteil. Eine Prüfung importiert + index.js in denselben Prozess -- und erbt damit die Netze. Wenn dann + ihr eigener Ablauf abstürzt (ein Playwright-Klick auf ein Element, + das es nicht gibt), wird der Absturz nur PROTOKOLLIERT. Danach + passiert nichts mehr: Das Skript ist zu Ende, aber der Express-Server + hält den Prozess am Leben. Kein Fehler, kein Ergebnis, kein Ende. + + Genau so blieb pruef-call-kategorien.mjs drei Stunden stehen -- und + mit ihr der gesamte Regressionslauf, der auf sie wartete. Von aussen + sieht "läuft noch" genauso aus wie "hängt für immer". + + --------------------------------------------------------------------- + WAS DIESE DATEI TUT + + Sie hängt ZWEI eigene Zuhörer daneben (process.on ergänzt, es + ersetzt nicht -- die Meldung von index.js bleibt also erhalten) und + beendet den Lauf mit einem Fehler. Und sie stellt einen Wecker: Wer + nach der Frist noch läuft, wartet nicht mehr, er hängt. + + Beides greift NUR in Prüfungen. Der Betrieb importiert diese Datei + nicht und behält seine Netze unverändert. + + --------------------------------------------------------------------- + BENUTZUNG — nach dem Import von index.js, vor dem ersten Browser: + + import { notbremse } from "./helfer-notbremse.mjs"; + await import("./index.js"); + notbremse(300_000, "die Kategorienpruefung"); +*/ + +/** @param {number} fristMs Nach dieser Zeit gilt der Lauf als haengend. + * @param {string} wer Name für die Meldung. */ +export function notbremse(fristMs = 300_000, wer = "diese Pruefung") { + let schonAus = false; + const raus = (art, was) => { + if (schonAus) return; + schonAus = true; + console.error(`\n FEHL ${wer}: ${art}`); + console.error(` ${String(was?.stack || was).split("\n").slice(0, 6).join("\n ")}`); + console.error(`\n Abgebrochen. Ohne diese Notbremse wuerde der Prozess`); + console.error(` weiterlaufen, weil der Server ihn am Leben haelt --`); + console.error(` ohne Fehler, ohne Ergebnis, ohne Ende.\n`); + process.exit(1); + }; + + process.on("uncaughtException", (f) => raus("unbehandelter Fehler", f)); + process.on("unhandledRejection", (g) => raus("unbehandeltes Versprechen", g)); + + /* unref(): Der Wecker allein soll den Prozess NICHT am Leben halten. + Sonst haette ausgerechnet die Notbremse dafuer gesorgt, dass eine + fertige Pruefung noch fuenf Minuten herumsteht. */ + const wecker = setTimeout(() => { + console.error(`\n FEHL ${wer} HING: nach ${Math.round(fristMs / 1000)} s war sie ` + + `noch nicht fertig.`); + console.error(` Haeufigste Ursachen: ein Warten auf ein Element, das nie kommt,`); + console.error(` oder eine Verbindung, die nie endet (Ereignisstrom).\n`); + process.exit(1); + }, fristMs); + wecker.unref?.(); + + return () => clearTimeout(wecker); +} diff --git a/server/pruef-abbrechen-optik.mjs b/server/pruef-abbrechen-optik.mjs index 4e78ee77..3f5fa393 100644 --- a/server/pruef-abbrechen-optik.mjs +++ b/server/pruef-abbrechen-optik.mjs @@ -28,6 +28,7 @@ import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-abopt-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4297, "die Optikpruefung des Abbrechens"); @@ -39,6 +40,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-abbrechen-optik"); await new Promise((r) => setTimeout(r, 900)); const BASIS = "http://127.0.0.1:4297"; diff --git a/server/pruef-agentur.mjs b/server/pruef-agentur.mjs new file mode 100644 index 00000000..9a518ad1 --- /dev/null +++ b/server/pruef-agentur.mjs @@ -0,0 +1,474 @@ +/* DER SECHSTE BEREICH — "Die Agentur bleibt angebunden" + + Kernprinzip 04 des Konzepts, und bis zum 06.09.2026 gab es dafuer + nichts: Fuenf Bereiche standen (LIVE, Content, Technik, Community, + Schutz), der sechste fehlte vollstaendig. + + --------------------------------------------------------------------- + DAS EIGENTLICHE RISIKO IST NICHT DER BEREICH, SONDERN DER UMBAU + + Der erlaubte Wertebereich einer Spalte steckt in SQLite in einem + CHECK, und ein CHECK laesst sich nicht aendern -- die Tabelle muss + neu gebaut werden. Dabei werden ALLE vorhandenen Eintraege + umgeschrieben: LIVE-Auswertungen, Content-Ideen, Technikprobleme, + Community-Konflikte, Vorfaelle. Wer dort eine Spalte vergisst, + verliert ihren Inhalt still. + + Und das ist ein Weg, den man nicht zurueckdrehen kann. Deshalb wird + er hier nicht "im Prinzip" geprueft, sondern WIRKLICH GEGANGEN: Die + Pruefung baut eine Datenbank im ALTEN Stand nach, fuellt sie, laesst + die echte Anwendung darueberlaufen und zaehlt danach jede Zeile und + jedes Feld nach. + + GEGENPROBEN durchgehend -- vor allem die eine, auf die es ankommt: + Der CHECK muss danach immer noch BEISSEN. Eine Umstellung, die + nebenbei die Schranke entfernt, sieht aus wie ein Erfolg und ist der + schlimmere Ausgang. + ===================================================================== */ + +import { mkdtempSync, rmSync, appendFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { DatabaseSync } from "node:sqlite"; +import { scryptSync, randomBytes } from "node:crypto"; +import { heuteLokal } from "./helfer-zeit.mjs"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const { portMussFreiSein } = await import("./helfer-port.mjs"); +const PORT = 4341; +await portMussFreiSein(PORT, "die Agentur-Pruefung"); +const BASIS = `http://127.0.0.1:${PORT}`; + +const ordner = mkdtempSync(join(tmpdir(), "ws-agentur-")); +const DB = join(ordner, "workspace.db"); + +const PROTOKOLL = join(process.cwd(), "pruef-agentur-lauf.txt"); +try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ } +const melde = (z) => { + console.log(z); + try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ } +}; +let fehler = 0; +let gemacht = 0; +const ok = (b, t) => { gemacht++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +/* --------------------------------------------------------------------- + Die Anwendung als eigener Prozess -- weil sie ZWEIMAL starten muss: + einmal, um die Datenbank anzulegen, und einmal nach dem Zurueckbau, + damit die Wanderung wirklich laeuft. In einem Prozess ginge das + nicht: Ein Modul wird nur einmal geladen. + --------------------------------------------------------------------- */ +let laufend = null; +async function serverStarten() { + const kind = spawn(process.execPath, [join(HIER, "index.js")], { + env: { + ...process.env, + WORKSPACE_DB: DB, + PORT: String(PORT), + SITE_ACCESS_SECRET: "lokaler-test", + SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + let aus = ""; + kind.stdout.on("data", (d) => { aus += d; }); + kind.stderr.on("data", (d) => { aus += d; }); + for (let i = 0; i < 80; i++) { + await new Promise((r) => setTimeout(r, 250)); + if (kind.exitCode !== null) break; + try { + const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); + if (a.status < 500) { laufend = kind; return { kind, aus: () => aus }; } + } catch { /* noch nicht oben */ } + } + throw new Error("Server startete nicht:\n" + aus.slice(-1200)); +} +async function serverStoppen() { + if (!laufend) return; + laufend.kill(); + laufend = null; + /* Warten, bis der Port wirklich frei ist -- sonst startet der zweite + Lauf gegen einen halb geschlossenen Anschluss und misst Unsinn. */ + for (let i = 0; i < 40; i++) { + await new Promise((r) => setTimeout(r, 150)); + try { await fetch(BASIS + "/workspace/", { redirect: "manual" }); } catch { return; } + } +} + +/* ======================================================================= + 1. Erster Start: die Datenbank entsteht + ======================================================================= */ +melde("=== Die Datenbank anlegen ==="); +{ + const { aus } = await serverStarten(); + ok(true, "die Anwendung ist oben und hat das Schema angelegt"); + void aus; +} + +const jetzt = new Date().toISOString(); +/* ORTSZEIT fuer das Tagesdatum, nicht der Schnitt aus dem UTC-Stempel. + `jetzt.slice(0, 10)` liegt zwischen Mitternacht und zwei Uhr einen Tag + daneben -- die Pruefung waere dann nachts rot, ohne dass sich etwas + geaendert haette. Gefunden von pruef-struktur, das genau danach sucht. */ +const heute = heuteLokal(); + +function anlegen(d, name, rolle, code) { + const salz = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salz, 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salz, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} + +let idDogi, idLuna; +{ + const d = new DatabaseSync(DB); + idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001"); + idLuna = anlegen(d, "Luna", "creator", "CODE-CREA-0001"); + d.close(); +} + +/* ======================================================================= + 2. Die Datenbank auf den ALTEN Stand zurueckbauen + ======================================================================= + + Genau so sieht sie auf dem Server aus, bevor die Wanderung laeuft. + Ohne diesen Schritt pruefte die ganze Datei nur eine frisch angelegte + Tabelle -- und damit ausgerechnet den Fall, den es in der Wirklichkeit + nicht gibt. */ +melde("\n=== Zurueck auf den alten Stand (fuenf Bereiche) ==="); +await serverStoppen(); + +const ALTE_EINTRAEGE = [ + ["live", "auswertung", "Auswertung Montag", "Der Hook saß, die Mitte hing.", 4, "hoch"], + ["content", "idee", "Serie: Basteln mit Kindern", "Fünf Teile.", null, "mittel"], + ["technik", "problem", "Ton bricht ab", "Nur bei OBS 30.", null, "hoch"], + ["community", "konflikt", "Streit in den Kommentaren", "Zwei Zuschauer.", null, "niedrig"], + ["schutz", "vorfall", "Meldung wegen Sprache", "Geklärt.", null, "hoch"], +]; + +{ + const d = new DatabaseSync(DB); + d.exec("PRAGMA foreign_keys = OFF"); + d.exec("DROP TABLE IF EXISTS eintraege"); + /* WOERTLICH der alte Bauplan, mit allen Spalten, die zu diesem + Zeitpunkt schon da waren. */ + d.exec(` + CREATE TABLE eintraege ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bereich TEXT NOT NULL + CHECK (bereich IN ('live','content','technik','community','schutz')), + art TEXT NOT NULL, + titel TEXT NOT NULL, + text TEXT, + datum TEXT NOT NULL, + bewertung INTEGER CHECK (bewertung IS NULL OR (bewertung BETWEEN 1 AND 5)), + dringlichkeit TEXT NOT NULL DEFAULT 'mittel' + CHECK (dringlichkeit IN ('hoch','mittel','niedrig')), + status TEXT NOT NULL DEFAULT 'offen' + CHECK (status IN ('offen','erledigt')), + creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL, + erstellt TEXT NOT NULL, + erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL, + geaendert TEXT, + hook TEXT, + format TEXT, + saeule_id INTEGER, + geplant TEXT, + creator_extern TEXT + ); + CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id);`); + + const setzen = d.prepare(`INSERT INTO eintraege + (bereich, art, titel, text, datum, bewertung, dringlichkeit, status, + creator_id, erstellt, erstellt_von, hook, creator_extern) + VALUES (?,?,?,?,?,?,?, 'offen', ?,?,?,?,?)`); + for (const [b, art, titel, text, bew, dring] of ALTE_EINTRAEGE) { + setzen.run(b, art, titel, text, heute, bew, dring, idLuna, jetzt, idDogi, + b === "content" ? "Der Hook: 'Das habt ihr noch nie gesehen'" : null, + b === "live" ? "Gastauftritt Mia" : null); + } + d.exec("PRAGMA foreign_keys = ON"); + + const n = d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n; + ok(n === ALTE_EINTRAEGE.length, `${n} Eintraege im alten Stand angelegt`); + + /* GEGENPROBE: Der alte CHECK muss "agentur" WIRKLICH ablehnen -- + sonst gaebe es nichts umzustellen, und der Rest dieser Datei + bewiese nichts. */ + let abgelehnt = false; + try { + d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt) + VALUES ('agentur','kampagne','Darf noch nicht',?,?)`).run(heute, jetzt); + } catch { abgelehnt = true; } + ok(abgelehnt, "im alten Stand wird 'agentur' von der Datenbank abgelehnt"); + d.close(); +} + +/* ======================================================================= + 3. Die Wanderung -- der Weg, den man nicht zurueckdrehen kann + ======================================================================= */ +melde("\n=== Die Umstellung ==="); +let ausgabe = ""; +{ + const { aus } = await serverStarten(); + await new Promise((r) => setTimeout(r, 600)); + ausgabe = aus(); +} + +ok(/Bereich 'agentur' freigeschaltet/.test(ausgabe), + "die Anwendung meldet die Umstellung" + + (/agentur/.test(ausgabe) ? "" : ` — Ausgabe: ${ausgabe.slice(-300)}`)); + +{ + const d = new DatabaseSync(DB, { readOnly: true }); + const plan = d.prepare( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='eintraege'").get()?.sql || ""; + ok(/'agentur'/.test(plan), "der neue Bauplan kennt 'agentur'"); + + /* KEINE ZEILE DARF FEHLEN -- und keine darf sich VERAENDERT haben. + Die Zeilenzahl allein reichte nicht: Wer beim Umschreiben eine + Spalte vergisst, behaelt alle Zeilen und verliert ihren Inhalt. + Genau das faellt sonst erst Wochen spaeter auf. */ + const alle = d.prepare("SELECT * FROM eintraege ORDER BY id").all(); + ok(alle.length === ALTE_EINTRAEGE.length, + `alle ${alle.length} Eintraege sind noch da (vorher ${ALTE_EINTRAEGE.length})`); + + let abweichungen = []; + ALTE_EINTRAEGE.forEach(([b, art, titel, text, bew, dring], i) => { + const z = alle[i]; + if (!z) { abweichungen.push(`${b}: Zeile fehlt`); return; } + if (z.bereich !== b) abweichungen.push(`${b}: bereich = ${z.bereich}`); + if (z.art !== art) abweichungen.push(`${b}: art = ${z.art}`); + if (z.titel !== titel) abweichungen.push(`${b}: titel = ${z.titel}`); + if (z.text !== text) abweichungen.push(`${b}: text = ${z.text}`); + if (z.bewertung !== bew) abweichungen.push(`${b}: bewertung = ${z.bewertung}`); + if (z.dringlichkeit !== dring) abweichungen.push(`${b}: dringlichkeit = ${z.dringlichkeit}`); + if (z.creator_id !== idLuna) abweichungen.push(`${b}: creator_id = ${z.creator_id}`); + if (z.erstellt_von !== idDogi) abweichungen.push(`${b}: erstellt_von = ${z.erstellt_von}`); + }); + ok(abweichungen.length === 0, + abweichungen.length ? `Inhalte haben sich veraendert: ${abweichungen.slice(0, 5).join(" | ")}` + : `jedes Feld jeder Zeile ist unveraendert (${alle.length} Zeilen x 8 Felder geprueft)`); + + /* Auch die SELTENEN Spalten -- gerade die vergisst man beim + Abschreiben des Bauplans. */ + const mitHook = alle.find((z) => z.bereich === "content"); + const mitExtern = alle.find((z) => z.bereich === "live"); + ok(/Das habt ihr noch nie gesehen/.test(mitHook?.hook || ""), + `die Spalte "hook" hat ihren Inhalt behalten ("${String(mitHook?.hook).slice(0, 30)}…")`); + ok(mitExtern?.creator_extern === "Gastauftritt Mia", + `die Spalte "creator_extern" ebenfalls ("${mitExtern?.creator_extern}")`); + + /* Der Index muss nach dem Tausch wieder da sein -- er wird beim DROP + mit entfernt. Ohne ihn laeuft alles weiter, nur langsam, und + niemand merkt es je. */ + const indizes = d.prepare( + "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='eintraege'").all() + .map((r) => r.name); + ok(indizes.includes("idx_eintraege_bereich"), + `der Index steht wieder (${indizes.join(", ")})`); + d.close(); +} + +/* GEGENPROBE, DIE WICHTIGSTE: Die Schranke muss noch BEISSEN. Eine + Umstellung, die nebenbei den CHECK entfernt, sieht aus wie ein Erfolg + und ist der schlimmere Ausgang -- ab da kann jeder Tippfehler einen + Bereich erfinden, den es nicht gibt. */ +{ + const d = new DatabaseSync(DB); + let abgelehnt = false; + try { + d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt) + VALUES ('erfundenerbereich','x','Darf nicht',?,?)`).run(heute, jetzt); + } catch { abgelehnt = true; } + ok(abgelehnt, "ein erfundener Bereich wird weiterhin abgelehnt — der CHECK beisst noch"); + + let angenommen = false; + try { + d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt) + VALUES ('agentur','kampagne','Direkt in die Datenbank',?,?)`).run(heute, jetzt); + angenommen = true; + d.exec("DELETE FROM eintraege WHERE titel = 'Direkt in die Datenbank'"); + } catch { /* bleibt false */ } + ok(angenommen, "'agentur' wird jetzt angenommen"); + d.close(); +} + +/* ======================================================================= + 4. Der Bereich im Betrieb + ======================================================================= */ +melde("\n=== Der Bereich im Betrieb ==="); + +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("; "); + if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`); + return keks; +} +const keksDogi = await anmelden("admin", "CODE-DOGI-0001"); +const keksLuna = await anmelden("creator", "CODE-CREA-0001"); + +async function ruf(art, weg, keks, koerper) { + const a = await fetch(BASIS + weg, { + method: art, + headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS }, + body: koerper ? JSON.stringify(koerper) : undefined, + }); + let daten = null; + try { daten = await a.json(); } catch { /* kein JSON */ } + return { status: a.status, daten }; +} + +/* Die vier Arten -- jede einzeln, denn eine, die der Server nicht kennt, + waere im Formular waehlbar und beim Speichern ein Fehler. */ +const ARTEN = ["kampagne", "schulung", "anliegen", "zustaendig"]; +{ + const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksDogi); + ok(daten?.einstellung?.name === "Agentur", + `der Bereich meldet sich als "${daten?.einstellung?.name}"`); + const arten = Object.keys(daten?.einstellung?.arten || {}); + ok(arten.length === 4 && ARTEN.every((a) => arten.includes(a)), + `vier Arten: ${arten.join(", ")}`); + ok(daten?.einstellung?.dringlichkeit === true, + "Dringlichkeit ist an — ein Anliegen von gestern ist etwas anderes als eines von vor drei Wochen"); + ok(daten?.einstellung?.bewertung === false, + "Bewertung ist aus — hier wird zusammengearbeitet, nicht benotet"); +} + +let angelegt = 0; +for (const art of ARTEN) { + const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, { + art, titel: `Test ${art}`, text: "Inhalt", datum: heute, + dringlichkeit: "mittel", creator_id: idLuna, + }); + if (status === 200 || status === 201) angelegt++; + else ok(false, `Art "${art}" liess sich nicht anlegen (HTTP ${status})`); +} +ok(angelegt === ARTEN.length, `alle ${angelegt} Arten lassen sich anlegen`); + +/* GEGENPROBE: eine erfundene Art muss abgelehnt werden. Sonst pruefte + die Zeile darueber nur, dass der Server irgendetwas annimmt. */ +{ + const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, { + art: "erfundeneart", titel: "Darf nicht", datum: heute, + }); + ok(status >= 400, `eine erfundene Art wird abgelehnt (HTTP ${status})`); +} + +/* Der Creator SIEHT seinen Agentur-Bereich -- er ist der Grund, warum + es ihn gibt. */ +{ + const { status, daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksLuna); + ok(status === 200, `Luna darf den Bereich oeffnen (HTTP ${status})`); + ok((daten?.eintraege || []).length === ARTEN.length, + `sie sieht ihre ${(daten?.eintraege || []).length} Eintraege`); +} + +/* ======================================================================= + 5. Kommt er ueberall an, wo Bereiche vorkommen? + ======================================================================= */ +melde("\n=== Ueberall, wo Bereiche vorkommen ==="); +{ + /* Der Wochenbericht. Hier stand die Liste der fuenf Bereiche noch + einmal von Hand abgeschrieben -- der neue waere schlicht nicht + vorgekommen, ohne Fehler und ohne Luecke. */ + const { daten } = await ruf("GET", "/workspace/api/report", keksDogi); + const b = daten?.bereiche || {}; + ok(Object.keys(b).length === 6, + `der Wochenbericht kennt ${Object.keys(b).length} Bereiche: ${Object.keys(b).join(", ")}`); + ok(b.agentur && b.agentur.neu >= ARTEN.length, + `und zaehlt ${b.agentur?.neu} neue Agentur-Eintraege`); +} +{ + /* Die Suche. Ein Treffer ohne Ueberschrift sieht aus wie ein Fehler. */ + const { daten } = await ruf("GET", "/workspace/api/suche?q=" + encodeURIComponent("Test kampagne"), keksDogi); + const text = JSON.stringify(daten); + ok(/Agentur/.test(text), + "die Suche beschriftet Agentur-Treffer mit dem richtigen Namen"); +} + +/* ======================================================================= + 6. Die Kachel — mit einem echten Browser + ======================================================================= */ +melde("\n=== Die Kachel ==="); +{ + const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await pw.chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + const seite = await ctx.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); + 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: 25000 }); + await seite.waitForTimeout(1100); + + const kacheln = await seite.$$eval(".kachel", (k) => k.map((x) => ({ + name: x.querySelector(".kachel__name")?.textContent.trim(), + ziel: x.querySelector(".kachel__link")?.getAttribute("href"), + ton: x.dataset.ton, + pfade: x.querySelectorAll(".kachel__zeichen path").length, + }))); + const a = kacheln.find((x) => x.name === "Agentur"); + ok(!!a, `die Kachel "Agentur" steht da (${kacheln.length} Kacheln insgesamt)`); + ok(a?.ziel === "bereich.html?b=agentur", `sie fuehrt nach "${a?.ziel}"`); + ok(Number(a?.pfade) >= 4, `ihr Zeichen hat ${a?.pfade} Pfade — es ist wirklich gezeichnet`); + + /* KEINE ZWEI KACHELN MIT DEMSELBEN FARBTON NEBENEINANDER. Das Auge + vergleicht nur, was benachbart ist -- zwei gleiche Toene direkt + nebeneinander lesen sich als Fehler. */ + const toene = kacheln.map((x) => x.ton); + const nachbarnGleich = toene.filter((t, i) => i > 0 && t === toene[i - 1]); + ok(nachbarnGleich.length === 0, + nachbarnGleich.length ? `zwei Nachbarkacheln haben denselben Ton (${nachbarnGleich.join(", ")})` + : `keine zwei benachbarten Kacheln teilen einen Farbton (${toene.length} geprueft)`); + + /* Die Seite selbst muss aufgehen und ihren Namen tragen. */ + await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + const titel = await seite.$eval(".titel", (e) => e.textContent.trim()).catch(() => ""); + ok(/Agentur/i.test(titel), `die Bereichsseite heisst "${titel}"`); + + /* .eintrag-karte -- der Name aus bereich.js. Beim ersten Anlauf stand + hier geraten ".eintrag, .karte"; das traf nichts und meldete "0 + Eintraege", obwohl vier dastanden. Ein falscher Selektor meldet + einen Fehler, den es nicht gibt -- genauso schaedlich wie einer, + der etwas uebersieht. */ + const eintraege = await seite.$$eval(".eintrag-karte", (e) => e.length).catch(() => 0); + ok(eintraege >= ARTEN.length, + `sie zeigt ${eintraege} Eintraege (mindestens die ${ARTEN.length} angelegten)`); + + /* Und die Titel muessen die sein, die oben angelegt wurden -- sonst + bewiese die Zahl nur, dass IRGENDWAS gezeichnet wurde. */ + const kartenTitel = await seite.$$eval(".eintrag-titel", (e) => e.map((x) => x.textContent.trim())); + const fehlend = ARTEN.map((a) => `Test ${a}`).filter((t) => !kartenTitel.includes(t)); + ok(fehlend.length === 0, + fehlend.length ? `diese Eintraege fehlen auf der Seite: ${fehlend.join(", ")}` + : `alle vier Titel stehen da: ${kartenTitel.join(" · ")}`); + + /* GEGENPROBE zum Selektor: Ein erfundener Bereich darf NICHT wie ein + echter aussehen -- weder in der Ueberschrift noch mit Eintraegen. + Die Anwendung schickt einen dorthin zurueck, wo er hingehoert. */ + await seite.goto(BASIS + "/workspace/bereich.html?b=gibtesnicht", { waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + const falsch = await seite.$eval(".titel", (e) => e.textContent.trim()).catch(() => ""); + const falschKarten = await seite.$$eval(".eintrag-karte", (e) => e.length).catch(() => 0); + ok(!/Agentur/i.test(falsch) && falschKarten === 0, + `ein erfundener Bereich zeigt weder die Agentur noch Eintraege ` + + `("${falsch}", ${falschKarten} Karten)`); + + await ctx.close(); + await browser.close(); +} + +await serverStoppen(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + +melde(`\n${gemacht} Pruefungen`); +melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`); +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-ampel.mjs b/server/pruef-ampel.mjs index b6efcf34..8b062b84 100644 --- a/server/pruef-ampel.mjs +++ b/server/pruef-ampel.mjs @@ -19,7 +19,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-ampel"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4206"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_000).unref?.(); diff --git a/server/pruef-aufgabenbrett.mjs b/server/pruef-aufgabenbrett.mjs index 3765e497..8077a4da 100644 --- a/server/pruef-aufgabenbrett.mjs +++ b/server/pruef-aufgabenbrett.mjs @@ -43,7 +43,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-aufgabenbrett"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4216"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-barrierefrei-workspace.mjs b/server/pruef-barrierefrei-workspace.mjs index 6d0ea39b..76a25d0d 100644 --- a/server/pruef-barrierefrei-workspace.mjs +++ b/server/pruef-barrierefrei-workspace.mjs @@ -34,6 +34,7 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-a11y-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); /* Der Port muss frei sein -- sonst wird gegen einen fremden Server gemessen. Begruendung in helfer-port.mjs. */ +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4253, "die Barrierefreiheitspruefung"); @@ -45,6 +46,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-barrierefrei-workspace"); await new Promise((r) => setTimeout(r, 900)); const BASIS = "http://127.0.0.1:4253"; diff --git a/server/pruef-breiten.mjs b/server/pruef-breiten.mjs index 6e9c6fa8..66426ce7 100644 --- a/server/pruef-breiten.mjs +++ b/server/pruef-breiten.mjs @@ -37,6 +37,7 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-breit-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); /* Der Port muss frei sein -- sonst wird gegen einen fremden Server gemessen. Begruendung in helfer-port.mjs. */ +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4277, "die Breitenpruefung"); @@ -48,6 +49,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-breiten"); await new Promise((r) => setTimeout(r, 900)); const BASIS = "http://127.0.0.1:4277"; @@ -130,12 +137,44 @@ const BREITEN = [ [2560, 1440, "sehr breit"], ]; -const SEITEN = [ - "start.html", "uebersicht.html", "aufgaben.html", "kalender.html", "calls.html", - "dateien.html", "content.html", "bereich.html?b=live", "profil.html", - "startcheck.html", "report.html", "scouting.html", "personen.html", - "wissen.html", "automation.html", "steckbrief.html", -]; +/* ---------- WELCHE SEITEN? ALLE. ----------------------------------------- + + Hier stand eine Liste von Hand -- und wie ihre Schwester in + pruef-handy.mjs war sie veraltet: chat.html und leistung.html fehlten. + Ausgerechnet der Chat, den man auf dem Handy am ehesten benutzt, war + bei keiner einzigen der neun Breiten je gemessen worden. + + Eine Liste von Hand altert in dem Moment, in dem jemand weiterarbeitet, + und sie sagt nichts dazu. Der Lauf blieb gruen, die Zahl der + Pruefungen stimmte -- gemessen wurde trotzdem zu wenig. Deshalb kommt + sie jetzt aus dem Verzeichnis: Die naechste neue Seite ist von selbst + dabei, so wie sie durch die Schranke in workspace.js von selbst + geschuetzt ist. + + index.html bleibt draussen -- die Anmeldeseite leitet nach der + Anmeldung weiter und wuerde hier nur start.html ein zweites Mal + messen. */ +const { readdirSync } = await import("node:fs"); +const { dirname } = await import("node:path"); +const { fileURLToPath } = await import("node:url"); +/* Der Ort DIESER Datei, nicht process.cwd(): Der Gesamtlauf startet vom + Wurzelverzeichnis, von Hand startet man aus server/. */ +const SEITEN_ORDNER = join(dirname(fileURLToPath(import.meta.url)), "..", "workspace"); +const MIT_ABFRAGE = { "bereich.html": "?b=live" }; +const SEITEN = readdirSync(SEITEN_ORDNER) + .filter((n) => n.endsWith(".html") && n !== "index.html") + .sort() + .map((n) => n + (MIT_ABFRAGE[n] || "")); + +/* Die Zahl gehoert in die BEDINGUNG, nicht nur in den Meldetext: Faende + readdirSync nichts, liefe die Schleife unten null Mal -- und der Lauf + waere gruen, ohne eine einzige Seite angesehen zu haben. */ +if (SEITEN.length < 15) { + console.log(` FEHL nur ${SEITEN.length} Seiten in ${SEITEN_ORDNER} gefunden ` + + "-- die Breitenpruefung kann so nichts messen"); + process.exit(1); +} +console.log(`${SEITEN.length} Seiten x ${BREITEN.length} Breiten: ${SEITEN.join(", ")}`); const messen = () => { const sichtbar = (e) => { diff --git a/server/pruef-browser.mjs b/server/pruef-browser.mjs index 7b338958..2deb0338 100644 --- a/server/pruef-browser.mjs +++ b/server/pruef-browser.mjs @@ -39,6 +39,7 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-browser-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); /* Der Port muss frei sein -- sonst wird gegen einen fremden Server gemessen. Begruendung in helfer-port.mjs. */ +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4289, "die Browserpruefung"); @@ -50,6 +51,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-browser"); await new Promise((r) => setTimeout(r, 900)); const BASIS = "http://127.0.0.1:4289"; diff --git a/server/pruef-buehne.mjs b/server/pruef-buehne.mjs index 4b52958f..e89e02d8 100644 --- a/server/pruef-buehne.mjs +++ b/server/pruef-buehne.mjs @@ -25,7 +25,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-buehne"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4193"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 220_000).unref?.(); diff --git a/server/pruef-call-kategorien.mjs b/server/pruef-call-kategorien.mjs index cfb14295..15b77fee 100644 --- a/server/pruef-call-kategorien.mjs +++ b/server/pruef-call-kategorien.mjs @@ -26,6 +26,7 @@ import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-callkat-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4321, "die Kategorienpruefung"); @@ -37,6 +38,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-call-kategorien"); await new Promise((r) => setTimeout(r, 1000)); const BASIS = "http://127.0.0.1:4321"; @@ -81,25 +88,89 @@ const tag = (versatz) => { const p = (n) => String(n).padStart(2, "0"); return `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}`; }; -function call(titel, versatz) { +/* --------------------------------------------------------------------- + DIE WANDUHR DARF KEINE ANNAHME SEIN (korrigiert 06.09.2026) + + Hier stand vorher `${tag(versatz)}T18:00` und die feste Erwartung + "zwei Termine unter Heute". Das ging gut, solange jemand vormittags + prueft. Ab 18 Uhr sind die beiden heutigen Calls VORBEI -- der Server + schiebt sie dann von "anstehend" nach "Protokoll fehlt", und die + Oberflaeche unterteilt erst ab FUENF anstehenden ueberhaupt in + Zeitraeume. Uebrig blieben vier, die Bloecke verschwanden, und die + Pruefung meldete "0 Kategorien". + + Sie blieb daraufhin an einem Klick auf einen Block haengen, der gar + nicht da war -- DREI STUNDEN, und mit ihr der ganze Regressionslauf. + Der Fehler war also nicht nur falsch, er war auch teuer. + + Deshalb jetzt zweierlei: + + 1. Die Zeiten liegen RELATIV zu jetzt, nicht auf einer festen + Uhrzeit. Was "heute" heisst, wird nicht behauptet, sondern + gerechnet. + 2. Die Erwartung wird mit DERSELBEN Vorschrift aus den gesaeten + Zeitpunkten abgeleitet, die auch calls.js benutzt -- statt + Zahlen hinzuschreiben, die nur zu einer bestimmten Tageszeit + stimmen. Um 23:58 faellt "in 35 Minuten" auf morgen, und dann + erwartet die Pruefung ihn auch dort. + + Eine Pruefung, die um 17:59 gruen und um 18:01 rot ist, ohne dass + sich am Code etwas geaendert hat, misst den Kalender, nicht die + Software. */ +function callZeit(dat, titel) { + const p = (n) => String(n).padStart(2, "0"); + const beginn = `${dat.getFullYear()}-${p(dat.getMonth() + 1)}-${p(dat.getDate())}` + + `T${p(dat.getHours())}:${p(dat.getMinutes())}`; d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, teilnehmer_id, erstellt, erstellt_von) VALUES (?,'call',?,30,?,?,?,?)`) - .run(titel, `${tag(versatz)}T18:00`, idLuna, idLuna, jetzt, idDogi); + .run(titel, beginn, idLuna, idLuna, jetzt, idDogi); + return beginn; } -/* WIE VIELE TAGE bis Sonntag? Dieselbe Rechnung wie in calls.js -- - damit die Prüfung weiss, was sie erwarten darf, statt es zu raten. */ -const heute = new Date(); -const wochentag = (heute.getDay() + 6) % 7; // Mo=0 … So=6 -const bisSonntag = 6 - wochentag; +/** WOERTLICH dieselbe Vorschrift wie zeitraumVon() in calls.js. + * Absichtlich abgeschrieben und nicht importiert: calls.js laeuft im + * Browser und kennt kein export. Waere sie hier anders, pruefte die + * Datei ihre eigene Rechnung statt die der Anwendung -- deshalb steht + * weiter unten eine Gegenprobe, die beide gegeneinander haelt. */ +function zeitraumVon(beginn) { + const dd = new Date(String(beginn).replace(" ", "T")); + const h = new Date(); h.setHours(0, 0, 0, 0); + const t = new Date(dd); t.setHours(0, 0, 0, 0); + const tage = Math.round((t - h) / 86400000); + if (tage <= 0) return "heute"; + const wt = (h.getDay() + 6) % 7; // Mo=0 … So=6 + const bisSo = 6 - wt; + if (tage <= bisSo) return "woche"; + if (tage <= bisSo + 7) return "naechste"; + return "spaeter"; +} -/* Zwei heute. Dann einer, der sicher noch in diese Woche fällt (falls - es sie gibt), einer in die nächste, und drei weit weg. */ -call("Heute A", 0); -call("Heute B", 0); -if (bisSonntag >= 1) call("Diese Woche", 1); -call("Nächste Woche", bisSonntag + 2); -for (let i = 0; i < 3; i++) call(`Später ${i}`, bisSonntag + 20 + i); +const nachher = (min) => new Date(Date.now() + min * 60000); + +/* SIEBEN kuenftige Calls, gestreut. Sieben, weil die Oberflaeche erst + ab fuenf anstehenden unterteilt -- mit vieren pruefte diese Datei + einen Zustand, in dem es die Bloecke gar nicht geben soll. */ +/* Die Titel getrennt, weil weiter unten drei davon wieder geloescht + werden muessen -- ueber genau diese Liste und nicht ueber ein + abgeschriebenes Namensmuster. */ +const GESAET_TITEL = ["Gleich", "Etwas spaeter", "Morgen", "In drei Tagen", + "In neun Tagen", "In dreissig Tagen", "In einunddreissig Tagen"]; +const GESAET = [ + callZeit(nachher(5), "Gleich"), + callZeit(nachher(35), "Etwas spaeter"), + callZeit(nachher(60 * 26), "Morgen"), + callZeit(nachher(60 * 24 * 3), "In drei Tagen"), + callZeit(nachher(60 * 24 * 9), "In neun Tagen"), + callZeit(nachher(60 * 24 * 30), "In dreissig Tagen"), + callZeit(nachher(60 * 24 * 31), "In einunddreissig Tagen"), +]; + +/* Was MUSS also in welchem Block stehen? Gerechnet, nicht behauptet. */ +const ERWARTET = {}; +for (const b of GESAET) { + const z = zeitraumVon(b); + ERWARTET[z] = (ERWARTET[z] || 0) + 1; +} d.close(); const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); @@ -133,26 +204,32 @@ const { ctx, seite } = await callsSeite(); /* DIE ANZAHL IN DIE BEDINGUNG: Ohne Blöcke liefe alles Folgende über eine leere Menge und meldete grün. */ - ok(bloecke.length >= 3, `${bloecke.length} Kategorien (erwartet mindestens 3)`); + const erwarteteRaeume = Object.keys(ERWARTET); + ok(bloecke.length === erwarteteRaeume.length, + `${bloecke.length} Kategorien (gerechnet: ${erwarteteRaeume.length} — ` + + `${erwarteteRaeume.map((r) => `${r}:${ERWARTET[r]}`).join(", ")})`); - const heuteBlock = bloecke.find((b) => b.raum === "heute"); - ok(heuteBlock?.zahl === 2, `"Heute" enthält ${heuteBlock?.zahl} Termine (erwartet 2)`); + /* JEDER gerechnete Zeitraum einzeln. Nicht "mindestens drei": Welche + Bloecke es gibt, haengt vom Wochentag ab (am Sonntag gibt es kein + "Diese Woche" mehr, und das ist richtig). Gerechnet wird es mit + derselben Vorschrift wie in der Anwendung. */ + let stimmen = 0; + for (const [raum, soll] of Object.entries(ERWARTET)) { + const b = bloecke.find((x) => x.raum === raum); + const gut = b?.zahl === soll; + if (gut) stimmen++; + ok(gut, `"${raum}" enthält ${b?.zahl ?? "keinen Block"} (gerechnet: ${soll})`); + } + ok(stimmen === erwarteteRaeume.length && stimmen >= 3, + `${stimmen} von ${erwarteteRaeume.length} Zeitraeumen stimmen (mindestens 3 mussten es sein)`); - const spaeter = bloecke.find((b) => b.raum === "spaeter"); - ok(spaeter?.zahl === 3, `"Später" enthält ${spaeter?.zahl} (erwartet 3)`); - - const naechste = bloecke.find((b) => b.raum === "naechste"); - ok(naechste?.zahl === 1, `"Nächste Woche" enthält ${naechste?.zahl} (erwartet 1)`); - - /* Am Sonntag gibt es kein "Diese Woche" mehr -- das ist richtig und - kein Fehler. Deshalb wird die Erwartung aus dem Wochentag - abgeleitet, nicht festgeschrieben. */ - const diese = bloecke.find((b) => b.raum === "woche"); - const erwarteDiese = bisSonntag >= 1; - ok(!!diese === erwarteDiese, - erwarteDiese - ? `"Diese Woche" ist da (${diese?.zahl}) — heute ist noch ${bisSonntag} Tag(e) bis Sonntag` - : `"Diese Woche" fehlt zu Recht — heute ist Sonntag, danach kommt nichts mehr`); + /* GEGENPROBE zur abgeschriebenen Vorschrift: Die Summe ueber alle + Bloecke muss die Zahl der gesaeten Calls sein. Rechnete diese Datei + anders als die Anwendung, ginge die Summe nicht auf -- und genau + das faellt sonst nicht auf, weil beide Seiten "plausibel" aussehen. */ + const summe = bloecke.reduce((n, b) => n + b.zahl, 0); + ok(summe === GESAET.length, + `alle ${GESAET.length} Calls sind in genau einem Block gelandet (gezaehlt: ${summe})`); /* Die Zahl im Kopf muss zu den Karten darin passen. Eine Zahl, die etwas anderes sagt als der Inhalt, ist schlimmer als keine. */ @@ -211,10 +288,17 @@ melde("\n=== Wenige Termine brauchen keine Schubladen ==="); /* Vier Karten in vier Kategorien wären mehr Arbeit als Nutzen -- man klickt dreimal, um vier Zeilen zu sehen. */ const p = new DatabaseSync(process.env.WORKSPACE_DB); - p.prepare("DELETE FROM termine WHERE titel LIKE 'Später%' OR titel = 'Nächste Woche'").run(); + /* Drei der sieben weg, vier bleiben -- unterhalb der Schwelle, ab der + die Oberflaeche ueberhaupt unterteilt. Geloescht wird ueber die + TITEL AUS DER SAAT oben, nicht ueber Namen von Hand: Als die Saat + am 06.09.2026 umgestellt wurde, traf das alte `LIKE 'Später%'` + nichts mehr, es blieben sieben Termine stehen, und dieser Abschnitt + pruefte ab da einen Zustand, den er selbst nie hergestellt hatte. */ + const WEG = GESAET_TITEL.slice(-3); + p.prepare(`DELETE FROM termine WHERE titel IN (${WEG.map(() => "?").join(",")})`).run(...WEG); const rest = p.prepare("SELECT COUNT(*) AS n FROM termine").get().n; p.close(); - ok(rest <= 4, `nur noch ${rest} Termine übrig`); + ok(rest === 4, `nur noch ${rest} Termine übrig (drei entfernt: ${WEG.join(", ")})`); await seite.reload({ waitUntil: "networkidle" }); await seite.waitForTimeout(1400); @@ -234,10 +318,17 @@ melde("\n=== Gegenprobe ==="); gerade bewiesen, dass sie VERSCHWINDEN -- ohne diesen Nachweis bewiese "0 Blöcke" nur, dass der Selektor nie etwas trifft. */ const p = new DatabaseSync(process.env.WORKSPACE_DB); + const pp = (n) => String(n).padStart(2, "0"); for (let i = 0; i < 6; i++) { + /* Relativ zu jetzt, wie oben. Eine feste Uhrzeit haette hier + denselben Zeitzuender wie in der Saat: Ab 18 Uhr waeren die neuen + Termine schon vorbei und die Gegenprobe bewiese nichts. */ + const t = new Date(Date.now() + (40 + i) * 86400000); + const beginn = `${t.getFullYear()}-${pp(t.getMonth() + 1)}-${pp(t.getDate())}` + + `T${pp(t.getHours())}:${pp(t.getMinutes())}`; p.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, teilnehmer_id, erstellt, erstellt_von) VALUES (?,'call',?,30,?,?,?,?)`) - .run(`Wieder da ${i}`, `${tag(bisSonntag + 30 + i)}T18:00`, idLuna, idLuna, jetzt, idDogi); + .run(`Wieder da ${i}`, beginn, idLuna, idLuna, jetzt, idDogi); } p.close(); diff --git a/server/pruef-chat-optik.mjs b/server/pruef-chat-optik.mjs index 5821dc3f..ed95249b 100644 --- a/server/pruef-chat-optik.mjs +++ b/server/pruef-chat-optik.mjs @@ -30,6 +30,7 @@ import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-chatoptik-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4317, "die Chat-Optikpruefung"); @@ -41,6 +42,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-chat-optik"); await new Promise((r) => setTimeout(r, 1000)); const BASIS = "http://127.0.0.1:4317"; diff --git a/server/pruef-checkliste.mjs b/server/pruef-checkliste.mjs index 9b64337d..a9f76b84 100644 --- a/server/pruef-checkliste.mjs +++ b/server/pruef-checkliste.mjs @@ -27,7 +27,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-checkliste"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4207"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-code.mjs b/server/pruef-code.mjs index 53e3ebf9..bfd41d67 100644 --- a/server/pruef-code.mjs +++ b/server/pruef-code.mjs @@ -34,7 +34,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-code"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4195"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 240_000).unref?.(); diff --git a/server/pruef-content-ansicht.mjs b/server/pruef-content-ansicht.mjs index 958df87d..bc9d8a6b 100644 --- a/server/pruef-content-ansicht.mjs +++ b/server/pruef-content-ansicht.mjs @@ -15,7 +15,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-content-ansicht"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4185"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); diff --git a/server/pruef-dabei-optik.mjs b/server/pruef-dabei-optik.mjs index 875be318..8c78417f 100644 --- a/server/pruef-dabei-optik.mjs +++ b/server/pruef-dabei-optik.mjs @@ -33,6 +33,7 @@ import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-dabei-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4293, "die Optikpruefung der Teilnehmerwahl"); @@ -44,6 +45,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-dabei-optik"); await new Promise((r) => setTimeout(r, 900)); const BASIS = "http://127.0.0.1:4293"; diff --git a/server/pruef-formulare.mjs b/server/pruef-formulare.mjs index 1bab52a2..2d2cd48f 100644 --- a/server/pruef-formulare.mjs +++ b/server/pruef-formulare.mjs @@ -29,7 +29,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-formulare"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4203"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_000).unref?.(); diff --git a/server/pruef-freie-namen.mjs b/server/pruef-freie-namen.mjs index 1f04a87e..80bced68 100644 --- a/server/pruef-freie-namen.mjs +++ b/server/pruef-freie-namen.mjs @@ -35,7 +35,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-freie-namen"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4192"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 240_000).unref?.(); diff --git a/server/pruef-glocke.mjs b/server/pruef-glocke.mjs index 89411aec..d42b9b80 100644 --- a/server/pruef-glocke.mjs +++ b/server/pruef-glocke.mjs @@ -51,6 +51,7 @@ import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-glocke-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4299, "die Glockenpruefung"); @@ -62,6 +63,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-glocke"); await new Promise((r) => setTimeout(r, 900)); const BASIS = "http://127.0.0.1:4299"; diff --git a/server/pruef-grosscheck.mjs b/server/pruef-grosscheck.mjs index 612602b0..2234d6de 100644 --- a/server/pruef-grosscheck.mjs +++ b/server/pruef-grosscheck.mjs @@ -53,7 +53,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-grosscheck"); await new Promise((r) => setTimeout(r, 800)); const BASIS = "http://127.0.0.1:4218"; setTimeout(() => { console.log("ABBRUCH — zu lange gelaufen"); process.exit(1); }, 900_000).unref?.(); diff --git a/server/pruef-handy.mjs b/server/pruef-handy.mjs index 43bb47b2..336e8562 100644 --- a/server/pruef-handy.mjs +++ b/server/pruef-handy.mjs @@ -36,7 +36,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-handy"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4198"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 400_000).unref?.(); diff --git a/server/pruef-kalender.mjs b/server/pruef-kalender.mjs index eaade626..557a660b 100644 --- a/server/pruef-kalender.mjs +++ b/server/pruef-kalender.mjs @@ -22,7 +22,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-kalender"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4188"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); diff --git a/server/pruef-kopf-messen.mjs b/server/pruef-kopf-messen.mjs index d9ef1b91..bb55da18 100644 --- a/server/pruef-kopf-messen.mjs +++ b/server/pruef-kopf-messen.mjs @@ -35,7 +35,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-kopf-messen"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4179"; diff --git a/server/pruef-leistung-optik.mjs b/server/pruef-leistung-optik.mjs index 0f1554f9..61a8ffa6 100644 --- a/server/pruef-leistung-optik.mjs +++ b/server/pruef-leistung-optik.mjs @@ -35,6 +35,7 @@ import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-leistoptik-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4337, "die Zahlen-Optikpruefung"); @@ -46,6 +47,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-leistung-optik"); await new Promise((r) => setTimeout(r, 1000)); const BASIS = "http://127.0.0.1:4337"; diff --git a/server/pruef-lesbarkeit.mjs b/server/pruef-lesbarkeit.mjs index 32026380..b40e7511 100644 --- a/server/pruef-lesbarkeit.mjs +++ b/server/pruef-lesbarkeit.mjs @@ -36,7 +36,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-lesbarkeit"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4214"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-manager-sicht.mjs b/server/pruef-manager-sicht.mjs index 95bfa528..e03733fa 100644 --- a/server/pruef-manager-sicht.mjs +++ b/server/pruef-manager-sicht.mjs @@ -47,7 +47,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-manager-sicht"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4199"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-personen-formular.mjs b/server/pruef-personen-formular.mjs index e41e0e8d..6b041875 100644 --- a/server/pruef-personen-formular.mjs +++ b/server/pruef-personen-formular.mjs @@ -33,7 +33,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-personen-formular"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4198"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-personen-liste.mjs b/server/pruef-personen-liste.mjs index f5108f45..9938bb19 100644 --- a/server/pruef-personen-liste.mjs +++ b/server/pruef-personen-liste.mjs @@ -27,7 +27,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-personen-liste"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4187"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); diff --git a/server/pruef-protokoll-klappe.mjs b/server/pruef-protokoll-klappe.mjs index 28b83a6d..be1852ea 100644 --- a/server/pruef-protokoll-klappe.mjs +++ b/server/pruef-protokoll-klappe.mjs @@ -19,7 +19,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-protokoll-klappe"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4180"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); diff --git a/server/pruef-protokoll-loeschen.mjs b/server/pruef-protokoll-loeschen.mjs index 1ec20fac..56d91618 100644 --- a/server/pruef-protokoll-loeschen.mjs +++ b/server/pruef-protokoll-loeschen.mjs @@ -40,7 +40,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-protokoll-loeschen"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4197"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-rollen.mjs b/server/pruef-rollen.mjs index e2a997d5..5871316e 100644 --- a/server/pruef-rollen.mjs +++ b/server/pruef-rollen.mjs @@ -42,7 +42,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-rollen"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4193"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 540_000).unref?.(); diff --git a/server/pruef-schulung.mjs b/server/pruef-schulung.mjs index d97a94d6..1e30b4e4 100644 --- a/server/pruef-schulung.mjs +++ b/server/pruef-schulung.mjs @@ -26,7 +26,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-schulung"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4205"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_000).unref?.(); diff --git a/server/pruef-scout-zuteilung.mjs b/server/pruef-scout-zuteilung.mjs index 41c4e9a2..0c236d35 100644 --- a/server/pruef-scout-zuteilung.mjs +++ b/server/pruef-scout-zuteilung.mjs @@ -34,7 +34,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-scout-zuteilung"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4217"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-serien.mjs b/server/pruef-serien.mjs index 2a40d234..3d726d78 100644 --- a/server/pruef-serien.mjs +++ b/server/pruef-serien.mjs @@ -38,7 +38,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-serien"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4189"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); diff --git a/server/pruef-sicht.mjs b/server/pruef-sicht.mjs index 5d7d3eda..bc65b3ac 100644 --- a/server/pruef-sicht.mjs +++ b/server/pruef-sicht.mjs @@ -34,7 +34,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-sicht"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4215"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-sprung.mjs b/server/pruef-sprung.mjs index 015ba87e..b8ba9cd1 100644 --- a/server/pruef-sprung.mjs +++ b/server/pruef-sprung.mjs @@ -40,7 +40,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-sprung"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4212"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index 3fa58ce9..4ba80c98 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -15,7 +15,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-start-ansicht"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4186"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); diff --git a/server/pruef-steckbrief.mjs b/server/pruef-steckbrief.mjs index 5a1d5a9a..50876d7b 100644 --- a/server/pruef-steckbrief.mjs +++ b/server/pruef-steckbrief.mjs @@ -19,7 +19,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-steckbrief"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4196"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); diff --git a/server/pruef-tagesblick.mjs b/server/pruef-tagesblick.mjs index 1049ff02..45942683 100644 --- a/server/pruef-tagesblick.mjs +++ b/server/pruef-tagesblick.mjs @@ -28,6 +28,7 @@ import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-tagesblick-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4303, "die Tagesblickpruefung"); @@ -39,6 +40,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-tagesblick"); await new Promise((r) => setTimeout(r, 900)); const BASIS = "http://127.0.0.1:4303"; diff --git a/server/pruef-team.mjs b/server/pruef-team.mjs index cb0a1a71..5f80b16e 100644 --- a/server/pruef-team.mjs +++ b/server/pruef-team.mjs @@ -38,7 +38,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-team"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4213"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); diff --git a/server/pruef-tempo-workspace.mjs b/server/pruef-tempo-workspace.mjs index 9f12ae96..836b4a1f 100644 --- a/server/pruef-tempo-workspace.mjs +++ b/server/pruef-tempo-workspace.mjs @@ -37,6 +37,7 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-tempo-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); /* Der Port muss frei sein -- sonst wird gegen einen fremden Server gemessen. Begruendung in helfer-port.mjs. */ +import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4283, "die Tempopruefung"); @@ -48,6 +49,12 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-tempo-workspace"); await new Promise((r) => setTimeout(r, 900)); const BASIS = "http://127.0.0.1:4283"; diff --git a/server/pruef-uebersicht-browser.mjs b/server/pruef-uebersicht-browser.mjs index 102cfa6d..1c1f4950 100644 --- a/server/pruef-uebersicht-browser.mjs +++ b/server/pruef-uebersicht-browser.mjs @@ -23,7 +23,14 @@ const echteCookie = express.response.cookie; express.response.cookie = function (name, wert, opt) { return echteCookie.call(this, name, wert, { ...(opt || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; await import("./index.js"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(150_000, "pruef-uebersicht-browser"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4177"; @@ -38,11 +45,10 @@ const tagText = (d = new Date()) => let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; -const notbremse = setTimeout(() => { - console.log("\nABBRUCH: hing über 150 Sekunden."); - process.exit(1); -}, 150_000); -notbremse.unref?.(); +/* Die eigene Notbremse von hier ist am 06.09.2026 nach + helfer-notbremse.mjs gewandert und wird oben aufgerufen -- sie faengt + jetzt zusaetzlich den unbehandelten Absturz ab, nicht nur das + Zulangsamsein. Die knappe Frist dieser Datei bleibt erhalten. */ /* ---------- Daten ------------------------------------------------------ */ diff --git a/server/pruef-wissen-neu.mjs b/server/pruef-wissen-neu.mjs index d79a60c3..e8aef5f6 100644 --- a/server/pruef-wissen-neu.mjs +++ b/server/pruef-wissen-neu.mjs @@ -23,7 +23,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-wissen-neu"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4201"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); diff --git a/server/pruef-workspace-seiten.mjs b/server/pruef-workspace-seiten.mjs index 30fe7cd5..34fe1e72 100644 --- a/server/pruef-workspace-seiten.mjs +++ b/server/pruef-workspace-seiten.mjs @@ -18,7 +18,14 @@ const echteCookie = express.response.cookie; express.response.cookie = function (n, w, o) { return echteCookie.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; await import("./index.js"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-workspace-seiten"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4178"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 150_000).unref?.(); diff --git a/server/pruef-zustand-ansicht.mjs b/server/pruef-zustand-ansicht.mjs index 7f5e1547..5e219343 100644 --- a/server/pruef-zustand-ansicht.mjs +++ b/server/pruef-zustand-ansicht.mjs @@ -17,7 +17,14 @@ 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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-zustand-ansicht"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4182"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 180_000).unref?.(); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 3d5fda3d..640b6e9a 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -75,6 +75,43 @@ export const BEREICHE = { bewertung: false, dringlichkeit: true, }, + /* DER SECHSTE BEREICH (06.09.2026). + + Kernprinzip 04 des Konzepts: "Die Agentur bleibt angebunden." Fuenf + Bereiche standen, dieser fehlte -- und damit fehlte der einzige + Ort, an dem steht, welche Kampagne laeuft, welche Schulung ansteht + und wie ein Anliegen an die Agentur ausgegangen ist. Das lief + ueber private Nachrichten: nicht auffindbar, nicht nachvollziehbar, + und beim naechsten Mal fing man wieder von vorn an. + + VIER ARTEN, und die vierte ist der Punkt: + kampagne was laeuft, bis wann, unter welcher Bedingung + schulung Termine und Inhalte, verweist auf die Bibliothek + anliegen der OFFIZIELLE Weg hin -- mit Stand + zustaendig wer wofuer zustaendig ist: wir oder die Agentur + + Die letzte Art ist keine Verlegenheit, sondern das, was das Konzept + ausdruecklich fordert ("Betreuung & Umsetzung ist unsere Seite, + offizielle Wege und Plattformthemen sind Agenturseite"). Solange + das nur im Kopf steht, wird bei jedem Streitfall neu verhandelt, + wer eigentlich haette handeln muessen. + + Dringlichkeit: ja. Ein Anliegen an die Agentur, das drei Wochen + liegt, ist etwas anderes als eines von gestern -- und ohne Stufe + sieht man den Unterschied in einer Liste nicht. + Bewertung: nein. Hier wird nichts benotet; es ist ein + Zusammenarbeits-Bereich, kein Analysebereich. */ + agentur: { + name: "Agentur", + arten: { + kampagne: "Event & Kampagne", + schulung: "Schulung", + anliegen: "Anliegen an die Agentur", + zustaendig: "Zuständigkeit", + }, + bewertung: false, + dringlichkeit: true, + }, }; const DRINGLICHKEITEN = ["hoch", "mittel", "niedrig"]; diff --git a/server/workspace-reports.js b/server/workspace-reports.js index 0ce44beb..ffbb593b 100644 --- a/server/workspace-reports.js +++ b/server/workspace-reports.js @@ -22,6 +22,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, sichtbareCreatorIds, } from "./workspace.js"; +import { BEREICHE } from "./workspace-bereiche.js"; export const reportRouter = express.Router(); @@ -135,7 +136,12 @@ reportRouter.get("/workspace/api/report", (req, res) => { /* --- Bereiche -------------------------------------------------------- */ const bereiche = {}; - for (const b of ["live", "content", "technik", "community", "schutz"]) { + /* AUS DER EINEN QUELLE, nicht abgeschrieben (06.09.2026). + Hier stand die Liste der fuenf Bereiche noch einmal von Hand. Als + "agentur" dazukam, waere er im Wochenbericht schlicht nicht + vorgekommen -- ohne Fehler, ohne Luecke, einfach nicht da. Genau + so altert eine abgeschriebene Liste. */ + for (const b of Object.keys(BEREICHE)) { bereiche[b] = { neu: zahl( `SELECT COUNT(*) AS n FROM eintraege e WHERE e.bereich = ? AND e.erstellt >= ?${nurE}`, diff --git a/server/workspace-suche.js b/server/workspace-suche.js index f42f2496..d7dbd0cd 100644 --- a/server/workspace-suche.js +++ b/server/workspace-suche.js @@ -30,7 +30,7 @@ import { import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js"; import { sichtbar as sichtbarTermine } from "./workspace-kalender.js"; import { sichtbar as sichtbarDateien } from "./workspace-dateien.js"; -import { sichtbar as sichtbarEintraege } from "./workspace-bereiche.js"; +import { sichtbar as sichtbarEintraege, BEREICHE } from "./workspace-bereiche.js"; export const sucheRouter = express.Router(); @@ -38,10 +38,12 @@ const MIN = 2; const JE_BEREICH = 6; const AUSSCHNITT = 110; -const BEREICHSNAME = { - live: "LIVE-Analyse", content: "Content", technik: "Technik", - community: "Community", schutz: "Schutz", -}; +/* Die Namen kommen aus workspace-bereiche.js, nicht aus einer zweiten + Liste hier. Eine abgeschriebene Liste vergisst den naechsten neuen + Bereich -- und in der Suche hiesse das: Er wird gefunden und traegt + dann keine Ueberschrift. */ +const BEREICHSNAME = Object.fromEntries( + Object.entries(BEREICHE).map(([k, b]) => [k, b.name])); function angemeldet(req, res, next) { const person = sitzungLesen(req); diff --git a/server/workspace.js b/server/workspace.js index d31c33ea..a5a5c606 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -444,6 +444,100 @@ function umstellungen(d) { } } + /* ---- Der sechste Bereich: "agentur" (06.09.2026) ---- + + Kernprinzip 04 des Konzepts lautet woertlich "Die Agentur bleibt + angebunden". Fuenf der sechs Bereiche standen (LIVE, Content, + Technik, Community, Schutz) -- der sechste fehlte vollstaendig. Was + dadurch nirgends stand: welche Kampagne laeuft und bis wann, welche + Schulung ansteht, und vor allem der OFFIZIELLE WEG zur Agentur mit + Stand und Antwort. Das lief bisher ueber private Nachrichten und + war damit weder auffindbar noch nachvollziehbar. + + Und die Trennung, die das Konzept ausdruecklich verlangt und die im + Alltag sonst verschwimmt: Betreuung und Umsetzung sind UNSERE + Seite, offizielle Wege und Plattformthemen die der Agentur. Wer + wofuer zustaendig ist, gehoert auf den Bildschirm, nicht ins + Gedaechtnis. + + Derselbe Umbau wie bei "abgebrochen" darueber -- ein CHECK laesst + sich in SQLite nicht aendern, die Tabelle muss neu gebaut werden: + erst sichern, dann tauschen, Zeilen zaehlen, Verweise pruefen. */ + const eintraegePlan = d.prepare( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'eintraege'").get()?.sql || ""; + if (eintraegePlan && !eintraegePlan.includes("'agentur'")) { + const sicherung = `${DB_PFAD}.vor-agentur-${jetztStempel}`; + try { + d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`); + console.log("[workspace] Sicherung vor der Umstellung:", sicherung); + } catch (fehler) { + console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message); + return; + } + + d.exec("PRAGMA foreign_keys = OFF"); + try { + const vorher = d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n; + d.exec("BEGIN"); + d.exec(` + CREATE TABLE eintraege_neu ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bereich TEXT NOT NULL + CHECK (bereich IN ('live','content','technik','community','schutz','agentur')), + art TEXT NOT NULL, + titel TEXT NOT NULL, + text TEXT, + datum TEXT NOT NULL, + bewertung INTEGER CHECK (bewertung IS NULL OR (bewertung BETWEEN 1 AND 5)), + dringlichkeit TEXT NOT NULL DEFAULT 'mittel' + CHECK (dringlichkeit IN ('hoch','mittel','niedrig')), + status TEXT NOT NULL DEFAULT 'offen' + CHECK (status IN ('offen','erledigt')), + creator_id INTEGER REFERENCES personen(id) ON DELETE SET NULL, + erstellt TEXT NOT NULL, + erstellt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL, + geaendert TEXT, + hook TEXT, + format TEXT, + saeule_id INTEGER, + geplant TEXT, + creator_extern TEXT + ); + INSERT INTO eintraege_neu + (id, bereich, art, titel, text, datum, bewertung, dringlichkeit, status, + creator_id, erstellt, erstellt_von, geaendert, + hook, format, saeule_id, geplant, creator_extern) + SELECT id, bereich, art, titel, text, datum, bewertung, dringlichkeit, status, + creator_id, erstellt, erstellt_von, geaendert, + hook, format, saeule_id, geplant, creator_extern + FROM eintraege; + DROP TABLE eintraege; + ALTER TABLE eintraege_neu RENAME TO eintraege; + CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id); + `); + const nachher = d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n; + if (nachher !== vorher) { + d.exec("ROLLBACK"); + console.error(`[workspace] Umstellung abgebrochen: ${vorher} Eintraege vorher, ` + + `${nachher} nachher. Sicherung: ${sicherung}`); + } else { + d.exec("COMMIT"); + const kaputt = d.prepare("PRAGMA foreign_key_check").all(); + if (kaputt.length) { + console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length, + "verwaiste Verweise. Sicherung liegt unter", sicherung); + } else { + console.log(`[workspace] Bereich 'agentur' freigeschaltet, ${vorher} Eintraege, Verweise geprueft.`); + } + } + } catch (fehler) { + try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ } + console.error("[workspace] Umstellung 'agentur' fehlgeschlagen:", fehler?.message); + } finally { + d.exec("PRAGMA foreign_keys = ON"); + } + } + /* ---- Rolle "manager" erlauben ---- */ const bauplan = d.prepare( "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || ""; @@ -918,7 +1012,7 @@ export function db() { CREATE TABLE IF NOT EXISTS eintraege ( id INTEGER PRIMARY KEY AUTOINCREMENT, bereich TEXT NOT NULL - CHECK (bereich IN ('live','content','technik','community','schutz')), + CHECK (bereich IN ('live','content','technik','community','schutz','agentur')), art TEXT NOT NULL, titel TEXT NOT NULL, text TEXT, diff --git a/tools/alles-pruefen.mjs b/tools/alles-pruefen.mjs index 4828e5da..46d8f0c3 100644 --- a/tools/alles-pruefen.mjs +++ b/tools/alles-pruefen.mjs @@ -26,19 +26,73 @@ const dateien = readdirSync(ORDNER) console.log(`${dateien.length} Pruefdateien gefunden\n`); +/* ---------- JEDE PRUEFUNG BEKOMMT EINE FRIST (06.09.2026) --------------- + + VIERTER GRUND fuer diesen eigenen Laeufer, und er hat mich an einem + Tag ZWEIMAL erwischt: + + Eine Pruefung, die haengt, bringt den GANZEN Lauf zum Stillstand -- + ohne Fehlermeldung, ohne FEHL, ohne Absturz. Es steht einfach nichts + mehr da. Von aussen sieht "noch nicht fertig" genauso aus wie "haengt + fuer immer", und deshalb faellt es erst Stunden spaeter auf. + + * pruef-alle-wege lief in den Ereignisstrom des Chats und wartete + auf ein Ende, das es nie gibt (behoben in der Datei selbst). + * pruef-call-kategorien blieb DREI STUNDEN stehen. Sie hatte, anders + als pruef-handy, keine eigene Notbremse. + + Einzelne Dateien nachzuruesten hilft genau so lange, bis jemand die + naechste ohne Notbremse schreibt. Deshalb sitzt sie jetzt HIER, wo + sie fuer alle gilt -- auch fuer die, die es noch nicht gibt. + + Und eine ueberschrittene Frist ist ein FEHLER, kein Ueberspringen: Sie + wird gezaehlt, gemeldet und mit der letzten Ausgabe der Datei belegt, + damit man sieht, wo sie stehengeblieben ist. Ein stiller Stillstand + waere das Gegenteil. + + 600 Sekunden sind grosszuegig bemessen: Die langsamste ehrliche + Pruefung im Haus (pruef-barrierefrei-workspace) braucht 190 s. Wer + das Dreifache davon ueberschreitet, wartet nicht -- er haengt. */ +const FRIST_MS = 600_000; + const laufen = (datei) => new Promise((fertig) => { const start = Date.now(); const kind = spawn(process.execPath, [join(ORDNER, datei)], { stdio: ["ignore", "pipe", "pipe"], }); let aus = ""; + let abgewuergt = false; kind.stdout.on("data", (d) => { aus += d; }); kind.stderr.on("data", (d) => { aus += d; }); + + const wecker = setTimeout(() => { + abgewuergt = true; + /* Erst freundlich, dann bestimmt. Ein Playwright-Browser haengt + sonst als Waise weiter und belegt Speicher bis zum Neustart. */ + try { kind.kill("SIGTERM"); } catch { /* schon weg */ } + setTimeout(() => { try { kind.kill("SIGKILL"); } catch { /* schon weg */ } }, 3000); + }, FRIST_MS); + kind.on("close", (code) => { + clearTimeout(wecker); const sek = Math.round((Date.now() - start) / 100) / 10; const einzeln = (aus.match(/^\s{2}(ok|FEHL)\s/gm) || []).length; const schlecht = (aus.match(/^\s{2}FEHL\s/gm) || []).length; - fertig({ datei, code, sek, einzeln, schlecht, aus }); + if (abgewuergt) { + aus += `\n\n FEHL DIE PRUEFUNG HING. Nach ${Math.round(FRIST_MS / 1000)} s kam nichts mehr` + + " -- abgebrochen.\n Letzte Ausgabe davor steht oben; dort ist sie stehengeblieben." + + "\n Haeufigste Ursache: eine Verbindung, die nie endet (Ereignisstrom)," + + "\n oder ein warten auf etwas, das nicht kommt.\n"; + } + fertig({ + datei, code: abgewuergt ? 124 : code, sek, + /* Die abgewuergte Pruefung zaehlt als fehlgeschlagen, nicht als + "keine Pruefung gelaufen" -- das sind zwei verschiedene Sachen, + und beide muessen unterscheidbar bleiben. */ + einzeln: einzeln + (abgewuergt ? 1 : 0), + schlecht: schlecht + (abgewuergt ? 1 : 0), + aus, hing: abgewuergt, + }); }); }); diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index 448d03a9..896ed85e 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -27,6 +27,11 @@ technik: 'Setups, Fehlerbilder und Lösungen pro Creator sauber dokumentiert.', community: 'Moderation, Aktionen und Konflikte – ruhig statt Drama.', schutz: 'Richtlinien, Vorfälle und was daraus gelernt wurde.', + /* Der Satz nennt die Trennung beim Namen, die das Konzept fordert: + Betreuung und Umsetzung sind unsere Seite, offizielle Wege sind + Agenturseite. Solange das nur im Kopf steht, wird es bei jedem + Streitfall neu verhandelt. */ + agentur: 'Kampagnen, Schulungen und der offizielle Weg zur Agentur – mit Stand und Antwort.', }; let ich = null; @@ -203,7 +208,13 @@ Sinn ergibt: LIVE, Community und Technik. Bei Content sind es eigene Ideen des Creators -- die bewertet niemand mit einer Ampel. */ - if (["live", "community", "technik", "schutz"].includes(bereich) + /* "agentur" ist seit dem 06.09.2026 dabei, und aus demselben + Grund wie die anderen vier: Ein Anliegen an die Agentur hat + einen STAND -- eingereicht, in Klärung, beantwortet. Genau + dafür ist die Ampel da, samt der Nachrichten daran. Ohne sie + wäre der "offizielle Weg" wieder nur eine Liste, in der etwas + steht und niemand weiß, wie es ausgegangen ist. */ + if (["live", "community", "technik", "schutz", "agentur"].includes(bereich) && window.ampelAnhaengen && karten.size) { const stand = await window.ampelnHolen([...karten.keys()]); for (const [id, k] of karten) { diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js index 1ac73b83..8d388c0c 100644 --- a/workspace/assets/js/bereiche.js +++ b/workspace/assets/js/bereiche.js @@ -135,6 +135,13 @@ Bericht, sondern um einen VERLAUF. */ zahlen: [ 'M4 16 9.5 11.5 13 14 20 6.5 20 19.5 4 19.5z' + ], + /* Agentur (06.09.2026): ein Aktenkoffer. Bewusst kein + Haendedruck und kein Gebaeude -- beides wird bei 20 Pixeln zu + Matsch. Der Koffer ist die einzige Form, die man bei dieser + Groesse noch als "offizielle Seite" liest. */ + agentur: [ + 'M3.5 8h17v11h-17z' ] }; @@ -257,6 +264,15 @@ fuellung: ['M13.2 3.2 6 13.4h5.2l-.8 7.4 7.6-10.6h-5.2z'], linien: ['M13.2 3.2 6 13.4h5.2l-.8 7.4 7.6-10.6h-5.2z'], }, + agentur: { + /* Der Deckel als Flaeche -- daran erkennt man einen Koffer, so wie + man einen Kalender an seinem Kopf erkennt. */ + fuellung: ['M3.5 8h17v4.6h-17z'], + linien: ['M3.5 8h17v11h-17z', + 'M9 8V6.4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1V8', + 'M3.5 12.6h17', + 'M10.9 11.6h2.2v2h-2.2z'], + }, zahlen: { fuellung: ['M4 16 9.5 11.5 13 14 20 6.5 20 19.5 4 19.5z'], /* Der Linienzug selbst, die Grundlinie darunter und ein Punkt am @@ -368,6 +384,16 @@ rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=technik', szene: 'garage' }, { name: 'Schutz & Regeln', unter: 'Richtlinien und Vorfälle', zeichen: 'schutz', ton: 1, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=schutz', szene: 'halle' }, + /* DER SECHSTE BEREICH (06.09.2026). Kernprinzip 04 des Konzepts + lautet "Die Agentur bleibt angebunden" -- und dafuer gab es + bis heute nichts. Fuenf Bereiche standen, dieser fehlte. + Steht bei den anderen fuenf und nicht unter "Team & System": + Es ist ein Bereich wie LIVE, Technik oder Schutz, nur dass + die andere Seite des Tisches die Agentur ist. + Ton 9 (Orange) -- deutlich von seinen Nachbarn 1 (Blau) und + 14 (Gruen) entfernt. */ + { name: 'Agentur', unter: 'Kampagnen, Schulungen, offizieller Weg', zeichen: 'agentur', ton: 9, + rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=agentur', szene: 'halle' }, { name: 'Reports & Review', unter: 'Woche und Entscheidung', zeichen: 'reports', ton: 14, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'report.html', szene: 'showbuehne' }, ], diff --git a/workspace/assets/js/report.js b/workspace/assets/js/report.js index ad0cb6af..12f2a78a 100644 --- a/workspace/assets/js/report.js +++ b/workspace/assets/js/report.js @@ -15,7 +15,7 @@ const $ = (id) => document.getElementById(id); const BEREICHSNAME = { live: 'LIVE', content: 'Content', technik: 'Technik', - community: 'Community', schutz: 'Schutz', + community: 'Community', schutz: 'Schutz', agentur: 'Agentur', }; let ich = null; @@ -92,6 +92,7 @@ community: 'bereich.html?b=community', technik: 'bereich.html?b=technik', schutz: 'bereich.html?b=schutz', + agentur: 'bereich.html?b=agentur', content: 'content.html', }; const bereichZiel = (b, zeigen) => { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 7ea3ca02..9a02910d 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -9,9 +9,9 @@ - - - + + +
@@ -211,10 +211,10 @@ - - - - - + + + + +