B7 -- "wenn ich auf diese sachen drücke werd ich auf die startseite geschickt" und "ich will das es ein zwei kategorien mehr gibt und das soll geiler aussehen und jede seine eigene farbe, stark erkennbar" 1. DER RAUSWURF. Ein Druck auf einen Stufen-Knopf schickte auf die Startseite. Ursache: `hole()` stand ACHTMAL Zeichen fuer Zeichen gleich in acht Dateien, und darin warf JEDER 404 hinaus -- auch der auf eine blosse Handlung. Jetzt eine gemeinsame `assets/js/holen.js` mit der engeren, abgeleiteten Regel: Ein 404 wirft nur, solange die Seite noch gar nichts bekommen hat. 2. ZWEI STUFEN MEHR, und keine davon erfunden: "Fortgeschritten" gibt es im Entwicklungs-Katalog laengst (ENTWICKLUNG_ERWARTUNG), hier fehlte genau diese Mitte; "Vertretung" stand bis heute IN der Beschreibung von Senior. Keine Datenbankaenderung noetig -- die Spalte ist absichtlich ein TEXT ohne CHECK. 3. DIE FARBEN SIND GERECHNET. Nachgemessen lagen "Probe" und "Standard" bei 0,0576 OKLab-Abstand; MINDEST_ABSTAND ist 0,090 -- die beiden waren nebeneinander dieselbe Farbe, und alle drei lagen unter MINDEST_BUNTHEIT. Die neuen fuenf sind ein Faecher aus fuenf Farbwinkeln im Abstand von 72 Grad; der Versatz ist der, bei dem der kleinste Abstand am groessten wird. Ergebnis: 0,1531 untereinander, 0,1108 zur Rollenmarke daneben, Kontrast 7,4 bis 8,6. B8 -- "die sollen viel krasser geiler und übersichtlicher sein ... und die kacheln sollen viel krass geiler und spezieller aussehen auch der hintergrund, veränder das komplett" 4. DIE KARTE IST NEU: Kopfband in der Farbe der Stufe bis an die Kanten, Zeichen von 44 auf 52 px mit Ring, Zahlenband als eigene Flaeche, neuer Hintergrund (Farbschein unter dem Kopfband, feine Schraffur, dunklere Platte). 781 px hoch gemessen, danach 746. 5. DIE ROLLE STEHT JETZT DA -- als Marke auf der Karte und als Ueberschrift ueber jeder Gruppe. Sortiert war schon vorher nach Rolle; man konnte es nur nicht sehen. DREI FEHLER, DIE DABEI AUFFIELEN * `auto-fit` liess die einzelne Karte der rechten Hand ueber die ganze Bildschirmbreite laufen, sobald gruppiert wurde. `auto-fill` haelt die leeren Spalten offen. * Der Name der Person trug `tperson__namen` statt `tperson__name` -- ein Buchstabe, und damit die Klasse fuer graues Kleingedrucktes. Auf einer Seite ueber Personen war der Name kleiner als die Beschriftung darunter. Gefunden hat es die neue Pruefung, die ueberall "?" las. * `stufeSetzen()` suchte das Stufen-Schild mit `[class*="tmerkmal--"]:not(...)` -- einer Aufzaehlung dessen, was es NICHT ist. Sobald die Rollenmarke davorstand, traf die Suche sie: Ein Druck auf "Standard" haette aus "Modi" das Wort "Standard" gemacht. Und die Karte faerbte sich beim Setzen nicht mit um. DREI PRUEFUNGEN, ALLE MIT GEGENPROBE * pruef-holen.mjs (NEU, 14): fuehrt die echte Datei aus und misst fuenf Wege; dieselben fuenf noch einmal an der ALTEN Fassung, die bei genau zwei durchfallen muss. Dazu: laedt jede der acht Seiten holen.js, und zwar VOR ihrem Skript. * pruef-teamlage-karten.mjs (NEU, 22): im Browser. Gruppen, Rollenwort, Stufenknoepfe nur bei Modis, der Druck selbst (kein Sprung, ein Abruf, Schild und Kartenfarbe ziehen mit), Gegenprobe mit dem zweiten Druck, und die neuen Baender auf 390 px. * pruef-team-stufen.mjs (28 -> 47): die abgeschriebene Liste ["probe","standard","senior"] ist raus -- geprueft werden jetzt Eigenschaften und die Farbabstaende, gelesen aus team.css. Gegenprobe ist der Stand von gestern, der durch dieselbe Messung faellt. Co-Authored-By: Claude Opus 5 <[email protected]>
307 lines
13 KiB
JavaScript
307 lines
13 KiB
JavaScript
/* =====================================================================
|
|
DER GEMEINSAME ABRUF — und die Regel, wann ein 404 hinauswirft
|
|
=====================================================================
|
|
|
|
ENTSTANDEN AUS EINEM ECHTEN FEHLER (22.09.2026).
|
|
|
|
Filipe: „wenn ich auf diese sachen drücke werd ich auf die
|
|
startseite geschickt, scheck das bitte ab." Gemeint waren die
|
|
Stufen-Knöpfe auf der Team-Lage. Im Browser nachgestellt:
|
|
|
|
Druck auf „Standard"
|
|
→ PUT /workspace/api/teamlage/stufe 404
|
|
→ Adresse springt auf /workspace/start.html
|
|
|
|
Die Ursache stand in ACHT Dateien, Zeichen für Zeichen dieselbe:
|
|
|
|
if (a.status === 404) { location.assign('/workspace/start.html'); … }
|
|
|
|
Gemeint war: „Du darfst diese Seite nicht sehen — hier ist der
|
|
Ausgang." Geschrieben stand: „Irgendein Abruf hat 404 geliefert —
|
|
raus mit dir." Eine einzelne Handlung, die nicht klappt, warf damit
|
|
den Menschen von einer Seite, auf der alles andere in Ordnung war —
|
|
und er sah nicht einmal, was schiefging, weil die Meldung auf der
|
|
Seite erschienen wäre, die gerade verschwindet.
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DAS EINE DAUERHAFTE PRÜFUNG BRAUCHT
|
|
|
|
Weil die Regel eine ZEITLICHE ist und sich deshalb nicht ansehen
|
|
lässt: „Ein 404 wirft nur hinaus, solange die Seite noch gar nichts
|
|
bekommen hat." Wer die Datei liest, sieht drei Zeilen und findet sie
|
|
plausibel. Ob sie stimmen, zeigt erst der Ablauf — und zwar in
|
|
BEIDE Richtungen. Eine Prüfung, die nur den Rauswurf misst, wäre mit
|
|
der alten, falschen Fassung ebenfalls grün gewesen.
|
|
|
|
Deshalb misst diese Prüfung fünf Wege durch dieselbe Funktion und
|
|
führt danach dieselben fünf Messungen noch einmal gegen die ALTE
|
|
Fassung — sie MUSS dort durchfallen. Sonst wäre nicht bewiesen, dass
|
|
sie den Unterschied überhaupt bemerkt.
|
|
|
|
Und sie misst den zweiten Teil, der genauso wichtig ist: dass die
|
|
acht Seiten die gemeinsame Datei wirklich laden, und zwar VOR ihrem
|
|
eigenen Skript. Eine Regel, die auf einer Seite nicht geladen wird,
|
|
ist dort keine Regel — sondern ein „holen is not a function".
|
|
===================================================================== */
|
|
import { readFileSync, readdirSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { createContext, runInContext } from "node:vm";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
notbremse(60_000, "pruef-holen");
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const WORKSPACE = join(HIER, "..", "workspace");
|
|
const SKRIPTE = join(WORKSPACE, "assets", "js");
|
|
|
|
let fehler = 0, geprueft = 0, offen = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
|
|
|
console.log("=== Der gemeinsame Abruf (holen.js) ===");
|
|
console.log("");
|
|
|
|
/* =====================================================================
|
|
1. DAS VERHALTEN
|
|
|
|
Die echte Datei wird ausgeführt, nicht nachgebaut. Ein Nachbau
|
|
würde beweisen, dass der Nachbau stimmt.
|
|
===================================================================== */
|
|
console.log("--- Wie sich holen() verhält ---");
|
|
|
|
let quelle = null;
|
|
try {
|
|
quelle = readFileSync(join(SKRIPTE, "holen.js"), "utf8");
|
|
} catch (f) {
|
|
unklar(`assets/js/holen.js ist nicht lesbar (${f.code || f.message})`
|
|
+ " — ohne die Datei lässt sich hier nichts messen.");
|
|
}
|
|
|
|
/* Die ALTE Fassung, wörtlich so, wie sie in den acht Dateien stand.
|
|
Sie ist die Gegenprobe: Dieselben Messungen müssen an ihr
|
|
scheitern. */
|
|
const ALTE_FASSUNG = `
|
|
window.holen = async function holen(pfad, optionen = {}) {
|
|
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
|
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
|
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('nicht erlaubt'); }
|
|
return a;
|
|
};
|
|
`;
|
|
|
|
/** Führt eine Fassung aus und gibt eine Prüfbank zurück. */
|
|
function bank(js) {
|
|
const gesprungen = [];
|
|
const welt = {
|
|
window: {},
|
|
location: { assign: (z) => gesprungen.push(z) },
|
|
fetch: null,
|
|
console,
|
|
};
|
|
welt.globalThis = welt;
|
|
const raum = createContext(welt);
|
|
runInContext(js, raum, { timeout: 5000 });
|
|
return {
|
|
gesprungen,
|
|
/** Setzt die nächste Antwort und ruft holen() auf. */
|
|
async ruf(status, optionen) {
|
|
welt.fetch = async () => ({ status, ok: status >= 200 && status < 300 });
|
|
try {
|
|
return { antwort: await welt.window.holen("/irgendwo", optionen), wurf: null };
|
|
} catch (f) {
|
|
return { antwort: null, wurf: String(f && f.message) };
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Die fünf Wege. Gibt zurück, was dabei herauskam — als Text, damit
|
|
* beide Fassungen mit derselben Elle gemessen werden. */
|
|
async function fuenfWege(js) {
|
|
const b = bank(js);
|
|
const erg = {};
|
|
|
|
/* (1) Der erste Abruf einer Seite läuft ins Leere: Die Seite gibt es
|
|
für diesen Menschen nicht. Hier IST der Sprung richtig. */
|
|
const a1 = await b.ruf(404, undefined);
|
|
erg.ersterGetSpringt = b.gesprungen.at(-1) === "/workspace/start.html" && a1.antwort === null;
|
|
|
|
/* (2) Eine Seite, die steht, und danach eine Handlung, die nicht
|
|
klappt. Hier darf NICHTS springen. */
|
|
const b2 = bank(js);
|
|
await b2.ruf(200, undefined);
|
|
const vorher2 = b2.gesprungen.length;
|
|
const a2 = await b2.ruf(404, { method: "PUT" });
|
|
erg.handlungSpringtNicht = b2.gesprungen.length === vorher2
|
|
&& a2.antwort !== null && a2.antwort.status === 404;
|
|
|
|
/* (3) Auch ein späterer GET darf nicht mehr werfen — die Seite steht
|
|
ja. Ein Nachladen, das ins Leere geht, ist eine Meldung wert,
|
|
kein Rauswurf. */
|
|
const b3 = bank(js);
|
|
await b3.ruf(200, undefined);
|
|
const vorher3 = b3.gesprungen.length;
|
|
await b3.ruf(404, undefined);
|
|
erg.spaeterGetSpringtNicht = b3.gesprungen.length === vorher3;
|
|
|
|
/* (4) 401 wirft IMMER zur Anmeldung — auch mitten in der Arbeit.
|
|
Wer abgemeldet ist, kann nirgends mehr etwas tun. */
|
|
const b4 = bank(js);
|
|
await b4.ruf(200, undefined);
|
|
const a4 = await b4.ruf(401, { method: "POST" });
|
|
erg.abgemeldetSpringtImmer = b4.gesprungen.at(-1) === "/workspace/"
|
|
&& a4.antwort === null;
|
|
|
|
/* (5) Der Normalfall: eine Antwort kommt zurück, nichts springt. */
|
|
const b5 = bank(js);
|
|
const a5 = await b5.ruf(200, undefined);
|
|
erg.normalfallRuhig = b5.gesprungen.length === 0
|
|
&& a5.antwort !== null && a5.antwort.status === 200;
|
|
|
|
return erg;
|
|
}
|
|
|
|
const WEGE = {
|
|
ersterGetSpringt: "ein 404 auf den ERSTEN Abruf wirft zur Startseite",
|
|
handlungSpringtNicht: "ein 404 auf eine Handlung (PUT) wirft NICHT — die Seite bleibt stehen",
|
|
spaeterGetSpringtNicht: "ein 404 auf einen späteren Abruf wirft ebenfalls nicht",
|
|
abgemeldetSpringtImmer: "ein 401 wirft immer zur Anmeldung, auch mitten in der Arbeit",
|
|
normalfallRuhig: "der Normalfall gibt die Antwort zurück und springt nirgends",
|
|
};
|
|
|
|
let neuErg = null;
|
|
if (quelle) {
|
|
neuErg = await fuenfWege(quelle);
|
|
for (const [schluessel, satz] of Object.entries(WEGE)) {
|
|
ok(neuErg[schluessel] === true, satz);
|
|
}
|
|
}
|
|
|
|
/* ---- Die Gegenprobe --------------------------------------------------
|
|
Dieselben fünf Messungen an der alten Fassung. Zwei davon MÜSSEN
|
|
dort umschlagen — sonst misst diese Prüfung nicht, was sie
|
|
behauptet. */
|
|
console.log("");
|
|
console.log("--- Gegenprobe: dieselben Messungen an der alten Fassung ---");
|
|
const altErg = await fuenfWege(ALTE_FASSUNG);
|
|
const umgeschlagen = Object.keys(WEGE).filter((k) => altErg[k] !== true);
|
|
ok(umgeschlagen.length === 2
|
|
&& umgeschlagen.includes("handlungSpringtNicht")
|
|
&& umgeschlagen.includes("spaeterGetSpringtNicht"),
|
|
`die alte Fassung fällt bei genau den zwei Wegen durch, um die es ging`
|
|
+ ` (${umgeschlagen.length}: ${umgeschlagen.join(", ") || "keinem"})`);
|
|
ok(altErg.ersterGetSpringt === true && altErg.abgemeldetSpringtImmer === true
|
|
&& altErg.normalfallRuhig === true,
|
|
"und sie besteht die drei anderen — gemessen wird also die REGEL,"
|
|
+ " nicht einfach die Frage, ob es die neue Datei ist"
|
|
+ " (eine Prüfung, die jede alte Fassung verwirft, prüft nichts)");
|
|
|
|
/* =====================================================================
|
|
2. DIE EINBINDUNG
|
|
|
|
ABGELEITET, NICHT AUFGEZÄHLT: Welche Seitenskripte den gemeinsamen
|
|
Abruf benutzen, steht in den Dateien selbst. Eine Liste hier wäre
|
|
die, die beim neunten Skript nicht mitwächst.
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Wird holen.js überall geladen, und zwar zuerst? ---");
|
|
|
|
const skriptNamen = readdirSync(SKRIPTE).filter((n) => n.endsWith(".js"));
|
|
const nutzer = [];
|
|
for (const name of skriptNamen) {
|
|
if (name === "holen.js") continue;
|
|
const text = readFileSync(join(SKRIPTE, name), "utf8");
|
|
if (/window\.holen\s*\(/.test(text)) nutzer.push({ name, text });
|
|
}
|
|
ok(nutzer.length >= 8,
|
|
`${nutzer.length} Seitenskripte benutzen den gemeinsamen Abruf`
|
|
+ ` (${nutzer.map((n) => n.name.replace(/\.js$/, "")).join(", ")})`);
|
|
|
|
/* KEINE EIGENE KOPIE DER REGEL MEHR.
|
|
|
|
---------------------------------------------------------------------
|
|
DIE ERSTE FASSUNG DIESER PRÜFUNG WAR ZU STRENG — und das ist selbst
|
|
ein Befund wert. Sie verbot jedes `location.assign('/workspace/
|
|
start.html')` in einem Seitenskript und wurde sofort rot, bei
|
|
bereich.js und team.js. Nachgesehen: Alle drei Stellen sind richtig
|
|
und sollen bleiben.
|
|
|
|
bereich.js `?b=` ist kein gültiger Bereichsname → es gibt die
|
|
Seite nicht
|
|
bereich.js der erste Abruf des Bereichs ist nicht ok → diese
|
|
Person sieht diesen Bereich nicht
|
|
team.js die Rolle passt nicht → leere Seite vermeiden
|
|
|
|
Ein Sprung zur Startseite ist nicht das Problem. Das Problem war,
|
|
dass die ZEITLICHE Regel („nur solange die Seite noch nichts hat")
|
|
achtmal nachgebaut danebenstand. Eine Prüfung, die den gesunden
|
|
Fall bestraft, wird abgeschaltet — und mit ihr die Warnung, auf die
|
|
es ankommt.
|
|
|
|
Verboten ist deshalb genau zweierlei: ein eigener `hole()`-Helfer
|
|
neben dem gemeinsamen, und die Paarung „404 → Startseite" in einem
|
|
Seitenskript. Beides ist die Dopplung, nichts sonst.
|
|
--------------------------------------------------------------------- */
|
|
const eigenerHelfer = nutzer.filter((n) =>
|
|
/(async\s+)?function\s+hole\s*\(/.test(n.text));
|
|
ok(nutzer.length > 0 && eigenerHelfer.length === 0,
|
|
`keines baut sich daneben einen eigenen hole()-Helfer`
|
|
+ ` (${eigenerHelfer.map((n) => n.name).join(", ") || "keines"})`);
|
|
|
|
const mitEigenerRegel = nutzer.filter((n) =>
|
|
/status\s*===\s*404[\s\S]{0,120}?location\.assign\((['"])\/workspace\/start\.html\1\)/
|
|
.test(n.text));
|
|
ok(nutzer.length > 0 && mitEigenerRegel.length === 0,
|
|
`und keines entscheidet selbst, was ein 404 bedeutet`
|
|
+ ` (${mitEigenerRegel.map((n) => n.name).join(", ") || "keines"})`);
|
|
|
|
/* GEGENPROBE ZU DEN ZWEI REGELN OBEN. Zwei Suchen, die nichts finden,
|
|
sehen genauso aus wie zwei Suchen, die nichts finden KOENNEN — das
|
|
ist der leere `every()`-Fehler in anderer Gestalt. Also einmal an
|
|
dem Text messen, den es zu verhindern gilt: der alten, acht Mal
|
|
kopierten Fassung. Findet die Suche ihn nicht, ist sie kaputt. */
|
|
const ALTE_KOPIE = `
|
|
async function hole(pfad, optionen = {}) {
|
|
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
|
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
|
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('nicht erlaubt'); }
|
|
return a;
|
|
}
|
|
`;
|
|
ok(/(async\s+)?function\s+hole\s*\(/.test(ALTE_KOPIE)
|
|
&& /status\s*===\s*404[\s\S]{0,120}?location\.assign\((['"])\/workspace\/start\.html\1\)/
|
|
.test(ALTE_KOPIE),
|
|
"Gegenprobe: beide Suchen finden die alte Kopie — sie koennen also anschlagen");
|
|
|
|
/* Und jetzt die Seiten. Welche HTML-Datei welches Skript lädt, steht
|
|
in der HTML-Datei — auch das wird gelesen, nicht gepflegt. */
|
|
const seiten = readdirSync(WORKSPACE).filter((n) => n.endsWith(".html"));
|
|
let geprueftePaare = 0;
|
|
const ohneHolen = [], falscheReihenfolge = [];
|
|
for (const seite of seiten) {
|
|
const html = readFileSync(join(WORKSPACE, seite), "utf8");
|
|
for (const n of nutzer) {
|
|
const stelle = html.indexOf(`assets/js/${n.name}`);
|
|
if (stelle < 0) continue;
|
|
geprueftePaare++;
|
|
const holenStelle = html.indexOf("assets/js/holen.js");
|
|
if (holenStelle < 0) { ohneHolen.push(`${seite} → ${n.name}`); continue; }
|
|
/* VOR dem Seitenskript. Beide tragen `defer`, also zählt die
|
|
Reihenfolge im Quelltext — nicht die des Eintreffens. */
|
|
if (holenStelle > stelle) falscheReihenfolge.push(`${seite} → ${n.name}`);
|
|
}
|
|
}
|
|
ok(geprueftePaare >= 8,
|
|
`${geprueftePaare} Seite-Skript-Paare gefunden — es gibt also etwas zu prüfen`);
|
|
ok(geprueftePaare > 0 && ohneHolen.length === 0,
|
|
`jede dieser Seiten lädt holen.js (${ohneHolen.join(", ") || "keine Lücke"})`);
|
|
ok(geprueftePaare > 0 && falscheReihenfolge.length === 0,
|
|
`und immer VOR dem Seitenskript (${falscheReihenfolge.join(", ") || "überall richtig"})`);
|
|
|
|
/* ---- Ende ------------------------------------------------------------ */
|
|
console.log("");
|
|
console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}`
|
|
+ ` -- ${geprueft} geprüft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
|
|
process.exit(fehler === 0 ? 0 : 1);
|