Files
dogfather-universe/server/pruef-treff-werkzeuge.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

490 lines
24 KiB
JavaScript

/* =====================================================================
Die Werkzeuge des Treffs (11.09.2026)
pruef-treff.mjs fragt: WER SIEHT WAS. Diese Datei fragt: WAS
PASSIERT, wenn jemand etwas tut — „Will ich auch", anheften,
freigeben, melden, entfernen, sperren, übernehmen.
---------------------------------------------------------------------
ZU JEDER REGEL GEHÖRT HIER DIE GEGENPROBE, und zwar aus demselben
Grund wie überall in diesem Haus: Eine Prüfung, die immer bestätigt,
bestätigt nichts. Wenn „ein Modi darf nicht dauerhaft ausschließen"
grün ist, muss daneben stehen, dass DogFather es kann — sonst wäre
die Zeile auch dann grün, wenn NIEMAND es kann.
Konkret geprüft:
* „Will ich auch" ist EINE Stimme. Zweimal drücken ändert nichts,
und das Zurücknehmen geht.
* Am Anschlagbrett hängen höchstens drei. Die vierte wird
abgelehnt — und nach dem Abnehmen einer geht sie durch.
* Freigeben kann nur das Team. Ohne Freigabe ist der Eintrag für
die Community nicht da (steht in pruef-treff), mit Freigabe
schon.
* Melden ohne Grund geht nicht. Mit Grund landet es in der Liste
des Teams (DSA Art. 16).
* Entfernen ohne Grund geht nicht. Mit Grund ist der Eintrag weg
UND der Grund noch da (DSA Art. 17) — das ist der eigentliche
Punkt: Eine Begründung, die mit dem Beitrag verschwindet, ist
keine.
* Die Bannleiter: Ein Modi darf bis zu sieben Tage, DogFather auch
dauerhaft. Und der dauerhafte Ausschluss wirkt — die Sitzung ist
weg, nicht nur das Schreiben.
* Aus „Mitmachen" wird ein Talent, und zwar genau einmal.
Aufrufen mit: node server/pruef-treff-werkzeuge.mjs
===================================================================== */
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(4402, "pruef-treff-werkzeuge");
const ordner = mkdtempSync(join(tmpdir(), "ws-treffw-"));
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-werkzeuge");
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 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 heute = jetzt.slice(0, 10);
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);
/* Die Mitglieder werden ALT angelegt (40 Tage), damit sie schreiben
duerfen. Die Stufen selbst misst pruef-treff; hier waere ein frisches
Konto nur eine Huerde vor jeder einzelnen Zeile. */
const ALT = new Date(Date.now() - 40 * 86400000).toISOString();
function anlegen(name, rolle, code, erstellt = jetzt) {
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"), erstellt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Frida", "modi", "CODE-MODI-0001");
const idKessi = anlegen("Kessi", "gast", "CODE-GAST-0001", ALT);
const idBo = anlegen("Bo", "gast", "CODE-GAST-0002", ALT);
/* Ein dritter Zugang, nur fuer den Ausschluss. Ihn Bo zu geben waere
bequem gewesen und haette den Rest der Datei zerlegt: Ein
Ausgeschlossener hat keine Sitzung mehr, also waere ab da JEDE
weitere Zeile mit Bo gruen geworden, weil alles 401 antwortet --
und "die Talente sieht er nicht" haette nichts mehr bewiesen. */
const idPim = anlegen("Pim", "gast", "CODE-GAST-0003", ALT);
const rein = d.prepare(
"INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)"
+ " VALUES (?,?,?,?,?,?,?)");
const ersteArt = (b) => Object.keys(BEREICHE[b].arten)[0];
function eintrag(bereich, titel, von = idDogi) {
rein.run(bereich, ersteArt(bereich), titel, "", heute, jetzt, von);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const wunsch1 = eintrag("wunsch", "WUNSCH-EINS");
const anschlag = [1, 2, 3, 4].map((n) => eintrag("anschlag", `ANSAGE-${n}`));
const highlight1 = eintrag("highlight", "CLIP-EINS");
const bosBeitrag = eintrag("treff", "BO-SAGT-HALLO", idBo);
const mitmachen1 = eintrag("mitmachen", "ICH-HAETTE-LUST", idBo);
d.close();
/* EIN DELETE TRAEGT KEINEN KOERPER (gemessen 11.09.2026).
Nodes HTTP-Parser weist ein DELETE mit Koerper mit einem leeren 400
ab, bevor Express es sieht -- nachgemessen: PUT mit Koerper 200,
DELETE ohne Koerper 200, DELETE mit Koerper 400 mit genau einem
Kopffeld. Die Pruefung schickt deshalb gar keinen erst; wuerde sie
es tun, meldete sie einen Fehler in einer Route, in der keiner ist.
Genau so ist dieser Befund entstanden. */
function roh(pfad, host, kopf = {}, koerper = null, methode = null) {
if (methode === "DELETE") koerper = 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();
});
}
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
async function anmelden(rolle, code, host) {
const r = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
const kekse = [].concat(r.kopf?.["set-cookie"] || []);
return kekse.map((c) => c.split(";")[0]).join("; ");
}
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
const kKessi = await anmelden("gast", "CODE-GAST-0001", TREFF);
const kBo = await anmelden("gast", "CODE-GAST-0002", TREFF);
ok(!!kDogi && !!kModi && !!kKessi && !!kBo, "alle vier sind angemeldet");
/* =======================================================================
1. WILL ICH AUCH — eine Stimme, nicht zwei
======================================================================= */
melde("");
melde("=== 1. Will ich auch ===");
{
const weg = `/workspace/api/treff/eintrag/${wunsch1}/willich`;
const a = await roh(weg, TREFF, { cookie: kKessi }, {}, "PUT");
ok(a.code === 200 && json(a).stimmen === 1, `Kessi stimmt ab (${json(a).stimmen})`);
/* ZWEIMAL DRUECKEN IST EINMAL. Das ist die eigentliche Regel: Der
zusammengesetzte Schluessel laesst gar keine zweite Zeile zu. */
const b = await roh(weg, TREFF, { cookie: kKessi }, {}, "PUT");
ok(b.code === 200 && json(b).stimmen === 1, `zweimal druecken aendert nichts (${json(b).stimmen})`);
const c = await roh(weg, TREFF, { cookie: kBo }, {}, "PUT");
ok(c.code === 200 && json(c).stimmen === 2, `Bo auch (${json(c).stimmen})`);
const e = await roh(weg, TREFF, { cookie: kKessi }, {}, "DELETE");
ok(e.code === 200 && json(e).stimmen === 1 && json(e).meine === false,
`Kessi nimmt sie zurueck (${json(e).stimmen})`);
/* Und die Zahl kommt am Brett an -- sonst waere sie eine Zahl, die
nur die Route kennt. */
const brett = await roh("/workspace/api/bereich/wunsch", TREFF, { cookie: kBo });
const zeile = (json(brett).eintraege || []).find((x) => x.id === wunsch1);
ok(zeile?.stimmen === 1 && zeile?.meine_stimme === 1,
`am Brett steht sie auch (${zeile?.stimmen}, meine ${zeile?.meine_stimme})`);
/* GEGENPROBE: Auf einem anderen Brett gibt es keine Stimme. Ohne
diese Zeile waere "es geht" auch dann gruen, wenn es ueberall
ginge -- und dann waere die Wunschliste keine Abstimmung mehr,
sondern ein Nebeneffekt. */
const f = await roh(`/workspace/api/treff/eintrag/${bosBeitrag}/willich`, TREFF,
{ cookie: kKessi }, {}, "PUT");
ok(f.code === 400, `im Gespraech gibt es kein "Will ich auch" (${f.code})`);
}
/* =======================================================================
2. ANHEFTEN — höchstens drei
======================================================================= */
melde("");
melde("=== 2. Anheften ===");
{
const heften = (id, keks, m = "PUT") =>
roh(`/workspace/api/treff/eintrag/${id}/anheften`, m === "PUT" ? CREW : CREW,
{ cookie: keks }, {}, m);
let durch = 0;
for (const id of anschlag.slice(0, 3)) {
const a = await heften(id, kModi);
if (a.code === 200) durch++;
}
ok(durch === 3, `drei Ansagen haengen (${durch})`);
const vierte = await heften(anschlag[3], kModi);
ok(vierte.code === 409, `die vierte wird abgelehnt (${vierte.code})`);
ok(/3/.test(json(vierte).fehler || ""),
"und die Absage nennt die Zahl, statt nur nein zu sagen");
/* NACH DEM ABNEHMEN GEHT SIE DURCH. Ohne diese Zeile waere die
Grenze auch dann gruen, wenn ueberhaupt nichts mehr angeheftet
werden koennte. */
await heften(anschlag[0], kModi, "DELETE");
const nochmal = await heften(anschlag[3], kModi);
ok(nochmal.code === 200, `nach dem Abnehmen geht die vierte (${nochmal.code})`);
/* Und die Community heftet nichts an. */
const gast = await roh(`/workspace/api/treff/eintrag/${anschlag[1]}/anheften`, TREFF,
{ cookie: kKessi }, {}, "PUT");
ok(gast.code === 404, `ein Mitglied kann nichts anheften (${gast.code})`);
const brett = await roh("/workspace/api/bereich/anschlag", CREW, { cookie: kModi });
const oben = (json(brett).eintraege || [])[0];
ok(oben?.angeheftet === 1, `oben steht eine angeheftete (${oben?.titel})`);
}
/* =======================================================================
3. MELDEN — der Grund ist Pflicht (DSA Art. 16)
======================================================================= */
melde("");
melde("=== 3. Melden ===");
{
const weg = `/workspace/api/treff/eintrag/${bosBeitrag}/melden`;
const ohne = await roh(weg, TREFF, { cookie: kKessi }, { grund: "" });
ok(ohne.code === 400, `ohne Grund geht es nicht (${ohne.code})`);
const kurz = await roh(weg, TREFF, { cookie: kKessi }, { grund: "doof" });
ok(kurz.code === 400, `"doof" ist kein Grund (${kurz.code})`);
const mit = await roh(weg, TREFF, { cookie: kKessi },
{ grund: "Da steht die Handynummer von jemandem drin." });
ok(mit.code === 200, `mit Grund geht es (${mit.code})`);
const liste = await roh("/workspace/api/treff/meldungen", CREW, { cookie: kModi });
const m = (json(liste).meldungen || [])[0];
ok(liste.code === 200 && m?.eintrag_id === bosBeitrag,
`der Modi sieht die Meldung (${(json(liste).meldungen || []).length})`);
ok(m?.autor_rolle_name === "Community",
`und wer sie geschrieben hat, mit vollem Rollennamen (${m?.autor_rolle_name})`);
/* Die Community sieht die Meldungen NICHT -- sonst waere ablesbar,
wer wen gemeldet hat. */
const gast = await roh("/workspace/api/treff/meldungen", TREFF, { cookie: kBo });
ok(gast.code === 404, `ein Mitglied sieht die Liste nicht (${gast.code})`);
/* Entscheiden braucht ebenfalls einen Satz. */
const leer = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
{ cookie: kModi }, { status: "erledigt", entscheid: "" }, "PATCH");
ok(leer.code === 400, `entscheiden ohne Begruendung geht nicht (${leer.code})`);
const fertig = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
{ cookie: kModi }, { status: "erledigt", entscheid: "Nummer entfernt, Bo Bescheid gesagt." },
"PATCH");
ok(fertig.code === 200, `mit Begruendung schon (${fertig.code})`);
const zweimal = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
{ cookie: kModi }, { status: "abgelehnt", entscheid: "Doch nicht so schlimm gewesen." },
"PATCH");
ok(zweimal.code === 409, `zweimal entscheiden geht nicht (${zweimal.code})`);
}
/* =======================================================================
4. ENTFERNEN — mit Grund, und der Grund überlebt (DSA Art. 17)
======================================================================= */
melde("");
melde("=== 4. Entfernen ===");
{
const weg = `/workspace/api/bereich/treff/${bosBeitrag}`;
const ohne = await roh(weg, CREW, { cookie: kModi }, null, "DELETE");
ok(ohne.code === 400, `ohne Grund wird nichts entfernt (${ohne.code})`);
/* Und der Eintrag ist auch wirklich noch da. Ein 400 sagt nur, was
der Server GEANTWORTET hat -- nicht, was er getan hat. */
const noch = await roh("/workspace/api/bereich/treff", CREW, { cookie: kModi });
ok((json(noch).eintraege || []).some((x) => x.id === bosBeitrag),
"und der Beitrag steht noch da");
const mit = await roh(`${weg}?grund=`
+ encodeURIComponent("Enthielt die Telefonnummer einer anderen Person."),
CREW, { cookie: kModi }, null, "DELETE");
ok(mit.code === 200, `mit Grund geht es (${mit.code})`);
const danach = await roh("/workspace/api/bereich/treff", CREW, { cookie: kModi });
ok(!(json(danach).eintraege || []).some((x) => x.id === bosBeitrag),
"der Beitrag ist weg");
/* DAS IST DER EIGENTLICHE PUNKT: Die Begruendung ist NICHT weg.
Haenge sie mit ON DELETE CASCADE am Eintrag, waere sie im selben
Moment verschwunden wie das, was sie begruendet. */
const akte = await roh("/workspace/api/treff/entfernt", HAUS, { cookie: kDogi });
const e = (json(akte).entfernt || [])[0];
ok(e?.eintrag_id === bosBeitrag && /Telefonnummer/.test(e?.grund || ""),
`und der Grund steht noch da (${e?.grund?.slice(0, 30)}…)`);
ok(e?.titel === "BO-SAGT-HALLO" && e?.autor_name === "Bo",
`samt Titel und Verfasser (${e?.titel}, ${e?.autor_name})`);
}
/* =======================================================================
5. DIE BANNLEITER — Modis befristet, dauerhaft nur DogFather
======================================================================= */
melde("");
melde("=== 5. Massnahmen ===");
{
const weg = "/workspace/api/treff/massnahme";
const ohne = await roh(weg, CREW, { cookie: kModi },
{ person_id: idBo, art: "pause", tage: 3, grund: "" });
ok(ohne.code === 400, `ohne Grund geht keine Massnahme (${ohne.code})`);
const kurz = await roh(weg, CREW, { cookie: kModi },
{ person_id: idBo, art: "pause", tage: 3, grund: "Hat mehrfach gegen Regel 2 verstossen." });
ok(kurz.code === 200, `ein Modi darf drei Tage Pause geben (${kurz.code})`);
const lang = await roh(weg, CREW, { cookie: kModi },
{ person_id: idPim, art: "ausschluss", tage: 30, grund: "Immer wieder dasselbe, trotz Hinweis." });
ok(lang.code === 403, `dreissig Tage darf er nicht (${lang.code})`);
ok(/7/.test(json(lang).fehler || ""), "und die Absage nennt die Grenze");
const dauer = await roh(weg, CREW, { cookie: kModi },
{ person_id: idPim, art: "ausschluss", grund: "Endgueltig, nach mehreren Hinweisen." });
ok(dauer.code === 403, `und dauerhaft schon gar nicht (${dauer.code})`);
/* GEGENPROBE. Ohne sie waeren die beiden Zeilen darueber auch dann
gruen, wenn NIEMAND ausschliessen koennte. Sieben Tage, damit Pim
fuer die naechste Zeile noch hereinkommt. */
const dogi7 = await roh(weg, CREW, { cookie: kModi },
{ person_id: idPim, art: "ausschluss", tage: 7, grund: "Sieben Tage, das darf er." });
ok(dogi7.code === 200, `sieben Tage darf ein Modi (${dogi7.code})`);
/* Und gegen jemanden aus dem TEAM geht gar nichts -- das gehoert
nach "Personen & Zugaenge", nicht hierher. */
const gegenTeam = await roh(weg, HAUS, { cookie: kDogi },
{ person_id: idDogi, art: "pause", tage: 1, grund: "Nur ein Versuch, das darf nicht gehen." });
ok(gegenTeam.code === 404, `gegen das Team gibt es keine Massnahme (${gegenTeam.code})`);
}
/* =======================================================================
6. DER AUSSCHLUSS WIRKT — nicht nur beim Schreiben
======================================================================= */
melde("");
melde("=== 6. Ausschluss ===");
{
/* Kessi kommt herein -- vor dem Ausschluss. Ohne diese Zeile waere
"sie kommt nicht mehr herein" auch dann gruen, wenn sie es nie
konnte. */
/* "dabei" und nicht "stamm": Kessi ist 40 Tage dabei, hat aber
keinen eigenen Beitrag geschrieben. Beide Bedingungen muessen
erfuellt sein -- genau das misst diese Zeile nebenbei mit. */
const vorher = await roh("/workspace/api/treff/lage", TREFF, { cookie: kKessi });
ok(vorher.code === 200 && json(vorher).stufe === "dabei",
`Kessi ist drin, Stufe ${json(vorher).stufe}`);
const aus = await roh("/workspace/api/treff/massnahme", HAUS, { cookie: kDogi },
{ person_id: idKessi, art: "ausschluss", grund: "Dauerhaft, nach mehreren Vorfaellen." });
ok(aus.code === 200 && json(aus).bis === null,
`DogFather schliesst dauerhaft aus (bis: ${json(aus).bis})`);
/* DIE SITZUNG IST WEG, nicht nur das Schreiben. Ein
Ausgeschlossener, der weiter mitlesen kann, ist bei Belaestigung
genau der Teil, der weh tut. */
const lage = await roh("/workspace/api/treff/lage", TREFF, { cookie: kKessi });
ok(lage.code === 401, `ihre Sitzung gilt nicht mehr (${lage.code})`);
const brett = await roh("/workspace/api/bereich/treff", TREFF, { cookie: kKessi });
ok(brett.code === 401, `und sie liest auch nichts mehr (${brett.code})`);
/* Und sie kommt auch nicht wieder herein -- gar nicht erst. Ohne
diese Zeile waere die Anmeldung gelungen, das Plaetzchen gesetzt,
und dahinter eine Seite, auf der nichts geht. Das sieht aus wie
ein kaputtes Programm, nicht wie eine Entscheidung. */
const neu = await anmelden("gast", "CODE-GAST-0001", TREFF);
ok(!neu, "und sie kann sich nicht neu anmelden");
/* GEGENPROBE: Bo hat nur eine PAUSE -- kein Ausschluss. Er darf
weiter lesen, aber nicht schreiben. Ohne diese Zeile waeren die
vier darueber auch dann gruen, wenn jede Massnahme jeden
aussperrt. */
const boLage = await roh("/workspace/api/treff/lage", TREFF, { cookie: kBo });
ok(boLage.code === 200, `Bo liest weiter (${boLage.code})`);
ok(json(boLage).sperre?.art === "pause" && !!json(boLage).sperre?.bis,
`und sieht seine Pause mit Enddatum (${json(boLage).sperre?.bis?.slice(0, 10)})`);
ok(json(boLage).schreiben?.treff === false, "schreiben darf er nicht");
/* Und Pim, sieben Tage ausgeschlossen, kommt gar nicht mehr herein
-- ein befristeter Ausschluss ist ein Ausschluss, keine Pause. */
const pim = await anmelden("gast", "CODE-GAST-0003", TREFF);
ok(!pim, "ein befristeter Ausschluss sperrt ebenfalls aus");
const versuch = await roh("/workspace/api/bereich/treff", TREFF, { cookie: kBo },
{ art: ersteArt("treff"), titel: "BO-TROTZDEM", text: "Hallo?", datum: heute });
ok(versuch.code === 403, `und es geht auch wirklich nicht (${versuch.code})`);
}
/* =======================================================================
7. AUS „MITMACHEN" WIRD EIN TALENT — genau einmal
======================================================================= */
melde("");
melde("=== 7. Die Bruecke zu den Talenten ===");
{
const weg = `/workspace/api/treff/mitmachen/${mitmachen1}/uebernehmen`;
const a = await roh(weg, HAUS, { cookie: kDogi }, {});
ok(a.code === 200 && json(a).talent_id > 0, `Bo landet bei den Talenten (${a.code})`);
const b = await roh(weg, HAUS, { cookie: kDogi }, {});
ok(b.code === 409, `ein zweites Mal legt keinen zweiten an (${b.code})`);
const talente = await roh("/workspace/api/bereich/talente", HAUS, { cookie: kDogi });
const t = (json(talente).eintraege || []).filter((x) => x.creator_id === idBo);
ok(t.length === 1, `und es steht genau einer dort (${t.length})`);
ok(/Bo/.test(t[0]?.titel || ""), `mit seinem Namen (${t[0]?.titel})`);
/* Die Community kommt an die Talente nicht heran -- sonst waere die
Bruecke ein Fenster. */
const gast = await roh("/workspace/api/bereich/talente", TREFF, { cookie: kBo });
ok(gast.code === 404, `ein Mitglied sieht die Talente nicht (${gast.code})`);
}
/* =======================================================================
8. DIE REGELN NENNEN DIE ZAHLEN, DIE WIRKLICH GELTEN
======================================================================= */
melde("");
melde("=== 8. Der Regeltext ===");
{
const a = await roh("/workspace/api/treff/regeln", TREFF, { cookie: kBo });
const r = json(a);
ok(a.code === 200 && r.mindestalter === treffModul.MINDESTALTER,
`Mindestalter ${r.mindestalter} -- dieselbe Zahl wie im Code`);
ok(r.mindestalter === 18, "und sie ist 18 (Entscheidung Filipe, 11.09.2026)");
ok(Array.isArray(r.stufen) && r.stufen.length === treffModul.STUFEN.length,
`alle ${r.stufen?.length} Stufen kommen mit`);
ok(r.anschlag_max === treffModul.ANSCHLAG_MAX,
`und die Zahl am Anschlagbrett (${r.anschlag_max})`);
/* Die Seite selbst gehoert der Community UND denen, die moderieren
-- ein Modi, der die Regeln nicht sehen kann, moderiert nach
Gefuehl. */
const rechte = await import("./rechte.js");
const wer = rechte.SEITEN["/workspace/treff-regeln.html"] || [];
ok(wer.includes("gast") && wer.includes("modi") && wer.includes("admin"),
`die Regelseite steht fuer ${wer.join(", ")} offen`);
ok(!wer.includes("manager") && !wer.includes("creator") && !wer.includes("spicy"),
"und nicht fuer die Agentur");
/* "Wer sieht was" ist allein DogFathers Seite. */
const tafel = rechte.SEITEN["/workspace/rechte.html"] || [];
ok(tafel.length === 1 && tafel[0] === "admin",
`"Wer sieht was" nur fuer ${tafel.join(", ")}`);
const fremd = await roh("/workspace/api/rechte/tafel", CREW, { cookie: kModi });
ok(fremd.code === 404, `ein Modi bekommt die Tafel nicht (${fremd.code})`);
const meine = await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi });
const t = json(meine);
ok(meine.code === 200 && t.seiten?.length === rechte.alleSeiten().length,
`DogFather bekommt alle ${t.seiten?.length} Seiten`);
const community = (t.rollen || []).find((x) => x.schluessel === "gast");
ok(community && community.seiten < 5,
`und die Community steht bei ${community?.seiten} von ${t.seiten?.length}`);
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
/* Der Ordner bleibt manchmal liegen, weil der Dienst die Datenbank
unter Windows noch offen haelt. Das ist ein Aufraeumrest, kein
Befund -- deshalb faellt der Lauf daran nicht durch. */
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);