diff --git a/server/mess-alle-einzelsicht.mjs b/server/mess-alle-einzelsicht.mjs
index 51c9cb48..e4ee57ab 100644
--- a/server/mess-alle-einzelsicht.mjs
+++ b/server/mess-alle-einzelsicht.mjs
@@ -235,6 +235,24 @@ try {
if (await karte.count()) {
await karte.screenshot({ path: `server/mess-alle-${wie}.png` });
console.log(` Bild: server/mess-alle-${wie}.png`);
+ /* UND DIE RUECKSEITE. Ohne sie waere nur bewiesen, dass die
+ Namen von der Vorderseite verschwunden sind -- nicht, dass
+ sie hinten ankommen. */
+ await karte.locator('.vorlagenbrett__drehknopf').click().catch(() => {});
+ await seite.waitForTimeout(600);
+ const hinten = await seite.evaluate(() => {
+ const k = document.querySelector('.vorlagenbrett__karte[data-gedreht="ja"]');
+ const r = k?.querySelector('.vorlagenbrett__seite--hinten');
+ return {
+ gedreht: !!k,
+ schild: r?.querySelector('.vorlagenbrett__wer-schild')?.textContent,
+ knoepfe: [...(r?.querySelectorAll('button') || [])].map((b) => b.textContent),
+ namen: [...(r?.querySelectorAll('.vorlagenbrett__wer-name') || [])]
+ .map((x) => x.textContent),
+ };
+ });
+ console.log(' Rueckseite:', JSON.stringify(hinten));
+ await karte.screenshot({ path: `server/mess-alle-${wie}-hinten.png` }).catch(() => {});
}
await kontext.close();
}
diff --git a/server/mess-dialog-ausgang.mjs b/server/mess-dialog-ausgang.mjs
new file mode 100644
index 00000000..9308dcda
--- /dev/null
+++ b/server/mess-dialog-ausgang.mjs
@@ -0,0 +1,226 @@
+/* =====================================================================
+ KOMMT MAN AUS EINEM FENSTER WIEDER HERAUS? (25.09.2026)
+
+ ---------------------------------------------------------------------
+ DER ANLASS
+
+ Miss, über den Support-Weg: „Ich konnte da wieder nicht zurück gehen,
+ musste die App schließen damit ich wieder auf die Hauptseite kam."
+ Dazu ein Bildschirmfoto von „Aufgabe bearbeiten" auf dem Handy.
+
+ „WIEDER" IST DAS WICHTIGE WORT: Es ist nicht das erste Mal. Ein
+ Fenster, aus dem man nur herauskommt, indem man die App beendet, ist
+ die schlimmste Sorte Sackgasse -- man verliert dabei auch alles, was
+ man gerade eingetippt hat.
+
+ ---------------------------------------------------------------------
+ WAS GEMESSEN WIRD
+
+ Für JEDES Fenster auf JEDER Seite, auf der es eines gibt:
+
+ 1. Ist ein Ausgang (Abbrechen/Schließen) überhaupt im Baum?
+ 2. Liegt er INNERHALB des sichtbaren Bereichs -- oder unterhalb?
+ 3. Und wenn er unterhalb liegt: Lässt sich dorthin SCROLLEN?
+
+ Punkt 3 ist der eigentliche: Ein Knopf weiter unten ist harmlos,
+ solange man hinkommt. `.dialog` hat aber `overflow: hidden`, und nur
+ `.dialog--breit .neu` bekommt eine Scroll-Höhe. Bei allen anderen
+ wird der Rest schlicht abgeschnitten.
+
+ GEMESSEN WIRD AUF 412 x 915 -- der häufigsten Android-Größe. Auf dem
+ Rechner fällt das nie auf: Dort ist der Bildschirm hoch genug.
+
+ Aufrufen mit: node server/mess-dialog-ausgang.mjs
+ ===================================================================== */
+import { mkdtempSync, rmSync, readFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { execFileSync } from "node:child_process";
+import { request as httpAnfrage } from "node:http";
+import { createServer as httpsServer } from "node:https";
+
+const PORT = 5399, HP = 5400;
+const CREW = "crew.dogfather-universe.com";
+const ordner = mkdtempSync(join(tmpdir(), "ws-dialog-"));
+process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+process.env.PORT = String(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 });
+};
+import { notbremse } from "./helfer-notbremse.mjs";
+await import("./index.js");
+notbremse(240_000, "mess-dialog-ausgang");
+await new Promise((r) => setTimeout(r, 900));
+process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
+process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
+
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
+const d = new DatabaseSync(process.env.WORKSPACE_DB);
+const jetzt = new Date().toISOString();
+const sch = randomBytes(32).toString("hex");
+d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
+ .run("code_kennung_schluessel", sch, jetzt);
+const anlegen = (name, rolle, code) => {
+ const s = randomBytes(16).toString("hex");
+ const h = scryptSync(code, s, 64,
+ { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+ return d.prepare("INSERT INTO personen"
+ + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ + " VALUES (?,?,?,?,?,?,1,?) RETURNING id")
+ .get(name, rolle, h, s, 32768,
+ createHmac("sha256", sch).update(code).digest("hex"), jetzt).id;
+};
+const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
+const idModi = anlegen("Miss", "modi", "CODE-MODI-0001");
+/* Eine Aufgabe, die dem Modi gehoert -- sonst gibt es nichts zu
+ bearbeiten, und die Messung waere gruen ueber einer Null. */
+d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet,
+ verantwortlich_id, frist, erstellt, erstellt_von, haus)
+ VALUES (?,?, 'offen', 'hoch', ?, ?, ?, ?, 'crew')`)
+ .run("Die Regeln des Rudels einmal ganz lesen",
+ "Nicht überfliegen. Wer sie kennt, kann sie erklären – und Erklären ist in "
+ + "neun von zehn Fällen wirksamer als Löschen.",
+ idModi, "2026-09-26", jetzt, idDogi);
+d.close();
+
+const anfrage = (pfad, art, koerper) => new Promise((fertig, schief) => {
+ const a = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: pfad, method: art,
+ headers: { Host: CREW, "Content-Type": "application/json" },
+ }, (r) => {
+ let t = ""; r.on("data", (x) => { t += x; });
+ r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t }));
+ });
+ a.on("error", schief);
+ if (koerper) a.write(JSON.stringify(koerper));
+ a.end();
+});
+const an = await anfrage("/workspace/api/anmelden", "POST",
+ { rolle: "modi", code: "CODE-MODI-0001" });
+if (an.code !== 200) { console.log(`ABBRUCH: Anmeldung ${an.code}`); process.exit(2); }
+const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
+
+const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
+execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl,
+ "-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
+{ stdio: "ignore" });
+const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) },
+ (von, nach) => {
+ const w = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
+ headers: { ...von.headers, host: `${CREW}:${HP}` },
+ }, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
+ von.pipe(w);
+ });
+await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
+
+const pw = await import(
+ "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await pw.chromium.launch({
+ args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
+});
+/* Die Hoehen, die es wirklich gibt -- nicht die aus dem Datenblatt.
+ `app` ist die installierte App (nur die Systemleiste), `browser`
+ zusaetzlich mit Adresszeile, `klein` ein aelteres Geraet, `tastatur`
+ dasselbe Geraet mit offener Bildschirmtastatur. Die letzte ist die
+ wichtigste: Man hat ja gerade etwas eingetippt. */
+const GROESSEN = [
+ ["app", 412, 915],
+ ["browser", 412, 780],
+ ["klein", 360, 640],
+ ["tastatur", 412, 430],
+];
+let schlimm = 0;
+try {
+ /* MEHRERE GROESSEN UND NICHT EINE (25.09.2026). Der erste Anlauf
+ mass nur 412 x 915 -- die Angabe aus dem Geraetedatenblatt. Dort
+ passte es KNAPP: 782 px Inhalt in 784 px Fenster, Ausgang gerade
+ noch sichtbar, und die Messung meldete "JA".
+
+ Das ist der BESTFALL und nicht der Alltag: Im Browser steht die
+ Adresszeile darueber, in der App die Systemleiste, und beim Tippen
+ kommt die Tastatur. Jede dieser Hoehen ist kleiner. Eine Messung,
+ die nur den guenstigsten Fall kennt, sagt genau dann "in Ordnung",
+ wenn der Nutzer schon feststeckt. */
+ for (const [wie, breite, hoehe] of GROESSEN) {
+ const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
+ await kontext.addCookies(keks.split("; ").map((k) => {
+ const [n, ...r] = k.split("=");
+ return { name: n, value: r.join("="), domain: CREW, path: "/" };
+ }));
+ const seite = await kontext.newPage();
+ await seite.goto(`https://${CREW}:${HP}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1200);
+
+ /* Das Bearbeiten-Fenster oeffnen -- ueber den Stift an der Aufgabe,
+ genau so wie Miss es getan hat. */
+ const stift = seite.locator('.aufgabe [title*="earbeit"], .aufgabe button').first();
+ if (await stift.count()) { await stift.click().catch(() => {}); }
+ await seite.waitForTimeout(400);
+ const offen = await seite.evaluate(() => {
+ const dlg = document.getElementById("bearbeiten-dialog");
+ if (dlg && !dlg.open) { try { dlg.showModal(); } catch { /* egal */ } }
+ return !!(dlg && dlg.open);
+ });
+ await seite.waitForTimeout(500);
+
+ const mass = await seite.evaluate(() => {
+ const dlg = document.getElementById("bearbeiten-dialog");
+ if (!dlg || !dlg.open) return null;
+ const raus = dlg.querySelector("#b-abbrechen");
+ const rd = raus ? raus.getBoundingClientRect() : null;
+ const dd = dlg.getBoundingClientRect();
+ /* WELCHES ELEMENT KANN ROLLEN? Gefragt wird der gemessene Zustand
+ (scrollHeight > clientHeight UND overflow erlaubt es), nicht die
+ Absicht im Stilblatt. */
+ const rollt = (el) => {
+ if (!el) return false;
+ const s = getComputedStyle(el);
+ const geht = /auto|scroll/.test(s.overflowY);
+ return geht && el.scrollHeight - el.clientHeight > 4;
+ };
+ const form = dlg.querySelector("form");
+ return {
+ fensterHoehe: window.innerHeight,
+ dialogOben: Math.round(dd.top), dialogUnten: Math.round(dd.bottom),
+ dialogHoehe: Math.round(dd.height),
+ inhaltHoehe: form ? form.scrollHeight : null,
+ dialogOverflow: getComputedStyle(dlg).overflowY,
+ formOverflow: form ? getComputedStyle(form).overflowY : null,
+ ausgangDa: !!raus,
+ ausgangUnten: rd ? Math.round(rd.bottom) : null,
+ ausgangSichtbar: !!(rd && rd.bottom <= window.innerHeight + 1 && rd.top >= -1
+ && rd.width > 0 && rd.height > 0),
+ dialogRollt: rollt(dlg),
+ formRollt: rollt(form),
+ };
+ });
+
+ console.log(`\n=== Aufgabe bearbeiten, ${breite} x ${hoehe} (${wie}) ===\n`);
+ if (!offen || !mass) {
+ console.log(" ABBRUCH: Das Fenster liess sich nicht oeffnen -- gemessen wurde nichts.");
+ schlimm = 1;
+ } else {
+ for (const [k, v] of Object.entries(mass)) console.log(` ${k.padEnd(16)} ${v}`);
+ const rauskommen = mass.ausgangSichtbar || mass.dialogRollt || mass.formRollt;
+ console.log(`\n ==> Kommt man heraus? ${rauskommen ? "JA" : "NEIN -- SACKGASSE"}`);
+ if (!rauskommen) schlimm = 1;
+ await seite.locator("#bearbeiten-dialog")
+ .screenshot({ path: `server/mess-dialog-${wie}.png` }).catch(() => {});
+ console.log(` Bild: server/mess-dialog-${wie}.png`);
+ }
+ await kontext.close();
+ }
+} finally {
+ await browser.close();
+ vorbau.close();
+ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+ process.exit(schlimm);
+}
diff --git a/server/mess-eingang-optik.mjs b/server/mess-eingang-optik.mjs
new file mode 100644
index 00000000..296dc60d
--- /dev/null
+++ b/server/mess-eingang-optik.mjs
@@ -0,0 +1,182 @@
+/* =====================================================================
+ WIE DER EINGANG AUSSIEHT (25.09.2026)
+
+ Filipe zum Bildschirmfoto der Seite: „lass das bitte auch geiler
+ aussehen bitte."
+
+ SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder,
+ auf denen man nachsehen kann. Deshalb `mess-` und nicht `pruef-`.
+
+ ZWEI BREITEN, und die kleinere ist die wichtigere: Ab 760 px steht
+ das Bild neben dem Text, darunter untereinander. Wer nur die grosse
+ ansieht, sieht die Haelfte der Regel nicht.
+
+ EIGENE WEGWERF-DATENBANK: Es entsteht eine echte Meldung mit Bild.
+ In einem laufenden System waeren das Testdaten, und die sind
+ verboten. Hier liegt alles in einem `mktemp`-Ordner.
+
+ Aufrufen mit: node server/mess-eingang-optik.mjs
+ ===================================================================== */
+import { mkdtempSync, rmSync, readFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { execFileSync } from "node:child_process";
+import { request as httpAnfrage } from "node:http";
+import { createServer as httpsServer } from "node:https";
+
+const PORT = 5401, HP = 5402;
+const CREW = "crew.dogfather-universe.com";
+const ordner = mkdtempSync(join(tmpdir(), "ws-eingang-"));
+process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+process.env.PORT = String(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 });
+};
+import { notbremse } from "./helfer-notbremse.mjs";
+await import("./index.js");
+notbremse(240_000, "mess-eingang-optik");
+await new Promise((r) => setTimeout(r, 900));
+process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
+process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
+
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
+const d = new DatabaseSync(process.env.WORKSPACE_DB);
+const jetzt = new Date().toISOString();
+const sch = randomBytes(32).toString("hex");
+d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
+ .run("code_kennung_schluessel", sch, jetzt);
+const anlegen = (name, rolle, code) => {
+ const s = randomBytes(16).toString("hex");
+ const h = scryptSync(code, s, 64,
+ { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+ return d.prepare("INSERT INTO personen"
+ + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ + " VALUES (?,?,?,?,?,?,1,?) RETURNING id")
+ .get(name, rolle, h, s, 32768,
+ createHmac("sha256", sch).update(code).digest("hex"), jetzt).id;
+};
+anlegen("Filipe", "admin", "CODE-DOGI-0001");
+const idModi = anlegen("Miss", "modi", "CODE-MODI-0001");
+const idM2 = anlegen("Marina", "modi", "CODE-MODI-0002");
+
+/* Ein winziges PNG als Anhang -- es geht um die FORM der Karte, nicht
+ um das Motiv. 1x1 Bildpunkt reicht dafuer nicht: Der Kasten waere
+ dann zwei Pixel gross und man saehe die Spaltenaufteilung nicht.
+ Deshalb ein hochkantes Rechteck, so wie ein Handy-Bildschirmfoto. */
+const png = Buffer.from(
+ "iVBORw0KGgoAAAANSUhEUgAAAGQAAADwCAYAAAB7ovsMAAAAWklEQVR42u3QMQEAAAjAoNu/"
+ + "tBmeIAfpVoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAA"
+ + "AQIECBAgQIAAAQIECBAgQIAAAe4W1H8B4aXvOgAAAABJRU5ErkJggg==", "base64");
+d.prepare(`INSERT INTO support_meldungen
+ (person_id, text, seite, bild_datei, bild_typ, bild_breite, bild_hoehe,
+ stand, erstellt, geaendert)
+ VALUES (?,?,?,?,?,?,?,?,?,?)`)
+ .run(idModi,
+ "Ich konnte da wieder nicht zurück gehen, musste die App schließen "
+ + "damit ich wieder auf die Hauptseite kam",
+ "/workspace/support.html", "probe.png", "image/png", 100, 240,
+ "in_arbeit", jetzt, jetzt);
+d.prepare(`INSERT INTO support_meldungen
+ (person_id, text, seite, stand, erstellt, geaendert)
+ VALUES (?,?,?,?,?,?)`)
+ .run(idM2, "Der Kalender springt beim Wischen manchmal zwei Monate weiter.",
+ "/workspace/kalender.html", "neu", jetzt, jetzt);
+/* Die Bilddatei dort ablegen, wo die Route sie sucht. */
+const { DATEN_ORDNER } = await import("./workspace.js");
+try {
+ const { mkdirSync, writeFileSync } = await import("node:fs");
+ const ziel = join(DATEN_ORDNER, "support");
+ mkdirSync(ziel, { recursive: true });
+ writeFileSync(join(ziel, "probe.png"), png);
+} catch (f) { console.log("Bild nicht abgelegt:", f?.message); }
+d.close();
+
+const anfrage = (pfad, art, koerper) => new Promise((fertig, schief) => {
+ const a = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: pfad, method: art,
+ headers: { Host: CREW, "Content-Type": "application/json" },
+ }, (r) => {
+ let t = ""; r.on("data", (x) => { t += x; });
+ r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t }));
+ });
+ a.on("error", schief);
+ if (koerper) a.write(JSON.stringify(koerper));
+ a.end();
+});
+const an = await anfrage("/workspace/api/anmelden", "POST",
+ { rolle: "admin", code: "CODE-DOGI-0001" });
+if (an.code !== 200) { console.log(`ABBRUCH: Anmeldung ${an.code}`); process.exit(2); }
+const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
+
+const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
+execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl,
+ "-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
+{ stdio: "ignore" });
+const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) },
+ (von, nach) => {
+ const w = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
+ headers: { ...von.headers, host: `${CREW}:${HP}` },
+ }, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
+ von.pipe(w);
+ });
+await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
+
+const pw = await import(
+ "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await pw.chromium.launch({
+ args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
+});
+try {
+ for (const [wie, breite, hoehe] of [["computer", 1280, 1100], ["handy", 412, 915]]) {
+ const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
+ await kontext.addCookies(keks.split("; ").map((k) => {
+ const [n, ...r] = k.split("=");
+ return { name: n, value: r.join("="), domain: CREW, path: "/" };
+ }));
+ const seite = await kontext.newPage();
+ const konsole = [];
+ seite.on("console", (e) => {
+ if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(e.text())) {
+ konsole.push(e.text().slice(0, 140));
+ }
+ });
+ await seite.goto(`https://${CREW}:${HP}/workspace/support.html`, { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1400);
+ const blick = await seite.evaluate(() => {
+ const block = document.getElementById("eingang-block");
+ const karten = [...document.querySelectorAll(".s-karte")];
+ const mitBild = karten.find((k) => k.querySelector(".s-karte__bild"));
+ const spalten = mitBild ? getComputedStyle(mitBild).gridTemplateColumns : "-";
+ return {
+ eingangSichtbar: !!(block && !block.hidden),
+ karten: karten.length,
+ mitBild: !!mitBild,
+ spalten,
+ felder: document.querySelectorAll(".s-bilanz__feld").length,
+ leerMarkiert: document.querySelectorAll('.s-bilanz__feld[data-leer="ja"]').length,
+ };
+ });
+ console.log(`\n=== Eingang, ${breite} x ${hoehe} (${wie}) ===`);
+ for (const [k, v] of Object.entries(blick)) console.log(` ${k.padEnd(16)} ${v}`);
+ if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole);
+ const ziel = seite.locator("#eingang-block");
+ if (await ziel.count()) {
+ await ziel.screenshot({ path: `server/mess-eingang-${wie}.png` }).catch(() => {});
+ console.log(` Bild: server/mess-eingang-${wie}.png`);
+ }
+ await kontext.close();
+ }
+} finally {
+ await browser.close();
+ vorbau.close();
+ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+ process.exit(0);
+}
diff --git a/server/pruef-abbrechen-optik.mjs b/server/pruef-abbrechen-optik.mjs
index a2f1ef11..f86b30ae 100644
--- a/server/pruef-abbrechen-optik.mjs
+++ b/server/pruef-abbrechen-optik.mjs
@@ -49,6 +49,48 @@ notbremse(480_000, "pruef-abbrechen-optik");
await new Promise((r) => setTimeout(r, 900));
const BASIS = `http://127.0.0.1:${PORT}`;
+/* ==== DER ANZEIGENAME FUER „review" -- ABGELESEN, NICHT GETIPPT =====
+
+ Er steht in `STATUS_NAME` in workspace/assets/js/aufgaben.js und
+ hiess bis zum 20.09.2026 „Review", seither „Zur Freigabe". Diese
+ Pruefung hatte ihn abgeschrieben und war seitdem rot.
+
+ GELESEN WIRD DIE DATEI, nicht importiert: aufgaben.js ist ein
+ Browser-Skript (eine IIFE ohne Export) -- ein `import` bekaeme dort
+ nichts. Ein Auszug aus dem Quelltext ist der einzige Weg, der ohne
+ eine zweite Wahrheit auskommt.
+
+ FINDET ER NICHTS, BRICHT DIE PRUEFUNG AB. Ein stiller Rueckfall auf
+ „Review" waere genau der Zustand, der hier gerade repariert wird:
+ ein Wort, das niemand mehr pflegt. */
+const WORT_REVIEW = await (async () => {
+ const { readFileSync } = await import("node:fs");
+ const { fileURLToPath } = await import("node:url");
+ const { dirname, join: verbinde } = await import("node:path");
+ const hier = dirname(fileURLToPath(import.meta.url));
+ const quelle = readFileSync(
+ verbinde(hier, "..", "workspace", "assets", "js", "aufgaben.js"), "utf8");
+ /* GESUCHT WIRD IN STATUS_WORT, nicht irgendwo. Der erste Anlauf
+ nahm das erste `review:` der Datei -- und das gehoert einer
+ Uebergangstabelle (`review: 'erledigt'`, was der naechste
+ Schritt ist, nicht der Name). Die Pruefung las dadurch
+ "erledigt" und blieb rot. Ein Ausdruck, der die falsche
+ Stelle trifft, ist schlimmer als keiner: Er sieht aus wie
+ eine Ableitung. */
+ /* SIE HEISST STATUS_NAME, nicht STATUS_WORT -- die zweite
+ heisst so in entwicklung.js. Zwei Dateien, zwei Namen fuer
+ dieselbe Tafel; nachgesehen statt erinnert. */
+ const tafel = /STATUS_NAME\s*=\s*{([^}]*)}/.exec(quelle);
+ const treffer = tafel && /review:\s*'([^']+)'/.exec(tafel[1]);
+ if (!treffer) {
+ console.error("ABBRUCH: In aufgaben.js steht kein Anzeigename fuer `review`. "
+ + "Wurde STATUS_NAME umbenannt? Ohne ihn prueft diese Datei nichts.");
+ process.exit(3);
+ }
+ return treffer[1];
+})();
+console.log(` -- Anzeigename fuer "review": "${WORT_REVIEW}" (aus aufgaben.js gelesen)`);
+
const PROTOKOLL = join(process.cwd(), "pruef-abbrechen-optik-lauf.txt");
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
const melde = (z) => {
@@ -172,7 +214,17 @@ melde("\n=== Der Dialog verlangt einen Grund ===");
/* Sagt er, was passiert -- und was NICHT passiert? Die Sorge bei so
einem Knopf ist immer, ob etwas verloren geht. */
const was = await seite.$eval("#abbruch-was", (e) => e.textContent);
- ok(/nicht gelöscht/i.test(was) && /Review/.test(was),
+ /* „REVIEW" HEISST SEIT DEM 20.09.2026 „ZUR FREIGABE" (nachgezogen
+ 25.09.2026). Die Oberflaeche wurde umbenannt, diese Pruefung
+ nicht -- sie war seither rot, ohne dass etwas kaputt war, und
+ eine Warnung, die immer kommt, wird ueberlesen.
+
+ GEPRUEFT WIRD GEGEN DIE EINE QUELLE, nicht gegen ein zweites
+ abgeschriebenes Wort: `STATUS_WORT` in aufgaben.js ist die Stelle,
+ an der der Anzeigename steht. Wird er morgen wieder geaendert,
+ zieht diese Zeile von selbst mit -- und wer ihn aendert, ohne
+ dass der Dialog mitgeht, wird hier rot. */
+ ok(/nicht gelöscht/i.test(was) && new RegExp(WORT_REVIEW).test(was),
`er erklärt es: "${was.trim().slice(0, 95)}…"`);
/* Ohne Grund abschicken -- der Dialog muss offen bleiben. */
@@ -236,7 +288,7 @@ melde("\n=== Der Bereich 'Abgebrochen' ===");
ok(/Kampagne verschoben/.test(grund), `der Grund steht dran: "${grund}"`);
const vorher = await seite.$eval(".abbruch-karte__vorher", (e) => e.textContent.trim());
- ok(vorher === "Review", `und in welchem Status sie war: "${vorher}"`);
+ ok(vorher === WORT_REVIEW, `und in welchem Status sie war: "${vorher}"`);
const wer = await seite.$eval(".abbruch-karte__wer", (e) => e.textContent.trim());
ok(/Filipe/.test(wer), `und wer es war: "${wer}"`);
diff --git a/server/pruef-modi-katalog.mjs b/server/pruef-modi-katalog.mjs
index e1d2a0e1..2a1439c5 100644
--- a/server/pruef-modi-katalog.mjs
+++ b/server/pruef-modi-katalog.mjs
@@ -822,6 +822,78 @@ console.log("");
ok(/Marina/.test(nachWahl.liste),
'die Aufgabenliste darunter gehoert ihr ("' + nachWahl.liste + '")');
+ /* ==== DIE KACHEL DREHT SICH UM (25.09.2026) ======================
+
+ Filipe: „wenn ich drauf drücke dreht sich die kachel und dan seh
+ ich wer es gemacht hat, und immer noch die option es nochmal zu
+ verteilen falls neue leute ins team zustoßen."
+
+ DREI DINGE, UND JEDES EINZELN:
+ 1. Bei „Alle" gibt es ueberhaupt einen Weg auf die Rueckseite.
+ 2. Dort stehen die NAMEN -- so viele, wie es Zielbare gibt.
+ 3. Und ein Knopf, der wieder verteilt.
+
+ DIE ZAHL GEHOERT IN DIE BEDINGUNG: Eine leere Rueckseite wuerde
+ sonst als "gedreht, alles gut" durchgehen -- der Fehler, der in
+ diesem Haus am 01.09. dreimal gruen dastand. */
+ await seite.evaluate(() => {
+ document.querySelector('.stufenleiter--wer [data-alle="ja"]')?.click();
+ });
+ await seite.waitForTimeout(900);
+ const dreh = await seite.evaluate(() => {
+ const k = document.querySelector('.vorlagenbrett__karte[data-dreh="ja"]');
+ if (!k) return { drehbar: false };
+ const knopf = k.querySelector(".vorlagenbrett__drehknopf");
+ knopf?.click();
+ const hinten = k.querySelector(".vorlagenbrett__seite--hinten");
+ return {
+ drehbar: true,
+ gedreht: k.dataset.gedreht === "ja",
+ namen: [...(hinten?.querySelectorAll(".vorlagenbrett__wer-name") || [])]
+ .map((x) => x.textContent),
+ knoepfe: [...(hinten?.querySelectorAll("button") || [])]
+ .map((b) => b.textContent.trim()),
+ /* Und vorne darf danach NICHTS mehr von der Rueckseite stehen
+ -- sonst waere es kein Umdrehen, sondern ein Aufklappen. */
+ vorneNamen: k.querySelectorAll(
+ ".vorlagenbrett__seite--vorn .vorlagenbrett__wer-name").length,
+ };
+ });
+ ok(dreh.drehbar, "bei \u201eAlle\u201c ist die Kachel drehbar");
+ ok(dreh.gedreht === true, `und ein Druck dreht sie um (${dreh.gedreht})`);
+ ok((dreh.namen || []).length >= 2,
+ `hinten stehen die Namen (${(dreh.namen || []).join(", ") || "keine"})`);
+ ok((dreh.knoepfe || []).some((t) => /Nachholen|Nochmal/.test(t))
+ && (dreh.knoepfe || []).some((t) => /Zur\u00fcck/.test(t)),
+ `mit Verteil- und Zurueck-Knopf (${(dreh.knoepfe || []).join(" | ") || "keine"})`);
+ ok(dreh.vorneNamen === 0,
+ `und vorne steht kein Name mehr (${dreh.vorneNamen})`);
+
+ /* ==== UND DIE AUSWAHL WIEDER ZURUECKSTELLEN =====================
+
+ Der Klick auf "Alle" hat die gewaehlte Person veraendert -- und
+ die Pruefungen weiter unten arbeiten mit Marina. Beim ersten
+ Anlauf fielen dadurch zwei Zeilen um ("die Aufgabe liegt in
+ ihrer Liste (2 -> 2)"), und zwar an Stellen, an denen nichts
+ kaputt war. Eine Pruefung, die den Zustand fuer die naechste
+ verstellt, misst deren Ergebnis mit.
+
+ ES WIRD NACHGEWIESEN, NICHT GEHOFFT: Ohne die Zeile darunter
+ waere ein misslungenes Zuruecksetzen wieder ein Fehler an der
+ falschen Stelle -- und man suchte ihn dort. */
+ await seite.evaluate(() => {
+ const b = [...document.querySelectorAll(".stufenleiter--wer .stufenleiter__knopf")]
+ .find((x) => /Marina/.test(x.textContent));
+ b?.click();
+ });
+ await seite.waitForTimeout(900);
+ const zurueckGestellt = await seite.evaluate(() =>
+ [...document.querySelectorAll(".stufenleiter--wer .stufenleiter__knopf")]
+ .filter((x) => x.getAttribute("aria-pressed") === "true")
+ .map((x) => x.textContent.trim().replace(/\s+/g, " ")));
+ ok(zurueckGestellt.some((t) => /Marina/.test(t)),
+ `die Auswahl steht wieder auf Marina (${zurueckGestellt.join(", ") || "keine"})`);
+
/* GEZAEHLT WIRD IN IHRER LISTE, NICHT AUF DEM BRETT.
Auf "Aufgaben" stand unter dem Katalog das Brett, und die
diff --git a/server/pruef-modi-verborgen.mjs b/server/pruef-modi-verborgen.mjs
index a8ca27ca..6835fedb 100644
--- a/server/pruef-modi-verborgen.mjs
+++ b/server/pruef-modi-verborgen.mjs
@@ -325,11 +325,33 @@ console.log("\n=== 6. Was ein Modi auf seiner Startseite sieht ===\n");
/* Nichts aus der Agentur. Nicht "moeglichst wenig", sondern keine
einzige -- diese Seiten drehen sich um betreute Creator, um
Rechte oder um Automationen. */
+ /* `calls.html` STAND HIER UND IST AM 25.09.2026 HERAUSGENOMMEN.
+
+ Nicht, weil die Pruefung zu streng war, sondern weil sich die
+ Sache geaendert hat: Filipe hat die Calls-Kachel ausdruecklich
+ fuer alle verlangt, die einen Kalender haben („jeder der einen
+ kalender hat soll auch sowas haben danke"), und ein Modi hat
+ einen. Seither war diese Zeile rot, ohne dass etwas kaputt war
+ -- und eine Warnung, die immer kommt, wird ueberlesen.
+
+ DIE LISTE BLEIBT EINE LISTE VON HAND, und das ist ihre Schwaeche:
+ Sie altert genau so wie diese Zeile gealtert ist. Deshalb steht
+ darunter eine Gegenprobe -- ohne sie waere „nichts Verbotenes
+ dabei" auch dann gruen, wenn die Pruefung gar nichts mehr
+ prueft. */
const verboten = ["personen.html", "scouting.html", "automation.html",
- "report.html", "calls.html", "uebersicht.html", "content.html"];
- const drin = (b || []).map((x) => x.ziel).filter((z) => verboten.includes(z));
- ok(Array.isArray(b) && drin.length === 0,
- drin.length ? `ABER AUCH: ${drin.join(", ")} -- FEHLER` : "und nichts aus der Agentur");
+ "report.html", "uebersicht.html", "content.html"];
+ const ziele = (b || []).map((x) => x.ziel);
+ const drin = ziele.filter((z) => verboten.includes(z));
+ ok(Array.isArray(b) && verboten.length >= 6 && drin.length === 0,
+ drin.length ? `ABER AUCH: ${drin.join(", ")} -- FEHLER`
+ : `und nichts aus der Agentur (${verboten.length} Seiten geprueft)`);
+ /* GEGENPROBE: Haette er eine davon, MUSS die Zeile darueber
+ anschlagen. Gepruft wird an einer erfundenen Liste, nicht an
+ echten Daten -- es wird nichts angelegt und nichts veraendert. */
+ const alsHaette = [...ziele, "scouting.html"].filter((z) => verboten.includes(z));
+ ok(alsHaette.length === 1,
+ `und sie wuerde es merken, wenn doch (Probe: ${alsHaette.join(", ") || "merkt nichts"})`);
/* Die Beschriftung gehoert zum Empfaenger: "Alle Creator auf einen
Blick" waere fuer jemanden ohne Creator schlicht falsch. */
ok((b || []).every((x) => x.name && x.unter && x.zeichen && x.ziel),
@@ -409,6 +431,44 @@ console.log("\n=== 7. Dieselbe Seite mit einem echten Browser ===\n");
await seite.screenshot({ path: "pruef-modi-start.png", fullPage: true });
console.log(" -- Bildschirmfoto: server/pruef-modi-start.png");
+
+ /* ==== DER CREATOR-KATALOG GEHOERT IHM NICHT (25.09.2026) ========
+
+ Filipe, zu einem Bildschirmfoto der Aufgabenseite: „es gibt
+ keine creator auf dieser seite. die app ist nur fuer modis und
+ community also steht das falsch da."
+
+ Dort stand ein Katalog mit vier Bereichen und vier Stufen -- der
+ fuer CREATOR -- und darunter „Waehle oben einen Creator, dann
+ lassen sich diese Aufgaben uebernehmen". Auf dieser Adresse gibt
+ es keine; der Satz forderte zu etwas auf, das nie geht.
+
+ GEPRUEFT WIRD DER KASTEN UND DER SATZ, nicht nur einer von
+ beiden: Verschwindet der Satz und der leere Kasten bleibt, ist
+ es dieselbe Sackgasse in leise. */
+ await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1200);
+ const katalog = await seite.evaluate(() => {
+ const kasten = document.getElementById("vorlagenbrett");
+ return {
+ daUeberhaupt: !!kasten,
+ sichtbar: !!(kasten && !kasten.hidden && kasten.offsetHeight > 0),
+ text: (document.body.textContent || "").includes("Wähle oben einen Creator"),
+ /* GEGENPROBE IM SELBEN ZUG: Sein EIGENES Aufgabenbrett muss da
+ sein. Waere die ganze Seite leer, meldeten die zwei Zeilen
+ darueber brav "nichts Fremdes zu sehen". */
+ eigenes: !!document.querySelector("#brett, .spalten, .aufgabe, #aufgaben"),
+ };
+ });
+ ok(katalog.daUeberhaupt,
+ "der Kasten fuer den Katalog steht im Bauplan der Seite (Gegenprobe)");
+ ok(!katalog.sichtbar,
+ `der Creator-Katalog wird ihm NICHT gezeigt (sichtbar: ${katalog.sichtbar})`);
+ ok(!katalog.text,
+ "und der Satz „Waehle oben einen Creator“ steht nirgends");
+ ok(katalog.eigenes,
+ "sein eigenes Aufgabenbrett ist trotzdem da (Gegenprobe)");
+
await kontext.close();
} finally {
await browser.close();
@@ -434,7 +494,27 @@ console.log("\n=== 8. Aufgaben, Einträge und Dateien eines Modis ===\n");
method: "POST", headers: { "Content-Type": "application/json", cookie: an.keks },
body: JSON.stringify({ titel: MERKMAL + " Aufgabe" }),
});
- ok(t1.status === 201, `der Modi legt eine Aufgabe an (${t1.status})`);
+ /* ==== UMGEDREHT -- UND STRENGER (25.09.2026) ====================
+
+ Hier stand `status === 201`: „der Modi legt eine Aufgabe an". Das
+ war bis zum 22.09.2026 richtig. An dem Tag hat Filipe es
+ abgeschafft: „sie sich nicht selber aufgaben geben" und „die modis
+ sollen nur diese aufgaben wo sie von den drauf markiert werden
+ annehmen koennen". Seither antwortet der Server mit 403 -- und
+ diese Zeile war seitdem rot, ohne dass etwas kaputt war.
+
+ SIE PRUEFT JETZT MEHR ALS VORHER, nicht weniger: nicht nur die
+ Nummer, sondern auch das WORT. Eine 403 aus einem anderen Grund
+ (etwa weil die Anmeldung kaputt ist) saehe sonst genauso aus wie
+ die gewollte Absage -- und die Pruefung waere gruen ueber einem
+ ganz anderen Fehler.
+
+ WARUM DAS WORT ZAEHLT: `nur_leitung_legt_an` hat in meldung.js
+ einen ganzen Satz („Aufgaben werden dir zugeteilt -- anlegen
+ kannst du hier keine"). Eine nackte 403 haette ihn nicht. */
+ const t1Fehler = (await t1.json().catch(() => ({})))?.fehler;
+ ok(t1.status === 403 && t1Fehler === "nur_leitung_legt_an",
+ `der Modi legt KEINE Aufgabe an -- mit Begruendung (${t1.status} ${t1Fehler || "ohne Wort"})`);
/* (b) Ein Bereichs-Eintrag -- ebenfalls ueber den normalen Weg. */
const t2 = await fetch(BASIS + "/workspace/api/bereich/live", {
diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js
index cc03d844..8cdb0183 100644
--- a/server/workspace-aufgaben.js
+++ b/server/workspace-aufgaben.js
@@ -902,13 +902,17 @@ aufgabenRouter.post("/workspace/api/aufgaben", gleicheHerkunft, (req, res) => {
INSERT INTO aufgaben
(titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
creator_extern, verantwortlich_extern, frist, kategorie, kanal,
- erstellt, erstellt_von)
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
+ erstellt, erstellt_von, haus)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
+ /* DAS HAUS WIRD BEIM ANLEGEN GESCHRIEBEN (25.09.2026) -- siehe
+ das letzte Argument. Ohne es bekaeme eine Aufgabe, an der nur
+ DogFather haengt (Pool-Aufgabe ohne Verantwortlichen), kein
+ Haus -- und waere in beiden sichtbar. */
aus.titel, aus.beschreibung ?? null, aus.status ?? "offen",
aus.prioritaet ?? "mittel", aus.creator_id ?? null, aus.verantwortlich_id ?? null,
aus.creator_extern ?? null, aus.verantwortlich_extern ?? null,
aus.frist ?? null, aus.kategorie ?? null, aus.kanal ?? null,
- jetzt(), req.person.id);
+ jetzt(), req.person.id, req.person.haus || null);
/* AN MENSCHEN VERTEILEN (21.09.2026, Abschnitt 3).
Nur wer verteilen darf -- sonst koennte sich jemand selbst eine
diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js
index 00fe270a..8907e857 100644
--- a/server/workspace-bereiche.js
+++ b/server/workspace-bereiche.js
@@ -2125,10 +2125,11 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (r
die Zeile laesst sich hinterher nicht mehr aendern. */
if (!Object.hasOwn(einstellung.arten, v.art)) continue;
const { lastInsertRowid } = db().prepare(`
- INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von)
- VALUES (?,?,?,?,?, 'offen', ?, ?)`)
+ INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt,
+ erstellt_von, haus)
+ VALUES (?,?,?,?,?, 'offen', ?, ?, ?)`)
.run(bereich, v.art, v.titel, v.text ?? null,
- jetzt().slice(0, 10), jetzt(), req.person.id);
+ jetzt().slice(0, 10), jetzt(), req.person.id, req.person.haus || null);
/* UND SOFORT FREIGEBEN (17.09.2026) -- der Fehler, den Filipe
auf seinem Bildschirm gesehen hat.
@@ -2237,11 +2238,12 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/:id/daraus",
const { lastInsertRowid } = db().prepare(`
INSERT INTO eintraege (bereich, art, titel, text, datum, status,
- erstellt, erstellt_von, aus_eintrag_id)
- VALUES (?,?,?,?,?, 'offen', ?,?,?)`)
+ erstellt, erstellt_von, aus_eintrag_id, haus)
+ VALUES (?,?,?,?,?, 'offen', ?,?,?,?)`)
.run(nach, art, quelle.titel,
String(req.body?.text ?? "").trim().slice(0, TEXT_MAX) || null,
- jetzt().slice(0, 10), jetzt(), req.person.id, quelle.id);
+ jetzt().slice(0, 10), jetzt(), req.person.id, quelle.id,
+ req.person.haus || null);
protokolliere("bereich_daraus", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
@@ -2500,8 +2502,8 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
INSERT INTO eintraege
(bereich, art, titel, text, datum, uhrzeit, bewertung, dringlichkeit, status,
creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant,
- event_ende, event_aufgaben, event_regeln, einsatz, nur_leitung)
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
+ event_ende, event_aufgaben, event_regeln, einsatz, nur_leitung, haus)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
bereich, aus.art, aus.titel, aus.text ?? null, aus.datum, aus.uhrzeit ?? null,
aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen",
aus.creator_id ?? null, aus.creator_extern ?? null, jetzt(), req.person.id,
@@ -2512,7 +2514,9 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
unbekannt ist bei einer Vertraulichkeit die falsche Antwort.
Wer nichts waehlt, schreibt fuers Team -- das ist der offene
Fall, und der soll der Normalfall sein. */
- aus.nur_leitung ?? 0);
+ aus.nur_leitung ?? 0,
+ /* Das Haus aus der Adresse -- letzte Spalte, letzter Wert. */
+ req.person.haus || null);
protokolliere("eintrag_angelegt", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
@@ -2686,10 +2690,11 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/:id(\\d+)/bild",
const info = db().prepare(
"INSERT INTO dateien (name_original, name_datei, groesse, typ, status,"
- + " notiz, hochgeladen_von, erstellt, eintrag_id)"
- + " VALUES (?,?,?,?,'entwurf',NULL,?,?,?)")
+ + " notiz, hochgeladen_von, erstellt, eintrag_id, haus)"
+ + " VALUES (?,?,?,?,'entwurf',NULL,?,?,?,?)")
.run(name, dateiname, req.body.length, erkannt.typ,
- req.person.id, new Date().toISOString(), eintrag.id);
+ req.person.id, new Date().toISOString(), eintrag.id,
+ req.person.haus || null);
protokolliere("eintrag_bild", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
diff --git a/server/workspace-material.js b/server/workspace-material.js
index db5380cf..6416e8a5 100644
--- a/server/workspace-material.js
+++ b/server/workspace-material.js
@@ -127,7 +127,25 @@ function tabellen() {
Fehler, ein neues CREATE wuerde die vorhandenen Zeilen nicht
erreichen. Also nachsehen, was da ist, und nur ergaenzen, was
fehlt -- dieselbe Bauart wie bei den Personen-Spalten. */
- const NOETIG = [["gilt_ab", "TEXT"], ["gilt_bis", "TEXT"]];
+ /* ==== DAS HAUS GEHOERT IN DIESE LISTE, NICHT IN DIE ANDERE ======
+
+ (25.09.2026.) Beim Bau der Haustrennung habe ich `["material",
+ "haus", "TEXT"]` in die Spaltenliste von `workspace.js` gesetzt --
+ dort stehen die anderen sieben Wurzeltabellen. `material` ist die
+ einzige, die NICHT dort angelegt wird, sondern hier.
+
+ Auf der echten Datenbank fiel das nicht auf: Die Tabelle gab es
+ laengst, also hat `ALTER TABLE` gegriffen. Auf einer FRISCHEN
+ Datenbank lief die Liste in workspace.js dagegen, bevor es die
+ Tabelle gab -- sie wird dort uebersprungen --, und danach legte
+ diese Datei sie ohne `haus` an. Ergebnis: 78 Fehlschlaege mit
+ "table material has no column named haus", und zwar erst beim
+ SCHREIBEN, nicht beim Starten.
+
+ Gefunden hat das keine Ueberlegung, sondern `pruef-material`, weil
+ sie mit einer frischen Datenbank arbeitet. Dieselbe Falle wie
+ ueberall in diesem Haus: eine Liste an zwei Orten. */
+ const NOETIG = [["gilt_ab", "TEXT"], ["gilt_bis", "TEXT"], ["haus", "TEXT"]];
const da = new Set(d.prepare("PRAGMA table_info(material)").all().map((c) => c.name));
for (const [spalte, form] of NOETIG) {
if (da.has(spalte)) continue;
@@ -369,10 +387,13 @@ materialRouter.post(
const { lastInsertRowid } = db().prepare(`
INSERT INTO material (datei, dateiname, typ, art, groesse, text, von_id, erstellt,
- gilt_ab, gilt_bis)
- VALUES (?,?,?,?,?,?,?,?,?,?)
+ gilt_ab, gilt_bis, haus)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)
`).run(datei, name, typ, art, req.body.length, text, req.person.id,
- new Date().toISOString(), ab, bis);
+ new Date().toISOString(), ab, bis,
+ /* AM MATERIAL HAENGT NUR `von_id` -- ist das DogFather, gibt es
+ sonst keinen Beleg. Genau deshalb steht das Haus hier. */
+ req.person.haus || null);
protokolliere(req.person.id, "material_eingestellt",
`${art} "${name}" (${req.body.length} Bytes)`, echteIp(req));
diff --git a/server/workspace-video.js b/server/workspace-video.js
index 20cc3835..505055bd 100644
--- a/server/workspace-video.js
+++ b/server/workspace-video.js
@@ -290,10 +290,11 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => {
if (abgelegt) {
db().prepare(`
INSERT INTO dateien (name_original, name_datei, groesse, typ, status, notiz,
- hochgeladen_von, erstellt, eintrag_id)
- VALUES (?,?,?,?, 'entwurf', NULL, ?,?,?)`)
+ hochgeladen_von, erstellt, eintrag_id, haus)
+ VALUES (?,?,?,?, 'entwurf', NULL, ?,?,?,?)`)
.run(`cover-${auskunft.kanal}${abgelegt.name.slice(abgelegt.name.lastIndexOf("."))}`,
- abgelegt.name, abgelegt.groesse, abgelegt.art, req.person.id, jetzt, eintragId);
+ abgelegt.name, abgelegt.groesse, abgelegt.art, req.person.id, jetzt, eintragId,
+ req.person.haus || null);
abgelegt = null; /* ab hier gehoert die Datei dem Eintrag */
}
diff --git a/server/workspace-wissen.js b/server/workspace-wissen.js
index d4378d1f..684bf017 100644
--- a/server/workspace-wissen.js
+++ b/server/workspace-wissen.js
@@ -432,12 +432,20 @@ wissenRouter.post(
const { lastInsertRowid } = db().prepare(`
INSERT INTO wissen
(titel, beschreibung, kategorie, stufe, geraet, tags,
- name_original, name_datei, groesse, wichtig, veroeffentlicht, hochgeladen, erstellt_von)
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
+ name_original, name_datei, groesse, wichtig, veroeffentlicht, hochgeladen,
+ erstellt_von, haus)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
aus.titel, aus.beschreibung ?? null, aus.kategorie,
aus.stufe ?? "einsteiger", aus.geraet ?? "egal", aus.tags ?? null,
name, dateiname, req.body.length, aus.wichtig ?? 0,
- aus.veroeffentlicht ?? heute(), jetzt(), req.person.id);
+ aus.veroeffentlicht ?? heute(), jetzt(), req.person.id,
+ /* Die Wissensablage gibt es auf der Team-Adresse gar nicht
+ (siehe `angemeldet` weiter oben) -- hier kann also nur
+ "agentur" herauskommen. Trotzdem aus `person.haus` und nicht
+ fest: Eine feste Zeichenkette waere eine zweite Wahrheit
+ neben der Schranke, und die beiden liefen beim naechsten
+ Umbau auseinander. */
+ req.person.haus || null);
protokolliere("wissen_hochgeladen", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
diff --git a/server/workspace.js b/server/workspace.js
index 58a7bdad..f57e9534 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -3132,8 +3132,31 @@ export function haeuserNachtragen(d) {
["wissen", (r) => [r.erstellt_von].filter((x) => x != null), "titel",
() => "agentur"],
["material", (r) => [r.von_id].filter((x) => x != null), "dateiname"],
- ["termin_serien", (r) => [r.creator_id, r.teilnehmer_id, r.erstellt_von]
- .filter((x) => x != null), "titel"],
+ /* DIE TEILNEHMER DER SERIE ZAEHLEN MIT (nachgetragen 25.09.2026).
+
+ Hier standen nur `creator_id`, `teilnehmer_id` und
+ `erstellt_von` -- bei den TERMINEN eine Zeile hoeher wird
+ dagegen auch `termin_teilnehmer` gefragt. Dieselbe Frage, zwei
+ verschiedene Antworten: genau die Bauart, vor der in dieser
+ Datei an fuenfzehn Stellen gewarnt wird, und ich habe sie beim
+ Nachruesten der Serien selbst gebaut.
+
+ GEMESSEN AM ECHTEN STAND nach dem Ausliefern: Zwei Serien
+ blieben ohne Haus -- „Community-Call" und „Schulung-Agentur".
+ An beiden haengt nur DogFather; ihre Teilnehmer sind fuenf
+ Creator und Scouts, also eindeutig die Agentur. Der Beleg war
+ da, er wurde nur nicht gelesen.
+
+ WAS DAS GEKOSTET HAETTE: Die 53 bereits erzeugten Termine sind
+ richtig zugeordnet (sie haben ja Teilnehmer). Aber jeder KUENFTIG
+ erzeugte haette `haus = null` geerbt -- und ein `null` ist in
+ BEIDEN Haeusern sichtbar. Die Trennung waere Woche fuer Woche
+ ein Stueck weiter aufgegangen, ohne dass etwas rot wird. */
+ ["termin_serien", (r) => [
+ ...d.prepare("SELECT person_id AS p FROM serie_teilnehmer WHERE serie_id = ?")
+ .all(r.id).map((z) => z.p),
+ r.creator_id, r.teilnehmer_id, r.erstellt_von,
+ ].filter((x) => x != null), "titel"],
];
/* DIE EINTRAEGE ZUERST -- die Dateien erben von ihnen. Stuenden
@@ -4112,7 +4135,11 @@ function umstellungen(d) {
["eintraege", "haus", "TEXT"],
["dateien", "haus", "TEXT"],
["wissen", "haus", "TEXT"],
- ["material", "haus", "TEXT"],
+ /* `material` steht hier ABSICHTLICH NICHT. Seine Tabelle wird in
+ workspace-material.js angelegt, und diese Schleife laeuft auf
+ einer frischen Datenbank vorher -- sie haette die Spalte still
+ uebersprungen, und das Schreiben waere erst im Betrieb
+ gescheitert. Sie steht dort, wo die Tabelle wohnt. */
/* Die WIEDERHOLUNGSREGEL braucht es auch: Der Nachfueller erzeugt
daraus Termine, und die muessen wissen, wohin sie gehoeren. Ohne
sie erbten sie `null` -- und ein `null` ist in beiden Haeusern
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index f6c99064..f76d2f83 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+