Files
dogfather-universe/server/pruef-struktur.mjs
T
DogFatherGitandClaude Opus 5 e0d8e6d55d Tagesblick: Termine als Karten, und vier Fehler, die dabei auffielen
Filipe zu der Liste mit einem einzigen Termin darin: "das soll viel
geiler und krasser aussehen bitte."

Er hatte recht, und der Grund war der Einzelfall. Ein Zeitstrahl lebt
davon, dass er etwas VERBINDET -- bei einem Eintrag verbindet er nichts.
Uebrig blieben eine magere Zeile, ein Strich ins Leere und Weissraum bis
zur Plakette am rechten Rand.

Jetzt traegt jede Karte fuer sich: eigene Flaeche mit farbiger Kante,
die Uhrzeit gross und in der Farbe der Terminart (vorher war sie
kleiner als der Titel -- dabei ist sie das, was man sucht), ein Balken
fuer die Dauer, und "JETZT" am naechsten Termin statt eines etwas
helleren Hintergrunds. Erledigtes bekommt einen Haken und verliert die
Farbe, bleibt aber voll lesbar; vorher wurde alles blasser, was auch
den Text traf.

Kein Neon dazu. Die Wirkung kommt aus Kontrast und Hierarchie.

VIER FEHLER, DIE DIE PRUEFUNGEN GEFUNDEN HABEN

1. ZEITZONE, in NEUN Pruefungen. Um 00:10 meldete pruef-teilnehmer
   ploetzlich 13 Fehlschlaege an einer Datei, die seit Stunden niemand
   angefasst hatte:

       Ortszeit:  06.09.2026, 00:10
       UTC:       05.09.2026, 22:10

   Sie bildeten ihr Tagesdatum mit toISOString() -- also UTC -- legten
   ihre Termine auf gestern und suchten heute. ZWEI STUNDEN AM TAG waren
   sie damit rot, im Winter eine. Wer nur tagsueber laeuft, sieht das
   nie. Jetzt gibt es helfer-zeit.mjs mit derselben Rechenweise wie die
   Anwendung, und pruef-struktur sucht das Muster kuenftig automatisch.

2. MEIN UMSTELL-SKRIPT VERSAGTE STILL. Es pruefte
   `if "helfer-zeit.mjs" not in s` -- und mein eigener Kommentar
   enthielt den Dateinamen. Ergebnis: keine einzige der neun Dateien
   bekam den Import, alle waeren zur Laufzeit abgestuerzt. `node --check`
   findet das nicht. Aufgefallen, weil danach nachgezaehlt wurde statt
   der Erfolgsmeldung zu glauben.

3. DIE KACHEL LIESS DIE SEITE SPRINGEN. Der Layout-Sprung auf
   start.html stieg von 0 auf 0,96 -- zweimal bestaetigt. Sie erschien
   erst nach dem Laden und schob alles darunter weg. Das ist kein
   Schoenheitsfehler, sondern der Grund, warum man auf den falschen
   Knopf drueckt.

   Gemessen wurden die echten Hoehen (1 Termin 169 px, 3 → 327, 5 →
   486). Daraus zwei Konsequenzen: Platz vorher reservieren, und
   hoechstens DREI Termine zeigen -- das halbiert die Spanne und ist
   die klarere Aussage. Der Rest steht als "1 weiterer Termin heute"
   darunter, nicht stillschweigend abgeschnitten. Von 0,96 auf 0,163.

4. SCHRIFTGROESSE, zweimal am selben Tag: erst die Art-Plaketten mit
   10,56 px, dann -- nach der Korrektur -- die neue Jetzt-Marke mit
   10,88. Beide Male gemeldet von pruef-handy und pruef-grosscheck,
   beide Male erst nach einem mehrminuetigen Browserlauf.

ZWEI PRUEFUNGEN, DIE SICH SELBST IM WEG STANDEN

pruef-tempo-workspace meldete "NEUE Doppelabfrage: start.html 2x
/workspace/api/termine". Nachgemessen an einem einzelnen Seitenaufruf:
genau eine Anfrage. Die Pruefung startete ihren Zaehler, bevor die
Anmeldung zur Ruhe gekommen war, und schrieb der Seite an, was die
vorherige noch offen hatte.

pruef-tagesblick fiel zum zweiten Mal auf dieselbe Falle herein: Sie
mass die Artfarbe an einem vorbeigezogenen Termin, der absichtlich grau
ist. Diesmal an der Wurzel geloest -- die Daempfung wird fuer die
Messung kurz abgeschaltet und sofort zurueckgesetzt. Damit ist die
Zuordnung fuer JEDEN Termin geprueft, unabhaengig von der Uhrzeit des
Laufs, und zusaetzlich beweist die Pruefung, dass die Daempfung greift.

NEU: Schriftgroessen werden jetzt AN DER QUELLE geprueft, in Sekunden
statt Minuten. Im Bestand stehen 43 solche Stellen; sie alle rot zu
melden haette die Pruefung ab Tag eins wertlos gemacht. Deshalb eine
Grundlinie wie beim Layout-Sprung: Sie haelt den Stand fest und
schlaegt an, sobald es MEHR werden. Heute haette das zweimal gegriffen.
Die Browserpruefung bleibt daneben -- sie sieht, was am Ende auf dem
Schirm steht, die CSS-Pruefung nur, was gemeint war.

Gesamtlauf: 56 von 56 Dateien, 2002 von 2002 Punkten.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 01:32:33 +02:00

583 lines
26 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`);
}
/* ---------- 10. Sind die Symbole fuer JEDES Geraet richtig gebaut? -------
PC und Handy holen ihr Symbol an drei verschiedenen Stellen, und
jede stellt andere Anforderungen:
Browser-Reiter das normale Symbol, darf runde Ecken haben
Android die maskable-Fassung, davon wird aussen bis zu
ein Fuenftel weggeschnitten
iPhone <link rel="apple-touch-icon"> -- und iOS rundet
SELBST ab und fuellt alles Durchsichtige mit
SCHWARZ
Der letzte Punkt ist der, der auffiel ("auf dem einen klappt es und
auf dem anderen nicht"): Ein apple-touch-icon mit eigener Rundung
bekommt auf dem iPhone schwarze Zipfel. Auf dem PC sieht dieselbe
Datei tadellos aus -- deshalb faellt es nur auf einem der beiden
Geraete auf, und deshalb muss es gemessen werden statt angesehen.
Geprueft werden die Dateinamen, nicht die Bilder selbst -- die
Bildpruefung braucht einen Browser und laeuft in pruef-symbole.mjs.
Hier geht es darum, dass ueberhaupt fuer jede App und jedes Geraet
eine Datei da ist. */
console.log(`
=== Symbole fuer alle Geraete ===`);
{
const symOrdner = join(REPO, "assets", "img", "app-symbole");
if (!existsSync(symOrdner)) {
ok(false, "der Ordner assets/img/app-symbole fehlt");
} else {
const da = readdirSync(symOrdner);
const apps = [...new Set(da.map((n) => n.replace(/-(\d+)(-maskable)?\.png$/, "")))]
.filter((n) => n.endsWith(".png") === false);
ok(apps.length >= 8, `${apps.length} Apps mit eigenen Symbolen`);
const noetig = ["192", "512", "192-maskable", "512-maskable", "180", "32"];
const fehlend = [];
for (const a of apps) {
for (const g of noetig) {
if (!da.includes(`${a}-${g}.png`)) fehlend.push(`${a}-${g}.png`);
}
}
ok(fehlend.length === 0,
fehlend.length ? `fehlende Symbole: ${fehlend.slice(0, 6).join(", ")}`
: `alle ${apps.length * noetig.length} Symbole vorhanden `
+ `(Reiter, Android-maskable, iPhone)`);
/* Jede Seite muss BEIDES nennen: das Reiter-Symbol und das fuers
iPhone. Fehlt das zweite, nimmt iOS einen Bildschirmausschnitt
der Seite -- unbrauchbar und unerkennbar. */
let ohneApple = [];
for (const seite of seiten) {
const text = readFileSync(join(WS, seite), "utf8");
if (!/rel="apple-touch-icon"/i.test(text)) ohneApple.push(seite);
}
ok(ohneApple.length === 0,
ohneApple.length ? `ohne apple-touch-icon (iPhone nimmt sonst einen Screenshot): ${ohneApple.join(", ")}`
: `alle ${seiten.length} Workspace-Seiten nennen ein iPhone-Symbol`);
}
}
/* ---------- 11. Laesst sich jedes Server-Modul ueberhaupt laden? --------
`node --check datei.js` prueft eine .js-Datei als CommonJS. Dieses
Haus faehrt aber ESM ("type": "module" in package.json). Der
Unterschied ist keine Formsache: Ein Syntaxfehler kann dabei
durchrutschen und die Datei trotzdem beim Start abstuerzen lassen.
Genau das ist am 05.09.2026 passiert. In einem SQL-Block -- einem
Template-Literal mit Backticks -- stand in einem KOMMENTAR ein Wort
in Backticks. Das beendete das Literal mitten im Text. `node --check`
meldete "Syntax ok", der Import scheiterte mit "missing ) after
argument list", und der Server waere beim naechsten Start gar nicht
hochgekommen.
Deshalb wird hier jedes Modul WIRKLICH geladen. Das ist die einzige
Pruefung, die dieselbe Frage stellt wie der Serverstart. */
console.log(`\n=== Laedt jedes Server-Modul? ===`);
{
/* GEPRUEFT WIRD MIT NODE SELBST, nicht mit einem Suchmuster.
Der erste Entwurf suchte per regulaerem Ausdruck nach Backticks in
SQL-Bloecken -- und schlug bei drei Bloecken an, die voellig in
Ordnung sind: Enthaelt ein Block ein ${...}, grenzt das Muster ihn
falsch ab. Eine Warnung, die falsch ist, ist schlimmer als keine;
sie kostet beim naechsten Mal zehn Minuten und wird danach
ignoriert.
Node kann die Frage selbst beantworten. Die Datei wird als .mjs
kopiert und mit `node --check` geprueft -- damit gilt dieselbe
Regel wie beim Start (ESM statt CommonJS). Genau darin lag der
Fehler vom 05.09.2026: `node --check datei.js` meldete "ok",
der Import scheiterte trotzdem. */
const { execFileSync } = await import("node:child_process");
const { writeFileSync, unlinkSync, mkdtempSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const module = readdirSync(SERVER).filter((n) => n.endsWith(".js"));
ok(module.length >= 20, `${module.length} Server-Module gefunden`);
const ordner = mkdtempSync(join(tmpdir(), "modulcheck-"));
const kaputt = [];
for (const datei of module) {
const kopie = join(ordner, datei.replace(/\.js$/, ".mjs"));
writeFileSync(kopie, readFileSync(join(SERVER, datei), "utf8"));
try {
execFileSync(process.execPath, ["--check", kopie], { stdio: "pipe" });
} catch (f) {
const meldung = String(f.stderr || f.message).split(/\r?\n/)
.find((z) => z.includes("Error")) || "Syntaxfehler";
kaputt.push(`${datei}: ${meldung.trim().slice(0, 70)}`);
}
try { unlinkSync(kopie); } catch { /* egal */ }
}
ok(kaputt.length === 0,
kaputt.length
? `als ESM NICHT ladbar: ${kaputt.slice(0, 5).join(" · ")}`
: `alle ${module.length} Module lassen sich als ESM einlesen`);
/* GEGENPROBE mit genau dem Fehler von heute.
Nachgebaut wird der echte Fall: ein Backtick in einem KOMMENTAR,
der innerhalb eines Template-Literals steht. Sieht harmlos aus,
beendet aber die Zeichenkette. */
const boese = join(ordner, "gegenprobe.mjs");
writeFileSync(boese, [
"const d = { exec: () => {} };",
"d.exec(`",
" CREATE TABLE x (a TEXT);",
" /* Der Wert `endpunkt` ist der Schluessel. */",
"`);",
].join("\n"));
let erkannt = false;
try { execFileSync(process.execPath, ["--check", boese], { stdio: "pipe" }); }
catch { erkannt = true; }
ok(erkannt, erkannt
? "Gegenprobe: ein Backtick im SQL-Kommentar wird als Fehler erkannt"
: "Gegenprobe MISSLUNGEN — die Pruefung wuerde den Fehler durchlassen");
/* Und der Beweis, dass sie nicht einfach alles ablehnt. */
const brav = join(ordner, "gegenprobe-ok.mjs");
writeFileSync(brav, "export const x = `alles gut ${1 + 1}`;\n");
let bravOk = true;
try { execFileSync(process.execPath, ["--check", brav], { stdio: "pipe" }); }
catch { bravOk = false; }
ok(bravOk, "Gegenprobe: eine saubere Datei wird durchgelassen");
for (const f of [boese, brav]) { try { unlinkSync(f); } catch { /* egal */ } }
}
/* ---------- Rechnet jede Pruefung den TAG so wie die Anwendung? --------
GEFUNDEN AM 06.09.2026 UM 00:10, und zwar auf die harte Tour: Der
Gesamtlauf meldete 13 Fehlschlaege in pruef-teilnehmer.mjs -- an
einer Datei, die seit Stunden niemand angefasst hatte.
Ortszeit (MESZ): 06.09.2026, 00:10
UTC: 05.09.2026, 22:10
Die Pruefung bildete ihr Datum mit `toISOString().slice(0, 10)` --
das ist UTC. Sie legte ihre Termine also auf gestern und fand heute
nichts. ZWEI STUNDEN AM TAG war sie damit rot (im Winter eine).
Wer nur tagsueber laeuft, sieht das nie. Wer nachts arbeitet, sucht
stundenlang am falschen Ende -- die Meldungen sagen "der Termin ist
weg", nicht "dein Datum ist falsch".
Deshalb wird es hier gesucht, statt darauf zu warten, dass es wieder
jemanden trifft. Gemeint ist nur das TAGESDATUM; ein Zeitstempel
(`erstellt`) darf und soll UTC sein. */
console.log(`\n=== Rechnen die Pruefungen mit Ortszeit? ===`);
{
const dateien = readdirSync(SERVER).filter((n) => n.startsWith("pruef-") && n.endsWith(".mjs"));
ok(dateien.length >= 30, `${dateien.length} Pruefdateien durchgesehen`);
const verdaechtig = [];
for (const datei of dateien) {
/* Die eigene Datei bleibt aussen vor: Die Gegenprobe unten MUSS
das gesuchte Muster als Text enthalten, sonst könnte sie nicht
beweisen, dass sie es erkennt. Sie würde sich sonst selbst
anzeigen -- eine Warnung, die immer kommt, und die liest bald
niemand mehr. */
if (datei === "pruef-struktur.mjs") continue;
const text = readFileSync(join(SERVER, datei), "utf8").replace(/\/\*[\s\S]*?\*\//g, " ");
/* Das Muster ist eng gefasst: NUR toISOString() gefolgt von einem
Schnitt auf zehn Zeichen -- also genau die Bildung eines
Tagesdatums. `new Date().toISOString()` allein (ein voller
Zeitstempel) ist voellig in Ordnung und faellt nicht darunter. */
for (const treffer of text.matchAll(/toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) {
const zeile = text.slice(0, treffer.index).split("\n").length;
verdaechtig.push(`${datei}:${zeile}`);
}
/* Und die zweite Schreibweise: erst in eine Variable, dann geschnitten. */
for (const treffer of text.matchAll(/=\s*(\w+)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) {
const name = treffer[1];
/* Nur melden, wenn diese Variable aus toISOString() stammt. */
if (new RegExp(`(const|let)\\s+${name}\\s*=\\s*new Date\\(\\)\\.toISOString\\(\\)`).test(text)) {
const zeile = text.slice(0, treffer.index).split("\n").length;
verdaechtig.push(`${datei}:${zeile} (über ${name})`);
}
}
}
ok(verdaechtig.length === 0,
verdaechtig.length
? `bilden ihr Tagesdatum aus UTC statt Ortszeit — nachts zwischen `
+ `00:00 und 02:00 schlagen sie fälschlich an:\n `
+ verdaechtig.join("\n ")
: `keine Prüfung bildet ihr Tagesdatum aus UTC (alle nutzen helfer-zeit.mjs)`);
/* GEGENPROBE: Findet das Muster überhaupt etwas? Ohne diesen
Nachweis wäre "0 Treffer" auch dann grün, wenn der reguläre
Ausdruck gar nichts erkennt. */
const probe = "const jetzt = new Date().toISOString();\nconst heute = jetzt.slice(0, 10);";
const erkanntA = /toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/
.test("new Date().toISOString().slice(0, 10)");
const erkanntB = /=\s*(\w+)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/.test(probe)
&& /(const|let)\s+jetzt\s*=\s*new Date\(\)\.toISOString\(\)/.test(probe);
ok(erkanntA && erkanntB,
"Gegenprobe: beide Schreibweisen werden erkannt (direkt und über eine Variable)");
/* Und ein harmloser Zeitstempel darf NICHT anschlagen -- sonst wäre
die Prüfung eine Warnung, die immer kommt. */
const harmlos = /toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/
.test("const jetzt = new Date().toISOString();");
ok(!harmlos, "Gegenprobe: ein voller Zeitstempel (ohne Schnitt) schlägt nicht an");
}
/* ---------- 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);