Files
dogfather-universe/server/pruef-community-sicht.mjs
T
DogFatherGitandClaude Opus 5 1ba9d07af2 Zwoelf rote Pruefungen -- und kein einziger Fehler im Programm
Der Rundumcheck geht weiter. Diese zwoelf standen im Nachtlauf als rot
und waren es nicht: Jede hat den Umbau vom 24.09. (die Haustrennung),
den vom 25.09. (die zugetragenen Punkte) oder eine Umbenennung
mitbekommen -- nur ihre Erwartung nicht.

DAS IST KEIN TROST. Ein Fehlalarm zeigt auf eine echte Stelle und nennt
eine plausible Zahl; man sucht danach an richtigem Code. Und zwoelf rote
Zeilen, die immer rot sind, nehmen dem ganzen Satz die Stimme.

=== DIESELBE WURZEL, SECHSMAL: DAS FALSCHE HAUS ===

Seit dem 24.09. antwortet der Server auf der Agenturadresse nicht mehr
ueber Team-Dogi-Leute -- und genau das taten diese Pruefungen:

  pruef-nachwuchs      legte einen Modi ueber die Agenturadresse an
                       (400). Einundzwanzig Meldungen hingen an diesem
                       einen Aufruf. 82 Zeilen auf crew. umgestellt,
                       aus „Mara, Managerin" wurde die linke Hand --
                       im Teamhaus ist sie das, was gemeint war.
                       230 -> 262 Pruefungen, 0 Fehler.
  pruef-schritt        dasselbe beim Aufgabenbrett (404/400).
  pruef-treff-werkzeuge fragte die Einladungsliste eines Modis auf der
                       Agenturadresse ab -- dort ist er nicht.
  pruef-rechte-umstellen benutzte `wissen.html` als zweite Probeseite.
                       Die ist laut Rechtetafel fuer alle offen und auf
                       crew. trotzdem zu: Dort kommt man nur auf Seiten,
                       fuer die es eine Kachel gibt. Jetzt material.html.
  pruef-personen-formular verglich eine Erwartung FUER die Agentur mit
                       einer Messung OHNE Adresse (127.0.0.1) -- vier
                       gegen acht Rollen. Beides war richtig, nur nicht
                       dasselbe.
  pruef-treff          suchte „Support" zwischen den Brettern. Er steht
                       in „Fuer dich", wo er hingehoert -- eine kaputte
                       Seite zu melden ist kein Aushang. Die Liste stand
                       ZWEIMAL in der Datei; nachgezogen wurde eine.
                       Jetzt eine, von beiden benutzt.

=== ZWEIMAL: EINE FESTE ZAHL, DIE DIE SEITE UEBERHOLT HAT ===

  pruef-community-sicht erlaubte „3 bis 8 Seiten". Es sind zwoelf, und
                       jede gehoert dorthin. Statt einer Spanne steht
                       jetzt die LISTE da: Kommt eine dazu, wird die
                       Zeile rot und nennt sie beim Namen. Eine Spanne
                       haette elf statt zwoelf stillschweigend
                       durchgelassen -- in beide Richtungen.
  pruef-nachwuchs      schaltete EINE Person ab und erwartete, dass die
                       Ampel danach schweigt. Der Kommentar darueber
                       sagt selbst: „Die Schwelle wird nicht
                       abgeschrieben, sondern aus dem Verhalten
                       abgeleitet" -- eine feste Anzahl abzuschalten ist
                       aber eine Abschrift in anderer Waehrung. Jetzt
                       wird abgeschaltet, BIS es kippt.

=== UND VIERMAL EIN WERKZEUG, DAS STUMPF WAR ===

  pruef-scout-zuteilung suchte `.person__zeile` -- eine Klasse, die es
                       nicht mehr gibt. `querySelectorAll` liefert dafuer
                       ein leeres Feld, `.some()` darauf ist immer
                       false: Die Pruefung war nicht rot, weil etwas
                       fehlte, sondern weil sie nichts ansehen konnte.
                       Jetzt `.person`, und die ANZAHL der gefundenen
                       Karten steht in einer eigenen Bedingung.
  pruef-loeschen       suchte `::before` in einem Fenster von 600
                       Zeichen ab dem Selektor. Das misst die Laenge des
                       KOMMENTARS: Am 25.09. kam eine Begruendung von
                       zwanzig Zeilen dazu, und die Regel rutschte
                       hinaus. Jetzt wird nach dem Selektor gesucht.
  pruef-portnummern    hielt jedes `listen(` fuer einen Serverstart --
                       auch das blosse Nachsehen, ob eine Nummer frei
                       ist. Damit mahnte sie ausgerechnet die Datei an,
                       die das Vergeben der Nummern prueft. Jetzt zaehlt
                       der Import von `index.js`; dazu vier Gegenproben,
                       damit die engere Fassung nicht zum blinden Fleck
                       wird.
  pruef-spicy          stellte eine Person auf „manager" und dann
                       weiter. An Managern aendert seit dem 22.09. nur
                       DogFather etwas -- die Pruefung hatte sich selbst
                       die Tuer zugezogen. Jetzt kommt „manager" zuletzt,
                       und dass danach nichts mehr geht, ist eine eigene
                       Zeile. Aus dem Stolperstein wird eine Aussage.

=== EINER WAR FAST EIN BEFUND ===

  pruef-alle-sehen-es  meldete „highlight: DogFather 0, Community 0".
                       Das Brett hat als einziges keinen Katalog (dort
                       stehen echte Hoehepunkte, keine Vorlagen), und an
                       einem leeren Brett ist „sehen beide dasselbe?"
                       nicht zu messen: null gleich null waere auch dann
                       wahr, wenn die Community gar nichts duerfte.
                       Die Pruefung legt dort jetzt selbst einen Eintrag
                       an -- und musste dabei zweimal lernen: Der Eintrag
                       gehoert VOR den Katalog (danach ist die
                       Schreibbremse ausgeloest, 429), und er muss
                       FREIGEGEBEN werden, sonst sieht die Community ihn
                       zu Recht nicht. Beides steht jetzt als Aussage in
                       der Pruefung.

GEMESSEN: alle zwoelf gruen -- 43, 10, 31, 262, 43, 15, 56, 69, 37, 85,
73 und 80 Pruefungen, zusammen 804, kein Fehler.

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

335 lines
14 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 { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta, "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 ===");
/* ==== EINE LISTE STATT EINER SPANNE (25.09.2026) ===================
Hier stand `>= 3 && <= 8`. Eine Spanne sagt nichts darueber, WELCHE
Seiten offen sind -- und genau das ist die Frage. Sie war ausserdem
seit dem 24.09. rot: Mit dem Support sind es zwoelf geworden, und
jede einzelne davon gehoert dorthin (nachgesehen, nicht vermutet --
dieselben zwoelf stehen auf der Kachelwand eines Mitglieds).
AB JETZT WIRD DIE LISTE FESTGEHALTEN. Kommt eine Seite dazu, wird
diese Zeile rot und nennt sie beim Namen; wer sie hinzugefuegt hat,
traegt sie hier nach und hat damit BESTAETIGT, dass die Community
sie sehen soll. Verschwindet eine, faellt es genauso auf. Eine
Spanne kann beides nicht: Sie haette elf statt zwoelf stillschweigend
durchgehen lassen -- und zwar in beide Richtungen.
DAS IST DIESELBE BAUART wie die festgelegten Kachelfarben: nicht
„ungefaehr so viele", sondern „genau diese, und hier steht warum". */
const COMMUNITY_SEITEN = [
"bereich.html", // die Bretter (Highlights, Anschlag, Wunschliste ...)
"bewerben.html", // Mitmachen
"chat.html", // der Treff
"hilfe.html", // Regeln & Hilfe
"material.html", // Dogi-Media
"start.html", // die Wand
"steckbrief.html", // der eigene Steckbrief
"support.html", // etwas ist kaputt (seit 24.09. fuer alle)
"treff-regeln.html", // die Regeln des Rudels
"unsere-seiten.html", // Draussen
"unterstuetzen.html", // Unterstuetzen
"willkommen.html", // die Begruessung
];
const zuViel = erlaubt.filter((s) => !COMMUNITY_SEITEN.includes(s));
const fehltDa = COMMUNITY_SEITEN.filter((s) => !erlaubt.includes(s));
ok(zuViel.length === 0 && fehltDa.length === 0,
`ein Mitglied darf genau die ${COMMUNITY_SEITEN.length} vorgesehenen Seiten`
+ (zuViel.length ? ` -- ZU VIEL: ${zuViel.join(", ")}` : "")
+ (fehltDa.length ? ` -- ES FEHLT: ${fehltDa.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",
];
/* ZWEI BREITEN, NICHT EINE.
Die Community ist ueberwiegend mit dem Telefon unterwegs. Eine
Messung auf 1400 px beantwortet fuer sie die falsche Frage -- und
genau auf dem Handy sind heute die haesslichsten Sachen aufgefallen
(drei verschiedene Kartenanordnungen, ein Hinweis quer ueber der
naechsten Beschriftung). Was auf einer Breite in Ordnung ist, muss
es auf der anderen nicht sein. */
const BREITEN = [
{ name: "Rechner", breite: 1400, hoehe: 1000 },
{ name: "Handy", breite: 412, hoehe: 915 },
];
const browser = await pw.chromium.launch();
try {
for (const geraet of BREITEN) {
const ctx = await browser.newContext({
viewport: { width: geraet.breite, height: geraet.hoehe },
isMobile: geraet.breite < 700, hasTouch: geraet.breite < 700 });
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 (${geraet.name}) ===`);
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. */
/* DIE UNTERGRENZE WIRD ABGELEITET, NICHT GESETZT.
Hier stand `>= 20`. Auf dem Rechner sind es 27 Wege, auf dem Handy
18 -- dort klappt die Kopfleiste um, und ein Teil der Links ist
nicht sichtbar. Die feste Zahl machte die Pruefung auf der
zweiten Breite rot, ohne dass irgendetwas falsch war.
Sie einfach auf 15 zu senken waere derselbe Fehler mit einer
anderen Zahl. Verlangt wird jetzt, was die Sache selbst hergibt:
mindestens ein sichtbarer Weg je Seite. Findet sich weniger, hat
etwas nicht geladen -- und dann ist „0 tote Wege" keine Auskunft,
sondern ein Messfehler. */
ok(geprueftLinks >= ziele.length && 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 (${geraet.name}) ===`);
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? ===");
if (geraet.breite > 700) {
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? (${geraet.name}) ===`);
ok(skriptfehler.length === 0,
`kein Skriptfehler auf ${ziele.length} Seiten (${geraet.name})`
+ (skriptfehler.length ? ` -- ${skriptfehler[0]}` : ""));
await ctx.close();
}
} 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);