diff --git a/server/bild-checkliste-stufen.mjs b/server/bild-checkliste-stufen.mjs
new file mode 100644
index 00000000..aa9892ac
--- /dev/null
+++ b/server/bild-checkliste-stufen.mjs
@@ -0,0 +1,142 @@
+/* Bild der vier Urteilsstufen (24.09.2026)
+
+ Gebaut wurde heute die zweite Skala fuer das Agenturhaus: Passt,
+ Verbesserung moeglich, Passt nicht, Trifft nicht zu. Die Zahlen
+ stimmen (pruef-checkliste-stufen: 48 Pruefungen), aber eine Zahl
+ sagt nicht, ob vier Knoepfe auf einem Handy noch nebeneinander
+ passen und ob das neue Rosé neben dem Gelb noch ruhig wirkt.
+
+ Zwei Breiten, zwei Rollen: die Betreuung (Scout, mit Knoepfen) und
+ der Creator (ohne, dafuer mit Notiz).
+
+ Aufrufen mit: node server/bild-checkliste-stufen.mjs
+*/
+import { mkdtempSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+const ordner = mkdtempSync(join(tmpdir(), "ws-bild-st-"));
+process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+const { portMussFreiSein } = await import("./helfer-port.mjs");
+await portMussFreiSein(4333, "das Bildwerkzeug fuer die Stufen");
+process.env.PORT = "4333";
+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, 1200));
+const BASIS = "http://127.0.0.1:4333";
+
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes } = await import("node:crypto");
+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 j = 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, j);
+ return d.prepare("SELECT last_insert_rowid() AS id").get().id;
+}
+anlegen("Filipe", "admin", "CODE-DOGI-0001");
+const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
+const idCrea = anlegen("Luna", "creator", "CODE-CREA-0001");
+d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
+ .run(idCrea, idScout, j);
+d.close();
+
+const anmelden = async (rolle, code) => {
+ const a = await fetch(BASIS + "/workspace/api/anmelden", {
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ rolle, code }) });
+ return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
+};
+const kScout = await anmelden("scout", "CODE-SCOU-0001");
+
+/* Jede der vier Stufen einmal setzen -- sonst zeigt das Bild vier
+ graue Knoepfe und beantwortet keine der beiden Fragen. */
+for (const [schluessel, stufe, grund] of [
+ ["ton-geprueft", "gut", ""],
+ ["licht-sitzt", "verbessern", "Das Gesicht liegt im Schatten, der Rest ist hell. Lampe eine Handbreit höher."],
+ ["titel-und-thema-stehen", "passt_nicht", "Der Titel sagt nichts über das Thema. Bitte vor dem nächsten Mal ändern."],
+ ["moderation-geklaert", "entfaellt", "Du sendest bisher ohne Gäste – der Punkt kommt bei dir nicht vor."],
+]) {
+ const a = await fetch(`${BASIS}/workspace/api/checkliste/live/${schluessel}`, {
+ method: "PUT", headers: { "Content-Type": "application/json", Cookie: kScout },
+ body: JSON.stringify({ stufe, begruendung: grund, creator_id: idCrea }) });
+ if (!a.ok) console.log(" konnte nicht setzen:", schluessel, a.status, await a.text());
+}
+
+const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await pw.chromium.launch();
+
+for (const [wer, rolle, code, breite, datei] of [
+ ["Scout", "scout", "CODE-SCOU-0001", 1280, "bild-stufen-betreuung.png"],
+ ["Creator", "creator", "CODE-CREA-0001", 1280, "bild-stufen-creator.png"],
+ ["Scout", "scout", "CODE-SCOU-0001", 390, "bild-stufen-handy.png"],
+]) {
+ const ctx = await browser.newContext({
+ viewport: { width: breite, height: breite < 500 ? 900 : 1000 }, deviceScaleFactor: 2 });
+ const seite = await ctx.newPage();
+ const laut = [];
+ seite.on("console", (m) => { if (m.type() === "error") laut.push(m.text()); });
+ seite.on("pageerror", (f) => laut.push(String(f)));
+ await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
+ await seite.click(`.rolle[data-rolle="${rolle}"]`);
+ await seite.fill("#code", code);
+ await seite.click("button[type=submit]");
+ await seite.waitForURL("**/start.html", { timeout: 15000 });
+ await seite.goto(BASIS + "/workspace/bereich.html?b=live", { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1200);
+ /* Die erste Gruppe aufklappen -- zugeklappt sieht man nichts von dem,
+ was hier zu beurteilen ist. */
+ await seite.evaluate(() => {
+ const k = document.querySelector(".fest-gruppe__kopf");
+ if (k && k.getAttribute("aria-expanded") !== "true") k.click();
+ });
+ await seite.waitForTimeout(500);
+ /* NUR DIE ERSTEN VIER KARTEN. Eine Aufnahme ueber 200 Punkte wird
+ so lang, dass man beim Ansehen jedes Detail wegskaliert -- und
+ genau die Details sind die Frage: Passen vier Knoepfe nebeneinander,
+ ist das Rosé ruhig genug, steht die Notiz lesbar da. */
+ await seite.evaluate(() => {
+ const k = [...document.querySelectorAll(".fest-punkt")];
+ for (let i = 4; i < k.length; i++) k[i].remove();
+ for (const g of [...document.querySelectorAll(".fest-gruppe")].slice(1)) g.remove();
+ });
+ await seite.waitForTimeout(200);
+ const block = await seite.$(".liste-fest");
+ await (block || seite).screenshot({ path: datei });
+ /* Und das Nachfragefenster einmal offen -- dort steht die Frage,
+ die der Katalog zu dieser Stufe mitbringt. */
+ if (datei === "bild-stufen-betreuung.png") {
+ await seite.evaluate(() => {
+ const b = [...document.querySelectorAll('.stufe-knopf[data-stufe="passt_nicht"]')][1];
+ if (b) b.click();
+ });
+ await seite.waitForTimeout(700);
+ await seite.screenshot({ path: "bild-stufen-nachfrage.png" });
+ }
+ const zahlen = await seite.evaluate(() => ({
+ knoepfe: document.querySelectorAll(".fest-punkt:not([hidden]) .stufe-knopf").length,
+ bilanz: [...document.querySelectorAll(".bilanz-zahl")]
+ .map((z) => `${z.querySelector(".bilanz-zahl__n")?.textContent} ${z.querySelector(".bilanz-zahl__text")?.textContent}`),
+ ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
+ }));
+ console.log(` ${datei} (${wer}, ${breite}px): ${zahlen.knoepfe} Knöpfe, `
+ + `Bilanz [${zahlen.bilanz.join(" | ")}], ${zahlen.ueber}px über`
+ + (laut.length ? ` KONSOLE: ${laut.join(" | ")}` : ""));
+ await ctx.close();
+}
+await browser.close();
+try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+process.exit(0);
diff --git a/server/pruef-checkliste-stufen.mjs b/server/pruef-checkliste-stufen.mjs
new file mode 100644
index 00000000..80cdf4fa
--- /dev/null
+++ b/server/pruef-checkliste-stufen.mjs
@@ -0,0 +1,520 @@
+/* =====================================================================
+ pruef-checkliste-stufen.mjs — die zwei Urteilsskalen und die Grenze
+ zwischen den Häusern (24.09.2026)
+
+ Filipe, mit einem Bildschirmfoto der LIVE-Punkte:
+ *"diese aufgaben auf screen. alle auf dieser app getrennt von denen
+ auf der team dogi website bitte, sehr wichtig. die sollen die manager
+ und scouts bewerten können mit passt passt nicht verbesserung möglich
+ und was weiß ich. und die creator sollen sehen was bei ihnen passt
+ oder nicht mit der notiz vom manager oder scout. ... und wie gesagt
+ von der team dogi seite da ist ein anderes system auf diesen
+ aufgaben."*
+
+ WAS ER GESEHEN HATTE, UND WARUM ES KEINE ANSICHTSSACHE WAR.
+ Auf workspace.dogfather-universe.com stand unter "Vor der Sendung"
+ die Liste eines MODIS. Nachgemessen am echten Bestand: Die Auswahl
+ füllte sich aus allen Creatorn PLUS allen Modis, sortiert nach Namen,
+ und ohne ausdrückliche Wahl nimmt die Seite den ersten. Der erste
+ Name im Haus ist "Diene" -- eine Modi. Bewerten ließ sich dort
+ nichts, weil ein Modi-Bericht nur dem Modi selbst gehört.
+
+ DIESE PRÜFUNG MISST VIER DINGE, und jedes einzeln:
+
+ 1. DIE GRENZE. Auf der Agenturadresse steht kein Mensch aus dem
+ Teamhaus zur Auswahl, auf der Teamadresse kein Creator -- auch
+ nicht für DogFather, der in beiden Häusern arbeitet. Und die
+ Grenze hält auch, wenn jemand die Nummer von Hand einträgt.
+
+ 2. DIE SKALA. Vier Stufen im Agenturhaus, zwei im Teamhaus. Was
+ hier gilt, gilt dort nicht: "Passt nicht" an einem Modi muss
+ abgelehnt werden.
+
+ 3. DIE NOTIZ. Ohne Satz kein "Passt nicht" und kein "Verbesserung
+ möglich" -- und der Creator sieht den Satz samt Rolle dessen,
+ der ihn geschrieben hat.
+
+ 4. DIE UMSTELLUNG DER DATENBANK. Die CHECK-Regel ließ bis heute
+ nur drei Werte zu; sie lässt sich in SQLite nicht ändern, die
+ Tabelle muss neu gebaut werden. Am 11.09.2026 hat genau dieser
+ Griff drei Spalten samt Inhalt gekostet, ohne Fehlermeldung.
+ Deshalb wird hier eine Datenbank mit dem ALTEN Bauplan und
+ echten Zeilen darin hergestellt und danach nachgezählt --
+ Zeilen UND Spalten.
+
+ GEGENPROBEN, weil eine Prüfung, die immer bestätigt, nichts
+ bestätigt: Zu jeder Schranke steht daneben der Fall, der durchkommen
+ MUSS. Eine Absage allein beweist nur, dass etwas abgelehnt wird --
+ nicht, dass das Richtige abgelehnt wird.
+ ===================================================================== */
+
+import { mkdtempSync, readdirSync, 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-stufen-"));
+process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+
+const { eigenerPort } = await import("./helfer-port.mjs");
+const PORT = await eigenerPort(import.meta, "die Checklisten-Stufen");
+
+process.env.PORT = `${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 }); };
+await import("./index.js");
+/* Die Notbremse NACH index.js -- der Server hängt dort zwei
+ Auffangnetze ein, die einen Absturz nur protokollieren. Richtig für
+ den Betrieb, fatal für eine Prüfung: Ohne eigene Zuhörer bliebe der
+ Prozess nach einem Fehler ewig stehen. */
+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?.();
+await new Promise((r) => setTimeout(r, 900));
+
+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, 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: PORT, 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();
+ });
+}
+
+/* ---- Bestand -------------------------------------------------------- */
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes } = await import("node:crypto");
+await hole("/workspace/api/anmelden", { host: WS, 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;
+}
+/* Die Namen sind ABSICHTLICH so gewählt, dass die Modi alphabetisch
+ VOR dem Creator steht -- genau die Lage, die den Fehler auf Filipes
+ Bildschirm erzeugt hat. Hieße die Modi "Zoe", wäre die Prüfung grün,
+ ohne je etwas gemessen zu haben. */
+const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
+const idModi = anlegen("Ayla", "modi", "CODE-MODI-0001");
+const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
+const idMana = anlegen("Cigdem", "manager", "CODE-MANA-0001");
+const idCrea = anlegen("Luna", "creator", "CODE-CREA-0001");
+const idSpicy = anlegen("Agentur", "spicy", "CODE-SPIC-0001");
+d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idCrea, idScout, jetzt);
+
+/* =====================================================================
+ DIE ALTE TABELLE HERSTELLEN -- sonst prüft die Umstellung nichts
+
+ Der Server baut sie beim ersten Aufruf einer Checkliste mit der
+ NEUEN Schranke. Eine Prüfung, die danach nachsieht, findet die neue
+ Schranke vor und meldet "in Ordnung", ohne dass je etwas umgestellt
+ wurde. Das ist die Sorte grüner Haken, die nichts bedeutet.
+
+ Deshalb wird hier der Zustand von gestern hergestellt: der alte
+ Bauplan, die vier später ergänzten Spalten, und echte Zeilen darin.
+ Die Zeilen tragen einen Wert in JEDER Spalte -- sonst könnte ein
+ Verlust unbemerkt bleiben, weil überall ohnehin NULL stünde.
+ ===================================================================== */
+d.exec(`
+ CREATE TABLE punkt_stand (
+ bereich TEXT NOT NULL,
+ schluessel TEXT NOT NULL,
+ creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
+ stufe TEXT NOT NULL CHECK (stufe IN ('offen','gut','verbessern')),
+ begruendung TEXT,
+ von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
+ am TEXT NOT NULL,
+ behandelt_am TEXT, behandelt_von INTEGER, behandelt_art TEXT, aufgabe_id INTEGER,
+ PRIMARY KEY (bereich, schluessel, creator_id)
+ );
+`);
+d.prepare(`INSERT INTO punkt_stand
+ (bereich, schluessel, creator_id, stufe, begruendung, von, am,
+ behandelt_am, behandelt_von, behandelt_art, aufgabe_id)
+ VALUES ('live','ton-geprueft',?,'gut','Klang sauber.',?,?,?,?,'aufgabe',77)`)
+ .run(idCrea, idScout, jetzt, jetzt, idDogi);
+d.prepare(`INSERT INTO punkt_stand
+ (bereich, schluessel, creator_id, stufe, begruendung, von, am)
+ VALUES ('live','m-ton-von-anfang-an',?,'verbessern','Erste Minute stumm.',?,?)`)
+ .run(idModi, idModi, jetzt);
+const VORHER_ZEILEN = d.prepare("SELECT COUNT(*) AS n FROM punkt_stand").get().n;
+const VORHER_SPALTEN = d.prepare("PRAGMA table_info(punkt_stand)").all().map((z) => z.name);
+d.close();
+
+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 dogiWs = await anmelden("admin", "CODE-DOGI-0001", WS);
+const dogiCrew = await anmelden("admin", "CODE-DOGI-0001", CREW);
+const scout = await anmelden("scout", "CODE-SCOU-0001", WS);
+const mana = await anmelden("manager", "CODE-MANA-0001", WS);
+const crea = await anmelden("creator", "CODE-CREA-0001", WS);
+const modi = await anmelden("modi", "CODE-MODI-0001", CREW);
+const spicy = await anmelden("spicy", "CODE-SPIC-0001", WS);
+ok(dogiWs.ok && dogiCrew.ok && scout.ok && mana.ok && crea.ok && modi.ok && spicy.ok,
+ "alle sieben sind angemeldet");
+
+const liste = (host, keks, zusatz = "") =>
+ hole(`/workspace/api/checkliste/live${zusatz}`, { host, keks });
+const setzen = (host, keks, schluessel, rumpf) =>
+ hole(`/workspace/api/checkliste/live/${schluessel}`, { host, keks,
+ methode: "PUT", rumpf: JSON.stringify(rumpf) });
+
+/* =======================================================================
+ 1. Die Umstellung der Datenbank
+ ======================================================================= */
+console.log("");
+console.log("=== Die Tabelle wurde umgebaut, ohne etwas zu verlieren ===");
+{
+ /* Der erste Aufruf löst tabellen() aus -- vorher ist nichts passiert. */
+ await liste(WS, dogiWs.keks);
+
+ const p = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
+ const bauplan = p.prepare(
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='punkt_stand'").get()?.sql || "";
+ const zeilen = p.prepare("SELECT COUNT(*) AS n FROM punkt_stand").get().n;
+ const spalten = p.prepare("PRAGMA table_info(punkt_stand)").all().map((z) => z.name);
+ const alt = p.prepare(
+ "SELECT * FROM punkt_stand WHERE creator_id = ? AND schluessel = 'ton-geprueft'").get(idCrea);
+ const rest = p.prepare(
+ "SELECT COUNT(*) AS n FROM sqlite_master WHERE name = 'punkt_stand_neu'").get().n;
+ p.close();
+
+ ok(/passt_nicht/.test(bauplan) && /entfaellt/.test(bauplan),
+ "die Schranke lässt jetzt alle Stufen beider Häuser zu");
+ ok(zeilen === VORHER_ZEILEN,
+ `alle Zeilen sind noch da (${zeilen} von ${VORHER_ZEILEN})`);
+ /* DIE ZEILENZÄHLUNG ALLEIN BEWEIST NICHTS -- am 11.09.2026 war sie
+ unverändert, während drei Spalten samt Inhalt verschwunden waren.
+ Deshalb daneben die Spalten. */
+ const verloren = VORHER_SPALTEN.filter((sp) => !spalten.includes(sp));
+ ok(verloren.length === 0,
+ `alle ${VORHER_SPALTEN.length} Spalten sind noch da`
+ + (verloren.length ? ` — fehlen: ${verloren.join(", ")}` : ""));
+ /* UND DER INHALT DER SPALTEN, nicht nur ihre Namen. Eine Tabelle mit
+ den richtigen Überschriften und leeren Feldern zählt genauso viele
+ Zeilen und Spalten wie eine richtige. */
+ ok(alt?.begruendung === "Klang sauber." && alt?.von === idScout
+ && alt?.behandelt_art === "aufgabe" && alt?.aufgabe_id === 77,
+ `auch der Inhalt der später ergänzten Spalten steht noch `
+ + `(behandelt_art=${alt?.behandelt_art}, aufgabe_id=${alt?.aufgabe_id})`);
+ ok(rest === 0, "die Behelfstabelle ist wieder weg");
+
+ /* UND DIE SICHERUNG? Sie ist die einzige der fuenf Absicherungen,
+ die man nach einem gelungenen Lauf nicht mehr sieht -- und die
+ einzige, die zaehlt, wenn er misslingt. Beim ersten Anlauf dieses
+ Umbaus hatte ich die Umstellung selbst geschrieben und genau sie
+ vergessen; gefunden habe ich das nicht durch eine Pruefung,
+ sondern beim Nachsehen, wie das Haus dasselbe Problem schon
+ dreimal geloest hat. Diese Zeile sorgt dafuer, dass es beim
+ naechsten Mal die Pruefung findet. */
+ const sicherungen = readdirSync(ordner).filter((f) => f.includes(".vor-passt_nicht-"));
+ ok(sicherungen.length === 1,
+ `vor dem Umbau wurde die ganze Datenbank gesichert (${sicherungen[0] || "keine gefunden"})`);
+
+ /* GEGENPROBE: Die Prüfung oben kann einen Verlust überhaupt sehen. */
+ const erfunden = VORHER_SPALTEN.filter((sp) => !["bereich", "aufgabe_id"].includes(sp));
+ ok(VORHER_SPALTEN.filter((sp) => !erfunden.includes(sp)).length === 2,
+ "Gegenprobe: eine Spaltenliste mit zwei fehlenden Namen fällt auf");
+}
+
+/* =======================================================================
+ 2. Die Grenze zwischen den Häusern
+ ======================================================================= */
+console.log("");
+console.log("=== Auf welcher Adresse steht wer zur Auswahl? ===");
+{
+ const ws = (await liste(WS, dogiWs.keks)).daten;
+ const cr = (await liste(CREW, dogiCrew.keks)).daten;
+ const namenWs = (ws?.creatoren || []).map((z) => z.name);
+ const namenCr = (cr?.creatoren || []).map((z) => z.name);
+
+ ok(namenWs.includes("Luna") && !namenWs.includes("Ayla"),
+ `auf workspace. nur die Agentur (${namenWs.join(", ") || "leer"})`);
+ ok(namenCr.includes("Ayla") && !namenCr.includes("Luna"),
+ `auf crew. nur das Team (${namenCr.join(", ") || "leer"})`);
+
+ /* DAS WAR DER EIGENTLICHE BEFUND: nicht die Auswahlliste, sondern
+ das, was ohne Wahl VORAUSGEWÄHLT wird. "Ayla" steht alphabetisch
+ vor "Luna" -- bis zum 24.09.2026 bekam DogFather deshalb auf der
+ Agenturadresse zuverlässig die Liste einer Modi vorgesetzt. */
+ ok(ws?.creator_id === idCrea,
+ `ohne Wahl steht auf workspace. der Creator da (${ws?.creator_id} = ${idCrea})`);
+ ok(cr?.creator_id === idModi,
+ `und auf crew. die Modi (${cr?.creator_id} = ${idModi})`);
+
+ /* Und die Punkte selbst: Die Teamfassung erkennt man am Präfix "m-". */
+ const ersterWs = ws?.gruppen?.[0]?.punkte?.[0]?.schluessel || "";
+ const ersterCr = cr?.gruppen?.[0]?.punkte?.[0]?.schluessel || "";
+ ok(!ersterWs.startsWith("m-"),
+ `auf workspace. steht der Creator-Katalog ("${ersterWs}")`);
+ ok(ersterCr.startsWith("m-"),
+ `auf crew. der des Teams ("${ersterCr}")`);
+ ok(ws?.gruppen?.[0]?.text !== cr?.gruppen?.[0]?.text,
+ "auch der Satz unter der Überschrift ist nicht derselbe");
+}
+
+console.log("");
+console.log("=== Die Grenze hält auch gegen eine von Hand eingetragene Nummer ===");
+{
+ /* Die Auswahlliste auszudünnen ist Kosmetik, solange die Nummer in
+ der Adresse durchgeht. Genau so sind in diesem Haus schon Wege
+ offen geblieben, die auf keinem Bildschirm zu sehen waren. */
+ const a = await liste(WS, dogiWs.keks, `?creator_id=${idModi}`);
+ ok(a.status === 404,
+ `DogFather kommt auf workspace. nicht an die Liste der Modi (${a.status})`);
+ const b = await liste(CREW, dogiCrew.keks, `?creator_id=${idCrea}`);
+ ok(b.status === 404,
+ `und auf crew. nicht an die des Creators (${b.status})`);
+ const c = await setzen(WS, dogiWs.keks, "m-ton-von-anfang-an",
+ { stufe: "gut", creator_id: idModi });
+ ok(c.status === 404, `auch schreibend nicht (${c.status})`);
+
+ /* GEGENPROBE: Auf der jeweils RICHTIGEN Adresse geht derselbe Griff
+ durch. Ohne sie bewiese die 404 oben nur, dass irgendetwas
+ abgelehnt wird -- nicht, dass die Adresse den Unterschied macht. */
+ const d1 = await liste(CREW, dogiCrew.keks, `?creator_id=${idModi}`);
+ ok(d1.status === 200,
+ `auf crew. dagegen schon -- die Adresse macht den Unterschied (${d1.status})`);
+ const d2 = await liste(WS, dogiWs.keks, `?creator_id=${idCrea}`);
+ ok(d2.status === 200, `und auf workspace. der Creator (${d2.status})`);
+}
+
+console.log("");
+console.log("=== Wer gar keine Liste hat, bekommt auch keine ===");
+{
+ /* `darfCreator` sagt für DogFather bei JEDER Personennummer ja. Bis
+ zum 24.09.2026 konnte er damit einen Stand an einer Managerin
+ setzen -- für einen Punkt, der in ihrer Ansicht nie erscheint.
+ Die Zeile lag danach in der Datenbank und war nirgends zu sehen. */
+ const a = await liste(WS, dogiWs.keks, `?creator_id=${idMana}`);
+ ok(a.status === 404, `eine Managerin hat keine Checkliste (${a.status})`);
+ const b = await setzen(WS, dogiWs.keks, "ton-geprueft",
+ { stufe: "gut", creator_id: idMana });
+ ok(b.status === 404, `und bekommt auch keinen Stand gesetzt (${b.status})`);
+ const c = await setzen(WS, dogiWs.keks, "ton-geprueft",
+ { stufe: "gut", creator_id: idDogi });
+ ok(c.status === 404, `DogFather sich selbst ebenfalls nicht (${c.status})`);
+}
+
+/* =======================================================================
+ 3. Die Skala -- vier Stufen hier, zwei dort
+ ======================================================================= */
+console.log("");
+console.log("=== Zwei Häuser, zwei Skalen ===");
+{
+ const ws = (await liste(WS, dogiWs.keks)).daten;
+ const cr = (await liste(CREW, dogiCrew.keks)).daten;
+ const wsS = (ws?.stufen || []).map((s) => s.schluessel);
+ const crS = (cr?.stufen || []).map((s) => s.schluessel);
+
+ ok(wsS.join(",") === "gut,verbessern,passt_nicht,entfaellt",
+ `im Agenturhaus vier Stufen (${wsS.join(", ")})`);
+ ok(crS.join(",") === "gut,verbessern",
+ `im Teamhaus zwei (${crS.join(", ")})`);
+ /* Jede Stufe braucht ein Wort und eine Bilanzzeile -- sonst steht in
+ der Oberfläche ein leerer Knopf oder eine leere Kachel. */
+ const stumm = (ws?.stufen || []).filter((s) => !s.name || !s.bilanz);
+ ok(stumm.length === 0,
+ `jede Stufe hat Wort und Bilanzzeile (${(ws?.stufen || []).length} geprüft)`);
+ /* Und "offen" darf NICHT darin stehen: Es ist keine Stufe, sondern
+ das Zurücknehmen einer. Stünde es im Katalog, baute die Oberfläche
+ einen Knopf "Offen" neben die anderen. */
+ ok(!wsS.includes("offen"), "„offen“ steht in keinem Katalog");
+}
+
+console.log("");
+console.log("=== Was hier gilt, gilt dort nicht ===");
+{
+ const a = await setzen(WS, scout.keks, "ton-geprueft",
+ { stufe: "passt_nicht", begruendung: "Der Ton rauscht durchgehend.", creator_id: idCrea });
+ ok(a.status === 200, `ein Scout setzt „Passt nicht“ bei seinem Creator (${a.status})`);
+
+ const b = await hole("/workspace/api/checkliste/live/m-ton-von-anfang-an",
+ { host: CREW, keks: modi.keks, methode: "PUT",
+ rumpf: JSON.stringify({ stufe: "passt_nicht", begruendung: "Ging gar nicht." }) });
+ ok(b.status === 400,
+ `dieselbe Stufe an einem Modi-Bericht wird abgelehnt (${b.status})`);
+ ok(/Passt so|Verbessern/.test(b.daten?.fehler || ""),
+ `und die Absage nennt, was dort möglich ist ("${(b.daten?.fehler || "").slice(0, 60)}…")`);
+
+ /* GEGENPROBE: Der Modi kommt mit SEINER Stufe durch. Ohne sie hieße
+ die 400 oben womöglich nur "der Modi darf gar nichts". */
+ const c = await hole("/workspace/api/checkliste/live/m-ton-von-anfang-an",
+ { host: CREW, keks: modi.keks, methode: "PUT",
+ rumpf: JSON.stringify({ stufe: "verbessern", begruendung: "Erste Minute stumm." }) });
+ ok(c.status === 200, `„Verbessern“ geht dort weiterhin (${c.status})`);
+}
+
+console.log("");
+console.log("=== Ein Punkt aus dem anderen Katalog geht nicht durch ===");
+{
+ /* Bis zum 24.09.2026 lagen beide Kataloge in EINER Menge gültiger
+ Schlüssel. Ein Scout konnte damit bei einem Creator den Stand
+ eines Modi-Punktes setzen: angenommen, gespeichert, nie zu sehen. */
+ const a = await setzen(WS, scout.keks, "m-ton-von-anfang-an",
+ { stufe: "gut", creator_id: idCrea });
+ ok(a.status === 404, `ein Modi-Punkt an einem Creator: abgelehnt (${a.status})`);
+ /* EIN ANDERER PUNKT ALS ton-geprueft -- der traegt weiter unten das
+ "Passt nicht", das der Creator sehen soll. Eine Gegenprobe, die
+ den Bestand der naechsten Pruefung umschreibt, macht aus einem
+ echten Befund einen Fehlalarm; genau das ist beim ersten Lauf
+ dieser Datei passiert. */
+ const b = await setzen(WS, scout.keks, "angekuendigt",
+ { stufe: "gut", creator_id: idCrea });
+ ok(b.status === 200, `Gegenprobe: sein eigener Punkt geht (${b.status})`);
+}
+
+/* =======================================================================
+ 4. Die Notiz -- und wer sie geschrieben hat
+ ======================================================================= */
+console.log("");
+console.log("=== Ohne Satz kein Urteil ===");
+{
+ const a = await setzen(WS, scout.keks, "licht-sitzt",
+ { stufe: "passt_nicht", creator_id: idCrea });
+ ok(a.status === 400, `„Passt nicht“ ohne Satz wird abgelehnt (${a.status})`);
+ const b = await setzen(WS, scout.keks, "licht-sitzt",
+ { stufe: "verbessern", begruendung: "kurz", creator_id: idCrea });
+ ok(b.status === 400, `vier Zeichen sind noch kein Satz (${b.status})`);
+ /* GEGENPROBE: "Passt" braucht keinen -- eine Pflicht an der
+ häufigsten und harmlosesten Handlung wäre nur eine Hürde. */
+ const c = await setzen(WS, scout.keks, "licht-sitzt",
+ { stufe: "gut", creator_id: idCrea });
+ ok(c.status === 200, `„Passt“ dagegen schon (${c.status})`);
+ /* Und "Trifft nicht zu" ebenfalls nicht -- sie fragt freiwillig. */
+ const e = await setzen(WS, scout.keks, "verbindung-getestet",
+ { stufe: "entfaellt", creator_id: idCrea });
+ ok(e.status === 200, `„Trifft nicht zu“ ebenso (${e.status})`);
+}
+
+console.log("");
+console.log("=== Was der Creator sieht ===");
+{
+ const r = (await liste(WS, crea.keks)).daten;
+ const s = r?.stand?.["ton-geprueft"];
+ ok(r?.creator_id === idCrea, `er sieht immer nur sich selbst (${r?.creator_id})`);
+ ok(r?.darf_bewerten === false, `und darf nichts setzen (${r?.darf_bewerten})`);
+ ok(s?.stufe === "passt_nicht", `die Stufe steht da (${s?.stufe})`);
+ ok(s?.begruendung === "Der Ton rauscht durchgehend.", "der Satz dazu ebenfalls");
+ /* Filipe: "mit der notiz vom manager oder scout." Ein Vorname allein
+ beantwortet nicht, WER das ist -- im ersten Monat kennt ein
+ Creator die Namen nicht. */
+ ok(s?.von_name === "Patrick" && s?.von_rolle_name === "Scout",
+ `mit Name UND Rolle (${s?.von_name}, ${s?.von_rolle_name})`);
+ /* Die Skala bekommt er auch -- ohne sie könnte er die Marke an
+ seinem Punkt nicht beschriften. */
+ ok((r?.stufen || []).length === 4, `und die vier Stufen (${(r?.stufen || []).length})`);
+ ok(/Betreuung/.test(r?.einleitung || ""),
+ `der Satz oben ist seiner, nicht der der Betreuung`);
+}
+
+console.log("");
+console.log("=== Ein Manager darf dasselbe wie ein Scout ===");
+{
+ /* Filipe: "die sollen die manager und scouts bewerten können."
+ Ein Manager ohne zugeteilten Creator sieht keinen -- das ist die
+ Betreuungsregel und nicht die Haustrennung. Geprüft wird deshalb
+ mit Zuteilung. */
+ const p = new DatabaseSync(process.env.WORKSPACE_DB);
+ p.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?) "
+ + "ON CONFLICT(creator_id) DO UPDATE SET betreuer_id = excluded.betreuer_id")
+ .run(idCrea, idMana, jetzt);
+ p.close();
+ const r = (await liste(WS, mana.keks)).daten;
+ ok(r?.darf_bewerten === true, `die Managerin darf drücken (${r?.darf_bewerten})`);
+ const a = await setzen(WS, mana.keks, "titel-und-thema-stehen",
+ { stufe: "verbessern", begruendung: "Titel sagt noch nichts über das Thema.",
+ creator_id: idCrea });
+ ok(a.status === 200, `und tut es (${a.status})`);
+ ok(a.daten?.stand?.stufe === "verbessern", "der Stand kommt zurück");
+}
+
+/* =======================================================================
+ 5. Und die Zahl, die Filipe oben sieht
+ ======================================================================= */
+console.log("");
+console.log("=== Spicy Media und DogFather drücken wie vorher ===");
+{
+ /* Filipe: "spicy und dogfather sollen auch bewerten können wie
+ vorher." An ihren Rechten sollte dieser Umbau nichts ändern -- aber
+ "sollte nichts ändern" ist eine Absicht und keine Messung. Beide
+ laufen durch dieselbe neue Haus-Schranke wie alle anderen, und
+ eine Schranke, die zu viel wegnimmt, sieht von außen genauso aus
+ wie eine, die richtig sitzt. */
+ for (const [wer, sitzung] of [["Spicy Media", spicy], ["DogFather", dogiWs]]) {
+ const r = (await liste(WS, sitzung.keks)).daten;
+ ok(r?.darf_bewerten === true, `${wer} darf drücken (${r?.darf_bewerten})`);
+ ok((r?.creatoren || []).some((z) => z.id === idCrea),
+ ` und hat den Creator in der Auswahl (${(r?.creatoren || []).map((z) => z.name).join(", ")})`);
+ ok((r?.stufen || []).length === 4, ` mit allen vier Stufen (${(r?.stufen || []).length})`);
+ }
+ const a = await setzen(WS, spicy.keks, "technik-hat-gehakt",
+ { stufe: "passt_nicht", begruendung: "Zweimal komplett weg, ohne Ansage.", creator_id: idCrea });
+ ok(a.status === 200, `und Spicy Media setzt auch wirklich (${a.status})`);
+ /* Zurück auf offen -- sonst zählt der Hinweis weiter unten drei statt
+ zwei, und die nächste Prüfung meldete einen Fehler, den diese hier
+ verursacht hat. */
+ await setzen(WS, spicy.keks, "technik-hat-gehakt", { stufe: "offen", creator_id: idCrea });
+}
+
+console.log("");
+console.log("=== „Passt nicht“ landet auf der Startseite des Creators ===");
+{
+ /* Der Zähler auf der Kachel zählte fest `stufe = 'verbessern'`. Ohne
+ Nachziehen wäre die stärkste Rückmeldung, die es gibt, als
+ Einzige nicht auf der Startseite erschienen -- der Creator sähe
+ ein ruhiges Kästchen, während sein Scout auf etwas wartet. */
+ const r = (await hole("/workspace/api/hinweise", { host: WS, keks: crea.keks })).daten;
+ const h = (r?.hinweise || []).find((x) => x.art === "markiert_live");
+ ok(!!h, `der Hinweis zur LIVE-Analyse steht da (${h ? h.anzahl : "fehlt"})`);
+ /* Zwei markierte Punkte: „Passt nicht" bei ton-geprueft und
+ „Verbesserung möglich" bei titel-und-thema-stehen. */
+ ok(h?.anzahl === 2, `und zählt beide markierten Stufen (${h?.anzahl} von 2)`);
+}
+
+console.log("");
+console.log(`${geprueft} Pruefungen, ${fehler} Fehlschlaege.`);
+try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+process.exit(fehler ? 1 : 0);
diff --git a/server/pruef-checkliste.mjs b/server/pruef-checkliste.mjs
index 33802ea1..21157909 100644
--- a/server/pruef-checkliste.mjs
+++ b/server/pruef-checkliste.mjs
@@ -334,9 +334,22 @@ const browser = await chromium.launch();
await senden(`/workspace/api/checkliste/live/${punkt}`, kScout, "PUT",
{ stufe: "verbessern", begruendung: "Mikro naeher heranstellen.", creator_id: idTili });
-for (const [rolle, code, name, darf] of [
- ["scout", "CODE-SCOU-0001", "Patrick", true],
- ["creator", "CODE-ANNA-0001", "Tili", false],
+/* WIE VIELE KNOEPFE UND KACHELN ES SEIN MUESSEN, WIRD NICHT MEHR
+ HINGESCHRIEBEN (24.09.2026).
+
+ Hier standen feste Zahlen: drei Bilanzkacheln, zwei Knoepfe je Punkt.
+ Am Tag, an dem das Agenturhaus vier Stufen bekam, war diese Pruefung
+ rot -- nicht weil etwas kaputt war, sondern weil sie eine Zahl von
+ gestern gegen einen Stand von heute hielt. Genau die Sorte
+ Fehlalarm, die Vertrauen in einen ganzen Pruefsatz kostet.
+
+ Jetzt fragt sie fuer JEDE Rolle und JEDEN Bereich die Schnittstelle
+ und rechnet daraus, was auf dem Bildschirm stehen muss. Das ist kein
+ Selbstgespraech: Die Antwort kommt vom Server, gezaehlt wird im
+ Browser -- weichen sie voneinander ab, faellt es auf. */
+for (const [rolle, code, name, darf, keksRolle] of [
+ ["scout", "CODE-SCOU-0001", "Patrick", true, kScout],
+ ["creator", "CODE-ANNA-0001", "Tili", false, kTili],
]) {
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1100 } });
const konsole = [];
@@ -387,15 +400,27 @@ for (const [rolle, code, name, darf] of [
})(),
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
}));
+ /* Dieselbe Auskunft, die auch die Seite bekommen hat. */
+ const api = await (await ruf(`/workspace/api/checkliste/${bereich}`, keksRolle)).json();
+ const inListe = api.gruppen.flatMap((g) => g.punkte);
+ const gesetzt = new Set(inListe.map((p) => api.stand[p.schluessel]?.stufe || "offen"));
+ /* Eine "stille" Stufe bekommt erst dann eine Kachel, wenn sie
+ vorkommt -- "0 trifft nicht zu" ueber jeder Liste waere eine
+ Auskunft, die niemand gesucht hat. Dazu immer "noch offen". */
+ const sollBilanz = api.stufen.filter((x) => !x.still || gesetzt.has(x.schluessel)).length + 1;
+ const sollKnoepfe = api.darf_bewerten ? r.punkte * api.stufen.length : 0;
const gut = r.stufenleiter === 5
- && r.da && r.gruppen === 3 && r.punkte >= 6 && r.bilanz === 3 && r.gestaltet === true
+ && r.da && r.gruppen === 3 && r.punkte >= 6 && r.bilanz === sollBilanz
+ && r.gestaltet === true
&& r.marken === r.punkte && r.ueber <= 0
- && (darf ? r.stufenKnoepfe === r.punkte * 2 : r.stufenKnoepfe === 0);
+ && r.stufenKnoepfe === sollKnoepfe
+ && api.darf_bewerten === darf;
if (!gut) fehler++;
console.log(` ${gut ? "ok " : "FEHL"} ${name} · ${bereich}: `
- + `${r.gruppen} Gruppen, ${r.punkte} Punkte, ${r.bilanz} Bilanzzahlen, `
- + `${r.stufenKnoepfe} Bewertungsknoepfe, ${r.marken} Marken, `
- + `Stufenleiste ${r.stufenleiter}/5, ${r.ueber}px über`
+ + `${r.gruppen} Gruppen, ${r.punkte} Punkte, `
+ + `${r.bilanz}/${sollBilanz} Bilanzzahlen, `
+ + `${r.stufenKnoepfe}/${sollKnoepfe} Bewertungsknoepfe (${api.stufen.length} Stufen), `
+ + `${r.marken} Marken, Stufenleiste ${r.stufenleiter}/5, ${r.ueber}px über`
+ (r.gestaltet ? "" : " STIL FEHLT"));
}
ok(konsole.length === 0,
@@ -414,7 +439,15 @@ for (const [rolle, code, name, darf] of [
} : null;
});
ok(!!s, "der Creator sieht den Punkt als 'zu verbessern' markiert");
- ok(/Verbessern/i.test(s?.marke || ""), ` mit lesbarer Marke ("${s?.marke}")`);
+ /* DAS WORT AUF DER MARKE KOMMT VOM SERVER und steht hier nicht als
+ Muster. Es hiess bis zum 24.09.2026 "Verbessern" und heisst im
+ Agenturhaus jetzt "Verbesserung möglich" -- ein `/Verbessern/i`
+ hier war damit rot, ohne dass etwas kaputt war. Geprueft wird die
+ Sache dahinter: Auf der Marke steht GENAU das Wort, das der
+ Katalog fuer diese Stufe vorsieht. */
+ const wort = (listen.live.stufen || []).find((x) => x.schluessel === "verbessern")?.name;
+ ok(!!wort && (s?.marke || "").trim() === wort,
+ ` mit dem Wort aus dem Katalog ("${s?.marke}" = "${wort}")`);
ok(/Mikro/.test(s?.grund || ""), ` und dem Grund ("${s?.grund}")`);
await seite.screenshot({ path: "pruef-checkliste.png", fullPage: false });
diff --git a/server/workspace-checkliste.js b/server/workspace-checkliste.js
index 5fa8e3f3..4d9899d7 100644
--- a/server/workspace-checkliste.js
+++ b/server/workspace-checkliste.js
@@ -36,8 +36,9 @@ import express from "express";
import {
db, sitzungLesen, istLeitung, istDogFather, siehtAlles, darfCreator, betreuteIds,
protokolliere, echteIp,
- siehtModis, TEAM_DOGI_ROLLEN, istHand,
+ siehtModis, TEAM_DOGI_ROLLEN, istHand, ROLLEN_NAME, checkListeErweitern,
} from "./workspace.js";
+import { hausVonRolle } from "./crew-adresse.js";
import { MODI_PUNKTE, MODI_GRUPPENTEXTE } from "./workspace-modi-punkte.js";
import {
CONTENT_IDEEN, LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE,
@@ -46,8 +47,152 @@ import {
export const checklisteRouter = express.Router();
const jetzt = () => new Date().toISOString();
-const STUFEN = new Set(["offen", "gut", "verbessern"]);
const GRUND_MAX = 2000;
+const GRUND_MIN = 5;
+
+/* =====================================================================
+ ZWEI URTEILSSKALEN -- EINE JE HAUS (24.09.2026)
+
+ Filipe, mit dem Bildschirmfoto der LIVE-Punkte:
+ *"die sollen die manager und scouts bewerten können mit passt passt
+ nicht verbesserung möglich und was weiß ich. und die creator sollen
+ sehen was bei ihnen passt oder nicht mit der notiz vom manager oder
+ scout. ... und wie gesagt von der team dogi seite da ist ein anderes
+ system auf diesen aufgaben."*
+
+ ES SIND ZWEI VERSCHIEDENE DINGE, DIE BIS HEUTE DIESELBEN DREI WORTE
+ BENUTZT HABEN.
+
+ Im AGENTURHAUS urteilt eine Betreuung ueber die Arbeit eines
+ Creators: Manager, Scout, Spicy Media, DogFather druecken, der
+ Creator liest. Dafuer reichen zwei Stufen nicht. "Verbessern" muss
+ heute alles tragen, von "fast perfekt, eine Kleinigkeit" bis "so
+ geht es gar nicht" -- und ein Creator kann an dem Wort nicht
+ ablesen, wie ernst es gemeint ist. Genau danach hat Filipe gefragt.
+
+ Im TEAMHAUS ist es umgekehrt: Der Modi BERICHTET ueber eine Sendung,
+ die er gesehen hat, und DogFather behandelt den Bericht im Eingang
+ ("Dein Team"). Dort ist "Verbessern" kein Urteil, sondern ein
+ Eingang -- aus ihm wird eine Aufgabe oder eine begruendete Absage.
+ Eine dritte Stufe "Passt nicht" haette dort keinen Empfaenger; sie
+ wuerde im Eingang entweder verschwinden oder ihn verdoppeln.
+
+ DESHALB ZWEI KATALOGE UND NICHT EIN GEMEINSAMER MIT AUSNAHMEN. Ein
+ gemeinsamer waere die Stelle, an der beim naechsten Wunsch wieder
+ beides zusammenwaechst.
+
+ WARUM "TRIFFT NICHT ZU" DAZUGEHOERT und nicht Beiwerk ist: Ohne sie
+ bleibt ein Punkt, der bei diesem Creator gar nicht vorkommt (kein
+ Gast, kein Sendeplan, keine Zweitkamera), fuer immer auf "offen"
+ stehen. Die Bilanz oben zaehlt ihn dann als unerledigt mit -- und
+ eine Zahl, die etwas mitzaehlt, was nie erledigt werden kann, wird
+ nach zwei Wochen nicht mehr angesehen.
+
+ `grund: "pflicht"` heisst: ohne Satz geht es nicht. Er steht dort,
+ wo der Creator sonst raten muesste, was gemeint ist -- und nur dort.
+ Ein Pflichtfeld an "Passt" waere eine Huerde vor der haeufigsten und
+ harmlosesten Handlung.
+
+ DIE WORTE STEHEN HIER UND NICHT IM BROWSER. Der Browser baut die
+ Knoepfe aus dieser Liste. Stuenden sie dort noch einmal, haette das
+ Haus zwei Wahrheiten darueber, was es gerade misst -- und die
+ Oberflaeche wuerde einem Modi eines Tages einen Knopf zeigen, den
+ der Server mit 400 abweist. Das ist schlechter als kein Knopf.
+ ===================================================================== */
+/* Die Felder, die jede Stufe traegt -- einmal erklaert, damit die
+ Listen darunter ohne Kommentar auskommen:
+
+ schluessel steht in der Datenbank. Aendert sich nie.
+ name steht auf dem Knopf und auf der Marke am Punkt.
+ bilanz steht in der Zaehlkachel oben, HINTER der Zahl
+ ("8 passt so"). Deshalb klein und ohne Punkt.
+ grund "pflicht" = ohne Satz keine Bewertung.
+ frage die Zeile ueber dem Textfeld im Nachfragefenster --
+ UND zugleich die Entscheidung, ob ueberhaupt gefragt
+ wird. Eine Stufe ohne `frage` wird mit einem Klick
+ gesetzt. Das ist Absicht und keine Sparsamkeit:
+ "Passt" ist der haeufigste Klick einer Betreuung,
+ die vierzig Punkte durchgeht, und er braucht keine
+ Erklaerung. Ein Nachfragefenster davor macht aus
+ einem Durchgang eine Sitzung. Alles, was der andere
+ verstehen oder tun muss, fragt dagegen nach.
+ merk zaehlt in das Merkzeichen (die Raute) und in den
+ Sprung "zeig mir die markierten Punkte".
+ still erscheint in der Bilanz erst, wenn es vorkommt.
+ Eine Kachel "0 trifft nicht zu" ueber jeder Liste
+ waere eine Auskunft, die niemand gesucht hat. */
+export const STUFEN_AGENTUR = [
+ { schluessel: "gut", name: "Passt", bilanz: "passt so", grund: "frei",
+ merk: false, still: false, frage: null, platzhalter: null },
+ { schluessel: "verbessern", name: "Verbesserung möglich", bilanz: "zu verbessern",
+ grund: "pflicht", merk: true, still: false,
+ frage: "Was genau lässt sich verbessern?",
+ platzhalter: "z. B. Die Ankündigung kam erst 10 Minuten vorher." },
+ { schluessel: "passt_nicht", name: "Passt nicht", bilanz: "passt nicht",
+ grund: "pflicht", merk: true, still: false,
+ frage: "Was passt nicht – und was soll stattdessen passieren?",
+ platzhalter: "z. B. Der Titel sagt nichts über das Thema. Bitte vorher ändern." },
+ { schluessel: "entfaellt", name: "Trifft nicht zu", bilanz: "trifft nicht zu",
+ grund: "frei", merk: false, still: true,
+ frage: "Warum trifft das hier nicht zu? (freiwillig)",
+ platzhalter: "z. B. Du sendest ohne Gäste – der Punkt kommt bei dir nicht vor." },
+];
+
+export const STUFEN_CREW = [
+ { schluessel: "gut", name: "Passt so", bilanz: "passt so", grund: "frei",
+ merk: false, still: false, frage: null, platzhalter: null },
+ { schluessel: "verbessern", name: "Verbessern", bilanz: "zu verbessern", grund: "pflicht",
+ merk: true, still: false,
+ frage: "Was genau soll verbessert werden?",
+ platzhalter: "z. B. Fehlt noch das Impressum unten auf der Seite." },
+];
+
+/** Welche Skala gilt fuer die Liste DIESER Person?
+ *
+ * Nach der Rolle dessen, UM DEN es geht -- nicht nach der Rolle
+ * dessen, der zusieht. Genau so entscheidet schon `gruppenFuer`,
+ * welche Punkte in der Liste stehen; stuenden hier zwei verschiedene
+ * Fragen, koennte ein Katalog aus dem einen Haus mit den Stufen des
+ * anderen bewertet werden.
+ */
+export function stufenFuer(zielRolle) {
+ const katalog = TEAM_DOGI_ROLLEN.has(zielRolle) ? STUFEN_CREW : STUFEN_AGENTUR;
+ /* Nur, was die Datenbank auch annimmt -- siehe
+ stufenSchrankeNachziehen(). Im Normalfall ist das der ganze
+ Katalog; solange niemand nachgesehen hat (`null`), ebenfalls. */
+ if (!stufenInDatenbank) return katalog;
+ return katalog.filter((s) => stufenInDatenbank.has(s.schluessel));
+}
+
+/* "offen" steht in keinem der beiden Kataloge und ist trotzdem in
+ beiden gueltig: Es ist keine Stufe, sondern das Zuruecknehmen einer
+ Stufe -- der Datensatz wird dabei geloescht. Waere es ein Eintrag im
+ Katalog, stuende in der Oberflaeche ein Knopf "Offen" neben den
+ anderen, und niemand wuesste, was er aussagt. */
+const ZURUECK = "offen";
+
+/* =====================================================================
+ WAS DIE DATENBANK ZULASSEN MUSS -- ABGELEITET, NIE ABGESCHRIEBEN
+
+ Die Spalte `stufe` traegt beide Haeuser; die Schranke in der
+ Datenbank muss deshalb die Vereinigung beider Kataloge kennen. Wer
+ sie als feste Liste hinschreibt, schreibt eine Liste, die beim
+ naechsten Wunsch veraltet -- und zwar still: Die Oberflaeche zeigt
+ den neuen Knopf, der Server nimmt ihn an, und SQLite lehnt ab. Der
+ Fehler landet dann in einem 503, der nach Ausfall aussieht.
+
+ Am 11.09.2026 hat genau diese Bauart -- eine von Hand gepflegte
+ Spaltenliste -- drei Spalten samt Inhalt gekostet. Die Lehre stand
+ danach als Kommentar daneben und hat den naechsten Verlust nicht
+ verhindert. Hier ist sie deshalb kein Kommentar, sondern eine
+ Ableitung: Die Liste KANN nicht veralten, weil niemand sie pflegt.
+ ===================================================================== */
+const ALLE_STUFEN = [...new Set([
+ ZURUECK,
+ ...STUFEN_AGENTUR.map((s) => s.schluessel),
+ ...STUFEN_CREW.map((s) => s.schluessel),
+])];
+const STUFEN_CHECK = ALLE_STUFEN.map((s) => `'${s}'`).join(",");
/* ---------------------------------------------------------------------
Die Listen, gruppiert und beschriftet.
@@ -161,18 +306,82 @@ function gruppenFuer(bereich, fuerModi) {
.filter((g) => g.punkte.length);
}
-/* BEIDE SAETZE sind gueltige Schluessel. Stuenden hier nur die
- Creator-Punkte, liesse sich der Stand eines Modi-Punktes nicht
- setzen -- die Liste kaeme an, und jeder Klick darauf brachte einen
- 404. Genau die Sorte Fehler, die man erst beim Benutzen merkt. */
-const GUELTIG = {};
-for (const [bereich, liste] of Object.entries(LISTEN)) {
- GUELTIG[bereich] = new Set(liste.gruppen.flatMap((g) => g.punkte.map((p) => p.schluessel)));
- for (const punkte of Object.values(MODI_PUNKTE[bereich] || {})) {
- for (const p of punkte) GUELTIG[bereich].add(p.schluessel);
+/* =====================================================================
+ GUELTIGE SCHLUESSEL -- JE SATZ GETRENNT (getrennt am 24.09.2026)
+
+ Hier stand EINE Menge je Bereich, in der beide Saetze zusammenlagen:
+ die Creator-Punkte und die des Teams. Das war gut gemeint (sonst
+ haette ein Modi auf jeden Klick eine 404 bekommen) und es war die
+ Stelle, an der die beiden Haeuser sich beruehrten.
+
+ GEMESSEN, WAS DAS BEDEUTET: Ein Scout konnte bei einem Creator den
+ Stand von "m-ton-von-anfang-an" setzen -- einem Punkt aus dem
+ Teamkatalog. Der Server nahm ihn an, die Zeile lag in der Datenbank,
+ und zu sehen war sie nie: Die Liste des Creators kennt diesen
+ Schluessel nicht. Kein Fehler, keine Meldung, nur eine Zeile, die es
+ nicht geben duerfte -- und beim naechsten Zaehlen taucht sie in
+ einer Summe auf, die niemand erklaeren kann.
+
+ JETZT ZWEI MENGEN, und gefragt wird die des ZIELS: Wessen Liste ist
+ es? Dessen Katalog gilt. Genau dieselbe Frage entscheidet schon,
+ welche Punkte ausgeliefert werden (gruppenFuer) und welche Stufen
+ erlaubt sind (stufenFuer) -- drei Stellen, eine Frage.
+ ===================================================================== */
+const GUELTIG = { creator: {}, team: {} };
+for (const bereich of Object.keys(LISTEN)) {
+ for (const [satz, fuerModi] of [["creator", false], ["team", true]]) {
+ GUELTIG[satz][bereich] = new Set(
+ (gruppenFuer(bereich, fuerModi) || []).flatMap((g) => g.punkte.map((p) => p.schluessel)));
}
}
+/** Gibt es diesen Punkt im Katalog DER PERSON, um die es geht? */
+function gueltigerPunkt(bereich, schluessel, zielRolle) {
+ const satz = TEAM_DOGI_ROLLEN.has(zielRolle) ? "team" : "creator";
+ return !!GUELTIG[satz][bereich]?.has(schluessel);
+}
+
+/* =====================================================================
+ WER UEBERHAUPT EINE LISTE HAT -- UND IN WELCHEM HAUS SIE STEHT
+ (24.09.2026)
+
+ Filipe: *"alle auf dieser app getrennt von denen auf der team dogi
+ website bitte, sehr wichtig."*
+
+ ER HAT ES AUF SEINEM BILDSCHIRM GESEHEN, nicht vermutet: Auf
+ workspace.dogfather-universe.com stand unter "Vor der Sendung" die
+ Liste eines MODIS ("Die Ankündigung kam rechtzeitig", darueber der
+ Satz "Was du vor und beim Start gesehen hast"). Nachgemessen am
+ echten Bestand: Die Auswahl "Person" fuellte sich aus allen Creatorn
+ PLUS allen Modis, sortiert nach Namen -- und der erste Name im Haus
+ ist "Diene", eine Modi. Ohne ausdrueckliche Wahl nimmt die Seite den
+ ersten. DogFather bekam also auf der Agenturadresse zuverlaessig das
+ Teamhaus zu sehen, und bewerten konnte er dort nichts, weil ein
+ Modi-Bericht nur dem Modi selbst gehoert.
+
+ DREI LAGEN UND NICHT ZWEI -- dieselbe Einteilung wie bei den
+ Aufgaben (workspace-aufgaben.js):
+ crew -> nur Rollen des Teamhauses
+ agentur -> nur Rollen des Agenturhauses
+ keine Adresse (localhost, Pruefungen) -> wie bisher, ungefiltert.
+ Der dritte Ausgang ist kein Schlupfloch, sondern die Bedingung
+ dafuer, dass die Pruefungen ueberhaupt noch etwas messen koennen --
+ crew-adresse.js warnt genau davor (Falle 2).
+
+ HAT_LISTE IST DIE ZWEITE HAELFTE und sie fehlte bisher ganz:
+ `darfCreator` sagt fuer DogFather bei JEDER Personennummer ja. Er
+ konnte damit einen Stand an einer Managerin, einem Scout oder an
+ sich selbst setzen -- fuer Punkte, die dort nie erscheinen. Eine
+ Liste haben genau die, fuer die ein Katalog existiert.
+ ===================================================================== */
+const HAT_LISTE = new Set(["creator", ...TEAM_DOGI_ROLLEN]);
+
+function imSelbenHaus(person, zielRolle) {
+ const haus = person?.haus;
+ if (haus !== "crew" && haus !== "agentur") return true;
+ return hausVonRolle(zielRolle) === haus;
+}
+
let bereit = false;
/* AUSGEGEBEN, seit die Eingang-Seite dieselben Tabellen liest
(10.09.2026).
@@ -201,7 +410,7 @@ function tabellen() {
bereich TEXT NOT NULL,
schluessel TEXT NOT NULL,
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
- stufe TEXT NOT NULL CHECK (stufe IN ('offen','gut','verbessern')),
+ stufe TEXT NOT NULL CHECK (stufe IN (${STUFEN_CHECK})),
begruendung TEXT,
von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
am TEXT NOT NULL,
@@ -254,9 +463,93 @@ function tabellen() {
console.error(`[workspace] Spalte '${spalte}':`, fehler?.message);
}
}
+
+ stufenSchrankeNachziehen();
bereit = true;
}
+/* =====================================================================
+ DIE SCHRANKE IN DER DATENBANK AN DIE KATALOGE ANGLEICHEN (24.09.2026)
+
+ Eine CHECK-Regel laesst sich in SQLite nicht aendern. Wer eine Stufe
+ dazunimmt, muss die Tabelle neu bauen -- und genau dabei hat dieses
+ Haus am 11.09.2026 drei Spalten samt Inhalt verloren, ohne
+ Fehlermeldung und bei unveraenderter Zeilenzahl.
+
+ DIESEN GRIFF GIBT ES SCHON, UND ZWAR GENAU EINMAL:
+ `checkListeErweitern` in workspace.js hat ihn seit dem 09.09.2026,
+ weil er damals zum dritten Mal gebraucht wurde. Er bringt fuenf
+ Absicherungen mit -- eine Sicherung der ganzen Datenbank davor
+ (VACUUM INTO), die Zeilenzaehlung INNERHALB der Transaktion, die
+ Spaltenliste aus PRAGMA statt aus dem Gedaechtnis, die Indizes, und
+ `PRAGMA foreign_key_check` danach.
+
+ ICH HATTE IHN HIER ZUERST EIN ZWEITES MAL GESCHRIEBEN. Meine Fassung
+ hatte die Zeilenzaehlung und die Spaltenliste, aber weder die
+ Sicherung davor noch die Verweispruefung danach. Sie haette
+ funktioniert -- und waere die naechste Stelle gewesen, an der eine
+ Absicherung fehlt, die anderswo laengst steht. Das ist genau die
+ Bauart, vor der der Kommentar an jener Funktion warnt.
+
+ WAS DIE STELLE HIER NOCH SELBST TUT: nachsehen, was danach wirklich
+ erlaubt ist. `checkListeErweitern` bricht bei jedem Zweifel ab und
+ sagt es laut -- die Tabelle bleibt dann unveraendert und voellig
+ brauchbar, nur mit den alten Stufen. Die Oberflaeche soll in dem Fall
+ ZWEI Knoepfe zeigen statt vier und keinen, der beim Druecken
+ scheitert. Deshalb wird die Schranke danach gelesen, nicht
+ vorausgesetzt. */
+let stufenInDatenbank = null; /* null = noch nicht nachgesehen */
+
+/** Welche Werte laesst `punkt_stand.stufe` gerade zu? Aus dem Bauplan
+ * der Tabelle gelesen -- nicht aus einer Fassungsnummer. Eine
+ * Fassungsnummer sagt, was gemeint war; der Bauplan sagt, was gilt. */
+function erlaubteWerteLesen() {
+ const bauplan = db().prepare(
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'punkt_stand'").get()?.sql;
+ if (!bauplan) return null;
+ const stelle = /CHECK\s*\(\s*stufe\s+IN\s*\(([^)]*)\)/i.exec(bauplan);
+ if (!stelle) {
+ /* Keine CHECK-Regel heisst: Die Spalte ist offen, es gibt nichts
+ einzuschraenken. Steht "CHECK" aber darin und wir finden die
+ Stelle trotzdem nicht, stimmt unsere Annahme ueber den Bauplan
+ nicht -- dann waere Schweigen das Falsche. */
+ if (/CHECK/i.test(bauplan)) {
+ console.error("[workspace] punkt_stand: CHECK gefunden, aber die Stufenliste nicht. "
+ + "Bauplan:", bauplan.replace(/\s+/g, " ").slice(0, 300));
+ }
+ return null;
+ }
+ return new Set(stelle[1].split(",").map((w) => w.trim().replace(/^'|'$/g, "")));
+}
+
+function stufenSchrankeNachziehen() {
+ stufenInDatenbank = erlaubteWerteLesen();
+ if (!stufenInDatenbank) return;
+ const fehlt = ALLE_STUFEN.filter((s) => !stufenInDatenbank.has(s));
+ if (!fehlt.length) return;
+
+ /* DER MARKER IST DIE ERSTE FEHLENDE STUFE und keine hingeschriebene.
+ `checkListeErweitern` erkennt daran, ob schon umgestellt wurde --
+ eine feste Angabe waere an dem Tag falsch, an dem eine weitere
+ Stufe dazukommt und die alte schon dasteht. Uebergeben wird in
+ jedem Fall die VOLLSTAENDIGE Liste; ein Durchgang holt alles nach. */
+ const stempel = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
+ checkListeErweitern(db(), "punkt_stand", "stufe", fehlt[0], ALLE_STUFEN, stempel);
+
+ /* NACHSEHEN STATT ANNEHMEN. Die Umstellung bricht bei jedem Zweifel
+ ab (fehlgeschlagene Sicherung, unerwarteter Bauplan, abweichende
+ Zeilenzahl) und sagt es auf der Protokollzeile. Wer ihr Gelingen
+ einfach voraussetzt, liefert danach Knoepfe aus, die der Server
+ annimmt und SQLite ablehnt. */
+ stufenInDatenbank = erlaubteWerteLesen();
+ const jetztFehlt = ALLE_STUFEN.filter((s) => !stufenInDatenbank?.has(s));
+ if (jetztFehlt.length) {
+ console.error("[workspace] punkt_stand: Die Stufen "
+ + `${jetztFehlt.join(", ")} sind weiterhin nicht erlaubt. `
+ + "Bis das behoben ist, werden sie auch nicht angeboten.");
+ }
+}
+
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
@@ -305,7 +598,14 @@ function creatorAus(req) {
Aus der MENGE gelesen und nicht als zwei Vergleiche hingeschrieben
-- sonst steht hier beim naechsten Mal einer zu wenig. */
if (req.person.rolle === "creator" || TEAM_DOGI_ROLLEN.has(req.person.rolle)) {
- return req.person.id;
+ /* AUCH DIE EIGENE LISTE GEHOERT IN IHR HAUS (24.09.2026). Die
+ Zugangswand laesst einen Modi ohnehin nicht auf die
+ Agenturadresse und umgekehrt -- diese Zeile ist deshalb nicht
+ die erste Schranke, sondern die letzte. Zwei Sicherungen fuer
+ dieselbe Sache sind hier richtig: Faellt die aeussere einmal aus
+ (eine neue Adresse, eine vergessene Liste), stuende sonst ein
+ ganzes Haus offen, ohne dass irgendwo etwas rot wird. */
+ return imSelbenHaus(req.person, req.person.rolle) ? req.person.id : -1;
}
const id = Number(req.body?.creator_id || req.query?.creator_id) || 0;
if (!id) return 0;
@@ -320,9 +620,27 @@ function creatorAus(req) {
* Spicy Media aber nichts an; das ist die Regel des ganzen Hauses,
* und sie darf nicht ausgerechnet hier eine Ausnahme haben.
*/
+/** Die Rolle einer Person -- oder `null`, wenn es sie nicht gibt.
+ *
+ * Vier Stellen brauchten dieselbe Zeile; als vierte Abschrift waere
+ * sie die gewesen, die beim naechsten Umbau nicht mitgeht.
+ */
+function rolleVon(id) {
+ return db().prepare("SELECT rolle FROM personen WHERE id = ?").get(Number(id))?.rolle ?? null;
+}
+
function darfPerson(person, zielId) {
const ziel = db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(Number(zielId));
if (!ziel) return false;
+ /* NUR WER EINE LISTE HAT (24.09.2026). Ohne diese Zeile sagte
+ `darfCreator` fuer DogFather bei JEDER Nummer ja -- er konnte
+ einen Stand an einer Managerin, einem Scout oder an sich selbst
+ setzen. Die Zeile lag danach in der Datenbank und war nirgends zu
+ sehen, weil keine dieser Rollen eine Liste oeffnet. */
+ if (!HAT_LISTE.has(ziel.rolle)) return false;
+ /* UND NUR IM EIGENEN HAUS. Die Begruendung steht oben bei
+ imSelbenHaus(); hier ist die Stelle, an der sie greift. */
+ if (!imSelbenHaus(person, ziel.rolle)) return false;
/* Fuer die rechte Hand gilt dasselbe wie fuer einen Modi: Ihre Liste
geht Spicy Media und die Manager nichts an. Aus derselben Menge --
zwei Vergleiche waeren zwei Gelegenheiten, einen zu vergessen. */
@@ -371,10 +689,28 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
for (const z of db().prepare(
"SELECT id FROM personen WHERE rolle = 'modi' AND aktiv = 1").all()) ids.push(z.id);
}
- creatoren = ids.length
- ? db().prepare(`SELECT id, name FROM personen
+ /* ==== EIN SIEB, NICHT DREI BEDINGUNGEN (24.09.2026) ===========
+
+ Oben werden Nummern aus drei Quellen zusammengetragen: alle
+ Creator, die betreuten, alle Modis. Die Haustrennung an jede
+ dieser Stellen einzeln zu schreiben waere die Bauart, die in
+ diesem Haus schon viermal Daten gekostet hat -- beim naechsten
+ Zweig fehlt sie dann an einer Stelle, und genau dort faellt es
+ niemandem auf.
+
+ Deshalb EIN Sieb dahinter, durch das jede Nummer muss. Es
+ fragt dieselbe Frage wie darfPerson(): Hat diese Person
+ ueberhaupt eine Liste, und steht sie in diesem Haus?
+
+ DIE ROLLE KOMMT NUR ZUM SIEBEN MIT und wird nicht
+ ausgeliefert -- die Auswahl selbst sieht danach aus wie
+ vorher, nur mit den richtigen Namen darin. */
+ creatoren = (ids.length
+ ? db().prepare(`SELECT id, name, rolle FROM personen
WHERE id IN (${ids.map(() => "?").join(",")}) ORDER BY name`).all(...ids)
- : [];
+ : [])
+ .filter((z) => HAT_LISTE.has(z.rolle) && imSelbenHaus(req.person, z.rolle))
+ .map((z) => ({ id: z.id, name: z.name }));
/* Ohne ausdrueckliche Wahl den ERSTEN nehmen. Sonst sieht ein
Betreuer beim Oeffnen eine Liste ohne Bewertungsknoepfe und
@@ -387,11 +723,25 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
const stand = {};
const nachrichten = {};
if (creatorId > 0) {
+ /* WER ES GESETZT HAT -- MIT ROLLE (24.09.2026).
+ Filipe: "die creator sollen sehen was bei ihnen passt oder
+ nicht mit der notiz vom manager oder scout."
+ Bis hierher stand am Satz nur der Name. Ob "Patrick" der
+ eigene Scout, die Managerin oder DogFather ist, konnte ein
+ Creator nur wissen, wenn er die Namen ohnehin alle kennt --
+ und beim ersten Monat kennt er sie nicht. Der Rollenname kommt
+ aus ROLLEN_NAME und nicht aus dem Browser: Eine Tabelle im
+ ausgelieferten Skript waere die zweite Wahrheit, die beim
+ naechsten Rollenwechsel auseinanderlaeuft. */
for (const z of db().prepare(`
- SELECT s.*, p.name AS von_name FROM punkt_stand s
+ SELECT s.*, p.name AS von_name, p.rolle AS von_rolle FROM punkt_stand s
LEFT JOIN personen p ON p.id = s.von
WHERE s.bereich = ? AND s.creator_id = ?`).all(bereich, creatorId)) {
- stand[z.schluessel] = z;
+ stand[z.schluessel] = {
+ ...z,
+ von_rolle_name: z.von_rolle ? (ROLLEN_NAME[z.von_rolle] ?? null) : null,
+ };
+ delete stand[z.schluessel].von_rolle;
}
for (const z of db().prepare(`
SELECT schluessel, COUNT(*) AS n FROM punkt_nachricht
@@ -403,9 +753,7 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
/* WELCHER PUNKTESATZ? Der der Person, um die es geht -- nicht der
der Person, die zusieht. DogFather waehlt einen Modi aus und
bekommt dessen Liste, nicht die eines Creators. */
- const zielRolle = creatorId > 0
- ? db().prepare("SELECT rolle FROM personen WHERE id = ?").get(creatorId)?.rolle
- : req.person.rolle;
+ const zielRolle = creatorId > 0 ? rolleVon(creatorId) : req.person.rolle;
/* WELCHER KATALOG? (11.09.2026 erweitert)
Es gibt zwei Punktsaetze: den fuer Creator ("Ton geprueft") und
@@ -421,11 +769,55 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
Eingang". Beides stimmte. */
const gruppen = gruppenFuer(bereich, TEAM_DOGI_ROLLEN.has(zielRolle));
+ /* EINMAL AUSGERECHNET, ZWEIMAL GEBRAUCHT: fuer den Knopf und fuer
+ den Satz darueber. Als zweiter Ausdruck weiter unten waere es
+ die Sorte Abschrift, die beim naechsten Rollenzuschnitt
+ auseinanderlaeuft -- dann stuende in der Einleitung "markiere",
+ und es gaebe nichts zu druecken.
+ Die ausfuehrliche Begruendung steht unten bei `darf_bewerten`. */
+ const darfBewerten = TEAM_DOGI_ROLLEN.has(zielRolle)
+ ? req.person.id === creatorId
+ : (req.person.rolle !== "creator" && !TEAM_DOGI_ROLLEN.has(req.person.rolle));
+
res.json({
bereich, name: liste.name,
gruppen,
stand, nachrichten,
creatoren, creator_id: creatorId > 0 ? creatorId : null,
+ /* =============================================================
+ DIE SKALA KOMMT MIT (24.09.2026)
+
+ Vier Stufen im Agenturhaus, zwei im Teamhaus -- und der
+ Browser baut die Knoepfe daraus, statt sie zu kennen. Stuenden
+ sie dort noch einmal, muesste er ausserdem wissen, WANN
+ welche gilt; das waere ein Rollenvergleich im ausgelieferten
+ Skript und damit die zweite Wahrheit ueber die Haustrennung.
+
+ Auch ein Creator bekommt sie, obwohl er nichts druecken darf:
+ Er braucht die Worte und die Toene, um die Marke an seinem
+ Punkt und die Zaehlkacheln oben zu beschriften. */
+ stufen: stufenFuer(zielRolle),
+ /* =============================================================
+ DER SATZ UNTER DER ÜBERSCHRIFT -- JE HAUS EIN EIGENER
+ (24.09.2026)
+
+ Er stand im Browser und sprach in beiden Häusern vom
+ "Creator": Ein Modi, der seinen eigenen Bericht schreibt, las
+ dort "markiere, was der Creator verbessern sollte". Auf der
+ Teamadresse gibt es keine Creator und wird es nie welche
+ geben.
+
+ DIE TEAMFASSUNG BLEIBT WORTGLEICH WIE BISHER. Das ist
+ Absicht: Ab dem 24.09.2026 wird jeder Umbau je Haus getrennt
+ geführt, und dieser hier ist der Umbau des Agenturhauses. Den
+ Satz des anderen Hauses nebenbei mitzuändern -- und sei es
+ zum Besseren -- wäre genau die Kopplung, die weg soll. Er
+ gehört in einen eigenen Durchgang.
+
+ Die Agenturfassung nennt dagegen die vier Stufen beim Namen,
+ weil ein Knopf "Trifft nicht zu" ohne ein Wort dazu wie ein
+ Ausweichen aussieht und nicht wie eine Antwort. */
+ einleitung: einleitungFuer(zielRolle, darfBewerten),
/* WIE DIE AUSWAHL HEISST. Fuer einen Manager oder Scout stehen
dort nur Creator -- "Creator" ist dann richtig. Bei der
DogFather-Rolle stehen seit dem 10.09.2026 auch Modis darin,
@@ -458,9 +850,7 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
DogFather und VanVan LESEN ihn und handeln im Eingang: Aus
"Verbessern" wird dort mit einem Klick eine Aufgabe -- oder eine
begruendete Absage, die beim Verfasser ankommt. */
- darf_bewerten: TEAM_DOGI_ROLLEN.has(zielRolle)
- ? req.person.id === creatorId
- : (req.person.rolle !== "creator" && !TEAM_DOGI_ROLLEN.has(req.person.rolle)),
+ darf_bewerten: darfBewerten,
ich: req.person.id,
});
} catch (fehler) {
@@ -469,6 +859,28 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
}
});
+/** Der Satz unter "Feste Punkte" -- siehe die Begruendung an der
+ * Aufrufstelle. Vier Faelle: zwei Haeuser mal "darf druecken".
+ */
+function einleitungFuer(zielRolle, darf) {
+ if (TEAM_DOGI_ROLLEN.has(zielRolle)) {
+ return darf
+ ? "Diese Punkte gelten für alle. Markiere, was gut läuft und was der "
+ + "Creator noch verbessern sollte – er sieht es sofort."
+ : "Diese Punkte gelten für alle. Deine Betreuung markiert hier, was gut "
+ + "läuft und was du noch verbessern solltest. Antworten kannst du an "
+ + "jedem Punkt.";
+ }
+ return darf
+ ? "Diese Punkte gelten für alle. Setze je Punkt, ob er passt, ob sich "
+ + "etwas verbessern lässt, ob er so nicht passt – oder ob er bei "
+ + "diesem Creator gar nicht vorkommt. Er sieht es sofort, samt "
+ + "deiner Notiz."
+ : "Diese Punkte gelten für alle. Deine Betreuung setzt hier, was passt, "
+ + "was sich verbessern lässt und was so nicht passt – mit einer Notiz "
+ + "dazu. Antworten kannst du an jedem Punkt.";
+}
+
/* ---------- Bewerten (NUR die Betreuung) ----------------------------------- */
checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHerkunft,
@@ -479,9 +891,7 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe
/* Wessen Liste ist es? Danach entscheidet sich, wer druecken
darf -- siehe die Begruendung beim Lesen weiter oben. */
const zielId = creatorAus(req);
- const zielRolle = zielId > 0
- ? db().prepare("SELECT rolle FROM personen WHERE id = ?").get(zielId)?.rolle
- : null;
+ const zielRolle = zielId > 0 ? rolleVon(zielId) : null;
/* "Der eigene Bericht" heisst jetzt: eine Liste aus dem Team,
und ich bin die Person, um die es geht. Das trifft auf einen
Modi zu und seit dem 11.09.2026 ebenso auf die rechte Hand. */
@@ -513,28 +923,47 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe
}
const bereich = String(req.params.bereich);
const schluessel = String(req.params.schluessel);
- if (!GUELTIG[bereich]?.has(schluessel)) {
+ /* DER KATALOG DES ZIELS, nicht die Vereinigung beider Haeuser
+ (24.09.2026) -- Begruendung oben bei GUELTIG. Ein Scout kann
+ damit bei einem Creator keinen Punkt mehr setzen, den nur die
+ Modis kennen. */
+ if (!gueltigerPunkt(bereich, schluessel, zielRolle)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const creatorId = creatorAus(req);
if (creatorId <= 0) return res.status(404).json({ fehler: "nicht_gefunden" });
+ /* WELCHE STUFEN HIER GELTEN, entscheidet dieselbe Frage wie beim
+ Lesen: Wessen Liste ist es? Im Agenturhaus vier, im Teamhaus
+ zwei. "Passt nicht" bei einem Modi abzulehnen ist kein
+ Formfehler -- dort gaebe es fuer diese Stufe keinen Empfaenger
+ (siehe die Begruendung bei STUFEN_CREW). */
const stufe = String(req.body.stufe || "");
- if (!STUFEN.has(stufe)) return res.status(400).json({ fehler: "Unbekannte Stufe." });
+ const erlaubteStufen = stufenFuer(zielRolle);
+ const gewaehlt = erlaubteStufen.find((s) => s.schluessel === stufe);
+ if (stufe !== ZURUECK && !gewaehlt) {
+ return res.status(400).json({
+ fehler: `Hier gibt es diese Stufe nicht. Möglich sind: `
+ + `${erlaubteStufen.map((s) => s.name).join(", ")}.`,
+ });
+ }
const grund = String(req.body.begruendung ?? "").trim().slice(0, GRUND_MAX);
- /* "verbessern" ohne Grund ist fuer den Creator nicht umsetzbar --
- nur entmutigend. */
- if (stufe === "verbessern" && grund.length < 5) {
+ /* Ein Urteil ohne Satz ist fuer den, der es liest, nicht
+ umsetzbar -- nur entmutigend. WELCHE Stufen einen Satz
+ brauchen, steht im Katalog und nicht hier: Stuende es hier,
+ waere die naechste Stufe die, bei der es jemand vergisst. */
+ if (gewaehlt?.grund === "pflicht" && grund.length < GRUND_MIN) {
return res.status(400).json({
- fehler: "Bitte kurz schreiben, was verbessert werden soll – sonst kann er nichts damit anfangen.",
+ fehler: `${gewaehlt.frage} Ohne diesen Satz kann der andere `
+ + "nichts damit anfangen.",
});
}
/* "offen" heisst: zurueck auf Anfang. Dann wird der Stand
geloescht statt auf "offen" gesetzt -- ein Datensatz, der nichts
aussagt, ist Ballast. */
- if (stufe === "offen") {
+ if (stufe === ZURUECK) {
/* Auch der Vermerk faellt mit -- eine neu gesetzte Stufe ist
eine neue Meldung, keine schon behandelte. */
db().prepare("DELETE FROM punkt_stand WHERE bereich = ? AND schluessel = ? AND creator_id = ?")
@@ -576,9 +1005,14 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich/:schluessel/nachrichten
try {
const bereich = String(req.params.bereich);
const schluessel = String(req.params.schluessel);
- if (!GUELTIG[bereich]?.has(schluessel)) return res.status(404).json({ fehler: "nicht_gefunden" });
+ /* ERST WESSEN LISTE, DANN OB ES DEN PUNKT DARIN GIBT. Die
+ Reihenfolge ist keine Kosmetik: Der gueltige Schluessel haengt
+ am Katalog der Person, um die es geht. */
const creatorId = creatorAus(req);
if (creatorId <= 0) return res.status(404).json({ fehler: "nicht_gefunden" });
+ if (!gueltigerPunkt(bereich, schluessel, rolleVon(creatorId))) {
+ return res.status(404).json({ fehler: "nicht_gefunden" });
+ }
const nachrichten = db().prepare(`
SELECT n.*, p.name AS von_name, p.rolle AS von_rolle
@@ -597,9 +1031,11 @@ checklisteRouter.post("/workspace/api/checkliste/:bereich/:schluessel/nachrichte
try {
const bereich = String(req.params.bereich);
const schluessel = String(req.params.schluessel);
- if (!GUELTIG[bereich]?.has(schluessel)) return res.status(404).json({ fehler: "nicht_gefunden" });
const creatorId = creatorAus(req);
if (creatorId <= 0) return res.status(404).json({ fehler: "nicht_gefunden" });
+ if (!gueltigerPunkt(bereich, schluessel, rolleVon(creatorId))) {
+ return res.status(404).json({ fehler: "nicht_gefunden" });
+ }
const text = String(req.body.text ?? "").trim();
if (text.length < 2) return res.status(400).json({ fehler: "Die Nachricht ist leer." });
@@ -630,11 +1066,18 @@ checklisteRouter.delete("/workspace/api/checkliste/nachricht/:id(\\d+)", gleiche
const n = db().prepare("SELECT * FROM punkt_nachricht WHERE id = ?").get(Number(req.params.id));
if (!n) return res.status(404).json({ fehler: "nicht_gefunden" });
/* Erst pruefen, ob die Person den Creator ueberhaupt sehen darf --
- sonst verraet die Antwort, dass es die Nachricht gibt. */
- const erlaubt = req.person.rolle === "creator"
- ? n.creator_id === req.person.id
- : darfCreator(req.person, n.creator_id);
- if (!erlaubt) return res.status(404).json({ fehler: "nicht_gefunden" });
+ sonst verraet die Antwort, dass es die Nachricht gibt.
+
+ SEIT 24.09.2026 UEBER darfPerson() und nicht mehr ueber
+ darfCreator(). Der Unterschied ist die Haustrennung: Hier
+ stand ein Sonderfall fuer die Creator-Rolle und darunter die
+ allgemeine Regel, die fuer DogFather bei JEDER Nummer ja sagt
+ -- auf der Agenturadresse also auch fuer eine Nachricht aus
+ dem Teamhaus. darfPerson beantwortet beides an einer Stelle,
+ den Creator-Fall eingeschlossen. */
+ if (!darfPerson(req.person, n.creator_id)) {
+ return res.status(404).json({ fehler: "nicht_gefunden" });
+ }
if (n.von !== req.person.id) {
return res.status(403).json({
fehler: "Fremde Nachrichten bleiben stehen. Schreib eine neue, wenn etwas richtigzustellen ist.",
diff --git a/server/workspace-hinweise.js b/server/workspace-hinweise.js
index 64df8558..0949f580 100644
--- a/server/workspace-hinweise.js
+++ b/server/workspace-hinweise.js
@@ -32,6 +32,7 @@ import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js";
import { sichtbar as sichtbarTermine } from "./workspace-kalender.js";
import { sichtbar as sichtbarDateien } from "./workspace-dateien.js";
import { sichtbar as sichtbarEintraege } from "./workspace-bereiche.js";
+import { stufenFuer } from "./workspace-checkliste.js";
export const hinweisRouter = express.Router();
@@ -248,6 +249,9 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => {
gemeint sind. Ein Sammelhinweis "6 markierte Punkte" liesse offen,
wo man nachsehen soll. */
if (person.rolle === "creator") {
+ /* Aus dem Katalog des Agenturhauses abgeleitet -- ein Creator
+ wohnt dort und nirgends sonst. */
+ const MARKIERT = stufenFuer("creator").filter((s) => s.merk).map((s) => s.schluessel);
const BEREICHE = [
["live", "LIVE-Analyse", "bereich.html?b=live"],
["content", "Content-Ideen", "content.html"],
@@ -260,9 +264,21 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => {
/* zaehle() faengt Fehler ab -- gibt es die Tabelle noch nicht
(niemand hat je eine Checkliste geoeffnet), steht hier 0
statt eines Ausfalls der ganzen Uebersicht. */
+ /* WELCHE STUFEN HIER ZAEHLEN, sagt der Katalog und nicht
+ diese Zeile (24.09.2026). Hier stand `stufe = 'verbessern'`
+ -- eine abgeschriebene Liste mit genau einem Eintrag.
+ Am Tag, an dem "Passt nicht" dazukam, waere die staerkste
+ Rueckmeldung, die es gibt, als Einzige NICHT auf der
+ Startseite erschienen: Der Creator saehe ein ruhiges
+ Kaestchen, waehrend sein Scout auf etwas wartet. Genau
+ die Sorte Luecke, die nicht falsch aussieht, sondern
+ gar nicht aussieht.
+
+ `merk` traegt dieselbe Bedeutung wie am Punkt selbst und
+ am Gruppenkopf: "hier wartet etwas auf dich". */
zaehle(`SELECT COUNT(*) n FROM punkt_stand
- WHERE bereich = ? AND creator_id = ? AND stufe = 'verbessern'`,
- [schluessel, person.id]));
+ WHERE bereich = ? AND creator_id = ? AND stufe IN (${MARKIERT.map(() => "?").join(",")})`,
+ [schluessel, person.id, ...MARKIERT]));
}
}
diff --git a/server/workspace.js b/server/workspace.js
index 13d72c3d..4ce52c98 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -2850,7 +2850,15 @@ let _dbFehler = null;
@param marker Die Rolle, an der erkannt wird, ob schon umgestellt ist.
@param rollen Die vollstaendige neue Liste erlaubter Rollen.
===================================================================== */
-function checkListeErweitern(d, tabelle, spalte, marker, werte, jetztStempel) {
+/* AUSGEGEBEN SEIT DEM 24.09.2026. Die Checkliste brauchte denselben
+ Griff fuer `punkt_stand.stufe`, und ich hatte ihn dort schon ein
+ zweites Mal hingeschrieben -- mit der Zeilenzaehlung und der
+ Spaltenliste aus PRAGMA, aber OHNE die Sicherung davor, ohne die
+ Indizes und ohne die Pruefung auf verwaiste Verweise danach. Drei
+ von fuenf Absicherungen fehlten, und keine davon haette gefehlt,
+ wenn ich die vorhandene Funktion benutzt haette.
+ Genau davor warnt der Absatz "WARUM ALS FUNKTION" oben. */
+export function checkListeErweitern(d, tabelle, spalte, marker, werte, jetztStempel) {
const rollenPlan = d.prepare(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(tabelle)?.sql || "";
/* DOPPELTE BACKSLASHES, und das ist kein Schoenheitsfehler:
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 91b81838..c6773a55 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+