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`. --> - - + + - + + - +