DER BEFUND
Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:
[uncaughtException] Error: spawnSync openssl ENOENT
Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.
GEMESSEN, NICHT VERMUTET
openssl liegt hier: C:\Program Files\Git\mingw64\bin\openssl.exe
C:\Program Files\Git\usr\bin\openssl.exe
im System-PATH steht: C:\Program Files\Git\cmd (nur git.exe)
im Benutzer-PATH: nichts mit Git
Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.
Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.
DIE REPARATUR
`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.
Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.
KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.
GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:
vorher (direkter Aufruf): ABSTURZ: spawnSync openssl ENOENT
nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes
Und eine echte Pruefung unter denselben Bedingungen:
PATH ohne mingw64 -> pruef-chat-neu-stelle
63 Pruefungen, 0 Fehler, 0 Abstuerze
Genau diese Datei stand im Nachtlauf mit ENOENT rot.
EINE WACHE DAGEGEN
pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.
Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.
DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT
`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.
NEBENBEI: pruef-community-sicht
Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.
Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.
GEPRUEFT
pruef-struktur ALLES IN ORDNUNG (mit der neuen Wache)
pruef-community-sicht 10 / 0
pruef-chat-neu-stelle 63 / 0 (mit dem PATH des Nachtlaufs)
pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
mess-quer EXIT 0
29 Dateien: node --check auf allen, kein direkter Aufruf mehr
835 lines
40 KiB
JavaScript
835 lines
40 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,
|
|
headers: { ...von.headers, host: CREW },
|
|
}, (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));
|
|
|
|
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);
|