Files
dogfather-universe/server/pruef-vorlagen.mjs
T
DogFatherGitandClaude Opus 5 9267797d1d Feste Checklisten statt Vorschlaege - in allen vier Bereichen
"Die neuen Sachen in der LIVE-Analyse soll der Creator FEST sehen, auch
schoen kategorisiert, und der Ansprechpartner soll anklicken koennen,
wenn er findet, dass der Creator was verbessern sollte."

DER UMBAU. Vorher waren die Punkte Vorschlaege zum Uebernehmen: Man
holte sie sich und bekam einen eigenen Eintrag. Das war falsch gedacht.
Eine Checkliste, die man erst anfordern muss, ist keine Checkliste --
und schlimmer: Zwei Creator haetten unterschiedliche Listen gehabt, je
nachdem wer sich was geholt hat. Genau das macht einen Vergleich
unmoeglich, und um Vergleich geht es bei einer Betreuung.

Jetzt stehen dieselben Punkte fuer JEDEN fest da, gruppiert:

  LIVE       21 Punkte -- vor, waehrend, nach der Sendung
  Community  14 Punkte -- Moderation vorbereiten, aufbauen, wenn es kippt
  Technik    12 Punkte -- Einrichtung, Ausfall, was geholfen hat
  Content    13 Ideen  -- nach Saeule (70/20/10) statt nach Ablauf

Jede Gruppe hat einen Satz, der erklaert, wofuer sie da ist. Eine
Ueberschrift allein sagt das nicht.

Was sich je Creator unterscheidet, ist nur der STAND -- und den setzt
die Betreuung: "Passt so" oder "Verbessern". Ein Creator kann sich nicht
selbst bewerten; koennte er es, stuende alles auf gruen. "Verbessern"
verlangt einen Satz, WAS zu verbessern ist -- eine Bewertung, mit der er
nichts anfangen kann, ist nicht streng, sondern nur entmutigend.

Der Creator sieht alles: die Stufe, den Grund, den Namen und den
Zeitpunkt. Und er kann an JEDEM Punkt antworten -- das ist der Kanal,
ueber den er ueberhaupt etwas sagen kann.

Oben steht eine Bilanz in einer Zeile: wie viele passen, wie viele sind
zu verbessern, wie viele hat noch niemand angesehen. Das ist die Frage,
die beide Seiten zuerst haben.

STABILE SCHLUESSEL statt Positionen. Ein Stand haengt am Schluessel des
Punktes ("ton-geprueft"), nicht an seiner Nummer. Haenge er an der
Position, waere beim Einfuegen eines Punktes in der Mitte jede Bewertung
dahinter am falschen Punkt -- und niemand wuerde es merken, weil beides
plausibel aussieht. 64 Punkte haben jetzt einen.

"Offen" loescht den Stand, statt ihn auf "offen" zu setzen: Ein
Datensatz, der nichts aussagt, ist Ballast.

ZWEI EIGENE FEHLER, VON DEN PRUEFUNGEN GEFUNDEN:

1. DAS CSS LAG IN DER FALSCHEN DATEI. Ich hatte es in content.css
   geschrieben -- bereich.html laedt die gar nicht. Die Punkte standen
   auf drei von vier Seiten nackt und ohne Flaeche da. Es ist derselbe
   Fehler wie im August bei .knopf-still, .schalter und .kopf-zeile, und
   es gibt seitdem eine Warnung dazu in start.css. Sie hat nichts
   genutzt, solange keine Pruefung sie nachhielt.

   Jetzt gibt es eine: Sie misst, ob eine Karte wirklich eine Kante und
   Polsterung hat -- nicht nur, ob das Element existiert. Gegenprobe
   gemacht: Klasse umbenannt, Pruefung meldet "STIL FEHLT".

2. Ein Betreuer sah beim Oeffnen keine Bewertungsknoepfe, weil noch kein
   Creator gewaehlt war -- und musste erst raten, dass er oben jemanden
   auswaehlen soll. Jetzt nimmt der Server den ersten betreuten Creator,
   wenn keiner angegeben ist.

Die alte Oberflaechenpruefung fuer den Vorschlaege-Block wurde entfernt
statt angepasst: Sie verlangte etwas, das es nicht mehr gibt. Eine
dauerhaft rote Pruefung ist schlimmer als keine -- man gewoehnt sich
daran, und beim naechsten echten Fehler sieht niemand hin.

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

228 lines
12 KiB
JavaScript

/* Prueft die fertigen Vorlagen und was daraus wird.
Der Kern ist nicht "es gibt Vorlagen", sondern was beim UEBERNEHMEN
passiert: Aus einem Vorschlag muss ein ganz normaler Eintrag werden --
aenderbar, loeschbar, und ab dann unabhaengig von der Vorlage.
Und "Hilfe anfordern" muss eine AUFGABE erzeugen, keine Nachricht.
Eine Nachricht ist gelesen und dann weg; eine Aufgabe bleibt stehen,
bis sie jemand erledigt. Geprueft wird deshalb, dass sie beim
richtigen Betreuer landet und eine Frist hat. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-vorl-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4204";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
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, 700));
const BASIS = "http://127.0.0.1:4204";
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_000).unref?.();
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
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 jetzt = new Date().toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idScout, jetzt);
d.close();
const keks = 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 kDogi = await keks("admin", "CODE-DOGI-0001");
const kTili = await keks("creator", "CODE-ANNA-0001");
const hole = (pfad, k, o = {}) => fetch(BASIS + pfad, { ...o, headers: { Cookie: k, ...(o.headers || {}) } });
/* ---------- Was es gibt --------------------------------------------------- */
console.log("\n=== Die Vorlagen");
let vorlagen;
{
const a = await hole("/workspace/api/vorlagen", kTili);
vorlagen = await a.json();
ok(a.ok, "die Vorlagen sind abrufbar");
ok(vorlagen.content.length >= 12, `genug Content-Ideen (${vorlagen.content.length})`);
ok(vorlagen.live.vorbereitung.length >= 6, `Vorbereitungspunkte (${vorlagen.live.vorbereitung.length})`);
ok(vorlagen.live.mitschrift.length >= 5, `Punkte fuer waehrend LIVE (${vorlagen.live.mitschrift.length})`);
ok(vorlagen.live.auswertung.length >= 5, `Auswertungspunkte (${vorlagen.live.auswertung.length})`);
/* Jede Content-Idee braucht einen ausformulierten Aufhaenger -- die
ersten Sekunden entscheiden, und "mach was Persoenliches" hilft
niemandem. */
const ohneHook = vorlagen.content.filter((v) => !v.hook || v.hook.length < 15);
ok(ohneHook.length === 0,
`jede Idee hat einen ausformulierten Aufhaenger (${ohneHook.length} ohne)`);
const ohneFormat = vorlagen.content.filter((v) => !v.format);
ok(ohneFormat.length === 0, `und ein Format (${ohneFormat.length} ohne)`);
/* 70/20/10: der groesste Teil muss Inhalte mit Wert sein. */
const zaehl = {};
for (const v of vorlagen.content) zaehl[v.saeule] = (zaehl[v.saeule] || 0) + 1;
const anteilWert = zaehl.Wert / vorlagen.content.length;
ok(anteilWert >= 0.45,
`der groesste Teil sind Inhalte mit Wert (${Math.round(anteilWert * 100)} %: ${JSON.stringify(zaehl)})`);
/* Punkte, bei denen man Hilfe braucht, muessen als solche erkennbar
sein -- sonst weiss niemand, wo er fragen darf. */
const mitHilfe = Object.values(vorlagen.live).flat().filter((v) => v.hilfe);
ok(mitHilfe.length >= 5, `Punkte mit Hilfe-Angebot (${mitHilfe.length})`);
}
/* ---------- Uebernehmen --------------------------------------------------- */
console.log("\n=== Uebernehmen");
{
/* Ein Creator uebernimmt fuer sich selbst -- ohne auf jemanden zu warten. */
const a = await hole("/workspace/api/vorlagen/uebernehmen", kTili, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ art: "content", nr: 0 }) });
const r = await a.json();
ok(a.ok && r.angelegt === 1, `ein Creator darf selbst uebernehmen (${r.angelegt})`);
const liste = await (await hole("/workspace/api/bereich/content", kTili)).json();
const drin = liste.eintraege.find((e) => e.titel === vorlagen.content[0].titel);
ok(!!drin, `die Idee steht jetzt in seiner Liste ("${drin?.titel}")`);
/* Der Aufhaenger muss MITKOMMEN -- sonst ist die halbe Vorlage weg. */
ok(drin?.hook === vorlagen.content[0].hook, `mit dem Aufhaenger (${drin?.hook?.slice(0, 30)}…)`);
ok(drin?.format === vorlagen.content[0].format, `und dem Format (${drin?.format})`);
ok(drin?.art === "idee", `und beginnt bei "Idee" (${drin?.art})`);
/* Sie ist ab jetzt ein NORMALER Eintrag: aenderbar. Das ist der
eigentliche Punkt an Vorlagen gegenueber fest eingebauten Daten. */
const p = await hole(`/workspace/api/bereich/content/${drin.id}`, kTili, {
method: "PATCH", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ titel: "Selbst umbenannt" }) });
ok(p.ok, `und laesst sich aendern (${p.status})`);
/* Alle auf einmal. */
const b = await hole("/workspace/api/vorlagen/uebernehmen", kTili, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ art: "live_alle", unterart: "vorbereitung" }) });
const rb = await b.json();
ok(b.ok && rb.angelegt === vorlagen.live.vorbereitung.length,
`alle Vorbereitungspunkte auf einmal (${rb.angelegt})`);
const live = await (await hole("/workspace/api/bereich/live", kTili)).json();
ok(live.eintraege.filter((e) => e.art === "vorbereitung").length === rb.angelegt,
"und sie stehen alle im Bereich LIVE");
}
/* Ein Betreuer uebernimmt fuer SEINEN Creator -- und nur fuer den. */
console.log("\n=== Wer fuer wen");
{
const fremd = anlegenFremd();
const a = await hole("/workspace/api/vorlagen/uebernehmen", kDogi, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ art: "content", nr: 1, creator_id: idTili }) });
ok(a.ok, `DogFather darf fuer Tili uebernehmen (${a.status})`);
const kScout = await keks("scout", "CODE-SCOU-0001");
const b = await hole("/workspace/api/vorlagen/uebernehmen", kScout, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ art: "content", nr: 2, creator_id: fremd }) });
ok(b.status === 404,
`ein Scout NICHT fuer einen fremden Creator -- und erfaehrt nicht, dass es ihn gibt (${b.status})`);
}
function anlegenFremd() {
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-FREM-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d2.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run("Fremd", "creator", hash, salt, 32768, jetzt);
const id = d2.prepare("SELECT last_insert_rowid() AS id").get().id;
d2.close();
return id;
}
/* ---------- Hilfe anfordern ----------------------------------------------- */
console.log("\n=== Hilfe anfordern");
{
const a = await hole("/workspace/api/vorlagen/hilfe", kTili, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ thema: "Ton geprüft", notiz: "Mein Mikro rauscht." }) });
ok(a.ok, `ein Creator kann Hilfe anfordern (${a.status})`);
/* Es muss eine AUFGABE geworden sein, nicht eine Nachricht. */
const d3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
const auf = d3.prepare(
"SELECT * FROM aufgaben WHERE titel LIKE 'Hilfe gewünscht%' ORDER BY id DESC LIMIT 1").get();
d3.close();
ok(!!auf, "daraus wird eine Aufgabe");
ok(auf?.verantwortlich_id === idScout,
`sie geht an seinen Betreuer Patrick (${auf?.verantwortlich_id} = ${idScout})`);
ok(auf?.creator_id === idTili, "und haengt an ihm");
ok(auf?.prioritaet === "hoch", `mit hoher Prioritaet (${auf?.prioritaet})`);
/* Ohne Frist bleibt sie liegen -- das ist der Unterschied zwischen
einer Aufgabe und einem Zettel. */
ok(!!auf?.frist, `und einer Frist (${auf?.frist})`);
ok(String(auf?.beschreibung || "").includes("Mein Mikro rauscht"),
"die Notiz steht mit drin");
/* Der Betreuer sieht sie auf seiner Startseite. */
const kScout = await keks("scout", "CODE-SCOU-0001");
/* Sie steht in SEINER Aufgabenliste. Als Hinweis auf der Startseite
erscheint sie erst, wenn die Frist naeher rueckt -- eine Aufgabe mit
drei Tagen Zeit ist weder ueberfaellig noch heute faellig, und das
ist richtig so. Ein erster Anlauf hat genau das geprueft und war
rot, obwohl das Verhalten stimmte. */
const seine = await (await hole("/workspace/api/aufgaben", kScout)).json();
const dabei = (seine.aufgaben || []).some((x) => String(x.titel).startsWith("Hilfe gewünscht"));
ok(dabei, `und steht in seiner Aufgabenliste (${(seine.aufgaben || []).length} Aufgaben)`);
/* Ohne Thema geht nichts -- eine leere Bitte hilft niemandem. */
const leer = await hole("/workspace/api/vorlagen/hilfe", kTili, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ thema: "" }) });
ok(leer.status === 400, `ohne Thema wird abgelehnt (${leer.status})`);
}
/* ---------- Die Oberflaeche ----------------------------------------------
Der Browser-Teil stand hier und prueft den frueheren
"Vorschlaege"-Block. Den gibt es seit dem Umbau auf die feste
Checkliste nicht mehr: Die Punkte stehen jetzt fuer jeden gleich da,
statt einzeln geholt zu werden.
Geprueft wird die Oberflaeche deshalb in pruef-checkliste.mjs. Die
Schnittstelle oben bleibt hier -- "Uebernehmen" gibt es weiterhin,
fuer eigene Ergaenzungen ueber die feste Liste hinaus.
Eine Pruefung stehen zu lassen, die etwas Abgeschafftes verlangt,
waere schlimmer als keine: Sie ist dauerhaft rot, man gewoehnt sich
daran, und beim naechsten echten Fehler sieht niemand hin. */
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
process.exit(fehler ? 1 : 0);