Bauplan Etappen 4 und 6: Kachelplatz, Aufstiegskarte, und das Alte raeumt ab
Drei Dinge, die zusammengehoeren, weil sie alle denselben Satz aus
Kapitel 01 des Bauplans beantworten: Wer neu ist, soll nicht alles
gleich wichtig vorgesetzt bekommen.
1) DIE KACHEL STEHT BEIM TEAM OBEN (Kapitel 03)
Gemessen stand "Willkommen & Anleitung" bei einem Modi in der Gruppe
"Community" -- der FUENFTEN von sechs, hinter allen Brettern, die er
moderiert. Der Plan sagt dazu "zusaetzlich als erste Kachel in 'Fuer
dich', weil sie bei ihnen sonst weit unten steht".
Ich habe sie VERSCHOBEN statt verdoppelt, und das ist eine bewusste
Abweichung vom Wortlaut: Zwei Kacheln mit demselben Namen und
demselben Ziel hatte dieses Haus am 19.09. schon einmal (zweimal
"Chat"), und der Satz, der daraus wurde, steht seitdem im Quelltext --
"Zwei gleich benannte Wege zum selben Ort sind schlimmer als ein
fehlender". Das Ziel des Plans ist so erreicht, der Nebeneffekt bleibt
aus. Fuer die Community aendert sich nichts: Bei ihr ist "Community"
die erste Gruppe, dort steht sie schon vorne und breit.
Entschieden wird das an AUSSEN_ROLLEN, nicht an Rollennamen.
2) "NEU FUER DICH" BEIM AUFSTIEG (Kapitel 10, Etappe 6)
Erreicht ein Mitglied "Dabei" oder "Stamm", steht auf der Zentrale
einmal eine Karte mit dem, was jetzt geht, und einem Sprung auf die
passende Karte der Anleitung.
WAS JETZT GEHT, IST ABGELEITET -- aus TREFF_SCHREIBEN, derselben
Tafel, die es auch ENTSCHEIDET. Eine Liste im Code haette beim
naechsten neuen Brett entweder etwas versprochen, das die Person gar
nicht darf, oder verschwiegen, was sie duerfte.
Der Schluessel ist (Person, STUFE) und nicht (Person): Wer von Neu auf
Dabei steigt, bekommt die Karte, und Wochen spaeter beim Sprung auf
Stamm noch einmal, mit anderem Inhalt. Eine einzelne Spalte "schon
gesehen" haette den zweiten Aufstieg verschluckt -- und das waere
niemandem aufgefallen, es fehlt ja nur etwas.
Gemerkt wird beim ZEIGEN, nicht beim Wegklicken. Dieselbe Entscheidung
wie bei der Begruessung, aus demselben gemessenen Grund.
Dazu springt `anleitung.html?karte=<schluessel>` jetzt auf eine
bestimmte Karte. Erst beim Messen fiel auf, dass der Sprung ins Leere
ging, obwohl Karte und Abschnitt richtig waren: Unter den Karten
werden danach noch drei Abschnitte eingehaengt, und das sanfte
Scrollen zielte auf eine Hoehe, die sich dabei verschob. Der Sprung
steht jetzt ganz am Ende des Aufbaus.
3) DAS ABGELOESTE WILLKOMMEN IST WEG
willkommen.html ist seit heute frueh eine Weiterleitung. Der Unterbau
lief aber weiter: eine Schnittstelle, die jede Kachel einer Rolle
gleich ausfuehrlich erklaerte (genau das, was Kapitel 01 als Problem
beschreibt), dazu ein Skript und ein Stilblatt, die keine Seite mehr
lud. Entfernt: server/workspace-willkommen.js, assets/js/willkommen.js,
assets/css/willkommen.css und der Router in index.js.
Aufgefallen ist es, weil pruef-willkommen rot wurde und danach in ihre
Notbremse lief -- eine Pruefung, die eine zurueckgenommene Regel
verteidigt. Sie ist mitgegangen: Die inhaltlichen Fragen beantwortet
jetzt pruef-anleitung; hier bleibt die eine, die sonst niemand stellt
("verliert die alte Adresse jemanden?") plus der Nachweis, dass das
Alte wirklich fort ist. 15 Pruefungen statt der alten Fassung, und der
Rueckgang ist damit erklaert.
Nebenbefund: tools/kachel-regenbogen.mjs verwies auf pruef-willkommen
als Wache ueber den gerechneten CSS-Block. Die Datei hat ihn nie
angesehen -- die Wache ist pruef-kachelfarben. Zeiger berichtigt.
AUSSERDEM: .zeileneditor__weg war 34x34 und das einzige ungedeckte
Tippziel des Hauses (pruef-tippziele war deshalb rot). Er wirft eine
Zeile aus einem Eintrag. Auf Fingergeraeten jetzt 44x44, das Kreuz
mitgerechnet statt geraten. Der Knopf stammt aus einer anderen
Sitzung; ich habe ihn Filipe gemeldet und keine Antwort bekommen --
eine rote Pruefung, die liegen bleibt, wird nach dem zweiten Mal
weggeklickt, deshalb jetzt behoben statt weiter gemeldet.
GEPRUEFT
pruef-anleitung 137 -> 166 (neu: Kachelplatz je Rolle mit
Gegenprobe aufs Agenturhaus, der ganze
Aufstiegsweg inkl. ZWEITEM Aufstieg, und die
Abnahme-Checkliste aus Kapitel 13 -- 14/29/29/34
Karten MIT ihrer Aufteilung 5/4/5, 7/7/15,
7/7/15, 8/7/19 und "Nicht deins")
pruef-willkommen neu gefasst, 15, gruen
pruef-sackgassen 14, 145 Kacheln, 0 ins Leere
pruef-start-ansicht gruen, keine Konsolenfehler
pruef-rollen 454, gruen (das eine "nicht nachsehbar" ist das
Haus-Wechsel-Feld und war immer so)
pruef-tippziele 13, jetzt 0 Fehlschlaege
Gegenproben gefahren: Umsortierung ausgebaut -> drei FEHL (und die
Meldung zeigte den alten Zustand, Gruppe 5 von 6). Datenbank vor der
Schemaaenderung gesichert (sicherungen/vor-aufstieg-20261006-172614.db,
integrity_check ok).
Im Browser nachgemessen (echte Anmeldung, 390 px, HTTPS-Vorbau):
Karte da, Satz richtig, drei Wege, 0 zu kleine Tippziele, Sprung
landet auf der richtigen Karte, Abschnitt aufgeklappt, im Bild -- und
beim zweiten Aufruf ist sie weg.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+113
-356
@@ -1,39 +1,48 @@
|
||||
/* =====================================================================
|
||||
pruef-willkommen.mjs — DIE SEITE, DIE ERKLAERT, WAS ES HIER GIBT
|
||||
pruef-willkommen.mjs — DIE ALTE ADRESSE DARF NIEMANDEN VERLIEREN
|
||||
=====================================================================
|
||||
|
||||
Auftrag vom 21.09.2026, Abschnitte 7 und 8.
|
||||
WAS HIER FRUEHER STAND, UND WARUM ES WEG IST (06.10.2026)
|
||||
|
||||
WAS HIER GEMESSEN WIRD — und warum gerade das:
|
||||
Diese Datei pruefte bis heute eine Seite, die es nicht mehr gibt:
|
||||
`willkommen.html` erklaerte JEDE Kachel einer Rolle, alle gleich
|
||||
ausfuehrlich. Genau das beschreibt der Bauplan „Willkommen &
|
||||
Anleitung" in Kapitel 01 als das Problem — wer neu ist, bekam
|
||||
sechsunddreissig gleichwertige Absaetze und wusste danach nicht,
|
||||
womit er anfangen soll. An ihre Stelle ist `anleitung.html`
|
||||
getreten: dieselben Kacheln, aber nach Muss, Regelmaessig und Bei
|
||||
Bedarf sortiert, je Rolle.
|
||||
|
||||
(1) DASS DIE ERKLAERUNG ZU DEN RECHTEN PASST. Abschnitt 8 verlangt
|
||||
woertlich, die Erklaerung solle sich "automatisch an die
|
||||
tatsaechlich vorhandenen Rechte" anpassen. Der Fehler, den man
|
||||
dabei macht, ist eine gepflegte Liste: Sie stimmt am ersten Tag
|
||||
und erzaehlt am zweiten von einer Seite, die es fuer diesen
|
||||
Menschen nie gab. Geprueft wird deshalb nicht "steht da etwas",
|
||||
sondern: JEDE erklaerte Kachel muss diese Person auch oeffnen
|
||||
duerfen — und zwar gemessen an derselben Rechtetafel, die der
|
||||
Server durchsetzt.
|
||||
DIE ALTE PRUEFUNG WAR DAMIT EINE, DIE EINE ZURUECKGENOMMENE REGEL
|
||||
VERTEIDIGT — und sie hat es auch gemeldet: Beim ersten Lauf nach
|
||||
dem Umbau wurde sie rot („der Name steht oben: leer", „0 Kacheln
|
||||
werden erklaert") und lief danach in ihre Notbremse. Das war kein
|
||||
Befund am Haus, sondern einer an ihr selbst. Eine Pruefung muss mit
|
||||
der Aenderung mitgehen, sonst erzeugt sie Laerm, der die echten
|
||||
Befunde zudeckt.
|
||||
|
||||
(2) DIE GEGENPROBE DAZU. Eine Pruefung, die nur "alles erlaubt"
|
||||
bestaetigt, bestaetigt nichts. Also wird ausdruecklich gezaehlt,
|
||||
dass ein Modi die Seiten, die er NICHT hat (Personen, Hilfe,
|
||||
Bewerbungen), auch nicht erklaert bekommt — und dass DogFather
|
||||
mehr erklaert bekommt als er.
|
||||
MITGEGANGEN IST SIE SO: Die inhaltlichen Fragen („bekommt jede Rolle
|
||||
ihre eigenen Texte?", „wird nur erklaert, was man oeffnen darf?")
|
||||
beantwortet jetzt `pruef-anleitung.mjs` — an derselben Rechtetafel
|
||||
und mit denselben Gegenproben. Hier bleibt die eine Frage, die dort
|
||||
NICHT hingehoert und die sonst niemand stellt:
|
||||
|
||||
(3) DASS DIE ROLLEN WIRKLICH VERSCHIEDENE TEXTE BEKOMMEN. "Sie muss
|
||||
vollstaendig rollenbasiert aufgebaut werden" ist nur erfuellt,
|
||||
wenn sich die Texte auch unterscheiden. Vier gleiche Absaetze
|
||||
mit vier verschiedenen Namen davor waeren keine Rollenseite.
|
||||
Verliert die alte Adresse jemanden?
|
||||
|
||||
(4) DASS DIE AUSGELIEFERTE DATEI KEINE ROLLENNAMEN TRAEGT. Sie geht
|
||||
an jeden Browser. Steht die Aufteilung dort, ist sie lesbar und
|
||||
veraltet ausserdem. Dieselbe Regel wie bei pruef-modi-wortleck.
|
||||
Es gibt Lesezeichen, Verweise in alten Nachrichten und
|
||||
ausgedruckte Zettel. Der Bauplan verlangt in Kapitel 03
|
||||
ausdruecklich: „Adresse: bleibt willkommen.html. Alte Verweise
|
||||
funktionieren weiter."
|
||||
|
||||
(5) UND AM ECHTEN BILDSCHIRM, dass ein Mensch es lesen und antippen
|
||||
kann. Ein richtiger Datensatz, den niemand sieht, ist kein
|
||||
Erfolg.
|
||||
UND DIE ZWEITE FRAGE: Ist das Alte wirklich WEG — oder liegt nur
|
||||
die Seite still und der Unterbau laeuft weiter? Ein Server, der
|
||||
eine Schnittstelle anbietet, die niemand mehr aufruft, ist kein
|
||||
harmloser Rest: Er ist die zweite Wahrheit, die beim naechsten
|
||||
Rechteumbau vergessen wird. Deshalb wird hier nachgesehen, dass es
|
||||
sie nicht mehr gibt — mit Gegenprobe, damit die Zeile nicht nur
|
||||
deshalb gruen ist, weil sie gar nichts findet.
|
||||
|
||||
AUFRUF: node server/pruef-willkommen.mjs
|
||||
===================================================================== */
|
||||
|
||||
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
||||
@@ -45,7 +54,8 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-willk-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
||||
/* Kein HTTPS-Vorbau mehr: Hier wird keine Seite im Browser
|
||||
geladen, sondern nur geantwortet. */
|
||||
const { eigenerPort } = await import("./helfer-port.mjs");
|
||||
const PORT = await eigenerPort(import.meta, "die Willkommenspruefung");
|
||||
|
||||
@@ -60,9 +70,12 @@ await import("./index.js");
|
||||
notbremse(300_000, "pruef-willkommen");
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
|
||||
let fehler = 0;
|
||||
let fehler = 0, geprueft = 0;
|
||||
const melde = (t) => console.log(t);
|
||||
const ok = (b, t) => { melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
/* Die ANZAHL gehoert ans Ende, nicht nur das Ergebnis: Ein Rueckgang
|
||||
ist genauso ein Befund wie ein Fehlschlag -- er heisst, dass still
|
||||
etwas nicht mehr geprueft wird. */
|
||||
const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
const HOST = "crew.dogfather-universe.com";
|
||||
function rufe(pfad, wie = {}) {
|
||||
@@ -123,338 +136,82 @@ async function alsWer(rolle, code) {
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
1. DIE SCHRANKE
|
||||
1. DIE ALTE ADRESSE FUEHRT WEITER
|
||||
--------------------------------------------------------------------- */
|
||||
melde("\n=== Die Schranke ===");
|
||||
{
|
||||
const ohne = await rufe("/workspace/api/willkommen");
|
||||
ok(ohne.code === 401, `unangemeldet gibt es nichts (${ohne.code})`);
|
||||
}
|
||||
melde("\n=== Die alte Adresse ===");
|
||||
const keksModi = await alsWer("modi", "CODE-MOD1-0001");
|
||||
const alt = await rufe("/workspace/willkommen.html", { keks: keksModi });
|
||||
ok(alt.code === 200,
|
||||
`willkommen.html antwortet (${alt.code}) — und zwar mit einer Seite, `
|
||||
+ "nicht mit einer Umleitung");
|
||||
/* DREI WEGE, UND ALLE DREI WERDEN GEBRAUCHT. Der Kopfbefehl wirkt auch
|
||||
ohne JavaScript, `location.replace` wirkt sofort und OHNE Eintrag im
|
||||
Verlauf (sonst landete man beim Zurueckgehen wieder hier), und der
|
||||
sichtbare Verweis faengt den Fall ab, dass beides nicht greift. */
|
||||
ok(/http-equiv="refresh"[^>]*anleitung\.html/.test(alt.text),
|
||||
"…mit dem Kopfbefehl, der auch ohne JavaScript weiterleitet");
|
||||
ok(/location\.replace\(/.test(alt.text),
|
||||
"…mit location.replace — ohne Eintrag im Verlauf");
|
||||
ok(/<a[^>]+href="anleitung\.html"/.test(alt.text),
|
||||
"…und mit einem sichtbaren Verweis als letzter Rettung");
|
||||
/* Wer mit einem Zusatz in der Adresse kommt, soll dort landen, wohin
|
||||
er wollte. Gemessen wird der Code, nicht die Absicht. */
|
||||
ok(/location\.search/.test(alt.text),
|
||||
"…und der Zusatz in der Adresse wird mitgenommen");
|
||||
|
||||
/* Die Community kommt auf dieselbe Seite. Sie ist die Gruppe, bei der
|
||||
die alte Kachel am laengsten im Gedaechtnis steht. */
|
||||
const keksGast = await alsWer("gast", "CODE-GAST-0001");
|
||||
const altGast = await rufe("/workspace/willkommen.html", { keks: keksGast });
|
||||
ok(altGast.code === 200 && /anleitung\.html/.test(altGast.text),
|
||||
`auch fuer die Community (${altGast.code})`);
|
||||
|
||||
/* GEGENPROBE ZUR SCHRANKE: Ohne Anmeldung gibt es sie nicht. Sonst
|
||||
waeren die Zeilen oben auch gruen, wenn die Seite fuer jeden offen
|
||||
stuende. */
|
||||
const altOhne = await rufe("/workspace/willkommen.html");
|
||||
ok(altOhne.code === 302 || altOhne.code === 401,
|
||||
`ohne Anmeldung gibt es sie nicht (${altOhne.code})`);
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
2. JEDE ROLLE BEKOMMT IHRE EIGENE SEITE
|
||||
2. DAS ALTE IST WIRKLICH WEG
|
||||
--------------------------------------------------------------------- */
|
||||
melde("\n=== Jede Rolle bekommt ihre eigene Seite ===");
|
||||
const LEUTE = [
|
||||
{ rolle: "admin", code: "CODE-DOGI-0001", name: "Filipe" },
|
||||
{ rolle: "hand", code: "CODE-HAND-0001", name: "Funny" },
|
||||
{ rolle: "linke", code: "CODE-LINK-0001", name: "Lino" },
|
||||
{ rolle: "modi", code: "CODE-MOD1-0001", name: "Kessi" },
|
||||
{ rolle: "gast", code: "CODE-GAST-0001", name: "Robin" },
|
||||
];
|
||||
const gesehen = {};
|
||||
for (const p of LEUTE) {
|
||||
const keks = await alsWer(p.rolle, p.code);
|
||||
const a = await rufe("/workspace/api/willkommen", { keks });
|
||||
gesehen[p.rolle] = { keks, daten: a.json };
|
||||
ok(a.code === 200, `${p.rolle}: die Seite antwortet (${a.code})`);
|
||||
ok(a.json?.gruss === `Willkommen, ${p.name}!`,
|
||||
` und begruesst mit Namen ("${a.json?.gruss}")`);
|
||||
ok(!!a.json?.einleitung?.was && !!a.json?.einleitung?.dazu,
|
||||
" mit einer Einleitung, die aus zwei Teilen besteht");
|
||||
}
|
||||
melde("\n=== Der abgeloeste Unterbau ===");
|
||||
const api = await rufe("/workspace/api/willkommen", { keks: keksModi });
|
||||
ok(api.code === 404,
|
||||
`die alte Schnittstelle gibt es nicht mehr (${api.code})`);
|
||||
/* GEGENPROBE: Eine Schnittstelle, die es GIBT, antwortet anders.
|
||||
Ohne diese Zeile waere die obige auch dann gruen, wenn der ganze
|
||||
Server nicht mehr liefe. */
|
||||
const lebt = await rufe("/workspace/api/anleitung", { keks: keksModi });
|
||||
ok(lebt.code === 200,
|
||||
`Gegenprobe: die neue Schnittstelle antwortet sehr wohl (${lebt.code})`);
|
||||
|
||||
/* DIE EINLEITUNGEN MUESSEN SICH UNTERSCHEIDEN. Sonst ist es keine
|
||||
rollenbasierte Seite, sondern eine mit Namensfeld. */
|
||||
{
|
||||
const saetze = LEUTE.map((p) => gesehen[p.rolle]?.daten?.einleitung?.was);
|
||||
const verschieden = new Set(saetze).size;
|
||||
ok(verschieden === LEUTE.length,
|
||||
`${verschieden} von ${LEUTE.length} Einleitungen sind verschieden`);
|
||||
}
|
||||
const fehltDatei = (pfad) => {
|
||||
try { readFileSync(new URL(pfad, import.meta.url)); return false; }
|
||||
catch { return true; }
|
||||
};
|
||||
ok(fehltDatei("./workspace-willkommen.js"), "server/workspace-willkommen.js ist fort");
|
||||
ok(fehltDatei("../workspace/assets/js/willkommen.js"), "assets/js/willkommen.js ist fort");
|
||||
ok(fehltDatei("../workspace/assets/css/willkommen.css"), "assets/css/willkommen.css ist fort");
|
||||
/* GEGENPROBE: Dieselbe Frage an eine Datei, die es gibt. Ohne sie
|
||||
waeren die drei Zeilen darueber auch dann gruen, wenn `fehltDatei`
|
||||
immer „ja" sagte — zum Beispiel, weil der Pfad falsch gebaut wird. */
|
||||
ok(!fehltDatei("./workspace-anleitung.js"),
|
||||
"Gegenprobe: eine vorhandene Datei wird als vorhanden erkannt");
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
3. DER KERN: ERKLAERT WIRD NUR, WAS MAN AUCH DARF
|
||||
---------------------------------------------------------------------
|
||||
/* Und niemand haengt den Router wieder ein. Kommentare werden vorher
|
||||
entfernt — sonst wuerde dieser Hinweis hier selbst zum Treffer. */
|
||||
const indexRoh = readFileSync(new URL("./index.js", import.meta.url), "utf8");
|
||||
const indexCode = indexRoh
|
||||
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
||||
.replace(/(^|[^:])\/\/[^\n]*/g, "$1 ");
|
||||
ok(!/willkommenRouter/.test(indexCode),
|
||||
"index.js haengt den alten Router nicht mehr ein");
|
||||
ok(/anleitungRouter/.test(indexCode),
|
||||
"Gegenprobe: der neue Router steht sehr wohl darin");
|
||||
|
||||
Gemessen gegen die Rechtetafel selbst, nicht gegen eine Liste in
|
||||
dieser Datei -- sonst pruefe ich meine Abschrift gegen seine. */
|
||||
melde("\n=== Erklaert wird nur, was man auch oeffnen darf ===");
|
||||
const { darfSeite } = await import("./rechte.js");
|
||||
const { bereicheFuer, zusatzBereicheFuer } = await import("./workspace.js");
|
||||
const { istCrewAdresse } = await import("./crew-adresse.js");
|
||||
|
||||
/* DAS HAUS GEHOERT DAZU, und es wird ABGELEITET wie beim Server.
|
||||
Gemessen am 21.09.: DogFather hat auf der Crew-Adresse 30 Kacheln
|
||||
und auf der Agenturadresse 1 (die Tuer hinueber) -- er ist die
|
||||
einzige Rolle, die in beiden Haeusern lebt. Eine Vergleichsrechnung
|
||||
ohne Haus misst deshalb ausgerechnet bei ihm das falsche Haus und
|
||||
meldet einen Fehler, den es nicht gibt. */
|
||||
const HAUS = istCrewAdresse(HOST) ? "crew" : "agentur";
|
||||
const alsPerson = (rolle) => ({ rolle, haus: HAUS });
|
||||
|
||||
for (const p of LEUTE) {
|
||||
const daten = gesehen[p.rolle]?.daten;
|
||||
const kacheln = (daten?.gruppen || []).flatMap((g) => g.kacheln || []);
|
||||
ok(kacheln.length > 0, `${p.rolle}: es wird ueberhaupt etwas erklaert (${kacheln.length})`);
|
||||
|
||||
const verboten = kacheln.filter((k) => {
|
||||
const ziel = String(k.ziel || "").split("?")[0];
|
||||
if (!ziel || /^https?:/.test(ziel)) return false;
|
||||
return !darfSeite(alsPerson(p.rolle), "/workspace/" + ziel);
|
||||
});
|
||||
ok(verboten.length === 0, verboten.length === 0
|
||||
? " keine einzige davon ist fuer ihn gesperrt"
|
||||
: ` ERKLAERT GESPERRTE SEITEN: ${verboten.map((v) => v.ziel).join(", ")}`);
|
||||
|
||||
/* Und die andere Richtung: die Zahl muss zu seinen echten Kacheln
|
||||
passen. Zu WENIG waere genauso falsch -- dann verschweigt die
|
||||
Seite etwas, das er hat. */
|
||||
const roh = [
|
||||
...(bereicheFuer(alsPerson(p.rolle)) || []),
|
||||
...(zusatzBereicheFuer(alsPerson(p.rolle)) || []),
|
||||
];
|
||||
const sollte = roh.filter((k) => {
|
||||
if (k?.aussen) return true;
|
||||
const ziel = String(k?.ziel || "");
|
||||
if (!ziel) return false;
|
||||
return darfSeite(alsPerson(p.rolle), "/workspace/" + ziel.split("?")[0]);
|
||||
}).length;
|
||||
ok(kacheln.length === sollte,
|
||||
` und es sind genau seine ${sollte} Kacheln (erklaert: ${kacheln.length})`);
|
||||
}
|
||||
|
||||
/* GEGENPROBE MIT ZAHLEN: Kann die Pruefung ueberhaupt Unterschiede
|
||||
sehen? Wenn alle gleich viel bekaemen, waere oben nichts bewiesen. */
|
||||
{
|
||||
const zahl = (r) => (gesehen[r]?.daten?.gruppen || []).flatMap((g) => g.kacheln || []).length;
|
||||
const chef = zahl("admin"), modi = zahl("modi"), gast = zahl("gast");
|
||||
ok(chef > modi && modi > gast,
|
||||
`DogFather ${chef} > Modi ${modi} > Community ${gast} -- die Seite unterscheidet wirklich`);
|
||||
|
||||
/* DIE DREI, DIE EIN MODI NICHT HAT. Namentlich, damit ein stiller
|
||||
Durchrutscher auffaellt. */
|
||||
const modiZiele = (gesehen.modi?.daten?.gruppen || [])
|
||||
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
||||
for (const seite of ["personen.html", "bewerbungen.html", "rechte.html"]) {
|
||||
ok(!modiZiele.includes(seite), `einem Modi wird "${seite}" nicht erklaert`);
|
||||
}
|
||||
/* Und der Gegenbeweis, dass diese Namen ueberhaupt vorkommen koennen: */
|
||||
const chefZiele = (gesehen.admin?.daten?.gruppen || [])
|
||||
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
||||
ok(chefZiele.includes("personen.html"),
|
||||
"DogFather dagegen schon (sonst waere die Probe oben wertlos)");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
4. DIE LINKE HAND BEKOMMT IHRE GRENZEN GESAGT
|
||||
--------------------------------------------------------------------- */
|
||||
melde("\n=== Niemand erfaehrt, was er NICHT hat ===");
|
||||
{
|
||||
/* UMGEDREHT AM 22.09.2026. Hier stand: "ihr wird gesagt, was sie
|
||||
nicht hat -- einmal, klar, ohne Liste."
|
||||
|
||||
Filipe: "keiner soll wissen was er nicht sieht das geht keinen was
|
||||
an. sie sehen die sachen ja nicht weil es sie nichts angeht und das
|
||||
muss auch nicht erwaehnt werden. kontrollier das ueberall bitte."
|
||||
|
||||
Das "ueberall" ist der Grund, warum hier nicht eine Zeile
|
||||
umgedreht, sondern JEDE ROLLE gemessen wird. Ein Satz, der
|
||||
aufzaehlt, was fehlt, hebt ausserdem die Sorgfalt wieder auf, mit
|
||||
der der Server ueberall 404 statt 403 antwortet -- damit ein "das
|
||||
darfst du nicht" gar nicht erst verraet, dass es etwas gibt. */
|
||||
const VERRAETERISCH = [
|
||||
/nicht f(u|ue|\u00fc)r dich freigeschaltet/i,
|
||||
/was du nicht siehst/i,
|
||||
/was du nicht hast/i,
|
||||
/hast du nicht/i,
|
||||
/siehst du nicht/i,
|
||||
/darfst du nicht/i,
|
||||
/bleiben seine/i,
|
||||
/kein zugriff/i,
|
||||
];
|
||||
const gefunden = [];
|
||||
for (const pp of LEUTE) {
|
||||
const d = gesehen[pp.rolle]?.daten;
|
||||
const text = [
|
||||
d?.einleitung?.was, d?.einleitung?.dazu,
|
||||
...(d?.hinweise || []).flatMap((h) => [h.titel, h.text]),
|
||||
].filter(Boolean).join(" ");
|
||||
for (const m of VERRAETERISCH) {
|
||||
if (m.test(text)) gefunden.push(pp.rolle + ": " + m.source);
|
||||
}
|
||||
}
|
||||
ok(gefunden.length === 0, gefunden.length === 0
|
||||
? `keine der ${LEUTE.length} Rollen bekommt gesagt, was ihr fehlt`
|
||||
: `ZAEHLT AUF, WAS FEHLT: ${gefunden.join(" | ")}`);
|
||||
|
||||
/* GEGENPROBE: Wuerde dieses Muster so einen Satz ueberhaupt finden?
|
||||
Ohne sie waere die Zeile oben gruen, sobald die Liste kaputt ist. */
|
||||
ok(VERRAETERISCH.some((m) => m.test("Das ist nicht fuer dich freigeschaltet.")),
|
||||
"und ein solcher Satz wuerde hier wirklich auffallen");
|
||||
const ziele = (gesehen.linke?.daten?.gruppen || [])
|
||||
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
||||
ok(!ziele.includes("personen.html") && !ziele.includes("hilfe.html"),
|
||||
"und Personen/Meldeweg stehen nicht in ihrer Erklaerung");
|
||||
ok(ziele.includes("aufgaben.html"),
|
||||
"die Aufgaben dagegen schon");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
5. DIE AUSGELIEFERTE DATEI TRAEGT KEINE ROLLENAUFTEILUNG
|
||||
--------------------------------------------------------------------- */
|
||||
melde("\n=== Was im Browser landet ===");
|
||||
{
|
||||
const js = readFileSync(new URL("../workspace/assets/js/willkommen.js", import.meta.url), "utf8");
|
||||
const html = readFileSync(new URL("../workspace/willkommen.html", import.meta.url), "utf8");
|
||||
const ohneKommentar = js
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
||||
|
||||
for (const wort of ["admin", "linke", "gast"]) {
|
||||
const treffer = new RegExp("[\"'`]" + wort + "[\"'`]").test(ohneKommentar);
|
||||
ok(!treffer, `die Datei kennt "${wort}" nicht (sie kommt vom Server)`);
|
||||
}
|
||||
ok(!/aufgaben\.html|personen\.html/.test(ohneKommentar),
|
||||
"und fuehrt auch keine eigene Kachelliste");
|
||||
ok(html.includes("willkommen.css"), "die Seite laedt ihre Gestaltung");
|
||||
ok(html.includes("meldung.js"), "und den gemeinsamen Meldeweg");
|
||||
|
||||
/* GEGENPROBE: Findet dieses Muster ueberhaupt etwas? Sonst waere die
|
||||
Aussage oben nur die Bestaetigung eines kaputten Suchmusters. */
|
||||
const probe = 'const x = "admin";';
|
||||
ok(new RegExp("[\"'`]admin[\"'`]").test(probe),
|
||||
"das Suchmuster wuerde einen Rollennamen finden");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
6. DIE KACHEL FUEHRT HIN
|
||||
--------------------------------------------------------------------- */
|
||||
melde("\n=== Der Weg zur Seite ===");
|
||||
{
|
||||
const kacheln = [
|
||||
...(bereicheFuer(alsPerson("gast")) || []),
|
||||
...(zusatzBereicheFuer(alsPerson("gast")) || []),
|
||||
];
|
||||
const willk = kacheln.filter((k) => String(k.ziel || "").startsWith("willkommen.html"));
|
||||
ok(willk.length === 1, `die Community hat genau eine Kachel dorthin (${willk.length})`);
|
||||
ok(willk[0]?.name === "Willkommen", `und sie heisst so (${willk[0]?.name})`);
|
||||
|
||||
/* DAS TREFF-BRETT DARF NICHT STILL VERSCHWINDEN. Gemessen am
|
||||
21.09.: null Eintraege, 29 Chatnachrichten daneben -- geredet wird
|
||||
im Chat. Falls das Brett je wieder eine Kachel bekommt, soll diese
|
||||
Zeile daran erinnern, dass hier einmal eine war. */
|
||||
const brett = kacheln.filter((k) => String(k.ziel || "").includes("b=treff"));
|
||||
ok(brett.length === 0,
|
||||
`das leere Rudel-Brett hat keine eigene Kachel mehr (${brett.length})`);
|
||||
const chat = kacheln.filter((k) => String(k.ziel || "").includes("chat.html"));
|
||||
ok(chat.length >= 1, `der Rudel-Chat dagegen schon (${chat.length})`);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
7. AM ECHTEN BILDSCHIRM
|
||||
--------------------------------------------------------------------- */
|
||||
melde("\n=== Am echten Bildschirm ===");
|
||||
{
|
||||
const { execFileSync } = await import("node:child_process");
|
||||
const { createServer: httpsServer } = await import("node:https");
|
||||
const { request: httpAnfrage } = await import("node:http");
|
||||
|
||||
const schluessel = join(ordner, "pruef.key");
|
||||
const zertifikat = join(ordner, "pruef.crt");
|
||||
zertifikatBauen(schluessel, zertifikat, HOST);
|
||||
|
||||
const HTTPS_PORT = PORT + 1;
|
||||
const vorbau = httpsServer({
|
||||
key: readFileSync(schluessel), cert: readFileSync(zertifikat),
|
||||
}, (von, nach) => {
|
||||
const weiter = httpAnfrage({
|
||||
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
||||
headers: { ...von.headers, host: `${HOST}:${HTTPS_PORT}` },
|
||||
}, (antwort) => {
|
||||
nach.writeHead(antwort.statusCode || 502, antwort.headers);
|
||||
antwort.pipe(nach);
|
||||
});
|
||||
von.pipe(weiter);
|
||||
});
|
||||
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
||||
const SEITE = `https://${HOST}:${HTTPS_PORT}`;
|
||||
|
||||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await pw.chromium.launch({
|
||||
args: [`--host-resolver-rules=MAP ${HOST} 127.0.0.1`, "--ignore-certificate-errors"],
|
||||
});
|
||||
const seitenFehler = [];
|
||||
|
||||
async function alsSeite(rolle, code, breite = 1280) {
|
||||
const ctx = await browser.newContext({ viewport: { width: breite, height: 950 }, hasTouch: breite <= 860 });
|
||||
const s = await ctx.newPage();
|
||||
s.on("pageerror", (e) => seitenFehler.push(`${rolle}: ${String(e).slice(0, 110)}`));
|
||||
await s.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" });
|
||||
await s.click(`.rolle[data-rolle="${rolle}"]`);
|
||||
await s.fill("#code", code);
|
||||
await s.click("button[type=submit]");
|
||||
await s.waitForTimeout(800);
|
||||
await s.goto(`${SEITE}/workspace/willkommen.html`, { waitUntil: "networkidle" });
|
||||
await s.waitForTimeout(900);
|
||||
return { ctx, seite: s };
|
||||
}
|
||||
|
||||
/* ---- Ein Modi, am Handy ---- */
|
||||
const pModi = await alsSeite("modi", "CODE-MOD1-0001", 390);
|
||||
try {
|
||||
const bild = await pModi.seite.evaluate(() => {
|
||||
const kacheln = [...document.querySelectorAll(".w-kachel")];
|
||||
const breite = document.documentElement.scrollWidth;
|
||||
return {
|
||||
gruss: document.getElementById("gruss")?.textContent || "",
|
||||
rolle: document.getElementById("rollentext")?.textContent || "",
|
||||
was: document.getElementById("einleitung-was")?.textContent || "",
|
||||
kacheln: kacheln.length,
|
||||
mitLang: kacheln.filter((k) => k.querySelector(".w-kachel__lang")).length,
|
||||
laedtWeg: document.getElementById("laedt")?.hidden === true,
|
||||
busy: document.getElementById("haupt")?.getAttribute("aria-busy"),
|
||||
breite,
|
||||
ueberlauf: kacheln.filter((k) => k.getBoundingClientRect().right > breite + 1).length,
|
||||
zuKlein: kacheln.filter((k) => k.getBoundingClientRect().height < 44).length,
|
||||
ersteZiel: kacheln[0]?.getAttribute("href") || "",
|
||||
};
|
||||
});
|
||||
ok(bild.gruss.includes("Kessi"), `der Name steht oben ("${bild.gruss}")`);
|
||||
ok(bild.was.length > 20, "darunter ein Satz, der etwas sagt");
|
||||
ok(bild.kacheln > 0, `${bild.kacheln} Kacheln werden erklaert`);
|
||||
ok(bild.mitLang > 0, `davon ${bild.mitLang} mit ausfuehrlichem Satz`);
|
||||
ok(bild.laedtWeg, "der Ladehinweis ist weg");
|
||||
ok(bild.busy !== "true", "und die Seite meldet sich nicht mehr als ladend");
|
||||
ok(bild.breite <= 390, `nichts sprengt das Handy (${bild.breite}px)`);
|
||||
ok(bild.ueberlauf === 0, `keine Kachel ragt hinaus (${bild.ueberlauf})`);
|
||||
ok(bild.zuKlein === 0, `jede ist mindestens 44px hoch (${bild.zuKlein} zu klein)`);
|
||||
|
||||
/* SIE MUSS AUCH HINFUEHREN. Eine Erklaerung ohne Weg ist ein
|
||||
Prospekt. */
|
||||
await pModi.seite.click(".w-kachel");
|
||||
await pModi.seite.waitForTimeout(700);
|
||||
const wohin = pModi.seite.url();
|
||||
ok(!wohin.includes("willkommen.html"),
|
||||
`ein Antippen fuehrt wirklich hin (${wohin.split("/").pop()})`);
|
||||
} finally { await pModi.ctx.close(); }
|
||||
|
||||
/* ---- DogFather, am Rechner ---- */
|
||||
const pChef = await alsSeite("admin", "CODE-DOGI-0001");
|
||||
try {
|
||||
const bild = await pChef.seite.evaluate(() => ({
|
||||
gruss: document.getElementById("gruss")?.textContent || "",
|
||||
kacheln: document.querySelectorAll(".w-kachel").length,
|
||||
gruppen: [...document.querySelectorAll(".w-gruppe__titel")].map((t) => t.textContent),
|
||||
hinweise: document.querySelectorAll(".w-hinweis").length,
|
||||
}));
|
||||
ok(bild.gruss.includes("Filipe"), `sein Name steht oben ("${bild.gruss}")`);
|
||||
ok(bild.gruppen.length >= 2,
|
||||
`nach Gruppen geordnet (${bild.gruppen.join(" · ")})`);
|
||||
ok(bild.hinweise > 0, `mit ${bild.hinweise} Hinweisen zu seiner Rolle`);
|
||||
ok(bild.kacheln > 10, `und allem, was er hat (${bild.kacheln})`);
|
||||
} finally { await pChef.ctx.close(); }
|
||||
|
||||
ok(seitenFehler.length === 0, seitenFehler.length === 0
|
||||
? "kein Skript ist unterwegs gestolpert"
|
||||
: `Skriptfehler: ${seitenFehler.join(" | ")}`);
|
||||
|
||||
await browser.close();
|
||||
await new Promise((r) => vorbau.close(r));
|
||||
}
|
||||
|
||||
melde(fehler === 0
|
||||
? "\nAlles in Ordnung.\n"
|
||||
: `\n${fehler} FEHLER.\n`);
|
||||
/* ---------- Abschluss ------------------------------------------------ */
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
melde(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`);
|
||||
process.exit(fehler ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user