Audit des Creator Workspace, App-Symbole fuer alle Apps der Domain
AUDIT (Auftrag: vollstaendiger Durchgang, Fehler direkt beheben)
Ausgangslage waren 40 Pruefungen mit 1616 Einzelpunkten, alle gruen.
Acht neue Pruefungen kamen dazu; sie haben gefunden, was die alten nicht
sehen konnten.
Der schwerste Fund: Die Zugangsschranke verglich req.path EXAKT gegen
eine Liste. Express raeumt Punkt-Segmente selbst weg, mehrfache
Schraegstriche aber nicht. Damit kam //workspace/start.html OHNE
Anmeldung mit HTTP 200, und ein Creator bekam ueber
/workspace//personen.html die Verwaltungsseite. Die DATEN waren nie
betroffen (nachgemessen: 404 bzw. 401). Behoben durch Normalisierung
UND eine Umkehr der Logik -- jetzt ist jede .html geschuetzt ausser der
Anmeldeseite, statt nur die in der Liste. Eine vergessene neue Seite
steht damit nicht mehr versehentlich offen.
Weiter behoben:
* Kaputter/abgebrochener Rumpf ergab 500 in HTML statt 400 in JSON --
die Oberflaeche ruft ueberall a.json() und lief in einen zweiten
Fehler; der Knopf hing ohne Meldung.
* POST /zustand/sichern war der einzige von 60 schreibenden Wegen
ohne Herkunftspruefung.
* workspace-sicherung.js gab interne Pfade in Fehlermeldungen nach
aussen; alle 23 anderen Module antworten neutral.
* admin_notiz war als einziges von 14 Feldern ohne <label>.
* Der aktive Filter hatte keinen sichtbaren Fokus (CSS-Spezifitaet
0,3,0 schlug 0,2,0) -- genau der Knopf, auf dem man steht.
* h1 -> h3 ohne Zwischenstufe auf zwei Seiten.
* HSTS ging auch ueber http mit (RFC 6797, 7.2 verbietet das).
* upgrade-insecure-requests galt auch auf 127.0.0.1 -- dadurch war
WebKit/Safari ueberhaupt nicht pruefbar, also der Browser, den
jedes iPhone benutzt.
* pruef-grosscheck las readdirSync(".") und pruefte aus server/
gestartet NULL oeffentliche Seiten -- meldete aber "ok".
Neue Pruefungen: struktur, schranke, haerte, alle-wege,
barrierefrei-workspace, breiten, tempo-workspace, browser.
Jede mit Gegenprobe und mit der geprueften Anzahl in der Bedingung.
Vier davon sind beim Bauen durch die eigene Gegenprobe aufgeflogen und
haetten sonst dauerhaft gruen gemeldet, ohne etwas zu messen.
APP-SYMBOLE (Wunsch: alle Apps der Domain, jede anders, ausser
safeaddress)
Zehn Apps, zehn Stile, zehn in OKLCH gerechnete Farben. Zusammen haelt
sie dasselbe Logo, dieselbe Eckenrundung und eine gemeinsame gedeckte
Farbreihe. Beim Bauen wird gemessen, ob sich das Logo vom Grund abhebt
(19 bis 58 Helligkeitsstufen).
Dabei aufgefallen: Das Kundenportal hatte kein eigenes Manifest und
trug Namen und Symbol der Webdesign-Seite. Der Workspace hatte gar
keins und war als App nicht installierbar. Beide haben jetzt eins.
Geaendert wurde AUSSCHLIESSLICH das Symbol. Ein Zwischenstand hatte
auch die Themenfarben gesetzt; das war mehr als bestellt und wurde
zurueckgenommen.
Werkzeuge: tools/logo-freistellen.mjs, tools/app-symbole.mjs,
tools/app-symbole-einbinden.mjs -- alles im Browser gerechnet, kein
Bildprogramm, keine neue Abhaengigkeit.
Gitea und Nextcloud sind bereits live und nachgeprueft.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
/* 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)`);
|
||||
}
|
||||
|
||||
/* ---------- 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);
|
||||
Reference in New Issue
Block a user