diff --git a/server/mess-hand-personen.mjs b/server/mess-hand-personen.mjs
new file mode 100644
index 00000000..60338070
--- /dev/null
+++ b/server/mess-hand-personen.mjs
@@ -0,0 +1,242 @@
+/* =====================================================================
+ WAS SIEHT DIE RECHTE HAND AUF DER PERSONENSEITE? (25.09.2026)
+
+ ---------------------------------------------------------------------
+ WARUM ES DIESE DATEI GIBT
+
+ Filipe, zum wiederholten Mal: „die rechte hand sieht das immer noch
+ nicht obwohl ich will dass die rechte hand das auch sieht." Dazu ein
+ Bildschirmfoto der Personenseite -- Rollenkarten oben, „Letzte
+ Ereignisse" unten.
+
+ ICH KOENNTE RATEN, welches „das" gemeint ist. Am 24.09.2026 habe ich
+ genau das getan, auf ein Bildschirmfoto hin an der falschen Seite
+ gebaut und es im Commit selbst notiert („ZUERST EIN IRRTUM VON MIR").
+ Diesmal wird nachgesehen, bevor gebaut wird: Zwei Anmeldungen,
+ dieselbe Seite, und die Unterschiede werden AUFGEZAEHLT.
+
+ SIE PRUEFT NICHTS und meldet keinen Fehlschlag. Sie macht eine Liste
+ und zwei Bilder, auf denen man nachsehen kann. Deshalb `mess-` und
+ nicht `pruef-`: Die Portvergabe leitet sich aus den `pruef-`-Namen
+ ab, und eine Messdatei darf die Nummern der Pruefungen nicht
+ verschieben.
+
+ EIGENE WEGWERF-DATENBANK, NIE DIE ECHTE. Es entstehen echte Personen
+ und echte Protokollzeilen; in einem laufenden System waeren das
+ Testdaten, und die sind verboten.
+
+ Aufrufen mit: node server/mess-hand-personen.mjs
+ ===================================================================== */
+import { mkdtempSync, rmSync, readFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { execFileSync } from "node:child_process";
+import { request as httpAnfrage } from "node:http";
+import { createServer as httpsServer } from "node:https";
+
+const PORT = 5405, HP = 5406;
+const CREW = "crew.dogfather-universe.com";
+const ordner = mkdtempSync(join(tmpdir(), "ws-hand-pers-"));
+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, "mess-hand-personen");
+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); });
+
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
+const d = new DatabaseSync(process.env.WORKSPACE_DB);
+const jetzt = new Date().toISOString();
+const sch = randomBytes(32).toString("hex");
+d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
+ .run("code_kennung_schluessel", sch, jetzt);
+
+const anlegen = (name, rolle, code) => {
+ const s = randomBytes(16).toString("hex");
+ const h = scryptSync(code, s, 64,
+ { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+ return d.prepare("INSERT INTO personen"
+ + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ + " VALUES (?,?,?,?,?,?,1,?) RETURNING id")
+ .get(name, rolle, h, s, 32768,
+ createHmac("sha256", sch).update(code).digest("hex"), jetzt).id;
+};
+
+/* Dieselbe Besetzung wie auf Filipes Bildschirmfoto: beide Haende,
+ mehrere Modis, mehrere aus der Community. */
+anlegen("Filipe", "admin", "CODE-DOGI-0001");
+anlegen("VanVan", "hand", "CODE-HAND-0001");
+anlegen("Kessi", "linke", "CODE-LINKE-0001");
+anlegen("Diene", "modi", "CODE-MODI-0001");
+anlegen("Marina", "modi", "CODE-MODI-0002");
+anlegen("Ghost", "modi", "CODE-MODI-0003");
+anlegen("Anni", "gast", "CODE-GAST-0001");
+anlegen("Cleo", "gast", "CODE-GAST-0002");
+/* Ein paar Protokollzeilen, damit „Letzte Ereignisse" nicht leer ist --
+ sonst sieht „sieht nichts" bei beiden Rollen gleich aus. */
+for (let i = 0; i < 6; i++) {
+ d.prepare("INSERT INTO protokoll (zeitpunkt, person_id, rolle, aktion, detail, ip)"
+ + " VALUES (?,1,'admin','einstellung_geaendert',?,'127.0.0.1')")
+ .run(new Date(Date.now() - i * 60000).toISOString(), `probe_${i}`);
+}
+d.close();
+
+const anfrage = (pfad, art, koerper, keks) => new Promise((fertig, schief) => {
+ const a = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: pfad, method: art,
+ headers: {
+ Host: CREW, "Content-Type": "application/json",
+ ...(keks ? { Cookie: keks } : {}),
+ },
+ }, (r) => {
+ let t = ""; r.on("data", (x) => { t += x; });
+ r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t }));
+ });
+ a.on("error", schief);
+ if (koerper) a.write(JSON.stringify(koerper));
+ a.end();
+});
+
+const anmelden = async (rolle, code) => {
+ const a = await anfrage("/workspace/api/anmelden", "POST", { rolle, code });
+ if (a.code !== 200) {
+ console.log(`ABBRUCH: ${rolle} ließ sich nicht anmelden (HTTP ${a.code}) ${a.text.slice(0, 120)}`);
+ process.exit(2);
+ }
+ return (a.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
+};
+
+const kekse = {
+ DogFather: await anmelden("admin", "CODE-DOGI-0001"),
+ "rechte Hand": await anmelden("hand", "CODE-HAND-0001"),
+};
+
+/* ---- Erst die ANTWORTEN vergleichen, dann den Bildschirm ------------
+ Wenn der Server schon verschieden antwortet, muss man im Browser gar
+ nicht mehr suchen. */
+console.log("\n=== Was der Server den beiden gibt ===");
+for (const [wer, keks] of Object.entries(kekse)) {
+ const p = await anfrage("/workspace/api/verwaltung/personen", "GET", null, keks);
+ const prot = await anfrage("/workspace/api/verwaltung/protokoll?anzahl=25", "GET", null, keks);
+ let leute = 0, rollen = "";
+ try {
+ const j = JSON.parse(p.text);
+ leute = (j.personen || []).length;
+ rollen = [...new Set((j.personen || []).map((x) => x.rolle))].join(", ");
+ } catch { /* zeigt sich am Code */ }
+ let zeilen = -1;
+ try { zeilen = (JSON.parse(prot.text).eintraege || []).length; } catch { /* egal */ }
+ console.log(` ${wer.padEnd(12)} Personen HTTP ${p.code} → ${leute} Leute (${rollen})`);
+ console.log(` ${"".padEnd(12)} Protokoll HTTP ${prot.code} → ${zeilen < 0 ? prot.text.slice(0, 60) : zeilen + " Zeilen"}`);
+}
+
+const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
+execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl,
+ "-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
+{ stdio: "ignore" });
+const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) },
+ (von, nach) => {
+ const w = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
+ headers: { ...von.headers, host: `${CREW}:${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 pw = await import(
+ "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await pw.chromium.launch({
+ args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
+});
+const MARKE = process.env.MARKE || "hand";
+const befund = {};
+try {
+ for (const [wer, keks] of Object.entries(kekse)) {
+ const kontext = await browser.newContext({ viewport: { width: 1440, height: 1500 } });
+ await kontext.addCookies(keks.split("; ").map((k) => {
+ const [n, ...r] = k.split("=");
+ return { name: n, value: r.join("="), domain: CREW, path: "/" };
+ }));
+ const seite = await kontext.newPage();
+ const konsole = [];
+ seite.on("console", (e) => {
+ if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(e.text())) {
+ konsole.push(e.text().slice(0, 140));
+ }
+ });
+ await seite.goto(`https://${CREW}:${HP}/workspace/personen.html`,
+ { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1800);
+
+ const lage = await seite.evaluate(() => {
+ const sicht = (e) => !!(e && e.offsetParent !== null);
+ const text = (s) => (document.querySelector(s)?.textContent || "").trim();
+ return {
+ wo: location.pathname,
+ gruppen: [...document.querySelectorAll(".gruppe__titel, .rollen-block__titel, h2")]
+ .map((e) => e.textContent.trim()).filter(Boolean).slice(0, 12),
+ karten: [...document.querySelectorAll("[data-rolle]")]
+ .map((e) => e.dataset.rolle).filter((x, i, a) => x && a.indexOf(x) === i),
+ knoepfe: [...document.querySelectorAll("button")]
+ .filter(sicht).map((e) => e.textContent.trim())
+ .filter(Boolean).filter((x, i, a) => a.indexOf(x) === i).slice(0, 25),
+ protokollBlock: !!document.querySelector(".protokoll-block"),
+ protokollSichtbar: sicht(document.querySelector(".protokoll-block")),
+ protokollZeilen: document.querySelectorAll(".protokoll-zeile").length,
+ protokollText: text("#protokoll").slice(0, 60),
+ schalter: sicht(document.getElementById("protokoll-schalter")),
+ };
+ });
+ befund[wer] = lage;
+ await seite.screenshot({
+ path: `server/mess-${MARKE}-personen-${wer === "DogFather" ? "dogi" : "hand"}.png`,
+ fullPage: true,
+ });
+ if (konsole.length) console.log(` [${wer}] Browser meldet: ${konsole[0]}`);
+ await kontext.close();
+ }
+} finally {
+ await browser.close();
+ await new Promise((r) => vorbau.close(r));
+}
+
+console.log("\n=== Was auf dem Bildschirm steht ===");
+for (const [wer, l] of Object.entries(befund)) {
+ console.log(`\n --- ${wer} (${l.wo}) ---`);
+ console.log(` Rollen-Karten: ${l.karten.join(", ") || "(keine)"}`);
+ console.log(` Protokoll-Block: ${l.protokollBlock ? "im Dokument" : "FEHLT"}`
+ + ` / sichtbar: ${l.protokollSichtbar}`
+ + ` / Zeilen: ${l.protokollZeilen}`
+ + ` / Schalter: ${l.schalter}`);
+ console.log(` Protokoll-Text: ${JSON.stringify(l.protokollText)}`);
+ console.log(` Knoepfe: ${l.knoepfe.join(" · ")}`);
+}
+
+/* DER UNTERSCHIED IST DIE ANTWORT -- alles andere ist Beiwerk. */
+const a = befund.DogFather, b = befund["rechte Hand"];
+if (a && b) {
+ console.log("\n=== Der Unterschied ===");
+ const fehltKarte = a.karten.filter((x) => !b.karten.includes(x));
+ const fehltKnopf = a.knoepfe.filter((x) => !b.knoepfe.includes(x));
+ console.log(` Karten, die ihr fehlen: ${fehltKarte.join(", ") || "(keine)"}`);
+ console.log(` Knoepfe, die ihr fehlen: ${fehltKnopf.join(" · ") || "(keine)"}`);
+ console.log(` Protokollzeilen: DogFather ${a.protokollZeilen}, rechte Hand ${b.protokollZeilen}`);
+}
+console.log(`\n Bilder: server/mess-${MARKE}-personen-{dogi,hand}.png\n`);
+
+try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+process.exit(0);
diff --git a/server/pruef-hand-personen.mjs b/server/pruef-hand-personen.mjs
index 2801ae39..6877abaa 100644
--- a/server/pruef-hand-personen.mjs
+++ b/server/pruef-hand-personen.mjs
@@ -325,6 +325,126 @@ melde("\n=== 5. Gegenproben (jede MUSS anschlagen) ===");
+ `${[...new Set(namen)].join(", ")})`);
}
+/* =====================================================================
+ 6. DAS PROTOKOLL UND DIE AUSBAUSTUFE DER SEITE (25.09.2026)
+ =====================================================================
+
+ Filipe, zum wiederholten Mal und mit einem Bildschirmfoto der
+ Personenseite: „die rechte hand sieht das immer noch nicht obwohl
+ ich will dass die rechte hand das auch sieht."
+
+ ZWEI DINGE FEHLTEN, und nur eines davon war eine Schranke:
+
+ 1. Der Server antwortete ihr auf das Protokoll mit 404.
+ 2. Die OBERFLAECHE hat ihr die Liste versteckt, die sie laengst
+ geladen hatte -- `personen.js` entschied die Ausbaustufe mit
+ `ich.rolle !== "admin"`, und `personen.css` blendet bei
+ `data-nur-anlegen` die Liste aus. Gemessen mit
+ mess-hand-personen.mjs: acht Personen in der Antwort, null auf
+ dem Bildschirm.
+
+ BEIDES WIRD HIER GEPRUEFT, und das zweite ist das wichtigere: Eine
+ Rechtepruefung, die nur Serverantworten ansieht, haette diesen
+ Fehler zwei Tage lang nicht bemerkt -- genau so ist es gewesen.
+ Deshalb steht die Auskunft, aus der die Oberflaeche ihre
+ Ausbaustufe baut (`darf_zugaenge_fuehren`), hier ausdruecklich mit
+ drin.
+
+ MIT GEGENPROBEN: Was die rechte Hand darf, darf die LINKE nicht --
+ fuer sie gilt ausdruecklich „sieht weder Bewerbungen noch den
+ vertraulichen Meldeweg", und ein Sammelbegriff (`istHand`) haette
+ diese Entscheidung stillschweigend umgedreht.
+ ===================================================================== */
+melde("\n=== 6. Das Protokoll und die Ausbaustufe ===");
+{
+ const dogiProt = json(await roh("/workspace/api/verwaltung/protokoll?anzahl=25", kDogi));
+ const handProt = await roh("/workspace/api/verwaltung/protokoll?anzahl=25", kHand);
+ const handJson = json(handProt);
+ /* DIE ANZAHL STEHT IN DER BEDINGUNG. Waere das Protokoll leer,
+ waeren „0 Zeilen fuer beide" gruen, ohne dass irgendjemand
+ irgendetwas sieht. Es ist nie leer -- die Anmeldungen dieser
+ Pruefung stehen selbst darin. */
+ const dogiZeilen = (dogiProt.eintraege || []).length;
+ const handZeilen = (handJson.eintraege || []).length;
+ ok(dogiZeilen > 0, `DogFather sieht das Protokoll (${dogiZeilen} Zeilen)`);
+ ok(handProt.code === 200 && handZeilen === dogiZeilen,
+ `die rechte Hand sieht dasselbe Protokoll (HTTP ${handProt.code}, `
+ + `${handZeilen} Zeilen, DogFather ${dogiZeilen})`);
+
+ /* ---- FRISCHE ZUGAENGE FUER DIE GEGENPROBEN ---------------------
+
+ Die aus Abschnitt 1 sind hier nicht mehr zu gebrauchen: Die
+ Abschnitte davor SPERREN und LOESCHEN absichtlich Leute, und eine
+ tote Sitzung antwortet mit 401. Das sieht aus wie „darf nicht"
+ und heisst „gibt es nicht mehr" -- ein 401 als Gegenprobe fuer
+ ein 404 waere ein Haken ohne Gegenstand. Beim ersten Lauf ist
+ genau das passiert, viermal.
+
+ Angelegt wird direkt in der Datenbank, wie oben: ueber die Route
+ ginge es auch, aber dann haengt die Gegenprobe an einem Recht,
+ das sie gar nicht pruefen will. */
+ const d3 = new DatabaseSync(process.env.WORKSPACE_DB);
+ const frisch = (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");
+ d3.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", ks).update(code).digest("hex"),
+ new Date().toISOString());
+ };
+ frisch("Kessi2", "linke", "CODE-LINK-0002");
+ frisch("Miss2", "modi", "CODE-MODI-0009");
+ d3.close();
+ const kLinke2 = await anmelden("linke", "CODE-LINK-0002");
+ const kModi2 = await anmelden("modi", "CODE-MODI-0009");
+
+ /* GEGENPROBE 1: die linke Hand. Sie steht in derselben Menge wie die
+ rechte (`WIE_RECHTE_HAND`), und genau deshalb wird sie hier
+ einzeln gefragt: Haette jemand `istHand` statt der Rolle benutzt,
+ bekaeme sie das Protokoll mit -- unbemerkt. */
+ const linkeProt = await roh("/workspace/api/verwaltung/protokoll?anzahl=25", kLinke2);
+ ok(linkeProt.code === 404,
+ `Gegenprobe: die linke Hand sieht es nicht (${linkeProt.code}) — `
+ + `„keine Bewerbungen, kein vertraulicher Meldeweg" gilt unveraendert`);
+
+ /* GEGENPROBE 2: ein Modi. Wer hier durchkaeme, saehe jede Anmeldung
+ mit IP-Adresse. */
+ const modiProt = await roh("/workspace/api/verwaltung/protokoll?anzahl=25", kModi2);
+ ok(modiProt.code === 404, `Gegenprobe: ein Modi sieht es nicht (${modiProt.code})`);
+ /* DASS DIE FRISCHEN ZUGAENGE WIRKLICH LEBEN, steht in der Bedingung
+ der naechsten Zeilen: `darf_zugaenge_fuehren === false` bekommt
+ man nur aus einer echten Antwort. Bei einer toten Sitzung stuende
+ dort `undefined`, und die Zeile waere rot -- so wie beim ersten
+ Lauf. */
+
+ /* ---- UND DIE AUSKUNFT, AUS DER DIE SEITE IHRE STUFE BAUT -------
+ Sie ist der eigentliche Fund vom 25.09.2026: Der Server war
+ grosszuegiger als die Oberflaeche, und niemand hat es gemerkt,
+ weil keine Pruefung die Oberflaeche gefragt hat. */
+ const ichDogi = json(await roh("/workspace/api/ich", kDogi));
+ const ichHand = json(await roh("/workspace/api/ich", kHand));
+ const ichLinke = json(await roh("/workspace/api/ich", kLinke2));
+ ok(ichDogi.darf_zugaenge_fuehren === true,
+ `DogFather bekommt die volle Seite gesagt (${ichDogi.darf_zugaenge_fuehren})`);
+ ok(ichHand.darf_zugaenge_fuehren === true,
+ `und die rechte Hand genauso (${ichHand.darf_zugaenge_fuehren}) — `
+ + `daran haengen Liste, Rollenkarten und Protokoll`);
+ ok(ichLinke.darf_zugaenge_fuehren === false,
+ `Gegenprobe: die linke Hand nicht (${ichLinke.darf_zugaenge_fuehren})`);
+
+ /* DIE AUSKUNFT MUSS ZUR TUER PASSEN. Zwei Werte, die dasselbe sagen
+ sollen, laufen sonst auseinander -- und dann zeigt die Seite einen
+ Kasten, den der Server mit 404 beantwortet. Genau diese Sorte
+ Widerspruch hat in dieser Datei am 22. und 24.09. schon zweimal
+ Rechte verschluckt, nur in die andere Richtung. */
+ ok(ichHand.darf_zugaenge_fuehren === (handProt.code === 200),
+ "die Auskunft an die Oberflaeche und die Tuer am Server sagen dasselbe");
+ ok(ichLinke.darf_zugaenge_fuehren === (linkeProt.code === 200),
+ " und bei der linken Hand ebenfalls");
+}
+
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log("");
diff --git a/server/workspace-personen.js b/server/workspace-personen.js
index bfd045cc..46e70ad1 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, darfRolleAendern, rollenZumAendern, hausBedingung, istHand,
+ 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, istHand, fuehrtDieZugaenge,
} from "./workspace.js";
import { sicherungJetzt } from "./workspace-sicherung.js";
@@ -188,8 +188,32 @@ function nurAdmin(req, res, next) {
const vorschauWeg = req.method === "GET"
&& /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/loeschbar$/
.test(req.baseUrl + req.path);
- if (person.rolle === "hand"
- && (anlegeWeg || codeWeg || sperrWeg || loeschWeg || vorschauWeg)) {
+ /* UND DAS PROTOKOLL (25.09.2026).
+
+ Filipe, mit einem Bildschirmfoto genau dieses Kastens: „die rechte
+ hand sieht das immer noch nicht obwohl ich will dass die rechte
+ hand das auch sieht."
+
+ Damit ist auch der letzte Satz von oben ueberholt („Codes,
+ Sperren, Loeschen, Zuteilung und Protokoll bleiben bei
+ DogFather"). Seine Ansage vom 24.09. -- „die selben rechte da
+ haben wie dogfather, das einzige was sie nicht kann ist die
+ dogfather rolle oder leute anfassen" -- laesst fuer das Protokoll
+ keinen Rest.
+
+ NUR LESEN: eine Methode (GET), eine Adresse, keine Nummer im Pfad.
+ Dieselbe Bauweise wie die fuenf Wege darueber.
+
+ `fuehrtDieZugaenge` STATT `person.rolle === "hand"` -- hier stand
+ der Rollenname, und dieselbe Frage stand in zwei weiteren Dateien
+ noch einmal. DogFather kommt weiter unten ohnehin durch; dass er
+ hier mitgemeint ist, aendert nichts und macht die Zeile
+ wahrheitsfaehig. */
+ const protokollWeg = req.method === "GET"
+ && req.baseUrl + req.path === "/workspace/api/verwaltung/protokoll";
+ if (fuehrtDieZugaenge(person)
+ && (anlegeWeg || codeWeg || sperrWeg || loeschWeg || vorschauWeg
+ || protokollWeg)) {
req.person = person;
return next();
}
diff --git a/server/workspace.js b/server/workspace.js
index f57e9534..b87bce57 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -141,6 +141,42 @@ export const WIE_RECHTE_HAND = new Set(["hand", "linke"]);
/** Hat diese Person die Team-Rechte einer Hand (rechts oder links)? */
export const istHand = (person) => !!person && WIE_RECHTE_HAND.has(person.rolle);
+/* ===== WER FUEHRT DIE ZUGAENGE? (25.09.2026) ========================
+
+ Filipe, zum wiederholten Mal: „die rechte hand sieht das immer noch
+ nicht obwohl ich will dass die rechte hand das auch sieht." Dazu ein
+ Bildschirmfoto der Personenseite mit den Rollenkarten und „Letzte
+ Ereignisse".
+
+ NACHGEMESSEN, NICHT GERATEN (mess-hand-personen.mjs): Sie bekam vom
+ Server alle acht Personen (HTTP 200) -- und sah auf dem Bildschirm
+ NICHTS davon. Zwei Stellen hielten sie auf, keine davon eine
+ Sicherung:
+
+ 1. personen.js entschied die Ausbaustufe mit `ich.rolle !== "admin"`.
+ Ein Rollenname im Browser -- genau die zweite Wahrheit, die in
+ dieser Datei am 22. und am 24.09. schon zweimal veraltet ist.
+ 2. personen.css blendet bei `data-nur-anlegen` die Liste aus. Die
+ Regel stammt vom 07.09. und war fuer Manager und Spicy Media
+ gedacht; die rechte Hand ist erst danach dazugekommen und fiel
+ stillschweigend mit hinein.
+
+ Ihr Protokoll war zusaetzlich am Server zu (HTTP 404).
+
+ WARUM ES EINE EIGENE FUNKTION IST UND KEIN ROLLENVERGLEICH: Dieselbe
+ Frage wird an DREI Stellen gestellt -- die Tuer zum Protokoll, die
+ Auskunft an die Oberflaeche und die Ausbaustufe der Seite. Drei
+ Abschriften waeren drei Gelegenheiten, dass die naechste Aenderung
+ nur zwei davon trifft. Genau so ist dieser Fehler entstanden.
+
+ `istHand` WAERE FALSCH. Es fasst beide Haende zusammen, und fuer die
+ linke gilt ausdruecklich das Gegenteil: „Kann aber keine Personen
+ anlegen, keine Rollen aendern und sieht weder Bewerbungen noch den
+ vertraulichen Meldeweg." Wer hier den Sammelbegriff nimmt, dreht
+ eine ausgesprochene Entscheidung stillschweigend um. */
+export const fuehrtDieZugaenge = (person) =>
+ !!person && (person.rolle === "admin" || person.rolle === "hand");
+
/* =====================================================================
WER EINE KATALOG-VORLAGE BEKOMMEN KANN (24.09.2026)
=====================================================================
@@ -6876,6 +6912,12 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => {
zuruecknehmen laesst. Heute dieselbe Antwort; sollte Filipe es
spaeter wieder einschraenken, ist hier die Stelle. */
darf_personen_loeschen: person?.rolle === "admin" || person?.rolle === "hand",
+ /* UND OB SIE DIE PERSONENSEITE UEBERHAUPT GANZ SIEHT (25.09.2026).
+ Liste, Rollenkarten, Protokoll -- siehe `fuehrtDieZugaenge`. Die
+ Oberflaeche hat das bisher am Rollennamen entschieden und dabei
+ zwei Tage lang die Liste versteckt, die sie laengst geladen
+ hatte. */
+ darf_zugaenge_fuehren: fuehrtDieZugaenge(person),
/* OB DER CHAT-KNOPF IN DER KOPFLEISTE ERSCHEINT (18.09.2026).
Er wurde auf JEDER Seite gebaut. Fuer ein Mitglied der Community
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 707d05dc..adc5f77b 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`. -->
-
-
+
+
-
+