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]>
269 lines
11 KiB
JavaScript
269 lines
11 KiB
JavaScript
/* =====================================================================
|
|
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);
|