From 68bc116c36221e7db550f05a3b9b4b36f7818cd0 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Mon, 7 Sep 2026 04:56:07 +0200 Subject: [PATCH] Die Rolle "Spicy Media" -- und drei Fehler, die nur ihre Pruefung fand DIE PERSONENTABELLE WURDE NEU GEBAUT. Eine Rolle ist ein erlaubter Wert in einer Spalte, und der steckt in einem CHECK -- den kann SQLite nicht aendern. Auf `personen` zeigen ZWEIUNDFUENFZIG Fremdschluessel. Deshalb wird der Bauplan NICHT abgeschrieben, sondern gelesen: Der CREATE-Text kommt aus sqlite_master, darin wird ausschliesslich die Rollenliste ersetzt, und die Kopierliste kommt aus PRAGMA table_info. Die beiden aelteren Umstellungen schreiben ihre Spalten von Hand ab -- `personen` hat seit damals SIEBEN dazubekommen (bild, ueber_mich, tiktok ...). Wer hier abschreibt, verliert alle Profilbilder. ZWEI FRAGEN, DIE MAN AUSEINANDERHALTEN MUSS: siehtAlles() = DogFather ODER Spicy Media -> Listen, Uebersichten istDogFather() = nur DogFather -> loeschen, Rollen, Codes Es waere weniger Arbeit gewesen, istDogFather() um "spicy" zu erweitern -- und genau das waere der Fehler: Spicy Media koennte dann DogFather loeschen. DER CHAT BRAUCHTE NICHTS. Er haengt allein an der Teilnehmerliste und kennt kein "das Management sieht alles". Spicy Media sieht fremde Gespraeche nicht, weil es dafuer keinen Weg gibt -- nicht, weil eine Abfrage es verbietet. DREI ECHTE FEHLER, alle von pruef-spicy gefunden, keiner vorher sichtbar: 1. MEIN EIGENER KOMMENTAR STAND IM BAUPLAN. SQLite hebt den CREATE-Text woertlich auf, samt Kommentaren. Ich hatte "'spicy' steht HIER mit drin" hineingeschrieben -- und die Erkennung suchte genau dieses Wort im ganzen Text. Ergebnis: Die Umstellung hielt die Tabelle fuer erledigt, obwohl die CHECK-Regel noch die alte war. Jetzt wird die Regel herausgeschnitten und NUR darin gesucht; der Kommentar steht ausserhalb des SQL. 2. DER SICHERUNGSNAME HATTE NUR MINUTEN. `VACUUM INTO` weigert sich, eine vorhandene Datei zu ueberschreiben -- zu Recht. Zwei Umstellungen in derselben Minute wollten in dieselbe Datei, die zweite scheiterte, und weil ohne Sicherung nicht umgestellt wird, blieb sie aus. Es sah nach "lief" aus (die Datei lag ja da) und war keine. Jetzt mit Sekunden. 3. EINE FRISCHE DATENBANK LEGTE DIE ALTE ROLLENLISTE AN und stellte beim allerersten Start sofort um -- Tabelle neu bauen, Sicherung schreiben, fuer nichts. Ein Bauplan, der sofort umgebaut werden muss, ist der falsche. Und ein vierter in der Pruefung selbst: Der Chat-Aufbau benutzte einen falschen Weg, das Gespraech entstand gar nicht -- "Spicy Media sieht 0 Gespraeche" war trotzdem gruen, weil es keine gab. Jetzt ist das Anlegen selbst eine Pruefung, und eine Gegenprobe zeigt, dass Max es sehr wohl sieht. DAZU: Manager sehen die Kachel "Personen & Zugaenge" -- die SEITE geht auf, die Schnittstellen nicht. Alles unter /verwaltung haengt weiter an `nurAdmin` und antwortet 404; sie sehen genau den Teil, fuer den es einen Weg gibt. Spicy Media legt Creator UND Manager an (zweite enge Tuer, Rolle steht auch dort nicht im Aufruf; ein Manager kommt durch sie nicht). Der Rollentext des Managers stimmte nicht mehr -- er legt jetzt Creator an. pruef-spicy: 36 Pruefungen, alle gruen. Ausserdem gruen: personen-liste (33), creator-anlegen (29), sicht (48), css-klassen (15), alle-wege (19), haerte (20), manager-sicht (43). Co-Authored-By: Claude Opus 5 --- server/pruef-spicy.mjs | 365 ++++++++++++++++++++++++++++++ server/workspace-bereiche.js | 4 +- server/workspace-calls.js | 4 +- server/workspace-checkliste.js | 4 +- server/workspace-content.js | 4 +- server/workspace-dateien.js | 4 +- server/workspace-hinweise.js | 6 +- server/workspace-kalender.js | 4 +- server/workspace-personen.js | 41 +++- server/workspace-profil.js | 4 +- server/workspace-reports.js | 4 +- server/workspace-schulung.js | 4 +- server/workspace-scouts.js | 4 +- server/workspace-suche.js | 6 +- server/workspace.js | 257 +++++++++++++++++++-- workspace/assets/css/gate.css | 3 + workspace/assets/css/personen.css | 16 ++ workspace/assets/js/bereiche.js | 46 ++-- workspace/assets/js/personen.js | 81 ++++++- workspace/assets/js/start.js | 6 +- workspace/aufgaben.html | 16 +- workspace/automation.html | 18 +- workspace/bereich.html | 26 +-- workspace/calls.html | 20 +- workspace/chat.html | 16 +- workspace/content.html | 22 +- workspace/dateien.html | 18 +- workspace/index.html | 20 +- workspace/kalender.html | 20 +- workspace/leistung.html | 18 +- workspace/personen.html | 23 +- workspace/profil.html | 20 +- workspace/report.html | 20 +- workspace/scouting.html | 18 +- workspace/start.html | 16 +- workspace/startcheck.html | 20 +- workspace/steckbrief.html | 18 +- workspace/uebersicht.html | 16 +- workspace/wissen.html | 18 +- 39 files changed, 983 insertions(+), 247 deletions(-) create mode 100644 server/pruef-spicy.mjs diff --git a/server/pruef-spicy.mjs b/server/pruef-spicy.mjs new file mode 100644 index 00000000..650241bf --- /dev/null +++ b/server/pruef-spicy.mjs @@ -0,0 +1,365 @@ +/* ===================================================================== + DIE ROLLE "SPICY MEDIA" + + Wunsch Filipe (07.09.2026): "eine neue rolle ... soll die gleichen + rechte haben wie dogfather ausser, bei automationen sollen die nicht + sehen und die kategorie personen und zugaenge sollen die nur leute + hinzufuegen koennen ... aber die sollen meine privaten chats und so + nicht sehen." + + --------------------------------------------------------------------- + ZWEI DINGE WERDEN HIER GEPRUEFT, UND DAS ZWEITE IST DAS WICHTIGERE + + 1. DER UMBAU DER PERSONENTABELLE. Eine Rolle ist ein erlaubter Wert in + einer Spalte, und der steckt in einem CHECK. Ein CHECK laesst sich + nicht aendern -- die Tabelle muss neu gebaut werden. Auf `personen` + zeigen ZWEIUNDFUENFZIG Fremdschluessel; jede Sitzung, jede Aufgabe, + jeder Termin haengt daran. Deshalb wird der Weg hier nicht "im + Prinzip" geprueft, sondern WIRKLICH GEGANGEN: Die Pruefung baut + eine Datenbank im alten Stand nach, fuellt sie mit Personen UND + abhaengigen Zeilen, laesst die echte Anwendung darueberlaufen und + zaehlt danach jede Zeile und jedes Feld nach. + + 2. DIE GRENZEN DER ROLLE. Zu jedem "darf" steht das "darf nicht" + daneben. Eine Rolle, von der nur geprueft ist, was sie kann, ist + keine Rolle, sondern eine Vermutung. + + Aufruf: node server/pruef-spicy.mjs + ===================================================================== */ +import { mkdtempSync, rmSync, existsSync, readdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname, basename } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { DatabaseSync } from "node:sqlite"; +import { scryptSync, randomBytes } from "node:crypto"; +import { notbremse } from "./helfer-notbremse.mjs"; + +notbremse(300_000, "die Spicy-Pruefung"); + +const HIER = dirname(fileURLToPath(import.meta.url)); +const { portMussFreiSein } = await import("./helfer-port.mjs"); +const PORT = 4351; +await portMussFreiSein(PORT, "die Spicy-Pruefung"); +const BASIS = `http://127.0.0.1:${PORT}`; + +const ordner = mkdtempSync(join(tmpdir(), "ws-spicy-")); +const DB = join(ordner, "workspace.db"); + +let fehler = 0, gemacht = 0; +const ok = (b, t) => { gemacht++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +let kind = null; +async function starten() { + kind = spawn(process.execPath, [join(HIER, "index.js")], { + env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT), + SITE_ACCESS_SECRET: "lokaler-test", + SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" }, + stdio: ["ignore", "pipe", "pipe"] }); + let aus = ""; + kind.stdout.on("data", (d) => { aus += d; }); + kind.stderr.on("data", (d) => { aus += d; }); + for (let i = 0; i < 100; i++) { + await new Promise((r) => setTimeout(r, 250)); + if (kind.exitCode !== null) break; + try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); + if (a.status < 500) return () => aus; } catch { /* noch nicht oben */ } + } + throw new Error("Server startete nicht:\n" + aus.slice(-1500)); +} +async function stoppen() { + if (!kind) return; + kind.kill(); kind = null; + await new Promise((r) => setTimeout(r, 700)); +} + +/* ======================================================================= + 1. Eine Datenbank im ALTEN Stand -- mit Personen und allem, was daran + haengt. Ohne die abhaengigen Zeilen pruefte der Umbau nur eine + Tabelle statt eines Netzes. + ======================================================================= */ +console.log("=== Der alte Stand ==="); +await starten(); +await stoppen(); + +const jetzt = new Date().toISOString(); +const anlegen = (d, name, rolle, code) => { + const salz = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salz, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salz, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +}; + +let idDogi, idMax, idLuna, spaltenVorher, personenVorher; +{ + const d = new DatabaseSync(DB); + /* Die Tabelle auf den alten CHECK zuruecksetzen -- WOERTLICH so, wie + sie vor heute aussah, aber mit allen Spalten, die inzwischen + dazugekommen sind. Genau das ist der Fall, der gefaehrlich ist. */ + d.exec("PRAGMA foreign_keys = OFF"); + const alt = d.prepare( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='personen'").get().sql; + const zurueck = alt + /* Auch hier muss `IF NOT EXISTS` ins Muster -- sonst waere der + "alte Stand" gar keiner: Die Ersetzung liefe ins Leere, `CREATE + TABLE IF NOT EXISTS personen` waere wegen des vorhandenen Tisches + ein Leerlauf, und die Pruefung testete den heutigen Stand gegen + sich selbst. */ + .replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i, + "CREATE TABLE personen_alt") + .replace(/rolle\s+IN\s*\([^)]*\)/i, "rolle IN ('admin','manager','scout','creator')"); + /* Wenn der Rueckbau nicht greift, ist alles danach wertlos -- dann + prueft diese Datei den heutigen Stand gegen sich selbst. Deshalb + hier abbrechen statt weitermachen. */ + const regelDanach = zurueck.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || ""; + if (!zurueck.includes("personen_alt") || regelDanach.includes("'spicy'")) { + console.log(" FEHL der alte Stand liess sich nicht nachbauen -- Muster passt nicht."); + process.exit(1); + } + d.exec("BEGIN"); + d.exec(zurueck); + const sp = d.prepare("PRAGMA table_info(personen)").all().map((z) => `"${z.name}"`).join(", "); + d.exec(`INSERT INTO personen_alt (${sp}) SELECT ${sp} FROM personen;`); + d.exec("DROP TABLE personen;"); + d.exec("ALTER TABLE personen_alt RENAME TO personen;"); + d.exec("COMMIT"); + d.exec("PRAGMA foreign_keys = ON"); + + idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001"); + idMax = anlegen(d, "Max", "manager", "CODE-MAX-00001"); + idLuna = anlegen(d, "Luna", "creator", "CODE-LUNA-0001"); + /* Ein Profilbild und ein Feld aus einer SPAETER ergaenzten Spalte -- + genau die verliert man beim Abschreiben einer Spaltenliste. */ + d.prepare("UPDATE personen SET bild = ?, ueber_mich = ?, tiktok = ? WHERE id = ?") + .run("abcdef0123456789abcdef0123456789.jpg", "Ich streame seit 2024.", "luna_live", idLuna); + /* Abhaengige Zeilen aus verschiedenen Tabellen. */ + d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)") + .run(idLuna, idMax, jetzt); + d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, creator_id, erstellt, erstellt_von) + VALUES ('technik','problem','Ton bricht ab',?,?,?,?)`) + .run(jetzt.slice(0, 10), idLuna, jetzt, idMax); + + spaltenVorher = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name); + personenVorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; + + /* GEGENPROBE: Der ALTE Stand muss 'spicy' wirklich ablehnen -- sonst + gaebe es nichts umzustellen, und alles danach bewiese nichts. */ + let abgelehnt = false; + try { + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,'spicy','x','y',1,1,?)") + .run("Darf noch nicht", jetzt); + } catch { abgelehnt = true; } + d.close(); + ok(abgelehnt, "der alte Stand lehnt die Rolle 'spicy' wirklich ab"); + ok(personenVorher === 3, `${personenVorher} Personen und abhaengige Zeilen angelegt`); + ok(spaltenVorher.includes("bild") && spaltenVorher.includes("tiktok"), + `die Tabelle hat ${spaltenVorher.length} Spalten, auch die spaeter ergaenzten`); +} + +/* ======================================================================= + 2. Die Anwendung laeuft darueber + ======================================================================= */ +console.log("\n=== Die Umstellung ==="); +const ausgabe = await starten(); +/* Die Umstellung laeuft beim ersten Datenbankzugriff, nicht beim + Zuhoeren am Anschluss -- ihre Meldung kann also erst kurz nach dem + Start in der Ausgabe stehen. Eine Anfrage stellen und kurz warten. */ +await fetch(BASIS + '/workspace/', { redirect: 'manual' }).catch(() => {}); +await new Promise((r) => setTimeout(r, 900)); +console.log("--- Serverausgabe ---"); +for (const z of ausgabe().split(String.fromCharCode(10))) if (z.includes("workspace]")) console.log(" " + z); +{ + ok(/Rolle 'spicy' freigeschaltet/.test(ausgabe()), + "die Umstellung meldet sich im Protokoll"); + ok(/Sicherung vor der Umstellung/.test(ausgabe()), + "und hat vorher eine Sicherung geschrieben"); + const sicherungen = readdirSync(ordner).filter((n) => n.includes("vor-spicy")); + ok(sicherungen.length === 1, `die Sicherungsdatei liegt da (${sicherungen[0] || "keine"})`); +} +{ + const d = new DatabaseSync(DB, { readOnly: true }); + const spalten = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name); + const fehlend = spaltenVorher.filter((n) => !spalten.includes(n)); + ok(fehlend.length === 0, + fehlend.length ? `nach der Umstellung fehlen Spalten: ${fehlend.join(", ")}` + : `alle ${spalten.length} Spalten sind noch da`); + + const n = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; + ok(n === personenVorher, `alle ${n} Personen sind noch da (vorher ${personenVorher})`); + + const luna = d.prepare("SELECT * FROM personen WHERE id = ?").get(idLuna); + ok(luna?.bild === "abcdef0123456789abcdef0123456789.jpg", + `das Profilbild hat den Umbau ueberlebt (${luna?.bild ? "ja" : "WEG"})`); + ok(luna?.ueber_mich === "Ich streame seit 2024." && luna?.tiktok === "luna_live", + "auch die spaeter ergaenzten Textspalten"); + + const b = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?").get(idLuna); + ok(b?.betreuer_id === idMax, `die Betreuung zeigt noch auf Max (${b?.betreuer_id})`); + const e = d.prepare("SELECT creator_id FROM eintraege WHERE titel = 'Ton bricht ab'").get(); + ok(e?.creator_id === idLuna, `der Technik-Eintrag noch auf Luna (${e?.creator_id})`); + const kaputt = d.prepare("PRAGMA foreign_key_check").all(); + ok(kaputt.length === 0, `keine verwaisten Verweise (${kaputt.length})`); + + /* GEGENPROBE, DIE WICHTIGSTE: Die Schranke muss noch BEISSEN. Eine + Umstellung, die nebenbei den CHECK entfernt, sieht aus wie ein + Erfolg und ist der schlimmere Ausgang. */ + d.close(); + const s = new DatabaseSync(DB); + let erfunden = false; + try { + s.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,'quatsch','x','y',1,1,?)") + .run("Erfundene Rolle", jetzt); + } catch { erfunden = true; } + ok(erfunden, "die Schranke beisst noch -- eine erfundene Rolle wird abgelehnt"); + const idSpicy = anlegen(s, "Agentur", "spicy", "CODE-SPICY-001"); + ok(Number.isInteger(idSpicy) && idSpicy > 0, "'spicy' laesst sich jetzt anlegen"); + s.close(); +} + +/* ======================================================================= + 3. Was die Rolle darf -- und was nicht + ======================================================================= */ +console.log("\n=== Rechte ==="); +const anmelden = async (rolle, code) => { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code }) }); + const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); + if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`); + return keks; +}; +const keksSpicy = await anmelden("spicy", "CODE-SPICY-001"); +const keksDogi = await anmelden("admin", "CODE-DOGI-0001"); +const keksMax = await anmelden("manager", "CODE-MAX-00001"); + +const ruf = async (art, weg, keks, koerper) => { + const a = await fetch(BASIS + weg, { method: art, + headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS }, + body: koerper ? JSON.stringify(koerper) : undefined }); + let daten = null; try { daten = await a.json(); } catch { /* leer erlaubt */ } + return { status: a.status, daten }; +}; + +{ + const { status, daten } = await ruf("GET", "/workspace/api/ich", keksSpicy); + ok(status === 200 && daten?.rolle === "spicy", `die Anmeldung klappt (${daten?.rolle})`); + ok(daten?.rolle_name === "Spicy Media", `und heisst in der Oberflaeche "${daten?.rolle_name}"`); +} +{ + /* SIEHT ALLES: alle Personen, alle Bereiche. */ + const { daten } = await ruf("GET", "/workspace/api/personen", keksSpicy); + const namen = (daten?.personen || []).map((p) => p.name).sort(); + ok(namen.includes("Luna") && namen.includes("Max") && namen.includes("Filipe"), + `sie sieht alle: ${namen.join(", ")}`); +} +{ + const { status } = await ruf("GET", "/workspace/api/uebersicht/creator", keksSpicy); + ok(status === 200, `das Dashboard steht ihr offen (HTTP ${status})`); +} +{ + /* NICHT: Automationen. */ + const a = await fetch(BASIS + "/workspace/automation.html", { + headers: { Cookie: keksSpicy }, redirect: "manual" }); + ok(a.status === 302 || a.status === 403 || a.status === 404, + `die Automationen-Seite bleibt zu (HTTP ${a.status})`); + const { status } = await ruf("GET", "/workspace/api/sicherung/zustand", keksSpicy); + ok(status === 404, `und die Sicherungs-Schnittstelle auch (HTTP ${status})`); +} +{ + /* NICHT: die Personenverwaltung. Anlegen ja, alles andere nein. */ + const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksSpicy); + ok(status === 404, `die Personenverwaltung bleibt zu (HTTP ${status})`); + const loeschen = await ruf("DELETE", `/workspace/api/verwaltung/personen/${idLuna}`, keksSpicy); + ok(loeschen.status === 404, `loeschen geht nicht (HTTP ${loeschen.status})`); + const code = await ruf("POST", `/workspace/api/verwaltung/personen/${idLuna}/code`, keksSpicy); + ok(code.status === 404, `einen Code neu setzen auch nicht (HTTP ${code.status})`); +} +{ + /* ABER: Leute anlegen -- Creator und Manager. */ + const c = await ruf("POST", "/workspace/api/creator-anlegen", keksSpicy, { name: "NeuerCreator" }); + ok(c.status === 201 && c.daten?.rolle === "creator", + `sie legt einen Creator an (HTTP ${c.status}, ${c.daten?.rolle})`); + const m = await ruf("POST", "/workspace/api/manager-anlegen", keksSpicy, { name: "NeuerManager" }); + ok(m.status === 201 && m.daten?.rolle === "manager", + `und einen Manager (HTTP ${m.status}, ${m.daten?.rolle})`); +} +{ + /* GEGENPROBE: Ein Manager kann KEINEN Manager anlegen -- sonst waere + die zweite Tuer nur Zierde. */ + const { status } = await ruf("POST", "/workspace/api/manager-anlegen", keksMax, { name: "VonMax" }); + ok(status === 404, `ein Manager kommt durch diese Tuer NICHT (HTTP ${status})`); +} +{ + /* NICHT: fremde Chats. + + Der Chat haengt ausschliesslich an der Teilnehmerliste -- Spicy + Media ist nirgends Teilnehmerin, also sieht sie nichts. Geprueft + wird mit einem ECHTEN Gespraech zwischen DogFather und Max. + + DER AUFBAU MUSS GELINGEN, SONST BEWEIST DER REST NICHTS. Beim + ersten Anlauf stand hier ein falscher Weg (/chat/raum statt + /chat/direkt). Das Gespraech entstand gar nicht -- und "Spicy Media + sieht 0 Gespraeche" war trotzdem gruen, weil es ueberhaupt keine + gab. Genau die Sorte gruener Haken, die nichts geprueft hat. + Deshalb ist das Anlegen jetzt selbst eine Pruefung, und die + Raumnummer wird nachgehalten. */ + const raum = await ruf("POST", "/workspace/api/chat/direkt", keksDogi, { mit: idMax }); + const raumId = raum.daten?.raum_id; + ok(raum.status === 200 && Number.isInteger(raumId), + `DogFather und Max haben ein Gespraech (HTTP ${raum.status}, Raum ${raumId})`); + const nachricht = await ruf("POST", `/workspace/api/chat/raeume/${raumId}/nachrichten`, + keksDogi, { text: "Streng geheim." }); + ok(nachricht.status === 200 || nachricht.status === 201, + `und eine Nachricht darin (HTTP ${nachricht.status})`); + + /* GEGENPROBE: Max SIEHT das Gespraech -- sonst pruefte die Zeile + darunter nur, dass die Liste immer leer ist. */ + const beiMax = await ruf("GET", "/workspace/api/chat/raeume", keksMax); + ok((beiMax.daten?.raeume || []).length === 1, + `Max sieht es (${(beiMax.daten?.raeume || []).length})`); + + const meine = await ruf("GET", "/workspace/api/chat/raeume", keksSpicy); + const raeume = meine.daten?.raeume || []; + ok(raeume.length === 0, `Spicy Media sieht 0 Gespraeche (${raeume.length})`); + const rein = await ruf("GET", `/workspace/api/chat/raeume/${raumId}/nachrichten`, keksSpicy); + ok(rein.status === 403 || rein.status === 404, + `und kommt auch mit der Raumnummer nicht hinein (HTTP ${rein.status})`); +} +{ + /* Und DogFather bleibt der Einzige mit dem letzten Wort. */ + const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi); + ok(status === 200, `DogFather kommt weiterhin in die Verwaltung (HTTP ${status})`); +} + +/* ======================================================================= + 4. Die Anmeldeseite kennt die Rolle + ======================================================================= */ +console.log("\n=== Die Anmeldeseite ==="); +{ + const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await pw.chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 1440, height: 950 } }); + const seite = await ctx.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); + const rollen = await seite.$$eval(".rolle", (k) => k.map((x) => x.dataset.rolle)); + ok(rollen[0] === "spicy", `sie steht ganz oben (${rollen.join(", ")})`); + await seite.click('.rolle[data-rolle="spicy"]'); + await seite.fill("#code", "CODE-SPICY-001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 25000 }); + await seite.waitForTimeout(1500); + const kacheln = await seite.$$eval(".kachel__name", (k) => k.map((x) => x.textContent.trim())); + ok(kacheln.includes("Personen & Zugänge"), + `sie sieht die Personen-Kachel (${kacheln.length} Kacheln)`); + ok(!kacheln.includes("Automationen"), "und die Automationen-Kachel NICHT"); + await ctx.close(); + await browser.close(); +} + +await stoppen(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ } + +console.log(`\n${gemacht} Pruefungen`); +console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`); +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 7f5a3625..472dc29e 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -16,7 +16,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, + db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, externPruefen, externSql, } from "./workspace.js"; @@ -262,7 +262,7 @@ export function sichtbar(person) { darf (freigeben, aendern, Personen verwalten), haengt weiterhin an istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch stillschweigend seine halben Rechte mitgenommen. */ - if (istDogFather(person)) return { wo: "1=1", werte: [] }; + if (siehtAlles(person)) return { wo: "1=1", werte: [] }; /* "ODER ich habe ihn selbst angelegt" kam am 02.09.2026 dazu -- aus demselben Grund wie bei den Aufgaben: Ein Eintrag ohne creator_id diff --git a/server/workspace-calls.js b/server/workspace-calls.js index 7765c3a3..57a47abc 100644 --- a/server/workspace-calls.js +++ b/server/workspace-calls.js @@ -18,7 +18,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, einstellung, einstellungSetzen, istLeitung, istDogFather, + db, protokolliere, echteIp, sitzungLesen, einstellung, einstellungSetzen, istLeitung, istDogFather, siehtAlles, } from "./workspace.js"; import { sichtbar } from "./workspace-kalender.js"; @@ -203,7 +203,7 @@ callRouter.put("/workspace/api/call-link", gleicheHerkunft, (req, res) => { CASCADE auf der Zwischentabelle), die Aufgaben selbst nicht. */ callRouter.delete("/workspace/api/calls/:id/protokoll", gleicheHerkunft, (req, res) => { try { - if (!istDogFather(req.person)) { + if (!siehtAlles(req.person)) { return res.status(403).json({ fehler: "Nur DogFather darf ein Protokoll löschen." }); } const id = Number(req.params.id); diff --git a/server/workspace-checkliste.js b/server/workspace-checkliste.js index 01a1ff55..25b171cf 100644 --- a/server/workspace-checkliste.js +++ b/server/workspace-checkliste.js @@ -34,7 +34,7 @@ import express from "express"; import { - db, sitzungLesen, istLeitung, istDogFather, darfCreator, betreuteIds, + db, sitzungLesen, istLeitung, istDogFather, siehtAlles, darfCreator, betreuteIds, protokolliere, echteIp, } from "./workspace.js"; import { @@ -207,7 +207,7 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => { if (req.person.rolle !== "creator") { /* Nur DogFather waehlt aus allen Creatorn; ein Manager aus denen, die ihm zugeteilt sind (01.09.2026). */ - const ids = istDogFather(req.person) + const ids = siehtAlles(req.person) ? db().prepare("SELECT id FROM personen WHERE rolle = 'creator' AND aktiv = 1") .all().map((z) => z.id) : betreuteIds(req.person); diff --git a/server/workspace-content.js b/server/workspace-content.js index 5298f2d2..c24bd5f1 100644 --- a/server/workspace-content.js +++ b/server/workspace-content.js @@ -26,7 +26,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, - istLeitung, istDogFather, tagLokal, + istLeitung, istDogFather, siehtAlles, tagLokal, } from "./workspace.js"; export const contentRouter = express.Router(); @@ -85,7 +85,7 @@ function sichtbareCreator(person) { darf (freigeben, aendern, Personen verwalten), haengt weiterhin an istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch stillschweigend seine halben Rechte mitgenommen. */ - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; if (person.rolle === "creator") return [person.id]; return betreuteIds(person); } diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index 6c8f91cb..253665b9 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -29,7 +29,7 @@ import { randomBytes } from "node:crypto"; import { join, extname, basename } from "node:path"; import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from "node:fs"; import { - db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, ROLLEN_SORTIERUNG, + db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, externSql, EXTERN_MAX, sichtbarePersonenIds, } from "./workspace.js"; @@ -79,7 +79,7 @@ export function sichtbar(person) { darf (freigeben, aendern, Personen verwalten), haengt weiterhin an istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch stillschweigend seine halben Rechte mitgenommen. */ - if (istDogFather(person)) return { wo: "1=1", werte: [] }; + if (siehtAlles(person)) return { wo: "1=1", werte: [] }; /* Ausdrueckliche Freigabe -- gilt fuer jede Rolle gleich. Wer in der Liste steht, sieht die Datei, egal ob Creator oder Scout. */ diff --git a/server/workspace-hinweise.js b/server/workspace-hinweise.js index 2d0da0a4..bd331b35 100644 --- a/server/workspace-hinweise.js +++ b/server/workspace-hinweise.js @@ -24,7 +24,7 @@ import express from "express"; import { - db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds, + db, sitzungLesen, betreuteIds, istLeitung, istDogFather, siehtAlles, pipelineIds, } from "./workspace.js"; import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js"; import { sichtbar as sichtbarTermine } from "./workspace-kalender.js"; @@ -222,8 +222,8 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => { Scouts -- dieselbe Regel wie in der Pipeline selbst, aus derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der mehr verraet als die Seite dahinter, waere ein Leck. */ - if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") { - const ids = istDogFather(person) ? null : pipelineIds(person); + if (siehtAlles(person) || person.rolle === "scout" || person.rolle === "manager") { + const ids = siehtAlles(person) ? null : pipelineIds(person); const nur = ids === null ? "1=1" : ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1"; const werte = ids === null ? [] : ids; diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index c78df009..1dc613c2 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -13,7 +13,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, betreutWo, termineSichtbar, heuteLokal, + db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, siehtAlles, betreutWo, termineSichtbar, heuteLokal, externPruefen, externSql, betreuteIds, einladbareIds, ROLLEN_SORTIERUNG, } from "./workspace.js"; import { @@ -282,7 +282,7 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { const eigenA = "(a.creator_id = ? OR a.verantwortlich_id = ? OR a.erstellt_von = ?)"; const werteA = [sicht.id, sicht.id, sicht.id]; const bA = betreutWo(sicht, "a.creator_id"); - const aufgabenWo = istDogFather(sicht) + const aufgabenWo = siehtAlles(sicht) ? { wo: "1=1", werte: [] } : bA ? { wo: `(${eigenA} OR ${bA.wo})`, werte: [...werteA, ...bA.werte] } diff --git a/server/workspace-personen.js b/server/workspace-personen.js index dd32a3d7..ddfce9ca 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, ROLLEN_SORTIERUNG, ROLLEN_REIHE, + db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, } from "./workspace.js"; import { sicherungJetzt } from "./workspace-sicherung.js"; @@ -368,6 +368,45 @@ personenRouter.post("/workspace/api/creator-anlegen", gleicheHerkunft, nurLeitun } }); +/* ZWEITE TUER: EINEN MANAGER ANLEGEN -- nur Spicy Media und DogFather + (07.09.2026). + + Filipe: "die agentur manager und creator und die dan auch einteilen." + + Warum nicht einfach ein Feld `rolle` an der Tuer daneben? Weil damit + genau die Eigenschaft verloren ginge, die sie sicher macht: Sie KANN + nichts anderes, als einen Creator anzulegen. Sobald eine Abfrage + entscheidet, welche Rolle erlaubt ist, ist eine vergessene Abfrage ein + zweiter Zugang mit vollen Rechten. + + Also zwei Tueren, jede mit genau einer Aufgabe. Die Rolle steht auch + hier nicht im Aufruf. + + EINEN MANAGER LEGT MAN NICHT NEBENBEI AN: Er sieht danach jeden + Creator, den er betreut, und legt selbst welche an. Deshalb bleibt + diese Tuer Spicy Media und DogFather vorbehalten -- ein Manager kann + keinen zweiten Manager schaffen. */ +personenRouter.post("/workspace/api/manager-anlegen", gleicheHerkunft, (req, res) => { + try { + const person = sitzungLesen(req); + if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); + if (!siehtAlles(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + + const name = String(req.body?.name ?? "").trim(); + if (name.length < 2) return res.status(400).json({ fehler: "Name fehlt." }); + if (name.length > NAME_MAX) return res.status(400).json({ fehler: "Name ist zu lang." }); + const vorhanden = db().prepare( + "SELECT 1 FROM personen WHERE lower(name) = lower(?) AND aktiv = 1").get(name); + if (vorhanden) return res.status(409).json({ fehler: "Diesen Namen gibt es schon." }); + + const neu = personAnlegen(name, "manager", { ...person, ip: echteIp(req) }); + res.status(201).json({ id: neu.id, name: neu.name, rolle: neu.rolle, code: neu.code }); + } catch (fehler) { + console.error("[workspace] Manager anlegen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + /* Welche Scouts kann ich einem neuen Creator gleich mitgeben? Dieselbe Regel wie oben, nur lesend -- damit die Oberflaeche genau die Liste anbietet, die der Server auch annimmt. Zwei getrennte diff --git a/server/workspace-profil.js b/server/workspace-profil.js index a2e29eb4..b0ac16ba 100644 --- a/server/workspace-profil.js +++ b/server/workspace-profil.js @@ -14,7 +14,7 @@ import express from "express"; import { sichtbareCreatorIds, db, protokolliere, echteIp, sitzungLesen, darfCreator, betreuteIds, istLeitung, - istDogFather, + istDogFather, siehtAlles, } from "./workspace.js"; export const profilRouter = express.Router(); @@ -71,7 +71,7 @@ function darf(person, zielId) { Ein Manager behaelt volle Rechte an SEINEN Creators: darfCreator prueft die Zuteilung, und "voll" bleibt es dort auch -- er soll die Verwaltungsfelder seiner eigenen Leute pflegen koennen. */ - if (istDogFather(person)) return "voll"; + if (siehtAlles(person)) return "voll"; if (person.rolle === "creator") return person.id === zielId ? "eigen" : null; if (person.rolle === "manager") return darfCreator(person, zielId) ? "voll" : null; if (person.rolle === "scout") return darfCreator(person, zielId) ? "betreuend" : null; diff --git a/server/workspace-reports.js b/server/workspace-reports.js index ffbb593b..c305b50d 100644 --- a/server/workspace-reports.js +++ b/server/workspace-reports.js @@ -20,7 +20,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, sichtbareCreatorIds, + db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, siehtAlles, sichtbareCreatorIds, } from "./workspace.js"; import { BEREICHE } from "./workspace-bereiche.js"; @@ -69,7 +69,7 @@ function zielCreator(req) { bei ihm "alle MEINE". Vorher lieferte gewuenscht=null eine Auswertung ueber den gesamten Bestand -- die Zahlen darin stammten zum Teil von Creators, die ihm nie zugeteilt waren. (03.09.2026) */ - if (istDogFather(req.person)) return gewuenscht; + if (siehtAlles(req.person)) return gewuenscht; /* Scout: darf nur ueber die eigenen Creator einen Report ziehen. Ein fremder Wunsch wird nicht abgelehnt, sondern still auf den ersten diff --git a/server/workspace-schulung.js b/server/workspace-schulung.js index ea8a6199..27755094 100644 --- a/server/workspace-schulung.js +++ b/server/workspace-schulung.js @@ -44,7 +44,7 @@ import { import { join } from "node:path"; import { readFileSync, existsSync } from "node:fs"; import { - db, sitzungLesen, istLeitung, istDogFather, darfCreator, betreuteIds, + db, sitzungLesen, istLeitung, istDogFather, siehtAlles, darfCreator, betreuteIds, protokolliere, echteIp, DATEN_ORDNER, sichtbareCreatorIds, } from "./workspace.js"; @@ -406,7 +406,7 @@ schulungRouter.delete("/workspace/api/schulung/:id(\\d+)/bestaetigen", gleicheHe anzutasten. */ schulungRouter.delete("/workspace/api/schulung/:id(\\d+)", gleicheHerkunft, (req, res) => { - if (!istDogFather(req.person)) return res.status(403).json({ fehler: "nicht_erlaubt" }); + if (!siehtAlles(req.person)) return res.status(403).json({ fehler: "nicht_erlaubt" }); try { db().prepare("UPDATE schulungen SET aktiv = 0 WHERE id = ?").run(Number(req.params.id)); protokolliere("schulung_abgeschaltet", req.person, echteIp(req), `Schulung ${req.params.id}`); diff --git a/server/workspace-scouts.js b/server/workspace-scouts.js index e1ad4a91..47cad658 100644 --- a/server/workspace-scouts.js +++ b/server/workspace-scouts.js @@ -17,7 +17,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, pipelineIds, + db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, siehtAlles, pipelineIds, } from "./workspace.js"; export const scoutRouter = express.Router(); @@ -70,7 +70,7 @@ scoutRouter.use("/workspace/api/leads", angemeldet); dreimal abgeschrieben waere dreimal Gelegenheit, sie einmal falsch zu aendern. */ function sichtbar(person) { - if (istDogFather(person)) return { wo: "1=1", werte: [] }; + if (siehtAlles(person)) return { wo: "1=1", werte: [] }; const ids = pipelineIds(person); if (!ids.length) return { wo: "0=1", werte: [] }; return { wo: `l.scout_id IN (${ids.map(() => "?").join(",")})`, werte: ids }; diff --git a/server/workspace-suche.js b/server/workspace-suche.js index 72395b5f..dec0738f 100644 --- a/server/workspace-suche.js +++ b/server/workspace-suche.js @@ -24,7 +24,7 @@ import express from "express"; import { - db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds, + db, sitzungLesen, betreuteIds, istLeitung, istDogFather, siehtAlles, pipelineIds, sichtbareCreatorIds, } from "./workspace.js"; import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js"; @@ -198,8 +198,8 @@ sucheRouter.get("/workspace/api/suche", (req, res) => { Scouts -- dieselbe Regel wie in der Pipeline selbst, aus derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der mehr verraet als die Seite dahinter, waere ein Leck. */ - if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") { - const ids = istDogFather(person) ? null : pipelineIds(person); + if (siehtAlles(person) || person.rolle === "scout" || person.rolle === "manager") { + const ids = siehtAlles(person) ? null : pipelineIds(person); const nur = ids === null ? "1=1" : ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1"; const werte = ids === null ? [] : ids; diff --git a/server/workspace.js b/server/workspace.js index 731e0a8e..47fa1249 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -57,17 +57,19 @@ const COOKIE = "dfw_sitzung"; const SITZUNG_STUNDEN = 12; const VERSUCHE_MAX = 8; // pro IP const VERSUCHE_FENSTER_MIN = 10; -const ROLLEN = new Set(["admin", "manager", "scout", "creator"]); +const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator"]); -/* Die Reihenfolge, in der Rollen ueberall erscheinen: DogFather zuerst, - dann Manager, dann Scout, dann Creator. Steht hier einmal, damit keine - Liste eine eigene Reihenfolge erfindet. */ -export const ROLLEN_REIHE = ["admin", "manager", "scout", "creator"]; +/* 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"]; /* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch - (admin, creator, manager, scout) -- also fast genau falsch herum. */ + (admin, creator, manager, scout, spicy) -- also fast genau falsch + herum. */ export const ROLLEN_SORTIERUNG = - "CASE rolle WHEN 'admin' THEN 0 WHEN 'manager' THEN 1 WHEN 'scout' THEN 2 ELSE 3 END"; + "CASE rolle WHEN 'spicy' THEN 0 WHEN 'admin' THEN 1 WHEN 'manager' THEN 2" + + " WHEN 'scout' THEN 3 ELSE 4 END"; /* LEITUNG = DogFather und Manager. Ein Manager darf alles, was DogFather darf -- mit genau zwei Ausnahmen, die in @@ -75,10 +77,44 @@ export const ROLLEN_SORTIERUNG = Leitung veraendern. Sonst koennte er sich selbst zum DogFather machen oder den echten aussperren. "Nur DogFather hat alle endgueltigen Rechte" heisst genau das. */ -const LEITUNG = new Set(["admin", "manager"]); +const LEITUNG = new Set(["spicy", "admin", "manager"]); export const istLeitung = (person) => !!person && LEITUNG.has(person.rolle); export const istDogFather = (person) => !!person && person.rolle === "admin"; +/* ======================================================================= + SPICY MEDIA (07.09.2026) + + Wunsch Filipe: "eine neue rolle ... die den namen traegt, spicy media, + und soll die gleichen rechte haben wie dogfather ausser, bei + automationen sollen die nicht sehen und die kategorie personen und + zugaenge sollen die nur leute hinzufuegen koennen ... aber die sollen + meine privaten chats und so nicht sehen." + + ZWEI FRAGEN, DIE MAN AUSEINANDERHALTEN MUSS -- und genau daran haette + man diesen Umbau kaputtmachen koennen: + + WER SIEHT ALLES? -> siehtAlles() = DogFather ODER Spicy Media + WER ENTSCHEIDET? -> istDogFather() = nur DogFather + + Die erste Frage stellt sich bei Listen, Uebersichten und Auswertungen: + Spicy Media soll alle Manager, Scouts und Creator sehen. Die zweite + bei allem Endgueltigen: loeschen, Rollen aendern, Codes neu setzen, + Sicherungen, die KI abschalten. Da bleibt DogFather allein. + + Es waere viel weniger Arbeit gewesen, istDogFather() einfach um + "spicy" zu erweitern -- und genau das waere der Fehler: Spicy Media + koennte dann DogFather loeschen. Zwei Namen, zwei Bedeutungen, und an + jeder Stelle steht sichtbar, welche gemeint ist. + + DER CHAT BRAUCHTE NICHTS. Er haengt ausschliesslich an der + Teilnehmerliste (chat_teilnehmer) und kennt kein "das Management sieht + alles". Spicy Media sieht fremde Gespraeche also nicht, weil es dafuer + gar keinen Weg gibt -- nicht, weil eine Abfrage es verbietet. + ======================================================================= */ +export const istSpicy = (person) => !!person && person.rolle === "spicy"; +export const siehtAlles = (person) => !!person + && (person.rolle === "admin" || person.rolle === "spicy"); + /* Der Rollenschluessel bleibt "admin" -- er steckt in der CHECK-Regel der Datenbank, in jeder Sitzung und in jeder Rechteabfrage. Umbenannt wird nur, was man LIEST. Diese Zuordnung ist die einzige Stelle dafuer: @@ -89,6 +125,7 @@ export const istDogFather = (person) => !!person && person.rolle === "admin"; Rolle "admin" weiterhin "das Management". Gemeint ist dieselbe Rolle, die in der Oberflaeche "DogFather" heisst. */ export const ROLLEN_NAME = { + spicy: "Spicy Media", admin: "DogFather", manager: "Manager", scout: "Scout", @@ -120,7 +157,21 @@ let _dbFehler = null; waehrend laufender Schreibvorgaenge). ===================================================================== */ function umstellungen(d) { - const jetztStempel = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, ""); + /* MIT SEKUNDEN, nicht nur mit Minuten (07.09.2026). + + `VACUUM INTO` weigert sich, eine vorhandene Datei zu ueberschreiben + ("output file already exists") -- zu Recht, eine Sicherung darf + nichts wegwerfen. Der Stempel hatte aber nur Minutenaufloesung: + Zwei Umstellungen innerhalb derselben Minute wollten in dieselbe + Datei, die zweite scheiterte, und weil ohne Sicherung nicht + umgestellt wird, blieb sie einfach aus. + + Das ist genau die unangenehme Sorte Fehler: Es sieht nach "die + Umstellung lief" aus (die Datei liegt ja da) und war doch keine. + Gefunden von pruef-spicy, das den Server zweimal in derselben + Minute startet -- im Betrieb genuegt dafuer ein Neustart im + falschen Moment. */ + const jetztStempel = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, ""); /* ---- Zeitpunkt des Hochladens in der Bibliothek ---- Eine neue Spalte laesst SQLite anstandslos anhaengen -- anders als @@ -635,6 +686,131 @@ function umstellungen(d) { d.exec("PRAGMA foreign_keys = ON"); } } + + /* ===================================================================== + ROLLE "spicy" (Spicy Media) FREISCHALTEN — 07.09.2026 + + Wunsch Filipe: "ich will dass ueber dogfather auch eine kategorie, + eine neue rolle entsteht die den namen traegt, spicy media." + + --------------------------------------------------------------------- + DAS IST DER RISKANTESTE UMBAU IM GANZEN HAUS + + Eine Rolle ist ein erlaubter Wert in einer Spalte, und der steckt in + SQLite in einem CHECK. Ein CHECK laesst sich nicht aendern -- die + Tabelle muss neu gebaut werden. Bei `eintraege` war das schon + unangenehm; hier geht es um `personen`, und auf die zeigen + ZWEIUNDFUENFZIG Fremdschluessel aus einem Dutzend Tabellen. Jede + Sitzung, jede Aufgabe, jeder Termin, jede Datei haengt daran. + + DESHALB WIRD DER BAUPLAN NICHT ABGESCHRIEBEN, SONDERN GELESEN. + + Die beiden Umstellungen darueber schreiben ihre Spaltenliste von + Hand ab. Das ging gut, solange die Liste stimmte -- aber `personen` + hat seit damals SIEBEN Spalten dazubekommen (bild, ueber_mich, + tiktok, instagram, youtube, twitch und weitere, siehe den + Spalten-Nachtrag oben). Wer hier eine Liste abschreibt, verliert + beim naechsten Mal genau die Spalte, die jemand letzte Woche + ergaenzt hat -- samt allen Profilbildern. + + Also: Der vorhandene CREATE-Text wird aus sqlite_master geholt und + darin AUSSCHLIESSLICH die Rollenliste ersetzt. Alles andere -- jede + Spalte, jeder Typ, jede Vorgabe -- bleibt woertlich stehen. Und die + Kopierliste kommt aus PRAGMA table_info, also aus der Tabelle + selbst. + + WENN DIE ERSETZUNG NICHT GREIFT, WIRD NICHT UMGESTELLT. Ein + `replace`, das nichts findet, gibt den Text unveraendert zurueck -- + die Umstellung liefe dann durch, baute dieselbe Tabelle noch einmal + und meldete Erfolg. Genau die Sorte gruener Haken, die nichts + geprueft hat. Deshalb wird danach nachgesehen, ob 'spicy' wirklich + drinsteht. + ===================================================================== */ + const rollenPlan = d.prepare( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || ""; + /* GEPRUEFT WIRD DIE REGEL, NICHT DER TEXT (07.09.2026). + + Hier stand `rollenPlan.includes("'spicy'")`. Das ist eine Suche im + ganzen gespeicherten Bauplan -- und SQLite hebt den woertlich auf, + mitsamt allen Kommentaren darin. Ein erklaerender Satz mit dem Wort + 'spicy' genuegte, und die Umstellung hielt die Tabelle fuer schon + umgestellt, obwohl die CHECK-Regel noch die alte war. Beim Bauen + genau so passiert. + + Jetzt wird die Regel selbst herausgeschnitten und NUR darin gesucht. + Findet sich keine, gibt es nichts umzustellen. */ + const regel = rollenPlan.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || ""; + if (rollenPlan && regel && !regel.includes("'spicy'")) { + const sicherung = `${DB_PFAD}.vor-spicy-${jetztStempel}`; + try { + d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`); + console.log("[workspace] Sicherung vor der Umstellung:", sicherung); + } catch (fehler) { + console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message); + return; + } + + /* Nur die Rollenliste ersetzen -- und pruefen, dass es geklappt hat. */ + const neuerPlan = rollenPlan + /* `IF NOT EXISTS` MUSS MIT INS MUSTER (07.09.2026, gefunden von + pruef-spicy). SQLite hebt den CREATE-Text WOERTLICH auf -- die + Tabelle wurde mit `CREATE TABLE IF NOT EXISTS personen` angelegt, + also steht das auch in sqlite_master. Ohne diese drei Woerter im + Muster griff die Ersetzung nicht, der Bauplan blieb unveraendert, + die Schutzabfrage darunter schlug an -- und die Umstellung waere + auf dem echten Server NIE gelaufen. Sie haette es gesagt (das ist + der Wert der Abfrage), aber sie waere nie gelaufen. */ + .replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i, + "CREATE TABLE personen_neu") + .replace(/rolle\s+IN\s*\([^)]*\)/i, "rolle IN ('spicy','admin','manager','scout','creator')"); + if (!neuerPlan.includes("'spicy'") || !neuerPlan.includes("personen_neu")) { + console.error("[workspace] Umstellung abgebrochen: Der Bauplan liess sich nicht " + + "umschreiben. Steht die CHECK-Regel noch so da wie erwartet?"); + return; + } + + /* Die Spaltenliste kommt aus der Tabelle, nicht aus dem Gedaechtnis. */ + const spalten = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name); + if (!spalten.length) { + console.error("[workspace] Umstellung abgebrochen: keine Spalten gefunden."); + return; + } + const liste = spalten.map((n) => `"${n}"`).join(", "); + + d.exec("PRAGMA foreign_keys = OFF"); + try { + const vorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; + d.exec("BEGIN"); + d.exec(neuerPlan); + d.exec(`INSERT INTO personen_neu (${liste}) SELECT ${liste} FROM personen;`); + const nachher = d.prepare("SELECT COUNT(*) AS n FROM personen_neu").get().n; + /* Die Zaehlung steht INNERHALB der Transaktion -- stimmt sie nicht, + wird zurueckgerollt und die alte Tabelle bleibt unberuehrt. */ + if (nachher !== vorher) { + d.exec("ROLLBACK"); + console.error(`[workspace] Umstellung abgebrochen: ${vorher} Personen vorher, ` + + `${nachher} nachher. Sicherung: ${sicherung}`); + } else { + d.exec("DROP TABLE personen;"); + d.exec("ALTER TABLE personen_neu RENAME TO personen;"); + d.exec("COMMIT"); + const kaputt = d.prepare("PRAGMA foreign_key_check").all(); + if (kaputt.length) { + console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length, + "verwaiste Verweise. Sicherung liegt unter", sicherung); + } else { + console.log(`[workspace] Rolle 'spicy' freigeschaltet, ${nachher} Personen, ` + + `${spalten.length} Spalten uebernommen, Verweise geprueft.`); + } + } + } catch (fehler) { + try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ } + console.error("[workspace] Umstellung fehlgeschlagen:", fehler?.message, + "-- Sicherung:", sicherung); + } finally { + d.exec("PRAGMA foreign_keys = ON"); + } + } } export function db() { @@ -648,10 +824,23 @@ export function db() { PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; + /* ROLLENLISTE: 'spicy' steht hier mit drin, nicht nur in der + Umstellung (07.09.2026, gefunden von pruef-spicy). Vorher legte + eine frische Datenbank die alte Liste an, und die Umstellung + lief beim allerersten Start sofort hinterher -- Tabelle neu + bauen, Sicherung schreiben, umbenennen, fuer nichts. Ein + Bauplan, der sofort umgebaut werden muss, ist der falsche. + + KEIN KOMMENTAR INNERHALB DIESES CREATE-TEXTES. SQLite hebt ihn + woertlich in sqlite_master auf -- samt Kommentaren. Ein Satz mit + dem Wort 'spicy' darin haette bedeutet, dass die Umstellung die + Tabelle fuer bereits umgestellt haelt, obwohl die CHECK-Regel + noch die alte ist. Genau das ist beim Bauen passiert: Der + Bauplan enthielt das Wort, die Regel nicht. */ CREATE TABLE IF NOT EXISTS personen ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - rolle TEXT NOT NULL CHECK (rolle IN ('admin','manager','scout','creator')), + rolle TEXT NOT NULL CHECK (rolle IN ('spicy','admin','manager','scout','creator')), code_hash TEXT NOT NULL, code_salt TEXT NOT NULL, code_n INTEGER NOT NULL, @@ -1456,27 +1645,49 @@ const GESCHUETZT = { Lesezeichen hat, waere weiterhin hineingekommen. Beides gehoert zusammen: bereiche.js zeigt sie nur noch DogFather, diese Zeile laesst nur ihn hinein. */ - "/workspace/leistung.html": ["admin"], + "/workspace/leistung.html": ["spicy", "admin"], /* NUR DogFather (02.09.2026). Die Rollenauswahl verspricht das seit jeher ("Manager -- dieselben Rechte, ausser der Personenverwaltung"), die Schranke hielt sich nur nicht daran. Wunsch: "die manager sollen diese kategorien garnicht sehen". */ - "/workspace/personen.html": ["admin"], - "/workspace/profil.html": ["admin", "manager", "scout", "creator"], + /* PERSONEN & ZUGAENGE: jetzt auch fuer Manager und Spicy Media + (07.09.2026). + + Filipe: "die manager sehen immer noch nicht die kachel personen & + zugangscode, wo sie dan die neuen creator hinzufuegen koennen ... + und die sollen in der kategorie wo die jetzt sehen werden auch noch + das mit den hinzufuegen sehen, alles andere auf der seite sollen die + weiterhin nicht sehen." + + DIE SEITE OEFFNET SICH, DIE SCHNITTSTELLEN NICHT. Das ist der ganze + Trick und der Grund, warum das hier ungefaehrlich ist: Alles unter + /workspace/api/verwaltung haengt weiterhin an `nurAdmin` -- Rollen + aendern, Codes neu setzen, sperren, loeschen, Protokoll lesen + bleiben zu und antworten mit 404. Wer die Seite oeffnet, bekommt + also genau die Teile zu sehen, fuer die es auch einen Weg gibt. + + Eine Seite ist kein Schutz, sie ist ein Weg. Der Schutz steht in den + Schnittstellen, und der ist unveraendert. */ + "/workspace/personen.html": ["spicy", "admin", "manager"], + "/workspace/profil.html": ["spicy", "admin", "manager", "scout", "creator"], /* Der eigene Steckbrief -- jede Rolle hat einen. Ein Creator wird dort nicht hingeschickt (er sieht seinen auf profil.html), darf die Seite aber aufrufen: Sie zeigt ihm dasselbe, nur ohne die Akte. */ - "/workspace/steckbrief.html": ["admin", "manager", "scout", "creator"], + "/workspace/steckbrief.html": ["spicy", "admin", "manager", "scout", "creator"], "/workspace/kalender.html": null, "/workspace/dateien.html": null, - "/workspace/bereich.html": ["admin", "manager", "scout", "creator"], - "/workspace/report.html": ["admin", "manager", "scout", "creator"], - "/workspace/scouting.html": ["admin", "manager", "scout"], + "/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator"], + "/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"], + "/workspace/scouting.html": ["spicy", "admin", "manager", "scout"], "/workspace/calls.html": null, "/workspace/startcheck.html": null, "/workspace/wissen.html": null, /* Ebenfalls nur DogFather: Was von allein passiert, bestimmt, was allen anderen zugeschoben wird. */ + /* AUTOMATIONEN BLEIBEN BEI DogFather (07.09.2026). Ausdruecklich: + "bei automationen sollen die nicht sehen." Dahinter liegen + Sicherungen, die KI und der Zustand des Servers -- das ist Betrieb, + nicht Betreuung. */ "/workspace/automation.html": ["admin"], }; @@ -1870,7 +2081,7 @@ export function scoutZuteilungSetzen(scoutId, managerId, akteur = null) { * null = alle (nur DogFather). */ export function sichtbareCreatorIds(person) { if (!person) return []; - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; if (person.rolle === "creator") return [person.id]; return betreuteIds(person); // Manager: eigene + die seiner Scouts } @@ -1883,7 +2094,7 @@ export function sichtbareCreatorIds(person) { * seine Scouts dazu -- er führt sie, er muss sie eintragen können. */ export function sichtbarePersonenIds(person) { if (!person) return []; - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; const creator = sichtbareCreatorIds(person) || []; const scouts = person.rolle === "manager" ? scoutsVon(person.id) : []; /* DOGFATHER IST FUER ALLE SICHTBAR (07.09.2026, Wunsch Filipe: @@ -1968,7 +2179,7 @@ export function betreuerIds(person) { * null = alle (nur DogFather), wie überall in dieser Datei. */ export function einladbareIds(person) { if (!person) return []; - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; const sichtbar = sichtbarePersonenIds(person) || []; /* DogFather gehört immer dazu: Er ist der Einzige, mit dem jede Rolle zu tun hat, und ihn nicht einladen zu können wäre der erste @@ -2001,7 +2212,7 @@ export function einladbareIds(person) { * null = alle (nur DogFather). */ export function schreibbareIds(person) { if (!person) return []; - if (istDogFather(person)) return null; + if (siehtAlles(person)) return null; const basis = new Set(einladbareIds(person) || []); @@ -2118,7 +2329,7 @@ export const externSql = (personSpalte, externSpalte) => spaeter auseinandergelaufen -- und dann haette eine Wiederholung jemandem etwas gezeigt, was der einzelne Termin ihm verbirgt. */ export function termineSichtbar(person, praefix = "t") { - if (istDogFather(person)) return { wo: "1=1", werte: [] }; + if (siehtAlles(person)) return { wo: "1=1", werte: [] }; const p = praefix; /* MITGEZAEHLT WIRD AUCH DIE TEILNEHMERLISTE (05.09.2026). @@ -2236,7 +2447,7 @@ export function darfCreator(person, creatorId) { Ein Manager faellt jetzt in dieselbe Zeile wie ein Scout -- die Kette "eigene plus die meiner Scouts" steckt in betreuteIds. */ - if (istDogFather(person)) return true; + if (siehtAlles(person)) return true; if (person.rolle === "creator") return person.id === Number(creatorId); return betreuteIds(person).includes(Number(creatorId)); } diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css index 87c93636..ca754260 100644 --- a/workspace/assets/css/gate.css +++ b/workspace/assets/css/gate.css @@ -1543,6 +1543,9 @@ body.gate { Scout Grün -- Schild Creator Blau -- Person Alle gedeckt, keine davon neon: augenschonend bleibt Vorgabe. */ + /* Spicy Media traegt das Rot der Marke -- dieselbe Farbe wie ihr Name + im Kopf der Karte darueber. */ + .tafel .rolle[data-rolle="spicy"] { --rton: #e9848c; } .tafel .rolle[data-rolle="admin"] { --rton: var(--k-gold); } .tafel .rolle[data-rolle="manager"] { --rton: var(--k-mittel); } .tafel .rolle[data-rolle="scout"] { --rton: var(--k-gut); } diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index 891fb23c..b8bd334f 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -301,3 +301,19 @@ .rollenwahl__knopf::after { border-color: CanvasText; } .rollenwahl__knopf[data-an="ja"]::after { background: Highlight; color: HighlightText; } } + +/* ===================================================================== + ZWEI AUSBAUSTUFEN DERSELBEN SEITE (07.09.2026) + + DogFather sieht diese Seite wie bisher. Manager und Spicy Media sehen + nur das Anlegen -- alles andere hier haengt am Server an `nurAdmin` + und antwortet ihnen mit 404. + + DAS AUSBLENDEN IST NICHT DIE SICHERUNG, sondern die Zusage, dass + niemand vor leeren Kaesten sitzt, die sich nie fuellen. Der Schutz + steht in den Schnittstellen; wuerde diese Regel fehlen, saehen sie + Ladefehler, aber keine Daten. + ===================================================================== */ +body[data-nur-anlegen="ja"] #liste, +body[data-nur-anlegen="ja"] .protokoll-block, +body[data-nur-anlegen="ja"] #rest-schalter { display: none; } diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js index b4f9aeb0..62bf53fd 100644 --- a/workspace/assets/js/bereiche.js +++ b/workspace/assets/js/bereiche.js @@ -310,22 +310,22 @@ unter: 'Was du sowieso jeden Tag aufmachst', bereiche: [ { name: 'Dashboard', unter: 'Alle Creator auf einen Blick', zeichen: 'dashboard', ton: 1, - gross: true, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'uebersicht.html', szene: 'showbuehne' }, + gross: true, rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'uebersicht.html', szene: 'showbuehne' }, { name: 'Aufgaben', unter: 'Offen, in Arbeit, Review', zeichen: 'aufgaben', ton: 13, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'aufgaben.html', szene: 'garage' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'aufgaben.html', szene: 'garage' }, { name: 'Kalender', unter: 'Termine und Calls', zeichen: 'kalender', ton: 8, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'kalender.html', szene: 'skyline' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'kalender.html', szene: 'skyline' }, { name: 'Calls & Protokolle', unter: 'Gespräche mit To-dos', zeichen: 'calls', ton: 15, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'calls.html', szene: 'lounge' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'calls.html', szene: 'lounge' }, /* CHAT (06.09.2026). Steht neben "Calls & Protokolle", weil beides Gespraech ist -- das eine geplant und protokolliert, das andere zwischendurch. Ton 14 (Gruen), weil die taegliche Gruppe sonst 3, 11, 16, 9 und 2 traegt und sich der Chat davon abheben soll. */ { name: 'Chat', unter: 'Nachrichten mit deinem Team', zeichen: 'chat', ton: 4, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'chat.html', szene: 'lounge' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'chat.html', szene: 'lounge' }, { name: 'Dateien', unter: 'Ablage und Freigaben', zeichen: 'dateien', ton: 19, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'dateien.html', szene: 'arena' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'dateien.html', szene: 'arena' }, ], }, { @@ -370,9 +370,9 @@ erst wer jemand IST, dann was er vorhat, dann was dabei herauskommt. Die Zahlen zuerst zu zeigen dreht das um. */ { name: 'Mein Steckbrief', unter: 'Dein Bild und deine Kanäle', zeichen: 'steckbrief', ton: 10, - rollen: ['admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' }, + rollen: ['spicy', 'admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' }, { name: 'Creator-Profile', unter: 'Ziele und 90-Tage-Plan', zeichen: 'profil', ton: 20, - rollen: ['admin', 'manager', 'scout'], ziel: 'profil.html', szene: 'halle' }, + rollen: ['spicy', 'admin', 'manager', 'scout'], ziel: 'profil.html', szene: 'halle' }, /* KEINE eigene Steckbrief-Kachel fuer Creator, und das ist Absicht: Ihr Steckbrief samt Profilbild steht bereits auf `profil.html` (der Block mit `data-nur-creator="ja"`). Eine @@ -389,19 +389,19 @@ wegzunehmen, ohne die Seite zu schliessen, waere nur ein versteckter Weg und keine Regel. */ { name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 2, - rollen: ['admin'], ziel: 'leistung.html', szene: 'arena' }, + rollen: ['spicy', 'admin'], ziel: 'leistung.html', szene: 'arena' }, { name: 'Start-Check', unter: 'Erstanalyse, 16 Punkte', zeichen: 'startcheck', ton: 16, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' }, { name: 'LIVE-Analyse', unter: 'Vorher, während, danach', zeichen: 'live', ton: 3, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=live', szene: 'portal' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=live', szene: 'portal' }, { name: 'Content-Ideen', unter: 'Idee bis Upload', zeichen: 'content', ton: 12, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'content.html', szene: 'portal' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'content.html', szene: 'portal' }, { name: 'Community', unter: 'Moderation und Konflikte', zeichen: 'community', ton: 6, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=community', szene: 'lounge' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=community', szene: 'lounge' }, { name: 'Technik', unter: 'Setup, Fehler, Lösungen', zeichen: 'technik', ton: 14, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=technik', szene: 'garage' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=technik', szene: 'garage' }, { name: 'Schutz & Regeln', unter: 'Richtlinien und Vorfälle', zeichen: 'schutz', ton: 5, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=schutz', szene: 'halle' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=schutz', szene: 'halle' }, /* DER SECHSTE BEREICH (06.09.2026). Kernprinzip 04 des Konzepts lautet "Die Agentur bleibt angebunden" -- und dafuer gab es bis heute nichts. Fuenf Bereiche standen, dieser fehlte. @@ -415,9 +415,9 @@ von pruef-start-ansicht. Ein Untertitel, der mitten im Wort endet, ist schlechter als ein kuerzerer. */ { name: 'Agentur', unter: 'Kampagnen und offizielle Wege', zeichen: 'agentur', ton: 17, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=agentur', szene: 'halle' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=agentur', szene: 'halle' }, { name: 'Reports & Review', unter: 'Woche und Entscheidung', zeichen: 'reports', ton: 7, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'report.html', szene: 'showbuehne' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'report.html', szene: 'showbuehne' }, ], }, { @@ -443,11 +443,17 @@ einzeln: Man weiss danach nicht mehr, welcher von beiden man glauben soll. */ { name: 'Personen & Zugänge', unter: 'Rollen und Codes', zeichen: 'personen', ton: 11, - rollen: ['admin'], ziel: 'personen.html', szene: 'halle' }, + /* MANAGER SEHEN DIESE KACHEL JETZT AUCH (07.09.2026). + Filipe: "die manager sehen immer noch nicht die kachel + personen & zugangscode, wo sie dan die neuen creator + hinzufuegen koennen." Die SEITE zeigt ihnen nur das + Anlegen -- Codes, Sperren, Loeschen und das Protokoll + haengen weiterhin an DogFather (siehe personen.js). */ + rollen: ['spicy', 'admin', 'manager'], ziel: 'personen.html', szene: 'halle' }, { name: 'Scout-Pipeline', unter: 'Entdeckt bis übergeben', zeichen: 'scouting', ton: 21, - rollen: ['admin', 'manager', 'scout'], ziel: 'scouting.html', szene: 'wald' }, + rollen: ['spicy', 'admin', 'manager', 'scout'], ziel: 'scouting.html', szene: 'wald' }, { name: 'Wissens-Bibliothek', unter: 'Anleitungen und PDFs', zeichen: 'wissen', ton: 9, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'wissen.html', szene: 'arena' }, + rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'wissen.html', szene: 'arena' }, /* Ebenfalls nur DogFather (02.09.2026). Was von allein passiert, legt fest, was ALLEN anderen automatisch zugeschoben wird -- das gehört in dieselbe Hand wie die Rollenvergabe. */ diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index 28b7877d..78374f58 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -15,10 +15,15 @@ /* Reihenfolge und Erklaerung an EINER Stelle. Sie gilt fuer die Rollenwahl im Formular und fuer alles, was sonst Rollen auflistet. */ const ROLLEN = [ + { wert: 'spicy', name: 'Spicy Media', symbol: 'chili', + text: 'Überblick über alle – ohne Automationen, ohne fremde Chats. Legt Leute an.' }, { wert: 'admin', name: 'DogFather', symbol: 'krone', - text: 'Alle Rechte, endgültig. Nur DogFather legt DogFather und Manager an.' }, + text: 'Alle Rechte, endgültig. Nur DogFather legt DogFather, Spicy Media und Manager an.' }, + /* 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. */ { wert: 'manager', name: 'Manager', symbol: 'stern', - text: 'Dieselben Rechte wie DogFather – außer der Personenverwaltung.' }, + text: 'Dieselben Rechte wie DogFather, ohne Personenverwaltung – legt aber Creator an.' }, { wert: 'scout', name: 'Scout', symbol: 'schutz', text: 'Eigene Pipeline und die Creator, die ihm zugeteilt sind.' }, { wert: 'creator', name: 'Creator', symbol: 'person', @@ -32,7 +37,19 @@ /* DogFather und Manager kann nur DogFather vergeben -- der Server lehnt es ohnehin ab, aber ein Knopf, der immer scheitert, gehoert gar nicht erst hin. */ - for (const r of ROLLEN) { + /* WER WELCHE ROLLE VERGEBEN DARF (07.09.2026). + + DogFather: alle. Spicy Media: Manager und Creator. Manager: nur + Creator. Der Server setzt dasselbe durch (drei getrennte Wege, + siehe workspace-personen.js) -- hier geht es nur darum, niemandem + einen Knopf hinzustellen, der eine Absage bringt. + + Aus derselben Liste wie die Beschreibungen, damit die beiden nicht + auseinanderlaufen. */ + const darf = ich?.rolle === 'admin' ? ['spicy', 'admin', 'manager', 'scout', 'creator'] + : ich?.rolle === 'spicy' ? ['manager', 'creator'] + : ['creator']; + for (const r of ROLLEN.filter((x) => darf.includes(x.wert))) { if ((r.wert === 'admin' || r.wert === 'manager') && ich.rolle !== 'admin') continue; const b = el('button', 'rollenwahl__knopf'); b.type = 'button'; @@ -627,10 +644,27 @@ const knopf = $('neu-speichern'); knopf.disabled = true; try { - const a = await hole('/workspace/api/verwaltung/personen', { + /* DREI TUEREN, EINE AUSWAHL (07.09.2026). + + DogFather geht durch die Verwaltungstuer und kann jede Rolle + vergeben. Manager und Spicy Media haben dort nichts zu suchen -- + fuer sie gibt es zwei enge Tueren, die je genau eine Rolle + anlegen und die Rolle gar nicht erst als Feld annehmen. + + Die Zuordnung steht hier und nicht in einer Abfrage im Server: + Der Server prueft ohnehin, WER durch welche Tuer darf. Diese + Zeilen sorgen nur dafuer, dass niemand gegen eine verschlossene + laeuft. */ + const rolle = gewaehlteRolle(); + const weg = ich.rolle === 'admin' ? '/workspace/api/verwaltung/personen' + : rolle === 'manager' ? '/workspace/api/manager-anlegen' + : '/workspace/api/creator-anlegen'; + const a = await hole(weg, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: $('f-name').value, rolle: gewaehlteRolle() }), + body: JSON.stringify(ich.rolle === 'admin' + ? { name: $('f-name').value, rolle } + : { name: $('f-name').value }), }); if (!a.ok) { $('neu-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.'; @@ -653,6 +687,43 @@ } catch { return; } if (!LEITUNG.has(ich.rolle)) { location.assign('/workspace/start.html'); return; } window.werZeigen?.(ich); + + /* =================================================================== + DIE SEITE HAT ZWEI AUSBAUSTUFEN (07.09.2026) + + Filipe: "die sollen in der kategorie wo die jetzt sehen werden + auch noch das mit den hinzufuegen sehen, alles andere auf der + seite sollen die weiterhin nicht sehen." + + DogFather bekommt die Seite wie bisher: Liste, Codes, Sperren, + Loeschen, Protokoll. Manager und Spicy Media bekommen genau EINEN + Teil davon -- das Anlegen. + + DAS VERSTECKEN IST NICHT DIE SICHERUNG. Alles, was hier + ausgeblendet wird, haengt am Server an `nurAdmin` und antwortet + mit 404. Wuerde diese Zeile fehlen, saehen sie leere Kaesten und + Fehlermeldungen -- aber keine Daten. Das Ausblenden erspart ihnen + nur den Anblick von Dingen, die es fuer sie nicht gibt. + + Und deshalb wird auch `laden()` NICHT aufgerufen: Es fragt die + Verwaltungsschnittstelle ab, und die ist zu. Eine Abfrage, von der + man weiss, dass sie 404 gibt, sollte man nicht stellen -- sie + fuellt nur das Serverlog. */ + const nurAnlegen = ich.rolle !== 'admin'; + if (nurAnlegen) { + document.body.dataset.nurAnlegen = 'ja'; + $('unterzeile') && ($('unterzeile').textContent = + 'Hier legst du neue Leute an. Codes, Sperren und das Protokoll bleiben bei DogFather.'); + baueRollenwahl(); + /* Das Formular steht offen da -- es ist der einzige Grund, warum + diese Seite fuer sie ueberhaupt aufgeht. Ein Knopf, der eine + leere Seite oeffnet, waere ein Klick ohne Zweck. */ + $('neu').hidden = false; + $('neu-oeffnen').hidden = true; + $('neu-abbrechen').hidden = true; + return; + } + baueRollenwahl(); await laden(); })(); diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index bc992a5a..120d5bdf 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -27,8 +27,12 @@ 'use strict'; const ROLLENTEXT = { + spicy: 'Spicy Media · Überblick über alle, ohne Automationen', admin: 'DogFather · Gesamtübersicht und Freigaben', - manager: 'Manager · dieselben Rechte, ohne Personenverwaltung', + /* Der Satz stimmte bis zum 07.09.2026. Seither legen Manager + Creator an -- und ein Rollentext, der etwas anderes verspricht als + die Oberflaeche kann, ist schlimmer als gar keiner. */ + manager: 'Manager · dieselben Rechte, ohne Personenverwaltung – legt Creator an', creator: 'Creator · dein eigener Bereich', scout: 'Scout · deine Pipeline und die Creator, die du betreust', }; diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index cd2a7ed8..0cdfa329 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -9,9 +9,9 @@ - - - + + + @@ -211,10 +211,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 09bb641f..c1f29f30 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -134,10 +134,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index b1d15922..e7a0401c 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -143,14 +143,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index b9e7fd42..36b9b1c7 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -76,11 +76,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 68a66706..dc6e5134 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -9,16 +9,16 @@ - - + + - - + + @@ -154,9 +154,9 @@ - - - - + + + + diff --git a/workspace/content.html b/workspace/content.html index d46dd303..f8627b9d 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -166,12 +166,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 2202d4a9..93babda8 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -95,10 +95,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index 08ec7513..349d1b9e 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -27,7 +27,7 @@ - + @@ -101,6 +101,22 @@

Womit meldest du dich an?

+ + + + @@ -123,10 +128,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 86367b68..14931a6b 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -201,11 +201,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index a409f8a6..34e08558 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -93,11 +93,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 1b898796..161d9a62 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -126,10 +126,10 @@
- - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 2560fdbb..7b8444d2 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -9,11 +9,11 @@ - - + + - + @@ -253,10 +253,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 7c9dfdb0..bec20140 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -72,11 +72,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index d3016b40..207d40ec 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -113,10 +113,10 @@ - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index eff7ff53..19f7b182 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -9,12 +9,12 @@ - - + + - - + + @@ -136,10 +136,10 @@ - + - - - + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 11738aef..c95f5014 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -177,10 +177,10 @@ - - - - - + + + + +