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 <[email protected]>
This commit is contained in:
2026-09-07 04:56:07 +02:00
co-authored by Claude Opus 5
parent 4bb5afc942
commit 68bc116c36
39 changed files with 983 additions and 247 deletions
+365
View File
@@ -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);
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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);
}
+2 -2
View File
@@ -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. */
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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] }
+40 -1
View File
@@ -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
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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}`);
+2 -2
View File
@@ -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 };
+3 -3
View File
@@ -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;
+234 -23
View File
@@ -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));
}
+3
View File
@@ -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); }
+16
View File
@@ -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; }
+26 -20
View File
@@ -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. */
+76 -5
View File
@@ -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();
})();
+5 -1
View File
@@ -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',
};
+8 -8
View File
@@ -9,9 +9,9 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
</head>
<body class="start">
@@ -211,10 +211,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/aufgaben.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/aufgaben.js?v=202609070453" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609070453" />
</head>
<body class="start">
@@ -134,10 +134,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/automation.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/automation.js?v=202609070453" defer></script>
</body>
</html>
+13 -13
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609070453" />
</head>
<body class="start">
@@ -143,14 +143,14 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/checkliste.js?v=202609070433" defer></script>
<script src="assets/js/ampel.js?v=202609070433" defer></script>
<script src="assets/js/schulung.js?v=202609070433" defer></script>
<script src="assets/js/vorlagen.js?v=202609070433" defer></script>
<script src="assets/js/bereich.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/checkliste.js?v=202609070453" defer></script>
<script src="assets/js/ampel.js?v=202609070453" defer></script>
<script src="assets/js/schulung.js?v=202609070453" defer></script>
<script src="assets/js/vorlagen.js?v=202609070453" defer></script>
<script src="assets/js/bereich.js?v=202609070453" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609070453" />
</head>
<body class="start">
@@ -76,11 +76,11 @@
</main>
<script src="assets/js/ki.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/calls.js?v=202609070433" defer></script>
<script src="assets/js/ki.js?v=202609070453" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/calls.js?v=202609070453" defer></script>
</body>
</html>
+8 -8
View File
@@ -9,16 +9,16 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609070453" />
</head>
<body class="start">
@@ -154,9 +154,9 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609070433"></script>
<script src="assets/js/kopf.js?v=202609070433"></script>
<script src="assets/js/glocke.js?v=202609070433"></script>
<script src="assets/js/chat.js?v=202609070433"></script>
<script src="assets/js/bereiche.js?v=202609070453"></script>
<script src="assets/js/kopf.js?v=202609070453"></script>
<script src="assets/js/glocke.js?v=202609070453"></script>
<script src="assets/js/chat.js?v=202609070453"></script>
</body>
</html>
+11 -11
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/content.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/content.css?v=202609070453" />
</head>
<body class="start">
@@ -166,12 +166,12 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/checkliste.js?v=202609070433" defer></script>
<script src="assets/js/vorlagen.js?v=202609070433" defer></script>
<script src="assets/js/content.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/checkliste.js?v=202609070453" defer></script>
<script src="assets/js/vorlagen.js?v=202609070453" defer></script>
<script src="assets/js/content.js?v=202609070453" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609070453" />
</head>
<body class="start">
@@ -95,10 +95,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/dateien.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/dateien.js?v=202609070453" defer></script>
</body>
</html>
+18 -2
View File
@@ -27,7 +27,7 @@
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
</head>
<body class="gate">
@@ -101,6 +101,22 @@
<div class="rollen" role="radiogroup" aria-labelledby="rollen-schild">
<p class="feldschild" id="rollen-schild">Womit meldest du dich an?</p>
<!-- SPICY MEDIA steht ganz oben (07.09.2026, Wunsch Filipe: "ueber
dogfather auch eine kategorie, eine neue rolle"). Der Chili ist
das Zeichen der Marke -- dieselbe Form wie im Logo, nur als
Umriss, damit sie neben Krone, Stern und Schild dieselbe
Strichstaerke hat. -->
<button type="button" class="rolle" data-rolle="spicy" role="radio" aria-checked="false">
<svg class="rolle__zeichen" viewBox="0 0 24 24" aria-hidden="true">
<path d="M13.4 4.6c1.5-.6 2.8.1 3 1.4"/>
<path d="M13.4 4.6c-.6 1.4-.3 2.6.6 3.4 3 .3 5 2.6 5 5.4 0 3.4-3 6-6.8 6-4.6 0-8.2-2.9-9.7-6.7 3.7.9 6.6-.4 8-3.1.6-1.2.4-2.4-.4-3.1"/>
</svg>
<span class="rolle__text">
<span class="rolle__name">Spicy Media</span>
<span class="rolle__zusatz">Überblick über alle</span>
</span>
</button>
<!-- Krone für DogFather: steht für die Gesamtübersicht und die
Freigaben. Das Schild ist zum Scout gewandert -- dort passt es
besser, weil ein Scout die Pipeline abschirmt und prüft, bevor
@@ -186,6 +202,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609070433" defer></script>
<script src="assets/js/gate.js?v=202609070453" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609070453" />
</head>
<body class="start">
@@ -257,11 +257,11 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/kalender.js?v=202609070433" defer></script>
<script src="assets/js/kalender-abo.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/kalender.js?v=202609070453" defer></script>
<script src="assets/js/kalender-abo.js?v=202609070453" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609070453" />
</head>
<body class="start">
@@ -211,10 +211,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609070433"></script>
<script src="assets/js/bereiche.js?v=202609070433"></script>
<script src="assets/js/kopf.js?v=202609070433"></script>
<script src="assets/js/glocke.js?v=202609070433"></script>
<script src="assets/js/leistung.js?v=202609070433"></script>
<script src="assets/js/wahl.js?v=202609070453"></script>
<script src="assets/js/bereiche.js?v=202609070453"></script>
<script src="assets/js/kopf.js?v=202609070453"></script>
<script src="assets/js/glocke.js?v=202609070453"></script>
<script src="assets/js/leistung.js?v=202609070453"></script>
</body>
</html>
+14 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609070453" />
</head>
<body class="start">
@@ -37,6 +37,11 @@
</header>
<svg style="display:none" aria-hidden="true"><defs>
<!-- Chili fuer Spicy Media -- dasselbe Zeichen wie auf der
Anmeldeseite. Zwei Rollen mit derselben Krone waeren genau der
Fehler, den die Kacheln schon einmal hatten: Man klickt zweimal
falsch, bevor man merkt, dass es zwei verschiedene sind. -->
<g id="r-chili"><path d="M13.4 4.6c1.5-.6 2.8.1 3 1.4"/><path d="M13.4 4.6c-.6 1.4-.3 2.6.6 3.4 3 .3 5 2.6 5 5.4 0 3.4-3 6-6.8 6-4.6 0-8.2-2.9-9.7-6.7 3.7.9 6.6-.4 8-3.1.6-1.2.4-2.4-.4-3.1"/></g>
<g id="r-krone"><path d="M4 17.6 5.4 7.4l4 3.4L12 5.2l2.6 5.6 4-3.4L20 17.6z"/><path d="M4.6 20.4h14.8"/></g>
<g id="r-stern"><path d="m12 3.4 2.6 5.6 6 .8-4.4 4.2 1.1 6-5.3-2.9-5.3 2.9 1.1-6L3.4 9.8l6-.8z"/></g>
<g id="r-schutz"><path d="M12 3.4 5.8 5.9v5c0 3.5 2.5 6.7 6.2 8 3.7-1.3 6.2-4.5 6.2-8v-5z"/><path d="m9.5 11.8 1.8 1.8 3.3-3.5"/></g>
@@ -123,10 +128,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/personen.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/personen.js?v=202609070453" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609070453" />
</head>
<body class="start">
@@ -201,11 +201,11 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/steckbrief.js?v=202609070433" defer></script>
<script src="assets/js/profil.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/steckbrief.js?v=202609070453" defer></script>
<script src="assets/js/profil.js?v=202609070453" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/report.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/report.css?v=202609070453" />
</head>
<body class="start">
@@ -93,11 +93,11 @@
</main>
<script src="assets/js/ki.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/report.js?v=202609070433" defer></script>
<script src="assets/js/ki.js?v=202609070453" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/report.js?v=202609070453" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609070453" />
</head>
<body class="start">
@@ -126,10 +126,10 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/scouting.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/scouting.js?v=202609070453" defer></script>
</body>
</html>
+8 -8
View File
@@ -9,11 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
<link rel="stylesheet" href="assets/css/heim.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/heim.css?v=202609070453" />
</head>
<body class="start">
@@ -253,10 +253,10 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/start.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/start.js?v=202609070453" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609070453" />
</head>
<body class="start">
@@ -72,11 +72,11 @@
</main>
<script src="assets/js/ki.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/startcheck.js?v=202609070433" defer></script>
<script src="assets/js/ki.js?v=202609070453" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/startcheck.js?v=202609070453" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609070453" />
</head>
<body class="start">
@@ -113,10 +113,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/steckbrief.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/steckbrief.js?v=202609070453" defer></script>
</body>
</html>
+8 -8
View File
@@ -9,12 +9,12 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609070453" />
</head>
<body class="start">
@@ -136,10 +136,10 @@
</main>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/uebersicht.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/uebersicht.js?v=202609070453" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609070433" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070453" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609070453" />
</head>
<body class="start">
@@ -177,10 +177,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070433" defer></script>
<script src="assets/js/bereiche.js?v=202609070433" defer></script>
<script src="assets/js/kopf.js?v=202609070433" defer></script>
<script src="assets/js/glocke.js?v=202609070433" defer></script>
<script src="assets/js/wissen.js?v=202609070433" defer></script>
<script src="assets/js/wahl.js?v=202609070453" defer></script>
<script src="assets/js/bereiche.js?v=202609070453" defer></script>
<script src="assets/js/kopf.js?v=202609070453" defer></script>
<script src="assets/js/glocke.js?v=202609070453" defer></script>
<script src="assets/js/wissen.js?v=202609070453" defer></script>
</body>
</html>