diff --git a/server/crew-adresse.js b/server/crew-adresse.js index 83ad4a43..2160bdb5 100644 --- a/server/crew-adresse.js +++ b/server/crew-adresse.js @@ -74,6 +74,25 @@ export const CREW_ADRESSE = "https://crew.dogfather-universe.com"; +/* ===================================================================== + DAS TEAM VON DOGI -- WELCHE ROLLEN DAZUGEHOEREN + + Blueprint V3.0, Kapitel 1.1 und 3.1: DogFather (Owner), die "rechte + Hand" (Deputy, eine Person, austauschbar) und die Modis. Die beiden + letzten leben ausschliesslich auf dieser Adresse und sind fuer die + Workspace-Welt unsichtbar. DogFather steht in beiden Welten und + gehoert deshalb NICHT in diese Menge -- sie beantwortet die Frage + "wer ist hier verborgen", nicht "wer gehoert zum Team". + + WARUM SIE HIER STEHT UND NICHT IN workspace.js, wo das uebrige + Rollenmodell liegt: workspace.js importiert diese Datei. Stuende die + Menge dort, muesste diese Datei zurueckimportieren -- ein Kreis, den + Node zwar aufloest, aber mit halb gefuellten Modulen, und das faellt + erst zur Laufzeit auf. Diese Datei hat bewusst KEINE eigenen + Importe; sie ist damit der einzige Ort, an dem so etwas gefahrlos + stehen kann. workspace.js gibt sie fuer die Fachmodule weiter. */ +export const TEAM_DOGI_ROLLEN = new Set(["hand", "modi"]); + /** Die Adresse der Modi-App. Feste Menge, kein Muster -- Falle 1. */ const CREW_HOSTS = new Set(["crew.dogfather-universe.com"]); @@ -121,10 +140,13 @@ export function istWorkspacePfad(pfad) { * @param {string} hostKopfzeile Inhalt des Host-Kopffeldes */ export function sitzungPasstZurAdresse(rolle, hostKopfzeile) { - const modi = rolle === "modi"; - if (istCrewAdresse(hostKopfzeile)) return modi; // crew: nur Modis - if (istOhneModiAdresse(hostKopfzeile)) return !modi; // echte alte Adressen: keine Modis - return true; // localhost, Pruefadressen: unberuehrt + const team = TEAM_DOGI_ROLLEN.has(rolle); + /* crew.: das Team -- und DogFather, der in beiden Welten arbeitet. */ + if (istCrewAdresse(hostKopfzeile)) return team || rolle === "admin"; + /* Die echten alten Adressen: alle ausser dem Team. */ + if (istOhneModiAdresse(hostKopfzeile)) return !team; + /* localhost, Pruefadressen: unberuehrt. */ + return true; } /** diff --git a/server/pruef-crew-adresse.mjs b/server/pruef-crew-adresse.mjs index 051d524e..48efbbb1 100644 --- a/server/pruef-crew-adresse.mjs +++ b/server/pruef-crew-adresse.mjs @@ -109,6 +109,7 @@ function anlegen(name, rolle, code) { anlegen("Filipe", "admin", "CODE-DOGI-0001"); anlegen("Schulle", "manager", "CODE-MAN-0001"); +anlegen("Rieke", "hand", "CODE-HAND-0001"); const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); const CREW = "crew.dogfather-universe.com"; @@ -155,14 +156,23 @@ ok(istWorkspacePfad("/workspaceXYZ") === false, "/workspaceXYZ gehoert NICHT daz ok(istWorkspacePfad("/workspace-alt") === false, "/workspace-alt gehoert NICHT dazu (Falle 3)"); ok(istWorkspacePfad("/assets/img/x.png") === false, "/assets liegt ausserhalb"); -const ROLLEN = ["admin", "spicy", "manager", "scout", "creator", "modi"]; +/* SEIT DEM 10.09.2026 DREI ROLLEN AUF crew. -- Filipe: "3 rollen. + dogfather. rechte hand und modis." + + DogFather steht damit auf BEIDEN Adressen. Das ist keine Luecke in + der Trennung, sondern ihr Sinn: Er ist die einzige Person, die in + beiden Welten arbeitet. Die rechte Hand und die Modis dagegen gibt + es nur hier -- und auf workspace. gar nicht. */ +const ROLLEN = ["admin", "spicy", "manager", "scout", "creator", "hand", "modi"]; +const TEAM = new Set(["hand", "modi"]); let passtWahr = 0, passtFalsch = 0; for (const r of ROLLEN) { const aufCrew = sitzungPasstZurAdresse(r, CREW); const aufWs = sitzungPasstZurAdresse(r, WS); const aufLokal = sitzungPasstZurAdresse(r, LOKAL); - ok(aufCrew === (r === "modi"), `crew.: ${r} ${r === "modi" ? "darf" : "darf nicht"}`); - ok(aufWs === (r !== "modi"), `workspace.: ${r} ${r !== "modi" ? "darf" : "darf nicht"}`); + const darfCrew = TEAM.has(r) || r === "admin"; + ok(aufCrew === darfCrew, `crew.: ${r} ${darfCrew ? "darf" : "darf nicht"}`); + ok(aufWs === !TEAM.has(r), `workspace.: ${r} ${TEAM.has(r) ? "darf nicht" : "darf"}`); ok(aufLokal === true, `127.0.0.1: ${r} unveraendert erlaubt`); for (const w of [aufCrew, aufWs, aufLokal]) { if (w) passtWahr++; else passtFalsch++; } } @@ -197,7 +207,27 @@ ok(/ungueltig/.test(managerAufCrew.text), ok(!managerAufCrew.keks.includes("dfw_sitzung"), "crew.: und keine Sitzung"); const adminAufCrew = await anmelden(CREW, "admin", "CODE-DOGI-0001"); -ok(adminAufCrew.status === 401, "crew.: auch DogFather selbst kommt dort nicht herein"); +ok(adminAufCrew.status === 200 && adminAufCrew.keks.includes("dfw_sitzung"), + "crew.: DogFather kommt herein -- er arbeitet in beiden Welten"); + +const handAufCrew = await anmelden(CREW, "hand", "CODE-HAND-0001"); +ok(handAufCrew.status === 200 && handAufCrew.keks.includes("dfw_sitzung"), + "crew.: die rechte Hand kommt herein"); + +/* Und sie kommt NICHT auf die andere Adresse -- genau wie ein Modi. + Sie bekommt dort denselben Hinweis auf die neue Adresse. */ +const handAufWs = await anmelden(WS, "creator", "CODE-HAND-0001"); +let zielHand = {}; +try { zielHand = JSON.parse(handAufWs.text); } catch { /* bleibt leer */ } +ok(handAufWs.status === 200 && zielHand.weiter === `${CREW_ADRESSE}/workspace/start.html`, + "workspace.: die rechte Hand wird auf die neue Adresse verwiesen"); +ok(!handAufWs.keks.includes("dfw_sitzung"), + "workspace.: und bekommt dort keine Sitzung"); + +/* Eine Kachel, die es auf crew. nicht gibt, ist dort so gut wie + erfunden -- auch mit einem gueltigen Code dahinter. */ +const spicyAufCrew = await anmelden(CREW, "spicy", "CODE-DOGI-0001"); +ok(spicyAufCrew.status === 401, "crew.: eine fremde Kachel gibt es dort nicht"); const modiAufWs = await anmelden(WS, "creator", "CODE-MODI-0001"); ok(modiAufWs.status === 200, "workspace.: der Modi bekommt eine Antwort"); @@ -333,7 +363,14 @@ ok(/die Modis/.test(wandCrew.text), "crew.: der Satz nennt DogFather und die Mod Zugang waere sie eine Bedienhuerde, keine Auswahl. */ const kachelnCrew = (wandCrew.text.match(/class="rolle"/g) || []).length; const kachelnWs = (wandWs.text.match(/class="rolle"/g) || []).length; -ok(kachelnCrew === 0, `crew.: keine Rollenkacheln (gezaehlt: ${kachelnCrew})`); +ok(kachelnCrew === 3, `crew.: drei Rollenkacheln (gezaehlt: ${kachelnCrew})`); +for (const [rolle, name] of [["admin", "DogFather"], ["hand", "Rechte Hand"], ["modi", "Modi"]]) { + ok(wandCrew.text.includes(`data-rolle="${rolle}"`) && wandCrew.text.includes(`>${name}<`), + `crew.: die Kachel "${name}" steht da`); +} +/* Die Namen kommen aus den Kacheln, nicht aus einer Liste in gate.js -- + sonst stuenden sie in einer Datei, die jeder bekommt. */ +ok(!/Rechte Hand/.test(wandWs.text), "workspace.: dort steht die dritte Rolle nicht"); ok(kachelnWs === 5, `workspace.: dort weiterhin fuenf (gezaehlt: ${kachelnWs})`); ok(/SpicyMedia/.test(wandWs.text), "workspace.: die alte Wand ist unveraendert"); diff --git a/server/pruef-crew-wand-bild.mjs b/server/pruef-crew-wand-bild.mjs index f61f4025..745a1a23 100644 --- a/server/pruef-crew-wand-bild.mjs +++ b/server/pruef-crew-wand-bild.mjs @@ -99,7 +99,18 @@ for (const geraet of [ ok(kacheln === 5, `${geraet.name}: die alte Wand hat ihre fuenf Kacheln (${kacheln})`); } else { ok(/Team Dogi/.test(titel), `${geraet.name}: der Reiter heisst "${titel}"`); - ok(kacheln === 0, `${geraet.name}: keine Rollenkacheln (${kacheln})`); + /* DREI, seit Filipe am 10.09.2026 sagte: "3 rollen. dogfather. + rechte hand und modis." Hier stand vorher `=== 0` -- am Vormittag + richtig, am Nachmittag nicht mehr. */ + ok(kacheln === 3, `${geraet.name}: drei Rollenkacheln (${kacheln})`); + /* Jede muss auch bedienbar sein. Eine Kachel, die im HTML steht, + aber 0 px hoch ist, zaehlt oben mit und laesst sich trotzdem + nicht antippen -- genau die Sorte Fehler, die eine Zaehlung + allein nicht findet. */ + const zuKlein = await seite.evaluate(() => + [...document.querySelectorAll(".rolle")] + .filter((e) => e.getBoundingClientRect().height < 40).length); + ok(zuKlein === 0, `${geraet.name}: alle drei sind gross genug zum Antippen`); } /* Die Karte muss sichtbar und bedienbar sein. */ diff --git a/server/pruef-modi-verborgen.mjs b/server/pruef-modi-verborgen.mjs index 7c2dbb36..532a5c25 100644 --- a/server/pruef-modi-verborgen.mjs +++ b/server/pruef-modi-verborgen.mjs @@ -82,13 +82,26 @@ const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001"); const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001"); const idModi1 = anlegen("Marina", "modi", "CODE-MODI-0001"); const idModi2 = anlegen("Kim", "modi", "CODE-MODI-0002"); +/* DIE STELLVERTRETUNG GEHOERT IN DIESELBE PRUEFUNG (10.09.2026). + + Sie ist seit heute die zweite verborgene Rolle. Ohne diese Zeile + waere der ganze Lauf weiter gruen gewesen -- und haette die neue + Rolle kein einziges Mal angesehen. Genau die Sorte gruener Haken, + vor der die Hausregeln warnen: Er bestaetigt etwas, das er nicht + geprueft hat. + + Sie wird bewusst in dieselben Listen aufgenommen wie die Modis, statt + danebenzustehen: Was fuer einen Modi gilt, gilt fuer sie -- und wenn + morgen jemand einen Weg hinzufuegt, deckt er beide ab, ohne dass es + ihm jemand sagen muss. */ +const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001"); d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)") .run(idCreat, idScout, jetzt); d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)") .run(idScout, idMan, jetzt); d.close(); -const MODIS = [idModi1, idModi2]; +const MODIS = [idModi1, idModi2, idHand]; void idSpicy; /* ---------- Anmelden ueber die Schnittstelle ------------------------- */ @@ -155,7 +168,7 @@ const WEGE = [ ]; /* Die Namen der Modis -- fuer die Wege, die keine Nummern liefern. */ -const MODI_NAMEN = ["Marina", "Kim"]; +const MODI_NAMEN = ["Marina", "Kim", "Rieke"]; const LEUTE = [ ["DogFather", "admin", "CODE-DOGI-0001", true], @@ -165,6 +178,7 @@ const LEUTE = [ ["Scout", "scout", "CODE-SCOU-0001", false], ["Creator", "creator", "CODE-ANNA-0001", false], ["Modi Marina", "creator", "CODE-MODI-0001", true], + ["Rechte Hand", "creator", "CODE-HAND-0001", true], ]; async function hole(keks, pfad) { diff --git a/server/pruef-modi-wortleck.mjs b/server/pruef-modi-wortleck.mjs index 1d419f36..edc20677 100644 --- a/server/pruef-modi-wortleck.mjs +++ b/server/pruef-modi-wortleck.mjs @@ -58,6 +58,30 @@ function dateien(pfad, aus = []) { console.log("\n=== Der Rollenname darf in keiner ausgelieferten Datei stehen ===\n"); +/* ===================================================================== + DIE EINE DATEI, DIE DEN NAMEN TRAGEN DARF (10.09.2026) + + crew-index.html ist die Zugangswand von crew.dogfather-universe.com. + Filipe: "3 rollen. dogfather. rechte hand und modis." Auf dieser Wand + STEHEN die drei Namen -- als Kacheln, sichtbar, so gewollt. + + WARUM DAS KEIN WIDERSPRUCH ZUR REGEL IST: Die Regel schuetzt nicht + den Namen, sondern die Tatsache, dass es einen eigenen Zugang zum + WORKSPACE gibt. Diese Datei wird ausserhalb von crew. ueberhaupt + nicht ausgeliefert -- der Server antwortet dort mit "gibt es nicht". + + UND DAS IST DIE BEDINGUNG DER AUSNAHME, nicht ihre Begruendung: Sie + gilt nur, solange jene Sperre wirklich haelt. Gemessen wird das nicht + hier, sondern in pruef-crew-adresse.mjs ("die zweite Wand gibt es + dort nicht", drei Adressen plus doppelter Schraegstrich). Faellt jene + Pruefung, ist diese Ausnahme ungueltig -- deshalb steht der Verweis + hier und nicht nur in einem Kommentar irgendwo. + + AUSDRUECKLICH EINE EINZELNE DATEI und kein Muster wie "crew-*": + Sonst waere die naechste crew-Datei stillschweigend mit ausgenommen, + und niemand haette entschieden, ob sie es sein darf. */ +const AUSGENOMMEN = new Set(["crew-index.html"]); + const alle = dateien(ORDNER); /* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Faende die Suche keine einzige Datei -- verschobener Ordner, andere Endungen --, waere unten alles @@ -66,7 +90,9 @@ const alle = dateien(ORDNER); ok(alle.length >= 30, `${alle.length} ausgelieferte Dateien durchsucht`); const treffer = []; +let ausgenommen = 0; for (const datei of alle) { + if (AUSGENOMMEN.has(relative(ORDNER, datei))) { ausgenommen++; continue; } const text = readFileSync(datei, "utf8"); const zeilen = text.split("\n"); for (let i = 0; i < zeilen.length; i++) { @@ -81,6 +107,16 @@ ok(treffer.length === 0, treffer.length + treffer.slice(0, 12).join("\n ") : "keine einzige Fundstelle"); +/* DIE AUSNAHME MUSS AUCH WIRKLICH GEBRAUCHT WERDEN. + + Waere crew-index.html eines Tages umbenannt oder geloescht, stuende + hier eine Ausnahme fuer eine Datei, die es nicht gibt -- und beim + naechsten Umbau wuerde jemand annehmen, sie decke etwas ab. Eine + Ausnahme, die ins Leere zeigt, ist schlimmer als keine: Sie sieht aus + wie ein Schutz. */ +ok(ausgenommen === AUSGENOMMEN.size, + `${ausgenommen} von ${AUSGENOMMEN.size} ausgenommenen Dateien gibt es wirklich`); + /* ---------- Gegenprobe ------------------------------------------------ Findet dieselbe Suche den Namen ueberhaupt, wenn er dasteht? Ohne diesen Schritt bewiese der Haken oben nichts: Er waere auch gruen, diff --git a/server/pruef-personen-formular.mjs b/server/pruef-personen-formular.mjs index db2be130..a3ba1545 100644 --- a/server/pruef-personen-formular.mjs +++ b/server/pruef-personen-formular.mjs @@ -127,7 +127,11 @@ const mass = await dogi.evaluate(() => { aus der Wahl, müsste das auffallen -- eine Zahl, die sich selbst nachzählt, kann das nicht. - DIE SECHSTE IST NICHT WIE DIE ANDEREN. Die fünf stehen in + SIEBEN SEIT DEM 10.09.2026. Filipe: "3 rollen. dogfather. rechte + hand und modis." Die Stellvertretung ist damit die siebte Karte -- + und sie kommt auf demselben Weg wie die sechste. + + DIE SECHSTE UND SIEBTE SIND NICHT WIE DIE ANDEREN. Die fünf stehen in assets/js/personen.js, also in einer Datei, die jeder bekommt, der die Seite öffnet. "Modi" darf dort nicht stehen -- ein Blick in den Quelltext würde sonst genügen, um den verborgenen Zugang zu @@ -137,11 +141,22 @@ 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 === 6, `alle sechs Rollen stehen zur Wahl (${mass.karten})`); +ok(mass.karten === 7, `alle sieben Rollen stehen zur Wahl (${mass.karten})`); ok(JSON.stringify([...mass.kartenRollen].sort()) - === JSON.stringify(["admin", "creator", "manager", "modi", "scout", "spicy"]), + === JSON.stringify(["admin", "creator", "hand", "manager", "modi", "scout", "spicy"]), `und es sind die richtigen (${mass.kartenRollen.join(", ")})`); +/* DIE STELLVERTRETUNG TRAEGT EIN EIGENES ZEICHEN. + + Zweimal derselbe Husky waere auf zwei getrennten Kacheln in Ordnung + (dort unterscheiden Name und Farbe), in einer AUSWAHL aber nicht: + Dort liegen sie untereinander, und zwei gleiche Zeichen laden zum + Vertippen ein. Gemessen wird deshalb, dass es sich vom Husky + UNTERSCHEIDET -- nicht, wie es aussieht. */ +ok(mass.kartenZeichen.hand && mass.kartenZeichen.hand !== mass.kartenZeichen.admin, + `die Stellvertretung hat ein eigenes Zeichen (${mass.kartenZeichen.hand} / ` + + `${mass.kartenZeichen.admin})`); + /* DASSELBE ZEICHEN WIE BEI DOGFATHER (Wunsch Filipe, 10.09.2026: "das modi symbol soll das gleiche sein wie bei dogfather"). diff --git a/server/pruef-rollen.mjs b/server/pruef-rollen.mjs index 1505fd7d..5580cc59 100644 --- a/server/pruef-rollen.mjs +++ b/server/pruef-rollen.mjs @@ -100,6 +100,11 @@ person("Neu-Manager", "manager", "CODE-NEUM-00001"); anderen Modis. Er laeuft hier ueber JEDE Seite, denn "verborgen" heisst nicht "darf kaputt sein": Was er sieht, muss auch gehen. */ person("Marina", "modi", "CODE-MODI-0001"); +/* Und die Stellvertretung (10.09.2026). Sie sieht mehr als ein Modi + -- unter anderem den Eingang --, ist aber genauso verborgen. Beides + muss durch denselben Rundgang: Wer mehr Kacheln hat, hat auch mehr + Stellen, an denen eine ins Leere fuehren kann. */ +person("Rieke", "hand", "CODE-HAND-0001"); /* Cigdem betreut Tili, Ben ebenfalls -- so hat jede betreuende Rolle etwas zu sehen. Genau diese Zuteilung fehlte im echten Bestand, und @@ -163,6 +168,10 @@ const ROLLEN = [ ausgerechnet "creator" dasteht, ist Absicht und beweist nebenbei, dass die Kachel wirklich gleichgueltig ist. */ { rolle: "creator", code: "CODE-MODI-0001", name: "Modi (Marina)" }, + /* Auch sie meldet sich ueber eine fremde Kachel an -- auf der + gewohnten Wand gibt es fuer sie keine, und der verborgene Zugang + erkennt sie am Code. Genau so kommt sie auch im Betrieb herein. */ + { rolle: "creator", code: "CODE-HAND-0001", name: "Rechte Hand (Rieke)" }, ]; const { chromium } = await import( diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 865780b4..1172e691 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -13,7 +13,7 @@ import { db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, MODI_KATEGORIEN, kategorienFuer, kategoriePersonen, - ohneModi, siehtModis, + ohneTeamDogi, siehtModis, TEAM_DOGI_ROLLEN, } from "./workspace.js"; export const aufgabenRouter = express.Router(); @@ -67,7 +67,7 @@ export function sichtbar(person) { const regel = sichtbarRoh(person); if (!regel || siehtModis(person)) return regel; return { - wo: `(${regel.wo}) AND ${ohneModi("a", ["creator_id", "verantwortlich_id", "erstellt_von"])}`, + wo: `(${regel.wo}) AND ${ohneTeamDogi("a", ["creator_id", "verantwortlich_id", "erstellt_von"])}`, werte: regel.werte, }; } @@ -148,11 +148,25 @@ function sichtbarRoh(person) { Start: Kommt ein Modi dazu, muessen ihn die anderen sofort sehen. Eine Liste, die beim Serverstart entsteht, waere bis zum naechsten Neustart falsch -- und niemand wuesste, warum. */ + /* BEIDE ROLLEN DES TEAMS TEILEN SICH DIESE SICHT (10.09.2026). + + Die rechte Hand soll laut Blueprint 3.1 "gleichen Ueberblick wie + Owner" haben. Innerhalb des Teams heisst das: dieselbe Sicht wie + ein Modi -- alle Aufgaben des Teams, nicht nur die eigenen. Der + Unterschied zwischen ihr und einem Modi liegt woanders (Eingang, + Entscheidungen), nicht hier. + + `case` ohne `break` faellt in den naechsten -- genau das ist hier + gewollt und der kuerzeste ehrliche Weg, "diese beiden gleich" zu + sagen. */ + case "hand": case "modi": { + const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", "); const modis = db().prepare( - "SELECT id FROM personen WHERE rolle = 'modi'").all().map((z) => z.id); - /* Kann nicht leer sein -- diese Person IST ein Modi. Die Abfrage - faende sonst `IN ()`, und das ist in SQLite ein Syntaxfehler. */ + `SELECT id FROM personen WHERE rolle IN (${liste})`).all().map((z) => z.id); + /* Kann nicht leer sein -- diese Person gehoert selbst dazu. Die + Abfrage faende sonst `IN ()`, und das ist in SQLite ein + Syntaxfehler. */ if (!modis.length) return { wo: "a.verantwortlich_id = ?", werte: [person.id] }; const platz = modis.map(() => "?").join(","); return { diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index aba1271d..28d8c633 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -18,7 +18,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, externPruefen, externSql, - ohneModi, siehtModis, MODI_BEREICHE_ERLAUBT, + ohneTeamDogi, siehtModis, MODI_BEREICHE_ERLAUBT, TEAM_DOGI_ROLLEN, } from "./workspace.js"; export const bereicheRouter = express.Router(); @@ -298,7 +298,12 @@ for (const geschuetzt of ["ideen", "angebote"]) { 404 wie ueberall: Ein Bereich, den es fuer mich nicht gibt, wirft keine Fragen auf. */ bereicheRouter.use("/workspace/api/bereich/:bereich", (req, res, next) => { - if (req.person?.rolle !== "modi") return next(); + /* Auch die Stellvertretung. Sie hat dieselben Kacheln wie ein Modi + und damit dieselben erlaubten Bereiche -- ohne diese Zeile kaeme + sie ueber die Adresszeile in die Agentur-Ablage, die sie nichts + angeht. Genau die Luecke, gegen die diese Schranke gebaut wurde, + nur eine Rolle spaeter. */ + if (!TEAM_DOGI_ROLLEN.has(req.person?.rolle)) return next(); if (MODI_BEREICHE_ERLAUBT.has(String(req.params.bereich))) return next(); return res.status(404).json({ fehler: "nicht_gefunden" }); }); @@ -405,7 +410,7 @@ export function sichtbar(person) { const regel = sichtbarRoh(person); if (!regel || siehtModis(person)) return regel; return { - wo: `(${regel.wo}) AND ${ohneModi("e", ["creator_id", "erstellt_von"])}`, + wo: `(${regel.wo}) AND ${ohneTeamDogi("e", ["creator_id", "erstellt_von"])}`, werte: regel.werte, }; } @@ -442,9 +447,16 @@ function sichtbarRoh(person) { (Entscheidung Filipe, 09.09.2026: "sie sind untereinander ein Team"). Ohne diesen Zweig saehe jeder Modi nur, was er selbst geschrieben hat, und eine gemeinsame Ideensammlung waere keine. */ - if (person.rolle === "modi") { - return { wo: `(e.erstellt_von IN (SELECT id FROM personen WHERE rolle = 'modi')` - + ` OR e.creator_id IN (SELECT id FROM personen WHERE rolle = 'modi'))`, werte: [] }; + /* NACHTRAG 10.09.2026: Die Stellvertretung gehoert in denselben + Zweig. Ohne sie faellt sie unten durch und landet bei der + Betreuungsregel -- die fuer sie nichts findet. Ergebnis waere ein + LEERES Ideen-Board und ein leeres Angebote-Brett, und das meldet + niemand: Ein leeres Brett sieht nicht nach Fehler aus. Genau so ist + es am 01.09. dem Manager und am 09.09. dem Modi ergangen. */ + if (TEAM_DOGI_ROLLEN.has(person.rolle)) { + const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", "); + return { wo: `(e.erstellt_von IN (SELECT id FROM personen WHERE rolle IN (${liste}))` + + ` OR e.creator_id IN (SELECT id FROM personen WHERE rolle IN (${liste})))`, werte: [] }; } const b = betreutWo(person, "e.creator_id"); return b @@ -489,7 +501,7 @@ export function sichtbarEintrag(person, praefix = "e") { nicht. Eine Regel, die nur im Formular gilt, ist keine Regel. */ if (siehtModis(person)) return mitAllen; return { - wo: `(${mitAllen.wo}) AND ${ohneModi(praefix)}`, + wo: `(${mitAllen.wo}) AND ${ohneTeamDogi(praefix)}`, werte: mitAllen.werte, }; } diff --git a/server/workspace-checkliste.js b/server/workspace-checkliste.js index baebcc9c..80c97818 100644 --- a/server/workspace-checkliste.js +++ b/server/workspace-checkliste.js @@ -327,7 +327,12 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => { Media, und die Modis gehen sie nichts an. Das ist die Regel des ganzen Hauses; sie darf nicht ausgerechnet hier eine Ausnahme haben, nur weil die Zeile daneben schon passte. */ - if (req.person.rolle === "admin") { + /* Auch die rechte Hand. Sie "kann Modis im Alltag koordinieren" + (Blueprint 3.1) -- dafuer muss sie deren Berichte oeffnen + koennen. Bewerten darf sie darin genauso wenig wie DogFather; + das entscheidet `darf_bewerten` weiter unten und nicht diese + Liste. Sehen und Druecken sind zwei Fragen. */ + if (req.person.rolle === "admin" || req.person.rolle === "hand") { for (const z of db().prepare( "SELECT id FROM personen WHERE rolle = 'modi' AND aktiv = 1").all()) ids.push(z.id); } @@ -380,7 +385,8 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => { ist schlicht falsch beschriftet. Das Wort kommt vom Server: Ein Rollenvergleich im Browser waere die Stelle, an der der Rollenname in einer ausgelieferten Datei landet. */ - wahl_name: req.person.rolle === "admin" ? "Person" : "Creator", + wahl_name: req.person.rolle === "admin" || req.person.rolle === "hand" + ? "Person" : "Creator", /* Die Oberflaeche soll nicht raten muessen, wer was darf -- sonst zeigt sie einem Creator einen Knopf, der beim Klicken 403 sagt. Das ist schlechter als kein Knopf. */ diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index e31de136..f9fdf17c 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -31,7 +31,7 @@ import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from import { db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, ROLLEN_SORTIERUNG, externSql, EXTERN_MAX, sichtbarePersonenIds, - ohneModi, siehtModis, + ohneTeamDogi, siehtModis, } from "./workspace.js"; export const dateienRouter = express.Router(); @@ -82,7 +82,7 @@ export function sichtbar(person) { const regel = sichtbarRoh(person); if (!regel || siehtModis(person)) return regel; return { - wo: `(${regel.wo}) AND ${ohneModi("d", ["creator_id", "hochgeladen_von"])}`, + wo: `(${regel.wo}) AND ${ohneTeamDogi("d", ["creator_id", "hochgeladen_von"])}`, werte: regel.werte, }; } diff --git a/server/workspace-personen.js b/server/workspace-personen.js index 45384434..a2e1c773 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, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, + db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, TEAM_DOGI_ROLLEN, } from "./workspace.js"; import { sicherungJetzt } from "./workspace-sicherung.js"; @@ -253,6 +253,26 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => { aus wie ein Fehler, weil es einer ist. */ text: "Moderation und Team-Aufgaben. Sichtbar nur für die " + "DogFather-Rolle und für die Modis untereinander.", + }, { + /* DIE STELLVERTRETUNG (10.09.2026). Filipe: "3 rollen. + dogfather. rechte hand und modis." + + Blueprint V3.0, Kapitel 3.1: "1 (austauschbar)" -- gedacht + ist EINE Person. Erzwungen wird das hier bewusst NICHT: + Beim Wechsel braucht es einen Moment, in dem die alte noch + da und die neue schon angelegt ist. Eine Sperre haette + genau diesen Moment unmoeglich gemacht. + + DIE PFOTE, nicht der Husky: Sie liegt am naechsten an + "rechte Hand", ohne eine Hand zu sein -- eine menschliche + Hand waere in diesem Zeichensatz der einzige Fremdkoerper. + Und sie unterscheidet die beiden Eintraege; zweimal + derselbe Husky waere in einer AUSWAHL etwas anderes als auf + zwei getrennten Kacheln. */ + wert: "hand", name: "Rechte Hand", symbol: "pfote", + text: "Vertritt dich im Alltag und koordiniert das Team. Sieht " + + "dieselben Aufgaben, Ideen und Angebote wie du – aber keine " + + "privaten Kalender und keine Einzelchats.", }] : [], }); @@ -560,8 +580,13 @@ personenRouter.post("/workspace/api/verwaltung/personen", gleicheHerkunft, (req, "das darfst du nicht" waere schlimmer als das Recht selbst -- es verriete, dass es die Rolle gibt. Deshalb wortgleich, mit demselben Statuscode. */ + /* Die verborgenen Rollen vergibt nur DogFather. Aus der Menge + gelesen und nicht als `rolle === "modi"` hingeschrieben: Seit dem + 10.09.2026 sind es zwei, und die zweite waere hier sonst still + durchgerutscht -- ein Manager haette eine rechte Hand anlegen + koennen, ohne dass irgendwo etwas rot wird. */ const unbekannt = !ROLLEN.includes(rolle) - || (rolle === "modi" && !istDogFather(req.person)); + || (TEAM_DOGI_ROLLEN.has(rolle) && !istDogFather(req.person)); if (unbekannt) return res.status(400).json({ fehler: "Unbekannte Rolle." }); /* ERSTER VORBEHALT: Eine Leitung anlegen darf nur DogFather. diff --git a/server/workspace-teamlage.js b/server/workspace-teamlage.js index 2d063355..240d8a02 100644 --- a/server/workspace-teamlage.js +++ b/server/workspace-teamlage.js @@ -6,10 +6,17 @@ kategorie geben für uns beide nur sonst keinen wo wir dass alles sehen und behandeln können. so wie mit dem ideen-board."* - NUR DIE DOGFATHER-ROLLE. Das sind Filipe und VanVan -- sie tragen - beide `admin`. Fuer alle anderen gibt es diese Seite nicht: 404 wie - bei einer Adresse, die niemand kennt. Ein "kein Zugriff" waere die - Auskunft, dass es sie gibt. + DIE DOGFATHER-ROLLE UND DIE RECHTE HAND. Das sind Filipe und VanVan + (beide `admin`) sowie seit dem 10.09.2026 die Stellvertretung. Der + Blueprint V3.0 sagt in Kapitel 3.1 ueber sie: "Kann im Alltag + vertretend Entscheidungen treffen." Genau das passiert hier -- aus + einer Rueckmeldung wird eine Aufgabe oder eine begruendete Absage. + Ihr diese Seite zu verwehren hiesse, ihr die Vertretung zu geben und + den Ort dafuer wegzunehmen. + + Fuer alle anderen gibt es diese Seite nicht: 404 wie bei einer + Adresse, die niemand kennt. Ein "kein Zugriff" waere die Auskunft, + dass es sie gibt. SEHEN UND BEHANDELN, in dieser Reihenfolge: Die Seite zaehlt nur -- bewertet wird dort, wo die Punkte stehen. Jede Zahl ist deshalb ein @@ -60,12 +67,23 @@ export const teamlageRouter = express.Router(); const GESAMT = Object.fromEntries( Object.entries(MODI_PUNKTE).map(([b, g]) => [b, Object.values(g).flat().length])); +/** Wer den Eingang oeffnen darf. + * + * An EINER Stelle und nicht dreimal `istDogFather(person) || + * person.rolle === "hand"`: Diese Datei hat drei Schranken, und die + * vierte kommt mit der naechsten Route. Wer eine davon vergisst, + * merkt es nicht -- eine vergessene Schranke faellt nur dem auf, der + * hindurchgeht. */ +function darfEingang(person) { + return istDogFather(person) || person?.rolle === "hand"; +} + teamlageRouter.get("/workspace/api/teamlage", (req, res) => { try { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); /* 404 und nicht 403 -- siehe Kopf. */ - if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!darfEingang(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); const modis = db().prepare( `SELECT id, name, bild, letzter_login FROM personen @@ -219,7 +237,7 @@ teamlageRouter.post("/workspace/api/teamlage/behandeln", gleicheHerkunft, try { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); - if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!darfEingang(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); const bereich = String(req.body?.bereich || ""); const schluessel = String(req.body?.schluessel || ""); @@ -319,7 +337,7 @@ teamlageRouter.post("/workspace/api/teamlage/angebot", gleicheHerkunft, try { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); - if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!darfEingang(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); const id = Number(req.body?.id) || 0; const art = String(req.body?.art || ""); diff --git a/server/workspace-zentrale.js b/server/workspace-zentrale.js index 07f799be..8cc46f7b 100644 --- a/server/workspace-zentrale.js +++ b/server/workspace-zentrale.js @@ -50,7 +50,8 @@ ===================================================================== */ import express from "express"; -import { db, sitzungLesen, istLeitung, betreuteIds, scoutsVon, verborgeneIds } from "./workspace.js"; +import { db, sitzungLesen, istLeitung, betreuteIds, scoutsVon, verborgeneIds, + TEAM_DOGI_ROLLEN } from "./workspace.js"; export const zentraleRouter = express.Router(); @@ -259,9 +260,10 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { die Zahl, die an dieser Stelle etwas bedeutet -- und sie bekommt nur zu sehen, wer ohnehin weiss, dass es die Runde gibt. */ - zeigt.rolle === "modi" + TEAM_DOGI_ROLLEN.has(zeigt.rolle) ? { wert: db().prepare( - "SELECT COUNT(*) AS n FROM personen WHERE rolle = 'modi' AND aktiv = 1").get().n, + `SELECT COUNT(*) AS n FROM personen WHERE rolle IN (${ + [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ")}) AND aktiv = 1`).get().n, schild: "im Team" } : { wert: betreuteIds(zeigt).length, schild: "betreut" }, ], diff --git a/server/workspace.js b/server/workspace.js index 915505c1..c52e5c76 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -27,8 +27,11 @@ import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { mkdirSync } from "node:fs"; import { createRequire } from "node:module"; -import { sitzungPasstZurAdresse, CREW_ADRESSE, istOhneModiAdresse, istCrewAdresse } - from "./crew-adresse.js"; +import { sitzungPasstZurAdresse, CREW_ADRESSE, istOhneModiAdresse, istCrewAdresse, + TEAM_DOGI_ROLLEN } from "./crew-adresse.js"; +/* Weitergereicht, damit die Fachmodule sie wie alles andere aus + workspace.js beziehen und nicht wissen muessen, wo sie wohnt. */ +export { TEAM_DOGI_ROLLEN }; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -59,12 +62,12 @@ const COOKIE = "dfw_sitzung"; const SITZUNG_STUNDEN = 12; const VERSUCHE_MAX = 8; // pro IP const VERSUCHE_FENSTER_MIN = 10; -const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator", "modi"]); +const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator", "hand", "modi"]); /* Die Reihenfolge, in der Rollen ueberall erscheinen: Spicy Media zuerst, dann DogFather, Manager, Scout, Creator. Steht hier einmal, damit keine Liste eine eigene Reihenfolge erfindet. */ -export const ROLLEN_REIHE = ["spicy", "admin", "manager", "scout", "creator", "modi"]; +export const ROLLEN_REIHE = ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"]; /* DIE ROLLEN MIT EINER KACHEL AUF DER ANMELDESEITE. @@ -81,12 +84,26 @@ export const ROLLEN_REIHE = ["spicy", "admin", "manager", "scout", "creator", "m -- oder es gaebe die Rolle gar nicht. */ const ROLLEN_KACHEL = new Set(["spicy", "admin", "manager", "scout", "creator"]); + +/** Die Rollen mit einer Kachel auf der Zugangswand von crew. + * + * DREI, seit Filipe am 10.09.2026 sagte: "3 rollen. dogfather. rechte + * hand und modis." Auf dieser Adresse gibt es also wieder etwas zu + * waehlen -- anders als in der Fassung davor, wo nur Modis + * hereinkamen und eine Kachelreihe mit einem Eintrag eine Huerde + * gewesen waere. + * + * DogFather steht auf BEIDEN Waenden. Er ist die einzige Person, die + * in beiden Welten arbeitet; das ist keine Ausnahme von der Trennung, + * sondern ihr Sinn. */ +const CREW_KACHEL = new Set(["admin", "hand", "modi"]); + /* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch (admin, creator, manager, scout, spicy) -- also fast genau falsch herum. */ export const ROLLEN_SORTIERUNG = "CASE rolle WHEN 'spicy' THEN 0 WHEN 'admin' THEN 1 WHEN 'manager' THEN 2" - + " WHEN 'scout' THEN 3 WHEN 'creator' THEN 4 ELSE 5 END"; + + " WHEN 'scout' THEN 3 WHEN 'creator' THEN 4 WHEN 'hand' THEN 5 ELSE 6 END"; /* LEITUNG = DogFather und Manager. Ein Manager darf alles, was DogFather darf -- mit genau zwei Ausnahmen, die in @@ -200,17 +217,28 @@ export function ohneDogFather(praefix, spalten = ["creator_id", "erstellt_von"]) jemand einen Fehler sieht. (Dieselbe Falle steht schon im Kommentar von ohneDogFather; sie ist es wert, zweimal dazustehen.) ===================================================================== */ -export function ohneModi(praefix, spalten = ["creator_id", "erstellt_von"]) { +export function ohneTeamDogi(praefix, spalten = ["creator_id", "erstellt_von"]) { + /* Die Rollenliste kommt aus TEAM_DOGI_ROLLEN und steht nicht hier als + Text. Sie hiess bis zum 10.09.2026 fest `rolle = 'modi'` -- mit dem + Hinzukommen der rechten Hand waere das eine stille Luecke geworden: + Ihre Zeilen waeren fuer Spicy Media wieder sichtbar gewesen, ohne + dass irgendwo etwas rot wird. + + Als Aufzaehlung in den SQL-Text eingesetzt und nicht als Parameter, + weil dieser Ausdruck in ein `WHERE` eingebaut wird, das an vielen + Stellen zusammengesetzt wird. Die Werte stammen aus einer festen + Menge im Code, nie aus einer Eingabe. */ + const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", "); return spalten .map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN ` - + `(SELECT id FROM personen WHERE rolle = 'modi'))`) + + `(SELECT id FROM personen WHERE rolle IN (${liste})))`) .join(" AND "); } /** Darf diese Person ueberhaupt etwas sehen, das einem Modi gehoert? * Nur die DogFather-Rolle und die Modis selbst. */ export const siehtModis = (person) => !!person - && (person.rolle === "admin" || person.rolle === "modi"); + && (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle)); export const siehtAlles = (person) => !!person && (person.rolle === "admin" || person.rolle === "spicy"); @@ -230,6 +258,7 @@ export const ROLLEN_NAME = { manager: "Manager", scout: "Scout", creator: "Creator", + hand: "Rechte Hand", modi: "Modi", }; @@ -317,8 +346,45 @@ const MODI_BEREICHE = [ * Liste hiesse das Zweite, und die fuenf bekannten Rollen haetten ab * sofort eine leere Startseite. */ +/* Die Eingangs-Kachel steht hier -- VOR ihrer ersten Verwendung. + + Sie stand zuerst weiter unten beim ZUSATZ_BEREICHE-Block, wo sie + thematisch hingehoert. Das Modul liess sich damit nicht mehr laden: + "Cannot access 'EINGANG_KACHEL' before initialization". `const` wird + erst an seiner Zeile gueltig, und HAND_BEREICHE greift weiter oben + darauf zu. + + `node --check` hat das NICHT gemeldet -- es prueft die Schreibweise, + nicht die Reihenfolge. Gefunden hat es erst der Versuch, das Modul + wirklich zu laden. Eine Syntaxpruefung ist keine Ladeprobe. */ +const EINGANG_KACHEL = { + gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", + name: "Eingang", unter: "Vorschläge und Angebote, die auf dich warten", + zeichen: "teamlage", ton: 24, ziel: "teamlage.html", szene: "halle", +}; + + +/* DIE RECHTE HAND SIEHT DIESELBEN KACHELN WIE EIN MODI -- PLUS EINE. + + Blueprint V3.0, Kapitel 3.1: "Gleicher Ueberblick wie Owner. Kann + Modis im Alltag koordinieren." Der gleiche Ueberblick heisst hier + ausdruecklich NICHT eine eigene Kachelwelt: Sie arbeitet an denselben + Dingen wie das Team, sieht darin aber alles statt nur das Eigene -- + das entscheidet die Sichtbarkeitsregel, nicht die Kachelliste. + + Dazu kommt der Eingang, denn sie "kann im Alltag vertretend + Entscheidungen treffen". Genau das passiert dort. + + ABGELEITET UND NICHT ABGESCHRIEBEN: Wer eine Kachel fuer die Modis + hinzufuegt, bekommt sie hier automatisch mit. Eine zweite Liste waere + die Stelle, an der die rechte Hand irgendwann weniger sieht als das + Team, das sie koordinieren soll. */ +const HAND_BEREICHE = [...MODI_BEREICHE, EINGANG_KACHEL]; + export function bereicheFuer(person) { - return person?.rolle === "modi" ? MODI_BEREICHE : null; + if (person?.rolle === "modi") return MODI_BEREICHE; + if (person?.rolle === "hand") return HAND_BEREICHE; + return null; } /* WELCHE BEREICHSSEITEN EIN MODI OEFFNEN DARF -- abgeleitet aus seinen @@ -334,7 +400,7 @@ export function bereicheFuer(person) { -- ohne diese Einschraenkung kaeme er ueber die Adresszeile auch in die Agentur-Ablage, die ihn nichts angeht. */ export const MODI_BEREICHE_ERLAUBT = new Set( - MODI_BEREICHE + HAND_BEREICHE .map((k) => (/bereich\.html\?b=([a-z]+)/.exec(k.ziel) || [])[1]) .filter(Boolean)); @@ -424,7 +490,8 @@ export const MODI_KATEGORIEN = [ */ export function kategorienFuer(person) { if (!person) return []; - return person.rolle === "modi" || person.rolle === "admin" ? MODI_KATEGORIEN : []; + return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin" + ? MODI_KATEGORIEN : []; } /** @@ -447,22 +514,34 @@ export function kategoriePersonen(person) { /* Ein Modi arbeitet ausschliesslich an eigenen Aufgaben -- dort gilt sie immer, unabhaengig davon, wen er eintraegt. */ if (person.rolle === "modi") return null; - if (person.rolle !== "admin") return []; + if (person.rolle !== "admin" && person.rolle !== "hand") return []; try { + /* Auch die rechte Hand selbst: Sie traegt Aufgaben ein -- fuer + andere UND fuer sich. Waere ihre eigene Nummer nicht dabei, + verschwaende das Kategoriefeld genau dann, wenn sie sich selbst + etwas notiert. */ + const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", "); return db().prepare( - "SELECT id FROM personen WHERE rolle = 'modi' AND aktiv = 1").all().map((z) => z.id); + `SELECT id FROM personen WHERE rolle IN (${liste}) AND aktiv = 1`).all().map((z) => z.id); } catch { /* Ohne Datenbank lieber kein Feld als ein falsches. */ return []; } } +/* Der Satz der rechten Hand sagt, was ihre Rolle ist, ohne sie ueber + die anderen zu stellen: Sie haelt den Ueberblick, sie steht nicht + darueber. */ +const HAND_ROLLENTEXT = "Rechte Hand · du hast alles im Blick und entscheidest mit"; + export function rollentextFuer(person) { - return person?.rolle === "modi" ? MODI_ROLLENTEXT : null; + if (person?.rolle === "modi") return MODI_ROLLENTEXT; + if (person?.rolle === "hand") return HAND_ROLLENTEXT; + return null; } export function markeFuer(person) { - return person?.rolle === "modi" ? MODI_MARKE : null; + return TEAM_DOGI_ROLLEN.has(person?.rolle) ? MODI_MARKE : null; } /* ===================================================================== @@ -501,13 +580,7 @@ export function markeFuer(person) { taeglichen Dingen, weil dort etwas WARTET -- Vorschlaege, ueber die entschieden werden will. Das ist eine Aufgabe fuer heute, keine Uebersicht fuer zwischendurch. */ -const ZUSATZ_BEREICHE = { - admin: [{ - gruppe: "Täglich", - name: "Eingang", unter: "Vorschläge und Angebote, die auf dich warten", - zeichen: "teamlage", ton: 24, ziel: "teamlage.html", szene: "halle", - }], -}; +const ZUSATZ_BEREICHE = { admin: [EINGANG_KACHEL] }; export function zusatzBereicheFuer(person) { return ZUSATZ_BEREICHE[person?.rolle] || []; @@ -1528,6 +1601,8 @@ function umstellungen(d) { ["spicy", "admin", "manager", "scout", "creator"], jetztStempel); checkListeErweitern(d, "personen", "rolle", "modi", ["spicy", "admin", "manager", "scout", "creator", "modi"], jetztStempel); + checkListeErweitern(d, "personen", "rolle", "hand", + ["spicy", "admin", "manager", "scout", "creator", "modi", "hand"], jetztStempel); /* DER BEREICH FUER DAS IDEEN-BOARD (10.09.2026, Kapitel 5.6). @@ -2553,7 +2628,7 @@ const GESCHUETZT = { Profil" und fuehrte auf eine Seite, deren Schnittstelle mit 404 antwortet, weil sie ausdruecklich nur Creator kennt. Gefunden vom neuen Kachel-Durchgang in pruef-rollen. */ - "/workspace/steckbrief.html": ["spicy", "admin", "manager", "scout", "creator", "modi"], + "/workspace/steckbrief.html": ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"], "/workspace/kalender.html": null, "/workspace/dateien.html": null, /* 'modi' kam am 10.09.2026 dazu -- und es war ein echter Fund: Ohne @@ -2567,7 +2642,7 @@ const GESCHUETZT = { WELCHE Bereiche ein Modi oeffnen darf, entscheidet die Liste seiner Kacheln (MODI_BEREICHE_ERLAUBT) -- sonst kaeme er ueber die Adresszeile auch in die Agentur-Ablage. */ - "/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator", "modi"], + "/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator", "hand", "modi"], "/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"], "/workspace/scouting.html": ["spicy", "admin", "manager", "scout"], /* DIE ARBEITSLAGE DES TEAMS (09.09.2026). @@ -2598,7 +2673,10 @@ const GESCHUETZT = { Schranke auf die Startseite; die Schnittstelle dahinter antwortet zusaetzlich mit 404. Zwei Schloesser, absichtlich: Faellt eines weg, haelt das andere. */ - "/workspace/teamlage.html": ["admin"], + /* Der Eingang: DogFather und seine rechte Hand. Blueprint 3.1 -- + "Kann im Alltag vertretend Entscheidungen treffen." Genau das + passiert auf dieser Seite. */ + "/workspace/teamlage.html": ["admin", "hand"], }; /* Die einzige Seite unter /workspace, die offen sein MUSS -- man kann @@ -2693,7 +2771,8 @@ function stillerZugang(code) { if (!kennung) return null; const k = db().prepare( "SELECT id, name, rolle, code_hash, code_salt, code_n FROM personen " - + "WHERE code_kennung = ? AND rolle = 'modi' AND aktiv = 1").get(kennung); + + `WHERE code_kennung = ? AND rolle IN (${ + [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ")}) AND aktiv = 1`).get(kennung); if (!k) return null; /* Trotz Treffer wird geprueft. Der Suchschluessel ist ein Wegweiser, kein Ausweis -- und ein Wegweiser, dem man blind @@ -2806,9 +2885,23 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => { 401 "ungueltig". Ein eigener Block waere messbar schneller oder langsamer gewesen als der Weg daneben, und genau daran erkennt man eine Sonderbehandlung. */ - const fremdAufCrew = istCrewAdresse(req.get("host")); + /* JE ADRESSE EIN ANDERER KACHELSATZ (10.09.2026). - if (!ROLLEN_KACHEL.has(rolle) || !codeBrauchbar || fremdAufCrew) { + Bis eben stand hier `fremdAufCrew` -- auf crew. wurde JEDE + Kachelanmeldung abgewiesen, weil dort nur Modis ueber den + verborgenen Weg hereinkamen. Seit Filipes "3 rollen. dogfather. + rechte hand und modis" gibt es dort drei Kacheln, und der + gewohnte Weg wird gebraucht. + + Zwei Mengen statt einer Bedingung mit Ausnahmen: Welche Kachel + auf welcher Wand steht, ist EINE Tatsache -- sie steht oben bei + ROLLEN_KACHEL und CREW_KACHEL, und hier wird nur gefragt, welche + Wand gerade dran ist. Ein Manager, der crew. erraet und seinen + Code eintippt, faellt dadurch in genau dieselbe Antwort wie + jemand mit einer erfundenen Rolle. */ + const kachelSatz = istCrewAdresse(req.get("host")) ? CREW_KACHEL : ROLLEN_KACHEL; + + if (!kachelSatz.has(rolle) || !codeBrauchbar) { db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt()); return res.status(401).json({ fehler: "ungueltig" }); } @@ -3221,7 +3314,8 @@ export function verborgeneIds(person) { "sehen sie ohne Einzelheiten", sondern gar nicht: kein Name, kein Eintrag, keine Zahl, die sie mitzaehlt. */ const modis = d.prepare( - "SELECT id FROM personen WHERE rolle = 'modi'").all().map((z) => z.id); + `SELECT id FROM personen WHERE rolle IN (${ + [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", ")})`).all().map((z) => z.id); /* Ohne bekannte Person wird BEIDES verborgen. Das ist die strengere Antwort, und bei einer Sichtbarkeitsregel ist die strengere die @@ -3229,7 +3323,7 @@ export function verborgeneIds(person) { const darfAdminsSehen = !!person && (person.id === admins[0] || versteckteAdmins.includes(person.id)); const darfModisSehen = !!person - && (person.rolle === "admin" || person.rolle === "modi"); + && (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle)); const weg = []; if (!darfAdminsSehen) weg.push(...versteckteAdmins); @@ -3301,10 +3395,11 @@ function sichtbarePersonenIdsRoh(person) { Modi nur fuer die DogFather-Rolle und fuer Modis sichtbar ist. Wer kein Modi ist, bekommt diese Nummern hier gar nicht erst. */ const modis = []; - if (person.rolle === "modi") { + if (TEAM_DOGI_ROLLEN.has(person.rolle)) { try { + const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", "); for (const z of db().prepare( - "SELECT id FROM personen WHERE rolle = 'modi' AND aktiv = 1").all()) modis.push(z.id); + `SELECT id FROM personen WHERE rolle IN (${liste}) AND aktiv = 1`).all()) modis.push(z.id); } catch { /* ohne Datenbank bleibt die Liste, wie sie war */ } } return [...new Set([person.id, ...creator, ...scouts, ...dogi, ...modis])]; diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 6a4983d6..3cbaef92 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=202609101351", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609101459", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609101351", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609101459", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609101351", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609101459", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609101351", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609101459", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css index c0595c7a..0cd4326b 100644 --- a/workspace/assets/css/gate.css +++ b/workspace/assets/css/gate.css @@ -2761,6 +2761,47 @@ body.gate--crew #knopf:hover:not([disabled]) { 0 14px 30px -14px rgba(126, 200, 242, .85); } +/* DIE KACHELREIHE. Ihre Fassung laeuft im Original von Rot ueber + Silber nach Babyblau -- sie zitiert damit den Rand der Zentrale + ("Wer sich anmeldet, sieht damit schon hier die Handschrift der + Seite, die dahinter liegt", Begruendung weiter oben). Hinter DIESER + Wand liegt aber eine durchweg blaue Seite; das Rot zitiert dort + nichts mehr. + + Die Form bleibt Stufe fuer Stufe gleich -- dunkel, Farbe, hell, + dunkel, Silber in der Mitte, dann dasselbe gespiegelt. Nur die + linke Farbe wechselt von Rot nach Blau. Wer die Fassung spaeter + umbaut, sieht an den gleichen Prozentzahlen sofort, dass beide + zusammengehoeren. */ +body.gate--crew .rollen { + background: linear-gradient(96deg, + #04080f 0%, #245a86 6%, #4f95bd 14%, #cfe9f8 20%, + #1d4a70 30%, #0a0c12 40%, #f4faff 50%, #0a0c12 60%, + #4f95bd 70%, #8fd8ff 78%, #dff2ff 84%, #3f7fa3 92%, #06080f 100%); +} +/* Das Innenglas: unten links glueht es rot. Dieselbe Stelle, kaltes + Licht. Der Rest der Ebene (Messraster, Verlauf) bleibt unangetastet + -- deshalb wird die ganze Liste noch einmal geschrieben und nicht + nur eine Zeile: `background` ist eine Kurzschreibweise, eine einzelne + Lage laesst sich nicht ersetzen. */ +body.gate--crew .rollen::before { + background: + linear-gradient(180deg, rgba(255, 255, 255, .05), transparent 34%), + repeating-linear-gradient(0deg, rgba(150, 186, 220, .05) 0 1px, transparent 1px 22px), + repeating-linear-gradient(90deg, rgba(150, 186, 220, .05) 0 1px, transparent 1px 22px), + radial-gradient(120% 80% at 12% 100%, rgba(96, 168, 224, .12), transparent 62%), + radial-gradient(110% 75% at 88% 0%, rgba(126, 200, 242, .09), transparent 60%), + linear-gradient(180deg, #0b111c, #070b13); +} +/* Der kleine Anschlag neben "Womit meldest du dich an?" -- oben rot, + unten babyblau. Hier oben im Ton von Team Dogi, unten babyblau: derselbe + Zweifarbenaufbau, beide Haelften kalt. */ +body.gate--crew .rollen .feldschild::before { + background-image: + linear-gradient(180deg, transparent 0 46%, rgba(143, 216, 255, .95) 46%), + repeating-linear-gradient(180deg, #5f8a9f 0 2px, transparent 2px 5px); +} + /* Die Karte in der gemalten Tafel. WORTGLEICH dieselbe Bedingung wie oben -- nicht global: Auf schmalen Geraeten hat die Karte einen anderen Aufbau, und ein globales `background` haette den ueberschrieben. diff --git a/workspace/assets/js/gate.js b/workspace/assets/js/gate.js index 8128ad6d..a877e0bd 100644 --- a/workspace/assets/js/gate.js +++ b/workspace/assets/js/gate.js @@ -25,16 +25,48 @@ abfragen. Damit es auffaellt, faellt ein unbekannter Schluessel jetzt auf den Schluessel selbst zurueck statt auf `undefined` -- "spicy" waere haesslich gewesen, aber sichtbar. */ - const NAMEN = { spicy: 'Spicy Media', admin: 'DogFather', manager: 'Manager', - scout: 'Scout', creator: 'Creator' }; - const rollenName = (r) => NAMEN[r] || r || ''; + /* DER NAME STEHT IN DER KACHEL -- ES GIBT KEINE ZWEITE LISTE MEHR. + + Hier stand eine Zuordnung { spicy: 'Spicy Media', admin: + 'DogFather', ... }, und der Kommentar daneben nannte sie selbst + "genau die Stelle, die beim naechsten Mal wieder vergessen wird". + Das war sie: Beim Hinzufuegen der fuenften Rolle fehlte sie, und + unter dem Codefeld stand "PERSOENLICHER ZUGANGSCODE · undefined". + + Zwei Gruende, sie jetzt ganz zu entfernen: + + 1. ES GIBT SEIT DEM 10.09.2026 ZWEI ZUGANGSWAENDE mit + verschiedenen Kacheln. Eine gemeinsame Liste haette die Namen + BEIDER Waende enthalten muessen -- diese Datei wird aber an + jeden ausgeliefert. Die Namen der einen Wand haetten damit + auf der anderen im Quelltext gestanden. + + 2. Der Name steht ohnehin schon da, sichtbar, in der Kachel. Ihn + von dort zu lesen kann gar nicht veralten: Wer eine Kachel + anlegt, schreibt den Namen genau einmal. + + Faellt der Text weg, bleibt der Schluessel stehen -- haesslich, + aber sichtbar. Das war schon vorher die richtige Entscheidung. */ + const rollenName = (r) => rollen.find((b) => b.dataset.rolle === r) + ?.querySelector('.rolle__name')?.textContent?.trim() || r || ''; const SPEICHER = 'dfw.rolle'; let gewaehlt = null; - /* AUF DER ZWEITEN ADRESSE GIBT ES KEINE KACHELN (10.09.2026). + /* EINE WAND OHNE KACHELN IST MOEGLICH (10.09.2026). - Dort fuehrt genau eine Sorte Zugang hinein, also ist nichts zu - waehlen -- jene Wand liefert die Reihe erst gar nicht mit. + Am Vormittag gab es sie: Die zweite Zugangswand fuehrte nur eine + Sorte Zugang herein, und eine Kachelreihe mit einem Eintrag waere + eine Bedienhuerde gewesen. Am Nachmittag hat Filipe entschieden, + dass dort drei Rollen stehen ("3 rollen. dogfather. rechte hand + und modis") -- heute hat also wieder jede Wand ihre Reihe. + + DIESE ZEILE BLEIBT TROTZDEM, und das ist bewusst: Sie kostet drei + Zeichen und beantwortet eine Frage, die dieses Skript sonst gar + nicht stellt. Ohne sie bliebe `gewaehlt` auf einer kachellosen + Wand ewig null, und der Anmeldeknopf antwortete auf jeden Druck + mit "Bitte zuerst auswaehlen" -- vor einer Seite, auf der es + nichts auszuwaehlen gibt. Kein Fehler im Protokoll, keine Meldung, + nur eine Anmeldung, die nicht geht. Geprueft wird deshalb, OB es Kacheln gibt, statt sie vorauszusetzen. Ohne diese Zeile bliebe `gewaehlt` dort ewig null, @@ -74,7 +106,10 @@ // Zuletzt genutzte Rolle vorauswählen - spart bei täglicher Nutzung einen Klick. try { const zuletzt = localStorage.getItem(SPEICHER); - if (zuletzt && NAMEN[zuletzt]) waehle(zuletzt); + /* Gefragt wird, ob es die Kachel HIER gibt -- der Speicher gilt je + Adresse, aber eine Rolle kann verschwinden. Ohne diese Pruefung + waere eine Rolle vorausgewaehlt, die es nicht mehr gibt. */ + if (zuletzt && rollen.some((b) => b.dataset.rolle === zuletzt)) waehle(zuletzt); } catch { /* egal */ } /* ---------- Code anzeigen / verbergen ------------------------------ */ diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 40c3c961..4017db90 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,12 +12,12 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + @@ -245,10 +245,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 9aee69ae..89166665 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -140,10 +140,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 9c541579..5e29e7bd 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -157,14 +157,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index d74a7020..789261bd 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -82,11 +82,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index c9ae4f7f..b904c60e 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + @@ -247,10 +247,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 94142a26..95f7b2f2 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -172,12 +172,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index 5dfba821..32403836 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -30,28 +30,76 @@ - - - + + + - + Von den fuenf Zeichen der anderen Wand bleibt nur der Husky -- die + uebrigen gehoeren zu Rollen, die es hier nicht gibt, und ihre + Namen haetten im Quelltext nichts verloren. + + ZWEIMAL DERSELBE HUSKY, und das ist Absicht: Filipe am 09.09.2026 + zum Rollenbild: "das modi symbol soll das gleiche sein wie bei + dogfather." Unterschieden wird ueber Name und Farbe, nicht ueber + das Zeichen -- so wie `profil` auf der Startseite ohnehin schon + zweimal steht. + + DIE PFOTE fuer die Stellvertretung ist neu. Sie ist das Zeichen, + das am naechsten an "rechte Hand" liegt, ohne eine Hand zu sein -- + eine menschliche Hand waere in diesem Zeichensatz der einzige + Fremdkoerper. Vier Zehen und ein Ballen tragen bei 22 px, weil es + fuenf klar getrennte Flaechen sind und keine Innenzeichnung. + ================================================================= --> + @@ -123,17 +171,40 @@

Privater Bereich für DogFather und die Modis.

- + + + + + + +