Files
dogfather-universe/server/pruef-crew-adresse.mjs
T
DogFatherGitandClaude Opus 5 63b3ace3fa Die rechte Hand -- drei Rollen auf der Adresse des Teams
Filipe: "3 rollen. dogfather. rechte hand und modis. perfektionier das."

DIE RECHTE MUSSTE ICH NICHT ERFINDEN. Sie stehen im Blueprint V3.0,
Kapitel 3.1 und in der Sichtbarkeitsmatrix 3.2: "Gleicher Ueberblick wie
Owner. Kann Modis im Alltag koordinieren. Verwaltungsrechte optional
durch Owner freischaltbar." Also: alle Aufgaben, Ideen und Angebote des
Teams, der Eingang samt Entscheidungen, die Checklisten der Modis zum
Ansehen -- aber keine Personenverwaltung (laut Blueprint "optional",
also standardmaessig aus) und keine privaten Kalender oder Einzelchats.

DER EIGENTLICHE UMBAU WAR NICHT DIE ROLLE, SONDERN EINE MENGE.
Bis heute hiess "verborgen" im Code `rolle === "modi"` -- an acht
Stellen. Bei ZWEI verborgenen Rollen ist das genau die Sorte Stelle, die
man an sieben von acht Orten nachzieht; die achte faellt niemandem auf,
weil dort dann einfach jemand sichtbar ist, der es nicht sein sollte.
Ein vergessener Rechteschutz meldet sich nie.

Jetzt lesen alle Regeln aus TEAM_DOGI_ROLLEN: die SQL-Ausblendung
(ohneModi heisst deshalb jetzt ohneTeamDogi), die verborgenen Nummern,
die Marke, die Adressregel, die Schranke beim Anlegen. Eine dritte
verborgene Rolle waere eine Zeile.

Die Menge wohnt in crew-adresse.js und nicht bei den uebrigen Rollen:
workspace.js importiert jene Datei. Andersherum waere es ein Kreis --
Node loest ihn auf, aber mit halb gefuellten Modulen, und das faellt
erst zur Laufzeit auf.

ZWEI LOECHER, GEFUNDEN BEIM SYSTEMATISCHEN NACHLESEN
1. Die Bereichsschranke griff nur bei Modis -- die rechte Hand waere
   ueber die Adresszeile in die Agentur-Ablage gekommen.
2. Ihr Ideen-Board und ihr Angebote-Brett waeren LEER geblieben: Sie
   fiel durch den Team-Zweig hindurch in die Betreuungsregel, die fuer
   sie nichts findet. Derselbe Fehler wie am 01.09. beim Manager und am
   09.09. beim Modi -- und er meldet sich nie, weil ein leeres Brett
   nicht nach Fehler aussieht.

NEBENBEI EINE ALTE SCHWACHSTELLE WEG
gate.js hatte eine zweite Namensliste fuer die Rollen, mit dem Kommentar
daneben, sie sei "genau die Stelle, die beim naechsten Mal wieder
vergessen wird" -- was schon passiert war. Mit zwei Zugangswaenden
haette sie die Namen BEIDER tragen muessen, in einer Datei, die jeder
bekommt. Sie liest den Namen jetzt aus der Kachel, wo er ohnehin steht.

Die Zugangswand hat drei Kacheln: DogFather (Husky), Rechte Hand
(Pfote, neu) und Modi (derselbe Husky, Wunsch vom 09.09.). Die Pfote
liegt am naechsten an "rechte Hand", ohne eine Hand zu sein.

GEMESSEN
pruef-rollen           274 (statt 245; 128 statt 112 Durchgaenge)
pruef-crew-adresse     114   pruef-modi-verborgen     78 (statt 75)
pruef-modi-checkliste   59   pruef-crew-wand-bild     45
pruef-modi-ideen        30   pruef-personen-formular  27 (statt 25)
pruef-modi-katalog      29   pruef-modi-kategorien    25
pruef-zwischenspeicher  21   pruef-modi-livecheck     16
pruef-modi-wortleck      5   pruef-start-ansicht, pruef-bereiche-lesend

Jede gestiegene Zahl hat einen Grund: Die neue Rolle laeuft in DENSELBEN
Listen mit wie die Modis, nicht in eigenen. pruef-modi-verborgen war
vorher gruen, ohne sie ein einziges Mal angesehen zu haben.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 15:18:26 +02:00

488 lines
25 KiB
JavaScript

/* =====================================================================
DIE EIGENE ADRESSE DER MODI-APP
Geprueft wird nicht "die neue Adresse geht", sondern die vier Fragen,
an denen dieser Umbau scheitern kann:
1. KOMMT EIN MODI AUF crew. HEREIN -- und sonst niemand?
2. IST DER WORKSPACE UNVERAENDERT? Derselbe Dienst bedient beide
Adressen. Wer nicht auf den Hostnamen prueft, sperrt mit einer
einzigen Zeile alle Creator, Scouts und Manager aus, sofort nach
dem Neustart und ohne Vorwarnung.
3. BLEIBEN DIE PRUEFADRESSEN UNBERUEHRT? Waere die Regel "alles
ausser crew", dann wuerden ab sofort sieben andere Pruefdateien
messen, dass ein Modi nirgends hereinkommt -- gruen, weil sie
nichts mehr finden. Ein Lauf, der nichts mehr prueft, ist
schlimmer als ein roter.
4. HEISST DIE APP AUF DEM HANDY DES MODIS ANDERS? Sonst war der
ganze Umzug umsonst.
MIT GEGENPROBE. Ganz unten bekommt der Modi in der Datenbank eine
andere Rolle -- danach MUSS ihn dieselbe Sitzung auf crew. nicht mehr
hereinlassen. Ohne diesen Teil waere nicht bewiesen, dass die
Pruefung ueberhaupt anschlagen kann.
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request } from "node:http";
const ordner = mkdtempSync(join(tmpdir(), "ws-crew-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4371";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(180_000, "pruef-crew-adresse");
await new Promise((r) => setTimeout(r, 700));
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
let offen = 0;
const unklar = (t) => { offen++; console.log(" -- " + t); };
/* ---------------------------------------------------------------------
Ein Aufruf mit frei waehlbarem Host-Kopf.
Bewusst node:http und nicht fetch: `Host` gilt in der Fetch-Norm als
verbotener Kopf, und ob undici das durchlaesst, ist eine Annahme, auf
die diese ganze Pruefung nicht gebaut sein soll. Hier wird der Kopf
nachweislich gesetzt.
------------------------------------------------------------------ */
function hole(pfad, { host, methode = "GET", rumpf = null, keks = "" } = {}) {
return new Promise((fertig, schief) => {
const kopf = { Host: host };
if (rumpf) { kopf["Content-Type"] = "application/json"; kopf["Content-Length"] = Buffer.byteLength(rumpf); }
if (keks) kopf.Cookie = keks;
const a = request({ host: "127.0.0.1", port: 4371, path: pfad, method: methode, headers: kopf },
(antwort) => {
let text = "";
antwort.setEncoding("utf8");
antwort.on("data", (t) => { text += t; });
antwort.on("end", () => fertig({
status: antwort.statusCode,
kopf: antwort.headers,
text,
keks: (antwort.headers["set-cookie"] || [])
.map((z) => z.split(";")[0]).join("; "),
}));
});
a.on("error", schief);
a.setTimeout(15_000, () => { a.destroy(new Error("Zeitueberschreitung")); });
if (rumpf) a.write(rumpf);
a.end();
});
}
const anmelden = (host, rolle, code) =>
hole("/workspace/api/anmelden", { host, methode: "POST", rumpf: JSON.stringify({ rolle, code }) });
/* ---------- Personen anlegen ---------------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
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);
const kennung = (code) => createHmac("sha256", schluessel).update(code).digest("hex");
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, kennung(code), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Schulle", "manager", "CODE-MAN-0001");
anlegen("Rieke", "hand", "CODE-HAND-0001");
const idModi = anlegen("Marina", "modi", "CODE-MODI-0001");
const CREW = "crew.dogfather-universe.com";
const WS = "workspace.dogfather-universe.com";
const HAUPT = "dogfather-universe.com";
const LOKAL = "127.0.0.1:4371";
/* =====================================================================
TEIL A -- DIE REGELN, OHNE SERVER
Sie stehen als eigene Funktionen in crew-adresse.js, damit genau das
hier moeglich ist: die Entscheidung pruefen, nicht ihre Auswirkung.
===================================================================== */
console.log("\n--- Die Regeln selbst ---");
const { istCrewAdresse, istOhneModiAdresse, istWorkspacePfad, sitzungPasstZurAdresse, CREW_ADRESSE }
= await import("./crew-adresse.js");
ok(istCrewAdresse(CREW) === true, "crew.dogfather-universe.com ist die Modi-Adresse");
ok(istCrewAdresse("CREW.Dogfather-Universe.COM") === true, "Gross/klein macht keinen Unterschied");
ok(istCrewAdresse(CREW + ":443") === true, "ein Port angehaengt macht keinen Unterschied");
ok(istCrewAdresse(WS) === false, "workspace. ist NICHT die Modi-Adresse");
ok(istCrewAdresse(HAUPT) === false, "die Hauptseite ist NICHT die Modi-Adresse");
ok(istCrewAdresse("crew.dogfather-universe.com.boese.de") === false,
"eine fremde Domain mit dem Namen darin zaehlt nicht");
ok(istCrewAdresse("") === false && istCrewAdresse(null) === false,
"leer und fehlend sind nicht die Modi-Adresse");
ok(istOhneModiAdresse(WS) === true, "auf workspace. haben Modis nichts verloren");
ok(istOhneModiAdresse(HAUPT) === true, "auf der Hauptseite ebenfalls nicht");
ok(istOhneModiAdresse("www." + HAUPT) === true, "auf www. ebenfalls nicht");
ok(istOhneModiAdresse(CREW) === false, "crew. gehoert NICHT dazu (Falle 1)");
ok(istOhneModiAdresse(LOKAL) === false, "127.0.0.1 gehoert NICHT dazu (Falle 2)");
ok(istOhneModiAdresse("localhost:4100") === false, "localhost gehoert NICHT dazu (Falle 2)");
/* Die beiden Mengen duerfen sich nirgends beruehren. Ein Muster statt
einer Liste haette hier sofort angeschlagen. */
const alleAdressen = [CREW, WS, HAUPT, "www." + HAUPT, LOKAL, "localhost:4100", "git." + HAUPT];
ok(alleAdressen.every((h) => !(istCrewAdresse(h) && istOhneModiAdresse(h))),
"keine Adresse liegt in beiden Mengen");
ok(istWorkspacePfad("/workspace") === true, "/workspace gehoert dazu");
ok(istWorkspacePfad("/workspace/start.html") === true, "/workspace/start.html gehoert dazu");
ok(istWorkspacePfad("/workspaceXYZ") === false, "/workspaceXYZ gehoert NICHT dazu (Falle 3)");
ok(istWorkspacePfad("/workspace-alt") === false, "/workspace-alt gehoert NICHT dazu (Falle 3)");
ok(istWorkspacePfad("/assets/img/x.png") === false, "/assets liegt ausserhalb");
/* SEIT DEM 10.09.2026 DREI ROLLEN AUF crew. -- Filipe: "3 rollen.
dogfather. rechte hand und modis."
DogFather steht damit auf BEIDEN Adressen. Das ist keine Luecke in
der Trennung, sondern ihr Sinn: Er ist die einzige Person, die in
beiden Welten arbeitet. Die rechte Hand und die Modis dagegen gibt
es nur hier -- und auf workspace. gar nicht. */
const ROLLEN = ["admin", "spicy", "manager", "scout", "creator", "hand", "modi"];
const TEAM = new Set(["hand", "modi"]);
let passtWahr = 0, passtFalsch = 0;
for (const r of ROLLEN) {
const aufCrew = sitzungPasstZurAdresse(r, CREW);
const aufWs = sitzungPasstZurAdresse(r, WS);
const aufLokal = sitzungPasstZurAdresse(r, LOKAL);
const darfCrew = TEAM.has(r) || r === "admin";
ok(aufCrew === darfCrew, `crew.: ${r} ${darfCrew ? "darf" : "darf nicht"}`);
ok(aufWs === !TEAM.has(r), `workspace.: ${r} ${TEAM.has(r) ? "darf nicht" : "darf"}`);
ok(aufLokal === true, `127.0.0.1: ${r} unveraendert erlaubt`);
for (const w of [aufCrew, aufWs, aufLokal]) { if (w) passtWahr++; else passtFalsch++; }
}
/* Die Gegenprobe zur Regel selbst: Eine Funktion, die immer `true`
liefert, haette alle 18 Zeilen darueber ebenfalls teilweise bestanden.
Beide Antworten muessen vorkommen. */
ok(passtWahr > 0 && passtFalsch > 0,
`die Regel unterscheidet wirklich (${passtWahr}x ja, ${passtFalsch}x nein)`);
/* =====================================================================
TEIL B -- AM LAUFENDEN SERVER
===================================================================== */
console.log("\n--- Anmeldung je Adresse ---");
const modiAufCrew = await anmelden(CREW, "modi", "CODE-MODI-0001");
ok(modiAufCrew.status === 200, "crew.: der Modi kommt herein");
let zielModi = {};
try { zielModi = JSON.parse(modiAufCrew.text); } catch { /* bleibt leer */ }
ok(zielModi.weiter === "/workspace/start.html", "crew.: er landet auf seiner Startseite");
ok(modiAufCrew.keks.includes("dfw_sitzung"), "crew.: er bekommt eine Sitzung");
/* Der Code des Modis wird bewusst mit der Kachel 'creator' geschickt --
genau so kommt er auch aus der Zugangswand, denn eine Modi-Kachel
gibt es nicht. */
const modiOhneKachel = await anmelden(CREW, "creator", "CODE-MODI-0001");
ok(modiOhneKachel.status === 200, "crew.: die gewaehlte Kachel ist egal (stiller Zugang)");
const managerAufCrew = await anmelden(CREW, "manager", "CODE-MAN-0001");
ok(managerAufCrew.status === 401, "crew.: ein Manager mit gueltigem Code kommt NICHT herein");
ok(/ungueltig/.test(managerAufCrew.text),
"crew.: er bekommt dieselbe Antwort wie bei einem Tippfehler");
ok(!managerAufCrew.keks.includes("dfw_sitzung"), "crew.: und keine Sitzung");
const adminAufCrew = await anmelden(CREW, "admin", "CODE-DOGI-0001");
ok(adminAufCrew.status === 200 && adminAufCrew.keks.includes("dfw_sitzung"),
"crew.: DogFather kommt herein -- er arbeitet in beiden Welten");
const handAufCrew = await anmelden(CREW, "hand", "CODE-HAND-0001");
ok(handAufCrew.status === 200 && handAufCrew.keks.includes("dfw_sitzung"),
"crew.: die rechte Hand kommt herein");
/* Und sie kommt NICHT auf die andere Adresse -- genau wie ein Modi.
Sie bekommt dort denselben Hinweis auf die neue Adresse. */
const handAufWs = await anmelden(WS, "creator", "CODE-HAND-0001");
let zielHand = {};
try { zielHand = JSON.parse(handAufWs.text); } catch { /* bleibt leer */ }
ok(handAufWs.status === 200 && zielHand.weiter === `${CREW_ADRESSE}/workspace/start.html`,
"workspace.: die rechte Hand wird auf die neue Adresse verwiesen");
ok(!handAufWs.keks.includes("dfw_sitzung"),
"workspace.: und bekommt dort keine Sitzung");
/* Eine Kachel, die es auf crew. nicht gibt, ist dort so gut wie
erfunden -- auch mit einem gueltigen Code dahinter. */
const spicyAufCrew = await anmelden(CREW, "spicy", "CODE-DOGI-0001");
ok(spicyAufCrew.status === 401, "crew.: eine fremde Kachel gibt es dort nicht");
const modiAufWs = await anmelden(WS, "creator", "CODE-MODI-0001");
ok(modiAufWs.status === 200, "workspace.: der Modi bekommt eine Antwort");
let zielWs = {};
try { zielWs = JSON.parse(modiAufWs.text); } catch { /* bleibt leer */ }
ok(zielWs.weiter === `${CREW_ADRESSE}/workspace/start.html`,
"workspace.: sie zeigt ihm die neue Adresse");
ok(!modiAufWs.keks.includes("dfw_sitzung"),
"workspace.: aber KEINE Sitzung -- er meldet sich nur noch auf crew. an");
/* DER WICHTIGSTE PUNKT DIESER DATEI. Derselbe Dienst bedient beide
Adressen; ein Denkfehler hier sperrt das ganze Team aus. */
const managerAufWs = await anmelden(WS, "manager", "CODE-MAN-0001");
ok(managerAufWs.status === 200 && managerAufWs.keks.includes("dfw_sitzung"),
"workspace.: der Manager kommt herein wie immer -- der Workspace ist unberuehrt");
const managerLokal = await anmelden(LOKAL, "manager", "CODE-MAN-0001");
const modiLokal = await anmelden(LOKAL, "creator", "CODE-MODI-0001");
ok(managerLokal.status === 200 && managerLokal.keks.includes("dfw_sitzung"),
"127.0.0.1: der Manager kommt herein (Pruefadressen unberuehrt)");
ok(modiLokal.status === 200 && modiLokal.keks.includes("dfw_sitzung"),
"127.0.0.1: der Modi ebenfalls -- die sieben anderen Modi-Pruefungen messen weiter");
/* ---------------------------------------------------------------------
Eine Sitzung gehoert zu genau einer Adresse.
------------------------------------------------------------------ */
console.log("\n--- Eine Sitzung, eine Adresse ---");
const keksModi = modiAufCrew.keks;
const keksManager = managerAufWs.keks;
const modiDaheim = await hole("/workspace/api/ich", { host: CREW, keks: keksModi });
ok(modiDaheim.status === 200, "crew.: der angemeldete Modi wird erkannt");
ok(/Marina/.test(modiDaheim.text), "crew.: und zwar als er selbst");
const modiFremd = await hole("/workspace/api/ich", { host: WS, keks: keksModi });
ok(modiFremd.status === 401, "workspace.: dieselbe Sitzung zaehlt dort nicht");
const managerDaheim = await hole("/workspace/api/ich", { host: WS, keks: keksManager });
ok(managerDaheim.status === 200, "workspace.: der Manager wird erkannt");
const managerFremd = await hole("/workspace/api/ich", { host: CREW, keks: keksManager });
ok(managerFremd.status === 401, "crew.: seine Sitzung zaehlt dort nicht");
/* Auch die Seitenschranke, nicht nur die Schnittstelle: Eine Seite, die
ausgeliefert wird, waere ein Loch, selbst wenn die Daten dahinter
leer blieben. */
const seiteFremd = await hole("/workspace/start.html", { host: CREW, keks: keksManager });
ok(seiteFremd.status === 302, "crew.: die Startseite schickt den Manager zur Zugangswand");
const seiteDaheim = await hole("/workspace/start.html", { host: CREW, keks: keksModi });
ok(seiteDaheim.status === 200, "crew.: dem Modi liefert sie die Seite");
/* ---------------------------------------------------------------------
Die Weiche: was es auf crew. ueberhaupt gibt.
------------------------------------------------------------------ */
console.log("\n--- Die Weiche ---");
const wurzel = await hole("/", { host: CREW });
ok(wurzel.status === 302 && wurzel.kopf.location === "/workspace/",
"crew.: die Hauptseite gibt es dort nicht, es geht zur Zugangswand");
const wurzelWs = await hole("/", { host: HAUPT });
ok(wurzelWs.status === 200, "die Hauptseite selbst ist unveraendert erreichbar");
const fastWorkspace = await hole("/workspaceXYZ", { host: CREW });
ok(fastWorkspace.status === 302, "crew.: /workspaceXYZ gehoert nicht dazu (Falle 3)");
const robots = await hole("/workspace/", { host: CREW });
ok(String(robots.kopf["x-robots-tag"] || "").includes("noindex"),
"crew.: nicht indizieren -- die Adresse soll in keiner Suchmaschine stehen");
const robotsWs = await hole("/workspace/", { host: WS });
ok(!robotsWs.kopf["x-robots-tag"],
"workspace.: dort steht der Kopf NICHT (Gegenprobe zur Weiche)");
/* ---------------------------------------------------------------------
Die App heisst anders.
------------------------------------------------------------------ */
console.log("\n--- Zwei Apps, zwei Namen ---");
const manifestCrew = await hole("/workspace/app.webmanifest", { host: CREW });
const manifestWs = await hole("/workspace/app.webmanifest", { host: WS });
let mc = {}, mw = {};
try { mc = JSON.parse(manifestCrew.text); } catch { /* bleibt leer */ }
try { mw = JSON.parse(manifestWs.text); } catch { /* bleibt leer */ }
ok(manifestCrew.status === 200 && manifestWs.status === 200,
"beide Adressen liefern ein Manifest");
ok(mc.name === "Team Dogi", `crew.: die App heisst "Team Dogi" (gelesen: ${mc.name || "?"})`);
ok(mw.name === "Creator Workspace — Spicy & Dogi",
`workspace.: dort heisst sie unveraendert (gelesen: ${mw.name || "?"})`);
ok(Array.isArray(mc.icons) && mc.icons.length > 0
&& mc.icons.every((s) => /crew-/.test(s.src)),
`crew.: alle ${(mc.icons || []).length} Symbole sind die eigenen`);
ok(Array.isArray(mw.icons) && mw.icons.length > 0
&& mw.icons.every((s) => /workspace-/.test(s.src)),
"workspace.: dort weiterhin die alten");
ok(mc.start_url === "/workspace/start.html" && mc.scope === "/workspace/",
"crew.: Bereich und Startseite stimmen");
/* Die Symboldateien muessen es auch wirklich geben -- ein Manifest, das
auf nichts zeigt, faellt sonst erst auf dem Startbildschirm auf. */
let symboleDa = 0;
for (const s of mc.icons || []) {
const pfad = String(s.src).split("?")[0];
const a = await hole(pfad, { host: CREW });
if (a.status === 200) symboleDa++;
}
ok((mc.icons || []).length > 0 && symboleDa === (mc.icons || []).length,
`crew.: alle ${symboleDa} Symbole werden auch ausgeliefert`);
/* =====================================================================
DIE ZUGANGSWAND
Filipe am 10.09.2026: "alles soll auf dogfather und die modis
perfektionniert werden auf der neuen modi seite." Bis dahin stand auf
crew. die gewohnte Wand -- Spicy-Media-Logo, "Creator Workspace",
fuenf Rollenkacheln. Fuer einen Modi war davon nichts richtig.
Zwei Dinge werden hier gemessen, und das zweite ist das wichtigere:
dass die ALTE Wand unangetastet blieb.
===================================================================== */
console.log("");
console.log("--- Zwei Waende ---");
const wandCrew = await hole("/workspace/", { host: CREW });
const wandWs = await hole("/workspace/", { host: WS });
ok(wandCrew.status === 200, "crew.: die Zugangswand kommt (keine Weiterleitungsschleife)");
ok(wandWs.status === 200, "workspace.: die Zugangswand kommt");
ok(/Team&nbsp;Dogi|Team Dogi/.test(wandCrew.text), "crew.: dort steht Team Dogi");
ok(/DogFather/.test(wandCrew.text), "crew.: und DogFather daneben");
ok(!/Spicy/i.test(wandCrew.text), "crew.: von Spicy Media steht dort nichts mehr");
ok(!/Creator/i.test(wandCrew.text), "crew.: und vom Creator Workspace auch nicht");
ok(/die Modis/.test(wandCrew.text), "crew.: der Satz nennt DogFather und die Modis");
/* Die Kachelreihe ist weg -- auf einer Adresse mit genau einer Sorte
Zugang waere sie eine Bedienhuerde, keine Auswahl. */
const kachelnCrew = (wandCrew.text.match(/class="rolle"/g) || []).length;
const kachelnWs = (wandWs.text.match(/class="rolle"/g) || []).length;
ok(kachelnCrew === 3, `crew.: drei Rollenkacheln (gezaehlt: ${kachelnCrew})`);
for (const [rolle, name] of [["admin", "DogFather"], ["hand", "Rechte Hand"], ["modi", "Modi"]]) {
ok(wandCrew.text.includes(`data-rolle="${rolle}"`) && wandCrew.text.includes(`>${name}<`),
`crew.: die Kachel "${name}" steht da`);
}
/* Die Namen kommen aus den Kacheln, nicht aus einer Liste in gate.js --
sonst stuenden sie in einer Datei, die jeder bekommt. */
ok(!/Rechte Hand/.test(wandWs.text), "workspace.: dort steht die dritte Rolle nicht");
ok(kachelnWs === 5, `workspace.: dort weiterhin fuenf (gezaehlt: ${kachelnWs})`);
ok(/SpicyMedia/.test(wandWs.text), "workspace.: die alte Wand ist unveraendert");
/* Auch der ausgeschriebene Weg. Ihn zu vergessen waere nicht
aufgefallen -- im Alltag tippt ihn niemand. */
const wandCrewLang = await hole("/workspace/index.html", { host: CREW });
ok(wandCrewLang.status === 200 && /Team Dogi|Team&nbsp;Dogi/.test(wandCrewLang.text),
"crew.: auch /workspace/index.html fuehrt zur neuen Wand");
/* ---------------------------------------------------------------------
DIE ZWEITE WAND GIBT ES NUR AUF crew.
Sie liegt als Datei im selben Ordner wie alles andere. Ohne Sperre
koennte sie jeder unter ihrem Dateinamen aufrufen -- auch auf
workspace. -- und saehe dort eine Anmeldeseite, die es dort nicht
geben soll.
------------------------------------------------------------------ */
/* ZWEI NUMMERN, EINE AUSSAGE. Auf workspace. antwortet die Sperre mit
404 "gibt es nicht". Auf der Hauptseite und www. kommt schon der
Umzugs-Zwischenschritt vom 06.09.2026 davor und beantwortet den
GANZEN Pfad /workspace mit 410 "gab es, ist absichtlich weg".
Erwartet werden deshalb beide -- was zaehlt, ist: kein Inhalt und
keine 200. Auf eine der zwei Nummern zu bestehen hiesse, eine
fremde, aeltere Entscheidung nachtraeglich zu einem Fehler zu
erklaeren. */
for (const [name, host] of [["workspace.", WS], ["die Hauptseite", HAUPT], ["www.", "www." + HAUPT]]) {
const a = await hole("/workspace/crew-index.html", { host });
ok(a.status === 404 || a.status === 410,
`${name}: die zweite Wand gibt es dort nicht (${a.status})`);
ok(!/Team Dogi/.test(a.text), `${name}: und ihr Inhalt kommt auch nicht durch`);
}
/* Doppelte Schraegstriche haben diese Schranke im Haus schon einmal
umgangen (04.09.2026, //workspace/start.html kam mit 200). */
const schraeg = await hole("/workspace//crew-index.html", { host: WS });
ok(schraeg.status === 404 || !/Team Dogi/.test(schraeg.text),
`workspace.: auch mit doppeltem Schraegstrich nicht (${schraeg.status})`);
/* GEGENPROBE: Auf crew. und auf den Pruefadressen bleibt sie erreichbar
-- sonst haette ich sie fuer alle gesperrt, auch fuer die Pruefung,
die sie im Browser ansieht. Die waere dann gruen und wertlos. */
const dortSchon = await hole("/workspace/crew-index.html", { host: CREW });
ok(dortSchon.status === 200, "crew.: dort gibt es sie natuerlich");
const lokalSchon = await hole("/workspace/crew-index.html", { host: LOKAL });
ok(lokalSchon.status === 200, "127.0.0.1: und auf den Pruefadressen auch");
/* ---------------------------------------------------------------------
WAS gate.js AUS DEM HTML BRAUCHT -- auf BEIDEN Seiten.
Es gibt jetzt zwei Wanddateien und ein gemeinsames Skript. Wer nur
eine anfasst, macht die andere kaputt, und zwar lautlos: Ein
fehlendes Element ist im Browser ein `null`, der Fehler steht in der
Konsole und sonst nirgends. Deshalb wird beides gegen dieselbe Liste
gehalten.
------------------------------------------------------------------ */
const GEBRAUCHT = ["formular", "code", "auge", "fehler", "hinweis", "knopf", "fuer"];
for (const [name, wand] of [["crew.", wandCrew], ["workspace.", wandWs]]) {
const fehlend = GEBRAUCHT.filter((id) => !wand.text.includes(`id="${id}"`));
ok(fehlend.length === 0,
`${name} die Wand hat alle ${GEBRAUCHT.length} Elemente, die gate.js sucht`
+ (fehlend.length ? ` -- es fehlen: ${fehlend.join(", ")}` : ""));
ok(wand.text.includes("knopf__text") && wand.text.includes("buehne__bild"),
`${name} auch die beiden Klassen, die gate.js anspricht`);
}
/* ---------------------------------------------------------------------
Die Buehne.
------------------------------------------------------------------ */
ok(/crew-gate-\d+\.webp/.test(wandCrew.text), "crew.: die eigene Buehne ist eingebunden");
ok(!/buehne\/gate-\d+\.webp/.test(wandCrew.text), "crew.: die alte Buehne nicht mehr");
ok(/buehne\/gate-\d+\.webp/.test(wandWs.text), "workspace.: dort weiterhin die alte");
ok(/crew-32\.png/.test(wandCrew.text) && /crew-180\.png/.test(wandCrew.text),
"crew.: eigenes Zeichen im Reiter und auf dem Startbildschirm");
/* Ein Verweis beweist nicht, dass die Datei da ist. */
const buehnen = [...new Set(wandCrew.text.match(/assets\/img\/buehne\/crew-gate-[a-z0-9-]+\.webp/g) || [])];
let buehnenDa = 0;
for (const b of buehnen) {
const a = await hole("/workspace/" + b, { host: CREW });
if (a.status === 200) buehnenDa++;
}
ok(buehnen.length >= 8 && buehnenDa === buehnen.length,
`crew.: alle ${buehnenDa} von ${buehnen.length} Buehnenbildern werden ausgeliefert`);
/* =====================================================================
DIE GEGENPROBE
Bis hierhin koennte alles auch dann gruen sein, wenn die Regel gar
nicht auf die Rolle sieht. Deshalb wird sie jetzt ausgehebelt: Der
Modi bekommt in der Datenbank die Rolle 'manager'. Seine Sitzung
bleibt dieselbe -- aber auf crew. darf sie ab jetzt NICHT mehr
gelten.
===================================================================== */
console.log("\n--- Gegenprobe: die Regel ausgehebelt ---");
d.prepare("UPDATE personen SET rolle = 'manager' WHERE id = ?").run(idModi);
const nachher = await hole("/workspace/api/ich", { host: CREW, keks: keksModi });
ok(nachher.status === 401,
"aus dem Modi wurde ein Manager -- seine Sitzung gilt auf crew. nicht mehr");
const nachherWs = await hole("/workspace/api/ich", { host: WS, keks: keksModi });
ok(nachherWs.status === 200,
"und dieselbe Sitzung gilt jetzt auf workspace. -- die Regel sieht wirklich auf die Rolle");
d.prepare("UPDATE personen SET rolle = 'modi' WHERE id = ?").run(idModi);
const zurueck = await hole("/workspace/api/ich", { host: CREW, keks: keksModi });
ok(zurueck.status === 200, "zurueckgedreht gilt sie wieder");
/* ---------- Ende ------------------------------------------------------ */
d.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt die Datei manchmal */ }
console.log(`\n${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}`
+ ` -- ${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
process.exit(fehler === 0 ? 0 : 1);