Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
1291 lines
60 KiB
JavaScript
1291 lines
60 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 { sep, 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 ------------------------------------------ */
|
|
|
|
/* ZWEI SEITEN SIND KEINE SEITEN FUER MENSCHEN.
|
|
|
|
`buehne.html` und `tafel.html` sind Quellen fuer OBS und TikTok
|
|
Studio: Sie laufen in einem Programmfenster, werden nie auf einem
|
|
Telefon geoeffnet und nie installiert. Ein Symbol fuer den
|
|
Startbildschirm, ein Manifest und eine Leistenfarbe waeren dort
|
|
ohne Sinn.
|
|
|
|
BENANNT UND MIT GRUND, nicht still: Eine Ausnahme ohne Namen ist
|
|
die, die spaeter auch die naechste Seite verschluckt. */
|
|
const NUR_FUER_OBS = new Set(["buehne.html", "tafel.html"]);
|
|
|
|
const alleSeiten = readdirSync(WS).filter((n) => n.endsWith(".html"));
|
|
const seiten = alleSeiten.filter((n) => !NUR_FUER_OBS.has(n));
|
|
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 = "";
|
|
/* HIER `alleSeiten` UND NICHT `seiten` (29.09.2026).
|
|
|
|
`seiten` laesst `buehne.html` und `tafel.html` aus -- mit gutem
|
|
Grund, aber fuer eine ANDERE Frage: Die beiden brauchen kein
|
|
Startbildschirm-Symbol und kein Manifest. Beim Suchen nach totem
|
|
CSS hat dieselbe Ausnahme eine falsche Antwort gegeben: Eine
|
|
Klasse, die nur in einer OBS-Quelle steht, galt als unbenutzt.
|
|
|
|
Aufgefallen an `.obs-buehne`, das genau dort steht. Der Wachter
|
|
haette also verlangt, eine Regel zu loeschen, die den Stream
|
|
traegt. Eine Ausnahme wirkt immer auf ALLES, was hinter ihr
|
|
kommt -- nicht nur auf die Frage, fuer die sie gemacht wurde. */
|
|
for (const n of alleSeiten) 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=== Das deutsche Schlusszeichen ===`);
|
|
/* ===================================================================
|
|
AM 01.10.2026 NACHGEMESSEN: 199 Stellen, die ein Mensch liest,
|
|
schlossen ein deutsches Anfuehrungszeichen mit einem GERADEN " --
|
|
auf sechzehn oeffentlichen Seiten, in vierzig Dateien des
|
|
Workspace und in zwoelf Servermodulen, die Texte verschicken.
|
|
|
|
Deutsch oeffnet mit \u201E und schliesst mit \u201C. Auf dem
|
|
Bildschirm sieht man den Unterschied sofort; beim Schreiben nicht,
|
|
denn das gerade " liegt auf der Tastatur und die anderen nicht.
|
|
Ohne Wache sind es in vier Wochen wieder zwanzig.
|
|
|
|
WAS BEWUSST STEHEN BLEIBT -- und warum die Regel so und nicht
|
|
einfacher ist: Ein gerades " ist an vielen Stellen SYNTAX und kein
|
|
Schriftzeichen. Es begrenzt eine Zeichenkette, ein HTML-Attribut
|
|
oder steht in einem regulaeren Ausdruck. Ein erster Anlauf, der
|
|
stumpf ersetzte, musste zurueckgenommen werden.
|
|
|
|
Die Unterscheidung laeuft an Merkmalen, nicht an einer Liste von
|
|
Ausnahmen:
|
|
< > = dazwischen -> HTML-Marke oder Attribut
|
|
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
|
|
Satz geht in der naechsten Zeile weiter. Ein deutsches
|
|
Schlusszeichen steht NIE hinter einem Leerzeichen.
|
|
Rueckstrich mittendrin -> regulaerer Ausdruck
|
|
${ ohne } -> mitten in einem Ausdruck
|
|
=================================================================== */
|
|
{
|
|
const AUF = "\u201E"; /* „ */
|
|
const ZU = "\u201C"; /* “ */
|
|
/* Zusammengesetzt und nicht ausgeschrieben: Sonst zeigte diese
|
|
Datei sich beim naechsten Lauf selbst an. */
|
|
const suche = new RegExp(AUF + '([^' + ZU + AUF + '"`\n]{1,140})"', "g");
|
|
|
|
const warumNicht = (innen) => {
|
|
if (/[<>=]/.test(innen)) return "HTML-Marke oder Attribut";
|
|
if (innen.endsWith(" ")) return "Satz geht in der naechsten Zeile weiter";
|
|
if (innen.replace(/\\+$/, "").includes("\\")) return "regulaerer Ausdruck";
|
|
if ((innen.match(/\$\{/g) || []).length !== (innen.match(/\}/g) || []).length) {
|
|
return "offener Ausdruck";
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/* WAS EIN MENSCH LIEST: die oeffentlichen Seiten, der Workspace und
|
|
die Servermodule, die Texte verschicken. Pruef- und Messdateien
|
|
schreiben nur in meine Konsole -- dort waere es Kosmetik, und
|
|
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. */
|
|
const sammeln = (ordner, tiefe = 0) => {
|
|
const raus = [];
|
|
for (const e of readdirSync(ordner, { withFileTypes: true })) {
|
|
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
const voll = join(ordner, e.name);
|
|
if (e.isDirectory()) { if (tiefe < 4) raus.push(...sammeln(voll, tiefe + 1)); continue; }
|
|
if (/\.(js|mjs|html|css)$/.test(e.name)) raus.push(voll);
|
|
}
|
|
return raus;
|
|
};
|
|
const dateien = [
|
|
...sammeln(join(SERVER, "..", "workspace")),
|
|
...sammeln(join(SERVER, "..", "assets")),
|
|
...readdirSync(join(SERVER, "..")).filter((n) => n.endsWith(".html"))
|
|
.map((n) => join(SERVER, "..", n)),
|
|
...readdirSync(SERVER).filter((n) => n.startsWith("workspace-") && n.endsWith(".js"))
|
|
.map((n) => join(SERVER, n)),
|
|
];
|
|
ok(dateien.length > 100, `${dateien.length} Dateien mit sichtbarem Text angesehen`);
|
|
|
|
const falsch = [];
|
|
const stehenGelassen = [];
|
|
for (const pfad of dateien) {
|
|
const art = pfad.slice(pfad.lastIndexOf("."));
|
|
let text = readFileSync(pfad, "utf8")
|
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
|
|
.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, " "));
|
|
if (art === ".js" || art === ".mjs") {
|
|
text = text.replace(/^\s*\/\/.*$/gm, "");
|
|
}
|
|
/* Der Pfad AB DER WURZEL des Projekts -- ein abgeschnittener
|
|
Pfad ist eine Meldung, der man nicht folgen kann. */
|
|
const kurz = pfad.split(sep).join("/").split("/DogiHompage/").pop();
|
|
for (const t of text.matchAll(suche)) {
|
|
const zeile = text.slice(0, t.index).split("\n").length;
|
|
const nein = warumNicht(t[1]);
|
|
if (nein) stehenGelassen.push(`${kurz}:${zeile} (${nein})`);
|
|
else falsch.push(`${kurz}:${zeile} ${AUF}${t[1].slice(0, 40)}"`);
|
|
}
|
|
}
|
|
ok(falsch.length === 0,
|
|
falsch.length
|
|
? `schliessen ein deutsches Anfuehrungszeichen mit einem geraden ":\n `
|
|
+ falsch.slice(0, 15).join("\n ")
|
|
+ (falsch.length > 15 ? `\n ... und ${falsch.length - 15} weitere` : "")
|
|
: `kein sichtbarer Text schliesst mit einem geraden " (${dateien.length} Dateien)`);
|
|
|
|
/* DIE AUSNAHMEN WERDEN GEZAEHLT UND GENANNT. Eine Ausnahme, die
|
|
niemand sieht, waechst -- und irgendwann steht der echte Fall
|
|
darin und faellt nicht mehr auf. */
|
|
ok(stehenGelassen.length <= 12,
|
|
`${stehenGelassen.length} Stellen bleiben mit Grund stehen`
|
|
+ ` (erlaubt: 12) — ${stehenGelassen.slice(0, 3).join(", ") || "—"}`);
|
|
|
|
/* ---- GEGENPROBEN ------------------------------------------------ */
|
|
const findet = (code) => {
|
|
const treffer = [...code.matchAll(new RegExp(suche.source, "g"))];
|
|
return treffer.filter((t) => !warumNicht(t[1])).length;
|
|
};
|
|
ok(findet(`Er sagte ${AUF}hallo".`) === 1,
|
|
"Gegenprobe: ein gerades Schlusszeichen faellt auf");
|
|
ok(findet(`Er sagte ${AUF}hallo${ZU}.`) === 0, " das richtige NICHT");
|
|
ok(findet(`${AUF}<a href="x">y</a>`) === 0,
|
|
" ein HTML-Attribut ist kein Schlusszeichen");
|
|
ok(findet(`"… nichts ${AUF}mal " + "eben …"`) === 0,
|
|
" und die Begrenzung einer Zeichenkette, die weitergeht, auch nicht");
|
|
ok(findet('/^["\'' + AUF + '»\\s]+|["]/') === 0,
|
|
" ein regulaerer Ausdruck ebenfalls nicht");
|
|
ok(findet(AUF + "${a.titel}\"") === 1,
|
|
" ein eingesetzter Wert dagegen schon");
|
|
}
|
|
|
|
console.log(`\n=== Ruft ein Modul etwas, das es nie eingefuehrt hat? ===`);
|
|
/* ===================================================================
|
|
AM 01.10.2026 SELBST GEBAUT UND FAST AUSGELIEFERT.
|
|
|
|
Ein Werkzeug von mir hat in elf Modulen `heuteLokal()` eingesetzt
|
|
und die Einfuhr weggelassen. `node --check` sagt dazu nichts, und
|
|
„Laedt jedes Server-Modul?" ein Stueck weiter oben auch nicht: Die
|
|
Datei ist syntaktisch tadellos. Erst der AUFRUF faellt um:
|
|
|
|
ReferenceError: heuteLokal is not defined
|
|
|
|
Gefunden hat es pruef-video -- zufaellig, weil dort genau diese
|
|
Zeile geprueft wird. Die anderen zehn Module waeren durchgerutscht,
|
|
und bei einer Zeile, die nur nachts laeuft, faellt das erst Wochen
|
|
spaeter jemandem auf.
|
|
|
|
DAS IST EINE DRITTE SORTE FEHLER an dieser Stelle: nicht „die Datei
|
|
ist kaputt" und nicht „die Datei tut das Falsche", sondern „die
|
|
Datei faellt erst um, wenn jemand genau diese Zeile braucht".
|
|
|
|
ABGELEITET, NICHT AUFGEZAEHLT: Die Namen des Hauses kommen aus den
|
|
`export`-Zeilen von workspace.js und der helfer-Module. Eine
|
|
gepflegte Liste waere hier genau die, die beim naechsten Export
|
|
nicht mitwaechst -- derselbe Fehler, an dem dieses Haus schon
|
|
viermal haengengeblieben ist.
|
|
=================================================================== */
|
|
{
|
|
/** Kommentare und Zeichenketten weg, Zeilen behalten. Sonst zaehlt
|
|
* ein Name in einem SQL-Text oder in einer Begruendung als Aufruf. */
|
|
const nurCode = (text) => text
|
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
|
|
.replace(/^\s*\/\/.*$/gm, "")
|
|
.replace(/`(?:[^`\\]|\\.)*`/g, (m) => m.replace(/[^\n]/g, " "))
|
|
.replace(/"(?:[^"\\\n]|\\.)*"/g, "\"\"")
|
|
.replace(/'(?:[^'\\\n]|\\.)*'/g, "''");
|
|
|
|
/** Was eine Datei nach aussen gibt -- aus ihren eigenen Zeilen. */
|
|
const exporte = (text) => {
|
|
const raus = new Set();
|
|
for (const t of text.matchAll(
|
|
/^export\s+(?:async\s+)?(?:function|const|let|class)\s+(\w+)/gm)) raus.add(t[1]);
|
|
return raus;
|
|
};
|
|
|
|
const quellen = ["workspace.js", "helfer-zeit.mjs", "helfer-tag.mjs"];
|
|
const hausNamen = new Map(); // Name -> woher
|
|
for (const q of quellen) {
|
|
for (const n of exporte(readFileSync(join(SERVER, q), "utf8"))) hausNamen.set(n, q);
|
|
}
|
|
ok(hausNamen.size >= 30,
|
|
`${hausNamen.size} ausgeleitete Namen aus ${quellen.length} Quellen gelesen`);
|
|
|
|
/** Welche Namen kennt diese Datei? Eingefuehrte, selbst erklaerte
|
|
* und aus einer Zerlegung entnommene -- alles, was nicht zu einem
|
|
* ReferenceError fuehren kann. */
|
|
const bekannt = (code) => {
|
|
const kennt = new Set();
|
|
for (const t of code.matchAll(/import\s*\{([^}]*)\}\s*from/g)) {
|
|
for (const stueck of t[1].split(",")) {
|
|
const n = stueck.trim().split(/\s+as\s+/).pop().trim();
|
|
if (n) kennt.add(n);
|
|
}
|
|
}
|
|
for (const t of code.matchAll(/(?:const|let|var)\s*\{([^}]*)\}\s*=/g)) {
|
|
for (const stueck of t[1].split(",")) {
|
|
const n = stueck.trim().split(":").pop().trim();
|
|
if (n) kennt.add(n);
|
|
}
|
|
}
|
|
for (const t of code.matchAll(
|
|
/(?:^|\s)(?:const|let|var|function|class)\s+(\w+)/g)) kennt.add(t[1]);
|
|
for (const t of code.matchAll(/import\s+(\w+)\s+from/g)) kennt.add(t[1]);
|
|
return kennt;
|
|
};
|
|
|
|
const dateien = readdirSync(SERVER)
|
|
.filter((n) => (n.endsWith(".js") || n.endsWith(".mjs")) && !quellen.includes(n));
|
|
const fehlt = [];
|
|
let gesucht = 0;
|
|
for (const datei of dateien) {
|
|
if (datei === "pruef-struktur.mjs") continue; /* siehe Gegenprobe */
|
|
const code = nurCode(readFileSync(join(SERVER, datei), "utf8"));
|
|
const kennt = bekannt(code);
|
|
for (const [name, woher] of hausNamen) {
|
|
/* Nur ein AUFRUF zaehlt. Ein Name in einer Eigenschaft
|
|
(`x.heuteLokal`) ist etwas anderes und gehoert nicht hierher. */
|
|
const ruft = new RegExp(`(^|[^\\w.])${name}\\s*\\(`, "m").test(code);
|
|
if (!ruft) continue;
|
|
gesucht++;
|
|
if (!kennt.has(name)) fehlt.push(`${datei}: ${name}() (aus ${woher})`);
|
|
}
|
|
}
|
|
ok(gesucht > 50, `${gesucht} Aufrufe von Hausnamen in ${dateien.length} Dateien angesehen`);
|
|
ok(fehlt.length === 0,
|
|
fehlt.length
|
|
? `rufen etwas, das sie nicht eingefuehrt haben — das faellt erst beim`
|
|
+ ` Aufruf um, nicht beim Laden:\n ` + fehlt.slice(0, 12).join("\n ")
|
|
: "jeder Aufruf eines Hausnamens hat auch seine Einfuhr");
|
|
|
|
/* ---- GEGENPROBEN --------------------------------------------------
|
|
Die Zeile darueber meldet „in Ordnung", solange sie nichts findet.
|
|
Heute Nacht haette sie elf Dateien gefunden -- also einmal zeigen,
|
|
dass sie das auch kann. */
|
|
{
|
|
const probeFehlt = (code, name) => {
|
|
const c = nurCode(code);
|
|
const ruft = new RegExp(`(^|[^\\w.])${name}\\s*\\(`, "m").test(c);
|
|
return ruft && !bekannt(c).has(name);
|
|
};
|
|
ok(probeFehlt('const t = heuteLokal();', "heuteLokal"),
|
|
"Gegenprobe: ein Aufruf ohne Einfuhr faellt auf");
|
|
ok(!probeFehlt('import { heuteLokal } from "./workspace.js";\nconst t = heuteLokal();',
|
|
"heuteLokal"), " mit Einfuhr NICHT");
|
|
ok(!probeFehlt('const { heuteLokal } = await import("./workspace.js");\n'
|
|
+ 'const t = heuteLokal();', "heuteLokal"), " und ueber eine Zerlegung auch nicht");
|
|
ok(!probeFehlt('function heuteLokal() { return 1; }\nconst t = heuteLokal();',
|
|
"heuteLokal"), " wer ihn selbst erklaert, ebenfalls nicht");
|
|
ok(!probeFehlt('/* heuteLokal() waere hier falsch */\nconst t = 1;', "heuteLokal"),
|
|
" und ein Name im Kommentar ist kein Aufruf");
|
|
ok(!probeFehlt('const t = uhr.heuteLokal();', "heuteLokal"),
|
|
" eine Eigenschaft an einem Objekt auch nicht");
|
|
}
|
|
}
|
|
|
|
console.log(`\n=== Wird der TAG in Ortszeit gebildet -- ueberall? ===`);
|
|
/* ===================================================================
|
|
AM 01.10.2026 UM 01:22 GEWACHSEN, weil diese Wache versagt hatte.
|
|
|
|
Sie sah seit dem 06.09. nur in die `pruef-*.mjs`. In der ANWENDUNG
|
|
standen zur selben Zeit SECHZEHN Stellen, die einen Kalendertag aus
|
|
UTC bilden -- zehn davon schrieben ihn in die Datenbank.
|
|
|
|
Aufgefallen ist es nicht hier, sondern in pruef-kreislauf: Ein
|
|
Wunsch mit dem heutigen Datum wurde zu einem Termin, und der trug
|
|
`2026-09-30` statt `2026-10-01`. Auf dem Brett „Was ansteht" landete
|
|
er damit unter „Vorbei" -- einem Abschnitt, der von sich aus
|
|
zugeklappt ist. Der Termin war da und war nicht zu sehen.
|
|
|
|
Eine Sicherung, die nur die halbe Menge kennt, faellt in der
|
|
anderen Haelfte aus -- lautlos, denn sie meldet ja „nichts
|
|
gefunden". Deshalb sieht sie jetzt in BEIDES: in die Anwendung und
|
|
in die Pruefungen.
|
|
=================================================================== */
|
|
{
|
|
const pruefungen = readdirSync(SERVER)
|
|
.filter((n) => (n.startsWith("pruef-") || n.startsWith("mess-")) && n.endsWith(".mjs"));
|
|
/* DIE ANWENDUNG -- abgeleitet, nicht aufgezaehlt: alles, was der
|
|
Dienst laedt, liegt als `.js` im Serverordner. Eine Liste von
|
|
Dateinamen waere die naechste, die beim uebernaechsten Modul
|
|
nicht mitwaechst. */
|
|
const anwendung = readdirSync(SERVER).filter((n) => n.endsWith(".js"));
|
|
const dateien = [...anwendung, ...pruefungen];
|
|
ok(anwendung.length >= 30 && pruefungen.length >= 30,
|
|
`${anwendung.length} Dateien der Anwendung und ${pruefungen.length} Pruef-/Messdateien durchgesehen`);
|
|
|
|
/* EINE UHRZEIT IM AUSDRUCK HEISST: DER TAG STEHT SCHON FEST.
|
|
`Date.parse(tag + "T12:00:00Z") + n * 86400000` bleibt in jeder
|
|
Zeitzone am selben Kalendertag -- das ist der dokumentierte Weg
|
|
des Hauses (workspace-kalender, workspace-teamlage,
|
|
workspace-serien). Erkannt wird das am Ausdruck selbst; eine
|
|
Liste erlaubter Ausnahmen braucht es dadurch nicht, und eine
|
|
Liste, die niemand pflegt, waere die naechste Stelle, an der es
|
|
auseinanderlaeuft. */
|
|
const festeUhrzeit = /T\d{2}:\d{2}:\d{2}Z/;
|
|
|
|
/** Alle Stellen einer Datei, an denen aus einem ZEITPUNKT ein
|
|
* Kalendertag wird. Gibt Zeilennummern zurueck. */
|
|
function utcTage(text) {
|
|
const treffer = [];
|
|
const zeile = (i) => text.slice(0, i).split("\n").length;
|
|
|
|
/* (1) Direkt: new Date(...).toISOString().slice(0, 10) */
|
|
for (const t of text.matchAll(
|
|
/new Date\(([^;]*?)\)\s*\.\s*toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) {
|
|
if (festeUhrzeit.test(t[1])) continue;
|
|
treffer.push(zeile(t.index));
|
|
}
|
|
|
|
/* (2) Ueber einen NAMEN. Zuerst sammeln, welche Namen einen
|
|
UTC-Zeitstempel liefern -- als Funktion ODER als Wert. Genau
|
|
diese Schreibweise fehlte bis heute:
|
|
|
|
const jetzt = () => new Date().toISOString();
|
|
... jetzt().slice(0, 10) ...
|
|
|
|
Und in zwei Schritten kommt sie auch vor:
|
|
|
|
const jetztIso = jetzt();
|
|
... jetztIso.slice(0, 10) ... */
|
|
const utcNamen = new Set();
|
|
for (const t of text.matchAll(
|
|
/(?:const|let)\s+(\w+)\s*=\s*(?:\(\)\s*=>\s*)?new Date\(\)\.toISOString\(\)/g)) {
|
|
utcNamen.add(t[1]);
|
|
}
|
|
for (let runde = 0; runde < 2; runde++) {
|
|
for (const t of text.matchAll(/(?:const|let)\s+(\w+)\s*=\s*(\w+)\(\)\s*;/g)) {
|
|
if (utcNamen.has(t[2])) utcNamen.add(t[1]);
|
|
}
|
|
}
|
|
for (const t of text.matchAll(/(\w+)(\(\))?\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) {
|
|
if (utcNamen.has(t[1])) treffer.push(zeile(t.index));
|
|
}
|
|
return treffer;
|
|
}
|
|
|
|
const verdaechtig = [];
|
|
for (const datei of dateien) {
|
|
/* Die eigene Datei bleibt aussen vor: Die Gegenproben unten
|
|
MUESSEN das gesuchte Muster als Text enthalten, sonst koennten
|
|
sie nicht beweisen, dass sie es erkennen. Sie wuerde sich sonst
|
|
selbst anzeigen -- eine Warnung, die immer kommt. */
|
|
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. Jetzt bleibt je Zeile eine
|
|
Zeile stehen. */
|
|
const text = readFileSync(join(SERVER, datei), "utf8")
|
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
|
|
.replace(/^\s*\/\/.*$/gm, "");
|
|
for (const z of utcTage(text)) verdaechtig.push(`${datei}:${z}`);
|
|
}
|
|
|
|
ok(verdaechtig.length === 0,
|
|
verdaechtig.length
|
|
? `bilden ihr Tagesdatum aus UTC statt Ortszeit — nachts zwischen `
|
|
+ `00:00 und 02:00 liegt es einen Tag zurueck:\n `
|
|
+ verdaechtig.join("\n ")
|
|
: `keine der ${dateien.length} Dateien bildet einen Kalendertag aus UTC`);
|
|
|
|
/* ---- GEGENPROBEN -------------------------------------------------
|
|
Eine Erkennung, die nichts findet, sieht genauso aus wie eine, die
|
|
nichts finden KANN. Heute Nacht war sie genau das: Die Schreibweise
|
|
mit einer Funktion kannte sie nicht, und die Anwendung sah sie gar
|
|
nicht an. Beides steht jetzt hier als Probe. */
|
|
const probe = (code) => utcTage(code).length;
|
|
ok(probe('const h = new Date().toISOString().slice(0, 10);') === 1,
|
|
"Gegenprobe: die direkte Schreibweise faellt auf");
|
|
ok(probe('const jetzt = () => new Date().toISOString();\nconst h = jetzt().slice(0, 10);') === 1,
|
|
" die ueber eine FUNKTION ebenfalls — genau die, die heute Nacht durchrutschte");
|
|
ok(probe('const jetzt = new Date().toISOString();\nconst h = jetzt.slice(0, 10);') === 1,
|
|
" und die ueber eine Variable");
|
|
ok(probe('const jetzt = () => new Date().toISOString();\n'
|
|
+ 'const iso = jetzt();\nconst h = iso.slice(0, 10);') === 1,
|
|
" auch ueber zwei Schritte");
|
|
ok(probe('const g = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);') === 1,
|
|
" und eine Rechnung auf Date.now()");
|
|
|
|
ok(probe('const jetzt = new Date().toISOString();') === 0,
|
|
"Gegenprobe: ein voller Zeitstempel ohne Schnitt schlaegt NICHT an");
|
|
ok(probe('const b = new Date(Date.parse(tag + "T12:00:00Z") + n * 86400000)'
|
|
+ '.toISOString().slice(0, 10);') === 0,
|
|
" und die Rechnung auf UTC-Mittag ebenfalls nicht — die ist richtig so");
|
|
ok(probe('const alsText = (d) => d.toISOString().slice(0, 10);') === 0,
|
|
" und ein Date, das von aussen kommt, auch nicht");
|
|
}
|
|
|
|
/* =======================================================================
|
|
WIRD DAS PROTOKOLL RICHTIG HERUM GERUFEN?
|
|
|
|
`protokolliere(aktion, { personId, rolle, detail, ip })` -- die
|
|
Aktion zuerst, alles Weitere benannt.
|
|
|
|
Aufgerufen wurde es an 17 Stellen andersherum:
|
|
`protokolliere(req.person.id, "aktion", detail, ip)`. JavaScript
|
|
beschwert sich darueber NICHT. Das zweite Argument ist ein Text,
|
|
und einen Text zu zerlegen ergibt lauter `undefined`. Der Eintrag
|
|
entsteht also -- nur steht die Personen-Kennung im Feld `aktion`,
|
|
und Person, Detail und IP sind leer.
|
|
|
|
AUF DEM ECHTEN SERVER NACHGEMESSEN: 39 Zeilen mit Aktionen wie
|
|
„16.0" und „4.0". Betroffen war unter anderem jede Aenderung an
|
|
Dogi-Media und jede Massnahme im Live-Chat -- also ausgerechnet
|
|
das, wofuer es ein Protokoll gibt.
|
|
|
|
Ein Kommentar haette das nicht verhindert (11.09.2026: „Ein
|
|
Kommentar, der vor einem Fehler warnt, verhindert ihn nicht").
|
|
Eine Pruefung schon.
|
|
======================================================================= */
|
|
console.log(`\n=== Wird das Protokoll richtig herum gerufen? ===`);
|
|
{
|
|
/* Das erste Argument muss etwas sein, das eine Aktion SEIN KANN:
|
|
eine Zeichenkette oder ein Ausdruck, der eine liefert (`a ? "x" :
|
|
"y"`). Was es nicht sein darf, ist eine Kennung -- und die
|
|
erkennt man daran, dass sie auf `.id` endet oder schlicht
|
|
`personId` heisst. */
|
|
const schlecht = [];
|
|
let gesehen = 0;
|
|
for (const datei of readdirSync(SERVER).filter((n) => n.endsWith(".js"))) {
|
|
const text = readFileSync(join(SERVER, datei), "utf8");
|
|
for (const m of text.matchAll(/\bprotokolliere\(\s*([^,)]{1,60})/g)) {
|
|
const erstes = m[1].trim();
|
|
if (erstes.startsWith("aktion")) continue; // die Erklaerung selbst
|
|
gesehen++;
|
|
if (/\.id\s*$/.test(erstes) || /^personId$/.test(erstes)
|
|
|| /^\d/.test(erstes)) {
|
|
const zeile = text.slice(0, m.index).split("\n").length;
|
|
schlecht.push(`${datei}:${zeile} -> ${erstes}`);
|
|
}
|
|
}
|
|
}
|
|
ok(gesehen > 60, `${gesehen} Protokollaufrufe im Haus gefunden`);
|
|
ok(schlecht.length === 0,
|
|
schlecht.length
|
|
? `FALSCH HERUM: ${schlecht.slice(0, 6).join(" | ")}`
|
|
: "alle rufen die Aktion zuerst");
|
|
|
|
/* GEGENPROBE: Kann die Pruefung „nein" sagen? Ohne sie waere sie
|
|
eine, die immer bestaetigt -- und die bestaetigt nichts. */
|
|
const probe = 'protokolliere(req.person.id, "test", "x");';
|
|
const trifft = [...probe.matchAll(/\bprotokolliere\(\s*([^,)]{1,60})/g)]
|
|
.some((m) => /\.id\s*$/.test(m[1].trim()));
|
|
ok(trifft, "Gegenprobe: ein falsch herum gebauter Aufruf wird erkannt");
|
|
const gut = 'protokolliere("test", { personId: req.person.id });';
|
|
const trifftNicht = [...gut.matchAll(/\bprotokolliere\(\s*([^,)]{1,60})/g)]
|
|
.some((m) => /\.id\s*$/.test(m[1].trim()));
|
|
ok(!trifftNicht, "Gegenprobe: ein richtiger Aufruf schlaegt 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");
|
|
|
|
/* =====================================================================
|
|
RUFT JEMAND `openssl` WIEDER DIREKT AUF?
|
|
|
|
WAS HIER SCHIEFGING (29.09.2026 gemessen): Im naechtlichen Lauf
|
|
standen DREIZEHN Pruefungen mit derselben Zeile rot --
|
|
|
|
[uncaughtException] Error: spawnSync openssl ENOENT
|
|
|
|
Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau
|
|
(der Sitzungskeks ist `secure`), und dafuer erzeugen sie ein
|
|
Wegwerf-Zertifikat. Auf diesem Rechner liegt openssl in
|
|
`Git\\mingw64\\bin`; im System-PATH steht aber nur `Git\\cmd`. Wer
|
|
aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas --
|
|
die Aufgabenplanung startet `node.exe` direkt und bekommt sie
|
|
nicht.
|
|
|
|
Bei mir gruen, nachts rot, und der Grund steht nicht im Code. Die
|
|
dreizehn roten Zeilen haben dabei monatelang die echten Befunde
|
|
zugedeckt.
|
|
|
|
`helfer-openssl.mjs` sucht das Programm jetzt auch ausserhalb des
|
|
PATH. Diese Wache sorgt dafuer, dass der direkte Aufruf nicht
|
|
zurueckkommt -- der naechste, der ihn schreibt, merkt es hier und
|
|
nicht erst in einem Nachtlauf, den niemand liest. */
|
|
console.log(`\n=== Ruft noch jemand openssl direkt auf? ===`);
|
|
{
|
|
const direkt = [];
|
|
let ueberHelfer = 0;
|
|
for (const datei of readdirSync(SERVER).filter((n) => /\.(mjs|js)$/.test(n))) {
|
|
if (datei === "helfer-openssl.mjs") continue;
|
|
const text = readFileSync(join(SERVER, datei), "utf8");
|
|
for (const m of text.matchAll(/exec(?:File)?Sync\(\s*["'`]openssl/g)) {
|
|
direkt.push(`${datei}:${text.slice(0, m.index).split("\n").length}`);
|
|
}
|
|
if (/zertifikatBauen\(/.test(text)) ueberHelfer++;
|
|
}
|
|
ok(ueberHelfer > 20,
|
|
`${ueberHelfer} Dateien bauen ihr Zertifikat ueber den Helfer`);
|
|
ok(direkt.length === 0,
|
|
direkt.length
|
|
? `DIREKT: ${direkt.slice(0, 6).join(" | ")} -- bitte zertifikatBauen() aus `
|
|
+ "helfer-openssl.mjs nehmen, sonst faellt es im Nachtlauf um"
|
|
: "keine Datei ruft openssl mehr am PATH vorbei");
|
|
|
|
/* GEGENPROBE IN BEIDE RICHTUNGEN. Eine Wache, die nur „nichts
|
|
gefunden" sagen kann, hat nichts bewiesen.
|
|
|
|
DIE PROBETEXTE WERDEN ZUSAMMENGESETZT und stehen nicht am Stueck
|
|
da. Beim ersten Anlauf schlug diese Wache auf ihre EIGENEN
|
|
Gegenproben an: Sie liest den Quelltext aller Serverdateien,
|
|
und dazu gehoert diese hier. Dasselbe ist mir heute schon
|
|
zweimal passiert -- eine Pruefung, die ihre eigene Begruendung
|
|
als Fund meldet, ist der haeufigste Selbsttreffer im Haus. */
|
|
const findet = (t) => [...t.matchAll(/exec(?:File)?Sync\(\s*["'`]openssl/g)].length;
|
|
const O = "openssl";
|
|
ok(findet(`execFileSync("${O}", ["version"]);`) === 1,
|
|
"Gegenprobe: ein direkter Aufruf wird erkannt");
|
|
ok(findet(`execSync(\`${O} version\`);`) === 1,
|
|
"auch mit Schraegstrich-Anfuehrung");
|
|
ok(findet("zertifikatBauen(k, z, CREW);") === 0,
|
|
"und der Weg ueber den Helfer gilt nicht als Fund");
|
|
}
|
|
|
|
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
|
|
process.exit(fehler ? 1 : 0);
|