Files
DogFatherGitandClaude Opus 5 ba0e326585 Team-Lage: fuenf Stufen mit eigener Farbe, Karten neu gebaut, 404 wirft nicht mehr hinaus
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]>
2026-09-22 19:46:47 +02:00

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);