diff --git a/server/pruef-verteilen.mjs b/server/pruef-verteilen.mjs
new file mode 100644
index 00000000..d422ed1e
--- /dev/null
+++ b/server/pruef-verteilen.mjs
@@ -0,0 +1,174 @@
+/* =====================================================================
+ pruef-verteilen.mjs — Wer darf Aufgaben an andere geben?
+
+ Filipe, 20.09.2026: "jeder soll sich selber aufgabe vergeben koennen
+ aber nur die rechte hand und dogfather aufgaben an andere verteilen."
+
+ ---------------------------------------------------------------------
+ WARUM DAS EINE EIGENE PRUEFUNG BRAUCHT
+
+ Die Regel hat ZWEI Seiten, und beide muessen dasselbe sagen:
+
+ * Der Server lehnt ab, wer nicht darf.
+ * Die Seite zeigt die Felder nur dem, der darf.
+
+ Stimmt nur die erste, sieht die rechte Hand Felder, die nichts
+ bewirken. Stimmt nur die zweite, kommt jeder durch, der die
+ Schnittstelle direkt anspricht. Beides ist schon vorgekommen.
+
+ UND DIE GEFAEHRLICHE STELLE: `hand` steht bewusst NICHT in
+ `istLeitung` -- diese Menge wird an 57 Stellen gefragt, unter
+ anderem im vertraulichen Meldeweg. Die Pruefung haelt fest, dass
+ das Verteilen eine EIGENE Regel ist und nicht versehentlich zur
+ Leitung wird.
+
+ AUFRUF: node server/pruef-verteilen.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";
+import { notbremse } from "./helfer-notbremse.mjs";
+
+const PORT = await portMussFreiSein(4433, "pruef-verteilen");
+const ordner = mkdtempSync(join(tmpdir(), "ws-verteilen-"));
+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 }); };
+await import("./index.js");
+notbremse(180_000, "pruef-verteilen");
+await new Promise((r) => setTimeout(r, 900));
+
+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 ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
+const melde = (t) => console.log(t);
+
+console.log("\n=== Wer darf Aufgaben an andere geben? ===");
+
+/* ---- 1. Die Regel selbst, ohne Server ----------------------------- */
+melde("\n=== 1. Die Regel ===");
+{
+ const { darfAufgabenVerteilen, istLeitung } = await import("./workspace.js");
+ const darf = ["admin", "hand", "spicy", "manager"];
+ const nicht = ["modi", "scout", "creator", "gast"];
+ ok(darf.every((r) => darfAufgabenVerteilen({ rolle: r })),
+ `verteilen duerfen: ${darf.join(", ")}`);
+ ok(nicht.every((r) => !darfAufgabenVerteilen({ rolle: r })),
+ `und nicht: ${nicht.join(", ")}`);
+ /* DIE WICHTIGE ABGRENZUNG: Verteilen ist NICHT Leitung. Waere die
+ rechte Hand in `istLeitung` gerutscht, saehe sie damit auch
+ vertrauliche Meldungen ueber Menschen -- eine Entscheidung, die
+ niemand getroffen hat. */
+ ok(!istLeitung({ rolle: "hand" }),
+ "die rechte Hand darf verteilen, ist aber NICHT „Leitung“ — das bleibt getrennt");
+ ok(darfAufgabenVerteilen({ rolle: undefined }) === false
+ && darfAufgabenVerteilen(null) === false,
+ "Gegenprobe: ohne Rolle darf niemand");
+}
+
+/* ---- 2. Durch die Schnittstelle ----------------------------------- */
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
+const jetzt = new Date().toISOString();
+let idDogi, idHand, idModi, idModi2;
+{
+ const d = new DatabaseSync(process.env.WORKSPACE_DB);
+ const k = randomBytes(32).toString("hex");
+ d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,NULL)"
+ + " ON CONFLICT(schluessel) DO NOTHING").run("code_kennung_schluessel", k, jetzt);
+ const 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", k).update(code).digest("hex"), jetzt);
+ return d.prepare("SELECT last_insert_rowid() AS id").get().id;
+ };
+ idDogi = anlegen("Filipe", "admin", "CODE-A");
+ idHand = anlegen("VanVan", "hand", "CODE-H");
+ idModi = anlegen("Diene", "modi", "CODE-M");
+ idModi2 = anlegen("Ghost", "modi", "CODE-M2");
+ d.close();
+}
+
+const CREW = "crew.dogfather-universe.com";
+function ruf(pfad, keks, methode = "GET", koerper = null) {
+ return new Promise((fertig, schief) => {
+ const rumpf = koerper ? JSON.stringify(koerper) : null;
+ const a = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: pfad, method: methode,
+ headers: {
+ Host: CREW, ...(keks ? { Cookie: keks } : {}),
+ ...(rumpf ? { "Content-Type": "application/json", Origin: `https://${CREW}`,
+ "Content-Length": Buffer.byteLength(rumpf) } : {}),
+ },
+ }, (antwort) => {
+ let text = "";
+ antwort.on("data", (x) => { text += x; });
+ antwort.on("end", () => fertig({
+ code: antwort.statusCode, text,
+ keks: [].concat(antwort.headers["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "),
+ }));
+ });
+ a.on("error", schief);
+ if (rumpf) a.write(rumpf);
+ a.end();
+ });
+}
+const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
+const anmelden = async (rolle, code) =>
+ (await ruf("/workspace/api/anmelden", null, "POST", { rolle, code })).keks;
+
+melde("\n=== 2. Durch die Schnittstelle ===");
+const kDogi = await anmelden("admin", "CODE-A");
+const kHand = await anmelden("hand", "CODE-H");
+const kModi = await anmelden("modi", "CODE-M");
+ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet");
+
+/* Sagt der Server es auch der Seite? */
+for (const [name, keks, soll] of [["DogFather", kDogi, true], ["rechte Hand", kHand, true], ["Modi", kModi, false]]) {
+ const ich = json(await ruf("/workspace/api/ich", keks));
+ ok(ich.darf_verteilen === soll,
+ `„${name}“ bekommt darf_verteilen = ${ich.darf_verteilen} (erwartet ${soll})`);
+}
+
+/* Und die Tat: eine Aufgabe FUER JEMAND ANDEREN. */
+async function anlegenFuer(keks, zielId, titel) {
+ const a = await ruf("/workspace/api/aufgaben", keks, "POST",
+ { titel, verantwortlich_id: zielId, prioritaet: "mittel" });
+ if (a.code !== 201) return { code: a.code, verant: null };
+ const d = new DatabaseSync(process.env.WORKSPACE_DB);
+ const z = d.prepare("SELECT verantwortlich_id FROM aufgaben WHERE id = ?").get(json(a).id);
+ d.close();
+ return { code: a.code, verant: z?.verantwortlich_id ?? null };
+}
+
+const vonHand = await anlegenFuer(kHand, idModi, "Von der rechten Hand an Diene");
+ok(vonHand.code === 201 && vonHand.verant === idModi,
+ `die rechte Hand gibt eine Aufgabe an Diene (${vonHand.code}, liegt bei ${vonHand.verant})`);
+
+const vonDogi = await anlegenFuer(kDogi, idModi2, "Von DogFather an Ghost");
+ok(vonDogi.code === 201 && vonDogi.verant === idModi2,
+ `DogFather ebenso (${vonDogi.code}, liegt bei ${vonDogi.verant})`);
+
+/* DIE ENTSCHEIDENDE GEGENPROBE: Ein Modi versucht dasselbe. Die
+ Aufgabe wird NICHT abgelehnt -- sie landet bei ihm selbst. Das ist
+ freundlicher als ein Fehler und genau das, was Filipe wollte:
+ "jeder soll sich selber aufgabe vergeben koennen". */
+const vonModi = await anlegenFuer(kModi, idModi2, "Versuch eines Modi");
+ok(vonModi.code === 201 && vonModi.verant === idModi,
+ `Gegenprobe: ein Modi kann es versuchen — die Aufgabe landet bei ihm selbst `
+ + `(${vonModi.verant}, nicht ${idModi2})`);
+
+console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
+try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+process.exit(fehler ? 1 : 0);
diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js
index f2aea738..f17c82e8 100644
--- a/server/workspace-aufgaben.js
+++ b/server/workspace-aufgaben.js
@@ -10,7 +10,7 @@
import express from "express";
import {
- db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, betreuteIds,
+ db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, darfAufgabenVerteilen, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, betreuteIds,
externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds,
MODI_KATEGORIEN, kategorienFuer, kategoriePersonen,
KANAELE, kanaeleFuer,
@@ -742,7 +742,12 @@ aufgabenRouter.post("/workspace/api/aufgaben", gleicheHerkunft, (req, res) => {
/* Wer nicht Management ist, darf ausschließlich für sich selbst
anlegen -- egal, was im Aufruf steht. */
- if (!istLeitung(req.person)) {
+ /* VERTEILEN DARF NUR, WER ES DARF -- alle anderen legen die
+ Aufgabe auf sich selbst, egal was im Aufruf steht. Seit dem
+ 20.09.2026 gehoert die rechte Hand dazu (darfAufgabenVerteilen),
+ ein Modi nicht: Er kann sich Aufgaben notieren, aber keine an
+ andere geben. */
+ if (!darfAufgabenVerteilen(req.person)) {
aus.creator_id = req.person.rolle === "creator" ? req.person.id : null;
aus.verantwortlich_id = req.person.id;
}
diff --git a/server/workspace-personen.js b/server/workspace-personen.js
index 5864d969..313848c8 100644
--- a/server/workspace-personen.js
+++ b/server/workspace-personen.js
@@ -19,7 +19,7 @@ import express from "express";
naechsten Umzug stehen bleibt. */
import { anmeldeAdresseFuer } from "./crew-adresse.js";
import {
- db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtModis, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, TEAM_DOGI_ROLLEN, darfAnlegen, darfRollenWechseln, hausBedingung,
+ db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtModis, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, TEAM_DOGI_ROLLEN, darfAnlegen, darfRollenWechseln, darfRolleAendern, rollenZumAendern, hausBedingung,
} from "./workspace.js";
import { sicherungJetzt } from "./workspace-sicherung.js";
@@ -886,6 +886,23 @@ personenRouter.put("/workspace/api/verwaltung/personen/:id/rolle", gleicheHerkun
const person = db().prepare("SELECT id, name, rolle, aktiv FROM personen WHERE id = ?").get(id);
if (!person) return res.status(404).json({ fehler: "nicht_gefunden" });
+ /* ==== DARF ICH DIESEN MENSCHEN ANFASSEN? (20.09.2026) =========
+
+ Seit die rechte Hand Rollen aendern darf, genuegt "ist
+ angemeldet und kam durch die Schranke" nicht mehr. Die Regel
+ steht in darfRolleAendern() -- an EINER Stelle, damit die
+ Oberflaeche und der Server nicht auseinanderlaufen.
+
+ Sie beantwortet drei Fragen auf einmal: nicht die eigene Rolle,
+ nicht DogFather, und fuer die rechte Hand auch keine zweite
+ rechte Hand. */
+ if (!darfRolleAendern(req.person, person)) {
+ return res.status(403).json({ fehler: "Diese Rolle darfst du nicht ändern." });
+ }
+ if (!rollenZumAendern(req.person).includes(rolle)) {
+ return res.status(403).json({ fehler: "Diese Rolle vergibst du nicht." });
+ }
+
/* Dieselbe Auskunft wie beim Anlegen: Wer eine Rolle nicht
vergeben darf, darf sie auch nicht zuweisen. Sonst waere das
Zuweisen der bequemere Weg an der Regel vorbei. */
diff --git a/server/workspace.js b/server/workspace.js
index 89b67e61..f813f210 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -212,6 +212,93 @@ const LEITUNG = new Set(["spicy", "admin", "manager"]);
export const istLeitung = (person) => !!person && LEITUNG.has(person.rolle);
export const istDogFather = (person) => !!person && person.rolle === "admin";
+/* ===== WER DARF AUFGABEN AN ANDERE VERTEILEN? (20.09.2026) ==========
+
+ Filipe: "jeder soll sich selber aufgabe vergeben koennen aber nur die
+ rechte hand und dogfather aufgaben an andere verteilen."
+
+ WARUM EINE EIGENE REGEL UND NICHT `hand` IN DIE LEITUNG:
+ `istLeitung` wird an 57 Stellen in 19 Dateien gefragt -- unter
+ anderem im vertraulichen Meldeweg, in der Personenverwaltung und in
+ den Auswertungen ueber Menschen. Die rechte Hand dort ueberall
+ mitzunehmen waere eine Entscheidung ueber Einsicht in fremde
+ Meldungen, und die hat niemand getroffen.
+
+ Verteilen ist eine Frage fuer sich. Sie bekommt deshalb ihren
+ eigenen Namen: Man sieht an der Aufrufstelle, worum es geht, und
+ wer sie spaeter aendert, aendert genau diese eine Sache.
+
+ spicy und manager bleiben drin: Auf der Agenturseite verteilen sie
+ seit jeher Aufgaben, und daran soll sich nichts aendern. */
+export const darfAufgabenVerteilen = (person) =>
+ istLeitung(person) || person?.rolle === "hand";
+
+/* ===== WER DARF EINE ROLLE AENDERN -- UND ZU WELCHER? (20.09.2026) ==
+
+ Filipe: "ich will dass ich da auch die rollen der leute wechseln kann
+ ohne dass ich denen einen neuen account machen muss mit einem neuen
+ code. einfach rolle wechseln und gut ist. perfektionier das fuer die
+ rolle dogfather und rechte hand. nur die sollen das machen koennen."
+
+ Das Wechseln selbst gab es schon -- der Zugangscode bleibt dabei
+ derselbe, das stand auch in der Rueckfrage. Es konnte nur DogFather.
+
+ DREI GRENZEN, UND JEDE HAT EINEN GRUND:
+
+ (1) NIEMAND WIRD ZU "admin". Das gilt seit dem 11.09.2026 fuer alle,
+ auch fuer DogFather selbst -- siehe ANLEGBAR. Hier aendert sich
+ daran nichts.
+
+ (2) DIE RECHTE HAND FASST KEINE ROLLE AN, DIE UEBER ODER NEBEN IHR
+ STEHT. Sie kann also einen Modi zur Community machen und
+ umgekehrt -- aber keinen DogFather antasten und keine zweite
+ rechte Hand ernennen. Wer jemanden auf die eigene Ebene hebt,
+ vergibt Vertrauen, das ihm nicht gehoert; das bleibt bei
+ DogFather. Und wer eine Rolle UEBER sich aendern koennte,
+ koennte sich selbst befoerdern, indem er zuerst den anderen
+ herabstuft.
+
+ (3) NIEMAND AENDERT DIE EIGENE ROLLE. Sonst waere jede Grenze oben
+ nur ein Umweg: erst sich selbst hochstufen, dann alles duerfen.
+
+ Was sie danach vergeben darf, steht in `rollenZumAendern` -- an
+ EINER Stelle, damit die Oberflaeche keine zweite Liste fuehrt. Genau
+ daran ist es am 10.09.2026 schon einmal gescheitert. */
+const ROLLEN_ZUM_AENDERN = {
+ /* DogFather: alles, was er auch anlegen darf. */
+ admin: null, // null = dieselbe Liste wie ANLEGBAR
+ /* SPICY MEDIA STAND HIER SCHON, BEVOR ES DIESE TABELLE GAB -- sie
+ durfte Rollen wechseln, seit es den Weg gibt. Sie muss deshalb
+ drinstehen, sonst nimmt die neue Regel ihr etwas weg, das niemand
+ ihr wegnehmen wollte. `null` heisst: dasselbe wie beim Anlegen,
+ also Agenturrollen. */
+ spicy: null,
+ hand: ["modi", "gast"],
+};
+
+/** Welche Rollen darf diese Person an ANDERE vergeben? */
+export function rollenZumAendern(person) {
+ if (!person) return [];
+ const eigen = ROLLEN_ZUM_AENDERN[person.rolle];
+ if (eigen === undefined) return [];
+ return eigen === null ? darfAnlegen(person) : [...eigen];
+}
+
+/** Darf diese Person die Rolle DIESES Menschen anfassen? */
+export function darfRolleAendern(person, ziel) {
+ if (!person || !ziel) return false;
+ if (person.id === ziel.id) return false; // (3)
+ if (!rollenZumAendern(person).length) return false;
+ if (person.rolle === "admin") return ziel.rolle !== "admin";
+ /* Spicy Media wie bisher: das ganze Haus ausser DogFather. */
+ if (person.rolle === "spicy") return ziel.rolle !== "admin";
+ if (person.rolle === "hand") {
+ /* (2): nur unter sich -- nicht admin, nicht eine andere hand. */
+ return ziel.rolle !== "admin" && ziel.rolle !== "hand";
+ }
+ return false;
+}
+
/* =======================================================================
WER DARF WEN ANLEGEN — die einzige Liste dazu (10.09.2026)
@@ -335,8 +422,20 @@ export function darfAnlegen(person) {
* wechseln" und "darf DIESE Rolle vergeben" sind zwei Fragen, und
* ihre Antworten laufen auseinander, sobald eine Rolle dazukommt.
*/
+/* WER DARF DEN WEG "Rolle aendern" UEBERHAUPT BETRETEN?
+ *
+ * DIE RECHTE HAND KAM AM 20.09.2026 DAZU. Filipe: "perfektionier das
+ * fuer die rolle dogfather und rechte hand. nur die sollen das machen
+ * koennen."
+ *
+ * DIESE ZEILE OEFFNET NUR DIE TUER. Wen sie anfassen darf und wozu,
+ * entscheidet darfRolleAendern() / rollenZumAendern() weiter oben --
+ * dort steht auch, warum die rechte Hand keine zweite rechte Hand
+ * ernennen kann. Zwei getrennte Fragen, zwei getrennte Regeln: Wer
+ * hier etwas aendert, oeffnet keine Grenze versehentlich mit. */
export const darfRollenWechseln = (person) =>
- !!person && (person.rolle === "admin" || person.rolle === "spicy");
+ !!person && (person.rolle === "admin" || person.rolle === "spicy"
+ || person.rolle === "hand");
/* WER FUEHRT TEAM DOGI? (10.09.2026)
*
@@ -1061,27 +1160,8 @@ const TREFF_BEREICHE = [
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",
- /* ZEIGT JETZT AUF DIE SEITE, DIE DIE ANTWORT HAT (19.09.2026).
-
- Sie fuehrte auf `bereich.html?b=regeln` -- ein Brett, auf das die
- Leitung Regeln eintragen KANN und das am ersten Tag leer ist
- ("Hier stehen die Regeln des Treffs -- sobald sie eingetragen
- sind.").
-
- Die Regeln, die es WIRKLICH gibt, stehen auf `treff-regeln.html`:
- Begruessung, die fuenf Regeln, die drei Stufen, das Mindestalter,
- was bei Aerger zu tun ist. Diese Seite war von genau EINER Stelle
- aus erreichbar -- einem kleinen Textlink unter der Unterzeile
- einer Brettseite. Keine der elf Kacheln fuehrte hin.
-
- Damit war die einzige "So laeuft das hier"-Seite des Hauses fuer
- einen neuen Zuschauer praktisch unsichtbar, waehrend die Kachel,
- die ihren Namen trug, auf eine leere Liste zeigte.
-
- Das Brett gibt es weiter; treff-regeln.html verlinkt darauf. Ein
- Ort fuer die Frage, nicht zwei. */
- zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" },
+ { ...TREFF_GRUPPE, name: "Unsere Seiten", unter: "Website, Shop & dein Rabatt",
+ zeichen: "hinaus", ton: 38, ziel: "unsere-seiten.html", szene: "portal" },
{ ...TREFF_GRUPPE, name: "Mitmachen", unter: "Wie du dazugeh\u00f6ren kannst",
zeichen: "personen", ton: 32, ziel: "bereich.html?b=mitmachen", szene: "garage" },
/* UNSERE SEITEN (19.09.2026) -- die einzige Kachel, die HINAUSFUEHRT.
@@ -1104,8 +1184,27 @@ const TREFF_BEREICHE = [
Kachel sind es fuer die Community 7 + 1 Kacheln = 9 Plaetze, also
genau drei volle Reihen. Vorher waren es acht Plaetze und damit
eine Luecke am Ende. Sie passt also nicht nur inhaltlich. */
- { ...TREFF_GRUPPE, name: "Unsere Seiten", unter: "Website, Shop & dein Rabatt",
- zeichen: "hinaus", ton: 38, ziel: "unsere-seiten.html", szene: "portal" },
+ { ...TREFF_GRUPPE, name: "Regeln & Hilfe", unter: "Wie es hier l\u00e4uft",
+ /* ZEIGT JETZT AUF DIE SEITE, DIE DIE ANTWORT HAT (19.09.2026).
+
+ Sie fuehrte auf `bereich.html?b=regeln` -- ein Brett, auf das die
+ Leitung Regeln eintragen KANN und das am ersten Tag leer ist
+ ("Hier stehen die Regeln des Treffs -- sobald sie eingetragen
+ sind.").
+
+ Die Regeln, die es WIRKLICH gibt, stehen auf `treff-regeln.html`:
+ Begruessung, die fuenf Regeln, die drei Stufen, das Mindestalter,
+ was bei Aerger zu tun ist. Diese Seite war von genau EINER Stelle
+ aus erreichbar -- einem kleinen Textlink unter der Unterzeile
+ einer Brettseite. Keine der elf Kacheln fuehrte hin.
+
+ Damit war die einzige "So laeuft das hier"-Seite des Hauses fuer
+ einen neuen Zuschauer praktisch unsichtbar, waehrend die Kachel,
+ die ihren Namen trug, auf eine leere Liste zeigte.
+
+ Das Brett gibt es weiter; treff-regeln.html verlinkt darauf. Ein
+ Ort fuer die Frage, nicht zwei. */
+ zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" },
];
/* MELDUNGEN & MASSNAHMEN -- FUER DAS TEAM, NICHT FUER DIE COMMUNITY
@@ -5249,6 +5348,29 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => {
`ich.rolle === 'admin'`, und dieselbe Zeile schaltete auch das
Loeschen frei; die beiden gehoeren nicht zusammen. */
darf_rollen_wechseln: darfRollenWechseln(person),
+ /* ZWEI LISTEN STATT EINER RECHNUNG IM BROWSER (20.09.2026).
+
+ Die Rollenwahl nahm bisher die Knoepfe des Anlege-Formulars --
+ also `darf_anlegen`. Fuer die rechte Hand ist das leer (sie legt
+ niemanden an), und damit waere die Wahl leer geblieben, obwohl
+ sie Rollen aendern darf. Ein Recht, das man hat und nicht
+ ausueben kann, ist keines.
+
+ `rollen_zum_aendern` = wozu darf ich machen
+ `rollen_anfassbar` = wessen Rolle darf ich ueberhaupt anfassen
+
+ Beide aus denselben Funktionen, die auch die Route fragt. Die
+ Oberflaeche fuehrt damit keine eigene Liste mehr -- genau daran
+ ist es am 10.09.2026 schon einmal gescheitert, als zwei Listen
+ drei Zeilen auseinander einander widersprachen. */
+ rollen_zum_aendern: rollenZumAendern(person),
+ /* [...ROLLEN] UND NICHT ROLLEN_REIHE: Die Reihe ist die
+ SORTIERUNG, und in ihr fehlt "gast" mit Absicht. Wer sie hier
+ nimmt, verliert die Community stillschweigend -- genau diese
+ Verwechslung hat am 17.09.2026 schon einmal dafuer gesorgt, dass
+ sich niemand zu "Community" machen liess. */
+ rollen_anfassbar: [...ROLLEN].filter((r) =>
+ darfRolleAendern(person, { id: -1, rolle: r })),
/* OB DER CHAT-KNOPF IN DER KOPFLEISTE ERSCHEINT (18.09.2026).
Er wurde auf JEDER Seite gebaut. Fuer ein Mitglied der Community
@@ -5280,6 +5402,17 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => {
Rechnungen waeren die Stelle, an der die Seite ein Telefon
anbietet, das der Server ablehnt. */
darf_anrufen: darfTelefonieren(person),
+ /* DARF ICH AUFGABEN AN ANDERE GEBEN? (20.09.2026)
+
+ AUSDRUECKLICH GESAGT, NICHT ERRATEN. Das Aufgabenbrett fragte
+ bisher `LEITUNG.has(ich.rolle)` -- eine zweite Fassung derselben
+ Regel im Browser. Seit die rechte Hand verteilen darf, waere sie
+ die falsche gewesen: Der Server haette es erlaubt, die Seite
+ haette die Felder nicht gezeigt. Eine Berechtigung, die man
+ hat und nicht sieht, ist keine.
+
+ Aus DERSELBEN Funktion, die auch die Route benutzt. */
+ darf_verteilen: darfAufgabenVerteilen(person),
/* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN.
Fuer die fuenf bekannten Rollen steht hier `null`, und die
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 16513e64..1d9df34a 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+