Filipe hat den Bauplan "Willkommen & Anleitung -- Einweisung je Rolle"
(27 Seiten, Stand 06.10.2026) geschickt: pruefen, perfektionieren und
auf der Crew-Seite umsetzen.
AUSGANGSMESSUNG, bevor etwas angefasst wurde:
agentur creator 18 · manager 22 · scout 21 · spicy 23 Karten
crew 0 Karten -- die Schnittstelle antwortete 404 "nicht_hier"
Die Seite lud also, war aber leer. Baulich vorbereitet war sie schon:
Alle drei Inhaltstabellen tragen `haus` mit UNIQUE (haus, rolle,
kachel), und die Aussaat nimmt das Haus als Parameter.
WAS JETZT DA IST
gast 14 Karten (5 Muss, 4 Regelmaessig, 5 Bedarf)
modi 29 Karten (7 / 7 / 15)
linke 29 Karten (7 / 7 / 15)
hand 34 Karten (8 / 7 / 19)
Jede Zahl entspricht Kapitel 13 des Bauplans. Dazu je Rolle
Leitgedanke, drei Saetze, fuenf Erste Schritte, "Nicht deins",
Rhythmus und Fragen -- woertlich aus Kapitel 06 bis 09.
VOR DEM SCHREIBEN ABGEGLICHEN, nicht danach: Ein Skript prueft jede
Karte gegen die echte Kachelliste der Rolle. Ergebnis fuer alle vier
Fassungen: Kartenzahl wie im Plan, jede Karte trifft eine Kachel,
jede Kachel hat eine Karte, keine doppelt.
ZWEI KACHELN AUF EINER SEITE. Auf der Crew-Adresse fuehren "Chat"
(das Team) und "Rudel-Chat" (`?raum=treff`) beide auf chat.html.
`schluesselVonZiel` behielt nur `b` -- beide ergaben denselben
Schluessel, und damit konnten sie keine eigenen Karten haben, obwohl
der Bauplan ihnen verschiedene Einordnungen gibt (fuer die rechte
Hand: Team-Chat Muss, Rudel-Chat nur bei Bedarf). Jetzt wird auch
`raum` unterschieden. Auf der Agenturadresse gibt es nur eine
Chat-Kachel; dort aendert sich nichts.
DAS HAUS KOMMT AUS DER ADRESSE, nicht aus der angesehenen Rolle.
`const HAUS = "agentur"` ist einer Funktion gewichen. Nimmt DogFather
die Sicht einer anderen Rolle ein, wechselt er die ROLLE, nicht die
ADRESSE -- wer hier `req.sicht` naehme, bekaeme auf der Crew-Adresse
Agenturinhalte, sobald eine Sicht gesetzt ist.
`FASSUNGEN` stand an zehn Stellen fest und haette die Crew ueberall
abgewiesen. Jetzt `fassungenFuer(haus)` -- eine Funktion statt zweier
Listen an zehn Stellen.
Neue Zeilenart `vorweg` fuer "Drei Dinge vorweg" (nur die Community,
Bauplan Kapitel 04). Fehlt die Liste bei einer Rolle, wird nichts
eingetragen -- kein Sonderfall noetig.
Eigene Datei fuer die Crew-Inhalte: anleitung-tabellen.js traegt
schon die Agenturfassungen und ist 856 Zeilen lang. Getrennte Dateien
sind hier dasselbe Prinzip wie getrennte Haeuser.
pruef-anleitung 125 -> 127. Fuenf Zeilen darin verteidigten den alten
Zustand ("ein Modi bekommt die Anleitung nicht, erwartet 404") -- sie
sind mitgewandert statt stehenzubleiben. Dazu eine neue Gegenprobe
zur Haustrennung: In der Crew-Fassung darf keine Agenturkachel
vorkommen.
Datenbank vorher gesichert und zurueckgelesen (integrity_check,
84 Karten).
Co-Authored-By: Claude Opus 5 <[email protected]>
843 lines
42 KiB
JavaScript
843 lines
42 KiB
JavaScript
/* =====================================================================
|
|
DIE ANLEITUNG — stimmt sie mit den Kacheln ueberein? (Etappe 1)
|
|
=====================================================================
|
|
|
|
DIE EIGENTLICHE FRAGE DIESER DATEI steht im Bauplan gar nicht; sie
|
|
kam beim Nachmessen dazu, und sie ist der Grund, warum es diese
|
|
Pruefung ueberhaupt gibt:
|
|
|
|
Erklaert die Anleitung genau die Kacheln, die es gibt --
|
|
und zwar JEDE und KEINE DARUEBER HINAUS?
|
|
|
|
Beides kann lautlos kaputtgehen, und beides auf dieselbe Weise: Wer
|
|
eine Kachel hinzufuegt, vergisst die Karte -- dann steht auf der
|
|
Startseite etwas, das die Einweisung nicht kennt. Wer eine Kachel
|
|
entfernt, laesst die Karte stehen -- dann erklaert die Einweisung
|
|
etwas, das es nicht mehr gibt, mit einem Knopf, der ins Leere
|
|
fuehrt.
|
|
|
|
Der Bauplan sieht dafuer nur einen Hinweis in der Pflege-Ansicht vor
|
|
("Fuer diese Kachel fehlt ein Eintrag"). Der hilft nur, wenn jemand
|
|
hinsieht. Diese Pruefung sieht immer hin.
|
|
|
|
---------------------------------------------------------------------
|
|
WOHER DIE KACHELLISTE KOMMT
|
|
|
|
Aus `workspace/assets/js/bereiche.js`, und zwar indem die Datei
|
|
AUSGEFUEHRT wird -- nicht, indem jemand sie mit einem Muster
|
|
durchsucht. Ein Muster waere eine zweite, ungenaue Nachbildung der
|
|
Datei; sie wuerde bei der ersten ungewohnten Schreibweise still
|
|
danebenliegen und dann "alles in Ordnung" melden.
|
|
|
|
Der Server kennt diese Liste nicht (bereicheFuer gibt fuers
|
|
Agenturhaus `null` zurueck). Deshalb muss die Pruefung sie sich
|
|
selbst holen -- genau wie der Browser es tut.
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import vm from "node:vm";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-anleitung");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-anl-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(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 }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-anleitung");
|
|
await new Promise((r) => setTimeout(r, 800));
|
|
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
function braucht(bedingung, was) {
|
|
if (bedingung) return;
|
|
console.log(`\nABBRUCH: konnte nicht nachsehen — ${was}`);
|
|
console.log("Das ist KEIN Befund am Code. Die Pruefung konnte ihre");
|
|
console.log("Voraussetzung nicht herstellen und sagt das, statt zu schweigen.\n");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(2);
|
|
}
|
|
|
|
/* =====================================================================
|
|
DIE KACHELLISTE — bereiche.js wirklich ausfuehren
|
|
===================================================================== */
|
|
function bereicheLaden() {
|
|
const quelle = readFileSync(
|
|
new URL("../workspace/assets/js/bereiche.js", import.meta.url), "utf8");
|
|
const fenster = {};
|
|
const tot = () => ({ setAttribute() {}, append() {}, style: { setProperty() {} } });
|
|
const umgebung = { window: fenster, URLSearchParams,
|
|
document: { createElement: tot, createElementNS: tot } };
|
|
umgebung.globalThis = umgebung;
|
|
vm.createContext(umgebung);
|
|
vm.runInContext(quelle, umgebung, { filename: "bereiche.js" });
|
|
return fenster.Bereiche;
|
|
}
|
|
|
|
const B = bereicheLaden();
|
|
braucht(B && Array.isArray(B.GRUPPEN) && B.GRUPPEN.length > 0,
|
|
"bereiche.js liefert keine Kachelliste — ohne sie ist nichts zu vergleichen");
|
|
braucht(typeof B.schluesselVonZiel === "function",
|
|
"bereiche.js kennt schluesselVonZiel nicht");
|
|
|
|
melde("\n=== Der Kachel-Schluessel rechnet richtig ===");
|
|
const SCHLUESSEL_PROBEN = [
|
|
["uebersicht.html", "uebersicht"],
|
|
["bereich.html?b=live", "bereich:live"],
|
|
["bereich.html?b=agentur", "bereich:agentur"],
|
|
["profil.html", "profil"],
|
|
/* ZWEI KACHELN AUF EINER SEITE (06.10.2026). Hier stand "chat" --
|
|
und damit bekamen "Chat" (das Team) und "Rudel-Chat" (die
|
|
Community) denselben Schluessel. Auf der Agenturadresse fiel das
|
|
nie auf, dort gibt es nur eine Chat-Kachel. Die Crew hat beide,
|
|
und der Bauplan gibt ihnen verschiedene Einordnungen -- fuer die
|
|
rechte Hand ist der Team-Chat Muss, der Rudel-Chat nur bei
|
|
Bedarf. Ohne eigenen Schluessel waere das nicht abbildbar. */
|
|
["chat.html?raum=treff", "chat:treff"],
|
|
["chat.html", "chat"],
|
|
["", ""],
|
|
];
|
|
for (const [ziel, erwartet] of SCHLUESSEL_PROBEN) {
|
|
const ist = B.schluesselVonZiel(ziel);
|
|
ok(ist === erwartet, `"${ziel || "(leer)"}" -> "${ist}" (erwartet "${erwartet}")`);
|
|
}
|
|
/* GEGENPROBE: Die Rechnung muss auch TRENNEN koennen. Lieferte sie fuer
|
|
alles dasselbe, waeren die sechs Zeilen oben trotzdem teils gruen. */
|
|
ok(B.schluesselVonZiel("bereich.html?b=live")
|
|
!== B.schluesselVonZiel("bereich.html?b=technik"),
|
|
"Gegenprobe: zwei Bereiche derselben Seite bekommen verschiedene Schluessel");
|
|
|
|
function kachelnFuer(rolle) {
|
|
const nach = new Map();
|
|
for (const g of B.GRUPPEN) {
|
|
for (const k of (g.bereiche || [])) {
|
|
if (Array.isArray(k.rollen) && !k.rollen.includes(rolle)) continue;
|
|
const s = B.schluesselVonZiel(k.ziel);
|
|
if (s) nach.set(s, k);
|
|
}
|
|
}
|
|
return nach;
|
|
}
|
|
|
|
/* =====================================================================
|
|
LEUTE UND ANMELDUNG
|
|
===================================================================== */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetztIso = new Date().toISOString();
|
|
const kennSchluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", kennSchluessel, jetztIso);
|
|
|
|
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,"
|
|
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", kennSchluessel).update(code).digest("hex"), jetztIso);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
|
|
const ROLLEN = [["admin", "Filipe"], ["spicy", "Spicy Media"], ["manager", "Patrick"],
|
|
["scout", "Kevin"], ["creator", "Luna"], ["modi", "Miss"], ["hand", "VanVan"]];
|
|
const CODES = {};
|
|
for (const [i, [rolle, name]] of ROLLEN.entries()) {
|
|
CODES[rolle] = `CODE-A${String(i).padStart(2, "0")}-000001`;
|
|
anlegen(name, rolle, CODES[rolle]);
|
|
}
|
|
|
|
/* Wie viele Zeilen hat die Erstbefuellung angelegt? Direkt in der
|
|
Datenbank nachgezaehlt und nicht ueber die Schnittstelle: Sonst
|
|
pruefte man die Schnittstelle mit sich selbst. */
|
|
const inDb = d.prepare(
|
|
"SELECT COUNT(*) n FROM anl_karte WHERE haus = 'agentur' AND rolle = 'creator'").get().n;
|
|
d.close();
|
|
|
|
const { request } = await import("node:http");
|
|
const CREW_HOST = "crew.dogfather-universe.com";
|
|
|
|
function roh(pfad, host, koerper, keks, methode) {
|
|
return new Promise((fertig, schief) => {
|
|
const daten = koerper ? JSON.stringify(koerper) : null;
|
|
const a = request({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: methode || (daten ? "POST" : "GET"),
|
|
headers: {
|
|
...(host ? { Host: host } : {}),
|
|
Origin: host ? `https://${host}` : BASIS,
|
|
...(keks ? { Cookie: keks } : {}),
|
|
...(daten ? { "Content-Type": "application/json",
|
|
"Content-Length": Buffer.byteLength(daten) } : {}),
|
|
},
|
|
}, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (t) => { text += t; });
|
|
antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers, text }));
|
|
});
|
|
a.on("error", schief);
|
|
if (daten) a.write(daten);
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
async function anmelden(rolle, host = null) {
|
|
const r = await roh("/workspace/api/anmelden", host, { rolle, code: CODES[rolle] });
|
|
if (r.code !== 200) return null;
|
|
return [].concat(r.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ") || null;
|
|
}
|
|
|
|
melde("\n=== Alle Rollen kommen herein ===");
|
|
const kekse = {};
|
|
for (const [rolle] of ROLLEN) {
|
|
const haus = (rolle === "modi" || rolle === "hand") ? CREW_HOST : null;
|
|
kekse[rolle] = await anmelden(rolle, haus);
|
|
ok(!!kekse[rolle], `${rolle}: angemeldet`);
|
|
}
|
|
braucht(Object.values(kekse).every(Boolean), "nicht alle Rollen konnten sich anmelden");
|
|
|
|
const holen = async (rolle, host = null) => {
|
|
const r = await roh("/workspace/api/anleitung", host, null, kekse[rolle]);
|
|
let j = null;
|
|
try { j = JSON.parse(r.text); } catch { /* bleibt null */ }
|
|
return { code: r.code, j };
|
|
};
|
|
|
|
/* =====================================================================
|
|
DIE ERSTBEFUELLUNG
|
|
===================================================================== */
|
|
melde("\n=== Die Inhalte sind eingespielt ===");
|
|
ok(inDb === 18, `Creator hat 18 Karten in der Datenbank (${inDb})`);
|
|
|
|
const creator = await holen("creator");
|
|
ok(creator.code === 200, `Creator bekommt die Anleitung (HTTP ${creator.code})`);
|
|
braucht(creator.code === 200 && creator.j, "die Schnittstelle antwortet dem Creator nicht");
|
|
ok(creator.j.hat_fassung === true, "Creator hat eine eigene Fassung");
|
|
ok(creator.j.anzahl === 18, `18 Karten ueber die Schnittstelle (${creator.j.anzahl})`);
|
|
ok((creator.j.saetze || []).length === 3, `drei Saetze „In 60 Sekunden" (${(creator.j.saetze || []).length})`);
|
|
ok((creator.j.schritte || []).length === 5, `fuenf Erste Schritte (${(creator.j.schritte || []).length})`);
|
|
ok((creator.j.nicht || []).length === 5, `fuenf Zeilen „Nicht deins" (${(creator.j.nicht || []).length})`);
|
|
ok((creator.j.takt || []).length === 4, `vier Rhythmus-Zeilen (${(creator.j.takt || []).length})`);
|
|
ok((creator.j.fragen || []).length === 4, `vier haeufige Fragen (${(creator.j.fragen || []).length})`);
|
|
ok(!!creator.j.leitgedanke && creator.j.leitgedanke.length > 40,
|
|
"der Leitgedanke steht da");
|
|
|
|
/* DIE ZAHLEN DER ABNAHMELISTE — GERECHNET, NICHT ABGESCHRIEBEN.
|
|
Der Bauplan nennt 5/6/7. Diese Zeile prueft nicht gegen die Zahl,
|
|
sondern gegen die Summe: Zusammen muessen es so viele sein, wie es
|
|
Karten gibt, und keine Karte darf eine unbekannte Stufe tragen. */
|
|
melde("\n=== Die Stufen ===");
|
|
const jeStufe = {};
|
|
for (const k of creator.j.karten) jeStufe[k.stufe] = (jeStufe[k.stufe] || 0) + 1;
|
|
const bekannt = new Set((creator.j.stufen || []).map((s) => s.schluessel));
|
|
/* Dieselbe Menge weiter unten fuer alle vier Rollen -- der Server
|
|
nennt die Stufen, diese Datei kennt keine eigene Liste. */
|
|
const STUFEN_DA = bekannt;
|
|
ok([...Object.keys(jeStufe)].every((s) => bekannt.has(s)),
|
|
`jede Karte traegt eine bekannte Stufe (${Object.keys(jeStufe).join(", ")})`);
|
|
ok(Object.values(jeStufe).reduce((a, b) => a + b, 0) === creator.j.karten.length,
|
|
"die Stufen zusammen ergeben alle Karten");
|
|
ok(jeStufe.muss === 5 && jeStufe.regelmaessig === 6 && jeStufe.bedarf === 7,
|
|
`Verteilung 5/6/7 wie im Bauplan (${jeStufe.muss}/${jeStufe.regelmaessig}/${jeStufe.bedarf})`);
|
|
|
|
/* =====================================================================
|
|
DIE ZUORDNUNG — IN BEIDE RICHTUNGEN
|
|
===================================================================== */
|
|
melde("\n=== Jede Kachel hat eine Karte, jede Karte eine Kachel ===");
|
|
const kacheln = kachelnFuer("creator");
|
|
/* Die Anleitung selbst bekommt keine Karte. Eine Karte, die die Seite
|
|
erklaert, auf der man gerade steht, ist Fuellmaterial. */
|
|
kacheln.delete("anleitung");
|
|
braucht(kacheln.size > 0, "fuer den Creator findet sich keine einzige Kachel");
|
|
|
|
const habenKarte = new Set(creator.j.karten.map((k) => k.kachel));
|
|
const ohneKarte = [...kacheln.keys()].filter((s) => !habenKarte.has(s));
|
|
const ohneKachel = [...habenKarte].filter((s) => !kacheln.has(s));
|
|
|
|
ok(ohneKarte.length === 0 && kacheln.size > 0,
|
|
ohneKarte.length
|
|
? `${ohneKarte.length} Kacheln OHNE Karte: ${ohneKarte.join(", ")}`
|
|
: `alle ${kacheln.size} Kacheln des Creators haben eine Karte`);
|
|
ok(ohneKachel.length === 0 && habenKarte.size > 0,
|
|
ohneKachel.length
|
|
? `${ohneKachel.length} Karten OHNE Kachel: ${ohneKachel.join(", ")}`
|
|
: `alle ${habenKarte.size} Karten zeigen auf eine Kachel, die es gibt`);
|
|
|
|
/* GEGENPROBE. Beide Zeilen oben koennen "in Ordnung" sagen. Koennen sie
|
|
auch "nicht in Ordnung"? Ohne diesen Beweis waere eine leere Menge
|
|
auf beiden Seiten gruen -- und genau so sieht ein kaputter Abgleich
|
|
aus. */
|
|
const erfundeneKachel = new Set([...kacheln.keys(), "gibtesnicht"]);
|
|
ok([...erfundeneKachel].filter((s) => !habenKarte.has(s)).length === 1,
|
|
"Gegenprobe: eine Kachel ohne Karte wird gefunden");
|
|
const erfundeneKarte = new Set([...habenKarte, "auchnicht"]);
|
|
ok([...erfundeneKarte].filter((s) => !kacheln.has(s)).length === 1,
|
|
"Gegenprobe: eine Karte ohne Kachel wird gefunden");
|
|
|
|
/* Jede Karte muss alle drei Saetze haben. Eine halb gefuellte Karte
|
|
sieht aus wie ein Ladefehler. */
|
|
const luecken = creator.j.karten.filter((k) => !k.wozu || !k.tust || !k.merke);
|
|
ok(luecken.length === 0,
|
|
luecken.length ? `${luecken.length} Karten ohne vollstaendige drei Zeilen: `
|
|
+ luecken.map((k) => k.kachel).join(", ")
|
|
: "jede Karte hat Wozu, Was du hier tust und Merke");
|
|
|
|
/* Die Erste-Schritte-Ziele muessen Kacheln dieser Rolle sein -- oder
|
|
gar kein Ziel haben ("Installieren" ist ein Knopf, keine Seite).
|
|
Ein Sprung auf eine Seite, die ein Creator nicht oeffnen darf, waere
|
|
ein Knopf, der auf die Startseite zurueckwirft. */
|
|
const schrittFehler = (creator.j.schritte || [])
|
|
.filter((s) => s.ziel)
|
|
.filter((s) => !kachelnFuer("creator").has(B.schluesselVonZiel(s.ziel)));
|
|
ok(schrittFehler.length === 0,
|
|
schrittFehler.length
|
|
? `Erste Schritte zeigen ins Leere: ${schrittFehler.map((s) => s.ziel).join(", ")}`
|
|
: "jeder Erste Schritt mit Ziel fuehrt auf eine Kachel dieser Rolle");
|
|
|
|
/* =====================================================================
|
|
WER DARF, WER NICHT
|
|
===================================================================== */
|
|
melde("\n=== Die drei Schranken ===");
|
|
|
|
const admin = await holen("admin");
|
|
ok(admin.code === 200 && admin.j?.hat_fassung === false,
|
|
`DogFather bekommt keine Fassung, sondern eine Auskunft (HTTP ${admin.code}, `
|
|
+ `hat_fassung ${admin.j?.hat_fassung})`);
|
|
ok(!!admin.j?.satz && admin.j.satz.length > 30,
|
|
"…und die Auskunft sagt ihm, wie er trotzdem hineinsieht");
|
|
ok(admin.j?.darf_pflegen === true, "DogFather darf pflegen");
|
|
|
|
const spicy = await holen("spicy");
|
|
ok(spicy.code === 200, `Spicy Media bekommt eine Antwort (HTTP ${spicy.code})`);
|
|
ok(spicy.j?.darf_pflegen === true,
|
|
"Spicy Media darf pflegen (Entscheidung 06.10.2026, wie bei den Manager-Zielen)");
|
|
/* Etappe 1 bringt nur den Creator. Scout, Manager und Spicy haben noch
|
|
keine Inhalte -- das ist der Stand der Arbeit und kein Fehler. Es
|
|
wird aber AUSGESPROCHEN, nicht uebersprungen. */
|
|
melde(` -- Scout, Manager und Spicy Media haben noch keine Inhalte `
|
|
+ `(Etappe 3) — Spicy meldet ${spicy.j?.anzahl ?? 0} Karten`);
|
|
|
|
const creatorPflege = creator.j.darf_pflegen;
|
|
ok(creatorPflege === false, "ein Creator darf NICHT pflegen");
|
|
|
|
/* ==== DIE CREW HAT JETZT IHRE EIGENE ANLEITUNG (06.10.2026) ========
|
|
|
|
Hier stand das Gegenteil: "ein Modi bekommt die Anleitung nicht
|
|
(erwartet 404)". Das war bis heute richtig -- die Anleitung gab es
|
|
nur im Agenturhaus. Mit dem Bauplan "Einweisung je Rolle" hat auch
|
|
Team Dogi vier Fassungen bekommen, und die Zeilen sind mitgewandert
|
|
statt stehenzubleiben.
|
|
|
|
DIE TRENNUNG BLEIBT TROTZDEM, und sie wird hier gemessen: Auf der
|
|
Crew-Adresse kommen CREW-Inhalte, nicht die der Agentur. Eine
|
|
Anleitung, die auf beiden Adressen dasselbe zeigt, waere der
|
|
Rueckschritt hinter die Haustrennung vom 24.09. */
|
|
const modi = await holen("modi", CREW_HOST);
|
|
ok(modi.code === 200 && (modi.j?.karten?.length ?? 0) === 29,
|
|
`ein Modi bekommt seine Anleitung (HTTP ${modi.code}, ${modi.j?.karten?.length ?? 0} Karten)`);
|
|
const hand = await holen("hand", CREW_HOST);
|
|
ok(hand.code === 200 && (hand.j?.karten?.length ?? 0) === 34,
|
|
`die rechte Hand auf der Crew-Adresse ebenso (HTTP ${hand.code}, `
|
|
+ `${hand.j?.karten?.length ?? 0} Karten)`);
|
|
/* GEGENPROBE ZUR HAUSTRENNUNG: Die Crew-Fassung darf keine
|
|
Agenturkachel enthalten. "leistung" gibt es nur dort. */
|
|
ok(!(modi.j?.karten || []).some((k) => k.kachel === "leistung"),
|
|
"und keine einzige Agenturkachel ist dabei");
|
|
|
|
/* Die Seite selbst, nicht nur die Schnittstelle.
|
|
=====================================================================
|
|
GEPRUEFT WIRD DAS ZIEL, NICHT NUR DIE 302.
|
|
|
|
Das Haus beantwortet "darfst du nicht" mit einer Umleitung und nicht
|
|
mit 403 (workspace.js, bei `darfSeite`) -- ausdruecklich, damit es
|
|
auf "nein" nur EINE Antwort gibt und niemand aus dem Unterschied
|
|
etwas ablesen kann.
|
|
|
|
Genau deshalb reicht hier "Status ist 302" nicht: NICHT ANGEMELDET
|
|
gibt ebenfalls 302. Eine Pruefung auf die blosse Zahl waere auch
|
|
dann gruen, wenn die Sitzung des Modi gar nicht erkannt wuerde --
|
|
sie haette dann die Anmeldung gemessen und nicht die Schranke.
|
|
Unterscheidbar sind die beiden nur am Ziel:
|
|
|
|
nicht angemeldet -> /workspace/
|
|
angemeldet, aber gesperrt -> /workspace/start.html
|
|
===================================================================== */
|
|
const seiteCreator = await roh("/workspace/anleitung.html", null, null, kekse.creator);
|
|
ok(seiteCreator.code === 200, `anleitung.html oeffnet fuer den Creator (${seiteCreator.code})`);
|
|
|
|
const seiteModi = await roh("/workspace/anleitung.html", CREW_HOST, null, kekse.modi);
|
|
ok(seiteModi.code === 302 && seiteModi.kopf.location === "/workspace/start.html",
|
|
`ein angemeldeter Modi wird zur Startseite umgeleitet `
|
|
+ `(${seiteModi.code} -> ${seiteModi.kopf.location})`);
|
|
|
|
const seiteOhne = await roh("/workspace/anleitung.html", null, null, null);
|
|
ok(seiteOhne.code === 302 && seiteOhne.kopf.location === "/workspace/",
|
|
`ohne Anmeldung zur Anmeldung, nicht zur Startseite `
|
|
+ `(${seiteOhne.code} -> ${seiteOhne.kopf.location})`);
|
|
|
|
/* GEGENPROBE ZU DEN BEIDEN: Sie unterscheiden wirklich. Zeigten beide
|
|
Umleitungen auf dasselbe, waeren die Zeilen oben zusammen trotzdem
|
|
gruen zu bekommen -- und die Schranke waere ungeprueft. */
|
|
ok(seiteModi.kopf.location !== seiteOhne.kopf.location,
|
|
"Gegenprobe: gesperrt und nicht angemeldet fuehren an verschiedene Orte");
|
|
|
|
/* =====================================================================
|
|
DIE ERSTBEFUELLUNG DARF NICHTS UEBERSCHREIBEN
|
|
=====================================================================
|
|
Die wichtigste Zeile in anleitung-tabellen.js: Eingespielt wird nur,
|
|
wenn fuer eine Rolle noch gar nichts da ist. Liefe sie bei jedem
|
|
Start, waere jede Aenderung aus der Pflege-Ansicht beim naechsten
|
|
Neustart weg -- lautlos, und man merkte es Wochen spaeter. */
|
|
melde("\n=== Ein zweiter Lauf laesst Aenderungen in Ruhe ===");
|
|
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
d2.prepare("UPDATE anl_karte SET merke = ? WHERE haus='agentur' AND rolle='creator' AND kachel='chat'")
|
|
.run("VON HAND GEAENDERT");
|
|
const { anleitungTabellen } = await import("./anleitung-tabellen.js");
|
|
anleitungTabellen(d2);
|
|
const nachher = d2.prepare(
|
|
"SELECT merke FROM anl_karte WHERE haus='agentur' AND rolle='creator' AND kachel='chat'").get();
|
|
const anzahlNachher = d2.prepare(
|
|
"SELECT COUNT(*) n FROM anl_karte WHERE haus='agentur' AND rolle='creator'").get().n;
|
|
d2.close();
|
|
ok(nachher?.merke === "VON HAND GEAENDERT",
|
|
`der geaenderte Text steht noch da ("${(nachher?.merke || "").slice(0, 24)}")`);
|
|
ok(anzahlNachher === 18, `und es sind keine Karten dazugekommen (${anzahlNachher})`);
|
|
|
|
/* =====================================================================
|
|
ALLE VIER FASSUNGEN — JEDE GEGEN IHRE EIGENEN KACHELN
|
|
=====================================================================
|
|
|
|
Oben ist der Creator Zeile fuer Zeile geprueft. Hier laufen alle vier
|
|
durch dieselbe Zuordnung, und die ZAHLEN WERDEN ABGELEITET, nicht
|
|
abgeschrieben: Wie viele Karten eine Rolle haben muss, sagt ihre
|
|
Kachelliste -- nicht die Abnahmeliste im Bauplan.
|
|
|
|
Der Bauplan nennt 18/21/22/23. Die stimmen heute. Am Tag, an dem
|
|
eine Kachel dazukommt, waere eine feste Zahl hier eine Luege, die
|
|
erst auffaellt, wenn jemand sie nachzaehlt. Also zaehlt die
|
|
Pruefung selbst -- und meldet die Zahl, damit man sie lesen kann.
|
|
===================================================================== */
|
|
melde("\n=== Jede Rolle erklaert genau ihre eigenen Kacheln ===");
|
|
const FASSUNGEN_HIER = ["creator", "scout", "manager", "spicy"];
|
|
for (const rolle of FASSUNGEN_HIER) {
|
|
const a = await holen(rolle);
|
|
if (a.code !== 200 || !a.j?.hat_fassung) {
|
|
ok(false, `${rolle.padEnd(8)} hat keine Fassung (HTTP ${a.code})`);
|
|
continue;
|
|
}
|
|
const soll = kachelnFuer(rolle);
|
|
soll.delete("anleitung"); // die Seite erklaert sich nicht selbst
|
|
const ist = new Set(a.j.karten.map((k) => k.kachel));
|
|
const fehlt = [...soll.keys()].filter((x) => !ist.has(x));
|
|
const zuviel = [...ist].filter((x) => !soll.has(x));
|
|
|
|
ok(soll.size > 0 && fehlt.length === 0 && zuviel.length === 0,
|
|
fehlt.length || zuviel.length
|
|
? `${rolle.padEnd(8)} stimmt nicht -- ohne Karte: ${fehlt.join(", ") || "nichts"}; `
|
|
+ `ohne Kachel: ${zuviel.join(", ") || "nichts"}`
|
|
: `${rolle.padEnd(8)} ${soll.size} Kacheln, ${ist.size} Karten, eins zu eins`);
|
|
|
|
/* Jede Karte vollstaendig. Eine halbe Karte sieht aus wie ein
|
|
Ladefehler, und zwar auf jeder der vier Seiten gleich. */
|
|
const halb = a.j.karten.filter((k) => !k.wozu || !k.tust || !k.merke);
|
|
ok(halb.length === 0,
|
|
halb.length ? `${rolle.padEnd(8)} ${halb.length} unvollstaendige Karten: `
|
|
+ halb.map((k) => k.kachel).join(", ")
|
|
: `${rolle.padEnd(8)} jede Karte hat alle drei Zeilen`);
|
|
|
|
/* Die Stufen -- gezaehlt, nicht vorgegeben. */
|
|
const je = {};
|
|
for (const k of a.j.karten) je[k.stufe] = (je[k.stufe] || 0) + 1;
|
|
const summe = Object.values(je).reduce((x, y) => x + y, 0);
|
|
ok(summe === a.j.karten.length
|
|
&& Object.keys(je).every((x) => STUFEN_DA.has(x)),
|
|
`${rolle.padEnd(8)} Stufen ${je.muss || 0}/${je.regelmaessig || 0}/${je.bedarf || 0} `
|
|
+ `(zusammen ${summe} von ${a.j.karten.length})`);
|
|
|
|
/* Die Ersten Schritte duerfen nur auf Kacheln dieser Rolle zeigen --
|
|
oder auf gar nichts ("Installieren", "Kopfleiste" sind Knoepfe in
|
|
der Kopfleiste, keine Seiten). */
|
|
const alleKacheln = kachelnFuer(rolle);
|
|
const ins_leere = (a.j.schritte || []).filter((x) => x.ziel)
|
|
.filter((x) => !alleKacheln.has(B.schluesselVonZiel(x.ziel)));
|
|
ok((a.j.schritte || []).length === 5 && ins_leere.length === 0,
|
|
ins_leere.length
|
|
? `${rolle.padEnd(8)} Schritte ins Leere: ${ins_leere.map((x) => x.ziel).join(", ")}`
|
|
: `${rolle.padEnd(8)} fuenf Erste Schritte, alle mit gueltigem Ziel`);
|
|
|
|
/* Und der Rest der Fassung ist da. Eine Rolle ohne Rhythmus oder
|
|
ohne "Nicht deins" waere eine halbe Einweisung. */
|
|
ok(!!a.j.leitgedanke && (a.j.saetze || []).length === 3
|
|
&& (a.j.nicht || []).length >= 3 && (a.j.takt || []).length >= 3
|
|
&& (a.j.fragen || []).length >= 3,
|
|
`${rolle.padEnd(8)} Leitgedanke, 3 Saetze, ${(a.j.nicht || []).length} x Nicht deins, `
|
|
+ `${(a.j.takt || []).length} Takt-Zeilen, ${(a.j.fragen || []).length} Fragen`);
|
|
}
|
|
|
|
/* GEGENPROBE ZUR SCHLEIFE: Sie darf nicht leer durchgelaufen sein.
|
|
Vier Rollen, und jede musste wirklich etwas liefern. */
|
|
ok(FASSUNGEN_HIER.length === 4, `vier Fassungen durchgegangen`);
|
|
|
|
/* Und: Die Fassungen muessen sich UNTERSCHEIDEN. Lieferte der Server
|
|
allen dieselbe Liste, waeren alle Zeilen oben trotzdem gruen --
|
|
solange die Kachellisten zufaellig passten. */
|
|
{
|
|
const koepfe = [];
|
|
for (const rolle of FASSUNGEN_HIER) {
|
|
const a = await holen(rolle);
|
|
koepfe.push(a.j?.leitgedanke || "");
|
|
}
|
|
ok(new Set(koepfe).size === 4,
|
|
`Gegenprobe: vier verschiedene Leitgedanken (${new Set(koepfe).size} verschiedene)`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
ETAPPE 2 — DIE ERSTEN SCHRITTE ZUM ABHAKEN
|
|
===================================================================== */
|
|
melde("\n=== Abhaken: der Anfangszustand ===");
|
|
const schritte0 = creator.j.schritte || [];
|
|
ok(schritte0.every((s) => Number.isInteger(s.id) && s.id > 0),
|
|
"jeder Schritt hat eine Nummer");
|
|
ok(schritte0.every((s) => s.erledigt === false),
|
|
"am Anfang ist kein Schritt abgehakt");
|
|
ok(creator.j.fortschritt?.erledigt === 0 && creator.j.fortschritt?.gesamt === 5,
|
|
`Fortschritt 0 von 5 (${creator.j.fortschritt?.erledigt} von ${creator.j.fortschritt?.gesamt})`);
|
|
ok(creator.j.darf_abhaken === true, "ein Creator darf seine eigenen Schritte abhaken");
|
|
|
|
const haken = (rolle, id, erledigt, host = null) =>
|
|
roh(`/workspace/api/anleitung/schritt/${id}`, host, { erledigt }, kekse[rolle], "PUT");
|
|
|
|
melde("\n=== Abhaken: setzen, zaehlen, wegnehmen ===");
|
|
const ersterId = schritte0[0].id;
|
|
const r1 = await haken("creator", ersterId, true);
|
|
let j1 = null; try { j1 = JSON.parse(r1.text); } catch { /* bleibt null */ }
|
|
ok(r1.code === 200 && j1?.erledigt === true, `Schritt abgehakt (HTTP ${r1.code})`);
|
|
ok(j1?.fortschritt?.erledigt === 1, `Fortschritt jetzt 1 von 5 (${j1?.fortschritt?.erledigt})`);
|
|
|
|
/* ZWEIMAL DASSELBE IST DERSELBE ZUSTAND. Bei wackeligem Mobilfunk
|
|
tippt man nach, und das darf nicht doppelt zaehlen. */
|
|
const r2 = await haken("creator", ersterId, true);
|
|
let j2 = null; try { j2 = JSON.parse(r2.text); } catch { /* bleibt null */ }
|
|
ok(r2.code === 200 && j2?.fortschritt?.erledigt === 1,
|
|
`zweimal abhaken zaehlt einmal (${j2?.fortschritt?.erledigt})`);
|
|
|
|
/* Und er haelt ueber einen neuen Abruf -- sonst stuende er nur im
|
|
Arbeitsspeicher der Antwort. */
|
|
const nachHaken = await holen("creator");
|
|
ok(nachHaken.j?.fortschritt?.erledigt === 1,
|
|
`der Haken steht auch beim naechsten Laden (${nachHaken.j?.fortschritt?.erledigt} von 5)`);
|
|
ok(nachHaken.j?.schritte?.find((s) => s.id === ersterId)?.erledigt === true,
|
|
"…und zwar an genau dem Schritt, der angetippt wurde");
|
|
|
|
const r3 = await haken("creator", ersterId, false);
|
|
let j3 = null; try { j3 = JSON.parse(r3.text); } catch { /* bleibt null */ }
|
|
ok(r3.code === 200 && j3?.fortschritt?.erledigt === 0,
|
|
`der Haken laesst sich wieder wegnehmen (${j3?.fortschritt?.erledigt})`);
|
|
|
|
melde("\n=== Abhaken: wer darf das nicht ===");
|
|
/* Ein Creator darf keinen Schritt abhaken, der es gar nicht ist --
|
|
sonst meldete der Fortschritt eines Tages 7 von 5. */
|
|
const eineFrage = await (async () => {
|
|
const d3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const z = d3.prepare("SELECT id FROM anl_zeile WHERE art = 'frage' LIMIT 1").get();
|
|
d3.close();
|
|
return z?.id;
|
|
})();
|
|
braucht(!!eineFrage, "keine Frage-Zeile gefunden, mit der sich das pruefen liesse");
|
|
const rFrage = await haken("creator", eineFrage, true);
|
|
ok(rFrage.code === 404,
|
|
`eine Frage laesst sich nicht abhaken (HTTP ${rFrage.code}, erwartet 404)`);
|
|
|
|
/* DogFather hat keine eigene Fassung -- also auch keine Schritte. */
|
|
const rDogi = await haken("admin", ersterId, true);
|
|
ok(rDogi.code === 404,
|
|
`DogFather kann einen Creator-Schritt nicht abhaken (HTTP ${rDogi.code}, erwartet 404)`);
|
|
const rModi = await haken("modi", ersterId, true, CREW_HOST);
|
|
ok(rModi.code === 404, `ein Modi auch nicht (HTTP ${rModi.code}, erwartet 404)`);
|
|
|
|
/* Unsinn als Nummer darf nicht durchrutschen. */
|
|
const rUnsinn = await roh("/workspace/api/anleitung/schritt/abc", null,
|
|
{ erledigt: true }, kekse.creator, "PUT");
|
|
ok(rUnsinn.code === 400 || rUnsinn.code === 404,
|
|
`keine Nummer -> abgelehnt (HTTP ${rUnsinn.code})`);
|
|
|
|
/* GEGENPROBE: Die Ablehnungen oben koennten auch davon kommen, dass
|
|
der Weg gar nicht existiert. Dass er funktioniert, ist oben bewiesen
|
|
-- hier wird es noch einmal unmittelbar daneben gezeigt. */
|
|
const rEcht = await haken("creator", schritte0[1].id, true);
|
|
ok(rEcht.code === 200,
|
|
"Gegenprobe: derselbe Weg nimmt einen gueltigen Schritt an");
|
|
await haken("creator", schritte0[1].id, false);
|
|
|
|
/* =====================================================================
|
|
DER HINWEIS „Erste Schritte noch offen"
|
|
===================================================================== */
|
|
melde("\n=== Der Hinweis ===");
|
|
async function hinweise(rolle, host = null) {
|
|
const r = await roh("/workspace/api/hinweise", host, null, kekse[rolle]);
|
|
let j = null; try { j = JSON.parse(r.text); } catch { /* bleibt null */ }
|
|
return { code: r.code, liste: j?.hinweise || [] };
|
|
}
|
|
|
|
const hCreator = await hinweise("creator");
|
|
const hSchritte = hCreator.liste.find((h) => h.art === "anl_schritte");
|
|
ok(hCreator.code === 200 && !!hSchritte,
|
|
`der Creator bekommt den Hinweis (HTTP ${hCreator.code})`);
|
|
ok(hSchritte?.anzahl === 5, `…mit allen fuenf offenen Schritten (${hSchritte?.anzahl})`);
|
|
ok(hSchritte?.ziel === "anleitung.html", `…und dem Weg zur Anleitung (${hSchritte?.ziel})`);
|
|
|
|
/* Die Crew darf den Hinweis NICHT bekommen -- die dritte Tuer, durch
|
|
die am 15.09.2026 ein Weg fuer die Community gerutscht ist. */
|
|
const hModi = await hinweise("modi", CREW_HOST);
|
|
/* DIE ANTWORT MUSS ANGEKOMMEN SEIN. Ohne `code === 200` waere diese
|
|
Zeile auch dann gruen, wenn die ganze Hinweis-Schnittstelle fuer den
|
|
Modi scheitert -- eine leere Liste sieht genauso aus wie "kein
|
|
Hinweis fuer ihn dabei". Dieselbe Falle wie am 03.09.2026 beim
|
|
Gitea-Abgleich: Eine Pruefung, die ihre Voraussetzung nicht hat,
|
|
darf nicht wie Erfolg aussehen. */
|
|
ok(hModi.code === 200 && !hModi.liste.some((h) => h.art === "anl_schritte"),
|
|
`ein Modi bekommt den Hinweis nicht (HTTP ${hModi.code}, `
|
|
+ `${hModi.liste.length} andere Hinweise)`);
|
|
const hDogi = await hinweise("admin");
|
|
ok(hDogi.code === 200 && !hDogi.liste.some((h) => h.art === "anl_schritte"),
|
|
`DogFather bekommt ihn auch nicht — er hat keine Einweisung `
|
|
+ `(HTTP ${hDogi.code}, ${hDogi.liste.length} andere Hinweise)`);
|
|
|
|
/* ---- Und er verschwindet von selbst ---- */
|
|
melde("\n=== Alles abgehakt: der Hinweis geht von allein ===");
|
|
for (const s of schritte0) await haken("creator", s.id, true);
|
|
const fertig = await holen("creator");
|
|
ok(fertig.j?.fortschritt?.erledigt === 5,
|
|
`alle fuenf abgehakt (${fertig.j?.fortschritt?.erledigt} von ${fertig.j?.fortschritt?.gesamt})`);
|
|
const hNachher = await hinweise("creator");
|
|
ok(!hNachher.liste.some((h) => h.art === "anl_schritte"),
|
|
"der Hinweis ist weg, ohne dass ihn jemand abgeschaltet hat");
|
|
|
|
/* GEGENPROBE: Er kommt wieder, wenn wieder etwas offen ist. Ohne das
|
|
waere „weg" auch dann gruen, wenn der Hinweis gar nicht mehr
|
|
entstehen KANN. */
|
|
await haken("creator", ersterId, false);
|
|
const hWieder = await hinweise("creator");
|
|
ok(hWieder.liste.some((h) => h.art === "anl_schritte" && h.anzahl === 1),
|
|
"Gegenprobe: ein wieder geoeffneter Schritt bringt ihn zurueck");
|
|
|
|
/* Aufraeumen, damit die Pruefungen darunter einen sauberen Stand
|
|
sehen -- der Haken auf Schritt 2 und die vier von eben. */
|
|
for (const s of schritte0) await haken("creator", s.id, false);
|
|
|
|
/* =====================================================================
|
|
ETAPPE 4 — DIE PFLEGE
|
|
===================================================================== */
|
|
melde("\n=== Pflege: wer darf ===");
|
|
const pput = (rolle, pfad, rumpf, host = null) =>
|
|
roh(pfad, host, rumpf, kekse[rolle], "PUT");
|
|
|
|
const eineKarte = (await holen("creator")).j.karten.find((k) => k.kachel === "chat");
|
|
braucht(!!eineKarte?.id, "keine Karte zum Bearbeiten gefunden");
|
|
|
|
const pCreator = await pput("creator", `/workspace/api/anleitung/karte/${eineKarte.id}`,
|
|
{ stufe: "bedarf", wozu: "x", tust: "y", merke: "z" });
|
|
ok(pCreator.code === 403, `ein Creator darf nicht pflegen (HTTP ${pCreator.code}, erwartet 403)`);
|
|
const pScout = await pput("scout", `/workspace/api/anleitung/karte/${eineKarte.id}`,
|
|
{ stufe: "bedarf", wozu: "x", tust: "y", merke: "z" });
|
|
ok(pScout.code === 403, `ein Scout auch nicht (HTTP ${pScout.code}, erwartet 403)`);
|
|
const pModi = await pput("modi", `/workspace/api/anleitung/karte/${eineKarte.id}`,
|
|
{ stufe: "bedarf", wozu: "x", tust: "y", merke: "z" }, CREW_HOST);
|
|
/* 403 STATT 404 (seit 06.10.2026): Die Seite gibt es auf der
|
|
Crew-Adresse jetzt -- pflegen darf ein Modi sie trotzdem nicht. */
|
|
ok(pModi.code === 403, `ein Modi darf nicht pflegen (HTTP ${pModi.code}, erwartet 403)`);
|
|
|
|
melde("\n=== Pflege: Spicy aendert eine Stufe, und sie gilt sofort ===");
|
|
const vorher = eineKarte.stufe;
|
|
const pSpicy = await pput("spicy", `/workspace/api/anleitung/karte/${eineKarte.id}`,
|
|
{ stufe: "bedarf", wozu: "Nachrichten mit deinem Team.",
|
|
tust: "Mit „Neu“ ein Gespräch beginnen.", merke: "GEAENDERT VON SPICY" });
|
|
ok(pSpicy.code === 200, `Spicy Media darf (HTTP ${pSpicy.code})`);
|
|
const nachSpicy = await holen("creator");
|
|
const chatNeu = nachSpicy.j.karten.find((k) => k.kachel === "chat");
|
|
ok(chatNeu?.stufe === "bedarf",
|
|
`die Stufe steht sofort in der Creator-Fassung (${vorher} -> ${chatNeu?.stufe})`);
|
|
ok(chatNeu?.merke === "GEAENDERT VON SPICY", "…und der Text auch");
|
|
|
|
/* DIE MARKE "NEU". Der Creator hat die Seite gerade geladen -- damit
|
|
ist sein Besuch juenger als die Aenderung davor, aber AELTER als die
|
|
von eben. Die Reihenfolge ist der ganze Beweis. */
|
|
ok(chatNeu?.neu === true, "die geaenderte Karte traegt die Marke „Neu“");
|
|
const nochmal = await holen("creator");
|
|
ok(nochmal.j.karten.find((k) => k.kachel === "chat")?.neu === false,
|
|
"…und beim zweiten Hinsehen nicht mehr");
|
|
|
|
/* Zurueck auf Anfang, damit die Zuordnungspruefung oben weiter stimmt. */
|
|
await pput("spicy", `/workspace/api/anleitung/karte/${eineKarte.id}`,
|
|
{ stufe: vorher, wozu: "Nachrichten mit deinem Team.",
|
|
tust: "Mit „Neu“ ein Gespräch beginnen.",
|
|
merke: "Fehler an der Seite gehören in Support, nicht in den Chat." });
|
|
|
|
melde("\n=== Pflege: was nicht durchgehen darf ===");
|
|
const pUnsinn = await pput("admin", `/workspace/api/anleitung/karte/${eineKarte.id}`,
|
|
{ stufe: "gibtesnicht", wozu: "", tust: "", merke: "" });
|
|
ok(pUnsinn.code === 400, `eine erfundene Stufe wird abgelehnt (HTTP ${pUnsinn.code})`);
|
|
const pWeg = await pput("admin", "/workspace/api/anleitung/karte/999999",
|
|
{ stufe: "muss", wozu: "", tust: "", merke: "" });
|
|
ok(pWeg.code === 404, `eine Karte, die es nicht gibt (HTTP ${pWeg.code})`);
|
|
|
|
melde("\n=== Pflege: fehlenden Eintrag anlegen und wieder entfernen ===");
|
|
/* Erst loeschen, dann ist er "fehlend" -- genau der Fall, den der
|
|
Bauplan "Fuer diese Kachel fehlt ein Eintrag" nennt. */
|
|
const weg = await roh(`/workspace/api/anleitung/karte/${eineKarte.id}`, null, null,
|
|
kekse.admin, "DELETE");
|
|
ok(weg.code === 200, `DogFather kann eine Karte entfernen (HTTP ${weg.code})`);
|
|
const ohne = await holen("creator");
|
|
ok(ohne.j.karten.length === 17 && !ohne.j.karten.some((k) => k.kachel === "chat"),
|
|
`danach 17 Karten, die Chat-Karte fehlt (${ohne.j.karten.length})`);
|
|
|
|
const neu1 = await roh("/workspace/api/anleitung/karte", null,
|
|
{ rolle: "creator", kachel: "chat", stufe: "muss",
|
|
wozu: "Nachrichten mit deinem Team.", tust: "Mit „Neu“ ein Gespräch beginnen.",
|
|
merke: "Fehler an der Seite gehören in Support, nicht in den Chat." },
|
|
kekse.admin, "POST");
|
|
ok(neu1.code === 200, `…und wieder anlegen (HTTP ${neu1.code})`);
|
|
const neu2 = await roh("/workspace/api/anleitung/karte", null,
|
|
{ rolle: "creator", kachel: "chat", stufe: "muss", wozu: "", tust: "", merke: "" },
|
|
kekse.admin, "POST");
|
|
ok(neu2.code === 409,
|
|
`ein zweites Mal nicht -- eine Kachel hat genau eine Karte (HTTP ${neu2.code})`);
|
|
const zurueck = await holen("creator");
|
|
ok(zurueck.j.karten.length === 18,
|
|
`wieder 18 Karten (${zurueck.j.karten.length})`);
|
|
|
|
melde("\n=== Pflege: eine Zeile aendern ===");
|
|
const einSchritt = zurueck.j.schritte[0];
|
|
const zAend = await pput("admin", `/workspace/api/anleitung/zeile/${einSchritt.id}`,
|
|
{ titel: einSchritt.titel, text: "GEAENDERTER TEXT", ziel: einSchritt.ziel });
|
|
ok(zAend.code === 200, `eine Listenzeile laesst sich aendern (HTTP ${zAend.code})`);
|
|
const nachZeile = await holen("creator");
|
|
ok(nachZeile.j.schritte[0]?.text === "GEAENDERTER TEXT", "…und der Text steht da");
|
|
await pput("admin", `/workspace/api/anleitung/zeile/${einSchritt.id}`,
|
|
{ titel: einSchritt.titel, text: einSchritt.text, ziel: einSchritt.ziel });
|
|
|
|
melde("\n=== Pflege: wer ist wie weit ===");
|
|
const standR = await roh("/workspace/api/anleitung/stand", null, null, kekse.admin);
|
|
let standJ = null; try { standJ = JSON.parse(standR.text); } catch { /* bleibt null */ }
|
|
ok(standR.code === 200 && Array.isArray(standJ?.leute),
|
|
`DogFather sieht den Stand (HTTP ${standR.code}, ${standJ?.leute?.length ?? 0} Leute)`);
|
|
ok((standJ?.leute || []).some((p) => p.rolle === "creator" && p.gesamt === 5),
|
|
"…und der Creator steht mit fuenf Schritten drin");
|
|
const standC = await roh("/workspace/api/anleitung/stand", null, null, kekse.creator);
|
|
ok(standC.code === 403, `ein Creator sieht ihn nicht (HTTP ${standC.code}, erwartet 403)`);
|
|
|
|
/* =====================================================================
|
|
ETAPPE 5 — DIE SUCHE
|
|
===================================================================== */
|
|
melde("\n=== Suche ===");
|
|
async function suchen(rolle, wort, host = null) {
|
|
const r = await roh(`/workspace/api/suche?q=${encodeURIComponent(wort)}`, host, null, kekse[rolle]);
|
|
let j = null; try { j = JSON.parse(r.text); } catch { /* bleibt null */ }
|
|
const g = (j?.gruppen || []).find((x) => x.titel === "Anleitung");
|
|
return { code: r.code, treffer: g?.treffer || [], gruppen: (j?.gruppen || []).length };
|
|
}
|
|
|
|
/* Ein Wort, das NUR in der Creator-Fassung steht. */
|
|
const sCreator = await suchen("creator", "Jemand schaut noch drauf");
|
|
ok(sCreator.code === 200 && sCreator.treffer.length > 0,
|
|
`der Creator findet seine eigene Karte (${sCreator.treffer.length} Treffer)`);
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. `every()` liefert auf einer LEEREN
|
|
Liste `true` -- diese Zeile war beim ersten Lauf gruen, ohne einen
|
|
einzigen Treffer angesehen zu haben. Genau der Fehler vom 01.09.2026
|
|
im Shop, nur mit anderen Namen. */
|
|
ok(sCreator.treffer.length > 0
|
|
&& sCreator.treffer.every((t) => t.ziel && !t.ziel.includes("undefined")),
|
|
`…und jeder der ${sCreator.treffer.length} Treffer hat ein Ziel `
|
|
+ `(${sCreator.treffer.map((t) => t.ziel).join(", ") || "keine"})`);
|
|
|
|
/* Ein Wort, das NUR in der Manager-Fassung steht. */
|
|
const sFremd = await suchen("creator", "Zuteilen, freigeben");
|
|
ok(sFremd.treffer.length === 0,
|
|
`der Creator findet die Manager-Karte NICHT (${sFremd.treffer.length} Treffer)`);
|
|
const sManager = await suchen("manager", "Zuteilen, freigeben");
|
|
ok(sManager.treffer.length > 0,
|
|
`Gegenprobe: der Manager findet sie sehr wohl (${sManager.treffer.length} Treffer)`);
|
|
|
|
const sModi = await suchen("modi", "Freigabe heißt", CREW_HOST);
|
|
ok(sModi.code === 200 && sModi.treffer.length === 0,
|
|
`ein Modi findet nichts davon (HTTP ${sModi.code}, ${sModi.treffer.length} Treffer)`);
|
|
|
|
/* Die Adresse im Treffer muss den Zusatz hinter dem Fragezeichen
|
|
tragen -- sonst landen fuenf Karten auf derselben Seite. */
|
|
const sBereich = await suchen("creator", "Ruhig statt Drama");
|
|
ok(sBereich.treffer.some((t) => t.ziel === "bereich.html?b=community"),
|
|
`ein Bereichs-Treffer traegt seinen Zusatz (${sBereich.treffer.map((t) => t.ziel).join(", ")})`);
|
|
|
|
/* =====================================================================
|
|
ETAPPE 6 — BEGRUESSUNG UND RUNDGANG
|
|
===================================================================== */
|
|
melde("\n=== Einweisung beim ersten Anmelden ===");
|
|
async function einweisung(rolle, host = null) {
|
|
const r = await roh("/workspace/api/anleitung/einweisung", host, null, kekse[rolle]);
|
|
let j = null; try { j = JSON.parse(r.text); } catch { /* bleibt null */ }
|
|
return { code: r.code, j };
|
|
}
|
|
|
|
const e1 = await einweisung("creator");
|
|
ok(e1.code === 200 && e1.j?.zeigen === true,
|
|
`beim ersten Mal wird sie gezeigt (HTTP ${e1.code}, zeigen ${e1.j?.zeigen})`);
|
|
ok(Array.isArray(e1.j?.muss) && e1.j.muss.length === 5,
|
|
`…mit den fuenf Muss-Kacheln (${e1.j?.muss?.length})`);
|
|
|
|
const gesehen = await roh("/workspace/api/anleitung/einweisung/gesehen", null, {}, kekse.creator,
|
|
"POST");
|
|
ok(gesehen.code === 200, `„gesehen“ laesst sich merken (HTTP ${gesehen.code})`);
|
|
const e2 = await einweisung("creator");
|
|
ok(e2.j?.zeigen === false, "beim zweiten Mal nicht mehr");
|
|
|
|
/* DIE CREW BEKOMMT 200 UND NICHT 404. Das Skript laeuft auf JEDER
|
|
Startseite; ein Fehlschlag waere eine rote Zeile in der Konsole bei
|
|
jedem Aufruf -- und die verdeckt die naechste, die echt ist. */
|
|
/* Seit dem 06.10. hat der Modi eine Fassung -- also soll die
|
|
Einweisung bei ihm auch ANGEBOTEN werden. Vorher war die ruhige
|
|
Absage richtig, weil es nichts einzuweisen gab. */
|
|
const eModi = await einweisung("modi", CREW_HOST);
|
|
ok(eModi.code === 200 && eModi.j?.zeigen === true,
|
|
`ein Modi bekommt seine Einweisung angeboten (HTTP ${eModi.code}, zeigen ${eModi.j?.zeigen})`);
|
|
const eDogi = await einweisung("admin");
|
|
ok(eDogi.code === 200 && eDogi.j?.zeigen === false,
|
|
`DogFather auch -- er braucht keine Einweisung (zeigen ${eDogi.j?.zeigen})`);
|
|
|
|
/* DER ROLLENWECHSEL. Der Bauplan verlangt, dass die Einweisung dann
|
|
einmal neu startet. Gebaut ist das NICHT als Sonderregel, sondern
|
|
ueber den Schluessel (Person, Rolle) -- die neue Rolle hat schlicht
|
|
noch keine Zeile. Genau das wird hier nachgemessen. */
|
|
{
|
|
const d4 = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
d4.prepare("UPDATE personen SET rolle = 'scout' WHERE rolle = 'creator'").run();
|
|
d4.close();
|
|
const neuAn = await anmelden("scout");
|
|
const alt = kekse.scout;
|
|
kekse.scout = neuAn || alt;
|
|
const eNach = await einweisung("scout");
|
|
kekse.scout = alt;
|
|
ok(eNach.j?.zeigen === true,
|
|
`nach einem Rollenwechsel startet sie einmal neu (zeigen ${eNach.j?.zeigen})`);
|
|
const d5 = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
d5.prepare("UPDATE personen SET rolle = 'creator' WHERE name = 'Luna'").run();
|
|
d5.close();
|
|
}
|
|
|
|
/* ---------- Abschluss ------------------------------------------------ */
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`);
|
|
process.exit(fehler ? 1 : 0);
|