Files
dogfather-universe/server/pruef-personen-formular.mjs
T
DogFatherGitandClaude Opus 5 390f592967 Der Modi traegt denselben Husky wie DogFather
Wunsch Filipe (Bildschirmfoto, 10.09.2026): "das modi symbol soll das
gleiche sein wie bei dogfather".

Vorher stand dort das Schild, und das war doppelt falsch: Es gehoert
schon dem Scout, und es stellte die Modis neben die Agentur statt zu
DogFather. Sie sind SEIN Team -- das Zeichen sagt das jetzt auch.

Kein neues Zeichen dafuer: `#r-husky` steht in personen.html laengst.
Ein eigenes waere eine Zeile mehr in einer Datei, die jeder bekommt --
und eine, die nur bei einer einzigen Rolle benutzt wird.

DIE PRUEFUNG VERGLEICHT GEGEN DIE ADMIN-KARTE, nicht gegen "#r-husky".
Waere dort morgen ein anderes Zeichen, muessten beide mitwandern --
der Wunsch war "dasselbe wie", nicht "der Husky".

AUF DEMSELBEN BILDSCHIRMFOTO stand "Sichtbar nur fuer die
DogFather-Rolle". In Kommentaren schreibt das Haus ASCII, in TEXT, den
jemand liest, nicht -- dort sieht es aus wie ein Fehler, weil es einer
ist. Berichtigt und mitgeprueft.

NEBENBEFUND, NICHT ANGEFASST: Im Creator-Katalog stehen zehn weitere
sichtbare Texte mit ASCII-Ersatz ("dafuer", "zaehlt", "spaet",
"groesser", "uebersteuert"). Sie stehen live auf den Bildschirmen der
Creator. Nachgemessen: In meinem eigenen Katalog aus Teil 2 sind es
0 von 120 sichtbaren Texten. Die zehn gehoeren berichtigt, aber nicht
nebenbei in einem Commit ueber ein Symbol.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 01:24:26 +02:00

289 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
Prüfung: das Formular "Neue Person" und die Rechte auf den beiden
Verwaltungsbereichen (02.09.2026).
Zwei Meldungen:
"das sieht ja mal richtig scheisse aus. mach das besser bitte."
-- Die Rollenauswahl lief unten aus dem Formular heraus und legte
sich über die Personenliste. Ursache war keine schlampige
Gestaltung, sondern eine Raster-Regel für EINZEILIGE Felder
(feste Zeilenhöhe 44 px), die auch auf den Block aus vier hohen
Karten angewandt wurde.
"die manager sollen diese kategorien garnicht sehen"
-- "Personen & Zugänge" und "Automationen" gehören allein DogFather.
Der zweite Punkt wird auf DREI Ebenen geprüft, nicht nur an der
Kachel: Wegnehmen, was man sieht, ist kein Rechteentzug. Es zählt,
was der Server antwortet.
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-pform-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4198";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
notbremse(480_000, "pruef-personen-formular");
await new Promise((r) => setTimeout(r, 700));
const BASIS = "http://127.0.0.1:4198";
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.();
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Dogfather", "admin", "CODE-DOGI-0001");
anlegen("Cigdem", "manager", "CODE-CIGDEM-01");
anlegen("Tili", "creator", "CODE-TILI-0001");
d.close();
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
async function anmelden(rolle, code, breite = 1440) {
const kontext = await browser.newContext({ viewport: { width: breite, height: 1000 } });
const seite = await kontext.newPage();
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
await seite.click(`.rolle[data-rolle="${rolle}"]`);
await seite.fill("#code", code);
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 15000 });
return seite;
}
/* ===================================================================
TEIL 1 — Das Formular läuft nicht mehr über
=================================================================== */
console.log("\n— Das Formular „Neue Person“ —");
const dogi = await anmelden("admin", "CODE-DOGI-0001");
await dogi.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
await dogi.waitForTimeout(700);
await dogi.click("#neu-oeffnen").catch(() => {});
await dogi.waitForTimeout(500);
const mass = await dogi.evaluate(() => {
const form = document.getElementById("neu");
const wahl = document.getElementById("f-rolle");
const karten = [...document.querySelectorAll(".rollenwahl__knopf")];
const f = form.getBoundingClientRect();
const w = wahl.getBoundingClientRect();
/* Das erste Element NACH dem Formular -- über das die Karten vorher
hinweggelaufen sind. */
const danach = document.querySelector("#liste");
const dn = danach ? danach.getBoundingClientRect() : null;
return {
formUnten: Math.round(f.bottom),
wahlUnten: Math.round(w.bottom),
wahlOben: Math.round(w.top),
listeOben: dn ? Math.round(dn.top) : null,
karten: karten.length,
kartenRollen: karten.map((k) => k.dataset.rolle),
/* Welches Zeichen an welcher Karte haengt -- fuer die Frage, ob
zwei Rollen dasselbe tragen. */
kartenZeichen: Object.fromEntries(karten.map((k) => [k.dataset.rolle,
(k.querySelector("use") || {}).getAttribute?.("href") || ""])),
kartenTexte: Object.fromEntries(karten.map((k) => [k.dataset.rolle,
(k.querySelector(".rollenwahl__erklaerung") || {}).textContent || ""])),
kartenBreiten: karten.map((k) => Math.round(k.getBoundingClientRect().width)),
/* Liegt eine Karte weiter rechts als das Formular breit ist? */
ueberRand: karten.some((k) => k.getBoundingClientRect().right > f.right + 1),
};
});
/* SECHS, seit es die verborgene Modi-Rolle gibt (09.09.2026) -- vorher
fünf, seit es Spicy Media gibt. Die Erwartung steht hier absichtlich
fest und wird nicht aus der Oberfläche abgeleitet: Fiele eine Rolle
aus der Wahl, müsste das auffallen -- eine Zahl, die sich selbst
nachzählt, kann das nicht.
DIE SECHSTE IST NICHT WIE DIE ANDEREN. Die fünf stehen in
assets/js/personen.js, also in einer Datei, die jeder bekommt, der
die Seite öffnet. "Modi" darf dort nicht stehen -- ein Blick in den
Quelltext würde sonst genügen, um den verborgenen Zugang zu
entdecken. Sie kommt deshalb aus der Antwort des Servers und nur an
die DogFather-Rolle. Dass sie hier auftaucht, ist gleichzeitig der
Nachweis, dass dieser Weg funktioniert.
Geprüft wird ab jetzt die LISTE und nicht nur die Anzahl: Sechs
Karten könnten auch fünf richtige und eine doppelte sein. */
ok(mass.karten === 6, `alle sechs Rollen stehen zur Wahl (${mass.karten})`);
ok(JSON.stringify([...mass.kartenRollen].sort())
=== JSON.stringify(["admin", "creator", "manager", "modi", "scout", "spicy"]),
`und es sind die richtigen (${mass.kartenRollen.join(", ")})`);
/* DASSELBE ZEICHEN WIE BEI DOGFATHER (Wunsch Filipe, 10.09.2026: "das
modi symbol soll das gleiche sein wie bei dogfather").
Verglichen wird gegen die Admin-Karte und nicht gegen "#r-husky":
Waere dort morgen ein anderes Zeichen, muessten beide mitwandern --
der Wunsch war "dasselbe wie", nicht "der Husky". */
ok(mass.kartenZeichen.modi && mass.kartenZeichen.modi === mass.kartenZeichen.admin,
`Modi trägt dasselbe Zeichen wie DogFather (${mass.kartenZeichen.modi} / `
+ `${mass.kartenZeichen.admin})`);
/* Und die Beschreibung ist auf Deutsch geschrieben, nicht auf ASCII.
Auf dem Bildschirmfoto stand "Sichtbar nur fuer die DogFather-Rolle"
-- in einem Text, den jemand liest, sieht das aus wie ein Fehler,
weil es einer ist. */
ok(mass.kartenTexte.modi && !/(fuer|ueber|waehrend|koennen|moeglich)/
.test(mass.kartenTexte.modi),
`und ihre Beschreibung hat echte Umlaute ("${mass.kartenTexte.modi.slice(0, 60)}…")`);
ok(mass.wahlUnten <= mass.formUnten,
`die Rollenauswahl endet INNERHALB des Formulars (${mass.wahlUnten} ≤ ${mass.formUnten})`);
ok(!mass.ueberRand, "keine Karte ragt seitlich aus dem Formular");
ok(mass.listeOben === null || mass.wahlUnten <= mass.listeOben,
`die Karten liegen nicht über der Personenliste (${mass.wahlUnten} ≤ ${mass.listeOben})`);
/* Alle Karten gleich breit -- ungleiche Kacheln sind das, was "sieht
scheisse aus" in Zahlen bedeutet. */
const breiteste = Math.max(...mass.kartenBreiten);
const schmalste = Math.min(...mass.kartenBreiten);
ok(breiteste - schmalste <= 2,
`alle Karten sind gleich breit (${schmalste}–${breiteste} px)`);
/* Die gewählte Rolle muss man SEHEN, nicht erraten. */
const vorher = await dogi.evaluate(() =>
getComputedStyle(document.querySelector('.rollenwahl__knopf[data-an="ja"]'), "::after").content);
ok(vorher.includes("✓"), `die gewählte Rolle trägt ein Häkchen (${vorher})`);
await dogi.screenshot({ path: "pruef-personen-formular.png", fullPage: false });
/* Auf dem Handy darf nichts herausragen. */
const handy = await anmelden("admin", "CODE-DOGI-0001", 390);
await handy.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
await handy.waitForTimeout(700);
await handy.click("#neu-oeffnen").catch(() => {});
await handy.waitForTimeout(500);
const engUeber = await handy.evaluate(() =>
document.documentElement.scrollWidth - document.documentElement.clientWidth);
ok(engUeber <= 0, `auf dem Handy ragt nichts heraus (${engUeber}px)`);
await handy.screenshot({ path: "pruef-personen-formular-handy.png", fullPage: false });
/* ===================================================================
TEIL 2 — Die beiden Bereiche gehören DogFather allein
=================================================================== */
console.log("\n— Automationen: nur DogFather. Personen: auch die Leitung —");
/* AM 07.09.2026 GETEILT. Hier standen beide Bereiche unter derselben
Regel: „gehören DogFather allein". Für die Automationen gilt das
weiter. Für die Personenseite hat Filipe es umgedreht -- Manager
sollen „creator und auch wirklich nur creator hinzufügen können",
und dafür müssen sie hinein.
Aus einer Regel sind damit ZWEI geworden, und beide werden gemessen:
Die Seite geht auf, die Verwaltungsdaten dahinter bleiben zu. Genau
diese Trennung IST die Sicherung. Hätte ich hier nur die Zeilen
gelöscht, wäre der neue Weg ab sofort ungeprüft -- und die Zahl der
Prüfungen wäre gesunken, ohne dass jemand hinsieht. */
const cigdem = await anmelden("manager", "CODE-CIGDEM-01");
await cigdem.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
await cigdem.waitForTimeout(800);
const kacheln = await cigdem.evaluate(() =>
[...document.querySelectorAll(".kachel")].map((k) => k.innerText.split("\n")[0].trim()));
ok(kacheln.some((t) => /Personen/.test(t)),
"1. die Kachel „Personen & Zugänge“ steht jetzt bei der Managerin");
ok(!kacheln.some((t) => /Automationen/.test(t)),
"1. die Kachel „Automationen“ fehlt weiterhin");
ok(kacheln.length > 8,
` – und der Rest steht auch da (${kacheln.length} Kacheln)`);
/* 2. Die SEITEN, falls jemand die Adresse kennt. */
{
await cigdem.goto(BASIS + "/workspace/automation.html", { waitUntil: "networkidle" });
ok(cigdem.url().endsWith("/start.html"),
`2. automation.html führt zurück zur Startseite (${cigdem.url().split("/").pop()})`);
await cigdem.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
await cigdem.waitForTimeout(900);
ok(cigdem.url().endsWith("/personen.html"),
`2. personen.html geht bei ihr auf (${cigdem.url().split("/").pop()})`);
/* Und sie sieht dort NUR das Anlegen -- keine Liste, kein Protokoll,
und keinen Ladehinweis ohne Ende. Der stand dort wirklich:
`laden()` wird für sie mit Absicht nicht aufgerufen, und niemand
hat aria-busy zurückgesetzt. Für einen Screenreader lud die Seite
dann für immer. */
const seitenbild = await cigdem.evaluate(() => ({
nurAnlegen: document.body.dataset.nurAnlegen || null,
laedtNoch: document.querySelectorAll('[aria-busy="true"]').length,
erklaerung: document.getElementById("unterzeile")?.textContent.trim() || "",
formularDa: !!document.getElementById("neu") && !document.getElementById("neu").hidden,
}));
ok(seitenbild.nurAnlegen === "ja" && seitenbild.formularDa === true,
"2. sie sieht dort das Anlegen-Formular");
ok(seitenbild.laedtNoch === 0,
`2. und nichts hängt auf „wird geladen“ (${seitenbild.laedtNoch})`);
ok(seitenbild.erklaerung.includes("DogFather"),
`2. ein Satz erklärt, warum hier so wenig steht ("${seitenbild.erklaerung.slice(0, 40)}…")`);
}
/* 3. Die SCHNITTSTELLE, falls jemand ganz an der Seite vorbeigeht.
Das ist die einzige Ebene, die wirklich zählt -- die beiden darüber
sind Bequemlichkeit. */
const api = (seite, pfad, optionen) => seite.evaluate(async ([p, o]) => {
const a = await fetch(p, { credentials: "same-origin", ...(o || {}) });
return a.status;
}, [pfad, optionen]);
await cigdem.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
ok(await api(cigdem, "/workspace/api/verwaltung/personen") === 404,
"3. die Personenverwaltung antwortet ihr mit 404");
ok(await api(cigdem, "/workspace/api/zustand") === 404,
"3. der Systemzustand ebenfalls");
ok(await api(cigdem, "/workspace/api/ki/schalter", { method: "PUT" }) !== 200,
"3. und den KI-Schalter darf sie nicht umlegen");
/* GEGENPROBE: Bei DogFather geht das alles weiterhin. Ohne sie wäre
jede Zeile oben auch dann grün, wenn die Bereiche für ALLE kaputt
wären -- und das würde erst auffallen, wenn er sie braucht. */
ok(await api(dogi, "/workspace/api/verwaltung/personen") === 200,
"GEGENPROBE: DogFather kommt weiterhin an die Personenverwaltung");
ok(await api(dogi, "/workspace/api/zustand") === 200,
"GEGENPROBE: und an den Systemzustand");
/* Und die Managerin behält, was sie behalten soll. */
ok(await api(cigdem, "/workspace/api/aufgaben") === 200,
"die Managerin arbeitet ansonsten unverändert weiter (Aufgaben)");
ok(await api(cigdem, "/workspace/api/calls") === 200, " und die Gespräche auch");
const ERWARTET = 17;
console.log("");
ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`);
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`);
process.exit(fehler ? 1 : 0);