Zwei Wuensche vom 05.09.2026, dazu drei Fehler, die dabei ans Licht kamen.
ABBRECHEN (Wunsch: "in jedem status die aufgaben auch abbrechen koennen,
nur ich die manager und scouts")
Neuer Status mit Pflicht-Grund, aus jedem der vier Status heraus.
Festgehalten wird auch, WO die Aufgabe stand -- "im Review abgebrochen"
ist eine andere Aussage als "nie angefangen", und das Wiederaufnehmen
geht dorthin zurueck statt nach "offen".
Eigener Weg statt "abgebrochen" in der Statusliste: Dort entscheidet
darfAendern(), und das laesst auch den zustaendigen Creator aendern.
Der gewoehnliche PATCH kann diesen Zustand deshalb gar nicht erreichen
-- auch nicht fuer DogFather, sonst waere die Grund-Pflicht umgehbar.
Abgebrochenes steht in einem zugeklappten Bereich unter dem Brett, nicht
als fuenfte Spalte: Am Handy waeren dann alle fuenf unlesbar schmal.
Verschwinden darf es nicht, sonst waere der Abbruch ein Loeschen mit
Zwischenschritt.
Die Tabelle musste dafuer getauscht werden (SQLite kann CHECK nicht
aendern). Vorher auf einer Kopie durchgespielt: 40 von 40 Aufgaben,
Inhalte, Verweise und Indizes geprueft, Gegenprobe zeigt, dass der CHECK
noch lebt.
TEILNEHMERWAHL (Wunsch: "das soll viel besser aussehen und fuer jeden
verfuegbar sein")
Auf dem Bildschirm klebten die Namen aneinander: "DogfatherDogFather".
Ursache war, dass kalender.html das Stylesheet mit diesen Klassen nie
eingebunden hat -- sie standen in dateien.css. Vierzig gruene Pruefungen
zur Teilnehmerwahl hatten das nicht gemerkt, weil keine je gefragt hat,
ob es AUSSIEHT wie gedacht.
Jetzt eigene Klassen im eigenen Stylesheet, nach Rollen gruppiert: Die
Rolle steht einmal als Ueberschrift statt neunmal am Namen. Damit ist
das Kleben an der Wurzel weg, nicht zugepflastert.
"Fuer jeden" war mehr als ein hidden zu entfernen: darfEintragen() haette
einem Creator nur sich selbst erlaubt. Er haette seinen Scout gesehen,
angeklickt, und der Server haette ihn still weggelassen -- ein Knopf, der
nichts tut. einladbareIds() schaut jetzt in beide Richtungen, bewusst
getrennt von /api/personen: Wer die erweitert, gibt einem Creator
nebenbei die Moeglichkeit, seinem Scout Aufgaben zuzuweisen.
BENACHRICHTIGUNGEN (Wunsch: "sowas, und dass es perfekt funktioniert
fuer jeden")
Web Push nach RFC 8291/8292, ohne fremde Abhaengigkeit. Der Knopf sagt
in jeder Lage die Wahrheit, auch die unbequemen: abgelehnt (mit dem
Hinweis, wo man es zuruecknimmt), iPhone im Reiter (mit Anleitung),
Browser ohne Push. Ein Knopf, der bei abgelehnter Berechtigung nur
nichts tut, ist der sichere Weg zu "das funktioniert nicht".
DREI FEHLER, DIE DABEI AUFFIELEN
1. Ein defekter Zugangsdatensatz sperrte ALLE einer Rolle aus. Wirft
hashe() bei einer Person, flog die ganze Anmeldung in den catch: 503
"nicht verfuegbar" fuer jeden mit dieser Rolle. Aufgefallen durch
einen eigenen Testfehler. Jetzt wird die defekte Person uebersprungen
und laut protokolliert; die Gegenprobe zeigt, dass ein falscher Code
weiterhin abgelehnt wird.
2. Die Glocke sprengte die Kopfleiste -- zweimal. Bei 320 px lag die
Lupe des Suchknopfes auf dem Sicht-Umschalter (ein Knopf, der auf 12
Seiten ins Leere tippt), bei 768 px wurde der Abmelden-Knopf bis zu
15 px aus dem Bild geschoben, weil die Textgrenze auf 760 stand und
ein Tablet 768 hat. Nachgewiesen durch Messen mit und ohne Glocke,
nicht durch Vermuten.
3. .block__frage war viermal gestaltet und stand auf einer Seite, die
keine dieser Dateien laedt -- derselbe Fehler wie bei der
Teilnehmerwahl. Gefunden von der neuen Klassenpruefung beim ersten
Lauf.
NEUE PRUEFUNGEN
pruef-css-klassen jede gestaltete Klasse muss auf ihrer Seite ankommen
(unterscheidet Struktur-Anker von echtem Verlust)
pruef-dabei-optik die Wahl im Browser, an den echten Pixeln
pruef-abbrechen Umstellung auf einer Kopie, Rechte, Rueckweg
pruef-abbrechen-optik Knopf, Dialog, Bereich, Handy
pruef-glocke Zustaende, An/Abmelden, jede Rolle
pruef-push(-weg) Rechnung gegen die RFC-Vektoren, Zustellung
pruef-struktur prueft jetzt zusaetzlich, ob sich jedes Server-Modul als
ESM laden laesst. node --check auf einer .js-Datei prueft als CommonJS
und meldete "ok", waehrend der Import scheiterte.
Gesamtlauf: 55 von 55 Dateien, 1973 von 1973 Punkten.
Was NICHT geprueft werden konnte und deshalb dasteht: Der Schritt
"Browser holt eine Adresse beim Push-Dienst" braucht eine Verbindung zu
Googles FCM, die ein Pruef-Browser nicht hat. Die Pruefung misst das
zuerst und meldet es als dritten Ausgang, statt gruen zu sein.
Verschluesselung und Zustellung sind getrennt geprueft; diese eine
Strecke beweist sich erst auf dem Server.
Co-Authored-By: Claude Opus 5 <[email protected]>
505 lines
22 KiB
JavaScript
505 lines
22 KiB
JavaScript
/* STRUKTURPRUEFUNG — findet Kaputtes, das kein Browser meldet.
|
|
|
|
Auftrag vom 04.09.2026: vollstaendiger Audit des Creator Workspace.
|
|
|
|
WAS DIESE PRUEFUNG SUCHT — drei Sorten stiller Fehler:
|
|
|
|
1. Eine Seite laedt eine Datei, die es nicht gibt.
|
|
Der Browser meldet das nur in der Konsole; die Seite sieht
|
|
einfach etwas anders aus. Bei CSS faellt es auf, bei einem
|
|
einzelnen JS-Baustein oft monatelang nicht.
|
|
|
|
2. Ein Verweis fuehrt ins Leere.
|
|
Eine Kachel, ein Zurueck-Knopf, ein Menuepunkt — Ziel existiert
|
|
nicht. Erst der Klick zeigt es.
|
|
|
|
3. Die Oberflaeche ruft eine Schnittstelle auf, die der Server
|
|
nicht (mehr) hat.
|
|
Das ist der gefaehrlichste Fall: Der Knopf ist da, er tut nur
|
|
nichts. Genau so verschwindet eine Funktion still, wenn eine
|
|
Route umbenannt wird.
|
|
|
|
DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Faende das Skript keine einzige
|
|
Seite, waere "0 kaputte Verweise" das beste Ergebnis ueberhaupt --
|
|
und voellig wertlos. Deshalb wird jede Menge gezaehlt und eine leere
|
|
Menge ist ein Fehler.
|
|
|
|
GEGENPROBE am Ende: ein absichtlich falscher Verweis MUSS gemeldet
|
|
werden. Eine Pruefung, die immer bestaetigt, bestaetigt nichts. */
|
|
|
|
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
import { join, dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = join(__dirname, "..");
|
|
const WS = join(REPO, "workspace");
|
|
const SERVER = __dirname;
|
|
|
|
let fehler = 0;
|
|
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
/* ---------- Bestand einsammeln ------------------------------------------ */
|
|
|
|
const seiten = readdirSync(WS).filter((n) => n.endsWith(".html"));
|
|
console.log(`\n=== Bestand ===`);
|
|
ok(seiten.length >= 10, `${seiten.length} Workspace-Seiten gefunden`);
|
|
|
|
const serverDateien = readdirSync(SERVER).filter((n) => /^workspace.*\.js$/.test(n));
|
|
ok(serverDateien.length >= 20, `${serverDateien.length} Server-Module gefunden`);
|
|
|
|
/* Alle Routen, die der Server wirklich anbietet. Gelesen wird der Code,
|
|
nicht eine gepflegte Liste -- eine Liste altert, der Code nicht. */
|
|
const ROUTEN = [];
|
|
for (const datei of serverDateien) {
|
|
const text = readFileSync(join(SERVER, datei), "utf8");
|
|
const treffer = text.matchAll(
|
|
/Router\.(get|post|patch|put|delete|use)\(\s*\n?\s*"(\/workspace\/[^"]*)"/g);
|
|
for (const t of treffer) ROUTEN.push({ art: t[1], pfad: t[2], datei });
|
|
}
|
|
ok(ROUTEN.length >= 80, `${ROUTEN.length} Server-Routen eingelesen`);
|
|
|
|
/** Ein Pfad aus der Oberflaeche gegen die Routenliste halten.
|
|
* Platzhalter (:id) und eingesetzte Werte (${...}) gelten als gleich.
|
|
*
|
|
* WICHTIG — .use() zaehlt NICHT als Gegenstueck. Diese Eintraege sind
|
|
* Schranken ("jeder Pfad unter /workspace/api braucht eine Anmeldung"),
|
|
* keine Endpunkte. Sie mitzuzaehlen war der erste Entwurf, und die
|
|
* Gegenprobe hat ihn sofort ueberfuehrt: /workspace/api/gibtesnicht
|
|
* galt als vorhanden, weil aufgabenRouter.use("/workspace/api", …)
|
|
* darueber liegt. Damit haette diese ganze Pruefung nie einen einzigen
|
|
* toten Aufruf gefunden und trotzdem gruen gemeldet. */
|
|
function routeGibtEs(pfad, art) {
|
|
const teileA = pfad.split("/").filter(Boolean);
|
|
return ROUTEN.some((r) => {
|
|
if (r.art === "use") return false;
|
|
if (art && r.art !== art) return false;
|
|
const teileB = r.pfad.split("/").filter(Boolean);
|
|
if (teileA.length !== teileB.length) return false;
|
|
return teileB.every((t, i) => t.startsWith(":") || t === "*" || t === teileA[i]);
|
|
});
|
|
}
|
|
|
|
/* ---------- 1. Geladene Dateien ----------------------------------------- */
|
|
|
|
console.log(`\n=== Geladene Dateien (script/link/img) ===`);
|
|
let geprueftAssets = 0;
|
|
for (const seite of seiten) {
|
|
const text = readFileSync(join(WS, seite), "utf8");
|
|
const quellen = [
|
|
...text.matchAll(/<script[^>]+src="([^"]+)"/g),
|
|
...text.matchAll(/<link[^>]+href="([^"]+\.css[^"]*)"/g),
|
|
...text.matchAll(/<img[^>]+src="([^"]+)"/g),
|
|
].map((m) => m[1]);
|
|
for (const roh of quellen) {
|
|
if (/^(https?:|data:|\/\/)/.test(roh)) continue;
|
|
const ohneStempel = roh.split("?")[0];
|
|
const pfad = ohneStempel.startsWith("/")
|
|
? join(REPO, ohneStempel)
|
|
: resolve(WS, ohneStempel);
|
|
geprueftAssets++;
|
|
if (!existsSync(pfad)) ok(false, `${seite}: Datei fehlt -> ${roh}`);
|
|
}
|
|
}
|
|
ok(geprueftAssets > 0, `${geprueftAssets} geladene Dateien geprueft, alle vorhanden`);
|
|
|
|
/* ---------- 2. Verweise auf andere Seiten -------------------------------- */
|
|
|
|
console.log(`\n=== Verweise (href) ===`);
|
|
let geprueftLinks = 0;
|
|
const quellenMitLinks = [
|
|
...seiten.map((n) => ({ name: n, pfad: join(WS, n) })),
|
|
...readdirSync(join(WS, "assets", "js"))
|
|
.filter((n) => n.endsWith(".js"))
|
|
.map((n) => ({ name: "assets/js/" + n, pfad: join(WS, "assets", "js", n) })),
|
|
];
|
|
for (const { name, pfad: quelle } of quellenMitLinks) {
|
|
const text = readFileSync(quelle, "utf8");
|
|
/* Beide Schreibweisen: absolut (/workspace/aufgaben.html) und relativ
|
|
("aufgaben.html"). Der erste Entwurf suchte nur die absolute Form
|
|
und meldete deshalb "0 Verweise geprueft" -- die Seiten verlinken
|
|
aber fast ausschliesslich relativ. Eine Pruefung, die nichts
|
|
findet, prueft nichts; deshalb steht die Anzahl unten in der
|
|
Bedingung. */
|
|
const ziele = [
|
|
...[...text.matchAll(/href\s*=\s*["'`](\/workspace\/[^"'`#?${]*)/g)].map((m) => m[1]),
|
|
...[...text.matchAll(/(?:location\.href|location\.assign\()\s*=?\s*["'`](\/workspace\/[^"'`#?${]*)/g)].map((m) => m[1]),
|
|
...[...text.matchAll(/href\s*=\s*["'`]([a-z0-9-]+\.html)/g)].map((m) => "/workspace/" + m[1]),
|
|
...[...text.matchAll(/(?:location\.href\s*=|location\.assign\()\s*["'`]([a-z0-9-]+\.html)/g)].map((m) => "/workspace/" + m[1]),
|
|
];
|
|
for (const ziel of ziele) {
|
|
if (ziel.startsWith("/workspace/api/")) continue;
|
|
geprueftLinks++;
|
|
const rel = ziel.replace(/^\/workspace\/?/, "");
|
|
const datei = rel === "" ? "index.html" : rel;
|
|
const voll = join(WS, datei);
|
|
if (!existsSync(voll)) ok(false, `${name}: Verweis ins Leere -> ${ziel}`);
|
|
}
|
|
}
|
|
ok(geprueftLinks > 0, `${geprueftLinks} Verweise geprueft, alle mit Ziel`);
|
|
|
|
/* ---------- 3. Aufgerufene Schnittstellen -------------------------------- */
|
|
|
|
console.log(`\n=== Aufgerufene Schnittstellen ===`);
|
|
let geprueftApi = 0;
|
|
let nurPraefix = 0;
|
|
const jsDateien = readdirSync(join(WS, "assets", "js")).filter((n) => n.endsWith(".js"));
|
|
for (const name of [...jsDateien.map((n) => "assets/js/" + n), ...seiten]) {
|
|
const text = readFileSync(join(WS, name), "utf8");
|
|
/* Jede Zeichenkette, die mit /workspace/api/ beginnt -- egal ob in
|
|
fetch(), in einer Hilfsfunktion oder in einer Variablen. */
|
|
for (const m of text.matchAll(/["'`](\/workspace\/api\/[^"'`\s]*)/g)) {
|
|
const roh = m[1].replace(/\$\{[^}]*\}/g, "1").split("?")[0];
|
|
if (!roh || roh === "/workspace/api" || roh === "/workspace/api/") continue;
|
|
|
|
/* DER DRITTE AUSGANG. Endet die Zeichenkette auf "/", wurde im Code
|
|
per + noch etwas angehaengt ('/workspace/api/ki/' + pfad). Was
|
|
dort steht, weiss nur die Laufzeit -- statisch ist der volle Pfad
|
|
NICHT zu ermitteln.
|
|
|
|
Der erste Entwurf schnitt den Schraegstrich einfach weg und
|
|
meldete vier Fehler, die keine waren. Genau davor warnt die
|
|
eigene Regel: eine Pruefung, die ihre Voraussetzung nicht hat,
|
|
muss das SAGEN und nicht raten. Geprueft wird deshalb nur, dass
|
|
es unter diesem Praefix ueberhaupt Routen gibt -- die vollen
|
|
Pfade deckt der Rundgang im Browser ab. */
|
|
if (roh.endsWith("/")) {
|
|
const praefix = roh.slice(0, -1);
|
|
nurPraefix++;
|
|
const gibtEsDarunter = ROUTEN.some(
|
|
(r) => r.art !== "use" && r.pfad.startsWith(praefix + "/"));
|
|
if (!gibtEsDarunter) {
|
|
ok(false, `${name}: unter ${praefix}/ gibt es gar keine Schnittstelle`);
|
|
}
|
|
continue;
|
|
}
|
|
geprueftApi++;
|
|
if (!routeGibtEs(roh)) ok(false, `${name}: Schnittstelle gibt es nicht -> ${roh}`);
|
|
}
|
|
}
|
|
ok(geprueftApi >= 40, `${geprueftApi} vollstaendige Aufrufe geprueft, alle mit Gegenstueck`);
|
|
ok(nurPraefix > 0, `${nurPraefix} zusammengesetzte Aufrufe: nur Praefix pruefbar (Rest deckt der Browser-Rundgang)`);
|
|
|
|
/* ---------- 4. Jede Seite ist erreichbar --------------------------------- */
|
|
|
|
console.log(`\n=== Erreichbarkeit ===`);
|
|
const verlinkt = new Set(["index.html", "start.html"]);
|
|
for (const { pfad: quelle } of quellenMitLinks) {
|
|
const text = readFileSync(quelle, "utf8");
|
|
for (const m of text.matchAll(/["'`](?:\/workspace\/)?([a-z0-9-]+\.html)/g)) {
|
|
verlinkt.add(m[1]);
|
|
}
|
|
}
|
|
for (const seite of seiten) {
|
|
if (!verlinkt.has(seite)) ok(false, `Seite ${seite} ist von nirgendwo verlinkt`);
|
|
}
|
|
ok(verlinkt.size >= seiten.length, `alle ${seiten.length} Seiten sind verlinkt`);
|
|
|
|
/* ---------- 5. Versionsstempel ------------------------------------------- */
|
|
|
|
/* Ohne Stempel haelt Cloudflare bis zu vier Stunden die alte Fassung --
|
|
dann sieht niemand die Aenderung, und man sucht den Fehler im Code. */
|
|
console.log(`\n=== Versionsstempel an JS und CSS ===`);
|
|
let ohneStempel = [];
|
|
for (const seite of seiten) {
|
|
const text = readFileSync(join(WS, seite), "utf8");
|
|
for (const m of text.matchAll(/(?:src|href)="(\/workspace\/assets\/(?:js|css)\/[^"]+)"/g)) {
|
|
if (!m[1].includes("?v=")) ohneStempel.push(`${seite}: ${m[1]}`);
|
|
}
|
|
}
|
|
ok(ohneStempel.length === 0,
|
|
ohneStempel.length ? `ohne Versionsstempel: ${ohneStempel.slice(0, 8).join(", ")}`
|
|
: "jede JS- und CSS-Einbindung traegt einen Stempel");
|
|
|
|
/* ---------- 6. Reste aus der Entwicklung --------------------------------- */
|
|
|
|
console.log(`\n=== Produktionsreste ===`);
|
|
const resteMuster = [
|
|
[/console\.log\(/g, "console.log"],
|
|
[/\bdebugger\b/g, "debugger"],
|
|
[/\bTODO\b|\bFIXME\b|\bHACK\b/g, "TODO/FIXME"],
|
|
[/localhost:\d+/g, "localhost-Adresse"],
|
|
[/Lorem ipsum/gi, "Platzhaltertext"],
|
|
];
|
|
let reste = [];
|
|
for (const name of [...jsDateien.map((n) => "assets/js/" + n), ...seiten]) {
|
|
const text = readFileSync(join(WS, name), "utf8");
|
|
/* Kommentare zaehlen nicht -- dort steht Begruendung, kein Code. */
|
|
const ohneKommentar = text
|
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
.replace(/^\s*\/\/.*$/gm, "")
|
|
.replace(/<!--[\s\S]*?-->/g, "");
|
|
for (const [muster, was] of resteMuster) {
|
|
const t = ohneKommentar.match(muster);
|
|
if (t) reste.push(`${name}: ${was} (${t.length}x)`);
|
|
}
|
|
}
|
|
ok(reste.length === 0, reste.length ? reste.join(" · ") : "keine Entwicklungsreste im Auslieferstand");
|
|
|
|
/* ---------- 7. Groesse der Assets ---------------------------------------- */
|
|
|
|
console.log(`\n=== Dateigroessen ===`);
|
|
let schwer = [];
|
|
for (const unter of ["assets/js", "assets/css", "assets/img"]) {
|
|
const ordner = join(WS, unter);
|
|
if (!existsSync(ordner)) continue;
|
|
for (const n of readdirSync(ordner)) {
|
|
const p = join(ordner, n);
|
|
if (statSync(p).isDirectory()) continue;
|
|
const kb = Math.round(statSync(p).size / 1024);
|
|
if (kb > 200) schwer.push(`${unter}/${n} ${kb} KB`);
|
|
}
|
|
}
|
|
ok(schwer.length === 0, schwer.length ? `ueber 200 KB: ${schwer.join(", ")}` : "keine Datei ueber 200 KB");
|
|
|
|
/* ---------- 8. Totes CSS ------------------------------------------------- */
|
|
|
|
/* start.css wird auf JEDER Seite geladen und ist 126 KB gross. Was darin
|
|
niemand mehr benutzt, laedt trotzdem jeder mit.
|
|
|
|
ZWEI BEKANNTE AUSNAHMEN, bewusst stehen gelassen: Beide stehen als
|
|
Glied einer Selektorliste (".a, .b, .code-feld { … }") und kosten je
|
|
rund fuenfzehn Byte. Sie zu entfernen braechte nichts und traegt das
|
|
kleine Risiko, dass die Klasse doch irgendwo zur Laufzeit gesetzt
|
|
wird. Sie stehen hier namentlich, damit NEUES totes CSS auffaellt --
|
|
eine Pruefung, die von Anfang an rot ist, wird weggeklickt.
|
|
|
|
DER DRITTE AUSGANG: Klassen, die zusammengesetzt werden koennen
|
|
('kachel--' + ton), zaehlen nicht als tot. Ihr Stamm wird gesucht;
|
|
findet er sich, gilt die Klasse als moeglicherweise dynamisch und
|
|
wird getrennt ausgewiesen statt geraten. */
|
|
console.log(`\n=== Totes CSS ===`);
|
|
const BEKANNT_TOT = [".content-wurzel", ".code-feld"];
|
|
{
|
|
const cssOrdner = join(WS, "assets", "css");
|
|
const jsOrdner = join(WS, "assets", "js");
|
|
let quelle = "";
|
|
for (const n of seiten) quelle += readFileSync(join(WS, n), "utf8");
|
|
for (const n of readdirSync(jsOrdner).filter((x) => x.endsWith(".js"))) {
|
|
quelle += readFileSync(join(jsOrdner, n), "utf8");
|
|
}
|
|
|
|
let gesamt = 0, dynamisch = 0;
|
|
const tot = [];
|
|
for (const datei of readdirSync(cssOrdner).filter((n) => n.endsWith(".css"))) {
|
|
const text = readFileSync(join(cssOrdner, datei), "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
for (const k of new Set([...text.matchAll(/\.([a-zA-Z][a-zA-Z0-9_-]{2,})/g)].map((m) => m[1]))) {
|
|
gesamt++;
|
|
if (quelle.includes(k)) continue;
|
|
const stamm = k.split(/--|__/)[0];
|
|
if (stamm !== k && quelle.includes(stamm)) { dynamisch++; continue; }
|
|
if (BEKANNT_TOT.includes("." + k)) continue;
|
|
tot.push(`${datei}: .${k}`);
|
|
}
|
|
}
|
|
ok(gesamt > 500, `${gesamt} CSS-Klassen geprueft (${dynamisch} moeglicherweise dynamisch gebaut)`);
|
|
ok(tot.length === 0,
|
|
tot.length ? `NEUES totes CSS: ${tot.slice(0, 8).join(" · ")}`
|
|
: `kein neues totes CSS (${BEKANNT_TOT.length} bekannte Reste stehen bewusst)`);
|
|
}
|
|
|
|
/* ---------- 9. Kommt jedes Manifest auch WIRKLICH an? --------------------
|
|
|
|
Ein Manifest kann fehlerfrei im Repo liegen und trotzdem nie beim
|
|
Browser ankommen -- wenn eine Zugangswand davorsteht und HTML statt
|
|
JSON zurueckgibt. Dann bietet der Browser "App installieren" gar
|
|
nicht erst an, und niemand merkt etwas: Die Datei ist ja da.
|
|
|
|
Genau das ist DREIMAL passiert (webdesign/app.webmanifest,
|
|
verwaltung.webmanifest, portal.webmanifest). Zweimal stand die
|
|
Begruendung danach im Code -- und beim dritten Mal wurde sie trotzdem
|
|
uebersehen. Ein Kommentar verhindert nichts; eine Pruefung schon.
|
|
|
|
Geprueft wird gegen die Ausnahmeliste in webdesign-gate.js: Jedes
|
|
Manifest unter /webdesign/ MUSS dort stehen. */
|
|
console.log(`
|
|
=== Manifeste hinter der Zugangswand ===`);
|
|
{
|
|
const tor = join(SERVER, "webdesign-gate.js");
|
|
const torText = existsSync(tor) ? readFileSync(tor, "utf8") : "";
|
|
ok(torText.length > 1000, "webdesign-gate.js gelesen");
|
|
|
|
const manifeste = [];
|
|
const suche = (ordner, vorn) => {
|
|
for (const n of readdirSync(ordner, { withFileTypes: true })) {
|
|
if (n.name.startsWith(".") || n.name === "node_modules") continue;
|
|
const rel = vorn ? `${vorn}/${n.name}` : n.name;
|
|
if (n.isDirectory()) { if (rel === "webdesign") suche(join(ordner, n.name), rel); continue; }
|
|
if (n.name.endsWith(".webmanifest") || n.name === "manifest.json") manifeste.push(rel);
|
|
}
|
|
};
|
|
suche(REPO, "");
|
|
|
|
const hinterWand = manifeste.filter((m) => m.startsWith("webdesign/"));
|
|
ok(hinterWand.length >= 2, `${hinterWand.length} Manifeste unter /webdesign/ gefunden`);
|
|
|
|
const fehlend = hinterWand.filter((m) => !torText.includes(`"/${m}"`));
|
|
ok(fehlend.length === 0,
|
|
fehlend.length
|
|
? `NICHT in der Ausnahmeliste von webdesign-gate.js: ${fehlend.join(", ")} `
|
|
+ `— der Browser bekaeme HTML statt JSON und boete "App installieren" nicht an`
|
|
: `alle ${hinterWand.length} stehen in der Ausnahmeliste`);
|
|
}
|
|
|
|
/* ---------- 10. Sind die Symbole fuer JEDES Geraet richtig gebaut? -------
|
|
|
|
PC und Handy holen ihr Symbol an drei verschiedenen Stellen, und
|
|
jede stellt andere Anforderungen:
|
|
|
|
Browser-Reiter das normale Symbol, darf runde Ecken haben
|
|
Android die maskable-Fassung, davon wird aussen bis zu
|
|
ein Fuenftel weggeschnitten
|
|
iPhone <link rel="apple-touch-icon"> -- und iOS rundet
|
|
SELBST ab und fuellt alles Durchsichtige mit
|
|
SCHWARZ
|
|
|
|
Der letzte Punkt ist der, der auffiel ("auf dem einen klappt es und
|
|
auf dem anderen nicht"): Ein apple-touch-icon mit eigener Rundung
|
|
bekommt auf dem iPhone schwarze Zipfel. Auf dem PC sieht dieselbe
|
|
Datei tadellos aus -- deshalb faellt es nur auf einem der beiden
|
|
Geraete auf, und deshalb muss es gemessen werden statt angesehen.
|
|
|
|
Geprueft werden die Dateinamen, nicht die Bilder selbst -- die
|
|
Bildpruefung braucht einen Browser und laeuft in pruef-symbole.mjs.
|
|
Hier geht es darum, dass ueberhaupt fuer jede App und jedes Geraet
|
|
eine Datei da ist. */
|
|
console.log(`
|
|
=== Symbole fuer alle Geraete ===`);
|
|
{
|
|
const symOrdner = join(REPO, "assets", "img", "app-symbole");
|
|
if (!existsSync(symOrdner)) {
|
|
ok(false, "der Ordner assets/img/app-symbole fehlt");
|
|
} else {
|
|
const da = readdirSync(symOrdner);
|
|
const apps = [...new Set(da.map((n) => n.replace(/-(\d+)(-maskable)?\.png$/, "")))]
|
|
.filter((n) => n.endsWith(".png") === false);
|
|
ok(apps.length >= 8, `${apps.length} Apps mit eigenen Symbolen`);
|
|
|
|
const noetig = ["192", "512", "192-maskable", "512-maskable", "180", "32"];
|
|
const fehlend = [];
|
|
for (const a of apps) {
|
|
for (const g of noetig) {
|
|
if (!da.includes(`${a}-${g}.png`)) fehlend.push(`${a}-${g}.png`);
|
|
}
|
|
}
|
|
ok(fehlend.length === 0,
|
|
fehlend.length ? `fehlende Symbole: ${fehlend.slice(0, 6).join(", ")}`
|
|
: `alle ${apps.length * noetig.length} Symbole vorhanden `
|
|
+ `(Reiter, Android-maskable, iPhone)`);
|
|
|
|
/* Jede Seite muss BEIDES nennen: das Reiter-Symbol und das fuers
|
|
iPhone. Fehlt das zweite, nimmt iOS einen Bildschirmausschnitt
|
|
der Seite -- unbrauchbar und unerkennbar. */
|
|
let ohneApple = [];
|
|
for (const seite of seiten) {
|
|
const text = readFileSync(join(WS, seite), "utf8");
|
|
if (!/rel="apple-touch-icon"/i.test(text)) ohneApple.push(seite);
|
|
}
|
|
ok(ohneApple.length === 0,
|
|
ohneApple.length ? `ohne apple-touch-icon (iPhone nimmt sonst einen Screenshot): ${ohneApple.join(", ")}`
|
|
: `alle ${seiten.length} Workspace-Seiten nennen ein iPhone-Symbol`);
|
|
}
|
|
}
|
|
|
|
/* ---------- 11. Laesst sich jedes Server-Modul ueberhaupt laden? --------
|
|
|
|
`node --check datei.js` prueft eine .js-Datei als CommonJS. Dieses
|
|
Haus faehrt aber ESM ("type": "module" in package.json). Der
|
|
Unterschied ist keine Formsache: Ein Syntaxfehler kann dabei
|
|
durchrutschen und die Datei trotzdem beim Start abstuerzen lassen.
|
|
|
|
Genau das ist am 05.09.2026 passiert. In einem SQL-Block -- einem
|
|
Template-Literal mit Backticks -- stand in einem KOMMENTAR ein Wort
|
|
in Backticks. Das beendete das Literal mitten im Text. `node --check`
|
|
meldete "Syntax ok", der Import scheiterte mit "missing ) after
|
|
argument list", und der Server waere beim naechsten Start gar nicht
|
|
hochgekommen.
|
|
|
|
Deshalb wird hier jedes Modul WIRKLICH geladen. Das ist die einzige
|
|
Pruefung, die dieselbe Frage stellt wie der Serverstart. */
|
|
console.log(`\n=== Laedt jedes Server-Modul? ===`);
|
|
{
|
|
/* GEPRUEFT WIRD MIT NODE SELBST, nicht mit einem Suchmuster.
|
|
|
|
Der erste Entwurf suchte per regulaerem Ausdruck nach Backticks in
|
|
SQL-Bloecken -- und schlug bei drei Bloecken an, die voellig in
|
|
Ordnung sind: Enthaelt ein Block ein ${...}, grenzt das Muster ihn
|
|
falsch ab. Eine Warnung, die falsch ist, ist schlimmer als keine;
|
|
sie kostet beim naechsten Mal zehn Minuten und wird danach
|
|
ignoriert.
|
|
|
|
Node kann die Frage selbst beantworten. Die Datei wird als .mjs
|
|
kopiert und mit `node --check` geprueft -- damit gilt dieselbe
|
|
Regel wie beim Start (ESM statt CommonJS). Genau darin lag der
|
|
Fehler vom 05.09.2026: `node --check datei.js` meldete "ok",
|
|
der Import scheiterte trotzdem. */
|
|
const { execFileSync } = await import("node:child_process");
|
|
const { writeFileSync, unlinkSync, mkdtempSync } = await import("node:fs");
|
|
const { tmpdir } = await import("node:os");
|
|
|
|
const module = readdirSync(SERVER).filter((n) => n.endsWith(".js"));
|
|
ok(module.length >= 20, `${module.length} Server-Module gefunden`);
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "modulcheck-"));
|
|
const kaputt = [];
|
|
for (const datei of module) {
|
|
const kopie = join(ordner, datei.replace(/\.js$/, ".mjs"));
|
|
writeFileSync(kopie, readFileSync(join(SERVER, datei), "utf8"));
|
|
try {
|
|
execFileSync(process.execPath, ["--check", kopie], { stdio: "pipe" });
|
|
} catch (f) {
|
|
const meldung = String(f.stderr || f.message).split(/\r?\n/)
|
|
.find((z) => z.includes("Error")) || "Syntaxfehler";
|
|
kaputt.push(`${datei}: ${meldung.trim().slice(0, 70)}`);
|
|
}
|
|
try { unlinkSync(kopie); } catch { /* egal */ }
|
|
}
|
|
ok(kaputt.length === 0,
|
|
kaputt.length
|
|
? `als ESM NICHT ladbar: ${kaputt.slice(0, 5).join(" · ")}`
|
|
: `alle ${module.length} Module lassen sich als ESM einlesen`);
|
|
|
|
/* GEGENPROBE mit genau dem Fehler von heute.
|
|
|
|
Nachgebaut wird der echte Fall: ein Backtick in einem KOMMENTAR,
|
|
der innerhalb eines Template-Literals steht. Sieht harmlos aus,
|
|
beendet aber die Zeichenkette. */
|
|
const boese = join(ordner, "gegenprobe.mjs");
|
|
writeFileSync(boese, [
|
|
"const d = { exec: () => {} };",
|
|
"d.exec(`",
|
|
" CREATE TABLE x (a TEXT);",
|
|
" /* Der Wert `endpunkt` ist der Schluessel. */",
|
|
"`);",
|
|
].join("\n"));
|
|
let erkannt = false;
|
|
try { execFileSync(process.execPath, ["--check", boese], { stdio: "pipe" }); }
|
|
catch { erkannt = true; }
|
|
ok(erkannt, erkannt
|
|
? "Gegenprobe: ein Backtick im SQL-Kommentar wird als Fehler erkannt"
|
|
: "Gegenprobe MISSLUNGEN — die Pruefung wuerde den Fehler durchlassen");
|
|
|
|
/* Und der Beweis, dass sie nicht einfach alles ablehnt. */
|
|
const brav = join(ordner, "gegenprobe-ok.mjs");
|
|
writeFileSync(brav, "export const x = `alles gut ${1 + 1}`;\n");
|
|
let bravOk = true;
|
|
try { execFileSync(process.execPath, ["--check", brav], { stdio: "pipe" }); }
|
|
catch { bravOk = false; }
|
|
ok(bravOk, "Gegenprobe: eine saubere Datei wird durchgelassen");
|
|
|
|
for (const f of [boese, brav]) { try { unlinkSync(f); } catch { /* egal */ } }
|
|
}
|
|
|
|
/* ---------- 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);
|