Files
dogfather-universe/server/mess-hand-personen.mjs
T
DogFatherGitandClaude Opus 5 63a3fb4af8 Die rechte Hand sieht die Personenseite wirklich -- Liste, Rollenkarten und das Protokoll
Filipe, zum wiederholten Mal und mit einem Bildschirmfoto genau dieser
Seite: "zum hunderstenmal, also bitte mach dass es jetzt endlich
klappt, die rechte hand sieht das immer noch nicht obwohl ich will dass
die rechte hand das auch sieht."

ZUERST NACHGEMESSEN, NICHT GERATEN. Am 24.09. habe ich auf ein
Bildschirmfoto hin an der falschen Seite gebaut und es im Commit selbst
notiert. Diesmal zuerst mess-hand-personen.mjs: dieselbe Seite, zwei
Anmeldungen, und der Unterschied wird aufgezaehlt. Ergebnis in einer
Zeile -- sie bekam vom Server alle acht Personen (HTTP 200) und sah auf
dem Bildschirm NICHTS davon. Nur das Anlege-Formular, darueber der Satz
"Codes, Sperren und das Protokoll bleiben bei DogFather".

ZWEI URSACHEN, UND NUR EINE WAR EINE SCHRANKE:

  1. Die OBERFLAECHE hat die Liste versteckt, die sie laengst geladen
     hatte. `personen.js` entschied die Ausbaustufe mit
     `ich.rolle !== 'admin'`, setzte damit `data-nur-anlegen`, und
     `personen.css` blendet darauf hin die Liste, das Protokoll und
     "Alle aufklappen" aus. Diese CSS-Regel stammt vom 07.09. und war
     fuer Manager und Spicy Media gedacht; die rechte Hand ist erst
     danach dazugekommen und fiel stillschweigend mit hinein.

     Das ist in dieser einen Datei die DRITTE Stelle, an der ein
     Rollenvergleich im Browser veraltet ist -- nach dem 22.09.
     ("keine Knoepfe") und dem 24.09. ("keine Rollenwahl"). Jedes Mal
     hatte sie das Recht und sah es nicht.

  2. Das Protokoll war am Server zu (HTTP 404). Damit ist der Satz von
     oben ueberholt: Filipes Ansage vom 24.09. -- "die selben rechte da
     haben wie dogfather, das einzige was sie nicht kann ist die
     dogfather rolle oder leute anfassen" -- laesst dafuer keinen Rest.

EINE AUSKUNFT FUER DREI STELLEN. `fuehrtDieZugaenge(person)` steht
jetzt in workspace.js und beantwortet dieselbe Frage fuer die Tuer am
Server, fuer `/api/ich` (`darf_zugaenge_fuehren`) und fuer die
Ausbaustufe der Seite. Drei Abschriften waeren drei Gelegenheiten, dass
die naechste Aenderung nur zwei davon trifft -- genau so ist dieser
Fehler entstanden.

`istHand` WAERE FALSCH GEWESEN. Es fasst beide Haende zusammen, und
fuer die linke gilt ausdruecklich das Gegenteil ("sieht weder
Bewerbungen noch den vertraulichen Meldeweg"). Wer hier den
Sammelbegriff nimmt, dreht eine ausgesprochene Entscheidung
stillschweigend um. Die Prueflung fragt sie deshalb einzeln.

DIE PRUEFUNG ZIEHT NACH (40 -> 49). Abschnitt 6 prueft beides: dass
die rechte Hand dasselbe Protokoll bekommt wie DogFather, und dass die
Auskunft, aus der die Oberflaeche ihre Ausbaustufe baut, mit der Tuer
am Server uebereinstimmt. Genau dieser Abgleich hat gefehlt: Eine
Rechtepruefung, die nur Serverantworten ansieht, hat den Fehler zwei
Tage lang nicht bemerkt. Dazu drei Gegenproben (linke Hand 404, Modi
404, linke Hand `darf_zugaenge_fuehren === false`).

Beim ersten Lauf waren diese Gegenproben rot -- mit 401 statt 404. Die
Abschnitte davor sperren und loeschen absichtlich Leute, und eine tote
Sitzung antwortet mit 401: Das sieht aus wie "darf nicht" und heisst
"gibt es nicht mehr". Ein 401 als Gegenprobe fuer ein 404 ist ein Haken
ohne Gegenstand. Abschnitt 6 legt sich deshalb frische Zugaenge an.

Geprueft: pruef-hand-personen (49, 0 Fehler), pruef-personen-liste,
pruef-personen-kachel (45), pruef-personen-loeschen,
pruef-modi-verborgen (85). Unveraendert rot und an HEAD nachgemessen,
also nicht von diesem Umbau: pruef-personen-formular (2),
pruef-community-sicht (1), pruef-spicy (3).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-25 02:47:07 +02:00

243 lines
11 KiB
JavaScript

/* =====================================================================
WAS SIEHT DIE RECHTE HAND AUF DER PERSONENSEITE? (25.09.2026)
---------------------------------------------------------------------
WARUM ES DIESE DATEI GIBT
Filipe, zum wiederholten Mal: „die rechte hand sieht das immer noch
nicht obwohl ich will dass die rechte hand das auch sieht." Dazu ein
Bildschirmfoto der Personenseite -- Rollenkarten oben, „Letzte
Ereignisse" unten.
ICH KOENNTE RATEN, welches „das" gemeint ist. Am 24.09.2026 habe ich
genau das getan, auf ein Bildschirmfoto hin an der falschen Seite
gebaut und es im Commit selbst notiert („ZUERST EIN IRRTUM VON MIR").
Diesmal wird nachgesehen, bevor gebaut wird: Zwei Anmeldungen,
dieselbe Seite, und die Unterschiede werden AUFGEZAEHLT.
SIE PRUEFT NICHTS und meldet keinen Fehlschlag. Sie macht eine Liste
und zwei Bilder, auf denen man nachsehen kann. Deshalb `mess-` und
nicht `pruef-`: Die Portvergabe leitet sich aus den `pruef-`-Namen
ab, und eine Messdatei darf die Nummern der Pruefungen nicht
verschieben.
EIGENE WEGWERF-DATENBANK, NIE DIE ECHTE. Es entstehen echte Personen
und echte Protokollzeilen; in einem laufenden System waeren das
Testdaten, und die sind verboten.
Aufrufen mit: node server/mess-hand-personen.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 = 5405, HP = 5406;
const CREW = "crew.dogfather-universe.com";
const ordner = mkdtempSync(join(tmpdir(), "ws-hand-pers-"));
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-hand-personen");
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;
};
/* Dieselbe Besetzung wie auf Filipes Bildschirmfoto: beide Haende,
mehrere Modis, mehrere aus der Community. */
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("VanVan", "hand", "CODE-HAND-0001");
anlegen("Kessi", "linke", "CODE-LINKE-0001");
anlegen("Diene", "modi", "CODE-MODI-0001");
anlegen("Marina", "modi", "CODE-MODI-0002");
anlegen("Ghost", "modi", "CODE-MODI-0003");
anlegen("Anni", "gast", "CODE-GAST-0001");
anlegen("Cleo", "gast", "CODE-GAST-0002");
/* Ein paar Protokollzeilen, damit „Letzte Ereignisse" nicht leer ist --
sonst sieht „sieht nichts" bei beiden Rollen gleich aus. */
for (let i = 0; i < 6; i++) {
d.prepare("INSERT INTO protokoll (zeitpunkt, person_id, rolle, aktion, detail, ip)"
+ " VALUES (?,1,'admin','einstellung_geaendert',?,'127.0.0.1')")
.run(new Date(Date.now() - i * 60000).toISOString(), `probe_${i}`);
}
d.close();
const anfrage = (pfad, art, koerper, keks) => new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: art,
headers: {
Host: CREW, "Content-Type": "application/json",
...(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(JSON.stringify(koerper));
a.end();
});
const anmelden = async (rolle, code) => {
const a = await anfrage("/workspace/api/anmelden", "POST", { rolle, code });
if (a.code !== 200) {
console.log(`ABBRUCH: ${rolle} ließ sich nicht anmelden (HTTP ${a.code}) ${a.text.slice(0, 120)}`);
process.exit(2);
}
return (a.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
};
const kekse = {
DogFather: await anmelden("admin", "CODE-DOGI-0001"),
"rechte Hand": await anmelden("hand", "CODE-HAND-0001"),
};
/* ---- Erst die ANTWORTEN vergleichen, dann den Bildschirm ------------
Wenn der Server schon verschieden antwortet, muss man im Browser gar
nicht mehr suchen. */
console.log("\n=== Was der Server den beiden gibt ===");
for (const [wer, keks] of Object.entries(kekse)) {
const p = await anfrage("/workspace/api/verwaltung/personen", "GET", null, keks);
const prot = await anfrage("/workspace/api/verwaltung/protokoll?anzahl=25", "GET", null, keks);
let leute = 0, rollen = "";
try {
const j = JSON.parse(p.text);
leute = (j.personen || []).length;
rollen = [...new Set((j.personen || []).map((x) => x.rolle))].join(", ");
} catch { /* zeigt sich am Code */ }
let zeilen = -1;
try { zeilen = (JSON.parse(prot.text).eintraege || []).length; } catch { /* egal */ }
console.log(` ${wer.padEnd(12)} Personen HTTP ${p.code} → ${leute} Leute (${rollen})`);
console.log(` ${"".padEnd(12)} Protokoll HTTP ${prot.code} → ${zeilen < 0 ? prot.text.slice(0, 60) : zeilen + " Zeilen"}`);
}
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"],
});
const MARKE = process.env.MARKE || "hand";
const befund = {};
try {
for (const [wer, keks] of Object.entries(kekse)) {
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1500 } });
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/personen.html`,
{ waitUntil: "networkidle" });
await seite.waitForTimeout(1800);
const lage = await seite.evaluate(() => {
const sicht = (e) => !!(e && e.offsetParent !== null);
const text = (s) => (document.querySelector(s)?.textContent || "").trim();
return {
wo: location.pathname,
gruppen: [...document.querySelectorAll(".gruppe__titel, .rollen-block__titel, h2")]
.map((e) => e.textContent.trim()).filter(Boolean).slice(0, 12),
karten: [...document.querySelectorAll("[data-rolle]")]
.map((e) => e.dataset.rolle).filter((x, i, a) => x && a.indexOf(x) === i),
knoepfe: [...document.querySelectorAll("button")]
.filter(sicht).map((e) => e.textContent.trim())
.filter(Boolean).filter((x, i, a) => a.indexOf(x) === i).slice(0, 25),
protokollBlock: !!document.querySelector(".protokoll-block"),
protokollSichtbar: sicht(document.querySelector(".protokoll-block")),
protokollZeilen: document.querySelectorAll(".protokoll-zeile").length,
protokollText: text("#protokoll").slice(0, 60),
schalter: sicht(document.getElementById("protokoll-schalter")),
};
});
befund[wer] = lage;
await seite.screenshot({
path: `server/mess-${MARKE}-personen-${wer === "DogFather" ? "dogi" : "hand"}.png`,
fullPage: true,
});
if (konsole.length) console.log(` [${wer}] Browser meldet: ${konsole[0]}`);
await kontext.close();
}
} finally {
await browser.close();
await new Promise((r) => vorbau.close(r));
}
console.log("\n=== Was auf dem Bildschirm steht ===");
for (const [wer, l] of Object.entries(befund)) {
console.log(`\n --- ${wer} (${l.wo}) ---`);
console.log(` Rollen-Karten: ${l.karten.join(", ") || "(keine)"}`);
console.log(` Protokoll-Block: ${l.protokollBlock ? "im Dokument" : "FEHLT"}`
+ ` / sichtbar: ${l.protokollSichtbar}`
+ ` / Zeilen: ${l.protokollZeilen}`
+ ` / Schalter: ${l.schalter}`);
console.log(` Protokoll-Text: ${JSON.stringify(l.protokollText)}`);
console.log(` Knoepfe: ${l.knoepfe.join(" · ")}`);
}
/* DER UNTERSCHIED IST DIE ANTWORT -- alles andere ist Beiwerk. */
const a = befund.DogFather, b = befund["rechte Hand"];
if (a && b) {
console.log("\n=== Der Unterschied ===");
const fehltKarte = a.karten.filter((x) => !b.karten.includes(x));
const fehltKnopf = a.knoepfe.filter((x) => !b.knoepfe.includes(x));
console.log(` Karten, die ihr fehlen: ${fehltKarte.join(", ") || "(keine)"}`);
console.log(` Knoepfe, die ihr fehlen: ${fehltKnopf.join(" · ") || "(keine)"}`);
console.log(` Protokollzeilen: DogFather ${a.protokollZeilen}, rechte Hand ${b.protokollZeilen}`);
}
console.log(`\n Bilder: server/mess-${MARKE}-personen-{dogi,hand}.png\n`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(0);