diff --git a/.gitignore b/.gitignore
index 57069a29..30c115af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,3 +118,10 @@ tools/_*
# der Vault-Notiz (02 Projekte/Pruefstand.md), die Werkzeuge selbst
# sind versioniert.
tools/.nachtlauf-*
+
+# Zwischenstaende meiner Mess- und Sammellaeufe. Rohausgaben, die sich
+# bei jedem Lauf aendern -- im Repo waeren sie taeglicher Konflikt ohne
+# Aussage. Die Werkzeuge selbst sind versioniert.
+tools/.sammellauf*
+tools/.nachpruef*
+tools/.handy-detail.txt
diff --git a/server/mess-gifs-handy.mjs b/server/mess-gifs-handy.mjs
new file mode 100644
index 00000000..e75eafce
--- /dev/null
+++ b/server/mess-gifs-handy.mjs
@@ -0,0 +1,307 @@
+/* =====================================================================
+ DIE GIF-KISTE AUF DEM HANDY -- UND WAS PASSIERT, WENN MAN DRAUFTIPPT
+ =====================================================================
+
+ Filipe, 25.09.2026: „mach auch das mit den gifs fertig. eine
+ perfektion von perfektion bitte."
+
+ `pruef-gifs` misst den WEG (17 Pruefungen, alle gruen): hineinlegen,
+ anzeigen, verschicken, herausnehmen, und dass ein Creator nicht
+ herankommt. Was sie NICHT misst, ist das, was man sieht:
+
+ 1. Wie die Kiste auf 390 px aussieht -- der Breite, auf der die
+ Modis tatsaechlich tippen. Eine Tafel, die am Rechner schoen
+ ist und auf dem Handy in einer Spalte klebt, ist auf dem
+ Handy kaputt, und keine Route merkt es.
+ 2. Ob ein verschicktes GIF sich in der BILDSCHAU oeffnet (seit
+ gestern) -- und ob es dort noch laeuft. Ein GIF, das beim
+ Vergroessern zum Standbild wird, ist kein GIF mehr.
+
+ SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder
+ und Zahlen, auf denen man nachsehen kann. Deshalb `mess-`.
+
+ EIGENE WEGWERF-DATENBANK in einem mktemp-Ordner: Es entstehen echte
+ GIFs und echte Nachrichten. In einem laufenden System waeren das
+ Testdaten, und die sind verboten.
+
+ Aufrufen mit: node server/mess-gifs-handy.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 = 5431, HP = 5432;
+const CREW = "crew.dogfather-universe.com";
+const ordner = mkdtempSync(join(tmpdir(), "ws-gifs-"));
+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(300_000, "mess-gifs-handy");
+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 idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
+d.close();
+
+const anfrage = (pfad, art, koerper, keks, typ) => new Promise((fertig, schief) => {
+ const a = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: pfad, method: art,
+ headers: {
+ Host: CREW,
+ ...(koerper ? { "Content-Type": typ || "application/json" } : {}),
+ ...(keks ? { Cookie: keks } : {}),
+ },
+ }, (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(Buffer.isBuffer(koerper) ? koerper : 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("; ");
+
+/* ---- Ein gueltiges GIF -------------------------------------------
+ Dasselbe wie in `pruef-gifs`: ein 1x1-Bildpunkt, der den strengen
+ Kopfcheck des Servers besteht ("GIF89a"). Mein erster Versuch war
+ ein selbstgebautes Zweibild-GIF und wurde mit 415 abgelehnt -- die
+ Bytefolge war naeher am Wunsch als an der Norm. Wo es eine
+ funktionierende Vorlage im Haus gibt, wird sie benutzt und nicht
+ nachgebaut.
+
+ Fuer diese Messung reicht ein Standbild: Gefragt sind die GROESSEN
+ der Kiste, ihrer Kacheln und Knoepfe -- und ob die Bildschau
+ aufgeht. Ob die Einzelbilder danach weiterlaufen, entscheidet der
+ Browser und nicht diese Seite. */
+const GIF = Buffer.from(
+ "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", "base64");
+
+/* Die Route nimmt den nackten Rumpf mit `x-dateiname` im Kopf --
+ nachgesehen in workspace-chat.js, nicht geraten (multipart wurde
+ mit 415 abgelehnt). */
+const rein = await new Promise((fertig, schief) => {
+ const req = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: "/workspace/api/chat/gifs", method: "POST",
+ headers: {
+ Host: CREW, Cookie: keks,
+ "x-dateiname": encodeURIComponent("Winken.gif"),
+ "Content-Type": "application/octet-stream",
+ "Content-Length": GIF.length,
+ Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/chat.html`,
+ },
+ }, (r) => {
+ let x = ""; r.on("data", (c) => { x += c; });
+ r.on("end", () => fertig({ code: r.statusCode, text: x }));
+ });
+ req.on("error", schief);
+ req.write(GIF);
+ req.end();
+});
+console.log(`GIF in die Kiste: HTTP ${rein.code} ${rein.text.slice(0, 90)}`);
+
+/* Ein Gespraech, in dem das GIF landen kann. */
+const raum = await anfrage("/workspace/api/chat/direkt", "POST", { mit: idHand }, keks);
+const raumId = JSON.parse(raum.text || "{}").raum_id;
+console.log(`Gespraech: HTTP ${raum.code}, Raum ${raumId}`);
+
+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"],
+});
+
+async function blicken(wie, breite, hoehe, mobil) {
+ const kontext = await browser.newContext({
+ viewport: { width: breite, height: hoehe },
+ deviceScaleFactor: mobil ? 2 : 1, isMobile: mobil, hasTouch: mobil,
+ });
+ 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/chat.html?raum=${raumId}`,
+ { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1500);
+
+ console.log(`\n=== ${wie} (${breite} x ${hoehe}) ===`);
+
+ /* ---- 1. Die Kiste aufmachen ----------------------------------- */
+ const knopf = seite.locator("#gif-auf").first();
+ const knopfDa = await knopf.count();
+ if (knopfDa) await knopf.click().catch(() => {});
+ await seite.waitForTimeout(700);
+
+ const kiste = await seite.evaluate(() => {
+ const tafel = document.getElementById("gif-tafel");
+ if (tafel && tafel.hidden) return { da: true, zu: true };
+ if (!tafel) return { da: false };
+ const r = tafel.getBoundingClientRect();
+ const st = getComputedStyle(tafel);
+ const kacheln = [...tafel.querySelectorAll("img")].map((b) => {
+ const k = b.getBoundingClientRect();
+ return { b: Math.round(k.width), h: Math.round(k.height), src: b.currentSrc || b.src };
+ });
+ const knoepfe = [...tafel.querySelectorAll("button")].map((b) => {
+ const k = b.getBoundingClientRect();
+ return { t: (b.textContent || b.getAttribute("aria-label") || "").trim().slice(0, 18),
+ b: Math.round(k.width), h: Math.round(k.height) };
+ });
+ return {
+ da: true,
+ links: Math.round(r.left), rechts: Math.round(r.right),
+ breite: Math.round(r.width), hoehe: Math.round(r.height),
+ ausDemBild: r.left < 0 || r.right > window.innerWidth,
+ spalten: st.gridTemplateColumns,
+ kacheln, knoepfe,
+ /* NUR AM FINGER STRENG. Die 44 px sind die Hausgroesse fuer
+ Beruehrziele -- am Rechner waere derselbe Massstab falsch:
+ Dort ist ein 28-px-Kreis in der Ecke einer Kachel richtig, und
+ ein Knopf von 44 px wuerde die halbe Kachel verdecken. Der
+ erste Anlauf dieser Messung hat am Rechner „ZU KLEIN" gemeldet
+ und damit zwei Stellen als Befund ausgegeben, die Absicht
+ sind. Eine Messung, die den Zusammenhang nicht kennt, erzeugt
+ Arbeit statt Erkenntnis. */
+ zuKlein: (matchMedia("(pointer: coarse)").matches
+ ? knoepfe.filter((k) => k.h > 0 && k.h < 44).map((k) => `${k.t} ${k.h}px`)
+ : []),
+ };
+ });
+ console.log(` Knopf da: ${knopfDa ? "ja" : "NEIN"}`);
+ if (!kiste.da) console.log(" Tafel: NICHT GEFUNDEN");
+ else {
+ console.log(` Tafel ${kiste.breite}x${kiste.hoehe} px, links ${kiste.links}, `
+ + `rechts ${kiste.rechts}${kiste.ausDemBild ? " STEHT AUS DEM BILD" : ""}`);
+ console.log(` Spalten ${kiste.spalten}`);
+ console.log(` Kacheln ${kiste.kacheln.map((k) => `${k.b}x${k.h}`).join(", ") || "keine"}`);
+ console.log(` Knoepfe ${kiste.knoepfe.map((k) => `"${k.t}" ${k.b}x${k.h}`).join(", ")}`);
+ if (kiste.zuKlein.length) console.log(` ZU KLEIN zum Tippen: ${kiste.zuKlein.join(", ")}`);
+ }
+ await seite.screenshot({ path: `server/mess-gifs-${wie}-kiste.png` }).catch(() => {});
+
+ /* ---- 2. Verschicken und in der Bildschau oeffnen --------------- */
+ const kachel = seite.locator("#gif-tafel .gif-kachel__knopf").first();
+ if (await kachel.count()) {
+ await kachel.click().catch(() => {});
+ await seite.waitForTimeout(1200);
+ }
+ const imVerlauf = await seite.evaluate(() => {
+ const bilder = [...document.querySelectorAll(".chat-nachricht img, .chat-anhang img")];
+ const g = bilder.find((b) => /\.gif|gifs\/|anhang/.test(b.currentSrc || b.src));
+ if (!g) return { da: false, wieviele: bilder.length };
+ const r = g.getBoundingClientRect();
+ return { da: true, b: Math.round(r.width), h: Math.round(r.height),
+ src: (g.currentSrc || g.src).slice(-40) };
+ });
+ console.log(` Im Verlauf: ${imVerlauf.da
+ ? `${imVerlauf.b}x${imVerlauf.h} px (${imVerlauf.src})`
+ : `NICHT GEFUNDEN (${imVerlauf.wieviele} Bilder im Verlauf)`}`);
+
+ if (imVerlauf.da) {
+ await seite.locator(".chat-nachricht img, .chat-anhang img").first().click()
+ .catch(() => {});
+ await seite.waitForTimeout(900);
+ const schau = await seite.evaluate(() => {
+ const dlg = document.querySelector("dialog.bildschau");
+ if (!dlg) return { da: false };
+ /* und NICHT das erste
: Der Dialog traegt
+ oben ein Husky-Zeichen und an den Seiten zwei Figuren. Mein
+ erster Anlauf nahm und maass damit das
+ Logo -- die Meldung lautete "Mitte 174 von 640", und die
+ Bildschau sah kaputt aus, obwohl sie es nicht war. Ein
+ Messfehler, der wie ein Befund aussieht, kostet mehr Zeit als
+ gar keine Messung. */
+ /* `.bildschau__bild` und NICHT das erste
: Der Dialog
+ traegt oben ein Husky-Zeichen und an den Seiten zwei Figuren.
+ Der erste Anlauf nahm `querySelector("img")` und maass damit
+ das Logo -- die Meldung lautete „Mitte 174 von 640", und die
+ Bildschau sah kaputt aus, obwohl sie mittig stand. Ein
+ Messfehler, der wie ein Befund aussieht, kostet mehr Zeit als
+ gar keine Messung. */
+ const b = dlg.querySelector(".bildschau__bild");
+ const r = b?.getBoundingClientRect();
+ return {
+ da: true, offen: dlg.open === true,
+ bildSrc: (b?.currentSrc || b?.src || "").slice(-40),
+ bild: r ? `${Math.round(r.width)}x${Math.round(r.height)}` : "kein Bild",
+ istGif: /\.gif|gifs\/|anhang/.test(b?.currentSrc || b?.src || ""),
+ mitte: r ? Math.round(r.left + r.width / 2) : 0,
+ fensterMitte: Math.round(window.innerWidth / 2),
+ };
+ });
+ if (!schau.da) console.log(" Bildschau: GEHT NICHT AUF");
+ else {
+ console.log(` Bildschau offen=${schau.offen}, Bild ${schau.bild}, `
+ + `GIF=${schau.istGif}, Mitte ${schau.mitte} (Fenster ${schau.fensterMitte})`);
+ await seite.screenshot({ path: `server/mess-gifs-${wie}-schau.png` }).catch(() => {});
+ }
+ }
+
+ if (konsole.length) console.log(` FEHLER IN DER KONSOLE: ${konsole.join(" | ")}`);
+ await kontext.close();
+}
+
+try {
+ await blicken("handy", 390, 844, true);
+ await blicken("rechner", 1280, 900, false);
+} finally {
+ await browser.close();
+ vorbau.close();
+ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+ process.exit(0);
+}
diff --git a/server/mess-support-ueberstand.mjs b/server/mess-support-ueberstand.mjs
new file mode 100644
index 00000000..a1246af5
--- /dev/null
+++ b/server/mess-support-ueberstand.mjs
@@ -0,0 +1,143 @@
+/* =====================================================================
+ WER MACHT DIE SUPPORT-SEITE ZU BREIT? (25.09.2026)
+ =====================================================================
+
+ `pruef-breiten` meldet auf jeder Breite unter 1440 px einen
+ Ueberstand -- 13 px auf dem Handy, 41 px auf dem Tablet quer -- und
+ die Liste der Schuldigen bleibt leer: `[]`. Damit weiss man, DASS
+ etwas zu breit ist, und nicht WAS.
+
+ DIESE MESSUNG SUCHT DEN SCHULDIGEN, indem sie jedes Element im
+ Dokument fragt, ob seine rechte Kante ueber die Dokumentbreite
+ hinausragt -- und dann den KLEINSTEN nennt, der es tut. Der groesste
+ ist immer , und der sagt nichts; der kleinste ist der, den man
+ anfassen muss.
+
+ Aufrufen mit: node server/mess-support-ueberstand.mjs
+ ===================================================================== */
+import { mkdtempSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+const PORT = 5441;
+const ordner = mkdtempSync(join(tmpdir(), "ws-ueber-"));
+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-support-ueberstand");
+await new Promise((r) => setTimeout(r, 900));
+process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
+
+const BASIS = `http://127.0.0.1:${PORT}`;
+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");
+/* Eine Meldung, damit die Seite nicht leer ist -- ein leerer Eingang
+ kann gar nicht zu breit sein. */
+d.prepare(`INSERT INTO support_meldungen
+ (person_id, von_name, von_rolle, text, seite, stand, erstellt, geaendert)
+ VALUES (?,?,?,?,?,?,?,?)`)
+ .run(idModi, "Miss", "modi",
+ "Der Kalender springt beim Wischen manchmal zwei Monate weiter, "
+ + "und danach komme ich nicht mehr zurueck.",
+ "/workspace/kalender.html", "neu", jetzt, jetzt);
+d.close();
+
+const an = await fetch(BASIS + "/workspace/api/anmelden", {
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
+});
+const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
+
+const pw = await import(
+ "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await pw.chromium.launch();
+
+for (const [wie, breite] of [["Handy", 390], ["Tablet quer", 1024], ["Laptop", 1280]]) {
+ const kontext = await browser.newContext({ viewport: { width: breite, height: 900 } });
+ await kontext.addCookies(keks.split("; ").map((k) => {
+ const [n, ...r] = k.split("=");
+ return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" };
+ }));
+ const seite = await kontext.newPage();
+ await seite.goto(BASIS + "/workspace/" + (process.env.SEITE || "support.html"), { waitUntil: "networkidle" });
+ await seite.waitForTimeout(900);
+
+ const fund = await seite.evaluate(() => {
+ const doc = document.documentElement;
+ const ueber = doc.scrollWidth - doc.clientWidth;
+ const grenze = doc.clientWidth;
+ const schuld = [];
+ for (const e of document.querySelectorAll("*")) {
+ const r = e.getBoundingClientRect();
+ if (r.width === 0 && r.height === 0) continue;
+ const rechts = r.right + window.scrollX;
+ if (rechts <= grenze + 0.5) continue;
+ /* Nur das KLEINSTE, das herausragt -- ein Elternteil ragt
+ immer mit heraus, wenn es ein Kind tut, und nennt den Grund
+ nicht. */
+ const hatSchuldigesKind = [...e.children].some((k) => {
+ const kr = k.getBoundingClientRect();
+ return kr.right + window.scrollX > grenze + 0.5;
+ });
+ if (hatSchuldigesKind) continue;
+ const st = getComputedStyle(e);
+ schuld.push({
+ was: e.tagName.toLowerCase()
+ + (e.id ? "#" + e.id : "")
+ + (e.className && typeof e.className === "string"
+ ? "." + e.className.trim().split(/\s+/).join(".") : ""),
+ links: Math.round(r.left), rechts: Math.round(rechts),
+ breite: Math.round(r.width),
+ ueberstand: Math.round(rechts - grenze),
+ position: st.position,
+ transform: st.transform === "none" ? "" : st.transform.slice(0, 40),
+ minBreite: st.minWidth,
+ text: (e.textContent || "").trim().slice(0, 30),
+ });
+ }
+ return { ueber, grenze, schuld: schuld.sort((a, b) => b.ueberstand - a.ueberstand) };
+ });
+
+ console.log(`\n=== ${wie} (${breite} px) -- Ueberstand ${fund.ueber} px ===`);
+ if (!fund.schuld.length) console.log(" niemand ragt heraus (dann ist es ein Rand/Schatten)");
+ for (const s of fund.schuld.slice(0, 6)) {
+ console.log(` ${s.was}`);
+ console.log(` links ${s.links}, rechts ${s.rechts} (Grenze ${fund.grenze}), `
+ + `breit ${s.breite}, ueber ${s.ueberstand}`);
+ console.log(` position:${s.position} min-width:${s.minBreite}`
+ + (s.transform ? ` transform:${s.transform}` : "")
+ + (s.text ? ` "${s.text}"` : ""));
+ }
+ await kontext.close();
+}
+
+await browser.close();
+try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+process.exit(0);
diff --git a/server/pruef-anruf.mjs b/server/pruef-anruf.mjs
index 012fca4c..0fdb38b2 100644
--- a/server/pruef-anruf.mjs
+++ b/server/pruef-anruf.mjs
@@ -60,7 +60,24 @@ const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t);
const unklar = (t) => { offen++; console.log(" -- " + t); };
const CREW = "crew.dogfather-universe.com";
-const HAUS = "workspace.dogfather-universe.com";
+/* ==== ZWEI ADRESSEN, ZWEI HAEUSER (25.09.2026) ==============
+
+ Seit dem 24.09. filtert der Server heraus, wer nicht zum
+ Haus der Adresse gehoert. Diese Pruefung meldete DogFather
+ auf der AGENTUR-Adresse an und liess ihn dann ein Gespraech
+ mit der rechten Hand aufmachen -- die es dort nicht gibt.
+ Antwort: 403, und das war richtig. Dreissig Folgefehler
+ haengen daran, alle aus derselben Wurzel.
+
+ TEAM DOGI TELEFONIERT AUF crew., die Agentur auf workspace.
+ DogFather ist in beiden Haeusern -- welches gilt, entscheidet
+ die Adresse, ueber die er sich anmeldet, nicht seine Rolle.
+
+ DIE AGENTUR-ADRESSE BLEIBT IM SPIEL, und zwar dort, wo sie
+ hingehoert: beim Creator Tili. Seine Gegenprobe ist dadurch
+ schaerfer als vorher -- ein Fremder aus dem ANDEREN AGENTUR
+ bekommt 404 auf einen Team-Dogi-Anruf. */
+const AGENTUR = "workspace.dogfather-universe.com";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
@@ -111,10 +128,10 @@ async function anmelden(rolle, code, host) {
const r = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
}
-const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
+const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
const kHand = await anmelden("hand", "CODE-HAND-0001", CREW);
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
-const kFremd = await anmelden("creator", "CODE-TILI-0001", HAUS);
+const kFremd = await anmelden("creator", "CODE-TILI-0001", AGENTUR);
ok(!!kDogi && !!kHand && !!kModi && !!kFremd, "vier Leute sind angemeldet");
/* =======================================================================
@@ -136,23 +153,23 @@ let raumId = null;
{
/* Die Route heisst `direkt`, nicht `mit` -- nachgesehen statt
geraten. */
- const r = await roh("/workspace/api/chat/direkt", HAUS, { cookie: kDogi },
+ const r = await roh("/workspace/api/chat/direkt", CREW, { cookie: kDogi },
{ mit: idHand });
raumId = json(r).raum?.id || json(r).raum_id || json(r).id;
ok(!!raumId, `ein Gespraech entsteht (${r.code}, Raum ${raumId})`);
}
{
- const r = await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi });
+ const r = await roh(`/workspace/api/anruf/${raumId}`, CREW, { cookie: kDogi });
ok(r.code === 200 && json(r).laeuft === false,
`am Anfang laeuft kein Anruf (${json(r).laeuft})`);
}
{
- const r = await roh(`/workspace/api/anruf/${raumId}/start`, HAUS,
+ const r = await roh(`/workspace/api/anruf/${raumId}/start`, CREW,
{ cookie: kDogi }, { video: false });
ok(r.code === 200, `anrufen geht (${r.code})`);
- const l = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
+ const l = json(await roh(`/workspace/api/anruf/${raumId}`, CREW, { cookie: kDogi }));
ok(l.laeuft === true, "und danach laeuft einer");
ok(l.wer?.length === 1 && l.wer[0].id === idDogi,
`mit genau einem Teilnehmer (${l.wer?.map((w) => w.name).join(", ")})`);
@@ -176,19 +193,19 @@ let raumId = null;
melde("");
melde("=== 3. Die Signalisierung ===");
{
- const r = await roh(`/workspace/api/anruf/${raumId}/signal`, HAUS, { cookie: kDogi },
+ const r = await roh(`/workspace/api/anruf/${raumId}/signal`, CREW, { cookie: kDogi },
{ an: idHand, inhalt: { angebot: { type: "offer", sdp: "v=0" } } });
ok(r.code === 200, `ein Angebot geht durch (${r.code})`);
}
{
- const r = await roh(`/workspace/api/anruf/${raumId}/signal`, HAUS, { cookie: kDogi },
+ const r = await roh(`/workspace/api/anruf/${raumId}/signal`, CREW, { cookie: kDogi },
{ an: idFremd, inhalt: { angebot: {} } });
ok(r.code === 404,
`an jemanden AUSSERHALB des Raums geht nichts (${r.code}) -- sonst waere das `
+ "ein Weg, jedem Angemeldeten JSON zu schicken");
}
{
- const r = await roh(`/workspace/api/anruf/${raumId}/signal`, HAUS, { cookie: kDogi },
+ const r = await roh(`/workspace/api/anruf/${raumId}/signal`, CREW, { cookie: kDogi },
{ an: idHand });
ok(r.code === 400, `ohne Inhalt wird abgewiesen (${r.code})`);
}
@@ -209,18 +226,18 @@ for (const [pfad, methode, koerper] of [
["/ende", "POST", {}],
["/signal", "POST", { an: idDogi, inhalt: { weg: {} } }],
]) {
- const r = await roh(`/workspace/api/anruf/${raumId}${pfad}`, HAUS,
+ const r = await roh(`/workspace/api/anruf/${raumId}${pfad}`, AGENTUR,
{ cookie: kFremd }, koerper, methode);
ok(r.code === 404,
`${(pfad || "/lage").padEnd(8)} ist fuer Fremde zu -- und zwar 404, `
+ `nicht „kein Zugriff" (${r.code})`);
}
{
- const r = await roh(`/workspace/api/anruf/${raumId}`, HAUS, {});
+ const r = await roh(`/workspace/api/anruf/${raumId}`, CREW, {});
ok(r.code === 401, `ohne Anmeldung gar nichts (${r.code})`);
}
{
- const r = await roh("/workspace/api/anruf/999999/start", HAUS,
+ const r = await roh("/workspace/api/anruf/999999/start", CREW,
{ cookie: kDogi }, { video: false });
ok(r.code === 404, `ein erfundener Raum wird abgewiesen (${r.code})`);
}
@@ -233,11 +250,11 @@ melde("=== 5. Auflegen ===");
{
const r = await roh(`/workspace/api/anruf/${raumId}/ende`, CREW, { cookie: kHand }, {}, "POST");
ok(r.code === 200, `die rechte Hand legt auf (${r.code})`);
- const l = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
+ const l = json(await roh(`/workspace/api/anruf/${raumId}`, CREW, { cookie: kDogi }));
ok(l.laeuft === true && l.wer?.length === 1,
`der Anruf laeuft weiter, solange noch jemand drin ist (${l.wer?.length})`);
- await roh(`/workspace/api/anruf/${raumId}/ende`, HAUS, { cookie: kDogi }, {}, "POST");
- const l2 = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
+ await roh(`/workspace/api/anruf/${raumId}/ende`, CREW, { cookie: kDogi }, {}, "POST");
+ const l2 = json(await roh(`/workspace/api/anruf/${raumId}`, CREW, { cookie: kDogi }));
ok(l2.laeuft === false, "wenn der Letzte geht, ist er vorbei");
}
@@ -334,22 +351,22 @@ melde("=== 5. Auflegen ===");
melde("");
melde("=== 5g. Anrufer legt auf, bevor jemand rangeht ===");
{
- const raum = json(await roh("/workspace/api/chat/raeume", HAUS,
+ const raum = json(await roh("/workspace/api/chat/raeume", CREW,
{ cookie: kDogi })).raeume?.[0];
if (!raum) {
unklar("kein Gespraech vorhanden -- nicht nachgesehen");
} else {
- const vorher = (json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
+ const vorher = (json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, CREW,
{ cookie: kDogi })).nachrichten || []).length;
- await roh(`/workspace/api/anruf/${raum.id}/start`, HAUS, { cookie: kDogi }, { video: false });
+ await roh(`/workspace/api/anruf/${raum.id}/start`, CREW, { cookie: kDogi }, { video: false });
/* Kurz warten wie im echten Leben -- und auflegen, ohne dass
jemand rangegangen ist. */
await new Promise((r) => setTimeout(r, 800));
- const ende = await roh(`/workspace/api/anruf/${raum.id}/ende`, HAUS, { cookie: kDogi }, {});
+ const ende = await roh(`/workspace/api/anruf/${raum.id}/ende`, CREW, { cookie: kDogi }, {});
ok(ende.code === 200, `aufgelegt (${ende.code})`);
- const nachher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
+ const nachher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, CREW,
{ cookie: kDogi })).nachrichten || [];
ok(nachher.length === vorher + 1,
`es steht trotzdem eine Zeile im Gespraech (${vorher} -> ${nachher.length})`);
@@ -364,7 +381,7 @@ melde("=== 5g. Anrufer legt auf, bevor jemand rangeht ===");
zweite Zeile schreiben -- der Anruf ist ja schon geloescht. */
await new Promise((r) => setTimeout(r, 300));
await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand });
- const nochmal = (json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
+ const nochmal = (json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, CREW,
{ cookie: kDogi })).nachrichten || []).length;
ok(nochmal === nachher.length,
`und bleibt bei einer (${nochmal})`);
@@ -374,24 +391,24 @@ melde("=== 5g. Anrufer legt auf, bevor jemand rangeht ===");
melde("");
melde("=== 5f. Ein gefuehrter Anruf ===");
{
- const raum = json(await roh("/workspace/api/chat/raeume", HAUS,
+ const raum = json(await roh("/workspace/api/chat/raeume", CREW,
{ cookie: kDogi })).raeume?.[0];
if (!raum) {
unklar("kein Gespraech vorhanden -- nicht nachgesehen");
} else {
- const vorher = (json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
+ const vorher = (json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, CREW,
{ cookie: kDogi })).nachrichten || []).length;
- await roh(`/workspace/api/anruf/${raum.id}/start`, HAUS, { cookie: kDogi }, { video: false });
+ await roh(`/workspace/api/anruf/${raum.id}/start`, CREW, { cookie: kDogi }, { video: false });
const dabei = await roh(`/workspace/api/anruf/${raum.id}/dabei`, CREW, { cookie: kHand }, {});
ok(dabei.code === 200, `die rechte Hand geht ran (${dabei.code})`);
/* Kurz reden, dann auflegen -- beide. */
await new Promise((r) => setTimeout(r, 1500));
await roh(`/workspace/api/anruf/${raum.id}/ende`, CREW, { cookie: kHand }, {});
- await roh(`/workspace/api/anruf/${raum.id}/ende`, HAUS, { cookie: kDogi }, {});
+ await roh(`/workspace/api/anruf/${raum.id}/ende`, CREW, { cookie: kDogi }, {});
- const nachher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
+ const nachher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, CREW,
{ cookie: kDogi })).nachrichten || [];
ok(nachher.length === vorher + 1,
`es steht eine Zeile im Gespraech (${vorher} -> ${nachher.length})`);
@@ -414,15 +431,15 @@ melde("=== 5f. Ein gefuehrter Anruf ===");
melde("");
melde("=== 5e. Ein verpasster Anruf ===");
{
- const raum = json(await roh("/workspace/api/chat/raeume", HAUS,
+ const raum = json(await roh("/workspace/api/chat/raeume", CREW,
{ cookie: kDogi })).raeume?.[0];
if (!raum) {
unklar("kein Gespraech vorhanden -- nicht nachgesehen");
} else {
- const vorher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
+ const vorher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, CREW,
{ cookie: kDogi })).nachrichten?.length ?? 0;
- const start = await roh(`/workspace/api/anruf/${raum.id}/start`, HAUS,
+ const start = await roh(`/workspace/api/anruf/${raum.id}/start`, CREW,
{ cookie: kDogi }, { video: false });
ok(start.code === 200 || start.code === 201, `Anruf gestartet (${start.code})`);
@@ -432,7 +449,7 @@ melde("=== 5e. Ein verpasster Anruf ===");
await new Promise((r) => setTimeout(r, 10600));
await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand });
- const nachher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
+ const nachher = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, CREW,
{ cookie: kDogi })).nachrichten || [];
ok(nachher.length === vorher + 1,
`es steht genau EINE neue Zeile im Gespraech (${vorher} -> ${nachher.length})`);
@@ -447,7 +464,7 @@ melde("=== 5e. Ein verpasster Anruf ===");
eine weitere Zeile. */
await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand });
await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand });
- const nochmal = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, HAUS,
+ const nochmal = json(await roh(`/workspace/api/chat/raeume/${raum.id}/nachrichten`, CREW,
{ cookie: kDogi })).nachrichten || [];
ok(nochmal.length === nachher.length,
`und bleibt bei einer, auch nach weiteren Abfragen (${nochmal.length})`);
@@ -485,7 +502,7 @@ melde("=== 5d. Was in der Benachrichtigung steht ===");
melde("");
melde("=== 5c. Wer erst durch die Benachrichtigung kommt ===");
{
- const raum = json(await roh("/workspace/api/chat/raeume", HAUS,
+ const raum = json(await roh("/workspace/api/chat/raeume", CREW,
{ cookie: kDogi })).raeume?.[0];
if (!raum) {
unklar("kein Gespraech vorhanden -- nicht nachgesehen");
@@ -496,7 +513,7 @@ melde("=== 5c. Wer erst durch die Benachrichtigung kommt ===");
ok(Array.isArray(vorher.anrufe) && vorher.anrufe.length === 0,
`vorher klingelt nichts (${vorher.anrufe?.length})`);
- const start = await roh(`/workspace/api/anruf/${raum.id}/start`, HAUS,
+ const start = await roh(`/workspace/api/anruf/${raum.id}/start`, CREW,
{ cookie: kDogi }, { video: false });
ok(start.code === 200 || start.code === 201, `DogFather ruft an (${start.code})`);
@@ -512,17 +529,17 @@ melde("=== 5c. Wer erst durch die Benachrichtigung kommt ===");
/* DER ANRUFER SELBST DARF NICHT KLINGELN. Er hat den Anruf
gestartet; ein zweites Fenster von ihm wuerde sonst mitlaeuten. */
- const beimAnrufer = json(await roh("/workspace/api/anruf/offen", HAUS, { cookie: kDogi }));
+ const beimAnrufer = json(await roh("/workspace/api/anruf/offen", CREW, { cookie: kDogi }));
ok(beimAnrufer.anrufe?.length === 0,
`beim Anrufer selbst klingelt nichts (${beimAnrufer.anrufe?.length})`);
/* UND WER NICHT IN DEN RAUM GEHOERT, ERFAEHRT NICHTS. Dieselbe
Frage wie ueberall sonst -- eine neue Route ist eine neue Tuer. */
- const fremd = json(await roh("/workspace/api/anruf/offen", HAUS, { cookie: kFremd }));
+ const fremd = json(await roh("/workspace/api/anruf/offen", AGENTUR, { cookie: kFremd }));
ok(fremd.anrufe?.length === 0,
`ein Fremder erfaehrt nichts davon (${fremd.anrufe?.length})`);
- await roh(`/workspace/api/anruf/${raum.id}/ende`, HAUS, { cookie: kDogi }, {});
+ await roh(`/workspace/api/anruf/${raum.id}/ende`, CREW, { cookie: kDogi }, {});
const danach = json(await roh("/workspace/api/anruf/offen", CREW, { cookie: kHand }));
ok(danach.anrufe?.length === 0,
`nach dem Auflegen klingelt es nicht mehr (${danach.anrufe?.length})`);
@@ -532,7 +549,7 @@ melde("=== 5c. Wer erst durch die Benachrichtigung kommt ===");
melde("");
melde("=== 6. Die Verbindungsadressen ===");
{
- const r = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
+ const r = json(await roh("/workspace/api/anruf/adressen", CREW, { cookie: kDogi }));
ok(Array.isArray(r.adressen), `die Liste kommt (${r.adressen?.length} Eintraege)`);
ok(r.nur_gleiches_netz === true,
"und sie sagt ehrlich, dass es ohne Eintrag nur im selben Netz geht");
@@ -543,7 +560,7 @@ melde("=== 6. Die Verbindungsadressen ===");
ok(r.code === 404, `die rechte Hand darf sie nicht setzen (${r.code})`);
}
{
- const r = await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }, {
+ const r = await roh("/workspace/api/anruf/adressen", CREW, { cookie: kDogi }, {
adressen: [
{ urls: "stun:turn.dogfather-universe.com:3478" },
{ urls: "turn:turn.dogfather-universe.com:3478", username: "a", credential: "b" },
@@ -566,9 +583,9 @@ melde("=== 6. Die Verbindungsadressen ===");
Eine Pruefung, die der naechsten den Boden verstellt, macht deren
Ergebnis unlesbar -- genau das ist heute schon einmal passiert
(die Anfragen, Abschnitt 8b). */
- await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi },
+ await roh("/workspace/api/anruf/adressen", CREW, { cookie: kDogi },
{ adressen: [] }, "PUT");
- const zurueck = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
+ const zurueck = json(await roh("/workspace/api/anruf/adressen", CREW, { cookie: kDogi }));
ok(zurueck.adressen?.length === 0,
"danach ist die Liste wieder leer -- der naechste Abschnitt faengt sauber an");
}
@@ -688,7 +705,7 @@ melde("=== 6b. Zugangsdaten fuer die Vermittlung ===");
/* --- Und jetzt ueber die Schnittstelle, wie der Browser sie sieht - */
{
- await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }, {
+ await roh("/workspace/api/anruf/adressen", CREW, { cookie: kDogi }, {
adressen: [{ urls: "turn:turn.dogfather-universe.com:3478" }],
}, "PUT");
const a = await roh("/workspace/api/anruf/adressen", CREW, { cookie: kModi });
@@ -738,7 +755,7 @@ melde("=== 6b. Zugangsdaten fuer die Vermittlung ===");
/* Jeder bekommt EIGENE Zugangsdaten -- im coturn-Protokoll steht
damit die Nummer, nicht ein Sammelkonto. */
- const rDogi = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
+ const rDogi = json(await roh("/workspace/api/anruf/adressen", CREW, { cookie: kDogi }));
const dogiTurn = (rDogi.adressen || []).find(
(e) => /^turns?:/.test(String(e.urls || "")));
ok(!!dogiTurn?.username && dogiTurn.username !== ersterTurn?.username,
@@ -753,10 +770,10 @@ melde("=== 6b. Zugangsdaten fuer die Vermittlung ===");
/* --- Aufraeumen: der naechste Abschnitt faengt sauber an ---------- */
{
- await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi },
+ await roh("/workspace/api/anruf/adressen", CREW, { cookie: kDogi },
{ adressen: [] }, "PUT");
try { unlinkSync(datei); } catch { /* schon weg */ }
- const zurueck = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }));
+ const zurueck = json(await roh("/workspace/api/anruf/adressen", CREW, { cookie: kDogi }));
ok(zurueck.adressen?.length === 0 && !zurueck.turn_grund,
"danach ist wieder alles leer -- keine erfundene Adresse bleibt stehen");
}
@@ -875,7 +892,7 @@ melde("=== 7. Im Browser ===");
ok(toene > 0, `es klingelt hoerbar -- ${toene} Toene erzeugt`);
/* Und der Server weiss davon. */
- const l = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
+ const l = json(await roh(`/workspace/api/anruf/${raumId}`, CREW, { cookie: kDogi }));
ok(l.laeuft === true, "der Server kennt den Anruf");
await seite.screenshot({ path: "server/pruef-anruf.png", fullPage: false });
@@ -885,7 +902,7 @@ melde("=== 7. Im Browser ===");
await seite.waitForTimeout(900);
ok(await seite.evaluate(() => document.getElementById("anruf")?.hidden === true),
"nach dem Auflegen ist der Kasten weg");
- const l2 = json(await roh(`/workspace/api/anruf/${raumId}`, HAUS, { cookie: kDogi }));
+ const l2 = json(await roh(`/workspace/api/anruf/${raumId}`, CREW, { cookie: kDogi }));
ok(l2.laeuft === false, "und der Server auch");
ok(konsole.length === 0,
@@ -931,7 +948,7 @@ melde("=== 8. Zu dritt ===");
unklar("kein Playwright -- der Gruppenanruf wurde nicht ausprobiert");
} else {
/* Ein Gruppenraum mit allen dreien. */
- const g = await roh("/workspace/api/chat/gruppe", HAUS, { cookie: kDogi },
+ const g = await roh("/workspace/api/chat/gruppe", CREW, { cookie: kDogi },
{ name: "Testrunde", mit: [idHand, idModi] });
const gruppeId = json(g).raum?.id || json(g).raum_id || json(g).id;
ok(!!gruppeId, `ein Gruppengespraech entsteht (${g.code}, Raum ${gruppeId})`);
@@ -986,7 +1003,7 @@ melde("=== 8. Zu dritt ===");
await a.se.waitForTimeout(6000);
/* Der Server: drei drin. */
- const lage = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi }));
+ const lage = json(await roh(`/workspace/api/anruf/${gruppeId}`, CREW, { cookie: kDogi }));
ok(lage.wer?.length === 3,
`der Server zaehlt drei Teilnehmer (${lage.wer?.map((w) => w.name).join(", ")})`);
@@ -1172,7 +1189,7 @@ melde("=== 8. Zu dritt ===");
/* Einer legt auf -- fuer die anderen laeuft es weiter. */
await c.se.click("#anruf-auflegen");
await c.se.waitForTimeout(2000);
- const lage2 = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi }));
+ const lage2 = json(await roh(`/workspace/api/anruf/${gruppeId}`, CREW, { cookie: kDogi }));
ok(lage2.laeuft === true && lage2.wer?.length === 2,
`einer geht, zwei telefonieren weiter (${lage2.wer?.length})`);
const zA = await zaehlen(a);
@@ -1197,7 +1214,7 @@ melde("=== 8. Zu dritt ===");
await a.se.click("#anruf-auflegen");
await b.se.click("#anruf-auflegen");
await a.se.waitForTimeout(1200);
- const lage3 = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi }));
+ const lage3 = json(await roh(`/workspace/api/anruf/${gruppeId}`, CREW, { cookie: kDogi }));
ok(lage3.laeuft === false, "wenn alle auflegen, ist der Anruf vorbei");
await a.k.close(); await b.k.close(); await c.k.close();
diff --git a/server/pruef-crew-wand-bild.mjs b/server/pruef-crew-wand-bild.mjs
index 96bcaf7f..f521d23e 100644
--- a/server/pruef-crew-wand-bild.mjs
+++ b/server/pruef-crew-wand-bild.mjs
@@ -38,6 +38,10 @@ process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
+/* `rollenImHaus` kommt aus derselben Datei, aus der der Server die
+ Zugangswand baut -- damit kann diese Pruefung nicht veralten,
+ wenn jemand eine Rolle dazunimmt. */
+import { rollenImHaus } from "./crew-adresse.js";
import { notbremse } from "./helfer-notbremse.mjs";
import { eigenerPort } from "./helfer-port.mjs";
await import("./index.js");
@@ -138,9 +142,33 @@ const meldungen = [];
hat, sieht sofort, ob das Absicht war. */
const rollen = (await seite.locator(".rolle").evaluateAll(
(ns) => ns.map((n) => n.dataset.rolle || ""))).sort();
- const SOLL = ["admin", "gast", "hand", "modi"];
- ok(JSON.stringify(rollen) === JSON.stringify(SOLL),
- `${geraet.name}: die vier Rollen stehen da (${rollen.join(", ") || "keine"})`);
+ /* ==== DRITTER ANLAUF, UND DIESMAL OHNE ABSCHRIFT (25.09.2026) ===
+
+ Hier stand `=== 3` (10.09.), dann die Liste
+ ["admin","gast","hand","modi"] (19.09.) -- und beide sind aus
+ demselben Grund veraltet: Am 21.09. kam die LINKE HAND dazu. Die
+ Wand zeigte sie richtig an, die Pruefung kannte sie nicht, und
+ sie war seither rot, ohne dass etwas kaputt war.
+
+ Der Kommentar direkt darueber warnt seit dem 19.09. wortwoertlich
+ vor festen Namen in Pruefungen („eine Zeitbombe mit Datum") --
+ und die Zeile darunter war selbst eine. Ein Kommentar, der vor
+ einem Fehler warnt, verhindert ihn nicht.
+
+ JETZT WIRD ABGELEITET: `rollenImHaus("crew")` ist dieselbe
+ Quelle, aus der der Server die Wand baut. Wer morgen eine Rolle
+ dazunimmt oder verschiebt, verschiebt diese Pruefung mit. Und
+ weicht die Wand doch einmal ab, steht in der Meldung, WELCHE
+ Rolle fehlt oder zu viel ist -- eine Zahl sagt nur „vier
+ erwartet, fuenf gefunden" und verschweigt das Entscheidende. */
+ const SOLL = [...rollenImHaus("crew")].sort();
+ const fehlt = SOLL.filter((r) => !rollen.includes(r));
+ const zuviel = rollen.filter((r) => !SOLL.includes(r));
+ ok(fehlt.length === 0 && zuviel.length === 0,
+ `${geraet.name}: alle ${SOLL.length} Rollen des Hauses stehen da `
+ + `(${rollen.join(", ") || "keine"})`
+ + (fehlt.length ? ` -- ES FEHLT: ${fehlt.join(", ")}` : "")
+ + (zuviel.length ? ` -- ZU VIEL: ${zuviel.join(", ")}` : ""));
/* Jede muss auch bedienbar sein. Eine Kachel, die im HTML steht,
aber 0 px hoch ist, zaehlt oben mit und laesst sich trotzdem
nicht antippen -- genau die Sorte Fehler, die eine Zaehlung
diff --git a/server/pruef-entwicklung-kacheln.mjs b/server/pruef-entwicklung-kacheln.mjs
index b0345ccb..49aa5bb0 100644
--- a/server/pruef-entwicklung-kacheln.mjs
+++ b/server/pruef-entwicklung-kacheln.mjs
@@ -124,7 +124,39 @@ neueAufgabe("Clips vom Samstag schneiden", idDiene, GESTERN, "offen");
neueAufgabe("Plan für nächste Woche", idDiene, IN_DREI, "arbeit");
neueAufgabe("Ideen sammeln", idDiene, null, "offen");
neueAufgabe("Längst abgehakt", idDiene, GESTERN, "erledigt");
+
+/* =====================================================================
+ ZUGETRAGENE PUNKTE (25.09.2026) -- der Nenner der Kachel
+ =====================================================================
+
+ Filipe: „es sollen nur die menge angezeigt werden die wir zutragen
+ und erledigte dan auch nur die die erledigt wurden von den
+ zugetragenen."
+
+ BIS HEUTE stand auf jeder Kachel „x von 68" -- 68 war der ganze
+ Katalog. Jetzt ist das Ganze das, was dieser Mensch bekommen hat.
+ Wer nichts bekommen hat, hat keinen Nenner, und dort steht ein Satz
+ statt einer Null.
+
+ BEIDE FAELLE WERDEN GEBAUT, und das ist der Punkt: Diene bekommt
+ vier Punkte zugetragen und bei einem davon eine Einschaetzung
+ (also 1 von 4 = 25 %), VanVan bekommt nichts. Eine Pruefung, die nur
+ den einen Fall kennt, haette den Umbau nicht bemerkt -- genau das
+ ist heute frueh passiert: Sie war rot mit „noch nichts zugetragen,
+ noch nichts zugetragen" und konnte nicht sagen, ob das richtig ist.
+
+ DIE FUENFTE EINSCHAETZUNG IST DIE GEGENPROBE. Sie steht auf einem
+ Punkt, den Diene NICHT zugetragen bekommen hat. Zaehlte die
+ Uebersicht sie mit, stuende dort „2 von 4" -- und in der Karte
+ daneben „1 von 4". Zwei Zahlen fuer dieselbe Sache. */
+const { ENTWICKLUNG_FREMD } = await import("./workspace-entwicklungs-punkte.js");
+const VIER = ENTWICKLUNG_FREMD.slice(0, 4).map((x) => x.schluessel);
+const FUENFTER = ENTWICKLUNG_FREMD[4].schluessel;
d.close();
+/* Eingetragen wird weiter unten ueber die ECHTEN Routen -- die
+ Tabellen des Entwicklungsmoduls entstehen erst beim ersten Zugriff
+ darauf, vorher gibt es sie gar nicht. Und ueber die Route geprueft
+ zu werden ist ohnehin mehr wert als ein INSERT von der Seite. */
const an = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
@@ -132,6 +164,25 @@ const an = await fetch(BASIS + "/workspace/api/anmelden", {
});
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
+/* ---- Jetzt die Zuteilung, ueber die Wege, die es wirklich gibt ----
+ Vier Punkte fuer Diene, eine Einschaetzung auf einem davon und eine
+ auf einem FUENFTEN, den sie nicht bekommen hat (die Gegenprobe:
+ zaehlt die Uebersicht sie mit, stuende dort 2 statt 1). */
+const mitKeks = (pfad, art, koerper) => fetch(BASIS + pfad, {
+ method: art,
+ headers: { "Content-Type": "application/json", Cookie: keks,
+ Origin: BASIS, Referer: BASIS + "/workspace/entwicklung.html" },
+ body: koerper ? JSON.stringify(koerper) : undefined,
+});
+const zut = await mitKeks(`/workspace/api/entwicklung/person/${idDiene}/punkte`,
+ "PUT", { punkte: VIER });
+melde(` -- Zuteilung: HTTP ${zut.status}`);
+for (const s of [VIER[0], FUENFTER]) {
+ const r = await mitKeks(`/workspace/api/entwicklung/person/${idDiene}/${s}`,
+ "PUT", { stufe: "laeuft" });
+ if (r.status >= 400) melde(` -- Einschaetzung ${s}: HTTP ${r.status}`);
+}
+
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
@@ -235,17 +286,46 @@ try {
};
}));
- const gesamt = lage?.gesamt_fremd || 0;
- const sollAnteil = (p) => (gesamt > 0
- ? Math.round((Number(p?.gesetzt) || 0) / gesamt * 100) : 0);
+ /* ==== DAS GANZE IST, WAS ZUGETRAGEN IST (25.09.2026) ==========
+
+ Hier stand `gesamt_fremd` -- die Zahl aller Punkte im Katalog
+ (68). Die gibt es weiterhin, sie ist aber nur noch die Groesse
+ der AUSWAHL, nicht der Nenner der Kachel. Der Nenner steht seit
+ heute je Person in `zugeteilt`.
+
+ WER NICHTS ZUGETRAGEN BEKOMMEN HAT, hat keinen Nenner: Dort
+ steht „–" im Ring und ein Satz in der Zeile, keine Null. „0 von
+ 0 erledigt" liest sich wie ein Versaeumnis; es ist aber keins,
+ sondern eine offene Aufgabe der Leitung. */
+ const katalog = lage?.gesamt_fremd || 0;
+ const ganzes = (p) => Number(p?.zugeteilt) || 0;
+ const sollAnteil = (p) => (ganzes(p) > 0
+ ? Math.round((Number(p?.gesetzt) || 0) / ganzes(p) * 100) : 0);
const nachNr = new Map((lage?.personen || []).map((p) => [Number(p.id), p]));
+ /* Dass die Testdaten ueberhaupt angekommen sind, wird gemessen und
+ nicht vorausgesetzt: Eine Pruefung, deren Voraussetzung fehlt,
+ darf nicht still gruen werden. */
+ const mitZuteilung = [...nachNr.values()].filter((x) => ganzes(x) > 0);
+ const ohneZuteilung = [...nachNr.values()].filter((x) => ganzes(x) === 0);
+ ok(mitZuteilung.length >= 1 && ohneZuteilung.length >= 1,
+ `beide Faelle liegen vor: ${mitZuteilung.length} mit Zuteilung, `
+ + `${ohneZuteilung.length} ohne`);
+ ok(mitZuteilung.every((x) => ganzes(x) === 4 && Number(x.gesetzt) === 1),
+ `die zugetragenen Punkte zaehlen, der fuenfte nicht `
+ + `(${mitZuteilung.map((x) => `${x.name}: ${x.gesetzt} von ${ganzes(x)}`).join(", ")})`);
+ ok(katalog > 4,
+ `und der Katalog ist weiterhin groesser als das Zugetragene (${katalog})`);
+
ok(z.length > 0 && z.every((k) => nachNr.has(k.nr)),
`jede Kachel gehoert zu einer Person aus der Auskunft (${z.map((k) => k.nr).join(", ")})`);
- const ringOk = z.every((k) => k.ringZahl === String(sollAnteil(nachNr.get(k.nr))));
+ const ringOk = z.every((k) => {
+ const pp = nachNr.get(k.nr);
+ return k.ringZahl === (ganzes(pp) > 0 ? String(sollAnteil(pp)) : "\u2013");
+ });
ok(ringOk, `die Ziffer im Ring ist der gerechnete Anteil `
- + `(${z.map((k) => `${k.name}: ${k.ringZahl}%`).join(", ")})`);
+ + `(${z.map((k) => `${k.name}: ${k.ringZahl}`).join(", ")})`);
/* Der BOGEN ist das, was man sieht. Umfang 100 -- die erste Zahl
im stroke-dasharray ist damit buchstaeblich der Prozentwert. */
@@ -257,23 +337,28 @@ try {
/* DIE ZEILE DARUNTER nennt beide Zahlen, aus denen der Anteil
entsteht -- ohne sie waere "68 %" eine Behauptung ohne Beleg. */
const standOk = z.every((k) => {
- const p = nachNr.get(k.nr);
- return k.standZahl === String(Number(p?.gesetzt) || 0)
- && k.standRest === ` von ${gesamt} angesehen`;
+ const pp = nachNr.get(k.nr);
+ return ganzes(pp) > 0
+ ? (k.standZahl === String(Number(pp?.gesetzt) || 0)
+ && k.standRest === ` von ${ganzes(pp)} erledigt`)
+ : (k.standZahl === "" && k.standRest === "noch nichts zugetragen");
});
- ok(standOk, `"x von ${gesamt} angesehen" stimmt bei jeder Kachel `
- + `(${z.map((k) => k.standZahl + k.standRest).join(", ")})`);
+ ok(standOk, `"x von n erledigt" bzw. der Satz stimmt bei jeder Kachel `
+ + `(${z.map((k) => k.standZahl + k.standRest).join(" | ")})`);
/* UND DAS VORLESEPROGRAMM HOERT DASSELBE. Ein Ring ist ein Bild;
ohne Schild ist er fuer ein Vorleseprogramm gar nicht da. */
ok(z.every((k) => k.rolle === "img"), "der Ring gibt sich als Bild zu erkennen");
const schildOk = z.every((k) => {
- const p = nachNr.get(k.nr);
- const soll = `${Number(p?.gesetzt) || 0} von ${gesamt} angesehen `
- + `(${sollAnteil(p)} Prozent)`;
+ const pp = nachNr.get(k.nr);
+ const soll = ganzes(pp) > 0
+ ? `${Number(pp?.gesetzt) || 0} von ${ganzes(pp)} erledigt `
+ + `(${sollAnteil(pp)} Prozent)`
+ : "noch nichts zugetragen";
return k.schild === soll;
});
- ok(schildOk, `und sagt dasselbe wie das Bild ("${z[0]?.schild}")`);
+ ok(schildOk, `und sagt dasselbe wie das Bild `
+ + `(${z.map((k) => `"${k.schild}"`).join(" | ")})`);
/* EINE NULL IST KEINE WARNUNG: "verschieden gesehen" steht nur da,
wenn es etwas gibt. Im Testbestand hat niemand etwas anders
diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js
index 1ecbd90a..29ba859e 100644
--- a/server/workspace-entwicklung.js
+++ b/server/workspace-entwicklung.js
@@ -987,7 +987,16 @@ entwicklungRouter.put("/workspace/api/entwicklung/person/:id/punkt/:schluessel",
danach in keiner Liste steht und auch nicht wegzubekommen
ist. Dieselbe Pruefung wie beim Fenster. */
if (!ENTWICKLUNG_FREMD.some((pt) => pt.schluessel === schluessel)) {
- return res.status(400).json({ fehler: "unbekannter_punkt" });
+ /* EIN SATZ, KEINE KENNUNG (25.09.2026, gefunden von
+ pruef-meldungen). Das Haus verlangt zu jeder Kennung einen
+ Satz in meldung.js -- sonst steht auf dem Bildschirm ein
+ Wort mit Unterstrich. Diesen Fall trifft ein Mensch nie (die
+ Oberfläche schickt nur Schlüssel aus ihrer eigenen Liste),
+ aber „trifft nie ein" ist genau die Begründung, mit der
+ solche Stellen stehen bleiben, bis sie doch jemand sieht. */
+ return res.status(400).json({
+ fehler: "Diesen Punkt gibt es nicht – lade die Seite neu.",
+ });
}
const an = req.body?.an === true;
if (an) {
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 5e7e2ca0..67107f71 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,24 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+
+
-
+