/* ===================================================================== DER CREW — die Community und ihre sieben Bretter (11.09.2026) Filipe: "eine community rolle und community kategorie. wo die community auch dan nur die sieht und die modis rechte hand und ich also dogfather sehen die aber auch. die community aber nur die dann." "wieso nur eine kachel fuer die community ich will dass die mehrere kacheln haben. mit mehreren optionen." --------------------------------------------------------------------- DIESE PRUEFUNG MISST IN BEIDE RICHTUNGEN, UND DAS IST DER GANZE PUNKT Eine Trennung kann auf zwei Arten falsch sein, und die zweite ist die gefaehrlichere: 1. SIE GREIFT NICHT -- die Community sieht Dinge des Hauses. Das faellt auf, wenn jemand hinsieht. 2. SIE GREIFT ZU WEIT -- die Community sieht ihren eigenen Raum nicht. Das faellt NICHT auf: Ein leeres Brett sieht aus wie "hier ist nichts los", nicht wie ein Fehler. Genau so ist am 01.09. ein Manager vor einem leeren Aufgabenbrett gestanden und hat es nicht gemeldet. Deshalb steht zu jeder Zeile "sieht es nicht" eine Zeile "und der da sieht es sehr wohl". Ohne die zweite waere die erste auch dann gruen, wenn schlicht gar nichts mehr geht. --------------------------------------------------------------------- WARUM node:http UND NICHT fetch Das Team und die Community melden sich an verschiedenen Waenden an, und welche Wand gilt, entscheidet der Host-Kopf. `fetch` in Node setzt den selbst und ignoriert einen gesetzten -- die Anmeldung ginge dann an die falsche Wand und scheiterte, ohne dass man saehe warum. Dieselbe Falle wie am 10.09. bei den Kategorie-Kanaelen. ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { request as httpAnfrage } from "node:http"; import { portMussFreiSein } from "./helfer-port.mjs"; const PORT = await portMussFreiSein(4398, "pruef-treff"); const ordner = mkdtempSync(join(tmpdir(), "ws-treff-")); 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(240_000, "pruef-treff"); await new Promise((r) => setTimeout(r, 700)); process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); 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++; }; const CREW = "crew.dogfather-universe.com"; const HAUS = "workspace.dogfather-universe.com"; const HAUPT = "dogfather-universe.com"; const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const w = await import("./workspace.js"); const treffModul = await import("./workspace-treff.js"); const { BEREICHE } = await import("./workspace-bereiche.js"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const schluessel = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", schluessel, jetzt); 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", schluessel).update(code).digest("hex"), jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); anlegen("Rieke", "hand", "CODE-HAND-0001"); anlegen("Frida", "modi", "CODE-MODI-0001"); anlegen("Mara", "manager", "CODE-MANA-0001"); anlegen("Luna", "creator", "CODE-CREA-0001"); const idGast = anlegen("Kessi", "gast", "CODE-GAST-0001"); /* EIN EINTRAG IN JEDEM DER SIEBEN BRETTER. Ohne Daten waere jede Zeile "sieht nichts" trivial wahr -- der gruene Haken ueber der Null. */ const rein = d.prepare( "INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)" + " VALUES (?,?,?,?,?,?,?)"); const BRETTER = ["anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln", "mitmachen"]; for (const b of BRETTER) rein.run(b, Object.keys((await import("./workspace-bereiche.js")).BEREICHE[b].arten)[0], `CREW-PROBE-${b.toUpperCase()}`, "", jetzt.slice(0, 10), jetzt, idDogi); /* Und einer im Haus, den die Community NICHT sehen darf. */ rein.run("ideen", "content", "HAUS-GEHEIM-4711", "", jetzt.slice(0, 10), jetzt, idDogi); d.close(); /* Die Methode ist ein eigenes Argument, seit es Wege gibt, die PUT oder DELETE heissen. Vorher wurde sie aus "gibt es einen Koerper?" erraten -- das ging, solange nur GET und POST vorkamen, und haette beim ersten PUT stillschweigend ein POST geschickt. Der Server haette mit 404 geantwortet, und man sucht den Fehler im Server. */ function roh(pfad, host, kopf = {}, koerper = null, methode = null) { return new Promise((fertig, schief) => { const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode || (koerper ? "POST" : "GET"), headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf }, }, (antwort) => { let text = ""; antwort.on("data", (s) => { text += s; }); antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); }); a.on("error", schief); if (koerper) a.write(JSON.stringify(koerper)); a.end(); }); } async function anmelden(rolle, code, host) { const r = await roh("/workspace/api/anmelden", host, {}, { rolle, code }); const kekse = [].concat(r.kopf?.["set-cookie"] || []); return { keks: kekse.map((c) => c.split(";")[0]).join("; "), code: r.code }; } /* ======================================================================= 1. DIE WAND: die Community kommt auf genau eine Adresse ======================================================================= */ melde(""); melde("=== 1. Eine Rolle, eine Wand ==="); const gast = await anmelden("gast", "CODE-GAST-0001", CREW); ok(gast.code === 200 && gast.keks.length > 0, `die Community kommt auf crew. herein (${gast.code})`); /* AUF DEN ALTEN ADRESSEN NICHT -- und zwar mit derselben Antwort wie ein erfundener Code. Ein anderer Fehlercode waere die Auskunft "diesen Zugang gibt es, nur nicht hier", und wer das liest, sucht weiter. */ for (const [name, host] of [["workspace.", HAUS], ["der Hauptseite", HAUPT]]) { const a = await anmelden("gast", "CODE-GAST-0001", host); const b = await anmelden("gast", "VOELLIG-ERFUNDEN-XYZ", host); /* NICHT AUF EINEN BESTIMMTEN CODE FESTLEGEN (gemessen 11.09.2026). Hier stand `=== 401`. Auf workspace. stimmt das; auf der Hauptseite antwortet der Server 410 -- der Workspace ist von dort weggezogen, und "Gone" ist die richtige Antwort fuer JEDEN, der es dort noch versucht. Die Pruefung war rot, obwohl genau das Richtige passierte. Gemessen wird jetzt, was die Zeile eigentlich behaupten will: Er kommt nicht herein, und er kann an der Antwort nicht ablesen, dass es seinen Zugang gibt. */ ok(a.code !== 200 && a.keks === "", `auf ${name} nicht (${a.code})`); ok(a.code === b.code, `und zwar mit derselben Antwort wie ein erfundener Code (${a.code}/${b.code})`); } { /* DIE GEGENPROBE: Auf derselben Wand kommt auch das Team herein -- sonst waere "die Community kommt auf crew. herein" auch dann gruen, wenn dort ueberhaupt niemand mehr durchkaeme. */ const a = await anmelden("admin", "CODE-DOGI-0001", CREW); const b = await anmelden("modi", "CODE-MODI-0001", CREW); ok(a.code === 200 && b.code === 200, `und das Team ebenfalls -- eine Wand fuer beide (${a.code}/${b.code})`); /* UND DIE AGENTUR WEITERHIN NICHT. Dass der Treff hier eingezogen ist, hat daran nichts geaendert -- und genau das ist die Stelle, an der ein Umbau so etwas versehentlich aufmacht. */ const c = await anmelden("manager", "CODE-MANA-0001", CREW); const d = await anmelden("creator", "CODE-CREA-0001", CREW); ok(c.code !== 200 && d.code !== 200, `eine Managerin und eine Creatorin aber nicht (${c.code}/${d.code})`); } const kDogi = (await anmelden("admin", "CODE-DOGI-0001", HAUS)).keks; const kModi = (await anmelden("creator", "CODE-MODI-0001", CREW)).keks; const kCrea = (await anmelden("creator", "CODE-CREA-0001", HAUS)).keks; const kMana = (await anmelden("manager", "CODE-MANA-0001", HAUS)).keks; ok([kDogi, kModi, kCrea, kMana].every((k) => k.length > 0), "die vier Vergleichsrollen sind angemeldet"); /* ======================================================================= 1b. EINE WAND FÜR BEIDE — und keine dritte mehr Bis zum Mittag des 11.09.2026 hatte der Treff eine eigene Adresse, eine eigene Wand und ein eigenes Manifest. Filipe hat das verworfen: *"das soll keine app für sich sein, das soll in der crew seite adaptiert werden."* Was dieser Abschnitt jetzt misst, ist deshalb das Gegenteil von vorher — und er misst es, weil ein halber Rückbau die gefährlichste Lage wäre: eine Wand, die es nicht mehr geben soll, die aber noch als Datei herumliegt und von express.static ausgeliefert wird. ======================================================================= */ melde(""); melde("=== 1b. Eine Wand für beide ==="); { /* DIE DATEIEN DER DRITTEN ADRESSE SIND WEG. Nicht "nicht mehr verlinkt" -- weg. Eine Zugangswand, die niemand mehr meint, die aber unter ihrem Dateinamen weiterhin herumliegt, ist genau die Sorte Rest, die man ein halbes Jahr spaeter findet. GEFRAGT WIRD DIE PLATTE, NICHT DER SERVER (gemessen 11.09.2026). Ueber HTTP antwortet eine geloeschte Seite mit 302 auf die Startseite -- weil die Schranke davor greift, bevor express.static ueberhaupt nachsieht. Das ist im Betrieb genau richtig (eine Seite, die es nicht gibt, verhaelt sich wie eine, die man nicht darf), macht die Frage "gibt es die Datei noch?" per HTTP aber unbeantwortbar: 302 kommt in beiden Faellen. Wer hier 404 erwartet, prueft die Schranke und nennt es Dateipruefung. */ { const { existsSync } = await import("node:fs"); const { fileURLToPath } = await import("node:url"); /* fileURLToPath statt .pathname mit Handarbeit: Unter Windows liefert .pathname "/C:/..." -- mit einem fuehrenden Schraegstrich, den existsSync nicht mag. Wer den mit einem Muster wegschneidet, hat eine Zeile, die auf Linux nichts tut und auf Windows alles entscheidet. Der eingebaute Weg kennt beide Faelle. */ const ordner = fileURLToPath(new URL("../workspace/", import.meta.url)); const reste = ["treff-index.html", "treff.webmanifest", "assets/img/buehne/treff-gate-1280.webp"] .filter((f) => existsSync(ordner + f)); ok(reste.length === 0, `von der dritten Adresse liegt nichts mehr herum${reste.length ? " -- REST: " + reste.join(", ") : ""}`); /* GEGENPROBE: Die Pruefung sieht ueberhaupt an der richtigen Stelle nach. Ohne sie waere die Zeile darueber auch bei einem falschen Pfad gruen -- der gruene Haken ueber der Null. */ ok(existsSync(ordner + "crew-index.html"), "und die Wand von Team Dogi liegt dort, wo gesucht wurde"); } /* DIE WAND ZEIGT VIER KACHELN. Die Zahl steht nicht als Zahl da, sondern kommt aus der Rollenliste des Servers -- wer eine fünfte Rolle auf diese Wand lässt, ohne eine Kachel zu bauen, bekommt hier eine rote Zeile statt eines stummen Lochs. */ const wand = await roh("/workspace/", CREW); ok(wand.code === 200 && /Team Dogi|Team Dogi/.test(wand.text), `die Wand von Team Dogi steht (${wand.code})`); const kacheln = [...wand.text.matchAll(/data-rolle="([a-z]+)"/g)].map((m) => m[1]); ok(kacheln.length === 4 && kacheln.includes("gast"), `vier Kacheln, eine davon die Community: ${kacheln.join(", ")}`); ok(kacheln[kacheln.length - 1] === "gast", "und sie steht zuletzt -- wer täglich kommt, findet oben, was er täglich braucht"); /* DER TEXT AUF DER WAND MUSS FÜR BEIDE STIMMEN. Er stand auf "Privater Arbeitsbereich für DogFather und die Modis" -- das liest ab heute auch die Community, und für sie ist es schlicht falsch. */ ok(!/Arbeitsbereich für DogFather und die Modis/.test(wand.text), "und der Untertitel behauptet nicht mehr, das hier sei nur für das Team"); /* DAS MANIFEST GILT JE ADRESSE und kann sich nicht nach der Rolle richten. Filipes Entscheidung: ein Name, der für beide passt. */ const man = await roh("/workspace/app.webmanifest", CREW); let name = "(kein JSON)"; try { name = JSON.parse(man.text).name; } catch { /* bleibt stehen */ } ok(name === "DogFather Universe", `die App heißt "${name}" -- für Team und Community`); const manHaus = await roh("/workspace/app.webmanifest", HAUS); let nameHaus = "(kein JSON)"; try { nameHaus = JSON.parse(manHaus.text).name; } catch { /* bleibt stehen */ } ok(nameHaus !== name, `und auf der Agenturadresse weiterhin etwas anderes ("${nameHaus}")`); /* DIE WAND VON TEAM DOGI BLEIBT AUF DEN ALTEN ADRESSEN UNSICHTBAR. 404 und keine Weiterleitung: "Gibt es nicht" verrät nichts. */ const fremd = await roh("/workspace/crew-index.html", HAUS); const schraeg = await roh("/workspace//crew-index.html", HAUS); ok(fremd.code === 404 && schraeg.code === 404, `die Wand gibt es auf workspace. nicht -- auch mit doppeltem Schrägstrich (${fremd.code}/${schraeg.code})`); /* NICHT INDIZIEREN -- als Kopf und nicht nur als , damit er auch für Manifest, Bilder und jede Antwort ohne HTML gilt. */ ok(/noindex/i.test(wand.kopf?.["x-robots-tag"] || ""), `der Kopf sagt noindex (${wand.kopf?.["x-robots-tag"]})`); } /* ======================================================================= 2. DIE KACHELN: sieben, und sonst nichts ======================================================================= */ melde(""); melde("=== 2. Sieben Kacheln — und keine achte ==="); let namen = []; { const a = await roh("/workspace/api/ich", CREW, { cookie: gast.keks }); const j = JSON.parse(a.text); ok(Array.isArray(j.bereiche) && j.bereiche.length === 7, `die Community bekommt sieben Kacheln (${j.bereiche?.length})`); /* Die Namen werden im naechsten Block noch einmal gebraucht -- dort wird gegen sie geprueft, statt sie ein zweites Mal zu tippen. Zwei Abschriften derselben sieben Namen waeren zwei Gelegenheiten, einen davon zu aendern und den anderen zu vergessen. */ namen = (j.bereiche || []).map((k) => k.name); ok(namen.join(" | ") === "Anschlagbrett | Was ansteht | Der Treff | Wunschliste | Highlights | Regeln & Hilfe | Mitmachen", `und zwar diese: ${namen.join(", ")}`); ok((j.bereiche || []).every((k) => k.gruppe === "Community"), "alle in der Gruppe „Community“"); const toene = new Set((j.bereiche || []).map((k) => k.ton)); ok(toene.size === 7, `sieben verschiedene Farben (${[...toene].join(", ")})`); ok(j.rolle_name === "Community", `die Rolle heißt „${j.rolle_name}“`); } { /* GEGENPROBE: Das Team sieht sie mit -- sonst waere "sieben Kacheln" auch dann gruen, wenn sie sonst niemand bekommt. UND ES SIEHT EINE MEHR (11.09.2026): "Meldungen & Massnahmen". Sie ist ausdruecklich KEIN Brett und steht deshalb nicht in TREFF_BEREICHE -- sonst waere sie bei der Community gelandet, und zwar lautlos: Sie saehe dort aus wie die anderen sieben, und erst beim Klicken haette der Server 404 gesagt. Geprueft wird deshalb nicht eine Zahl, sondern die BEZIEHUNG: Die sieben der Community sind eine echte Teilmenge dessen, was das Team sieht -- und der Unterschied ist genau die Moderationskachel. Eine Zahl waere beim naechsten Umbau wieder rot, ohne dass etwas kaputt ist; diese Bedingung nicht. */ const MOD = "Meldungen & Ma\u00dfnahmen"; const a = JSON.parse((await roh("/workspace/api/ich", CREW, { cookie: kModi })).text); const beimModi = (a.bereiche || []).filter((k) => k.gruppe === "Community").map((k) => k.name); const fehlt = namen.filter((n) => !beimModi.includes(n)); ok(fehlt.length === 0, `ein Modi sieht alle sieben der Community mit${fehlt.length ? " -- FEHLT: " + fehlt.join(", ") : ""}`); ok(beimModi.includes(MOD) && !namen.includes(MOD), `und genau eine mehr: ${beimModi.filter((n) => !namen.includes(n)).join(", ")}`); const b = JSON.parse((await roh("/workspace/api/ich", HAUS, { cookie: kDogi })).text); const beiDogi = (b.bereiche_zusatz || []).filter((k) => k.gruppe === "Community").map((k) => k.name); ok(namen.every((n) => beiDogi.includes(n)) && beiDogi.includes(MOD), `und DogFather auf seiner Adresse auch (${beiDogi.length})`); const c = JSON.parse((await roh("/workspace/api/ich", HAUS, { cookie: kMana })).text); const beiMana = (c.bereiche_zusatz || []).filter((k) => k.gruppe === "Community"); ok(beiMana.length === 0, `eine Managerin nicht (${beiMana.length})`); /* UND DIE MODERATIONSKACHEL IST BEI DER COMMUNITY WIRKLICH NICHT DA -- nicht nur "nicht in dieser Liste". Die Kachel ist ein Weg; wenn der Weg zu ist, muss auch die Tuer zu sein. */ const d = JSON.parse((await roh("/workspace/api/ich", CREW, { cookie: gast.keks })).text); const alleNamen = [...(d.bereiche || []), ...(d.bereiche_zusatz || [])].map((k) => k.name); ok(!alleNamen.includes(MOD), `die Community hat keine Moderationskachel (${alleNamen.length} Kacheln)`); } /* ======================================================================= 3. DIE BRETTER: was die Community sieht — und was nicht ======================================================================= */ melde(""); melde("=== 3. Die sieben Bretter ==="); async function brett(b, host, keks) { const a = await roh(`/workspace/api/bereich/${b}`, host, { cookie: keks }); let titel = []; try { titel = (JSON.parse(a.text).eintraege || []).map((e) => e.titel); } catch { /* kein JSON */ } return { code: a.code, titel }; } { let gefunden = 0; for (const b of BRETTER) { const r = await brett(b, CREW, gast.keks); if (r.code === 200 && r.titel.includes(`CREW-PROBE-${b.toUpperCase()}`)) gefunden++; } /* FUENF VON SIEBEN, UND DAS IST DAS ERGEBNIS -- kein Mangel. Termine und Highlights sind fuer die Community erst da, wenn jemand aus dem Team sie freigegeben hat (TREFF_FREIGABE_BRETTER). Die Zahl steht deshalb nicht als 7 hier, sondern wird aus der Tafel gerechnet: Wer ein drittes Freigabe-Brett anlegt, bekommt hier keine rote Zeile fuer etwas Richtiges. */ const OFFENE = BRETTER.filter((b) => !treffModul.TREFF_FREIGABE_BRETTER.includes(b)); ok(gefunden === OFFENE.length, `die Community sieht die ${OFFENE.length} freien Bretter mit Inhalt (${gefunden}/${OFFENE.length})`); /* DIE GEGENPROBE IN BEIDE RICHTUNGEN. Ohne sie waere die Zeile oben auch dann gruen, wenn die Freigabe gar nicht wirkt und die beiden Bretter aus einem anderen Grund leer sind. */ let vorher = 0; for (const b of treffModul.TREFF_FREIGABE_BRETTER) { const r = await brett(b, CREW, gast.keks); if (r.titel.includes(`CREW-PROBE-${b.toUpperCase()}`)) vorher++; } ok(vorher === 0, `ohne Freigabe sieht die Community Termine und Highlights nicht (${vorher})`); /* Jetzt freigeben -- ueber den echten Weg, nicht in der Datenbank. */ let freigegeben = 0; for (const b of treffModul.TREFF_FREIGABE_BRETTER) { const beiDogi2 = await roh(`/workspace/api/bereich/${b}`, HAUS, { cookie: kDogi }); const eintrag = (JSON.parse(beiDogi2.text).eintraege || []) .find((e) => e.titel === `CREW-PROBE-${b.toUpperCase()}`); if (!eintrag) continue; const a = await roh(`/workspace/api/treff/eintrag/${eintrag.id}/freigeben`, HAUS, { cookie: kDogi }, {}, "PUT"); if (a.code === 200) freigegeben++; } ok(freigegeben === treffModul.TREFF_FREIGABE_BRETTER.length, `DogFather gibt beide frei (${freigegeben})`); let nachher = 0; for (const b of treffModul.TREFF_FREIGABE_BRETTER) { const r = await brett(b, CREW, gast.keks); if (r.titel.includes(`CREW-PROBE-${b.toUpperCase()}`)) nachher++; } ok(nachher === treffModul.TREFF_FREIGABE_BRETTER.length, `nach der Freigabe sieht sie beide (${nachher})`); /* Und ein Modi darf nicht freigeben, was er nicht sehen darf -- hier: dass die Route ueberhaupt Rechte prueft. Eine Creatorin bekommt 404, nicht 403: den Treff gibt es fuer sie nicht. */ const fremd = await roh("/workspace/api/treff/eintrag/1/freigeben", HAUS, { cookie: kCrea }, {}, "PUT"); ok(fremd.code === 404, `eine Creatorin kann nichts freigeben (${fremd.code})`); } { /* KEIN EINZIGES BRETT DES HAUSES. Ueber die Adresszeile probiert, nicht ueber die Kacheln -- eine Regel, die nur im Formular gilt, ist keine Regel. */ const fremde = ["ideen", "angebote", "rueckmeldung", "entwicklung", "talente", "agentur", "live", "content"]; const durch = []; for (const b of fremde) { const r = await brett(b, CREW, gast.keks); if (r.code === 200) durch.push(b); } ok(durch.length === 0, `und kein einziges des Hauses${durch.length ? " -- DURCH: " + durch.join(", ") : ""}`); /* DIE SUCHE SPIEGELT DIE FRAGE ZURUECK (Feld `frage`). Wer den Rohtext nach dem Suchwort durchsucht, findet deshalb IMMER einen Treffer -- und meldet ein Leck, das es nicht gibt. Genau darauf bin ich am 10.09. schon einmal hereingefallen und haette fast ein Loch repariert, das keines war. Gezaehlt wird, was die Suche WIRKLICH gefunden hat. */ const suche = await roh("/workspace/api/suche?q=HAUS-GEHEIM-4711", CREW, { cookie: gast.keks }); let treffer = -1; try { treffer = JSON.parse(suche.text).gesamt; } catch { treffer = -1; } ok(treffer === 0, `auch die Suche findet nichts aus dem Haus (${treffer} Treffer)`); } { /* UND DIE AGENTUR SIEHT DEN CREW NICHT. Beide Richtungen, beide ueber die Adresszeile. */ const durch = []; for (const b of BRETTER) { const r = await brett(b, HAUS, kCrea); if (r.code === 200 && r.titel.some((t) => t.startsWith("CREW-PROBE"))) durch.push(b); } ok(durch.length === 0, `eine Creatorin sieht kein Brett des Treffs${durch.length ? " -- DURCH: " + durch.join(", ") : ""}`); const durchM = []; for (const b of BRETTER) { const r = await brett(b, HAUS, kMana); if (r.code === 200 && r.titel.some((t) => t.startsWith("CREW-PROBE"))) durchM.push(b); } ok(durchM.length === 0, `eine Managerin auch nicht${durchM.length ? " -- DURCH: " + durchM.join(", ") : ""}`); /* GEGENPROBE: Der Modi sieht sie sehr wohl. Ohne diese Zeile waeren die beiden darueber auch gruen, wenn die Bretter fuer NIEMANDEN lesbar waeren. */ let sieht = 0; for (const b of BRETTER) { const r = await brett(b, CREW, kModi); if (r.code === 200 && r.titel.includes(`CREW-PROBE-${b.toUpperCase()}`)) sieht++; } ok(sieht === 7, `ein Modi sieht alle sieben (${sieht}/7)`); } /* ======================================================================= 4. DIE SEITEN: die aus der Tafel, und keine einzige mehr ======================================================================= */ melde(""); melde("=== 4. Nur die Seiten aus der Tafel ==="); { /* DIE LISTE WIRD NICHT GETIPPT, SIE WIRD GEHOLT (11.09.2026). Hier standen zwei Seiten und daneben "und keine dritte". Beim Anlegen der Regelseite wurde daraus eine dritte -- die Prüfung war rot, obwohl nichts kaputt war, und die naheliegende Reaktion wäre gewesen, die Zahl von zwei auf drei zu setzen. Beim vierten Mal wieder. Gemessen wird jetzt das, was die Zeile eigentlich behaupten will: Die Tafel und die Wirklichkeit stimmen überein — jede Seite, die in rechte.js für "gast" steht, geht auf; jede andere ausgelieferte Seite geht NICHT auf. Das ist strenger als die alte Fassung (die elf Namen waren von Hand gewählt) und altert nicht. */ const rechte = await import("./rechte.js"); const { readdirSync } = await import("node:fs"); const ordner = new URL("../workspace/", import.meta.url).pathname .replace(/^\/([A-Za-z]:)/, "$1"); const alleDateien = readdirSync(ordner).filter((f) => f.endsWith(".html")) .map((f) => "/workspace/" + f); const erlaubt = rechte.seitenFuer("gast"); const verboten = alleDateien .filter((p) => !erlaubt.includes(p) && !rechte.OHNE_ANMELDUNG.includes(p)); ok(erlaubt.length > 0 && verboten.length > 0, `die Tafel gibt der Community ${erlaubt.length} Seiten und verbietet ${verboten.length}`); const zu = []; for (const p of erlaubt) { const a = await roh(p, CREW, { cookie: gast.keks }); if (a.code !== 200) zu.push(`${p.split("/").pop()} (${a.code})`); } ok(zu.length === 0, `alle ${erlaubt.length} erlaubten öffnen sich${zu.length ? " -- ZU: " + zu.join(", ") : ""}`); const durch = []; for (const p of verboten) { const a = await roh(p, CREW, { cookie: gast.keks }); if (a.code === 200) durch.push(p.split("/").pop()); } ok(durch.length === 0, `und keine der ${verboten.length} anderen${durch.length ? " -- DURCH: " + durch.join(", ") : ""}`); /* UND DIE MODERATIONSSEITE AUSDRÜCKLICH NICHT. Sie steht zwar in der Liste oben mit drin -- aber sie ist die eine, bei der ein Fehler wirklich weh täte: Dort stehen die Namen derer, die gemeldet haben. Eine eigene Zeile dafür, damit der Befund einen Namen hat und nicht in "eine von 20" untergeht. */ const mod = await roh("/workspace/treff-moderation.html", CREW, { cookie: gast.keks }); ok(mod.code !== 200, `die Moderationsseite bleibt zu (${mod.code})`); const modApi = await roh("/workspace/api/treff/meldungen", CREW, { cookie: gast.keks }); ok(modApi.code === 404, `und die Meldungen dahinter auch (${modApi.code})`); } /* ======================================================================= 5. DAS HAUS BLEIBT ZU: Aufgaben, Termine, Dateien, Personen ======================================================================= */ melde(""); melde("=== 5. Vom Haus sieht sie nichts ==="); for (const [name, pfad] of [ ["Aufgaben", "/workspace/api/aufgaben"], ["Termine", "/workspace/api/termine"], ["Dateien", "/workspace/api/dateien"], ["Personen", "/workspace/api/personen"], ["Gespräche", "/workspace/api/chat/raeume"], ]) { const a = await roh(pfad, CREW, { cookie: gast.keks }); let leer = a.code !== 200; if (a.code === 200) { try { const j = JSON.parse(a.text); const felder = Object.values(j).filter(Array.isArray); leer = felder.every((f) => f.length === 0); /* Die eigene Person in einer Personenliste ist kein Leck. */ if (!leer && pfad.endsWith("personen")) { leer = felder.every((f) => f.every((p) => p.id === idGast)); } } catch { leer = true; } } ok(leer, `${name}: nichts (${a.code})`); } /* ======================================================================= 6. SCHREIBEN — und wo es ankommt ======================================================================= */ melde(""); melde("=== 6. Schreiben ==="); { /* DIE ERSTE WOCHE IST EINE SANDKISTE. Wer eben erst dazugekommen ist, liest und stimmt ab -- mehr nicht. Das wird von BEIDEN Seiten der Grenze gemessen, sonst beweist es nichts: Eine Schreibsperre, die immer sperrt, ist keine Stufe. */ const neu = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, { art: "hallo", titel: "GAST-ZU-FRUEH-0001", text: "Hallo zusammen.", datum: jetzt.slice(0, 10) }); ok(neu.code === 403, `wer neu ist, schreibt noch nicht (${neu.code})`); const lage0 = JSON.parse((await roh("/workspace/api/treff/lage", CREW, { cookie: gast.keks })).text); ok(lage0.stufe === "neu", `und die Lage sagt es ihm auch: ${lage0.stufe}`); ok(lage0.schreiben?.treff === false, "der Knopf waere aus"); /* Acht Tage zurueckdatieren -- genau EIN Tag ueber der Grenze. Eine Grenze, die nur von weit weg geprueft wird, koennte um Tage danebenliegen, ohne dass es auffaellt. */ const dd = new DatabaseSync(process.env.WORKSPACE_DB); dd.prepare("UPDATE personen SET erstellt = ? WHERE id = ?") .run(new Date(Date.now() - 8 * 86400000).toISOString(), idGast); dd.close(); const a = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, { art: "hallo", titel: "GAST-SCHREIBT-8899", text: "Hallo zusammen.", datum: jetzt.slice(0, 10) }); ok(a.code === 200 || a.code === 201, `nach acht Tagen schon (${a.code})`); /* UND SECHS TAGE REICHEN NICHT. Die andere Seite derselben Grenze -- ohne sie waere "acht Tage gehen" auch dann gruen, wenn die Stufe gar nicht geprueft wird. */ const dd2 = new DatabaseSync(process.env.WORKSPACE_DB); dd2.prepare("UPDATE personen SET erstellt = ? WHERE id = ?") .run(new Date(Date.now() - 6 * 86400000).toISOString(), idGast); dd2.close(); const knapp = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, { art: "hallo", titel: "GAST-ZU-FRUEH-0002", text: "Und?", datum: jetzt.slice(0, 10) }); ok(knapp.code === 403, `sechs Tage reichen nicht (${knapp.code})`); const dd3 = new DatabaseSync(process.env.WORKSPACE_DB); dd3.prepare("UPDATE personen SET erstellt = ? WHERE id = ?") .run(new Date(Date.now() - 8 * 86400000).toISOString(), idGast); dd3.close(); /* Und auf ein Brett des Teams kommt er auch als "Dabei" nicht. */ const brettDesTeams = await roh("/workspace/api/bereich/anschlag", CREW, { cookie: gast.keks }, { art: Object.keys(BEREICHE.anschlag.arten)[0], titel: "GAST-AM-ANSCHLAG-0001", text: "Ansage", datum: jetzt.slice(0, 10) }); ok(brettDesTeams.code === 403, `aufs Anschlagbrett schreibt nur das Team (${brettDesTeams.code})`); const beiDogi = await brett("treff", HAUS, kDogi); ok(beiDogi.titel.includes("GAST-SCHREIBT-8899"), "DogFather sieht den Beitrag"); const beiModi = await brett("treff", CREW, kModi); ok(beiModi.titel.includes("GAST-SCHREIBT-8899"), "der Modi auch"); const beiCrea = await brett("treff", HAUS, kCrea); ok(!beiCrea.titel.includes("GAST-SCHREIBT-8899"), "die Creatorin nicht"); } { /* UND NICHT IN EIN BRETT DES HAUSES. */ const a = await roh("/workspace/api/bereich/ideen", CREW, { cookie: gast.keks }, { art: "content", titel: "GAST-IM-HAUS-0000", text: "", datum: jetzt.slice(0, 10) }); ok(a.code !== 200 && a.code !== 201, `aber nicht in ein Brett des Hauses (${a.code})`); } melde(""); console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest, kein Befund */ } process.exit(fehler === 0 ? 0 : 1);