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:
@@ -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);
|
||||
Reference in New Issue
Block a user