Wunsch: "die sollen nirgendwo Hilfe anfordern koennen, sondern nur Rueckmeldungen." Es ist auch die klarere Loesung. Es gab zwei Wege, dasselbe zu sagen: einen Knopf, der eine Aufgabe erzeugte, und ein Textfeld, das eine Nachricht schrieb. Zwei Wege fuer eine Sache heisst, dass niemand weiss, welcher der richtige ist -- und dass Antworten mal als Aufgabe und mal als Nachricht landen. Wer dann nachsieht, findet die Haelfte nicht. Geblieben ist die Rueckmeldung an jedem Punkt: ein Verlauf, in dem beide Seiten schreiben koennen, sichtbar dort, wo es hingehoert -- am Punkt selbst und nicht in einer zweiten Liste. VOLLSTAENDIG entfernt, nicht nur ausgeblendet: * der Knopf in der Checkliste * der Knopf und die Marke "Hilfe moeglich" im Vorlagenblock * die Route /workspace/api/vorlagen/hilfe * 47 Datenfelder hilfe: true/false in den Vorlagen * die zugehoerigen Stile * der Abschnitt in pruef-vorlagen.mjs Datenfelder, die nichts mehr bewirken, und Routen, die niemand mehr aufruft, werden beim naechsten Mal fuer lebenden Code gehalten und mitgepflegt. Das ist teurer als das Entfernen heute. Co-Authored-By: Claude Opus 5 <[email protected]>
186 lines
9.2 KiB
JavaScript
186 lines
9.2 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)})`);
|
|
|
|
}
|
|
|
|
/* ---------- 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;
|
|
}
|
|
|
|
/* Der Abschnitt "Hilfe anfordern" stand hier. Der Weg ist am
|
|
01.09.2026 entfallen -- geblieben ist die Rückmeldung an jedem Punkt,
|
|
geprüft in pruef-checkliste.mjs. */
|
|
|
|
/* ---------- 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);
|