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`. --> - - + + - +