diff --git a/server/pruef-dateien-liste.mjs b/server/pruef-dateien-liste.mjs
new file mode 100644
index 00000000..1c31a647
--- /dev/null
+++ b/server/pruef-dateien-liste.mjs
@@ -0,0 +1,163 @@
+/* DATEIEN: DIE LISTE EINES EINTRAGS (01.10.2026)
+
+ Filipe: "es sieht sogar noch richtig scheisse aus wenn ich da was
+ hochladen will." Auf dem Handy lag "Fassung 2" quer ueber dem Datum,
+ "1,9 MB" brach in zwei Zeilen, und am Rechner stand "SICHTBAR FUER"
+ zweizeilig ganz links am Rand.
+
+ URSACHE (gemessen): .datei ist ein Raster mit den Spalten
+ `34px 1fr auto`. Die ersten vier Kinder sitzen richtig -- Zeichen,
+ Name, Knoepfe, Metazeile. Alles DANACH (Fassungs-Marke, Freigaben,
+ Personenwahl) wird automatisch platziert und landet in der ersten
+ Spalte. Die ist 34 Pixel breit. Darin steht dann "Fassung 2", und
+ was nicht hineinpasst, legt sich ueber den Nachbarn.
+
+ GEMESSEN WIRD: Bekommt jeder Block unterhalb der Kopfzeile genug
+ Breite, und laeuft sein Inhalt ueber? Beides sind Zahlen.
+
+ DREI AUSGAENGE: 0 in Ordnung / 1 Befunde / 2 konnte nicht arbeiten. */
+import { mkdtempSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+const ordner = mkdtempSync(join(tmpdir(), "ws-dateienliste-"));
+process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+
+const { eigenerPort } = await import("./helfer-port.mjs");
+const PORT = await eigenerPort(import.meta, "die Dateilisten-Pruefung");
+process.env.PORT = `${PORT}`;
+process.env.SITE_ACCESS_SECRET = "lokaler-test";
+process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
+
+const express = (await import("express")).default;
+const ec = express.response.cookie;
+express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
+await import("./index.js");
+const { notbremse } = await import("./helfer-notbremse.mjs");
+notbremse(420_000, "pruef-dateien-liste");
+await new Promise((r) => setTimeout(r, 1200));
+const BASIS = `http://127.0.0.1:${PORT}`;
+
+let geprueft = 0, fehler = 0;
+const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
+
+/* ---- eine Person, die ablegen darf ---------------------------------- */
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes } = await import("node:crypto");
+await fetch(BASIS + "/workspace/api/anmelden", {
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
+for (let i = 0; i < 60; i++) {
+ try {
+ const p = new DatabaseSync(process.env.WORKSPACE_DB);
+ try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
+ } catch { await new Promise((r) => setTimeout(r, 250)); }
+}
+const d = new DatabaseSync(process.env.WORKSPACE_DB);
+const salt = randomBytes(16).toString("hex");
+const hash = scryptSync("CODE-DOGI-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
+ .run("Filipe", "admin", hash, salt, 32768, new Date().toISOString());
+d.close();
+
+/* Eine Datei mit langem Namen -- kurze Namen verdecken genau den Fehler,
+ um den es hier geht. */
+const PROBE = join(ordner, "16 Events und Kampagnen \u2014 Spicy Media x Dogfather.html");
+writeFileSync(PROBE, "
Probe" + "x".repeat(400000));
+
+const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await pw.chromium.launch();
+
+const BREITEN = [1280, 900, 390];
+for (const breite of BREITEN) {
+ const ctx = await browser.newContext({ viewport: { width: breite, height: 1000 }, hasTouch: breite <= 860 });
+ const seite = await ctx.newPage();
+ const konsole = [];
+ seite.on("pageerror", (f) => konsole.push("Absturz: " + f.message));
+ seite.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
+
+ 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.goto(BASIS + "/workspace/dateien.html", { waitUntil: "networkidle" });
+ await seite.waitForTimeout(700);
+
+ console.log(`\n--- ${breite} px ---`);
+ /* ZWEIMAL dieselbe Datei: Beim zweiten Mal entsteht "Fassung 2" und
+ "nicht mehr aktuell" -- genau der Zustand auf Filipes Bild. Ohne das
+ waere die Pruefung blind fuer den Fall, der gemeldet wurde. */
+ for (let n = 0; n < 2; n++) {
+ await seite.setInputFiles("#feld-datei", PROBE);
+ await seite.waitForTimeout(2500);
+ }
+
+ const befund = await seite.evaluate(() => {
+ const eintrag = document.querySelector("#liste .datei");
+ if (!eintrag) return { fehlt: true };
+ const eig = eintrag.getBoundingClientRect();
+ const bloecke = [...eintrag.children].map((el) => {
+ const r = el.getBoundingClientRect();
+ return {
+ klasse: (el.className.baseVal ?? el.className ?? "").toString().split(" ")[0] || el.tagName,
+ breite: Math.round(r.width),
+ ueberlauf: Math.round(el.scrollWidth - el.clientWidth),
+ text: (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 24),
+ o: r.top, u: r.bottom, l: r.left, re: r.right,
+ };
+ });
+ const ueber = [];
+ for (let i = 0; i < bloecke.length; i++) {
+ for (let j = i + 1; j < bloecke.length; j++) {
+ const a = bloecke[i], b = bloecke[j];
+ const sen = Math.min(a.u, b.u) - Math.max(a.o, b.o);
+ const waa = Math.min(a.re, b.re) - Math.max(a.l, b.l);
+ if (sen > 1 && waa > 1) ueber.push(`${a.klasse} auf ${b.klasse} (${Math.round(sen)} px)`);
+ }
+ }
+ return { fehlt: false, breiteEintrag: Math.round(eig.width), bloecke, ueber };
+ });
+
+ if (befund.fehlt) {
+ console.log(" KONNTE NICHT ARBEITEN: Kein Eintrag in der Liste -- Upload hat nicht geklappt.");
+ await ctx.close(); await browser.close(); process.exit(2);
+ }
+
+ ok(befund.bloecke.length >= 5, `der Eintrag hat ${befund.bloecke.length} Bloecke`);
+
+ /* Das Zeichen DARF schmal sein -- es ist die 34-px-Spalte. Alles andere
+ nicht: Ein Textblock in 34 px Breite ist der Fehler selbst. */
+ const schmal = befund.bloecke.filter(
+ (b) => b.klasse !== "datei__zeichen" && b.breite < 80);
+ ok(schmal.length === 0,
+ `kein Textblock steckt in der Zeichenspalte (${schmal.length})`);
+ for (const s of schmal) console.log(` "${s.text}" nur ${s.breite} px breit (${s.klasse})`);
+
+ const laeuft = befund.bloecke.filter((b) => b.ueberlauf > 1);
+ ok(laeuft.length === 0, `kein Block laeuft seitlich ueber (${laeuft.length})`);
+ for (const l of laeuft) console.log(` "${l.text}" ragt ${l.ueberlauf} px heraus`);
+
+ ok(befund.ueber.length === 0, `nichts liegt aufeinander (${befund.ueber.length})`);
+ for (const u of befund.ueber) console.log(` ${u}`);
+
+ ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
+
+ if (process.env.SCHUSS === "ja") {
+ const { mkdirSync } = await import("node:fs");
+ mkdirSync(join(process.cwd(), "schuss-dateienliste"), { recursive: true });
+ await seite.locator("#liste").screenshot({
+ path: join(process.cwd(), "schuss-dateienliste", `liste-${breite}.png`) });
+ console.log(` Bild: schuss-dateienliste/liste-${breite}.png`);
+ }
+ await ctx.close();
+}
+await browser.close();
+
+console.log(`\n${geprueft} Pruefungen, ${fehler} Befunde`);
+if (geprueft < BREITEN.length * 5) {
+ console.log("ERGEBNIS: KONNTE NICHT ARBEITEN (zu wenige Messungen)");
+ process.exit(2);
+}
+console.log(fehler ? "ERGEBNIS: NICHT IN ORDNUNG" : "ERGEBNIS: IN ORDNUNG");
+process.exit(fehler ? 1 : 0);
diff --git a/server/pruef-wissen-formular.mjs b/server/pruef-wissen-formular.mjs
new file mode 100644
index 00000000..9854c75a
--- /dev/null
+++ b/server/pruef-wissen-formular.mjs
@@ -0,0 +1,191 @@
+/* WISSEN: DAS FORMULAR "NEUE ANLEITUNG" (01.10.2026)
+
+ Filipe zum Bildschirmfoto: "erstens sieht das richtig scheisse aus".
+ Zu sehen war: die Kategorie-Auswahl lag quer ueber "STUFE", "GERAET",
+ "VEROEFFENTLICHT AM" und den Tag-Knoepfen. Text auf Text.
+
+ URSACHE (gemessen): aufgaben.css zwingt JEDES direkte Kind von
+ .neu__raster in zwei Rasterzeilen -- `grid-template-rows: 1fr 44px`.
+ Das ist richtig und wichtig, damit alle Eingabefelder auf derselben
+ Linie sitzen. Die Zelle "Hauptkategorie" enthaelt aber keine 44 px
+ hohe Eingabe, sondern .katwahl: sechs Gruppen mit zwanzig Knoepfen.
+ Die passen nicht in 44 px und laufen ueber alles darunter.
+
+ Dieselbe Falle war schon einmal da und ist im CSS dokumentiert
+ ("EINE ZELLE, DIE AUFKLAPPT, PASST NICHT IN EINE FESTE ZEILE") --
+ damals fuer die aufklappbare Personenliste. Diese Pruefung sorgt
+ dafuer, dass sie nicht ein drittes Mal zuschnappt.
+
+ GEMESSEN WIRD NICHT "sieht gut aus", sondern: Ueberlappen sich zwei
+ Felder des Rasters? Das ist eine Zahl, und sie ist eindeutig.
+
+ DREI AUSGAENGE: 0 in Ordnung / 1 Befunde / 2 konnte nicht arbeiten. */
+import { mkdtempSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+const ordner = mkdtempSync(join(tmpdir(), "ws-wissenform-"));
+process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+
+const { eigenerPort } = await import("./helfer-port.mjs");
+const PORT = await eigenerPort(import.meta, "die Wissen-Formularpruefung");
+process.env.PORT = `${PORT}`;
+process.env.SITE_ACCESS_SECRET = "lokaler-test";
+process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
+
+const express = (await import("express")).default;
+const ec = express.response.cookie;
+express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
+await import("./index.js");
+const { notbremse } = await import("./helfer-notbremse.mjs");
+notbremse(420_000, "pruef-wissen-formular");
+await new Promise((r) => setTimeout(r, 1200));
+const BASIS = `http://127.0.0.1:${PORT}`;
+
+let geprueft = 0, fehler = 0;
+const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
+
+/* ---- eine Person, die ablegen darf ---------------------------------- */
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes } = await import("node:crypto");
+await fetch(BASIS + "/workspace/api/anmelden", {
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
+for (let i = 0; i < 60; i++) {
+ try {
+ const p = new DatabaseSync(process.env.WORKSPACE_DB);
+ try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
+ } catch { await new Promise((r) => setTimeout(r, 250)); }
+}
+const d = new DatabaseSync(process.env.WORKSPACE_DB);
+const salt = randomBytes(16).toString("hex");
+const hash = scryptSync("CODE-DOGI-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
+ .run("Filipe", "admin", hash, salt, 32768, new Date().toISOString());
+d.close();
+
+const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await pw.chromium.launch();
+
+/* Die Breiten sind nicht geraten: 1165 ist die aus Filipes Bildschirmfoto,
+ 1280 der haeufigste Notebook-Wert, 900 und 412 die beiden Umbruchstufen
+ des Rasters (860 und 560) von oben bzw. unten her. */
+const BREITEN = [1440, 1280, 1165, 900, 412];
+
+for (const breite of BREITEN) {
+ const ctx = await browser.newContext({ viewport: { width: breite, height: 1000 }, hasTouch: breite <= 860 });
+ const seite = await ctx.newPage();
+ const konsole = [];
+ seite.on("pageerror", (f) => konsole.push("Absturz: " + f.message));
+ seite.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
+
+ 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.goto(BASIS + "/workspace/wissen.html", { waitUntil: "networkidle" });
+ await seite.waitForTimeout(800);
+
+ console.log(`\n--- ${breite} px ---`);
+ const aufKnopf = seite.locator("#neu-auf");
+ if (!(await aufKnopf.count()) || !(await aufKnopf.isVisible())) {
+ console.log(" KONNTE NICHT ARBEITEN: Knopf \"PDF hinzufuegen\" nicht sichtbar.");
+ await ctx.close(); await browser.close(); process.exit(2);
+ }
+ await aufKnopf.click();
+ await seite.waitForTimeout(700);
+
+ /* Eine Kategorie waehlen: Erst dann erscheint der Beschreibungstext
+ unter der Auswahl -- und genau der lag auf Filipes Bild quer ueber
+ der naechsten Ueberschrift. Ohne Auswahl waere die Pruefung blind
+ fuer den schlimmeren der beiden Faelle. */
+ const ersteKat = seite.locator("#n-kategorie button").first();
+ if (await ersteKat.count()) { await ersteKat.click(); await seite.waitForTimeout(400); }
+
+ const befund = await seite.evaluate(() => {
+ const raster = document.querySelector("#neu-form .neu__raster");
+ if (!raster) return { fehlt: true };
+ const kinder = [...raster.children];
+ const kasten = (el) => { const r = el.getBoundingClientRect();
+ return { o: r.top, u: r.bottom, l: r.left, r: r.right, h: r.height }; };
+
+ /* 1. Ueberlappen sich zwei Felder? */
+ const ueber = [];
+ for (let i = 0; i < kinder.length; i++) {
+ for (let j = i + 1; j < kinder.length; j++) {
+ const a = kasten(kinder[i]), b = kasten(kinder[j]);
+ const sen = Math.min(a.u, b.u) - Math.max(a.o, b.o);
+ const waa = Math.min(a.r, b.r) - Math.max(a.l, b.l);
+ if (sen > 1 && waa > 1) {
+ ueber.push({ a: i + 1, b: j + 1, px: Math.round(sen),
+ at: (kinder[i].textContent || "").trim().slice(0, 28),
+ bt: (kinder[j].textContent || "").trim().slice(0, 28) });
+ }
+ }
+ }
+
+ /* 2. Laeuft der INHALT der Kategorie-Auswahl aus ihr heraus?
+ ERST FALSCH GEMESSEN (01.10.2026): Der erste Versuch verglich
+ den KASTEN der Auswahl mit dem der Zelle -- und meldete gruen,
+ waehrend das Bildschirmfoto die Ueberlappung deutlich zeigte.
+ Der Kasten ist naemlich brav 44 px hoch; herausgelaufen ist
+ sein INHALT. Eine Pruefung, die den Kasten misst, kann das
+ nie sehen. Gemessen wird deshalb scrollHeight gegen
+ clientHeight und zusaetzlich das unterste Kind. */
+ const wahl = raster.querySelector(".katwahl");
+ let raus = null, ueberHang = null, hoch = null;
+ if (wahl) {
+ ueberHang = Math.round(wahl.scrollHeight - wahl.clientHeight);
+ const w = wahl.getBoundingClientRect();
+ hoch = Math.round(w.height);
+ let tiefstes = w.bottom;
+ for (const kind of wahl.children) {
+ const k = kind.getBoundingClientRect();
+ if (k.bottom > tiefstes) tiefstes = k.bottom;
+ }
+ raus = Math.round(tiefstes - w.bottom);
+ }
+ return { fehlt: false, anzahl: kinder.length, ueber, raus, ueberHang, hoch };
+ });
+
+ if (befund.fehlt) {
+ console.log(" KONNTE NICHT ARBEITEN: Das Formularraster wurde nicht gefunden.");
+ await ctx.close(); await browser.close(); process.exit(2);
+ }
+
+ ok(befund.anzahl >= 6, `das Raster hat ${befund.anzahl} Felder`);
+ ok(befund.raus !== null, `die Kategorie-Auswahl ist da (${befund.hoch} px hoch)`);
+ ok(befund.ueberHang !== null && befund.ueberHang <= 1,
+ `der Inhalt passt in die Auswahl (${befund.ueberHang} px Ueberhang)`);
+ ok(befund.raus !== null && befund.raus <= 1,
+ `nichts ragt unten heraus (${befund.raus} px)`);
+ ok(befund.ueber.length === 0,
+ `kein Feld liegt auf einem anderen (${befund.ueber.length} Ueberlappungen)`);
+ for (const u of befund.ueber) {
+ console.log(` Feld ${u.a} ("${u.at}") liegt ${u.px} px auf Feld ${u.b} ("${u.bt}")`);
+ }
+ ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
+
+ /* Zum Hinsehen, wenn eine Zahl nicht reicht: SCHUSS=ja node pruef-wissen-formular.mjs */
+ if (process.env.SCHUSS === "ja") {
+ const { mkdirSync } = await import("node:fs");
+ mkdirSync(join(process.cwd(), "schuss-wissenform"), { recursive: true });
+ await seite.locator("#neu-block").screenshot({
+ path: join(process.cwd(), "schuss-wissenform", `form-${breite}.png`) });
+ console.log(` Bild: schuss-wissenform/form-${breite}.png`);
+ }
+
+ await ctx.close();
+}
+await browser.close();
+
+console.log(`\n${geprueft} Pruefungen, ${fehler} Befunde`);
+/* Die Zahl gehoert in die BEDINGUNG: Eine Pruefung, die nichts gemessen
+ hat, darf nicht gruen sein. */
+if (geprueft < BREITEN.length * 6) {
+ console.log("ERGEBNIS: KONNTE NICHT ARBEITEN (zu wenige Messungen)");
+ process.exit(2);
+}
+console.log(fehler ? "ERGEBNIS: NICHT IN ORDNUNG" : "ERGEBNIS: IN ORDNUNG");
+process.exit(fehler ? 1 : 0);
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 09c9850c..4af532d9 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,8 +16,8 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+