diff --git a/server/pruef-haus-trennung.mjs b/server/pruef-haus-trennung.mjs
index 14bffd05..9fb69f59 100644
--- a/server/pruef-haus-trennung.mjs
+++ b/server/pruef-haus-trennung.mjs
@@ -681,12 +681,50 @@ console.log("=== Alle Kategorien ===");
ok((beiHand?.zusatzrollen || []).length > 0,
"samt der Ueberschriften");
- /* SIE LIEST NUR. Jeder schreibende Weg bleibt zu -- gemessen, nicht
- angenommen, denn genau hier waere aus "sehen" versehentlich
- "verwalten" geworden. */
- const codeVersuch = await hole(`/workspace/api/verwaltung/personen/${idModi}/code`,
+ /* SIE DARF AUCH CODES NEU SETZEN (22.09.2026).
+ ------------------------------------------------------------------
+ Hier stand `=== 404` ("sie liest nur"). Filipe hat das umgedreht:
+ "die codes genau so sieht wie dogfather, damit sie das auch machen
+ kann wenn er live ist."
+
+ Ein Code wird genau einmal angezeigt. Geht er verloren, ist der
+ Zugang ohne diesen Weg tot -- und dann muesste doch wieder
+ DogFather ran, also genau das, was nicht sein soll. */
+ /* EIN FRISCHER MODI, KEIN GEBRAUCHTER (berichtigt 22.09.2026).
+ Hier stand `idModi` -- und der ist weiter oben laengst zur rechten
+ Hand befoerdert worden. Die Pruefung mass damit "darf sie an einer
+ rechten Hand etwas aendern" und nicht "darf sie an einem Modi".
+ Dieselbe Falle wie am 21.09. bei Ayla. */
+ const frischFuerCode = await hole("/workspace/api/verwaltung/personen",
+ { host: CREW, methode: "POST", keks: dogi.keks,
+ rumpf: JSON.stringify({ name: "Codeprobe", rolle: "modi" }) });
+ const idFuerCode = frischFuerCode.daten?.person?.id ?? frischFuerCode.daten?.id;
+ ok(!!idFuerCode, `ein frischer Modi zum Messen (${idFuerCode || "keiner"})`);
+ const codeVersuch = await hole(`/workspace/api/verwaltung/personen/${idFuerCode}/code`,
{ host: CREW, methode: "POST", keks: hand.keks, rumpf: "{}" });
- ok(codeVersuch.status === 404, `sie kann keinen Code neu setzen (${codeVersuch.status})`);
+ ok(codeVersuch.status === 200,
+ `sie kann einen Code neu setzen (${codeVersuch.status})`);
+ ok(typeof codeVersuch.daten?.code === "string" && codeVersuch.daten.code.length > 6,
+ `und sieht ihn dabei (${codeVersuch.daten?.code ? "ja" : "NEIN"})`);
+
+ /* DIE GRENZE DAZU: an einer LINKEN HAND aendert sie nichts. Ohne
+ diese Schranke haette sie einen Zugang in der Hand, der fast so
+ viel darf wie sie selbst -- und aufgefallen waere es nicht, denn
+ die alte Schranke kannte nur "admin" und "manager". */
+ {
+ const frisch = await hole("/workspace/api/verwaltung/personen",
+ { host: CREW, methode: "POST", keks: dogi.keks,
+ rumpf: JSON.stringify({ name: "Lino Zweit", rolle: "linke" }) });
+ const idLinke = frisch.daten?.person?.id ?? frisch.daten?.id;
+ if (idLinke) {
+ const anLinke = await hole(`/workspace/api/verwaltung/personen/${idLinke}/code`,
+ { host: CREW, methode: "POST", keks: hand.keks, rumpf: "{}" });
+ ok(anLinke.status === 403,
+ `an einer linken Hand aendert sie nichts (${anLinke.status})`);
+ } else {
+ ok(false, "die Gegenprobe konnte keine linke Hand anlegen -- nicht messbar");
+ }
+ }
/* ROLLEN WECHSELN DARF SIE SEIT DEM 20.09. -- aber nicht alles.
Filipe: "ich will dass ich da auch die rollen der leute wechseln
@@ -740,10 +778,24 @@ console.log("=== Alle Kategorien ===");
{ host: CREW, methode: "PUT", keks: hand.keks, rumpf: JSON.stringify({ rolle: "hand" }) });
ok(zuHoch.status === 400,
`eine zweite rechte Hand ernennt sie nicht (${zuHoch.status})`);
+ /* UND SIE LEGT PERSONEN AN (22.09.2026) -- Filipes ausdruecklicher
+ Wunsch. Hier stand `=== 404`. */
const anlegen = await hole("/workspace/api/verwaltung/personen",
{ host: CREW, methode: "POST", keks: hand.keks,
rumpf: JSON.stringify({ name: "Nochwer", rolle: "modi" }) });
- ok(anlegen.status === 404, `und niemanden anlegen (${anlegen.status})`);
+ ok(anlegen.status === 201, `sie legt einen Modi an (${anlegen.status})`);
+ ok(typeof (anlegen.daten?.code) === "string",
+ `und bekommt den Code zu sehen (${anlegen.daten?.code ? "ja" : "NEIN"})`);
+
+ /* ABER KEINE ZWEITE RECHTE HAND UND KEINE LINKE -- sonst waere
+ "sie darf anlegen" ein Weg, sich selbst hoehere Rechte zu bauen. */
+ for (const rolle of ["hand", "linke", "admin"]) {
+ const zuHochAnlegen = await hole("/workspace/api/verwaltung/personen",
+ { host: CREW, methode: "POST", keks: hand.keks,
+ rumpf: JSON.stringify({ name: "Zu hoch " + rolle, rolle }) });
+ ok(zuHochAnlegen.status >= 400,
+ `eine Rolle "${rolle}" legt sie nicht an (${zuHochAnlegen.status})`);
+ }
/* GEGENPROBE: DogFather darf all das sehr wohl -- sonst hiessen die
drei 404 oben auch dann gruen, wenn die Wege ueberhaupt kaputt
diff --git a/server/pruef-personen-formular.mjs b/server/pruef-personen-formular.mjs
index e3153ad5..cd931682 100644
--- a/server/pruef-personen-formular.mjs
+++ b/server/pruef-personen-formular.mjs
@@ -169,10 +169,25 @@ const mass = await dogi.evaluate(() => {
DIE ZEILE WURDE GEDREHT, NICHT GELOESCHT: Dass `admin` FEHLT, ist
jetzt eine eigene Aussage -- sonst haette die Liste unten still eine
Rolle mehr bekommen koennen, ohne dass es jemand merkt. */
-ok(mass.karten === 7, `alle sieben vergebbaren Rollen stehen zur Wahl (${mass.karten})`);
-ok(JSON.stringify([...mass.kartenRollen].sort())
- === JSON.stringify(["creator", "gast", "hand", "manager", "modi", "scout", "spicy"]),
- `und es sind die richtigen (${mass.kartenRollen.join(", ")})`);
+/* ABGELEITET, NICHT GEZAEHLT (berichtigt 22.09.2026).
+ ---------------------------------------------------------------------
+ Hier stand `=== 7` und eine abgeschriebene Liste von sieben Namen.
+ Am 21.09. kam die Rolle "linke" dazu, und beide Zeilen wurden rot,
+ obwohl das Formular richtig war -- gemessen acht Karten, erwartet
+ sieben. Die Pruefung war einen Tag lang rot und hat dabei nichts
+ gefunden ausser sich selbst.
+
+ Die eigentliche Aussage ist auch eine andere: Das Formular soll
+ GENAU DAS anbieten, was der Server auch annimmt. Zwei Listen an zwei
+ Stellen sind zwei Gelegenheiten, eine davon zu vergessen -- genau
+ daran ist es am 17.09. schon einmal gescheitert ("wieso kann ich
+ immer noch keine community rolle erstellen?"). */
+const w = await import("./workspace.js");
+const sollKarten = [...w.darfAnlegen({ rolle: "admin", haus: "agentur" })].sort();
+ok(mass.karten === sollKarten.length,
+ `alle ${sollKarten.length} vergebbaren Rollen stehen zur Wahl (${mass.karten})`);
+ok(JSON.stringify([...mass.kartenRollen].sort()) === JSON.stringify(sollKarten),
+ `und es sind genau die, die der Server annimmt (${mass.kartenRollen.join(", ")})`);
ok(!mass.kartenRollen.includes("admin"),
"DogFather steht NICHT zur Wahl -- auch nicht fuer DogFather");
@@ -413,6 +428,164 @@ const ERWARTET = 17;
console.log("");
ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`);
+/* ===================================================================
+ TEIL: DIE RECHTE HAND LEGT SELBST JEMANDEN AN (22.09.2026)
+ ===================================================================
+
+ Filipe: "dan will ich dass die rechte hand auch neue personen
+ hinzufuegen kann ... und die codes genau so sieht wie dogfather,
+ damit sie das auch machen kann wenn er live ist."
+
+ GEPRUEFT WIRD DER GANZE WEG, nicht die Rechtetabelle. Die sagt seit
+ heute "ja"; ob ein Mensch damit jemanden anlegen kann, ist eine
+ andere Frage. Beim ersten Anlauf war sie mit "nein" zu beantworten:
+ Die Seite entschied den Sendeweg an `ich.rolle === 'admin'`, und
+ fuer die rechte Hand gab es damit GAR KEINEN -- sie bekam "Fuer die
+ Rolle modi gibt es hier keinen Weg", einen Satz, der wie ein Fehler
+ im Formular klingt.
+ =================================================================== */
+console.log("\n— Die rechte Hand legt jemanden an —");
+{
+ /* EIGENER BROWSER AUF DER CREW-ADRESSE.
+ ------------------------------------------------------------------
+ 127.0.0.1 ist die AGENTURWAND -- dort gibt es die Kachel "rechte
+ Hand" gar nicht, und der erste Anlauf lief in eine
+ Zeitueberschreitung. Das war kein Befund, sondern die falsche Tuer.
+
+ Chromium zwingt `crew.dogfather-universe.com` auf https (die
+ Adresse steht in seiner eingebauten HSTS-Liste), deshalb ein
+ Vorbau mit eigenem Zertifikat -- derselbe Weg wie in
+ pruef-handy-teamdogi und pruef-zuteilung. */
+ const { execFileSync } = await import("node:child_process");
+ const { createServer: httpsServer } = await import("node:https");
+ const { request: httpAnfrage } = await import("node:http");
+ const { readFileSync: lesen } = await import("node:fs");
+ const HOST = "crew.dogfather-universe.com";
+ const sk = join(ordner, "pf.key"), zt = join(ordner, "pf.crt");
+ execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
+ "-keyout", sk, "-out", zt, "-days", "2", "-subj", `/CN=${HOST}`,
+ "-addext", `subjectAltName=DNS:${HOST}`], { stdio: "ignore" });
+ const HP = PORT + 1;
+ const vorbau = httpsServer({ key: lesen(sk), cert: lesen(zt) }, (von, nach) => {
+ const w = httpAnfrage({ host: "127.0.0.1", port: PORT, path: von.url,
+ method: von.method, headers: { ...von.headers, host: `${HOST}:${HP}` } },
+ (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
+ von.pipe(w);
+ });
+ await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
+ const CREW = `https://${HOST}:${HP}`;
+ const crewBrowser = await chromium.launch({
+ args: [`--host-resolver-rules=MAP ${HOST} 127.0.0.1`, "--ignore-certificate-errors"],
+ });
+ const kontext = await crewBrowser.newContext({ viewport: { width: 1440, height: 1000 } });
+ const hand = await kontext.newPage();
+ try {
+ await hand.goto(CREW + "/workspace/", { waitUntil: "domcontentloaded" });
+ await hand.click('.rolle[data-rolle="hand"]');
+ await hand.fill("#code", "CODE-HAND-0001");
+ await hand.click("button[type=submit]");
+ await hand.waitForURL("**/start.html", { timeout: 20000 });
+ await hand.goto(CREW + "/workspace/personen.html", { waitUntil: "networkidle" });
+ await hand.waitForTimeout(900);
+ await hand.click("#neu-oeffnen").catch(() => {});
+ await hand.waitForTimeout(500);
+
+ const bild = await hand.evaluate(async () => {
+ const a = await fetch("/workspace/api/ich").then((r) => r.json()).catch(() => ({}));
+ const f = document.getElementById("neu");
+ const auf = document.getElementById("neu-oeffnen");
+ return {
+ formDa: !!f && !f.hidden,
+ knopfDa: !!auf && !auf.hidden,
+ rollen: [...document.querySelectorAll("#neu .rollenwahl__knopf")]
+ .map((k) => k.dataset.rolle || k.getAttribute("data-rolle")).filter(Boolean),
+ woSonst: [...document.querySelectorAll(".rollenwahl__knopf")]
+ .map((k) => (k.closest("[id]") || {}).id + ":" + (k.dataset.rolle || "?")),
+ vomServer: a.darf_anlegen,
+ meineRolle: a.rolle,
+ };
+ });
+ console.log(" /api/ich sagt: rolle=" + bild.meineRolle
+ + " darf_anlegen=" + JSON.stringify(bild.vomServer)
+ + " Knopf=" + bild.knopfDa + " Form offen=" + bild.formDa);
+ console.log(" Rollenknoepfe: " + JSON.stringify(bild.woSonst));
+ ok(bild.formDa, "sie bekommt das Formular ueberhaupt zu sehen");
+ ok(bild.rollen.length > 0 && bild.rollen.every((r) => ["modi", "gast"].includes(r)),
+ `und genau die Rollen, die sie darf (${bild.rollen.join(", ") || "keine"})`);
+
+ /* JETZT WIRKLICH ANLEGEN. Ein Formular, das dasteht und beim
+ Absenden scheitert, ist schlimmer als keines. */
+ const name = "Neue Modi " + Date.now().toString().slice(-5);
+ await hand.evaluate((n) => {
+ const k = [...document.querySelectorAll(".rollenwahl__knopf")]
+ .find((x) => (x.dataset.rolle || "") === "modi");
+ if (k) k.click();
+ document.getElementById("f-name").value = n;
+ }, name);
+ await hand.waitForTimeout(250);
+ await hand.click("#neu button[type=submit]").catch(async () => {
+ await hand.evaluate(() => document.getElementById("neu").requestSubmit());
+ });
+ await hand.waitForTimeout(2200);
+
+ /* DIE ABSCHNITTE DER LISTE SIND ZUGEKLAPPT -- `innerText` sieht
+ zugeklappten Inhalt nicht. Dieselbe Falle wie am 21.09. bei
+ pruef-nachfrage. Erst aufklappen, dann messen. */
+ await hand.evaluate(() => {
+ for (const d of document.querySelectorAll("details")) d.open = true;
+ });
+ await hand.waitForTimeout(400);
+ const danach = await hand.evaluate((n) => {
+ const kasten = document.querySelector(".code-kasten, #code-kasten");
+ const text = document.body.innerText;
+ return {
+ fehler: (document.getElementById("neu-fehler") || {}).textContent || "",
+ codeDa: !!kasten && !kasten.hidden,
+ codeText: kasten ? (kasten.innerText || "").slice(0, 200) : "",
+ stehtInListe: text.includes(n),
+ listeDa: !!document.getElementById("liste"),
+ listeText: (document.getElementById("liste") || {}).innerText
+ ? document.getElementById("liste").innerText.replace(/\s+/g, " ").slice(-260) : "(leer)",
+ gesucht: n,
+ };
+ }, name);
+ ok(!danach.fehler, `kein Fehler beim Absenden (${danach.fehler || "keiner"})`);
+ ok(danach.codeDa, "der Zugangscode wird ihr angezeigt");
+ ok(/[A-Z0-9]{4}/.test(danach.codeText),
+ `und er sieht auch wie einer aus (${danach.codeText.replace(/\s+/g, " ").slice(0, 60)})`);
+ /* GEMESSEN WIRD DIE LISTE DES SERVERS, NICHT DER SICHTBARE TEXT
+ (berichtigt 22.09.2026).
+
+ Hier stand `document.body.innerText.includes(name)`. Die
+ Abschnitte der Personenliste sind zugeklappt und zeigen nur die
+ Anfangsbuchstaben -- der volle Name steht dort also gar nicht,
+ obwohl die Person laengst angelegt ist. Die Zeile war rot, und
+ kaputt war nur sie selbst.
+
+ Jetzt wird gefragt, was sie WIRKLICH bekommt: dieselbe Auskunft,
+ aus der die Liste gebaut wird. Das beweist beides auf einmal --
+ dass sie anlegen kann UND dass sie das Ergebnis sieht. */
+ const ausListe = await hand.evaluate(async (n) => {
+ const a = await fetch("/workspace/api/verwaltung/personen")
+ .then((r) => r.json()).catch(() => null);
+ return {
+ code: Array.isArray(a?.personen) ? 200 : 0,
+ dabei: (a?.personen || []).some((x) => x.name === n),
+ zahl: (a?.personen || []).length,
+ };
+ }, name);
+ ok(ausListe.code === 200,
+ `sie bekommt die Personenliste (${ausListe.zahl} Menschen)`);
+ ok(ausListe.dabei, ausListe.dabei
+ ? "und die eben angelegte Person steht darin"
+ : `die eben angelegte Person fehlt in ihrer Liste (${name})`);
+ } finally {
+ await kontext.close();
+ await crewBrowser.close();
+ await new Promise((r) => vorbau.close(r));
+ }
+}
+
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`);
diff --git a/server/workspace-personen.js b/server/workspace-personen.js
index e5ed89f7..f65574f3 100644
--- a/server/workspace-personen.js
+++ b/server/workspace-personen.js
@@ -128,6 +128,36 @@ function nurAdmin(req, res, next) {
WELCHE Rolle sie vergeben darf, steht NICHT hier -- das entscheidet
ANLEGBAR in der Route. Diese Zeile oeffnet nur die Tuer. */
+ /* DIE RECHTE HAND LEGT PERSONEN AN UND SIEHT IHRE CODES (22.09.2026).
+
+ Filipe: "damit sie das auch machen kann wenn er live ist."
+
+ NUR DIE RECHTE, NICHT DIE LINKE. `istHand()` wuerde beide treffen;
+ fuer die linke Hand ist "legt niemanden an" eine ausdrueckliche
+ Entscheidung vom 21.09. Deshalb steht hier die Rolle und nicht der
+ Sammelbegriff -- der haette die Entscheidung stillschweigend
+ umgedreht.
+
+ ZWEI WEGE, NICHT DIE GANZE VERWALTUNG: anlegen und einen Code neu
+ erzeugen. Sperren, Loeschen, Betreuung und Protokoll bleiben bei
+ DogFather. Und WELCHE Rolle sie anlegen darf, entscheidet nicht
+ diese Zeile, sondern ANLEGBAR in der Route -- diese Tuer oeffnet
+ nur den Weg.
+
+ WARUM AUCH "Code neu erzeugen": Ein Code wird genau einmal
+ angezeigt. Geht er verloren, ist der Zugang ohne diesen Weg tot --
+ und dann muesste doch wieder DogFather ran, also genau das, was
+ nicht sein soll. */
+ const anlegeWeg = req.method === "POST"
+ && req.baseUrl + req.path === "/workspace/api/verwaltung/personen";
+ const codeWeg = req.method === "POST"
+ && /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/code$/
+ .test(req.baseUrl + req.path);
+ if (person.rolle === "hand" && (anlegeWeg || codeWeg)) {
+ req.person = person;
+ return next();
+ }
+
const rollenWeg = req.method === "PUT"
&& /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/rolle$/
.test(req.baseUrl + req.path);
@@ -169,6 +199,25 @@ function nurDogFatherBeiLeitung(req, res, next) {
const id = Number(req.params.id);
if (!Number.isInteger(id)) return next();
const ziel = db().prepare("SELECT rolle FROM personen WHERE id = ?").get(id);
+
+ /* DIE RECHTE HAND NUR AN DENEN, DIE SIE AUCH ANLEGEN DUERFTE
+ (22.09.2026).
+
+ Ohne diese Zeile koennte sie den Code einer LINKEN HAND neu
+ erzeugen -- und haette damit einen Zugang, der fast so viel darf
+ wie sie selbst, in der Hand. Das ist nicht gemeint und waere auch
+ nicht aufgefallen: Die Schranke darunter kennt nur "admin" und
+ "manager".
+
+ Gefragt wird dieselbe Liste wie beim Anlegen. Eine zweite waere
+ die, die beim naechsten Umbau auseinanderlaeuft. */
+ if (req.person?.rolle === "hand" && ziel
+ && !darfAnlegen(req.person).includes(ziel.rolle)) {
+ return res.status(403).json({
+ fehler: "An dieser Rolle ändert die rechte Hand nichts.",
+ });
+ }
+
if (ziel && (ziel.rolle === "admin" || ziel.rolle === "manager")) {
return res.status(403).json({
fehler: "An DogFather und Managern ändert nur DogFather etwas.",
diff --git a/server/workspace.js b/server/workspace.js
index bad63857..274915ac 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -452,6 +452,24 @@ const ANLEGBAR = {
spicy: ["spicy", "manager", "scout", "creator"],
/* Ein Manager stellt Creator ein, die er dann auch betreut. */
manager: ["creator"],
+ /* DIE RECHTE HAND LEGT PERSONEN AN (22.09.2026).
+
+ Filipe: "dan will ich dass die rechte hand auch neue personen
+ hinzufuegen kann. also neue erstellen kann und die codes genau so
+ sieht wie dogfather, damit sie das auch machen kann wenn er live
+ ist."
+
+ DIESELBE LISTE, DIE SIE AUCH VERGEBEN DARF -- abgeleitet aus
+ ROLLEN_ZUM_AENDERN, nicht danebengeschrieben. Zwei Listen mit
+ denselben zwei Rollen waeren zwei Gelegenheiten, eine davon zu
+ aendern und die andere zu vergessen; dann duerfte sie eine Rolle
+ anlegen, aber nicht vergeben, und niemand wuesste warum.
+
+ WAS DAMIT AUSDRUECKLICH NICHT GEHT: eine zweite rechte Hand, eine
+ linke Hand oder einen zweiten DogFather. Sie kann sich also auch
+ ueber den Umweg "neuen Zugang anlegen" keine hoeheren Rechte
+ verschaffen. */
+ hand: ROLLEN_ZUM_AENDERN.hand,
/* DIE LINKE HAND LEGT NIEMANDEN AN (21.09.2026) -- ausdruecklich im
Auftrag. Sie stuende auch ohne diese Zeile auf [] (darfAnlegen
nimmt `?? []`), aber dann saehe es aus wie vergessen. Eine
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 9c14d79f..9c9cc12b 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`. -->
-
-
+
+
-
+