From e1ee778c04a7390f4f7fc22f30292249b0598f6f Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 24 Sep 2026 23:49:01 +0200 Subject: [PATCH] Vier Stufen im Agenturhaus -- und die Modi-Liste verschwindet von dort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filipe, mit dem 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. spicy und dogfather sollen auch bewerten können wie vorher. ... und wie gesagt von der team dogi seite da ist ein anderes system auf diesen aufgaben." WAS AUF DEM BILDSCHIRMFOTO STAND, WAR NICHT SEINE SEITE Unter "Vor der Sendung" stand die Liste eines MODIS -- erkennbar am Satz darueber ("Was du vor und beim Start gesehen hast") und an den Punkten ("Die Ankuendigung kam rechtzeitig"). Am echten Bestand nachgemessen: Die Auswahl "Person" fuellte sich aus allen Creatorn PLUS allen Modis, sortiert nach Namen. Der erste Name im Haus ist "Diene", eine Modi -- und ohne ausdrueckliche Wahl nimmt die Seite den ersten. DogFather bekam auf der Agenturadresse also zuverlaessig das Teamhaus zu sehen, und druecken konnte er dort nichts, weil ein Modi-Bericht nur dem Modi selbst gehoert. DIE GRENZE, AN DREI STELLEN STATT AN EINER * Die Auswahl geht durch EIN Sieb (hat diese Person ueberhaupt eine Liste, und steht sie in diesem Haus?) statt durch drei einzeln gepflegte Bedingungen. * Die Grenze haelt auch gegen eine von Hand eingetragene Nummer -- eine ausgeduennte Auswahlliste ist Kosmetik, solange ?creator_id= durchgeht. * Die gueltigen Punkt-Schluessel lagen fuer beide Haeuser in EINER Menge. Ein Scout konnte damit bei einem Creator den Stand eines Modi-Punktes setzen: angenommen, gespeichert, nie zu sehen. * Dazu: `darfCreator` sagt fuer DogFather bei JEDER Nummer ja -- er konnte einen Stand an einer Managerin oder an sich selbst setzen. Drei Lagen wie bei den Aufgaben: crew / agentur / keine Adresse. Der dritte Ausgang ist kein Schlupfloch, sondern die Bedingung dafuer, dass die Pruefungen ueberhaupt noch etwas messen koennen. ZWEI SKALEN, WEIL ES ZWEI VERSCHIEDENE DINGE SIND Agentur (Betreuung urteilt, Creator liest): Passt / Verbesserung moeglich / Passt nicht / Trifft nicht zu. Team (Modi berichtet, DogFather behandelt im Eingang): Passt so / Verbessern, unveraendert -- eine Stufe "Passt nicht" haette dort keinen Empfaenger. "Trifft nicht zu" ist kein Beiwerk: Ohne sie steht ein Punkt, der bei diesem Creator gar nicht vorkommt, fuer immer auf "offen" und die Bilanz zaehlt ihn als unerledigt mit. Die Worte, die Toene und die Frage im Nachfragefenster kommen vom Server. Der Browser baut Knoepfe, Marken und Kacheln daraus und kennt keine Stufe beim Namen -- sonst muesste er ausserdem wissen, WANN welche gilt, und das waere ein Rollenvergleich in einer Datei, die jeder herunterladen kann. DIE NOTIZ TRAEGT JETZT AUCH DIE ROLLE "mit der notiz vom manager oder scout" -- bis hierher stand am Satz nur ein Vorname. Wer die Namen im ersten Monat nicht kennt, weiss nicht, wer da urteilt. Jetzt: "Patrick, Scout · 24.09., 23:43". DIE UMSTELLUNG DER DATENBANK KOMMT NICHT VON MIR Eine CHECK-Regel laesst sich in SQLite nicht aendern; die Tabelle muss neu gebaut werden. Ich hatte den Griff hier zuerst ein zweites Mal geschrieben -- mit Zeilenzaehlung und PRAGMA-Spaltenliste, aber OHNE die Sicherung davor, ohne die Indizes und ohne `foreign_key_check` danach. Drei von fuenf Absicherungen fehlten, und keine davon haette gefehlt, wenn ich die vorhandene Funktion benutzt haette. Genau davor warnt ihr eigener Kommentar seit dem 09.09.2026. Jetzt: `checkListeErweitern` aus workspace.js, ausgegeben statt nachgebaut. Der Marker ist die erste fehlende Stufe und keine hingeschriebene -- eine feste Angabe waere an dem Tag falsch, an dem eine weitere dazukommt. Und danach wird NACHGESEHEN, was wirklich erlaubt ist: Bricht die Umstellung ab, werden die neuen Stufen auch nicht angeboten. Ein Knopf, der beim Druecken scheitert, ist schlechter als kein Knopf. WAS SONST NOCH NACHGEZOGEN WURDE * Der Zaehler auf der Creator-Startseite zaehlte fest `stufe = 'verbessern'`. Die staerkste Rueckmeldung, die es gibt, waere als Einzige nicht dort erschienen. Jetzt aus dem Katalog. * Der Satz unter "Feste Punkte" stand im Browser und sprach in BEIDEN Haeusern vom "Creator". Die Teamfassung bleibt wortgleich -- ab dem 24.09. wird jeder Umbau je Haus getrennt gefuehrt, und dies ist der des Agenturhauses. * "Passt" setzt weiterhin mit einem Klick. Ein Nachfragefenster vor dem haeufigsten Klick einer Betreuung, die vierzig Punkte durchgeht, macht aus einem Durchgang eine Sitzung. GEMESSEN * pruef-checkliste-stufen.mjs, neu: 56 Pruefungen, 0 Fehler. Darin die Umstellung an einer Datenbank mit dem ALTEN Bauplan und echten Zeilen -- Zeilen, Spalten UND Spalteninhalte nachgezaehlt, plus die Sicherung. Zu jeder Schranke die Gegenprobe, die durchkommen muss. * pruef-checkliste 97, pruef-modi-checkliste 75, pruef-haus-trennung 97, pruef-manager-sicht 43 -- alle unveraendert gruen. * pruef-checkliste rechnete mit festen Zahlen (drei Bilanzkacheln, zwei Knoepfe je Punkt) und war rot, ohne dass etwas kaputt war. Sie fragt die Zahlen jetzt bei der Schnittstelle ab und zaehlt sie im Browser nach. Gleich viele Pruefstellen, 57. * Bildschirmfotos bei 1280 px und 390 px (Betreuung, Creator, das Nachfragefenster): vier Knoepfe passen auf dem Handy als 2x2, 0 px Ueberhang, keine Konsolenfehler. * Die neuen Toene sind gerechnet, nicht gegriffen: #d97f87 hat die relative Helligkeit 0,317 -- so hell wie das vorhandene Gruen (0,320) und heller als das Blaugrau von "offen" (0,241), das den Barrierefreiheits-Lauf schon bestanden hat. Kein Signalrot: Ein gedaempftes Rosé sagt "das gehoert geaendert", ein Rot sagt "du hast versagt". Co-Authored-By: Claude Opus 5 --- server/bild-checkliste-stufen.mjs | 142 ++++++++ server/pruef-checkliste-stufen.mjs | 520 ++++++++++++++++++++++++++++ server/pruef-checkliste.mjs | 51 ++- server/workspace-checkliste.js | 525 ++++++++++++++++++++++++++--- server/workspace-hinweise.js | 20 +- server/workspace.js | 10 +- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/start.css | 68 +++- workspace/assets/js/checkliste.js | 158 ++++++--- workspace/aufgaben.html | 38 +-- workspace/automation.html | 32 +- workspace/befinden.html | 34 +- workspace/bereich.html | 44 +-- workspace/bewerben.html | 38 +-- workspace/bewerbungen.html | 36 +- workspace/calls.html | 34 +- workspace/chat.html | 38 +-- workspace/content.html | 34 +- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +- workspace/entwicklung.html | 36 +- workspace/hilfe.html | 30 +- workspace/index.html | 10 +- workspace/kalender.html | 34 +- workspace/leistung.html | 32 +- workspace/material.html | 32 +- workspace/personen.html | 32 +- workspace/profil.html | 36 +- workspace/rechte.html | 34 +- workspace/report.html | 36 +- workspace/scouting.html | 32 +- workspace/start.html | 32 +- workspace/startcheck.html | 34 +- workspace/steckbrief.html | 36 +- workspace/support.html | 28 +- workspace/talente.html | 36 +- workspace/team.html | 32 +- workspace/teamlage.html | 34 +- workspace/teilen.html | 30 +- workspace/treff-moderation.html | 32 +- workspace/treff-regeln.html | 32 +- workspace/uebersicht.html | 32 +- workspace/unsere-seiten.html | 30 +- workspace/unterstuetzen.html | 30 +- workspace/werdegang.html | 32 +- workspace/willkommen.html | 30 +- workspace/wissen.html | 34 +- 49 files changed, 2012 insertions(+), 740 deletions(-) create mode 100644 server/bild-checkliste-stufen.mjs create mode 100644 server/pruef-checkliste-stufen.mjs 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`. --> - - + + - +