diff --git a/server/index.js b/server/index.js index 596dc89b..722405b4 100644 --- a/server/index.js +++ b/server/index.js @@ -25,6 +25,7 @@ import { dateienRouter } from "./workspace-dateien.js"; import { bereicheRouter } from "./workspace-bereiche.js"; import { treffRouter } from "./workspace-treff.js"; import { entwicklungRouter } from "./workspace-entwicklung.js"; +import { rechteRouter, tafelLaden } from "./workspace-rechte.js"; import { reportRouter } from "./workspace-reports.js"; import { scoutRouter } from "./workspace-scouts.js"; import { callRouter } from "./workspace-calls.js"; @@ -249,6 +250,8 @@ app.use(treffRouter); -- beides nebeneinander ist Absicht: Der Katalog ist das, was man regelmaessig durchgeht, die Notiz das, was dazwischen auffaellt. */ app.use(entwicklungRouter); +/* Die Rechtetafel -- ansehen und umstellen (11.09.2026). */ +app.use(rechteRouter); app.use(reportRouter); app.use(scoutRouter); app.use(callRouter); diff --git a/server/pruef-rechte-umstellen.mjs b/server/pruef-rechte-umstellen.mjs new file mode 100644 index 00000000..90579e3e --- /dev/null +++ b/server/pruef-rechte-umstellen.mjs @@ -0,0 +1,338 @@ +/* ===================================================================== + Die Rechtetafel von Hand umstellen (11.09.2026) + + Seit heute kann DogFather die Tafel auf der Seite „Wer sieht was" + ändern. Das ist die gefährlichste Einstellung im ganzen Haus: Ein + Klick öffnet oder schließt eine Seite für eine ganze Rolle. + + Deshalb wird hier nicht geprüft, ob die Antwort 200 lautet, sondern + ob sich das VERHALTEN ändert — also ob ein Modi die Seite danach + wirklich nicht mehr aufbekommt. Mit Gegenprobe davor, sonst beweist + es nichts. + + Und die drei Riegel: + · Nur DogFather stellt um. Die rechte Hand sieht dieselbe Tafel + und bekommt 403. + · Drei Felder sind fest — er kann sich nicht selbst aussperren. + · Alles zurück auf Grundstand ist ein Knopf, und danach gilt genau + das, was im Code steht. + + Aufrufen mit: node server/pruef-rechte-umstellen.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(4405, "pruef-rechte-umstellen"); + +const ordner = mkdtempSync(join(tmpdir(), "ws-rechte-")); +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-rechte-umstellen"); +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 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 rechte = await import("./rechte.js"); +const modul = await import("./workspace-rechte.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; +} +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Rieke", "hand", "CODE-HAND-0001"); +anlegen("Frida", "modi", "CODE-MODI-0001"); +anlegen("Mara", "manager", "CODE-MANA-0001"); +d.close(); + +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 }); + return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); +} +const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS); +const kHand = await anmelden("hand", "CODE-HAND-0001", CREW); +const kModi = await anmelden("modi", "CODE-MODI-0001", CREW); +const kMana = await anmelden("manager", "CODE-MANA-0001", HAUS); +ok(!!kDogi && !!kHand && !!kModi && !!kMana, "vier Rollen sind angemeldet"); + +/* ======================================================================= + 1. WER DIE SEITE BEKOMMT — und wer sie umstellen darf + ======================================================================= */ +melde(""); +melde("=== 1. Wer die Tafel sieht ==="); +{ + for (const [wer, host, keks, soll] of [ + ["DogFather", HAUS, kDogi, true], + ["die rechte Hand", CREW, kHand, true], + ["ein Modi", CREW, kModi, false], + ["eine Managerin", HAUS, kMana, false], + ]) { + const seite = await roh("/workspace/rechte.html", host, { cookie: keks }); + const api = await roh("/workspace/api/rechte/tafel", host, { cookie: keks }); + ok((seite.code === 200) === soll && (api.code === 200) === soll, + `${wer}: Seite ${seite.code}, Tafel ${api.code} — ${soll ? "beides offen" : "beides zu"}`); + } + + /* UMSTELLEN IST ETWAS ANDERES ALS SEHEN. */ + const alsDogi = json(await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi })); + const alsHand = json(await roh("/workspace/api/rechte/tafel", CREW, { cookie: kHand })); + ok(alsDogi.darf_aendern === true, "DogFather darf umstellen"); + ok(alsHand.darf_aendern === false, "die rechte Hand sieht dieselbe Tafel, darf aber nicht"); + ok(alsHand.seiten?.length === alsDogi.seiten?.length, + `und sie bekommt wirklich dieselbe (${alsHand.seiten?.length} Seiten)`); + + /* DIE KACHEL AUF BEIDEN ADRESSEN (Filipe, 11.09.2026). */ + for (const [wer, host, keks, soll] of [ + ["DogFather auf crew.", CREW, kDogi, true], + ["die rechte Hand auf crew.", CREW, kHand, true], + ["ein Modi auf crew.", CREW, kModi, false], + ]) { + const ich = json(await roh("/workspace/api/ich", host, { cookie: keks })); + const alle = [...(ich.bereiche || []), ...(ich.bereiche_zusatz || [])]; + const da = alle.some((k) => k.ziel === "rechte.html"); + ok(da === soll, `${wer}: Kachel ${da ? "da" : "nicht da"}`); + } +} + +/* ======================================================================= + 2. UMSTELLEN ÄNDERT DAS VERHALTEN — nicht nur die Anzeige + ======================================================================= */ +melde(""); +melde("=== 2. Ein Klick, und die Seite ist zu ==="); +const PROBE = "/workspace/chat.html"; +{ + /* GEGENPROBE ZUERST. Ohne sie wäre „danach zu" auch dann grün, wenn + die Seite vorher schon zu gewesen wäre. */ + const vorher = await roh(PROBE, CREW, { cookie: kModi }); + ok(vorher.code === 200, `ein Modi kann chat.html öffnen (${vorher.code})`); + + const stellen = await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: PROBE, rolle: "modi", erlaubt: false }, "PUT"); + ok(stellen.code === 200, `DogFather nimmt sie ihm weg (${stellen.code})`); + ok(json(stellen).abweichungen === 1, `eine Abweichung vom Grundstand (${json(stellen).abweichungen})`); + + /* DAS IST DER EIGENTLICHE BEWEIS: nicht die Antwort, sondern die + Schranke. */ + const nachher = await roh(PROBE, CREW, { cookie: kModi }); + ok(nachher.code !== 200, `und die Schranke lässt ihn nicht mehr durch (${nachher.code})`); + + /* Und die Kachel ist auch weg -- sonst führte ein Knopf ins Leere. */ + const ich = json(await roh("/workspace/api/ich", CREW, { cookie: kModi })); + const alle = [...(ich.bereiche || []), ...(ich.bereiche_zusatz || [])]; + ok(!alle.some((k) => (k.ziel || "").startsWith("chat.html")), + "und die Kachel dazu verschwindet mit"); + + /* DIE ANZEIGE SAGT, DASS ES VON HAND GESETZT IST. */ + const tafel = json(await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi })); + const zeile = tafel.seiten.find((s) => s.pfad === PROBE); + ok(!zeile.rollen.includes("modi") && zeile.grund.includes("modi"), + "die Tafel zeigt: jetzt nein, im Grundstand ja"); + const modi = tafel.rollen.find((r) => r.schluessel === "modi"); + ok(modi.abweichend === 1, `und zählt es bei der Rolle (${modi.abweichend})`); +} + +/* ======================================================================= + 3. ZURÜCK — auf zwei Wegen + ======================================================================= */ +melde(""); +melde("=== 3. Zurück ==="); +{ + /* WEG EINS: dasselbe Feld wieder anklicken. Die Abweichung muss + dabei VERSCHWINDEN und nicht als „erlaubt" stehenbleiben -- sonst + stünde in der Datenbank eine Zeile, die dasselbe sagt wie der + Code, und die Anzeige meldete „von Hand gesetzt", obwohl nichts + verstellt ist. */ + const zurueck = await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: PROBE, rolle: "modi", erlaubt: true }, "PUT"); + ok(zurueck.code === 200 && json(zurueck).abweichungen === 0, + `zurückgeklickt, keine Abweichung mehr (${json(zurueck).abweichungen})`); + const wieder = await roh(PROBE, CREW, { cookie: kModi }); + ok(wieder.code === 200, `und der Modi kommt wieder hinein (${wieder.code})`); + + /* WEG ZWEI: der Knopf. Erst wieder etwas verstellen, sonst prüft er + nichts. */ + await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: PROBE, rolle: "modi", erlaubt: false }, "PUT"); + await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: "/workspace/wissen.html", rolle: "modi", erlaubt: false }, "PUT"); + const vorAllem = json(await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi })); + ok(vorAllem.abweichungen === 2, `zwei Felder verstellt (${vorAllem.abweichungen})`); + + const alles = await roh("/workspace/api/rechte/abweichungen", HAUS, { cookie: kDogi }, + null, "DELETE"); + ok(alles.code === 200 && json(alles).zurueckgenommen === 2, + `der Knopf nimmt beide zurück (${json(alles).zurueckgenommen})`); + const danach = json(await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi })); + ok(danach.abweichungen === 0, "und die Tafel steht wieder auf Grundstand"); + + /* Und das Verhalten stimmt auch wieder -- die Zahl allein wäre nur + eine Behauptung. */ + const chat = await roh(PROBE, CREW, { cookie: kModi }); + const wissen = await roh("/workspace/wissen.html", CREW, { cookie: kModi }); + ok(chat.code === 200 && wissen.code === 200, + `beide Seiten sind für den Modi wieder offen (${chat.code}/${wissen.code})`); +} + +/* ======================================================================= + 4. DIE DREI FESTEN FELDER + ======================================================================= */ +melde(""); +melde("=== 4. Aussperren geht nicht ==="); +{ + for (const u of rechte.UNANTASTBAR) { + const a = await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: u.pfad, rolle: u.rolle, erlaubt: false }, "PUT"); + ok(a.code === 400, `${u.pfad.replace("/workspace/", "")} bleibt bei ${u.rolle} (${a.code})`); + ok(/wegnehmen/.test(json(a).fehler || ""), + "und die Absage sagt, warum – nicht nur nein"); + } + /* GEGENPROBE: Dieselbe Seite bei einer ANDEREN Rolle geht sehr wohl. + Ohne diese Zeile wäre „fest" auch dann grün, wenn gar nichts + umstellbar wäre. */ + const andere = await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: "/workspace/rechte.html", rolle: "hand", erlaubt: false }, "PUT"); + ok(andere.code === 200, `dieselbe Seite bei der rechten Hand schon (${andere.code})`); + const handSeite = await roh("/workspace/rechte.html", CREW, { cookie: kHand }); + ok(handSeite.code !== 200, `und sie kommt danach nicht mehr hinein (${handSeite.code})`); + await roh("/workspace/api/rechte/abweichungen", HAUS, { cookie: kDogi }, null, "DELETE"); + + /* Die Oberfläche bekommt die feste Liste mitgeliefert -- damit sie + den Knopf gar nicht erst als Knopf zeigt. */ + const tafel = json(await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi })); + const zeile = tafel.seiten.find((s) => s.pfad === "/workspace/rechte.html"); + ok(zeile?.fest?.includes("admin"), `und die Tafel sagt, welches Feld fest ist (${zeile?.fest})`); +} + +/* ======================================================================= + 5. WER NICHT DARF, DARF AUCH ÜBER DIE SCHNITTSTELLE NICHT + ======================================================================= */ +melde(""); +melde("=== 5. Nur DogFather stellt um ==="); +{ + const hand = await roh("/workspace/api/rechte/feld", CREW, { cookie: kHand }, + { pfad: PROBE, rolle: "modi", erlaubt: false }, "PUT"); + ok(hand.code === 403, `die rechte Hand bekommt 403 (${hand.code})`); + ok(/DogFather/.test(json(hand).fehler || ""), "und erfährt, wer es darf"); + + const modi = await roh("/workspace/api/rechte/feld", CREW, { cookie: kModi }, + { pfad: PROBE, rolle: "modi", erlaubt: true }, "PUT"); + ok(modi.code === 404, `ein Modi bekommt 404 – für ihn gibt es die Tafel nicht (${modi.code})`); + + const alles = await roh("/workspace/api/rechte/abweichungen", CREW, { cookie: kHand }, + null, "DELETE"); + ok(alles.code === 403, `und zurücksetzen darf sie auch nicht (${alles.code})`); + + /* GEGENPROBE: Nach all dem ist die Tafel unverändert. Ein + abgewiesener Versuch, der trotzdem etwas ändert, wäre der + schlimmste Ausgang von allen. */ + const tafel = json(await roh("/workspace/api/rechte/tafel", HAUS, { cookie: kDogi })); + ok(tafel.abweichungen === 0, `nichts davon hat etwas verändert (${tafel.abweichungen})`); +} + +/* ======================================================================= + 6. ERFUNDENES WIRD ABGEWIESEN + ======================================================================= */ +melde(""); +melde("=== 6. Erfundenes ==="); +{ + const seite = await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: "/workspace/gibt-es-nicht.html", rolle: "modi", erlaubt: true }, "PUT"); + ok(seite.code === 400, `eine erfundene Seite (${seite.code})`); + const rolle = await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: PROBE, rolle: "chef", erlaubt: true }, "PUT"); + ok(rolle.code === 400, `eine erfundene Rolle (${rolle.code})`); + /* Und die Zugangswände stehen gar nicht erst in der Tafel -- sie + lassen sich deshalb auch nicht schließen. */ + const wand = await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: "/workspace/crew-index.html", rolle: "admin", erlaubt: false }, "PUT"); + ok(wand.code === 400, `und eine Zugangswand auch nicht (${wand.code})`); +} + +/* ======================================================================= + 7. EINE UMSTELLUNG ÜBERLEBT EINEN NEUSTART + ======================================================================= */ +melde(""); +melde("=== 7. Nach einem Neustart ==="); +{ + await roh("/workspace/api/rechte/feld", HAUS, { cookie: kDogi }, + { pfad: PROBE, rolle: "modi", erlaubt: false }, "PUT"); + + /* DEN NEUSTART NACHSTELLEN: die Tafel auf den Grundstand + zuruecksetzen (so, wie sie nach einem frischen Start waere) und + dann den Ladevorgang aufrufen, den index.js beim Start macht. + + OHNE DIESEN AUFRUF GAELTE NACH EINEM NEUSTART WIEDER DER + GRUNDSTAND -- und niemand haette es gemerkt, denn es saehe aus wie + vorher. Genau das ist die Sorte Fehler, die man nur findet, wenn + man danach sucht. */ + rechte.tafelSetzen(null); + ok(rechte.darfSeite({ rolle: "modi" }, PROBE) === true, + "ohne geladene Abweichungen gilt der Grundstand"); + const stand = modul.tafelLaden(); + ok(stand.abweichungen === 1, `tafelLaden() findet die Abweichung (${stand.abweichungen})`); + ok(rechte.darfSeite({ rolle: "modi" }, PROBE) === false, + "und danach gilt sie wieder"); + + const nachher = await roh(PROBE, CREW, { cookie: kModi }); + ok(nachher.code !== 200, `auch für den Modi im Browser (${nachher.code})`); + + await roh("/workspace/api/rechte/abweichungen", HAUS, { cookie: kDogi }, null, "DELETE"); + ok(rechte.darfSeite({ rolle: "modi" }, PROBE) === true, "und am Ende ist alles wieder offen"); +} + +/* ------------------------------------------------------------------- */ +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-treff-werkzeuge.mjs b/server/pruef-treff-werkzeuge.mjs index 30045cb9..390b3e49 100644 --- a/server/pruef-treff-werkzeuge.mjs +++ b/server/pruef-treff-werkzeuge.mjs @@ -533,10 +533,22 @@ melde("=== 8. Der Regeltext ==="); ok(!wer.includes("manager") && !wer.includes("creator") && !wer.includes("spicy"), "und nicht fuer die Agentur"); - /* "Wer sieht was" ist allein DogFathers Seite. */ + /* "Wer sieht was" gehoert denen, die das Team fuehren. + + STAND HIER BIS ZUM ABEND AUF `length === 1 && [0] === "admin"`. + Filipe danach: "ich will die kategorie auch auf der team dogi seite + fuer die dogfather und rechte hand rollen" -- die Zeile war rot, + obwohl genau das Richtige passierte. + + Geprueft wird jetzt, was die Zeile eigentlich behaupten will: NICHT + die Agentur, NICHT die Modis, NICHT die Community. Wer dazukommt, + ist eine Entscheidung; wer NIE dazugehoert, ist der Riegel. */ const tafel = rechte.SEITEN["/workspace/rechte.html"] || []; - ok(tafel.length === 1 && tafel[0] === "admin", - `"Wer sieht was" nur fuer ${tafel.join(", ")}`); + const drin = ["admin", "hand"].filter((r) => tafel.includes(r)); + const drausen = ["spicy", "manager", "scout", "creator", "modi", "gast"] + .filter((r) => tafel.includes(r)); + ok(drin.length === 2 && drausen.length === 0, + `"Wer sieht was" fuer ${tafel.join(", ")} -- und fuer sonst niemanden`); const fremd = await roh("/workspace/api/rechte/tafel", CREW, { cookie: kModi }); ok(fremd.code === 404, `ein Modi bekommt die Tafel nicht (${fremd.code})`); diff --git a/server/rechte.js b/server/rechte.js index 99d67c50..f322e3af 100644 --- a/server/rechte.js +++ b/server/rechte.js @@ -305,10 +305,21 @@ export const SEITEN = { 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"], + /* WER SIEHT WAS. DogFather und die rechte Hand (Filipe, 11.09.2026: + "ich will die kategorie auch auf der team dogi seite fuer die + dogfather und rechte hand rollen"). + + ANSEHEN BEIDE, UMSTELLEN NUR ER. Sie fuehren das Team zusammen; + wer mitentscheidet, muss nachsehen koennen. Wer aber Rechte + vergeben kann, kann sich Rechte vergeben -- und diese eine Tuer + bleibt bei ihm. Durchgesetzt wird das in workspace-rechte.js, nicht + hier: Diese Tafel beantwortet, wer die SEITE oeffnen darf, nicht + was er darauf tun kann. + + Weiterhin ausdruecklich NICHT fuer die Agentur und nicht fuer die + Modis: 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", "hand"], }; /* Die Zugangswaende. Sie haben keine Rolle, weil sich dort noch @@ -335,10 +346,62 @@ export const OHNE_ANMELDUNG = [ * @param {{rolle?: string}|null} person * @param {string} pfad z. B. "/workspace/start.html" */ +/* ===================================================================== + DIE TAFEL IST SEIT DEM 11.09.2026 VERAENDERBAR — aber nur auf EINE Art + ===================================================================== + + Filipe: *"so dass ich die da auch manuel wechseln und speichern kann."* + + WAS OBEN STEHT, IST AB JETZT DER GRUNDSTAND, nicht mehr die ganze + Wahrheit. Was DogFather auf der Seite "Wer sieht was" umstellt, liegt + in der Datenbank und wird hier darübergelegt. + + DREI ENTSCHEIDUNGEN, DIE MAN DEM CODE NICHT ANSIEHT: + + 1. GESPEICHERT WIRD NUR DER UNTERSCHIED. In der Datenbank steht eine + Zeile nur dann, wenn sie vom Grundstand ABWEICHT. Damit bedeutet + der Code oben weiterhin etwas -- man sieht, was gedacht war, und + daneben, was jemand daraus gemacht hat. Eine vollstaendige Kopie + in der Datenbank waere nach einer Woche die einzige Wahrheit, und + der Code oben wuerde still veralten. + + 2. DER GRUNDSTAND IST DER RUECKFALL, wenn die Abweichungen nicht + gelesen werden koennen. Das ist die richtige Richtung: Er ist der + geprüfte Zustand, nicht der zufaellige. Ein Fehlschlag wird laut + protokolliert -- eine Tafel, die still auf etwas anderes + zurueckfaellt, waere die schlimmste Sorte Fehler in dieser Datei. + + 3. GEAENDERT WIRD NICHT HIER. Diese Datei kennt keine Datenbank; sie + nimmt eine fertige Tafel entgegen. Wer sie setzt und wer das darf, + steht in workspace-rechte.js. Zwei Dateien, zwei Fragen -- und die + Rechtepruefung bleibt ohne laufenden Server pruefbar. + ===================================================================== */ + +/** Die Tafel, die gerade gilt. Am Anfang der Grundstand. */ +let TAFEL = SEITEN; + +/** Setzt die geltende Tafel. "null" stellt den Grundstand wieder her. */ +export function tafelSetzen(neu) { + TAFEL = neu && typeof neu === "object" ? neu : SEITEN; +} + +/** Die Tafel, die gerade gilt -- fuer die Anzeige und fuer Pruefungen. */ +export function tafelJetzt() { + return TAFEL; +} + +/** Weicht diese Seite/Rolle vom Grundstand ab? Fuer die Anzeige: Man + * soll sehen, was jemand von Hand geaendert hat. */ +export function weichtAb(pfad, rolle) { + const grund = Array.isArray(SEITEN[pfad]) && SEITEN[pfad].includes(rolle); + const jetzt = Array.isArray(TAFEL[pfad]) && TAFEL[pfad].includes(rolle); + return grund !== jetzt; +} + export function darfSeite(person, pfad) { const rolle = person?.rolle; if (!rolle) return false; - const erlaubt = SEITEN[String(pfad || "")]; + const erlaubt = TAFEL[String(pfad || "")]; /* Kein Eintrag heisst NEIN. Vorher hiess es ja -- und genau darin lag das Loch, das man einer neuen Seite nicht ansieht. */ if (!Array.isArray(erlaubt)) return false; @@ -353,7 +416,7 @@ export function darfSeite(person, pfad) { * Stelle, an der die beiden auseinanderlaufen. */ export function seitenFuer(rolle) { - return Object.entries(SEITEN) + return Object.entries(TAFEL) .filter(([, r]) => r.includes(rolle)) .map(([p]) => p) .sort(); @@ -361,5 +424,43 @@ export function seitenFuer(rolle) { /** Alle Seiten der Tafel -- fuer die Pruefung gegen die Platte. */ export function alleSeiten() { + /* AUS DEM GRUNDSTAND, nicht aus der geltenden Tafel. + + Die Liste der SEITEN aendert sich durch eine Umstellung nicht -- + nur, wer sie oeffnen darf. Wer hier TAFEL naehme, bekaeme bei einem + Fehler in der Zusammenfuehrung eine kuerzere Liste, und + pruef-rechtetafel ("jede Datei hat einen Eintrag") wuerde dann + stillschweigend weniger pruefen. */ return Object.keys(SEITEN).sort(); } + +/** Nur DogFather darf die Tafel umstellen -- und selbst er nicht alles. + * + * DIESE DREI SEITEN KANN ER SICH NICHT SELBST WEGNEHMEN. Ohne + * "rechte.html" koennte er die Aenderung nicht zuruecknehmen, ohne + * "personen.html" keinen Zugang mehr vergeben, ohne "start.html" + * nirgends mehr hin. Eine Sperre, aus der man sich aussperren kann, + * ist keine Einstellung, sondern eine Falle -- und sie waere genau + * einen Fehlklick entfernt. + * + * Fuer JEDE andere Rolle darf er jede Seite umstellen. Das ist der + * Sinn der Sache: "soll jede rolle immer nur das sehen was sie sehen + * sollen" -- wer das entscheidet, ist er. */ +export const UNANTASTBAR = [ + { pfad: "/workspace/rechte.html", rolle: "admin" }, + { pfad: "/workspace/personen.html", rolle: "admin" }, + { pfad: "/workspace/start.html", rolle: "admin" }, +]; + +/** @returns {string|null} Grund, warum das nicht geht -- oder null. */ +export function darfUmstellen(pfad, rolle, erlaubt) { + if (!Object.prototype.hasOwnProperty.call(SEITEN, pfad)) return "Diese Seite gibt es nicht."; + if (!ALLE_ROLLEN.includes(rolle)) return "Diese Rolle gibt es nicht."; + if (erlaubt) return null; + const fest = UNANTASTBAR.find((u) => u.pfad === pfad && u.rolle === rolle); + if (fest) { + return "Diese Seite kannst du dir nicht selbst wegnehmen – sonst käme " + + "niemand mehr an die Rechte, an die Zugänge oder auf die Startseite."; + } + return null; +} diff --git a/server/workspace-rechte.js b/server/workspace-rechte.js new file mode 100644 index 00000000..6274f7ae --- /dev/null +++ b/server/workspace-rechte.js @@ -0,0 +1,268 @@ +/* ===================================================================== + Die Rechtetafel von Hand umstellen (11.09.2026) + ===================================================================== + + Filipe: *„ich will die kategorie auch auf der team dogi seite für die + dogfather und rechte hand rollen … und wenn man drauf drückt, + perfektionieren so dass ich die da auch manuell wechseln und + speichern kann."* + + --------------------------------------------------------------------- + WAS GESPEICHERT WIRD: NUR DER UNTERSCHIED + + In `seitenrecht` steht eine Zeile nur, wenn sie vom Grundstand in + rechte.js ABWEICHT. Drei Gründe, und der dritte ist der wichtigste: + + * Der Code oben bedeutet weiterhin etwas. Man sieht, was gedacht + war — und daneben, was daraus geworden ist. + * „Zurück auf Grundstand" ist ein DELETE, kein Umschreiben. + * Eine neue Seite erbt automatisch den Grundstand. Eine + vollständige Kopie in der Datenbank hätte sie nicht gekannt, und + sie wäre für alle zu gewesen — ohne dass es jemand entschieden + hätte. + + --------------------------------------------------------------------- + WER DARF WAS + + SEHEN: DogFather und die rechte Hand. Sie führen das Team zusammen; + wer mitentscheidet, muss nachsehen können. + + UMSTELLEN: nur DogFather. Das ist dieselbe Linie wie bei den + Zugängen („Zugang anlegen: nur DogFather") — wer Rechte vergeben + kann, kann sich Rechte vergeben, und diese eine Tür bleibt bei ihm. + Das stellt ihn nicht über sein Team; es ist die Aufgabe, die er + ohnehin hat. + + UND DREI ZEILEN KANN AUCH ER NICHT ANFASSEN (rechte.js, UNANTASTBAR): + sich selbst die Rechte-, die Personen- oder die Startseite + wegzunehmen. Eine Einstellung, aus der man sich aussperren kann, ist + keine Einstellung, sondern eine Falle — und sie wäre genau einen + Fehlklick entfernt. + + --------------------------------------------------------------------- + WANN DIE TAFEL NEU GELADEN WIRD + + Beim Start und nach jeder Änderung — in DIESEM Prozess. Der Server + läuft als ein Prozess; eine zweite Instanz gäbe es nur bei einem + Neustart, und der lädt ohnehin neu. Ein Zwischenspeicher mit + Laufzeit wäre hier falsch: Wer ein Recht wegnimmt, will nicht, dass + es noch zehn Minuten gilt. + + FÄLLT DAS LESEN AUS, gilt der Grundstand — und es wird laut + protokolliert. Das ist die richtige Richtung: Der Grundstand ist der + geprüfte Zustand. Eine Tafel, die still auf etwas anderes + zurückfällt, wäre die schlimmste Sorte Fehler in diesem Haus. + ===================================================================== */ + +import express from "express"; +import { + db, sitzungLesen, protokolliere, echteIp, fuehrtTeamDogi, ROLLEN_NAME, +} from "./workspace.js"; +import { + SEITEN, ALLE_ROLLEN, OHNE_ANMELDUNG, UNANTASTBAR, + tafelSetzen, tafelJetzt, darfUmstellen, alleSeiten, +} from "./rechte.js"; + +export const rechteRouter = express.Router(); + +const jetzt = () => new Date().toISOString(); + +let bereit = false; +function tabelle() { + if (bereit) return; + db().exec(` + /* Eine Abweichung vom Grundstand. NUR Abweichungen -- was hier + nicht steht, gilt so, wie es in rechte.js steht. */ + CREATE TABLE IF NOT EXISTS seitenrecht ( + pfad TEXT NOT NULL, + rolle TEXT NOT NULL, + erlaubt INTEGER NOT NULL CHECK (erlaubt IN (0, 1)), + von INTEGER REFERENCES personen(id) ON DELETE SET NULL, + am TEXT NOT NULL, + PRIMARY KEY (pfad, rolle) + ); + `); + bereit = true; +} + +/** + * Baut die geltende Tafel aus Grundstand + Abweichungen und setzt sie. + * + * WIRD BEIM START GERUFEN (index.js) UND NACH JEDER AENDERUNG. Ohne den + * Aufruf beim Start gaelte nach einem Neustart wieder der Grundstand -- + * und niemand haette es gemerkt, denn es saehe aus wie vorher. + */ +export function tafelLaden() { + try { + tabelle(); + const zeilen = db().prepare("SELECT pfad, rolle, erlaubt FROM seitenrecht").all(); + if (!zeilen.length) { tafelSetzen(null); return { abweichungen: 0 }; } + + /* Eine KOPIE, nicht der Grundstand selbst -- sonst waere er nach der + ersten Aenderung ueberschrieben und "zurueck auf Grundstand" + haette nichts mehr, wohin es zurueckkann. */ + const neu = {}; + for (const [pfad, rollen] of Object.entries(SEITEN)) neu[pfad] = [...rollen]; + + let gezaehlt = 0; + for (const z of zeilen) { + if (!Object.prototype.hasOwnProperty.call(neu, z.pfad)) continue; // Seite gibt es nicht mehr + if (!ALLE_ROLLEN.includes(z.rolle)) continue; // Rolle gibt es nicht mehr + const drin = neu[z.pfad].includes(z.rolle); + if (z.erlaubt && !drin) { neu[z.pfad].push(z.rolle); gezaehlt++; } + if (!z.erlaubt && drin) { + neu[z.pfad] = neu[z.pfad].filter((r) => r !== z.rolle); + gezaehlt++; + } + } + tafelSetzen(neu); + return { abweichungen: gezaehlt }; + } catch (f) { + /* LAUT, NICHT LEISE. Eine Tafel, die still auf den Grundstand + zurueckfaellt, sieht aus wie eine Tafel, die stimmt. */ + console.error("[rechte] Tafel konnte nicht geladen werden, es gilt der " + + "Grundstand aus rechte.js:", f?.message); + tafelSetzen(null); + return { abweichungen: 0, fehler: String(f?.message || f) }; + } +} + +/* --------------------------------------------------------------------- + Die Wege + ------------------------------------------------------------------- */ + +function angemeldet(req, res, next) { + const person = sitzungLesen(req); + if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); + req.person = person; + try { tabelle(); } catch (f) { + console.error("[rechte] Tabelle:", f?.message); + return res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + next(); +} + +/** Sehen: DogFather und die rechte Hand. 404 fuer alle anderen -- wer + * "kein Zugriff" liest, fragt als Naechstes, wer denn dann. */ +function nurLeitung(req, res, next) { + if (fuehrtTeamDogi(req.person)) return next(); + return res.status(404).json({ fehler: "nicht_gefunden" }); +} + +/** Umstellen: nur DogFather. */ +function nurDogFather(req, res, next) { + if (req.person?.rolle === "admin") return next(); + return res.status(403).json({ + fehler: "Umstellen kann das nur DogFather. Ansehen darfst du alles.", + }); +} + +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(); +} + +rechteRouter.use("/workspace/api/rechte", angemeldet, nurLeitung); + +rechteRouter.get("/workspace/api/rechte/tafel", (req, res) => { + try { + const tafel = tafelJetzt(); + const seiten = alleSeiten().map((pfad) => ({ + pfad, + datei: pfad.replace("/workspace/", ""), + rollen: [...(tafel[pfad] || [])], + grund: [...(SEITEN[pfad] || [])], + /* Was DogFather hier NICHT abwaehlen kann -- damit die Oberflaeche + den Knopf gar nicht erst als Knopf zeigt statt ihn eine Absage + bringen zu lassen. */ + fest: UNANTASTBAR.filter((u) => u.pfad === pfad).map((u) => u.rolle), + })); + + res.json({ + darf_aendern: req.person.rolle === "admin", + rollen: ALLE_ROLLEN.map((r) => ({ + schluessel: r, + name: ROLLEN_NAME[r] ?? r, + seiten: seiten.filter((s) => s.rollen.includes(r)).length, + /* Wie viele davon von Hand gesetzt sind -- die Zahl, an der man + sieht, ob jemand etwas verstellt hat. */ + abweichend: seiten.filter((s) => s.rollen.includes(r) !== s.grund.includes(r)).length, + })), + seiten, + ohne_anmeldung: [...OHNE_ANMELDUNG], + abweichungen: seiten.reduce((n, s) => n + + ALLE_ROLLEN.filter((r) => s.rollen.includes(r) !== s.grund.includes(r)).length, 0), + }); + } catch (f) { + console.error("[rechte] Tafel lesen:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + +/* EIN FELD UMSTELLEN. + Gespeichert wird sofort. Ein "Speichern"-Knopf am Ende waere bei + einer Tafel mit 200 Feldern die Stelle, an der man eine halbe + Aenderung verliert -- und bei Rechten ist eine halbe Aenderung die + gefaehrlichste Lage von allen. Zurueckgenommen wird mit demselben + Klick. */ +rechteRouter.put("/workspace/api/rechte/feld", gleicheHerkunft, nurDogFather, (req, res) => { + try { + const pfad = String(req.body?.pfad || ""); + const rolle = String(req.body?.rolle || ""); + const erlaubt = req.body?.erlaubt === true; + + const nein = darfUmstellen(pfad, rolle, erlaubt); + if (nein) return res.status(400).json({ fehler: nein }); + + const imGrundstand = (SEITEN[pfad] || []).includes(rolle); + if (erlaubt === imGrundstand) { + /* WIEDER WIE IM GRUNDSTAND -> die Abweichung verschwindet. + Eine Zeile, die dasselbe sagt wie der Code, waere eine + Abweichung, die keine ist -- und sie wuerde in der Anzeige als + "von Hand gesetzt" stehen, obwohl nichts verstellt ist. */ + db().prepare("DELETE FROM seitenrecht WHERE pfad = ? AND rolle = ?").run(pfad, rolle); + } else { + db().prepare(`INSERT INTO seitenrecht (pfad, rolle, erlaubt, von, am) + VALUES (?,?,?,?,?) + ON CONFLICT(pfad, rolle) DO UPDATE SET + erlaubt = excluded.erlaubt, von = excluded.von, am = excluded.am`) + .run(pfad, rolle, erlaubt ? 1 : 0, req.person.id, jetzt()); + } + + const stand = tafelLaden(); + /* JEDE AENDERUNG INS PROTOKOLL. Rechte sind das Einzige im Haus, + bei dem man spaeter fragt "seit wann eigentlich?" -- und dann + muss es dastehen. */ + protokolliere("rechte_umgestellt", { + personId: req.person.id, ip: echteIp(req), + detail: `${pfad.replace("/workspace/", "")} · ${rolle} · ${erlaubt ? "darf" : "darf nicht"}`, + }); + res.json({ ok: true, erlaubt, abweichungen: stand.abweichungen }); + } catch (f) { + console.error("[rechte] Feld:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + +/* ALLES ZURUECK AUF DEN GRUNDSTAND. + Ein Knopf, nicht 200 Klicks. Er ist der Grund, warum man sich hier + ueberhaupt trauen kann, etwas auszuprobieren. */ +rechteRouter.delete("/workspace/api/rechte/abweichungen", gleicheHerkunft, nurDogFather, + (req, res) => { + try { + const { n } = db().prepare("SELECT COUNT(*) AS n FROM seitenrecht").get(); + db().prepare("DELETE FROM seitenrecht").run(); + tafelLaden(); + protokolliere("rechte_zurueckgesetzt", { + personId: req.person.id, ip: echteIp(req), detail: `${n} Abweichungen`, + }); + res.json({ ok: true, zurueckgenommen: n }); + } catch (f) { + console.error("[rechte] Zuruecksetzen:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); diff --git a/server/workspace.js b/server/workspace.js index 99b870e9..21098b6b 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -27,7 +27,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { mkdirSync } from "node:fs"; import { createRequire } from "node:module"; -import { darfSeite, OHNE_ANMELDUNG, SEITEN, ALLE_ROLLEN } from "./rechte.js"; +import { darfSeite, OHNE_ANMELDUNG, seitenFuer } 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. */ @@ -717,6 +717,28 @@ function vorGruppe(liste, gruppe, ...neue) { Teil des Teams, nicht darueber -- wenn es jemandem zu viel wird, dann ihnen genauso. Eine Kachel, die nur die anderen ausfuellen, waere genau die Schieflage, die dieses Haus nicht haben soll. */ +/* WER SIEHT WAS -- auf BEIDEN Adressen (Filipe, 11.09.2026). + + Sie stand nur bei den Zusatzkacheln der Agenturseite. Auf der + Team-Dogi-Seite arbeiten DogFather und die rechte Hand aber taeglich; + dort nicht nachsehen zu koennen, wer was darf, hiesse, die Frage an + dem Ort zu stellen, an dem man gerade nicht ist. + + EINMAL BESCHRIEBEN, ZWEIMAL BENUTZT. Zwei Kachelobjekte mit + demselben Ziel waeren zwei Gelegenheiten, eines davon umzubenennen. + + 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. Gefunden hat es pruef-start-ansicht, die ueber die ECHTE Ansicht + zaehlt ("34 Farben auf 35 Kacheln"). Ton 35 ist gemessen, nicht + geraten. */ +const RECHTE_KACHEL = { + name: "Wer sieht was", unter: "Jede Rolle, jede Seite – ausgerechnet, nicht notiert", + zeichen: "schutz", ton: 35, ziel: "rechte.html", szene: "studio", +}; + const BEFINDEN_KACHEL = { gruppe: "Für dich", gruppeUnter: "Dein Platz im Team", name: "Wie geht's dir?", unter: "Sechs Fragen – die Antworten sieht nur du", @@ -727,7 +749,10 @@ const HAND_BEREICHE = vorGruppe( [...MODI_BEREICHE, EINGANG_KACHEL, PERSONEN_KACHEL], "Rund ums Live", ENTWICKLUNG_KACHEL, TALENTE_KACHEL, -).concat(BEFINDEN_KACHEL); +).concat(BEFINDEN_KACHEL, { + gruppe: "Für dich", gruppeUnter: "Dein Platz im Team", + ...RECHTE_KACHEL, +}); /* ===================================================================== @@ -829,6 +854,40 @@ const HAND_MIT_TREFF = [...HAND_BEREICHE, ...TREFF_BEREICHE, TREFF_MODERATION_KA importiert von dort. Eine zweite Menge waere die, die auseinanderlaeuft. */ export { AUSSEN_ROLLEN }; +/* ===================================================================== + KACHEL UND TUER GEHOEREN ZUSAMMEN (11.09.2026) + ===================================================================== + + Seit DogFather die Rechtetafel von Hand umstellen kann, kann eine + Seite zugehen, ohne dass ihre Kachel verschwindet. Gefunden hat das + pruef-rechte-umstellen mit einer einzigen Zeile: Er nahm einem Modi + chat.html weg -- die Schranke griff sofort, und die Kachel stand + weiterhin da. Ein Knopf, der auf die Startseite zurueckwirft. + + Der Satz "Kachel und Tuer gehoeren zusammen" steht seit dem + 06.09.2026 in assets/js/bereiche.js. Bis heute war er eine Bitte an + den, der beides pflegt. Jetzt ist er eine Rechnung: Die Kachelliste + wird gegen dieselbe Tafel gefiltert, aus der die Schranke ihre + Entscheidung holt. + + NACH DEM PFAD UND NICHT NACH DEM GANZEN ZIEL: "bereich.html?b=live" + ist die Seite bereich.html. Wer die Frage am Fragezeichen nicht + abschneidet, filtert jede Brett-Kachel weg -- lautlos, denn eine + fehlende Kachel sieht aus wie eine Rolle, die sie eben nicht hat. + + WAS KEIN ZIEL HAT, BLEIBT. Eine Kachel ohne "ziel" ist kein Weg zu + einer Seite, und "ich kenne die Seite nicht" darf nicht "also weg + damit" heissen. */ +function nurOffeneKacheln(liste, person) { + if (!Array.isArray(liste) || !person) return liste; + return liste.filter((k) => { + const ziel = String(k?.ziel || ""); + if (!ziel) return true; + const pfad = "/workspace/" + ziel.split("?")[0]; + return darfSeite(person, pfad); + }); +} + export function bereicheFuer(person) { if (person?.rolle === "modi") return MODI_MIT_TREFF; if (person?.rolle === "hand") return HAND_MIT_TREFF; @@ -1201,22 +1260,8 @@ const ZUSATZ_BEREICHE = { 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", + ...TEAM_GRUPPE, ...RECHTE_KACHEL, + gruppe: TEAM_GRUPPE.gruppe, gruppeUnter: TEAM_GRUPPE.gruppeUnter, }, { ...ENTWICKLUNG_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe }, { ...TALENTE_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe }, @@ -3590,57 +3635,13 @@ function stillerZugang(code) { } } -/* ===================================================================== - WER SIEHT WAS — die Tafel, wie sie wirklich ist (11.09.2026) - ===================================================================== +/* DIE TAFEL LIEGT SEIT DEM 11.09.2026 IN workspace-rechte.js. - 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, - }); -}); + Hier stand ein nur LESENDER Weg. Filipe: "so dass ich die da auch + manuell wechseln und speichern kann" -- damit gehoeren Lesen und + Umstellen zusammen, samt der Tabelle dahinter und der Frage, wer was + darf. Ein Leseweg hier und ein Schreibweg dort waeren zwei Orte fuer + dieselbe Sache. */ workspaceRouter.post("/workspace/api/anmelden", (req, res) => { const ip = echteIp(req); @@ -3958,12 +3959,21 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => { sehen. Genau daran war beim Sicht-Umschalter schon einmal zu erkennen, dass er nicht greift ("ich seh immer noch die Seite genau wie meine"). */ - bereiche: bereicheFuer(angesehen || person), + bereiche: nurOffeneKacheln(bereicheFuer(angesehen || person), angesehen || person), rolle_text: rollentextFuer(angesehen || person), marke: markeFuer(angesehen || person, req.get("host")), /* Kacheln, die zur eigenen Liste DAZUkommen -- im Gegensatz zu `bereiche`, das sie ersetzt. Leer fuer alle, die keine haben. */ - bereiche_zusatz: zusatzBereicheFuer(angesehen || person), + bereiche_zusatz: nurOffeneKacheln(zusatzBereicheFuer(angesehen || person), + angesehen || person), + /* DIE EIGENEN SEITEN -- damit die Oberflaeche ihre EIGENE + Kachelliste (assets/js/bereiche.js) ebenfalls filtern kann. + + Geliefert wird, was die Person DARF, nicht was sie nicht darf: + Eine Liste der verbotenen Seiten waere eine Aufzaehlung dessen, + was es sonst noch gibt -- und damit genau die Auskunft, die + niemand bekommen soll. */ + seiten: seitenFuer((angesehen || person).rolle), }); }); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 8f49c029..ed7f69ea 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609112217", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609112230", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609112217", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609112230", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609112217", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609112230", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609112217", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609112230", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/treff.css b/workspace/assets/css/treff.css index 3873db79..90b66579 100644 --- a/workspace/assets/css/treff.css +++ b/workspace/assets/css/treff.css @@ -294,3 +294,80 @@ .treff-fall__zeile { display: block; } .treff-fall__schild { display: block; padding: 0 0 1px; } } + +/* ===================================================================== + DIE TAFEL IST SEIT DEM 11.09.2026 BEDIENBAR + + Aus jedem Feld wird ein Schalter. Zwei Dinge müssen dabei sichtbar + sein, und beide sind leicht zu übersehen: + + 1. DASS man klicken kann. Ein Punkt in einer Tabelle sieht nach + Anzeige aus, nicht nach Bedienelement — deshalb bekommt jedes + Feld eine Fläche, die auf Mauszeiger und Tastaturfokus + antwortet. + + 2. WAS von Hand gesetzt ist. Der Code ist der Grundstand; was davon + abweicht, hat jemand entschieden. Ohne Markierung wüsste nach + vier Wochen niemand mehr, was gedacht war. + + UND DIE FLÄCHE IST GROSS GENUG ZUM TIPPEN. 34 mal 30 Pixel — unter + der üblichen Empfehlung von 44, aber das ist eine Tabelle mit acht + Spalten auf einem Handy, und ein größeres Feld hieße weniger Spalten. + Der Ausgleich ist der Abstand: zwischen zwei Feldern liegen die + Zellenränder, sodass man nicht auf den Nachbarn kommt. + ===================================================================== */ + +.rechte-leiste { + display: flex; flex-wrap: wrap; align-items: center; gap: 12px; + margin: 0 0 20px; padding: 12px 16px; + background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%); + border: 1px solid var(--rand); +} +.rechte-leiste__satz { + flex: 1 1 240px; font-size: .85rem; line-height: 1.55; color: var(--text-leise); +} + +.rechte-feld { + display: inline-flex; align-items: center; justify-content: center; + width: 34px; height: 30px; padding: 0; cursor: pointer; + font: inherit; + background: transparent; + border: 1px solid transparent; border-radius: 6px; + transition: background .14s ease, border-color .14s ease; +} +.rechte-feld:hover:not(:disabled) { + background: rgba(255, 255, 255, .07); + border-color: var(--rand-hell); +} +.rechte-feld:focus-visible { + outline: 2px solid var(--akzent); outline-offset: 1px; +} +.rechte-feld:disabled { opacity: .45; cursor: default; } + +/* FEST heißt: kein Schalter, sondern eine Tatsache. Er sieht deshalb + nicht aus wie etwas, das man drücken könnte — der Server würde es + ohnehin ablehnen, und ein Knopf, der immer eine Absage bringt, wird + trotzdem gedrückt. */ +.rechte-feld[data-fest="ja"] { + cursor: default; + border-style: dashed; + border-color: var(--rand); +} +.rechte-feld[data-fest="ja"]:hover { background: transparent; } + +/* VON HAND GESETZT. Ein Rahmen, keine Farbe: Die Zelle sagt damit + „hier hat jemand entschieden", ohne zu sagen, ob das gut war. */ +.rechte-tafel td[data-anders="ja"] { + background: color-mix(in srgb, var(--akzent) 9%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--akzent) 34%, transparent); +} + +.rechte-zahlen__anders { color: var(--text-still); } + +/* Die Meldung nach dem Speichern ist kein Fehler — sie darf nicht + aussehen wie einer. */ +.fehler[data-art="gut"] { color: var(--text-leise); } + +@media (prefers-reduced-motion: reduce) { + .rechte-feld { transition: none; } +} diff --git a/workspace/assets/js/rechte.js b/workspace/assets/js/rechte.js index 918698a8..86974074 100644 --- a/workspace/assets/js/rechte.js +++ b/workspace/assets/js/rechte.js @@ -1,14 +1,33 @@ /* ===================================================================== - „Wer sieht was" — die Rechtetafel, gezeichnet (11.09.2026) + „Wer sieht was" — ansehen und umstellen (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. + Filipe: *„so dass ich die da auch manuell wechseln und speichern + kann."* - 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. + --------------------------------------------------------------------- + GESPEICHERT WIRD SOFORT, MIT JEDEM KLICK + + Kein „Speichern"-Knopf am Ende. Bei einer Tafel mit zweihundert + Feldern ist der die Stelle, an der man eine halbe Änderung verliert — + und eine halbe Änderung an Rechten ist die gefährlichste Lage von + allen. Zurückgenommen wird mit demselben Klick. + + --------------------------------------------------------------------- + WAS HIER NICHT ENTSCHIEDEN WIRD + + Ob jemand etwas darf. Der Browser fragt und zeichnet; jede Absage + kommt vom Server. Die rechte Hand sieht dieselbe Tafel, bekommt aber + keine Knöpfe — nicht weil dieses Skript sie versteckt, sondern weil + der Server `darf_aendern: false` schickt. Ein Knopf weniger ist + Bequemlichkeit, kein Riegel. + + --------------------------------------------------------------------- + UND EIN UNTERSCHIED, DEN MAN SEHEN MUSS + + Der Code in rechte.js ist der GRUNDSTAND. Was davon abweicht, ist von + Hand gesetzt — und das steht an der Zelle. Ohne diese Markierung wüsste + nach vier Wochen niemand mehr, was gedacht war und was jemand + daraus gemacht hat. ===================================================================== */ (() => { const $ = (id) => document.getElementById(id); @@ -18,104 +37,200 @@ if (text !== undefined) n.textContent = text; return n; }; - - const melde = (t) => { + const melde = (t, art) => { const f = $('fehler'); - if (f) { f.textContent = t; f.hidden = !t; } + if (!f) return; + f.textContent = t; + f.hidden = !t; + f.dataset.art = art || 'fehler'; }; - (async () => { - let r; + let daten = null; + + 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; + } + + /* ---- Ein Feld ------------------------------------------------------- */ + function feld(seite, rolle) { + const darf = seite.rollen.includes(rolle.schluessel); + const anders = darf !== seite.grund.includes(rolle.schluessel); + const fest = seite.fest.includes(rolle.schluessel); + + const td = document.createElement('td'); + if (anders) td.dataset.anders = 'ja'; + + /* OHNE RECHT ZUM UMSTELLEN BLEIBT ES EIN ZEICHEN, KEIN KNOPF. + Ein Knopf, der nichts tut, ist schlimmer als keiner — man drückt + ihn immer wieder. */ + if (!daten.darf_aendern) { + td.append(zeichen(darf)); + if (anders) td.title = 'Von Hand gesetzt – weicht vom Grundstand ab.'; + return td; + } + + const b = el('button', 'rechte-feld'); + b.type = 'button'; + b.setAttribute('aria-pressed', String(darf)); + b.setAttribute('aria-label', + `${rolle.name} · ${seite.datei}: ${darf ? 'darf' : 'darf nicht'}`); + if (fest) b.dataset.fest = 'ja'; + b.title = fest + ? 'Diese Seite kannst du dir nicht selbst wegnehmen – sonst kämst du ' + + 'nicht mehr an die Rechte, die Zugänge oder die Startseite.' + : (anders ? 'Von Hand gesetzt. Klicken stellt es um.' : 'Klicken stellt es um.'); + b.append(zeichen(darf)); + b.addEventListener('click', () => umstellen(seite, rolle, !darf, b)); + td.append(b); + return td; + } + + function zeichen(darf) { + if (darf) { + const punkt = el('span', 'rechte-ja'); + punkt.setAttribute('role', 'img'); + punkt.setAttribute('aria-label', 'darf'); + return punkt; + } + /* 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'); + return nein; + } + + async function umstellen(seite, rolle, erlaubt, knopf) { + /* EINE RÜCKFRAGE NUR BEIM ÖFFNEN, nicht beim Schließen. + Etwas wegzunehmen kann man in einer Sekunde zurückdrehen und es + sieht sofort jemand. Etwas aufzumachen merkt unter Umständen + lange niemand — und genau das ist der Klick, bei dem eine + Sekunde Nachdenken etwas wert ist. */ + if (erlaubt && !confirm( + `„${rolle.name}" darf danach ${seite.datei} öffnen.\n\n` + + 'Das gilt sofort, für alle mit dieser Rolle. Sicher?')) return; + + knopf.disabled = true; 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 a = await hole('/workspace/api/rechte/feld', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pfad: seite.pfad, rolle: rolle.schluessel, erlaubt }), + }); + if (!a.ok) { + melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); + return; + } + melde(`Gespeichert: „${rolle.name}" darf ${seite.datei} ` + + `${erlaubt ? 'jetzt' : 'nicht mehr'} öffnen.`, 'gut'); + await laden(); + } finally { + knopf.disabled = false; } + } - const gesamt = r.seiten.length; + /* ---- Zeichnen --------------------------------------------------------- */ + function zeichne() { + const gesamt = daten.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. */ + /* Die Zahlen je Rolle. Sortiert nach Anzahl — man sieht 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); + zahlen.textContent = ''; + for (const rolle of [...daten.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); + if (rolle.abweichend) { + const m = el('span', 'rechte-zahlen__anders', ` · ${rolle.abweichend} von Hand`); + m.title = 'So viele Felder weichen bei dieser Rolle vom Grundstand ab.'; + li.append(m); } + zahlen.append(li); } - /* ---- Die Tafel --------------------------------------------------- */ + /* Die Leiste: was ist von Hand gesetzt, und wie kommt man zurück. */ + const leiste = $('leiste'); + leiste.textContent = ''; + if (daten.abweichungen) { + leiste.hidden = false; + leiste.append(el('span', 'rechte-leiste__satz', + `${daten.abweichungen} ${daten.abweichungen === 1 ? 'Feld ist' : 'Felder sind'} ` + + 'von Hand gesetzt und weicht vom Grundstand ab.')); + if (daten.darf_aendern) { + const b = el('button', 'schritt', 'Alles zurück auf Grundstand'); + b.type = 'button'; + b.addEventListener('click', async () => { + if (!confirm(`${daten.abweichungen} Änderungen zurücknehmen?\n\n` + + 'Danach gilt wieder genau das, was im Code steht.')) return; + b.disabled = true; + const a = await hole('/workspace/api/rechte/abweichungen', { method: 'DELETE' }); + b.disabled = false; + if (!a.ok) { melde('Ging nicht.'); return; } + melde('Alles zurück auf Grundstand.', 'gut'); + await laden(); + }); + leiste.append(b); + } + } else { + leiste.hidden = false; + leiste.append(el('span', 'rechte-leiste__satz', + daten.darf_aendern + ? 'Alles steht so, wie es im Code steht. Klick auf ein Feld, um es umzustellen – ' + + 'das wird sofort gespeichert.' + : 'Alles steht so, wie es im Code steht. Umstellen kann das nur DogFather.')); + } + + /* Die Tafel. */ const ziel = $('tafel'); - if (ziel) { - ziel.textContent = ''; - const tab = document.createElement('table'); + 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'); + const kopf = document.createElement('thead'); + const kz = document.createElement('tr'); + kz.append(el('th', null, 'Seite')); + for (const rolle of daten.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 daten.seiten) { + const tr = document.createElement('tr'); + tr.append(el('td', null, seite.datei)); + for (const rolle of daten.rollen) tr.append(feld(seite, rolle)); + 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)); - } - } - })(); + offen.textContent = ''; + for (const pfad of daten.ohne_anmeldung) offen.append(el('li', null, pfad)); + } + + async function laden() { + let a; + try { + a = await hole('/workspace/api/rechte/tafel'); + } catch { return; } + if (!a.ok) { melde('Die Tafel konnte nicht geladen werden.'); return; } + daten = await a.json(); + zeichne(); + } + + (async () => { try { await laden(); } catch { /* umgeleitet */ } })(); })(); diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index 5208baa7..cb1fd611 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -155,7 +155,17 @@ "keine Kacheln". Die beiden auseinanderzuhalten ist kein Feinschliff: Verwechselte man sie, haetten die fuenf bekannten Rollen ab sofort eine leere Startseite. */ - function eigeneGruppen(wer) { + /* Ist die Seite hinter diesem Ziel fuer mich offen? + Ohne Auskunft vom Server: ja -- siehe oben. */ + function seiteOffen(ziel) { + const offen = ich?.seiten; + if (!Array.isArray(offen) || !offen.length) return true; + const z2 = String(ziel || ""); + if (!z2) return true; + return offen.includes('/workspace/' + z2.split('?')[0]); + } + + function eigeneGruppen(wer) { const vomServer = ich && ich.bereiche; if (Array.isArray(vomServer)) { /* Nach Gruppen buendeln, in der Reihenfolge, in der sie kommen @@ -186,7 +196,21 @@ kaputtgeht. */ const zusatz = Array.isArray(ich?.bereiche_zusatz) ? ich.bereiche_zusatz : []; const gruppen = GRUPPEN - .map((g) => ({ ...g, bereiche: g.bereiche.filter((b) => b.rollen.includes(wer.rolle)) })) + /* UND GEGEN DIE RECHTETAFEL (11.09.2026). + + `rollen` an der Kachel ist die Liste in dieser Datei -- sie + wird von Hand gepflegt und kennt nichts davon, dass DogFather + eine Seite umgestellt hat. `ich.seiten` kommt vom Server und + ist dieselbe Auskunft, aus der auch die Schranke ihre + Entscheidung holt. + + BEIDES UND NICHT NUR EINES: Die Liste hier entscheidet, was + ueberhaupt angeboten wird; die Tafel, ob es noch offen ist. + Faellt `ich.seiten` aus (alte Fassung im Zwischenspeicher), + bleibt es beim alten Verhalten -- eine Kachel zu viel ist + aergerlich, eine fehlende sieht aus wie ein Fehler. */ + .map((g) => ({ ...g, bereiche: g.bereiche.filter((b) => b.rollen.includes(wer.rolle) + && seiteOffen(b.ziel)) })) .filter((g) => g.bereiche.length); for (const b of zusatz) { const g = gruppen.find((x) => x.name === b.gruppe); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 0c225977..dd262ade 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -252,10 +252,10 @@ - - - - - + + + + +