Entwicklung: die Auswertung je Person -- und elf Seiten ohne Farbe

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]>
This commit is contained in:
2026-09-19 10:22:02 +02:00
co-authored by Claude Opus 5
parent 0b73fbb501
commit 19e9f471d3
50 changed files with 3307 additions and 464 deletions
+4
View File
@@ -25,6 +25,9 @@ import { dateienRouter } from "./workspace-dateien.js";
import { bereicheRouter } from "./workspace-bereiche.js";
import { treffRouter } from "./workspace-treff.js";
import { entwicklungRouter } from "./workspace-entwicklung.js";
/* Die Auswertung UEBER den Katalog -- eigene Datei, eigener Gegenstand.
Sie liest nur; geschrieben wird die Spur in workspace-entwicklung.js. */
import { werdegangRouter } from "./workspace-werdegang.js";
import { rechteRouter, tafelLaden } from "./workspace-rechte.js";
import { aufbewahrungRouter, aufbewahrungStarten } from "./workspace-aufbewahrung.js";
import { auskunftRouter } from "./workspace-auskunft.js";
@@ -287,6 +290,7 @@ app.use(treffRouter);
-- beides nebeneinander ist Absicht: Der Katalog ist das, was man
regelmaessig durchgeht, die Notiz das, was dazwischen auffaellt. */
app.use(entwicklungRouter);
app.use(werdegangRouter);
/* Die Rechtetafel -- ansehen und umstellen (11.09.2026). */
app.use(rechteRouter);
app.use(aufbewahrungRouter);
+57 -4
View File
@@ -48,6 +48,56 @@ function dunkelsterStopp(backgroundImage) {
}
const DUNKELSTER_QUELLE = dunkelsterStopp.toString();
/* ZWOELF SEITEN KAMEN AM 19.09.2026 DAZU -- und zwar nicht, weil sie neu
sind, sondern weil sie bis dahin GAR KEINE Buehne hatten.
Der Umbau vom 19.09. frueh heisst "Jede Seite traegt die Farbe ihrer
Kachel". Nachgemessen traf das auf 18 von 34 Seiten zu: `zuSeite()`
sucht nur in `Bereiche.GRUPPEN`, und dort stehen ausschliesslich die
Kacheln der AGENTUR. Die Kacheln von Team Dogi und vom Treff liefert
der Server -- die kannte die Stelle nicht. Seit der Reparatur in
kopf.js bekommen auch sie Ton UND Buehne, und damit steht auf ihnen
zum ersten Mal Text ueber einem Foto. Genau das misst diese Datei.
NUR EINE TEILMENGE LAUFEN LASSEN: `BUEHNE_SEITEN=werdegang.html`.
Ohne die Angabe laeuft die ganze Liste -- das dauert spuerbar und
gehoert deshalb in einen abgesprochenen Lauf, nicht in jede
Zwischenpruefung. */
const ALLE_SEITEN = [
"start.html", "uebersicht.html", "personen.html", "content.html",
"aufgaben.html", "kalender.html",
/* Die zwoelf ohne Buehne, bis zum 19.09.2026: */
"werdegang.html", "entwicklung.html", "talente.html", "befinden.html",
"hilfe.html", "rechte.html", "teamlage.html", "teilen.html",
"bewerbungen.html", "treff-moderation.html", "treff-regeln.html",
"unsere-seiten.html",
];
const SEITEN = process.env.BUEHNE_SEITEN
? process.env.BUEHNE_SEITEN.split(",").map((s) => s.trim()).filter(Boolean)
: ALLE_SEITEN;
/* DRITTER AUSGANG: Wer sich vertippt, soll es merken, statt eine
Prüfung über null Seiten für bestanden zu halten. */
{
const unbekannt = SEITEN.filter((s) => !ALLE_SEITEN.includes(s));
if (!SEITEN.length || unbekannt.length) {
console.log("ABBRUCH: unbekannte Seite(n) in BUEHNE_SEITEN: " + unbekannt.join(", "));
process.exit(2);
}
}
console.log(`Gemessen werden ${SEITEN.length} von ${ALLE_SEITEN.length} Seiten.`);
/* WIE LANGE DAS DAUERN DARF -- aus der Listenlaenge, nicht aus dem Kopf.
Sechs Seiten auf zwei Bildschirmgroessen brauchten bisher knapp
220 Sekunden, also rund 18 s je Seite und Groesse. Aufgerundet auf 22,
damit ein langsamer Lauf nicht faelschlich als Fehlschlag endet.
Eine feste Zahl waere hier eine Zeitbombe: Mit den zwoelf neuen
Seiten waere die alte Frist gerissen, und der Abbruch saehe aus wie
ein Befund. */
const SEITEN_ZEIT = SEITEN.length * 2 * 22_000;
console.log(`Zeitrahmen: ${Math.round((100_000 + SEITEN_ZEIT) / 1000)} s.`);
const ordner = mkdtempSync(join(tmpdir(), "ws-buehne-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4193";
@@ -66,10 +116,14 @@ await import("./index.js");
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
notbremse(480_000, "pruef-buehne");
notbremse(120_000 + SEITEN_ZEIT, "pruef-buehne");
await new Promise((r) => setTimeout(r, 700));
const BASIS = "http://127.0.0.1:4193";
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 220_000).unref?.();
/* Die Frist waechst mit der Liste: 220 s reichten fuer sechs Seiten.
Eine feste Zahl waere hier eine Zeitbombe -- die zwoelf neuen
haetten sie gerissen, und der Abbruch saehe aus wie ein Befund. */
setTimeout(() => { console.log("ABBRUCH: Zeit ueberschritten"); process.exit(1); },
100_000 + SEITEN_ZEIT).unref?.();
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
@@ -109,8 +163,7 @@ const browser = await chromium.launch();
/* Der Kalender kam am 01.09.2026 dazu -- er bringt die meisten neuen
Farbflächen mit (vier Arten, Feiertagsfärbung, Anlassmarken), und
genau dort entscheidet sich, ob der hellere Hintergrund noch trägt. */
const SEITEN = ["start.html", "uebersicht.html", "personen.html", "content.html",
"aufgaben.html", "kalender.html"];
for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"]]) {
console.log(`\n=== ${wie} (${breite}x${hoehe})`);
+626
View File
@@ -0,0 +1,626 @@
/* =====================================================================
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);
+15
View File
@@ -345,6 +345,21 @@ export const SEITEN = {
"/workspace/befinden.html": ["admin", "hand", "modi"],
"/workspace/entwicklung.html": ["admin", "hand", "modi"],
/* DIE AUSWERTUNG (19.09.2026) -- Kachel "Entwicklung".
MIT "modi", obwohl die Liste aller Karten nur die Leitung sieht.
Die Seite hat zwei Gesichter: Wer das Team fuehrt, sieht die
Karten; wer dazugehoert, sieht ausschliesslich die Bewegung in der
EIGENEN -- ohne Namen, ohne Anlass, ohne wer-hat-was.
Dass die Person ihren eigenen Fortschritt sieht, ist kein
Zugestaendnis, sondern der Punkt: Sichtbarer Fortschritt ist nach
der Forschung der staerkste einzelne Treiber guter Arbeit. Die
Schranke dahinter sitzt in der Route (`/werdegang/liste` und
`/werdegang/person/:id` antworten einem Modi mit 404), nicht hier.
Das hier ist der Riegel davor, nicht statt. */
"/workspace/werdegang.html": ["admin", "hand", "modi"],
/* TALENTE. Hier stehen Namen von Menschen, die nichts davon wissen --
ausdruecklich OHNE "modi": Wer beobachtet wird, ohne es zu wissen,
hat ein Recht darauf, dass der Kreis klein bleibt. */
+161 -22
View File
@@ -13,7 +13,7 @@
---------------------------------------------------------------------
EINE EIGENE TABELLE — UND WARUM DER PLAN DARIN FALSCH LAG
Der Plan im Vault sagt: „zwei Spalten, keine neue Tabelle" — der
Der Plan im Vault sagt: „zwei Spalten, keine neue Tabelle“ — der
Stand solle in `punkt_stand` liegen, derselben Tabelle wie die
Checkliste. Nachgemessen geht das NICHT:
@@ -32,7 +32,7 @@
---------------------------------------------------------------------
DREI RIEGEL, DIE VON ANFANG AN STEHEN
1. BLOCK 5 GEHOERT DER PERSON. „Wie geht es dir damit?" beantwortet
1. BLOCK 5 GEHOERT DER PERSON. „Wie geht es dir damit?“ beantwortet
nur sie selbst — lesend UND schreibend. Auch DogFather nicht. Was
nach aussen dringt, ist eine Ampel ohne Namen.
@@ -50,10 +50,10 @@
UND EINE ABWEICHUNG VOM PLAN, DIE FILIPES SATZ GESCHULDET IST
Der Plan verlangt einen Anlass (die drei SBI-Felder) bei JEDEM Klick
ausser „kann ich nicht sagen". Bei 28 Punkten je Person waeren das 28
Textfelder — und „so einfach wie moeglich" war die andere Haelfte des
ausser „kann ich nicht sagen“. Bei 28 Punkten je Person waeren das 28
Textfelder — und „so einfach wie moeglich“ war die andere Haelfte des
Auftrags. Pflicht ist der Anlass deshalb dort, wo er etwas
entscheidet: bei **„Da hakt es"**. Bei „Laeuft" und „Waechst gerade"
entscheidet: bei **„Da hakt es“**. Bei „Laeuft“ und „Waechst gerade“
steht dasselbe Feld da, freiwillig — wer etwas Konkretes gesehen hat,
schreibt es hin, und beim Gespraech steht es da.
===================================================================== */
@@ -171,6 +171,55 @@ export function tabellen() {
CREATE INDEX IF NOT EXISTS idx_entwicklung_person
ON entwicklung_stand (person_id, am);
/* =================================================================
DIE SPUR (19.09.2026) — jede Aenderung, angehaengt statt ueberschrieben.
Die Tabelle darueber hat den Schluessel (person, punkt, beurteiler)
und wird bei jeder Aenderung UEBERSCHRIEBEN. Das ist dort richtig:
Eine Beobachtung ueber einen Menschen soll aktuell sein, nicht
historisch.
Nur: Damit existierte nichts, woraus man einen Verlauf zeichnen
koennte. Am 19.09. nachgemessen: 43 Zeilen Stand, NULL Zeilen
Geschichte. Ein Diagramm ueber einen einzigen Zeitpunkt ist kein
Diagramm, sondern ein Balken.
SIE STEHT HIER UND NICHT IN workspace-werdegang.js, obwohl sie
dort ausgewertet wird: Eine Tabelle gehoert zu dem Modul, das sie
SCHREIBT. 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 in der Tabelle
oben ein Satz, woran man es gemerkt hat. Der ist fuer ein
Gespraech aufgeschrieben. Eine CHRONIK dieser Saetze waere das
Belastendste, was dieses Haus speichern kann -- jede laengst
geklaerte Beobachtung bliebe auf Dauer lesbar. Dasselbe Prinzip
wie beim Befinden-Verlauf.
VORHER steht dabei, obwohl es aus der vorigen Zeile abzuleiten
waere: Fuer die Startaufnahme (siehe unten) gibt es kein
"vorher", und NULL sagt das, waehrend eine abgeleitete Kette es
verschweigen wuerde.
NACHGETRAGEN unterscheidet beobachtete Uebergaenge von der
Startaufnahme. Ohne dieses Feld saehe der Tag, an dem die Spur
entstand, wie ein Tag aus, an dem 43 Dinge gleichzeitig passiert
sind.
============================================================= */
CREATE TABLE IF NOT EXISTS entwicklung_lauf (
id INTEGER PRIMARY KEY AUTOINCREMENT,
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
schluessel TEXT NOT NULL,
von_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
stufe TEXT NOT NULL CHECK (stufe IN ('laeuft','waechst','hakt','unklar')),
vorher TEXT,
nachgetragen INTEGER NOT NULL DEFAULT 0,
am TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_lauf_person
ON entwicklung_lauf (person_id, am);
/* Ein angeklicktes Merkmal an einem Kandidaten.
KEIN von_id IM SCHLUESSEL, anders als oben -- und das ist
@@ -293,7 +342,7 @@ export function tabellen() {
liegen -- und die Standzeit an der Karte sagt zwar, DASS etwas
liegt, aber nicht, wer es aufheben sollte.
`zustaendig_seit` kommt mit: „seit gestern uebernommen" und „seit
`zustaendig_seit` kommt mit: „seit gestern uebernommen“ und „seit
drei Wochen uebernommen" sind zwei verschiedene Auskuenfte, und
die zweite ist die, die man sehen will. */
["zustaendig_id", "INTEGER"],
@@ -307,9 +356,83 @@ export function tabellen() {
}
} catch (f) { console.error(`[talente] Spalte '${spalte}':`, f?.message); }
}
startaufnahme();
bereit = true;
}
/** Die Spur einmalig aus dem heutigen Stand fuellen (19.09.2026).
*
* WARUM UEBERHAUPT: Ohne sie waere die Auswertungsseite am ersten Tag
* leer, und zwar fuer Monate -- bis jemand genug Einschaetzungen
* geaendert hat. Eine Seite, die nach dem Bauen leer bleibt, sieht aus
* wie ein Fehler und wird kein zweites Mal geoeffnet.
*
* WARUM DAS EHRLICH IST: Jede Zeile in `entwicklung_stand` traegt ihr
* echtes `am` -- den Tag, an dem jemand wirklich geklickt hat. Es wird
* nichts erfunden, nur uebernommen, was ohnehin dasteht. Was sich
* NICHT rekonstruieren laesst -- ob vorher schon etwas anderes
* dastand --, bleibt NULL und traegt `nachgetragen = 1`. Die
* Auswertung zaehlt diese Zeilen ausdruecklich NICHT als Bewegung:
* Der Tag der Startaufnahme ist der Nullpunkt, nicht ein Tag, an dem
* etwas passiert ist.
*
* GENAU EINMAL: nur wenn die Spur leer ist. Ein zweiter Lauf traege
* jeden Stand ein zweites Mal ein, und ab da zaehlte jede Bewegung
* doppelt.
*
* UND SIE DARF NIE DEN SERVER AUFHALTEN. Schlaegt sie fehl, ist die
* Auswertung duenn -- das ist aergerlich. Wuerde dadurch `tabellen()`
* scheitern, waere die ganze Entwicklungsseite mit 503 tot, und das
* waere ungleich schlimmer. */
function startaufnahme() {
try {
const da = db().prepare("SELECT COUNT(*) AS n FROM entwicklung_lauf").get();
if (da.n > 0) return;
const staende = db().prepare(
`SELECT person_id, schluessel, von_id, stufe, am
FROM entwicklung_stand ORDER BY am`).all();
if (!staende.length) return;
const rein = db().prepare(
`INSERT INTO entwicklung_lauf
(person_id, schluessel, von_id, stufe, vorher, nachgetragen, am)
VALUES (?,?,?,?,NULL,1,?)`);
let n = 0;
for (const s of staende) {
/* DIE SELBSTAUSKUNFT FAELLT HERAUS. Block 5 ("Wie geht es dir
damit?") hat mit dem Befinden-Verlauf eine eigene Tabelle, die
niemand sonst liest. Hier waere sie eine Aussage, die ein
Mensch ueber sich selbst gemacht hat -- und die ploetzlich in
einer Auswertung fuer die Leitung stuende. */
if (!ENTWICKLUNG_FREMD.some((x) => x.schluessel === s.schluessel)) continue;
rein.run(s.person_id, s.schluessel, s.von_id, s.stufe, s.am);
n++;
}
if (n) console.log(`[entwicklung] Startaufnahme: ${n} Staende in die Spur uebernommen.`);
} catch (f) {
console.error("[entwicklung] Startaufnahme:", f?.message);
}
}
/** Eine Aenderung in der Spur festhalten.
*
* SIE DARF NIE DEN KLICK VERHINDERN. Geht hier etwas schief, ist die
* Auswertung lueckenhaft. Wuerde dadurch die Einschaetzung nicht
* gespeichert, waere die Arbeit weg. Deshalb faengt sie ihren Fehler
* selbst und meldet ihn nur ins Protokoll der Konsole. */
function spurLegen({ personId, schluessel, vonId, stufe, vorher }) {
try {
db().prepare(
`INSERT INTO entwicklung_lauf
(person_id, schluessel, von_id, stufe, vorher, nachgetragen, am)
VALUES (?,?,?,?,?,0,?)`)
.run(personId, schluessel, vonId, stufe, vorher ?? null, jetzt());
} catch (f) {
console.error("[entwicklung] Spur:", f?.message);
}
}
/* =====================================================================
DIE HUERDEN
===================================================================== */
@@ -498,7 +621,7 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req,
aufgefallen ist.
Das ist keine Nebensaechlichkeit. Beim ersten Beurteilen ist
genau das die Frage: „Woher kommt der eigentlich?" -- und wer sie
genau das die Frage: „Woher kommt der eigentlich?“ -- und wer sie
nicht beantwortet bekommt, faengt bei null an, obwohl vier Wochen
Beobachtung vorliegen.
@@ -556,7 +679,7 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req,
Die Quellen zur laufenden Entwicklungsbegleitung sagen übereinstimmend
zweierlei: weg von der Bewertung, hin zum Gespräch -- und ein
Entwicklungsplan wirkt dann, wenn er an einer ECHTEN Aufgabe hängt.
Nicht „daran arbeiten wir", sondern etwas, das auf dem Aufgabenbrett
Nicht „daran arbeiten wir“, sondern etwas, das auf dem Aufgabenbrett
steht, einen Verantwortlichen hat und irgendwann erledigt ist.
Genau dieses Muster gibt es im Haus schon: Aus einer Idee auf einem
@@ -610,8 +733,8 @@ entwicklungRouter.post("/workspace/api/entwicklung/person/:id/:schluessel/schrit
const punkt = ENTWICKLUNG_FREMD.find((x) => x.schluessel === schluessel);
if (!punkt) return res.status(404).json({ fehler: "nicht_gefunden" });
/* NUR AUS EINEM „DA HAKT ES" WIRD EIN SCHRITT -- und nur aus dem
EIGENEN. Aus „läuft" einen Schritt zu machen hieße, jemandem
/* NUR AUS EINEM „DA HAKT ES“ WIRD EIN SCHRITT -- und nur aus dem
EIGENEN. Aus „läuft“ einen Schritt zu machen hieße, jemandem
Arbeit zu geben für etwas, das gut läuft; aus der Beobachtung
eines anderen hieße, in seinem Namen zu handeln. */
const stand = db().prepare(
@@ -682,7 +805,7 @@ entwicklungRouter.put("/workspace/api/entwicklung/person/:id/:schluessel",
if (!STUFEN_SATZ.has(stufe)) return res.status(400).json({ fehler: "unbekannte_stufe" });
/* DER ANLASS IST PFLICHT, WO ER ETWAS ENTSCHEIDET.
„Da hakt es" ohne Beispiel ist eine Meinung; mit Beispiel ist
„Da hakt es“ ohne Beispiel ist eine Meinung; mit Beispiel ist
es ein Gespraech. Bei den anderen beiden steht dasselbe Feld
freiwillig da. */
if (stufe === "hakt" && anlass.length < 10) {
@@ -692,6 +815,15 @@ entwicklungRouter.put("/workspace/api/entwicklung/person/:id/:schluessel",
});
}
/* WAS VORHER DASTAND -- VOR dem Schreiben gelesen.
Danach ist es weg: Die Tabelle ueberschreibt. Genau deshalb
gibt es die Spur, und genau deshalb steht diese Zeile hier
oben und nicht unten. */
const vorher = db().prepare(
`SELECT stufe FROM entwicklung_stand
WHERE person_id = ? AND schluessel = ? AND von_id = ?`)
.get(id, schluessel, req.person.id)?.stufe || null;
db().prepare(`INSERT INTO entwicklung_stand
(person_id, schluessel, von_id, stufe, anlass, am)
VALUES (?,?,?,?,?,?)
@@ -699,6 +831,13 @@ entwicklungRouter.put("/workspace/api/entwicklung/person/:id/:schluessel",
stufe = excluded.stufe, anlass = excluded.anlass, am = excluded.am`)
.run(id, schluessel, req.person.id, stufe, anlass || null, jetzt());
/* ERST SPEICHERN, DANN MITSCHREIBEN. Die Reihenfolge ist die
Aussage: Wenn die Spur scheitert, fehlt eine Zeile in einer
Auswertung. Wenn das Speichern scheitert, ist die Arbeit weg. */
spurLegen({
personId: id, schluessel, vonId: req.person.id, stufe, vorher,
});
res.json({ ok: true });
} catch (f) {
console.error("[entwicklung] Setzen:", f?.message);
@@ -851,7 +990,7 @@ entwicklungRouter.get("/workspace/api/entwicklung/meine-karte", (req, res) => {
/* WAS HIER HERAUSKOMMT UND WAS NICHT.
Heraus kommt: wie viele im Team gerade mindestens einen Punkt aus
Block 5 auf „da hakt es" stehen haben -- und ab zwei Personen auch
Block 5 auf „da hakt es“ stehen haben -- und ab zwei Personen auch
das nicht mehr genau, sondern als Spanne.
Nicht heraus kommt: wer, welcher Punkt, seit wann.
@@ -861,7 +1000,7 @@ entwicklungRouter.get("/workspace/api/entwicklung/meine-karte", (req, res) => {
daraus niemanden erkennen kann.
AB WELCHER ZAHL SIE UEBERHAUPT ETWAS SAGT: Bei einem Team von zwei
Personen waere „einer hat gerade viel" dasselbe wie ein Name.
Personen waere „einer hat gerade viel“ dasselbe wie ein Name.
Deshalb schweigt sie unter MINDESTENS_TEAM Personen -- mit Grund,
nicht stillschweigend. */
const MINDESTENS_TEAM = 3;
@@ -908,7 +1047,7 @@ entwicklungRouter.get("/workspace/api/entwicklung/ampel", nurLeitung, (req, res)
res.json({
sagbar: true, team, geantwortet,
/* Keine genaue Zahl: „einer" waere bei kleinen Teams wieder ein
/* Keine genaue Zahl: „einer“ waere bei kleinen Teams wieder ein
Name. Es gibt drei Antworten, und mehr braucht es nicht. */
lage: betroffen === 0 ? "ruhig" : betroffen === 1 ? "jemand" : "mehrere",
text: betroffen === 0
@@ -1180,7 +1319,7 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => {
heraussuchen, Kategorie und Stufe waehlen und uebernehmen.
Vier Schritte, die niemand macht, waehrend er gerade jemanden
aufnimmt. Genau dieses Muster hat heute schon einmal zu einer
Karte auf „Im Team" ohne Menschen darin gefuehrt.
Karte auf „Im Team“ ohne Menschen darin gefuehrt.
ES BLEIBT EIN ANGEBOT, KEINE AUTOMATIK. Filipes Entscheidung
bei der Live-Checkliste gilt hier genauso: „vierzehn Aufgaben,
@@ -1189,8 +1328,8 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => {
hier nur, WAS es gaebe -- angelegt wird auf Klick, eine
Kategorie auf einmal.
KEIN ROLLENNAME, nirgends. Die Kategorien heissen „chat",
„team", „technik" -- aus keiner davon laesst sich schliessen,
KEIN ROLLENNAME, nirgends. Die Kategorien heissen „chat“,
„team“, „technik“ -- aus keiner davon laesst sich schliessen,
fuer wen sie gedacht sind. Wer den Katalog nicht bekommen
darf, bekommt `null` und die Seite baut nichts; ein leeres
Feld sagt nichts, ein Feld mit sprechendem Namen saehe jeder
@@ -1205,7 +1344,7 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => {
/* WAS SIE SCHON HABEN -- je Person, in EINER Abfrage.
Ohne diese Angabe legt man beim zweiten Hinsehen dasselbe noch
einmal an; der Knopf sagt dann „6 Aufgaben" und meint die
einmal an; der Knopf sagt dann „6 Aufgaben“ und meint die
siebte bis zwoelfte. Der Server verhindert Doppelte nicht --
er soll es auch nicht, denn dieselbe Aufgabe ein zweites Mal
kann gewollt sein. Sichtbar muss es trotzdem sein. */
@@ -1444,7 +1583,7 @@ entwicklungRouter.put("/workspace/api/talente/:id/kuemmern", gleicheHerkunft,
} else {
/* Noch keine Stufenzeile -- die entsteht erst beim ersten
Schritt. Wer sich vorher kuemmern will, soll das koennen:
„Ich sehe ihn mir an" ist der haeufigste erste Schritt
„Ich sehe ihn mir an“ ist der haeufigste erste Schritt
ueberhaupt, und er faellt sonst durch. */
db().prepare(`INSERT INTO talent_stufe
(eintrag_id, stufe, seit, von_id, zustaendig_id, zustaendig_seit)
@@ -1506,7 +1645,7 @@ entwicklungRouter.put("/workspace/api/talente/:id/wieder", gleicheHerkunft,
/** Buddy und Datum prüfen. Gibt `{ fehlt: [...] }` oder `{ buddyId, schicht }`.
*
* EINE STELLE, ZWEI AUFRUFER: der Stufenwechsel auf „Probe" und das
* EINE STELLE, ZWEI AUFRUFER: der Stufenwechsel auf „Probe“ und das
* spaetere Aendern. Zwei Abschriften derselben Regeln waeren zwei
* Regeln, und die zweite ist immer die, die etwas durchlaesst. */
function uebergangPruefen(koerper, { pflicht }) {
@@ -1576,13 +1715,13 @@ entwicklungRouter.put("/workspace/api/talente/:id/stufe", gleicheHerkunft, (req,
/* AUF DIE PROBE KOMMT NIEMAND OHNE BUDDY UND DATUM.
In der Stufenbeschreibung stand „fester Buddy" seit dem ersten
In der Stufenbeschreibung stand „fester Buddy“ seit dem ersten
Tag -- ein Feld dafuer gab es nicht. Ein Versprechen ohne Feld
ist eines, an das sich nach zwei Wochen niemand erinnert, und
genau an dieser Stelle verlieren Teams die Leute, die sie gerade
gewonnen haben.
EINE ABSAGE MIT GRUND, nicht ein „ging nicht": Die Oberflaeche
EINE ABSAGE MIT GRUND, nicht ein „ging nicht“: Die Oberflaeche
fragt beides vorher ab, aber die Regel gehoert hierher. Was nur
im Browser steht, gilt nur fuer den, der den Browser benutzt. */
const p = stufe === "probe"
+753
View File
@@ -0,0 +1,753 @@
/* =====================================================================
ENTWICKLUNG — die Auswertung je Person (19.09.2026)
Filipe, mit dem Bildschirmfoto der alten Kachel:
„diese kategorie ist ja die aufgaben die ich an die modis habe,
also eher aufgabe und dan perfektionierst du eine neue wo
diagramme texte und so zu jedem modi gemacht wird und diese
kategorie kannst du entwicklung nenen.“
Die Umbenennung war der erste Teil und ist seit dem 19.09. früh
erledigt: Die alte Kachel heißt „Eure Aufgaben“, weil sie das ist —
ein Katalog dessen, was ein Modi können soll. Hier kommt der zweite
Teil: die Auswertung, die es noch nie gab.
---------------------------------------------------------------------
WARUM DIESE DATEI NICHT „entwicklung.js“ HEISST
`workspace-entwicklung.js` gibt es seit dem 11.09. und es ist der
KATALOG. Diese Datei ist die AUSWERTUNG darüber. Zwei Dateien mit
fast demselben Namen wären die zuverlässigste Art, in einem halben
Jahr die falsche zu ändern.
Dasselbe gilt für die Seite: `entwicklung.html` ist der Katalog
(„Eure Aufgaben“), `werdegang.html` ist diese Auswertung
(„Entwicklung“). Dass die Kachel anders heißt als die Datei, ist
Absicht und geprüft — `pruef-werdegang` verlangt, dass die Kachel
„Entwicklung“ auf `werdegang.html` zeigt und nicht woanders hin.
Ein Umbenennen der alten Datei wäre die sauberere Endform, aber es
wären zwei Änderungen in einer: Wenn danach etwas klemmt, weiß
niemand, welche von beiden es war.
---------------------------------------------------------------------
DAS EIGENTLICHE PROBLEM: ES GAB KEINE VERGANGENHEIT
`entwicklung_stand` hat den Schlüssel (person, punkt, beurteiler)
und wird bei jeder Änderung ÜBERSCHRIEBEN. Das ist dort richtig —
eine Beobachtung über einen Menschen soll aktuell sein, nicht
historisch. Nur: Damit existiert nichts, woraus man einen Verlauf
zeichnen könnte. Am 19.09.2026 nachgemessen standen 43 Zeilen in der
Tabelle und NULL Zeilen Geschichte.
Ein Diagramm über einen einzigen Zeitpunkt ist kein Diagramm,
sondern ein Balken. Deshalb legt diese Datei als Erstes an, was
gefehlt hat: eine Spur, die mitschreibt, statt zu überschreiben.
---------------------------------------------------------------------
WAS HIER AUSDRÜCKLICH NICHT ENTSTEHT
Keine Punktzahl. Kein Prozentwert. Kein Durchschnitt. Keine
Rangliste. Kein Vergleich zwischen zwei Menschen — nirgends, auch
nicht versteckt als „Team-Mittelwert“.
Das ist keine Vorsicht, das ist die Recherche:
* ÜBERRECHTFERTIGUNG (Deci 1971, Selbstbestimmungstheorie). Wer
für etwas, das er gern tut, eine äußere Bewertung bekommt,
verliert die innere Motivation. Die Ausnahme — und die ist der
ganze Trick — ist Rückmeldung, die KOMPETENZ BESTÄTIGT, ohne eine
Zahl daneben.
* SOZIALER VERGLEICH. Beurteilungen, die Menschen nebeneinander
stellen, erzeugen Neid und senken die Leistung derer, die
schlechter dastehen. Ein Balken „Diene 71 %, VanVan 88 %“ wäre
genau das, und er wäre nie wieder aus den Köpfen zu bekommen.
* SCHWÄCHENFOKUS. Rückmeldung, die auf Defizite zeigt, senkt die
Leistung messbar. Deshalb steht hier IMMER zuerst, was sich
bewegt hat und was trägt — und erst danach, was liegt.
Quellen:
https://hbr.org/2011/05/the-power-of-small-wins
https://www.simplypsychology.org/self-determination-theory.html
https://www.profit.co/blog/performance-management/adverse-effects-of-employee-comparison-during-performance-appraisals/
https://www.cambridge.org/core/journals/industrial-and-organizational-psychology/article/abs/working-with-social-comparisons-in-the-appraisal-and-management-of-performance/2366D9737F8A543A16E6C884D1E8A5F8
---------------------------------------------------------------------
WAS STATTDESSEN GEZEIGT WIRD: BEWEGUNG
Amabile und Kramer haben rund 12 000 Tagebucheinträge von 238
Menschen ausgewertet. Das Ergebnis, das seitdem „progress principle“
heißt: Der stärkste einzelne Treiber guter Arbeit ist SICHTBARER
FORTSCHRITT in einer Sache, die einem wichtig ist. Nicht Lob, nicht
Belohnung, nicht Druck — und die Schritte dürfen klein sein.
Deshalb ist die erste Zahl auf dieser Seite nicht, WIE GUT jemand
ist, sondern WAS SICH BEWEGT HAT. Und die zweite sagt etwas über
UNS: wie alt die Einschätzungen sind. Eine Karte, die seit vier
Monaten niemand angefasst hat, ist keine Auskunft über den
Menschen — sie ist eine über die, die hinsehen sollten.
---------------------------------------------------------------------
DER ANLASS KOMMT NICHT MIT
In `entwicklung_stand` steht neben jedem „da hakt es“ ein Satz, woran
man es gemerkt hat. Der ist für ein Gespräch aufgeschrieben. Eine
CHRONIK dieser Sätze wäre das Belastendste, was dieses Haus
speichern könnte — jede alte Beobachtung bliebe auf Dauer lesbar,
auch die, die längst geklärt ist.
Die Spur hier merkt sich deshalb nur: welcher Punkt, welche Stufe,
wann, von wem. Kein Anlass, nie. Dasselbe Prinzip wie beim
Befinden-Verlauf — dort steht im Protokoll auch nur, DASS eine Runde
war.
===================================================================== */
import express from "express";
import {
db, sitzungLesen, fuehrtTeamDogi, ROLLEN_NAME,
} from "./workspace.js";
import {
ENTWICKLUNG_BLOECKE, ENTWICKLUNG_PUNKTE, ENTWICKLUNG_STUFEN,
ENTWICKLUNG_ERWARTUNG, ENTWICKLUNG_FREMD, ENTWICKLUNG_NACH_SCHLUESSEL,
} from "./workspace-entwicklungs-punkte.js";
/* NUR IN EINE RICHTUNG. Diese Datei liest aus `workspace-entwicklung.js`;
jene weiß von dieser nichts. Die Spur-Tabelle steht deshalb DORT, bei
`entwicklung_stand`, zu dem sie gehört — nicht hier.
Der bequeme Weg wäre gewesen, die Tabelle hier anzulegen und von dort
ein `spurLegen()` zu importieren. Das ergäbe einen Ring zwischen zwei
Modulen, und Ringe funktionieren in JavaScript so lange, bis jemand
eine Konstante auf oberster Ebene benutzt statt in einer Funktion.
Dann kommt ein Fehler, den niemand mit dem Import in Verbindung
bringt. */
import {
VERBLASST_NACH_TAGEN, tabellen as entwicklungTabellen,
} from "./workspace-entwicklung.js";
export const werdegangRouter = express.Router();
const TAG = 86400000;
/** Die Rollen, über die eine Karte geführt wird.
*
* ABGELEITET WÄRE HIER FALSCH, und das ist die Ausnahme: Es ist
* dieselbe Liste wie in `workspace-entwicklung.js`, und sie MUSS
* dieselbe sein — eine Auswertung über jemanden, der gar keine Karte
* hat, wäre leer. Sie steht trotzdem hier noch einmal, weil ein
* Import nur wegen zweier Zeichenketten die beiden Module
* aneinanderbindet; `pruef-werdegang` vergleicht sie und wird rot,
* wenn sie auseinanderlaufen. */
export const KARTEN_ROLLEN = ["hand", "modi"];
/** Wie viele Monatsspalten der Verlauf zeigt.
*
* SECHS. Genug, um ein halbes Jahr zu sehen — und wenig genug, dass
* die Spalten auf einem Handy noch lesbar sind. Mehr Spalten heißt
* nicht mehr Auskunft: Was vor einem Jahr war, ist für die Frage
* „bewegt sich etwas?“ ohne Belang. */
export const MONATE = 6;
/** Ab wann ein „läuft“ als getragen gilt und ausdrücklich gesagt wird.
*
* 60 TAGE. Anerkennung wirkt nur, wenn sie sich auf etwas Bestimmtes
* bezieht — „du machst das gut“ ist Höflichkeit, „das steht seit zwei
* Monaten und ist nie gekippt“ ist eine Beobachtung. Unter zwei
* Monaten wäre es ein Zwischenstand, kein Befund. */
export const TRAEGT_AB_TAGEN = 60;
/** Ab wann ein „da hakt es“ von selbst zur Sprache kommt.
*
* 30 TAGE OHNE BEWEGUNG. Die Hausregel dazu steht im Plan vom 11.09.:
* „Ein ‚Da hakt es', das nirgends hinführt, ist eine Notiz über einen
* Menschen — und die will hier niemand sammeln.“ */
export const LIEGT_AB_TAGEN = 30;
/** Ab wie vielen Monatsspalten mit Inhalt überhaupt etwas gezeichnet wird.
*
* ZWEI. Ein Punkt ist kein Verlauf. Darunter sagt die Seite das —
* sie zeigt keine Linie zwischen einem einzigen Wert und dem Rand.
* Das ist der dritte Ausgang: nicht in Ordnung / in Ordnung / KONNTE
* NOCH NICHT NACHSEHEN, mit Grund. */
export const VERLAUF_AB = 2;
/* ---------------------------------------------------------------------
DIE SPUR LIEGT NEBENAN
`entwicklung_lauf` wird in `workspace-entwicklung.js` angelegt und
dort auch geschrieben — bei `entwicklung_stand`, zu dem sie gehört.
Diese Datei liest sie nur. Ein `tabellen()` hier wäre eine zweite
Stelle, an der dieselbe Tabelle entstehen kann, und die zweite ist
immer die, die eine Spalte vergisst.
------------------------------------------------------------------- */
function tabellen() { entwicklungTabellen(); }
/* ---------------------------------------------------------------------
RECHNEN
------------------------------------------------------------------- */
/** Die Rangfolge der drei aussagekräftigen Stufen.
*
* „Kann ich nicht sagen“ hat KEINEN Rang und bekommt auch keinen. Es
* ist keine schlechtere Antwort als „läuft“, sondern eine andere Art
* von Antwort — sie sagt etwas über den Beurteiler, nicht über den
* Beurteilten. Ihr eine Zahl zu geben hieße, sie in dieselbe Reihe zu
* stellen, und ab da würde „ich habe es nicht gesehen“ wie ein
* halbes „da hakt es“ gelesen. */
const RANG = { hakt: 0, waechst: 1, laeuft: 2 };
/** Wie ein Übergang zu nennen ist. Gerechnet, nicht gespeichert. */
export function artVon(vorher, stufe, nachgetragen) {
if (nachgetragen) return "anfang";
if (!vorher) return "neu";
if (vorher === stufe) return "gleich";
if (vorher === "unklar") return "geklaert";
if (stufe === "unklar") return "offen";
const a = RANG[vorher], b = RANG[stufe];
if (a === undefined || b === undefined) return "gleich";
return b > a ? "vor" : "zurueck";
}
/** Der Monat eines Zeitpunkts in Ortszeit — „2026-09“.
*
* ÜBER Intl UND NICHT ÜBER slice(0,7). `toISOString()` rechnet in UTC;
* zwischen Mitternacht und zwei Uhr liefert das den VORMONAT, und zwar
* genau am Monatsersten. Ein Diagramm, das in den ersten Stunden des
* Monats eine Spalte zu wenig hat, fällt niemandem auf — es sieht
* einfach nach wenig Bewegung aus. Dieselbe Falle hat am 06.09. eine
* Prüfung zwei Stunden pro Tag rot gemacht. */
const MONATSFORM = new Intl.DateTimeFormat("sv-SE", {
timeZone: "Europe/Berlin", year: "numeric", month: "2-digit",
});
export function monatVon(iso) {
const t = Date.parse(iso || "");
if (!Number.isFinite(t)) return null;
return MONATSFORM.format(new Date(t)).slice(0, 7);
}
/** Die letzten N Monate, älteste zuerst — „2026-04“ … „2026-09“. */
export function monatsspalten(anzahl = MONATE, bis = new Date()) {
const spalten = [];
/* ÜBER DEN 15. GERECHNET und nicht über den heutigen Tag: Wer am
31. März einen Monat abzieht, landet in JavaScript im März (der
31. Februar existiert nicht und rollt über). Der 15. ist in jedem
Monat vorhanden. */
const roh = monatVon(bis.toISOString()) || monatVon(new Date().toISOString());
const [j, m] = roh.split("-").map(Number);
for (let i = anzahl - 1; i >= 0; i--) {
const d = new Date(Date.UTC(j, m - 1 - i, 15, 12));
spalten.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`);
}
return spalten;
}
const tageSeit = (iso) => {
const t = Date.parse(iso || "");
return Number.isFinite(t) ? Math.floor((Date.now() - t) / TAG) : null;
};
/** Alle Spur-Zeilen einer Person, älteste zuerst. */
function spurVon(personId) {
return db().prepare(
`SELECT l.schluessel, l.von_id, l.stufe, l.vorher, l.nachgetragen, l.am,
p.name AS von_name
FROM entwicklung_lauf l
LEFT JOIN personen p ON p.id = l.von_id
WHERE l.person_id = ?
ORDER BY l.am ASC, l.id ASC`).all(personId);
}
/* =====================================================================
DIAGRAMM 1 — WAS SICH BEWEGT HAT
Je Monat: wie viele Punkte sind nach vorn gegangen, wie viele zurück,
wie viele wurden zum ersten Mal angesehen.
ES IST BEWUSST EIN DIAGRAMM MIT ZWEI RICHTUNGEN und nicht zwei
nebeneinander: Vorwärts und rückwärts sind dieselbe Größe mit
verschiedenem Vorzeichen. Zwei getrennte Diagramme würden dazu
verleiten, sie zu vergleichen, als wären es zwei Themen.
RÜCKWÄRTS IST NICHT ROT. Auf dieser Seite gibt es kein Rot — das ist
seit dem 11.09. die wichtigste Farbentscheidung des Bereichs. Ein
Rückschritt ist eine Beobachtung, kein Alarm, und oft genug heißt er
nur, dass jemand genauer hingesehen hat als beim letzten Mal.
===================================================================== */
export function bewegungVon(spur, spalten) {
const leer = () => ({ vor: 0, zurueck: 0, neu: 0, geklaert: 0, offen: 0 });
const nach = new Map(spalten.map((s) => [s, leer()]));
for (const z of spur) {
const m = monatVon(z.am);
if (!nach.has(m)) continue;
const art = artVon(z.vorher, z.stufe, z.nachgetragen);
/* „anfang“ und „gleich“ zählen nicht als Bewegung. Die
Startaufnahme ist keine Veränderung, sondern der Nullpunkt — und
wer denselben Wert noch einmal setzt, hat bestätigt, nicht
bewegt. Beides mitzuzählen wäre der bequemste Weg zu einem
Diagramm, das immer etwas zeigt. */
if (art === "anfang" || art === "gleich") continue;
nach.get(m)[art] += 1;
}
return spalten.map((m) => ({ monat: m, ...nach.get(m) }));
}
/* =====================================================================
DIAGRAMM 2 — DER STREIFEN
Eine Zeile je Punkt, eine Spalte je Monat, jede Zelle die Stufe, die
AM ENDE DIESES MONATS galt.
FORTGESCHRIEBEN, NICHT LÜCKENHAFT: Wenn im Juli niemand etwas
geändert hat, galt im Juli weiter, was im Juni gesetzt wurde. Eine
leere Zelle würde „keine Einschätzung“ bedeuten, und das wäre falsch.
ABER SICHTBAR ALT: Eine fortgeschriebene Zelle, deren Ursprung länger
als 90 Tage zurückliegt, wird gedämpft gezeichnet. Sonst sähe eine
vier Monate alte Einschätzung genauso frisch aus wie die von gestern
— und das ist die Lüge mit Zeitstempel, vor der der Plan vom 11.09.
ausdrücklich warnt.
DIE FARBE IST NICHT DIE AUSKUNFT. Jede Zelle trägt das Zeichen ihrer
Stufe (✓ ↗ ! –) als Text. Wer Farben schlecht unterscheidet, liest
dasselbe. Das ist im Haus seit dem Befinden-Verlauf so und bleibt so.
===================================================================== */
export function streifenVon(spur, spalten, beurteilerId) {
/* NUR DER BLICK EINES BEURTEILERS je Streifen. Zwei Meinungen in
einer Zelle zu mischen hieße, einen Mittelwert zu bilden — und ein
Mittelwert aus „läuft“ und „da hakt es“ ist „wächst gerade“, was
niemand gesagt hat. Die Abweichung zwischen den beiden ist das
Ergebnis (so steht es im Plan) und hat ihren eigenen Platz. */
const meine = spur.filter((z) => z.von_id === beurteilerId);
if (!meine.length) return [];
/* Nur Punkte, zu denen es überhaupt etwas gibt — der ganze Katalog
wäre eine Wand aus Strichen. */
const punkte = [...new Set(meine.map((z) => z.schluessel))];
const grenze = Date.now() - VERBLASST_NACH_TAGEN * TAG;
return punkte.map((schluessel) => {
const pt = ENTWICKLUNG_NACH_SCHLUESSEL.get(schluessel);
const zeilen = meine.filter((z) => z.schluessel === schluessel);
let stufe = null, seit = null;
const zellen = spalten.map((m) => {
/* Alles, was BIS EINSCHLIESSLICH dieses Monats passiert ist. */
for (const z of zeilen) {
if (monatVon(z.am) <= m) { stufe = z.stufe; seit = z.am; }
}
if (!stufe) return { monat: m, stufe: null };
return {
monat: m, stufe, seit,
/* Nur die LETZTE Spalte kann verblassen — bei den früheren
sagt die Spalte selbst schon, wann es war. Ein gedämpfter
Märzwert im März wäre Unsinn. */
alt: m === spalten.at(-1) && Date.parse(seit) < grenze,
};
});
return {
schluessel,
titel: pt?.titel || schluessel,
block: pt?.block || null,
zellen,
};
});
}
/* =====================================================================
DIE TEXTE
Filipe wollte „diagramme texte und so“. Die Texte sind der Teil, den
ein Diagramm nicht kann: Es zeigt, DASS sich etwas bewegt hat — nicht
WAS, und schon gar nicht, was man damit macht.
DIE REIHENFOLGE IST DIE AUSSAGE, und sie ist aus der Forschung
abgeleitet, nicht aus Höflichkeit:
1. WAS SICH BEWEGT HAT — progress principle: sichtbarer
Fortschritt ist der stärkste Treiber.
2. WAS TRÄGT — Anerkennung wirkt nur konkret; deshalb
mit Punkt und Dauer, nicht als Lob.
3. WAS LIEGT — erst an dritter Stelle. Schwächenfokus
senkt die Leistung messbar.
4. WAS NIEMAND ANSIEHT — die einzige Zeile, die von UNS handelt.
5. WAS ALS NÄCHSTES KOMMT— der Weg nach vorn, nicht die Lücke.
JEDER ABSATZ NENNT DEN PUNKT, auf den er sich bezieht. Ein Satz wie
„es gibt Bewegung“ ist keine Auskunft, sondern eine Behauptung.
===================================================================== */
const zaehl = (n, eins, viele) => (n === 1 ? eins : viele.replace("%n", n));
export function texteVon({ spur, beurteilerId, stand, person }) {
const texte = [];
const meine = spur.filter((z) => z.von_id === beurteilerId);
const titel = (s) => ENTWICKLUNG_NACH_SCHLUESSEL.get(s)?.titel || s;
/* ---- 1. Was sich bewegt hat ------------------------------------- */
const bewegt = meine.filter((z) => {
const art = artVon(z.vorher, z.stufe, z.nachgetragen);
return art === "vor" || art === "zurueck" || art === "geklaert";
});
const vor = bewegt.filter((z) => artVon(z.vorher, z.stufe, z.nachgetragen) === "vor");
const zurueck = bewegt.filter((z) => artVon(z.vorher, z.stufe, z.nachgetragen) === "zurueck");
if (vor.length) {
texte.push({
art: "vor",
titel: "Was sich bewegt hat",
satz: zaehl(vor.length,
`Ein Punkt ist weitergekommen: „${titel(vor[0].schluessel)}“.`,
`%n Punkte sind weitergekommen.`)
+ " Das ist die Sorte Veränderung, die man aussprechen sollte –"
+ " sichtbarer Fortschritt hält Menschen bei der Sache, stärker"
+ " als Lob es je könnte.",
punkte: vor.slice(-5).reverse().map((z) => ({
titel: titel(z.schluessel), von: z.vorher, zu: z.stufe, am: z.am,
})),
});
}
if (zurueck.length) {
texte.push({
art: "zurueck",
titel: "Wo du es anders siehst als vorher",
satz: zaehl(zurueck.length,
"Bei einem Punkt hast du zurückgesetzt.",
"Bei %n Punkten hast du zurückgesetzt.")
+ " Das ist kein Rückschritt dieser Person – es heißt zuerst,"
+ " dass du genauer hingesehen hast als beim letzten Mal."
+ " Beides gehört ins Gespräch, und zwar in dieser Reihenfolge.",
punkte: zurueck.slice(-5).reverse().map((z) => ({
titel: titel(z.schluessel), von: z.vorher, zu: z.stufe, am: z.am,
})),
});
}
/* ---- 2. Was trägt ------------------------------------------------ */
const traegt = stand
.filter((s) => s.stufe === "laeuft" && (tageSeit(s.am) ?? 0) >= TRAEGT_AB_TAGEN)
.sort((a, b) => Date.parse(a.am) - Date.parse(b.am));
if (traegt.length) {
const t = traegt[0];
texte.push({
art: "traegt",
titel: "Was seit Längerem trägt",
satz: zaehl(traegt.length,
`„${titel(t.schluessel)}“ steht seit ${tageSeit(t.am)} Tagen auf „Läuft“ und ist nie gekippt.`,
`%n Punkte stehen seit über ${Math.round(TRAEGT_AB_TAGEN / 30)} Monaten auf „Läuft“ und sind nie gekippt.`)
+ " Genau so etwas wird nie gesagt, weil nichts passiert –"
+ " und fehlende Anerkennung ist der am häufigsten genannte"
+ " Grund, warum Leute in dieser Arbeit aufhören.",
punkte: traegt.slice(0, 5).map((s) => ({
titel: titel(s.schluessel), zu: "laeuft", am: s.am, tage: tageSeit(s.am),
})),
});
}
/* ---- 3. Was liegt ------------------------------------------------ */
const liegt = stand
.filter((s) => s.stufe === "hakt" && (tageSeit(s.am) ?? 0) >= LIEGT_AB_TAGEN)
.sort((a, b) => Date.parse(a.am) - Date.parse(b.am));
if (liegt.length) {
texte.push({
art: "liegt",
titel: "Was seit Längerem unverändert steht",
satz: zaehl(liegt.length,
`„${titel(liegt[0].schluessel)}“ steht seit ${tageSeit(liegt[0].am)} Tagen unverändert auf „Da hakt es“.`,
`%n Punkte stehen seit über ${LIEGT_AB_TAGEN} Tagen unverändert auf „Da hakt es“.`)
+ " Ein „da hakt es“, das nirgends hinführt, ist nach einer Weile"
+ " keine Beobachtung mehr, sondern eine Notiz über einen Menschen."
+ " Auf der Karte steht neben jedem Punkt „Daraus einen Schritt machen“.",
punkte: liegt.slice(0, 5).map((s) => ({
titel: titel(s.schluessel), zu: "hakt", am: s.am, tage: tageSeit(s.am),
})),
});
}
/* ---- 4. Was niemand mehr angesehen hat --------------------------- */
const alt = stand.filter((s) => (tageSeit(s.am) ?? 0) >= VERBLASST_NACH_TAGEN);
if (alt.length) {
texte.push({
art: "alt",
titel: "Das sagt etwas über uns, nicht über sie",
satz: zaehl(alt.length,
`Eine Einschätzung ist älter als ${VERBLASST_NACH_TAGEN} Tage.`,
`%n Einschätzungen sind älter als ${VERBLASST_NACH_TAGEN} Tage.`)
+ " Sie zählen in der Auswertung nicht mehr mit. Eine Einschätzung"
+ " vom Frühjahr, heute unverändert angezeigt, wäre eine Lüge mit"
+ " Zeitstempel – deshalb verblasst sie, statt zu verschwinden.",
punkte: alt.slice(0, 5).map((s) => ({
titel: titel(s.schluessel), zu: s.stufe, am: s.am, tage: tageSeit(s.am),
})),
});
}
/* ---- 5. Was als Nächstes kommt ----------------------------------- */
const gesetzt = new Set(stand.map((s) => s.schluessel));
const offen = ENTWICKLUNG_FREMD.filter((p) => !gesetzt.has(p.schluessel));
if (offen.length) {
/* NACH ERWARTUNGSSTUFE SORTIERT, niedrigste zuerst: Was ab Tag eins
gilt, ist die sinnvollere nächste Frage als das, wofür man
jemanden fragt. */
const rang = new Map(ENTWICKLUNG_ERWARTUNG.map((e) => [e.schluessel, e.rang]));
const sortiert = [...offen].sort(
(a, b) => (rang.get(a.stufe) ?? 9) - (rang.get(b.stufe) ?? 9));
texte.push({
art: "offen",
titel: "Wozu du noch nichts gesagt hast",
satz: zaehl(offen.length,
"Ein Punkt des Katalogs ist von dir noch unbeantwortet.",
"%n Punkte des Katalogs sind von dir noch unbeantwortet.")
+ " Das ist kein Versäumnis – „Kann ich nicht sagen“ ist eine"
+ " gültige Antwort und ehrlicher als zu raten. Es steht hier,"
+ " damit man weiß, worüber die Auswertung schweigt.",
punkte: sortiert.slice(0, 5).map((p) => ({
titel: p.titel,
/* DER NAME, NICHT DER SCHLUESSEL -- und schon gar nicht der
`rang` daneben. ENTWICKLUNG_ERWARTUNG traegt eine Zahl je
Stufe ("rang"), die es nur zum Sortieren gibt. Sie mitzuliefern
hiesse, eine Zahl ueber einen Menschen auszuliefern, die
irgendwann jemand addiert. Aufgefallen ist das der eigenen
Pruefung, nicht dem Lesen. */
erwartung: ENTWICKLUNG_ERWARTUNG.find((e) => e.schluessel === p.stufe)?.name
|| p.stufe,
})),
});
}
return texte;
}
/** Der eine Satz oben — die ganze Auswertung in einer Zeile.
*
* WER NUR IHN LIEST, SOLL RICHTIG INFORMIERT SEIN. Dieselbe Regel wie
* bei der Befinden-Analyse: Der Satz ist kein Aufhänger, er ist die
* Kurzfassung. */
export function satzVon({ bewegung, stand, texte, spalten }) {
const vor = bewegung.reduce((s, m) => s + m.vor, 0);
const zurueck = bewegung.reduce((s, m) => s + m.zurueck, 0);
const neu = bewegung.reduce((s, m) => s + m.neu + m.geklaert, 0);
if (!stand.length) {
return "Zu dieser Person hast du noch nichts eingetragen. Die Auswertung "
+ "beginnt mit der ersten Einschätzung – vorher stünde hier eine Grafik "
+ "über nichts.";
}
if (vor + zurueck + neu === 0) {
const alt = texte.find((t) => t.art === "alt");
return alt
? `In den letzten ${spalten.length} Monaten hat sich an dieser Karte nichts `
+ "bewegt – und ein Teil davon ist inzwischen älter als drei Monate. "
+ "Das ist eine Auskunft über uns, nicht über sie."
: `In den letzten ${spalten.length} Monaten hat sich an dieser Karte nichts `
+ "bewegt. Das kann heißen, dass alles ruhig läuft – oder dass "
+ "länger niemand hingesehen hat. Die Karte selbst kann das nicht "
+ "unterscheiden.";
}
const teile = [];
if (vor) teile.push(zaehl(vor, "ein Punkt nach vorn", "%n Punkte nach vorn"));
if (zurueck) teile.push(zaehl(zurueck, "einer zurück", "%n zurück"));
if (neu) teile.push(zaehl(neu, "einer zum ersten Mal beantwortet",
"%n zum ersten Mal beantwortet"));
return `In den letzten ${spalten.length} Monaten: ${teile.join(", ")}. `
+ (vor >= zurueck
? "Das Erste gehört ausgesprochen – es ist der Teil, den sonst niemand bemerkt."
: "Das ist kein schlechtes Zeichen: Wer zurücksetzt, hat hingesehen.");
}
/* =====================================================================
DIE WEGE
===================================================================== */
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
try { tabellen(); } catch (f) {
console.error("[werdegang] Tabellen:", f?.message);
return res.status(503).json({ fehler: "nicht_verfuegbar" });
}
next();
}
/** Nur DogFather und die rechte Hand. 404 statt 403 — wer „kein
* Zugriff“ liest, fragt als Nächstes, wer denn dann. */
function nurLeitung(req, res, next) {
if (fuehrtTeamDogi(req.person)) return next();
return res.status(404).json({ fehler: "nicht_gefunden" });
}
werdegangRouter.use("/workspace/api/werdegang", angemeldet);
/* ---- Die Liste ------------------------------------------------------
Wer eine Karte hat, und was sich dort zuletzt getan hat.
AUSDRÜCKLICH OHNE SORTIERUNG NACH „AKTIVITÄT“. Eine Liste, die die
mit der meisten Bewegung nach oben stellt, ist eine Rangliste mit
einem anderen Namen. Sortiert wird nach Namen.
------------------------------------------------------------------- */
werdegangRouter.get("/workspace/api/werdegang/liste", nurLeitung, (req, res) => {
try {
const leute = db().prepare(
`SELECT id, name, rolle FROM personen
WHERE aktiv = 1 AND rolle IN (${KARTEN_ROLLEN.map(() => "?").join(",")})
ORDER BY name COLLATE NOCASE`).all(...KARTEN_ROLLEN);
const spalten = monatsspalten();
const liste = leute.map((p) => {
const spur = spurVon(p.id);
const bewegung = bewegungVon(spur.filter((z) => z.von_id === req.person.id), spalten);
const stand = db().prepare(
"SELECT schluessel, stufe, am FROM entwicklung_stand WHERE person_id = ? AND von_id = ?")
.all(p.id, req.person.id)
.filter((s) => ENTWICKLUNG_FREMD.some((x) => x.schluessel === s.schluessel));
const juengste = stand.map((s) => s.am).sort().at(-1) || null;
return {
id: p.id,
name: p.name,
rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle,
gesetzt: stand.length,
gesamt: ENTWICKLUNG_FREMD.length,
bewegt: bewegung.reduce((s, m) => s + m.vor + m.zurueck + m.neu + m.geklaert, 0),
vor: bewegung.reduce((s, m) => s + m.vor, 0),
zuletzt: juengste,
zuletzt_tage: juengste ? tageSeit(juengste) : null,
};
});
res.json({
liste,
monate: spalten.length,
/* WAS DIESE SEITE NICHT TUT, steht in der Antwort und nicht nur
im Kopf des Entwicklers: Die Oberfläche zeigt den Satz. */
hinweis: "Keine Punktzahl, kein Vergleich zwischen Personen. "
+ "Jede Karte wird nur mit sich selbst über die Zeit verglichen.",
});
} catch (f) {
console.error("[werdegang] Liste:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---- Eine Person ---------------------------------------------------- */
werdegangRouter.get("/workspace/api/werdegang/person/:id", nurLeitung, (req, res) => {
try {
const id = Number(req.params.id);
const p = Number.isInteger(id) && id > 0
&& db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
if (!p || !KARTEN_ROLLEN.includes(p.rolle)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const spur = spurVon(p.id);
const spalten = monatsspalten();
const meine = spur.filter((z) => z.von_id === req.person.id);
const bewegung = bewegungVon(meine, spalten);
const streifen = streifenVon(spur, spalten, req.person.id);
const stand = db().prepare(
"SELECT schluessel, stufe, am FROM entwicklung_stand WHERE person_id = ? AND von_id = ?")
.all(p.id, req.person.id)
.filter((s) => ENTWICKLUNG_FREMD.some((x) => x.schluessel === s.schluessel));
const texte = texteVon({ spur, beurteilerId: req.person.id, stand, person: p });
/* MONATE MIT INHALT, nicht Monate überhaupt. Sechs leere Spalten
sind kein Verlauf — sie sehen nur aus wie einer. */
const gefuellt = streifen.length
? spalten.filter((m) => streifen.some(
(z) => z.zellen.find((c) => c.monat === m)?.stufe)).length
: 0;
res.json({
person: { id: p.id, name: p.name, rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle },
stufen: ENTWICKLUNG_STUFEN,
/* ENTWICKLUNG_ERWARTUNG wird NICHT mitgeschickt. Die Seite braucht
die Liste nicht, und sie traegt ein Feld `rang` -- eine Zahl je
Erwartungsstufe, die es nur zum Sortieren gibt. Auf einer Seite
ueber Menschen hat keine Zahl etwas verloren, die man addieren
koennte. Der Name der Stufe steht dort, wo er gebraucht wird. */
bloecke: ENTWICKLUNG_BLOECKE.filter((b) => !b.nurSelbst)
.map((b) => ({ schluessel: b.schluessel, name: b.name })),
spalten,
bewegung,
streifen,
texte,
satz: satzVon({ bewegung, stand, texte, spalten }),
gesetzt: stand.length,
gesamt: ENTWICKLUNG_FREMD.length,
/* DER DRITTE AUSGANG. Nicht „in Ordnung“ und nicht „nicht in
Ordnung“, sondern: konnte noch nicht nachsehen — mit Grund.
Die Oberfläche zeichnet dann KEIN Diagramm, sondern schreibt
den Grund hin. */
verlauf_moeglich: gefuellt >= VERLAUF_AB,
verlauf_ab: VERLAUF_AB,
monate_mit_inhalt: gefuellt,
verblasst_nach_tagen: VERBLASST_NACH_TAGEN,
});
} catch (f) {
console.error("[werdegang] Person:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---- Die eigene Entwicklung -----------------------------------------
Der Teil, der den Menschen selbst angeht — und der nach der
Forschung der wirksamste ist: Wer seinen eigenen Fortschritt sieht,
bleibt dabei.
OHNE NAMEN, OHNE ANLASS, OHNE WER-HAT-WAS. Genau wie bei
`meine-karte`: Sie sieht, DASS ein Punkt sich bewegt hat und wann —
nicht, wer ihn gesetzt hat und was danebensteht. Der Anlass ist für
ein Gespräch aufgeschrieben, nicht zum Lesen ohne den Menschen dazu.
UND ERST, WENN ÜBERHAUPT JEMAND GESETZT HAT. Eine leere eigene Seite
würde wie ein Fehler aussehen; ein Satz ist eine Auskunft.
------------------------------------------------------------------- */
werdegangRouter.get("/workspace/api/werdegang/mich", (req, res) => {
try {
if (!KARTEN_ROLLEN.includes(req.person.rolle)) {
return res.json({ hat_karte: false });
}
const spur = spurVon(req.person.id).filter((z) => z.von_id !== req.person.id);
const spalten = monatsspalten();
/* ZUSAMMENGEFASST ÜBER ALLE BEURTEILER. Hier geht es nicht darum,
wer was gesagt hat — sondern darum, ob sich etwas bewegt.
Deshalb ist eine Bewegung eine Bewegung, egal von wem. */
const bewegung = bewegungVon(spur, spalten);
const vor = bewegung.reduce((s, m) => s + m.vor, 0);
const zurueck = bewegung.reduce((s, m) => s + m.zurueck, 0);
let satz;
if (!spur.length) {
satz = "Zu dir wurde noch nichts eingetragen. Sobald das passiert ist, "
+ "steht hier, was sich über die Zeit bewegt hat.";
} else if (!vor && !zurueck) {
satz = `In den letzten ${spalten.length} Monaten hat sich an den `
+ "Einschätzungen zu dir nichts geändert.";
} else if (vor) {
satz = vor === 1
? "Seit dem letzten Mal ist ein Punkt weitergekommen."
: `Seit dem letzten Mal sind ${vor} Punkte weitergekommen.`;
} else {
satz = "Seit dem letzten Mal wurde ein Punkt anders eingeschätzt als vorher. "
+ "Das ist kein Urteil – frag danach, dann weißt du, worum es geht.";
}
res.json({
hat_karte: true,
spalten,
bewegung,
satz,
/* KEIN STREIFEN. Er würde zeigen, WELCHER Punkt wann gewackelt
hat — und damit mittelbar, was jemand über sie gesagt hat,
ohne dass derjenige dabei ist. Die Zahl reicht; für alles
Weitere ist das Gespräch da, und der Satz sagt das auch. */
hinweis: "Welche Punkte das sind, steht nicht hier. Frag danach – "
+ "eine Einschätzung ohne den Menschen dazu ist eine Behauptung.",
});
} catch (f) {
console.error("[werdegang] Mich:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
+44 -1
View File
@@ -750,6 +750,48 @@ const ENTWICKLUNG_KACHEL = {
zeichen: "steckbrief", ton: 26, ziel: "entwicklung.html", szene: "studio",
};
/* DIE KACHEL, FUER DIE DER NAME FREI GEMACHT WURDE (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."
SIE ZEIGT AUF werdegang.html UND NICHT AUF entwicklung.html. Das ist
Absicht und sieht beim ersten Hinsehen verdreht aus:
entwicklung.html = der KATALOG, Kachel "Eure Aufgaben"
werdegang.html = die AUSWERTUNG, Kachel "Entwicklung"
Die saubere Endform waere, die alte Datei umzubenennen. Das waeren
25 Fundstellen in 11 Dateien -- und damit zwei Aenderungen in einer:
Wenn danach etwas klemmt, weiss niemand, welche der beiden es war.
Deshalb steht es hier als Kommentar und in `pruef-werdegang` als
Pruefung, die rot wird, wenn die Kachel woanders hinzeigt.
EIGENER TON 40: GEMESSEN, NICHT GERATEN -- und der erste Anlauf lag
falsch. 38 sah frei aus, weil 37 die hoechste Nummer in DIESER Datei
ist. Gezaehlt in start.css sind es aber 39, alle vergeben: 38 ist
"Unsere Seiten", 39 "Vertraulich melden" -- beide kamen am 19.09.
frueh dazu und stehen nicht als `ton:` in workspace.js.
Genau davor warnt das Farbwerkzeug in seinem eigenen Kopf ("Zweiter
Anlauf war Ton 22, weil er frei AUSSAH"). Gezaehlt wird deshalb in
der Datei, die die Farben wirklich enthaelt.
Nach dem Hinzufuegen laeuft `node tools/kachel-farben.mjs --schreiben`
-- es rechnet alle 40 Toene neu gegeneinander, damit sich keine zwei
aehneln.
EIGENES ZEICHEN "werdegang": Die vorhandenen 22 sind entweder
vergeben (steckbrief viermal) oder sagen etwas anderes -- "zahlen"
ist eine Messkurve, "teamlage" sind zwei Koepfe. Hier geht es um
EINEN Menschen und seinen Weg. */
const WERDEGANG_KACHEL = {
...FUEHRUNG_GRUPPE,
name: "Entwicklung", unter: "Wie sich jemand über die Zeit bewegt – ohne Note",
zeichen: "werdegang", ton: 40, ziel: "werdegang.html", szene: "wald",
};
const TALENTE_KACHEL = {
...FUEHRUNG_GRUPPE,
name: "Talente", unter: "Zuschauer, die ins Team passen könnten",
@@ -875,7 +917,7 @@ const BEFINDEN_KACHEL = {
const HAND_BEREICHE = vorGruppe(
[...MODI_BEREICHE, EINGANG_KACHEL, PERSONEN_KACHEL],
"Rund ums Live",
ENTWICKLUNG_KACHEL, TALENTE_KACHEL,
ENTWICKLUNG_KACHEL, WERDEGANG_KACHEL, TALENTE_KACHEL,
).concat(BEFINDEN_KACHEL, {
gruppe: "Für dich", gruppeUnter: "Dein Platz im Team",
...RECHTE_KACHEL,
@@ -1640,6 +1682,7 @@ const ZUSATZ_BEREICHE = {
gruppe: TEAM_GRUPPE.gruppe, gruppeUnter: TEAM_GRUPPE.gruppeUnter,
},
{ ...ENTWICKLUNG_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
{ ...WERDEGANG_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
{ ...TALENTE_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
/* DER TREFF -- GANZ UNTEN, UND ZWAR ABSICHTLICH.