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`. --> - - + + - +