Files
dogfather-universe/server/pruef-treff.mjs
T
DogFatherGitandClaude Opus 5 7b21f247eb Der Treff: eine eigene Tür für die Community, und sie sieht nur, was dasteht
Filipe: "eine community rolle und community kategorie, wo die community
auch dann nur die sieht" · "wieso nur eine kachel — ich will mehrere,
mit mehreren optionen" · "egal welche rolle hinzugefügt wird soll immer
nur das sehen was ich erlaube".

DIE DRITTE ADRESSE. treff.dogfather-universe.com bekommt eine eigene
Weiche (treff-adresse.js), eine eigene Zugangswand, ein eigenes Manifest
und acht eigene Bühnenbilder. Ohne sie stünde dort die Wand der Agentur:
Spicy-Logo, "Creator Workspace", fünf Rollenkacheln mit den Namen Admin,
Manager, Scout, Creator — die komplette Struktur des Unternehmens, vor
der Anmeldung, für jeden mit der Adresse.

Welche Wand zu welcher Tür gehört, steht jetzt in EINER Tafel
(istFremdeWand) statt als Sonderfall im Code. Bei zwei Wänden war ein
Sonderfall richtig; bei drei wären es drei geworden, bei vier sechs.

SIEBEN BRETTER UND EIN ACHTES NUR FÜRS TEAM. Anschlagbrett, Was ansteht,
Der Treff, Wunschliste, Highlights, Regeln & Hilfe, Mitmachen — dazu
"Meldungen & Maßnahmen", das ausdrücklich NICHT in TREFF_BEREICHE steht:
dort wäre es lautlos bei der Community gelandet und hätte ausgesehen wie
die anderen sieben.

WAS AUF DEN BRETTERN PASSIERT: "Will ich auch" (eine Stimme je Person,
erzwungen durch den Schlüssel, nicht durch eine Prüfung) · Anheften,
höchstens drei, gezählt IN der Transaktion · Freigabe vor Sichtbarkeit
für Termine und Highlights, wobei Abwesenheit der Ruhezustand ist ·
Melden mit Pflicht-Grund (DSA Art. 16) · Entfernen mit Grund, der den
Beitrag ÜBERLEBT (DSA Art. 17) · drei Stufen, gerechnet statt
gespeichert · die Bannleiter: Modi bis 7 Tage, dauerhaft nur DogFather
(seine Entscheidung) · "Mitmachen" wird zum Talent, genau einmal.

Der Ausschluss wirkt in sitzungLesen() — also nicht nur beim Schreiben.
Wer ausgeschlossen ist, ist weg, nicht stumm.

ZWEI SEITEN MEHR. treff-regeln.html holt seine ZAHLEN vom Server
(Mindestalter 18, Fristen, Anschläge) — ein Regeltext, der eine andere
Zahl nennt als die Regel, ist schlimmer als keiner. rechte.html rechnet
"Wer sieht was" aus derselben Tafel aus, aus der die Schranke ihre
Entscheidung holt. Die Community steht dort bei 3 von 23.

SECHS BEFUNDE, KEINEN HAT DAS LESEN GEFUNDEN:
 · Ein Modi kam über den verborgenen Zugang in den Treff — die Bedingung
   war durch Ausschluss formuliert und nahm die neue Wand automatisch mit
 · /api/personen gab einem Mitglied Namen und Rolle von DogFather
 · Ein DELETE mit Körper wird von Nodes HTTP-Parser mit einem leeren 400
   abgewiesen, bevor Express ihn sieht (gemessen). Der Grund reist jetzt
   in der Adresse
 · Der Kommentar, der erklärt, warum die Zugangswand keine Rollennamen
   nennt, nannte selbst einen — in einer ausgelieferten Datei
 · Zwei Listen für "welche Seiten sind ohne Anmeldung offen". Sie
   stimmten, weil beide am selben Tag entstanden
 · Ein Portkonflikt, den ich selbst gebaut hatte (4391 gehört
   pruef-chat-aufloesen)

Dazu zwei feste Zahlen abgeschafft (die 20 in pruef-rechtetafel, die
Kachelzahl in pruef-treff) und zwei Werkzeuge, die jetzt SUCHEN statt
aufzuzählen: Der Versionsstempel findet seine Manifeste selbst — das
dritte hätte sonst keinen bekommen, und die neue App hätte bis zu vier
Stunden lang das falsche Zeichen getragen. Das Bildwerkzeug baut beide
Zugangswände aus einer Vorlage mit zwei Werten.

Zwei neue Kacheltöne, gemessen statt gewählt: #2f2f7f (Abstand 31,4 in
Lab) und #7c2771 (29,7). Das schwächste Paar der vorhandenen 34 liegt
bei 24,9.

Geprüft: treff 69 · treff-werkzeuge 60 · rollen 312 · start-ansicht 147 ·
crew-adresse 129 · sicht 84 · modi-verborgen 80 · haus-trennung 62 ·
bereiche-lesend 37 · css-klassen 30 · zwischenspeicher 21 · alle-wege 19 ·
rechtetafel 19 — alles grün.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-11 17:58:34 +02:00

614 lines
30 KiB
JavaScript

/* =====================================================================
DER TREFF — 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 TREFF = "treff.dogfather-universe.com";
const CREW = "crew.dogfather-universe.com";
const HAUS = "workspace.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],
`TREFF-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", TREFF);
ok(gast.code === 200 && gast.keks.length > 0, `die Community kommt auf treff. herein (${gast.code})`);
for (const [name, host] of [["crew.", CREW], ["workspace.", HAUS]]) {
const a = await anmelden("gast", "CODE-GAST-0001", host);
const b = await anmelden("gast", "VOELLIG-ERFUNDEN-XYZ", host);
ok(a.code === 401 && 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})`);
}
{
/* UND DIE GEGENRICHTUNG: Auf der Wand des Treffs kommt sonst niemand
herein -- auch DogFather nicht. Er hat dort nichts zu suchen; er
sieht den Treff auf seiner eigenen Adresse. */
const a = await anmelden("admin", "CODE-DOGI-0001", TREFF);
const b = await anmelden("modi", "CODE-MODI-0001", TREFF);
ok(a.code !== 200 && b.code !== 200,
`auf treff. kommt sonst niemand herein -- auch DogFather nicht (${a.code}/${b.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. DIE WEICHE: drei Adressen, drei Türen, keine Verwechslung
======================================================================= */
melde("");
melde("=== 1b. Die Weiche des Treffs ===");
{
/* DIE WAND. Ohne die Weiche stünde auf der Adresse der Community die
Wand der Agentur: Spicy-Media-Logo, "Creator Workspace", fünf
Rollenkacheln mit den Namen Admin, Manager, Scout, Creator -- die
komplette Struktur des Unternehmens, VOR der Anmeldung, für jeden,
der die Adresse hat. Das ist das eigentliche Leck, gegen das
treffWeiche gebaut ist. */
for (const pfad of ["/workspace/", "/workspace/index.html"]) {
const a = await roh(pfad, TREFF);
const ist = a.code === 200 && /Der&nbsp;Treff|Der Treff/.test(a.text);
ok(ist, `${pfad} zeigt die Wand des Treffs (${a.code})`);
ok(!/Creator\s*Workspace|Spicy/i.test(a.text),
`und nennt weder Workspace noch Spicy (${pfad})`);
}
/* KEINE ROLLENNAMEN DES HAUSES in der ausgelieferten Datei. Die
beiden anderen Wände nennen ihre Rollen offen -- dort ist das
richtig, denn dort steht nur, wer ohnehin dazugehört. Hier steht
ein Mensch, der NICHT zum Team gehört. */
{
const a = await roh("/workspace/treff-index.html", TREFF);
const verraten = ["manager", "scout", "creator", "modi", "rechte hand"]
.filter((w) => new RegExp(w, "i").test(a.text));
ok(verraten.length === 0,
`die Wand nennt keine Rolle des Hauses${verraten.length ? " -- FUND: " + verraten.join(", ") : ""}`);
}
/* DAS MANIFEST MACHT DIE APP. Drei Adressen, drei Namen -- sonst
hieße die App auf dem Startbildschirm eines Mitglieds "Team Dogi",
der Name eines Bereichs, zu dem es nicht gehört. */
const namen = {};
for (const [was, host] of [["treff", TREFF], ["crew", CREW], ["haus", HAUS]]) {
const a = await roh("/workspace/app.webmanifest", host);
try { namen[was] = JSON.parse(a.text).name; } catch { namen[was] = `(kein JSON: ${a.code})`; }
}
ok(namen.treff === "Der Treff", `das Manifest heißt "${namen.treff}"`);
ok(namen.crew === "Team Dogi" && namen.treff !== namen.crew,
`und auf crew. weiterhin "${namen.crew}"`);
ok(new Set(Object.values(namen)).size === 3,
`drei Adressen, drei Namen (${Object.values(namen).join(" · ")})`);
/* JEDE WAND GEHÖRT ZU GENAU EINER TÜR. Beide Richtungen, sonst
beweist es nichts: Die eigene Wand MUSS erreichbar sein. */
const waende = [
["/workspace/crew-index.html", CREW, TREFF],
["/workspace/treff-index.html", TREFF, CREW],
["/workspace/treff-index.html", TREFF, HAUS],
];
for (const [datei, zuhause, fremd] of waende) {
const a = await roh(datei, zuhause);
const b = await roh(datei, fremd);
ok(a.code === 200, `${datei} gibt es zu Hause (${a.code})`);
ok(b.code === 404, `und auf der fremden Adresse nicht (${b.code})`);
}
/* Auch mit doppeltem Schrägstrich -- express.static zieht sie später
selbst zusammen und liefert die Datei aus. Genau daran ist am
04.09.2026 die Schranke des Workspace vorbeigelaufen. */
const schraeg = await roh("/workspace//treff-index.html", CREW);
ok(schraeg.code === 404, `auch mit doppeltem Schrägstrich nicht (${schraeg.code})`);
/* NICHT INDIZIEREN -- als Kopf und nicht nur als <meta>, damit er
auch für Manifest, Bilder und jede Antwort ohne HTML gilt. */
const kopf = await roh("/workspace/", TREFF);
ok(/noindex/i.test(kopf.kopf?.["x-robots-tag"] || ""),
`der Kopf sagt noindex (${kopf.kopf?.["x-robots-tag"]})`);
/* NUR DER WORKSPACE. Sonst läge die komplette Dogfather-Website ein
zweites Mal unter einer zweiten Adresse. */
const haupt = await roh("/", TREFF);
ok(haupt.code === 302 && haupt.kopf?.location === "/workspace/",
`die Hauptseite gibt es hier nicht (${haupt.code} → ${haupt.kopf?.location})`);
/* DAS HAUS. haus.css ist auf der Agenturadresse leer; auf treff.
muss dahinter das Farbhaus von Team Dogi stehen -- sonst käme die
Seite grau an und färbte sich erst im Browser um. */
const haus = await roh("/workspace/assets/css/haus.css?v=1", TREFF);
ok(haus.code === 200 && /--sternenfeld/.test(haus.text),
`haus.css bringt das Farbhaus mit (${haus.text.length} Zeichen)`);
/* DIE BÜHNEN INNEN. Die Motive der Agentur zeigen das
Spicy-Media-Logo selbst -- teils vielfach nebeneinander. */
const a1 = await roh("/workspace/assets/img/buehne-lounge-breit.webp", TREFF);
const a2 = await roh("/workspace/assets/img/crew-buehne-lounge-breit.webp", TREFF);
const a3 = await roh("/workspace/assets/img/buehne-lounge-breit.webp", HAUS);
ok(a1.code === 200 && a1.text.length === a2.text.length,
`die Bühne kommt von Team Dogi (${a1.text.length} = ${a2.text.length})`);
ok(a1.text.length !== a3.text.length,
`und ist eine andere als die der Agentur (${a1.text.length} ≠ ${a3.text.length})`);
/* DAS ZEICHEN IN DER KOPFLEISTE. start.css setzt dort eine Chili --
als CSS-Hintergrund, auf jeder Seite, den ganzen Tag. */
const m1 = await roh("/workspace/assets/img/marke-spicy.webp", TREFF);
const m2 = await roh("/workspace/assets/img/marke-husky.webp", TREFF);
const m3 = await roh("/workspace/assets/img/marke-spicy.webp", HAUS);
ok(m1.code === 200 && m1.text.length === m2.text.length,
`in der Kopfleiste steht der Husky (${m1.text.length})`);
ok(m1.text.length !== m3.text.length,
`und nicht die Chili (${m1.text.length} ≠ ${m3.text.length})`);
}
/* =======================================================================
2. DIE KACHELN: sieben, und sonst nichts
======================================================================= */
melde("");
melde("=== 2. Sieben Kacheln — und keine achte ===");
let namen = [];
{
const a = await roh("/workspace/api/ich", TREFF, { 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", TREFF, { 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, TREFF, gast.keks);
if (r.code === 200 && r.titel.includes(`TREFF-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, TREFF, gast.keks);
if (r.titel.includes(`TREFF-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 === `TREFF-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, TREFF, gast.keks);
if (r.titel.includes(`TREFF-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, TREFF, 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", TREFF, { 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 TREFF 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("TREFF-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("TREFF-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(`TREFF-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, TREFF, { 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, TREFF, { 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", TREFF, { cookie: gast.keks });
ok(mod.code !== 200, `die Moderationsseite bleibt zu (${mod.code})`);
const modApi = await roh("/workspace/api/treff/meldungen", TREFF, { 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, TREFF, { 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", TREFF, { 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", TREFF, { 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", TREFF, { 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", TREFF, { 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", TREFF, { 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", TREFF, { 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);