Files
dogfather-universe/server/pruef-struktur.mjs
T
DogFatherGitandClaude Opus 5 37c2930af0 Kundenportal war nicht installierbar: Manifest hinter der Zugangswand
Live gemessen nach dem Deploy: Symbole 200, portal.webmanifest 302 auf
zugang.html. Der Browser bekam HTML statt JSON und bot "App
installieren" gar nicht erst an -- die ganze Arbeit am Symbol waere
fuer das Portal wirkungslos geblieben, ohne dass irgendwo etwas rot
geworden waere.

Das ist zum DRITTEN Mal derselbe Fehler an derselben Stelle
(app.webmanifest, verwaltung.webmanifest, jetzt portal.webmanifest).
Zweimal stand die Begruendung danach im Code -- beim dritten Mal wurde
sie trotzdem uebersehen. Ein Kommentar verhindert nichts.

Deshalb zusaetzlich eine Pruefung in pruef-struktur.mjs: Jedes Manifest
unter /webdesign/ MUSS in der Ausnahmeliste von webdesign-gate.js
stehen. Gegengeprobt -- Eintrag entfernt, Pruefung schlaegt an.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 11:56:43 +02:00

356 lines
16 KiB
JavaScript

/* STRUKTURPRUEFUNG — findet Kaputtes, das kein Browser meldet.
Auftrag vom 04.09.2026: vollstaendiger Audit des Creator Workspace.
WAS DIESE PRUEFUNG SUCHT — drei Sorten stiller Fehler:
1. Eine Seite laedt eine Datei, die es nicht gibt.
Der Browser meldet das nur in der Konsole; die Seite sieht
einfach etwas anders aus. Bei CSS faellt es auf, bei einem
einzelnen JS-Baustein oft monatelang nicht.
2. Ein Verweis fuehrt ins Leere.
Eine Kachel, ein Zurueck-Knopf, ein Menuepunkt — Ziel existiert
nicht. Erst der Klick zeigt es.
3. Die Oberflaeche ruft eine Schnittstelle auf, die der Server
nicht (mehr) hat.
Das ist der gefaehrlichste Fall: Der Knopf ist da, er tut nur
nichts. Genau so verschwindet eine Funktion still, wenn eine
Route umbenannt wird.
DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Faende das Skript keine einzige
Seite, waere "0 kaputte Verweise" das beste Ergebnis ueberhaupt --
und voellig wertlos. Deshalb wird jede Menge gezaehlt und eine leere
Menge ist ein Fehler.
GEGENPROBE am Ende: ein absichtlich falscher Verweis MUSS gemeldet
werden. Eine Pruefung, die immer bestaetigt, bestaetigt nichts. */
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
import { join, dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO = join(__dirname, "..");
const WS = join(REPO, "workspace");
const SERVER = __dirname;
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* ---------- Bestand einsammeln ------------------------------------------ */
const seiten = readdirSync(WS).filter((n) => n.endsWith(".html"));
console.log(`\n=== Bestand ===`);
ok(seiten.length >= 10, `${seiten.length} Workspace-Seiten gefunden`);
const serverDateien = readdirSync(SERVER).filter((n) => /^workspace.*\.js$/.test(n));
ok(serverDateien.length >= 20, `${serverDateien.length} Server-Module gefunden`);
/* Alle Routen, die der Server wirklich anbietet. Gelesen wird der Code,
nicht eine gepflegte Liste -- eine Liste altert, der Code nicht. */
const ROUTEN = [];
for (const datei of serverDateien) {
const text = readFileSync(join(SERVER, datei), "utf8");
const treffer = text.matchAll(
/Router\.(get|post|patch|put|delete|use)\(\s*\n?\s*"(\/workspace\/[^"]*)"/g);
for (const t of treffer) ROUTEN.push({ art: t[1], pfad: t[2], datei });
}
ok(ROUTEN.length >= 80, `${ROUTEN.length} Server-Routen eingelesen`);
/** Ein Pfad aus der Oberflaeche gegen die Routenliste halten.
* Platzhalter (:id) und eingesetzte Werte (${...}) gelten als gleich.
*
* WICHTIG — .use() zaehlt NICHT als Gegenstueck. Diese Eintraege sind
* Schranken ("jeder Pfad unter /workspace/api braucht eine Anmeldung"),
* keine Endpunkte. Sie mitzuzaehlen war der erste Entwurf, und die
* Gegenprobe hat ihn sofort ueberfuehrt: /workspace/api/gibtesnicht
* galt als vorhanden, weil aufgabenRouter.use("/workspace/api", …)
* darueber liegt. Damit haette diese ganze Pruefung nie einen einzigen
* toten Aufruf gefunden und trotzdem gruen gemeldet. */
function routeGibtEs(pfad, art) {
const teileA = pfad.split("/").filter(Boolean);
return ROUTEN.some((r) => {
if (r.art === "use") return false;
if (art && r.art !== art) return false;
const teileB = r.pfad.split("/").filter(Boolean);
if (teileA.length !== teileB.length) return false;
return teileB.every((t, i) => t.startsWith(":") || t === "*" || t === teileA[i]);
});
}
/* ---------- 1. Geladene Dateien ----------------------------------------- */
console.log(`\n=== Geladene Dateien (script/link/img) ===`);
let geprueftAssets = 0;
for (const seite of seiten) {
const text = readFileSync(join(WS, seite), "utf8");
const quellen = [
...text.matchAll(/<script[^>]+src="([^"]+)"/g),
...text.matchAll(/<link[^>]+href="([^"]+\.css[^"]*)"/g),
...text.matchAll(/<img[^>]+src="([^"]+)"/g),
].map((m) => m[1]);
for (const roh of quellen) {
if (/^(https?:|data:|\/\/)/.test(roh)) continue;
const ohneStempel = roh.split("?")[0];
const pfad = ohneStempel.startsWith("/")
? join(REPO, ohneStempel)
: resolve(WS, ohneStempel);
geprueftAssets++;
if (!existsSync(pfad)) ok(false, `${seite}: Datei fehlt -> ${roh}`);
}
}
ok(geprueftAssets > 0, `${geprueftAssets} geladene Dateien geprueft, alle vorhanden`);
/* ---------- 2. Verweise auf andere Seiten -------------------------------- */
console.log(`\n=== Verweise (href) ===`);
let geprueftLinks = 0;
const quellenMitLinks = [
...seiten.map((n) => ({ name: n, pfad: join(WS, n) })),
...readdirSync(join(WS, "assets", "js"))
.filter((n) => n.endsWith(".js"))
.map((n) => ({ name: "assets/js/" + n, pfad: join(WS, "assets", "js", n) })),
];
for (const { name, pfad: quelle } of quellenMitLinks) {
const text = readFileSync(quelle, "utf8");
/* Beide Schreibweisen: absolut (/workspace/aufgaben.html) und relativ
("aufgaben.html"). Der erste Entwurf suchte nur die absolute Form
und meldete deshalb "0 Verweise geprueft" -- die Seiten verlinken
aber fast ausschliesslich relativ. Eine Pruefung, die nichts
findet, prueft nichts; deshalb steht die Anzahl unten in der
Bedingung. */
const ziele = [
...[...text.matchAll(/href\s*=\s*["'`](\/workspace\/[^"'`#?${]*)/g)].map((m) => m[1]),
...[...text.matchAll(/(?:location\.href|location\.assign\()\s*=?\s*["'`](\/workspace\/[^"'`#?${]*)/g)].map((m) => m[1]),
...[...text.matchAll(/href\s*=\s*["'`]([a-z0-9-]+\.html)/g)].map((m) => "/workspace/" + m[1]),
...[...text.matchAll(/(?:location\.href\s*=|location\.assign\()\s*["'`]([a-z0-9-]+\.html)/g)].map((m) => "/workspace/" + m[1]),
];
for (const ziel of ziele) {
if (ziel.startsWith("/workspace/api/")) continue;
geprueftLinks++;
const rel = ziel.replace(/^\/workspace\/?/, "");
const datei = rel === "" ? "index.html" : rel;
const voll = join(WS, datei);
if (!existsSync(voll)) ok(false, `${name}: Verweis ins Leere -> ${ziel}`);
}
}
ok(geprueftLinks > 0, `${geprueftLinks} Verweise geprueft, alle mit Ziel`);
/* ---------- 3. Aufgerufene Schnittstellen -------------------------------- */
console.log(`\n=== Aufgerufene Schnittstellen ===`);
let geprueftApi = 0;
let nurPraefix = 0;
const jsDateien = readdirSync(join(WS, "assets", "js")).filter((n) => n.endsWith(".js"));
for (const name of [...jsDateien.map((n) => "assets/js/" + n), ...seiten]) {
const text = readFileSync(join(WS, name), "utf8");
/* Jede Zeichenkette, die mit /workspace/api/ beginnt -- egal ob in
fetch(), in einer Hilfsfunktion oder in einer Variablen. */
for (const m of text.matchAll(/["'`](\/workspace\/api\/[^"'`\s]*)/g)) {
const roh = m[1].replace(/\$\{[^}]*\}/g, "1").split("?")[0];
if (!roh || roh === "/workspace/api" || roh === "/workspace/api/") continue;
/* DER DRITTE AUSGANG. Endet die Zeichenkette auf "/", wurde im Code
per + noch etwas angehaengt ('/workspace/api/ki/' + pfad). Was
dort steht, weiss nur die Laufzeit -- statisch ist der volle Pfad
NICHT zu ermitteln.
Der erste Entwurf schnitt den Schraegstrich einfach weg und
meldete vier Fehler, die keine waren. Genau davor warnt die
eigene Regel: eine Pruefung, die ihre Voraussetzung nicht hat,
muss das SAGEN und nicht raten. Geprueft wird deshalb nur, dass
es unter diesem Praefix ueberhaupt Routen gibt -- die vollen
Pfade deckt der Rundgang im Browser ab. */
if (roh.endsWith("/")) {
const praefix = roh.slice(0, -1);
nurPraefix++;
const gibtEsDarunter = ROUTEN.some(
(r) => r.art !== "use" && r.pfad.startsWith(praefix + "/"));
if (!gibtEsDarunter) {
ok(false, `${name}: unter ${praefix}/ gibt es gar keine Schnittstelle`);
}
continue;
}
geprueftApi++;
if (!routeGibtEs(roh)) ok(false, `${name}: Schnittstelle gibt es nicht -> ${roh}`);
}
}
ok(geprueftApi >= 40, `${geprueftApi} vollstaendige Aufrufe geprueft, alle mit Gegenstueck`);
ok(nurPraefix > 0, `${nurPraefix} zusammengesetzte Aufrufe: nur Praefix pruefbar (Rest deckt der Browser-Rundgang)`);
/* ---------- 4. Jede Seite ist erreichbar --------------------------------- */
console.log(`\n=== Erreichbarkeit ===`);
const verlinkt = new Set(["index.html", "start.html"]);
for (const { pfad: quelle } of quellenMitLinks) {
const text = readFileSync(quelle, "utf8");
for (const m of text.matchAll(/["'`](?:\/workspace\/)?([a-z0-9-]+\.html)/g)) {
verlinkt.add(m[1]);
}
}
for (const seite of seiten) {
if (!verlinkt.has(seite)) ok(false, `Seite ${seite} ist von nirgendwo verlinkt`);
}
ok(verlinkt.size >= seiten.length, `alle ${seiten.length} Seiten sind verlinkt`);
/* ---------- 5. Versionsstempel ------------------------------------------- */
/* Ohne Stempel haelt Cloudflare bis zu vier Stunden die alte Fassung --
dann sieht niemand die Aenderung, und man sucht den Fehler im Code. */
console.log(`\n=== Versionsstempel an JS und CSS ===`);
let ohneStempel = [];
for (const seite of seiten) {
const text = readFileSync(join(WS, seite), "utf8");
for (const m of text.matchAll(/(?:src|href)="(\/workspace\/assets\/(?:js|css)\/[^"]+)"/g)) {
if (!m[1].includes("?v=")) ohneStempel.push(`${seite}: ${m[1]}`);
}
}
ok(ohneStempel.length === 0,
ohneStempel.length ? `ohne Versionsstempel: ${ohneStempel.slice(0, 8).join(", ")}`
: "jede JS- und CSS-Einbindung traegt einen Stempel");
/* ---------- 6. Reste aus der Entwicklung --------------------------------- */
console.log(`\n=== Produktionsreste ===`);
const resteMuster = [
[/console\.log\(/g, "console.log"],
[/\bdebugger\b/g, "debugger"],
[/\bTODO\b|\bFIXME\b|\bHACK\b/g, "TODO/FIXME"],
[/localhost:\d+/g, "localhost-Adresse"],
[/Lorem ipsum/gi, "Platzhaltertext"],
];
let reste = [];
for (const name of [...jsDateien.map((n) => "assets/js/" + n), ...seiten]) {
const text = readFileSync(join(WS, name), "utf8");
/* Kommentare zaehlen nicht -- dort steht Begruendung, kein Code. */
const ohneKommentar = text
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/^\s*\/\/.*$/gm, "")
.replace(/<!--[\s\S]*?-->/g, "");
for (const [muster, was] of resteMuster) {
const t = ohneKommentar.match(muster);
if (t) reste.push(`${name}: ${was} (${t.length}x)`);
}
}
ok(reste.length === 0, reste.length ? reste.join(" · ") : "keine Entwicklungsreste im Auslieferstand");
/* ---------- 7. Groesse der Assets ---------------------------------------- */
console.log(`\n=== Dateigroessen ===`);
let schwer = [];
for (const unter of ["assets/js", "assets/css", "assets/img"]) {
const ordner = join(WS, unter);
if (!existsSync(ordner)) continue;
for (const n of readdirSync(ordner)) {
const p = join(ordner, n);
if (statSync(p).isDirectory()) continue;
const kb = Math.round(statSync(p).size / 1024);
if (kb > 200) schwer.push(`${unter}/${n} ${kb} KB`);
}
}
ok(schwer.length === 0, schwer.length ? `ueber 200 KB: ${schwer.join(", ")}` : "keine Datei ueber 200 KB");
/* ---------- 8. Totes CSS ------------------------------------------------- */
/* start.css wird auf JEDER Seite geladen und ist 126 KB gross. Was darin
niemand mehr benutzt, laedt trotzdem jeder mit.
ZWEI BEKANNTE AUSNAHMEN, bewusst stehen gelassen: Beide stehen als
Glied einer Selektorliste (".a, .b, .code-feld { … }") und kosten je
rund fuenfzehn Byte. Sie zu entfernen braechte nichts und traegt das
kleine Risiko, dass die Klasse doch irgendwo zur Laufzeit gesetzt
wird. Sie stehen hier namentlich, damit NEUES totes CSS auffaellt --
eine Pruefung, die von Anfang an rot ist, wird weggeklickt.
DER DRITTE AUSGANG: Klassen, die zusammengesetzt werden koennen
('kachel--' + ton), zaehlen nicht als tot. Ihr Stamm wird gesucht;
findet er sich, gilt die Klasse als moeglicherweise dynamisch und
wird getrennt ausgewiesen statt geraten. */
console.log(`\n=== Totes CSS ===`);
const BEKANNT_TOT = [".content-wurzel", ".code-feld"];
{
const cssOrdner = join(WS, "assets", "css");
const jsOrdner = join(WS, "assets", "js");
let quelle = "";
for (const n of seiten) quelle += readFileSync(join(WS, n), "utf8");
for (const n of readdirSync(jsOrdner).filter((x) => x.endsWith(".js"))) {
quelle += readFileSync(join(jsOrdner, n), "utf8");
}
let gesamt = 0, dynamisch = 0;
const tot = [];
for (const datei of readdirSync(cssOrdner).filter((n) => n.endsWith(".css"))) {
const text = readFileSync(join(cssOrdner, datei), "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
for (const k of new Set([...text.matchAll(/\.([a-zA-Z][a-zA-Z0-9_-]{2,})/g)].map((m) => m[1]))) {
gesamt++;
if (quelle.includes(k)) continue;
const stamm = k.split(/--|__/)[0];
if (stamm !== k && quelle.includes(stamm)) { dynamisch++; continue; }
if (BEKANNT_TOT.includes("." + k)) continue;
tot.push(`${datei}: .${k}`);
}
}
ok(gesamt > 500, `${gesamt} CSS-Klassen geprueft (${dynamisch} moeglicherweise dynamisch gebaut)`);
ok(tot.length === 0,
tot.length ? `NEUES totes CSS: ${tot.slice(0, 8).join(" · ")}`
: `kein neues totes CSS (${BEKANNT_TOT.length} bekannte Reste stehen bewusst)`);
}
/* ---------- 9. Kommt jedes Manifest auch WIRKLICH an? --------------------
Ein Manifest kann fehlerfrei im Repo liegen und trotzdem nie beim
Browser ankommen -- wenn eine Zugangswand davorsteht und HTML statt
JSON zurueckgibt. Dann bietet der Browser "App installieren" gar
nicht erst an, und niemand merkt etwas: Die Datei ist ja da.
Genau das ist DREIMAL passiert (webdesign/app.webmanifest,
verwaltung.webmanifest, portal.webmanifest). Zweimal stand die
Begruendung danach im Code -- und beim dritten Mal wurde sie trotzdem
uebersehen. Ein Kommentar verhindert nichts; eine Pruefung schon.
Geprueft wird gegen die Ausnahmeliste in webdesign-gate.js: Jedes
Manifest unter /webdesign/ MUSS dort stehen. */
console.log(`
=== Manifeste hinter der Zugangswand ===`);
{
const tor = join(SERVER, "webdesign-gate.js");
const torText = existsSync(tor) ? readFileSync(tor, "utf8") : "";
ok(torText.length > 1000, "webdesign-gate.js gelesen");
const manifeste = [];
const suche = (ordner, vorn) => {
for (const n of readdirSync(ordner, { withFileTypes: true })) {
if (n.name.startsWith(".") || n.name === "node_modules") continue;
const rel = vorn ? `${vorn}/${n.name}` : n.name;
if (n.isDirectory()) { if (rel === "webdesign") suche(join(ordner, n.name), rel); continue; }
if (n.name.endsWith(".webmanifest") || n.name === "manifest.json") manifeste.push(rel);
}
};
suche(REPO, "");
const hinterWand = manifeste.filter((m) => m.startsWith("webdesign/"));
ok(hinterWand.length >= 2, `${hinterWand.length} Manifeste unter /webdesign/ gefunden`);
const fehlend = hinterWand.filter((m) => !torText.includes(`"/${m}"`));
ok(fehlend.length === 0,
fehlend.length
? `NICHT in der Ausnahmeliste von webdesign-gate.js: ${fehlend.join(", ")} `
+ `— der Browser bekaeme HTML statt JSON und boete "App installieren" nicht an`
: `alle ${hinterWand.length} stehen in der Ausnahmeliste`);
}
/* ---------- Gegenprobe --------------------------------------------------- */
console.log(`\n=== Gegenprobe (muss anschlagen) ===`);
ok(!routeGibtEs("/workspace/api/gibtesnicht"),
"erfundene Schnittstelle wird als fehlend erkannt");
ok(routeGibtEs("/workspace/api/aufgaben"),
"vorhandene Schnittstelle wird als vorhanden erkannt");
ok(!existsSync(join(WS, "gibtesnicht.html")),
"erfundene Seite wird als fehlend erkannt");
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
process.exit(fehler ? 1 : 0);