diff --git a/server/pruef-modi-ideen.mjs b/server/pruef-modi-ideen.mjs new file mode 100644 index 00000000..f1658849 --- /dev/null +++ b/server/pruef-modi-ideen.mjs @@ -0,0 +1,365 @@ +/* ===================================================================== + DAS IDEEN-BOARD (Kapitel 5.6 des Anforderungsdokuments) + + "Sammelstelle für Content-, Live- und Community-Ideen mit + Priorisierung." + + ZWEI GANZ VERSCHIEDENE FRAGEN, und die erste ist die gefaehrliche: + + 1. Kommt der neue Bereich auf einer BESTEHENDEN Datenbank + ueberhaupt an? Die erlaubten Bereiche stehen als CHECK-Regel in + der Tabelle, und SQLite kann eine CHECK-Regel nicht aendern -- + die Tabelle muss neu gebaut werden. Das ist der Moment, in dem + Daten verlorengehen. + 2. Sieht ihn nur, wer ihn sehen darf? + + DIE ERSTE WIRD HIER AUF EINER ECHTEN ALTEN TABELLE GEPRUEFT, nicht + auf einer frischen. Eine frische Datenbank bringt den Bereich schon + im Bauplan mit -- die Umstellung liefe gar nicht erst an, und die + Pruefung waere gruen, ohne das Riskante angesehen zu haben. Genau so + ein gruener Haken hat im Haus schon zweimal nichts bewiesen. + + Beim Bauen hat sich das gelohnt: Das Muster fuer den Tabellenkopf + stand in einem Template-Literal, JavaScript verschluckte den + Backslash, und die Umstellung haette STILLSCHWEIGEND nichts getan -- + kein Fehler, kein Hinweis, nur ein Bereich, den es nie gegeben + haette. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-ideen-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4365"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +let fehler = 0; +let geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +let offen = 0; +const unklar = (t) => { offen++; console.log(" -- " + t); }; + +/* ---------- Eine ALTE Datenbank bauen -------------------------------- */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const jetzt = new Date().toISOString(); + +console.log("\n=== 1. Eine bestehende Datenbank ohne den neuen Bereich ===\n"); +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + /* Der Bauplan von gestern -- sechs Bereiche, kein 'ideen'. Die + Spalten sind die echten; kaemen hier weniger, pruefte der Test + nicht, ob die Umstellung ALLE mitnimmt. */ + d.exec(` + CREATE TABLE eintraege ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bereich TEXT NOT NULL + CHECK (bereich IN ('live','content','technik','community','schutz','agentur')), + art TEXT NOT NULL, + titel TEXT NOT NULL, + text TEXT, + datum TEXT NOT NULL, + bewertung INTEGER CHECK (bewertung IS NULL OR (bewertung BETWEEN 1 AND 5)), + dringlichkeit TEXT NOT NULL DEFAULT 'mittel' + CHECK (dringlichkeit IN ('hoch','mittel','niedrig')), + status TEXT NOT NULL DEFAULT 'offen' + CHECK (status IN ('offen','erledigt')), + creator_id INTEGER, + erstellt TEXT NOT NULL, + erstellt_von INTEGER, + geaendert TEXT + );`); + for (let i = 1; i <= 7; i++) { + d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, erstellt) VALUES (?,?,?,?,?)") + .run("live", "vorbereitung", `Altbestand ${i}`, jetzt.slice(0, 10), jetzt); + } + const abgelehnt = (() => { + try { + d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, erstellt) VALUES (?,?,?,?,?)") + .run("ideen", "content", "geht noch nicht", jetzt.slice(0, 10), jetzt); + return false; + } catch { return true; } + })(); + /* DIE GEGENPROBE VORWEG: Der alte Stand muss den Bereich wirklich + ablehnen. Taete er es nicht, bewiese die Umstellung unten nichts. */ + ok(abgelehnt, "der alte Stand lehnt den Bereich 'ideen' wirklich ab"); + ok(d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n === 7, + "sieben Alt-Einträge liegen darin"); + d.close(); +} + +/* ---------- Jetzt erst den Server starten ---------------------------- */ +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-modi-ideen"); +await new Promise((r) => setTimeout(r, 900)); +const BASIS = "http://127.0.0.1:4365"; + +console.log("\n=== 2. Die Umstellung ===\n"); +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const plan = d.prepare( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'eintraege'").get()?.sql || ""; + const regel = plan.match(/bereich\s+IN\s*\(([^)]*)\)/i)?.[1] || ""; + ok(regel.includes("'ideen'"), `die Regel kennt den Bereich jetzt (${regel})`); + /* UND DIE ALTEN BEREICHE SIND NOCH DA. Eine Umstellung, die den neuen + hinzufuegt und dabei einen alten verliert, ist schlimmer als keine. */ + ok(["live", "content", "technik", "community", "schutz", "agentur"] + .every((b) => regel.includes(`'${b}'`)), "und alle sechs alten ebenfalls"); + ok(d.prepare("SELECT COUNT(*) AS n FROM eintraege").get().n === 7, + "alle sieben Alt-Einträge sind noch da"); + const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((z) => z.name); + ok(["bereich", "art", "titel", "text", "datum", "bewertung", "dringlichkeit", + "status", "creator_id", "erstellt", "erstellt_von", "geaendert"] + .every((sp) => spalten.includes(sp)), + `keine Spalte verloren (${spalten.length} Spalten)`); + ok(d.prepare("PRAGMA foreign_key_check").all().length === 0, "keine verwaisten Verweise"); + d.close(); +} + +/* ---------- Personen ------------------------------------------------- */ +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const schluessel = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", schluessel, jetzt); +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, + createHmac("sha256", schluessel).update(code).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Cigdem", "spicy", "CODE-SPIC-0001"); +anlegen("Schulle", "manager", "CODE-MAN-0001"); +anlegen("Tili", "creator", "CODE-ANNA-0001"); +const idModi1 = anlegen("Marina", "modi", "CODE-MODI-0001"); +const idModi2 = anlegen("Kim", "modi", "CODE-MODI-0002"); +d.close(); +void idModi1; + +async function anmelden(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((z) => z.split(";")[0]).join("; "); + return { status: a.status, keks }; +} +async function hole(keks, pfad, aufruf = {}) { + const a = await fetch(BASIS + pfad, { + ...aufruf, headers: { ...(aufruf.headers || {}), cookie: keks }, + }); + return { status: a.status, daten: await a.json().catch(() => null) }; +} +const jsonKopf = { "Content-Type": "application/json" }; + +/* ---------- 3. Sammeln und priorisieren ------------------------------ */ +console.log("\n=== 3. Ideen sammeln und nach Dringlichkeit ordnen ===\n"); +const modiAn = await anmelden("creator", "CODE-MODI-0001"); +const modi2An = await anmelden("creator", "CODE-MODI-0002"); +const dogiAn = await anmelden("admin", "CODE-DOGI-0001"); +{ + const IDEEN = [ + ["content", "Reihe: Die drei häufigsten Fragen im Chat", "hoch"], + ["live", "Zuschauer-Challenge am Freitag", "mittel"], + ["community", "Willkommens-Runde für Neue", "niedrig"], + ["sonstiges", "Kooperation mit der Nachbar-Community", "mittel"], + ]; + let angelegt = 0; + for (const [art, titel, dringlichkeit] of IDEEN) { + const a = await hole(modiAn.keks, "/workspace/api/bereich/ideen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art, titel, text: "aus dem Team", dringlichkeit }), + }); + if (a.status === 201 || a.status === 200) angelegt++; + else unklar(`"${titel}" ging nicht (${a.status}: ${JSON.stringify(a.daten)})`); + } + ok(angelegt === IDEEN.length, `${angelegt} von ${IDEEN.length} Ideen angelegt`); + + const b = await hole(modiAn.keks, "/workspace/api/bereich/ideen"); + const liste = b.daten?.eintraege || b.daten?.liste || []; + ok(liste.length === IDEEN.length, `er sieht sie alle wieder (${liste.length})`); + /* DIE PRIORISIERUNG IST DER PUNKT aus Kapitel 5.6 -- ohne sie waere es + eine Zettelsammlung. */ + const stufen = liste.map((z) => z.dringlichkeit).sort(); + ok(stufen.includes("hoch") && stufen.includes("mittel") && stufen.includes("niedrig"), + `mit Dringlichkeit (${stufen.join(", ")})`); + /* Und eine Idee gehoert der Runde, nicht einer Person. */ + ok(liste.every((z) => z.creator_id === null), + "keine Idee hängt an einer einzelnen Person"); +} + +/* Das Team teilt sich die Sammlung -- sonst waere es keine. */ +{ + const b = await hole(modi2An.keks, "/workspace/api/bereich/ideen"); + const liste = b.daten?.eintraege || b.daten?.liste || []; + ok(liste.length >= 4, `der zweite Modi sieht dieselben Ideen (${liste.length})`); +} +{ + const b = await hole(dogiAn.keks, "/workspace/api/bereich/ideen"); + const liste = b.daten?.eintraege || b.daten?.liste || []; + ok(liste.length >= 4, `DogFather ebenfalls (${liste.length})`); +} + +/* ---------- 4. Für alle anderen gibt es den Bereich nicht ------------ */ +console.log("\n=== 4. Und für alle anderen gibt es ihn nicht ===\n"); +for (const [name, kachel, code] of [ + ["Spicy Media", "spicy", "CODE-SPIC-0001"], + ["Manager", "manager", "CODE-MAN-0001"], + ["Creator", "creator", "CODE-ANNA-0001"], +]) { + const an = await anmelden(kachel, code); + const a = await hole(an.keks, "/workspace/api/bereich/ideen"); + const b = await hole(an.keks, "/workspace/api/bereich/gibtesnicht"); + /* WORTGLEICH mit einem erfundenen Bereich. Ein eigener Fehlertext + waere die Auskunft, dass es ihn gibt. */ + ok(a.status === 404 && a.status === b.status + && JSON.stringify(a.daten) === JSON.stringify(b.daten), + `${name.padEnd(12)} bekommt dieselbe Antwort wie für einen erfundenen ` + + `(${a.status} ${JSON.stringify(a.daten)})`); + /* Auch nicht schreiben -- eine Sperre, die nur beim Lesen gilt, ist + keine. */ + const c = await hole(an.keks, "/workspace/api/bereich/ideen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "content", titel: "reingeschmuggelt" }), + }); + ok(c.status === 404, `${name.padEnd(12)} kann dort auch nichts anlegen (${c.status})`); +} + +/* Und die Ideen tauchen auch nirgends sonst auf -- die Suche ist der + Weg, auf dem so etwas erfahrungsgemaess durchsickert. */ +{ + const an = await anmelden("spicy", "CODE-SPIC-0001"); + const a = await hole(an.keks, "/workspace/api/suche?q=Zuschauer-Challenge"); + if (a.status !== 200) { + unklar(`Suche für Spicy Media nicht abfragbar (${a.status})`); + } else { + /* NICHT DEN ROHEN TEXT DURCHSUCHEN. Beim ersten Anlauf stand hier + `!JSON.stringify(antwort).includes("Zuschauer-Challenge")` -- und + das schlug an, obwohl nichts gefunden wurde: Die Antwort spiegelt + den SUCHBEGRIFF im Feld `frage` zurueck. Die Pruefung fand ihre + eigene Frage wieder und meldete ein Leck, das es nicht gibt. + Beinahe haette ich daraufhin etwas repariert, das heil war. + + Gezaehlt wird deshalb, was die Suche wirklich gefunden hat. */ + ok(a.daten?.gesamt === 0 && (a.daten?.gruppen || []).length === 0, + `auch die Suche findet nichts (${a.daten?.gesamt} Treffer)`); + } + const b = await hole(dogiAn.keks, "/workspace/api/suche?q=Zuschauer-Challenge"); + if (b.status !== 200) { + unklar(`Suche für DogFather nicht abfragbar (${b.status})`); + } else { + /* GEGENPROBE: Dieselbe Suche muss die Idee bei DogFather SEHR WOHL + finden -- sonst waere der Haken darüber auch dann grün, wenn die + Suche gar nichts findet. */ + const treffer = (b.daten?.gruppen || []) + .flatMap((g) => g.treffer || g.eintraege || []) + .some((z) => String(z.titel || "").includes("Zuschauer-Challenge")); + ok(b.daten?.gesamt > 0 && treffer, + `bei DogFather findet dieselbe Suche sie sehr wohl (${b.daten?.gesamt} Treffer)`); + } +} + +/* ---------- 5. Im echten Browser ------------------------------------- */ +console.log("\n=== 5. Das Board im echten Browser ===\n"); +{ + const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await chromium.launch(); + try { + const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } }); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); }); + seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); + const schlecht = []; + seite.on("response", (r) => { + if (r.url().includes("/workspace/api/") && r.status() >= 400) { + schlecht.push(`${r.status()} ${r.url().replace(BASIS, "")}`); + } + }); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="creator"]'); + await seite.fill("#code", "CODE-MODI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + /* WARTEN, BIS DIE KACHELN DA SIND. Sie entstehen erst, wenn + /api/ich geantwortet hat -- direkt nach waitForURL ist die Seite + zwar geladen, aber leer. Beim ersten Anlauf meldete diese + Pruefung deshalb "keine Kachel", obwohl fuenf dastanden. */ + await seite.waitForSelector(".gruppe a[href]", { timeout: 15000 }); + + /* Erst die Kachel auf der Startseite -- ohne sie findet niemand den + Weg dorthin, und ein Bereich, den man nur ueber die Adresszeile + erreicht, ist keiner. */ + const kachel = await seite.evaluate(() => + [...document.querySelectorAll(".gruppe a[href]")] + .some((a) => a.getAttribute("href").includes("b=ideen"))); + ok(kachel, "die Kachel steht auf seiner Startseite"); + + schlecht.length = 0; + await seite.goto(BASIS + "/workspace/bereich.html?b=ideen", { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + const bild = await seite.evaluate(() => ({ + text: document.body.innerText, + ober: (document.getElementById("ober") || {}).textContent || "", + /* Die Dringlichkeit steht als Merkmal an der Karte und faerbt + ihre linke Kante. Sie IST die Priorisierung aus Kapitel 5.6 -- + ohne sie waere das Board eine Zettelsammlung. */ + reihenfolge: [...document.querySelectorAll("[data-dringlich]")] + .map((k) => k.dataset.dringlich), + })); + /* Die Adresse gehoert in die Bedingung: Bliebe eine Umleitung + unbemerkt, pruefte alles darunter die Startseite. Genau das ist + hier passiert -- der Server liess `modi` nicht auf bereich.html, + und die Pruefung sah eine Seite, die "Ideen-Board" enthielt (den + Text der KACHEL) und hielt sie fuer das Board. */ + ok(seite.url().includes("b=ideen"), + `die Seite bleibt stehen (${seite.url().replace(BASIS, "")})`); + ok(/Ideen/i.test(bild.text), "die Seite nennt sich Ideen-Board"); + /* Die vier Arten aus Kapitel 5.6 muessen zur Wahl stehen -- sonst + ist es eine Liste, keine Sammelstelle. */ + const arten = ["Content-Idee", "Live-Idee", "Community-Idee", "Sonstiges"]; + const gefunden = arten.filter((a) => bild.text.includes(a)); + ok(gefunden.length === arten.length, + `alle vier Arten stehen zur Wahl (${gefunden.join(", ") || "keine"})`); + ok(/Zuschauer-Challenge/.test(bild.text), "und die angelegten Ideen stehen darin"); + /* NACH DRINGLICHKEIT GEORDNET -- das Dringende zuerst. Eine + Sammlung ohne Ordnung waere genau der Zettelhaufen, den das Board + ersetzen soll. */ + const rang = { hoch: 0, mittel: 1, niedrig: 2 }; + const geordnet = bild.reihenfolge.every((d, i) => + i === 0 || rang[bild.reihenfolge[i - 1]] <= rang[d]); + ok(bild.reihenfolge.length === 4 && geordnet, + `nach Dringlichkeit geordnet (${bild.reihenfolge.join(" → ")})`); + /* Und das Wort ueber dem Titel passt: "Betreuung" waere hier falsch + -- eine Ideensammlung wird nicht ueber jemanden gefuehrt. */ + ok(bild.ober.trim() === "Sammelstelle" && !/Betreuung/i.test(bild.ober), + `die Oberzeile passt ("${bild.ober.trim()}")`); + ok(schlecht.length === 0, + schlecht.length ? `abgewiesene Anfragen: ${schlecht.join(" | ")}` : "keine Anfrage abgewiesen"); + ok(konsole.length === 0, + konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); + await seite.screenshot({ path: "pruef-modi-ideen.png", fullPage: false }); + console.log(" -- Bildschirmfoto: server/pruef-modi-ideen.png"); + await kontext.close(); + } finally { + await browser.close(); + } +} +console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- ` + + `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-rollen.mjs b/server/pruef-rollen.mjs index 018b0975..1505fd7d 100644 --- a/server/pruef-rollen.mjs +++ b/server/pruef-rollen.mjs @@ -252,6 +252,49 @@ for (const r of ROLLEN) { if (probleme.length) berichte.push({ rolle: r.name, pfad, probleme }); } + + /* ===================================================================== + JEDE KACHEL, DIE EINE ROLLE BEKOMMT, MUSS AUCH AUFGEHEN + (10.09.2026) + + Anlass: Ein Modi bekam zehn Kacheln, und FUENF davon leiteten + wortlos auf die Startseite zurueck -- der Server liess seine Rolle + nicht auf bereich.html und profil.html. Der Rundgang oben meldete + alle Seiten trotzdem als "ok", und zu Recht: Eine Umleitung ist + kein Fehler. Die Seite laedt, es gibt keinen roten Eintrag in der + Konsole, keine 4xx-Antwort. Sie ist nur eine andere. + + Das ist eine eigene Fehlerklasse: nicht "kaputt", sondern "fuehrt + woandershin". Sie faellt niemandem auf, der die Anwendung nicht + selbst benutzt -- und dem Benutzer faellt sie auf, weil ein Knopf + nichts tut. + + GEPRUEFT WIRD DIE ZUSAGE, NICHT DIE SEITENLISTE OBEN: Was eine + Rolle angeboten bekommt, steht auf ihrer Startseite. Genau das wird + hier durchgeklickt. Eine feste Liste daneben ginge am Punkt vorbei + -- sie koennte selbst veralten. + ===================================================================== */ + await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); + await seite.waitForSelector(".gruppe a[href]", { timeout: 15000 }).catch(() => {}); + const kacheln = await seite.evaluate(() => + [...document.querySelectorAll(".gruppe a[href]")] + .map((a) => a.getAttribute("href")) + .filter((h) => h && !h.startsWith("#"))); + /* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Faende die Suche keine Kachel -- + anderer Aufbau, anderer Klassenname --, waere unten alles gruen, + ohne dass ein einziger Knopf gedrueckt wurde. */ + ok(kacheln.length > 0, `${r.name.padEnd(18)} bekommt ${kacheln.length} Kacheln angeboten`); + for (const href of kacheln) { + const ziel = href.split("#")[0]; + const seitenname = ziel.split("?")[0]; + await seite.goto(BASIS + "/workspace/" + ziel, { waitUntil: "networkidle" }); + await seite.waitForTimeout(400); + const wo = seite.url().replace(BASIS + "/workspace/", ""); + ok(wo.split("?")[0] === seitenname, + `${r.name.padEnd(18)} Kachel ${ziel.padEnd(26)} ` + + (wo.split("?")[0] === seitenname ? "kommt an" : `LANDET AUF ${wo}`)); + } + await seite.screenshot({ path: `pruef-rollen-${r.rolle}.png`, fullPage: false }); await kontext.close(); } diff --git a/server/pruef-spicy.mjs b/server/pruef-spicy.mjs index 20201c56..15b12277 100644 --- a/server/pruef-spicy.mjs +++ b/server/pruef-spicy.mjs @@ -170,7 +170,14 @@ 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()), + /* DER WORTLAUT HAT SICH GEAENDERT (10.09.2026), die Sache nicht. + Die Umstellung gilt seither fuer JEDE Tabelle mit einer CHECK-Liste + (auch fuer eintraege.bereich), deshalb nennt die Meldung Tabelle + und Spalte statt "Rolle". Geprueft wird jetzt, was gemeint ist -- + dass 'spicy' freigeschaltet wurde --, und nicht der genaue Satz: + Sonst bricht diese Zeile beim naechsten Feinschliff wieder, ohne + dass irgendetwas kaputt waere. */ + ok(/'spicy'.*freigeschaltet/.test(ausgabe()), "die Umstellung meldet sich im Protokoll"); ok(/Sicherung vor der Umstellung/.test(ausgabe()), "und hat vorher eine Sicherung geschrieben"); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index c6fcf737..4131ebea 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -18,7 +18,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, externPruefen, externSql, - ohneModi, siehtModis, + ohneModi, siehtModis, MODI_BEREICHE_ERLAUBT, } from "./workspace.js"; export const bereicheRouter = express.Router(); @@ -120,6 +120,55 @@ export const BEREICHE = { Zwei Schalter, weil es zwei verschiedene Fragen sind: fuerAlle -- jeder sieht jeden Eintrag dieses Bereichs ohneCreatorBezug -- ein Eintrag gehoert hier NIEMANDEM einzeln */ + /* ===================================================================== + DAS IDEEN-BOARD (10.09.2026, Kapitel 5.6 des Anforderungsdokuments) + + "Sammelstelle fuer Content-, Live- und Community-Ideen mit + Priorisierung." Genau das steht hier -- und fast nichts davon + musste neu gebaut werden: `eintraege` hat Titel, Text, Status und + `dringlichkeit` (hoch/mittel/niedrig), und das IST die + Priorisierung. Eine eigene Tabelle daneben waere eine VIERTE + Sichtbarkeitsregel gewesen; genau deren Vervielfaeltigung hat heute + schon ein Leck verursacht. + + `ohneCreatorBezug`: Eine Idee gehoert der Runde, nicht einer + Person. Stuende ein Name daneben, laese sich der Eintrag wie ein + Auftrag an diese Person -- und niemand faende ihn spaeter wieder, + wenn sie nicht mehr da ist. + + KEIN `fuerAlle`. Das waere der naheliegende Griff gewesen (die + Agentur-Ablage darueber macht es so) und der falsche: `fuerAlle` + heisst woertlich JEDER, auch Manager, Scouts und Creator. Wer die + Sammlung sehen darf, entscheidet stattdessen dieselbe Regel wie + ueberall -- sichtbar() haengt die Modi-Bedingung an, und der Zugang + zur Seite haengt an siehtModis(). Zwei Wege, eine Antwort. + + DIE ARTEN SIND DIE DREI AUS DEM DOKUMENT, dazu "Sonstiges": Eine + Idee, die in keine der drei passt, soll nicht ungeschrieben + bleiben, weil das Formular sie nicht kennt. */ + ideen: { + name: "Ideen-Board", + /* DAS WORT UEBER DEM TITEL. Ueberall sonst steht dort "Betreuung", + weil die Bereichsseiten Betreuungsakten sind -- ueber jemanden + gefuehrt, von seinen Betreuern. Eine Ideensammlung ist das + Gegenteil: Sie gehoert niemandem und wird von allen gefuellt. + + Es stand fest in bereich.html und wurde nie gesetzt; aufgefallen + ist es auf dem Bildschirmfoto. Das Feld ist ab jetzt Teil der + Bereichsangaben, damit der naechste Bereich es einfach mitbringen + kann statt es zu erben. */ + ober: "Sammelstelle", + arten: { + content: "Content-Idee", + live: "Live-Idee", + community: "Community-Idee", + sonstiges: "Sonstiges", + }, + bewertung: false, + dringlichkeit: true, + ohneCreatorBezug: true, + }, + agentur: { name: "Agentur", arten: { @@ -166,6 +215,51 @@ function gleicheHerkunft(req, res, next) { bereicheRouter.use("/workspace/api/bereich", angemeldet); +/* ===================================================================== + DAS IDEEN-BOARD GIBT ES NUR FUER DIE, DIE ES SEHEN DUERFEN + (10.09.2026) + + 404 und nicht 403: "Kein Zugriff" waere die Auskunft, dass es den + Bereich gibt -- und wer nach "warum darf ich das nicht" fragt, fragt + als naechstes "wer denn dann". Ein Bereich, den es fuer mich nicht + gibt, wirft keine Fragen auf. Wortgleich mit der Antwort auf einen + erfundenen Bereichsnamen (siehe `if (!BEREICHE[bereich])` weiter + unten). + + ALS EIGENE HUERDE UND NICHT ALS ABFRAGE IN DEN VIER HANDLERN: Es gibt + Lesen, Anlegen, Aendern und Loeschen. Vier Abfragen sind vier + Gelegenheiten, eine zu vergessen -- und die vergessene faellt nicht + auf, weil an ihrer Stelle nichts steht. Der Pfad-Praefix greift auch + fuer die Wege mit einer Nummer dahinter. + + ZWEI SCHLOESSER, ABSICHTLICH: Hier haengt der ZUGANG zur Seite, + sichtbar() haengt die Bedingung an die ZEILEN. Faellt eines weg, + haelt das andere. */ +bereicheRouter.use("/workspace/api/bereich/ideen", (req, res, next) => { + if (siehtModis(req.person)) return next(); + return res.status(404).json({ fehler: "nicht_gefunden" }); +}); + +/* --------------------------------------------------------------------- + UND UMGEKEHRT: Ein Modi kommt nur in SEINE Bereiche. + + Seit dem 10.09.2026 darf er bereich.html oeffnen -- ohne diese Huerde + auch `?b=agentur`, die Ablage der Agentur, die ihn nichts angeht. Die + Kacheln bieten sie ihm nicht an; eine Regel, die nur im Formular + gilt, ist aber keine. + + Welche erlaubt sind, steht nicht hier, sondern wird aus seinen + Kacheln abgeleitet (MODI_BEREICHE_ERLAUBT). Zwei Listen waeren die + Stelle, an der es auseinanderlaeuft. + + 404 wie ueberall: Ein Bereich, den es fuer mich nicht gibt, wirft + keine Fragen auf. */ +bereicheRouter.use("/workspace/api/bereich/:bereich", (req, res, next) => { + if (req.person?.rolle !== "modi") return next(); + if (MODI_BEREICHE_ERLAUBT.has(String(req.params.bereich))) return next(); + return res.status(404).json({ fehler: "nicht_gefunden" }); +}); + /* ---------- Die Bereiche sind fuer Creator zum LESEN da ---------------- Wunsch Filipe, 31.08.2026: "die creator sollen da nur die sehen die wir diff --git a/server/workspace.js b/server/workspace.js index 85335df6..0c9f2d7f 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -281,6 +281,10 @@ const MODI_BEREICHE = [ name: "Dateien", unter: "Ablage und Freigaben", zeichen: "dateien", ton: 19, ziel: "dateien.html", szene: "arena" }, + { gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", + name: "Ideen-Board", unter: "Gesammelt und nach Dringlichkeit sortiert", + zeichen: "content", ton: 23, ziel: "bereich.html?b=ideen", szene: "portal" }, + { gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung", name: "Live-Ablauf", unter: "Checkliste für vorher, mittendrin und danach", zeichen: "live", ton: 3, ziel: "bereich.html?b=live", szene: "portal" }, @@ -292,8 +296,8 @@ const MODI_BEREICHE = [ ton: 14, ziel: "bereich.html?b=technik", szene: "garage" }, { gruppe: "Für dich", gruppeUnter: "Deine Seite im Team", - name: "Mein Profil", unter: "Deine Angaben und dein Bild", zeichen: "profil", - ton: 20, ziel: "profil.html", szene: "halle" }, + name: "Mein Steckbrief", unter: "Dein Bild und deine Kanäle", zeichen: "steckbrief", + ton: 10, ziel: "steckbrief.html", szene: "halle" }, { gruppe: "Für dich", gruppeUnter: "Deine Seite im Team", name: "Wissen", unter: "Nachschlagen statt nachfragen", zeichen: "wissen", ton: 9, ziel: "wissen.html", szene: "arena" }, @@ -312,6 +316,23 @@ export function bereicheFuer(person) { return person?.rolle === "modi" ? MODI_BEREICHE : null; } +/* WELCHE BEREICHSSEITEN EIN MODI OEFFNEN DARF -- abgeleitet aus seinen + eigenen Kacheln und nicht danebengeschrieben. + + Eine zweite Liste waere die Stelle, an der es auseinanderlaeuft: Wer + eine Kachel hinzufuegt und den Eintrag vergisst, baut einen Knopf, + der auf die Startseite zurueckwirft. Wer eine Kachel entfernt und den + Eintrag vergisst, laesst eine Tuer offen. So kann beides nicht + passieren. + + Gebraucht wird sie, weil `bereich.html` seit heute fuer ihn offen ist + -- ohne diese Einschraenkung kaeme er ueber die Adresszeile auch in + die Agentur-Ablage, die ihn nichts angeht. */ +export const MODI_BEREICHE_ERLAUBT = new Set( + MODI_BEREICHE + .map((k) => (/bereich\.html\?b=([a-z]+)/.exec(k.ziel) || [])[1]) + .filter(Boolean)); + /* Der Satz unter der Begruessung. Die fuenf bekannten Rollen haben ihn in assets/js/start.js stehen ("Creator · dein eigener Bereich"); fuer einen Modi darf er dort nicht stehen und kommt deshalb von hier. @@ -493,10 +514,19 @@ let _dbFehler = null; @param marker Die Rolle, an der erkannt wird, ob schon umgestellt ist. @param rollen Die vollstaendige neue Liste erlaubter Rollen. ===================================================================== */ -function rollenRegelUmstellen(d, marker, rollen, jetztStempel) { +function checkListeErweitern(d, tabelle, spalte, marker, werte, jetztStempel) { const rollenPlan = d.prepare( - "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || ""; - const regel = rollenPlan.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || ""; + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(tabelle)?.sql || ""; + /* DOPPELTE BACKSLASHES, und das ist kein Schoenheitsfehler: + In einem Template-Literal verschluckt JavaScript den einfachen + Backslash -- aus `\s` wird schlicht `s`, das Muster hiesse dann + "bereichs+INs*(...)" und traefe nie etwas. Die Umstellung haette + daraufhin STILLSCHWEIGEND nichts getan: keine Regel gefunden, + also nichts umzustellen, kein Fehler, kein Hinweis. Gemessen + statt ueberlegt -- der Unterschied war im Quelltext nicht zu + sehen. */ + const muster = new RegExp(`${spalte}\\s+IN\\s*\\(([^)]*)\\)`, "i"); + const regel = rollenPlan.match(muster)?.[1] || ""; /* Keine Regel gefunden heisst: Es gibt nichts umzustellen. Nicht etwa "dann bauen wir sie neu" -- eine Tabelle ohne CHECK ist ein Zustand, den jemand ansehen sollte, kein Fall fuer Automatik. */ @@ -522,19 +552,32 @@ function rollenRegelUmstellen(d, marker, rollen, jetztStempel) { 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. */ - const neuerPlan = rollenPlan - .replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i, - "CREATE TABLE personen_neu") - .replace(/rolle\s+IN\s*\([^)]*\)/i, - `rolle IN (${rollen.map((r) => `'${r}'`).join(",")})`); - if (!neuerPlan.includes(`'${marker}'`) || !neuerPlan.includes("personen_neu")) { + /* KEIN MUSTER FUER DEN TABELLENKOPF -- alles vor der ersten Klammer + IST der Kopf ("CREATE TABLE IF NOT EXISTS eintraege "). Hier stand + eine Regel mit Anfuehrungszeichen, Backticks und Backslashes in + einem Template-Literal, und genau daran ist sie gescheitert: + JavaScript verschluckt darin den einfachen Backslash, aus dem + Muster wurde "CREATE TABLEs+..." und es traf nie etwas. Die + Umstellung haette daraufhin STILLSCHWEIGEND nichts getan. + + Ein Schnitt an der ersten Klammer braucht keine Maskierung, ist in + einer Zeile zu lesen und kann nicht falsch escaped sein. */ + const klammer = rollenPlan.indexOf('('); + if (klammer < 0) { + console.error('[workspace] Umstellung abgebrochen: Bauplan ohne Klammer.'); + return; + } + const neuerPlan = (`CREATE TABLE ${tabelle}_neu ` + rollenPlan.slice(klammer)) + .replace(new RegExp(`${spalte}\\s+IN\\s*\\([^)]*\\)`, "i"), + `${spalte} IN (${werte.map((r) => `'${r}'`).join(",")})`); + if (!neuerPlan.includes(`'${marker}'`) || !neuerPlan.includes(`${tabelle}_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); + const spalten = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((z) => z.name); if (!spalten.length) { console.error("[workspace] Umstellung abgebrochen: keine Spalten gefunden."); return; @@ -546,28 +589,28 @@ function rollenRegelUmstellen(d, marker, rollen, jetztStempel) { Transaktion umschalten. */ d.exec("PRAGMA foreign_keys = OFF"); try { - const vorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; + const vorher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}`).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; + d.exec(`INSERT INTO ${tabelle}_neu (${liste}) SELECT ${liste} FROM ${tabelle};`); + const nachher = d.prepare(`SELECT COUNT(*) AS n FROM ${tabelle}_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, ` + console.error(`[workspace] Umstellung abgebrochen: ${vorher} Zeilen vorher, ` + `${nachher} nachher. Sicherung: ${sicherung}`); } else { - d.exec("DROP TABLE personen;"); - d.exec("ALTER TABLE personen_neu RENAME TO personen;"); + d.exec(`DROP TABLE ${tabelle};`); + d.exec(`ALTER TABLE ${tabelle}_neu RENAME TO ${tabelle};`); 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 '${marker}' freigeschaltet, ${nachher} Personen, ` - + `${spalten.length} Spalten uebernommen, Verweise geprueft.`); + console.log(`[workspace] '${marker}' in ${tabelle}.${spalte} freigeschaltet, ` + + `${nachher} Zeilen, ${spalten.length} Spalten uebernommen, Verweise geprueft.`); } } } catch (fehler) { @@ -1417,10 +1460,20 @@ function umstellungen(d) { rollenRegelUmstellen() -- einmal, nicht je Rolle abgeschrieben. Beide Aufrufe stehen hier: Auf einer Datenbank, die 'spicy' schon kennt, tut der erste nichts und nur der zweite laeuft. */ - rollenRegelUmstellen(d, "spicy", + checkListeErweitern(d, "personen", "rolle", "spicy", ["spicy", "admin", "manager", "scout", "creator"], jetztStempel); - rollenRegelUmstellen(d, "modi", + checkListeErweitern(d, "personen", "rolle", "modi", ["spicy", "admin", "manager", "scout", "creator", "modi"], jetztStempel); + + /* DER BEREICH FUER DAS IDEEN-BOARD (10.09.2026, Kapitel 5.6). + + Dieselbe Umstellung, andere Tabelle -- und genau deshalb steht sie + ab jetzt EINMAL da. Der alte agentur-Block weiter unten schreibt + den ganzen Bauplan von Hand ab; im Kommentar dort steht, dass dabei + schon einmal drei Spalten vergessen wurden. Eine vierte Abschrift + waere die vierte Gelegenheit dazu. */ + checkListeErweitern(d, "eintraege", "bereich", "ideen", + ["live", "content", "technik", "community", "schutz", "agentur", "ideen"], jetztStempel); } export function db() { @@ -1892,7 +1945,7 @@ export function db() { CREATE TABLE IF NOT EXISTS eintraege ( id INTEGER PRIMARY KEY AUTOINCREMENT, bereich TEXT NOT NULL - CHECK (bereich IN ('live','content','technik','community','schutz','agentur')), + CHECK (bereich IN ('live','content','technik','community','schutz','agentur','ideen')), art TEXT NOT NULL, titel TEXT NOT NULL, text TEXT, @@ -2334,7 +2387,18 @@ export const workspaceRouter = express.Router(); const GESCHUETZT = { "/workspace/start.html": null, "/workspace/uebersicht.html": null, - "/workspace/content.html": null, + /* AUSDRUECKLICH OHNE 'modi' (10.09.2026). Vorher stand hier `null` + ("jede angemeldete Rolle") -- und mit der neuen Rolle hiess das + auch: sie. Die Seite laedt dann zwar, ihre Schnittstelle antwortet + ihm aber mit 404, weil `content` nicht zu seinen Bereichen gehoert. + Eine Seite, die man oeffnen kann und die dann leer bleibt, sieht + aus wie ein Fehler -- und ist einer. + + Das ist die Kehrseite von "geschuetzt ist die Regel, nicht die + Ausnahme": Eine NEUE ROLLE erbt jedes `null` automatisch. Wer eine + Rolle hinzufuegt, muss diese Liste durchgehen -- der Kachel-Durchgang + in pruef-rollen findet die Faelle, die dabei auffallen. */ + "/workspace/content.html": ["spicy", "admin", "manager", "scout", "creator"], "/workspace/aufgaben.html": null, /* Chat (06.09.2026): jede Rolle. WEN man erreicht, entscheidet schreibbareIds() -- nicht der Zugang zur Seite. Ein Creator, der @@ -2388,10 +2452,28 @@ const GESCHUETZT = { /* 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": ["spicy", "admin", "manager", "scout", "creator"], + /* 'modi' kam am 10.09.2026 dazu. Der Steckbrief ist die EIGENE Seite + ("Dein Bild und deine Kanäle"); profil.html daneben ist der + Entwicklungsplan eines Creators und fuer ihn sinnlos. Der erste + Anlauf hatte genau das verwechselt -- die Kachel hiess "Mein + Profil" und fuehrte auf eine Seite, deren Schnittstelle mit 404 + antwortet, weil sie ausdruecklich nur Creator kennt. Gefunden vom + neuen Kachel-Durchgang in pruef-rollen. */ + "/workspace/steckbrief.html": ["spicy", "admin", "manager", "scout", "creator", "modi"], "/workspace/kalender.html": null, "/workspace/dateien.html": null, - "/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator"], + /* 'modi' kam am 10.09.2026 dazu -- und es war ein echter Fund: Ohne + diese Zeile leiteten VIER seiner Kacheln (Live-Ablauf, Community, + Technik, Ideen-Board) wortlos auf die Startseite zurueck. Der + Rollen-Rundgang meldete sie trotzdem als "ok": Eine Umleitung ist + kein Fehler, die Seite laedt ja -- nur eben eine andere. Seither + prueft pruef-rollen fuer JEDE Rolle, dass jede Kachel, die sie + bekommt, auch wirklich dort ankommt. + + WELCHE Bereiche ein Modi oeffnen darf, entscheidet die Liste + seiner Kacheln (MODI_BEREICHE_ERLAUBT) -- sonst kaeme er ueber die + Adresszeile auch in die Agentur-Ablage. */ + "/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator", "modi"], "/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"], "/workspace/scouting.html": ["spicy", "admin", "manager", "scout"], /* DIE ARBEITSLAGE DES TEAMS (09.09.2026). diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 4d46125f..aaed8da4 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609100119", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609100219", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609100119", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609100219", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100119", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100219", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100119", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100219", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index 595f03cf..35d36c78 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -942,6 +942,12 @@ body.start > main { position: relative; z-index: 1; } Leitung. Zwischen den kuehlen Toenen der Arbeitsbereiche faellt ein warmer auf, ohne lauter zu sein. */ [data-ton="22"] { --ton: #ffd166; } /* Team-Lage */ +/* Ton 23 (10.09.2026, Ideen-Board). NICHT nach Gefuehl gewaehlt: Die + 22 vorhandenen Toene sind dicht besetzt, deshalb wurde der Abstand zu + allen ausgerechnet. #5f8a9f hat mit 127 den zweitgroessten -- der + groesste war ein Graubraun, und eine graue Kachel zwischen bunten + sieht abgeschaltet aus. Gedeckt statt grell, wie alles hier. */ +[data-ton="23"] { --ton: #5f8a9f; } /* Ideen-Board */ /* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat. Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index 76abe447..c9dfdf2e 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -475,6 +475,11 @@ document.title = einstellung.name + ' · Creator Workspace'; $('marke-bereich').textContent = einstellung.name; $('titel').textContent = einstellung.name; + /* Das Wort ueber dem Titel. Es steht fest in bereich.html + ("Betreuung") und stimmte fuer jede Akte -- nicht aber fuer eine + Sammelstelle, die niemandem gehoert. Bringt ein Bereich ein + eigenes mit, gilt seines; sonst bleibt es wie bisher. */ + if (einstellung.ober && $('ober')) $('ober').textContent = einstellung.ober; $('unterzeile').textContent = UNTERZEILE[bereich] || ''; /* Fuer den Creator verschwindet alles Schreibende -- und die diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js index 1519b75f..6b4a18b8 100644 --- a/workspace/assets/js/bereiche.js +++ b/workspace/assets/js/bereiche.js @@ -346,6 +346,13 @@ rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'calls.html', szene: 'lounge' }, { name: 'Dateien', unter: 'Ablage und Freigaben', zeichen: 'dateien', ton: 19, rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'dateien.html', szene: 'arena' }, + /* Das Ideen-Board (Kapitel 5.6 des Anforderungsdokuments). + Nur DogFather -- es ist die Sammelstelle SEINES Teams. Wer + sonst die Adresse aufruft, bekommt einen 404 wie bei einem + Bereich, den es nicht gibt (siehe workspace-bereiche.js). */ + { name: 'Ideen-Board', unter: 'Gesammelt und nach Dringlichkeit sortiert', + zeichen: 'content', ton: 23, rollen: ['admin'], + ziel: 'bereich.html?b=ideen', szene: 'portal' }, ], }, { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 02598c58..993ee9d5 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,12 +12,12 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + @@ -245,10 +245,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index bedb2eb9..a9e9ec5b 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -140,10 +140,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index fa7e490a..a3ee0ac0 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -149,14 +149,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 81db71b7..520be077 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -82,11 +82,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index ff8058e7..f51a7523 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + @@ -247,10 +247,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index a1f309e1..c9740ff6 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -172,12 +172,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 94240fc0..327bfc08 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -101,10 +101,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index ef07cef5..3aa73469 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index cdc65df0..bc9b6615 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -370,11 +370,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 140b28e4..7ebbcd91 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -217,10 +217,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index bec02eda..fcac2681 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -335,10 +335,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 849d3429..487b120b 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -207,11 +207,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index 5bc9ebb5..1944848b 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -99,11 +99,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 9a089d75..f9fb3efd 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -132,10 +132,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index f91d4de0..3c6a0068 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,14 +12,14 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + @@ -667,10 +667,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 11c5a59f..c5fb0a19 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -78,11 +78,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 4748065c..6455e24f 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -119,10 +119,10 @@ - - - - - + + + + + diff --git a/workspace/team.html b/workspace/team.html index 796ba81a..672442b4 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -103,10 +103,10 @@ - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index e7e29d92..2eaeeafb 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,15 +12,15 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + @@ -142,11 +142,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 1eb19f21..4b3a3986 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -183,10 +183,10 @@ - - - - - + + + + +