Filipe: "ich will das alles auch so perfekt dass ich es ganz einfach
und easy mit obs oder mit tiktok studio verbinden kann. also so dass
man dan nur die kamera und das video sieht."
WARUM OHNE ANMELDUNG -- nachgesehen, nicht angenommen
OBS speichert die Anmeldung einer Browser-Quelle NICHT zuverlaessig;
im OBS-Forum stehen dazu Meldungen bis in die aktuelle Fassung 31.
Eine Quelle, bei der man sich nach jedem Programmstart neu anmelden
muss, ist mitten in einer Sendung unbrauchbar.
Deshalb ein SCHLUESSEL in der Adresse -- derselbe Weg, den jedes
Alert-Werkzeug im Netz geht. 32 Byte aus dem Zufall des Systems,
verglichen wird zeitgleich (`timingSafeEqual`): Ein gewoehnlicher
Vergleich bricht beim ersten falschen Zeichen ab, und aus den
Bruchteilen einer Millisekunde laesst sich ein Schluessel Zeichen
fuer Zeichen erraten.
DREI QUELLEN, WEIL DREI DINGE VERSCHIEDEN SIND
buehne.html Das laufende YouTube-Video, auf die Sekunde genau wie
bei allen anderen. Stumm (der Ton kommt aus dem
Mischpult) und ohne jede Bedienung -- was hier zu
sehen ist, geht in den Stream.
DIE EIGENE KAMERA IST ABSICHTLICH NICHT DRIN. Sie ist
in OBS direkt als Geraet verfuegbar, in besserer
Qualitaet und frei in Groesse und Lage -- genau das,
was Filipe will ("meine kamera groesser machen video
kleiner"). Den Umweg ueber den Browser zu nehmen
hiesse, Qualitaet gegen nichts einzutauschen und die
Groesse festzulegen statt sie freizugeben.
tafel.html Nur die Spendenkarten, auf DURCHSICHTIGEM Grund.
Groesse und Lage stehen in der Adresse (`&g=1.6`,
`&pos=or`): Wer in OBS eine Quelle einrichtet, hat die
Adresse ohnehin vor sich -- ein Wert, den man
stattdessen im Regiepult suchen muesste, waere ein
Fensterwechsel mitten im Einrichten. Alles rechnet in
`rem`, ein Wert nimmt Schrift, Bild und Polsterung
gleichmaessig mit.
Buehnenmodus `reaktion.html?nur=buehne` -- dieselbe Seite, nur ohne
alles Bedienbare. Fuer den Fall, dass GAESTE im Bild
sind: Deren Kameras kommen ueber eine
Direktverbindung an, und die braucht eine angemeldete
Seite. Diese eine wird als Fenster aufgenommen.
ES IST DIESELBE SEITE UND NICHT EINE ZWEITE. Eine
eigene muesste Video, Kameras, Verbindungsaufbau und
Nachfuehrung noch einmal enthalten -- und beim
naechsten Umbau saehe eine von beiden anders aus.
WAS HERAUSKOMMT, IST DIE EIGENTLICHE FRAGE
Wer den Schluessel hat, sieht genau das, was ohnehin im Stream
steht: Video, Stand, Sekunde, Titel -- und die Spendenkarten. Kein
Chat, keine Namen von Zusehenden, keine Zahlen ueber das Haus. Die
Pruefung zaehlt die Felder der Auskunft EINZELN auf und weist jedes
verbotene namentlich nach; eine Auskunft, die "ungefaehr das
Richtige" enthaelt, ist bei einem Weg ohne Anmeldung keine.
Ein neuer Schluessel macht die alten Adressen sofort tot -- und
schliesst die laufenden Quellen. Sonst liefe eine mit dem alten
weiter, obwohl er zurueckgezogen ist, und man haelt sich fuer
sicher, ohne es zu sein.
SIE MUESSEN TAGE LAUFEN, OHNE DASS JEMAND HINSIEHT
Das ist der Unterschied zu einer Seite im Browser: Wer eine Seite
offen hat, merkt, wenn sie haengt. Eine Quelle in OBS laeuft im
Hintergrund, und ein Stillstand faellt erst auf, wenn die erste
Spende nicht erscheint -- mitten in der Sendung. Deshalb ein
Lebenszeichen alle 25 Sekunden, eine eigene Wache (70 Sekunden ohne
alles = neu verbinden) und ein sofortiger Neuaufbau, wenn der
Rechner aus dem Ruhezustand kommt.
Und: Ein Fehler wird angezeigt, aber nur der, der etwas bedeutet --
ein falscher Schluessel. Alles andere bleibt still, weil jede
Flaeche hier im Stream zu sehen waere.
ZWEI EIGENE FEHLER, BEIDE VON DER MESSUNG GEFUNDEN
1. Die Quellen kamen mit 401 zurueck, obwohl die Seiten laengst
geladen waren: `aufgabenRouter` haengt eine Schranke ueber ALLE
Pfade unter /workspace/api. Genau dafuer stehen `sicherungRouter`
und der Weg fuers Profilbild schon davor -- die Buehne ist der
dritte Fall derselben Art und steht jetzt dort.
2. `waitUntil: "networkidle"` auf einer Seite mit Ereignisstrom. Der
Strom endet absichtlich nie; die Messung wartete auf einen
Zustand, der nicht eintreten kann, und brach nach 30 Sekunden ab.
Dieselbe Falle wie am 06.09. beim Regressionslauf.
ZWEI PRUEFUNGEN WURDEN DABEI GENAUER
`pruef-struktur` verlangte von den zwei OBS-Quellen ein Symbol fuer
den Startbildschirm, ein Manifest und eine Leistenfarbe. Sie
laufen in einem Programmfenster und werden nie installiert -- sie
fallen aus dieser Frage heraus, benannt und mit Grund.
Die Namensstreit-Regel zaehlte jede Klasse, die irgendwo in einem
Selektor vorkommt. Damit galt auch
`body[data-nur="buehne"] .kopfleiste { display: none }` als eigene
Klasse -- dabei ist das das Gegenteil: eine absichtliche
Bezugnahme, um sie im Buehnenmodus wegzunehmen. Gezaehlt wird
jetzt nur, was am ANFANG einer Regel steht, also als eigenes
Bauteil gemeint ist. Mit Gegenprobe in beide Richtungen -- sonst
haette ich eine Regel nur so lange geschaerft, bis sie schweigt.
GEMESSEN
mess-buehne (neu) Ein Browserfenster OHNE jeden Keks: beide
Quellen arbeiten, 0 Kekse, Grund durchsichtig
(rgba(0,0,0,0)), Karte laeuft an (25 EUR,
Rudel-Legende, 348x178). Falscher Schluessel: kein
Inhalt, Grund im Bild. Neuer Schluessel: alter 404,
neuer 200. Buehnenmodus: Kopf, Chat, Pult und
Schild weg, Leinwand da, Saal 720 von 720.
pruef-buehne (neu) 36 Punkte, 0 Fehler
pruef-reaktion 156 (war 154), spenden 46, haus-trennung 100,
haus-seiten 38, struktur 35, css-klassen 33,
verborgen 25, rechtetafel 19, portnummern 15,
ports 8 -- alle 0 Fehler.
845 lines
39 KiB
JavaScript
845 lines
39 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 ------------------------------------------ */
|
|
|
|
/* 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 = "";
|
|
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);
|