Files
dogfather-universe/server/pruef-entwicklung.mjs
T
DogFatherGitandClaude Opus 5 9b47bd3ca0 Entwicklung & Nachwuchs -- zwei Bretter fuer DogFather und die rechte Hand
Filipe: "ich will dass ich genau so eine kategorie habe in der dogfather
und rechten hand rollen, wo wir aufgaben oder bewertungen ueber modis
eingeben koennen. auch fuer zuschauer die vielleicht modis werden
koennten waere auch geil. mach dich schlau informier dich so krass wie
es nur geht, hol die besten der besten und krassesten sachen,
perfektionnier die dan auch alle und dan erst setzt du alles um."

=== WAS DIE RECHERCHE ERGEBEN HAT ===

DREI BEFUNDE HABEN DAS DESIGN BESTIMMT, und der erste hat es fast
umgedreht:

  WARUM MODERATOREN AUFHOEREN (Schoepke-Gonzalez u. a., New Media &
  Society 2024): zwei Hauptgruende -- zu wenig Zeit, und Konflikte im
  Team beziehungsweise schaedliches Verhalten der Leitung. Eine
  Bewertungsfunktion ist damit genau das Werkzeug, mit dem man ein Team
  verliert, wenn man sie als Ueberwachung baut. Das ist kein Bauchgefuehl
  und keine Zimperlichkeit -- es ist der haeufigste gemessene Grund.

  WAS HAELT: Anerkennung. Dank und Rueckmeldung erhoehen die
  Verweildauer messbar; Rollenklarheit senkt Burnout.

  WORAN MAN GUTE MODERATOREN ERKENNT (ModSquad, Kitfox Games,
  Discord-Leitfaeden): nicht an Zahlen. Ruhig bleiben, wenn es hitzig
  wird; von selbst helfen; die Regeln UND die Leute kennen; regelmaessig
  da sein. Ausdruecklich NICHT: "schreibt viel" -- angenehm im Chat zu
  sein ist nachweislich etwas anderes. Und der treffsicherste Weg
  ueberhaupt ist die Empfehlung aus dem Team, gefolgt von einer
  Probezeit.

=== WAS DARAUS GEBAUT WURDE ===

ENTWICKLUNG. Keine Note, sondern eine Aufzeichnung ueber die Zeit, je
Person. Fuenf Arten, und ihre REIHENFOLGE ist Absicht: "Das laeuft gut"
und "Danke dafuer" stehen vorn. Wer ein Formular oeffnet, dessen erstes
Feld "Problem" heisst, schreibt Probleme auf.

"ZU VIEL GERADE" IST DIE WICHTIGSTE ART und die, die es sonst nirgends
gibt. Der haeufigste Grund zu gehen ist Zeitmangel, und der zeigt sich
frueh -- nur schreibt ihn niemand auf, weil es kein Feld dafuer gibt.

TALENTE. Die vier Merkmale sind die aus der Literatur, nicht
ausgedacht, dazu die Empfehlung aus dem Team. Der Status IST die
Probezeit: `offen` heisst beobachtet, `angenommen` heisst angesprochen
-- und was daraus wird, entscheidet sich in "Personen & Zugaenge" mit
einem Zugang auf Stufe "Probe". Eine eigene Kandidatentabelle waere ein
zweiter Ort fuer dieselbe Frage.

DIE BRUECKE. Die Person sieht den Entwicklungs-Bereich NICHT -- eine
halb sichtbare Akte ist schlimmer als eine geschlossene, weil niemand
mehr weiss, was der andere gerade liest. Damit Anerkennung trotzdem
ankommt, laesst sich jeder Eintrag EINMAL als Nachricht in den Chat
schicken, mit Art, Titel und dem, was daraus folgen soll. Danach steht
im Eintrag, wann es geschehen ist: Die Frage "habe ich ihr das
eigentlich schon gesagt?" beantwortet man nach zwei Wochen falsch.

ZWEI KACHELN UND NICHT DREI: Aufgaben an das Team gibt es laengst, mit
Person, Frist und Status. Eine zweite Stelle dafuer waere ein zweiter
Ort, an dem man nachsehen muesste, welche Aufgabe wirklich gilt.

DIE GRUPPE HEISST NICHT "TEAM FUEHREN". Das waere der bequeme Name und
der falsche: Im Haus gilt, dass DogFather nicht ueber seinem Team steht.
"Entwicklung & Nachwuchs" sagt, was drinsteht.

=== KEIN NEUER BAUKASTEN ===

Beides sind BEREICHE wie das Ideen-Board: dieselbe Tabelle, dieselbe
Seite, dieselben Regeln fuers Anlegen, Aendern und Loeschen. Neu sind
eine Spalte (`gesendet_am`) und eine Route. Die Sendefunktion selbst
steht im Chat-Modul und nicht daneben: Ein Zweier-Gespraech darf es nur
einmal geben, die Leseraender muessen mitwandern, Live-Strom und
Benachrichtigung haengen daran -- wer das nachbaut, hat zwei Fassungen,
und die zweite ist die, in der jemand eine Nachricht nicht bekommt.

=== WAS DIE PRUEFUNG GEFUNDEN HAT ===

"Zugeordneter Creator existiert nicht" -- beim Anlegen eines Eintrags
ueber ein Teammitglied. Die Regel verlangte einen Creator; bei
Entwicklung geht es um Menschen aus dem Team. Die Meldung war dabei
selbst irrefuehrend: Die Person existiert sehr wohl, sie ist nur kein
Creator. Beides behoben.

Und meine EIGENE Pruefung von vor einer Stunde wurde rot: Sie verlangte,
dass jede Zusatzkachel in der Gruppe "Team Dogi" steht. Die naheliegende
Reparatur waere gewesen, die Gruppe gar nicht mehr zu pruefen -- das
haette die Aussage weggeworfen. Jetzt steht die erlaubte Liste
ausgeschrieben da: Eine Kachel fuer zwei Menschen darf nicht in einer
Gruppe landen, die alle sehen. Kommt eine dritte Gruppe, wird die Zeile
rot, und das ist Absicht.

Die zwei neuen Farbtoene trennen ueber Saettigung und Helligkeit statt
ueber den Farbwinkel -- bei 25 Toenen ist der Kreis voll, und die
groessten Luecken waeren ein drittes Gruen zwischen zwei vorhandenen.
Bei einer achtundzwanzigsten Kachel brauchen die Kacheln Gruppenfarben
statt Einzelfarben; das steht als Notiz im Quelltext.

pruef-entwicklung 33 (neu) · pruef-start-ansicht 27 Kacheln, sechs
Gruppen · pruef-rollen 278 -> 282 · pruef-modi-checkliste 70 ·
pruef-haus-trennung 62 · pruef-rueckmeldung 30 · pruef-modi-ideen 30 ·
pruef-modi-verborgen 80 · pruef-css-klassen gruen · pruef-modi-wortleck 5.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-11 01:11:30 +02:00

268 lines
13 KiB
JavaScript

/* ENTWICKLUNG UND TALENTE (11.09.2026)
Filipe: "ich will dass ich genau so eine kategorie habe in der
dogfather und rechten hand rollen, wo wir aufgaben oder bewertungen
über modis eingeben können. auch für zuschauer die vielleicht modis
werden könnten wäre auch geil."
---------------------------------------------------------------------
DIE PRÜFUNG, DERENTWEGEN ES DIESE DATEI GIBT
In „Entwicklung" steht, was über Menschen aufgeschrieben wird. Wenn
diese beiden Bereiche irgendwo durchsickern, ist der Schaden nicht
technisch, sondern menschlich -- und er lässt sich nicht zurücknehmen.
Zwei Schlösser sichern das: der ZUGANG (fuehrtTeamDogi) und die
SICHTBARKEIT der Zeilen. Geprüft werden beide, und zwar von der
falschen Seite aus: Ein Modi klopft an die Liste, an eine einzelne
Zeile, ans Ändern und ans Löschen -- und muss überall dieselbe
Antwort bekommen wie jemand, für den es diese Bereiche nicht gibt.
Dazu die Brücke: Ein Eintrag lässt sich EINMAL an die Person
schicken. Gemessen wird, dass die Nachricht wirklich in ihrem Chat
ankommt (sonst wäre „geschickt" nur ein Datum) und dass ein zweiter
Versuch abgelehnt wird.
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request } from "node:http";
const ordner = mkdtempSync(join(tmpdir(), "ws-entw-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { portMussFreiSein } = await import("./helfer-port.mjs");
await portMussFreiSein(4389, "die Entwicklungspruefung");
process.env.PORT = "4389";
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 }); };
await import("./index.js");
await new Promise((r) => setTimeout(r, 1000));
for (const art of ["uncaughtException", "unhandledRejection"]) {
process.on(art, (f) => { console.error(""); console.error("ABBRUCH:", f?.stack || f); process.exit(1); });
}
setTimeout(() => { console.error(""); console.error("ABBRUCH: 150 s vorbei."); process.exit(1); },
150000).unref?.();
let fehler = 0, geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const CREW = "crew.dogfather-universe.com";
const WS = "workspace.dogfather-universe.com";
function hole(pfad, { host = WS, methode = "GET", rumpf = null, keks = "" } = {}) {
return new Promise((fertig, schief) => {
const kopf = { Host: host };
if (rumpf) { kopf["Content-Type"] = "application/json"; kopf["Content-Length"] = Buffer.byteLength(rumpf); }
if (keks) kopf.Cookie = keks;
const a = request({ host: "127.0.0.1", port: 4389, path: pfad, method: methode, headers: kopf },
(antwort) => {
let text = "";
antwort.setEncoding("utf8");
antwort.on("data", (t) => { text += t; });
antwort.on("end", () => {
let daten = null;
try { daten = JSON.parse(text); } catch { /* kein JSON */ }
fertig({ status: antwort.statusCode, text, daten,
keks: (antwort.headers["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ") });
});
});
a.on("error", schief);
a.setTimeout(15000, () => { a.destroy(new Error("Zeitueberschreitung")); });
if (rumpf) a.write(rumpf);
a.end();
});
}
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
await hole("/workspace/api/anmelden", { methode: "POST",
rumpf: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
function 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("VanVan", "hand", "CODE-HAND-0001");
const idModi = anlegen("Ayla", "modi", "CODE-TEAM-0001");
anlegen("Cigdem", "manager", "CODE-MANA-0001");
async function anmelden(rolle, code, wand) {
const a = await hole("/workspace/api/anmelden", { host: wand, methode: "POST",
rumpf: JSON.stringify({ rolle, code }) });
return { ok: a.status === 200, keks: a.keks };
}
const dogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
const hand = await anmelden("hand", "CODE-HAND-0001", CREW);
const ayla = await anmelden("modi", "CODE-TEAM-0001", CREW);
const mana = await anmelden("manager", "CODE-MANA-0001", WS);
ok(dogi.ok && hand.ok && ayla.ok && mana.ok, "alle vier sind angemeldet");
const ENTW = "/workspace/api/bereich/entwicklung";
const TAL = "/workspace/api/bereich/talente";
/* =======================================================================
1. Es gibt die beiden Bereiche -- und ihre Felder
======================================================================= */
console.log("");
console.log("=== Die beiden Bereiche ===");
{
const a = await hole(ENTW, { host: CREW, keks: dogi.keks });
ok(a.daten?.einstellung?.name === "Entwicklung",
`DogFather kann Entwicklung oeffnen (${a.daten?.einstellung?.name})`);
const arten = Object.keys(a.daten?.einstellung?.arten || {});
ok(arten.length === 5, `fuenf Arten (${arten.join(", ")})`);
/* DIE REIHENFOLGE IST ABSICHT: Wer ein Formular oeffnet, dessen
erstes Feld "Problem" heisst, schreibt Probleme auf. */
ok(arten[0] === "staerke" && arten[1] === "dank",
"und die ersten beiden sind die guten");
ok(arten.includes("last"),
"„Zu viel gerade“ ist dabei -- der haeufigste Grund aufzuhoeren");
const b = await hole(TAL, { host: CREW, keks: hand.keks });
ok(b.daten?.einstellung?.name === "Talente",
`die rechte Hand kann Talente oeffnen (${b.daten?.einstellung?.name})`);
const tArten = Object.keys(b.daten?.einstellung?.arten || {});
ok(tArten.includes("empfohlen"),
`die Empfehlung aus dem Team ist dabei (${tArten.join(", ")})`);
}
/* =======================================================================
2. DIE EIGENTLICHE PRUEFUNG: Wer NICHT hineinkommt
======================================================================= */
console.log("");
console.log("=== Wer nicht hineinkommt ===");
let eintragId = 0;
{
/* Erst ein Eintrag, damit es ueberhaupt etwas zu verbergen gibt --
sonst waere jede Sperre unten gruen ueber einer leeren Liste. */
const a = await hole(ENTW, { host: CREW, methode: "POST", keks: dogi.keks,
rumpf: JSON.stringify({
art: "staerke", titel: "Ruhig geblieben, als es kippte",
text: "Hat den Streit im Chat gedreht, ohne jemanden bloßzustellen.",
datum: "2026-09-11", creator_id: idModi, einsatz: "Beim nächsten Live wieder ihr überlassen",
}) });
eintragId = a.daten?.id;
ok(a.status === 201 && eintragId > 0,
`ein Eintrag ueber Ayla (${a.status}) ${a.status === 201 ? "" : a.text.slice(0, 160)}`);
for (const [wer, keks, wand] of [["ein Teammitglied", ayla.keks, CREW],
["ein Manager", mana.keks, WS]]) {
const liste = await hole(ENTW, { host: wand, keks });
ok(liste.status === 404, `${wer}: die Liste gibt es nicht (${liste.status})`);
const aendern = await hole(`${ENTW}/${eintragId}`, { host: wand, methode: "PATCH",
keks, rumpf: JSON.stringify({ titel: "umgeschrieben" }) });
ok(aendern.status === 404, `${wer}: aendern geht nicht (${aendern.status})`);
const weg = await hole(`${ENTW}/${eintragId}`, { host: wand, methode: "DELETE", keks });
ok(weg.status === 404, `${wer}: loeschen auch nicht (${weg.status})`);
const tal = await hole(TAL, { host: wand, keks });
ok(tal.status === 404, `${wer}: Talente ebenfalls nicht (${tal.status})`);
}
/* Und die Zeile steht unveraendert da -- sonst waeren die 404 oben
Antworten auf Anfragen, die trotzdem gewirkt haben. */
const heil = d.prepare("SELECT titel FROM eintraege WHERE id = ?").get(eintragId);
ok(heil?.titel === "Ruhig geblieben, als es kippte", "der Eintrag ist unveraendert");
/* GEGENPROBE: Die beiden, die duerfen, sehen ihn sehr wohl. Ohne sie
hiesse "niemand sieht ihn" auch dann gruen, wenn er gar nicht
angekommen waere. */
for (const [wer, keks] of [["DogFather", dogi.keks], ["die rechte Hand", hand.keks]]) {
const a = await hole(ENTW, { host: CREW, keks });
ok((a.daten?.eintraege || []).some((e) => e.id === eintragId),
`${wer} sieht ihn`);
}
}
/* =======================================================================
3. Die Bruecke: einmal an die Person schicken
======================================================================= */
console.log("");
console.log("=== An die Person schicken ===");
{
const a = await hole(`${ENTW}/${eintragId}/senden`, { host: CREW, methode: "POST",
keks: dogi.keks, rumpf: "{}" });
ok(a.status === 200, `geschickt (${a.status})`);
/* KOMMT SIE WIRKLICH AN? Ohne diese Zeile waere "geschickt" nur ein
Datum in einer Spalte. Nachgesehen wird in AYLAS Chat, nicht in
der Antwort der Route. */
const raeume = await hole("/workspace/api/chat/raeume", { host: CREW, keks: ayla.keks });
const raum = (raeume.daten?.raeume || [])[0];
ok(!!raum, `Ayla hat ein Gespraech (${(raeume.daten?.raeume || []).length})`);
const verlauf = await hole(`/workspace/api/chat/raeume/${raum?.id}/nachrichten`,
{ host: CREW, keks: ayla.keks });
const texte = (verlauf.daten?.nachrichten || []).map((n) => n.text || "").join(" ");
ok(/Ruhig geblieben, als es kippte/.test(texte), "und die Nachricht steht darin");
ok(/Das läuft gut/.test(texte),
"mit der Art davor -- sonst waere es ein Fetzen ohne Zusammenhang");
ok(/Beim nächsten Live wieder ihr überlassen/.test(texte),
"und dem, was daraus folgen soll");
/* EINMAL IST EINMAL. Zweimal dasselbe Lob ist kein doppeltes Lob. */
const b = await hole(`${ENTW}/${eintragId}/senden`, { host: CREW, methode: "POST",
keks: dogi.keks, rumpf: "{}" });
ok(b.status === 409, `ein zweites Mal wird abgelehnt (${b.status})`);
const nachher = await hole(`/workspace/api/chat/raeume/${raum?.id}/nachrichten`,
{ host: CREW, keks: ayla.keks });
ok((nachher.daten?.nachrichten || []).length === (verlauf.daten?.nachrichten || []).length,
"und es kam keine zweite Nachricht an");
/* Ein Eintrag ohne Person laesst sich nicht schicken -- an wen auch. */
const ohne = await hole(ENTW, { host: CREW, methode: "POST", keks: dogi.keks,
rumpf: JSON.stringify({ art: "wachsen", titel: "Allgemeine Notiz", datum: "2026-09-11" }) });
const c = await hole(`${ENTW}/${ohne.daten?.id}/senden`, { host: CREW, methode: "POST",
keks: dogi.keks, rumpf: "{}" });
ok(c.status === 400, `ein Eintrag ohne Person geht nicht raus (${c.status})`);
}
/* =======================================================================
4. Die Kacheln
======================================================================= */
console.log("");
console.log("=== Die Kacheln ===");
{
for (const [wer, keks] of [["DogFather", dogi.keks], ["die rechte Hand", hand.keks]]) {
const a = await hole("/workspace/api/ich", { host: CREW, keks });
const alle = [...(a.daten?.bereiche || []), ...(a.daten?.bereiche_zusatz || [])];
const beide = alle.filter((k) => /b=(entwicklung|talente)/.test(k.ziel || ""));
ok(beide.length === 2, `${wer}: beide Kacheln (${beide.map((k) => k.name).join(", ") || "keine"})`);
ok(beide.every((k) => k.gruppe === "Entwicklung & Nachwuchs"),
`${wer}: in einer eigenen Gruppe (${[...new Set(beide.map((k) => k.gruppe))].join(", ")})`);
}
const a = await hole("/workspace/api/ich", { host: CREW, keks: ayla.keks });
const alle = [...(a.daten?.bereiche || []), ...(a.daten?.bereiche_zusatz || [])];
ok(!alle.some((k) => /b=(entwicklung|talente)/.test(k.ziel || "")),
`ein Teammitglied bekommt sie nicht angeboten (${alle.length} Kacheln)`);
/* Kein Farbton doppelt -- zwei Kacheln in derselben Farbe sind ein
Fehler, den man sonst erst am Bildschirm findet. */
const dogiIch = await hole("/workspace/api/ich", { host: CREW, keks: dogi.keks });
const toene = [...(dogiIch.daten?.bereiche || [])].map((k) => k.ton).filter((t) => t !== undefined);
ok(new Set(toene).size === toene.length, `kein Farbton doppelt (${toene.length} Kacheln)`);
}
d.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log("");
console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"} -- ${geprueft} geprueft, ${fehler} Fehler`);
process.exit(fehler ? 1 : 0);