Community: der Chat-Knopf fuehrte auf JEDER Seite ins Leere

ERST DIE METHODE, DANN DER FUND. An einem Tag habe ich in der
Community elf Dinge gefunden, die dort nicht hingehoerten -- jedes
einzelne, weil ich zufaellig hingesehen habe. Das ist keine Methode.
Der Workspace ist fuer die Agentur gebaut worden, und die Community
hat ihn geerbt; solche Reste findet man nicht durch Nachdenken,
sondern indem man ALLE Seiten durchgeht, die ein Mitglied erreichen
kann.

server/pruef-community-sicht.mjs tut genau das. Sie leitet aus
rechte.js ab, welche Seiten offenstehen (4) und welche nicht (27) --
von Hand aufgezaehlt waere das eine Liste, die bei der naechsten Seite
niemand nachzieht --, oeffnet jede davon als Mitglied und fragt
zweierlei: Fuehrt ein sichtbarer Weg auf eine verbotene Seite? Steht
dort die Sprache eines Arbeitsplatzes?

BEIM ERSTEN LAUF: 27 sichtbare Wege, davon ZEHN ins Leere -- und alle
zehn derselbe Knopf. Der Chat steht oben rechts in der Kopfleiste, auf
jeder einzelnen Seite, mit Zaehler. `chat.html` steht einem Mitglied
aber nicht offen: Der Klick landet wieder auf der Startseite. Keine
Meldung, kein Grund, nichts passiert -- an der Stelle, die man am
ehesten drueckt.

Der Knopf haengt jetzt an `darf_chat` aus /api/ich, und das kommt aus
derselben Rechtetabelle wie die Schranke dahinter. Die Oberflaeche
vergleicht keine Rollennamen -- das waere eine zweite Wahrheit, die
bei der naechsten Rechteaenderung auseinanderlaeuft. Dieselbe
Ueberlegung steht zwei Zeilen weiter oben schon einmal.

UND DIE PRUEFUNG HAT GLEICH MEINEN EIGENEN FEHLER GEFUNDEN: Die erste
Fassung stand in `aufbauChat`, wo es die Person gar nicht gibt --
ReferenceError auf allen zehn Seiten. Ohne den Skriptfehler-Abschnitt
waere der Knopf verschwunden UND die Seite kaputt gewesen, und das
haette wie ein Erfolg ausgesehen.

Jetzt haengt es dort, wo die Person ankommt (`werZeigen`) -- ein
vorhandener Knopf laesst sich immer entfernen, auf die Reihenfolge des
Ladens zu bauen waere eine Annahme.

GEGENPROBE: DogFather hat seinen Chat-Knopf weiterhin und 24 Wege auf
andere Seiten. Ohne diesen Abschnitt saehe eine abgeschaffte Funktion
genauso aus wie eine, die richtig entscheidet.

7 Pruefungen, 0 Fehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-18 21:59:53 +02:00
co-authored by Claude Opus 5
parent c2a8c5f15f
commit 542070e864
36 changed files with 707 additions and 402 deletions
+268
View File
@@ -0,0 +1,268 @@
/* =====================================================================
WAS EIN MITGLIED DER COMMUNITY SIEHT -- SYSTEMATISCH (18.09.2026)
Filipe: „perfektioniere jede einzelne kategorie der community."
---------------------------------------------------------------------
WARUM ES DIESE PRÜFUNG GIBT
An einem Tag habe ich in der Community elf Dinge gefunden, die dort
nicht hingehörten: Aufgabenzähler, „gönn dir was", „0 betreut",
sieben Links auf eine Seite, die ein Mitglied gar nicht öffnen darf.
Jeder einzelne Fund kam daher, dass ich zufällig hingesehen habe.
Das ist keine Methode. Der Workspace ist für die Agentur gebaut
worden, und die Community hat ihn geerbt -- überall dort, wo
niemand hingesehen hat, steht noch die Sprache und die Logik eines
Arbeitsplatzes. Solche Reste findet man nicht durch Nachdenken,
sondern indem man ALLE Seiten durchgeht, die ein Mitglied erreichen
kann.
---------------------------------------------------------------------
ZWEI FRAGEN, BEIDE MESSBAR
1. TOTE WEGE: Führt irgendein Link, Knopf oder Formular auf eine
Seite, die diese Person nicht öffnen darf? Sie landet dann wieder
auf der Startseite -- ohne Meldung, ohne Grund. Man drückt, es
passiert nichts, und beim nächsten Knopf zweifelt man an sich.
2. FREMDE SPRACHE: Stehen dort Wörter, die es hier nicht gibt --
„Review", „betreut", „Pipeline", „Creator anlegen"? Sie sind
nicht falsch, sie gehören nur jemand anderem.
Aufrufen mit: node server/pruef-community-sicht.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { portMussFreiSein } from "./helfer-port.mjs";
const PORT = await portMussFreiSein(4198, "pruef-community-sicht");
const ordner = mkdtempSync(join(tmpdir(), "ws-cs-"));
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(180_000, "pruef-community-sicht");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const CREW = "crew.dogfather-universe.com";
/* ---------------------------------------------------------------------
WELCHE SEITEN DARF EIN MITGLIED? -- aus der Rechtetabelle abgeleitet.
NICHT VON HAND AUFGEZÄHLT. Eine Liste hier müsste bei jeder neuen
Seite nachgezogen werden, und genau das vergisst man. Die Tabelle
in rechte.js ist ohnehin die Wahrheit -- also wird sie gefragt.
--------------------------------------------------------------------- */
const { darfSeite } = await import("./rechte.js");
const { readFileSync, readdirSync } = await import("node:fs");
const alleSeiten = readdirSync(join(import.meta.dirname, "..", "workspace"))
.filter((d) => d.endsWith(".html"));
const GAST = { rolle: "gast", haus: "crew" };
const erlaubt = alleSeiten.filter((d) => darfSeite(GAST, `/workspace/${d}`));
const verboten = alleSeiten.filter((d) => !darfSeite(GAST, `/workspace/${d}`));
melde("");
melde("=== 1. Was ueberhaupt offensteht ===");
ok(erlaubt.length >= 3 && erlaubt.length <= 8,
`ein Mitglied darf ${erlaubt.length} Seiten: ${erlaubt.join(", ")}`);
ok(verboten.length >= 15,
`und ${verboten.length} nicht -- so viele Wege gibt es, ins Leere zu zeigen`);
/* ---------------------------------------------------------------------
DIE MESSUNG IM BROWSER
--------------------------------------------------------------------- */
function roh(pfad, host, kopf = {}, koerper = null) {
return new Promise((f, s) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad,
method: koerper ? "POST" : "GET",
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
}, (an) => {
let t = "";
an.on("data", (c) => { t += c; });
an.on("end", () => f({ code: an.statusCode, text: t, kopf: an.headers }));
});
a.on("error", s);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
{
const d = new DatabaseSync(process.env.WORKSPACE_DB);
for (const [name, rolle, code] of [
["Filipe", "admin", "CODE-DOGI-0001"],
["Mara", "gast", "CODE-GAST-0001"],
]) {
const salz = randomBytes(16).toString("hex");
const hash = scryptSync(code, salz, 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, salz, 32768, new Date().toISOString());
}
d.close();
}
async function anmelden(rolle, code) {
let a = await roh("/workspace/api/anmelden", CREW, {}, { rolle, code });
if (a.code === 400 && a.text.includes("alter_offen")) {
a = await roh("/workspace/api/anmelden", CREW, {}, { rolle, code, alter_ok: true });
}
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
}
const kGast = await anmelden("gast", "CODE-GAST-0001");
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
/* Damit die Bretter nicht leer sind -- auf einer leeren Seite findet
man keine Wege, die ins Leere fuehren. */
for (const b of ["treff", "anschlag", "ansteht", "wunsch", "highlight", "regeln", "mitmachen"]) {
await roh(`/workspace/api/bereich/${b}/start`, CREW,
{ cookie: kDogi, origin: `http://${CREW}:${PORT}` }, {});
}
let pw = null;
try {
pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
} catch { /* dritter Ausgang unten */ }
if (!pw) {
melde("");
melde("KANN NICHT NACHSEHEN: Playwright fehlt -- die Seiten wurden nicht geoeffnet.");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
process.exit(3);
}
/* WÖRTER, DIE JEMAND ANDEREM GEHÖREN.
Keine Rollennamen -- die stehen bewusst nirgends in ausgelieferten
Dateien. Gesucht wird nach der SPRACHE eines Arbeitsplatzes. */
const FREMDE_WOERTER = [
"Vorarbeiten", "gönn dir was", "betreut", "Review",
"Pipeline", "überfällig", "In Arbeit", "Abgebrochen",
];
const browser = await pw.chromium.launch();
try {
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
await ctx.addCookies(kGast.split("; ").filter(Boolean).map((z) => {
const [name, ...rest] = z.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await ctx.newPage();
const skriptfehler = [];
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 100)));
/* Jede erlaubte Seite -- und bei bereich.html jedes der sieben
Bretter, denn sie sehen völlig verschieden aus. */
const ziele = [];
for (const d of erlaubt) {
if (d === "bereich.html") {
for (const b of ["treff", "anschlag", "ansteht", "wunsch", "highlight", "regeln", "mitmachen"]) {
ziele.push(`${d}?b=${b}`);
}
} else ziele.push(d);
}
melde("");
melde("=== 2. Wege, die ins Leere fuehren ===");
let toteWege = 0, geprueftLinks = 0;
const gefunden = [];
for (const ziel of ziele) {
await seite.goto(`http://127.0.0.1:${PORT}/workspace/${ziel}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const wege = await seite.evaluate(() =>
[...document.querySelectorAll("a[href]")]
.filter((a) => a.offsetParent !== null)
.map((a) => ({ ziel: a.getAttribute("href"), text: a.textContent.trim().slice(0, 30) })));
for (const w of wege) {
const datei = String(w.ziel).split("?")[0].split("#")[0].replace(/^\.?\//, "");
if (!datei.endsWith(".html")) continue;
geprueftLinks++;
if (verboten.includes(datei)) {
toteWege++;
gefunden.push(`${ziel}: „${w.text}" -> ${datei}`);
}
}
}
/* DIE ANZAHL GEHÖRT IN DIE BEDINGUNG. Ohne sie wäre „0 tote Wege"
auch dann wahr, wenn gar keine Seite geladen hätte. */
ok(geprueftLinks >= 20 && toteWege === 0,
`${geprueftLinks} sichtbare Wege auf ${ziele.length} Seiten, davon ${toteWege} ins Leere`);
for (const g of gefunden.slice(0, 10)) console.log(` -> ${g}`);
melde("");
melde("=== 3. Sprache, die jemand anderem gehoert ===");
let fremd = 0;
const wo = [];
for (const ziel of ziele) {
await seite.goto(`http://127.0.0.1:${PORT}/workspace/${ziel}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(700);
const text = await seite.evaluate(() => document.body.innerText);
for (const w of FREMDE_WOERTER) {
if (text.includes(w)) { fremd++; wo.push(`${ziel}: „${w}"`); }
}
}
ok(fremd === 0, `keine fremde Arbeitssprache auf ${ziele.length} Seiten (${fremd} Stellen)`);
for (const g of wo.slice(0, 10)) console.log(` -> ${g}`);
/* =====================================================================
4. DIE GEGENPROBE: BEHAELT DAS TEAM SEINE WEGE?
Was hier weggenommen wird, weil es fuer ein Mitglied ins Leere
fuehrt, muss fuer die anderen stehen bleiben. Ohne diesen
Abschnitt saehe eine abgeschaffte Funktion genauso aus wie eine,
die richtig entscheidet -- und zwar in beiden Faellen gruen.
===================================================================== */
melde("");
melde("=== 4. Und behaelt das Team seine Wege? ===");
{
const ctx2 = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
await ctx2.addCookies(kDogi.split("; ").filter(Boolean).map((z) => {
const [name, ...rest] = z.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite2 = await ctx2.newPage();
await seite2.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" });
await seite2.waitForTimeout(1200);
const z = await seite2.evaluate(() => ({
chat: !!document.querySelector(".chat-knopf"),
wege: [...document.querySelectorAll("a[href$='.html']")].length,
}));
ok(z.chat, "DogFather hat seinen Chat-Knopf weiterhin");
ok(z.wege >= 5, `und weiterhin Wege auf andere Seiten (${z.wege})`);
await ctx2.close();
}
melde("");
melde("=== 5. Und laeuft ueberhaupt alles? ===");
ok(skriptfehler.length === 0,
`kein Skriptfehler auf ${ziele.length} Seiten`
+ (skriptfehler.length ? ` -- ${skriptfehler[0]}` : ""));
} finally {
await browser.close();
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }
process.exit(fehler ? 1 : 0);