Files
DogFatherGitandClaude Opus 5 09375047a7 Entwicklung: die Karte geht auf dem Zugetragenen auf
Filipe, 01.10.2026: „mach alles los." Damit auch der offene Punkt von
gestern: „Zugetragen" ist beim Aufmachen einer Karte die Vorgabe.

DER ALTE EINWAND BLEIBT GUELTIG -- er steht jetzt in der Bedingung
statt im Weg. Er lautete: „Ein Filter, der beim Öffnen schon etwas
versteckt, lässt einen Punkte suchen, die gestern noch da waren."
Richtig, und er trifft genau EINEN Fall: den, in dem gar nichts
zugetragen ist. Dann zeigte „Zugetragen" eine LEERE Karte, und eine
leere Karte sieht aus wie ein Fehler.

Also: Hat dieser Mensch zugetragene Punkte, steht der Filter darauf.
Hat er keine, steht er auf „Alle". Beides ist eine Auskunft, keines
ist eine Suche.

UND ER WIRD JE MENSCH NEU ENTSCHIEDEN. Bisher blieb der Filter ueber
den Personenwechsel hinweg stehen -- richtig, solange er eine
Erwartungsstufe meinte („wer Fortgeschritten gewaehlt hat, will das
weiter sehen"). „Zugetragen" ist eine Aussage UEBER DIESE PERSON;
sie mitzunehmen waere die falsche Frage.

GEMESSEN, beide Faelle:

  Diene (2 zugetragen)   -> Filter „Zugetragen", 2 Punkte
  VanVan (nichts)        -> Filter „Alle", 68 Punkte
  ein Klick auf „Alle"   -> wieder 68

Die Messung fragt jetzt, WAS DASTEHT, bevor jemand etwas anfasst.
Vorher klickte sie auf den Filter und zaehlte nach -- seit er die
Vorgabe ist, haette derselbe Klick ihn AUSgeschaltet. Sie haette das
Gegenteil gemessen und trotzdem eine Zahl gemeldet.

ZWEI NACHZUEGLER AUS DEM SICHT-WEG VON GESTERN

`pruef-werdegang` wurde rot: „403 Forbidden" im Browser, ohne Adresse.
Die Meldung sagte nicht, WORAN sie scheitert -- also sagt sie es
jetzt (`403 /workspace/api/chat/sicht`).

Die Ursache lag in der Messumgebung, nicht im Programm: Der
HTTPS-Vorbau der Pruefung reicht den Host OHNE Port weiter, waehrend
der Browser seine Herkunft MIT Port schickt. `gleicheHerkunft`
vergleicht beides und antwortet folgerichtig 403. mess-reaktion macht
es seit Langem richtig; zwei Dateien nicht.

Aufgefallen ist es erst jetzt, weil der Sicht-Weg der erste
zustandsaendernde POST ist, der auf JEDER Seite laeuft. Live stimmen
Herkunft und Host ueberein (beide ohne Port, Caddy reicht den Host
durch) -- nachgemessen.

GEPRUEFT
pruef-werdegang 103/0 (war 103/1), pruef-entwicklung 79/0,
pruef-entwicklung-kacheln 34/0, mess-vanvan-karte ohne ein einziges
ACHTUNG, pruef-zwischenspeicher 34/0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 00:52:07 +02:00

850 lines
41 KiB
JavaScript

/* =====================================================================
ENTWICKLUNG — die Auswertung je Person (19.09.2026)
Filipe: „dan perfektionierst du eine neue wo diagramme texte und so
zu jedem modi gemacht wird und diese kategorie kannst du entwicklung
nenen."
---------------------------------------------------------------------
WAS HIER GEPRÜFT WIRD — UND WARUM GENAU DAS
Eine Seite, die Diagramme über MENSCHEN zeigt, kann auf drei Arten
falsch sein, und nur eine davon ist ein Programmfehler:
1. SIE ZEIGT ZU VIEL. Der Anlass eines „da hakt es“ ist für ein
Gespräch aufgeschrieben. Landete er in der Spur, wäre jede
längst geklärte Beobachtung auf Dauer lesbar. Block 5 („Wie
geht es dir damit?“) darf überhaupt nicht hierherkommen.
2. SIE BEWERTET. Punktzahl, Prozentwert, Rangliste, Vergleich
zwischen zwei Menschen — nichts davon darf entstehen, auch
nicht versehentlich über einen Umweg.
3. SIE BEHAUPTET EINEN VERLAUF, DEN ES NICHT GIBT. Ein Diagramm
über einen einzigen Zeitpunkt ist kein Diagramm. Dann muss die
Seite das SAGEN — der dritte Ausgang.
ZU JEDER PRÜFUNG, DIE „ALLES IN ORDNUNG“ SAGEN KANN, GEHÖRT EINE
GEGENPROBE, die beweist, dass sie auch „nicht in Ordnung“ sagen
kann. Eine Prüfung, die immer bestätigt, bestätigt nichts.
Aufrufen mit: node server/pruef-werdegang.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta, "pruef-werdegang");
const ordner = mkdtempSync(join(tmpdir(), "ws-weg-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs";
import { zertifikatBauen } from "./helfer-openssl.mjs";
await import("./index.js");
notbremse(180_000, "pruef-werdegang");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const CREW = "crew.dogfather-universe.com";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const W = await import("./workspace-werdegang.js");
const kat = await import("./workspace-entwicklungs-punkte.js");
const rechte = await import("./rechte.js");
const ent = await import("./workspace-entwicklung.js");
const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
/* ABGELEITET, NICHT ABGESCHRIEBEN. Jede feste Zahl in einer Prüfung ist
eine Rechnung von gestern -- am 17.09. wurden aus sechs Fragen zwölf
und vier Prüfungen waren rot, ohne dass etwas kaputt war. */
const PUNKTE = kat.ENTWICKLUNG_FREMD;
const SELBST = kat.ENTWICKLUNG_SELBST;
let idDogi, idHand, idModi, idCreator;
{
const d = oeffne();
const schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
const anlegen = (name, rolle, code) => {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
};
idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
idModi = anlegen("Diene", "modi", "CODE-MODI-0001");
idCreator = anlegen("Tili", "creator", "CODE-CREA-0001");
d.close();
}
function ruf(pfad, host, methode, kopf = {}, koerper = null) {
return new Promise((fertig, schief) => {
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf } },
(antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
async function anmelden(rolle, code, host) {
const r = await ruf("/workspace/api/anmelden", host, "POST", {}, { rolle, code });
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
}
const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
const kHand = await anmelden("hand", "CODE-HAND-0001", CREW);
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet");
/* =======================================================================
1. DIE KACHEL HEISST „ENTWICKLUNG“ UND FÜHRT AUCH DORTHIN
Der ganze Auftrag hängt an einer Umbenennung: Die alte Kachel heißt
seit heute früh „Eure Aufgaben“, damit der Name frei wird. Wenn die
neue Kachel auf die alte Seite zeigt, ist genau nichts gewonnen.
======================================================================= */
melde("");
melde("=== 1. Die Kachel ===");
{
const ich = json(await ruf("/workspace/api/ich", CREW, "GET", { cookie: kDogi }));
const alle = [...(ich.bereiche || []), ...(ich.bereiche_zusatz || [])];
const neu = alle.find((k) => k.name === "Entwicklung");
ok(!!neu, "eine Kachel „Entwicklung“ steht bei DogFather");
ok(neu?.ziel === "werdegang.html",
`und führt auf die Auswertung (${neu?.ziel})`);
/* GEGENPROBE. Ohne sie bewiese die Zeile darüber nichts: Der Katalog
muss weiterhin erreichbar sein, unter seinem neuen Namen. */
const alt = alle.find((k) => k.ziel === "entwicklung.html");
ok(alt?.name === "Eure Aufgaben",
`der Katalog heißt weiter „${alt?.name}“ und liegt woanders`);
ok(neu?.ziel !== alt?.ziel, "beide Kacheln zeigen auf VERSCHIEDENE Seiten");
/* ZWEI KACHELN, ZWEI FARBEN. Gleiche Farbe hieße, dass man sie auf
der Startseite verwechselt -- und sie stehen nebeneinander. */
ok(neu?.ton && alt?.ton && neu.ton !== alt.ton,
`verschiedene Farbtöne (${alt?.ton} / ${neu?.ton})`);
const css = readFileSync(
new URL("../workspace/assets/css/start.css", import.meta.url), "utf8");
ok(new RegExp(`\\[data-ton="${neu?.ton}"\\]`).test(css),
`Ton ${neu?.ton} ist in start.css wirklich definiert`);
/* UND BEI DER RECHTEN HAND AUCH. Sie arbeitet an denselben Dingen --
eine Kachel nur bei DogFather wäre die Über-/Unterordnung, die es
in diesem Haus nicht gibt. */
const beiHand = json(await ruf("/workspace/api/ich", CREW, "GET", { cookie: kHand }));
ok([...(beiHand.bereiche || []), ...(beiHand.bereiche_zusatz || [])]
.some((k) => k.ziel === "werdegang.html"),
"die rechte Hand hat dieselbe Kachel");
}
/* =======================================================================
2. WER HINEINDARF
======================================================================= */
melde("");
melde("=== 2. Wer hineindarf ===");
{
const tafel = rechte.tafelJetzt();
const erlaubt = tafel["/workspace/werdegang.html"] || [];
ok(erlaubt.length > 0, "die Seite steht in der Rechtetafel");
ok(erlaubt.includes("admin") && erlaubt.includes("hand"),
"DogFather und die rechte Hand dürfen sie öffnen");
ok(erlaubt.includes("modi"),
"ein Modi auch — er sieht dort seine eigene Bewegung");
/* GEGENPROBE: Die Agentur hat gar keine Entwicklungskarte. Eine Seite,
die man öffnen kann und die dann leer bleibt, sieht aus wie ein
Fehler -- und ist einer. */
ok(!erlaubt.includes("creator") && !erlaubt.includes("gast"),
"die Agentur und die Community nicht");
ok(json(await ruf("/workspace/api/werdegang/liste", CREW, "GET", { cookie: kDogi })).liste !== undefined,
"DogFather bekommt die Liste");
const beiModi = await ruf("/workspace/api/werdegang/liste", CREW, "GET", { cookie: kModi });
ok(beiModi.code === 404,
`ein Modi bekommt sie nicht (${beiModi.code} — nicht 403: wer „kein Zugriff“ liest, fragt als Nächstes, wer denn dann)`);
const fremdeKarte = await ruf(`/workspace/api/werdegang/person/${idHand}`, CREW, "GET", { cookie: kModi });
ok(fremdeKarte.code === 404, `und auch keine fremde Karte (${fremdeKarte.code})`);
const ohne = await ruf("/workspace/api/werdegang/liste", CREW, "GET", {});
ok(ohne.code === 401, `ohne Anmeldung gar nichts (${ohne.code})`);
}
/* =======================================================================
3. DIE SPUR — SIE MERKT SICH, WAS DIE TABELLE VERGISST
======================================================================= */
melde("");
melde("=== 3. Die Spur ===");
{
const p1 = PUNKTE[0].schluessel, p2 = PUNKTE[1].schluessel;
const a = await ruf(`/workspace/api/entwicklung/person/${idModi}/${p1}`, CREW, "PUT",
{ cookie: kDogi }, { stufe: "waechst" });
ok(a.code === 200, "eine Einschätzung lässt sich setzen");
const b = await ruf(`/workspace/api/entwicklung/person/${idModi}/${p1}`, CREW, "PUT",
{ cookie: kDogi }, { stufe: "laeuft" });
ok(b.code === 200, "und ändern");
const d = oeffne();
const spur = d.prepare(
"SELECT * FROM entwicklung_lauf WHERE person_id = ? ORDER BY id").all(idModi);
ok(spur.length === 2, `beide Schritte stehen in der Spur (${spur.length})`);
ok(spur[0].vorher === null && spur[0].stufe === "waechst",
"der erste hat kein „vorher“ — er war der erste");
ok(spur[1].vorher === "waechst" && spur[1].stufe === "laeuft",
"der zweite weiß, was vorher dastand (waechst → laeuft)");
ok(spur.every((z) => z.nachgetragen === 0),
"beide sind beobachtet, nicht nachgetragen");
/* ==== DIE WICHTIGSTE PRÜFUNG DER GANZEN DATEI =====================
KEIN ANLASS IN DER SPUR. Neben einem „da hakt es“ steht ein Satz,
woran man es gemerkt hat. Eine Chronik dieser Sätze wäre das
Belastendste, was dieses Haus speichern kann.
GEPRÜFT WIRD DIE TABELLE SELBST, nicht die Ausgabe: Eine Spalte,
die es gibt, wird eines Tages gefüllt.
============================================================== */
const spalten = d.prepare("PRAGMA table_info(entwicklung_lauf)").all().map((z) => z.name);
ok(!spalten.includes("anlass"),
`die Spur hat gar keine Spalte für den Anlass (${spalten.join(", ")})`);
/* Und der Anlass steht trotzdem dort, wo er hingehört -- sonst wäre
die Zeile oben nur deshalb grün, weil die Funktion gar nichts tut. */
await ruf(`/workspace/api/entwicklung/person/${idModi}/${p2}`, CREW, "PUT",
{ cookie: kDogi }, { stufe: "hakt", anlass: "Am Dienstag im Stream, zweimal nacheinander." });
const stand = d.prepare(
"SELECT anlass FROM entwicklung_stand WHERE person_id = ? AND schluessel = ?")
.get(idModi, p2);
ok(/Dienstag/.test(stand?.anlass || ""),
"im Katalog steht er weiterhin — dort gehört er hin");
const inSpur = d.prepare(
"SELECT COUNT(*) AS n FROM entwicklung_lauf WHERE person_id = ? AND schluessel = ?")
.get(idModi, p2).n;
ok(inSpur === 1, "die Spur kennt den Punkt, nicht den Satz dazu");
/* BLOCK 5 DARF NIE HIERHERKOMMEN. „Wie geht es dir damit?“ beantwortet
nur die Person selbst; diese Antworten haben eine eigene Tabelle,
die niemand sonst liest. */
const selbst = SELBST[0].schluessel;
await ruf(`/workspace/api/entwicklung/mich/${selbst}`, CREW, "PUT",
{ cookie: kModi }, { stufe: "hakt" });
const inSpur2 = d.prepare(
"SELECT COUNT(*) AS n FROM entwicklung_lauf WHERE schluessel = ?").get(selbst).n;
ok(inSpur2 === 0, "eine Selbstauskunft aus Block 5 landet NICHT in der Spur");
/* Gegenprobe: sie ist trotzdem gespeichert worden. */
const selbstStand = d.prepare(
"SELECT stufe FROM entwicklung_stand WHERE person_id = ? AND schluessel = ?")
.get(idModi, selbst);
ok(selbstStand?.stufe === "hakt",
"sie steht aber sehr wohl im Katalog — die Zeile darüber ist keine Ausrede");
d.close();
}
/* =======================================================================
4. DIE ÜBERGÄNGE HEISSEN RICHTIG
Reine Rechnung, ohne Server -- und mit Gegenprobe: Wer nur prüft,
dass „vor“ herauskommt, wenn „vor“ gemeint ist, hätte auch mit einer
Funktion recht, die IMMER „vor“ sagt.
======================================================================= */
melde("");
melde("=== 4. Die Übergänge ===");
{
ok(W.artVon(null, "laeuft", 0) === "neu", "zum ersten Mal gesetzt → neu");
ok(W.artVon("hakt", "waechst", 0) === "vor", "hakt → wächst ist ein Schritt nach vorn");
ok(W.artVon("waechst", "laeuft", 0) === "vor", "wächst → läuft auch");
ok(W.artVon("laeuft", "hakt", 0) === "zurueck", "läuft → hakt ist einer zurück");
ok(W.artVon("unklar", "laeuft", 0) === "geklaert",
"aus „kann ich nicht sagen“ wird etwas → geklärt, nicht „vor“");
ok(W.artVon("laeuft", "unklar", 0) === "offen",
"und umgekehrt → offen, nicht „zurück“");
ok(W.artVon("laeuft", "laeuft", 0) === "gleich", "derselbe Wert ist keine Bewegung");
ok(W.artVon(null, "laeuft", 1) === "anfang",
"die Startaufnahme ist der Nullpunkt, nicht ein Ereignis");
/* GEGENPROBE zur Rangfolge: „kann ich nicht sagen“ hat KEINEN Rang.
Bekäme sie einen, würde „ich habe es nicht gesehen“ irgendwann wie
ein halbes „da hakt es“ gelesen. */
ok(W.artVon("unklar", "hakt", 0) === "geklaert",
"auch unklar → hakt ist geklärt und kein Rückschritt");
}
/* =======================================================================
5. DIE MONATSSPALTEN — UND DIE FALLE AM MONATSENDE
======================================================================= */
melde("");
melde("=== 5. Die Monatsspalten ===");
{
const s = W.monatsspalten();
ok(s.length === W.MONATE, `${s.length} Spalten`);
ok(s.join(",") === [...s].sort().join(","), "älteste zuerst");
/* DIE FALLE: Wer am 31. März einen Monat abzieht, landet in
JavaScript wieder im März -- den 31. Februar gibt es nicht, und
das Datum rollt über. Eine Spalte fiele aus, und zwar still. */
const ende = W.monatsspalten(6, new Date("2026-03-31T12:00:00Z"));
ok(ende.join(" ") === "2025-10 2025-11 2025-12 2026-01 2026-02 2026-03",
`am 31. März stimmen alle sechs Monate (${ende.join(" ")})`);
ok(new Set(ende).size === 6, "kein Monat doppelt, keiner übersprungen");
/* UND DIE ZWEITE FALLE: toISOString rechnet in UTC. Zwischen
Mitternacht und zwei Uhr am Monatsersten liefert das den VORMONAT.
Gerechnet wird deshalb in Europe/Berlin. */
ok(W.monatVon("2026-04-01T00:30:00+02:00") === "2026-04",
"am 1. April um 00:30 Ortszeit ist es April, nicht März");
ok(W.monatVon("2026-03-31T23:30:00+02:00") === "2026-03",
"und am 31. März um 23:30 noch März");
ok(W.monatVon("kaputt") === null, "ein unlesbares Datum ergibt null, nicht „jetzt“");
}
/* =======================================================================
6. DIE AUSWERTUNG — UND DER DRITTE AUSGANG
======================================================================= */
melde("");
melde("=== 6. Die Auswertung ===");
{
const d = json(await ruf(`/workspace/api/werdegang/person/${idModi}`, CREW, "GET", { cookie: kDogi }));
ok(d.person?.name === "Diene", "die Karte gehört der richtigen Person");
ok(d.gesetzt === 2, `zwei Punkte sind beantwortet (${d.gesetzt})`);
ok(d.gesamt === PUNKTE.length,
`von ${PUNKTE.length} im Katalog (${d.gesamt}) — gezählt, nicht abgeschrieben`);
/* DER DRITTE AUSGANG. Alles ist heute passiert, also gibt es genau
EINEN Monat mit Inhalt. Die Seite muss das SAGEN statt eine Linie
zwischen einem Wert und dem Rand zu zeichnen. */
ok(d.verlauf_moeglich === false,
"mit einem einzigen Monat wird kein Verlauf behauptet");
ok(d.monate_mit_inhalt === 1 && d.verlauf_ab === W.VERLAUF_AB,
`und die Antwort sagt, warum (${d.monate_mit_inhalt} von ${d.verlauf_ab} nötigen Monaten)`);
/* KEINE NOTE. Nirgends im ganzen Datenpaket darf eine Punktzahl, ein
Prozentwert oder ein Durchschnitt stehen. Geprüft am ROHTEXT --
ein Feld, das ich hier namentlich abfrage, müsste ich kennen; ein
neues, das jemand später hinzufügt, fiele durch. */
const roh = JSON.stringify(d).toLowerCase();
ok(!/"(punktzahl|score|prozent|durchschnitt|note|rang|platz)"/.test(roh),
"kein Feld heißt Punktzahl, Prozent, Durchschnitt, Note oder Rang");
ok(!/anlass/.test(roh), "und der Anlass kommt nicht mit");
ok(!roh.includes("dienstag"),
"auch nicht sein Inhalt — die Zeile darüber ist keine Wortklauberei");
/* KEIN FREMDER NAME. Auf einer Karte steht genau ein Mensch. */
ok(!roh.includes("rieke") && !roh.includes("tili"),
"auf der Karte steht kein anderer Name");
ok(typeof d.satz === "string" && d.satz.length > 20,
"ein Satz fasst die Karte zusammen");
ok(Array.isArray(d.texte), "und es gibt Texte dazu");
const arten = d.texte.map((t) => t.art);
ok(arten.includes("offen"),
"darunter der Hinweis, worüber die Auswertung schweigt");
/* DIE REIHENFOLGE IST DIE AUSSAGE: Was sich bewegt hat und was trägt,
steht VOR dem, was liegt. Rückmeldung, die auf Defizite zeigt,
senkt die Leistung messbar. */
const iVor = arten.indexOf("vor"), iLiegt = arten.indexOf("liegt");
ok(iLiegt === -1 || iVor === -1 || iVor < iLiegt,
"und was sich bewegt hat steht vor dem, was liegt");
}
/* =======================================================================
7. ZWEI MONATE — JETZT ENTSTEHT EIN VERLAUF
GEGENPROBE zu Abschnitt 6: Ohne sie wäre „kein Verlauf möglich“ nur
deshalb grün, weil die Funktion vielleicht NIE einen Verlauf sieht.
======================================================================= */
melde("");
melde("=== 7. Zwei Monate ===");
{
/* Eine Zeile mit einem Datum aus dem Vormonat -- von Hand in die
Spur, weil man die Uhr nicht vorstellen kann. Der Weg dorthin ist
derselbe, den die Anwendung benutzt. */
const d = oeffne();
const vormonat = new Date();
vormonat.setDate(15);
vormonat.setMonth(vormonat.getMonth() - 1);
const p3 = PUNKTE[2].schluessel;
d.prepare(`INSERT INTO entwicklung_lauf
(person_id, schluessel, von_id, stufe, vorher, nachgetragen, am)
VALUES (?,?,?,?,NULL,0,?)`)
.run(idModi, p3, idDogi, "hakt", vormonat.toISOString());
d.prepare(`INSERT INTO entwicklung_stand
(person_id, schluessel, von_id, stufe, anlass, am)
VALUES (?,?,?,?,NULL,?)`)
.run(idModi, p3, idDogi, "hakt", vormonat.toISOString());
d.close();
const k = json(await ruf(`/workspace/api/werdegang/person/${idModi}`, CREW, "GET", { cookie: kDogi }));
ok(k.verlauf_moeglich === true, "mit zwei Monaten gibt es einen Verlauf");
ok(k.spalten.length === W.MONATE, `${k.spalten.length} Monatsspalten`);
ok(k.streifen.length === 3, `drei Punkte im Streifen (${k.streifen.length})`);
/* FORTGESCHRIEBEN, NICHT LÜCKENHAFT: Was im Vormonat galt, gilt auch
diesen Monat -- niemand hat es geändert. Eine leere Zelle hieße
„keine Einschätzung“, und das wäre falsch. */
const zeile = k.streifen.find((z) => z.schluessel === p3);
const letzte = zeile.zellen.at(-1);
ok(letzte.stufe === "hakt",
"eine Einschätzung aus dem Vormonat gilt auch diesen Monat weiter");
ok(zeile.zellen.filter((c) => c.stufe).length === 2,
"davor steht nichts — vor der ersten Einschätzung gab es keine");
/* DIE BEWEGUNG ZÄHLT DAS RICHTIGE. */
const summe = (f) => k.bewegung.reduce((s, m) => s + m[f], 0);
ok(summe("neu") === 3, `drei Punkte wurden zum ersten Mal gesetzt (${summe("neu")})`);
ok(summe("vor") === 1, `einer ging nach vorn (${summe("vor")})`);
ok(summe("zurueck") === 0, `keiner zurück (${summe("zurueck")})`);
}
/* =======================================================================
8. DIE STARTAUFNAHME ZÄHLT NICHT ALS BEWEGUNG
Der Tag, an dem die Spur entstand, war kein Tag, an dem 43 Dinge
gleichzeitig passiert sind.
======================================================================= */
melde("");
melde("=== 8. Die Startaufnahme ===");
{
const d = oeffne();
const p4 = PUNKTE[3].schluessel;
const alt = new Date(); alt.setDate(15); alt.setMonth(alt.getMonth() - 2);
d.prepare(`INSERT INTO entwicklung_lauf
(person_id, schluessel, von_id, stufe, vorher, nachgetragen, am)
VALUES (?,?,?,?,NULL,1,?)`)
.run(idHand, p4, idDogi, "laeuft", alt.toISOString());
d.prepare(`INSERT INTO entwicklung_stand
(person_id, schluessel, von_id, stufe, anlass, am)
VALUES (?,?,?,?,NULL,?)`)
.run(idHand, p4, idDogi, "laeuft", alt.toISOString());
d.close();
const k = json(await ruf(`/workspace/api/werdegang/person/${idHand}`, CREW, "GET", { cookie: kDogi }));
const summe = (f) => k.bewegung.reduce((s, m) => s + m[f], 0);
ok(summe("neu") + summe("vor") + summe("zurueck") + summe("geklaert") === 0,
"eine nachgetragene Zeile ist keine Bewegung");
/* Gegenprobe: sie ist trotzdem da -- im Streifen. Sonst wäre die
Zeile oben nur deshalb grün, weil gar nichts gespeichert wurde. */
ok(k.streifen.length === 1 && k.streifen[0].zellen.some((c) => c.stufe === "laeuft"),
"im Streifen steht sie sehr wohl");
ok(/nichts bewegt/.test(k.satz),
`und der Satz sagt es ehrlich: ${(k.satz || "").slice(0, 60)}…`);
}
/* =======================================================================
9. DIE EIGENE SICHT — OHNE NAMEN, OHNE ANLASS, OHNE STREIFEN
======================================================================= */
melde("");
melde("=== 9. Die eigene Sicht ===");
{
const m = json(await ruf("/workspace/api/werdegang/mich", CREW, "GET", { cookie: kModi }));
ok(m.hat_karte === true, "ein Modi hat eine eigene Karte");
ok(Array.isArray(m.bewegung) && m.bewegung.length === W.MONATE,
"und sieht seine Bewegung über dieselben Monate");
ok(m.streifen === undefined,
"aber KEINEN Streifen — welcher Punkt wackelte, sagt ihm nur ein Mensch");
const roh = JSON.stringify(m).toLowerCase();
ok(!roh.includes("filipe") && !roh.includes("rieke"),
"kein Name eines Beurteilers");
ok(!roh.includes("dienstag"), "kein Anlasstext");
ok(/frag/i.test(m.hinweis || ""),
"stattdessen der Hinweis, danach zu fragen");
/* Seine eigene Antwort aus Block 5 darf hier nicht auftauchen -- sie
hat er über SICH gegeben, nicht jemand über ihn. */
ok(m.bewegung.reduce((s, x) => s + x.neu, 0) === 3,
"gezählt werden nur die Einschätzungen anderer");
/* GEGENPROBE: DogFather hat keine Karte -- und bekommt einen Satz
statt einer leeren Seite. */
const beiDogi = json(await ruf("/workspace/api/werdegang/mich", CREW, "GET", { cookie: kDogi }));
ok(beiDogi.hat_karte === false, "für DogFather wird keine Karte geführt");
}
/* =======================================================================
10. DIE LISTE VERGLEICHT NIEMANDEN
======================================================================= */
melde("");
melde("=== 10. Die Liste ===");
{
const l = json(await ruf("/workspace/api/werdegang/liste", CREW, "GET", { cookie: kDogi }));
ok(l.liste.length === 2, `zwei Karten — die rechte Hand und der Modi (${l.liste.length})`);
/* GEGENPROBE: Ein Creator steht NICHT drin. Für die Agentur wird
keine Entwicklungskarte geführt. */
ok(!l.liste.some((p) => p.name === "Tili"), "ein Creator steht nicht darin");
ok(!l.liste.some((p) => p.name === "Filipe"), "DogFather auch nicht");
/* NACH ROLLE, DANN NACH NAMEN -- UND NICHT NACH AKTIVITAET
(nachgezogen 21.09.2026).
Hier stand "alphabetisch", und die Zeile war rot: Die Liste kam
als "Rieke, Diene" zurueck, also nicht alphabetisch. Nachgesehen
war das kein Mangel, sondern eine Entscheidung vom 19.09.2026.
Filipe damals: "die reihenfolge der listen soll immer angepasst
sein hatten wir doch schon. die rechte hand rolle immer zuerst
und dan die modis."
Der Server sortiert seitdem nach `ROLLEN_SORTIERUNG`, dann nach
Namen. Die Pruefung ist zwei Tage lang rot gewesen, weil sie die
alte Regel weiter verlangt hat -- und eine rote Zeile, die rot
bleibt, bringt Menschen dazu, Rot zu ueberlesen.
WAS BLEIBT, IST DIE EIGENTLICHE AUSSAGE: Sortiert wird nach etwas,
das nichts ueber die Person sagt. Eine Liste, die die mit der
meisten Bewegung nach oben stellt, waere eine Rangliste mit einem
anderen Namen. Eine Sortierung nach ROLLE ist das nicht -- sie
sagt nur, welche Aufgabe jemand hat.
ABGELEITET, NICHT ABGESCHRIEBEN: Die Reihenfolge kommt aus
derselben Konstante, die der Server benutzt. Wer dort eine Rolle
verschiebt, verschiebt sie hier mit -- und diese Zeile wird nicht
wieder rot, ohne dass etwas kaputt ist. */
const { ROLLEN_REIHE } = await import("./workspace.js");
const platz = (r) => {
const i = ROLLEN_REIHE.indexOf(r);
return i === -1 ? ROLLEN_REIHE.length : i;
};
const soll = [...l.liste].sort((a, b) =>
platz(a.rolle) - platz(b.rolle) || a.name.localeCompare(b.name, "de"));
const ist = l.liste.map((p) => `${p.rolle}:${p.name}`).join(", ");
ok(ist === soll.map((p) => `${p.rolle}:${p.name}`).join(", "),
`nach Rolle, dann nach Namen (${ist})`);
/* GEGENPROBE: Dass die Liste ueberhaupt MEHRERE Rollen enthaelt --
sonst waere "nach Rolle sortiert" bei einer einzigen Rolle
trivial wahr, und die Zeile darueber pruefte nur noch die
Namenssortierung. */
ok(new Set(l.liste.map((p) => p.rolle)).size >= 2,
`und es sind wirklich mehrere Rollen darin `
+ `(${[...new Set(l.liste.map((p) => p.rolle))].join(", ")})`);
ok(/kein vergleich/i.test(l.hinweis || ""),
"und die Seite sagt selbst, dass sie niemanden vergleicht");
/* DER SATZ UEBER EINER GRUPPE KOMMT VOM SERVER (neu am 21.09.2026).
Er stand bis dahin als Liste in werdegang.js -- also in einer
Datei, die jeder im Netz ohne Anmeldung herunterladen kann, mit
den Rollennamen als Schluessel. Verschoben wurde er, weil
Rollennamen der Community dort nicht hingehoeren.
WARUM DAS HIER GEMESSEN WIRD: Beim Verschieben kann man eine
Erklaerung leicht ganz verlieren -- die Oberflaeche zeigt dann
einfach keine Zeile mehr, ohne Fehlermeldung. Eine Gruppe mit
Ueberschrift und ohne Erklaerung sieht nicht kaputt aus, sie sieht
nur aermer aus. Genau solche Verluste faellt niemandem auf. */
const mitSatz = l.liste.filter((p) => p.rolle_satz && p.rolle_satz.length > 10);
ok(mitSatz.length === l.liste.length,
`jede Karte bringt den Satz ihrer Gruppe mit (${mitSatz.length} von ${l.liste.length})`);
/* GEGENPROBE: Er darf nicht fuer alle derselbe sein -- sonst haette
das Verschieben aus zwei Saetzen einen gemacht, und die Pruefung
darueber waere trotzdem gruen. */
const verschieden = new Set(l.liste.map((p) => p.rolle_satz)).size;
ok(verschieden === new Set(l.liste.map((p) => p.rolle)).size,
`und jede Rolle hat ihren eigenen (${verschieden} Saetze auf `
+ `${new Set(l.liste.map((p) => p.rolle)).size} Rollen)`);
/* UND DIE OBERFLAECHE DARF IHN NICHT MEHR SELBST KENNEN. Bliebe die
alte Liste dort stehen, waere nichts gewonnen -- der Rollenname
stuende weiter in einer ausgelieferten Datei, und beide Fassungen
wuerden auseinanderlaufen. */
const { readFileSync } = await import("node:fs");
const vorne = readFileSync(
new URL("../workspace/assets/js/werdegang.js", import.meta.url), "utf8");
ok(!/GRUPPENSATZ\s*=/.test(vorne),
"und die Oberflaeche fuehrt keine eigene Liste mehr");
}
/* =======================================================================
11. IM BROWSER
Der Teil, den keine Schnittstellenprüfung ersetzt: Zeichnet die Seite
wirklich etwas, und bleibt die Konsole still?
UND DER NEBENBEFUND VOM 19.09.: Bis heute bekamen elf Seiten von
Team Dogi KEINE Kachelfarbe, weil `zuSeite()` nur die Agenturliste
kennt. Hier wird nachgemessen, dass diese Seite ihre bekommt.
======================================================================= */
melde("");
melde("=== 11. Im Browser ===");
{
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
/* AUF DER ECHTEN ADRESSE MESSEN (berichtigt 22.09.2026).
==================================================================
Bis heute oeffnete dieser Abschnitt `http://127.0.0.1/...` -- also
die ADRESSE DER AGENTUR. Auf der ist `person.haus` nicht "crew",
und `bereicheFuer()` liefert eine andere Kachelliste. Die Kachel
zu dieser Seite steht dort nicht drin, also fand `kopf.js` keine
Farbe: `data-ton=null`.
Drei Pruefungen waren deshalb rot, und zwar dauerhaft -- an einer
Seite, die in Wirklichkeit in Ordnung ist. Nachgemessen: Alle vier
Rollen (admin, hand, linke, modi) HABEN die Kachel, sobald das
Haus stimmt.
Genau dieselbe Falle wie am 21.09.2026 bei pruef-rueckmeldung.
Dort ging es ueber `node:http` mit Host-Kopf; ein Browser kann das
nicht (Chromium verbietet den Host-Kopf). Der Weg im Haus ist ein
selbst unterschriebener Vorbau plus `--host-resolver-rules` --
derselbe wie in pruef-willkommen und pruef-zuteilung. */
const { createServer: httpsServer } = await import("node:https");
const { request: httpWeiter } = await import("node:http");
const { execFileSync } = await import("node:child_process");
const { mkdtempSync: tempOrdner, readFileSync: liesDatei } = await import("node:fs");
const { tmpdir: tempWurzel } = await import("node:os");
const { join: pfad } = await import("node:path");
const zOrdner = tempOrdner(pfad(tempWurzel(), "wg-zeugnis-"));
const zKey = pfad(zOrdner, "k.pem"), zCrt = pfad(zOrdner, "z.pem");
zertifikatBauen(zKey, zCrt, CREW);
const HTTPS_PORT = PORT + 1;
const vorbau = httpsServer(
{ key: liesDatei(zKey), cert: liesDatei(zCrt) },
(von, an) => {
const weiter = httpWeiter({
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
/* MIT PORT (01.10.2026). Der Browser schickt seine
Herkunft als `https://crew...:PORT`; wer den Host hier
ohne Port weitergibt, laesst `gleicheHerkunft` die
beiden vergleichen und 403 antworten. So macht es
mess-reaktion seit Langem richtig -- gefunden hat es
der erste POST, der auf JEDER Seite laeuft. */
headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` },
}, (antwort) => { an.writeHead(antwort.statusCode, antwort.headers); antwort.pipe(an); });
weiter.on("error", () => { try { an.destroy(); } catch { /* Zugabe */ } });
von.on("error", () => {});
von.pipe(weiter);
});
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
const SEITE = `https://${CREW}:${HTTPS_PORT}`;
const browser = await chromium.launch({
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
ignoreHTTPSErrors: true,
});
const kontext = await browser.newContext({
viewport: { width: 1280, height: 1000 }, ignoreHTTPSErrors: true });
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); });
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
/* WELCHE ADRESSE ES WAR (01.10.2026). „403 (Forbidden)" allein
nennt den Weg nicht -- und dann sucht man ihn im ganzen Haus.
Eine Meldung, die nicht sagt, WORAN sie scheitert, ist der
dritte Ausgang ohne Auskunft. */
seite.on("response", (a) => {
if (a.status() >= 400 && !/favicon/i.test(a.url())) {
konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`);
}
});
await kontext.addCookies(kDogi.split("; ").filter(Boolean).map((teil) => {
const i = teil.indexOf("=");
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: CREW, path: "/" };
}));
await seite.goto(`${SEITE}/workspace/werdegang.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const liste = await seite.evaluate(() => ({
karten: document.querySelectorAll('.w-karte').length,
hinweis: document.getElementById('liste-hinweis')?.textContent || '',
ton: document.body.dataset.ton || null,
buehne: document.body.dataset.buehne || null,
zeichen: !!document.querySelector('.kopf-zeichen'),
}));
ok(liste.karten === 2, `zwei Karten stehen auf der Seite (${liste.karten})`);
ok(/kein vergleich/i.test(liste.hinweis), "der Hinweis steht sichtbar darüber");
ok(liste.ton === "40",
`die Seite trägt die Farbe ihrer Kachel (data-ton=${liste.ton}) — das ging bis heute nur auf den Agenturseiten`);
ok(liste.buehne === "wald", `und ihre Bühne (${liste.buehne})`);
ok(liste.zeichen, "das Zeichen der Kachel steht im Kopf");
await seite.locator('.w-karte').first().click();
await seite.waitForTimeout(700);
const karte = await seite.evaluate(() => ({
name: document.getElementById('karte-name')?.textContent || '',
balken: document.querySelectorAll('.w-bewegung__balken').length,
zellen: document.querySelectorAll('.w-streifen__zelle').length,
/* GETRENNT NACH GESETZT UND LEER (21.09.2026). Vorher wurde
verlangt, dass JEDE Zelle Text traegt -- auch die leeren. Seit
dem 19.09. sind die aber absichtlich leer. */
zellenMitStufe: [...document.querySelectorAll('.w-streifen__zelle')]
.filter((z) => z.dataset.stufe && z.dataset.stufe !== 'leer').length,
zeichenInZellen: [...document.querySelectorAll('.w-streifen__zelle')]
.filter((z) => z.dataset.stufe && z.dataset.stufe !== 'leer')
.every((z) => (z.textContent || '').trim().length > 0),
leereMitZeichen: [...document.querySelectorAll('.w-streifen__zelle')]
.filter((z) => z.dataset.stufe === 'leer'
&& (z.textContent || '').trim().length > 0).length,
legende: document.querySelectorAll('.w-legende__punkt').length,
legendeStreifen: [...document.querySelectorAll(
'.w-legende--streifen .w-legende__zeichen')].map((z) => z.dataset.stufe),
texte: document.querySelectorAll('.w-text').length,
breiter: document.documentElement.scrollWidth > document.documentElement.clientWidth,
}));
ok(/Diene|Rieke/.test(karte.name), `eine Karte öffnet sich (${karte.name})`);
ok(karte.zellen > 0, `der Streifen ist gezeichnet (${karte.zellen} Zellen)`);
/* DIE FARBE IST NIE DIE AUSKUNFT. Jede Zelle MIT EINER STUFE traegt
ihr Zeichen als Text -- wer Farben schlecht unterscheidet, liest
dasselbe.
BIS ZUM 21.09.2026 STAND HIER "jede Zelle", ohne Unterschied.
Seit dem 19.09. sind leere Zellen aber absichtlich leer; im
Quelltext steht woertlich: "Leer ist jetzt WIRKLICH leer -- kein
Zeichen, dafuer ein gestrichelter Rahmen. Man sieht das Fehlen,
statt es zu lesen." Die Pruefung verlangte also das Gegenteil
einer Entscheidung und war deshalb rot.
Beide Haelften werden jetzt gemessen, und die zweite ist die
wichtigere: Stuende in einer leeren Zelle doch ein Zeichen, waere
"noch nichts gesetzt" nicht mehr von "gesetzt" zu unterscheiden --
und das faellt beim Hinsehen nicht auf, weil es nach Inhalt
aussieht. */
ok(karte.zellenMitStufe > 0,
`${karte.zellenMitStufe} Zellen tragen eine Stufe`);
ok(karte.zeichenInZellen,
"und jede davon ihr Zeichen, nicht nur ihre Farbe");
ok(karte.leereMitZeichen === 0,
`die leeren bleiben leer (${karte.leereMitZeichen} mit Zeichen)`);
/* DIE LEGENDE WIRD ABGELEITET, NICHT GEZAEHLT (21.09.2026).
Hier stand `karte.legende === 2`. Die Zahl stimmte, solange es
zwei Eintraege gab; inzwischen sind es sieben -- zwei fuer die
Bewegungsrichtung und fuenf fuer den Streifen (vier Stufen plus
"noch nichts gesetzt"). Wieder eine Rechnung von gestern.
Gefragt wird jetzt, was die Legende leisten muss: Zu JEDER Stufe,
die der Server kennt, gehoert ein Eintrag -- und einer fuer den
leeren Zustand. Kommt eine Stufe dazu, waechst die Erwartung von
selbst mit. Die Stufen kommen aus derselben Auskunft, aus der die
Seite sie baut; abgeschrieben waere sie die naechste Zahl von
gestern. */
/* DIE STUFEN WERDEN HIER NEU GEHOLT und nicht aus Abschnitt 10
mitgenommen: Jener Block hat seinen eigenen Geltungsbereich, und
eine Variable von dort waere hier schlicht nicht da. Beim ersten
Versuch war genau das der Fall -- die Pruefung brach mitten im
Browserteil ab, ohne Ergebnis. */
/* DIE STUFEN STEHEN AN DER EINZELNEN KARTE, nicht an der Liste --
nachgesehen in workspace-werdegang.js, nicht angenommen. Der
erste Versuch fragte die Liste und bekam null Stufen; gefangen
hat das die Zeile darunter, die die Zahl in der Bedingung hat. */
const wenListe = json(await ruf("/workspace/api/werdegang/liste", CREW, "GET", { cookie: kDogi }));
const wessen = wenListe.liste?.[0]?.id;
const fuerLegende = json(await ruf(
`/workspace/api/werdegang/person/${wessen}`, CREW, "GET", { cookie: kDogi }));
const stufenSoll = (fuerLegende.stufen || []).map((x) => x.schluessel);
ok(stufenSoll.length > 0,
`der Server kennt ${stufenSoll.length} Stufen (${stufenSoll.join(", ")})`);
const legendeSoll = [...stufenSoll, "leer"].join(",");
ok((karte.legendeStreifen || []).join(",") === legendeSoll,
`die Legende nennt jede davon und den leeren Zustand `
+ `(${(karte.legendeStreifen || []).join(", ") || "KEINE"})`);
ok(karte.legende === stufenSoll.length + 1 + 2,
`insgesamt ${karte.legende} Eintraege -- ${stufenSoll.length} Stufen, `
+ "der leere Zustand und zwei fuer die Bewegungsrichtung");
ok(karte.texte > 0, `und die Texte dazu (${karte.texte})`);
ok(!karte.breiter, "nichts steht über den rechten Rand hinaus");
/* HANDY. Zwölf Seiten hatten am 06.09. genau hier ihren Fehler: ein
Knopf lag über einem anderen, weil eine feste Schwelle nicht mehr
passte. */
/* EIN EIGENER KONTEXT MIT FINGER (29.09.2026).
Hier stand `seite.setViewportSize({ width: 412 })`. Das aendert
nur die Groesse: `hasTouch` gehoert zum KONTEXT und laesst sich
danach nicht mehr setzen. Ohne ihn meldet der Browser
`pointer: fine`, und KEINE Regel aus `@media (pointer: coarse)`
greift -- dort stehen die 44-Pixel-Beruehrziele, also genau die
Groessen, deren Ueberlappung dieser Abschnitt sucht.
Die Anmeldung wandert als Keks mit, die Adresse kommt von der
Seite selbst -- keine zweite Wahrheit darueber, was gemessen
wird. */
const handyKontext = await browser.newContext({
viewport: { width: 412, height: 915 }, hasTouch: true, isMobile: true });
await handyKontext.addCookies(await kontext.cookies());
const handySeite = await handyKontext.newPage();
await handySeite.goto(seite.url(), { waitUntil: "networkidle" });
await handySeite.waitForTimeout(500);
const handy = await handySeite.evaluate(() => ({
breiter: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
balkenSichtbar: [...document.querySelectorAll('.w-bewegung__balken')]
.every((b) => b.getBoundingClientRect().width >= 8),
}));
ok(!handy.breiter, "auf 412 px steht nichts über den Rand hinaus");
ok(handy.balkenSichtbar, "und die Balken sind noch breit genug, um sie zu sehen");
await handyKontext.close();
if (process.env.SCHIRM) {
/* `seite` war nie schmal -- das fruehere Zurueckstellen auf
1280 ist damit hinfaellig. */
await seite.waitForTimeout(400);
await seite.screenshot({
path: "C:/Users/qciga/Documents/Obelix/DogiHompage/pruef-werdegang.png",
fullPage: true,
});
}
ok(konsole.length === 0,
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 120) : "keine Fehler im Browser");
await browser.close();
await new Promise((r) => vorbau.close(r));
}
/* =======================================================================
12. DIE BEIDEN ROLLENLISTEN DÜRFEN NICHT AUSEINANDERLAUFEN
======================================================================= */
melde("");
melde("=== 12. Was zusammengehört ===");
{
const hier = W.KARTEN_ROLLEN.join(",");
const quelle = readFileSync(
new URL("./workspace-entwicklung.js", import.meta.url), "utf8");
const dort = (quelle.match(/const KARTEN_ROLLEN = \[([^\]]+)\]/) || [])[1] || "";
const sauber = dort.replace(/["\s]/g, "");
ok(sauber === hier,
`dieselben Rollen in beiden Dateien (${hier} / ${sauber})`);
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);