Der Treff zieht in Team Dogi ein — die dritte Adresse ist wieder weg

Filipe, nachdem er die dritte Wand gesehen hatte: "das soll keine app
für sich sein, dass soll in der crew seite adaptiert werden, da rein
perfektionniert."

WAS VERSCHWINDET: treff.dogfather-universe.com samt Weiche
(treff-adresse.js), Zugangswand, Manifest und acht Bühnenbildern. Kein
DNS-Eintrag, kein zweiter Caddy-Block, kein zweites App-Symbol.

Die Gründe, die DAFÜR sprachen, stehen jetzt als Kommentar in
crew-adresse.js statt als Code -- sie gelten weiter, und beim nächsten
Mal wird jemand dieselbe Idee haben: eine App je Ursprung, und ein
Schloss mehr zwischen der Community und den Daten der Agentur.

WAS DER VERZICHT KOSTET, benannt statt übersehen:
 · Die Community steht vor derselben Wand wie das Team und liest dort
   die Namen der drei Team-Rollen. Filipes Entscheidung auf die Frage:
   vierte Kachel "Community" statt gar keiner Kacheln.
 · Ein Schloss weniger. Was vorher die Adresse getrennt hat, muss jetzt
   jede einzelne Regel halten -- deshalb prüft pruef-treff-werkzeuge
   seit heute zehn Dinge mehr (70 statt 60).

EINE LÜCKE, DIE DABEI AUFGEFALLEN IST -- und die es schon vorher gab:
Ein Mitglied der Community stand in der Auswahl, wen man zu einem
Termin einlädt, wem man eine Aufgabe gibt und mit wem man einen Chat
anfängt. Für DogFather bedeutet fast jede Liste "alle". Jetzt zu, über
eine Hülle (ohneAussen) um die fünf Listen, die von Zusammenarbeit
handeln -- damit sie auch für die Listen gilt, die es noch nicht gibt.
Die Verwaltungsliste in "Personen & Zugänge" zeigt sie weiterhin, sonst
legt er einen Zugang an und der verschwindet im selben Moment.

DREI BEFUNDE AUS DEN PRÜFUNGEN, wieder keiner vom Lesen:
 · Mein eigener Kommentar auf der Wand nannte eine Rolle der Agentur --
   in einer Datei, die jedes Community-Mitglied im Quelltext liest.
   Zum zweiten Mal an einem Tag, und zum zweiten Mal ausgerechnet in
   einem Kommentar, der erklärt, warum man das nicht tut.
 · Die Prüfung "der Satz nennt die Modis" durchsuchte den QUELLTEXT und
   wäre grün geblieben, obwohl der Satz auf dem Bildschirm längst ein
   anderer ist -- sie fand ihn in einem Kommentar. Sie misst jetzt die
   sichtbare Unterzeile.
 · Der erste Entwurf der Adressregel sperrte die Community auch auf
   localhost aus -- eine Bedingung durch AUSSCHLUSS, zum dritten Mal an
   einem Tag. Jede Zeile nennt jetzt die Adresse, FÜR DIE sie gilt.

Und zwei feste Zahlen weniger: Die Kachelreihe wird nicht mehr gezählt,
sondern beim Namen genannt (admin, hand, modi, gast) -- eine Zahl war
dort schon zweimal rot, ohne dass etwas kaputt war. Der App-Name wird
nicht nur geprüft, sondern der UNTERSCHIED zur Agenturadresse.

Die App heißt auf crew. jetzt "DogFather Universe" statt "Team Dogi" --
sie gehört ab heute beiden (Entscheidung Filipe).

Geprüft: rollen 312 · crew-adresse 132 · kalender 104 · sicht 84 ·
modi-verborgen 80 · chat-kanaele 79 · treff-werkzeuge 70 · haus-trennung
62 · treff 58 · chat 48 · treff-seiten 42 · bereiche-lesend 37 ·
start-ansicht 147 · css-klassen 30 · zwischenspeicher 21 · alle-wege 19 ·
rechtetafel 19 -- alles grün.

Die 58 bei pruef-treff sind elf weniger als gestern: Mit der dritten
Adresse fallen ihre 26 Weichen-Prüfungen weg, ersetzt durch 10 für die
eine Wand plus die neuen in 7b.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-11 18:38:05 +02:00
co-authored by Claude Opus 5
parent 1ebaf8a864
commit 39dd666b17
47 changed files with 856 additions and 987 deletions
+64 -26
View File
@@ -105,14 +105,36 @@ export const TEAM_DOGI_ROLLEN = new Set(["hand", "modi"]);
bekommt nur, was ausdruecklich fuer sie da ist. */
export const AUSSEN_ROLLEN = new Set(["gast"]);
/* Die Adresse des Treffs. Eigene Wand, eigenes Haus -- und ausdruecklich
NICHT crew.: Diese Adresse ist absichtlich verborgen (kein Eintrag bei
Suchmaschinen, keine Rollennamen in ausgelieferten Dateien, Codes nur
persoenlich). Dutzende Zuschauer heben jede dieser Eigenschaften auf.
Nicht durch einen Fehler, sondern durch die Nutzung. */
const TREFF_HOSTS = new Set(["treff.dogfather-universe.com"]);
/* DER TREFF HAT KEINE EIGENE ADRESSE (Entscheidung Filipe, 11.09.2026:
"das soll keine app fuer sich sein, das soll in der crew seite
adaptiert werden").
/** Die Adresse der Modi-App. Feste Menge, kein Muster -- Falle 1. */
Am Vormittag stand hier treff.dogfather-universe.com -- eine dritte
Tuer mit eigener Wand, eigenem Manifest, eigenen Buehnenbildern. Sie
ist wieder weg, und das gehoert aufgeschrieben, weil die Gruende
dafuer weiterhin gelten und beim naechsten Mal jemand dieselbe Idee
haben wird:
WAS DAFUER SPRACH: Auf einem Ursprung laesst sich genau EINE App
installieren. Und zwischen einem Mitglied der Community und den
Daten der Agentur stand damit ein Schloss mehr.
WAS DAGEGEN ENTSCHIEDEN HAT: Es waere ein drittes Haus gewesen --
mit eigener Wand, eigenem Namen, eigener App neben Team Dogi. Der
Treff ist aber kein drittes Haus, sondern ein Raum in Dogis.
WAS DER VERZICHT KOSTET, ehrlich benannt: Die Community steht jetzt
vor derselben Wand wie das Team und liest dort die Namen der drei
Team-Rollen (Filipes Entscheidung: vierte Kachel "Community" statt
gar keiner Kacheln). Und sie ist auf demselben Ursprung angemeldet
wie die Modis -- die Trennung haengt damit allein an der
Rechtetafel und an sitzungLesen(), nicht mehr zusaetzlich an der
Adresse. Das ist der Grund, warum pruef-treff seit heute NICHT
weniger prueft, sondern mehr: Was vorher die Adresse erledigt hat,
muss jetzt jede einzelne Regel halten. */
/** Die Adresse von Team Dogi -- und seit 11.09.2026 auch die des
* Treffs. Feste Menge, kein Muster -- Falle 1. */
const CREW_HOSTS = new Set(["crew.dogfather-universe.com"]);
/** Die echten Adressen, auf denen ein Modi nichts verloren hat.
@@ -155,11 +177,6 @@ export function istPruefAdresse(hostKopfzeile) {
|| h === "::1" || h === "[::1]";
}
/** Ist das die Adresse des Treffs? */
export function istTreffAdresse(hostKopfzeile) {
return TREFF_HOSTS.has(nurHost(hostKopfzeile));
}
/** Gehoert dieser Pfad zum Workspace-Teil? Genau "/workspace" oder
* alles darunter -- aber NICHT /workspaceXYZ (Falle 3). */
export function istWorkspacePfad(pfad) {
@@ -197,9 +214,12 @@ export function istWorkspacePfad(pfad) {
die jede Wand im Browser ansehen, keine mehr oeffnen -- sie blieben
gruen und wuerden nichts mehr messen. Ein gruener Lauf, der nichts
mehr prueft, ist schlimmer als ein roter. */
/* EINE benannte Wand, seit der Treff keine eigene mehr hat. Die Tafel
bleibt trotzdem eine Tafel und wird nicht wieder zum Sonderfall: Sie
ist der Ort, an dem die naechste Wand eingetragen wird, ohne dass
jemand drei Bedingungen umbauen muss. */
const BENANNTE_WAENDE = [
{ datei: "/workspace/crew-index.html", zuhause: istCrewAdresse },
{ datei: "/workspace/treff-index.html", zuhause: istTreffAdresse },
];
/** Wird hier eine Wand verlangt, die zu einer ANDEREN Tuer gehoert? */
@@ -212,8 +232,7 @@ export function istFremdeWand(hostKopfzeile, pfad) {
const wand = BENANNTE_WAENDE.find((w) => w.datei === p);
if (!wand) return false;
if (istPruefAdresse(hostKopfzeile)) return false;
const benannt = istOhneModiAdresse(hostKopfzeile)
|| istCrewAdresse(hostKopfzeile) || istTreffAdresse(hostKopfzeile);
const benannt = istOhneModiAdresse(hostKopfzeile) || istCrewAdresse(hostKopfzeile);
if (!benannt) return false;
return !wand.zuhause(hostKopfzeile);
}
@@ -234,18 +253,37 @@ export function sitzungPasstZurAdresse(rolle, hostKopfzeile) {
/* AUSSENROLLEN GEHOEREN AUF GENAU EINE ADRESSE (11.09.2026).
Und die Bedingung steht in BEIDE Richtungen: Die Community kommt
nur auf treff. herein, und auf treff. kommt NUR sie herein. Das
Team sieht den Treff auf seiner eigenen Adresse -- es hat dort
nichts zu suchen, und ein Konto, das an zwei Waenden funktioniert,
ist eine Tuer, die man beim naechsten Umbau uebersieht. */
NUR auf crew. herein (dort liegt der Treff), und auf den alten
Adressen kommt sie gar nicht herein. Ein Konto, das an zwei
Waenden funktioniert, ist eine Tuer, die man beim naechsten Umbau
uebersieht.
SEIT DEM 11.09.2026 IST DIESE ADRESSE crew. UND NICHT treff.
(Filipe: "das soll keine app fuer sich sein"). Die Richtung der
Regel hat sich dadurch umgedreht, die Regel selbst nicht: Es gibt
genau eine Adresse, auf der die Community angemeldet sein darf. */
const aussen = AUSSEN_ROLLEN.has(rolle);
if (istTreffAdresse(hostKopfzeile)) return aussen;
if (aussen) return false;
/* crew.: das Team -- und DogFather, der in beiden Welten arbeitet. */
if (istCrewAdresse(hostKopfzeile)) return team || rolle === "admin";
/* Die echten alten Adressen: alle ausser dem Team. */
if (istOhneModiAdresse(hostKopfzeile)) return !team;
/* localhost, Pruefadressen: unberuehrt. */
/* crew.: das Team, die Community -- und DogFather, der in beiden
Welten arbeitet. */
if (istCrewAdresse(hostKopfzeile)) return team || aussen || rolle === "admin";
/* Die echten alten Adressen: alle ausser dem Team und der Community. */
if (istOhneModiAdresse(hostKopfzeile)) return !team && !aussen;
/* localhost, Pruefadressen: unberuehrt.
UND DAS GILT AUCH FUER DIE COMMUNITY (gemessen 11.09.2026). Der
erste Entwurf hatte hier ein "wer von aussen ist, kommt sonst
nirgends herein" stehen -- eine Bedingung durch AUSSCHLUSS, und sie
traf damit auch localhost. Ergebnis: Jede Pruefung des Treffs haette
ab sofort gemessen, dass ein Mitglied nirgends hereinkommt, und
waere gruen geblieben, ohne noch irgendetwas zu pruefen. Genau die
Falle, vor der oben in diesem Kopf Nummer 2 warnt -- und ich bin
am selben Tag zum dritten Mal hineingelaufen.
Jetzt nennt jede Zeile die Adresse, FUER DIE sie gilt. Wer eine
vierte baut, faellt hier unten heraus und bekommt "erlaubt" --
das ist auf einer Pruefadresse richtig und auf einer echten faellt
es sofort auf, weil pruef-crew-adresse jede echte Adresse
einzeln aufzaehlt. */
return true;
}
-16
View File
@@ -13,7 +13,6 @@ import { fileURLToPath } from "node:url";
import { gateMiddleware } from "./gate.js";
import { workspaceUmzug } from "./workspace-umzug.js";
import { crewWeiche } from "./crew-adresse.js";
import { treffWeiche } from "./treff-adresse.js";
import { webdesignGate } from "./webdesign-gate.js";
import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js";
import { workspaceRouter, sichtSetzen } from "./workspace.js";
@@ -129,21 +128,6 @@ app.use(workspaceUmzug);
gehen. Zwei Orte fuer dieselbe Regel waeren einer zu viel. */
app.use(crewWeiche);
/* Die eigene Adresse des Treffs (11.09.2026): treff.dogfather-universe.com.
Begruendung in treff-adresse.js, geprueft in pruef-treff-adresse.mjs.
DIREKT HINTER crewWeiche UND NICHT DAVOR. Die Reihenfolge ist keine
Geschmacksfrage: crewWeiche beantwortet ueber die Wandtafel fuer JEDE
Adresse die Frage "wird hier eine fremde Zugangswand verlangt?" --
auch fuer treff. Stuende treffWeiche davor, wuerde sie
/workspace/crew-index.html erst auf die Treff-Wand umbiegen und die
Tafel kaeme nie zum Zug. Ein Mitglied der Community saehe dann keine
404, sondern seine eigene Wand -- und niemandem waere aufgefallen,
dass die Sperre nicht mehr greift.
Auch sie vergibt KEINE Rechte. Wer auf welcher Adresse angemeldet
sein darf, entscheidet sitzungLesen() in workspace.js. */
app.use(treffWeiche);
/* Inhaltsrichtlinie (26.08.2026). Steht bewusst HIER -- vor jeder
Schranke und vor express.static: Auch die Zugangswand und die
+50 -5
View File
@@ -320,7 +320,22 @@ 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 || "?"})`);
/* DER NAME GILT SEIT DEM 11.09.2026 FUER BEIDE (Entscheidung Filipe).
Er hiess "Team Dogi". Seit der Treff hier wohnt, wuerde ein Mitglied
der Community diesen Namen auf seinem Startbildschirm haben -- den
Namen eines Bereichs, zu dem es nicht gehoert. Das Manifest gilt je
ADRESSE und kann sich nicht nach der Rolle richten; also einer, der
fuer beide stimmt.
GEPRUEFT WIRD NICHT NUR DER NAME, SONDERN DER UNTERSCHIED. Ein
fester Name allein waere beim naechsten Umbenennen wieder rot, ohne
dass etwas kaputt ist. Dass die beiden Adressen sich UNTERSCHEIDEN,
ist die Eigenschaft, um die es geht -- sonst waere es eine App. */
ok(mc.name === "DogFather Universe",
`crew.: die App heisst "DogFather Universe" (gelesen: ${mc.name || "?"})`);
ok(mc.name && mw.name && mc.name !== mw.name,
`und anders als auf der Agenturadresse ("${mw.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
@@ -367,13 +382,43 @@ 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");
/* DER SATZ AUF DER WAND MUSS FUER BEIDE STIMMEN (11.09.2026).
/* Die Kachelreihe ist weg -- auf einer Adresse mit genau einer Sorte
Zugang waere sie eine Bedienhuerde, keine Auswahl. */
Hier stand `/die Modis/` -- und der Satz lautete "Privater Bereich
fuer DogFather und die Modis". Seit die Community durch dieselbe Wand
kommt, liest sie ihn auch, und fuer sie ist er schlicht falsch.
UND DIE ALTE PRUEFUNG WAERE TROTZDEM GRUEN GEBLIEBEN: Sie durchsuchte
den QUELLTEXT, und dort steht "die Modis" weiterhin in einem
Kommentar. Sie haette also bestaetigt, dass ein Satz dasteht, den es
auf dem Bildschirm gar nicht mehr gibt. Gemessen wird deshalb die
sichtbare Unterzeile, nicht die Datei. */
{
const unter = /<p class="unterzeile">([^<]*)<\/p>/.exec(wandCrew.text)?.[1] || "";
ok(/Team\s*Dogi/.test(unter) && /Treff/.test(unter),
`crew.: die Unterzeile nennt Team Dogi und den Treff ("${unter.trim()}")`);
ok(!/Arbeitsbereich/.test(unter),
"und behauptet nicht mehr, das hier sei nur zum Arbeiten");
}
/* DIE KACHELREIHE HAT SEIT DEM 11.09.2026 VIER EINTRAEGE.
Die Geschichte dieser Zeile ist lehrreich und steht deshalb hier:
Erst gab es keine Kachelreihe (eine Sorte Zugang, eine Reihe waere
eine Huerde gewesen). Dann drei ("3 rollen. dogfather. rechte hand
und modis"). Jetzt vier, weil der Treff hier eingezogen ist.
GEPRUEFT WIRD NICHT DIE ZAHL, SONDERN WELCHE. Eine Zahl war schon
zweimal rot, ohne dass etwas kaputt war. Die NAMEN dagegen sagen
etwas: Steht dort eines Tages "manager", ist das ein Befund und kein
Umbau. */
const rollenAufWand = [...wandCrew.text.matchAll(/data-rolle="([a-z]+)"/g)].map((m) => m[1]);
ok(JSON.stringify(rollenAufWand) === JSON.stringify(["admin", "hand", "modi", "gast"]),
`crew.: die vier Kacheln in dieser Reihenfolge (${rollenAufWand.join(", ")})`);
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})`);
ok(kachelnCrew === rollenAufWand.length,
`crew.: ${kachelnCrew} Rollenkacheln -- so viele, wie es Rollen auf der Wand gibt`);
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`);
+95 -23
View File
@@ -64,9 +64,9 @@ 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 HAUPT = "dogfather-universe.com";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
@@ -153,8 +153,8 @@ async function anmelden(rolle, code, host) {
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);
const kKessi = await anmelden("gast", "CODE-GAST-0001", CREW);
const kBo = await anmelden("gast", "CODE-GAST-0002", CREW);
ok(!!kDogi && !!kModi && !!kKessi && !!kBo, "alle vier sind angemeldet");
/* =======================================================================
@@ -164,24 +164,24 @@ melde("");
melde("=== 1. Will ich auch ===");
{
const weg = `/workspace/api/treff/eintrag/${wunsch1}/willich`;
const a = await roh(weg, TREFF, { cookie: kKessi }, {}, "PUT");
const a = await roh(weg, CREW, { 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");
const b = await roh(weg, CREW, { 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");
const c = await roh(weg, CREW, { 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");
const e = await roh(weg, CREW, { 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 brett = await roh("/workspace/api/bereich/wunsch", CREW, { 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})`);
@@ -190,7 +190,7 @@ melde("=== 1. Will ich auch ===");
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,
const f = await roh(`/workspace/api/treff/eintrag/${bosBeitrag}/willich`, CREW,
{ cookie: kKessi }, {}, "PUT");
ok(f.code === 400, `im Gespraech gibt es kein "Will ich auch" (${f.code})`);
}
@@ -225,7 +225,7 @@ melde("=== 2. Anheften ===");
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,
const gast = await roh(`/workspace/api/treff/eintrag/${anschlag[1]}/anheften`, CREW,
{ cookie: kKessi }, {}, "PUT");
ok(gast.code === 404, `ein Mitglied kann nichts anheften (${gast.code})`);
@@ -241,13 +241,13 @@ melde("");
melde("=== 3. Melden ===");
{
const weg = `/workspace/api/treff/eintrag/${bosBeitrag}/melden`;
const ohne = await roh(weg, TREFF, { cookie: kKessi }, { grund: "" });
const ohne = await roh(weg, CREW, { cookie: kKessi }, { grund: "" });
ok(ohne.code === 400, `ohne Grund geht es nicht (${ohne.code})`);
const kurz = await roh(weg, TREFF, { cookie: kKessi }, { grund: "doof" });
const kurz = await roh(weg, CREW, { cookie: kKessi }, { grund: "doof" });
ok(kurz.code === 400, `"doof" ist kein Grund (${kurz.code})`);
const mit = await roh(weg, TREFF, { cookie: kKessi },
const mit = await roh(weg, CREW, { cookie: kKessi },
{ grund: "Da steht die Handynummer von jemandem drin." });
ok(mit.code === 200, `mit Grund geht es (${mit.code})`);
@@ -260,7 +260,7 @@ melde("=== 3. Melden ===");
/* Die Community sieht die Meldungen NICHT -- sonst waere ablesbar,
wer wen gemeldet hat. */
const gast = await roh("/workspace/api/treff/meldungen", TREFF, { cookie: kBo });
const gast = await roh("/workspace/api/treff/meldungen", CREW, { cookie: kBo });
ok(gast.code === 404, `ein Mitglied sieht die Liste nicht (${gast.code})`);
/* Entscheiden braucht ebenfalls einen Satz. */
@@ -366,7 +366,7 @@ melde("=== 6. Ausschluss ===");
/* "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 });
const vorher = await roh("/workspace/api/treff/lage", CREW, { cookie: kKessi });
ok(vorher.code === 200 && json(vorher).stufe === "dabei",
`Kessi ist drin, Stufe ${json(vorher).stufe}`);
@@ -378,24 +378,24 @@ melde("=== 6. Ausschluss ===");
/* 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 });
const lage = await roh("/workspace/api/treff/lage", CREW, { cookie: kKessi });
ok(lage.code === 401, `ihre Sitzung gilt nicht mehr (${lage.code})`);
const brett = await roh("/workspace/api/bereich/treff", TREFF, { cookie: kKessi });
const brett = await roh("/workspace/api/bereich/treff", CREW, { 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);
const neu = await anmelden("gast", "CODE-GAST-0001", CREW);
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 });
const boLage = await roh("/workspace/api/treff/lage", CREW, { 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)})`);
@@ -403,10 +403,10 @@ melde("=== 6. Ausschluss ===");
/* 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);
const pim = await anmelden("gast", "CODE-GAST-0003", CREW);
ok(!pim, "ein befristeter Ausschluss sperrt ebenfalls aus");
const versuch = await roh("/workspace/api/bereich/treff", TREFF, { cookie: kBo },
const versuch = await roh("/workspace/api/bereich/treff", CREW, { cookie: kBo },
{ art: ersteArt("treff"), titel: "BO-TROTZDEM", text: "Hallo?", datum: heute });
ok(versuch.code === 403, `und es geht auch wirklich nicht (${versuch.code})`);
}
@@ -431,17 +431,89 @@ melde("=== 7. Die Bruecke zu den Talenten ===");
/* 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 });
const gast = await roh("/workspace/api/bereich/talente", CREW, { cookie: kBo });
ok(gast.code === 404, `ein Mitglied sieht die Talente nicht (${gast.code})`);
}
/* =======================================================================
7b. DIE COMMUNITY STEHT IN KEINER ARBEITSLISTE — aber in der Verwaltung
Seit der Treff in Team Dogi eingezogen ist, sind Mitglieder und Team
auf derselben Adresse. Was vorher die Adresse getrennt hat, muss
jetzt jede einzelne Liste halten.
DREI LISTEN, ZWEI ANTWORTEN. Einladen und Chatten: nein, und zwar
auch für DogFather, der sonst überall "alle" bekommt. Verwaltung: ja
— sonst legt er einen Zugang an und der verschwindet im selben
Moment aus der Liste.
MIT GEGENPROBE IN BEIDE RICHTUNGEN: Jede "steht nicht drin"-Zeile
steht neben einer, die beweist, dass die Liste überhaupt jemanden
enthält. Eine leere Liste erfüllt sonst jede Nein-Bedingung.
======================================================================= */
melde("");
melde("=== 7b. Keine Arbeitsliste, aber die Verwaltung ===");
{
const namen = (a) => {
try {
const j = JSON.parse(a.text);
const feld = Object.values(j).find(Array.isArray) || [];
return feld.map((x) => x?.name).filter(Boolean);
} catch { return []; }
};
for (const [was, weg] of [
["die Einladungsliste", "/workspace/api/einladbar"],
["die Chat-Partner", "/workspace/api/chat/partner"],
]) {
const a = await roh(weg, HAUS, { cookie: kDogi });
const liste = namen(a);
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG: Wäre die Liste leer, wäre
"Kessi steht nicht drin" trivial wahr. */
ok(a.code === 200 && liste.length > 0,
`${was} hat überhaupt Einträge (${liste.length})`);
ok(!liste.includes("Kessi") && !liste.includes("Bo"),
`${was} enthält niemanden aus der Community${
liste.filter((n) => n === "Kessi" || n === "Bo").join(", ")}`);
ok(liste.includes("Frida"),
`aber den Modi — ${was} filtert nicht einfach alles weg`);
}
/* UND DIE VERWALTUNG ZEIGT SIE. Auf der Team-Adresse, denn dort
wohnt der Treff jetzt. */
const verwaltung = await roh("/workspace/api/personen", CREW, { cookie: kDogi });
const inVerwaltung = namen(verwaltung);
ok(verwaltung.code === 200 && inVerwaltung.includes("Kessi"),
`in "Personen & Zugänge" auf crew. steht die Community (${inVerwaltung.length} Personen)`);
ok(inVerwaltung.includes("Frida"),
"und das Team ebenfalls — eine Liste, nicht zwei");
/* Die Rolle lässt sich dort auch VERGEBEN. Ein Knopf, den es nicht
gibt, macht die Liste nutzlos: Ohne ihn kann DogFather nirgends
einen Zugang zum Treff anlegen. */
const ich = await roh("/workspace/api/ich", CREW, { cookie: kDogi });
let darf = [];
try { darf = JSON.parse(ich.text).darf_anlegen || []; } catch { /* bleibt leer */ }
ok(darf.includes("gast"),
`und DogFather darf dort Community-Zugänge anlegen (${darf.join(", ")})`);
/* GEGENPROBE: Ein Modi darf das nicht — Zugänge vergibt DogFather.
Ohne diese Zeile wäre die darüber auch dann grün, wenn es jeder
dürfte. */
const ichModi = await roh("/workspace/api/ich", CREW, { cookie: kModi });
let darfModi = [];
try { darfModi = JSON.parse(ichModi.text).darf_anlegen || []; } catch { /* leer */ }
ok(!darfModi.includes("gast"),
`ein Modi nicht (${darfModi.length ? darfModi.join(", ") : "gar nichts"})`);
}
/* =======================================================================
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 a = await roh("/workspace/api/treff/regeln", CREW, { cookie: kBo });
const r = json(a);
ok(a.code === 200 && r.mindestalter === treffModul.MINDESTALTER,
`Mindestalter ${r.mindestalter} -- dieselbe Zahl wie im Code`);
+143 -131
View File
@@ -1,5 +1,5 @@
/* =====================================================================
DER TREFF — die Community und ihre sieben Bretter (11.09.2026)
DER CREW — die Community und ihre sieben Bretter (11.09.2026)
Filipe:
@@ -66,9 +66,9 @@ 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 HAUPT = "dogfather-universe.com";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
@@ -103,7 +103,7 @@ const rein = d.prepare(
+ " VALUES (?,?,?,?,?,?,?)");
const BRETTER = ["anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln", "mitmachen"];
for (const b of BRETTER) rein.run(b, Object.keys((await import("./workspace-bereiche.js")).BEREICHE[b].arten)[0],
`TREFF-PROBE-${b.toUpperCase()}`, "", jetzt.slice(0, 10), jetzt, idDogi);
`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();
@@ -140,23 +140,47 @@ async function anmelden(rolle, code, host) {
======================================================================= */
melde("");
melde("=== 1. Eine Rolle, eine Wand ===");
const gast = await anmelden("gast", "CODE-GAST-0001", TREFF);
ok(gast.code === 200 && gast.keks.length > 0, `die Community kommt auf treff. herein (${gast.code})`);
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})`);
for (const [name, host] of [["crew.", CREW], ["workspace.", HAUS]]) {
/* 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);
ok(a.code === 401 && a.keks === "", `auf ${name} nicht (${a.code})`);
/* 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})`);
}
{
/* UND DIE GEGENRICHTUNG: Auf der Wand des Treffs kommt sonst niemand
herein -- auch DogFather nicht. Er hat dort nichts zu suchen; er
sieht den Treff auf seiner eigenen Adresse. */
const a = await anmelden("admin", "CODE-DOGI-0001", TREFF);
const b = await anmelden("modi", "CODE-MODI-0001", TREFF);
ok(a.code !== 200 && b.code !== 200,
`auf treff. kommt sonst niemand herein -- auch DogFather nicht (${a.code}/${b.code})`);
/* 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;
@@ -166,108 +190,96 @@ const kMana = (await anmelden("manager", "CODE-MANA-0001", HAUS)).keks;
ok([kDogi, kModi, kCrea, kMana].every((k) => k.length > 0), "die vier Vergleichsrollen sind angemeldet");
/* =======================================================================
1b. DIE WEICHE: drei Adressen, drei Türen, keine Verwechslung
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. Die Weiche des Treffs ===");
melde("=== 1b. Eine Wand für beide ===");
{
/* DIE WAND. Ohne die Weiche stünde auf der Adresse der Community die
Wand der Agentur: Spicy-Media-Logo, "Creator Workspace", fünf
Rollenkacheln mit den Namen Admin, Manager, Scout, Creator -- die
komplette Struktur des Unternehmens, VOR der Anmeldung, für jeden,
der die Adresse hat. Das ist das eigentliche Leck, gegen das
treffWeiche gebaut ist. */
for (const pfad of ["/workspace/", "/workspace/index.html"]) {
const a = await roh(pfad, TREFF);
const ist = a.code === 200 && /Der&nbsp;Treff|Der Treff/.test(a.text);
ok(ist, `${pfad} zeigt die Wand des Treffs (${a.code})`);
ok(!/Creator\s*Workspace|Spicy/i.test(a.text),
`und nennt weder Workspace noch Spicy (${pfad})`);
}
/* 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.
/* KEINE ROLLENNAMEN DES HAUSES in der ausgelieferten Datei. Die
beiden anderen Wände nennen ihre Rollen offen -- dort ist das
richtig, denn dort steht nur, wer ohnehin dazugehört. Hier steht
ein Mensch, der NICHT zum Team gehört. */
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 a = await roh("/workspace/treff-index.html", TREFF);
const verraten = ["manager", "scout", "creator", "modi", "rechte hand"]
.filter((w) => new RegExp(w, "i").test(a.text));
ok(verraten.length === 0,
`die Wand nennt keine Rolle des Hauses${verraten.length ? " -- FUND: " + verraten.join(", ") : ""}`);
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");
}
/* DAS MANIFEST MACHT DIE APP. Drei Adressen, drei Namen -- sonst
hieße die App auf dem Startbildschirm eines Mitglieds "Team Dogi",
der Name eines Bereichs, zu dem es nicht gehört. */
const namen = {};
for (const [was, host] of [["treff", TREFF], ["crew", CREW], ["haus", HAUS]]) {
const a = await roh("/workspace/app.webmanifest", host);
try { namen[was] = JSON.parse(a.text).name; } catch { namen[was] = `(kein JSON: ${a.code})`; }
}
ok(namen.treff === "Der Treff", `das Manifest heißt "${namen.treff}"`);
ok(namen.crew === "Team Dogi" && namen.treff !== namen.crew,
`und auf crew. weiterhin "${namen.crew}"`);
ok(new Set(Object.values(namen)).size === 3,
`drei Adressen, drei Namen (${Object.values(namen).join(" · ")})`);
/* 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]);
ok(kacheln.length === 4 && kacheln.includes("gast"),
`vier Kacheln, eine davon die Community: ${kacheln.join(", ")}`);
ok(kacheln[kacheln.length - 1] === "gast",
"und sie steht zuletzt -- wer täglich kommt, findet oben, was er täglich braucht");
/* JEDE WAND GEHÖRT ZU GENAU EINER TÜR. Beide Richtungen, sonst
beweist es nichts: Die eigene Wand MUSS erreichbar sein. */
const waende = [
["/workspace/crew-index.html", CREW, TREFF],
["/workspace/treff-index.html", TREFF, CREW],
["/workspace/treff-index.html", TREFF, HAUS],
];
for (const [datei, zuhause, fremd] of waende) {
const a = await roh(datei, zuhause);
const b = await roh(datei, fremd);
ok(a.code === 200, `${datei} gibt es zu Hause (${a.code})`);
ok(b.code === 404, `und auf der fremden Adresse nicht (${b.code})`);
}
/* Auch mit doppeltem Schrägstrich -- express.static zieht sie später
selbst zusammen und liefert die Datei aus. Genau daran ist am
04.09.2026 die Schranke des Workspace vorbeigelaufen. */
const schraeg = await roh("/workspace//treff-index.html", CREW);
ok(schraeg.code === 404, `auch mit doppeltem Schrägstrich nicht (${schraeg.code})`);
/* 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. */
const kopf = await roh("/workspace/", TREFF);
ok(/noindex/i.test(kopf.kopf?.["x-robots-tag"] || ""),
`der Kopf sagt noindex (${kopf.kopf?.["x-robots-tag"]})`);
/* NUR DER WORKSPACE. Sonst läge die komplette Dogfather-Website ein
zweites Mal unter einer zweiten Adresse. */
const haupt = await roh("/", TREFF);
ok(haupt.code === 302 && haupt.kopf?.location === "/workspace/",
`die Hauptseite gibt es hier nicht (${haupt.code} → ${haupt.kopf?.location})`);
/* DAS HAUS. haus.css ist auf der Agenturadresse leer; auf treff.
muss dahinter das Farbhaus von Team Dogi stehen -- sonst käme die
Seite grau an und färbte sich erst im Browser um. */
const haus = await roh("/workspace/assets/css/haus.css?v=1", TREFF);
ok(haus.code === 200 && /--sternenfeld/.test(haus.text),
`haus.css bringt das Farbhaus mit (${haus.text.length} Zeichen)`);
/* DIE BÜHNEN INNEN. Die Motive der Agentur zeigen das
Spicy-Media-Logo selbst -- teils vielfach nebeneinander. */
const a1 = await roh("/workspace/assets/img/buehne-lounge-breit.webp", TREFF);
const a2 = await roh("/workspace/assets/img/crew-buehne-lounge-breit.webp", TREFF);
const a3 = await roh("/workspace/assets/img/buehne-lounge-breit.webp", HAUS);
ok(a1.code === 200 && a1.text.length === a2.text.length,
`die Bühne kommt von Team Dogi (${a1.text.length} = ${a2.text.length})`);
ok(a1.text.length !== a3.text.length,
`und ist eine andere als die der Agentur (${a1.text.length} ≠ ${a3.text.length})`);
/* DAS ZEICHEN IN DER KOPFLEISTE. start.css setzt dort eine Chili --
als CSS-Hintergrund, auf jeder Seite, den ganzen Tag. */
const m1 = await roh("/workspace/assets/img/marke-spicy.webp", TREFF);
const m2 = await roh("/workspace/assets/img/marke-husky.webp", TREFF);
const m3 = await roh("/workspace/assets/img/marke-spicy.webp", HAUS);
ok(m1.code === 200 && m1.text.length === m2.text.length,
`in der Kopfleiste steht der Husky (${m1.text.length})`);
ok(m1.text.length !== m3.text.length,
`und nicht die Chili (${m1.text.length} ≠ ${m3.text.length})`);
ok(/noindex/i.test(wand.kopf?.["x-robots-tag"] || ""),
`der Kopf sagt noindex (${wand.kopf?.["x-robots-tag"]})`);
}
/* =======================================================================
@@ -277,7 +289,7 @@ melde("");
melde("=== 2. Sieben Kacheln — und keine achte ===");
let namen = [];
{
const a = await roh("/workspace/api/ich", TREFF, { cookie: gast.keks });
const a = await roh("/workspace/api/ich", CREW, { cookie: gast.keks });
const j = JSON.parse(a.text);
ok(Array.isArray(j.bereiche) && j.bereiche.length === 7,
`die Community bekommt sieben Kacheln (${j.bereiche?.length})`);
@@ -330,7 +342,7 @@ let namen = [];
/* UND DIE MODERATIONSKACHEL IST BEI DER COMMUNITY WIRKLICH NICHT DA
-- nicht nur "nicht in dieser Liste". Die Kachel ist ein Weg; wenn
der Weg zu ist, muss auch die Tuer zu sein. */
const d = JSON.parse((await roh("/workspace/api/ich", TREFF, { cookie: gast.keks })).text);
const 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)`);
}
@@ -349,8 +361,8 @@ async function brett(b, host, keks) {
{
let gefunden = 0;
for (const b of BRETTER) {
const r = await brett(b, TREFF, gast.keks);
if (r.code === 200 && r.titel.includes(`TREFF-PROBE-${b.toUpperCase()}`)) gefunden++;
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.
@@ -368,8 +380,8 @@ async function brett(b, host, keks) {
Bretter aus einem anderen Grund leer sind. */
let vorher = 0;
for (const b of treffModul.TREFF_FREIGABE_BRETTER) {
const r = await brett(b, TREFF, gast.keks);
if (r.titel.includes(`TREFF-PROBE-${b.toUpperCase()}`)) vorher++;
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})`);
@@ -378,7 +390,7 @@ async function brett(b, host, keks) {
for (const b of treffModul.TREFF_FREIGABE_BRETTER) {
const beiDogi2 = await roh(`/workspace/api/bereich/${b}`, HAUS, { cookie: kDogi });
const eintrag = (JSON.parse(beiDogi2.text).eintraege || [])
.find((e) => e.titel === `TREFF-PROBE-${b.toUpperCase()}`);
.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");
@@ -389,8 +401,8 @@ async function brett(b, host, keks) {
let nachher = 0;
for (const b of treffModul.TREFF_FREIGABE_BRETTER) {
const r = await brett(b, TREFF, gast.keks);
if (r.titel.includes(`TREFF-PROBE-${b.toUpperCase()}`)) nachher++;
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})`);
@@ -409,7 +421,7 @@ async function brett(b, host, keks) {
const fremde = ["ideen", "angebote", "rueckmeldung", "entwicklung", "talente", "agentur", "live", "content"];
const durch = [];
for (const b of fremde) {
const r = await brett(b, TREFF, gast.keks);
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(", ") : ""}`);
@@ -420,25 +432,25 @@ async function brett(b, host, keks) {
bin ich am 10.09. schon einmal hereingefallen und haette fast ein
Loch repariert, das keines war. Gezaehlt wird, was die Suche
WIRKLICH gefunden hat. */
const suche = await roh("/workspace/api/suche?q=HAUS-GEHEIM-4711", TREFF, { cookie: gast.keks });
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 TREFF NICHT. Beide Richtungen, beide
/* 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("TREFF-PROBE"))) durch.push(b);
if (r.code === 200 && r.titel.some((t) => t.startsWith("CREW-PROBE"))) durch.push(b);
}
ok(durch.length === 0, `eine Creatorin sieht kein Brett des Treffs${durch.length ? " -- DURCH: " + durch.join(", ") : ""}`);
const durchM = [];
for (const b of BRETTER) {
const r = await brett(b, HAUS, kMana);
if (r.code === 200 && r.titel.some((t) => t.startsWith("TREFF-PROBE"))) durchM.push(b);
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(", ") : ""}`);
@@ -448,7 +460,7 @@ async function brett(b, host, keks) {
let sieht = 0;
for (const b of BRETTER) {
const r = await brett(b, CREW, kModi);
if (r.code === 200 && r.titel.includes(`TREFF-PROBE-${b.toUpperCase()}`)) sieht++;
if (r.code === 200 && r.titel.includes(`CREW-PROBE-${b.toUpperCase()}`)) sieht++;
}
ok(sieht === 7, `ein Modi sieht alle sieben (${sieht}/7)`);
}
@@ -488,7 +500,7 @@ melde("=== 4. Nur die Seiten aus der Tafel ===");
const zu = [];
for (const p of erlaubt) {
const a = await roh(p, TREFF, { cookie: gast.keks });
const a = await roh(p, CREW, { cookie: gast.keks });
if (a.code !== 200) zu.push(`${p.split("/").pop()} (${a.code})`);
}
ok(zu.length === 0,
@@ -496,7 +508,7 @@ melde("=== 4. Nur die Seiten aus der Tafel ===");
const durch = [];
for (const p of verboten) {
const a = await roh(p, TREFF, { cookie: gast.keks });
const a = await roh(p, CREW, { cookie: gast.keks });
if (a.code === 200) durch.push(p.split("/").pop());
}
ok(durch.length === 0,
@@ -507,9 +519,9 @@ melde("=== 4. Nur die Seiten aus der Tafel ===");
wirklich weh täte: Dort stehen die Namen derer, die gemeldet
haben. Eine eigene Zeile dafür, damit der Befund einen Namen hat
und nicht in "eine von 20" untergeht. */
const mod = await roh("/workspace/treff-moderation.html", TREFF, { cookie: gast.keks });
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", TREFF, { cookie: gast.keks });
const modApi = await roh("/workspace/api/treff/meldungen", CREW, { cookie: gast.keks });
ok(modApi.code === 404, `und die Meldungen dahinter auch (${modApi.code})`);
}
@@ -525,7 +537,7 @@ for (const [name, pfad] of [
["Personen", "/workspace/api/personen"],
["Gespräche", "/workspace/api/chat/raeume"],
]) {
const a = await roh(pfad, TREFF, { cookie: gast.keks });
const a = await roh(pfad, CREW, { cookie: gast.keks });
let leer = a.code !== 200;
if (a.code === 200) {
try {
@@ -551,11 +563,11 @@ melde("=== 6. Schreiben ===");
ist, liest und stimmt ab -- mehr nicht. Das wird von BEIDEN
Seiten der Grenze gemessen, sonst beweist es nichts: Eine
Schreibsperre, die immer sperrt, ist keine Stufe. */
const neu = await roh("/workspace/api/bereich/treff", TREFF, { cookie: gast.keks },
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", TREFF, { cookie: gast.keks })).text);
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");
@@ -567,7 +579,7 @@ melde("=== 6. Schreiben ===");
.run(new Date(Date.now() - 8 * 86400000).toISOString(), idGast);
dd.close();
const a = await roh("/workspace/api/bereich/treff", TREFF, { cookie: gast.keks },
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})`);
@@ -578,7 +590,7 @@ melde("=== 6. Schreiben ===");
dd2.prepare("UPDATE personen SET erstellt = ? WHERE id = ?")
.run(new Date(Date.now() - 6 * 86400000).toISOString(), idGast);
dd2.close();
const knapp = await roh("/workspace/api/bereich/treff", TREFF, { cookie: gast.keks },
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);
@@ -587,7 +599,7 @@ melde("=== 6. Schreiben ===");
dd3.close();
/* Und auf ein Brett des Teams kommt er auch als "Dabei" nicht. */
const brettDesTeams = await roh("/workspace/api/bereich/anschlag", TREFF, { cookie: gast.keks },
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})`);
@@ -601,7 +613,7 @@ melde("=== 6. Schreiben ===");
}
{
/* UND NICHT IN EIN BRETT DES HAUSES. */
const a = await roh("/workspace/api/bereich/ideen", TREFF, { cookie: gast.keks },
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})`);
}
-3
View File
@@ -300,9 +300,6 @@ export const SEITEN = {
export const OHNE_ANMELDUNG = [
"/workspace/index.html",
"/workspace/crew-index.html",
/* Die Wand des Treffs (11.09.2026). Dritte Adresse, dritte Wand --
und die erste, vor der jemand steht, der NICHT zum Team gehoert. */
"/workspace/treff-index.html",
];
/* =====================================================================
-171
View File
@@ -1,171 +0,0 @@
/* =====================================================================
Die eigene Adresse des Treffs (11.09.2026)
treff.dogfather-universe.com
Auftrag (Filipe, 11.09.2026): *"eine community rolle und community
kategorie, wo die community auch dann nur die sieht."*
---------------------------------------------------------------------
WARUM EINE DRITTE ADRESSE UND NICHT EINE DRITTE KACHEL
Drei Gruende, und jeder allein wuerde schon reichen:
1. EINE APP JE URSPRUNG. Auf einem Ursprung laesst sich genau EINE
App installieren (w3c/manifest Nr. 1180: verschachtelte Bereiche
sind "strongly not recommended", Chrome haelt sich daran). Team
Dogi hat seine App, der Workspace hat seine. Ohne eigene Adresse
haette die Community gar keine.
2. EINE FALSCHE ANMELDUNG DARF NICHT REICHEN. Waere der Treff eine
Kachel auf der Wand des Workspace, traefe jeder Tippfehler in
der Rechteprueung sofort echte Zuschauer. So steht zwischen
einem Mitglied der Community und den Daten der Agentur nicht nur
eine Rollenabfrage, sondern eine Adresse, ein Zwischenschritt
und die Adressregel in sitzungLesen(). Drei Schloesser -- faellt
eines, halten zwei.
3. DIE COMMUNITY IST DER ERSTE KREIS VON AUSSEN. Bisher hatte jede
Rolle in diesem Haus einen Vertrag, eine Rechnung oder eine
Absprache. Ein Mitglied des Treffs hat nur einen Code. Was diese
Menschen sehen, ist damit die einzige Stelle, an der ein Fehler
wirklich nach draussen geht.
---------------------------------------------------------------------
WAS SICH NICHT AENDERT: der Pfad /workspace/, der Dienst (4100), die
Datenbank, der Code. Getrennt wird ueber den HOSTNAMEN, so wie bei
Workspace und Team Dogi auch.
---------------------------------------------------------------------
WAS DIESE DATEI AUSDRUECKLICH NICHT TUT: Rechte vergeben.
Wer auf welcher Adresse angemeldet sein darf, entscheidet
sitzungPasstZurAdresse() -- aufgerufen in sitzungLesen(), durch das
alle 26 Fachmodule gehen. Was eine Rolle sehen darf, entscheidet die
Rechtetafel in rechte.js. Zwei Orte fuer dieselbe Regel waeren einer
zu viel; der zweite waere der, den beim naechsten Umbau niemand
anfasst.
---------------------------------------------------------------------
DASS DER TREFF WIE TEAM DOGI AUSSIEHT, IST ENTSCHIEDEN
Er benutzt crew-haus.css, die Buehnen von Team Dogi und deren
App-Symbol. Gemessen gaebe es durchaus einen eigenen Farbton
(Tannengruen liegt 60 Lab-Einheiten vom Marineblau entfernt, also
klar unterscheidbar). Er wuerde den Treff aber zu einem DRITTEN Haus
machen, neben Spicy Media und Team Dogi. Er ist kein drittes Haus,
sondern ein zweiter Raum in demselben -- "lila und babyblau ueberall"
(Filipe, 10.09.2026).
Unterschieden werden die Tueren durch das WORT darauf: Die Buehne der
Zugangswand traegt "DER TREFF" statt "TEAM DOGI" (tools/crew-buehne.mjs,
zwei Werte, ein Werkzeug), und die App heisst "Der Treff".
===================================================================== */
import {
istTreffAdresse, istWorkspacePfad,
} from "./crew-adresse.js";
/**
* Express-Zwischenschritt fuer die Adresse des Treffs.
*
* Er laeuft NACH crewWeiche und tut dort dasselbe wie diese auf ihrer
* Adresse -- mit drei Unterschieden: eigenes Manifest, eigene Wand,
* und die Buehnen kommen von Team Dogi statt von der Agentur.
*
* DIE FREMDEN WAENDE SPERRT ER NICHT SELBST. Das macht bereits
* crewWeiche fuer jede Adresse ueber die Wandtafel (istFremdeWand).
* Eine zweite Sperre hier waere dieselbe Regel an zwei Orten.
*/
export function treffWeiche(req, res, next) {
if (!istTreffAdresse(req.get("host"))) return next();
/* NICHT INDIZIEREN. Der Kopf steht hier und nicht nur als <meta> im
HTML, weil er dann auch fuer Manifest, Symbole, Bilder und jede
Antwort ohne HTML gilt. Bei dieser Adresse wiegt das schwerer als
bei den anderen beiden: Sie wird an Dutzende Menschen
weitergegeben, die nichts unterschrieben haben. Ein Eintrag bei
einer Suchmaschine waere nicht rueckholbar. */
res.setHeader("X-Robots-Tag", "noindex, nofollow");
/* DAS MANIFEST MACHT DIE APP. Alle drei Adressen liefern dieselben
HTML-Dateien aus, in jeder steht dieselbe Manifest-Zeile. Hier
liegt dahinter ein anderes: Name "Der Treff". Sonst hiesse die App
auf dem Startbildschirm eines Mitglieds "Team Dogi" -- der Name
eines Bereichs, zu dem es nicht gehoert.
Ueber die Adresse und nicht ueber eine zweite Zeile in 30
HTML-Dateien: Eine zweite Zeile muesste jemand bei jeder neuen
Seite mitschreiben, und wer sie vergisst, bekommt eine Seite, die
still zur falschen App gehoert. */
if (req.path === "/workspace/app.webmanifest") {
req.url = "/workspace/treff.webmanifest";
return next();
}
/* DIE ZUGANGSWAND. Ohne diese Zeile stuende auf der Adresse der
Community die Wand der Agentur: Spicy-Media-Logo, "Creator
Workspace", fuenf Rollenkacheln mit den Namen Admin, Manager,
Scout, Creator -- die komplette Struktur des Unternehmens, vor der
Anmeldung, fuer jeden, der die Adresse hat.
ZWEI PFADE, EINE DATEI: "/workspace/" und "/workspace/index.html"
fuehren beide zur Wand, also muessen beide umgebogen werden. Den
zweiten zu vergessen faellt im Alltag nicht auf -- ihn tippt
niemand -- und waere trotzdem genau das Leck von oben. */
if (req.path === "/workspace/" || req.path === "/workspace/index.html") {
req.url = "/workspace/treff-index.html";
return next();
}
/* DIE BUEHNEN INNEN. Die neun Motive der Agentur zeigen nicht bloss
rotes Licht, sondern das Spicy-Media-Logo selbst -- in `halle` und
`arena` sogar vielfach nebeneinander. Die Fassungen von Team Dogi
liegen laengst da und sind genau dafuer gebaut worden.
UND ES FLACKERT NICHT: Das Bild wird beim ERSTEN Abruf schon als
das richtige geliefert. Eine Umschaltung per JavaScript haette
erst die Spicy-Buehne gezeigt und dann getauscht. */
const buehne = /^\/workspace\/assets\/img\/(buehne-[a-z]+-(?:uhd|breit|schmal)\.webp)$/
.exec(req.path);
if (buehne) {
req.url = `/workspace/assets/img/crew-${buehne[1]}`;
return next();
}
/* DAS ZEICHEN IN DER KOPFLEISTE. start.css setzt links neben den
Namen eine Chili -- als CSS-Hintergrund, nicht als Bild im HTML.
Ohne diese Zeile stuende oben links auf jeder Seite des Treffs das
Zeichen von Spicy Media. */
if (req.path === "/workspace/assets/img/marke-spicy.webp") {
req.url = "/workspace/assets/img/marke-husky.webp";
return next();
}
/* DAS HAUS. Jede Seite laedt als LETZTE Stilvorlage haus.css; auf der
Agenturadresse ist die leer, hier steht dahinter das Farbhaus von
Team Dogi.
Der Dateiname reist mit einer Fassungsnummer (`?v=…`); die
Bedingung sieht deshalb nur auf den PFAD, nicht auf die ganze
Adresse. Genau daran waere sie sonst gescheitert -- lautlos, denn
ein nicht umgebogener Pfad liefert weiterhin eine gueltige Datei. */
if (req.path === "/workspace/assets/css/haus.css") {
req.url = "/workspace/assets/css/crew-haus.css"
+ (req.url.includes("?") ? req.url.slice(req.url.indexOf("?")) : "");
return next();
}
/* NUR DER WORKSPACE. Auf treff. gibt es die Hauptseite nicht -- sonst
laege die komplette Dogfather-Website ein zweites Mal unter einer
zweiten Adresse (doppelter Inhalt, und ein Weg, die Adresse ueber
eine Suchmaschine zu finden). Alles ausserhalb von /workspace und
/assets geht zur Zugangswand.
Caddy tut dasselbe bereits mit seiner eigenen Weiche. Dass es hier
ein zweites Mal steht, ist Absicht: Eine Schranke, die nur in einer
Datei ausserhalb des Projekts existiert, faellt beim naechsten
Serverumbau lautlos weg. */
if (istWorkspacePfad(req.path) || req.path.startsWith("/assets/")) return next();
return res.redirect(302, "/workspace/");
}
+96 -14
View File
@@ -32,7 +32,7 @@ import { darfSeite, OHNE_ANMELDUNG, SEITEN, ALLE_ROLLEN } from "./rechte.js";
obwohl workspace-treff.js seinerseits aus dieser Datei importiert.
Begruendung im Kopf von treff-tabellen.js. */
import { treffTabellen, treffSperreLesen } from "./treff-tabellen.js";
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istTreffAdresse, istPruefAdresse, AUSSEN_ROLLEN,
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istPruefAdresse, AUSSEN_ROLLEN,
TEAM_DOGI_ROLLEN } from "./crew-adresse.js";
/* Weitergereicht, damit die Fachmodule sie wie alles andere aus
workspace.js beziehen und nicht wissen muessen, wo sie wohnt. */
@@ -100,8 +100,22 @@ const ROLLEN_KACHEL = new Set(["spicy", "admin", "manager", "scout", "creator"])
*
* DogFather steht auf BEIDEN Waenden. Er ist die einzige Person, die
* in beiden Welten arbeitet; das ist keine Ausnahme von der Trennung,
* sondern ihr Sinn. */
const CREW_KACHEL = new Set(["admin", "hand", "modi"]);
* sondern ihr Sinn.
*
* VIER, SEIT DER TREFF HIER WOHNT (Entscheidung Filipe, 11.09.2026:
* "das soll keine app fuer sich sein, das soll in der crew seite
* adaptiert werden" -- und auf die Frage, wie die Community durch
* diese Wand kommt: "vierte Kachel Community").
*
* WAS DAS KOSTET, und es gehoert hierhin und nicht in eine Fussnote:
* Ein Mitglied der Community liest vor seiner Anmeldung die Namen der
* drei Team-Rollen. Die Alternative waere gewesen, die Kachelreihe
* ganz abzuschaffen und allein den Code entscheiden zu lassen -- dann
* haette niemand eine Rolle gesehen, aber Filipes Entscheidung vom
* 10.09. waere wieder umgeworfen worden. Er hat sich fuer die vierte
* Kachel entschieden; der Preis ist damit bewusst bezahlt und nicht
* uebersehen. */
const CREW_KACHEL = new Set(["admin", "hand", "modi", "gast"]);
/** Die Rollen mit einer Kachel auf der Zugangswand von treff.
*
@@ -114,7 +128,7 @@ const CREW_KACHEL = new Set(["admin", "hand", "modi"]);
* Die Menge steht trotzdem hier: Die Anmeldung prueft gegen sie, und
* eine Wand ohne Kacheln darf nicht heissen, dass jede Rolle
* durchkommt. */
const TREFF_KACHEL = new Set(["gast"]);
/* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch
(admin, creator, manager, scout, spicy) -- also fast genau falsch
@@ -3288,7 +3302,7 @@ export function sitzungLesen(req) {
(sichtPerson) das auch nicht tut. Wer hier etwas anderes einbaut,
macht aus einer Anzeigefrage eine Rechtefrage. */
const haus = istCrewAdresse(req.get?.("host")) ? "crew"
: istTreffAdresse(req.get?.("host")) ? "treff" : "agentur";
: "agentur";
/* DER AUSSCHLUSS WIRKT HIER -- an derselben Stelle wie alles andere
(11.09.2026).
@@ -3676,9 +3690,7 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
Wand gerade dran ist. Ein Manager, der crew. erraet und seinen
Code eintippt, faellt dadurch in genau dieselbe Antwort wie
jemand mit einer erfundenen Rolle. */
const kachelSatz = istTreffAdresse(req.get("host")) ? TREFF_KACHEL
: istCrewAdresse(req.get("host")) ? CREW_KACHEL
: ROLLEN_KACHEL;
const kachelSatz = istCrewAdresse(req.get("host")) ? CREW_KACHEL : ROLLEN_KACHEL;
if (!kachelSatz.has(rolle) || !codeBrauchbar) {
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
@@ -4865,7 +4877,21 @@ export function protokollLesen(anzahl = 20) {
Die Reihenfolge ist egal (beide verkleinern nur), die Trennung nicht.
===================================================================== */
const HAUS_TEAM_ROLLEN = new Set(["admin", "hand", "modi"]);
/* WER AUF DER TEAM-ADRESSE UEBERHAUPT VORKOMMT.
*
* Die Community steht seit dem 11.09.2026 mit drin (Filipe: "das soll
* keine app fuer sich sein, das soll in der crew seite adaptiert
* werden"). Der Treff wohnt jetzt hier, also muessen seine Mitglieder
* hier auch verwaltet werden koennen -- sonst legt DogFather einen
* Zugang an und er verschwindet im selben Moment aus der Liste.
*
* DAS MACHT SIE NICHT ZU KOLLEGEN. Diese Menge beantwortet nur "wer
* kommt auf dieser Adresse vor". Ob jemand in eine Einladung, eine
* Aufgabe oder einen Chat darf, beantwortet ohneAussen() weiter unten
* -- und das sagt bei der Community ueberall Nein. Zwei verschiedene
* Fragen, zwei verschiedene Funktionen; wer sie in eine legt, kann
* spaeter nicht mehr sagen, welche gerade zugeschlagen hat. */
const HAUS_TEAM_ROLLEN = new Set(["admin", "hand", "modi", "gast"]);
function nurHaus(ids, person) {
if (person?.haus !== "crew") return ids;
@@ -4971,10 +4997,66 @@ function nurEigene(ids, person) {
return person?.id ? [person.id] : [];
}
export const sichtbareCreatorIds = (person) => nurEigene(nurCreatorIds(nurHaus(ohneVerborgene(sichtbareCreatorIdsRoh(person), person), person)), person);
/* =====================================================================
WER VON AUSSEN IST, STEHT IN KEINER ARBEITSLISTE (11.09.2026)
=====================================================================
nurEigene() beantwortet "was sieht ein Mitglied der Community?".
Diese Huelle beantwortet die andere Richtung: "steht ein Mitglied der
Community in der Liste, die jemand ANDERS bekommt?"
DER ANLASS: Seit es die Rolle gibt, taucht sie in jeder Liste auf,
die "alle" bedeutet -- und fuer DogFather bedeutet fast jede Liste
"alle" (siehtAlles). Ein Mitglied der Community stand damit in der
Auswahl, wen man zu einem Termin einlaedt, wem man eine Aufgabe gibt
und mit wem man einen Chat anfaengt. Nichts davon ist vorgesehen, es
gab dafuer nie eine Entscheidung, und aufgefallen waere es zum ersten
Mal, wenn jemand versehentlich einen Zuschauer zu einer internen
Besprechung einlaedt.
WARUM ALS HUELLE UND NICHT IN DEN VIER FUNKTIONEN: Weil sie dann auch
fuer die Listen gilt, die es noch nicht gibt. Dieselbe Ueberlegung
steht schon bei nurHaus() und bei der Modi-Huelle in
workspace-bereiche.js -- und beide Male war der erste Anlauf ein
einzelner geflickter Fall, der den naechsten offen liess.
NULL HEISST "ALLE" -- und sobald hier etwas herausfaellt, darf es das
nicht mehr heissen. Die Liste wird deshalb AUSGESCHRIEBEN, so wie
ohneVerborgene() und nurHaus() es auch tun.
EIN FEHLSCHLAG SCHLIESST. Bei einer Datenbankstoerung eine
ungefilterte Liste zurueckzugeben hiesse, die Community genau dann in
die Einladungsauswahl zu setzen, wenn ohnehin gerade etwas nicht
stimmt. Eine leere Liste ist sichtbar falsch, eine zu volle nicht.
AUSDRUECKLICH NICHT ANGEWENDET auf sichtbarePersonenIds(): Das ist
die Verwaltungsliste, in der DogFather Zugaenge anlegt und Codes neu
setzt. Dort MUSS die Community stehen. Die Einladungsliste leitet
sich zwar davon ab -- aber sie bekommt diese Huelle aussen herum,
und damit ist die Verwaltung offen und die Zusammenarbeit zu. */
function ohneAussen(ids) {
const rollen = [...AUSSEN_ROLLEN];
if (!rollen.length) return ids;
try {
const platz = rollen.map(() => "?").join(", ");
const aussen = db().prepare(
`SELECT id FROM personen WHERE rolle IN (${platz})`).all(...rollen).map((z) => z.id);
if (!aussen.length) return ids;
if (ids === null) {
return db().prepare(
`SELECT id FROM personen WHERE rolle NOT IN (${platz})`).all(...rollen).map((z) => z.id);
}
const menge = new Set(aussen);
return ids.filter((i) => !menge.has(i));
} catch {
return [];
}
}
export const sichtbareCreatorIds = (person) => ohneAussen(nurEigene(nurCreatorIds(nurHaus(ohneVerborgene(sichtbareCreatorIdsRoh(person), person), person)), person));
export const sichtbarePersonenIds = (person) => nurEigene(nurHaus(ohneVerborgene(sichtbarePersonenIdsRoh(person), person), person), person);
export const betreuerIds = (person) => nurEigene(nurHaus(ohneVerborgene(betreuerIdsRoh(person), person), person), person);
export const einladbareIds = (person) => nurEigene(nurHaus(ohneVerborgene(einladbareIdsRoh(person), person), person), person);
export const betreuerIds = (person) => ohneAussen(nurEigene(nurHaus(ohneVerborgene(betreuerIdsRoh(person), person), person), person));
export const einladbareIds = (person) => ohneAussen(nurEigene(nurHaus(ohneVerborgene(einladbareIdsRoh(person), person), person), person));
/* SICH SELBST NICHT -- UND ZWAR AUCH DANN NICHT, WENN DIE LISTE
AUSGESCHRIEBEN WIRD (10.09.2026).
@@ -4996,7 +5078,7 @@ export const einladbareIds = (person) => nurEigene(nurHaus(ohneVerborgene(einlad
seit es ueberhaupt jemanden zu verbergen gibt; vorher konnte es ihn
nicht geben. */
export const schreibbareIds = (person) => {
const ids = nurHaus(ohneVerborgene(schreibbareIdsRoh(person), person), person);
const ids = ohneAussen(nurHaus(ohneVerborgene(schreibbareIdsRoh(person), person), person));
return ids === null ? null : ids.filter((i) => i !== person?.id);
};
export const pipelineIds = (person) => nurHaus(ohneVerborgene(pipelineIdsRoh(person), person), person);
export const pipelineIds = (person) => ohneAussen(nurHaus(ohneVerborgene(pipelineIdsRoh(person), person), person));