Beide Befunde sind heute beim Umzug des Vorlagenkatalogs aufgefallen,
gehoeren aber nicht dazu -- sie lagen schon vorher da und haetten bei
jedem Lauf mitgemeldet, bis sie niemand mehr liest.
pruef-struktur las Server-Routen nur in doppelten Anfuehrungszeichen.
Wer eine Route in einer Schleife registriert, schreibt sie aber als
Template:
for (const weg of ["annehmen", "ablehnen"])
router.post(`/workspace/api/vorlagen/bewerbung/${weg}`, ...)
Diese Routen fehlten in der Liste, und die Aufrufe dorthin galten als
"Schnittstelle gibt es nicht" -- obwohl sie laufen. Die Erfassung der
AUFRUFE kannte alle drei Zeichen laengst; nur die der ROUTEN nicht.
Zwei Regeln fuer dieselbe Frage, und eine davon war aelter. Gemessen:
319 statt 315 Routen, und der Fehlalarm ist weg. Die Gegenproben
schlagen weiter an ("erfundene Schnittstelle wird als fehlend
erkannt").
pruef-aufgaben-vorlagen zaehlte KARTEN und erwartete +1. Seit das
Brett gleiche Aufgaben zu einer Sammelkarte zusammenfasst, stimmt das
nicht mehr -- die Pruefung legt kurz davor sieben Vorlagen derselben
Stufe an, und die neue wanderte in eine bestehende Karte. Sie meldete
"9 -> 9" und behauptete damit, das Uebernehmen sei kaputt; drei Zeilen
weiter erkannte sie dieselbe Aufgabe als "schon uebernommen" wieder.
Jetzt zaehlt sie Aufgaben: "9 -> 10 Aufgaben in 9 Karten".
Dieselbe Stelle gab es zweimal. In pruef-modi-katalog ist sie gestern
repariert worden -- hier nicht, weil ich nach dem ersten Fund nicht
weitergesucht habe.
Co-Authored-By: Claude Opus 5 <[email protected]>
832 lines
38 KiB
JavaScript
832 lines
38 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";
|
|
import { brotliCompressSync, constants } from "node:zlib";
|
|
|
|
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");
|
|
/* AUCH ROUTEN IN ANFUEHRUNGSSTRICHEN UND BACKTICKS (23.09.2026).
|
|
|
|
Hier stand nur das doppelte Anfuehrungszeichen. Wer eine Route in
|
|
einer Schleife registriert, schreibt sie aber als Template:
|
|
|
|
for (const weg of ["annehmen", "ablehnen"])
|
|
router.post(`/workspace/api/vorlagen/bewerbung/${weg}`, ...)
|
|
|
|
Diese Routen fehlten in der Liste -- und die Aufrufe dorthin
|
|
wurden als "Schnittstelle gibt es nicht" gemeldet, obwohl sie
|
|
jeden Tag laufen. Ein Fehlalarm, den man zweimal liest und dann
|
|
nicht mehr; genau davor warnt der Kommentar weiter unten bei den
|
|
sechs angeblich verwaisten Seiten.
|
|
|
|
Die Erfassung der AUFRUFE kannte alle drei Zeichen schon
|
|
(["'`]) -- nur die Erfassung der ROUTEN nicht. Zwei Regeln fuer
|
|
dieselbe Frage, und eine davon war aelter. */
|
|
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;
|
|
/* ${...} IN EINER ROUTE IST EIN PLATZHALTER wie :id -- was dort
|
|
eingesetzt wird, entscheidet die Schleife beim Start. Der
|
|
Aufrufpfad ist oben schon zu "1" normalisiert worden; ohne diese
|
|
Zeile verglichen wir "1" mit "${weg}" und faenden nie etwas. */
|
|
return teileB.every((t, i) => t.startsWith(":") || t === "*"
|
|
|| t.includes("${") || 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 ===`);
|
|
/* WER AUF EINE SEITE ZEIGT, IST NICHT NUR EIN href (16.09.2026).
|
|
|
|
Diese Zeile meldete sechs Seiten als "von nirgendwo verlinkt":
|
|
crew-index, entwicklung, rechte, talente, teamlage und
|
|
treff-moderation. Keine davon war verwaist -- man kommt auf jede,
|
|
indem man eine KACHEL antippt. Die Kacheln stehen aber im Server
|
|
(`ziel: "talente.html"`), und der wurde hier nie durchsucht.
|
|
|
|
Eine Pruefung, die seit Wochen sechs Fehler meldet, die keine sind,
|
|
ist schlimmer als keine: Man liest sie nicht mehr, und dann uebersieht
|
|
man die siebte, die echt ist. Der blinde Fleck lag in der Pruefung,
|
|
nicht im Haus.
|
|
|
|
ABGELEITET, NICHT NACHGETRAGEN: Statt die sechs von Hand
|
|
auszunehmen, kommen die Dateien dazu, in denen die Ziele wirklich
|
|
stehen. Wer morgen eine Kachel anlegt, ist damit automatisch
|
|
abgedeckt -- und wer eine Seite anlegt, auf die nichts zeigt, faellt
|
|
weiterhin auf. */
|
|
const zeigerQuellen = [
|
|
...quellenMitLinks,
|
|
/* Die Kachellisten -- dort steht `ziel: "talente.html"`. */
|
|
{ name: "server/workspace.js", pfad: join(SERVER, "workspace.js") },
|
|
/* Die Zugangswaende -- crew-index.html wird dort ausgeliefert. */
|
|
{ name: "server/crew-adresse.js", pfad: join(SERVER, "crew-adresse.js") },
|
|
/* EIN TEILEN-ZIEL IST EIN VERWEIS (17.09.2026).
|
|
|
|
teilen.html stand hier als "von nirgendwo verlinkt" -- und das war
|
|
falsch: Auf sie zeigt der Eintrag `share_target.action` im
|
|
Manifest. Genau dafuer gibt es die Seite; einen Verweis im HTML
|
|
hat sie absichtlich nicht, weil man sie nicht aufsucht, sondern
|
|
von TikTok aus dort landet.
|
|
|
|
Die Manifeste kommen deshalb als Quelle dazu -- nicht die eine
|
|
Seite als Ausnahme. Eine Ausnahmeliste haette beim naechsten
|
|
Teilen-Ziel wieder gefehlt. */
|
|
...["app.webmanifest", "crew.webmanifest"]
|
|
.map((n) => ({ name: n, pfad: join(WS, n) }))
|
|
.filter((q) => existsSync(q.pfad)),
|
|
];
|
|
const verlinkt = new Set(["index.html", "start.html"]);
|
|
for (const { pfad: quelle } of zeigerQuellen) {
|
|
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 ===`);
|
|
/** Ist diese Text-Datei zu schwer? Gibt den Grund oder `null`.
|
|
*
|
|
* ALS EIGENE FUNKTION, damit die Gegenprobe sie mit erfundenem Inhalt
|
|
* aufrufen kann. Eine Messung, die nur ueber die echten Dateien
|
|
* laeuft, ist genau so lange gruen, wie zufaellig keine zu gross ist
|
|
* -- und beweist nie, dass sie ueberhaupt anschlagen kann. */
|
|
function textZuSchwer(roh) {
|
|
const gepackt = Math.round(brotliCompressSync(roh, {
|
|
params: { [constants.BROTLI_PARAM_QUALITY]: 4 },
|
|
}).length / 1024);
|
|
if (gepackt > 200) return `${gepackt} KB uebertragen`;
|
|
const ohneKommentar = Math.round(
|
|
roh.toString("utf8").replace(/\/\*[\s\S]*?\*\//g, "").length / 1024);
|
|
if (ohneKommentar > 200) return `${ohneKommentar} KB ohne Kommentare`;
|
|
return null;
|
|
}
|
|
|
|
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);
|
|
/* GESTUFTE BILDER DUERFEN GROESSER SEIN -- aber nur die (07.09.2026).
|
|
|
|
Die Grenze von 200 KB soll verhindern, dass eine Seite unnoetig
|
|
viel laedt. Bei einem Bild, das in mehreren Groessen vorliegt und
|
|
von dem der Browser IMMER NUR EINE holt, misst sie das Falsche:
|
|
Die 2400-px-Fassung einer Buehne wiegt 210 KB, wird aber nur auf
|
|
einem Schirm angefordert, der sie auch braucht -- ein Handy laedt
|
|
weiterhin 60 KB derselben Szene.
|
|
|
|
Ohne diese Ausnahme haette ich die Wahl zwischen einer Pruefung,
|
|
die bei jedem Lauf grundlos rot ist, und weichen Bildern auf
|
|
grossen Schirmen. Beides waere falsch. Die Grenze fuer alles
|
|
andere bleibt, wo sie war -- gerade weil sie fuer Skripte und
|
|
Stile weiterhin gilt.
|
|
|
|
ERKANNT AM NAMEN, nicht am Ordner: `-uhd` und `gate-2560` sagen
|
|
ausdruecklich "das ist die grosse Stufe". Wer eine Datei so nennt,
|
|
ohne dass es Stufen gibt, umgeht die Grenze -- deshalb prueft die
|
|
Zeile darunter, dass es die kleineren Stufen auch wirklich gibt. */
|
|
/* `-breit` GEHOERT DAZU (07.09.2026, Nachtrag am selben Tag).
|
|
|
|
Die Ausnahme kannte nur die groesste Stufe. Dann wurden die Bilder
|
|
nachgeschaerft -- und Schaerfe ist genau das, was Bytes kostet:
|
|
Sie fuegt hohe Frequenzen hinzu, und die kann kein Verfahren
|
|
wegrechnen. Die MITTLERE Stufe wuchs damit von rund 170 auf 230
|
|
bis 300 KB und lief in eine Grenze, die fuer EINZELDATEIEN gedacht
|
|
ist, nicht fuer eine von drei Stufen.
|
|
|
|
Die Regel bleibt inhaltlich unveraendert: Eine Datei darf gross
|
|
sein, WENN es nachweislich eine kleinere Stufe daneben gibt. Fuer
|
|
`-breit` ist das `-schmal` (85 bis 130 KB) -- ein Handy holt
|
|
weiterhin die kleine. Was wirklich zaehlt, misst ohnehin
|
|
pruef-tempo-workspace an der fertigen Seite: Dort liegt die
|
|
Grenze bei 1,2 MB, und start.html kommt auf 290 KB. */
|
|
const gestuft = /-(uhd|breit)\.webp$|^gate-(1920|2560)\.webp$|^gate-hoch-900\.webp$/.test(n);
|
|
if (gestuft) {
|
|
/* JE STUFE GENAU EINE naechstkleinere -- nicht verkettet.
|
|
`replace(uhd->breit).replace(breit->schmal)` haette bei einer
|
|
uhd-Datei BEIDE Ersetzungen ausgefuehrt und am Ende nach
|
|
"-schmal" gesucht. Das faellt nicht auf, solange alle drei
|
|
Stufen da sind, und genau deshalb waere es ein stiller Fehler:
|
|
Fehlte eines Tages `-breit`, meldete die Pruefung trotzdem
|
|
"in Ordnung". */
|
|
const kleiner = n.endsWith("-uhd.webp") ? n.replace("-uhd.webp", "-breit.webp")
|
|
: n.endsWith("-breit.webp") ? n.replace("-breit.webp", "-schmal.webp")
|
|
: n
|
|
.replace(/^gate-(1920|2560)\.webp$/, "gate-1280.webp")
|
|
.replace("gate-hoch-900.webp", "gate-hoch-440.webp");
|
|
if (!existsSync(join(ordner, kleiner))) {
|
|
schwer.push(`${unter}/${n} ${kb} KB (als Stufe ausgewiesen, aber ${kleiner} fehlt)`);
|
|
}
|
|
continue;
|
|
}
|
|
/* BEI TEXT MISST DIE ROHE GROESSE DAS FALSCHE (16.09.2026).
|
|
|
|
Die Grenze soll verhindern, "dass eine Seite unnoetig viel
|
|
laedt" -- so steht es oben. Fuer Bilder ist die Datei genau das,
|
|
was geladen wird. Fuer CSS und JS nicht: Caddy komprimiert sie
|
|
unterwegs. Nachgemessen auf der echten Seite am 16.09.:
|
|
|
|
start.css auf der Platte 346 KB uebertragen 110 KB (br)
|
|
chat.css 59 KB 16 KB
|
|
entwicklung.css 33 KB 8 KB
|
|
|
|
Die Pruefung meldete start.css mit 347 KB -- davon sind 227 KB
|
|
KOMMENTAR (64 %). Sie bestrafte damit genau das, was dieses Haus
|
|
absichtlich tut: jede Entscheidung danebenschreiben. Und sie
|
|
haette eine Datei mit 199 KB dichtem, unkommentiertem CSS
|
|
durchgewunken.
|
|
|
|
Dasselbe Argument steht drei Absaetze weiter oben schon einmal,
|
|
fuer die gestuften Bilder: "Bei einem Bild, das in mehreren
|
|
Groessen vorliegt, misst sie das Falsche."
|
|
|
|
JETZT ZWEI ZAHLEN, weil es zwei verschiedene Fragen sind:
|
|
|
|
1. WAS DER BESUCHER LAEDT -- brotli, Stufe 4. Die Stufe ist
|
|
nicht geraten: Bei 4 liefert node 111 KB, Caddy liefert
|
|
110. Jede andere Stufe waere eine Zahl, die mit der
|
|
Wirklichkeit nichts zu tun hat.
|
|
2. WAS EIN MENSCH PFLEGEN MUSS -- die Datei ohne Kommentare.
|
|
Sonst koennte man die erste Zahl klein halten, indem man
|
|
immer mehr Prosa schreibt, und niemand merkt, dass das CSS
|
|
selbst ausgeufert ist. */
|
|
if (/\.(css|js)$/.test(n)) {
|
|
const grund = textZuSchwer(readFileSync(p));
|
|
if (grund) schwer.push(`${unter}/${n} ${grund}`);
|
|
continue;
|
|
}
|
|
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");
|
|
|
|
/* GEGENPROBE ZUR GROESSENMESSUNG. Sie ist seit dem 16.09.2026 kein
|
|
blosser Blick auf die Dateigroesse mehr, sondern packt den Inhalt
|
|
zusammen -- und eine Messung, die niemand je hat anschlagen sehen,
|
|
ist eine Behauptung. Drei Faelle, und jeder muss anders ausgehen. */
|
|
{
|
|
/* 1. Viel echtes CSS, das sich nicht wegpacken laesst. */
|
|
let dicht = "";
|
|
for (let i = 0; i < 9000; i++) {
|
|
dicht += `.k${i.toString(36)}-${(i * 7919).toString(36)} { color: #${
|
|
(i * 2654435761 % 16777215).toString(16).padStart(6, "0")}; }\n`;
|
|
}
|
|
ok(textZuSchwer(Buffer.from(dicht)) !== null,
|
|
`dichtes CSS von ${Math.round(dicht.length / 1024)} KB faellt durch `
|
|
+ `(${textZuSchwer(Buffer.from(dicht))})`);
|
|
|
|
/* 2. Dieselbe Menge Text, aber als Prosa im Kommentar -- die packt
|
|
sich auf ein Zehntel und darf durchgehen. Das ist der ganze
|
|
Grund fuer die Umstellung. */
|
|
const prosa = "/* " + "Jede Entscheidung gehoert danebengeschrieben. ".repeat(9000) + " */\n"
|
|
+ ".a { color: #fff; }\n";
|
|
ok(textZuSchwer(Buffer.from(prosa)) === null,
|
|
`${Math.round(prosa.length / 1024)} KB Kommentar gehen durch — sie kosten `
|
|
+ "den Besucher fast nichts");
|
|
|
|
/* 3. UND DIE ZWEITE ZAHL MUSS AUCH ANSCHLAGEN: viel echtes CSS, das
|
|
sich zwar gut packt (weil es sich wiederholt), aber trotzdem
|
|
niemand mehr pflegen kann. Ohne diesen Fall koennte man die
|
|
erste Grenze mit Wiederholung unterlaufen. */
|
|
const wiederholt = ".gleich { color: #ffffff; padding: 4px; }\n".repeat(6000);
|
|
ok(textZuSchwer(Buffer.from(wiederholt)) !== null,
|
|
`${Math.round(wiederholt.length / 1024)} KB sich wiederholendes CSS faellt `
|
|
+ `ebenfalls durch (${textZuSchwer(Buffer.from(wiederholt))})`);
|
|
}
|
|
|
|
/* ---------- 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`);
|
|
|
|
/* UND DAS MANIFEST (06.09.2026 nachgetragen).
|
|
|
|
Gefunden, weil ich es selbst zweimal vergessen habe: chat.html und
|
|
leistung.html -- die beiden neuesten Seiten -- hatten kein
|
|
<link rel="manifest">. Folge auf dem Handy: Wer die Anwendung
|
|
installiert hat und ueber eine Benachrichtigung auf einer dieser
|
|
Seiten landet, verlaesst damit den App-Rahmen. Die Seite oeffnet
|
|
in der Adressleiste des Browsers statt in der App, mit falscher
|
|
Leistenfarbe und ohne Zurueckweg in den Rest.
|
|
|
|
Es faellt niemandem auf, weil am Rechner alles normal aussieht.
|
|
Genau darum steht es jetzt hier: Was man beim Anlegen einer neuen
|
|
Seite vergessen kann, muss eine Pruefung nachhalten -- nicht das
|
|
Gedaechtnis. Und weil `seiten` aus dem Verzeichnis kommt, ist die
|
|
naechste neue Seite von selbst dabei.
|
|
|
|
theme-color gehoert dazu: Ohne sie zeichnet Android die
|
|
Systemleiste weiss -- ein greller Balken ueber einer dunklen
|
|
Oberflaeche, das Gegenteil von augenschonend. */
|
|
const ohneManifest = [];
|
|
const ohneFarbe = [];
|
|
for (const seite of seiten) {
|
|
const text = readFileSync(join(WS, seite), "utf8");
|
|
if (!/rel="manifest"/i.test(text)) ohneManifest.push(seite);
|
|
if (!/name="theme-color"/i.test(text)) ohneFarbe.push(seite);
|
|
}
|
|
ok(seiten.length > 10 && ohneManifest.length === 0,
|
|
ohneManifest.length
|
|
? `ohne <link rel="manifest"> — oeffnet auf dem Handy ausserhalb der App: ${ohneManifest.join(", ")}`
|
|
: `alle ${seiten.length} Seiten binden das Manifest ein`);
|
|
ok(seiten.length > 10 && ohneFarbe.length === 0,
|
|
ohneFarbe.length
|
|
? `ohne theme-color — Android zeichnet die Leiste dann weiss: ${ohneFarbe.join(", ")}`
|
|
: `alle ${seiten.length} Seiten setzen eine theme-color`);
|
|
}
|
|
}
|
|
|
|
/* ---------- 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;
|
|
/* KOMMENTARE RAUS, ZEILEN BEHALTEN (16.09.2026).
|
|
|
|
Hier stand `replace(…, " ")` -- ein Blockkommentar ueber zwanzig
|
|
Zeilen wurde also zu einem Leerzeichen, und ab da zaehlte
|
|
`split("\n").length` falsch. Gemeldet wurden Zeilennummern, die
|
|
auf voellig anderen Code zeigten: pruef-eskalation.mjs:43 ist
|
|
eine Zeile ueber Kekse.
|
|
|
|
Wer einer solchen Meldung nachgeht, findet dort nichts, haelt
|
|
die Pruefung fuer kaputt und sieht beim naechsten Mal gar nicht
|
|
mehr nach. Jetzt bleibt je Zeile eine Zeile stehen. */
|
|
const text = readFileSync(join(SERVER, datei), "utf8")
|
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/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);
|