Gemessen: 165 Pruefdateien, 129 verschiedene Nummern -- NEUNZEHN
doppelt, vier davon dreifach. Niemand hatte das gewollt; jede neue
Pruefung wurde von einer vorhandenen abgeschrieben, und die Nummer kam
mit. Meine eigene Notiz sagte "sieben" -- auch eine Bestandsliste
altert.
Der Waechter faengt den Schaden ab, aber er kann nur melden, was schon
passiert ist: Zwei Pruefungen mit derselben Nummer koennen nie
gleichzeitig laufen, und ein liegengebliebener Prozess der einen laesst
die andere abbrechen mit einer Meldung, die wie ein Befund aussieht.
Genau das ist mir am 20.09. zweimal passiert.
Jetzt leitet jede Datei ihre Nummer aus ihrer STELLE IM ALPHABET ab
(eigenerPort in helfer-port.mjs), zwei je Datei. Nicht ueber eine
Pruefsumme: Bei 165 Namen in 4900 Nummern waeren nach dem
Geburtstagsproblem rund DREI Zusammenstoesse zu erwarten -- ein Hash
tauscht eine sichtbare Doppelung gegen eine unsichtbare. Die Stelle im
Alphabet ist eindeutig von der Bauart her.
--- ZWEI FEHLER AUF DEM WEG, BEIDE LEHRREICH -------------------------
1. DER ERSTE VERSUCH WAR GRUEN UND KAPUTT. Ersetzt wurde mit einem
Muster: "([^"]*4231[^"]*)". Das hielt
{ host: "127.0.0.1", port: 4231, path: "/404.html" }
fuer eine Zeichenkette -- ein Muster kann eine oeffnende nicht von
einer schliessenden Anfuehrung unterscheiden. Heraus kam
{ host: "127.0.0.1`, port: ${PORT}, path: `/404.html" }
also GUELTIGER Code ohne port-Feld. `node --check` sagte gruen fuer
alle 164 Dateien. Aufgefallen ist es erst, weil ich vier Vertreter
gegen eine vorher gemessene Grundlinie laufen liess: pruef-schranke
39/0 vorher, 38/1 nachher.
Alles zurueckgenommen und mit einem Zerleger neu gemacht, der weiss,
ob eine Stelle Code, Zeichenkette, Vorlage, Kommentar oder
regulaerer Ausdruck ist. In pruef-ics stand die Nummer in einem
regulaeren Ausdruck -- der wird jetzt gebaut statt hingeschrieben.
2. EIN MODUL, DAS BEIM IMPORTIEREN ARBEITET, IST EINE FALLE. Der
zweite Durchgang importierte den ersten, um seine Mechanik zu
benutzen -- und fuehrte dessen Hauptlauf gleich mit aus. Die
zweiten Nummern wurden dadurch als erste behandelt, zwei Aufrufe
bekamen dieselbe Nummer, und in pruef-content stand `const PORT`
zweimal.
--- WAS DAS DAUERHAFT HAELT -----------------------------------------
pruef-portnummern.mjs (neu, 8 Pruefungen) fragt nicht "welche Nummern
sind doppelt", sondern "wer traegt ueberhaupt noch eine von Hand ein"
und "wer startet einen Server, ohne seine Nummer abzuleiten". Die
zweite Frage hat sofort etwas gefunden, das in KEINER Doppelungsliste
stand: pruef-push-weg belegte 4341 und 4342, rief den Waechter aber
gar nicht auf -- dieselbe Nummer wie pruef-agentur. Eine Liste zeigt
nur, was auf ihr steht.
Mit Gegenprobe: Eine unbekannte Datei bekommt keine geratene Nummer,
sondern einen Abbruch, und eine dritte Nummer je Datei gibt es nicht.
--- NACHGEMESSEN ----------------------------------------------------
Sechzehn Pruefungen gegen ihre vorher gemessene Grundlinie, je eine
Vertreterin jeder umgestellten Bauweise (eine Nummer, zwei Nummern,
Nummer in einer Zeichenkette, in einer Vorlage, in einem regulaeren
Ausdruck, dynamische Einfuhr, Nachtlauf mit zwei Laeufen):
crew-adresse 132/0 · schranke 39/0 · content 45/0 · entwicklung 46/0
anruf 127/0 · ics 37/0 · push-weg 20/0 · arten 28/0 · video 67/0
kanalzeile 14/0 · agentur 62/0 · spicy 83/0 · creator-anlegen 50/0
push-ziel 10/0 · portnummern 8/0
Alle exakt wie vorher. Zwei waren schon vorher rot und sind es
unveraendert geblieben (crew-wand-bild 41/4, teilen 14/3) -- per
`git stash` belegt, nicht angenommen.
Co-Authored-By: Claude Opus 5 <[email protected]>
300 lines
13 KiB
JavaScript
300 lines
13 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 ===");
|
|
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",
|
|
];
|
|
|
|
/* 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);
|