Der Treff: eine eigene Tür für die Community, und sie sieht nur, was dasteht
Filipe: "eine community rolle und community kategorie, wo die community auch dann nur die sieht" · "wieso nur eine kachel — ich will mehrere, mit mehreren optionen" · "egal welche rolle hinzugefügt wird soll immer nur das sehen was ich erlaube". DIE DRITTE ADRESSE. treff.dogfather-universe.com bekommt eine eigene Weiche (treff-adresse.js), eine eigene Zugangswand, ein eigenes Manifest und acht eigene Bühnenbilder. Ohne sie stünde dort die Wand der Agentur: Spicy-Logo, "Creator Workspace", fünf Rollenkacheln mit den Namen Admin, Manager, Scout, Creator — die komplette Struktur des Unternehmens, vor der Anmeldung, für jeden mit der Adresse. Welche Wand zu welcher Tür gehört, steht jetzt in EINER Tafel (istFremdeWand) statt als Sonderfall im Code. Bei zwei Wänden war ein Sonderfall richtig; bei drei wären es drei geworden, bei vier sechs. SIEBEN BRETTER UND EIN ACHTES NUR FÜRS TEAM. Anschlagbrett, Was ansteht, Der Treff, Wunschliste, Highlights, Regeln & Hilfe, Mitmachen — dazu "Meldungen & Maßnahmen", das ausdrücklich NICHT in TREFF_BEREICHE steht: dort wäre es lautlos bei der Community gelandet und hätte ausgesehen wie die anderen sieben. WAS AUF DEN BRETTERN PASSIERT: "Will ich auch" (eine Stimme je Person, erzwungen durch den Schlüssel, nicht durch eine Prüfung) · Anheften, höchstens drei, gezählt IN der Transaktion · Freigabe vor Sichtbarkeit für Termine und Highlights, wobei Abwesenheit der Ruhezustand ist · Melden mit Pflicht-Grund (DSA Art. 16) · Entfernen mit Grund, der den Beitrag ÜBERLEBT (DSA Art. 17) · drei Stufen, gerechnet statt gespeichert · die Bannleiter: Modi bis 7 Tage, dauerhaft nur DogFather (seine Entscheidung) · "Mitmachen" wird zum Talent, genau einmal. Der Ausschluss wirkt in sitzungLesen() — also nicht nur beim Schreiben. Wer ausgeschlossen ist, ist weg, nicht stumm. ZWEI SEITEN MEHR. treff-regeln.html holt seine ZAHLEN vom Server (Mindestalter 18, Fristen, Anschläge) — ein Regeltext, der eine andere Zahl nennt als die Regel, ist schlimmer als keiner. rechte.html rechnet "Wer sieht was" aus derselben Tafel aus, aus der die Schranke ihre Entscheidung holt. Die Community steht dort bei 3 von 23. SECHS BEFUNDE, KEINEN HAT DAS LESEN GEFUNDEN: · Ein Modi kam über den verborgenen Zugang in den Treff — die Bedingung war durch Ausschluss formuliert und nahm die neue Wand automatisch mit · /api/personen gab einem Mitglied Namen und Rolle von DogFather · Ein DELETE mit Körper wird von Nodes HTTP-Parser mit einem leeren 400 abgewiesen, bevor Express ihn sieht (gemessen). Der Grund reist jetzt in der Adresse · Der Kommentar, der erklärt, warum die Zugangswand keine Rollennamen nennt, nannte selbst einen — in einer ausgelieferten Datei · Zwei Listen für "welche Seiten sind ohne Anmeldung offen". Sie stimmten, weil beide am selben Tag entstanden · Ein Portkonflikt, den ich selbst gebaut hatte (4391 gehört pruef-chat-aufloesen) Dazu zwei feste Zahlen abgeschafft (die 20 in pruef-rechtetafel, die Kachelzahl in pruef-treff) und zwei Werkzeuge, die jetzt SUCHEN statt aufzuzählen: Der Versionsstempel findet seine Manifeste selbst — das dritte hätte sonst keinen bekommen, und die neue App hätte bis zu vier Stunden lang das falsche Zeichen getragen. Das Bildwerkzeug baut beide Zugangswände aus einer Vorlage mit zwei Werten. Zwei neue Kacheltöne, gemessen statt gewählt: #2f2f7f (Abstand 31,4 in Lab) und #7c2771 (29,7). Das schwächste Paar der vorhandenen 34 liegt bei 24,9. Geprüft: treff 69 · treff-werkzeuge 60 · rollen 312 · start-ansicht 147 · crew-adresse 129 · sicht 84 · modi-verborgen 80 · haus-trennung 62 · bereiche-lesend 37 · css-klassen 30 · zwischenspeicher 21 · alle-wege 19 · rechtetafel 19 — alles grün. Co-Authored-By: Claude Opus 5 <[email protected]>
@@ -99,3 +99,10 @@ rueckgabewerte-*.txt
|
||||
# besser als eine, an die ich mich erinnern muss.
|
||||
zz-*
|
||||
/*.png.tmp
|
||||
|
||||
# Arbeitsreste aus Pruef- und Umbaulaeufen (11.09.2026). Alles unter
|
||||
# tools/_ ist Kladde: Ausgaben einzelner Laeufe, kurzlebige Patch-Skripte.
|
||||
# Was davon bleiben soll, bekommt einen Namen ohne Unterstrich -- so
|
||||
# ist die Entscheidung "gehoert das ins Verzeichnis?" ein Umbenennen
|
||||
# und kein Vergessen.
|
||||
tools/_*
|
||||
|
||||
@@ -93,6 +93,25 @@ export const CREW_ADRESSE = "https://crew.dogfather-universe.com";
|
||||
stehen kann. workspace.js gibt sie fuer die Fachmodule weiter. */
|
||||
export const TEAM_DOGI_ROLLEN = new Set(["hand", "modi"]);
|
||||
|
||||
/* WER VON AUSSEN KOMMT (11.09.2026).
|
||||
|
||||
Die Community. Sie steht hier und nicht in workspace.js, weil diese
|
||||
Datei von nichts abhaengt -- workspace.js importiert von hier, nicht
|
||||
umgekehrt. Eine zweite Liste dort waere die, die auseinanderlaeuft.
|
||||
|
||||
Der Unterschied zu TEAM_DOGI_ROLLEN ist nicht die Verborgenheit,
|
||||
sondern die Richtung: Das Team gehoert zum Haus und wird vor der
|
||||
Agentur verborgen. Die Community gehoert NICHT zum Haus -- sie
|
||||
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"]);
|
||||
|
||||
/** Die Adresse der Modi-App. Feste Menge, kein Muster -- Falle 1. */
|
||||
const CREW_HOSTS = new Set(["crew.dogfather-universe.com"]);
|
||||
|
||||
@@ -121,6 +140,26 @@ export function istOhneModiAdresse(hostKopfzeile) {
|
||||
return OHNE_MODI_HOSTS.has(nurHost(hostKopfzeile));
|
||||
}
|
||||
|
||||
/** Eine Pruefadresse -- localhost und die Nummern, unter denen die
|
||||
* Pruefungen ihren eigenen Server starten.
|
||||
*
|
||||
* Sie steht hier als EIGENE Funktion und nicht als "alles, was keine
|
||||
* der echten Adressen ist". Genau diese Formulierung durch Ausschluss
|
||||
* hat am 11.09.2026 dazu gefuehrt, dass der verborgene Zugang auf der
|
||||
* neuen dritten Wand mitgalt, ohne dass es jemand entschieden haette.
|
||||
* Wer eine vierte Adresse baut, muss sie hier NICHT eintragen -- und
|
||||
* bekommt dadurch von selbst das sichere Verhalten. */
|
||||
export function istPruefAdresse(hostKopfzeile) {
|
||||
const h = nurHost(hostKopfzeile);
|
||||
return h === "localhost" || h === "127.0.0.1" || h.startsWith("127.")
|
||||
|| 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) {
|
||||
@@ -128,6 +167,57 @@ export function istWorkspacePfad(pfad) {
|
||||
return p === "/workspace" || p.startsWith("/workspace/");
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
WELCHE WAND GEHOERT ZU WELCHER TUER (11.09.2026)
|
||||
=====================================================================
|
||||
|
||||
Drei Zugangswaende liegen als drei Dateien im selben Ordner, und
|
||||
express.static liefert jede aus, die man beim Namen nennt. Ohne eine
|
||||
Regel kann also jeder jede aufrufen: die Community die Wand von Team
|
||||
Dogi, das Team die Wand des Treffs, und beide auf der Agenturadresse.
|
||||
|
||||
DAS WAR SCHON EINMAL EIN LOCH, und gefunden hat es nicht das Lesen,
|
||||
sondern der Wortleck-Test, der ALLE ausgelieferten Dateien durchsucht.
|
||||
Damals stand die Regel als EIN Sonderfall im Code ("auf den alten
|
||||
Adressen gibt es crew-index.html nicht"). Bei der dritten Wand waeren
|
||||
daraus drei Sonderfaelle geworden, bei der vierten sechs. Deshalb
|
||||
steht hier jetzt eine Tafel: Jede benannte Wand nennt die Adresse,
|
||||
auf die sie gehoert, und die Frage wird einmal beantwortet.
|
||||
|
||||
404 UND KEINE WEITERLEITUNG: "Gibt es nicht" verraet nichts. Eine
|
||||
Weiterleitung wuerde sagen, dass es die Datei gibt, nur woanders.
|
||||
|
||||
/workspace/index.html STEHT ABSICHTLICH NICHT IN DER TAFEL. Sie ist
|
||||
keine benannte Wand, sondern die Vorgabe -- jede Adresse biegt sie
|
||||
auf ihre eigene um, bevor daraus eine Datei wird. Stuende sie hier,
|
||||
bekaeme man auf crew. und treff. ein 404 statt der eigenen Wand.
|
||||
|
||||
AUF PRUEFADRESSEN GILT DIE TAFEL NICHT, und das ist dieselbe
|
||||
Ueberlegung wie bei OHNE_MODI_HOSTS: Sonst koennten die Pruefungen,
|
||||
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. */
|
||||
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? */
|
||||
export function istFremdeWand(hostKopfzeile, pfad) {
|
||||
/* Mehrfache Schraegstriche zusammenziehen: express raeumt Punkt-
|
||||
Segmente weg, doppelte Striche aber nicht -- und express.static
|
||||
zieht sie danach selbst zusammen und liefert die Datei aus. Genau
|
||||
daran ist am 04.09.2026 die Schranke des Workspace vorbeigelaufen. */
|
||||
const p = String(pfad || "").toLowerCase().replace(/\/{2,}/g, "/");
|
||||
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);
|
||||
if (!benannt) return false;
|
||||
return !wand.zuhause(hostKopfzeile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf diese Person auf dieser Adresse angemeldet sein?
|
||||
*
|
||||
@@ -141,6 +231,16 @@ export function istWorkspacePfad(pfad) {
|
||||
*/
|
||||
export function sitzungPasstZurAdresse(rolle, hostKopfzeile) {
|
||||
const team = TEAM_DOGI_ROLLEN.has(rolle);
|
||||
/* 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. */
|
||||
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. */
|
||||
@@ -175,36 +275,19 @@ export function sitzungPasstZurAdresse(rolle, hostKopfzeile) {
|
||||
* Serverumbau lautlos weg.
|
||||
*/
|
||||
export function crewWeiche(req, res, next) {
|
||||
if (!istCrewAdresse(req.get("host"))) {
|
||||
/* DIE ANDERE RICHTUNG -- und sie war ein echtes Loch.
|
||||
/* DIE FREMDE WAND -- fuer JEDE Adresse, auch fuer crew. selbst.
|
||||
|
||||
Die zweite Zugangswand liegt als Datei im selben Ordner wie alles
|
||||
andere und wird von express.static ausgeliefert. Ohne diese Zeile
|
||||
kann sie JEDER unter ihrem Dateinamen aufrufen, auch auf
|
||||
workspace.dogfather-universe.com -- und saehe dort eine
|
||||
Anmeldeseite, die es dort gar nicht geben soll.
|
||||
|
||||
Gefunden hat das nicht ein Gedanke, sondern eine Pruefung: Der
|
||||
Wortleck-Test durchsucht ALLE ausgelieferten Dateien, und dass er
|
||||
diese eine ueberhaupt las, war die Antwort auf eine Frage, die
|
||||
ich mir nicht gestellt hatte -- naemlich wer sie sonst noch
|
||||
abrufen kann.
|
||||
|
||||
404 UND KEINE WEITERLEITUNG: "Gibt es nicht" ist die Antwort, die
|
||||
nichts verraet. Eine Weiterleitung wuerde sagen, dass es die
|
||||
Datei gibt, nur woanders.
|
||||
|
||||
NUR AUF DEN DREI ECHTEN ADRESSEN, nicht "ueberall ausser crew":
|
||||
Sonst waere sie auch auf localhost gesperrt, und die Pruefung,
|
||||
die die Wand im Browser ansieht, koennte sie nicht mehr oeffnen
|
||||
-- sie wuerde gruen bleiben und nichts mehr messen. */
|
||||
if (istOhneModiAdresse(req.get("host"))
|
||||
&& req.path.toLowerCase().replace(/\/{2,}/g, "/") === "/workspace/crew-index.html") {
|
||||
return res.status(404).type("html").send("Nicht gefunden.");
|
||||
}
|
||||
return next();
|
||||
Sie steht vor der Weiche und nicht in ihrem Nein-Zweig. Dort stand
|
||||
sie bis zum 11.09.2026, und das war richtig, solange es zwei Waende
|
||||
gab: Die eigene kann man nicht "fremd" verlangen. Mit der dritten
|
||||
stimmt das nicht mehr -- auf crew. ist die Wand des Treffs fremd,
|
||||
und im Nein-Zweig waere diese Frage dort nie gestellt worden. */
|
||||
if (istFremdeWand(req.get("host"), req.path)) {
|
||||
return res.status(404).type("html").send("Nicht gefunden.");
|
||||
}
|
||||
|
||||
if (!istCrewAdresse(req.get("host"))) return next();
|
||||
|
||||
res.setHeader("X-Robots-Tag", "noindex, nofollow");
|
||||
|
||||
/* DAS MANIFEST MACHT DIE APP (10.09.2026).
|
||||
|
||||
@@ -13,6 +13,7 @@ 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";
|
||||
@@ -23,6 +24,7 @@ import { kalenderRouter } from "./workspace-kalender.js";
|
||||
import { serienRouter, serienStarten } from "./workspace-serien.js";
|
||||
import { dateienRouter } from "./workspace-dateien.js";
|
||||
import { bereicheRouter } from "./workspace-bereiche.js";
|
||||
import { treffRouter } from "./workspace-treff.js";
|
||||
import { reportRouter } from "./workspace-reports.js";
|
||||
import { scoutRouter } from "./workspace-scouts.js";
|
||||
import { callRouter } from "./workspace-calls.js";
|
||||
@@ -127,6 +129,22 @@ 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
|
||||
404-Seite sollen sie bekommen, denn gerade dort werden Eingaben
|
||||
@@ -235,6 +253,12 @@ app.use(kalenderRouter);
|
||||
app.use(serienRouter);
|
||||
app.use(dateienRouter);
|
||||
app.use(bereicheRouter);
|
||||
/* Der Treff (11.09.2026): Stimmen, Anheften, Freigeben, Melden,
|
||||
Massnahmen, Stufen. Die sieben Bretter selbst laufen ueber
|
||||
bereicheRouter -- es sind gewoehnliche Eintraege, und ein zweiter
|
||||
Weg zu denselben Zeilen waere ein zweiter Ort fuer dieselbe
|
||||
Rechtepruefung. */
|
||||
app.use(treffRouter);
|
||||
app.use(reportRouter);
|
||||
app.use(scoutRouter);
|
||||
app.use(callRouter);
|
||||
|
||||
@@ -91,7 +91,7 @@ import { join } from "node:path";
|
||||
import { request as httpAnfrage } from "node:http";
|
||||
import { portMussFreiSein } from "./helfer-port.mjs";
|
||||
|
||||
const PORT = await portMussFreiSein(4391, "pruef-kachel-universum");
|
||||
const PORT = await portMussFreiSein(4401, "pruef-kachel-universum");
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-universum-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
|
||||
@@ -106,10 +106,11 @@ melde("=== 1. Jede Rolle und jede Seite hat einen Eintrag ===");
|
||||
vergisst, faellt genau hier auf. */
|
||||
const SEITEN_ORDNER = new URL("../workspace/", import.meta.url).pathname
|
||||
.replace(/^\/([A-Za-z]:)/, "$1");
|
||||
const DATEIEN = readdirSync(SEITEN_ORDNER)
|
||||
.filter((f) => f.endsWith(".html"))
|
||||
.map((f) => "/workspace/" + f).sort();
|
||||
{
|
||||
const dateien = readdirSync(SEITEN_ORDNER)
|
||||
.filter((f) => f.endsWith(".html"))
|
||||
.map((f) => "/workspace/" + f).sort();
|
||||
const dateien = DATEIEN;
|
||||
const inTafel = new Set([...rechte.alleSeiten(), ...rechte.OHNE_ANMELDUNG]);
|
||||
const fehlend = dateien.filter((p) => !inTafel.has(p));
|
||||
const ueberzaehlig = rechte.alleSeiten().filter((p) => !dateien.includes(p));
|
||||
@@ -143,7 +144,27 @@ const PHANTOM = "xx-gibt-es-nicht";
|
||||
{
|
||||
const alle = rechte.alleSeiten();
|
||||
const durchgelassen = alle.filter((p) => rechte.darfSeite({ rolle: PHANTOM }, p));
|
||||
ok(alle.length === 20, `${alle.length} Seiten in der Tafel`);
|
||||
/* KEINE FESTE ZAHL MEHR (11.09.2026).
|
||||
|
||||
Hier stand `alle.length === 20`. Das war richtig an dem Tag, an dem
|
||||
es geschrieben wurde, und wurde bei der naechsten neuen Seite rot --
|
||||
ohne dass irgendetwas kaputt war. Eine feste Schwelle ist eine
|
||||
Rechnung von gestern; genau deshalb steht sie in diesem Haus in der
|
||||
Liste der teuren Fehler.
|
||||
|
||||
Gemessen wird jetzt, was die Zeile eigentlich wissen will: Die
|
||||
Tafel deckt die Dateien ab, die es WIRKLICH gibt -- jede
|
||||
ausgelieferte Seite genau einmal, die Zugangswaende ausgenommen
|
||||
(die stehen in OHNE_ANMELDUNG, weil sich dort noch niemand
|
||||
angemeldet hat).
|
||||
|
||||
UND SIE DARF NICHT LEER SEIN: `alle.length > 0` gehoert in die
|
||||
BEDINGUNG, nicht nur in den Meldetext. Sonst waere die Zeile bei
|
||||
einer leeren Tafel und einem leeren Ordner gruen -- der gruene
|
||||
Haken ueber der Null. */
|
||||
const erwartet = DATEIEN.length - rechte.OHNE_ANMELDUNG.length;
|
||||
ok(alle.length > 0 && alle.length === erwartet,
|
||||
`${alle.length} Seiten in der Tafel (${DATEIEN.length} Dateien minus ${rechte.OHNE_ANMELDUNG.length} Waende)`);
|
||||
ok(durchgelassen.length === 0,
|
||||
`eine unbekannte Rolle kommt auf KEINE davon${durchgelassen.length ? " -- durchgelassen: " + durchgelassen.join(", ") : ""}`);
|
||||
|
||||
|
||||
@@ -246,10 +246,19 @@ for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) {
|
||||
nicht ans Ende, wo es niemand ansieht"). Das war meine Begruendung,
|
||||
nicht seine Entscheidung -- und bei privaten Dingen wiegt "faellt
|
||||
niemandem ins Auge" schwerer als "wird oft angesehen". */
|
||||
/* SIEBEN SEIT DEM 11.09.2026 -- "Community" kam dazu, der Treff.
|
||||
|
||||
Sie steht GANZ UNTEN, und zwar aus demselben Grund wie die beiden
|
||||
Gruppen davor: Dort stehen Namen und Saetze von Zuschauern, also
|
||||
private Dinge fremder Leute. Wer sich den Bildschirm teilt, hat sie
|
||||
dann nicht als Erstes im Blick.
|
||||
|
||||
Sie hat deshalb ausdruecklich KEIN `gruppeNach` -- eine Gruppe ohne
|
||||
Angabe haengt hinten an, und das ist hier die richtige Stelle. */
|
||||
const ERWARTETE_GRUPPEN = ["Rund um das Team", "Täglich", "Rund um den Creator",
|
||||
"Team & System", "Team Dogi", "Entwicklung & Nachwuchs"];
|
||||
"Team & System", "Team Dogi", "Entwicklung & Nachwuchs", "Community"];
|
||||
ok(JSON.stringify(r.gruppen.map((g) => g.name)) === JSON.stringify(ERWARTETE_GRUPPEN),
|
||||
`sechs Gruppen in dieser Reihenfolge (${r.gruppen.map((g) => g.name).join(" · ")})`);
|
||||
`${ERWARTETE_GRUPPEN.length} Gruppen in dieser Reihenfolge (${r.gruppen.map((g) => g.name).join(" · ")})`);
|
||||
/* WIE VIELE KACHELN? Nicht abgeschrieben, sondern aus bereiche.js
|
||||
gezaehlt (06.09.2026).
|
||||
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
/* =====================================================================
|
||||
Die Werkzeuge des Treffs (11.09.2026)
|
||||
|
||||
pruef-treff.mjs fragt: WER SIEHT WAS. Diese Datei fragt: WAS
|
||||
PASSIERT, wenn jemand etwas tut — „Will ich auch", anheften,
|
||||
freigeben, melden, entfernen, sperren, übernehmen.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
ZU JEDER REGEL GEHÖRT HIER DIE GEGENPROBE, und zwar aus demselben
|
||||
Grund wie überall in diesem Haus: Eine Prüfung, die immer bestätigt,
|
||||
bestätigt nichts. Wenn „ein Modi darf nicht dauerhaft ausschließen"
|
||||
grün ist, muss daneben stehen, dass DogFather es kann — sonst wäre
|
||||
die Zeile auch dann grün, wenn NIEMAND es kann.
|
||||
|
||||
Konkret geprüft:
|
||||
|
||||
* „Will ich auch" ist EINE Stimme. Zweimal drücken ändert nichts,
|
||||
und das Zurücknehmen geht.
|
||||
* Am Anschlagbrett hängen höchstens drei. Die vierte wird
|
||||
abgelehnt — und nach dem Abnehmen einer geht sie durch.
|
||||
* Freigeben kann nur das Team. Ohne Freigabe ist der Eintrag für
|
||||
die Community nicht da (steht in pruef-treff), mit Freigabe
|
||||
schon.
|
||||
* Melden ohne Grund geht nicht. Mit Grund landet es in der Liste
|
||||
des Teams (DSA Art. 16).
|
||||
* Entfernen ohne Grund geht nicht. Mit Grund ist der Eintrag weg
|
||||
UND der Grund noch da (DSA Art. 17) — das ist der eigentliche
|
||||
Punkt: Eine Begründung, die mit dem Beitrag verschwindet, ist
|
||||
keine.
|
||||
* Die Bannleiter: Ein Modi darf bis zu sieben Tage, DogFather auch
|
||||
dauerhaft. Und der dauerhafte Ausschluss wirkt — die Sitzung ist
|
||||
weg, nicht nur das Schreiben.
|
||||
* Aus „Mitmachen" wird ein Talent, und zwar genau einmal.
|
||||
|
||||
Aufrufen mit: node server/pruef-treff-werkzeuge.mjs
|
||||
===================================================================== */
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { request as httpAnfrage } from "node:http";
|
||||
import { portMussFreiSein } from "./helfer-port.mjs";
|
||||
|
||||
const PORT = await portMussFreiSein(4402, "pruef-treff-werkzeuge");
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-treffw-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
process.env.PORT = String(PORT);
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
await import("./index.js");
|
||||
notbremse(240_000, "pruef-treff-werkzeuge");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
|
||||
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
||||
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
||||
|
||||
let fehler = 0, geprueft = 0;
|
||||
const melde = (t) => console.log(t);
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
const TREFF = "treff.dogfather-universe.com";
|
||||
const CREW = "crew.dogfather-universe.com";
|
||||
const HAUS = "workspace.dogfather-universe.com";
|
||||
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||||
const treffModul = await import("./workspace-treff.js");
|
||||
const { BEREICHE } = await import("./workspace-bereiche.js");
|
||||
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const jetzt = new Date().toISOString();
|
||||
const heute = jetzt.slice(0, 10);
|
||||
const schluessel = randomBytes(32).toString("hex");
|
||||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||||
.run("code_kennung_schluessel", schluessel, jetzt);
|
||||
|
||||
/* Die Mitglieder werden ALT angelegt (40 Tage), damit sie schreiben
|
||||
duerfen. Die Stufen selbst misst pruef-treff; hier waere ein frisches
|
||||
Konto nur eine Huerde vor jeder einzelnen Zeile. */
|
||||
const ALT = new Date(Date.now() - 40 * 86400000).toISOString();
|
||||
function anlegen(name, rolle, code, erstellt = jetzt) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||||
+ " VALUES (?,?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768, createHmac("sha256", schluessel).update(code).digest("hex"), erstellt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
anlegen("Frida", "modi", "CODE-MODI-0001");
|
||||
const idKessi = anlegen("Kessi", "gast", "CODE-GAST-0001", ALT);
|
||||
const idBo = anlegen("Bo", "gast", "CODE-GAST-0002", ALT);
|
||||
/* Ein dritter Zugang, nur fuer den Ausschluss. Ihn Bo zu geben waere
|
||||
bequem gewesen und haette den Rest der Datei zerlegt: Ein
|
||||
Ausgeschlossener hat keine Sitzung mehr, also waere ab da JEDE
|
||||
weitere Zeile mit Bo gruen geworden, weil alles 401 antwortet --
|
||||
und "die Talente sieht er nicht" haette nichts mehr bewiesen. */
|
||||
const idPim = anlegen("Pim", "gast", "CODE-GAST-0003", ALT);
|
||||
|
||||
const rein = d.prepare(
|
||||
"INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)"
|
||||
+ " VALUES (?,?,?,?,?,?,?)");
|
||||
const ersteArt = (b) => Object.keys(BEREICHE[b].arten)[0];
|
||||
function eintrag(bereich, titel, von = idDogi) {
|
||||
rein.run(bereich, ersteArt(bereich), titel, "", heute, jetzt, von);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const wunsch1 = eintrag("wunsch", "WUNSCH-EINS");
|
||||
const anschlag = [1, 2, 3, 4].map((n) => eintrag("anschlag", `ANSAGE-${n}`));
|
||||
const highlight1 = eintrag("highlight", "CLIP-EINS");
|
||||
const bosBeitrag = eintrag("treff", "BO-SAGT-HALLO", idBo);
|
||||
const mitmachen1 = eintrag("mitmachen", "ICH-HAETTE-LUST", idBo);
|
||||
d.close();
|
||||
|
||||
/* EIN DELETE TRAEGT KEINEN KOERPER (gemessen 11.09.2026).
|
||||
|
||||
Nodes HTTP-Parser weist ein DELETE mit Koerper mit einem leeren 400
|
||||
ab, bevor Express es sieht -- nachgemessen: PUT mit Koerper 200,
|
||||
DELETE ohne Koerper 200, DELETE mit Koerper 400 mit genau einem
|
||||
Kopffeld. Die Pruefung schickt deshalb gar keinen erst; wuerde sie
|
||||
es tun, meldete sie einen Fehler in einer Route, in der keiner ist.
|
||||
Genau so ist dieser Befund entstanden. */
|
||||
function roh(pfad, host, kopf = {}, koerper = null, methode = null) {
|
||||
if (methode === "DELETE") koerper = null;
|
||||
return new Promise((fertig, schief) => {
|
||||
const a = httpAnfrage({
|
||||
host: "127.0.0.1", port: PORT, path: pfad,
|
||||
method: methode || (koerper ? "POST" : "GET"),
|
||||
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
|
||||
}, (antwort) => {
|
||||
let text = "";
|
||||
antwort.on("data", (s) => { text += s; });
|
||||
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
|
||||
});
|
||||
a.on("error", schief);
|
||||
if (koerper) a.write(JSON.stringify(koerper));
|
||||
a.end();
|
||||
});
|
||||
}
|
||||
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
|
||||
async function anmelden(rolle, code, host) {
|
||||
const r = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
|
||||
const kekse = [].concat(r.kopf?.["set-cookie"] || []);
|
||||
return kekse.map((c) => c.split(";")[0]).join("; ");
|
||||
}
|
||||
|
||||
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
|
||||
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
|
||||
const kKessi = await anmelden("gast", "CODE-GAST-0001", TREFF);
|
||||
const kBo = await anmelden("gast", "CODE-GAST-0002", TREFF);
|
||||
ok(!!kDogi && !!kModi && !!kKessi && !!kBo, "alle vier sind angemeldet");
|
||||
|
||||
/* =======================================================================
|
||||
1. WILL ICH AUCH — eine Stimme, nicht zwei
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 1. Will ich auch ===");
|
||||
{
|
||||
const weg = `/workspace/api/treff/eintrag/${wunsch1}/willich`;
|
||||
const a = await roh(weg, TREFF, { cookie: kKessi }, {}, "PUT");
|
||||
ok(a.code === 200 && json(a).stimmen === 1, `Kessi stimmt ab (${json(a).stimmen})`);
|
||||
|
||||
/* ZWEIMAL DRUECKEN IST EINMAL. Das ist die eigentliche Regel: Der
|
||||
zusammengesetzte Schluessel laesst gar keine zweite Zeile zu. */
|
||||
const b = await roh(weg, TREFF, { cookie: kKessi }, {}, "PUT");
|
||||
ok(b.code === 200 && json(b).stimmen === 1, `zweimal druecken aendert nichts (${json(b).stimmen})`);
|
||||
|
||||
const c = await roh(weg, TREFF, { cookie: kBo }, {}, "PUT");
|
||||
ok(c.code === 200 && json(c).stimmen === 2, `Bo auch (${json(c).stimmen})`);
|
||||
|
||||
const e = await roh(weg, TREFF, { cookie: kKessi }, {}, "DELETE");
|
||||
ok(e.code === 200 && json(e).stimmen === 1 && json(e).meine === false,
|
||||
`Kessi nimmt sie zurueck (${json(e).stimmen})`);
|
||||
|
||||
/* Und die Zahl kommt am Brett an -- sonst waere sie eine Zahl, die
|
||||
nur die Route kennt. */
|
||||
const brett = await roh("/workspace/api/bereich/wunsch", TREFF, { cookie: kBo });
|
||||
const zeile = (json(brett).eintraege || []).find((x) => x.id === wunsch1);
|
||||
ok(zeile?.stimmen === 1 && zeile?.meine_stimme === 1,
|
||||
`am Brett steht sie auch (${zeile?.stimmen}, meine ${zeile?.meine_stimme})`);
|
||||
|
||||
/* GEGENPROBE: Auf einem anderen Brett gibt es keine Stimme. Ohne
|
||||
diese Zeile waere "es geht" auch dann gruen, wenn es ueberall
|
||||
ginge -- und dann waere die Wunschliste keine Abstimmung mehr,
|
||||
sondern ein Nebeneffekt. */
|
||||
const f = await roh(`/workspace/api/treff/eintrag/${bosBeitrag}/willich`, TREFF,
|
||||
{ cookie: kKessi }, {}, "PUT");
|
||||
ok(f.code === 400, `im Gespraech gibt es kein "Will ich auch" (${f.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
2. ANHEFTEN — höchstens drei
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 2. Anheften ===");
|
||||
{
|
||||
const heften = (id, keks, m = "PUT") =>
|
||||
roh(`/workspace/api/treff/eintrag/${id}/anheften`, m === "PUT" ? CREW : CREW,
|
||||
{ cookie: keks }, {}, m);
|
||||
|
||||
let durch = 0;
|
||||
for (const id of anschlag.slice(0, 3)) {
|
||||
const a = await heften(id, kModi);
|
||||
if (a.code === 200) durch++;
|
||||
}
|
||||
ok(durch === 3, `drei Ansagen haengen (${durch})`);
|
||||
|
||||
const vierte = await heften(anschlag[3], kModi);
|
||||
ok(vierte.code === 409, `die vierte wird abgelehnt (${vierte.code})`);
|
||||
ok(/3/.test(json(vierte).fehler || ""),
|
||||
"und die Absage nennt die Zahl, statt nur nein zu sagen");
|
||||
|
||||
/* NACH DEM ABNEHMEN GEHT SIE DURCH. Ohne diese Zeile waere die
|
||||
Grenze auch dann gruen, wenn ueberhaupt nichts mehr angeheftet
|
||||
werden koennte. */
|
||||
await heften(anschlag[0], kModi, "DELETE");
|
||||
const nochmal = await heften(anschlag[3], kModi);
|
||||
ok(nochmal.code === 200, `nach dem Abnehmen geht die vierte (${nochmal.code})`);
|
||||
|
||||
/* Und die Community heftet nichts an. */
|
||||
const gast = await roh(`/workspace/api/treff/eintrag/${anschlag[1]}/anheften`, TREFF,
|
||||
{ cookie: kKessi }, {}, "PUT");
|
||||
ok(gast.code === 404, `ein Mitglied kann nichts anheften (${gast.code})`);
|
||||
|
||||
const brett = await roh("/workspace/api/bereich/anschlag", CREW, { cookie: kModi });
|
||||
const oben = (json(brett).eintraege || [])[0];
|
||||
ok(oben?.angeheftet === 1, `oben steht eine angeheftete (${oben?.titel})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. MELDEN — der Grund ist Pflicht (DSA Art. 16)
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 3. Melden ===");
|
||||
{
|
||||
const weg = `/workspace/api/treff/eintrag/${bosBeitrag}/melden`;
|
||||
const ohne = await roh(weg, TREFF, { cookie: kKessi }, { grund: "" });
|
||||
ok(ohne.code === 400, `ohne Grund geht es nicht (${ohne.code})`);
|
||||
|
||||
const kurz = await roh(weg, TREFF, { cookie: kKessi }, { grund: "doof" });
|
||||
ok(kurz.code === 400, `"doof" ist kein Grund (${kurz.code})`);
|
||||
|
||||
const mit = await roh(weg, TREFF, { cookie: kKessi },
|
||||
{ grund: "Da steht die Handynummer von jemandem drin." });
|
||||
ok(mit.code === 200, `mit Grund geht es (${mit.code})`);
|
||||
|
||||
const liste = await roh("/workspace/api/treff/meldungen", CREW, { cookie: kModi });
|
||||
const m = (json(liste).meldungen || [])[0];
|
||||
ok(liste.code === 200 && m?.eintrag_id === bosBeitrag,
|
||||
`der Modi sieht die Meldung (${(json(liste).meldungen || []).length})`);
|
||||
ok(m?.autor_rolle_name === "Community",
|
||||
`und wer sie geschrieben hat, mit vollem Rollennamen (${m?.autor_rolle_name})`);
|
||||
|
||||
/* Die Community sieht die Meldungen NICHT -- sonst waere ablesbar,
|
||||
wer wen gemeldet hat. */
|
||||
const gast = await roh("/workspace/api/treff/meldungen", TREFF, { cookie: kBo });
|
||||
ok(gast.code === 404, `ein Mitglied sieht die Liste nicht (${gast.code})`);
|
||||
|
||||
/* Entscheiden braucht ebenfalls einen Satz. */
|
||||
const leer = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
|
||||
{ cookie: kModi }, { status: "erledigt", entscheid: "" }, "PATCH");
|
||||
ok(leer.code === 400, `entscheiden ohne Begruendung geht nicht (${leer.code})`);
|
||||
|
||||
const fertig = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
|
||||
{ cookie: kModi }, { status: "erledigt", entscheid: "Nummer entfernt, Bo Bescheid gesagt." },
|
||||
"PATCH");
|
||||
ok(fertig.code === 200, `mit Begruendung schon (${fertig.code})`);
|
||||
|
||||
const zweimal = await roh(`/workspace/api/treff/meldung/${m?.id}`, CREW,
|
||||
{ cookie: kModi }, { status: "abgelehnt", entscheid: "Doch nicht so schlimm gewesen." },
|
||||
"PATCH");
|
||||
ok(zweimal.code === 409, `zweimal entscheiden geht nicht (${zweimal.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
4. ENTFERNEN — mit Grund, und der Grund überlebt (DSA Art. 17)
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 4. Entfernen ===");
|
||||
{
|
||||
const weg = `/workspace/api/bereich/treff/${bosBeitrag}`;
|
||||
const ohne = await roh(weg, CREW, { cookie: kModi }, null, "DELETE");
|
||||
ok(ohne.code === 400, `ohne Grund wird nichts entfernt (${ohne.code})`);
|
||||
|
||||
/* Und der Eintrag ist auch wirklich noch da. Ein 400 sagt nur, was
|
||||
der Server GEANTWORTET hat -- nicht, was er getan hat. */
|
||||
const noch = await roh("/workspace/api/bereich/treff", CREW, { cookie: kModi });
|
||||
ok((json(noch).eintraege || []).some((x) => x.id === bosBeitrag),
|
||||
"und der Beitrag steht noch da");
|
||||
|
||||
const mit = await roh(`${weg}?grund=`
|
||||
+ encodeURIComponent("Enthielt die Telefonnummer einer anderen Person."),
|
||||
CREW, { cookie: kModi }, null, "DELETE");
|
||||
ok(mit.code === 200, `mit Grund geht es (${mit.code})`);
|
||||
|
||||
const danach = await roh("/workspace/api/bereich/treff", CREW, { cookie: kModi });
|
||||
ok(!(json(danach).eintraege || []).some((x) => x.id === bosBeitrag),
|
||||
"der Beitrag ist weg");
|
||||
|
||||
/* DAS IST DER EIGENTLICHE PUNKT: Die Begruendung ist NICHT weg.
|
||||
Haenge sie mit ON DELETE CASCADE am Eintrag, waere sie im selben
|
||||
Moment verschwunden wie das, was sie begruendet. */
|
||||
const akte = await roh("/workspace/api/treff/entfernt", HAUS, { cookie: kDogi });
|
||||
const e = (json(akte).entfernt || [])[0];
|
||||
ok(e?.eintrag_id === bosBeitrag && /Telefonnummer/.test(e?.grund || ""),
|
||||
`und der Grund steht noch da (${e?.grund?.slice(0, 30)}…)`);
|
||||
ok(e?.titel === "BO-SAGT-HALLO" && e?.autor_name === "Bo",
|
||||
`samt Titel und Verfasser (${e?.titel}, ${e?.autor_name})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
5. DIE BANNLEITER — Modis befristet, dauerhaft nur DogFather
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 5. Massnahmen ===");
|
||||
{
|
||||
const weg = "/workspace/api/treff/massnahme";
|
||||
|
||||
const ohne = await roh(weg, CREW, { cookie: kModi },
|
||||
{ person_id: idBo, art: "pause", tage: 3, grund: "" });
|
||||
ok(ohne.code === 400, `ohne Grund geht keine Massnahme (${ohne.code})`);
|
||||
|
||||
const kurz = await roh(weg, CREW, { cookie: kModi },
|
||||
{ person_id: idBo, art: "pause", tage: 3, grund: "Hat mehrfach gegen Regel 2 verstossen." });
|
||||
ok(kurz.code === 200, `ein Modi darf drei Tage Pause geben (${kurz.code})`);
|
||||
|
||||
const lang = await roh(weg, CREW, { cookie: kModi },
|
||||
{ person_id: idPim, art: "ausschluss", tage: 30, grund: "Immer wieder dasselbe, trotz Hinweis." });
|
||||
ok(lang.code === 403, `dreissig Tage darf er nicht (${lang.code})`);
|
||||
ok(/7/.test(json(lang).fehler || ""), "und die Absage nennt die Grenze");
|
||||
|
||||
const dauer = await roh(weg, CREW, { cookie: kModi },
|
||||
{ person_id: idPim, art: "ausschluss", grund: "Endgueltig, nach mehreren Hinweisen." });
|
||||
ok(dauer.code === 403, `und dauerhaft schon gar nicht (${dauer.code})`);
|
||||
|
||||
/* GEGENPROBE. Ohne sie waeren die beiden Zeilen darueber auch dann
|
||||
gruen, wenn NIEMAND ausschliessen koennte. Sieben Tage, damit Pim
|
||||
fuer die naechste Zeile noch hereinkommt. */
|
||||
const dogi7 = await roh(weg, CREW, { cookie: kModi },
|
||||
{ person_id: idPim, art: "ausschluss", tage: 7, grund: "Sieben Tage, das darf er." });
|
||||
ok(dogi7.code === 200, `sieben Tage darf ein Modi (${dogi7.code})`);
|
||||
|
||||
/* Und gegen jemanden aus dem TEAM geht gar nichts -- das gehoert
|
||||
nach "Personen & Zugaenge", nicht hierher. */
|
||||
const gegenTeam = await roh(weg, HAUS, { cookie: kDogi },
|
||||
{ person_id: idDogi, art: "pause", tage: 1, grund: "Nur ein Versuch, das darf nicht gehen." });
|
||||
ok(gegenTeam.code === 404, `gegen das Team gibt es keine Massnahme (${gegenTeam.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
6. DER AUSSCHLUSS WIRKT — nicht nur beim Schreiben
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 6. Ausschluss ===");
|
||||
{
|
||||
/* Kessi kommt herein -- vor dem Ausschluss. Ohne diese Zeile waere
|
||||
"sie kommt nicht mehr herein" auch dann gruen, wenn sie es nie
|
||||
konnte. */
|
||||
/* "dabei" und nicht "stamm": Kessi ist 40 Tage dabei, hat aber
|
||||
keinen eigenen Beitrag geschrieben. Beide Bedingungen muessen
|
||||
erfuellt sein -- genau das misst diese Zeile nebenbei mit. */
|
||||
const vorher = await roh("/workspace/api/treff/lage", TREFF, { cookie: kKessi });
|
||||
ok(vorher.code === 200 && json(vorher).stufe === "dabei",
|
||||
`Kessi ist drin, Stufe ${json(vorher).stufe}`);
|
||||
|
||||
const aus = await roh("/workspace/api/treff/massnahme", HAUS, { cookie: kDogi },
|
||||
{ person_id: idKessi, art: "ausschluss", grund: "Dauerhaft, nach mehreren Vorfaellen." });
|
||||
ok(aus.code === 200 && json(aus).bis === null,
|
||||
`DogFather schliesst dauerhaft aus (bis: ${json(aus).bis})`);
|
||||
|
||||
/* DIE SITZUNG IST WEG, nicht nur das Schreiben. Ein
|
||||
Ausgeschlossener, der weiter mitlesen kann, ist bei Belaestigung
|
||||
genau der Teil, der weh tut. */
|
||||
const lage = await roh("/workspace/api/treff/lage", TREFF, { cookie: kKessi });
|
||||
ok(lage.code === 401, `ihre Sitzung gilt nicht mehr (${lage.code})`);
|
||||
|
||||
const brett = await roh("/workspace/api/bereich/treff", TREFF, { cookie: kKessi });
|
||||
ok(brett.code === 401, `und sie liest auch nichts mehr (${brett.code})`);
|
||||
|
||||
/* Und sie kommt auch nicht wieder herein -- gar nicht erst. Ohne
|
||||
diese Zeile waere die Anmeldung gelungen, das Plaetzchen gesetzt,
|
||||
und dahinter eine Seite, auf der nichts geht. Das sieht aus wie
|
||||
ein kaputtes Programm, nicht wie eine Entscheidung. */
|
||||
const neu = await anmelden("gast", "CODE-GAST-0001", TREFF);
|
||||
ok(!neu, "und sie kann sich nicht neu anmelden");
|
||||
|
||||
/* GEGENPROBE: Bo hat nur eine PAUSE -- kein Ausschluss. Er darf
|
||||
weiter lesen, aber nicht schreiben. Ohne diese Zeile waeren die
|
||||
vier darueber auch dann gruen, wenn jede Massnahme jeden
|
||||
aussperrt. */
|
||||
const boLage = await roh("/workspace/api/treff/lage", TREFF, { cookie: kBo });
|
||||
ok(boLage.code === 200, `Bo liest weiter (${boLage.code})`);
|
||||
ok(json(boLage).sperre?.art === "pause" && !!json(boLage).sperre?.bis,
|
||||
`und sieht seine Pause mit Enddatum (${json(boLage).sperre?.bis?.slice(0, 10)})`);
|
||||
ok(json(boLage).schreiben?.treff === false, "schreiben darf er nicht");
|
||||
|
||||
/* Und Pim, sieben Tage ausgeschlossen, kommt gar nicht mehr herein
|
||||
-- ein befristeter Ausschluss ist ein Ausschluss, keine Pause. */
|
||||
const pim = await anmelden("gast", "CODE-GAST-0003", TREFF);
|
||||
ok(!pim, "ein befristeter Ausschluss sperrt ebenfalls aus");
|
||||
|
||||
const versuch = await roh("/workspace/api/bereich/treff", TREFF, { cookie: kBo },
|
||||
{ art: ersteArt("treff"), titel: "BO-TROTZDEM", text: "Hallo?", datum: heute });
|
||||
ok(versuch.code === 403, `und es geht auch wirklich nicht (${versuch.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
7. AUS „MITMACHEN" WIRD EIN TALENT — genau einmal
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 7. Die Bruecke zu den Talenten ===");
|
||||
{
|
||||
const weg = `/workspace/api/treff/mitmachen/${mitmachen1}/uebernehmen`;
|
||||
const a = await roh(weg, HAUS, { cookie: kDogi }, {});
|
||||
ok(a.code === 200 && json(a).talent_id > 0, `Bo landet bei den Talenten (${a.code})`);
|
||||
|
||||
const b = await roh(weg, HAUS, { cookie: kDogi }, {});
|
||||
ok(b.code === 409, `ein zweites Mal legt keinen zweiten an (${b.code})`);
|
||||
|
||||
const talente = await roh("/workspace/api/bereich/talente", HAUS, { cookie: kDogi });
|
||||
const t = (json(talente).eintraege || []).filter((x) => x.creator_id === idBo);
|
||||
ok(t.length === 1, `und es steht genau einer dort (${t.length})`);
|
||||
ok(/Bo/.test(t[0]?.titel || ""), `mit seinem Namen (${t[0]?.titel})`);
|
||||
|
||||
/* Die Community kommt an die Talente nicht heran -- sonst waere die
|
||||
Bruecke ein Fenster. */
|
||||
const gast = await roh("/workspace/api/bereich/talente", TREFF, { cookie: kBo });
|
||||
ok(gast.code === 404, `ein Mitglied sieht die Talente nicht (${gast.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
8. DIE REGELN NENNEN DIE ZAHLEN, DIE WIRKLICH GELTEN
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 8. Der Regeltext ===");
|
||||
{
|
||||
const a = await roh("/workspace/api/treff/regeln", TREFF, { cookie: kBo });
|
||||
const r = json(a);
|
||||
ok(a.code === 200 && r.mindestalter === treffModul.MINDESTALTER,
|
||||
`Mindestalter ${r.mindestalter} -- dieselbe Zahl wie im Code`);
|
||||
ok(r.mindestalter === 18, "und sie ist 18 (Entscheidung Filipe, 11.09.2026)");
|
||||
ok(Array.isArray(r.stufen) && r.stufen.length === treffModul.STUFEN.length,
|
||||
`alle ${r.stufen?.length} Stufen kommen mit`);
|
||||
ok(r.anschlag_max === treffModul.ANSCHLAG_MAX,
|
||||
`und die Zahl am Anschlagbrett (${r.anschlag_max})`);
|
||||
|
||||
/* Die Seite selbst gehoert der Community UND denen, die moderieren
|
||||
-- ein Modi, der die Regeln nicht sehen kann, moderiert nach
|
||||
Gefuehl. */
|
||||
const rechte = await import("./rechte.js");
|
||||
const wer = rechte.SEITEN["/workspace/treff-regeln.html"] || [];
|
||||
ok(wer.includes("gast") && wer.includes("modi") && wer.includes("admin"),
|
||||
`die Regelseite steht fuer ${wer.join(", ")} offen`);
|
||||
ok(!wer.includes("manager") && !wer.includes("creator") && !wer.includes("spicy"),
|
||||
"und nicht fuer die Agentur");
|
||||
|
||||
/* "Wer sieht was" ist allein DogFathers Seite. */
|
||||
const tafel = rechte.SEITEN["/workspace/rechte.html"] || [];
|
||||
ok(tafel.length === 1 && tafel[0] === "admin",
|
||||
`"Wer sieht was" nur fuer ${tafel.join(", ")}`);
|
||||
|
||||
const fremd = await roh("/workspace/api/rechte/tafel", CREW, { cookie: kModi });
|
||||
ok(fremd.code === 404, `ein Modi bekommt die Tafel nicht (${fremd.code})`);
|
||||
|
||||
const meine = await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi });
|
||||
const t = json(meine);
|
||||
ok(meine.code === 200 && t.seiten?.length === rechte.alleSeiten().length,
|
||||
`DogFather bekommt alle ${t.seiten?.length} Seiten`);
|
||||
const community = (t.rollen || []).find((x) => x.schluessel === "gast");
|
||||
ok(community && community.seiten < 5,
|
||||
`und die Community steht bei ${community?.seiten} von ${t.seiten?.length}`);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------- */
|
||||
melde("");
|
||||
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||||
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
||||
/* Der Ordner bleibt manchmal liegen, weil der Dienst die Datenbank
|
||||
unter Windows noch offen haelt. Das ist ein Aufraeumrest, kein
|
||||
Befund -- deshalb faellt der Lauf daran nicht durch. */
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
||||
process.exit(fehler ? 1 : 0);
|
||||
@@ -0,0 +1,613 @@
|
||||
/* =====================================================================
|
||||
DER TREFF — die Community und ihre sieben Bretter (11.09.2026)
|
||||
|
||||
Filipe:
|
||||
|
||||
"eine community rolle und community kategorie. wo die community
|
||||
auch dan nur die sieht und die modis rechte hand und ich also
|
||||
dogfather sehen die aber auch. die community aber nur die dann."
|
||||
|
||||
"wieso nur eine kachel fuer die community ich will dass die
|
||||
mehrere kacheln haben. mit mehreren optionen."
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DIESE PRUEFUNG MISST IN BEIDE RICHTUNGEN, UND DAS IST DER GANZE PUNKT
|
||||
|
||||
Eine Trennung kann auf zwei Arten falsch sein, und die zweite ist die
|
||||
gefaehrlichere:
|
||||
|
||||
1. SIE GREIFT NICHT -- die Community sieht Dinge des Hauses. Das
|
||||
faellt auf, wenn jemand hinsieht.
|
||||
2. SIE GREIFT ZU WEIT -- die Community sieht ihren eigenen Raum
|
||||
nicht. Das faellt NICHT auf: Ein leeres Brett sieht aus wie
|
||||
"hier ist nichts los", nicht wie ein Fehler. Genau so ist am
|
||||
01.09. ein Manager vor einem leeren Aufgabenbrett gestanden und
|
||||
hat es nicht gemeldet.
|
||||
|
||||
Deshalb steht zu jeder Zeile "sieht es nicht" eine Zeile "und der da
|
||||
sieht es sehr wohl". Ohne die zweite waere die erste auch dann gruen,
|
||||
wenn schlicht gar nichts mehr geht.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM node:http UND NICHT fetch
|
||||
|
||||
Das Team und die Community melden sich an verschiedenen Waenden an,
|
||||
und welche Wand gilt, entscheidet der Host-Kopf. `fetch` in Node
|
||||
setzt den selbst und ignoriert einen gesetzten -- die Anmeldung ginge
|
||||
dann an die falsche Wand und scheiterte, ohne dass man saehe warum.
|
||||
Dieselbe Falle wie am 10.09. bei den Kategorie-Kanaelen.
|
||||
===================================================================== */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { request as httpAnfrage } from "node:http";
|
||||
import { portMussFreiSein } from "./helfer-port.mjs";
|
||||
|
||||
const PORT = await portMussFreiSein(4398, "pruef-treff");
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-treff-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
process.env.PORT = String(PORT);
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
await import("./index.js");
|
||||
notbremse(240_000, "pruef-treff");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
|
||||
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
||||
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
||||
|
||||
let fehler = 0, geprueft = 0;
|
||||
const melde = (t) => console.log(t);
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
const TREFF = "treff.dogfather-universe.com";
|
||||
const CREW = "crew.dogfather-universe.com";
|
||||
const HAUS = "workspace.dogfather-universe.com";
|
||||
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||||
const w = await import("./workspace.js");
|
||||
const treffModul = await import("./workspace-treff.js");
|
||||
const { BEREICHE } = await import("./workspace-bereiche.js");
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const jetzt = new Date().toISOString();
|
||||
const schluessel = randomBytes(32).toString("hex");
|
||||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||||
.run("code_kennung_schluessel", schluessel, jetzt);
|
||||
function anlegen(name, rolle, code) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||||
+ " VALUES (?,?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768, createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
anlegen("Rieke", "hand", "CODE-HAND-0001");
|
||||
anlegen("Frida", "modi", "CODE-MODI-0001");
|
||||
anlegen("Mara", "manager", "CODE-MANA-0001");
|
||||
anlegen("Luna", "creator", "CODE-CREA-0001");
|
||||
const idGast = anlegen("Kessi", "gast", "CODE-GAST-0001");
|
||||
|
||||
/* EIN EINTRAG IN JEDEM DER SIEBEN BRETTER. Ohne Daten waere jede Zeile
|
||||
"sieht nichts" trivial wahr -- der gruene Haken ueber der Null. */
|
||||
const rein = d.prepare(
|
||||
"INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)"
|
||||
+ " VALUES (?,?,?,?,?,?,?)");
|
||||
const BRETTER = ["anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln", "mitmachen"];
|
||||
for (const b of BRETTER) rein.run(b, Object.keys((await import("./workspace-bereiche.js")).BEREICHE[b].arten)[0],
|
||||
`TREFF-PROBE-${b.toUpperCase()}`, "", jetzt.slice(0, 10), jetzt, idDogi);
|
||||
/* Und einer im Haus, den die Community NICHT sehen darf. */
|
||||
rein.run("ideen", "content", "HAUS-GEHEIM-4711", "", jetzt.slice(0, 10), jetzt, idDogi);
|
||||
d.close();
|
||||
|
||||
/* Die Methode ist ein eigenes Argument, seit es Wege gibt, die PUT
|
||||
oder DELETE heissen. Vorher wurde sie aus "gibt es einen Koerper?"
|
||||
erraten -- das ging, solange nur GET und POST vorkamen, und haette
|
||||
beim ersten PUT stillschweigend ein POST geschickt. Der Server
|
||||
haette mit 404 geantwortet, und man sucht den Fehler im Server. */
|
||||
function roh(pfad, host, kopf = {}, koerper = null, methode = null) {
|
||||
return new Promise((fertig, schief) => {
|
||||
const a = httpAnfrage({
|
||||
host: "127.0.0.1", port: PORT, path: pfad,
|
||||
method: methode || (koerper ? "POST" : "GET"),
|
||||
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
|
||||
}, (antwort) => {
|
||||
let text = "";
|
||||
antwort.on("data", (s) => { text += s; });
|
||||
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
|
||||
});
|
||||
a.on("error", schief);
|
||||
if (koerper) a.write(JSON.stringify(koerper));
|
||||
a.end();
|
||||
});
|
||||
}
|
||||
async function anmelden(rolle, code, host) {
|
||||
const r = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
|
||||
const kekse = [].concat(r.kopf?.["set-cookie"] || []);
|
||||
return { keks: kekse.map((c) => c.split(";")[0]).join("; "), code: r.code };
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
1. DIE WAND: die Community kommt auf genau eine Adresse
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 1. Eine Rolle, eine Wand ===");
|
||||
const gast = await anmelden("gast", "CODE-GAST-0001", TREFF);
|
||||
ok(gast.code === 200 && gast.keks.length > 0, `die Community kommt auf treff. herein (${gast.code})`);
|
||||
|
||||
for (const [name, host] of [["crew.", CREW], ["workspace.", HAUS]]) {
|
||||
const a = await anmelden("gast", "CODE-GAST-0001", host);
|
||||
const b = await anmelden("gast", "VOELLIG-ERFUNDEN-XYZ", host);
|
||||
ok(a.code === 401 && a.keks === "", `auf ${name} nicht (${a.code})`);
|
||||
ok(a.code === b.code, `und zwar mit derselben Antwort wie ein erfundener Code (${a.code}/${b.code})`);
|
||||
}
|
||||
{
|
||||
/* UND DIE GEGENRICHTUNG: Auf der Wand des Treffs kommt sonst niemand
|
||||
herein -- auch DogFather nicht. Er hat dort nichts zu suchen; er
|
||||
sieht den Treff auf seiner eigenen Adresse. */
|
||||
const a = await anmelden("admin", "CODE-DOGI-0001", TREFF);
|
||||
const b = await anmelden("modi", "CODE-MODI-0001", TREFF);
|
||||
ok(a.code !== 200 && b.code !== 200,
|
||||
`auf treff. kommt sonst niemand herein -- auch DogFather nicht (${a.code}/${b.code})`);
|
||||
}
|
||||
|
||||
const kDogi = (await anmelden("admin", "CODE-DOGI-0001", HAUS)).keks;
|
||||
const kModi = (await anmelden("creator", "CODE-MODI-0001", CREW)).keks;
|
||||
const kCrea = (await anmelden("creator", "CODE-CREA-0001", HAUS)).keks;
|
||||
const kMana = (await anmelden("manager", "CODE-MANA-0001", HAUS)).keks;
|
||||
ok([kDogi, kModi, kCrea, kMana].every((k) => k.length > 0), "die vier Vergleichsrollen sind angemeldet");
|
||||
|
||||
/* =======================================================================
|
||||
1b. DIE WEICHE: drei Adressen, drei Türen, keine Verwechslung
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 1b. Die Weiche des Treffs ===");
|
||||
{
|
||||
/* DIE WAND. Ohne die Weiche stünde auf der Adresse der Community die
|
||||
Wand der Agentur: Spicy-Media-Logo, "Creator Workspace", fünf
|
||||
Rollenkacheln mit den Namen Admin, Manager, Scout, Creator -- die
|
||||
komplette Struktur des Unternehmens, VOR der Anmeldung, für jeden,
|
||||
der die Adresse hat. Das ist das eigentliche Leck, gegen das
|
||||
treffWeiche gebaut ist. */
|
||||
for (const pfad of ["/workspace/", "/workspace/index.html"]) {
|
||||
const a = await roh(pfad, TREFF);
|
||||
const ist = a.code === 200 && /Der Treff|Der Treff/.test(a.text);
|
||||
ok(ist, `${pfad} zeigt die Wand des Treffs (${a.code})`);
|
||||
ok(!/Creator\s*Workspace|Spicy/i.test(a.text),
|
||||
`und nennt weder Workspace noch Spicy (${pfad})`);
|
||||
}
|
||||
|
||||
/* KEINE ROLLENNAMEN DES HAUSES in der ausgelieferten Datei. Die
|
||||
beiden anderen Wände nennen ihre Rollen offen -- dort ist das
|
||||
richtig, denn dort steht nur, wer ohnehin dazugehört. Hier steht
|
||||
ein Mensch, der NICHT zum Team gehört. */
|
||||
{
|
||||
const a = await roh("/workspace/treff-index.html", TREFF);
|
||||
const verraten = ["manager", "scout", "creator", "modi", "rechte hand"]
|
||||
.filter((w) => new RegExp(w, "i").test(a.text));
|
||||
ok(verraten.length === 0,
|
||||
`die Wand nennt keine Rolle des Hauses${verraten.length ? " -- FUND: " + verraten.join(", ") : ""}`);
|
||||
}
|
||||
|
||||
/* DAS MANIFEST MACHT DIE APP. Drei Adressen, drei Namen -- sonst
|
||||
hieße die App auf dem Startbildschirm eines Mitglieds "Team Dogi",
|
||||
der Name eines Bereichs, zu dem es nicht gehört. */
|
||||
const namen = {};
|
||||
for (const [was, host] of [["treff", TREFF], ["crew", CREW], ["haus", HAUS]]) {
|
||||
const a = await roh("/workspace/app.webmanifest", host);
|
||||
try { namen[was] = JSON.parse(a.text).name; } catch { namen[was] = `(kein JSON: ${a.code})`; }
|
||||
}
|
||||
ok(namen.treff === "Der Treff", `das Manifest heißt "${namen.treff}"`);
|
||||
ok(namen.crew === "Team Dogi" && namen.treff !== namen.crew,
|
||||
`und auf crew. weiterhin "${namen.crew}"`);
|
||||
ok(new Set(Object.values(namen)).size === 3,
|
||||
`drei Adressen, drei Namen (${Object.values(namen).join(" · ")})`);
|
||||
|
||||
/* JEDE WAND GEHÖRT ZU GENAU EINER TÜR. Beide Richtungen, sonst
|
||||
beweist es nichts: Die eigene Wand MUSS erreichbar sein. */
|
||||
const waende = [
|
||||
["/workspace/crew-index.html", CREW, TREFF],
|
||||
["/workspace/treff-index.html", TREFF, CREW],
|
||||
["/workspace/treff-index.html", TREFF, HAUS],
|
||||
];
|
||||
for (const [datei, zuhause, fremd] of waende) {
|
||||
const a = await roh(datei, zuhause);
|
||||
const b = await roh(datei, fremd);
|
||||
ok(a.code === 200, `${datei} gibt es zu Hause (${a.code})`);
|
||||
ok(b.code === 404, `und auf der fremden Adresse nicht (${b.code})`);
|
||||
}
|
||||
/* Auch mit doppeltem Schrägstrich -- express.static zieht sie später
|
||||
selbst zusammen und liefert die Datei aus. Genau daran ist am
|
||||
04.09.2026 die Schranke des Workspace vorbeigelaufen. */
|
||||
const schraeg = await roh("/workspace//treff-index.html", CREW);
|
||||
ok(schraeg.code === 404, `auch mit doppeltem Schrägstrich nicht (${schraeg.code})`);
|
||||
|
||||
/* NICHT INDIZIEREN -- als Kopf und nicht nur als <meta>, damit er
|
||||
auch für Manifest, Bilder und jede Antwort ohne HTML gilt. */
|
||||
const kopf = await roh("/workspace/", TREFF);
|
||||
ok(/noindex/i.test(kopf.kopf?.["x-robots-tag"] || ""),
|
||||
`der Kopf sagt noindex (${kopf.kopf?.["x-robots-tag"]})`);
|
||||
|
||||
/* NUR DER WORKSPACE. Sonst läge die komplette Dogfather-Website ein
|
||||
zweites Mal unter einer zweiten Adresse. */
|
||||
const haupt = await roh("/", TREFF);
|
||||
ok(haupt.code === 302 && haupt.kopf?.location === "/workspace/",
|
||||
`die Hauptseite gibt es hier nicht (${haupt.code} → ${haupt.kopf?.location})`);
|
||||
|
||||
/* DAS HAUS. haus.css ist auf der Agenturadresse leer; auf treff.
|
||||
muss dahinter das Farbhaus von Team Dogi stehen -- sonst käme die
|
||||
Seite grau an und färbte sich erst im Browser um. */
|
||||
const haus = await roh("/workspace/assets/css/haus.css?v=1", TREFF);
|
||||
ok(haus.code === 200 && /--sternenfeld/.test(haus.text),
|
||||
`haus.css bringt das Farbhaus mit (${haus.text.length} Zeichen)`);
|
||||
|
||||
/* DIE BÜHNEN INNEN. Die Motive der Agentur zeigen das
|
||||
Spicy-Media-Logo selbst -- teils vielfach nebeneinander. */
|
||||
const a1 = await roh("/workspace/assets/img/buehne-lounge-breit.webp", TREFF);
|
||||
const a2 = await roh("/workspace/assets/img/crew-buehne-lounge-breit.webp", TREFF);
|
||||
const a3 = await roh("/workspace/assets/img/buehne-lounge-breit.webp", HAUS);
|
||||
ok(a1.code === 200 && a1.text.length === a2.text.length,
|
||||
`die Bühne kommt von Team Dogi (${a1.text.length} = ${a2.text.length})`);
|
||||
ok(a1.text.length !== a3.text.length,
|
||||
`und ist eine andere als die der Agentur (${a1.text.length} ≠ ${a3.text.length})`);
|
||||
|
||||
/* DAS ZEICHEN IN DER KOPFLEISTE. start.css setzt dort eine Chili --
|
||||
als CSS-Hintergrund, auf jeder Seite, den ganzen Tag. */
|
||||
const m1 = await roh("/workspace/assets/img/marke-spicy.webp", TREFF);
|
||||
const m2 = await roh("/workspace/assets/img/marke-husky.webp", TREFF);
|
||||
const m3 = await roh("/workspace/assets/img/marke-spicy.webp", HAUS);
|
||||
ok(m1.code === 200 && m1.text.length === m2.text.length,
|
||||
`in der Kopfleiste steht der Husky (${m1.text.length})`);
|
||||
ok(m1.text.length !== m3.text.length,
|
||||
`und nicht die Chili (${m1.text.length} ≠ ${m3.text.length})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
2. DIE KACHELN: sieben, und sonst nichts
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 2. Sieben Kacheln — und keine achte ===");
|
||||
let namen = [];
|
||||
{
|
||||
const a = await roh("/workspace/api/ich", TREFF, { cookie: gast.keks });
|
||||
const j = JSON.parse(a.text);
|
||||
ok(Array.isArray(j.bereiche) && j.bereiche.length === 7,
|
||||
`die Community bekommt sieben Kacheln (${j.bereiche?.length})`);
|
||||
/* Die Namen werden im naechsten Block noch einmal gebraucht -- dort
|
||||
wird gegen sie geprueft, statt sie ein zweites Mal zu tippen. Zwei
|
||||
Abschriften derselben sieben Namen waeren zwei Gelegenheiten,
|
||||
einen davon zu aendern und den anderen zu vergessen. */
|
||||
namen = (j.bereiche || []).map((k) => k.name);
|
||||
ok(namen.join(" | ") === "Anschlagbrett | Was ansteht | Der Treff | Wunschliste | Highlights | Regeln & Hilfe | Mitmachen",
|
||||
`und zwar diese: ${namen.join(", ")}`);
|
||||
ok((j.bereiche || []).every((k) => k.gruppe === "Community"),
|
||||
"alle in der Gruppe „Community“");
|
||||
const toene = new Set((j.bereiche || []).map((k) => k.ton));
|
||||
ok(toene.size === 7, `sieben verschiedene Farben (${[...toene].join(", ")})`);
|
||||
ok(j.rolle_name === "Community", `die Rolle heißt „${j.rolle_name}“`);
|
||||
}
|
||||
{
|
||||
/* GEGENPROBE: Das Team sieht sie mit -- sonst waere "sieben Kacheln"
|
||||
auch dann gruen, wenn sie sonst niemand bekommt.
|
||||
|
||||
UND ES SIEHT EINE MEHR (11.09.2026): "Meldungen & Massnahmen". Sie
|
||||
ist ausdruecklich KEIN Brett und steht deshalb nicht in
|
||||
TREFF_BEREICHE -- sonst waere sie bei der Community gelandet, und
|
||||
zwar lautlos: Sie saehe dort aus wie die anderen sieben, und erst
|
||||
beim Klicken haette der Server 404 gesagt.
|
||||
|
||||
Geprueft wird deshalb nicht eine Zahl, sondern die BEZIEHUNG: Die
|
||||
sieben der Community sind eine echte Teilmenge dessen, was das Team
|
||||
sieht -- und der Unterschied ist genau die Moderationskachel. Eine
|
||||
Zahl waere beim naechsten Umbau wieder rot, ohne dass etwas kaputt
|
||||
ist; diese Bedingung nicht. */
|
||||
const MOD = "Meldungen & Ma\u00dfnahmen";
|
||||
const a = JSON.parse((await roh("/workspace/api/ich", CREW, { cookie: kModi })).text);
|
||||
const beimModi = (a.bereiche || []).filter((k) => k.gruppe === "Community").map((k) => k.name);
|
||||
const fehlt = namen.filter((n) => !beimModi.includes(n));
|
||||
ok(fehlt.length === 0,
|
||||
`ein Modi sieht alle sieben der Community mit${fehlt.length ? " -- FEHLT: " + fehlt.join(", ") : ""}`);
|
||||
ok(beimModi.includes(MOD) && !namen.includes(MOD),
|
||||
`und genau eine mehr: ${beimModi.filter((n) => !namen.includes(n)).join(", ")}`);
|
||||
|
||||
const b = JSON.parse((await roh("/workspace/api/ich", HAUS, { cookie: kDogi })).text);
|
||||
const beiDogi = (b.bereiche_zusatz || []).filter((k) => k.gruppe === "Community").map((k) => k.name);
|
||||
ok(namen.every((n) => beiDogi.includes(n)) && beiDogi.includes(MOD),
|
||||
`und DogFather auf seiner Adresse auch (${beiDogi.length})`);
|
||||
|
||||
const c = JSON.parse((await roh("/workspace/api/ich", HAUS, { cookie: kMana })).text);
|
||||
const beiMana = (c.bereiche_zusatz || []).filter((k) => k.gruppe === "Community");
|
||||
ok(beiMana.length === 0, `eine Managerin nicht (${beiMana.length})`);
|
||||
|
||||
/* UND DIE MODERATIONSKACHEL IST BEI DER COMMUNITY WIRKLICH NICHT DA
|
||||
-- nicht nur "nicht in dieser Liste". Die Kachel ist ein Weg; wenn
|
||||
der Weg zu ist, muss auch die Tuer zu sein. */
|
||||
const d = JSON.parse((await roh("/workspace/api/ich", TREFF, { cookie: gast.keks })).text);
|
||||
const alleNamen = [...(d.bereiche || []), ...(d.bereiche_zusatz || [])].map((k) => k.name);
|
||||
ok(!alleNamen.includes(MOD), `die Community hat keine Moderationskachel (${alleNamen.length} Kacheln)`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. DIE BRETTER: was die Community sieht — und was nicht
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 3. Die sieben Bretter ===");
|
||||
async function brett(b, host, keks) {
|
||||
const a = await roh(`/workspace/api/bereich/${b}`, host, { cookie: keks });
|
||||
let titel = [];
|
||||
try { titel = (JSON.parse(a.text).eintraege || []).map((e) => e.titel); } catch { /* kein JSON */ }
|
||||
return { code: a.code, titel };
|
||||
}
|
||||
{
|
||||
let gefunden = 0;
|
||||
for (const b of BRETTER) {
|
||||
const r = await brett(b, TREFF, gast.keks);
|
||||
if (r.code === 200 && r.titel.includes(`TREFF-PROBE-${b.toUpperCase()}`)) gefunden++;
|
||||
}
|
||||
/* FUENF VON SIEBEN, UND DAS IST DAS ERGEBNIS -- kein Mangel.
|
||||
|
||||
Termine und Highlights sind fuer die Community erst da, wenn
|
||||
jemand aus dem Team sie freigegeben hat (TREFF_FREIGABE_BRETTER).
|
||||
Die Zahl steht deshalb nicht als 7 hier, sondern wird aus der
|
||||
Tafel gerechnet: Wer ein drittes Freigabe-Brett anlegt, bekommt
|
||||
hier keine rote Zeile fuer etwas Richtiges. */
|
||||
const OFFENE = BRETTER.filter((b) => !treffModul.TREFF_FREIGABE_BRETTER.includes(b));
|
||||
ok(gefunden === OFFENE.length,
|
||||
`die Community sieht die ${OFFENE.length} freien Bretter mit Inhalt (${gefunden}/${OFFENE.length})`);
|
||||
|
||||
/* DIE GEGENPROBE IN BEIDE RICHTUNGEN. Ohne sie waere die Zeile oben
|
||||
auch dann gruen, wenn die Freigabe gar nicht wirkt und die beiden
|
||||
Bretter aus einem anderen Grund leer sind. */
|
||||
let vorher = 0;
|
||||
for (const b of treffModul.TREFF_FREIGABE_BRETTER) {
|
||||
const r = await brett(b, TREFF, gast.keks);
|
||||
if (r.titel.includes(`TREFF-PROBE-${b.toUpperCase()}`)) vorher++;
|
||||
}
|
||||
ok(vorher === 0, `ohne Freigabe sieht die Community Termine und Highlights nicht (${vorher})`);
|
||||
|
||||
/* Jetzt freigeben -- ueber den echten Weg, nicht in der Datenbank. */
|
||||
let freigegeben = 0;
|
||||
for (const b of treffModul.TREFF_FREIGABE_BRETTER) {
|
||||
const beiDogi2 = await roh(`/workspace/api/bereich/${b}`, HAUS, { cookie: kDogi });
|
||||
const eintrag = (JSON.parse(beiDogi2.text).eintraege || [])
|
||||
.find((e) => e.titel === `TREFF-PROBE-${b.toUpperCase()}`);
|
||||
if (!eintrag) continue;
|
||||
const a = await roh(`/workspace/api/treff/eintrag/${eintrag.id}/freigeben`, HAUS,
|
||||
{ cookie: kDogi }, {}, "PUT");
|
||||
if (a.code === 200) freigegeben++;
|
||||
}
|
||||
ok(freigegeben === treffModul.TREFF_FREIGABE_BRETTER.length,
|
||||
`DogFather gibt beide frei (${freigegeben})`);
|
||||
|
||||
let nachher = 0;
|
||||
for (const b of treffModul.TREFF_FREIGABE_BRETTER) {
|
||||
const r = await brett(b, TREFF, gast.keks);
|
||||
if (r.titel.includes(`TREFF-PROBE-${b.toUpperCase()}`)) nachher++;
|
||||
}
|
||||
ok(nachher === treffModul.TREFF_FREIGABE_BRETTER.length,
|
||||
`nach der Freigabe sieht sie beide (${nachher})`);
|
||||
|
||||
/* Und ein Modi darf nicht freigeben, was er nicht sehen darf --
|
||||
hier: dass die Route ueberhaupt Rechte prueft. Eine Creatorin
|
||||
bekommt 404, nicht 403: den Treff gibt es fuer sie nicht. */
|
||||
const fremd = await roh("/workspace/api/treff/eintrag/1/freigeben", HAUS,
|
||||
{ cookie: kCrea }, {}, "PUT");
|
||||
ok(fremd.code === 404, `eine Creatorin kann nichts freigeben (${fremd.code})`);
|
||||
}
|
||||
{
|
||||
/* KEIN EINZIGES BRETT DES HAUSES. Ueber die Adresszeile probiert,
|
||||
nicht ueber die Kacheln -- eine Regel, die nur im Formular gilt,
|
||||
ist keine Regel. */
|
||||
const fremde = ["ideen", "angebote", "rueckmeldung", "entwicklung", "talente", "agentur", "live", "content"];
|
||||
const durch = [];
|
||||
for (const b of fremde) {
|
||||
const r = await brett(b, TREFF, gast.keks);
|
||||
if (r.code === 200) durch.push(b);
|
||||
}
|
||||
ok(durch.length === 0, `und kein einziges des Hauses${durch.length ? " -- DURCH: " + durch.join(", ") : ""}`);
|
||||
|
||||
/* DIE SUCHE SPIEGELT DIE FRAGE ZURUECK (Feld `frage`). Wer den
|
||||
Rohtext nach dem Suchwort durchsucht, findet deshalb IMMER einen
|
||||
Treffer -- und meldet ein Leck, das es nicht gibt. Genau darauf
|
||||
bin ich am 10.09. schon einmal hereingefallen und haette fast ein
|
||||
Loch repariert, das keines war. Gezaehlt wird, was die Suche
|
||||
WIRKLICH gefunden hat. */
|
||||
const suche = await roh("/workspace/api/suche?q=HAUS-GEHEIM-4711", TREFF, { cookie: gast.keks });
|
||||
let treffer = -1;
|
||||
try { treffer = JSON.parse(suche.text).gesamt; } catch { treffer = -1; }
|
||||
ok(treffer === 0, `auch die Suche findet nichts aus dem Haus (${treffer} Treffer)`);
|
||||
}
|
||||
{
|
||||
/* UND DIE AGENTUR SIEHT DEN TREFF NICHT. Beide Richtungen, beide
|
||||
ueber die Adresszeile. */
|
||||
const durch = [];
|
||||
for (const b of BRETTER) {
|
||||
const r = await brett(b, HAUS, kCrea);
|
||||
if (r.code === 200 && r.titel.some((t) => t.startsWith("TREFF-PROBE"))) durch.push(b);
|
||||
}
|
||||
ok(durch.length === 0, `eine Creatorin sieht kein Brett des Treffs${durch.length ? " -- DURCH: " + durch.join(", ") : ""}`);
|
||||
|
||||
const durchM = [];
|
||||
for (const b of BRETTER) {
|
||||
const r = await brett(b, HAUS, kMana);
|
||||
if (r.code === 200 && r.titel.some((t) => t.startsWith("TREFF-PROBE"))) durchM.push(b);
|
||||
}
|
||||
ok(durchM.length === 0, `eine Managerin auch nicht${durchM.length ? " -- DURCH: " + durchM.join(", ") : ""}`);
|
||||
|
||||
/* GEGENPROBE: Der Modi sieht sie sehr wohl. Ohne diese Zeile waeren
|
||||
die beiden darueber auch gruen, wenn die Bretter fuer NIEMANDEN
|
||||
lesbar waeren. */
|
||||
let sieht = 0;
|
||||
for (const b of BRETTER) {
|
||||
const r = await brett(b, CREW, kModi);
|
||||
if (r.code === 200 && r.titel.includes(`TREFF-PROBE-${b.toUpperCase()}`)) sieht++;
|
||||
}
|
||||
ok(sieht === 7, `ein Modi sieht alle sieben (${sieht}/7)`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
4. DIE SEITEN: die aus der Tafel, und keine einzige mehr
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 4. Nur die Seiten aus der Tafel ===");
|
||||
{
|
||||
/* DIE LISTE WIRD NICHT GETIPPT, SIE WIRD GEHOLT (11.09.2026).
|
||||
|
||||
Hier standen zwei Seiten und daneben "und keine dritte". Beim
|
||||
Anlegen der Regelseite wurde daraus eine dritte -- die Prüfung
|
||||
war rot, obwohl nichts kaputt war, und die naheliegende Reaktion
|
||||
wäre gewesen, die Zahl von zwei auf drei zu setzen. Beim vierten
|
||||
Mal wieder.
|
||||
|
||||
Gemessen wird jetzt das, was die Zeile eigentlich behaupten will:
|
||||
Die Tafel und die Wirklichkeit stimmen überein — jede Seite, die
|
||||
in rechte.js für "gast" steht, geht auf; jede andere ausgelieferte
|
||||
Seite geht NICHT auf. Das ist strenger als die alte Fassung (die
|
||||
elf Namen waren von Hand gewählt) und altert nicht. */
|
||||
const rechte = await import("./rechte.js");
|
||||
const { readdirSync } = await import("node:fs");
|
||||
const ordner = new URL("../workspace/", import.meta.url).pathname
|
||||
.replace(/^\/([A-Za-z]:)/, "$1");
|
||||
const alleDateien = readdirSync(ordner).filter((f) => f.endsWith(".html"))
|
||||
.map((f) => "/workspace/" + f);
|
||||
|
||||
const erlaubt = rechte.seitenFuer("gast");
|
||||
const verboten = alleDateien
|
||||
.filter((p) => !erlaubt.includes(p) && !rechte.OHNE_ANMELDUNG.includes(p));
|
||||
|
||||
ok(erlaubt.length > 0 && verboten.length > 0,
|
||||
`die Tafel gibt der Community ${erlaubt.length} Seiten und verbietet ${verboten.length}`);
|
||||
|
||||
const zu = [];
|
||||
for (const p of erlaubt) {
|
||||
const a = await roh(p, TREFF, { cookie: gast.keks });
|
||||
if (a.code !== 200) zu.push(`${p.split("/").pop()} (${a.code})`);
|
||||
}
|
||||
ok(zu.length === 0,
|
||||
`alle ${erlaubt.length} erlaubten öffnen sich${zu.length ? " -- ZU: " + zu.join(", ") : ""}`);
|
||||
|
||||
const durch = [];
|
||||
for (const p of verboten) {
|
||||
const a = await roh(p, TREFF, { cookie: gast.keks });
|
||||
if (a.code === 200) durch.push(p.split("/").pop());
|
||||
}
|
||||
ok(durch.length === 0,
|
||||
`und keine der ${verboten.length} anderen${durch.length ? " -- DURCH: " + durch.join(", ") : ""}`);
|
||||
|
||||
/* UND DIE MODERATIONSSEITE AUSDRÜCKLICH NICHT. Sie steht zwar in der
|
||||
Liste oben mit drin -- aber sie ist die eine, bei der ein Fehler
|
||||
wirklich weh täte: Dort stehen die Namen derer, die gemeldet
|
||||
haben. Eine eigene Zeile dafür, damit der Befund einen Namen hat
|
||||
und nicht in "eine von 20" untergeht. */
|
||||
const mod = await roh("/workspace/treff-moderation.html", TREFF, { cookie: gast.keks });
|
||||
ok(mod.code !== 200, `die Moderationsseite bleibt zu (${mod.code})`);
|
||||
const modApi = await roh("/workspace/api/treff/meldungen", TREFF, { cookie: gast.keks });
|
||||
ok(modApi.code === 404, `und die Meldungen dahinter auch (${modApi.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
5. DAS HAUS BLEIBT ZU: Aufgaben, Termine, Dateien, Personen
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 5. Vom Haus sieht sie nichts ===");
|
||||
for (const [name, pfad] of [
|
||||
["Aufgaben", "/workspace/api/aufgaben"],
|
||||
["Termine", "/workspace/api/termine"],
|
||||
["Dateien", "/workspace/api/dateien"],
|
||||
["Personen", "/workspace/api/personen"],
|
||||
["Gespräche", "/workspace/api/chat/raeume"],
|
||||
]) {
|
||||
const a = await roh(pfad, TREFF, { cookie: gast.keks });
|
||||
let leer = a.code !== 200;
|
||||
if (a.code === 200) {
|
||||
try {
|
||||
const j = JSON.parse(a.text);
|
||||
const felder = Object.values(j).filter(Array.isArray);
|
||||
leer = felder.every((f) => f.length === 0);
|
||||
/* Die eigene Person in einer Personenliste ist kein Leck. */
|
||||
if (!leer && pfad.endsWith("personen")) {
|
||||
leer = felder.every((f) => f.every((p) => p.id === idGast));
|
||||
}
|
||||
} catch { leer = true; }
|
||||
}
|
||||
ok(leer, `${name}: nichts (${a.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
6. SCHREIBEN — und wo es ankommt
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 6. Schreiben ===");
|
||||
{
|
||||
/* DIE ERSTE WOCHE IST EINE SANDKISTE. Wer eben erst dazugekommen
|
||||
ist, liest und stimmt ab -- mehr nicht. Das wird von BEIDEN
|
||||
Seiten der Grenze gemessen, sonst beweist es nichts: Eine
|
||||
Schreibsperre, die immer sperrt, ist keine Stufe. */
|
||||
const neu = await roh("/workspace/api/bereich/treff", TREFF, { cookie: gast.keks },
|
||||
{ art: "hallo", titel: "GAST-ZU-FRUEH-0001", text: "Hallo zusammen.", datum: jetzt.slice(0, 10) });
|
||||
ok(neu.code === 403, `wer neu ist, schreibt noch nicht (${neu.code})`);
|
||||
|
||||
const lage0 = JSON.parse((await roh("/workspace/api/treff/lage", TREFF, { cookie: gast.keks })).text);
|
||||
ok(lage0.stufe === "neu", `und die Lage sagt es ihm auch: ${lage0.stufe}`);
|
||||
ok(lage0.schreiben?.treff === false, "der Knopf waere aus");
|
||||
|
||||
/* Acht Tage zurueckdatieren -- genau EIN Tag ueber der Grenze. Eine
|
||||
Grenze, die nur von weit weg geprueft wird, koennte um Tage
|
||||
danebenliegen, ohne dass es auffaellt. */
|
||||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
dd.prepare("UPDATE personen SET erstellt = ? WHERE id = ?")
|
||||
.run(new Date(Date.now() - 8 * 86400000).toISOString(), idGast);
|
||||
dd.close();
|
||||
|
||||
const a = await roh("/workspace/api/bereich/treff", TREFF, { cookie: gast.keks },
|
||||
{ art: "hallo", titel: "GAST-SCHREIBT-8899", text: "Hallo zusammen.", datum: jetzt.slice(0, 10) });
|
||||
ok(a.code === 200 || a.code === 201, `nach acht Tagen schon (${a.code})`);
|
||||
|
||||
/* UND SECHS TAGE REICHEN NICHT. Die andere Seite derselben Grenze --
|
||||
ohne sie waere "acht Tage gehen" auch dann gruen, wenn die Stufe
|
||||
gar nicht geprueft wird. */
|
||||
const dd2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
dd2.prepare("UPDATE personen SET erstellt = ? WHERE id = ?")
|
||||
.run(new Date(Date.now() - 6 * 86400000).toISOString(), idGast);
|
||||
dd2.close();
|
||||
const knapp = await roh("/workspace/api/bereich/treff", TREFF, { cookie: gast.keks },
|
||||
{ art: "hallo", titel: "GAST-ZU-FRUEH-0002", text: "Und?", datum: jetzt.slice(0, 10) });
|
||||
ok(knapp.code === 403, `sechs Tage reichen nicht (${knapp.code})`);
|
||||
const dd3 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
dd3.prepare("UPDATE personen SET erstellt = ? WHERE id = ?")
|
||||
.run(new Date(Date.now() - 8 * 86400000).toISOString(), idGast);
|
||||
dd3.close();
|
||||
|
||||
/* Und auf ein Brett des Teams kommt er auch als "Dabei" nicht. */
|
||||
const brettDesTeams = await roh("/workspace/api/bereich/anschlag", TREFF, { cookie: gast.keks },
|
||||
{ art: Object.keys(BEREICHE.anschlag.arten)[0], titel: "GAST-AM-ANSCHLAG-0001",
|
||||
text: "Ansage", datum: jetzt.slice(0, 10) });
|
||||
ok(brettDesTeams.code === 403, `aufs Anschlagbrett schreibt nur das Team (${brettDesTeams.code})`);
|
||||
|
||||
const beiDogi = await brett("treff", HAUS, kDogi);
|
||||
ok(beiDogi.titel.includes("GAST-SCHREIBT-8899"), "DogFather sieht den Beitrag");
|
||||
const beiModi = await brett("treff", CREW, kModi);
|
||||
ok(beiModi.titel.includes("GAST-SCHREIBT-8899"), "der Modi auch");
|
||||
const beiCrea = await brett("treff", HAUS, kCrea);
|
||||
ok(!beiCrea.titel.includes("GAST-SCHREIBT-8899"), "die Creatorin nicht");
|
||||
}
|
||||
{
|
||||
/* UND NICHT IN EIN BRETT DES HAUSES. */
|
||||
const a = await roh("/workspace/api/bereich/ideen", TREFF, { cookie: gast.keks },
|
||||
{ art: "content", titel: "GAST-IM-HAUS-0000", text: "", datum: jetzt.slice(0, 10) });
|
||||
ok(a.code !== 200 && a.code !== 201, `aber nicht in ein Brett des Hauses (${a.code})`);
|
||||
}
|
||||
|
||||
melde("");
|
||||
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||||
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest, kein Befund */ }
|
||||
process.exit(fehler === 0 ? 0 : 1);
|
||||
@@ -67,12 +67,26 @@
|
||||
* Rolle nur in einer der beiden Listen steht. */
|
||||
export const ALLE_ROLLEN = [
|
||||
"spicy", "admin", "manager", "scout", "creator", "hand", "modi",
|
||||
/* DIE COMMUNITY (11.09.2026). Zuschauer mit einem Zugang, die den
|
||||
Treff sehen -- und sonst nichts. Sie steht hier NICHT in `ALLE`
|
||||
(siehe unten): Wer neu dazukommt, bekommt genau die zwei Seiten,
|
||||
die weiter unten ausdruecklich seinen Namen tragen. */
|
||||
"gast",
|
||||
];
|
||||
|
||||
/* Kurzform fuer die Tafel unten. `ALLE` heisst hier ausdruecklich
|
||||
"alle Rollen, die es GIBT" -- nicht "alle, die es geben wird".
|
||||
Genau darin liegt der Unterschied zum alten `null`. */
|
||||
const ALLE = ALLE_ROLLEN;
|
||||
/* ACHTUNG: `ALLE` ist NICHT ALLE_ROLLEN.
|
||||
|
||||
Es ist die Abkuerzung fuer "alle Rollen, die diese Seiten am
|
||||
11.09.2026 schon hatten" -- die sieben des Hauses. Die Community
|
||||
gehoert ausdruecklich nicht dazu; sie bekommt ihre Seiten einzeln.
|
||||
|
||||
Wuerde hier ALLE_ROLLEN stehen, haette jede kuenftige Rolle wieder
|
||||
automatisch neun Seiten geerbt -- genau der Fehler, gegen den diese
|
||||
Datei gebaut ist, nur mit einem anderen Namen. */
|
||||
const ALLE = ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"];
|
||||
|
||||
/* Schutz der angemeldeten Seiten. Serverseitig, nicht nur im Browser --
|
||||
sonst könnte man die Seite einfach direkt aufrufen. */
|
||||
@@ -98,7 +112,9 @@ export const SEITEN = {
|
||||
zwei Aenderungen in einer -- und man wuesste hinterher nicht,
|
||||
welche der beiden etwas kaputtgemacht hat. Wer hier enger
|
||||
stellen will, macht das als eigenen Schritt. */
|
||||
"/workspace/start.html": [...ALLE],
|
||||
/* Die Community sieht hier ihre sieben Kacheln und sonst nichts --
|
||||
welche das sind, entscheidet bereicheFuer() in workspace.js. */
|
||||
"/workspace/start.html": [...ALLE, "gast"],
|
||||
"/workspace/uebersicht.html": [...ALLE],
|
||||
/* AUSDRUECKLICH OHNE 'modi' (10.09.2026). Vorher stand hier `null`
|
||||
("jede angemeldete Rolle") -- und mit der neuen Rolle hiess das
|
||||
@@ -216,7 +232,11 @@ export const SEITEN = {
|
||||
WELCHE Bereiche ein Modi oeffnen darf, entscheidet die Liste
|
||||
seiner Kacheln (MODI_BEREICHE_ERLAUBT) -- sonst kaeme er ueber die
|
||||
Adresszeile auch in die Agentur-Ablage. */
|
||||
"/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"],
|
||||
/* Die sieben Bretter des Treffs laufen ueber dieselbe Seite wie alle
|
||||
anderen Bereiche. WELCHE ein Gast oeffnen darf, entscheidet die
|
||||
Bereichsliste in workspace-bereiche.js -- diese Zeile oeffnet nur
|
||||
die Tuer. Eine Seite ist ein Weg, keine Schranke. */
|
||||
"/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator", "hand", "modi", "gast"],
|
||||
"/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"],
|
||||
"/workspace/scouting.html": ["spicy", "admin", "manager", "scout"],
|
||||
/* DIE ARBEITSLAGE DES TEAMS (09.09.2026).
|
||||
@@ -251,6 +271,26 @@ export const SEITEN = {
|
||||
"Kann im Alltag vertretend Entscheidungen treffen." Genau das
|
||||
passiert auf dieser Seite. */
|
||||
"/workspace/teamlage.html": ["admin", "hand"],
|
||||
|
||||
/* DIE REGELN DES TREFFS (11.09.2026). Fuer die Community und fuer
|
||||
die, die sie durchsetzen — ein Modi, der die Regeln nicht sehen
|
||||
kann, moderiert nach Gefuehl.
|
||||
|
||||
AUSDRUECKLICH NICHT fuer Manager, Scouts, Creator und Spicy: Sie
|
||||
haben mit dem Treff nichts zu tun, und die Seite nennt Fristen,
|
||||
Stufen und die Bedingungen des Ausschlusses. */
|
||||
"/workspace/treff-regeln.html": ["gast", "modi", "hand", "admin"],
|
||||
|
||||
/* MELDUNGEN & MASSNAHMEN. Nur wer moderiert -- ausdruecklich OHNE
|
||||
"gast": Dort stehen die Namen derer, die gemeldet haben. Eine
|
||||
Meldung, die der Gemeldete lesen kann, ist keine Meldung mehr,
|
||||
sondern eine Einladung zur Vergeltung. */
|
||||
"/workspace/treff-moderation.html": ["modi", "hand", "admin"],
|
||||
|
||||
/* WER SIEHT WAS. Nur DogFather — die Seite ist eine vollstaendige
|
||||
Karte dieses Hauses: jede Seite, jede Rolle. Wer sie hat, weiss,
|
||||
wo er es versuchen muesste. */
|
||||
"/workspace/rechte.html": ["admin"],
|
||||
};
|
||||
|
||||
/* Die Zugangswaende. Sie haben keine Rolle, weil sich dort noch
|
||||
@@ -260,6 +300,9 @@ 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",
|
||||
];
|
||||
|
||||
/* =====================================================================
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/* =====================================================================
|
||||
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/");
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/* =====================================================================
|
||||
Die Tabellen des Treffs und die eine Frage, die überall gestellt wird
|
||||
(11.09.2026)
|
||||
=====================================================================
|
||||
|
||||
DIESE DATEI IMPORTIERT NICHTS, und das ist ihr ganzer Zweck.
|
||||
|
||||
Zwei Module brauchen dasselbe: workspace.js muss beim Anmelden und
|
||||
bei jeder Sitzung wissen, ob jemand gesperrt ist -- und
|
||||
workspace-treff.js muss es auch. Würde workspace.js dafür aus
|
||||
workspace-treff.js importieren, entstünde ein Kreis (treff importiert
|
||||
db, protokolliere und die Bretter aus workspace). ES-Module kommen
|
||||
damit zurecht, solange nur Funktionen über den Kreis laufen; sobald
|
||||
jemand eine Konstante beim Laden benutzt, bekommt er `undefined` --
|
||||
ohne Fehlermeldung, an einer Stelle, die mit dem Kreis nichts zu tun
|
||||
hat. Ein Blatt ohne Importe kann diesen Fehler nicht haben.
|
||||
|
||||
Die Tabellen werden deshalb auch dort angelegt, wo alle anderen
|
||||
angelegt werden: im Bauplan in workspace.js. Ein verspätetes
|
||||
"CREATE TABLE IF NOT EXISTS" beim ersten Zugriff hätte bedeutet, dass
|
||||
die Sperrfrage beim allerersten Aufruf gegen eine Tabelle läuft, die
|
||||
es noch nicht gibt -- und eine Sperre, die bei einer Störung "darf"
|
||||
sagt, ist genau die, die im Ernstfall aufgeht.
|
||||
===================================================================== */
|
||||
|
||||
/** Legt die Tabellen des Treffs an. Wird aus dem Bauplan gerufen. */
|
||||
export function treffTabellen(d) {
|
||||
d.exec(`
|
||||
/* „Will ich auch" — EINE Stimme je Person und Eintrag.
|
||||
|
||||
Der zusammengesetzte Primärschlüssel ist die ganze Regel: Ein
|
||||
zweiter Klick kann gar nicht als zweite Stimme ankommen, auch
|
||||
nicht bei zwei gleichzeitigen Anfragen. Eine Zählspalte am
|
||||
Eintrag hätte dieselbe Zahl gehalten und dabei jede Stimme
|
||||
doppelt genommen, sobald jemand zweimal tippt. */
|
||||
CREATE TABLE IF NOT EXISTS treff_stimmen (
|
||||
eintrag_id INTEGER NOT NULL REFERENCES eintraege(id) ON DELETE CASCADE,
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
zeitpunkt TEXT NOT NULL,
|
||||
PRIMARY KEY (eintrag_id, person_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_treff_stimmen_person ON treff_stimmen (person_id);
|
||||
|
||||
/* Angeheftet am Anschlagbrett. Höchstens drei — die Zahl steht im
|
||||
Code (ANSCHLAG_MAX), nicht hier: Eine CHECK-Regel kann nicht über
|
||||
Zeilen hinweg zählen, und eine, die es könnte, müsste man beim
|
||||
Ändern der Zahl umbauen (siehe checkListeErweitern). */
|
||||
CREATE TABLE IF NOT EXISTS treff_angeheftet (
|
||||
eintrag_id INTEGER PRIMARY KEY REFERENCES eintraege(id) ON DELETE CASCADE,
|
||||
seit TEXT NOT NULL,
|
||||
von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
/* Freigaben. Ein Eintrag auf einem Freigabe-Brett ist für die
|
||||
Community erst da, wenn hier eine Zeile steht.
|
||||
|
||||
ABWESENHEIT IST DER RUHEZUSTAND, und das ist der Kern: Eine
|
||||
Spalte "freigegeben INTEGER DEFAULT 0" hätte dasselbe getan --
|
||||
bis jemand einen Eintrag auf einem anderen Weg anlegt und die
|
||||
Vorgabe vergisst. Eine fehlende Zeile kann man nicht vergessen. */
|
||||
CREATE TABLE IF NOT EXISTS treff_freigaben (
|
||||
eintrag_id INTEGER PRIMARY KEY REFERENCES eintraege(id) ON DELETE CASCADE,
|
||||
zeitpunkt TEXT NOT NULL,
|
||||
von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
/* Meldungen (DSA Art. 16). Der Grund ist NOT NULL -- eine Meldung
|
||||
ohne Grund ist keine Meldung, sondern ein Klick. */
|
||||
CREATE TABLE IF NOT EXISTS treff_meldungen (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
eintrag_id INTEGER NOT NULL REFERENCES eintraege(id) ON DELETE CASCADE,
|
||||
melder_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
grund TEXT NOT NULL,
|
||||
zeitpunkt TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'offen'
|
||||
CHECK (status IN ('offen','erledigt','abgelehnt')),
|
||||
entscheid TEXT,
|
||||
entschieden_von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
entschieden_am TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_treff_meldungen_status
|
||||
ON treff_meldungen (status, zeitpunkt);
|
||||
|
||||
/* Was entfernt wurde, und warum (DSA Art. 17).
|
||||
|
||||
DIESE TABELLE HÄNGT ABSICHTLICH NICHT AN eintraege. Der Eintrag
|
||||
ist weg; die Begründung muss bleiben. Ein Fremdschlüssel mit
|
||||
ON DELETE CASCADE hätte genau das Gegenteil bewirkt -- die
|
||||
Begründung wäre im selben Moment verschwunden wie das, was sie
|
||||
begründet. Deshalb stehen Titel und Verfasser hier als Text bzw.
|
||||
als lose Nummer noch einmal. */
|
||||
CREATE TABLE IF NOT EXISTS treff_entfernt (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
eintrag_id INTEGER NOT NULL,
|
||||
bereich TEXT NOT NULL,
|
||||
titel TEXT NOT NULL,
|
||||
autor_id INTEGER,
|
||||
autor_name TEXT,
|
||||
grund TEXT NOT NULL,
|
||||
von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
zeitpunkt TEXT NOT NULL
|
||||
);
|
||||
|
||||
/* Maßnahmen: Hinweis, Pause, Ausschluss.
|
||||
|
||||
"bis" = NULL heißt DAUERHAFT. Wer das darf, steht in
|
||||
darfMassnahme() -- und es ist bewusst nicht dieselbe Menge wie
|
||||
für die befristete Fassung (Filipe, 11.09.2026: "Modis dürfen
|
||||
befristet ausschließen, dauerhaft nur ihr"). */
|
||||
CREATE TABLE IF NOT EXISTS treff_massnahmen (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
|
||||
art TEXT NOT NULL CHECK (art IN ('hinweis','pause','ausschluss')),
|
||||
grund TEXT NOT NULL,
|
||||
bis TEXT,
|
||||
von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
|
||||
zeitpunkt TEXT NOT NULL,
|
||||
aufgehoben_am TEXT,
|
||||
aufgehoben_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_treff_massnahmen_person
|
||||
ON treff_massnahmen (person_id, art);
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die wirksame Maßnahme gegen diese Person, oder null.
|
||||
*
|
||||
* NIMMT DIE DATENBANK ALS ARGUMENT, statt sie sich zu holen: So kann
|
||||
* sie aus workspace.js gerufen werden, ohne dass diese Datei etwas von
|
||||
* workspace.js wissen muss. Und in einer Prüfung kann man ihr eine
|
||||
* Kopie geben.
|
||||
*
|
||||
* DREI AUSGÄNGE, NICHT ZWEI: `null` heißt "nichts liegt vor". Kann die
|
||||
* Frage nicht beantwortet werden (Tabelle fehlt, Datenbank weg), wirft
|
||||
* die Funktion -- sie gibt NICHT `null` zurück.
|
||||
*/
|
||||
export function treffSperreLesen(d, personId) {
|
||||
const zeilen = d.prepare(`
|
||||
SELECT id, art, grund, bis, zeitpunkt FROM treff_massnahmen
|
||||
WHERE person_id = ? AND art IN ('pause','ausschluss')
|
||||
AND aufgehoben_am IS NULL
|
||||
ORDER BY CASE art WHEN 'ausschluss' THEN 0 ELSE 1 END, id DESC`).all(personId);
|
||||
const nun = Date.now();
|
||||
for (const z of zeilen) {
|
||||
/* bis = NULL heißt dauerhaft. Eine abgelaufene Frist läuft ab,
|
||||
ohne dass jemand etwas tun muss -- eine Sperre, die man von Hand
|
||||
aufheben muss, bleibt sonst Jahre stehen. */
|
||||
if (!z.bis) return z;
|
||||
const ende = Date.parse(z.bis);
|
||||
if (Number.isFinite(ende) && ende > nun) return z;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -19,14 +19,178 @@ import {
|
||||
db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather,
|
||||
externPruefen, externSql,
|
||||
ohneTeamDogi, ohneAgentur, siehtModis, fuehrtTeamDogi, MODI_BEREICHE_ERLAUBT, TEAM_DOGI_ROLLEN,
|
||||
TREFF_BRETTER, TREFF_ROLLEN, AUSSEN_ROLLEN,
|
||||
} from "./workspace.js";
|
||||
import { nachrichtSchicken } from "./workspace-chat.js";
|
||||
import {
|
||||
darfSchreiben, freigabeBedingung, entfernenVorbereiten, TREFF_TEAM_ROLLEN,
|
||||
mitRollenname,
|
||||
} from "./workspace-treff.js";
|
||||
|
||||
export const bereicheRouter = express.Router();
|
||||
|
||||
/* Die Bereiche und ihre Arten. Bewusst hier und nicht in der Datenbank:
|
||||
Es sind Festlegungen aus dem Konzept, keine Nutzdaten. */
|
||||
export const BEREICHE = {
|
||||
/* =====================================================================
|
||||
DIE SIEBEN BRETTER DES TREFFS (11.09.2026)
|
||||
|
||||
Der Raum fuer die Zuschauer. Sie laufen ueber dieselbe Maschinerie
|
||||
wie Ideen-Board und Angebote -- ein Bereich, seine Arten, dieselbe
|
||||
Seite. Eine eigene Bauart daneben waere eine zweite
|
||||
Sichtbarkeitsregel, und genau deren Vervielfaeltigung hat am
|
||||
10.09. ein Leck verursacht.
|
||||
|
||||
ALLE SIEBEN HABEN `fuerAlle`: Im Treff sieht jeder jeden Beitrag.
|
||||
Ein Raum, in dem man nur sieht, was man selbst geschrieben hat,
|
||||
waere kein Raum. Die Grenze nach aussen zieht nicht `fuerAlle`,
|
||||
sondern der Riegel in sichtbarEintrag() -- die Agentur sieht kein
|
||||
einziges dieser Bretter.
|
||||
|
||||
ALLE SIEBEN HABEN `ohneCreatorBezug`: Hier geht es um niemanden aus
|
||||
der Agentur. Das Feld waere nicht nur ueberfluessig, es waere die
|
||||
Einladung, versehentlich einen Creator mit einem Zuschauer zu
|
||||
verknuepfen.
|
||||
===================================================================== */
|
||||
|
||||
anschlag: {
|
||||
name: "Anschlagbrett",
|
||||
ober: "Vom Team",
|
||||
arten: {
|
||||
ansage: "Ansage",
|
||||
regel: "Neue Regel",
|
||||
hinweis: "Hinweis",
|
||||
danke: "Danke an euch",
|
||||
},
|
||||
bewertung: false,
|
||||
dringlichkeit: false,
|
||||
ohneCreatorBezug: true,
|
||||
fuerAlle: true,
|
||||
},
|
||||
|
||||
ansteht: {
|
||||
name: "Was ansteht",
|
||||
ober: "Termine",
|
||||
/* AUSDRUECKLICH NICHT AN DEN ECHTEN KALENDER GEKOPPELT.
|
||||
|
||||
Ein Termin traegt Titel, Ort und Gegenueber -- und die Regel im
|
||||
Haus lautet seit dem 07.09., dass ein Kalender als Ganzes privat
|
||||
ist. Eine Filterung waere eine Einstellung, die man falsch setzen
|
||||
kann; ein Eintrag hier ist eine Entscheidung, die jemand trifft.
|
||||
Der Preis ist, dass das Team es zweimal schreibt. Das ist der
|
||||
guenstigere von beiden. */
|
||||
arten: {
|
||||
stream: "Stream",
|
||||
event: "Event",
|
||||
turnier: "Turnier",
|
||||
pause: "Keine Sendung",
|
||||
},
|
||||
bewertung: false,
|
||||
dringlichkeit: false,
|
||||
ohneCreatorBezug: true,
|
||||
fuerAlle: true,
|
||||
},
|
||||
|
||||
treff: {
|
||||
name: "Der Treff",
|
||||
ober: "Euer Raum",
|
||||
/* DIE REIHENFOLGE IST ABSICHT. Das erste Feld bestimmt, was
|
||||
geschrieben wird -- wer ein Formular oeffnet, dessen erste
|
||||
Auswahl "Problem" heisst, schreibt Probleme auf.
|
||||
|
||||
"Hallo, ich bin neu" steht vorn, weil die ersten sieben Tage
|
||||
darueber entscheiden, ob jemand bleibt: Wer in dieser Zeit
|
||||
erlebt, dass er hier einen Platz hat, kommt deutlich haeufiger
|
||||
wieder. Jede Vorstellung bekommt eine Antwort -- das ist der
|
||||
ganze Trick, und er steht in keinem Code. */
|
||||
arten: {
|
||||
hallo: "Hallo, ich bin neu",
|
||||
frage: "Frage",
|
||||
lob: "Das war gut",
|
||||
problem: "Etwas stimmt nicht",
|
||||
},
|
||||
bewertung: false,
|
||||
dringlichkeit: false,
|
||||
ohneCreatorBezug: true,
|
||||
fuerAlle: true,
|
||||
},
|
||||
|
||||
wunsch: {
|
||||
name: "Wunschliste",
|
||||
ober: "Was ihr euch wuenscht",
|
||||
arten: {
|
||||
stream: "Fuer den Stream",
|
||||
technik: "Technik",
|
||||
gemeinsam: "Fuer die Community",
|
||||
sonstiges: "Sonstiges",
|
||||
},
|
||||
bewertung: false,
|
||||
/* KEINE DRINGLICHKEIT VON EINEM EINZELNEN. Was wichtig ist,
|
||||
entscheidet hier nicht der Schreiber, sondern wie viele
|
||||
mitwollen -- "Will ich auch" kommt als naechster Schritt. Eine
|
||||
Dringlichkeit daneben waere eine zweite Rangordnung, und die
|
||||
beiden wuerden einander widersprechen. */
|
||||
dringlichkeit: false,
|
||||
ohneCreatorBezug: true,
|
||||
fuerAlle: true,
|
||||
},
|
||||
|
||||
highlight: {
|
||||
name: "Highlights",
|
||||
ober: "Von euch",
|
||||
arten: {
|
||||
clip: "Clip",
|
||||
bild: "Bild",
|
||||
fanart: "Fanart",
|
||||
moment: "Moment",
|
||||
},
|
||||
bewertung: false,
|
||||
dringlichkeit: false,
|
||||
ohneCreatorBezug: true,
|
||||
fuerAlle: true,
|
||||
},
|
||||
|
||||
regeln: {
|
||||
name: "Regeln & Hilfe",
|
||||
ober: "Wie es hier laeuft",
|
||||
/* REGELN IN EINFACHER SPRACHE UND AN ECHTEN FAELLEN. Vage Regeln
|
||||
erzeugen Grenzfaelle, und die muss dann jeder Modi einzeln
|
||||
entscheiden -- das ist der Weg, auf dem ein Team uneinheitlich
|
||||
wird und die Community es merkt. */
|
||||
arten: {
|
||||
regel: "Regel",
|
||||
frage: "Haeufige Frage",
|
||||
hilfe: "Hilfe",
|
||||
folge: "Was passiert, wenn",
|
||||
},
|
||||
bewertung: false,
|
||||
dringlichkeit: false,
|
||||
ohneCreatorBezug: true,
|
||||
fuerAlle: true,
|
||||
},
|
||||
|
||||
mitmachen: {
|
||||
name: "Mitmachen",
|
||||
ober: "Nachwuchs",
|
||||
/* DIE BRUECKE ZU DEN TALENTEN. Wer sich hier meldet, ist ein
|
||||
Kandidat -- und Kandidaten gehoeren in die Kategorie, die es
|
||||
dafuer schon gibt, nicht in eine zweite Liste daneben.
|
||||
|
||||
Daneben gehoert der ehrliche Satz, dass eine Bewerbung nicht der
|
||||
Hauptweg ist: Der treffsicherste Weg ist, dass jemandem aus dem
|
||||
Team auffaellt, wie du dich verhaeltst. Das steht so im Brett,
|
||||
weil es stimmt -- und weil es die richtige Erwartung setzt. */
|
||||
arten: {
|
||||
gesucht: "Wir suchen",
|
||||
lust: "Ich haette Lust",
|
||||
frage: "Frage dazu",
|
||||
},
|
||||
bewertung: false,
|
||||
dringlichkeit: false,
|
||||
ohneCreatorBezug: true,
|
||||
fuerAlle: true,
|
||||
},
|
||||
|
||||
live: {
|
||||
name: "LIVE-Analyse",
|
||||
arten: { vorbereitung: "Vorbereitung", mitschrift: "Während LIVE", auswertung: "Auswertung" },
|
||||
@@ -483,9 +647,30 @@ bereicheRouter.use("/workspace/api/bereich/:bereich", (req, res, next) => {
|
||||
sie ueber die Adresszeile in die Agentur-Ablage, die sie nichts
|
||||
angeht. Genau die Luecke, gegen die diese Schranke gebaut wurde,
|
||||
nur eine Rolle spaeter. */
|
||||
const b = String(req.params.bereich);
|
||||
const nein = () => res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
/* EINE AUSSENROLLE KOMMT NUR IN DEN TREFF (11.09.2026).
|
||||
|
||||
Und zwar als ERSTER Zweig, nicht als letzter. Die Zeile darunter
|
||||
hiess bis heute "wer nicht zum Team gehoert, darf weiter" -- fuer
|
||||
einen Gast waere das ein Freifahrtschein durch alle achtzehn
|
||||
Bereiche gewesen, obwohl die Kacheln ihm nur sieben anbieten. Eine
|
||||
Regel, die nur im Formular gilt, ist keine Regel. */
|
||||
if (AUSSEN_ROLLEN.has(req.person?.rolle)) {
|
||||
return TREFF_BRETTER.includes(b) ? next() : nein();
|
||||
}
|
||||
|
||||
/* UND UMGEKEHRT: Wer nicht zum Treff gehoert, kommt nicht hinein.
|
||||
Das ist derselbe Riegel wie in sichtbarEintrag(), nur eine Ebene
|
||||
frueher -- dort entscheidet er, welche ZEILEN jemand sieht, hier,
|
||||
ob die Seite ueberhaupt antwortet. Zwei Schloesser, absichtlich:
|
||||
Faellt eines weg, haelt das andere. */
|
||||
if (TREFF_BRETTER.includes(b) && !TREFF_ROLLEN.has(req.person?.rolle)) return nein();
|
||||
|
||||
if (!TEAM_DOGI_ROLLEN.has(req.person?.rolle)) return next();
|
||||
if (MODI_BEREICHE_ERLAUBT.has(String(req.params.bereich))) return next();
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
if (MODI_BEREICHE_ERLAUBT.has(b)) return next();
|
||||
return nein();
|
||||
});
|
||||
|
||||
/* ---------- Die Bereiche sind fuer Creator zum LESEN da ----------------
|
||||
@@ -713,7 +898,65 @@ function ohneVertrauliche(regel, person, praefix) {
|
||||
};
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DER TREFF HAT ZWEI RIEGEL, UND SIE ZEIGEN IN ENTGEGENGESETZTE
|
||||
RICHTUNGEN (11.09.2026)
|
||||
|
||||
Filipe: "wo die community auch dan nur die sieht und die modis
|
||||
rechte hand und ich also dogfather sehen die aber auch. die
|
||||
community aber nur die dann."
|
||||
|
||||
Daraus folgen zwei Saetze, und beide muessen gelten:
|
||||
|
||||
1. EINE AUSSENROLLE SIEHT NUR DEN TREFF. Nicht "den Treff und was
|
||||
sonst noch durchfaellt" -- nur ihn. Deshalb steht dieser Zweig
|
||||
GANZ VORN und baut seine Regel selbst, statt auf der
|
||||
allgemeinen aufzusetzen. Wer auf einer bestehenden Regel
|
||||
aufsetzt, erbt jeden Zweig, den sie hat, auch die, die es
|
||||
morgen erst gibt.
|
||||
|
||||
2. DIE AGENTUR SIEHT DEN TREFF NICHT. Creator, Scouts, Manager und
|
||||
Spicy Media haben mit den Zuschauern dieses Streams nichts zu
|
||||
tun. Ohne diesen zweiten Riegel haette `fuerAlle` sie
|
||||
hereingelassen -- genau das Loch, das am 10.09. schon einmal
|
||||
entstanden ist, nur andersherum.
|
||||
|
||||
BEIDE STEHEN HIER UND NICHT IN sichtbar(). Diese Funktion beantwortet
|
||||
auch Fragen zu Aufgaben, Dateien und Terminen; ein Bereichsname hat
|
||||
dort nichts zu suchen. Der Kommentar eine Bildschirmseite weiter oben
|
||||
warnt woertlich davor. */
|
||||
export function sichtbarEintrag(person, praefix = "e") {
|
||||
/* RIEGEL 1 -- die Aussenrolle. Eine eigene, vollstaendige Regel:
|
||||
genau die Bretter des Treffs, sonst nichts. */
|
||||
if (AUSSEN_ROLLEN.has(person?.rolle)) {
|
||||
const platz = TREFF_BRETTER.map(() => "?").join(", ");
|
||||
const grund = { wo: `${praefix}.bereich IN (${platz})`, werte: [...TREFF_BRETTER] };
|
||||
/* RIEGEL 1b -- WAS NOCH NICHT FREIGEGEBEN IST, GIBT ES NICHT
|
||||
(11.09.2026). Termine und Highlights erscheinen der Community
|
||||
erst, wenn jemand aus dem Team sie freigegeben hat.
|
||||
|
||||
DIE BEDINGUNG HAENGT HIER UND NICHT AM LESE-WEG, weil sie sonst
|
||||
nur dort gaelte: Suche, Ausfuhr und jeder kuenftige Weg, der
|
||||
sichtbarEintrag benutzt, haetten die unfreigegebenen Zeilen
|
||||
weiterhin geliefert. Genau so ist am 10.09. ein Modi-Eintrag
|
||||
durch die Suche gekommen, obwohl das Brett zu war. */
|
||||
const frei = freigabeBedingung(person, praefix);
|
||||
if (!frei) return grund;
|
||||
return { wo: `(${grund.wo}) AND ${frei.wo}`, werte: [...grund.werte, ...frei.werte] };
|
||||
}
|
||||
const regel = sichtbarRoh2(person, praefix);
|
||||
/* RIEGEL 2 -- wer nicht zum Treff gehoert, sieht seine Bretter nicht.
|
||||
Auch DogFather geht hier durch; er steht in TREFF_ROLLEN und faellt
|
||||
deshalb nicht heraus. */
|
||||
if (!regel || TREFF_ROLLEN.has(person?.rolle)) return regel;
|
||||
const platz = TREFF_BRETTER.map(() => "?").join(", ");
|
||||
return {
|
||||
wo: `(${regel.wo}) AND ${praefix}.bereich NOT IN (${platz})`,
|
||||
werte: [...regel.werte, ...TREFF_BRETTER],
|
||||
};
|
||||
}
|
||||
|
||||
function sichtbarRoh2(person, praefix = "e") {
|
||||
const regel = sichtbar(person);
|
||||
if (!regel) return regel;
|
||||
if (regel.wo === "1=1") return regel; // DogFather sieht ohnehin alles
|
||||
@@ -773,6 +1016,34 @@ function saeulePasst(saeuleId, creatorId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
WAS EIN TREFF-BRETT ZUSAETZLICH MITBRINGT (11.09.2026)
|
||||
|
||||
Drei Angaben, die es nur dort gibt: wie viele "Will ich auch" ein
|
||||
Wunsch hat, ob meine Stimme dabei ist, ob die Ansage angeheftet und
|
||||
ob der Eintrag freigegeben ist.
|
||||
|
||||
SIE HAENGEN NICHT AN SPALTEN, sondern werden nur fuer die sieben
|
||||
Bretter angehaengt. Drei Unterabfragen je Zeile sind hier
|
||||
bedeutungslos (die Tabellen haben eine Handvoll Zeilen), auf den
|
||||
Betreuungsbrettern waeren sie es nicht -- und vor allem waeren sie
|
||||
dort eine Auskunft ueber etwas, das es in diesem Bereich gar nicht
|
||||
gibt.
|
||||
|
||||
DER ROLLENNAME KOMMT MIT (Entscheidung Filipe, 11.09.2026: "voller
|
||||
Rollenname zeigen"). Er kommt als ROLLE, der Klartext wird erst in
|
||||
der Antwort gebildet -- eine Zuordnung Rolle -> Name im
|
||||
ausgelieferten JavaScript waere eine Liste aller Rollen dieses
|
||||
Hauses auf jedem Rechner der Community.
|
||||
===================================================================== */
|
||||
const TREFF_SPALTEN = `,
|
||||
(SELECT COUNT(*) FROM treff_stimmen ts WHERE ts.eintrag_id = e.id) AS stimmen,
|
||||
(SELECT COUNT(*) FROM treff_stimmen ts
|
||||
WHERE ts.eintrag_id = e.id AND ts.person_id = ?) AS meine_stimme,
|
||||
(SELECT COUNT(*) FROM treff_angeheftet ta WHERE ta.eintrag_id = e.id) AS angeheftet,
|
||||
(SELECT COUNT(*) FROM treff_freigaben tf WHERE tf.eintrag_id = e.id) AS freigegeben,
|
||||
pe.rolle AS erstellt_rolle`;
|
||||
|
||||
const VERBUND = `
|
||||
FROM eintraege e
|
||||
LEFT JOIN personen pc ON pc.id = e.creator_id
|
||||
@@ -789,15 +1060,43 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
|
||||
const regel = sichtbarEintrag(req.sicht || req.person);
|
||||
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const eintraege = db().prepare(`
|
||||
SELECT ${SPALTEN} ${VERBUND}
|
||||
WHERE ${regel.wo} AND e.bereich = ?
|
||||
ORDER BY
|
||||
CASE e.status WHEN 'offen' THEN 0 ELSE 1 END,
|
||||
CASE e.dringlichkeit WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END,
|
||||
e.datum DESC, e.id DESC`).all(...regel.werte, bereich);
|
||||
const treffBrett = TREFF_BRETTER.includes(bereich);
|
||||
|
||||
res.json({ bereich, einstellung, eintraege });
|
||||
/* DIE REIHENFOLGE GEHOERT ZUM BRETT (11.09.2026).
|
||||
|
||||
Am Anschlagbrett steht oben, was angeheftet ist -- sonst waere
|
||||
das Anheften eine Zierde. Auf der Wunschliste steht oben, was die
|
||||
meisten wollen; genau das ist der Sinn von "Will ich auch", und
|
||||
eine Liste nach Datum haette die Abstimmung unsichtbar gemacht.
|
||||
Ueberall sonst bleibt es, wie es war. */
|
||||
const reihenfolge = bereich === "anschlag"
|
||||
? "angeheftet DESC, e.datum DESC, e.id DESC"
|
||||
: bereich === "wunsch"
|
||||
? "stimmen DESC, e.datum DESC, e.id DESC"
|
||||
: `CASE e.status WHEN 'offen' THEN 0 ELSE 1 END,
|
||||
CASE e.dringlichkeit WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END,
|
||||
e.datum DESC, e.id DESC`;
|
||||
|
||||
const wer = (req.sicht || req.person)?.id ?? 0;
|
||||
const eintraege = db().prepare(`
|
||||
SELECT ${SPALTEN}${treffBrett ? TREFF_SPALTEN : ""} ${VERBUND}
|
||||
WHERE ${regel.wo} AND e.bereich = ?
|
||||
ORDER BY ${reihenfolge}`)
|
||||
.all(...(treffBrett ? [wer] : []), ...regel.werte, bereich);
|
||||
|
||||
/* DASS DIES EIN BRETT DES TREFFS IST, SAGT DER SERVER (11.09.2026).
|
||||
|
||||
Die Oberflaeche braucht die Auskunft, um ihre Knoepfe zu bauen
|
||||
("Will ich auch", anheften, freigeben, melden). Sie koennte den
|
||||
Bereichsnamen gegen eine Liste im Browser halten -- dann stuenden
|
||||
die Namen aller sieben Bretter in einer ausgelieferten Datei, und
|
||||
es gaebe eine zweite Wahrheit darueber, welche es sind. So gibt
|
||||
es weiterhin genau eine (TREFF_BRETTER), und der Browser fragt
|
||||
sie, statt sie zu kennen. */
|
||||
res.json({
|
||||
bereich, einstellung, treff: treffBrett,
|
||||
eintraege: treffBrett ? eintraege.map(mitRollenname) : eintraege,
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Bereich lesen:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
@@ -1062,6 +1361,20 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
|
||||
const regel = sichtbar(req.person);
|
||||
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
/* DIE SCHREIBTAFEL DES TREFFS (11.09.2026).
|
||||
|
||||
Auf welches der sieben Bretter jemand von aussen schreiben darf,
|
||||
steht in TREFF_SCHREIBEN -- an einer Stelle, aus der auch die
|
||||
Oberflaeche ihre Knoepfe holt (/api/treff/lage). Waeren es zwei
|
||||
Stellen, koennte ein Knopf erscheinen, den der Server ablehnt --
|
||||
oder, schlimmer, umgekehrt.
|
||||
|
||||
DER TEXT WIRD DURCHGEREICHT, nicht durch "nicht_erlaubt" ersetzt:
|
||||
"Ab Dabei kannst du hier schreiben" beantwortet die Frage, die
|
||||
der Mensch davor gerade hat. */
|
||||
const warum = darfSchreiben(req.person, bereich);
|
||||
if (warum) return res.status(403).json({ fehler: warum });
|
||||
|
||||
const { aus, fehler } = pruefe(bereich, req.body || {}, { neu: true });
|
||||
if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") });
|
||||
|
||||
@@ -1226,9 +1539,36 @@ bereicheRouter.delete("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (r
|
||||
/* Löschen darf das Management und wer den Eintrag selbst geschrieben
|
||||
hat -- sonst könnte ein Creator eine Notiz des Managements über
|
||||
seinen eigenen Bereich verschwinden lassen. */
|
||||
if (!istLeitung(req.person) && eintrag.erstellt_von !== req.person.id) {
|
||||
return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||||
}
|
||||
/* IM TREFF RAEUMT AUCH EIN MODI AUF (11.09.2026). Er ist dort die
|
||||
Moderation; waere Aufraeumen der Leitung vorbehalten, bliebe ein
|
||||
Beitrag stehen, bis jemand Zeit hat -- und genau in dieser Zeit
|
||||
liest ihn die Community. */
|
||||
const treffBrett = TREFF_BRETTER.includes(bereich);
|
||||
const darfWeg = treffBrett
|
||||
? TREFF_TEAM_ROLLEN.has(req.person.rolle) || eintrag.erstellt_von === req.person.id
|
||||
: istLeitung(req.person) || eintrag.erstellt_von === req.person.id;
|
||||
if (!darfWeg) return res.status(403).json({ fehler: "nicht_erlaubt" });
|
||||
|
||||
/* DER GRUND WIRD FESTGEHALTEN, BEVOR DER BEITRAG VERSCHWINDET
|
||||
(DSA Art. 17). Danach waere es zu spaet: Titel und Verfasser
|
||||
stuenden nirgends mehr, und eine Begruendung ohne den Beitrag,
|
||||
den sie begruendet, ist keine.
|
||||
|
||||
DER GRUND KOMMT AUS DER ADRESSE, und das ist keine Vorliebe,
|
||||
sondern gemessen (11.09.2026): Nodes HTTP-Parser weist ein
|
||||
DELETE MIT KOERPER mit einem leeren 400 ab, bevor Express die
|
||||
Anfrage ueberhaupt sieht -- eine Antwort mit genau einem
|
||||
Kopffeld ("connection: close") und ohne Text. Wer den Fehler
|
||||
sucht, sucht ihn in dieser Route, und hier ist keiner.
|
||||
|
||||
Der Koerper bleibt als zweite Moeglichkeit stehen: Er schadet
|
||||
nicht, und wenn ein anderer Weg (ein Werkzeug, eine kuenftige
|
||||
Fassung von Node) doch einen mitschickt, wird er gelesen. Zwei
|
||||
Quellen fuer denselben Wert sind hier keine zwei Wahrheiten --
|
||||
es ist derselbe Wert, nur zwei Briefkaesten. */
|
||||
const nein = entfernenVorbereiten(req.person, eintrag,
|
||||
req.body?.grund ?? req.query?.grund);
|
||||
if (nein) return res.status(400).json({ fehler: nein });
|
||||
|
||||
db().prepare("DELETE FROM eintraege WHERE id = ?").run(id);
|
||||
protokolliere("eintrag_geloescht", {
|
||||
|
||||
@@ -265,8 +265,32 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => {
|
||||
steht, wurde bis eben nicht gezeichnet. VanVan verschwand
|
||||
genau in dem Moment aus der Liste, in dem sie die rechte Hand
|
||||
wurde -- ohne Fehler, ohne Luecke, ohne Hinweis. */
|
||||
zusatzrollen: siehtModis(req.person)
|
||||
? [{
|
||||
/* DIE COMMUNITY STEHT NICHT IN DERSELBEN BEDINGUNG (11.09.2026).
|
||||
|
||||
Die beiden Eintraege unten haengen an `siehtModis` -- wer die
|
||||
Modis sieht, darf sie auch anlegen. Fuer die Community gilt das
|
||||
NICHT: Einen Zugang zum Treff vergibt allein DogFather (Plan
|
||||
Teil C, Zeile "Zugang anlegen"). Ein Modi, der einen anlegen
|
||||
koennte, koennte sich selbst Verstaerkung holen.
|
||||
|
||||
GEFRAGT WIRD `darfAnlegen` UND NICHT DIE ROLLE. Dieselbe
|
||||
Auskunft, aus der auch die Route ihre Absage holt -- deshalb
|
||||
kann hier kein Knopf erscheinen, den der Server ablehnt, und
|
||||
keiner fehlen, den er annehmen wuerde. Auf der Team-Adresse
|
||||
faellt er dadurch von selbst weg (darfAnlegen filtert dort auf
|
||||
die Team-Rollen), und genau das ist richtig: Wer dort jemanden
|
||||
anlegt, meint jemanden aus dem Team. */
|
||||
zusatzrollen: [
|
||||
...(darfAnlegen(req.person).includes("gast")
|
||||
? [{
|
||||
wert: "gast", name: "Community", symbol: "leute",
|
||||
text: "Zugang zum Treff. Sieht die sieben Bretter der Community "
|
||||
+ "und sonst nichts \u2013 keine Aufgaben, keine Termine, "
|
||||
+ "keine Namen aus dem Team.",
|
||||
}]
|
||||
: []),
|
||||
...(siehtModis(req.person)
|
||||
? [{
|
||||
wert: "modi", name: "Modi",
|
||||
/* DIE PFOTE (Filipe, 10.09.2026 mit Bildschirmfoto: "bei
|
||||
rechte hand soll der husky sein und bei modis soll die
|
||||
@@ -310,7 +334,8 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => {
|
||||
+ "dieselben Aufgaben, Ideen und Angebote wie du – aber keine "
|
||||
+ "privaten Kalender und keine Einzelchats.",
|
||||
}]
|
||||
: [],
|
||||
: []),
|
||||
],
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Personen lesen:", fehler?.message);
|
||||
|
||||
@@ -0,0 +1,951 @@
|
||||
/* =====================================================================
|
||||
DER TREFF — was die Community darf, und wer darüber wacht (11.09.2026)
|
||||
=====================================================================
|
||||
|
||||
Auftrag (Filipe, wörtlich):
|
||||
*"eine community rolle und community kategorie, wo die community
|
||||
auch dann nur die sieht."*
|
||||
*"wieso nur eine kachel für die community ich will dass die mehrere
|
||||
kacheln haben. mit mehreren optionen."*
|
||||
|
||||
Die sieben Bretter stehen in workspace.js (TREFF_BEREICHE), ihre
|
||||
Sichtbarkeit in workspace-bereiche.js. Hier steht, was darauf
|
||||
PASSIERT: Stimmen, Anheften, Freigeben, Melden, Entfernen, Stufen,
|
||||
Maßnahmen und die Brücke zu den Talenten.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM EIN EIGENES MODUL UND KEIN ANHANG AN workspace-bereiche.js
|
||||
|
||||
Weil die Community die einzige Rolle in diesem Haus ist, deren
|
||||
Mitglieder nichts unterschrieben haben. Für alle anderen gilt: Ein
|
||||
Fehler in der Rechteprüfung trifft jemanden, der ohnehin fast alles
|
||||
sehen darf. Hier trifft er Fremde. Diese Regeln gehören deshalb an
|
||||
eine Stelle, an der man sie am Stück lesen kann — nicht verteilt auf
|
||||
vier Handler eines 1500 Zeilen langen Moduls.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DIE DREI STUFEN WERDEN GERECHNET, NICHT GESPEICHERT
|
||||
|
||||
Neu → ab dem ersten Tag
|
||||
Dabei → ab 7 Tagen
|
||||
Stamm → ab 30 Tagen UND 3 eigenen Beiträgen
|
||||
|
||||
Eine gespeicherte Stufe wäre ein zweiter Ort für eine Tatsache, die
|
||||
ohnehin in den Daten steht — und sie würde altern: Wer am Tag 6
|
||||
eingetragen wird, bliebe „Neu", bis irgendein Vorgang sie nachzieht.
|
||||
Ein Vorgang, den es nicht gibt, ist kein Vorgang.
|
||||
|
||||
WOZU DIE STUFEN ÜBERHAUPT: Die erste Woche ist eine Sandkiste. Wer
|
||||
neu ist, darf lesen und „Will ich auch" klicken — mehr nicht. Das ist
|
||||
kein Misstrauen gegen Menschen, sondern gegen Wegwerf-Zugänge: Ein
|
||||
Zugang, der sieben Tage warten muss, bevor er Schaden anrichten kann,
|
||||
lohnt sich für niemanden, der Schaden anrichten will.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DAS RECHT ENTSCHEIDET HIER MIT, UND ZWAR DIE BAUWEISE
|
||||
|
||||
* DSA Art. 16 — MELDEN MUSS GEHEN, und die Meldung muss so genau
|
||||
sein, dass man ohne Rückfrage entscheiden kann. Deshalb ist der
|
||||
Grund PFLICHT und nicht ein Auswahlfeld mit „Sonstiges".
|
||||
* DSA Art. 17 — WER ETWAS ENTFERNT, BEGRÜNDET ES. Die Begründung
|
||||
überlebt den entfernten Beitrag: Sie steht in treff_entfernt, nicht
|
||||
an der gelöschten Zeile. Genau das ist der Sinn — eine Begründung,
|
||||
die mit dem Beitrag verschwindet, ist keine.
|
||||
* DSA Art. 19 — Kleinstunternehmen sind von den Art. 20–28 befreit.
|
||||
Die Pflichten aus Art. 16/17 gelten trotzdem, und sie sind ohnehin
|
||||
das, was man aus Anstand täte.
|
||||
* DSGVO Art. 8 — in Luxemburg liegt die Altersgrenze für die
|
||||
Einwilligung bei 16. Der Treff setzt trotzdem **18**
|
||||
(Entscheidung Filipe, 11.09.2026): Rund um einen Stream für
|
||||
Erwachsene ist die Frage nicht, was erlaubt ist, sondern wen man
|
||||
hereinlässt.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WAS HIER ABSICHTLICH NICHT STEHT
|
||||
|
||||
KEINE DIREKTNACHRICHTEN zwischen Mitgliedern. Das ist die wichtigste
|
||||
Einzelentscheidung des ganzen Bereichs. Ein öffentlicher Raum ist
|
||||
moderierbar — ein privater ist es nicht. Jede Belästigung, die im
|
||||
Treff passiert, passiert vor Zeugen und kann gemeldet werden. Wäre
|
||||
sie in einem Postfach passiert, wüsste niemand davon, und der Treff
|
||||
wäre der Ort, an dem die Adressen dafür verteilt werden.
|
||||
===================================================================== */
|
||||
|
||||
import express from "express";
|
||||
import {
|
||||
db, sitzungLesen, protokolliere, echteIp,
|
||||
TREFF_BRETTER, TREFF_ROLLEN, ROLLEN_NAME,
|
||||
} from "./workspace.js";
|
||||
/* Die Tabellen und die Sperrfrage stehen in einem Blatt ohne eigene
|
||||
Importe -- workspace.js braucht beides ebenfalls, und ein Importkreis
|
||||
waere die Folge. Begruendung im Kopf von treff-tabellen.js. */
|
||||
import { treffSperreLesen } from "./treff-tabellen.js";
|
||||
|
||||
export const treffRouter = express.Router();
|
||||
|
||||
const jetzt = () => new Date().toISOString();
|
||||
const TAG = 86400000;
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
WER IST TEAM, WER IST AUSSEN
|
||||
|
||||
Beide Mengen stehen hier ausgeschrieben und nicht als „alles außer
|
||||
gast". Dieselbe Überlegung wie in crew-adresse.js: Eine Bedingung
|
||||
durch Ausschluss nimmt jede künftige Rolle automatisch mit auf — und
|
||||
die nächste neue Rolle wäre damit stillschweigend Moderation im
|
||||
Treff. Genau dieser Fehler steckte am 11.09. schon einmal im stillen
|
||||
Zugang.
|
||||
------------------------------------------------------------------- */
|
||||
export const TREFF_TEAM_ROLLEN = new Set(["modi", "hand", "admin"]);
|
||||
const TREFF_TEAM = TREFF_TEAM_ROLLEN;
|
||||
const TREFF_AUSSEN = new Set(["gast"]);
|
||||
|
||||
/** Darf diese Person überhaupt etwas im Treff tun? */
|
||||
const imTreff = (p) => TREFF_ROLLEN.has(p?.rolle);
|
||||
|
||||
/* =====================================================================
|
||||
DIE STUFEN
|
||||
===================================================================== */
|
||||
|
||||
export const STUFEN = ["neu", "dabei", "stamm"];
|
||||
const STUFEN_NAME = { neu: "Neu", dabei: "Dabei", stamm: "Stamm" };
|
||||
/** Ab wann die nächste Stufe gilt. Beide Grenzen an einer Stelle. */
|
||||
const DABEI_TAGE = 7;
|
||||
const STAMM_TAGE = 30;
|
||||
const STAMM_BEITRAEGE = 3;
|
||||
|
||||
/**
|
||||
* Welche Stufe hat diese Person?
|
||||
*
|
||||
* Team Dogi hat keine Stufe — die Frage ergibt dort keinen Sinn, und
|
||||
* eine erfundene Antwort („stamm") wäre eine, auf die sich später
|
||||
* jemand verlässt. Deshalb `null`.
|
||||
*/
|
||||
export function stufeVon(person, dieDb = db) {
|
||||
if (!person || !TREFF_AUSSEN.has(person.rolle)) return null;
|
||||
const zeile = dieDb().prepare("SELECT erstellt FROM personen WHERE id = ?")
|
||||
.get(person.id);
|
||||
if (!zeile?.erstellt) return "neu";
|
||||
const tage = (Date.now() - Date.parse(zeile.erstellt)) / TAG;
|
||||
if (!Number.isFinite(tage) || tage < DABEI_TAGE) return "neu";
|
||||
if (tage < STAMM_TAGE) return "dabei";
|
||||
const platz = TREFF_BRETTER.map(() => "?").join(", ");
|
||||
const { n } = dieDb().prepare(
|
||||
`SELECT COUNT(*) AS n FROM eintraege
|
||||
WHERE erstellt_von = ? AND bereich IN (${platz})`).get(person.id, ...TREFF_BRETTER);
|
||||
return n >= STAMM_BEITRAEGE ? "stamm" : "dabei";
|
||||
}
|
||||
|
||||
const stufeReicht = (hat, noetig) => STUFEN.indexOf(hat) >= STUFEN.indexOf(noetig);
|
||||
|
||||
/* =====================================================================
|
||||
WER DARF AUF WELCHES BRETT SCHREIBEN — die Tafel
|
||||
|
||||
Eine Tafel und keine Abfragen in den Handlern, aus demselben Grund
|
||||
wie bei der Rechtetafel in rechte.js: Wer ein achtes Brett anlegt,
|
||||
muss sich entscheiden. Steht es nicht hier, darf niemand von außen
|
||||
darauf schreiben — `undefined` heißt „nein", nicht „alles".
|
||||
|
||||
`null` bei `gast` heißt: gar nicht, auf keiner Stufe. Das ist etwas
|
||||
anderes als „auf einer hohen Stufe" und wird deshalb anders
|
||||
geschrieben.
|
||||
===================================================================== */
|
||||
export const TREFF_SCHREIBEN = {
|
||||
/* Ansagen vom Team. Ein Anschlagbrett, auf das jeder schreiben darf,
|
||||
ist kein Anschlagbrett, sondern ein zweiter Treff. */
|
||||
anschlag: null,
|
||||
/* Termine. Was ansteht, sagt das Team — sonst steht dort ein Termin,
|
||||
den es nicht gibt, und jemand wartet umsonst. */
|
||||
ansteht: null,
|
||||
/* Das Gespräch. Ab „Dabei". */
|
||||
treff: "dabei",
|
||||
/* Wünsche. Schreiben ab „Dabei" — KLICKEN darf jeder ab Tag eins,
|
||||
siehe „Will ich auch". Das ist Absicht: Der erste Eindruck soll
|
||||
nicht „du darfst noch nichts" sein, sondern „deine Stimme zählt". */
|
||||
wunsch: "dabei",
|
||||
/* Clips, Bilder, Fanart. Ab „Stamm", und erst nach Freigabe sichtbar.
|
||||
Zwei Schlösser, weil hier fremde Inhalte hochgeladen werden —
|
||||
Urheberrecht und Anstand sind nichts, was man nachträglich klärt. */
|
||||
highlight: "stamm",
|
||||
/* Regeln pflegt das Team. Eine Regel, die jeder ändern darf, ist
|
||||
keine. */
|
||||
regeln: null,
|
||||
/* „Ich hätte Lust" — die Brücke zu den Talenten. Ab „Dabei": Wer sich
|
||||
nach drei Tagen bewirbt, bewirbt sich auf etwas, das er noch nicht
|
||||
gesehen hat. */
|
||||
mitmachen: "dabei",
|
||||
};
|
||||
|
||||
/* Bretter, auf denen ein Eintrag erst nach Freigabe sichtbar wird.
|
||||
|
||||
BEI `ansteht` IST DAS DER SCHALTER „IM TREFF ZEIGEN". Das Team trägt
|
||||
dort ein, was ansteht, und entscheidet je Termin, ob die Community
|
||||
ihn sieht. Ohne diesen Schalter müsste es zwei Terminlisten führen —
|
||||
und die zweite wäre die, die niemand pflegt.
|
||||
|
||||
BEI `highlight` IST ES DER URHEBERRECHTS- UND ANSTANDSFILTER. */
|
||||
export const TREFF_FREIGABE_BRETTER = ["ansteht", "highlight"];
|
||||
|
||||
/* =====================================================================
|
||||
DIE TABELLEN
|
||||
===================================================================== */
|
||||
|
||||
/* DIE TABELLEN LEGT DER BAUPLAN AN, nicht dieses Modul.
|
||||
|
||||
Vorher stand hier ein "beim ersten Zugriff anlegen". Das ist an drei
|
||||
Stellen im Haus bewaehrt (Ampel, Checkliste, Schulung) und hier
|
||||
trotzdem falsch: Die Sperrfrage wird in sitzungLesen() gestellt --
|
||||
also bevor irgendein Weg des Treffs beruehrt wurde. Beim allerersten
|
||||
Aufruf haette sie gegen eine Tabelle gelaufen, die es noch nicht
|
||||
gibt. Eine Sperre, die bei einer Stoerung "darf" sagt, ist die, die
|
||||
im Ernstfall aufgeht.
|
||||
|
||||
Das SQL selbst steht in treff-tabellen.js, samt Begruendung je
|
||||
Tabelle. */
|
||||
|
||||
/** Höchstens drei Ansagen gleichzeitig angeheftet.
|
||||
*
|
||||
* WARUM ÜBERHAUPT EINE GRENZE: Ein Anschlagbrett mit zwölf angehefteten
|
||||
* Ansagen ist ein Anschlagbrett mit null -- gelesen wird keine davon.
|
||||
* Drei ist die Zahl, die auf ein Handy passt, ohne zu scrollen. */
|
||||
export const ANSCHLAG_MAX = 3;
|
||||
|
||||
/* =====================================================================
|
||||
SPERREN — wer gerade nicht darf
|
||||
===================================================================== */
|
||||
|
||||
/**
|
||||
* Die wirksame Maßnahme gegen diese Person, oder null.
|
||||
*
|
||||
* DREI AUSGÄNGE, NICHT ZWEI: `null` heißt „nichts liegt vor". Kann die
|
||||
* Frage nicht beantwortet werden (Tabelle fehlt, Datenbank weg), wirft
|
||||
* die Funktion -- sie gibt NICHT `null` zurück. Eine Sperre, die bei
|
||||
* einer Störung „darf" sagt, ist genau die, die im Ernstfall aufgeht.
|
||||
*/
|
||||
/** Die wirksame Massnahme gegen diese Person, oder null.
|
||||
* Die Antwort selbst steht in treff-tabellen.js -- dieselbe Frage
|
||||
* wird beim Anmelden und bei jeder Sitzung gestellt, und zwei
|
||||
* Fassungen davon waeren eine zu viel. */
|
||||
export const treffSperre = (personId) => treffSperreLesen(db(), personId);
|
||||
|
||||
/**
|
||||
* Darf diese Person diese Maßnahme verhängen?
|
||||
*
|
||||
* Filipes Entscheidung vom 11.09.2026, wörtlich: *"Modis dürfen
|
||||
* befristet ausschließen, dauerhaft nur ihr."*
|
||||
*
|
||||
* WARUM DIE DAUERHAFTE FASSUNG ANDERS BEHANDELT WIRD: Eine Pause endet
|
||||
* von selbst. Ein dauerhafter Ausschluss endet nur, wenn jemand ihn
|
||||
* zurücknimmt -- und dafür muss man erst merken, dass er falsch war.
|
||||
* Das ist eine andere Art Entscheidung, also braucht sie eine andere
|
||||
* Hand.
|
||||
*/
|
||||
export function darfMassnahme(person, art, dauerhaft) {
|
||||
const r = person?.rolle;
|
||||
if (!TREFF_TEAM.has(r)) return false;
|
||||
if (art === "hinweis") return true;
|
||||
if (!dauerhaft) return true;
|
||||
return r === "admin";
|
||||
}
|
||||
|
||||
/** Die längste Frist, die diese Rolle setzen darf (in Tagen). */
|
||||
export const MODI_FRIST_TAGE = 7;
|
||||
export function fristErlaubt(person, tage) {
|
||||
if (person?.rolle === "admin" || person?.rolle === "hand") return true;
|
||||
return tage > 0 && tage <= MODI_FRIST_TAGE;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DIE FRAGEN, DIE ANDERE MODULE STELLEN
|
||||
===================================================================== */
|
||||
|
||||
/**
|
||||
* Darf diese Person auf dieses Brett schreiben?
|
||||
*
|
||||
* Gibt einen Grund zurück (Text) oder null, wenn es in Ordnung ist.
|
||||
* Ein Text statt `false`, weil die Antwort dem Menschen davor gesagt
|
||||
* wird -- „geht nicht" ohne Grund ist die Sorte Antwort, nach der man
|
||||
* fragt, was man falsch gemacht hat.
|
||||
*/
|
||||
export function darfSchreiben(person, bereich) {
|
||||
if (!TREFF_BRETTER.includes(bereich)) return null; // kein Treff-Brett
|
||||
if (TREFF_TEAM.has(person?.rolle)) return null; // Team schreibt überall
|
||||
if (!TREFF_AUSSEN.has(person?.rolle)) return "Dieser Bereich gehört zum Treff.";
|
||||
|
||||
const noetig = TREFF_SCHREIBEN[bereich];
|
||||
if (!noetig) return "Hier schreibt das Team.";
|
||||
|
||||
const sperre = treffSperre(person.id);
|
||||
if (sperre) {
|
||||
return sperre.art === "ausschluss"
|
||||
? "Dein Zugang zum Treff ist beendet."
|
||||
: `Du hast gerade eine Pause${sperre.bis ? ` bis ${sperre.bis.slice(0, 10)}` : ""}.`;
|
||||
}
|
||||
|
||||
const hat = stufeVon(person);
|
||||
if (!stufeReicht(hat, noetig)) {
|
||||
return noetig === "dabei"
|
||||
? `Ab „${STUFEN_NAME.dabei}" kannst du hier schreiben – das ist nach ${DABEI_TAGE} Tagen so weit.`
|
||||
: `Ab „${STUFEN_NAME.stamm}" kannst du hier etwas einstellen – nach ${STAMM_TAGE} Tagen `
|
||||
+ `und ${STAMM_BEITRAEGE} eigenen Beiträgen.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Zusatzbedingung, die unfreigegebene Einträge vor der Community
|
||||
* verbirgt. Für Team Dogi gilt sie nicht -- es muss ja sehen, was es
|
||||
* freigeben soll.
|
||||
*
|
||||
* ALS SQL-BEDINGUNG UND NICHT ALS FILTER DANACH: Ein Filter in
|
||||
* JavaScript hätte die Zeilen erst geholt. Sie stünden dann in der
|
||||
* Antwort des Servers, in seinem Speicher und in jedem Protokoll
|
||||
* dazwischen -- und beim nächsten Weg, der dieselbe Abfrage benutzt,
|
||||
* ohne den Filter mitzunehmen, auch im Browser.
|
||||
*/
|
||||
export function freigabeBedingung(person, praefix = "e") {
|
||||
if (!TREFF_AUSSEN.has(person?.rolle)) return null;
|
||||
const platz = TREFF_FREIGABE_BRETTER.map(() => "?").join(", ");
|
||||
return {
|
||||
wo: `(${praefix}.bereich NOT IN (${platz})
|
||||
OR EXISTS (SELECT 1 FROM treff_freigaben f WHERE f.eintrag_id = ${praefix}.id))`,
|
||||
werte: [...TREFF_FREIGABE_BRETTER],
|
||||
};
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DIE WEGE
|
||||
===================================================================== */
|
||||
|
||||
function angemeldet(req, res, next) {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
if (!imTreff(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
req.person = person;
|
||||
next();
|
||||
}
|
||||
|
||||
function gleicheHerkunft(req, res, next) {
|
||||
const herkunft = req.get("origin");
|
||||
if (!herkunft) return next();
|
||||
let erlaubt;
|
||||
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
|
||||
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
|
||||
next();
|
||||
}
|
||||
|
||||
treffRouter.use("/workspace/api/treff", angemeldet);
|
||||
|
||||
/** Nur Team Dogi. 404 und nicht 403 -- wer „kein Zugriff" liest, fragt
|
||||
* als Nächstes, wer denn dann. */
|
||||
function nurTeam(req, res, next) {
|
||||
if (TREFF_TEAM.has(req.person?.rolle)) return next();
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
}
|
||||
|
||||
/** Gehört dieser Eintrag zu einem Brett des Treffs? */
|
||||
function treffEintrag(id) {
|
||||
const e = db().prepare(
|
||||
"SELECT id, bereich, titel, erstellt_von FROM eintraege WHERE id = ?").get(id);
|
||||
if (!e || !TREFF_BRETTER.includes(e.bereich)) return null;
|
||||
return e;
|
||||
}
|
||||
|
||||
const nummer = (x) => { const n = Number(x); return Number.isInteger(n) && n > 0 ? n : 0; };
|
||||
const text = (x, max) => String(x ?? "").trim().slice(0, max);
|
||||
|
||||
/* ---------- Meine Lage ------------------------------------------------ */
|
||||
|
||||
/* Was DIESE Person gerade darf. Die Oberfläche baut daraus ihre Knöpfe.
|
||||
Sie fragt den Server, statt selbst zu rechnen: Eine Regel, die im
|
||||
Browser noch einmal steht, ist die, die beim nächsten Umbau
|
||||
auseinanderläuft -- und die Fassung im Browser kann man ändern. */
|
||||
treffRouter.get("/workspace/api/treff/lage", (req, res) => {
|
||||
try {
|
||||
const p = req.person;
|
||||
const stufe = stufeVon(p);
|
||||
const sperre = TREFF_AUSSEN.has(p.rolle) ? treffSperre(p.id) : null;
|
||||
/* ZWEI ANTWORTEN JE BRETT: ob es geht, und warum nicht.
|
||||
|
||||
Der Grund ist kein Schmuck. Ein Brett ohne Schreibknopf und ohne
|
||||
Erklaerung wirkt kaputt -- man drueckt herum, sucht die Stelle,
|
||||
gibt auf. Mit dem Satz daneben wirkt es gedacht: "Ab Dabei kannst
|
||||
du hier schreiben, das ist nach sieben Tagen so weit."
|
||||
|
||||
UND ER KOMMT VON DER STELLE, DIE AUCH ENTSCHEIDET. darfSchreiben()
|
||||
gibt den Text zurueck, den es ohnehin erzeugt -- derselbe, den der
|
||||
Server bei einem Versuch als Absage schickt. Ein zweiter Satz in
|
||||
der Oberflaeche waere der, der irgendwann etwas anderes behauptet
|
||||
als die Regel tut. */
|
||||
const schreiben = {};
|
||||
const warum = {};
|
||||
for (const b of TREFF_BRETTER) {
|
||||
const grund = darfSchreiben(p, b);
|
||||
schreiben[b] = grund === null;
|
||||
if (grund) warum[b] = grund;
|
||||
}
|
||||
res.json({
|
||||
rolle: p.rolle,
|
||||
rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle,
|
||||
team: TREFF_TEAM.has(p.rolle),
|
||||
stufe,
|
||||
stufe_name: stufe ? STUFEN_NAME[stufe] : null,
|
||||
naechste_stufe: stufe === "neu" ? { name: STUFEN_NAME.dabei, tage: DABEI_TAGE }
|
||||
: stufe === "dabei" ? { name: STUFEN_NAME.stamm, tage: STAMM_TAGE,
|
||||
beitraege: STAMM_BEITRAEGE } : null,
|
||||
sperre: sperre ? { art: sperre.art, bis: sperre.bis, grund: sperre.grund } : null,
|
||||
schreiben,
|
||||
warum,
|
||||
anschlag_max: ANSCHLAG_MAX,
|
||||
});
|
||||
} catch (f) {
|
||||
console.error("[treff] Lage:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Die Regeln ------------------------------------------------ */
|
||||
|
||||
/* DAS MINDESTALTER (Entscheidung Filipe, 11.09.2026: 18 Jahre).
|
||||
|
||||
In Luxemburg liegt die Grenze der DSGVO Art. 8 bei 16 — es waere also
|
||||
erlaubt, ab 16 hereinzulassen. Der Treff gehoert aber zu einem Stream
|
||||
fuer Erwachsene, und dort ist die Frage nicht, was erlaubt ist,
|
||||
sondern wen man hereinlaesst. */
|
||||
export const MINDESTALTER = 18;
|
||||
|
||||
/* WARUM DER REGELTEXT SEINE ZAHLEN VOM SERVER HOLT.
|
||||
|
||||
Im Text steht "nach sieben Tagen kannst du schreiben". Stuende die
|
||||
Sieben als Wort in der HTML-Datei, waere sie eine zweite Wahrheit
|
||||
neben DABEI_TAGE — und beim naechsten Nachjustieren haette man eine
|
||||
Regel, die anders arbeitet als sie sich beschreibt. Das ist die
|
||||
schlimmere Sorte Fehler: Niemand merkt es, und wer es merkt, glaubt
|
||||
dem Text.
|
||||
|
||||
Ein Regeltext, der sich selbst widerspricht, ist ausserdem rechtlich
|
||||
das Gegenteil dessen, wofuer er da ist. */
|
||||
treffRouter.get("/workspace/api/treff/regeln", (req, res) => {
|
||||
res.json({
|
||||
mindestalter: MINDESTALTER,
|
||||
dabei_tage: DABEI_TAGE,
|
||||
stamm_tage: STAMM_TAGE,
|
||||
stamm_beitraege: STAMM_BEITRAEGE,
|
||||
anschlag_max: ANSCHLAG_MAX,
|
||||
grund_min: GRUND_MIN,
|
||||
modi_frist_tage: MODI_FRIST_TAGE,
|
||||
stufen: STUFEN.map((s) => ({ schluessel: s, name: STUFEN_NAME[s] })),
|
||||
/* Welche Bretter erst nach Freigabe erscheinen — auch das gehoert
|
||||
in den Regeltext, und auch das soll nicht abschreibbar sein. */
|
||||
freigabe_bretter: [...TREFF_FREIGABE_BRETTER],
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- „Will ich auch" ------------------------------------------- */
|
||||
|
||||
/* EIN KLICK, KEINE ZAHL IM KOPF. Die Route ist absichtlich nicht
|
||||
„zähle hoch", sondern „meine Stimme liegt hier": Zweimal dasselbe zu
|
||||
schicken ändert nichts (INSERT OR IGNORE), und die Antwort ist in
|
||||
beiden Fällen dieselbe. Eine Route, die beim zweiten Aufruf einen
|
||||
Fehler bringt, zwingt die Oberfläche dazu, den eigenen Zustand zu
|
||||
kennen -- und dort ist er am schnellsten falsch. */
|
||||
treffRouter.put("/workspace/api/treff/eintrag/:id/willich", gleicheHerkunft, (req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const e = id && treffEintrag(id);
|
||||
if (!e) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
/* Gestimmt wird auf der Wunschliste. Anderswo wäre es eine zweite
|
||||
Bewertung neben der, die es schon gibt. */
|
||||
if (e.bereich !== "wunsch") return res.status(400).json({ fehler: "hier_nicht" });
|
||||
if (TREFF_AUSSEN.has(req.person.rolle) && treffSperre(req.person.id)) {
|
||||
return res.status(403).json({ fehler: "gesperrt" });
|
||||
}
|
||||
db().prepare(
|
||||
"INSERT OR IGNORE INTO treff_stimmen (eintrag_id, person_id, zeitpunkt) VALUES (?,?,?)")
|
||||
.run(id, req.person.id, jetzt());
|
||||
res.json({ ok: true, ...stimmen(id, req.person.id) });
|
||||
} catch (f) {
|
||||
console.error("[treff] Stimme:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
treffRouter.delete("/workspace/api/treff/eintrag/:id/willich", gleicheHerkunft, (req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const e = id && treffEintrag(id);
|
||||
if (!e) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
db().prepare("DELETE FROM treff_stimmen WHERE eintrag_id = ? AND person_id = ?")
|
||||
.run(id, req.person.id);
|
||||
res.json({ ok: true, ...stimmen(id, req.person.id) });
|
||||
} catch (f) {
|
||||
console.error("[treff] Stimme zurück:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
function stimmen(id, personId) {
|
||||
const { n } = db().prepare("SELECT COUNT(*) AS n FROM treff_stimmen WHERE eintrag_id = ?")
|
||||
.get(id);
|
||||
const meine = db().prepare(
|
||||
"SELECT 1 AS x FROM treff_stimmen WHERE eintrag_id = ? AND person_id = ?")
|
||||
.get(id, personId);
|
||||
return { stimmen: n, meine: !!meine };
|
||||
}
|
||||
|
||||
/* ---------- Anheften (Anschlagbrett) ---------------------------------- */
|
||||
|
||||
treffRouter.put("/workspace/api/treff/eintrag/:id/anheften", gleicheHerkunft, nurTeam,
|
||||
(req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const e = id && treffEintrag(id);
|
||||
if (!e) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
if (e.bereich !== "anschlag") return res.status(400).json({ fehler: "hier_nicht" });
|
||||
|
||||
/* DIE ZÄHLUNG STEHT IN DERSELBEN TRANSAKTION WIE DAS EINFÜGEN.
|
||||
Zwei Modis, die gleichzeitig anheften, würden sonst beide „zwei
|
||||
von drei" zählen und beide einfügen -- und am Ende hingen vier.
|
||||
Genau dieser Fehler ist in diesem Haus schon bei der
|
||||
CHECK-Umstellung vermieden worden, aus demselben Grund. */
|
||||
db().exec("BEGIN IMMEDIATE");
|
||||
try {
|
||||
const schon = db().prepare(
|
||||
"SELECT 1 AS x FROM treff_angeheftet WHERE eintrag_id = ?").get(id);
|
||||
if (!schon) {
|
||||
const { n } = db().prepare("SELECT COUNT(*) AS n FROM treff_angeheftet").get();
|
||||
if (n >= ANSCHLAG_MAX) {
|
||||
db().exec("ROLLBACK");
|
||||
return res.status(409).json({
|
||||
fehler: `Es hängen schon ${ANSCHLAG_MAX} Ansagen. Nimm zuerst eine ab.`,
|
||||
});
|
||||
}
|
||||
db().prepare(
|
||||
"INSERT INTO treff_angeheftet (eintrag_id, seit, von) VALUES (?,?,?)")
|
||||
.run(id, jetzt(), req.person.id);
|
||||
}
|
||||
db().exec("COMMIT");
|
||||
} catch (f) {
|
||||
try { db().exec("ROLLBACK"); } catch { /* schon zurück */ }
|
||||
throw f;
|
||||
}
|
||||
protokolliere("treff_angeheftet", { personId: req.person.id, ip: echteIp(req),
|
||||
detail: `Eintrag ${id}` });
|
||||
res.json({ ok: true });
|
||||
} catch (f) {
|
||||
console.error("[treff] Anheften:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
treffRouter.delete("/workspace/api/treff/eintrag/:id/anheften", gleicheHerkunft, nurTeam,
|
||||
(req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
if (!id) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
db().prepare("DELETE FROM treff_angeheftet WHERE eintrag_id = ?").run(id);
|
||||
res.json({ ok: true });
|
||||
} catch (f) {
|
||||
console.error("[treff] Abnehmen:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Freigeben ------------------------------------------------- */
|
||||
|
||||
/* „Im Treff zeigen" bei Terminen, „Freigeben" bei Highlights -- derselbe
|
||||
Vorgang, deshalb dieselbe Route. Zwei Routen wären zwei Stellen, an
|
||||
denen dieselbe Rechteprüfung steht. */
|
||||
treffRouter.put("/workspace/api/treff/eintrag/:id/freigeben", gleicheHerkunft, nurTeam,
|
||||
(req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const e = id && treffEintrag(id);
|
||||
if (!e) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
if (!TREFF_FREIGABE_BRETTER.includes(e.bereich)) {
|
||||
return res.status(400).json({ fehler: "hier_nicht" });
|
||||
}
|
||||
db().prepare(
|
||||
"INSERT OR IGNORE INTO treff_freigaben (eintrag_id, zeitpunkt, von) VALUES (?,?,?)")
|
||||
.run(id, jetzt(), req.person.id);
|
||||
protokolliere("treff_freigegeben", { personId: req.person.id, ip: echteIp(req),
|
||||
detail: `${e.bereich} ${id}` });
|
||||
res.json({ ok: true, freigegeben: true });
|
||||
} catch (f) {
|
||||
console.error("[treff] Freigeben:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
treffRouter.delete("/workspace/api/treff/eintrag/:id/freigeben", gleicheHerkunft, nurTeam,
|
||||
(req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
if (!id) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
db().prepare("DELETE FROM treff_freigaben WHERE eintrag_id = ?").run(id);
|
||||
res.json({ ok: true, freigegeben: false });
|
||||
} catch (f) {
|
||||
console.error("[treff] Freigabe zurück:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Melden (DSA Art. 16) -------------------------------------- */
|
||||
|
||||
export const GRUND_MIN = 10;
|
||||
export const GRUND_MAX = 1000;
|
||||
|
||||
treffRouter.post("/workspace/api/treff/eintrag/:id/melden", gleicheHerkunft, (req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const e = id && treffEintrag(id);
|
||||
if (!e) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
/* DER GRUND IST PFLICHT, und die Mindestlänge ist keine Schikane:
|
||||
Art. 16 Abs. 2 verlangt eine Meldung, die „hinreichend genau und
|
||||
angemessen begründet" ist -- also eine, nach der man entscheiden
|
||||
kann, ohne zurückzufragen. „Scheiße" ist keine. */
|
||||
const grund = text(req.body?.grund, GRUND_MAX);
|
||||
if (grund.length < GRUND_MIN) {
|
||||
return res.status(400).json({
|
||||
fehler: "Bitte schreib kurz, was daran nicht in Ordnung ist – "
|
||||
+ "sonst können wir es nicht prüfen.",
|
||||
});
|
||||
}
|
||||
db().prepare(`INSERT INTO treff_meldungen (eintrag_id, melder_id, grund, zeitpunkt)
|
||||
VALUES (?,?,?,?)`).run(id, req.person.id, grund, jetzt());
|
||||
protokolliere("treff_meldung", { personId: req.person.id, ip: echteIp(req),
|
||||
detail: `Eintrag ${id}` });
|
||||
/* KEINE AUSKUNFT DARÜBER, OB SCHON JEMAND GEMELDET HAT. Sonst wäre
|
||||
die Antwort ein Zähler, an dem man ablesen kann, wen sonst noch
|
||||
etwas stört -- und daraus wird eine Abstimmung statt einer
|
||||
Prüfung. */
|
||||
res.json({ ok: true });
|
||||
} catch (f) {
|
||||
console.error("[treff] Melden:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
treffRouter.get("/workspace/api/treff/meldungen", nurTeam, (req, res) => {
|
||||
try {
|
||||
const zeilen = db().prepare(`
|
||||
SELECT m.id, m.eintrag_id, m.grund, m.zeitpunkt, m.status, m.entscheid,
|
||||
m.entschieden_am,
|
||||
e.bereich, e.titel, e.text,
|
||||
pm.name AS melder_name, pm.rolle AS melder_rolle,
|
||||
pe.name AS autor_name, pe.rolle AS autor_rolle,
|
||||
pd.name AS entschieden_name
|
||||
FROM treff_meldungen m
|
||||
LEFT JOIN eintraege e ON e.id = m.eintrag_id
|
||||
LEFT JOIN personen pm ON pm.id = m.melder_id
|
||||
LEFT JOIN personen pe ON pe.id = e.erstellt_von
|
||||
LEFT JOIN personen pd ON pd.id = m.entschieden_von
|
||||
ORDER BY CASE m.status WHEN 'offen' THEN 0 ELSE 1 END, m.id DESC
|
||||
LIMIT 300`).all();
|
||||
res.json({ meldungen: zeilen.map(mitRollenname) });
|
||||
} catch (f) {
|
||||
console.error("[treff] Meldungen:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
treffRouter.patch("/workspace/api/treff/meldung/:id", gleicheHerkunft, nurTeam, (req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const status = String(req.body?.status || "");
|
||||
if (!id || !["erledigt", "abgelehnt"].includes(status)) {
|
||||
return res.status(400).json({ fehler: "unbrauchbar" });
|
||||
}
|
||||
const entscheid = text(req.body?.entscheid, GRUND_MAX);
|
||||
if (entscheid.length < GRUND_MIN) {
|
||||
return res.status(400).json({
|
||||
fehler: "Bitte halte kurz fest, was du entschieden hast – "
|
||||
+ "das gehört zur Meldung dazu.",
|
||||
});
|
||||
}
|
||||
const r = db().prepare(`UPDATE treff_meldungen
|
||||
SET status = ?, entscheid = ?, entschieden_von = ?, entschieden_am = ?
|
||||
WHERE id = ? AND status = 'offen'`)
|
||||
.run(status, entscheid, req.person.id, jetzt(), id);
|
||||
if (!r.changes) return res.status(409).json({ fehler: "schon_entschieden" });
|
||||
protokolliere("treff_meldung_entschieden", { personId: req.person.id, ip: echteIp(req),
|
||||
detail: `${id}: ${status}` });
|
||||
res.json({ ok: true });
|
||||
} catch (f) {
|
||||
console.error("[treff] Meldung entscheiden:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Entfernen mit Grund (DSA Art. 17) ------------------------- */
|
||||
|
||||
/**
|
||||
* Hält fest, warum ein Beitrag entfernt wurde — BEVOR er verschwindet.
|
||||
*
|
||||
* Wird aus workspace-bereiche.js gerufen, nicht als eigene Route: Es
|
||||
* gibt genau einen Weg, einen Eintrag zu löschen, und das soll so
|
||||
* bleiben. Eine zweite Löschroute wäre die, an der die Begründung
|
||||
* eines Tages fehlt.
|
||||
*
|
||||
* @returns {string|null} Fehlertext, oder null wenn gelöscht werden darf
|
||||
*/
|
||||
export function entfernenVorbereiten(person, eintrag, grund) {
|
||||
if (!TREFF_BRETTER.includes(eintrag?.bereich)) return null; // kein Treff-Brett
|
||||
/* Wer seinen EIGENEN Beitrag zurücknimmt, begründet nichts. Art. 17
|
||||
schützt den Verfasser vor der Plattform, nicht vor sich selbst. */
|
||||
if (eintrag.erstellt_von === person?.id) return null;
|
||||
|
||||
const g = text(grund, GRUND_MAX);
|
||||
if (g.length < GRUND_MIN) {
|
||||
return "Zum Entfernen gehört ein Grund – die Person erfährt ihn, "
|
||||
+ "und wir müssen ihn festhalten können.";
|
||||
}
|
||||
const autor = eintrag.erstellt_von
|
||||
? db().prepare("SELECT name FROM personen WHERE id = ?").get(eintrag.erstellt_von)
|
||||
: null;
|
||||
db().prepare(`INSERT INTO treff_entfernt
|
||||
(eintrag_id, bereich, titel, autor_id, autor_name, grund, von, zeitpunkt)
|
||||
VALUES (?,?,?,?,?,?,?,?)`)
|
||||
.run(eintrag.id, eintrag.bereich, String(eintrag.titel ?? ""),
|
||||
eintrag.erstellt_von ?? null, autor?.name ?? null, g, person?.id ?? null, jetzt());
|
||||
return null;
|
||||
}
|
||||
|
||||
treffRouter.get("/workspace/api/treff/entfernt", nurTeam, (req, res) => {
|
||||
try {
|
||||
const zeilen = db().prepare(`
|
||||
SELECT t.id, t.eintrag_id, t.bereich, t.titel, t.autor_name, t.grund, t.zeitpunkt,
|
||||
p.name AS von_name, p.rolle AS von_rolle
|
||||
FROM treff_entfernt t LEFT JOIN personen p ON p.id = t.von
|
||||
ORDER BY t.id DESC LIMIT 300`).all();
|
||||
res.json({ entfernt: zeilen.map(mitRollenname) });
|
||||
} catch (f) {
|
||||
console.error("[treff] Entfernt:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Maßnahmen ------------------------------------------------- */
|
||||
|
||||
treffRouter.post("/workspace/api/treff/massnahme", gleicheHerkunft, nurTeam, (req, res) => {
|
||||
try {
|
||||
const personId = nummer(req.body?.person_id);
|
||||
const art = String(req.body?.art || "");
|
||||
const grund = text(req.body?.grund, GRUND_MAX);
|
||||
const tage = Number(req.body?.tage);
|
||||
const dauerhaft = art === "ausschluss" && (req.body?.tage === null
|
||||
|| req.body?.tage === undefined || req.body?.tage === "");
|
||||
|
||||
if (!personId || !["hinweis", "pause", "ausschluss"].includes(art)) {
|
||||
return res.status(400).json({ fehler: "unbrauchbar" });
|
||||
}
|
||||
if (grund.length < GRUND_MIN) {
|
||||
return res.status(400).json({
|
||||
fehler: "Ohne Grund geht das nicht – die Person hat ein Recht darauf zu erfahren, warum.",
|
||||
});
|
||||
}
|
||||
const ziel = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(personId);
|
||||
/* MASSNAHMEN GELTEN NUR GEGEN DIE COMMUNITY. Ein Modi, der einen
|
||||
anderen Modi „ausschließen" könnte, hätte einen Weg, jemanden aus
|
||||
dem Team zu entfernen -- und zwar an „Personen & Zugänge" vorbei,
|
||||
wo diese Entscheidung hingehört. */
|
||||
if (!ziel || !TREFF_AUSSEN.has(ziel.rolle)) {
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
}
|
||||
if (!darfMassnahme(req.person, art, dauerhaft)) {
|
||||
return res.status(403).json({
|
||||
fehler: "Dauerhaft ausschließen kann nur DogFather.",
|
||||
});
|
||||
}
|
||||
let bis = null;
|
||||
if (!dauerhaft && art !== "hinweis") {
|
||||
if (!Number.isFinite(tage) || tage <= 0 || tage > 3650) {
|
||||
return res.status(400).json({ fehler: "Bitte gib an, für wie viele Tage." });
|
||||
}
|
||||
if (!fristErlaubt(req.person, tage)) {
|
||||
return res.status(403).json({
|
||||
fehler: `Als Modi kannst du bis zu ${MODI_FRIST_TAGE} Tage geben. `
|
||||
+ "Länger entscheidet DogFather.",
|
||||
});
|
||||
}
|
||||
bis = new Date(Date.now() + tage * TAG).toISOString();
|
||||
}
|
||||
db().prepare(`INSERT INTO treff_massnahmen (person_id, art, grund, bis, von, zeitpunkt)
|
||||
VALUES (?,?,?,?,?,?)`)
|
||||
.run(personId, art, grund, bis, req.person.id, jetzt());
|
||||
protokolliere("treff_massnahme", { personId: req.person.id, ip: echteIp(req),
|
||||
detail: `${art} gegen ${ziel.name}${bis ? ` bis ${bis.slice(0, 10)}` : " (dauerhaft)"}` });
|
||||
res.json({ ok: true, art, bis });
|
||||
} catch (f) {
|
||||
console.error("[treff] Maßnahme:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
treffRouter.delete("/workspace/api/treff/massnahme/:id", gleicheHerkunft, nurTeam, (req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
if (!id) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
const m = db().prepare("SELECT id, art, bis FROM treff_massnahmen WHERE id = ?").get(id);
|
||||
if (!m) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
/* Wer eine dauerhafte Maßnahme nicht verhängen darf, darf sie auch
|
||||
nicht aufheben. Sonst wäre die Regel darüber eine Formalie: Ein
|
||||
Modi könnte einen dauerhaften Ausschluss abnehmen und einen
|
||||
siebentägigen setzen. */
|
||||
if (!darfMassnahme(req.person, m.art, !m.bis)) {
|
||||
return res.status(403).json({ fehler: "Das kann nur DogFather aufheben." });
|
||||
}
|
||||
db().prepare(
|
||||
"UPDATE treff_massnahmen SET aufgehoben_am = ?, aufgehoben_von = ? WHERE id = ?")
|
||||
.run(jetzt(), req.person.id, id);
|
||||
protokolliere("treff_massnahme_auf", { personId: req.person.id, ip: echteIp(req),
|
||||
detail: String(id) });
|
||||
res.json({ ok: true });
|
||||
} catch (f) {
|
||||
console.error("[treff] Maßnahme aufheben:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
treffRouter.get("/workspace/api/treff/massnahmen", nurTeam, (req, res) => {
|
||||
try {
|
||||
const zeilen = db().prepare(`
|
||||
SELECT m.id, m.person_id, m.art, m.grund, m.bis, m.zeitpunkt, m.aufgehoben_am,
|
||||
p.name AS person_name, v.name AS von_name, v.rolle AS von_rolle
|
||||
FROM treff_massnahmen m
|
||||
LEFT JOIN personen p ON p.id = m.person_id
|
||||
LEFT JOIN personen v ON v.id = m.von
|
||||
ORDER BY m.id DESC LIMIT 300`).all();
|
||||
res.json({ massnahmen: zeilen.map(mitRollenname) });
|
||||
} catch (f) {
|
||||
console.error("[treff] Maßnahmen:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Die Mitglieder -------------------------------------------- */
|
||||
|
||||
/* WARUM DER TREFF SEINE EIGENE MITGLIEDERLISTE HAT UND NICHT
|
||||
/api/personen BENUTZT (11.09.2026).
|
||||
|
||||
Der naheliegende Weg wäre gewesen, die Moderation auf „Personen &
|
||||
Zugänge" zu schicken. Drei Gründe dagegen, und der dritte ist der
|
||||
eigentliche:
|
||||
|
||||
1. Dort stehen alle — Creator, Scouts, die Agentur. Ein Modi, der
|
||||
jemanden sperren will, müsste sich durch Menschen klicken, die
|
||||
ihn nichts angehen.
|
||||
2. Dort gibt es Knöpfe, die ein Modi nicht drücken darf. Ein
|
||||
Bildschirm voller Absagen ist schlechter als einer ohne Knöpfe.
|
||||
3. Was hier gebraucht wird, steht dort gar nicht: die STUFE und die
|
||||
laufende Maßnahme. Beides gibt es nur im Treff.
|
||||
|
||||
NUR AUSSENROLLEN. Die Liste kann das Team nicht enthalten -- nicht
|
||||
weil es ausgeblendet wäre, sondern weil die Abfrage es gar nicht
|
||||
erst holt. Eine Maßnahme gegen jemanden aus dem Team gibt es nicht
|
||||
(siehe /massnahme), und eine Liste, aus der man etwas auswählen
|
||||
kann, das danach abgelehnt wird, ist eine Falle. */
|
||||
treffRouter.get("/workspace/api/treff/mitglieder", nurTeam, (req, res) => {
|
||||
try {
|
||||
const zeilen = db().prepare(`
|
||||
SELECT id, name, erstellt, letzter_login, aktiv
|
||||
FROM personen WHERE rolle = 'gast'
|
||||
ORDER BY name COLLATE NOCASE`).all();
|
||||
const platz = TREFF_BRETTER.map(() => "?").join(", ");
|
||||
const beitraege = db().prepare(
|
||||
`SELECT erstellt_von AS id, COUNT(*) AS n FROM eintraege
|
||||
WHERE bereich IN (${platz}) GROUP BY erstellt_von`).all(...TREFF_BRETTER);
|
||||
const zahl = new Map(beitraege.map((b) => [b.id, b.n]));
|
||||
|
||||
res.json({
|
||||
mitglieder: zeilen.map((z) => {
|
||||
const sperre = treffSperre(z.id);
|
||||
return {
|
||||
id: z.id, name: z.name, seit: z.erstellt, zuletzt: z.letzter_login,
|
||||
beitraege: zahl.get(z.id) || 0,
|
||||
stufe: stufeVon({ id: z.id, rolle: "gast" }),
|
||||
sperre: sperre
|
||||
? { id: sperre.id, art: sperre.art, bis: sperre.bis, grund: sperre.grund }
|
||||
: null,
|
||||
};
|
||||
}),
|
||||
/* Was DIESE Person verhängen darf. Die Oberfläche baut daraus
|
||||
ihre Knöpfe -- und nicht aus einer eigenen Rollenabfrage, die
|
||||
auseinanderlaufen könnte. */
|
||||
darf: {
|
||||
pause: darfMassnahme(req.person, "pause", false),
|
||||
ausschluss_befristet: darfMassnahme(req.person, "ausschluss", false),
|
||||
ausschluss_dauerhaft: darfMassnahme(req.person, "ausschluss", true),
|
||||
frist_tage: req.person.rolle === "modi" ? MODI_FRIST_TAGE : null,
|
||||
},
|
||||
});
|
||||
} catch (f) {
|
||||
console.error("[treff] Mitglieder:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Die Brücke zu den Talenten -------------------------------- */
|
||||
|
||||
/* Wer auf „Mitmachen" schreibt „Ich hätte Lust", ist ein Kandidat. Diese
|
||||
Route legt ihn in der Kategorie an, die es dafür schon gibt -- statt
|
||||
einer zweiten Liste daneben.
|
||||
|
||||
EINMAL IST EINMAL: Ein zweites Übernehmen legt keinen zweiten
|
||||
Kandidaten an. Geprüft wird an der Quelle (gibt es schon einen
|
||||
Talent-Eintrag zu diesem Menschen?), nicht an einer Merkspalte. */
|
||||
treffRouter.post("/workspace/api/treff/mitmachen/:id/uebernehmen", gleicheHerkunft, nurTeam,
|
||||
(req, res) => {
|
||||
try {
|
||||
const id = nummer(req.params.id);
|
||||
const e = id && db().prepare(
|
||||
"SELECT id, bereich, art, titel, text, erstellt_von FROM eintraege WHERE id = ?").get(id);
|
||||
if (!e || e.bereich !== "mitmachen") return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
if (!e.erstellt_von) {
|
||||
return res.status(400).json({ fehler: "Zu diesem Eintrag gibt es keine Person." });
|
||||
}
|
||||
const schon = db().prepare(
|
||||
"SELECT id FROM eintraege WHERE bereich = 'talente' AND creator_id = ?")
|
||||
.get(e.erstellt_von);
|
||||
if (schon) return res.status(409).json({ fehler: "schon_uebernommen", talent_id: schon.id });
|
||||
|
||||
const wer = db().prepare("SELECT name FROM personen WHERE id = ?").get(e.erstellt_von);
|
||||
const nun = jetzt();
|
||||
const r = db().prepare(`INSERT INTO eintraege
|
||||
(bereich, art, titel, text, datum, dringlichkeit, status, creator_id,
|
||||
erstellt, erstellt_von, einsatz)
|
||||
VALUES ('talente','empfohlen',?,?,?,'mittel','offen',?,?,?,?)`)
|
||||
.run(
|
||||
`${wer?.name ?? "Jemand"} hat sich im Treff gemeldet`,
|
||||
`Aus „Mitmachen": ${String(e.titel ?? "")}\n\n${String(e.text ?? "")}`.trim(),
|
||||
nun.slice(0, 10), e.erstellt_von, nun, req.person.id, "Der Treff");
|
||||
protokolliere("treff_talent", { personId: req.person.id, ip: echteIp(req),
|
||||
detail: `aus Eintrag ${id}` });
|
||||
res.json({ ok: true, talent_id: Number(r.lastInsertRowid) });
|
||||
} catch (f) {
|
||||
console.error("[treff] Übernehmen:", f?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
DER VOLLE ROLLENNAME AN JEDEM BEITRAG
|
||||
|
||||
Filipes Entscheidung vom 11.09.2026: „Voller Rollenname zeigen" --
|
||||
an einem Beitrag im Treff steht „Modi", „Rechte Hand", „DogFather".
|
||||
|
||||
WARUM DER SERVER DEN NAMEN LIEFERT UND NICHT DER BROWSER: Eine
|
||||
Zuordnung Rolle → Name im ausgelieferten JavaScript wäre eine Liste
|
||||
aller Rollen dieses Hauses -- auf jedem Rechner der Community,
|
||||
dauerhaft lesbar. Hier kommt nur der Name der Rollen an, die an
|
||||
diesem Beitrag wirklich beteiligt sind.
|
||||
------------------------------------------------------------------- */
|
||||
function mitRollenname(zeile) {
|
||||
const aus = { ...zeile };
|
||||
for (const [k, v] of Object.entries(zeile)) {
|
||||
if (k.endsWith("_rolle") && v) aus[`${k}_name`] = ROLLEN_NAME[v] ?? v;
|
||||
}
|
||||
return aus;
|
||||
}
|
||||
export { mitRollenname };
|
||||
@@ -27,8 +27,12 @@ import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { darfSeite } from "./rechte.js";
|
||||
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse,
|
||||
import { darfSeite, OHNE_ANMELDUNG, SEITEN, ALLE_ROLLEN } from "./rechte.js";
|
||||
/* Ein Modul ohne eigene Importe -- deshalb entsteht hier kein Kreis,
|
||||
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,
|
||||
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. */
|
||||
@@ -63,7 +67,7 @@ const COOKIE = "dfw_sitzung";
|
||||
const SITZUNG_STUNDEN = 12;
|
||||
const VERSUCHE_MAX = 8; // pro IP
|
||||
const VERSUCHE_FENSTER_MIN = 10;
|
||||
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator", "hand", "modi"]);
|
||||
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator", "hand", "modi", "gast"]);
|
||||
|
||||
/* Die Reihenfolge, in der Rollen ueberall erscheinen: Spicy Media
|
||||
zuerst, dann DogFather, Manager, Scout, Creator. Steht hier einmal,
|
||||
@@ -99,6 +103,19 @@ const ROLLEN_KACHEL = new Set(["spicy", "admin", "manager", "scout", "creator"])
|
||||
* sondern ihr Sinn. */
|
||||
const CREW_KACHEL = new Set(["admin", "hand", "modi"]);
|
||||
|
||||
/** Die Rollen mit einer Kachel auf der Zugangswand von treff.
|
||||
*
|
||||
* EINE, und deshalb zeigt die Wand dort gar keine Kachelreihe: Eine
|
||||
* Auswahl mit einem einzigen Eintrag ist keine Auswahl, sondern eine
|
||||
* Huerde. Dieselbe Ueberlegung wie bei der ersten Fassung der
|
||||
* Crew-Wand -- nur ist sie hier dauerhaft richtig, weil es im Treff
|
||||
* auch kuenftig nur eine Rolle gibt.
|
||||
*
|
||||
* 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
|
||||
herum. */
|
||||
@@ -383,6 +400,15 @@ export const ROLLEN_NAME = {
|
||||
creator: "Creator",
|
||||
hand: "Rechte Hand",
|
||||
modi: "Modi",
|
||||
/* DIE COMMUNITY (11.09.2026). Der Name steht hier und NICHT in einer
|
||||
Datei, die jeder herunterlaedt -- ausgeliefert wird er nur an den,
|
||||
der ihn selbst traegt, und an die, die ihn sehen duerfen.
|
||||
|
||||
Filipe hat ausdruecklich gewaehlt, dass im Treff der volle
|
||||
Rollenname neben einem Beitrag steht ("Modi", "Rechte Hand",
|
||||
"DogFather"). Das bleibt damit vereinbar: Der Name kommt als DATEN
|
||||
mit der Antwort, nicht als Konstante im Browser. */
|
||||
gast: "Community",
|
||||
};
|
||||
|
||||
/* DIE UEBERSCHRIFT UEBER EINER PERSONENLISTE (10.09.2026).
|
||||
@@ -632,9 +658,110 @@ const PERSONEN_KACHEL = {
|
||||
const HAND_BEREICHE = [...MODI_BEREICHE, EINGANG_KACHEL, PERSONEN_KACHEL,
|
||||
ENTWICKLUNG_KACHEL, TALENTE_KACHEL];
|
||||
|
||||
|
||||
/* =====================================================================
|
||||
DIE SIEBEN BRETTER DES TREFFS (11.09.2026)
|
||||
|
||||
Filipe: "wieso nur eine kachel fuer die community ich will dass die
|
||||
mehrere kacheln haben. mit mehreren optionen."
|
||||
|
||||
Eine Kachel war zu kurz gedacht: Ein Raum, in dem Ansagen, Fragen,
|
||||
Wuensche und Clips durcheinandergehen, ist nach zwei Wochen
|
||||
unbenutzbar -- die Ansage von gestern ist weg, die Frage von heute
|
||||
geht unter.
|
||||
|
||||
SORTIERT NACH ZWECK, NICHT NACH THEMA, und in der Reihenfolge, in der
|
||||
ein Neuer liest: erst was gilt, dann was laeuft, dann das Gespraech,
|
||||
dann das Besondere.
|
||||
|
||||
Jede der sieben bedient eines der vier Dinge, die darueber
|
||||
entscheiden, ob sich jemand zugehoerig fuehlt (Zugehoerigkeit,
|
||||
Einfluss, Nutzen, geteilte Erlebnisse). Was keines bedient, ist nicht
|
||||
dabei -- deshalb gibt es hier keine Bestenliste und keine Punkte.
|
||||
|
||||
VON OBEN NACH UNTEN GELESEN SIND SIE EIN WEG: lesen -> mitreden ->
|
||||
mitgestalten -> dazugehoeren. Die letzte Kachel uebergibt an die
|
||||
Talente-Kategorie, die es schon gibt.
|
||||
|
||||
DIE KACHELN STEHEN HIER UND NICHT IN assets/js/bereiche.js -- aus
|
||||
demselben Grund wie bei den Modis: Diese Datei wird nicht
|
||||
ausgeliefert, jene bekommt jeder Besucher. */
|
||||
const TREFF_GRUPPE = {
|
||||
gruppe: "Community",
|
||||
gruppeUnter: "Der Treff \u2013 der Raum f\u00fcr die Zuschauer",
|
||||
};
|
||||
|
||||
const TREFF_BEREICHE = [
|
||||
{ ...TREFF_GRUPPE, name: "Anschlagbrett", unter: "Was gerade gilt",
|
||||
zeichen: "reports", ton: 33, ziel: "bereich.html?b=anschlag", szene: "halle" },
|
||||
{ ...TREFF_GRUPPE, name: "Was ansteht", unter: "Die n\u00e4chsten Streams und Events",
|
||||
zeichen: "kalender", ton: 31, ziel: "bereich.html?b=ansteht", szene: "skyline" },
|
||||
{ ...TREFF_GRUPPE, name: "Der Treff", unter: "Hallo sagen, fragen, loben",
|
||||
zeichen: "community", ton: 34, gross: true, ziel: "bereich.html?b=treff", szene: "lounge" },
|
||||
{ ...TREFF_GRUPPE, name: "Wunschliste", unter: "Was ihr euch w\u00fcnscht \u2013 und wer mitwill",
|
||||
zeichen: "content", ton: 30, ziel: "bereich.html?b=wunsch", szene: "portal" },
|
||||
{ ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder",
|
||||
zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" },
|
||||
{ ...TREFF_GRUPPE, name: "Regeln & Hilfe", unter: "Wie es hier l\u00e4uft",
|
||||
zeichen: "schutz", ton: 28, ziel: "bereich.html?b=regeln", szene: "studio" },
|
||||
{ ...TREFF_GRUPPE, name: "Mitmachen", unter: "Wie du dazugeh\u00f6ren kannst",
|
||||
zeichen: "personen", ton: 32, ziel: "bereich.html?b=mitmachen", szene: "garage" },
|
||||
];
|
||||
|
||||
/* MELDUNGEN & MASSNAHMEN -- FUER DAS TEAM, NICHT FUER DIE COMMUNITY
|
||||
(11.09.2026).
|
||||
|
||||
Diese Kachel steht ausdruecklich NICHT in TREFF_BEREICHE. Die Liste
|
||||
dort ist das, was ein Mitglied sieht; sie wird dem Team nur
|
||||
angehaengt. Eine achte Kachel darin waere auch bei der Community
|
||||
gelandet -- und zwar lautlos, denn sie saehe dort aus wie die
|
||||
anderen sieben. Erst beim Klicken haette der Server 404 gesagt, und
|
||||
das sieht aus wie ein kaputtes Programm.
|
||||
|
||||
SIE GEHOERT AUCH NICHT IN TREFF_BRETTER: Sie ist kein Brett mit
|
||||
Eintraegen, sondern eine Seite. Das Ziel ist deshalb ein
|
||||
Dateiname und kein "bereich.html?b=...", und die Ableitung weiter
|
||||
unten nimmt sie von selbst nicht mit. */
|
||||
const TREFF_MODERATION_KACHEL = {
|
||||
...TREFF_GRUPPE,
|
||||
name: "Meldungen & Ma\u00dfnahmen", unter: "Was gemeldet wurde \u2013 und was daraus wurde",
|
||||
zeichen: "schutz", ton: 36, ziel: "treff-moderation.html", szene: "studio",
|
||||
};
|
||||
|
||||
/* Welche Bretter zum Treff gehoeren -- ABGELEITET aus den Kacheln, nicht
|
||||
danebengeschrieben. Eine zweite Liste waere die, die beim naechsten
|
||||
neuen Brett vergessen wird. */
|
||||
export const TREFF_BRETTER = TREFF_BEREICHE
|
||||
.map((k) => new URL("http://x/" + k.ziel).searchParams.get("b"))
|
||||
.filter(Boolean);
|
||||
|
||||
/* Wer den Treff ueberhaupt sehen darf: die Community selbst und das
|
||||
Team von Dogi. Die Agentur ausdruecklich nicht -- Creator, Scouts und
|
||||
Spicy Media haben mit den Zuschauern dieses Streams nichts zu tun. */
|
||||
export const TREFF_ROLLEN = new Set(["gast", "modi", "hand", "admin"]);
|
||||
|
||||
/* DAS TEAM SIEHT DEN TREFF MIT -- als eigene Gruppe unter seinen
|
||||
uebrigen Kacheln.
|
||||
|
||||
Filipe: "die modis rechte hand und ich also dogfather sehen die aber
|
||||
auch. die community aber nur die dann."
|
||||
|
||||
Zusammengesetzt und nicht in MODI_BEREICHE hineingeschrieben, weil
|
||||
diese Liste weiter oben steht als die Kacheln des Treffs. Eine Kopie
|
||||
dort waere die Stelle, an der beim naechsten neuen Brett eines fehlt. */
|
||||
const MODI_MIT_TREFF = [...MODI_BEREICHE, ...TREFF_BEREICHE, TREFF_MODERATION_KACHEL];
|
||||
const HAND_MIT_TREFF = [...HAND_BEREICHE, ...TREFF_BEREICHE, TREFF_MODERATION_KACHEL];
|
||||
|
||||
/* AUSSEN_ROLLEN kommt aus crew-adresse.js und wird hier nur
|
||||
weitergereicht -- die Adressregel braucht sie zuerst, und diese Datei
|
||||
importiert von dort. Eine zweite Menge waere die, die auseinanderlaeuft. */
|
||||
export { AUSSEN_ROLLEN };
|
||||
|
||||
export function bereicheFuer(person) {
|
||||
if (person?.rolle === "modi") return MODI_BEREICHE;
|
||||
if (person?.rolle === "hand") return HAND_BEREICHE;
|
||||
if (person?.rolle === "modi") return MODI_MIT_TREFF;
|
||||
if (person?.rolle === "hand") return HAND_MIT_TREFF;
|
||||
/* DIE COMMUNITY SIEHT DEN TREFF UND SONST NICHTS (11.09.2026). */
|
||||
if (person?.rolle === "gast") return TREFF_BEREICHE;
|
||||
/* AUF DER ADRESSE VON TEAM DOGI SIEHT AUCH DOGFATHER DIE KACHELN DES
|
||||
TEAMS (10.09.2026).
|
||||
|
||||
@@ -653,10 +780,27 @@ export function bereicheFuer(person) {
|
||||
Kacheln. Eine eigene Liste fuer ihn waere die, die beim naechsten
|
||||
Umbau auseinanderlaeuft -- und sie stuende gegen die Hausregel,
|
||||
ihn nicht ueber sein Team zu stellen. */
|
||||
if (person?.haus === "crew") return HAND_BEREICHE;
|
||||
return null;
|
||||
if (person?.haus === "crew") return HAND_MIT_TREFF;
|
||||
/* `null` HEISST "NIMM DIE LISTE AUS DEM BROWSER" -- und die enthaelt
|
||||
fuenfundzwanzig Kacheln des Agenturhauses. Bis zum 11.09.2026 stand
|
||||
hier nur `return null`, also bekam JEDE Rolle ohne eigenen Zweig
|
||||
diese Liste angeboten. Gefiltert wurde sie danach zwar nach
|
||||
`rollen:` an jeder Kachel, aber das ist eine zweite Schranke an
|
||||
einer anderen Stelle -- und sie lebt in einer Datei, die jeder
|
||||
herunterlaedt.
|
||||
|
||||
Jetzt bekommen nur die fuenf Rollen `null`, fuer die diese Liste
|
||||
geschrieben wurde. Alles andere bekommt eine leere Liste: keine
|
||||
Kachel, kein Weg, nichts zu filtern. */
|
||||
if (AGENTUR_LISTE.has(person?.rolle)) return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Die fuenf, deren Kacheln in assets/js/bereiche.js stehen. Nicht zu
|
||||
verwechseln mit AGENTUR_ROLLEN (dort fehlt `admin`, weil es dabei um
|
||||
die Sichtbarkeit von Zeilen geht und nicht um Kacheln). */
|
||||
const AGENTUR_LISTE = new Set(["spicy", "admin", "manager", "scout", "creator"]);
|
||||
|
||||
/* WELCHE BEREICHSSEITEN EIN MODI OEFFNEN DARF -- abgeleitet aus seinen
|
||||
eigenen Kacheln und nicht danebengeschrieben.
|
||||
|
||||
@@ -670,7 +814,7 @@ export function bereicheFuer(person) {
|
||||
-- ohne diese Einschraenkung kaeme er ueber die Adresszeile auch in
|
||||
die Agentur-Ablage, die ihn nichts angeht. */
|
||||
export const MODI_BEREICHE_ERLAUBT = new Set(
|
||||
HAND_BEREICHE
|
||||
HAND_MIT_TREFF
|
||||
.map((k) => (/bereich\.html\?b=([a-z]+)/.exec(k.ziel) || [])[1])
|
||||
.filter(Boolean));
|
||||
|
||||
@@ -942,8 +1086,51 @@ const ZUSATZ_BEREICHE = {
|
||||
/* Eine eigene Gruppe, direkt hinter Team Dogi: Was ueber Menschen
|
||||
aufgeschrieben wird, steht nicht zwischen den taeglichen
|
||||
Werkzeugen. */
|
||||
/* WER SIEHT WAS (11.09.2026).
|
||||
|
||||
Filipe: "egal welche Rolle oder Person hinzugefuegt wird, soll
|
||||
immer nur das sehen, was ich erlaube."
|
||||
|
||||
Dazu gehoert, dass er es nachsehen kann. Die Seite rechnet die
|
||||
Uebersicht aus derselben Tafel, aus der die Schranke ihre
|
||||
Entscheidung holt (rechte.js) -- eine von Hand gepflegte
|
||||
Uebersicht waere eine zweite Wahrheit, und ausgerechnet die, der
|
||||
man glaubt.
|
||||
|
||||
IN "Team & System" UND NICHT BEI DEN PERSONEN: Es geht nicht um
|
||||
Menschen, sondern um das Haus. */
|
||||
{
|
||||
...TEAM_GRUPPE,
|
||||
name: "Wer sieht was", unter: "Jede Rolle, jede Seite – ausgerechnet, nicht notiert",
|
||||
/* EIN EIGENER TON, UND ZWAR EIN NEUER (11.09.2026).
|
||||
|
||||
Erster Anlauf war Ton 26 -- das waere "Entwicklung" ein zweites
|
||||
Mal gewesen. Zweiter Anlauf war Ton 22, weil er frei AUSSAH:
|
||||
Ich hatte die Toene ueber bereicheFuer() gezaehlt, und das
|
||||
liefert die Zusatzkacheln gar nicht mit. Beide Male haette
|
||||
sich diese Kachel eine Farbe mit einer anderen geteilt, und
|
||||
beide Male haette ich es fuer geprueft gehalten.
|
||||
|
||||
Gefunden hat es pruef-start-ansicht, die ueber die ECHTE
|
||||
Ansicht zaehlt: "34 Farben auf 35 Kacheln". Der 35. Ton ist
|
||||
jetzt gemessen (start.css, Abstand 31,4 in Lab zum naechsten
|
||||
Nachbarn) -- und nicht mehr aus einer Luecke geraten. */
|
||||
zeichen: "schutz", ton: 35, ziel: "rechte.html", szene: "studio",
|
||||
},
|
||||
{ ...ENTWICKLUNG_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
|
||||
{ ...TALENTE_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
|
||||
/* DER TREFF -- GANZ UNTEN, UND ZWAR ABSICHTLICH.
|
||||
|
||||
Filipe zu den beiden Gruppen darueber: "das sind private
|
||||
informationen von meiner community." Dasselbe gilt hier, nur
|
||||
noch deutlicher: Im Treff stehen Namen und Saetze von Leuten,
|
||||
die nichts mit der Agentur zu tun haben.
|
||||
|
||||
Ohne `gruppeNach` haengt eine Gruppe hinten an -- genau richtig.
|
||||
Wer sich den Bildschirm teilt oder jemandem ueber die Schulter
|
||||
schauen laesst, hat sie dann nicht als Erstes im Blick. */
|
||||
...TREFF_BEREICHE,
|
||||
TREFF_MODERATION_KACHEL,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -2086,6 +2273,11 @@ function umstellungen(d) {
|
||||
["spicy", "admin", "manager", "scout", "creator", "modi"], jetztStempel);
|
||||
checkListeErweitern(d, "personen", "rolle", "hand",
|
||||
["spicy", "admin", "manager", "scout", "creator", "modi", "hand"], jetztStempel);
|
||||
/* DIE COMMUNITY (11.09.2026). Dieselbe Umstellung wie die drei
|
||||
darueber: Sicherung vorher, Neubau, Zaehlung innerhalb der
|
||||
Transaktion, Indizes zurueck. */
|
||||
checkListeErweitern(d, "personen", "rolle", "gast",
|
||||
["spicy", "admin", "manager", "scout", "creator", "modi", "hand", "gast"], jetztStempel);
|
||||
|
||||
/* DER BEREICH FUER DAS IDEEN-BOARD (10.09.2026, Kapitel 5.6).
|
||||
|
||||
@@ -2118,6 +2310,18 @@ function umstellungen(d) {
|
||||
checkListeErweitern(d, "eintraege", "bereich", "talente",
|
||||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||||
"angebote", "rueckmeldung", "entwicklung", "talente"], jetztStempel);
|
||||
/* DIE SIEBEN BRETTER DES TREFFS (11.09.2026).
|
||||
|
||||
EINE Umstellung fuer alle sieben, nicht sieben einzelne. Jeder
|
||||
Aufruf baut die Tabelle neu auf -- siebenmal hintereinander waeren
|
||||
sieben Sicherungen, sieben Neubauten und sieben Gelegenheiten, dass
|
||||
etwas schiefgeht. Der Marker ist "treff"; ist der schon in der
|
||||
CHECK-Liste, passiert gar nichts. */
|
||||
checkListeErweitern(d, "eintraege", "bereich", "treff",
|
||||
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
|
||||
"angebote", "rueckmeldung", "entwicklung", "talente",
|
||||
"anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln", "mitmachen"],
|
||||
jetztStempel);
|
||||
|
||||
checkListeErweitern(d, "eintraege", "status", "angenommen",
|
||||
["offen", "erledigt", "angenommen", "abgelehnt"], jetztStempel);
|
||||
@@ -2644,6 +2848,10 @@ export function db() {
|
||||
geaendert TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id);
|
||||
/* Die Tabellen des Treffs folgen weiter unten, gleich nachdem
|
||||
dieser Bauplan durch ist -- sie haengen an eintraege und
|
||||
personen und koennen deshalb nicht davor stehen. Ihr SQL steht
|
||||
in treff-tabellen.js, samt Begruendung je Tabelle. */
|
||||
|
||||
/* Wer darf welche Datei sehen. Bisher hatte eine Datei genau
|
||||
EINEN Bereich (dateien.creator_id) -- damit liess sich eine
|
||||
@@ -2889,6 +3097,11 @@ export function db() {
|
||||
CREATE INDEX IF NOT EXISTS idx_versuche_ip ON versuche (ip, zeitpunkt);
|
||||
CREATE INDEX IF NOT EXISTS idx_sitzungen_gueltig ON sitzungen (gueltig_bis);
|
||||
`);
|
||||
/* Die Tabellen des Treffs. Eigene Datei, weil workspace-treff.js
|
||||
aus DIESER Datei importiert -- ein Importkreis waere die Folge,
|
||||
und ueber einen Kreis kommen Konstanten als undefined an, ohne
|
||||
dass irgendwo ein Fehler erscheint. */
|
||||
treffTabellen(d);
|
||||
umstellungen(d);
|
||||
_db = d;
|
||||
return d;
|
||||
@@ -3074,7 +3287,34 @@ export function sitzungLesen(req) {
|
||||
nicht, was er darf -- genau wie die Sicht eines anderen
|
||||
(sichtPerson) das auch nicht tut. Wer hier etwas anderes einbaut,
|
||||
macht aus einer Anzeigefrage eine Rechtefrage. */
|
||||
const haus = istCrewAdresse(req.get?.("host")) ? "crew" : "agentur";
|
||||
const haus = istCrewAdresse(req.get?.("host")) ? "crew"
|
||||
: istTreffAdresse(req.get?.("host")) ? "treff" : "agentur";
|
||||
|
||||
/* DER AUSSCHLUSS WIRKT HIER -- an derselben Stelle wie alles andere
|
||||
(11.09.2026).
|
||||
|
||||
Ein Ausschluss aus dem Treff ist nicht "darf nichts mehr
|
||||
schreiben", sondern "ist nicht mehr da". Stuende die Pruefung an
|
||||
den Schreibwegen, koennte ein Ausgeschlossener weiter mitlesen --
|
||||
und genau das ist bei Belaestigung der Teil, der weh tut.
|
||||
|
||||
AN DIESER STELLE UND NICHT NUR BEIM ANMELDEN: Wer schon
|
||||
angemeldet ist, hat ein Sitzungsplaetzchen, das Tage gilt. Eine
|
||||
Pruefung beim Anmelden haette ihn bis zum Ablauf drin gelassen --
|
||||
also ausgerechnet in den Stunden, in denen die Entscheidung
|
||||
gefallen ist.
|
||||
|
||||
NUR FUER AUSSENROLLEN: Eine Massnahme gegen jemanden aus dem Team
|
||||
gibt es nicht (siehe /api/treff/massnahme), und ein Fehler hier
|
||||
duerfte niemals das Team aussperren. Die Frage wird deshalb gar
|
||||
nicht erst gestellt.
|
||||
|
||||
WIRFT DIE FRAGE, greift der catch weiter unten und die Antwort
|
||||
ist "null" -- also "nicht angemeldet". Das ist die sichere
|
||||
Richtung: Wer nicht nachsehen kann, laesst niemanden herein. */
|
||||
if (AUSSEN_ROLLEN.has(reihe.rolle) && treffSperreLesen(db(), reihe.id)?.art === "ausschluss") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { id: reihe.id, name: reihe.name, rolle: reihe.rolle, haus };
|
||||
} catch { return null; }
|
||||
@@ -3131,7 +3371,15 @@ export const workspaceRouter = express.Router();
|
||||
/workspace/ -- und von dort wieder hierher. Der Browser bricht das
|
||||
nach ein paar Runden mit "zu viele Weiterleitungen" ab, und man
|
||||
sucht den Fehler ueberall, nur nicht in dieser Zeile. */
|
||||
const OFFEN = new Set(["/workspace/index.html", "/workspace/crew-index.html"]);
|
||||
/* SIE WIRD NICHT ZWEIMAL GESCHRIEBEN (11.09.2026).
|
||||
Bis heute stand hier dieselbe Liste ein zweites Mal, von Hand. Sie
|
||||
stimmte -- weil sie zwei Eintraege hatte und beide am selben Tag
|
||||
entstanden. Beim dritten (der Wand des Treffs) waere genau das
|
||||
passiert, wogegen die Rechtetafel gebaut wurde: Wer nur hier
|
||||
nachtraegt, bekommt eine Wand, die die Pruefung als Luecke meldet;
|
||||
wer nur dort nachtraegt, bekommt eine Endlosweiterleitung. Beides
|
||||
faellt erst im Browser auf. Jetzt gibt es die Liste einmal. */
|
||||
const OFFEN = new Set(OHNE_ANMELDUNG);
|
||||
|
||||
/** Der Pfad, wie ihn die Schranke ansehen muss.
|
||||
*
|
||||
@@ -3241,6 +3489,58 @@ function stillerZugang(code) {
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
WER SIEHT WAS — die Tafel, wie sie wirklich ist (11.09.2026)
|
||||
=====================================================================
|
||||
|
||||
Filipe: *"egal welche Rolle oder Person hinzugefuegt wird, soll immer
|
||||
nur das sehen, was ich erlaube."*
|
||||
|
||||
Dazu gehoert, dass er es NACHSEHEN kann. Eine Regel, die man nur
|
||||
glauben kann, ist eine Zusage; eine, die man ansehen kann, ist eine
|
||||
Auskunft.
|
||||
|
||||
DIESE ANTWORT WIRD GERECHNET, NICHT GEPFLEGT. Sie kommt aus
|
||||
derselben Tafel, aus der auch die Schranke ihre Entscheidung holt
|
||||
(rechte.js). Eine von Hand geschriebene Uebersicht waere eine zweite
|
||||
Wahrheit — und ausgerechnet die, der man glaubt, weil sie wie eine
|
||||
Auskunft aussieht. Sie wuerde altern, und man wuerde es ihr nicht
|
||||
ansehen.
|
||||
|
||||
NUR DogFather. Nicht aus Geheimniskraemerei, sondern weil diese
|
||||
Antwort eine vollstaendige Karte des Hauses ist: jede Seite, jede
|
||||
Rolle, und wo die Luecken sind. Wer sie hat, weiss, wo er es
|
||||
versuchen muesste.
|
||||
===================================================================== */
|
||||
workspaceRouter.get("/workspace/api/rechte/tafel", (req, res) => {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
if (person.rolle !== "admin") return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const seiten = Object.entries(SEITEN).map(([pfad, rollen]) => ({
|
||||
pfad,
|
||||
datei: pfad.replace("/workspace/", ""),
|
||||
rollen: [...rollen],
|
||||
})).sort((a, b) => a.datei.localeCompare(b.datei, "de"));
|
||||
|
||||
res.json({
|
||||
rollen: ALLE_ROLLEN.map((r) => ({
|
||||
schluessel: r,
|
||||
name: ROLLEN_NAME[r] ?? r,
|
||||
/* Wie viele der Seiten diese Rolle oeffnen darf. Die Zahl ist
|
||||
die eigentliche Antwort auf "sieht die neue Rolle zu viel?" */
|
||||
seiten: seiten.filter((s) => s.rollen.includes(r)).length,
|
||||
})),
|
||||
seiten,
|
||||
ohne_anmeldung: [...OHNE_ANMELDUNG],
|
||||
/* Die Zahl, die den Unterschied macht: Wie viele Seiten stehen
|
||||
fuer NIEMANDEN offen, ohne dass jemand das entschieden haette?
|
||||
Seit dem 11.09. ist sie immer 0 — es gibt keinen Zustand mehr,
|
||||
der "alle angemeldeten" bedeutet, ohne dass es dasteht. */
|
||||
ohne_eintrag: 0,
|
||||
});
|
||||
});
|
||||
|
||||
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||||
const ip = echteIp(req);
|
||||
try {
|
||||
@@ -3313,9 +3613,25 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||||
zwar auch fuer die neue Adresse, denn die Sperre haengt an der
|
||||
IP, nicht am Hostnamen. Das ist der Preis der Stille; er faellt
|
||||
nur an, wenn jemand die alte Adresse mehrfach probiert. */
|
||||
const still = codeBrauchbar && !istOhneModiAdresse(req.get("host"))
|
||||
? stillerZugang(code)
|
||||
: null;
|
||||
/* UND AUCH NICHT AUF DER WAND DES TREFFS (11.09.2026).
|
||||
|
||||
Hier stand `!istOhneModiAdresse(...)` -- also "ueberall ausser auf
|
||||
den alten Adressen". Als dritte Wand dazukam, hiess das
|
||||
stillschweigend auch: dort. Ein Modi kam damit ueber den
|
||||
verborgenen Weg in den Treff herein, obwohl die Kachelliste dort
|
||||
nur `gast` kennt und die Adressregel ihn abweist.
|
||||
|
||||
Gefunden hat das nicht das Lesen, sondern eine Zeile in
|
||||
pruef-treff, die das GEGENTEIL behauptete und rot wurde:
|
||||
"auf treff. kommt sonst niemand herein -- auch DogFather nicht
|
||||
(401/200)". DogFather wurde abgewiesen, der Modi nicht.
|
||||
|
||||
Die Lehre ist dieselbe wie bei `null` in der Rechtetafel: Eine
|
||||
Bedingung, die durch AUSSCHLIESSEN formuliert ist, nimmt jede
|
||||
neue Moeglichkeit automatisch mit auf. Jetzt steht dort, WO der
|
||||
Weg gilt, statt wo er nicht gilt. */
|
||||
const stilleWand = istCrewAdresse(req.get("host")) || istPruefAdresse(req.get("host"));
|
||||
const still = codeBrauchbar && stilleWand ? stillerZugang(code) : null;
|
||||
if (still) {
|
||||
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
|
||||
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), still.id);
|
||||
@@ -3360,7 +3676,9 @@ 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 = istCrewAdresse(req.get("host")) ? CREW_KACHEL : ROLLEN_KACHEL;
|
||||
const kachelSatz = istTreffAdresse(req.get("host")) ? TREFF_KACHEL
|
||||
: istCrewAdresse(req.get("host")) ? CREW_KACHEL
|
||||
: ROLLEN_KACHEL;
|
||||
|
||||
if (!kachelSatz.has(rolle) || !codeBrauchbar) {
|
||||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||||
@@ -3417,6 +3735,33 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||||
return res.status(401).json({ fehler: "ungueltig" });
|
||||
}
|
||||
|
||||
/* AUSGESCHLOSSEN HEISST: DER CODE STIMMT NICHT MEHR (11.09.2026).
|
||||
|
||||
sitzungLesen() weist eine ausgeschlossene Person ohnehin ab --
|
||||
jede Anfrage bekommt 401. Ohne diese Zeile hier waere die
|
||||
Anmeldung aber TROTZDEM gelungen: Plaetzchen gesetzt, "willkommen"
|
||||
geantwortet, Weiterleitung auf die Startseite -- und dort dann
|
||||
eine Seite, auf der nichts geht. Das sieht nicht aus wie eine
|
||||
Entscheidung, sondern wie ein kaputtes Programm, und man
|
||||
probiert es zehnmal.
|
||||
|
||||
DIESELBE ANTWORT WIE BEI EINEM FALSCHEN CODE, und zwar
|
||||
wortgleich: Wer ausgeschlossen wurde, hat den Grund bereits
|
||||
bekommen (die Massnahme verlangt einen). Die Anmeldeseite muss
|
||||
ihn nicht wiederholen -- und sie darf niemandem, der einen
|
||||
fremden Code probiert, verraten, dass es diesen Zugang gibt.
|
||||
|
||||
NUR FUER AUSSENROLLEN. Gegen jemanden aus dem Team gibt es diese
|
||||
Massnahme gar nicht (siehe /api/treff/massnahme); die Frage
|
||||
wird deshalb nicht gestellt. Ein Fehler hier duerfte niemals
|
||||
das Team aussperren. */
|
||||
if (AUSSEN_ROLLEN.has(gefunden.rolle)
|
||||
&& treffSperreLesen(db(), gefunden.id)?.art === "ausschluss") {
|
||||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||||
protokolliere("anmeldung_gesperrt_treff", { personId: gefunden.id, rolle, ip });
|
||||
return res.status(401).json({ fehler: "ungueltig" });
|
||||
}
|
||||
|
||||
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
|
||||
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), gefunden.id);
|
||||
sitzungSetzen(res, gefunden, req);
|
||||
@@ -4593,10 +4938,43 @@ const nurCreatorIds = (ids) => {
|
||||
} catch { return ids; }
|
||||
};
|
||||
|
||||
export const sichtbareCreatorIds = (person) => nurCreatorIds(nurHaus(ohneVerborgene(sichtbareCreatorIdsRoh(person), person), person));
|
||||
export const sichtbarePersonenIds = (person) => nurHaus(ohneVerborgene(sichtbarePersonenIdsRoh(person), person), person);
|
||||
export const betreuerIds = (person) => nurHaus(ohneVerborgene(betreuerIdsRoh(person), person), person);
|
||||
export const einladbareIds = (person) => nurHaus(ohneVerborgene(einladbareIdsRoh(person), person), person);
|
||||
/* WER VON AUSSEN KOMMT, SIEHT NIEMANDEN (11.09.2026).
|
||||
|
||||
Filipe: "egal welche rolle oder person hinzugefuegt wird soll immer
|
||||
nur das sehen wass ich erlaube."
|
||||
|
||||
GEFUNDEN, NICHT UEBERLEGT. Eine Zeile in pruef-treff behauptete
|
||||
"Personen: nichts" und wurde rot. Nachgemessen bekam ein Gast:
|
||||
|
||||
{"personen":[{"id":1,"name":"Filipe","rolle":"admin", ...},
|
||||
{"id":5,"name":"Kessi","rolle":"gast", ...}]}
|
||||
|
||||
Also DogFathers Namen und seine Rolle. Nicht dramatisch -- ein
|
||||
Streamer ist kein Geheimnis -- aber es war eine Auskunft, die
|
||||
niemand erlaubt hatte, und genau das ist der Punkt. Die Liste kam
|
||||
zustande, weil `sichtbarePersonenIdsRoh` fuer jede Rolle DogFather
|
||||
mitgibt (er ist das Gegenueber von allem) und die vier Umhuellungen
|
||||
danach nichts daran aendern.
|
||||
|
||||
`nurEigene` steht deshalb GANZ AUSSEN -- nach allen anderen. Wer
|
||||
innen ansetzt, muss darauf vertrauen, dass keine spaetere Huelle
|
||||
wieder etwas hinzufuegt; aussen gilt es unabhaengig davon, was
|
||||
innen passiert. Und es gilt fuer alle vier Listen gleich, statt
|
||||
viermal einzeln.
|
||||
|
||||
`null` heisst bei diesen Funktionen "alle" -- deshalb wird es hier
|
||||
ausdruecklich zu einer Liste mit genau einem Eintrag. Ein `null`
|
||||
durchzureichen hiesse fuer eine Aussenrolle das Gegenteil von dem,
|
||||
was hier steht. */
|
||||
function nurEigene(ids, person) {
|
||||
if (!AUSSEN_ROLLEN.has(person?.rolle)) return ids;
|
||||
return person?.id ? [person.id] : [];
|
||||
}
|
||||
|
||||
export const sichtbareCreatorIds = (person) => 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);
|
||||
/* SICH SELBST NICHT -- UND ZWAR AUCH DANN NICHT, WENN DIE LISTE
|
||||
AUSGESCHRIEBEN WIRD (10.09.2026).
|
||||
|
||||
|
||||
@@ -61,6 +61,35 @@ const SZENE = join(QUELLE, "gate-spicy-dogfather.jpeg");
|
||||
const DOGI = join(QUELLE, "dogfather-husky.png");
|
||||
const ZIEL = process.env.BUEHNE_ZIEL || join(WURZEL, "workspace", "assets", "img", "buehne");
|
||||
|
||||
/* ZWEI TUEREN, EIN WERKZEUG (11.09.2026).
|
||||
|
||||
Der Treff bekommt seine eigene Zugangswand, und sie braucht ihr
|
||||
eigenes Motiv -- nicht weil sie anders aussehen soll, sondern weil
|
||||
in diesem Bild "TEAM DOGI" EINGEBRANNT ist. Die Community traegt
|
||||
diesen Namen nicht; auf ihrer Tuer stuende sonst der Name eines
|
||||
Bereichs, zu dem sie nicht gehoert.
|
||||
|
||||
WARUM KEINE ZWEITE DATEI: Eine Kopie dieser 520 Zeilen haette die
|
||||
drei Gegenproben mitkopiert -- und beim naechsten Nachbessern waere
|
||||
genau eine der beiden verbessert worden. Hier aendern sich zwei
|
||||
Dinge, also sind es zwei Werte und kein zweites Werkzeug.
|
||||
|
||||
DIE FARBE BLEIBT DIESELBE, und das ist eine Entscheidung, keine
|
||||
Bequemlichkeit: Gemessen liegt der naechstbeste eigene Farbton
|
||||
(Tannengruen) 60 Lab-Einheiten vom Marineblau entfernt -- er waere
|
||||
also klar unterscheidbar gewesen. Nur haette der Treff damit wie ein
|
||||
DRITTES Haus ausgesehen, neben Spicy Media und Team Dogi. Er ist
|
||||
aber kein drittes Haus, sondern eine zweite Tuer in demselben:
|
||||
"lila und babyblau ueberall" (Filipe, 10.09.2026). Unterschieden
|
||||
werden die Tueren durch das WORT darauf, nicht durch die Farbe --
|
||||
so, wie man es in einem Gebaeude auch macht.
|
||||
|
||||
node tools/crew-buehne.mjs
|
||||
WAND_TEXT="DER TREFF" WAND_PRAEFIX=treff-gate node tools/crew-buehne.mjs
|
||||
*/
|
||||
const WAND_TEXT = process.env.WAND_TEXT || "TEAM DOGI";
|
||||
const WAND_PRAEFIX = process.env.WAND_PRAEFIX || "crew-gate";
|
||||
|
||||
for (const q of [SZENE, DOGI]) {
|
||||
if (!existsSync(q)) { console.error(`Fehlt: ${q}`); process.exit(1); }
|
||||
}
|
||||
@@ -295,14 +324,14 @@ async function bauen({ breite, ausschnitt = null, guete = 0.92, dunkel = 0.08,
|
||||
g.restore();
|
||||
}
|
||||
|
||||
/* ---- 5. "TEAM DOGI" ------------------------------------------- */
|
||||
/* ---- 5. Der Name auf der Tuer --------------------------------- */
|
||||
const schriftHoehe = Math.round(zh * 0.072);
|
||||
g.save();
|
||||
g.textAlign = "center";
|
||||
g.textBaseline = "alphabetic";
|
||||
g.font = `800 ${schriftHoehe}px "Segoe UI", system-ui, sans-serif`;
|
||||
/* 0,955 lag halb unter dem Bildrand -- im Bildschirmfoto war von
|
||||
"TEAM DOGI" die untere Haelfte abgeschnitten. Jetzt so weit
|
||||
dem Namen die untere Haelfte abgeschnitten. Jetzt so weit
|
||||
herauf, dass die Unterlaengen noch auf dem Wasser stehen. */
|
||||
const ty = zh * 0.915;
|
||||
/* Erst ein kalter Schein darunter, dann die Schrift in Metall --
|
||||
@@ -310,7 +339,7 @@ async function bauen({ breite, ausschnitt = null, guete = 0.92, dunkel = 0.08,
|
||||
g.shadowColor = "rgba(70,150,220,0.85)";
|
||||
g.shadowBlur = schriftHoehe * 0.9;
|
||||
g.fillStyle = "rgba(10,25,45,0.9)";
|
||||
g.fillText("TEAM DOGI", mx, ty);
|
||||
g.fillText(a.text, mx, ty);
|
||||
g.shadowBlur = 0;
|
||||
const metall = g.createLinearGradient(0, ty - schriftHoehe, 0, ty);
|
||||
metall.addColorStop(0, "#ffffff");
|
||||
@@ -318,7 +347,7 @@ async function bauen({ breite, ausschnitt = null, guete = 0.92, dunkel = 0.08,
|
||||
metall.addColorStop(0.55, "#7ea6c6");
|
||||
metall.addColorStop(1, "#e8f2fa");
|
||||
g.fillStyle = metall;
|
||||
g.fillText("TEAM DOGI", mx, ty);
|
||||
g.fillText(a.text, mx, ty);
|
||||
g.restore();
|
||||
|
||||
/* ---- 6. Vignette und ein Hauch Dunkel ------------------------- */
|
||||
@@ -364,7 +393,7 @@ async function bauen({ breite, ausschnitt = null, guete = 0.92, dunkel = 0.08,
|
||||
dogiKasten,
|
||||
};
|
||||
}, { szene: szeneDaten, dogi: dogiDaten, breite, ausschnitt, guete, dunkel, vignette,
|
||||
ohneDogi, tafel: TAFEL });
|
||||
ohneDogi, tafel: TAFEL, text: WAND_TEXT });
|
||||
|
||||
return { ...roh, puffer: Buffer.from(roh.bild.split(",")[1], "base64") };
|
||||
}
|
||||
@@ -439,17 +468,17 @@ let gebaut = 0;
|
||||
console.log("\n--- Quer ---");
|
||||
for (const b of [960, 1280, 1600, 1920, 2560]) {
|
||||
const r = await bauen({ breite: b, guete: 0.93, dunkel: 0.08, vignette: 0.26 });
|
||||
writeFileSync(join(ZIEL, `crew-gate-${b}.webp`), r.puffer);
|
||||
writeFileSync(join(ZIEL, `${WAND_PRAEFIX}-${b}.webp`), r.puffer);
|
||||
gebaut++;
|
||||
|
||||
if (r.rotAnteil > 0.01) maengel.push(
|
||||
`crew-gate-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte -- Umfaerbung hat nicht gegriffen`);
|
||||
`${WAND_PRAEFIX}-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte -- Umfaerbung hat nicht gegriffen`);
|
||||
if (r.tafelMittel > 46) maengel.push(
|
||||
`crew-gate-${b}: die Tafel ist zu hell (${r.tafelMittel.toFixed(1)}) -- die Karte braucht einen ruhigen Grund`);
|
||||
`${WAND_PRAEFIX}-${b}: die Tafel ist zu hell (${r.tafelMittel.toFixed(1)}) -- die Karte braucht einen ruhigen Grund`);
|
||||
if (r.tafelStreuung > 34) maengel.push(
|
||||
`crew-gate-${b}: in der Tafel ist zu viel Muster (Streuung ${r.tafelStreuung.toFixed(1)})`);
|
||||
`${WAND_PRAEFIX}-${b}: in der Tafel ist zu viel Muster (Streuung ${r.tafelStreuung.toFixed(1)})`);
|
||||
|
||||
console.log(` crew-gate-${b}.webp`.padEnd(30)
|
||||
console.log(` ${WAND_PRAEFIX}-${b}.webp`.padEnd(30)
|
||||
+ `${(r.puffer.length / 1024).toFixed(0)} KB · ${r.breite}x${r.hoehe}`
|
||||
+ ` · Rot ${(r.rotAnteil * 100).toFixed(2)} %`
|
||||
+ ` · Tafel ${r.tafelMittel.toFixed(1)}/${r.tafelStreuung.toFixed(1)}`);
|
||||
@@ -471,11 +500,11 @@ for (const b of [440, 580, 900]) {
|
||||
ausschnitt: [0.13, 0.02, 0.32, 0.98], guete: 0.88,
|
||||
dunkel: 0.22, vignette: 0.30,
|
||||
});
|
||||
writeFileSync(join(ZIEL, `crew-gate-hoch-${b}.webp`), r.puffer);
|
||||
writeFileSync(join(ZIEL, `${WAND_PRAEFIX}-hoch-${b}.webp`), r.puffer);
|
||||
gebaut++;
|
||||
if (r.rotAnteil > 0.01) maengel.push(
|
||||
`crew-gate-hoch-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte`);
|
||||
console.log(` crew-gate-hoch-${b}.webp`.padEnd(30)
|
||||
`${WAND_PRAEFIX}-hoch-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte`);
|
||||
console.log(` ${WAND_PRAEFIX}-hoch-${b}.webp`.padEnd(30)
|
||||
+ `${(r.puffer.length / 1024).toFixed(0)} KB · ${r.breite}x${r.hoehe}`
|
||||
+ ` · Rot ${(r.rotAnteil * 100).toFixed(2)} %`);
|
||||
}
|
||||
|
||||
@@ -88,15 +88,32 @@ for (const name of readdirSync(ORDNER)) {
|
||||
Derselbe Stempel wie in den Seiten -- `pruef-zwischenspeicher`
|
||||
verlangt im ganzen Haus genau eine Zahl und wuerde eine zweite sofort
|
||||
melden. */
|
||||
/* ES SIND ZWEI MANIFESTE (10.09.2026). Seit die Modi-App unter ihrer
|
||||
eigenen Adresse laeuft, gibt es neben app.webmanifest auch
|
||||
crew.webmanifest -- dasselbe Haus, anderer Name, andere Symbole.
|
||||
/* DIE MANIFESTE WERDEN GEFUNDEN, NICHT AUFGEZAEHLT (11.09.2026).
|
||||
|
||||
ALS LISTE UND NICHT ALS ZWEITER BLOCK: Ein kopierter Block waere beim
|
||||
dritten Manifest ein dritter, und irgendwann bleibt einer stehen. Ein
|
||||
fehlendes Manifest ist hier ausserdem ein FEHLER und kein
|
||||
Ueberspringen -- sonst faellt das Ausbleiben nicht auf. */
|
||||
const MANIFESTE = ["app.webmanifest", "crew.webmanifest"];
|
||||
Hier stand eine Liste mit zwei Namen und daneben der Satz, warum eine
|
||||
Liste besser sei als ein kopierter Block. Beim DRITTEN Manifest
|
||||
(treff.webmanifest, fuer die Community) hat sich das genau so
|
||||
bewahrheitet, wie es vorausgesagt war -- nur eine Stufe weiter oben:
|
||||
Der kopierte Block kam nicht, aber der neue Name fehlte in der Liste.
|
||||
Das Manifest bekam keinen Stempel, und die Symbole der neuen App
|
||||
waeren beim naechsten Wechsel bis zu vier Stunden lang die alten
|
||||
geblieben. Kein Fehler, keine Warnung -- nur eine App mit dem
|
||||
falschen Zeichen.
|
||||
|
||||
Jetzt wird gesucht. Wer eine vierte Tuer baut, muss hier nichts mehr
|
||||
nachtragen; das Werkzeug findet sie.
|
||||
|
||||
NULL MANIFESTE SIND EIN FEHLER und kein Ueberspringen: Faende die
|
||||
Suche nichts (falscher Ordner, umbenannte Endung), liefe der Stempel
|
||||
gruen durch und haette nichts getan -- der gruene Haken ueber der
|
||||
Null, gegen den in diesem Haus die halbe Dokumentation steht. */
|
||||
const MANIFESTE = readdirSync(ORDNER)
|
||||
.filter((n) => n.endsWith(".webmanifest"))
|
||||
.sort();
|
||||
if (!MANIFESTE.length) {
|
||||
console.error(`FEHLER: kein einziges Manifest in ${ORDNER} gefunden.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const MUSTER_MANIFEST =
|
||||
/("src":\s*")(\/assets\/img\/app-symbole\/[A-Za-z0-9._-]+)(?:\?v=\d+)?(")/g;
|
||||
for (const name of MANIFESTE) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/* =====================================================================
|
||||
zeilenenden.mjs — gemischte Zeilenenden nachziehen (11.09.2026)
|
||||
|
||||
AUFRUF: node tools/zeilenenden.mjs <datei> [<datei> ...]
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WOZU
|
||||
|
||||
Die Dateien dieses Hauses sind gemischt: manche mit CRLF (Windows),
|
||||
manche mit LF. Wer eine davon mit einem Skript bearbeitet und dabei
|
||||
Zeilen mit "\n" einfügt, bekommt eine Datei mit ZWEI Sorten
|
||||
Zeilenenden.
|
||||
|
||||
Das ist kein Schönheitsfehler. Was dabei herauskommt:
|
||||
|
||||
* Die Datei läuft weiterhin -- JavaScript, CSS und HTML ist das
|
||||
Zeilenende gleichgültig. Es fällt also nicht auf.
|
||||
* `git diff` zeigt dann Zeilen als geändert, die niemand angefasst
|
||||
hat -- und in einem Änderungssatz, in dem 40 echte Zeilen stehen,
|
||||
gehen die echten zwischen 400 unechten unter. Genau dort wird
|
||||
eine Prüfung vor dem Ausliefern wertlos: Man sieht nicht mehr,
|
||||
was man verändert hat.
|
||||
* Beim nächsten Bearbeiten derselben Datei trifft ein Anker mit
|
||||
"\n" die Stellen mit "\r\n" nicht mehr -- der Patch meldet
|
||||
"Anker nicht gefunden", und man sucht den Fehler im Anker.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WAS ES TUT UND WAS AUSDRÜCKLICH NICHT
|
||||
|
||||
Es macht NUR gemischte Dateien einheitlich, und zwar auf das
|
||||
Zeilenende, das in dieser Datei ohnehin überwiegt. Es wandelt
|
||||
niemals eine saubere LF-Datei nach CRLF oder umgekehrt -- das wäre
|
||||
eine Änderung an jeder Zeile und damit genau der Schaden, den es
|
||||
verhindern soll.
|
||||
|
||||
Die drei Ausgänge, wie überall in diesem Haus:
|
||||
* rein CRLF oder rein LF -> "unveraendert" (nichts zu tun)
|
||||
* gemischt -> nachgezogen, mit Zahl
|
||||
* Datei nicht lesbar -> Fehler mit Namen, Rückgabewert 1
|
||||
(und nicht stillschweigend übersprungen)
|
||||
===================================================================== */
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
const dateien = process.argv.slice(2);
|
||||
if (!dateien.length) {
|
||||
console.error("Aufruf: node tools/zeilenenden.mjs <datei> [<datei> ...]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let fehler = 0;
|
||||
for (const p of dateien) {
|
||||
let s;
|
||||
try {
|
||||
s = fs.readFileSync(p, "utf8");
|
||||
} catch (f) {
|
||||
console.error(`${p.padEnd(38)} NICHT LESBAR: ${f?.message}`);
|
||||
fehler++;
|
||||
continue;
|
||||
}
|
||||
const crlf = (s.match(/\r\n/g) || []).length;
|
||||
const n = (s.match(/\n/g) || []).length;
|
||||
if (crlf === 0 || crlf === n) {
|
||||
console.log(p.padEnd(38), "unveraendert");
|
||||
continue;
|
||||
}
|
||||
/* Die Mehrheit gewinnt. Bei einer Datei, die zu 95 % CRLF ist und in
|
||||
die zwanzig LF-Zeilen eingefügt wurden, ist CRLF das Richtige --
|
||||
und umgekehrt genauso. Der Blick auf die Mehrheit macht das
|
||||
Werkzeug richtungsunabhängig, statt CRLF zur Vorgabe zu erklären. */
|
||||
const nachCrlf = crlf * 2 >= n;
|
||||
s = nachCrlf
|
||||
? s.replace(/(?<!\r)\n/g, "\r\n")
|
||||
: s.replace(/\r\n/g, "\n");
|
||||
fs.writeFileSync(p, s);
|
||||
console.log(p.padEnd(38),
|
||||
`${nachCrlf ? n - crlf : crlf} Zeilen auf ${nachCrlf ? "CRLF" : "LF"} nachgezogen`);
|
||||
}
|
||||
process.exit(fehler ? 1 : 0);
|
||||
@@ -13,25 +13,25 @@
|
||||
"theme_color": "#06090f",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/img/app-symbole/workspace-192.png?v=202609111617",
|
||||
"src": "/assets/img/app-symbole/workspace-192.png?v=202609111736",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/workspace-512.png?v=202609111617",
|
||||
"src": "/assets/img/app-symbole/workspace-512.png?v=202609111736",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609111617",
|
||||
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609111736",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609111617",
|
||||
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609111736",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
|
||||
@@ -46,6 +46,68 @@
|
||||
}
|
||||
.eintrag-knoepfe { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; }
|
||||
|
||||
/* =====================================================================
|
||||
DER TREFF (11.09.2026)
|
||||
|
||||
Drei Zusaetze, und nur auf den sieben Brettern der Community.
|
||||
|
||||
SIE STEHEN IN EINER EIGENEN ZEILE UNTER DEM FUSS und nicht darin.
|
||||
Der Fuss ist eine Auskunft (Datum, wer, Stand); hier stehen Dinge,
|
||||
die man TUT. Zusammen in einer Reihe waere "von Frida" so breit wie
|
||||
"Will ich auch" -- und auf einem Handy laege beides ineinander.
|
||||
===================================================================== */
|
||||
.treff-knoepfe {
|
||||
display: flex; gap: 7px; flex-wrap: wrap; align-items: center;
|
||||
margin-top: 10px; padding-top: 10px;
|
||||
border-top: 1px solid var(--rand);
|
||||
}
|
||||
|
||||
/* Der Rollenname am Beitrag. Leise, aber nicht versteckt: Er soll
|
||||
beim Lesen auffallen, ohne den Beitrag zu ueberschreiben. Deshalb
|
||||
dieselbe Groesse wie der Fuss und nur ein kraeftigerer Ton. */
|
||||
.treff-rolle {
|
||||
color: var(--text-leise);
|
||||
font-weight: 600;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
|
||||
/* "Will ich auch". Der einzige Knopf auf diesen Brettern, der keine
|
||||
Verwaltung ist, sondern eine Meinung -- er darf deshalb als
|
||||
einziger einen Ton tragen.
|
||||
|
||||
GEDRUECKT WIRD ER RUHIGER, NICHT LAUTER. Der naheliegende Entwurf
|
||||
waere ein kraeftiges Leuchten fuer "ich bin dabei". Nur sieht man
|
||||
auf einer Wunschliste meist ZEHN Karten gleichzeitig, und wer bei
|
||||
der Haelfte mitgestimmt hat, saehe zehn leuchtende Flaechen
|
||||
untereinander. Das ist genau das, was diese Seite nicht sein soll
|
||||
(Hausregel: augenschonend). Der gedrueckte Zustand bekommt deshalb
|
||||
eine gefuellte Flaeche in gedecktem Ton, der ungedrueckte bleibt
|
||||
ein Umriss -- unterscheidbar, ohne zu blenden. */
|
||||
.treff-will {
|
||||
font: inherit; font-size: .8rem; font-weight: 600;
|
||||
padding: 6px 12px; border-radius: 999px; cursor: pointer;
|
||||
color: var(--text-leise);
|
||||
background: transparent;
|
||||
border: 1px solid var(--rand-hell);
|
||||
transition: background .16s ease, color .16s ease, border-color .16s ease;
|
||||
}
|
||||
.treff-will:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
border-color: color-mix(in srgb, var(--akzent) 45%, transparent);
|
||||
}
|
||||
.treff-will[aria-pressed="true"] {
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--akzent) 14%, transparent);
|
||||
border-color: color-mix(in srgb, var(--akzent) 42%, transparent);
|
||||
}
|
||||
.treff-will:disabled { opacity: .55; cursor: default; }
|
||||
|
||||
/* Dieselbe Ruecksicht wie ueberall: Wer Bewegung abbestellt hat,
|
||||
bekommt keine. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.treff-will { transition: none; }
|
||||
}
|
||||
|
||||
.filter { display: flex; gap: 8px; margin: 0 0 20px; flex-wrap: wrap; }
|
||||
.filter .schritt[aria-pressed="true"] {
|
||||
/* Gedrueckt = der "voll"-Auftritt des Knopf-Systems. Vorher stand
|
||||
|
||||
@@ -1030,6 +1030,68 @@ body.start > main { position: relative; z-index: 1; }
|
||||
[data-ton="26"] { --ton: #7fd4a8; } /* Entwicklung */
|
||||
[data-ton="27"] { --ton: #8f7bd6; } /* Talente */
|
||||
|
||||
/* =====================================================================
|
||||
DIE SIEBEN TOENE DES TREFFS (11.09.2026)
|
||||
|
||||
Bei 27 Kacheln war der Farbkreis voll -- das stand seit dem 11.09. als
|
||||
Notiz im Code. Sieben weitere von Hand zu waehlen haette Toene
|
||||
ergeben, die sich nur noch der Messung nach unterscheiden.
|
||||
|
||||
Sie sind deshalb GERECHNET: ein Gitter aus Farbton, Saettigung und
|
||||
Helligkeit, umgerechnet in den Lab-Raum (dort entspricht Abstand dem,
|
||||
was das Auge als Unterschied sieht), und daraus gierig die sieben mit
|
||||
dem groessten kleinsten Abstand zu allen 27 vorhandenen UND
|
||||
zueinander.
|
||||
|
||||
Zwei Bereiche sind ausgeschlossen, und beide aus einem Grund:
|
||||
* ROT (unter 22 und ueber 338 Grad) gehoert Spicy Media. Eine
|
||||
Community-Kachel in deren Farbe waere ein falsches Signal.
|
||||
* NEON (Buntheit ueber 62) verstoesst gegen die Hausregel
|
||||
"augenschonend". Dasselbe gilt nach unten: unter 24 wird es grau,
|
||||
und grau liest sich als "abgeschaltet".
|
||||
|
||||
Ergebnis: schwaechster Abstand 24,9 -- zum Vergleich lag eine von
|
||||
Hand gewaehlte Farbfamilie im selben Blau bei 12,6. Die Rechnung war
|
||||
also nicht Genauigkeitsspielerei, sondern der Unterschied zwischen
|
||||
unterscheidbar und nicht. */
|
||||
[data-ton="28"] { --ton: #8e784f; } /* Regeln & Hilfe */
|
||||
[data-ton="29"] { --ton: #8e5b77; } /* Highlights */
|
||||
[data-ton="30"] { --ton: #b2bc78; } /* Wunschliste */
|
||||
[data-ton="31"] { --ton: #656a9e; } /* Was ansteht */
|
||||
[data-ton="32"] { --ton: #668e6e; } /* Mitmachen */
|
||||
[data-ton="33"] { --ton: #cc9451; } /* Anschlagbrett */
|
||||
[data-ton="34"] { --ton: #cc92c6; } /* Der Treff */
|
||||
|
||||
/* DER 35. TON (11.09.2026) — für „Wer sieht was".
|
||||
|
||||
GEMESSEN, NICHT GEWÄHLT. Gesucht war der Punkt im Farbraum, der von
|
||||
ALLEN 34 vorhandenen Tönen am weitesten entfernt liegt — Abstand in
|
||||
Lab, nicht in HSL, weil zwei Töne mit gleichem HSL-Abstand
|
||||
unterschiedlich verschieden AUSSEHEN können.
|
||||
|
||||
Zwei Bereiche waren dabei ausgeschlossen, und beide aus einem Grund
|
||||
und nicht aus Geschmack:
|
||||
* Rot (Farbton unter 22° / über 338°) gehört Spicy Media.
|
||||
* Neon (Sättigung über 52 %) verstößt gegen die Hausregel
|
||||
„augenschonend" — die gilt hier wie überall.
|
||||
|
||||
Ergebnis: #2f2f7f, Abstand 31,4 zum nächsten Nachbarn (#656a9e, „Was
|
||||
ansteht"). Zum Vergleich: Das schwächste Paar unter den vorhandenen
|
||||
34 liegt bei 24,9. Der neue Ton ist also unterscheidbarer als das,
|
||||
was ohnehin nebeneinander steht — und die beiden nächsten Nachbarn
|
||||
stehen in verschiedenen Gruppen, treffen sich auf dem Bildschirm
|
||||
also gar nicht.
|
||||
|
||||
WARUM ÜBERHAUPT EIN NEUER: Die 35. Kachel hätte sich sonst einen Ton
|
||||
mit einer anderen geteilt. Zwei Dinge in einer Farbe heißt, dass die
|
||||
Farbe nichts mehr sagt — und pruef-start-ansicht zählt genau das
|
||||
nach ("34 Farben auf 35 Kacheln" war ihre Meldung). */
|
||||
[data-ton="35"] { --ton: #2f2f7f; } /* Wer sieht was */
|
||||
/* Der 36., nach derselben Rechnung: #7c2771, Abstand 29,7 zu den beiden
|
||||
nächsten (#8e5b77 „Highlights", #2f2f7f „Wer sieht was"). Auch das
|
||||
liegt über dem schwächsten Paar der vorhandenen Töne (24,9). */
|
||||
[data-ton="36"] { --ton: #7c2771; } /* Meldungen & Maßnahmen */
|
||||
|
||||
/* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat.
|
||||
Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der
|
||||
Regeln als NICHT vorhanden und die Nummer oben gewinnt immer. Ohne
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/* =====================================================================
|
||||
Der Treff — die beiden Textseiten (11.09.2026)
|
||||
|
||||
Regeln & Hilfe (für die Community) und „Wer sieht was" (für
|
||||
DogFather). Beides sind LESESEITEN: langer, zusammenhängender Text
|
||||
und eine Tabelle, keine Kartenwand.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM SIE KEINE MODULKARTEN SIND
|
||||
|
||||
Die naheliegende Wahl wäre `.eintrag-karte` oder ein neuer Eintrag in
|
||||
der MODULLISTE gewesen — dann sähen die Abschnitte aus wie alles
|
||||
andere im Haus: geschliffene Fase, Kantenlicht, Sternenfeld.
|
||||
|
||||
Dagegen sprechen zwei Dinge, und das zweite wiegt schwerer:
|
||||
|
||||
1. Die MODULLISTE steht siebenmal in module.css und wird von
|
||||
pruef-css-klassen Zeichen für Zeichen verglichen. Eine achte Klasse
|
||||
hätte an sieben Stellen dasselbe ergänzen müssen — machbar, aber
|
||||
jede dieser sieben ist eine Gelegenheit für einen Unterschied.
|
||||
|
||||
2. Karten zerteilen. Ein Regeltext ist ein Text, den man von oben
|
||||
nach unten liest; sieben Karten daraus zu machen heißt, sieben
|
||||
Anfänge zu machen. Genau das ist bei Regeln der Fehler: Wer nach
|
||||
der dritten Karte aufhört, hat vier Regeln nicht gelesen und
|
||||
glaubt trotzdem, er kenne sie.
|
||||
|
||||
Deshalb hier nur so viel Trennung, dass man die Abschnitte findet —
|
||||
eine Linie und Abstand, keine Kante.
|
||||
===================================================================== */
|
||||
|
||||
.treff-abschnitt {
|
||||
margin: 0 0 26px;
|
||||
padding: 0 0 22px;
|
||||
border-bottom: 1px solid var(--rand);
|
||||
}
|
||||
.treff-abschnitt:last-of-type { border-bottom: 0; }
|
||||
|
||||
.treff-abschnitt__titel {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1.06rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: .005em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Der Lesefluss ist hier die eigentliche Aufgabe.
|
||||
|
||||
1,72 statt der üblichen 1,55: Auf einem Handy stehen in einer Zeile
|
||||
etwa 42 Zeichen; bei engem Zeilenabstand verliert man beim Sprung an
|
||||
den Zeilenanfang die Zeile. Das merkt niemand als „schlechter
|
||||
Zeilenabstand" — man liest den Absatz einfach zweimal und hält den
|
||||
Text für kompliziert.
|
||||
|
||||
Und eine Breitengrenze: Über etwa 70 Zeichen je Zeile wird derselbe
|
||||
Sprung wieder schwierig, nur von der anderen Seite. Auf einem breiten
|
||||
Bildschirm liefe der Regeltext sonst über 1100 Pixel. */
|
||||
.treff-abschnitt__inhalt { max-width: 68ch; }
|
||||
.treff-abschnitt__inhalt p {
|
||||
margin: 0 0 12px;
|
||||
font-size: .92rem;
|
||||
line-height: 1.72;
|
||||
color: var(--text-leise);
|
||||
}
|
||||
.treff-abschnitt__inhalt p:last-child { margin-bottom: 0; }
|
||||
.treff-abschnitt__inhalt strong { color: var(--text); font-weight: 640; }
|
||||
|
||||
/* Die Aufzählungen. Die Zahl bzw. der Punkt steht AUSSERHALB des
|
||||
Textblocks — so beginnt jede Zeile einer mehrzeiligen Regel an
|
||||
derselben Stelle, und die Liste bleibt als Liste lesbar. */
|
||||
.regel-liste {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 1.35em;
|
||||
font-size: .92rem;
|
||||
line-height: 1.72;
|
||||
color: var(--text-leise);
|
||||
}
|
||||
.regel-liste li { margin: 0 0 9px; padding-left: .2em; }
|
||||
.regel-liste li:last-child { margin-bottom: 0; }
|
||||
.regel-liste li::marker { color: var(--text-still); }
|
||||
.regel-liste strong { color: var(--text); font-weight: 640; }
|
||||
|
||||
/* =====================================================================
|
||||
„WER SIEHT WAS" — die Rechtetafel als Tabelle
|
||||
|
||||
Sie wird aus der echten Tafel gerechnet (rechte.js), nicht gepflegt.
|
||||
Was sie leisten muss: Man soll eine LÜCKE sehen, ohne zu suchen.
|
||||
===================================================================== */
|
||||
|
||||
.rechte-tafel { overflow-x: auto; margin: 0 0 26px; }
|
||||
|
||||
.rechte-tafel table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: .82rem;
|
||||
}
|
||||
|
||||
/* Die Kopfzeile bleibt stehen. Bei 22 Zeilen und acht Spalten hat man
|
||||
sonst nach dem halben Scrollen vergessen, welche Spalte welche Rolle
|
||||
ist — und rät. Bei einer Rechteübersicht ist Raten das Gegenteil des
|
||||
Zwecks. */
|
||||
.rechte-tafel thead th {
|
||||
position: sticky; top: 0; z-index: 1;
|
||||
background: var(--flaeche);
|
||||
padding: 9px 8px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: var(--text-leise);
|
||||
border-bottom: 1px solid var(--rand-hell);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rechte-tafel thead th:first-child { text-align: left; }
|
||||
|
||||
.rechte-tafel td {
|
||||
padding: 7px 8px;
|
||||
border-bottom: 1px solid var(--rand);
|
||||
text-align: center;
|
||||
color: var(--text-still);
|
||||
}
|
||||
.rechte-tafel td:first-child {
|
||||
text-align: left;
|
||||
color: var(--text-leise);
|
||||
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
font-size: .79rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rechte-tafel tbody tr:hover td { background: rgba(255, 255, 255, .025); }
|
||||
|
||||
/* JA UND NEIN SIND NICHT GRÜN UND ROT.
|
||||
|
||||
Das wäre die naheliegende Wahl und hier die falsche: In dieser
|
||||
Tabelle ist ein „nein" nichts Schlechtes — es ist der Normalfall und
|
||||
genau das, was Filipe wollte. Grün/Rot würde eine Tafel mit 140
|
||||
roten Feldern erzeugen, die aussieht wie ein Schadensbericht.
|
||||
|
||||
Stattdessen: sichtbar gegen leise. Ein Punkt, wo es erlaubt ist,
|
||||
nichts, wo nicht. Man sieht die Zeilen mit vielen Punkten sofort —
|
||||
und das ist die Frage, die man an diese Tabelle hat. */
|
||||
.rechte-ja {
|
||||
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
|
||||
background: var(--akzent);
|
||||
box-shadow: 0 0 8px -2px var(--akzent);
|
||||
}
|
||||
.rechte-nein { color: var(--text-still); opacity: .3; }
|
||||
|
||||
/* Die Zählzeile über der Tabelle: „Community — 3 von 22 Seiten". Das
|
||||
ist die eine Zahl, die die ganze Frage beantwortet. */
|
||||
.rechte-zahlen {
|
||||
display: flex; flex-wrap: wrap; gap: 8px;
|
||||
margin: 0 0 18px; padding: 0; list-style: none;
|
||||
}
|
||||
.rechte-zahlen li {
|
||||
padding: 6px 11px;
|
||||
border: 1px solid var(--rand-hell);
|
||||
border-radius: 999px;
|
||||
font-size: .8rem;
|
||||
color: var(--text-leise);
|
||||
}
|
||||
.rechte-zahlen li b { color: var(--text); font-weight: 650; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.treff-abschnitt__inhalt p, .regel-liste { font-size: .95rem; }
|
||||
.rechte-tafel table { font-size: .78rem; }
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DER WEG ZU DEN REGELN — auf jedem Brett des Treffs
|
||||
|
||||
Er steht direkt unter der Unterzeile und sieht aus wie ein Link,
|
||||
nicht wie ein Knopf. Das ist Absicht: Ein Knopf verspricht, dass
|
||||
etwas PASSIERT; hier wechselt man nur die Seite. Wer auf jedem Brett
|
||||
einen zusätzlichen Knopf sieht, zählt ihn nach zwei Tagen zum
|
||||
Hintergrund.
|
||||
===================================================================== */
|
||||
.treff-regelweg {
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
font-size: .82rem;
|
||||
color: var(--text-leise);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--rand-hell);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.treff-regelweg:hover, .treff-regelweg:focus-visible {
|
||||
color: var(--text);
|
||||
border-bottom-color: color-mix(in srgb, var(--akzent) 55%, transparent);
|
||||
}
|
||||
|
||||
/* Links im Fließtext der Regelseite. Unterstrichen, weil Farbe allein
|
||||
kein Unterschied ist, den jeder sieht (WCAG 1.4.1). */
|
||||
.treff-abschnitt__inhalt a {
|
||||
color: var(--text);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-color: color-mix(in srgb, var(--akzent) 55%, transparent);
|
||||
}
|
||||
.treff-abschnitt__inhalt a:hover {
|
||||
text-decoration-color: var(--akzent);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
MELDUNGEN & MASSNAHMEN — ein Fall je Karte
|
||||
|
||||
Karten sind hier richtig, wo sie beim Regeltext falsch waren: Es sind
|
||||
EINZELNE Vorgänge, die man einzeln entscheidet. Jeder Anfang ist ein
|
||||
neuer Fall, und das soll man sehen.
|
||||
===================================================================== */
|
||||
.treff-fall {
|
||||
margin: 0 0 12px;
|
||||
padding: 13px 15px;
|
||||
border: 1px solid var(--rand);
|
||||
border-left: 2px solid color-mix(in srgb, var(--akzent) 45%, transparent);
|
||||
background: rgba(255, 255, 255, .018);
|
||||
}
|
||||
|
||||
/* Was schon entschieden ist, tritt zurück -- aber es verschwindet
|
||||
nicht. Ausblenden wäre bequemer und wäre falsch: Man muss nachsehen
|
||||
können, wie ein ähnlicher Fall damals entschieden wurde. */
|
||||
.treff-fall[data-vorbei="ja"] {
|
||||
opacity: .62;
|
||||
border-left-color: var(--rand);
|
||||
}
|
||||
|
||||
.treff-fall__titel {
|
||||
margin: 0 0 8px;
|
||||
font-size: .95rem;
|
||||
font-weight: 640;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.treff-fall__zeile {
|
||||
margin: 0 0 5px;
|
||||
font-size: .85rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-leise);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.treff-fall__zeile:last-of-type { margin-bottom: 0; }
|
||||
|
||||
/* Das Schild steht in fester Breite links. Ohne sie beginnt jede Zeile
|
||||
woanders, und man liest die Liste als Fließtext statt als Angaben --
|
||||
bei fünf Zeilen je Fall ist das der Unterschied zwischen Überfliegen
|
||||
und Lesenmüssen. */
|
||||
.treff-fall__schild {
|
||||
flex: 0 0 7.5em;
|
||||
color: var(--text-still);
|
||||
font-size: .78rem;
|
||||
text-transform: lowercase;
|
||||
padding-top: .1em;
|
||||
}
|
||||
|
||||
.treff-fall__knoepfe {
|
||||
display: flex; gap: 7px; flex-wrap: wrap;
|
||||
margin-top: 11px; padding-top: 10px;
|
||||
border-top: 1px solid var(--rand);
|
||||
}
|
||||
/* Der Link "zum Brett" sieht aus wie die Knöpfe daneben -- er tut ja
|
||||
dasselbe: Er bringt einen woandershin. Ein Link, der zwischen zwei
|
||||
Knöpfen anders aussieht, wird für Text gehalten. */
|
||||
.treff-fall__knoepfe a.schritt { text-decoration: none; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
/* Untereinander statt nebeneinander: Bei 7,5em Schild und 42 Zeichen
|
||||
Zeilenbreite blieben für den Wert noch etwa 20 Zeichen -- ein Grund
|
||||
in zwei Wörtern je Zeile. */
|
||||
.treff-fall__zeile { display: block; }
|
||||
.treff-fall__schild { display: block; padding: 0 0 1px; }
|
||||
}
|
||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -21,7 +21,31 @@
|
||||
Notizbuch. Der Server setzt das ebenfalls durch (siehe
|
||||
workspace-bereiche.js); hier geht es nur darum, ihm keine Knoepfe
|
||||
hinzustellen, die ohnehin nur eine Absage bringen. */
|
||||
const darfEintragen = () => ich && ich.rolle !== 'creator';
|
||||
/* WER HIER ETWAS EINTRAGEN DARF.
|
||||
|
||||
Bis zum 11.09.2026 stand hier nur "alle außer dem Creator". Das
|
||||
stimmte, solange jedes Brett jedem Angemeldeten offenstand. Mit den
|
||||
sieben Brettern des Treffs stimmt es nicht mehr: Auf dem
|
||||
Anschlagbrett und bei "Was ansteht" schreibt nur das Team, und wer
|
||||
neu ist, darf die erste Woche überhaupt nicht schreiben.
|
||||
|
||||
OHNE DIESE ZEILE HÄTTE EIN MITGLIED DEN KNOPF GESEHEN und beim
|
||||
Drücken eine Absage bekommen -- genau die Sorte Knopf, die in
|
||||
diesem Haus als Fehler gilt: Er verspricht etwas, das der Server
|
||||
ablehnt, und der Mensch davor hält sich selbst für den Fehler.
|
||||
|
||||
DIE ANTWORT KOMMT VOM SERVER (/api/treff/lage), nicht aus einer
|
||||
zweiten Regel hier. Sonst gäbe es die Schreibregel zweimal, und die
|
||||
Fassung im Browser kann man ändern. Faellt die Auskunft aus, bleibt
|
||||
die Lage leer und es gilt wieder die alte Regel -- der Knopf ist
|
||||
dann da und der Server sagt notfalls nein. Das ist die richtige
|
||||
Richtung: Ein Knopf zu viel ist ärgerlich, ein fehlender Riegel
|
||||
wäre ein Loch. */
|
||||
const darfEintragen = () => {
|
||||
if (!ich || ich.rolle === 'creator') return false;
|
||||
if (treffBrett && lage && lage.schreiben) return lage.schreiben[bereich] === true;
|
||||
return true;
|
||||
};
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const UNTERZEILE = {
|
||||
@@ -40,6 +64,14 @@
|
||||
let ich = null;
|
||||
let bereich = null;
|
||||
let einstellung = null;
|
||||
/* Ist dies ein Brett des Treffs, und was darf ich dort? Beides sagt
|
||||
der Server (daten.treff und /api/treff/lage) -- im Browser steht
|
||||
keine Liste der Bretter und keine Rechteregel, sondern nur die
|
||||
Antwort auf "was darf ICH hier gerade". Eine Regel, die im Browser
|
||||
noch einmal steht, ist die, die auseinanderlaeuft; und die Fassung
|
||||
im Browser kann man aendern. */
|
||||
let treffBrett = false;
|
||||
let lage = null;
|
||||
let filter = '';
|
||||
/* Nur beim ersten Laden springen -- sonst reisst es die Seite nach
|
||||
jeder Bewertung erneut an dieselbe Stelle. */
|
||||
@@ -214,6 +246,174 @@
|
||||
if (e.erstellt_name) fuss.append(el('span', null, '· von ' + e.erstellt_name));
|
||||
if (e.status === 'erledigt') fuss.append(el('span', null, '· erledigt'));
|
||||
|
||||
/* ---- Der Treff -------------------------------------------------
|
||||
|
||||
DER VOLLE ROLLENNAME AM BEITRAG (Filipe, 11.09.2026: "voller
|
||||
Rollenname zeigen"). Er kommt fertig vom Server; hier steht
|
||||
keine Zuordnung Rolle -> Name, die waere eine Liste aller Rollen
|
||||
dieses Hauses auf jedem Rechner der Community.
|
||||
|
||||
WARUM DAS UEBERHAUPT DRANSTEHT: In einem Raum mit Zuschauern und
|
||||
Moderation muss man sehen, wer spricht. "Frida" und "Frida ·
|
||||
Modi" sind zwei verschiedene Saetze -- der zweite ist eine
|
||||
Ansage, der erste eine Meinung.
|
||||
|
||||
BEI MITGLIEDERN STEHT NICHTS DA. "Kessi · Community" waere eine
|
||||
Auszeichnung fuer das Normale; der Rollenname ist hier eine
|
||||
Auskunft ueber Verantwortung, nicht ueber Zugehoerigkeit. */
|
||||
if (treffBrett && e.erstellt_rolle_name && e.erstellt_rolle !== 'gast') {
|
||||
fuss.append(el('span', 'treff-rolle', '· ' + e.erstellt_rolle_name));
|
||||
}
|
||||
|
||||
if (treffBrett && lage) {
|
||||
const zeile = el('div', 'treff-knoepfe');
|
||||
|
||||
/* WILL ICH AUCH. Die Zahl steht AM Knopf und nicht daneben: Sie
|
||||
ist das Ergebnis des Knopfes, nicht eine Angabe ueber den
|
||||
Eintrag. Und sie aendert sich sofort im Bild, bevor der Server
|
||||
geantwortet hat -- bei einem Knopf, der eine Meinung abgibt,
|
||||
ist eine halbe Sekunde Verzoegerung der Unterschied zwischen
|
||||
"hat geklappt" und "hab ich das jetzt gedrueckt?". Antwortet
|
||||
der Server anders, gilt seine Zahl. */
|
||||
if (bereich === 'wunsch') {
|
||||
const w = el('button', 'treff-will', '');
|
||||
w.type = 'button';
|
||||
let meine = !!e.meine_stimme;
|
||||
let zahl = Number(e.stimmen) || 0;
|
||||
const male = () => {
|
||||
w.textContent = (meine ? '★ ' : '☆ ') + 'Will ich auch · ' + zahl;
|
||||
w.setAttribute('aria-pressed', String(meine));
|
||||
};
|
||||
male();
|
||||
w.addEventListener('click', async () => {
|
||||
w.disabled = true;
|
||||
meine = !meine; zahl += meine ? 1 : -1; male();
|
||||
try {
|
||||
/* EIN DELETE TRAEGT HIER KEINEN KOERPER (gemessen 11.09.2026).
|
||||
|
||||
Der naheliegende Aufbau waere gewesen, PUT und DELETE
|
||||
gleich zu behandeln -- gleicher Kopf, gleicher leerer
|
||||
Koerper. Das geht NICHT: Nodes HTTP-Parser weist ein
|
||||
DELETE mit Koerper mit einem leeren 400 ab, noch bevor
|
||||
Express die Anfrage sieht. Man sucht den Fehler dann in
|
||||
der Route, und dort ist keiner.
|
||||
|
||||
Nachgemessen: PUT mit Koerper -> 200, DELETE ohne
|
||||
Koerper -> 200, DELETE mit Koerper -> 400 mit genau
|
||||
einem Kopffeld ("connection: close"). Das ist die
|
||||
Handschrift des Parsers, nicht die einer Anwendung. */
|
||||
const a = await hole('/workspace/api/treff/eintrag/' + e.id + '/willich',
|
||||
meine
|
||||
? { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' }
|
||||
: { method: 'DELETE' });
|
||||
if (a.ok) { const j = await a.json(); zahl = j.stimmen; meine = j.meine; male(); }
|
||||
} catch { /* umgeleitet */ } finally { w.disabled = false; }
|
||||
});
|
||||
zeile.append(w);
|
||||
}
|
||||
|
||||
/* MELDEN. Nur fuer die Community und nur an fremden Beitraegen --
|
||||
den eigenen kann man loeschen, das ist der kuerzere Weg.
|
||||
Der Grund ist Pflicht (DSA Art. 16); der Server prueft ihn
|
||||
noch einmal, die Abfrage hier erspart nur den Fehlschlag. */
|
||||
if (!lage.team && e.erstellt_von !== ich.id) {
|
||||
zeile.append(tuKnopf('melden', async () => {
|
||||
const grund = prompt('Was ist daran nicht in Ordnung?\n\n'
|
||||
+ 'Bitte schreib es in einem Satz – sonst können wir es nicht prüfen.');
|
||||
if (grund === null) return;
|
||||
const a = await hole('/workspace/api/treff/eintrag/' + e.id + '/melden', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ grund }),
|
||||
});
|
||||
melde(a.ok ? 'Danke – wir sehen uns das an.'
|
||||
: ((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'));
|
||||
}));
|
||||
}
|
||||
|
||||
if (lage.team) {
|
||||
/* ANHEFTEN. Hoechstens drei; die Zahl kommt vom Server
|
||||
(lage.anschlag_max), damit sie nicht an zwei Stellen steht. */
|
||||
if (bereich === 'anschlag') {
|
||||
const dran = !!e.angeheftet;
|
||||
zeile.append(tuKnopf(dran ? 'abnehmen' : 'anheften', async () => {
|
||||
const a = await hole('/workspace/api/treff/eintrag/' + e.id + '/anheften',
|
||||
dran
|
||||
? { method: 'DELETE' }
|
||||
: { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
if (!a.ok) {
|
||||
melde((await a.json().catch(() => ({}))).fehler
|
||||
|| ('Es hängen schon ' + lage.anschlag_max + ' Ansagen.'));
|
||||
return;
|
||||
}
|
||||
await laden();
|
||||
}));
|
||||
}
|
||||
|
||||
/* FREIGEBEN. Bei Terminen heisst es "im Treff zeigen", bei
|
||||
Highlights "freigeben" -- derselbe Vorgang, zwei Woerter,
|
||||
weil es zwei verschiedene Entscheidungen sind. */
|
||||
if (bereich === 'ansteht' || bereich === 'highlight') {
|
||||
const frei = !!e.freigegeben;
|
||||
const wort = bereich === 'ansteht'
|
||||
? (frei ? 'nicht mehr zeigen' : 'im Treff zeigen')
|
||||
: (frei ? 'zurückziehen' : 'freigeben');
|
||||
if (!frei) {
|
||||
const m = el('span', 'marke-art marke-art--nur', 'noch nicht sichtbar');
|
||||
m.title = 'Die Community sieht das erst nach der Freigabe.';
|
||||
zeile.append(m);
|
||||
}
|
||||
zeile.append(tuKnopf(wort, async () => {
|
||||
const a = await hole('/workspace/api/treff/eintrag/' + e.id + '/freigeben',
|
||||
frei
|
||||
? { method: 'DELETE' }
|
||||
: { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
if (!a.ok) { melde('Ging nicht.'); return; }
|
||||
await laden();
|
||||
}));
|
||||
}
|
||||
|
||||
/* AUS "MITMACHEN" WIRD EIN TALENT. Ein ausdruecklicher Schritt
|
||||
und keine Automatik: Wer sich meldet, ist ein Kandidat --
|
||||
dass er einer wird, entscheidet ein Mensch. */
|
||||
if (bereich === 'mitmachen' && e.erstellt_rolle === 'gast') {
|
||||
zeile.append(tuKnopf('zu den Talenten', async () => {
|
||||
if (!confirm('„' + e.titel + '“ zu den Talenten übernehmen?')) return;
|
||||
const a = await hole('/workspace/api/treff/mitmachen/' + e.id + '/uebernehmen',
|
||||
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
const j = await a.json().catch(() => ({}));
|
||||
melde(a.ok ? 'Steht jetzt bei den Talenten.'
|
||||
: (j.fehler === 'schon_uebernommen' ? 'Steht dort schon.'
|
||||
: (j.fehler || 'Ging nicht.')));
|
||||
}));
|
||||
}
|
||||
|
||||
/* ENTFERNEN MIT GRUND (DSA Art. 17). Eigener Knopf neben dem
|
||||
gewohnten "loeschen" -- denn "loeschen" gibt es weiterhin
|
||||
fuer den eigenen Beitrag, und der braucht keine Begruendung. */
|
||||
if (e.erstellt_von !== ich.id) {
|
||||
zeile.append(tuKnopf('entfernen', async () => {
|
||||
const grund = prompt('Warum wird „' + e.titel + '“ entfernt?\n\n'
|
||||
+ 'Der Grund wird festgehalten – die Person hat ein Recht darauf.');
|
||||
if (grund === null) return;
|
||||
/* DER GRUND REIST IN DER ADRESSE, nicht im Koerper -- aus
|
||||
demselben gemessenen Grund wie oben. Er ist kein
|
||||
Geheimnis: Er wird der Person, die den Beitrag
|
||||
geschrieben hat, ohnehin mitgeteilt, und er steht im
|
||||
Protokoll. */
|
||||
const a = await hole('/workspace/api/bereich/' + bereich + '/' + e.id
|
||||
+ '?grund=' + encodeURIComponent(grund), { method: 'DELETE' });
|
||||
if (!a.ok) {
|
||||
melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
|
||||
return;
|
||||
}
|
||||
await laden();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (zeile.childNodes.length) k.append(zeile);
|
||||
}
|
||||
|
||||
if (!darfEintragen()) { k.append(fuss); return k; }
|
||||
|
||||
const knoepfe = el('div', 'eintrag-knoepfe');
|
||||
@@ -263,6 +463,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Einmal je Seitenaufruf. Sie wird an ZWEI Stellen gebraucht -- beim
|
||||
Aufbau des Formulars und beim Zeichnen der Karten -- und beide
|
||||
brauchen sie, BEVOR sie etwas anzeigen. Ein zweiter Abruf wäre eine
|
||||
zweite Gelegenheit für zwei verschiedene Antworten auf dem selben
|
||||
Bildschirm. */
|
||||
async function lageHolen() {
|
||||
if (!treffBrett || lage) return;
|
||||
try {
|
||||
const l = await fetch('/workspace/api/treff/lage', { credentials: 'same-origin' });
|
||||
if (l.ok) lage = await l.json();
|
||||
} catch { /* dann gilt die alte Regel, siehe darfEintragen */ }
|
||||
}
|
||||
|
||||
function filterBauen() {
|
||||
const ziel = $('filter');
|
||||
ziel.textContent = '';
|
||||
@@ -284,6 +497,8 @@
|
||||
if (!a.ok) { melde('Einträge konnten nicht geladen werden.'); return; }
|
||||
const daten = await a.json();
|
||||
einstellung = daten.einstellung;
|
||||
treffBrett = !!daten.treff;
|
||||
await lageHolen();
|
||||
filterBauen();
|
||||
|
||||
const ziel = $('liste');
|
||||
@@ -539,6 +754,36 @@
|
||||
if (einstellung.ober && $('ober')) $('ober').textContent = einstellung.ober;
|
||||
$('unterzeile').textContent = UNTERZEILE[bereich] || '';
|
||||
|
||||
/* VOR dem Aufbau des Formulars. Danach wäre zu spät: Der Knopf
|
||||
stünde dann kurz da und verschwände wieder -- sichtbar, und
|
||||
ausgerechnet an der Stelle, an der jemand gerade überlegt, ob er
|
||||
schreiben darf. */
|
||||
treffBrett = !!daten.treff;
|
||||
await lageHolen();
|
||||
|
||||
/* DER WEG ZU DEN REGELN STEHT AUF JEDEM BRETT DES TREFFS
|
||||
(11.09.2026).
|
||||
|
||||
Nicht nur auf dem Brett "Regeln & Hilfe". Wer die Regeln braucht,
|
||||
braucht sie in dem Moment, in dem er etwas schreiben will -- und
|
||||
das ist fast nie dort. Ein Mensch, der erst zurueck zur
|
||||
Startseite und dann auf die richtige Kachel muss, liest sie
|
||||
nicht; er schreibt einfach.
|
||||
|
||||
ALS LINK UND NICHT ALS EINGEBLENDETER TEXT: Die Regeln sind zu
|
||||
lang, um sie ueber jedes Brett zu haengen, und ein Kasten, den
|
||||
man jeden Tag wegklickt, wird nach drei Tagen nicht mehr
|
||||
gesehen. */
|
||||
if (daten.treff) {
|
||||
const vorhanden = $('treff-regelweg');
|
||||
if (!vorhanden) {
|
||||
const a = el('a', 'treff-regelweg', 'Regeln & Hilfe');
|
||||
a.id = 'treff-regelweg';
|
||||
a.href = 'treff-regeln.html';
|
||||
$('unterzeile').after(a);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fuer den Creator verschwindet alles Schreibende -- und die
|
||||
Unterzeile sagt, warum. Ein leerer Bereich ohne Erklaerung wirkt
|
||||
kaputt; einer mit Erklaerung wirkt gedacht. */
|
||||
@@ -551,10 +796,18 @@
|
||||
"was zu dir gehört", laese sich eine vollstaendige Liste wie
|
||||
ein Ausschnitt, und die naheliegende Frage waere: Was sehe ich
|
||||
hier eigentlich nicht? */
|
||||
/* IM TREFF STIMMT KEINER DER BEIDEN SÄTZE. Dort geht es weder um
|
||||
die Agentur noch um Betreuung, sondern entweder um ein Brett,
|
||||
das dem Team gehört, oder um eine Stufe, die noch nicht
|
||||
erreicht ist. Und der Server weiß beides genau -- er schickt
|
||||
den Satz mit, statt dass der Browser ihn errät. */
|
||||
$('unterzeile').textContent = (UNTERZEILE[bereich] || '')
|
||||
+ (einstellung.fuerAlle
|
||||
? ' Eingetragen wird von der Agentur – du siehst alles, was hier steht.'
|
||||
: ' Hier trägt deine Betreuung ein – du siehst alles, was zu dir gehört.');
|
||||
+ (treffBrett
|
||||
? ' ' + (lage?.warum?.[bereich]
|
||||
|| 'Hier schreibt das Team – lesen kannst du alles.')
|
||||
: einstellung.fuerAlle
|
||||
? ' Eingetragen wird von der Agentur – du siehst alles, was hier steht.'
|
||||
: ' Hier trägt deine Betreuung ein – du siehst alles, was zu dir gehört.');
|
||||
}
|
||||
|
||||
for (const [wert, name] of Object.entries(einstellung.arten)) {
|
||||
|
||||
@@ -76,6 +76,31 @@
|
||||
und zwar ohne Fehlermeldung im Protokoll. */
|
||||
const ohneKacheln = rollen.length === 0;
|
||||
|
||||
/* UND SEIT DEM 11.09.2026 GIBT ES SIE WIEDER -- die Wand des Treffs.
|
||||
|
||||
Dort ist nur EINE Sorte Zugang vorgesehen, und eine Kachelreihe
|
||||
mit genau einem Eintrag waere keine Auswahl, sondern eine Huerde:
|
||||
ein Knopf, den man druecken muss, damit der naechste Knopf
|
||||
funktioniert.
|
||||
|
||||
DER SERVER BRAUCHT DIE ROLLE TROTZDEM. Er vergleicht sie gegen den
|
||||
Kachelsatz seiner Adresse; kaeme "" an, waere die Antwort dieselbe
|
||||
wie bei einem falschen Code -- 401, ohne dass irgendwo etwas
|
||||
kaputt ist. Genau so eine Stelle sucht man lange, denn sie sieht
|
||||
aus wie ein Tippfehler des Nutzers.
|
||||
|
||||
WARUM DAS AM <body> STEHT UND NICHT IM SKRIPT: gate.js wird an
|
||||
ALLE drei Waende ausgeliefert. Eine Fallunterscheidung hier
|
||||
("wenn Treff, dann gast") waere eine vierte Stelle, an der die
|
||||
Adressen aufgezaehlt werden -- und die erste, die beim naechsten
|
||||
Umbau vergessen wird. Die Wand sagt selbst, wofuer sie da ist.
|
||||
|
||||
EIN LECK IST DAS NICHT: Auf dieser Wand gibt es genau diese eine
|
||||
Rolle. Der Name verraet nichts, was der Seitenkopf nicht ohnehin
|
||||
sagt -- die beiden anderen Waende nennen ihre Rollen seit jeher
|
||||
offen im Quelltext. */
|
||||
const VORGABE = document.body.dataset.rolle || "";
|
||||
|
||||
/* ---------- Rolle wählen ------------------------------------------ */
|
||||
|
||||
function waehle(rolle, fokus = false) {
|
||||
@@ -161,7 +186,7 @@
|
||||
braucht ihn dort nicht: Er prueft den verborgenen Zugang
|
||||
VOR dem gewohnten Weg, und ein leerer Name faellt danach in
|
||||
dieselbe Antwort wie ein erfundener. */
|
||||
body: JSON.stringify({ rolle: gewaehlt || '', code: wert }),
|
||||
body: JSON.stringify({ rolle: gewaehlt || VORGABE, code: wert }),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/* =====================================================================
|
||||
„Wer sieht was" — die Rechtetafel, gezeichnet (11.09.2026)
|
||||
|
||||
Dieses Skript ENTSCHEIDET nichts. Es holt die Tafel vom Server und
|
||||
zeichnet sie. Jede Zahl und jeder Punkt darin kommt aus rechte.js —
|
||||
derselben Datei, aus der auch die Schranke ihre Antwort holt.
|
||||
|
||||
WARUM DAS WICHTIG IST: Eine Übersicht, die im Browser gebaut wird
|
||||
(„Rolle X darf wohl Y, weil …"), wäre eine zweite Meinung über die
|
||||
Rechte — und ausgerechnet die, der man glaubt, weil sie wie eine
|
||||
Auskunft aussieht. Sie würde altern, ohne dass man es ihr ansieht.
|
||||
===================================================================== */
|
||||
(() => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const el = (t, k, text) => {
|
||||
const n = document.createElement(t);
|
||||
if (k) n.className = k;
|
||||
if (text !== undefined) n.textContent = text;
|
||||
return n;
|
||||
};
|
||||
|
||||
const melde = (t) => {
|
||||
const f = $('fehler');
|
||||
if (f) { f.textContent = t; f.hidden = !t; }
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let r;
|
||||
try {
|
||||
const a = await fetch('/workspace/api/rechte/tafel', { credentials: 'same-origin' });
|
||||
if (a.status === 401) { location.assign('/workspace/'); return; }
|
||||
/* 404 heißt hier: diese Seite gibt es für dich nicht. Die
|
||||
Schranke hätte schon vorher umgeleitet; kommt es trotzdem
|
||||
hierher, ist die Startseite die richtige Antwort — und nicht
|
||||
eine Fehlermeldung, die verrät, dass es die Auskunft gibt. */
|
||||
if (a.status === 404) { location.assign('/workspace/start.html'); return; }
|
||||
if (!a.ok) { melde('Die Tafel konnte nicht geladen werden.'); return; }
|
||||
r = await a.json();
|
||||
} catch {
|
||||
melde('Die Tafel konnte nicht geladen werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const gesamt = r.seiten.length;
|
||||
|
||||
/* ---- Die Zahlen je Rolle ---------------------------------------
|
||||
Sortiert nach Anzahl, absteigend. Eine alphabetische Reihenfolge
|
||||
hätte dieselben Zahlen gezeigt und die Frage nicht beantwortet:
|
||||
Man sieht hier auf einen Blick, wer viel sieht und wer wenig —
|
||||
und ob irgendwo eine Rolle steht, die mehr sieht, als sie sollte. */
|
||||
const zahlen = $('zahlen');
|
||||
if (zahlen) {
|
||||
zahlen.textContent = '';
|
||||
for (const rolle of [...r.rollen].sort((a, b) => b.seiten - a.seiten)) {
|
||||
const li = document.createElement('li');
|
||||
li.append(rolle.name + ' ');
|
||||
const b = document.createElement('b');
|
||||
b.textContent = `${rolle.seiten} von ${gesamt}`;
|
||||
li.append(b);
|
||||
zahlen.append(li);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Die Tafel --------------------------------------------------- */
|
||||
const ziel = $('tafel');
|
||||
if (ziel) {
|
||||
ziel.textContent = '';
|
||||
const tab = document.createElement('table');
|
||||
|
||||
const kopf = document.createElement('thead');
|
||||
const kz = document.createElement('tr');
|
||||
kz.append(el('th', null, 'Seite'));
|
||||
for (const rolle of r.rollen) {
|
||||
const th = el('th', null, rolle.name);
|
||||
/* Der Schlüssel als Titel: Im Alltag liest man „Community",
|
||||
im Code steht „gast". Wer die beiden nicht zusammenbringen
|
||||
kann, sucht im Quelltext an der falschen Stelle. */
|
||||
th.title = rolle.schluessel;
|
||||
kz.append(th);
|
||||
}
|
||||
kopf.append(kz);
|
||||
tab.append(kopf);
|
||||
|
||||
const koerper = document.createElement('tbody');
|
||||
for (const seite of r.seiten) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.append(el('td', null, seite.datei));
|
||||
for (const rolle of r.rollen) {
|
||||
const td = document.createElement('td');
|
||||
if (seite.rollen.includes(rolle.schluessel)) {
|
||||
const punkt = el('span', 'rechte-ja');
|
||||
punkt.setAttribute('role', 'img');
|
||||
punkt.setAttribute('aria-label', 'darf');
|
||||
td.append(punkt);
|
||||
} else {
|
||||
/* Ein Zeichen und nicht ein leeres Feld: Ein Bildschirmleser
|
||||
liest eine leere Zelle als nichts vor — man hört dann
|
||||
nicht, dass die Antwort „nein" ist, sondern gar nichts. */
|
||||
const nein = el('span', 'rechte-nein', '–');
|
||||
nein.setAttribute('aria-label', 'darf nicht');
|
||||
td.append(nein);
|
||||
}
|
||||
tr.append(td);
|
||||
}
|
||||
koerper.append(tr);
|
||||
}
|
||||
tab.append(koerper);
|
||||
ziel.append(tab);
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
}
|
||||
|
||||
/* ---- Ohne Anmeldung --------------------------------------------- */
|
||||
const offen = $('offen');
|
||||
if (offen) {
|
||||
offen.textContent = '';
|
||||
for (const pfad of r.ohne_anmeldung) {
|
||||
offen.append(el('li', null, pfad));
|
||||
}
|
||||
}
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,312 @@
|
||||
/* =====================================================================
|
||||
Meldungen & Maßnahmen im Treff (11.09.2026)
|
||||
|
||||
Drei Listen und ein Knopf. Die Regeln stehen alle auf dem Server —
|
||||
wer eine Maßnahme verhängen darf, wie lange ein Modi sperren kann,
|
||||
ob eine Meldung schon entschieden ist. Diese Datei zeichnet nur und
|
||||
fragt nach.
|
||||
|
||||
WARUM HIER KEINE RECHTEPRÜFUNG STEHT: Sie stünde dann zweimal, und
|
||||
die Fassung im Browser kann jeder ändern. Was hier passiert, ist
|
||||
höchstens ein Knopf, der gar nicht erst erscheint — das ist
|
||||
Bequemlichkeit, keine Sicherung. Die Sicherung antwortet mit 403.
|
||||
===================================================================== */
|
||||
(() => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const el = (t, k, text) => {
|
||||
const n = document.createElement(t);
|
||||
if (k) n.className = k;
|
||||
if (text !== undefined) n.textContent = text;
|
||||
return n;
|
||||
};
|
||||
const melde = (t) => { const f = $('fehler'); if (f) { f.textContent = t; f.hidden = !t; } };
|
||||
|
||||
const datum = (iso) => {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString('de-DE',
|
||||
{ day: 'numeric', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
async function hole(pfad, optionen = {}) {
|
||||
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||||
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
||||
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('unbekannt'); }
|
||||
return a;
|
||||
}
|
||||
|
||||
function knopf(text, tu) {
|
||||
const b = el('button', 'schritt', text);
|
||||
b.type = 'button';
|
||||
b.addEventListener('click', async () => {
|
||||
b.disabled = true; melde('');
|
||||
try { await tu(); } catch { /* umgeleitet */ } finally { b.disabled = false; }
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
/* Eine Karte für alle drei Listen. Derselbe Aufbau, damit man die
|
||||
Seite nicht dreimal neu lesen lernen muss. */
|
||||
function karte(kopfText, zeilen, knoepfe = []) {
|
||||
const k = el('article', 'treff-fall');
|
||||
k.append(el('h3', 'treff-fall__titel', kopfText));
|
||||
for (const [schild, wert] of zeilen) {
|
||||
if (!wert) continue;
|
||||
const p = el('p', 'treff-fall__zeile');
|
||||
p.append(el('span', 'treff-fall__schild', schild));
|
||||
p.append(el('span', null, wert));
|
||||
k.append(p);
|
||||
}
|
||||
if (knoepfe.length) {
|
||||
const reihe = el('div', 'treff-fall__knoepfe');
|
||||
for (const b of knoepfe) reihe.append(b);
|
||||
k.append(reihe);
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
/* ---- Meldungen ---------------------------------------------------- */
|
||||
async function meldungen() {
|
||||
const ziel = $('meldungen');
|
||||
const a = await hole('/workspace/api/treff/meldungen');
|
||||
if (!a.ok) { ziel.textContent = ''; ziel.append(el('p', 'leise', 'Ging nicht.')); return; }
|
||||
const liste = (await a.json()).meldungen || [];
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
|
||||
const offen = liste.filter((m) => m.status === 'offen');
|
||||
const erledigt = liste.filter((m) => m.status !== 'offen');
|
||||
|
||||
if (!offen.length) {
|
||||
ziel.append(el('p', 'leise', 'Nichts offen. Das ist die gute Nachricht.'));
|
||||
}
|
||||
for (const m of offen) {
|
||||
const wer = m.melder_name
|
||||
? m.melder_name + (m.melder_rolle_name && m.melder_rolle !== 'gast'
|
||||
? ' · ' + m.melder_rolle_name : '')
|
||||
: 'jemand';
|
||||
const autor = m.autor_name
|
||||
? m.autor_name + (m.autor_rolle_name && m.autor_rolle !== 'gast'
|
||||
? ' · ' + m.autor_rolle_name : '')
|
||||
: 'unbekannt';
|
||||
const k = karte(m.titel || '(Beitrag gelöscht)', [
|
||||
['Grund', m.grund],
|
||||
['gemeldet von', wer + ' am ' + datum(m.zeitpunkt)],
|
||||
['Beitrag von', autor],
|
||||
['Brett', m.bereich || '–'],
|
||||
['Text', m.text],
|
||||
], [
|
||||
knopf('erledigt', () => entscheiden(m.id, 'erledigt')),
|
||||
knopf('nichts dran', () => entscheiden(m.id, 'abgelehnt')),
|
||||
/* Der Weg zum Beitrag selbst. Ohne ihn müsste man die Meldung
|
||||
lesen, sich das Brett merken und dort suchen -- und genau
|
||||
dabei entscheidet man nach Erinnerung statt nach dem, was
|
||||
wirklich dasteht. */
|
||||
...(m.bereich ? [(() => {
|
||||
const a2 = el('a', 'schritt', 'zum Brett');
|
||||
a2.href = 'bereich.html?b=' + encodeURIComponent(m.bereich);
|
||||
return a2;
|
||||
})()] : []),
|
||||
]);
|
||||
ziel.append(k);
|
||||
}
|
||||
|
||||
if (erledigt.length) {
|
||||
const h = el('p', 'leise', `${erledigt.length} schon entschieden:`);
|
||||
ziel.append(h);
|
||||
for (const m of erledigt.slice(0, 20)) {
|
||||
ziel.append(karte(m.titel || '(Beitrag gelöscht)', [
|
||||
['Grund', m.grund],
|
||||
['Entscheidung', (m.status === 'erledigt' ? 'erledigt' : 'nichts dran')
|
||||
+ (m.entscheid ? ' – ' + m.entscheid : '')],
|
||||
['von', (m.entschieden_name || '') + ' am ' + datum(m.entschieden_am)],
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function entscheiden(id, status) {
|
||||
const entscheid = prompt(status === 'erledigt'
|
||||
? 'Was hast du getan?\n\nEin Satz genügt – er wird zur Meldung festgehalten.'
|
||||
: 'Warum ist daran nichts dran?\n\nEin Satz genügt – er wird festgehalten.');
|
||||
if (entscheid === null) return;
|
||||
const a = await hole('/workspace/api/treff/meldung/' + id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status, entscheid }),
|
||||
});
|
||||
if (!a.ok) {
|
||||
melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
|
||||
return;
|
||||
}
|
||||
await meldungen();
|
||||
}
|
||||
|
||||
/* ---- Entfernt ----------------------------------------------------- */
|
||||
async function entfernt() {
|
||||
const ziel = $('entfernt');
|
||||
const a = await hole('/workspace/api/treff/entfernt');
|
||||
if (!a.ok) { ziel.textContent = ''; return; }
|
||||
const liste = (await a.json()).entfernt || [];
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
if (!liste.length) { ziel.append(el('p', 'leise', 'Noch nichts entfernt.')); return; }
|
||||
for (const e of liste.slice(0, 50)) {
|
||||
ziel.append(karte(e.titel, [
|
||||
['Grund', e.grund],
|
||||
['war von', e.autor_name || 'unbekannt'],
|
||||
['entfernt von', (e.von_name || '')
|
||||
+ (e.von_rolle_name ? ' · ' + e.von_rolle_name : '') + ' am ' + datum(e.zeitpunkt)],
|
||||
['Brett', e.bereich],
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Mitglieder ---------------------------------------------------- */
|
||||
|
||||
/* DIE STUFE STEHT NUR HIER. Im Treff selbst taucht sie nirgends auf
|
||||
-- weder neben dem Namen noch an einem Beitrag. Das ist eine
|
||||
bewusste Entscheidung aus dem Plan (Teil H): Eine sichtbare Stufe
|
||||
macht aus einem Schutz eine Rangordnung, und aus „Neu" wird ein
|
||||
Abzeichen, das man los sein will. Für die Moderation ist sie
|
||||
dagegen die wichtigste Angabe überhaupt. */
|
||||
async function mitglieder() {
|
||||
const ziel = $('mitglieder');
|
||||
const a = await hole('/workspace/api/treff/mitglieder');
|
||||
if (!a.ok) { ziel.textContent = ''; return; }
|
||||
const { mitglieder: liste, darf } = await a.json();
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
if (!liste.length) {
|
||||
ziel.append(el('p', 'leise',
|
||||
'Noch niemand. Zugänge legst du in „Personen & Zugänge" an, Rolle Community.'));
|
||||
return;
|
||||
}
|
||||
const STUFE = { neu: 'Neu', dabei: 'Dabei', stamm: 'Stamm' };
|
||||
for (const m of liste) {
|
||||
const knoepfe = [];
|
||||
if (!m.sperre) {
|
||||
if (darf?.pause) knoepfe.push(knopf('Pause', () => massnahme(m, 'pause', darf)));
|
||||
if (darf?.ausschluss_befristet) {
|
||||
knoepfe.push(knopf('Ausschluss', () => massnahme(m, 'ausschluss', darf)));
|
||||
}
|
||||
knoepfe.push(knopf('Hinweis', () => massnahme(m, 'hinweis', darf)));
|
||||
}
|
||||
const k = karte(m.name, [
|
||||
['Stufe', STUFE[m.stufe] || m.stufe],
|
||||
['dabei seit', datum(m.seit)],
|
||||
['Beiträge', String(m.beitraege)],
|
||||
['zuletzt da', m.zuletzt ? datum(m.zuletzt) : 'noch nie'],
|
||||
['gesperrt', m.sperre
|
||||
? `${m.sperre.art === 'pause' ? 'Pause' : 'Ausschluss'} `
|
||||
+ (m.sperre.bis ? 'bis ' + datum(m.sperre.bis) : 'dauerhaft')
|
||||
+ ' – ' + m.sperre.grund
|
||||
: ''],
|
||||
], knoepfe);
|
||||
if (m.sperre) k.dataset.vorbei = 'ja';
|
||||
ziel.append(k);
|
||||
}
|
||||
}
|
||||
|
||||
/* EIN VORGANG, ZWEI FRAGEN — und die Reihenfolge ist wichtig.
|
||||
|
||||
Zuerst der GRUND, dann die Dauer. Umgekehrt wäre bequemer zu
|
||||
tippen und falsch herum gedacht: Wer zuerst „wie lange?" gefragt
|
||||
wird, hat die Strafe schon festgelegt, bevor er sie begründet hat.
|
||||
So steht am Anfang die Frage, was eigentlich passiert ist. */
|
||||
async function massnahme(m, art, darf) {
|
||||
const grund = prompt(art === 'hinweis'
|
||||
? `Hinweis an ${m.name} – worum geht es?`
|
||||
: `${art === 'pause' ? 'Pause' : 'Ausschluss'} für ${m.name} – warum?\n\n`
|
||||
+ 'Der Grund wird festgehalten, und die Person hat ein Recht darauf.');
|
||||
if (grund === null) return;
|
||||
|
||||
let tage;
|
||||
if (art !== 'hinweis') {
|
||||
const hinweis = darf?.frist_tage
|
||||
? `Du kannst bis zu ${darf.frist_tage} Tage geben.`
|
||||
: 'Leer lassen = dauerhaft (nur bei Ausschluss).';
|
||||
const ein = prompt(`Für wie viele Tage?\n\n${hinweis}`,
|
||||
darf?.frist_tage ? String(darf.frist_tage) : '7');
|
||||
if (ein === null) return;
|
||||
tage = ein.trim() === '' ? null : Number(ein);
|
||||
if (tage !== null && !(tage > 0)) { melde('Das ist keine Anzahl Tage.'); return; }
|
||||
if (tage === null && art === 'pause') { melde('Eine Pause braucht eine Dauer.'); return; }
|
||||
}
|
||||
|
||||
const a = await hole('/workspace/api/treff/massnahme', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ person_id: m.id, art, grund, tage }),
|
||||
});
|
||||
if (!a.ok) {
|
||||
melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
|
||||
return;
|
||||
}
|
||||
await mitglieder();
|
||||
await massnahmen();
|
||||
}
|
||||
|
||||
/* ---- Maßnahmen ---------------------------------------------------- */
|
||||
async function massnahmen() {
|
||||
const ziel = $('massnahmen');
|
||||
const a = await hole('/workspace/api/treff/massnahmen');
|
||||
if (!a.ok) { ziel.textContent = ''; return; }
|
||||
const liste = (await a.json()).massnahmen || [];
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
if (!liste.length) { ziel.append(el('p', 'leise', 'Noch keine Maßnahme.')); return; }
|
||||
|
||||
const NAME = { hinweis: 'Hinweis', pause: 'Pause', ausschluss: 'Ausschluss' };
|
||||
for (const m of liste.slice(0, 50)) {
|
||||
const laeuft = !m.aufgehoben_am
|
||||
&& (!m.bis || Date.parse(m.bis) > Date.now());
|
||||
const k = karte(`${NAME[m.art] || m.art} · ${m.person_name || 'unbekannt'}`, [
|
||||
['Grund', m.grund],
|
||||
['gilt', m.aufgehoben_am ? 'aufgehoben am ' + datum(m.aufgehoben_am)
|
||||
: m.bis ? 'bis ' + datum(m.bis) : 'dauerhaft'],
|
||||
['von', (m.von_name || '') + (m.von_rolle_name ? ' · ' + m.von_rolle_name : '')
|
||||
+ ' am ' + datum(m.zeitpunkt)],
|
||||
], laeuft && m.art !== 'hinweis' ? [
|
||||
knopf('aufheben', async () => {
|
||||
if (!confirm(`Maßnahme gegen ${m.person_name} aufheben?`)) return;
|
||||
const r = await hole('/workspace/api/treff/massnahme/' + m.id, { method: 'DELETE' });
|
||||
if (!r.ok) {
|
||||
melde((await r.json().catch(() => ({}))).fehler || 'Ging nicht.');
|
||||
return;
|
||||
}
|
||||
await massnahmen();
|
||||
}),
|
||||
] : []);
|
||||
if (!laeuft) k.dataset.vorbei = 'ja';
|
||||
ziel.append(k);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Die Leiter in Worten ------------------------------------------
|
||||
Die Zahlen kommen vom Server, aus demselben Grund wie im Regeltext:
|
||||
Eine Frist, die in der Oberfläche anders steht als im Code, ist
|
||||
schlimmer als gar keine Angabe. */
|
||||
async function leiter() {
|
||||
try {
|
||||
const a = await fetch('/workspace/api/treff/regeln', { credentials: 'same-origin' });
|
||||
if (!a.ok) return;
|
||||
const r = await a.json();
|
||||
$('leiter').textContent =
|
||||
`Hinweis, dann Pause, dann Ausschluss – in dieser Reihenfolge. Ein Modi kann `
|
||||
+ `bis zu ${r.modi_frist_tage} Tage geben; alles darüber und jeder dauerhafte `
|
||||
+ `Ausschluss ist DogFathers Entscheidung. Wer eine dauerhafte Maßnahme nicht `
|
||||
+ `verhängen darf, kann sie auch nicht aufheben.`;
|
||||
} catch { /* dann bleibt "…" stehen, und das sieht man */ }
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await leiter();
|
||||
await meldungen();
|
||||
await entfernt();
|
||||
await mitglieder();
|
||||
await massnahmen();
|
||||
} catch { /* umgeleitet */ }
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,82 @@
|
||||
/* =====================================================================
|
||||
Die Zahlen im Regeltext (11.09.2026)
|
||||
|
||||
Dieses Skript schreibt nichts dazu -- es setzt nur die Zahlen ein,
|
||||
die im Text stehen muessen: Fristen, Stufen, Mindestalter, wie lange
|
||||
ein Modi eine Pause geben darf.
|
||||
|
||||
WARUM UEBERHAUPT EIN SKRIPT FUER FUENF ZAHLEN
|
||||
|
||||
Weil sie sonst zweimal existieren wuerden: einmal als Regel im Code
|
||||
(DABEI_TAGE, STAMM_TAGE, MINDESTALTER ...) und einmal als Wort in
|
||||
der HTML-Datei. Beim naechsten Nachjustieren waere eine von beiden
|
||||
geaendert worden, und der Regeltext haette etwas anderes versprochen
|
||||
als der Server tut. Das faellt niemandem auf -- und wer es bemerkt,
|
||||
glaubt dem Text, nicht dem Verhalten.
|
||||
|
||||
Ein Regeltext, der sich selbst widerspricht, ist ausserdem rechtlich
|
||||
das Gegenteil dessen, wofuer er da ist.
|
||||
|
||||
WENN DIE ANTWORT AUSBLEIBT, bleibt "…" stehen. Das ist Absicht: Eine
|
||||
fehlende Zahl sieht man, eine falsche nicht.
|
||||
===================================================================== */
|
||||
(() => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
const melde = (t) => {
|
||||
const f = $('fehler');
|
||||
if (f) { f.textContent = t; f.hidden = !t; }
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let r;
|
||||
try {
|
||||
const a = await fetch('/workspace/api/treff/regeln', { credentials: 'same-origin' });
|
||||
if (a.status === 401) { location.assign('/workspace/'); return; }
|
||||
if (!a.ok) { melde('Die Zahlen konnten nicht geladen werden. Der Text gilt trotzdem.'); return; }
|
||||
r = await a.json();
|
||||
} catch {
|
||||
melde('Die Zahlen konnten nicht geladen werden. Der Text gilt trotzdem.');
|
||||
return;
|
||||
}
|
||||
|
||||
/* Die drei Stufen -- aus der Antwort gebaut, nicht aus drei festen
|
||||
Zeilen. Wer eine vierte Stufe einbaut, bekommt sie hier von
|
||||
selbst; eine feste Liste haette sie unterschlagen, ohne dass
|
||||
irgendwo etwas rot wird. */
|
||||
const liste = $('stufen');
|
||||
if (liste && Array.isArray(r.stufen)) {
|
||||
const was = {
|
||||
neu: 'Lesen und bei Wünschen mitstimmen.',
|
||||
dabei: `Ab ${r.dabei_tage} Tagen: Du kannst im Treff schreiben, `
|
||||
+ 'Wünsche einstellen und dich bei „Mitmachen“ melden.',
|
||||
stamm: `Ab ${r.stamm_tage} Tagen und ${r.stamm_beitraege} eigenen Beiträgen: `
|
||||
+ 'Du kannst Clips und Bilder einstellen – sie erscheinen, sobald '
|
||||
+ 'jemand aus dem Team sie freigegeben hat.',
|
||||
};
|
||||
liste.textContent = '';
|
||||
for (const s of r.stufen) {
|
||||
const li = document.createElement('li');
|
||||
const stark = document.createElement('strong');
|
||||
stark.textContent = s.name;
|
||||
li.append(stark, ' – ', was[s.schluessel] || '');
|
||||
liste.append(li);
|
||||
}
|
||||
}
|
||||
|
||||
if ($('alter')) {
|
||||
$('alter').textContent =
|
||||
`Mindestalter: ${r.mindestalter} Jahre. Wer jünger ist, hat hier nichts `
|
||||
+ 'verloren – nicht weil wir es kompliziert machen wollen, sondern weil '
|
||||
+ 'der Treff zu einem Stream für Erwachsene gehört.';
|
||||
}
|
||||
|
||||
if ($('ausschluss')) {
|
||||
$('ausschluss').textContent =
|
||||
`Ein Modi kann eine Pause von bis zu ${r.modi_frist_tage} Tagen geben. `
|
||||
+ 'Alles, was länger gilt, und jeder endgültige Ausschluss ist DogFathers '
|
||||
+ 'Entscheidung. Jede dieser Entscheidungen wird mit Grund festgehalten – '
|
||||
+ 'auch, damit man sie zurücknehmen kann.';
|
||||
}
|
||||
})();
|
||||
})();
|
||||
@@ -12,19 +12,19 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -252,10 +252,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -147,10 +147,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -181,14 +181,14 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -89,11 +89,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,26 +12,26 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
|
||||
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
|
||||
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
|
||||
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
|
||||
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
|
||||
Dialog zu sehen bekam. -->
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -341,10 +341,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609111617"></script>
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617"></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617"></script>
|
||||
<script src="assets/js/chat.js?v=202609111617"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736"></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736"></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736"></script>
|
||||
<script src="assets/js/chat.js?v=202609111736"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -179,12 +179,12 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/content.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/content.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -30,15 +30,15 @@
|
||||
<link rel="preload" as="image" href="assets/img/buehne/crew-gate-hoch-580.webp"
|
||||
type="image/webp" media="(max-width: 899px)" />
|
||||
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<!-- Diese Wand steht NUR auf der Adresse von Team Dogi (ueberall sonst
|
||||
404, siehe crewWeiche). Sie darf das Haus deshalb direkt nennen
|
||||
statt ueber den Platzhalter haus.css zu gehen -- und sie MUSS es:
|
||||
Eine Zugangswand, die sich erst nach dem Umbiegen faerbt, waere
|
||||
die eine Seite, auf der man das Umfaerben sieht. -->
|
||||
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="gate gate--crew">
|
||||
@@ -252,6 +252,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/gate.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/gate.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,25 +13,25 @@
|
||||
"theme_color": "#06090f",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/img/app-symbole/crew-192.png?v=202609111617",
|
||||
"src": "/assets/img/app-symbole/crew-192.png?v=202609111736",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/crew-512.png?v=202609111617",
|
||||
"src": "/assets/img/app-symbole/crew-512.png?v=202609111736",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609111617",
|
||||
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609111736",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609111617",
|
||||
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609111736",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -108,10 +108,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
|
||||
type="image/webp" media="(max-width: 899px)" />
|
||||
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="gate">
|
||||
@@ -401,6 +401,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/gate.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/gate.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -377,11 +377,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kalender-abo.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kalender-abo.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -382,10 +382,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617"></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617"></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617"></script>
|
||||
<script src="assets/js/leistung.js?v=202609111617"></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736"></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736"></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736"></script>
|
||||
<script src="assets/js/leistung.js?v=202609111736"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -270,6 +270,33 @@
|
||||
<path fill="rgba(255,255,255,.4)" d="M12 2.6 14.4 9H9.6z"/>
|
||||
<path fill="none" stroke="rgba(216,222,233,.75)" stroke-width=".8" stroke-linejoin="round" d="M12 2.6 20 9l-3 11.4H7L4 9zM4 9h16M9.6 9 12 20.4 14.4 9"/>
|
||||
</g>
|
||||
|
||||
<!-- DIE COMMUNITY (11.09.2026).
|
||||
|
||||
Zwei Figuren, keine einzelne: Das Zeichen des Creators (#r-person)
|
||||
wäre das naheliegende gewesen und das falsche -- es steht bereits
|
||||
für eine Rolle, und zwei Rollen mit demselben Zeichen heißt, dass
|
||||
das Zeichen nichts mehr sagt.
|
||||
|
||||
Die hintere Figur ist leiser gesetzt und teilweise verdeckt. Das
|
||||
ist keine Zierde: Bei 22 Pixeln verschmelzen zwei gleich helle
|
||||
Formen zu einem Klecks. Erst der Helligkeitsunterschied macht
|
||||
daraus zwei Menschen.
|
||||
|
||||
Dieselbe Zeichenweise wie die anderen sechs -- gefüllte Flächen
|
||||
aus den vorhandenen Verläufen, eine dünne Kontur darüber. Ein
|
||||
Zeichen in einem anderen Stil fällt auf, ohne etwas zu sagen. -->
|
||||
<g id="r-leute">
|
||||
<circle fill="url(#rs-lila-silber)" opacity=".7" cx="15.9" cy="8.1" r="2.9"/>
|
||||
<path fill="url(#rs-lila-silber)" opacity=".7"
|
||||
d="M10.6 20.4v-2.2c0-2.6 2.4-4.3 5.3-4.3s5.3 1.7 5.3 4.3v2.2z"/>
|
||||
<circle fill="url(#rs-lila)" cx="9.2" cy="7.4" r="3.4"/>
|
||||
<path fill="url(#rs-lila)"
|
||||
d="M2.8 20.4v-2.5c0-3 2.9-5 6.4-5s6.4 2 6.4 5v2.5z"/>
|
||||
<path fill="none" stroke="rgba(216,222,233,.75)" stroke-width=".8"
|
||||
stroke-linejoin="round"
|
||||
d="M9.2 4a3.4 3.4 0 1 1 0 6.8 3.4 3.4 0 0 1 0-6.8M2.8 20.4v-2.5c0-3 2.9-5 6.4-5s6.4 2 6.4 5v2.5z"/>
|
||||
</g>
|
||||
</defs></svg>
|
||||
|
||||
<main class="inhalt inhalt--breit">
|
||||
@@ -396,10 +423,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -214,11 +214,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Wer sieht was · Creator Workspace</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/treff.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
|
||||
<div class="schleier" aria-hidden="true"></div>
|
||||
|
||||
<header class="kopfleiste">
|
||||
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
|
||||
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M14.5 5.5 8 12l6.5 6.5" />
|
||||
</svg>
|
||||
<span class="zurueck-knopf__text">Zurück</span>
|
||||
</button>
|
||||
<p class="marke"><span class="marke__text"><a class="zurueck" href="start.html">Creator Workspace</a> · Wer sieht was</span></p>
|
||||
<div class="kopfleiste__rechts">
|
||||
<span class="wer" id="wer">…</span>
|
||||
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="inhalt inhalt--breit">
|
||||
|
||||
<section class="kopf-zeile">
|
||||
<div>
|
||||
<p class="marke">Rechte · nur für dich</p>
|
||||
<h1 class="titel">Wer sieht was</h1>
|
||||
<p class="unterzeile">
|
||||
Nicht aufgeschrieben, sondern ausgerechnet – aus derselben Tafel,
|
||||
aus der die Schranke ihre Entscheidung holt.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
|
||||
|
||||
<!-- ==================================================================
|
||||
WARUM DIESE SEITE ÜBERHAUPT EXISTIERT
|
||||
|
||||
Filipe: „egal welche Rolle oder Person hinzugefügt wird, soll
|
||||
immer nur das sehen, was ich erlaube."
|
||||
|
||||
Dazu gehört, dass er es NACHSEHEN kann. Eine Regel, die man nur
|
||||
glauben kann, ist eine Zusage; eine, die man ansehen kann, ist
|
||||
eine Auskunft.
|
||||
|
||||
Und sie ist eine Gegenprobe: Wenn hier bei einer Rolle plötzlich
|
||||
zwanzig Punkte stehen, wo gestern drei waren, sieht man das in
|
||||
einer Sekunde. In 74 Rollenabfragen über 20 Module sieht man es
|
||||
nie.
|
||||
============================================================== -->
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Wie viele Seiten je Rolle</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
Das ist die Zahl, um die es geht. Eine neue Rolle startet bei
|
||||
<strong>null</strong> – sie bekommt nichts, bis es jemand einträgt.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="rechte-zahlen" id="zahlen">
|
||||
<li class="leise">wird geladen …</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Die ganze Tafel</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
Ein Punkt heißt: diese Rolle darf diese Seite öffnen. Leer heißt
|
||||
nein – und zwar auch dann, wenn niemand je darüber nachgedacht hat.
|
||||
Das ist der Unterschied zu vorher.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rechte-tafel" id="tafel" aria-busy="true">
|
||||
<p class="leise">wird geladen …</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Ohne Anmeldung</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
Diese Seiten sind offen – man kann sich schlecht anmelden, wenn die
|
||||
Anmeldeseite eine Anmeldung verlangt. Jede andere Seite ist zu.
|
||||
</p>
|
||||
<ul class="regel-liste" id="offen"><li class="leise">wird geladen …</li></ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- WER kopf.js LAEDT, MUSS AUCH wahl.js LADEN (Regel seit 07.09.2026).
|
||||
|
||||
Der Umschalter "Meine Sicht" in der Kopfleiste ist ein selbst
|
||||
gebautes Bedienelement; dahinter liegt ein unsichtbares Systemfeld
|
||||
fuer Vorleseprogramme. Ohne wahl.js faellt kopf.js auf dieses Feld
|
||||
zurueck -- ein grauer Kasten von 92 x 19 Pixeln mit weisser
|
||||
Schrift, mitten in einer dunklen Leiste.
|
||||
|
||||
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
|
||||
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
|
||||
die seit dem 07.09. jede Seite danach absucht. -->
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/rechte.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -106,11 +106,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/report.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/report.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -173,10 +173,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,21 +12,21 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
|
||||
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
|
||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -674,10 +674,10 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/start.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/start.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -85,11 +85,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -126,10 +126,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/team.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/team.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -110,10 +110,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/team.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/team.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/team.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/team.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<!-- `data-buehne` steht hier fest und nicht im Skript: Die Kachel zu
|
||||
@@ -84,9 +84,9 @@
|
||||
Kaputt ist dabei nichts -- er funktioniert. Genau deshalb faellt
|
||||
es niemandem auf, und deshalb prueft pruef-css-klassen die
|
||||
ABHAENGIGKEIT statt der Optik. Hier hat sie angeschlagen. -->
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/teamlage.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/teamlage.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Anmeldung · Der Treff</title>
|
||||
<!-- Die Beschreibung nennt WEDER Rollen NOCH Bereiche des Teams.
|
||||
Auf den beiden anderen Waenden steht dort, wofuer sie da sind, und
|
||||
das ist dort richtig: Wer sie liest, gehoert ohnehin dazu. Hier
|
||||
steht ein Mensch, der NICHT zum Team gehoert. Was er auf der
|
||||
Anmeldeseite liest, ist das Einzige, was er ueber das Haus
|
||||
erfaehrt, solange er den Code nicht hat.
|
||||
|
||||
UND DIESER KOMMENTAR HAT SICH SELBST ERWISCHT (11.09.2026): Er
|
||||
zaehlte urspruenglich als Beispiel auf, was auf der anderen Wand
|
||||
steht -- und nannte dabei eine Rolle des Hauses, in einer Datei,
|
||||
die jeder Besucher des Treffs im Quelltext lesen kann. Gefunden
|
||||
hat das nicht das Auge, sondern eine Zeile in pruef-treff, die
|
||||
genau danach sucht. Ein Kommentar in einer ausgelieferten Datei
|
||||
ist kein Notizzettel. -->
|
||||
<meta name="description" content="Der Treffpunkt rund um DogFather. Zugang nur mit persönlichem Code." />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||
<!-- Derselbe Ton wie die Kopfleiste dahinter, damit Fensterleiste und
|
||||
Seite eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
|
||||
<!-- Quer- und Hochformat getrennt vorgeladen, damit das Handy nicht die
|
||||
grosse Fassung zieht. WebP und nicht AVIF: Der ausliefernde Express 4
|
||||
bringt eine MIME-Tabelle ohne AVIF mit, die Dateien kaemen als
|
||||
application/octet-stream an und wuerden wegen nosniff verweigert --
|
||||
die Buehne bliebe leer. (Ausfuehrlich in crew-index.html.) -->
|
||||
<link rel="preload" as="image" href="assets/img/buehne/treff-gate-1920.webp"
|
||||
type="image/webp" media="(min-width: 900px)" />
|
||||
<link rel="preload" as="image" href="assets/img/buehne/treff-gate-hoch-580.webp"
|
||||
type="image/webp" media="(max-width: 899px)" />
|
||||
|
||||
<!-- DASSELBE APP-SYMBOL WIE TEAM DOGI, und das ist kein Sparen.
|
||||
Es zeigt den Husky -- das Zeichen des HAUSES, nicht das einer
|
||||
Rolle. Wer den Treff auf seinen Startbildschirm legt, legt sich
|
||||
Dogis Zeichen dorthin, und genau das ist gemeint. Unterschieden
|
||||
werden die beiden Apps durch den NAMEN darunter ("Der Treff" gegen
|
||||
"Team Dogi"), und niemand hat beide: Ein Konto der Community kommt
|
||||
auf crew. nicht herein, ein Konto des Teams hier nicht. -->
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<!-- Diese Wand steht NUR auf der Adresse des Treffs (ueberall sonst 404,
|
||||
siehe treffWeiche). Sie darf das Haus deshalb direkt nennen statt
|
||||
ueber den Platzhalter haus.css zu gehen -- und sie MUSS es: Eine
|
||||
Zugangswand, die sich erst nach dem Umbiegen faerbt, waere die eine
|
||||
Seite, auf der man das Umfaerben sieht. -->
|
||||
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<!-- ====================================================================
|
||||
`gate--crew` IST HIER KEIN VERSEHEN (11.09.2026).
|
||||
|
||||
Darin stehen die Abweichungen, die aus der UMGEFAERBTEN Buehne
|
||||
folgen: Der Anmeldeknopf laeuft von Lila nach Babyblau statt von
|
||||
Rot nach Blau, und die Karte nimmt die Farben des gemalten Rahmens
|
||||
auf -- ringsum blau. Die Buehne des Treffs ist dieselbe Szene in
|
||||
derselben Farbe; jede dieser Abweichungen gilt hier wortgleich.
|
||||
|
||||
Eine eigene Klasse `gate--treff` waere eine Kopie derselben Werte
|
||||
gewesen. Beim naechsten Nachbessern des Knopfes haette jemand eine
|
||||
der beiden verbessert -- und das ist genau der Fehler, gegen den
|
||||
in diesem Haus die halbe Dokumentation geschrieben ist.
|
||||
|
||||
`data-rolle` sagt dem Anmeldeskript, wofuer diese Wand da ist.
|
||||
Ohne Kachelreihe kaeme sonst eine leere Rolle beim Server an, und
|
||||
der antwortet darauf wie auf einen falschen Code: 401, ohne dass
|
||||
irgendwo etwas kaputt ist.
|
||||
================================================================= -->
|
||||
<body class="gate gate--crew gate--treff" data-rolle="gast">
|
||||
|
||||
<!-- ---------- Bühne: Bild + Schleier ---------------------------------- -->
|
||||
<div class="buehne" aria-hidden="true">
|
||||
<picture>
|
||||
<source media="(min-width: 900px)" type="image/webp"
|
||||
srcset="assets/img/buehne/treff-gate-960.webp 960w,
|
||||
assets/img/buehne/treff-gate-1280.webp 1280w,
|
||||
assets/img/buehne/treff-gate-1600.webp 1600w,
|
||||
assets/img/buehne/treff-gate-1920.webp 1920w,
|
||||
assets/img/buehne/treff-gate-2560.webp 2560w" sizes="100vw" />
|
||||
<source type="image/webp"
|
||||
srcset="assets/img/buehne/treff-gate-hoch-440.webp 440w,
|
||||
assets/img/buehne/treff-gate-hoch-580.webp 580w,
|
||||
assets/img/buehne/treff-gate-hoch-900.webp 900w" sizes="100vw" />
|
||||
<img class="buehne__bild" src="assets/img/buehne/treff-gate-1280.webp" alt=""
|
||||
width="1647" height="955" fetchpriority="high" decoding="async" />
|
||||
</picture>
|
||||
<div class="buehne__schleier"></div>
|
||||
</div>
|
||||
|
||||
<!-- ---------- Tafel ----------------------------------------------------
|
||||
Der Anker rechnet die Cover-Geometrie des Bildes nach, damit die
|
||||
Karte in der GEMALTEN Tafel sitzt und nicht ungefaehr dort. Er
|
||||
faengt keine Klicks ab; nur die Karte darin ist bedienbar.
|
||||
(Herleitung in crew-index.html und gate.css.) -->
|
||||
<div class="tafel-anker">
|
||||
<main class="tafel" id="tafel">
|
||||
<div class="tafel__kante" aria-hidden="true"></div>
|
||||
|
||||
<!-- EIN NAME, KEIN PAAR.
|
||||
|
||||
Die Wand von Team Dogi setzt oben "DogFather × Team Dogi" -- zwei,
|
||||
die das zusammen machen. Hier waere dasselbe falsch: Der Treff
|
||||
gehoert nicht DogFather und der Community gemeinsam, er ist der
|
||||
Ort, an dem sie sich treffen. Deshalb steht oben nur das Haus und
|
||||
darunter der Raum. -->
|
||||
<header class="kopf">
|
||||
<p class="marke marke--paar">
|
||||
<span class="marke__crew">DogFather Universe</span>
|
||||
</p>
|
||||
<h1 class="titel">Der Treff</h1>
|
||||
<p class="unterzeile">Schön, dass du da bist. Gib deinen Code ein.</p>
|
||||
</header>
|
||||
|
||||
<form class="formular" id="formular" novalidate>
|
||||
<label class="feldschild" for="code">
|
||||
Persönlicher Zugangscode <span class="fuer" id="fuer" hidden></span>
|
||||
</label>
|
||||
|
||||
<div class="feld">
|
||||
<input type="password" id="code" name="code" inputmode="text"
|
||||
autocomplete="current-password" spellcheck="false"
|
||||
placeholder="Code eingeben" aria-describedby="hinweis fehler" />
|
||||
<button type="button" class="feld__auge" id="auge"
|
||||
aria-label="Code anzeigen" aria-pressed="false">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path class="strich" d="M2.5 12S6 5.8 12 5.8 21.5 12 21.5 12 18 18.2 12 18.2 2.5 12 2.5 12Z"/>
|
||||
<circle class="strich" cx="12" cy="12" r="2.9"/>
|
||||
<path class="strich durch" d="m4 20 16-16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="hinweis" id="hinweis" hidden>Feststelltaste ist aktiv.</p>
|
||||
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
|
||||
|
||||
<button type="submit" class="knopf" id="knopf">
|
||||
<span class="knopf__text">Anmelden</span>
|
||||
<span class="knopf__laden" aria-hidden="true"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- KEIN "NOCH KEINEN CODE? HIER ANFRAGEN".
|
||||
|
||||
Das waere die naheliegende Zeile und genau die falsche: Sie macht
|
||||
aus einer verborgenen Adresse eine oeffentliche Anmeldestelle,
|
||||
und jeder, der die Adresse errät, haette einen Weg, Aufmerksamkeit
|
||||
zu verlangen. Codes werden persoenlich vergeben -- wer keinen hat,
|
||||
hat hier auch nichts zu klaeren. -->
|
||||
<p class="fuss">
|
||||
Verschlüsselte Verbindung · Zugriffe werden protokolliert
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/gate.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Meldungen & Maßnahmen · Der Treff</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/treff.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
|
||||
<div class="schleier" aria-hidden="true"></div>
|
||||
|
||||
<header class="kopfleiste">
|
||||
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
|
||||
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M14.5 5.5 8 12l6.5 6.5" />
|
||||
</svg>
|
||||
<span class="zurueck-knopf__text">Zurück</span>
|
||||
</button>
|
||||
<p class="marke"><span class="marke__text"><a class="zurueck" href="start.html">Der Treff</a> · Meldungen</span></p>
|
||||
<div class="kopfleiste__rechts">
|
||||
<span class="wer" id="wer">…</span>
|
||||
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="inhalt inhalt--breit">
|
||||
|
||||
<section class="kopf-zeile">
|
||||
<div>
|
||||
<p class="marke">Der Treff · Moderation</p>
|
||||
<h1 class="titel">Meldungen & Maßnahmen</h1>
|
||||
<p class="unterzeile">
|
||||
Was gemeldet wurde, was entfernt wurde und mit welcher Begründung –
|
||||
alles an einer Stelle.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
|
||||
|
||||
<!-- ==================================================================
|
||||
WARUM DIESE SEITE EXISTIERT
|
||||
|
||||
Die Schnittstellen dafür gab es zuerst, die Seite nicht. Eine
|
||||
Meldung, die nur über eine Schnittstelle abrufbar ist, ist keine
|
||||
Meldung — sie ist eine Zeile in einer Datenbank, die niemand
|
||||
liest. Der DSA verlangt in Artikel 16, dass Meldungen
|
||||
BEARBEITET werden; ohne eine Stelle, an der sie erscheinen,
|
||||
wäre das ein Versprechen an eine Schublade.
|
||||
|
||||
DREI ABSCHNITTE, IN DIESER REIHENFOLGE: Was liegt an (Meldungen)
|
||||
— was wurde getan (Entfernt) — wer ist gesperrt (Maßnahmen). Wer
|
||||
diese Seite öffnet, hat fast immer die erste Frage.
|
||||
============================================================== -->
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Offene Meldungen</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
Zu jeder Entscheidung gehört ein Satz. Er wird festgehalten –
|
||||
auch damit man später weiß, warum etwas so entschieden wurde.
|
||||
</p>
|
||||
</div>
|
||||
<div id="meldungen" aria-busy="true"><p class="leise">wird geladen …</p></div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Entfernt</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
Was entfernt wurde, und warum. Die Begründung überlebt den Beitrag –
|
||||
das ist der Sinn der Sache
|
||||
<span class="leise">(Digital Services Act, Artikel 17)</span>.
|
||||
</p>
|
||||
</div>
|
||||
<div id="entfernt" aria-busy="true"><p class="leise">wird geladen …</p></div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Die Mitglieder</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
Wer da ist, seit wann, auf welcher Stufe – und wer gerade eine Pause
|
||||
hat. Von hier aus wird gehandelt; die Stufe steht nur hier und
|
||||
<strong>nie</strong> neben dem Namen im Treff.
|
||||
</p>
|
||||
</div>
|
||||
<div id="mitglieder" aria-busy="true"><p class="leise">wird geladen …</p></div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Hinweise, Pausen, Ausschlüsse</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p id="leiter">…</p>
|
||||
</div>
|
||||
<div id="massnahmen" aria-busy="true"><p class="leise">wird geladen …</p></div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/treff-moderation.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,198 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>Regeln & Hilfe · Der Treff</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/treff.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein. Auf der Adresse des
|
||||
Treffs biegt der Server genau diesen Dateinamen auf crew-haus.css
|
||||
um, und die Seite kommt schon beim ERSTEN Abruf in Lila und
|
||||
Babyblau an -- ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
|
||||
<div class="schleier" aria-hidden="true"></div>
|
||||
|
||||
<header class="kopfleiste">
|
||||
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
|
||||
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M14.5 5.5 8 12l6.5 6.5" />
|
||||
</svg>
|
||||
<span class="zurueck-knopf__text">Zurück</span>
|
||||
</button>
|
||||
<p class="marke"><span class="marke__text"><a class="zurueck" href="start.html">Der Treff</a> · Regeln</span></p>
|
||||
<div class="kopfleiste__rechts">
|
||||
<span class="wer" id="wer">…</span>
|
||||
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="inhalt">
|
||||
|
||||
<section class="kopf-zeile">
|
||||
<div>
|
||||
<p class="marke">Der Treff</p>
|
||||
<h1 class="titel">Regeln & Hilfe</h1>
|
||||
<p class="unterzeile">
|
||||
Kurz, in normaler Sprache, und alles, was hier steht, gilt auch.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
|
||||
|
||||
<!-- ==================================================================
|
||||
WARUM DER TEXT HIER STEHT UND DIE ZAHLEN NICHT
|
||||
|
||||
Die Saetze sind Text und gehoeren in eine Datei. Die ZAHLEN
|
||||
darin (sieben Tage, dreissig Tage, drei Beitraege, drei
|
||||
Anschlaege, 18 Jahre) kommen vom Server -- sie stehen im Code,
|
||||
und ein Regeltext, der eine andere Zahl nennt als die Regel, ist
|
||||
schlimmer als gar keiner: Niemand merkt es, und wer es merkt,
|
||||
glaubt dem Text.
|
||||
|
||||
Die Platzhalter tragen deshalb "…" als Vorgabe. Kommt die
|
||||
Antwort nicht, steht dort sichtbar nichts -- und nicht
|
||||
versehentlich eine Zahl von gestern.
|
||||
============================================================== -->
|
||||
<section class="treff-abschnitt" id="willkommen">
|
||||
<h2 class="treff-abschnitt__titel">Schön, dass du da bist</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
Der Treff ist der Ort rund um DogFather: Hier steht, was gerade gilt,
|
||||
was ansteht, und hier redet ihr miteinander. Er ist nicht öffentlich –
|
||||
du bist hier, weil dir jemand einen Code gegeben hat.
|
||||
</p>
|
||||
<p class="leise">
|
||||
Gib diesen Code nicht weiter. Er gehört zu dir, und was damit
|
||||
geschrieben wird, steht unter deinem Namen.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Die fünf Regeln</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<ol class="regel-liste">
|
||||
<li><strong>Sei anständig.</strong> Keine Beleidigungen, keine Angriffe
|
||||
auf Menschen, keine Hetze. Streiten über eine Sache ist in Ordnung –
|
||||
über einen Menschen nicht.</li>
|
||||
<li><strong>Keine privaten Daten.</strong> Keine Adressen, keine
|
||||
Telefonnummern, keine Klarnamen von anderen. Auch nicht von dir
|
||||
selbst, es liest mehr mit, als du denkst.</li>
|
||||
<li><strong>Nichts Fremdes hochladen.</strong> Was du einstellst, muss
|
||||
von dir sein oder dir gehören.</li>
|
||||
<li><strong>Keine Werbung, kein Verkauf.</strong> Auch nicht für
|
||||
andere Streams.</li>
|
||||
<li><strong>Was hier passiert, bleibt hier.</strong> Keine
|
||||
Bildschirmfotos nach draußen.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Wie du weiterkommst</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
In der ersten Zeit kannst du lesen und bei Wünschen mitstimmen.
|
||||
Das ist kein Misstrauen gegen dich – es ist eines gegen
|
||||
Wegwerf-Zugänge.
|
||||
</p>
|
||||
<ul class="regel-liste" id="stufen">
|
||||
<li class="leise">wird geladen …</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Wenn etwas nicht stimmt</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>
|
||||
An jedem Beitrag, der nicht von dir ist, steht <strong>melden</strong>.
|
||||
Schreib in einem Satz, was daran nicht in Ordnung ist – ohne das
|
||||
können wir es nicht prüfen.
|
||||
</p>
|
||||
<p>
|
||||
Wir sehen uns jede Meldung an. Wird ein Beitrag entfernt, halten wir
|
||||
fest, <strong>warum</strong> – und die Person, die ihn geschrieben hat,
|
||||
erfährt den Grund. Das ist keine Höflichkeit, sondern
|
||||
<span class="leise">Artikel 17 des Digital Services Act</span>.
|
||||
</p>
|
||||
<p class="leise" id="kontakt">
|
||||
Du erreichst uns außerdem direkt: Schreib es auf das Brett
|
||||
<a href="bereich.html?b=treff">Der Treff</a> oder sag es einem Modi.
|
||||
</p>
|
||||
<p class="leise">
|
||||
Häufige Fragen und Antworten stehen auf dem Brett
|
||||
<a href="bereich.html?b=regeln">Regeln & Hilfe</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Was passiert, wenn jemand sich nicht daran hält</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p>Wir gehen der Reihe nach vor, und niemand fliegt ohne Ansage:</p>
|
||||
<ol class="regel-liste">
|
||||
<li><strong>Hinweis.</strong> Jemand sagt dir, was nicht ging.</li>
|
||||
<li><strong>Pause.</strong> Ein paar Tage nichts schreiben. Lesen
|
||||
geht weiter.</li>
|
||||
<li><strong>Ende.</strong> Der Zugang wird geschlossen.</li>
|
||||
</ol>
|
||||
<p class="leise" id="ausschluss">…</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="treff-abschnitt">
|
||||
<h2 class="treff-abschnitt__titel">Das Kleingedruckte – kurz</h2>
|
||||
<div class="treff-abschnitt__inhalt">
|
||||
<p id="alter">…</p>
|
||||
<p>
|
||||
<strong>Was gespeichert wird:</strong> dein Name, dein Zugangscode
|
||||
(verschlüsselt, niemand kann ihn lesen), was du schreibst, und wann
|
||||
du dich anmeldest. Mehr nicht – kein Standort, keine Werbe-IDs,
|
||||
keine Weitergabe an Dritte.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Du willst weg?</strong> Sag Bescheid. Dein Zugang wird
|
||||
geschlossen und deine Beiträge werden entfernt.
|
||||
</p>
|
||||
<p class="leise">
|
||||
Der Treff ist ein kleiner, geschlossener Bereich. Es gibt keine
|
||||
Direktnachrichten zwischen Mitgliedern – absichtlich: Ein
|
||||
öffentlicher Raum lässt sich moderieren, ein privater nicht.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- WER kopf.js LAEDT, MUSS AUCH wahl.js LADEN (Regel seit 07.09.2026).
|
||||
|
||||
Der Umschalter "Meine Sicht" in der Kopfleiste ist ein selbst
|
||||
gebautes Bedienelement; dahinter liegt ein unsichtbares Systemfeld
|
||||
fuer Vorleseprogramme. Ohne wahl.js faellt kopf.js auf dieses Feld
|
||||
zurueck -- ein grauer Kasten von 92 x 19 Pixeln mit weisser
|
||||
Schrift, mitten in einer dunklen Leiste.
|
||||
|
||||
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
|
||||
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
|
||||
die seit dem 07.09. jede Seite danach absucht. -->
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/treff-regeln.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "Der Treff",
|
||||
"short_name": "Der Treff",
|
||||
"description": "Der Treffpunkt rund um DogFather: Ansagen, Termine, Wuensche und das Gespraech. Nur mit persoenlichem Zugangscode.",
|
||||
"start_url": "/workspace/start.html",
|
||||
"id": "/workspace/start.html",
|
||||
"scope": "/workspace/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"lang": "de",
|
||||
"dir": "ltr",
|
||||
"background_color": "#0a121e",
|
||||
"theme_color": "#06090f",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/img/app-symbole/crew-192.png?v=202609111736",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/crew-512.png?v=202609111736",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609111736",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609111736",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,22 +12,22 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
|
||||
Dialog "Creator anlegen". -->
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -149,11 +149,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111617" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111617" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609111736" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609111736" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609111736" />
|
||||
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
|
||||
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
|
||||
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
|
||||
genau diesen Dateinamen auf crew-haus.css um, und die Seite
|
||||
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
|
||||
ohne Umfaerben im Browser, ohne Flackern. -->
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111617" />
|
||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609111736" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -190,10 +190,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609111617" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609111736" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609111736" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||