Files
dogfather-universe/server/pruef-treff.mjs
T
DogFatherGitandClaude Opus 5 718b267de0 Zugang: die gewaehlte Kachel ist bindend
Filipe, 01.10.2026: „mach das. es soll fest sein."

VanVan hatte gemeldet: „Man kann sich mit seinem Zugangscode immer
noch über jeden Button der Startseite anmelden, egal welche Rolle man
hat."

ICH HATTE ZUERST ABGERATEN -- und lag falsch, weil ich eine alte Lage
beschrieben habe.

Am 09.09.2026 hatte Filipe entschieden, dass die Modis KEINE eigene
Eingangskachel bekommen: „damit die von der workspace auch nicht mal
sehen dass die modis von mir einen eigenen zugang haben." Wer keine
Kachel hat, muss irgendeine nehmen koennen -- daher der stille Zugang.

SEIT DER HAUSTRENNUNG AM 24.09.2026 STIMMT DAS NICHT MEHR. Auf
`crew.` steht laengst ein eigener Kachelsatz mit ALLEN fuenf Rollen:
DogFather, rechte Hand, linke Hand, Modi, Community (CREW_KACHEL, und
crew-index.html zeigt sie). Das Verbergen leistet seither die ADRESSE
-- wer sie nicht kennt, findet die Wand nicht; wer sie kennt, liest
die Rollennamen ohnehin offen darauf.

Der stille Zugang war damit ein Rest. Er hat niemanden mehr
geschuetzt und nur dafuer gesorgt, dass die Kachelwahl folgenlos
blieb. Nachgesehen habe ich das erst, NACHDEM Filipe widersprochen
hat; die Kachelsaetze standen die ganze Zeit im Quelltext.

WAS SICH NICHT AENDERT: Auf der Agenturwand war der stille Zugang nie
aktiv. Ein Team-Dogi-Code verhaelt sich dort weiterhin wie ein
erfundener -- gleiche Antwort, gleicher Weg, gleiche Dauer. Das ist
jetzt ausdruecklich gemessen.

EINE PRUEFADRESSE IST KEINE WAND. `127.0.0.1` ist weder crew. noch
Agentur. Ohne den stillen Zugang gaelte dort der Agentursatz -- und
kein Modi kaeme mehr herein. Fuenfzig Pruefdateien melden Team-Rollen
ueber diese Adresse an. Auf einer Adresse ohne Wand gibt es deshalb
ALLE Kacheln; welche auf welcher ECHTEN Wand steht, misst
pruef-modi-verborgen mit ausdruecklichem Host-Kopf.

GEPRUEFT -- pruef-modi-verborgen 87/0 (war 85; die fuenf Zeilen „jede
Kachel geht" sind durch sieben ersetzt, die die neue Regel und ihre
Gegenproben messen). Die Anzahl ist Zeile fuer Zeile verglichen.

  Modi-Kachel + Modi-Code      -> herein
  admin/hand/linke/gast        -> abgewiesen
  rechte Hand auf ihrer Kachel -> herein
  Agenturwand + Modi-Code      -> wie ein erfundener

pruef-crew-adresse 169/0 (unveraenderte Anzahl, zwei Zeilen
umgedreht).

SECHZEHN PRUEFDATEIEN MELDETEN SICH UEBER FREMDE KACHELN AN -- ein
Rest derselben Zeit. Systematisch gesucht statt einzeln entdeckt:
Waere ich dem roten Lauf hinterhergelaufen, haette ich beim zwoelften
aufgehoert.

UND DABEI EIN EIGENER FEHLER: Mein erster Durchlauf las die Rolle am
CODENAMEN ab (CODE-MODI- -> modi). Das ging gut, bis „Nane" kam: ein
Modi mit dem Code CODE-NANE-0001. Zwei Pruefungen wurden rot, und
zwar an einer Stelle („die Stimmen stimmen"), die mit Anmeldung
nichts zu tun hat. Ein Codename ist eine Beschriftung, keine
Tatsache -- die Rolle steht in `anlegen()`. Danach abgeleitet blieben
genau zwei Abweichungen uebrig, und beide sind absichtliche
Gegenproben.

Drei Browserpruefungen tippten die Creator-Kachel mit einem
Modi-Code. Die Modi-Kachel gibt es nur auf der Crew-Wand, und ein
Browser auf 127.0.0.1 bekommt die Agenturwand; sie melden sich jetzt
ueber die Schnittstelle an und bekommen den Keks. Gemessen werden
soll dort, was ein Modi SIEHT -- nicht, wie er hereinkommt.

Grün: pruef-modi-verborgen, pruef-crew-adresse, pruef-treff 85/0,
pruef-galerie, pruef-kanaele, pruef-modi-katalog 150/0,
pruef-modi-ideen, pruef-modi-kategorien, pruef-modi-checkliste 75/0,
pruef-modi-livecheck, pruef-kachelraster, pruef-team-ampel 32/0,
pruef-team-stufen 47/0, pruef-wunschliste, pruef-bremse,
pruef-gespraech, pruef-personen-formular 43/0, pruef-start-ansicht,
pruef-community-sicht, pruef-reaktion 421/0, pruef-abzeichen,
pruef-chat, pruef-chat-kanaele 81/0, pruef-entwicklung 79/0,
pruef-bewerbung-aufgaben 154/0, pruef-struktur,
pruef-zwischenspeicher 34/0.

`code_kennung` wird weiter geschrieben, aber nicht mehr gelesen --
sie war der Suchschluessel des stillen Weges. Stehen gelassen: Eine
Spalte zu entfernen ist eine Schemaaenderung mit Sicherung, und sie
kostet nichts.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 00:22:55 +02:00

1064 lines
54 KiB
JavaScript

/* =====================================================================
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, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
/* ORTSZEIT, NICHT UTC. `toISOString().slice(0,10)` gibt zwischen
Mitternacht und zwei Uhr den VORTAG -- ein Test, der nachts einen
anderen Tag meint als die Anwendung. `pruef-struktur` hat genau
diese Zeile bei mir gefunden, Minuten nachdem ich sie geschrieben
hatte. */
import { tagLokal } from "./helfer-zeit.mjs";
const PORT = await eigenerPort(import.meta, "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"];
/* ==== WAS DER EINZELNEN PERSON GEHOERT, IST KEIN BRETT ==============
„Vertraulich melden", „Mein Steckbrief" und seit dem 24.09. auch
„Support" stehen in der Gruppe „Fuer dich" -- nicht zwischen den
Brettern. Eine vertrauliche Meldung oder eine kaputte Seite ist eine
Sache zwischen einem Menschen und der Leitung; zwischen den
Aushaengen saehe sie aus wie ein oeffentlicher Anschlag.
EINE LISTE, ZWEI BENUTZER. Bis heute stand sie zweimal in dieser
Datei -- einmal bei den Brettern der Community, einmal beim
Vergleich mit dem Modi. Als Support dazukam, wurde die eine
nachgezogen und die andere nicht, und die Pruefung meldete „FEHLT:
Support" fuer etwas, das genau dort steht, wo es hingehoert. Zwei
Listen fuer dieselbe Sache sind zwei Gelegenheiten, eine davon zu
vergessen. */
const PERSOENLICH = ["Vertraulich melden", "Mein Steckbrief", "Support"];
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) {
/* Die Community bestaetigt beim ersten Mal ihr Alter (seit 15.09.2026).
Nur fuer 'gast' -- fuer alle anderen muss der Server das Feld gar
nicht erst sehen. */
const r = await roh("/workspace/api/anmelden", host, {},
rolle === "gast" ? { rolle, code, alter_ok: true } : { 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;
/* AUF DER MODI-KACHEL (01.10.2026). Hier stand „creator" -- bis zum
24.09. gab es fuer einen Modi keine eigene Kachel, und der stille
Zugang liess jede durch. Seit die Kachel bindend ist, scheiterte
die Anmeldung, und fuenf Pruefungen wurden rot, ohne dass am
Programm etwas falsch war. */
const kModi = (await anmelden("modi", "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&nbsp;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]);
/* ABGELEITET, NICHT GEZAEHLT (berichtigt 21.09.2026).
----------------------------------------------------------------
Hier stand `kacheln.length === 4`. Am 21.09. kam die linke Hand
dazu, und die Zeile wurde rot, obwohl die Wand richtig war.
Die eigentliche Aussage ist auch eine andere und wiegt schwerer:
Die Anmeldung sucht `WHERE rolle = ?`. Eine Rolle OHNE Kachel
kommt damit ueberhaupt nicht herein -- der Zugang existiert, und
niemand kann ihn benutzen. Geprueft wird deshalb, dass jede Rolle
dieser Adresse ihre Kachel hat. Das kann nicht veralten. */
const sollRollen = [...w.TREFF_ROLLEN];
const ohneKachel = sollRollen.filter((r) => !kacheln.includes(r));
ok(ohneKachel.length === 0, ohneKachel.length === 0
? `jede Rolle dieser Adresse hat ihre Kachel: ${kacheln.join(", ")}`
: `OHNE KACHEL und damit unerreichbar: ${ohneKachel.join(", ")}`);
ok(kacheln.includes("gast"), "die Community ist eine davon");
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 <meta>, 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. Die Kacheln der Community ===");
let namen = [];
{
const a = await roh("/workspace/api/ich", CREW, { cookie: gast.keks });
const j = JSON.parse(a.text);
/* ==== ABGELEITET STATT ABGESCHRIEBEN (19.09.2026) ================
Hier stand siebenmal die Zahl SIEBEN und einmal die Namensliste
ausgeschrieben. Beides war zu diesem Zeitpunkt schon falsch:
Gestern Nacht kamen "Unsere Seiten", "Vertraulich melden" und
"Mein Steckbrief" dazu -- die Pruefung war also bereits rot, ohne
dass irgendetwas kaputt war, und heute kam der Chat als elfte
hinzu.
Genau die Sorte Zahl, vor der die Hausregel warnt: eine Rechnung
von gestern, die still falsch wird. Gezaehlt wird ab jetzt gegen
die Liste, die der Server wirklich fuehrt. Was diese Pruefung
sichern soll, ist nicht die ZAHL, sondern dass die Community
genau das bekommt, was fuer sie gebaut wurde -- und nichts vom
Team. */
const { bereicheFuer } = await import("./workspace.js");
const erwartet = bereicheFuer({ rolle: "gast" }).map((k) => k.name);
ok(Array.isArray(j.bereiche) && j.bereiche.length === erwartet.length,
`die Community bekommt ${erwartet.length} 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);
/* DIE REIHENFOLGE IST TEIL DER AUSSAGE, nicht Zufall (17.09.2026).
"Das Rudel" steht seit heute VORNE, und zwar aus zwei Gruenden:
1. Das Raster hat drei Spalten, seine Kachel ist doppelt breit.
An dritter Stelle passte sie nicht mehr in die erste Reihe und
riss ein Loch hinein -- genau das, was Filipe auf dem
Bildschirmfoto gesehen hat. Eine breite Kachel gehoert an den
Anfang; steht sie hinten, faellt die Luecke in die Mitte.
2. Der Treff IST das Herz dieses Bereichs. Die Reihenfolge
stimmt jetzt auch inhaltlich.
Wer sie wieder aendert, aendert beides. `pruef-kachelraster`
rechnet zusaetzlich nach, ob daraus ein Loch entsteht. */
ok(namen.join(" | ") === erwartet.join(" | "),
`und zwar diese: ${namen.join(", ")}`);
/* UEBER DIE EIGENSCHAFT, NICHT UEBER DEN NAMEN (berichtigt
21.09.2026). Hier stand `namen[0] === "Das Rudel"`. Am 21.09.
wurde aus der Kachel die Willkommensseite, und die Zeile wurde
rot, obwohl das Raster stimmte. Gemeint war nie der Name, sondern:
die DOPPELT BREITE Kachel steht vorne. Genau das wird jetzt
gefragt. */
const breite = bereicheFuer({ rolle: "gast" }).filter((k) => k.gross);
ok(breite.length === 1, `genau eine Kachel ist doppelt breit (${breite.length})`);
ok(namen[0] === breite[0]?.name,
`und sie steht vorne (${namen[0]})`);
/* NICHTS VOM TEAM. Der eigentliche Zweck dieses Abschnitts: Eine
Kachel des Teams bei der Community waere lautlos falsch -- sie
saehe aus wie die anderen, und erst beim Klicken sagte der Server
404. */
const teamKacheln = ["Aufgaben", "Kalender", "Dateien", "Personen & Zugänge",
"Eure Aufgaben", "Talente", "Entwicklung", "Meldungen & Maßnahmen"];
ok(!namen.some((n) => teamKacheln.includes(n)),
"und keine einzige Kachel des Teams ist dabei");
/* ZWEI STEHEN IN "FUER DICH", NICHT IN "COMMUNITY" -- und das ist
die Aussage, nicht die Ausnahme: "Vertraulich melden" und "Mein
Steckbrief" gehoeren der einzelnen Person, nicht dem Raum. Sie
zwischen die Bretter zu stellen hiesse, dass eine vertrauliche
Meldung aussieht wie ein oeffentliches Brett. */
/* Siehe PERSOENLICH ganz oben -- eine Liste fuer beide Stellen. */
const bretter = (j.bereiche || []).filter((k) => !PERSOENLICH.includes(k.name));
ok(bretter.every((k) => k.gruppe === "Community"),
`die ${bretter.length} Bretter stehen in der Gruppe „Community“`);
ok((j.bereiche || []).filter((k) => PERSOENLICH.includes(k.name))
.every((k) => k.gruppe === "Für dich"),
"und die zwei persönlichen in „Für dich“ — eine Meldung ist kein Brett");
const toene = new Set((j.bereiche || []).map((k) => k.ton));
ok(toene.size === (j.bereiche || []).length,
`${toene.size} Kacheln, ${toene.size} 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);
/* ==== ZWEI KACHELN SIEHT EIN MODI AUSDRUECKLICH NICHT ============
Bis zum 18.09. stimmte "ein Modi sieht alle Kacheln der Community
mit". Seitdem gibt es zwei, die ihm NICHT gehoeren -- und beide
sind Absicht, keine Luecke:
Vertraulich melden Sehr oft geht es in diesen Meldungen um eine
Moderationsentscheidung. Wer beteiligt ist,
darf die Beschwerde ueber sich nicht lesen.
Mein Steckbrief Jeder hat seinen eigenen. "Der von jemand
anderem" ist hier gar nicht gemeint.
Die Pruefung war dadurch seit gestern rot, ohne dass etwas kaputt
war. Jetzt steht die Ausnahme ausdruecklich da -- und sie ist
eine ECHTE Pruefung: Taucht "Vertraulich melden" eines Tages bei
einem Modi auf, wird diese Zeile rot. */
/* DIESELBE LISTE WIE OBEN. Hier stand eine zweite, und sie kannte
„Support" nicht -- die Pruefung meldete deshalb, dem Modi fehle
ein Brett, das gar keines ist. */
const NUR_COMMUNITY = PERSOENLICH;
/* Verglichen wird die GRUPPE "Community" auf beiden Seiten -- die
zwei persoenlichen Kacheln stehen dort ohnehin nicht drin. */
const sollteSehen = namen.filter((n) => !NUR_COMMUNITY.includes(n));
const fehlt = sollteSehen.filter((n) => !beimModi.includes(n));
ok(fehlt.length === 0,
`ein Modi sieht die ${sollteSehen.length} Bretter der Community mit`
+ `${fehlt.length ? " -- FEHLT: " + fehlt.join(", ") : ""}`);
const zuViel = NUR_COMMUNITY.filter((n) => beimModi.includes(n));
ok(zuViel.length === 0,
`aber NICHT den vertraulichen Meldeweg${zuViel.length ? " -- DURCH: " + zuViel.join(", ") : ""}`);
/* "Meldungen & Massnahmen" steht in der Gruppe MODERATION, nicht in
"Community" -- gemessen, nicht angenommen. Gesucht wird deshalb in
seiner ganzen Liste. Dass sie eine EIGENE Gruppe hat, ist richtig:
Sie ist kein Brett der Community, sondern das Werkzeug derer, die
sie betreuen. */
const modiAlles = [...(a.bereiche || []), ...(a.bereiche_zusatz || [])]
.map((k) => k.name);
ok(modiAlles.includes(MOD) && !namen.includes(MOD),
`und genau eine mehr, die die Community NICHT hat: ${MOD}`);
/* AUF DER TEAM-ADRESSE GEFRAGT, NICHT AUF DER AGENTUR- (21.09.2026).
Hier stand `HAUS`, also workspace. -- und die Zeile war richtig,
solange DogFather dort auch die Bretter des Treffs bekam.
Seit dem 21.09. bekommt er sie dort nicht mehr. Filipe: "ich will
dass du im workspace alles von team dogi weg nimmst ... ich will
keine mischung mehr von kacheln." Der Treff steht nur noch auf
seiner eigenen Adresse; von der Agenturseite fuehrt genau EINE
Tuer hinueber.
Die Aussage bleibt dieselbe -- "DogFather sieht dieselben Bretter
wie die Community" --, sie wird nur dort gemessen, wo diese
Bretter zuhause sind. Auf der Agenturadresse waere sie ab jetzt
eine Frage nach etwas, das dort nicht sein soll. */
const b = JSON.parse((await roh("/workspace/api/ich", CREW, { cookie: kDogi })).text);
/* AUF crew. STEHEN SIE IN `bereiche`, NICHT IN `bereiche_zusatz`:
Dort ist der Treff sein Zuhause und keine Zugabe. Gesucht wird
deshalb in beiden Listen -- so misst die Zeile dasselbe, egal von
welcher Adresse sie kommt. */
const beiDogi = [...(b.bereiche || []), ...(b.bereiche_zusatz || [])]
.filter((k) => k.gruppe === "Community").map((k) => k.name);
/* DogFather sieht den Meldeweg sehr wohl -- er beantwortet ihn. */
/* Auch hier: MOD steht in der Gruppe "Moderation", nicht in
"Community". `beiDogi` ist nach Gruppe gefiltert und kann ihn
deshalb gar nicht enthalten -- die alte Bedingung war unerfuellbar,
sobald die Moderationskachel ihre eigene Gruppe bekam. */
const dogiAlles = [...(b.bereiche || []), ...(b.bereiche_zusatz || [])]
.map((k) => k.name);
ok(sollteSehen.every((n) => beiDogi.includes(n)),
`und DogFather sieht auf seiner Adresse dieselben ${beiDogi.length} Bretter`);
ok(dogiAlles.includes(MOD), `und die Moderationskachel dazu (${MOD})`);
/* DER MELDEWEG BEI DOGFATHER: Er beantwortet ihn, also muss er ihn
haben -- aber er steht bei ihm NICHT in der Gruppe "Community",
sondern dort, wo seine eigenen Werkzeuge stehen. Gesucht wird
deshalb in seiner GANZEN Liste, nicht nur in einer Gruppe. */
/* DER MELDEWEG LEBT AUF DER TEAM-ADRESSE, nicht auf der Agentur-.
Gemessen: Auf crew. hat DogFather ihn in der Gruppe "Fuer dich";
auf workspace. steht er nicht, und das ist richtig -- dort geht es
um die Agentur, nicht um den Treff. Geprueft wird deshalb dort, wo
er hingehoert. */
const cDogi = JSON.parse((await roh("/workspace/api/ich", CREW, { cookie: kDogi })).text);
const dogiAufCrew = [...(cDogi.bereiche || []), ...(cDogi.bereiche_zusatz || [])]
.map((k) => k.name);
ok(dogiAufCrew.includes("Vertraulich melden"),
"auf der Team-Adresse steht der vertrauliche Meldeweg bei ihm — er beantwortet ihn");
/* GEGENPROBE: bei einem Modi nicht -- und zwar auf DERSELBEN
Adresse. Ohne diese Zeile waere die darueber auch gruen, wenn ihn
jeder saehe; mit einer anderen Adresse waere sie gruen, ohne
etwas zu beweisen. */
ok(!modiAlles.includes("Vertraulich melden"),
"bei einem Modi auf derselben Adresse nicht — er ist oft der Anlass");
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})`);
}
/* =======================================================================
WAS DAS TEAM SELBST EINSETZT, IST SOFORT DA (30.09.2026)
=======================================================================
Filipe: „wenn wir bei highlights videos rein setzen will ich nicht
mehr dass wir sie freigeben muessen, sobald die reingesetzt wurden
sollen die sofort zu sehen sein."
Die Freigabe auf dem Highlight-Brett ist ein FILTER gegen fremde
Inhalte -- sie fragt „hat das jemand angesehen?". Wer ihn bedienen
darf und den Eintrag selbst anlegt, hat ihn damit schon passiert.
DER ABSCHNITT DARUEBER BEWEIST DAS NICHT. Er legt seine Eintraege
unmittelbar in der Datenbank an und prueft damit den Mechanismus,
nicht den Weg. Er blieb nach der Aenderung gruen -- zu Recht, und
ohne etwas ueber sie zu sagen.
======================================================================= */
melde("\n=== Was das Team selbst einsetzt, ist sofort da ===");
{
const anlegenUeberDenWeg = async (bereich, art, titel) => {
const a = await roh(`/workspace/api/bereich/${bereich}`, HAUS,
{ cookie: kDogi },
{ art, titel, datum: tagLokal() });
let id = 0;
try { id = JSON.parse(a.text).id || 0; } catch { /* kein JSON */ }
return { code: a.code, id };
};
const hl = await anlegenUeberDenWeg("highlight", "clip", "SOFORT-SICHTBAR");
ok(hl.code === 201 && hl.id > 0,
`DogFather legt ein Highlight ueber den echten Weg an (${hl.code})`);
const beiGast = await brett("highlight", CREW, gast.keks);
ok(beiGast.titel.includes("SOFORT-SICHTBAR"),
"die Community sieht es SOFORT -- ohne zweiten Klick");
/* ---- DIE ERSTE GEGENPROBE: der Schalter bleibt ein Schalter ----
Bei `ansteht` entscheidet das Team JE TERMIN, ob die Community
ihn sieht. Waere die Aenderung dort auch gelandet, stuende jeder
interne Termin sofort im Treff -- und danach hat niemand
gefragt. */
const tm = await anlegenUeberDenWeg("ansteht", "stream", "NICHT-SOFORT-SICHTBAR");
if (tm.code !== 201) {
melde(` -- konnte nicht nachsehen: Termin anlegen ergab ${tm.code}`);
} else {
const terminBeiGast = await brett("ansteht", CREW, gast.keks);
ok(!terminBeiGast.titel.includes("NICHT-SOFORT-SICHTBAR"),
"ein TERMIN bleibt bis zum Schalter „Im Treff zeigen\" verborgen");
}
/* ---- DIE ZWEITE GEGENPROBE: der Filter bleibt ein Filter ----
Ein Community-Mitglied ab „Stamm" darf auf demselben Brett etwas
einstellen. Sein Eintrag wartet weiter auf jemanden aus dem
Team -- Urheberrecht und Anstand sind nichts, was man
nachtraeglich klaert.
Gefragt wird die Regel unmittelbar: Eine Person mit „Stamm"
anzulegen brauchte Alter UND Beitraege, und diese Pruefung
wuerde damit den Stufenaufbau messen statt die Freigabe. */
const frei = treffModul.sofortFreigeben("highlight", { rolle: "gast", id: idGast }, hl.id);
ok(frei === false,
"fuer eine Rolle von aussen gibt die Regel NICHT frei");
const auchNicht = treffModul.sofortFreigeben("ansteht", { rolle: "admin", id: idDogi }, 1);
ok(auchNicht === false,
"und fuer Termine gibt sie ueberhaupt nicht frei");
}
{
/* 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 Rudels${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" IST HIER HERAUSGENOMMEN (19.09.2026).
Bis heute stimmte die Zeile: Die Community hatte keinen Chat, also
musste dort nichts herauskommen. Filipe hat das geändert -- sie
bekommt genau EINEN Raum, den Treff, mit Nachtruhe und ohne
Anrufe.
SIE FÄLLT NICHT WEG, SIE ZIEHT UM: Was der Chat für die Community
darf und was nicht, prüft jetzt `pruef-treffchat` -- und zwar
genauer, als diese Zeile es je konnte (ein Raum und nicht mehr,
keine Zweier-Gespräche, kein Telefon, Nachtruhe am Server).
Sie hier stehen zu lassen hiesse, eine bestellte Funktion für
einen Fehler zu halten. */
]) {
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);
/* EINE WORTLISTE IST KEIN LECK (15.09.2026).
Diese Pruefung fragte "enthaelt die Antwort IRGENDEIN gefuelltes
Feld" -- und wurde rot, als das Aufgabenbrett am 15.09. die drei
Aufwandsstufen mitzuliefern begann (klein/mittel/gross). Die sind
fuer jeden Menschen dieselben, sie sagen ueber niemanden etwas,
und sie kommen mit, damit im Browser keine zweite Liste gepflegt
werden muss.
Ab da meldete die Pruefung bei JEDEM Lauf einen Befund, der keiner
war -- und eine Warnung, die immer kommt, wird ueberlesen. Genau
deshalb steht sie hier jetzt praeziser, statt lauter.
WAS EINE WORTLISTE VON DATEN UNTERSCHEIDET: Ein Datensatz in
diesem Haus hat eine `id`. Ein Vokabular hat einen `schluessel`
und keine. Das ist keine Auslegung, sondern die Bauweise -- jede
Tabelle vergibt id, jede Wortliste in workspace-*.js nicht. Ein
echter Eintrag kann dadurch nicht versehentlich als Vokabular
durchgehen. */
const istWortliste = (f) => f.length > 0 && f.every((x) => x
&& typeof x === "object" && typeof x.schluessel === "string" && x.id === undefined);
const felder = Object.values(j).filter(Array.isArray).filter((f) => !istWortliste(f));
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})`);
}
/* =======================================================================
7. WEM EIN EINTRAG GILT, SAGT DER SERVER -- NICHT DIE OBERFLAECHE
Filipe, 11.09.2026: "auf dieser seite soll nichts stehen von agentur,
diese seite hier ist nur fuer mich meine modis und meine community."
Er hatte recht, und der Fehler war ein fest eingetragenes Wort: In
bereich.js stand woertlich "Agentur" -- richtig auf der
Agenturadresse, falsch auf jeder anderen.
GEPRUEFT WIRD MIT EINEM EINZIGEN MENSCHEN AUF ZWEI ADRESSEN. Dieselbe
Person, dasselbe Brett, zwei Waende: Wandert das Wort mit, kommt es
vom Haus. Waere es an der Rolle festgemacht, stuende hier zweimal
dasselbe -- und das ist genau der Fehler, den es zu finden gilt.
======================================================================= */
melde("");
melde("=== 7. Das Wort für „Gilt für“ kommt vom Server ===");
{
const kCrew = (await anmelden("admin", "CODE-DOGI-0001", CREW)).keks;
const kHaus = (await anmelden("admin", "CODE-DOGI-0001", HAUS)).keks;
ok(!!kCrew && !!kHaus, "DogFather ist an beiden Waenden angemeldet");
const lies = async (brett, host, keks) => {
const a = await roh(`/workspace/api/bereich/${brett}`, host, { cookie: keks });
let g = null;
try { g = JSON.parse(a.text).gehoert; } catch { /* bleibt null */ }
return { code: a.code, gehoert: g };
};
const treff = await lies("treff", CREW, kCrew);
ok(treff.code === 200 && treff.gehoert === "Das Rudel",
`ein Rudel-Brett gehoert dem Rudel (${treff.gehoert})`);
const crew = await lies("ideen", CREW, kCrew);
ok(crew.code === 200 && crew.gehoert === "Team Dogi",
`dasselbe Haus-Brett auf crew. gehoert Team Dogi (${crew.gehoert})`);
/* GEGENPROBE, und ohne sie beweist die Zeile darueber nichts: Auf der
Agenturadresse MUSS weiterhin "Agentur" stehen. Ein Wort, das
ueberall dasselbe sagt, sagt nichts. */
const haus = await lies("ideen", HAUS, kHaus);
ok(haus.code === 200 && haus.gehoert === "Agentur",
`und auf workspace. weiterhin Agentur (${haus.gehoert})`);
ok(crew.gehoert !== haus.gehoert,
"dieselbe Person, dasselbe Brett, zwei Woerter -- es haengt am Haus");
/* UND DIE OBERFLAECHE SCHREIBT ES NICHT MEHR SELBST HIN. */
const js = readFileSync(new URL("../workspace/assets/js/bereich.js", import.meta.url), "utf8");
const ohneKommentare = js.replace(/\/\*[\s\S]*?\*\//g, "");
ok(!/['"`]Agentur/.test(ohneKommentare),
"in bereich.js steht das Wort „Agentur“ in keinem Text mehr");
ok(/gehoert/.test(ohneKommentare),
"stattdessen fragt sie den Server danach");
/* Gegenprobe zum Sucher selbst: Er MUSS ein hingeschriebenes
"Agentur" auch wirklich finden, sonst ist die Zeile darueber nur
gruen, weil sie blind ist. */
ok(/['"`]Agentur/.test("const x = 'Agentur-Eintraege';"),
"und der Sucher findet so ein Wort auch wirklich");
}
/* =====================================================================
KEIN BRETT DARF ZWISCHEN DIE REGELN FALLEN (21.09.2026)
=====================================================================
Dreimal derselbe Fehler, dreimal derselbe Hergang:
19.09. Die Kachel "Regeln & Hilfe" bekam ein neues Ziel.
`regeln` fiel aus TREFF_BRETTER -- und war fuer die
Community 404, obwohl treff-regeln.html darauf verweist.
20.09. Dasselbe noch einmal, bei MODI_BEREICHE_ERLAUBT.
21.09. Die Kachel "Das Rudel" wurde zur Willkommensseite.
`treff` fiel heraus -- und diesmal ging es in die andere
Richtung: Die Schranke laesst eine AGENTURROLLE unbesehen
durch, wenn ein Brett in keiner Liste steht. Eine
Creatorin konnte das Brett des Treffs lesen.
DER GEMEINSAME KERN: Die Schranke kennt drei Toere -- Treff-Bretter,
Bretter mit eigener Kachel, Bretter hinter einer Seite. Ein Brett,
das durch KEINES davon abgedeckt ist, faellt nicht etwa heraus,
sondern DURCH: Fuer eine Agenturrolle endet der Weg bei
`return next()`.
Genau das wird hier gezaehlt. Nicht "ist treff drin" -- das waere
die Reparatur von gestern -- sondern: faellt IRGENDEIN Brett durch
alle Regeln? Wer morgen eine Kachel umleitet, bekommt es damit
gesagt, statt es zu verschenken.
===================================================================== */
melde("\n--- Kein Brett faellt zwischen die Regeln ---");
{
const bModul = await import("./workspace-bereiche.js");
const alleBretter = Object.keys(bModul.BEREICHE || {});
ok(alleBretter.length > 10,
`${alleBretter.length} Bretter angesehen`);
/* Die Tore der Schranke, aus dem Code geholt statt abgeschrieben. */
const durchTreff = new Set(w.TREFF_BRETTER);
const durchKachel = w.MODI_BEREICHE_ERLAUBT;
/* DIE EHRLICHE REGEL, GEMESSEN STATT GERATEN.
----------------------------------------------------------------
Mein erster Entwurf fragte "haengt dieses Brett an irgendeiner
Regel" -- und meldete sofort `content` und `schutz`. Beides sind
AGENTURBRETTER ("Content-Ideen", "Schutz & Regeln"), die eine
Agenturrolle zu Recht liest. Die Zeile war also zu breit und
haette zwei Fehlalarme dauerhaft festgeschrieben. Eine Pruefung,
die zu viel meldet, wird abgeschaltet -- dann haelt sie gar
nichts mehr.
Nachgemessen ist die Trennung exakt und in BEIDE Richtungen
deckungsgleich: Die Bretter mit `fuerAlle` sind genau die des
Treffs, plus die Agenturablage. Das ist eine Eigenschaft des
BRETTS und wandert nicht, wenn jemand eine Kachel umleitet --
genau der Unterschied, an dem es dreimal gescheitert ist. */
const fuerAlle = Object.entries(bModul.BEREICHE)
.filter(([, v]) => v && v.fuerAlle).map(([k]) => k);
ok(fuerAlle.length >= 7, `${fuerAlle.length} Bretter stehen allen offen`);
/* Die Agenturablage ist der eine gewollte Fall: Sie steht allen
offen, aber im ANDEREN Haus. Sie namentlich auszunehmen ist etwas
anderes als eine Liste zu pflegen -- es ist eine Ausnahme, die
man sieht. */
const GEMEINSCHAFT = fuerAlle.filter((b) => b !== "agentur");
const ungeschuetzt = GEMEINSCHAFT.filter((b) => !durchTreff.has(b));
ok(ungeschuetzt.length === 0, ungeschuetzt.length === 0
? `alle ${GEMEINSCHAFT.length} Bretter des Rudels stehen in TREFF_BRETTER`
: `NICHT GESCHUETZT -- eine Agenturrolle kaeme hinein: ${ungeschuetzt.join(", ")}`);
/* UND DIE ANDERE RICHTUNG: Nichts darf in TREFF_BRETTER stehen, das
dort nicht hingehoert -- sonst spannte die Liste einen Schutz ueber
ein Brett, das der Agentur gehoert, und sperrte sie aus. */
const zuviel = [...durchTreff].filter((b) => !GEMEINSCHAFT.includes(b));
ok(zuviel.length === 0, zuviel.length === 0
? "und nichts Fremdes steht darin"
: `ZUVIEL in TREFF_BRETTER: ${zuviel.join(", ")}`);
/* UND EIN MODI MUSS SIE AUCH OEFFNEN KOENNEN. Dieselbe Falle, zweite
Liste: `MODI_BEREICHE_ERLAUBT` wird ebenfalls aus Kachelzielen
abgeleitet und verliert ein Brett, sobald eine Kachel umzieht. */
const fuerModiZu = GEMEINSCHAFT.filter((b) => !durchKachel.has(b));
ok(fuerModiZu.length === 0, fuerModiZu.length === 0
? "und ein Modi kommt in jedes davon hinein"
: `fuer einen Modi gesperrt: ${fuerModiZu.join(", ")}`);
/* GEGENPROBE: Wuerden diese Zeilen ein ungeschuetztes Brett
ueberhaupt sehen? Ein erfundener Name steht in keiner Menge und
muss deshalb auffallen. */
const erfunden = "brett-das-es-nicht-gibt";
ok(!durchTreff.has(erfunden) && !durchKachel.has(erfunden),
"und ein Brett ohne Schutz wuerde hier wirklich auffallen");
}
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);