Filipe: "dan perfektionierst du eine neue wo diagramme texte und so zu
jedem modi gemacht wird und diese kategorie kannst du entwicklung nenen."
1. DAS EIGENTLICHE PROBLEM WAR, DASS ES KEINE VERGANGENHEIT GAB.
`entwicklung_stand` hat den Schluessel (person, punkt, beurteiler) und
wird bei jeder Aenderung UEBERSCHRIEBEN -- dort richtig, eine Beobachtung
ueber einen Menschen soll aktuell sein. Nur: Damit existierte nichts,
woraus sich ein Verlauf zeichnen liesse. Nachgemessen: 43 Zeilen Stand,
NULL Zeilen Geschichte. Ein Diagramm ueber einen einzigen Zeitpunkt ist
kein Diagramm, sondern ein Balken.
Neu: `entwicklung_lauf` haengt jede Aenderung an, statt zu ueberschreiben.
Sie steht in workspace-entwicklung.js, bei der Tabelle, zu der sie
gehoert -- die Auswertung liest nur. Andersherum gaebe es einen Ring
zwischen zwei Modulen, und Ringe halten in JavaScript genau so lange,
bis jemand eine Konstante auf oberster Ebene benutzt.
KEIN ANLASS, NIE. Neben jedem "da hakt es" steht ein Satz, woran man es
gemerkt hat -- fuer ein Gespraech aufgeschrieben. Eine Chronik dieser
Saetze waere das Belastendste, was dieses Haus speichern kann. Die Spur
hat gar keine Spalte dafuer, und die Pruefung liest die TABELLE, nicht
die Ausgabe: Eine Spalte, die es gibt, wird eines Tages gefuellt.
Die Startaufnahme uebernimmt den heutigen Stand mit seinen ECHTEN Daten,
damit die Seite am ersten Tag nicht leer ist -- und zaehlt ausdruecklich
NICHT als Bewegung. Der Tag, an dem die Spur entstand, war kein Tag, an
dem 43 Dinge gleichzeitig passiert sind.
2. WAS GEZEIGT WIRD: BEWEGUNG, KEINE NOTE.
Keine Punktzahl, kein Prozentwert, kein Durchschnitt, keine Rangliste,
kein Vergleich zwischen zwei Menschen. Das ist die Recherche, nicht
Vorsicht: Ueberrechtfertigung (Deci), sozialer Vergleich senkt die
Leistung derer, die schlechter dastehen, Schwaechenfokus ebenso.
Stattdessen das progress principle (Amabile/Kramer, ~12 000
Tagebucheintraege): Sichtbarer Fortschritt ist der staerkste einzelne
Treiber. Deshalb Diagramm 1 "Was sich bewegt hat" -- zwei Richtungen um
eine Nulllinie, ein Massstab fuer beide. Diagramm 2 der Streifen (Punkt
x Monat), fortgeschrieben, aber nach 90 Tagen sichtbar verblasst.
Und die Texte in dieser Reihenfolge: was sich bewegt hat, was traegt,
erst danach was liegt.
Selbst gezeichnet, kein Diagrammpaket: 60 kB ueber die Leitung von
jemandem, der mit dem Handy im Stream steht -- und es braechte eigene
Farben neben die Hausfarben. Jede Zelle traegt ihr Zeichen als Text,
jeder Balken seine Zahl, darunter eine Legende: Die Farbe ist nie die
Auskunft.
Zwei Gesichter: Die Leitung sieht die Karten, ein Modi ausschliesslich
die Bewegung in der eigenen -- ohne Namen, ohne Anlass, ohne Streifen.
3. NEBENBEFUND, GROESSER ALS ERWARTET: ELF SEITEN OHNE FARBE.
Der Umbau von heute frueh heisst "Jede Seite traegt die Farbe ihrer
Kachel". Nachgemessen traf das auf 18 von 34 Seiten zu. Elf Seiten mit
echter Kachel gingen leer aus -- ausgerechnet die von Team Dogi:
befinden, bewerben, bewerbungen, entwicklung, hilfe, rechte, talente,
teamlage, teilen, treff-moderation, treff-regeln, unsere-seiten.
Der Grund: `zuSeite()` sucht nur in `Bereiche.GRUPPEN`, und dort stehen
ausschliesslich die Kacheln der Agentur. Die von Team Dogi liefert der
Server (`ich.bereiche`, `ich.bereiche_zusatz`). Auffallen konnte das
nicht -- eine Seite ohne Farbe sieht aus wie eine, die eben keine hat.
kopf.js faerbt jetzt zweimal: sofort aus GRUPPEN (Agenturseiten ohne
Flackern wie bisher), und noch einmal aus den Serverkacheln, sobald
`werZeigen` sie bringt. Nur, wenn beim ersten Mal nichts gefunden wurde.
pruef-buehne misst den Kontrast an echten Bildpunkten und kannte diese
zwoelf Seiten nicht. Jetzt stehen sie in der Liste; ihre Frist waechst
mit der Laenge, statt als feste Zahl irgendwann zu reissen. Gemessen
fuer werdegang.html: 4,83:1 am Computer, 6,22:1 am Handy.
4. TON 38 SAH FREI AUS UND WAR ES NICHT.
Erster Anlauf war Ton 38, weil 37 die hoechste Nummer in workspace.js
ist. Gezaehlt in start.css sind es 39, alle vergeben. Genau davor warnt
das Farbwerkzeug im eigenen Kopf ("Ton 22, weil er frei AUSSAH").
Dabei aufgefallen: Die zwei Kacheln von heute Nacht wurden ohne das
Werkzeug gesetzt. Ton 39 lag 0,0362 von Ton 15 entfernt -- der engste
Abstand im ganzen Haus, zwei praktisch gleiche Cyan-Toene. Filipe dazu
mehrfach: "keine die sich irgendwie aehnlich sind".
Neues Werkzeug `tools/kachel-farbe-einzeln.mjs`: zieht EINE Farbe nach,
ohne die anderen anzufassen. Das grosse Werkzeug haette alle 40 neu
gefaerbt -- jede Kachel im Haus, ungefragt. Es bricht ab, wenn es nicht
alle Toene lesen kann: Der erste Anlauf las 31 von 40, weil einstellige
Nummern mit ZWEI Leerzeichen dastehen, und rechnete froehlich weiter.
Ergebnis: kleinster Abstand 0,0362 -> 0,0840, durch Aendern von zwei
Kacheln, die es erst seit gestern Nacht gibt.
5. anruf-probe.html lud `basis.css` und `kopf.css` -- beide gibt es
nicht. Ausgerechnet dort faellt es nicht auf, weil die Seite ihre eigene
Stilangabe im Kopf hat. pruef-struktur war deshalb rot (5 Befunde) und
ist jetzt gruen.
GEMESSEN: pruef-werdegang 95/0 (neu), pruef-entwicklung 43/0,
pruef-befinden 75/0, pruef-rechtetafel 19/0, pruef-zwischenspeicher 21/0,
pruef-start-ansicht 0 Fehler, pruef-struktur 0 Fehler (vorher 5),
pruef-buehne fuer werdegang.html 0 Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
627 lines
30 KiB
JavaScript
627 lines
30 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 { portMussFreiSein } from "./helfer-port.mjs";
|
|
|
|
const PORT = await portMussFreiSein(4422, "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";
|
|
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 NAMEN SORTIERT, NICHT NACH AKTIVITÄT. Eine Liste, die die mit
|
|
der meisten Bewegung nach oben stellt, ist eine Rangliste mit einem
|
|
anderen Namen. */
|
|
const namen = l.liste.map((p) => p.name);
|
|
ok(namen.join(",") === [...namen].sort((a, b) => a.localeCompare(b, "de")).join(","),
|
|
`alphabetisch (${namen.join(", ")})`);
|
|
ok(/kein vergleich/i.test(l.hinweis || ""),
|
|
"und die Seite sagt selbst, dass sie niemanden vergleicht");
|
|
}
|
|
|
|
/* =======================================================================
|
|
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");
|
|
const browser = await chromium.launch();
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
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: "127.0.0.1", path: "/" };
|
|
}));
|
|
|
|
await seite.goto(`http://127.0.0.1:${PORT}/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,
|
|
zeichenInZellen: [...document.querySelectorAll('.w-streifen__zelle')]
|
|
.every((z) => (z.textContent || '').trim().length > 0),
|
|
legende: document.querySelectorAll('.w-legende__punkt').length,
|
|
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 trägt ihr Zeichen als
|
|
Text -- wer Farben schlecht unterscheidet, liest dasselbe. */
|
|
ok(karte.zeichenInZellen, "jede Zelle trägt ihr Zeichen, nicht nur ihre Farbe");
|
|
ok(karte.legende === 2, `unter dem Diagramm steht eine Legende (${karte.legende} Einträge)`);
|
|
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. */
|
|
await seite.setViewportSize({ width: 412, height: 915 });
|
|
await seite.waitForTimeout(500);
|
|
const handy = await seite.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");
|
|
|
|
if (process.env.SCHIRM) {
|
|
await seite.setViewportSize({ width: 1280, height: 1100 });
|
|
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();
|
|
}
|
|
|
|
/* =======================================================================
|
|
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);
|