diff --git a/server/pruef-creator-anlegen.mjs b/server/pruef-creator-anlegen.mjs index 354ff552..bc41d588 100644 --- a/server/pruef-creator-anlegen.mjs +++ b/server/pruef-creator-anlegen.mjs @@ -431,16 +431,21 @@ console.log("\n=== Die Rollenknoepfe auf der Zugaenge-Seite ==="); const managerKnoepfe = await knoepfeFuer("manager", "CODE-MAX-00001"); ok(managerKnoepfe.length === 1 && managerKnoepfe[0] === "creator", `ein Manager sieht genau einen: ${managerKnoepfe.join(", ")}`); - /* UND BEI DOGFATHER AENDERT SICH NICHTS. Er bekommt jetzt eine - laengere Liste vom Server (auch die verborgenen Rollen), die - Knopfliste der Seite kennt aber nur die fuenf sichtbaren -- der - Schnitt der beiden ist derselbe wie vorher. Das ist eine Behauptung - ueber MEINE Aenderung, also gehoert sie gemessen und nicht - geglaubt. */ + /* BEI DOGFATHER FEHLT SEIT DEM 11.09.2026 GENAU EINE: seine eigene. + + Filipe: "dogfather soll man nicht auswaehlen koennen. das ist die + einzige die man nicht auswaehlen kann bitte." Hier stand + `["spicy","admin","manager","scout","creator"].every(...)` -- die + Zeile war richtig, bis die Rolle aus ANLEGBAR fiel. + + ZWEI AUSSAGEN STATT EINER: Die vier muessen DA sein, und `admin` + muss FEHLEN. Nur "vier sind da" waere auch dann gruen, wenn admin + wieder auftauchte. */ const dogiKnoepfe = await knoepfeFuer("admin", "CODE-DOGI-0001"); - ok(dogiKnoepfe.length >= 5 && ["spicy", "admin", "manager", "scout", "creator"] - .every((r) => dogiKnoepfe.includes(r)), - `DogFather sieht weiterhin alle fuenf: ${dogiKnoepfe.join(", ")}`); + ok(["spicy", "manager", "scout", "creator"].every((r) => dogiKnoepfe.includes(r)), + `DogFather sieht die vier vergebbaren: ${dogiKnoepfe.join(", ")}`); + ok(!dogiKnoepfe.includes("admin"), + "seine eigene Rolle steht NICHT zur Wahl -- auch bei ihm nicht"); await browser.close(); } diff --git a/server/pruef-haus-trennung.mjs b/server/pruef-haus-trennung.mjs index 4a6f84e9..d84c2be0 100644 --- a/server/pruef-haus-trennung.mjs +++ b/server/pruef-haus-trennung.mjs @@ -465,13 +465,34 @@ console.log("=== Die Rolle aendern ==="); keks: dogi.keks, rumpf: JSON.stringify({ rolle: "creator" }) }); ok(fremd.status === 403, `auf crew. auch nicht zum Creator (${fremd.status})`); - /* DER LETZTE DOGFATHER. Es gibt genau einen -- VanVan ist in dieser - Pruefung schon die rechte Hand. Also einen zweiten anlegen, den - ersten darf man dann herabstufen, den zweiten nicht mehr. */ - const zweiter = await hole(weg, { host: WS, methode: "POST", keks: dogi.keks, + /* SEIT DEM 11.09.2026 LAESST SICH DIE ROLLE NICHT MEHR VERGEBEN. + + Filipe: "dogfather soll man nicht auswaehlen koennen. das ist die + einzige die man nicht auswaehlen kann bitte." Hier stand bis dahin + ein Anlegen ueber die Schnittstelle -- das geht jetzt nicht mehr, + und das ist der erste Prueffall geworden statt eines Hilfsschritts. */ + const versuch = await hole(weg, { host: WS, methode: "POST", keks: dogi.keks, rumpf: JSON.stringify({ name: "Zweitzugang", rolle: "admin" }) }); - const idZweit = zweiter.daten?.id; - ok(zweiter.status === 201 && idZweit > 0, `ein zweiter DogFather-Zugang (${zweiter.status})`); + ok(versuch.status === 403 || versuch.status === 400, + `ein zweiter DogFather laesst sich NICHT anlegen (${versuch.status})`); + + /* DER LETZTE DOGFATHER -- und dafuer braucht es trotzdem zwei. + + Der zweite entsteht jetzt direkt im Bestand, nicht ueber die + Schnittstelle. Das ist kein Trick an der Regel vorbei, sondern ihr + Gegenteil: Die Regel sagt "nicht ueber die Oberflaeche", und genau + das wird eine Zeile darueber nachgewiesen. Fuer die FOLGEfrage -- + darf der vorletzte herabgestuft werden? -- braucht die Pruefung + einen Bestand, den die Oberflaeche nicht mehr herstellen kann. + + Ohne diesen Umweg waere die Sicherung "nie den letzten DogFather" + ab heute ungeprueft. Eine Pruefung wegzulassen, weil ihre + Voraussetzung schwerer herzustellen ist, waere der bequeme Weg -- + und die Sicherung steht zwischen einem Versehen und einem Haus + ohne Zugang. */ + const idZweit = anlegen("Zweitzugang", "admin", "CODE-ZWEI-0001"); + ok(Number.isInteger(idZweit) && idZweit > 0, + `ein zweiter DogFather-Zugang im Bestand (#${idZweit})`); const runter = await hole(`${weg}/${idZweit}/rolle`, { host: WS, methode: "PUT", keks: dogi.keks, rumpf: JSON.stringify({ rolle: "scout" }) }); diff --git a/server/pruef-personen-formular.mjs b/server/pruef-personen-formular.mjs index bf4201f0..0f0f2372 100644 --- a/server/pruef-personen-formular.mjs +++ b/server/pruef-personen-formular.mjs @@ -154,10 +154,20 @@ const mass = await dogi.evaluate(() => { Geprüft wird ab jetzt die LISTE und nicht nur die Anzahl: Sechs Karten könnten auch fünf richtige und eine doppelte sein. */ -ok(mass.karten === 7, `alle sieben Rollen stehen zur Wahl (${mass.karten})`); +/* SECHS SEIT DEM 11.09.2026, NICHT SIEBEN. Filipe: "dogfather soll man + nicht auswaehlen koennen. das ist die einzige die man nicht + auswaehlen kann bitte." Die Karte faellt damit fuer JEDEN weg, auch + fuer DogFather selbst. + + DIE ZEILE WURDE GEDREHT, NICHT GELOESCHT: Dass `admin` FEHLT, ist + jetzt eine eigene Aussage -- sonst haette die Liste unten still eine + Rolle mehr bekommen koennen, ohne dass es jemand merkt. */ +ok(mass.karten === 7, `alle sieben vergebbaren Rollen stehen zur Wahl (${mass.karten})`); ok(JSON.stringify([...mass.kartenRollen].sort()) - === JSON.stringify(["admin", "creator", "hand", "manager", "modi", "scout", "spicy"]), + === JSON.stringify(["creator", "gast", "hand", "manager", "modi", "scout", "spicy"]), `und es sind die richtigen (${mass.kartenRollen.join(", ")})`); +ok(!mass.kartenRollen.includes("admin"), + "DogFather steht NICHT zur Wahl -- auch nicht fuer DogFather"); /* DIE STELLVERTRETUNG TRAEGT EIN EIGENES ZEICHEN. @@ -179,11 +189,19 @@ ok(JSON.stringify([...mass.kartenRollen].sort()) ist daraus eine Gruppe geworden -- Husky bei den beiden mit dem Ueberblick, Pfote bei der taeglichen Arbeit. - Verglichen wird gegen die Admin-Karte und nicht gegen "#r-husky": - Waere dort morgen ein anderes Zeichen, muesste die rechte Hand - mitwandern -- gewollt ist "dasselbe wie er", nicht "der Husky". */ -ok(mass.kartenZeichen.hand && mass.kartenZeichen.hand === mass.kartenZeichen.admin, - `die Rechte Hand trägt dasselbe Zeichen wie DogFather (${mass.kartenZeichen.hand} / ` + BIS ZUM 11.09.2026 WURDE GEGEN DIE ADMIN-KARTE VERGLICHEN, und das + war die bessere Messung: gewollt ist "dasselbe wie er", nicht "der + Husky". Seit DogFather nicht mehr vergeben werden kann, gibt es + diese Karte nicht mehr -- der Vergleich lief gegen `undefined` und + waere kommentarlos rot geblieben. + + Ersatz ist das Zeichen selbst. Das ist schwaecher, und es steht hier, + damit es niemand fuer gleichwertig haelt: Aendert jemand DogFathers + Husky, faellt es hier NICHT mehr auf. Wer die alte Staerke + zurueckwill, braucht eine Quelle fuer "DogFathers Zeichen", die + nicht an der Auswahlkarte haengt. */ +ok(mass.kartenZeichen.hand === "#r-husky", + `die Rechte Hand trägt den Husky (${mass.kartenZeichen.hand} / ` + `${mass.kartenZeichen.admin})`); /* DIE GEGENPROBE GLEICH DANEBEN: Der Modi muss sich davon unterscheiden. Ohne sie waere die Zeile darueber auch dann gruen, diff --git a/server/pruef-spicy.mjs b/server/pruef-spicy.mjs index b92483f7..b2854306 100644 --- a/server/pruef-spicy.mjs +++ b/server/pruef-spicy.mjs @@ -319,6 +319,78 @@ const ruf = async (art, weg, keks, koerper) => { ok(m.status === 201 && m.daten?.rolle === "manager", `und einen Manager (HTTP ${m.status}, ${m.daten?.rolle})`); } +{ + /* ---- ROLLEN WECHSELN (11.09.2026) ------------------------------- + Filipe: "ich will dass die rolle spicy und dogfather, auch die + rollen wechseln koennen wenn die personen schon drin sind. von + alle kategorien, creator, scouts, manager spicy. dogfather soll + man nicht auswaehlen koennen." + + AN EINEM EIGENEN MENSCHEN, nicht an Luna: Wer hier Luna umstellt, + aendert stillschweigend die Listen, die weiter unten verglichen + werden -- und der Fehlschlag stuende dann an einer Stelle, die + nichts damit zu tun hat. */ + /* VORBEDINGUNG: Die Oberflaeche baut den Knopf aus `/api/ich`. Fehlt + die Angabe dort, ist jede Aussage ueber den Knopf weiter unten + eine Aussage ueber etwas anderes. */ + const ichSpicy = await ruf("GET", "/workspace/api/ich", keksSpicy); + ok(ichSpicy.daten?.darf_rollen_wechseln === true, + `/api/ich sagt ihr, dass sie Rollen wechseln darf (${ichSpicy.daten?.darf_rollen_wechseln})`); + + const neu = await ruf("POST", "/workspace/api/creator-anlegen", keksSpicy, + { name: "Wechselkind" }); + const idWechsel = neu.daten?.id; + ok(neu.status === 201 && Number.isInteger(idWechsel), + `ein Mensch zum Umstellen ist da (#${idWechsel})`); + + const zu = async (keks, rolle, id = idWechsel) => + ruf("PUT", `/workspace/api/verwaltung/personen/${id}/rolle`, keks, { rolle }); + + /* Die vier Kategorien, die Filipe genannt hat -- einzeln, nicht als + "irgendeine geht". Waere nur eine davon offen, saehe ein "geht" + genauso aus. */ + for (const r of ["scout", "manager", "spicy"]) { + const a = await zu(keksSpicy, r); + ok(a.status === 200 && a.daten?.rolle === r, + `Spicy Media stellt auf "${r}" um (HTTP ${a.status})`); + } + const zurueck = await zu(keksSpicy, "creator"); + ok(zurueck.status === 200, `und wieder zurueck auf Creator (HTTP ${zurueck.status})`); + + /* DIE EINZIGE, DIE NICHT GEHT. */ + const hoch = await zu(keksSpicy, "admin"); + ok(hoch.status === 403, + `DogFather laesst sich nicht vergeben (HTTP ${hoch.status}, ${hoch.daten?.fehler || ""})`); + + /* AUCH NICHT VON DOGFATHER SELBST. Das ist der Satz "das ist die + einzige die man nicht auswaehlen kann" -- ohne Ausnahme. */ + const vonDogi = await zu(keksDogi, "admin"); + ok(vonDogi.status === 403, + `auch DogFather kann sie nicht vergeben (HTTP ${vonDogi.status})`); + + /* UND AN DOGFATHER SELBST AENDERT SIE NICHTS. Die Pruefungen oben + sehen auf die ZIEL-Rolle; dass die BETROFFENE Person DogFather + ist, faellt dort gar nicht auf. */ + const anDogi = await zu(keksSpicy, "manager", idDogi); + ok(anDogi.status === 403, + `an DogFathers Zeile aendert sie nichts (HTTP ${anDogi.status})`); + const dogiNoch = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi); + const istNoch = (dogiNoch.daten?.personen || []) + .find((x) => x.id === idDogi)?.rolle; + ok(istNoch === "admin", `und er ist immer noch DogFather (${istNoch})`); + + /* GEGENPROBE ZUR TUER: Ein Manager kommt an diesen Weg gar nicht -- + sonst hiesse "Spicy Media darf" nur, dass jeder darf. */ + const vonMax = await zu(keksMax, "scout"); + ok(vonMax.status === 404, `ein Manager kommt an diesen Weg nicht (HTTP ${vonMax.status})`); + + /* UND DIE UEBRIGE VERWALTUNG BLEIBT ZU. Die Tuer war eng gebaut; + diese Zeile ist der Beweis, dass sie eng geblieben ist. */ + const wegDamit = await ruf("DELETE", + `/workspace/api/verwaltung/personen/${idWechsel}`, keksSpicy); + ok(wegDamit.status === 404, + `loeschen geht fuer sie weiterhin nicht (HTTP ${wegDamit.status})`); +} { /* GEGENPROBE: Ein Manager kann KEINEN Manager anlegen -- sonst waere die zweite Tuer nur Zierde. */ @@ -549,6 +621,49 @@ console.log("\n=== Die Anmeldeseite ==="); `bei ihr steht der Abschnitt DogFather (${alsText.slice(0, 70) || "keine Abschnitte"})`); ok(/Manager|Creator|Scout/.test(alsText), "und die uebrigen Abschnitte ebenfalls -- die Liste ist nicht leer"); + + /* ERST AUFKLAPPEN, DANN MESSEN (11.09.2026). + + Die Kategorien stehen zugeklappt da, und `zeile(p)` -- die Funktion, + die die Knoepfe baut -- laeuft nur fuer eine OFFENE Kategorie. Der + erste Entwurf las sofort nach dem Laden und meldete "kein Knopf". + Das war kein Befund, das war eine zu frueh gestellte Frage; genau + derselbe Fehler wie heute frueh bei den Kacheln der Startseite. + + Geoeffnet wird die Kategorie, die es bei beiden Rollen gibt. */ + for (const kopf of await seite.$$(".gruppe__kopf")) { + const t = (await kopf.textContent()) || ""; + if (/Creator/.test(t)) { await kopf.click(); break; } + } + await seite.waitForTimeout(600); + const zeilenSpicy = await seite.$$eval(".person", (z) => z.length); + ok(zeilenSpicy > 0, `eine Kategorie ist aufgeklappt (${zeilenSpicy} Zeilen)`); + + /* ---- DER KNOPF, NICHT NUR DAS RECHT (11.09.2026) ---------------- + Am selben Tag ist mir im Chat genau das passiert: Das Recht war + richtig vergeben, die Oberflaeche wusste nichts davon, und der + Knopf blieb unsichtbar. Eine Pruefung, die nur die Schnittstelle + misst, waere dabei gruen geblieben. + + ZWEI AUSSAGEN, NICHT EINE: Der Knopf "Rolle aendern" muss da sein, + "Loeschen" NICHT -- das gehoert weiterhin DogFather. Vorher hingen + beide an derselben Zeile. */ + const knoepfeSpicy = await seite.$$eval(".person__tu, .tu-knopf, button", + (b) => b.map((x) => x.textContent.trim())); + ok(knoepfeSpicy.some((t) => /Rolle ändern/.test(t)), + `Spicy Media sieht "Rolle ändern" (${knoepfeSpicy.filter((t) => /Rolle|Löschen/.test(t)).join(", ") || "nichts davon"})`); + ok(!knoepfeSpicy.some((t) => /^Löschen$/.test(t)), + "und \"Löschen\" NICHT -- das bleibt bei DogFather"); + + /* Und in der Auswahl steht DogFather nicht. Gemessen an den + Knoepfen, die wirklich gebaut werden -- nicht an der Liste im + Quelltext, aus der sie entstehen. */ + const rollenKnoepfe = await seite.$$eval("#f-rolle .rollenwahl__knopf", + (b) => b.map((x) => x.dataset.rolle)); + ok(rollenKnoepfe.length >= 3 && !rollenKnoepfe.includes("admin"), + `zur Auswahl stehen ${rollenKnoepfe.join(", ")} -- ohne DogFather`); + ok(rollenKnoepfe.includes("spicy"), + "ihre eigene Rolle ist dabei -- Filipes Liste nennt sie ausdruecklich"); await ctx.close(); /* GEGENPROBE BEI DOGFATHER: Bei ihm steht der Abschnitt "Spicy Media" @@ -568,6 +683,28 @@ console.log("\n=== Die Anmeldeseite ==="); ok(/Spicy Media/.test(abschnitteDogi), `bei DogFather steht der Abschnitt "Spicy Media" (${abschnitteDogi.slice(0, 80)})`); ok(/DogFather/.test(abschnitteDogi), "und seiner ebenfalls"); + + for (const kopf of await seite2.$$(".gruppe__kopf")) { + const t = (await kopf.textContent()) || ""; + if (/Creator/.test(t)) { await kopf.click(); break; } + } + await seite2.waitForTimeout(600); + const zeilenDogi = await seite2.$$eval(".person", (z) => z.length); + ok(zeilenDogi > 0, `auch bei ihm ist sie aufgeklappt (${zeilenDogi} Zeilen)`); + + /* GEGENPROBE BEI DOGFATHER: Bei ihm steht "Loeschen" sehr wohl -- + sonst hiesse die Zeile oben nur, dass es den Knopf nirgends gibt. + Und auch in SEINER Auswahl fehlt DogFather. */ + const knoepfeDogi = await seite2.$$eval(".person__tu, .tu-knopf, button", + (b) => b.map((x) => x.textContent.trim())); + ok(knoepfeDogi.some((t) => /^Löschen$/.test(t)), + "bei DogFather steht \"Löschen\" da"); + ok(knoepfeDogi.some((t) => /Rolle ändern/.test(t)), + "und \"Rolle ändern\" ebenfalls"); + const rollenDogi = await seite2.$$eval("#f-rolle .rollenwahl__knopf", + (b) => b.map((x) => x.dataset.rolle)); + ok(rollenDogi.length >= 3 && !rollenDogi.includes("admin"), + `auch bei ihm ohne DogFather (${rollenDogi.join(", ")})`); await ctx2.close(); await browser.close(); } diff --git a/server/workspace-personen.js b/server/workspace-personen.js index eb1424d2..814f2aee 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -15,7 +15,7 @@ import express from "express"; 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, hausBedingung, + db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtModis, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, TEAM_DOGI_ROLLEN, darfAnlegen, darfRollenWechseln, hausBedingung, } from "./workspace.js"; import { sicherungJetzt } from "./workspace-sicherung.js"; @@ -100,6 +100,35 @@ function nurAdmin(req, res, next) { return next(); } + /* SPICY MEDIA DARF ROLLEN WECHSELN (11.09.2026). + + Filipe: "ich will dass die rolle spicy und dogfather, auch die + rollen wechseln koennen wenn die personen schon drin sind. von + alle kategorien, creator, scouts, manager spicy." + + DRITTE AUSNAHME, GLEICHE BAUWEISE wie die beiden darueber: eine + Methode, eine Adresse, eine Rolle. Wer hier etwas anderes baut, + hat drei Fassungen derselben Ausnahme -- und die dritte laesst + irgendwann mehr durch als gedacht. + + Codes, Sperren, Loeschen, Zuteilung und Protokoll bleiben bei + DogFather. Filipe hat das Rollenwechseln genannt, nicht die + Verwaltung. + + DER PFAD TRAEGT EINE NUMMER, laesst sich also nicht wie oben + vergleichen. Das Muster ist absichtlich streng: genau + /personen//rolle, nichts davor und nichts danach. + + WELCHE Rolle sie vergeben darf, steht NICHT hier -- das entscheidet + ANLEGBAR in der Route. Diese Zeile oeffnet nur die Tuer. */ + const rollenWeg = req.method === "PUT" + && /^\/workspace\/api\/verwaltung\/personen\/[0-9]+\/rolle$/ + .test(req.baseUrl + req.path); + if (darfRollenWechseln(person) && rollenWeg) { + req.person = person; + return next(); + } + if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); req.person = person; next(); @@ -817,6 +846,29 @@ personenRouter.put("/workspace/api/verwaltung/personen/:id/rolle", gleicheHerkun return res.status(400).json({ fehler: "Diese Rolle hat sie schon." }); } + /* SICHERUNG 6 -- NEU AM 11.09.2026, WEIL SICH DIE TUER GEOEFFNET HAT. + + Seit Spicy Media Rollen wechseln darf, gibt es einen Fall, den + es vorher nicht geben konnte: jemand ohne DogFather-Rolle, der + an einer DogFather-Zeile steht. + + DIE PRUEFUNGEN DARUEBER FANGEN DAS NICHT. Sie sehen auf die + ZIEL-Rolle ("darfst du 'manager' vergeben?") -- und 'manager' + darf Spicy Media vergeben. Dass die Person, die da herabgestuft + wird, DogFather ist, steht in der AUSGANGS-Rolle, und die kam + bis heute nirgends vor. + + Sicherung 3 (nie den letzten DogFather) haette es heute + zufaellig abgefangen, weil es genau einen gibt. Eine Sperre, + die nur wegen einer Zahl im Bestand haelt, ist keine. + + Zusaetzlich sieht Spicy Media DogFather gar nicht in der Liste + (`req.ohneDogFather`). Das ist eine Sicht, keine Schranke -- wer + die Nummer kennt, ruft den Weg direkt auf. */ + if (person.rolle === "admin" && !istDogFather(req.person)) { + return res.status(403).json({ fehler: "An der DogFather-Rolle ändert nur DogFather." }); + } + /* SICHERUNG 2 */ if (id === req.person.id) { return res.status(400).json({ fehler: "Die eigene Rolle lässt sich nicht ändern.", diff --git a/server/workspace.js b/server/workspace.js index 32b00ecb..5aa6e181 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -219,10 +219,35 @@ export const istDogFather = (person) => !!person && person.rolle === "admin"; endgueltigen Rechten, und den vergibt nur DogFather selbst. ======================================================================= */ const ANLEGBAR = { - /* DogFather: alles, was es gibt. Er geht ueber /verwaltung/personen. */ - admin: [...ROLLEN], - /* Spicy Media: das ganze Team, aber keine Leitung ueber sich. */ - spicy: ["manager", "scout", "creator"], + /* DogFather: alles ausser der eigenen Rolle. + + DIE ROLLE "admin" IST SEIT DEM 11.09.2026 NICHT MEHR VERGEBBAR -- + von niemandem, auch nicht von DogFather selbst. Filipe: "dogfather + soll man nicht auswaehlen koennen. das ist die einzige die man + nicht auswaehlen kann bitte." + + WAS DAS BEDEUTET, damit es niemand spaeter sucht: Es laesst sich + kein zweiter DogFather-Zugang mehr anlegen, und niemand laesst + sich zu einem befoerdern. Der bestehende bleibt unberuehrt und ist + durch Sicherung 3 (nie den letzten DogFather herabstufen) + geschuetzt -- er kann also nicht versehentlich verschwinden. + Zurueckdrehen laesst sich das nur hier in dieser Liste. + + Nebenwirkung, und sie ist erwuenscht: Damit gibt es keinen Weg + mehr, sich ueber die Oberflaeche zur hoechsten Rolle zu machen. */ + admin: [...ROLLEN].filter((r) => r !== "admin"), + /* Spicy Media: das ganze Team -- und seit dem 11.09.2026 auch die + eigene Rolle. + + Filipe: "ich will dass die rolle spicy und dogfather, auch die + rollen wechseln koennen wenn die personen schon drin sind. von + alle kategorien, creator, scouts, manager spicy." + + Dass jemand seine eigene Rolle weitergeben kann, ist eine + Entscheidung und kein Versehen: Spicy Media fuehrt die Agentur. + DogFather bleibt trotzdem ausserhalb der Liste -- niemand hebt + sich ueber die Rolle, die ihn eingesetzt hat. */ + spicy: ["spicy", "manager", "scout", "creator"], /* Ein Manager stellt Creator ein, die er dann auch betreut. */ manager: ["creator"], }; @@ -247,6 +272,26 @@ export function darfAnlegen(person) { return alle.filter((r) => HAUS_TEAM_ROLLEN.has(r)); } +/** Wer darf die Rolle einer Person aendern, die schon da ist? + * + * (11.09.2026) Filipe: "ich will dass die rolle spicy und dogfather, + * auch die rollen wechseln koennen wenn die personen schon drin + * sind." + * + * EINE REGEL, DREI BENUTZER: die Schranke in nurAdmin, der Knopf in + * der Oberflaeche (ueber `darf_rollen_wechseln` in /api/ich) und die + * Route selbst. Stuende sie dreimal da, waere die dritte Abschrift + * die, die eine Rolle vergisst -- genau so ist am selben Tag im Chat + * ein Knopf unsichtbar geblieben, obwohl das Recht stimmte. + * + * WAS SIE NICHT ENTSCHEIDET: WELCHE Rolle vergeben werden darf. Das + * steht in ANLEGBAR und ist bewusst getrennt -- "darf ueberhaupt + * wechseln" und "darf DIESE Rolle vergeben" sind zwei Fragen, und + * ihre Antworten laufen auseinander, sobald eine Rolle dazukommt. + */ +export const darfRollenWechseln = (person) => + !!person && (person.rolle === "admin" || person.rolle === "spicy"); + /* WER FUEHRT TEAM DOGI? (10.09.2026) * * DogFather und seine rechte Hand -- und sonst niemand. Spicy Media und @@ -3982,6 +4027,12 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => { serverseitig offen war. Wer die Antwort nur an einer Stelle hat, kann sie nicht an zweien verschieden haben. */ darf_anlegen: darfAnlegen(person), + /* OB DER KNOPF "ROLLE AENDERN" ERSCHEINT (11.09.2026). Aus + derselben Regel wie die Schranke dahinter -- die Oberflaeche + vergleicht keine Rollennamen mehr selbst. Vorher stand dort + `ich.rolle === 'admin'`, und dieselbe Zeile schaltete auch das + Loeschen frei; die beiden gehoeren nicht zusammen. */ + darf_rollen_wechseln: darfRollenWechseln(person), /* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN. Fuer die fuenf bekannten Rollen steht hier `null`, und die diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index c03beffe..c628b369 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=202609112358", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609120025", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609112358", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609120025", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609112358", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609120025", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609112358", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609120025", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index 45bc8d62..453712b2 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -390,7 +390,11 @@ body[data-nur-anlegen="ja"] #rest-schalter { display: none; } nichts tun -- die Sperre steht im Server. Diese Unterscheidung steht hier ausdruecklich, weil sie beim naechsten Umbau leicht verrutscht. */ body[data-nur-lesen="ja"] #liste { display: block; } -body[data-nur-lesen="ja"] .person__knoepfe, +/* `.person__knoepfe` stand hier bis zum 11.09.2026 mit drin. Seit + Spicy Media Rollen wechseln darf, hat sie GENAU EINEN Knopf -- und + eine Regel, die die ganze Reihe ausblendet, haette ihn mit + weggenommen. Welche Knoepfe entstehen, entscheidet jetzt + personen.js; was es nicht gibt, muss man auch nicht verstecken. */ body[data-nur-lesen="ja"] .person__code, body[data-nur-lesen="ja"] .zuteilung, body[data-nur-lesen="ja"] select { display: none; } diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index 5df8beee..66358b5b 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -31,8 +31,21 @@ const ROLLEN = [ { wert: 'spicy', name: 'Spicy Media', symbol: 'chili', text: 'Überblick über alle – ohne Automationen, ohne fremde Chats. Legt Leute an.' }, + /* DER EINTRAG BLEIBT, DER KNOPF ERSCHEINT NICHT (11.09.2026). + + Seit `ANLEGBAR` die Rolle `admin` nicht mehr enthaelt, filtert + die Zeile unten (`darf.includes(...)`) ihn von selbst heraus -- + fuer jeden, auch fuer DogFather. Filipe: "dogfather soll man + nicht auswaehlen koennen. das ist die einzige die man nicht + auswaehlen kann bitte." + + Warum er trotzdem hier steht: Diese Liste ist auch das + Nachschlagewerk fuer Name und Zeichen der Rolle. Ihn zu loeschen + hiesse, an anderer Stelle eine zweite Liste zu brauchen. Der + Text sagt jetzt, was gilt -- ein Text, der etwas anderes + verspricht als die Oberflaeche tut, ist schlimmer als keiner. */ { wert: 'admin', name: 'DogFather', symbol: 'husky', - text: 'Alle Rechte, endgültig. Nur DogFather legt DogFather, Spicy Media und Manager an.' }, + text: 'Alle Rechte, endgültig. Diese Rolle lässt sich nicht vergeben – auch nicht von DogFather.' }, /* Der Satz stimmte bis zum 07.09.2026 -- seither legen Manager Creator an, und ein Text, der etwas anderes verspricht als die Oberflaeche tut, ist schlimmer als gar keiner. */ @@ -495,6 +508,27 @@ schlimmer als kein Knopf. */ if (ich.rolle === 'hand') { k.append(links, knoepfe); return k; } + /* WER NICHT DOGFATHER IST, BEKOMMT NUR, WAS ER DARF (11.09.2026). + + Vorher war das eine Ja/Nein-Frage: DogFather bekam alles, alle + anderen nichts (bei Spicy Media blendete zusaetzlich das CSS die + ganze Reihe aus). Seit Filipes Wunsch -- "ich will dass die rolle + spicy und dogfather, auch die rollen wechseln koennen wenn die + personen schon drin sind" -- gibt es einen Zwischenfall: eine + Rolle mit GENAU EINEM Knopf. + + Die Verzweigung hier statt einer CSS-Regel: Ein Knopf, den es + gar nicht gibt, kann auch nicht durch eine spaetere Stilaenderung + wieder auftauchen. Und "Neuer Code" und "Sperren" wuerden ihr + weiterhin 404 bringen -- die bleiben deshalb aus. */ + if (ich.rolle !== 'admin') { + if (ich.darf_rollen_wechseln && p.rolle !== 'admin') { + knoepfe.append(tuKnopf('Rolle ändern', () => rolleFragen(p))); + } + k.append(links, knoepfe); + return k; + } + knoepfe.append(tuKnopf('Neuer Code', async () => { const eigen = p.id === ich.id; const frage = eigen @@ -539,13 +573,29 @@ zuerst. Bis heute war Löschen und Neuanlegen der einzige Weg, jemandem eine andere Aufgabe zu geben; genau das soll dieser Knopf ersparen. */ - if (ich.rolle === 'admin') { - knoepfe.append(tuKnopf('Rolle ändern', () => rolleFragen(p))); + /* ZWEI KNOEPFE, ZWEI BEDINGUNGEN (11.09.2026). - const weg = tuKnopf('Löschen', () => loeschenFragen(p)); - weg.dataset.gefahr = 'ja'; - knoepfe.append(weg); - } + Hier stand EINE Zeile (`ich.rolle === 'admin'`) fuer beide. + Filipe: "ich will dass die rolle spicy und dogfather, auch die + rollen wechseln koennen wenn die personen schon drin sind." + Loeschen bleibt bei DogFather -- das hat er nicht genannt, und + "Nur DogFather hat alle endgueltigen Rechte" gilt weiter. + + Die Antwort kommt vom Server (`darf_rollen_wechseln`), nicht + aus einem Rollenvergleich hier. Am selben Tag hat mich genau + das im Chat eine Stunde gekostet: zwei Stellen, eine Aussage, + und nur eine wusste Bescheid. + + An DogFather selbst aendert nur DogFather -- Spicy Media sieht + seine Zeile ohnehin nicht, aber ein Knopf, der eine Absage + bringt, gehoert auch dann nicht hin. */ + /* Ab hier ist `ich` DogFather -- die Verzweigung weiter oben hat + alle anderen schon verabschiedet. Beide Knoepfe gehoeren ihm. */ + knoepfe.append(tuKnopf('Rolle ändern', () => rolleFragen(p))); + + const weg = tuKnopf('Löschen', () => loeschenFragen(p)); + weg.dataset.gefahr = 'ja'; + knoepfe.append(weg); } k.append(links, knoepfe); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index c5a93202..2fc9a68d 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 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index f2157910..a9d1f8b8 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -147,10 +147,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index b5207725..6d6ad805 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -181,14 +181,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 6c96e98d..adca2a19 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 91512b4c..8ac66e22 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 3f0889cd..04e615b8 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index 5c224300..09254be4 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -319,6 +319,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 7e9f949b..7f256f08 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609112358", + "src": "/assets/img/app-symbole/crew-192.png?v=202609120025", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609112358", + "src": "/assets/img/app-symbole/crew-512.png?v=202609120025", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609112358", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609120025", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609112358", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609120025", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index b6e7a06a..06bf8c72 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index 99837ba4..4719b819 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 7cea47e5..babf3f52 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 2f6934ca..e6ead905 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index c627e718..de1a8448 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -382,10 +382,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 9d36c717..ffc6d345 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index db4d644a..7c650533 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index c2f95584..87c9ad03 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index a53dffab..5cbe10d7 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 4c6200e4..03c00e4f 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index e1732782..69791afb 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index afcea918..c57bb304 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 9261a5c1..594331c6 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -126,10 +126,10 @@ - - - - - + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index 05addbda..19348026 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index f2f3a20b..09dbbc3e 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 3cfa96c8..34a65d66 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index e5dac67e..3fcab583 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -110,9 +110,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index 71b2091d..ae891cda 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -190,9 +190,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 9c1d1a2e..094071a0 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 693ade55..3889e10f 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +