diff --git a/server/pruef-modi-kategorien.mjs b/server/pruef-modi-kategorien.mjs new file mode 100644 index 00000000..b4430794 --- /dev/null +++ b/server/pruef-modi-kategorien.mjs @@ -0,0 +1,305 @@ +/* ===================================================================== + KATEGORIEN AN AUFGABEN -- aber nur bei den Modis + + Kapitel 6.1 des Anforderungsdokuments verlangt eine Kategorie an jeder + Aufgabe. Filipes Entscheidung dazu (10.09.2026): nur bei den Modis. + Fuer Creator, Scouts und Manager bleibt das Formular, wie es war. + + DREI FRAGEN, und die dritte wird gern vergessen: + + 1. Kann ein Modi eine Kategorie setzen und wiederfinden? + 2. Bekommt sie sonst niemand zu Gesicht? + 3. Was passiert, wenn jemand sie trotzdem SCHICKT? + + Die dritte entscheidet ueber die Verborgenheit. Eine Absage + ("Unbekannte Kategorie") waere die Auskunft, dass es das Feld gibt -- + also muss sie stillschweigend fallen. Wer sie benutzen darf, bekommt + bei einem Tippfehler dagegen sehr wohl eine Absage: Fuer ihn ist er + ein Fehler und kein Geheimnis. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-kat-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4361"; +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"; + +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-kategorien"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4361"; + +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); }; + +/* ---------- Personen ------------------------------------------------- */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); + +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; +} + +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idMan = anlegen("Schulle", "manager", "CODE-MAN-0001"); +const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001"); +const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001"); +const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)") + .run(idCreat, idScout, jetzt); +d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)") + .run(idScout, idMan, jetzt); +d.close(); +void idDogi; + +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) }; +} + +/* ---------- 1. Wer bekommt die Liste ueberhaupt? --------------------- */ +console.log("\n=== 1. Wer bekommt die Kategorien zu sehen? ===\n"); + +const modiAn = await anmelden("creator", "CODE-MODI-0001"); +const dogiAn = await anmelden("admin", "CODE-DOGI-0001"); +const manAn = await anmelden("manager", "CODE-MAN-0001"); +const creatAn = await anmelden("creator", "CODE-ANNA-0001"); + +let kategorien = []; +{ + const a = await hole(modiAn.keks, "/workspace/api/aufgaben"); + kategorien = a.daten?.kategorien || []; + /* Die ZAHL gehoert in die Bedingung: Eine leere Liste wuerde sonst + als "hat Kategorien bekommen" durchgehen. */ + ok(a.status === 200 && kategorien.length >= 6, + `der Modi bekommt ${kategorien.length} Kategorien`); + ok(kategorien.every((k) => k.wert && k.name), + "jede hat einen Schluessel und einen lesbaren Namen"); +} +{ + const a = await hole(dogiAn.keks, "/workspace/api/aufgaben"); + ok(a.status === 200 && (a.daten?.kategorien || []).length === kategorien.length, + "DogFather ebenso -- er legt den Modis Aufgaben an"); +} +for (const [name, an] of [["Manager", manAn], ["Creator", creatAn]]) { + const a = await hole(an.keks, "/workspace/api/aufgaben"); + const k = a.daten?.kategorien; + /* LEER und nicht "fehlt": Die Oberflaeche baut das Feld nur, wenn + etwas ankommt. Waere das Feld gar nicht da, faellt es auf dasselbe + hinaus -- aber eine leere Liste sagt ausdruecklich "nichts fuer + dich" statt "vergessen". */ + ok(a.status === 200 && Array.isArray(k) && k.length === 0, + `${name}: keine Kategorien (${JSON.stringify(k)})`); +} + +/* ---------- 2. Setzen, lesen, aendern, leeren ------------------------ */ +console.log("\n=== 2. Der Modi setzt eine Kategorie ===\n"); + +const jsonKopf = { "Content-Type": "application/json" }; +let aufgabeId = null; +{ + const a = await hole(modiAn.keks, "/workspace/api/aufgaben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ titel: "Clip vom Freitags-Live schneiden", kategorie: "clipping" }), + }); + aufgabeId = a.daten?.id ?? null; + ok(a.status === 201 && Number.isInteger(aufgabeId), `angelegt (${a.status})`); +} +if (aufgabeId) { + const a = await hole(modiAn.keks, "/workspace/api/aufgaben"); + const seine = (a.daten?.aufgaben || []).find((x) => x.id === aufgabeId); + ok(seine?.kategorie === "clipping", + `und steht mit ihrer Kategorie in der Liste (${seine?.kategorie})`); + + const b = await hole(modiAn.keks, `/workspace/api/aufgaben/${aufgabeId}`, { + method: "PATCH", headers: jsonKopf, body: JSON.stringify({ kategorie: "social" }), + }); + const c = await hole(modiAn.keks, "/workspace/api/aufgaben"); + ok(b.status === 200 + && (c.daten?.aufgaben || []).find((x) => x.id === aufgabeId)?.kategorie === "social", + "sie laesst sich aendern"); + + /* LEEREN MUSS GEHEN. Ohne diesen Weg bliebe eine einmal gesetzte + Kategorie fuer immer stehen -- und ein Feld, das man nicht + zuruecknehmen kann, ist eine Falle. */ + const e = await hole(modiAn.keks, `/workspace/api/aufgaben/${aufgabeId}`, { + method: "PATCH", headers: jsonKopf, body: JSON.stringify({ kategorie: "" }), + }); + const f = await hole(modiAn.keks, "/workspace/api/aufgaben"); + ok(e.status === 200 + && (f.daten?.aufgaben || []).find((x) => x.id === aufgabeId)?.kategorie === null, + "und wieder leeren"); +} + +/* ---------- 3. Ein Tippfehler ist ein Fehler ------------------------- */ +console.log("\n=== 3. Was bei einem falschen Wert passiert ===\n"); +{ + const a = await hole(modiAn.keks, "/workspace/api/aufgaben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ titel: "Irgendwas", kategorie: "gibtesnicht" }), + }); + ok(a.status === 400 && /Kategorie/i.test(a.daten?.fehler || ""), + `dem Modi wird es gesagt (${a.status} ${a.daten?.fehler || ""})`); +} + +/* ---------- 4. Und wenn ein Manager sie schickt? --------------------- */ +console.log("\n=== 4. Ein Manager schickt eine Kategorie mit ===\n"); +{ + /* DER HEIKELSTE FALL. Eine Absage waere die Auskunft, dass es das + Feld gibt. Also: Die Aufgabe entsteht ganz normal, der Wert faellt + lautlos weg. */ + const a = await hole(manAn.keks, "/workspace/api/aufgaben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ + titel: "Ganz normale Manager-Aufgabe", kategorie: "clipping", + verantwortlich_id: idMan, + }), + }); + ok(a.status === 201, `die Aufgabe entsteht ganz normal (${a.status})`); + if (a.daten?.id) { + const b = await hole(dogiAn.keks, "/workspace/api/aufgaben"); + const seine = (b.daten?.aufgaben || []).find((x) => x.id === a.daten.id); + ok(seine && seine.kategorie === null, + `aber ohne Kategorie (${JSON.stringify(seine?.kategorie)})`); + } else { + unklar("ohne Nummer laesst sich das Ergebnis nicht nachsehen"); + } +} + +/* ---------- 5. Im Browser -------------------------------------------- */ +console.log("\n=== 5. Das Feld im echten Formular ===\n"); +{ + const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await chromium.launch(); + try { + async function formular(kachel, code) { + const k = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + const s = await k.newPage(); + const konsole = []; + s.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); }); + s.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); + await s.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await s.click(`.rolle[data-rolle="${kachel}"]`); + await s.fill("#code", code); + await s.click("button[type=submit]"); + await s.waitForURL("**/start.html", { timeout: 15000 }); + await s.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" }); + await s.waitForTimeout(900); + await s.click("#neu-oeffnen").catch(() => {}); + await s.waitForTimeout(500); + return { kontext: k, seite: s, konsole }; + } + + /* Der Modi: Feld da, Auswahl gefuellt. */ + { + const { kontext, seite, konsole } = await formular("creator", "CODE-MODI-0001"); + const z = await seite.evaluate(() => { + const h = document.getElementById("feld-kategorie"); + const f = document.getElementById("f-kategorie"); + return { + sichtbar: !!h && !h.hidden, + auswahl: f ? [...f.options].map((o) => o.textContent) : [], + }; + }); + ok(z.sichtbar, "der Modi sieht das Feld"); + ok(z.auswahl.length >= 7, + `mit ${z.auswahl.length} Eintraegen (${z.auswahl.slice(1, 4).join(", ")} …)`); + /* KEIN CREATOR-VORLAGENBRETT. Es haelt 80 Aufgaben fuer den Aufbau + eines Kanals bereit -- fuer jemanden, der moderiert, ist davon + nichts gedacht. Gefunden auf dem Bildschirmfoto, nicht im Code: + Die Kachel laedt fehlerfrei, sie zeigt nur das Falsche. */ + const vorlagen = await seite.evaluate(() => { + const v = document.getElementById("vorlagenbrett"); + return { da: !!v && !v.hidden, text: document.body.innerText }; + }); + ok(!vorlagen.da, "und kein Creator-Vorlagenbrett"); + ok(!/vier Bereichen und vier Stufen/.test(vorlagen.text), + "auch nicht als Text auf der Seite"); + ok(konsole.length === 0, + konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); + await seite.screenshot({ path: "pruef-modi-kategorie.png", fullPage: false }); + await kontext.close(); + } + + /* Der Manager: kein Feld -- und kein Wort davon im Formular. */ + { + const { kontext, seite, konsole } = await formular("manager", "CODE-MAN-0001"); + const z = await seite.evaluate(() => { + const h = document.getElementById("feld-kategorie"); + const f = document.getElementById("f-kategorie"); + return { + sichtbar: !!h && !h.hidden, + /* Nur der Platzhalter darf dastehen. Waeren die Namen + eingesetzt und das Feld bloss verborgen, stuenden sie im + Seitenquelltext -- und waeren zu finden. */ + eintraege: f ? f.options.length : 0, + text: document.body.innerText, + }; + }); + ok(!z.sichtbar, "der Manager sieht das Feld nicht"); + ok(z.eintraege <= 1, + `und die Auswahl ist gar nicht erst gefuellt (${z.eintraege} Eintrag)`); + ok(!/Chat-Moderation|Clipping & Schnitt/.test(z.text), + "kein Kategoriename steht auf seiner Seite"); + ok(konsole.length === 0, + konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); + await kontext.close(); + } + + /* GEGENPROBE ZUM BROWSERTEIL: Findet die Suche die Namen ueberhaupt, + wenn sie dastehen? Ohne diesen Schritt bewiese der Haken oben + nichts -- er waere auch gruen, wenn innerText leer waere. */ + { + const { kontext, seite } = await formular("creator", "CODE-MODI-0001"); + const text = await seite.evaluate(() => document.body.innerText); + ok(/Clipping & Schnitt/.test(text), + "beim Modi findet dieselbe Suche den Namen sehr wohl"); + 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-modi-verborgen.mjs b/server/pruef-modi-verborgen.mjs index 5ec2ced9..add60da4 100644 --- a/server/pruef-modi-verborgen.mjs +++ b/server/pruef-modi-verborgen.mjs @@ -290,13 +290,116 @@ console.log("\n=== 5. Anlegen wie im Betrieb: DogFather legt einen Modi an ===\n `der Manager sieht ihn nicht (${namen.length} Personen: ${namen.join(", ")})`); } } -/* ---------- 6. Die Gegenprobe ---------------------------------------- +/* ---------- 6. Die Startseite eines Modis ---------------------------- */ +console.log("\n=== 6. Was ein Modi auf seiner Startseite sieht ===\n"); +{ + const an = await anmelden("creator", "CODE-MODI-0001"); + const { status, daten } = await hole(an.keks, "/workspace/api/ich"); + if (status !== 200) { + unklar(`/api/ich nicht lesbar (${status})`); + } else { + const b = daten?.bereiche; + ok(Array.isArray(b) && b.length > 0, + `der Server schickt ihm ${Array.isArray(b) ? b.length : "keine"} Kacheln`); + /* Nichts aus der Agentur. Nicht "moeglichst wenig", sondern keine + einzige -- diese Seiten drehen sich um betreute Creator, um + Rechte oder um Automationen. */ + const verboten = ["personen.html", "scouting.html", "automation.html", + "report.html", "calls.html", "uebersicht.html", "content.html"]; + const drin = (b || []).map((x) => x.ziel).filter((z) => verboten.includes(z)); + ok(Array.isArray(b) && drin.length === 0, + drin.length ? `ABER AUCH: ${drin.join(", ")} -- FEHLER` : "und nichts aus der Agentur"); + /* Die Beschriftung gehoert zum Empfaenger: "Alle Creator auf einen + Blick" waere fuer jemanden ohne Creator schlicht falsch. */ + ok((b || []).every((x) => x.name && x.unter && x.zeichen && x.ziel), + "jede Kachel hat Beschriftung, Zeichen und Ziel"); + } + + /* GEGENPROBE ZUR QUELLE: Fuer eine bekannte Rolle muss `null` + herauskommen -- nicht eine leere Liste. Die Oberflaeche + unterscheidet die beiden, und verwechselte man sie, haetten alle + anderen ab sofort eine leere Startseite. */ + const m = await anmelden("manager", "CODE-MAN-0001"); + const mi = await hole(m.keks, "/workspace/api/ich"); + ok(mi.status === 200 && mi.daten?.bereiche === null, + `beim Manager steht dort null, nicht [] (${JSON.stringify(mi.daten?.bereiche)})`); +} + +/* ---------- 7. Und im echten Browser? -------------------------------- */ +console.log("\n=== 7. Dieselbe Seite mit einem 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: 390, height: 844 } }); + 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)); + + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + /* Irgendeine Kachel -- fuer einen Modi gibt es keine eigene. */ + 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 }); + await seite.waitForTimeout(1200); + + const bild = await seite.evaluate(() => ({ + kacheln: [...document.querySelectorAll(".gruppe a[href]")].map((a) => a.getAttribute("href")), + gruppen: [...document.querySelectorAll(".gruppe__kopf")].map((k) => k.textContent.trim().slice(0, 40)), + rolle: (document.getElementById("zgruss-rolle") || {}).textContent || "", + rollentext: (document.getElementById("rollentext") || {}).textContent || "", + marke: (document.querySelector(".zmarke") || {}).textContent || "", + rauten: document.querySelectorAll(".zmarke .zraute").length, + })); + + /* Die ZAHL gehoert in die Bedingung, nicht nur in den Text: Eine + leere Seite wuerde sonst als "nichts Verbotenes dabei" durchgehen. */ + ok(bild.kacheln.length >= 8, + `${bild.kacheln.length} Kacheln stehen wirklich auf der Seite (${bild.gruppen.join(" | ")})`); + ok(bild.kacheln.some((h) => h.startsWith("aufgaben.html")), + "darunter das Aufgabenbrett -- das Herzstueck aus Kapitel 6"); + ok(!bild.kacheln.some((h) => /personen|scouting|automation|report/.test(h)), + "und nichts aus der Agentur"); + /* Der Anzeigename kommt aus /api/ich, nicht aus einer Datei im + Browser -- sonst stuende das Wort in einer Datei, die jeder + bekommt. Dass er hier steht, ist der Nachweis, dass der Weg geht. */ + ok(bild.rolle.trim() === "MODI", + `die Plakette sagt "${bild.rolle.trim()}" (kommt vom Server)`); + /* EIN SATZ, KEIN WORT. Die fuenf bekannten Rollen bekommen unter + der Begruessung eine Erklaerung ("Creator · dein eigener + Bereich"). Stuende dort nur "Modi", saehe die Seite nicht + verborgen aus, sondern unfertig -- und genau das stand da, bis + diese Pruefung es gemeldet hat. */ + ok(bild.rollentext.includes("·") && bild.rollentext.trim().length > 12, + `und die Zeile darunter ist ein ganzer Satz ("${bild.rollentext.trim()}")`); + /* DIE MARKE UEBER DEM TITEL. Ein Modi gehoert nicht zur Agentur -- + stand dort "Spicy Media", war die Seite fuer ihn schlicht falsch + beschriftet. Gefunden auf dem Bildschirmfoto, nicht im Code. */ + ok(bild.marke.includes("Team Dogi") && !bild.marke.includes("Spicy"), + `die Zierzeile nennt seinen Betrieb ("${bild.marke.trim()}")`); + /* Und die beiden Zierrauten sind noch da: Sie beim Austausch des + Textes zu verlieren waere lautlos passiert. */ + ok(bild.rauten === 2, `die beiden Zierrauten stehen noch (${bild.rauten})`); + ok(konsole.length === 0, + konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); + + await seite.screenshot({ path: "pruef-modi-start.png", fullPage: true }); + console.log(" -- Bildschirmfoto: server/pruef-modi-start.png"); + await kontext.close(); + } finally { + await browser.close(); + } +} +/* ---------- 8. Die Gegenprobe ---------------------------------------- Sie steht ABSICHTLICH GANZ UNTEN: Sie haengt eine Person absichtlich aus der Regel aus. Lief sie weiter oben, tauchte diese Person in jeder Liste DANACH auf -- und jeder spaetere Abschnitt maesse einen Zustand, den es im Betrieb nicht gibt. -------------------------------------------------------------------- */ -console.log("\n=== 6. Gegenprobe: kann diese Pruefung ueberhaupt anschlagen? ===\n"); +console.log("\n=== 8. Gegenprobe: kann diese Pruefung ueberhaupt anschlagen? ===\n"); { const d2 = new DatabaseSync(process.env.WORKSPACE_DB); d2.prepare("UPDATE personen SET rolle = 'creator' WHERE id = ?").run(idModi1); diff --git a/server/pruef-rollen.mjs b/server/pruef-rollen.mjs index 5871316e..018b0975 100644 --- a/server/pruef-rollen.mjs +++ b/server/pruef-rollen.mjs @@ -63,17 +63,30 @@ const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); machen selten Fehler. Jede Rolle bekommt deshalb etwas zu sehen. */ const { DatabaseSync } = await import("node:sqlite"); -const { scryptSync, randomBytes } = await import("node:crypto"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); /* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. */ const tag = (v) => tagLokal(v); +/* DER SUCHSCHLUESSEL ZUM CODE (09.09.2026). + + Ohne ihn kaeme ein Modi nicht herein: Er hat keine Kachel, der stille + Zugang findet ihn allein ueber diesen Wert. Er muss dastehen, BEVOR + die erste Person angelegt wird -- die Personen entstehen hier von + Hand und nicht ueber personAnlegen(), das ihn sonst selbst schreibt. */ +const kennungSchluessel = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", kennungSchluessel, jetzt); + function person(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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") - .run(name, rolle, hash, salt, 32768, jetzt); + const kennung = createHmac("sha256", kennungSchluessel).update(code).digest("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, kennung, jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = person("Filipe", "admin", "CODE-DOGI-0001"); @@ -83,6 +96,10 @@ const idTili = person("Tili", "creator", "CODE-TILI-0001"); /* Zwei Neue ohne jede Zuteilung -- der Zustand am ersten Tag. */ person("Neu-Scout", "scout", "CODE-NEU-000001"); person("Neu-Manager", "manager", "CODE-NEUM-00001"); +/* Ein Modi -- verborgen fuer alle ausser der DogFather-Rolle und den + anderen Modis. Er laeuft hier ueber JEDE Seite, denn "verborgen" + heisst nicht "darf kaputt sein": Was er sieht, muss auch gehen. */ +person("Marina", "modi", "CODE-MODI-0001"); /* Cigdem betreut Tili, Ben ebenfalls -- so hat jede betreuende Rolle etwas zu sehen. Genau diese Zuteilung fehlte im echten Bestand, und @@ -138,6 +155,14 @@ const ROLLEN = [ { rolle: "creator", code: "CODE-TILI-0001", name: "Creator (Tili)" }, { rolle: "scout", code: "CODE-NEU-000001", name: "Scout (OHNE Creator)" }, { rolle: "manager", code: "CODE-NEUM-00001", name: "Manager (OHNE Creator)" }, + /* DER SIEBTE DURCHGANG: ein Modi (09.09.2026). + + `rolle` ist hier die KACHEL, auf die geklickt wird -- nicht die + Rolle, die dabei herauskommt. Fuer einen Modi gibt es keine Kachel; + er tippt auf irgendeine vorhandene, und der Code entscheidet. Dass + ausgerechnet "creator" dasteht, ist Absicht und beweist nebenbei, + dass die Kachel wirklich gleichgueltig ist. */ + { rolle: "creator", code: "CODE-MODI-0001", name: "Modi (Marina)" }, ]; const { chromium } = await import( diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index edf8fd92..7468ff5d 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -176,7 +176,25 @@ for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) { console.log(" Gruppen:", r.gruppen.map((g) => `${g.name} (${g.kacheln})`).join(" · ")); console.log(" Zahlen: ", r.kacheln.filter((k) => k.zahl).map((k) => `${k.name}=${k.zahl}${k.warn ? "!" : ""}`).join(" · ")); - ok(r.gruppen.length === 3, `drei Gruppen (${r.gruppen.length})`); + /* VIER, nicht drei (berichtigt 10.09.2026). + + Die Erwartung stand auf drei, seit b45de94 eine vierte Gruppe + hinzugefuegt hat ("Rund um das Team -- die Arbeitslage von Managern + und Scouts"). Die Anwendung war richtig, die Zahl hier alt -- und + diese Pruefung damit rot, ohne dass es jemandem aufgefallen ist. + + Gefunden hat sie nicht ein Gesamtlauf, sondern die Frage "was kann + an meiner Aenderung kaputtgehen?" -- und danach die Gegenprobe auf + dem unveraenderten Stand, die zeigte: schon vorher rot, nicht von + mir. + + GEPRUEFT WERDEN JETZT DIE NAMEN, nicht nur die Anzahl. Vier Gruppen + koennten auch drei richtige und eine doppelte sein, und dann stuende + hier wieder eine gruene Zahl ueber einem falschen Bildschirm. */ + const ERWARTETE_GRUPPEN = ["Täglich", "Rund um den Creator", + "Rund um das Team", "Team & System"]; + ok(JSON.stringify(r.gruppen.map((g) => g.name)) === JSON.stringify(ERWARTETE_GRUPPEN), + `vier Gruppen in dieser Reihenfolge (${r.gruppen.map((g) => g.name).join(" · ")})`); /* WIE VIELE KACHELN? Nicht abgeschrieben, sondern aus bereiche.js gezaehlt (06.09.2026). diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 45e7e6af..54472efc 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -12,6 +12,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, + MODI_KATEGORIEN, kategorienFuer, } from "./workspace.js"; export const aufgabenRouter = express.Router(); @@ -99,6 +100,46 @@ export function sichtbar(person) { ? { wo: `(${eigen} OR ${b.wo})`, werte: [person.id, person.id, ...b.werte] } : { wo: eigen, werte: [person.id, person.id] }; } + /* ================================================================= + MODI (10.09.2026) -- UND FAST WAERE DERSELBE FEHLER PASSIERT WIE + AM 01.09.2026 BEIM MANAGER. + + Er stand hier nicht, fiel in den Default und sah damit auf dem + Aufgabenbrett GAR NICHTS -- eigene Aufgaben eingeschlossen. Und + wie der Kommentar oben schon sagt: Ein leeres Brett sieht aus wie + "nichts zu tun", nicht wie ein Fehler. Der Rollen-Rundgang + (pruef-rollen) meldete fuer den Modi brav "aufgaben.html ok", + denn die Seite laedt ja, sie ist nur leer. Gefunden hat es erst + eine Pruefung, die eine Aufgabe ANLEGT und sie danach + wiederzufinden versucht. + + WAS ER SIEHT: die Aufgaben des ganzen Modi-Teams. Filipes + Entscheidung vom 09.09.2026 -- "ja, sie sind untereinander ein + Team". Ohne das koennte niemand eine Schicht tauschen oder eine + Eskalation weiterreichen, ohne dass DogFather sie einzeln + durchstellt. + + AENDERN DARF ER TROTZDEM NUR SEINE EIGENEN. Das entscheidet + darfAendern() weiter unten, und es bleibt dabei: Sehen, um sich + abzustimmen, ist etwas anderes als in fremder Arbeit schreiben. + + WARUM DIE NUMMERN FRISCH GELESEN WERDEN und nicht einmal beim + Start: Kommt ein Modi dazu, muessen ihn die anderen sofort sehen. + Eine Liste, die beim Serverstart entsteht, waere bis zum naechsten + Neustart falsch -- und niemand wuesste, warum. */ + case "modi": { + const modis = db().prepare( + "SELECT id FROM personen WHERE rolle = 'modi'").all().map((z) => z.id); + /* Kann nicht leer sein -- diese Person IST ein Modi. Die Abfrage + faende sonst `IN ()`, und das ist in SQLite ein Syntaxfehler. */ + if (!modis.length) return { wo: "a.verantwortlich_id = ?", werte: [person.id] }; + const platz = modis.map(() => "?").join(","); + return { + wo: `(a.creator_id IN (${platz}) OR a.verantwortlich_id IN (${platz})` + + ` OR a.erstellt_von IN (${platz}))`, + werte: [...modis, ...modis, ...modis], + }; + } default: return { wo: "0=1", werte: [] }; // unbekannte Rolle sieht nichts } @@ -123,6 +164,13 @@ const SPALTEN = ` /* Die Vorlage, aus der sie entstanden ist (09.09.2026) -- damit das Vorlagenbrett zeigen kann, was schon geholt wurde. */ a.vorlage, + /* Die Kategorie (10.09.2026, Kapitel 6.1 des Anforderungsdokuments). + Sie kommt fuer JEDE Aufgabe mit -- wer sie nicht benutzen darf, + hat ohnehin keine Aufgabe mit einem Wert darin. Sie hier je nach + Rolle wegzulassen waere eine zweite Sichtbarkeitsregel neben der, + die es schon gibt, und damit eine zweite Gelegenheit, sie falsch + zu ziehen. */ + a.kategorie, pa.name AS abbruch_von_name`; const VERBUND = ` @@ -163,7 +211,15 @@ aufgabenRouter.get("/workspace/api/aufgaben", (req, res) => { CASE WHEN a.frist IS NULL THEN 1 ELSE 0 END, a.frist, CASE a.prioritaet WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END, a.id DESC`).all(...werte, heuteLokal()); - res.json({ aufgaben: reihen }); + res.json({ + aufgaben: reihen, + /* Die Kategorien kommen mit der Liste, nicht ueber einen eigenen + Aufruf: Das Brett braucht beides im selben Augenblick, und ein + zweiter Aufruf waere ein zweiter Weg, der einzeln scheitern + kann. Wer sie nicht benutzen darf, bekommt eine leere Liste -- + und die Oberflaeche zeigt dann gar kein Feld. */ + kategorien: kategorienFuer(req.person), + }); } catch (fehler) { console.error("[workspace] Aufgaben lesen:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); @@ -278,7 +334,9 @@ aufgabenRouter.get("/workspace/api/personen", (req, res) => { /* ---------- Anlegen und Ändern ------------------------------------------ */ -function pruefeFelder(körper, { neu }) { +const KATEGORIEN = new Set(MODI_KATEGORIEN.map((k) => k.wert)); + +function pruefeFelder(körper, { neu, person }) { const fehler = []; const aus = {}; @@ -316,6 +374,24 @@ function pruefeFelder(körper, { neu }) { else aus[feld] = zahl; } + /* DIE KATEGORIE (10.09.2026). + + STILLSCHWEIGEND FALLEN GELASSEN, NICHT ABGELEHNT, wenn jemand sie + schickt, der sie nicht benutzen darf. Eine Fehlermeldung + ("Unbekannte Kategorie") waere die Auskunft, dass es das Feld gibt + -- und genau die soll niemand bekommen. Wer sie benutzen darf, + bekommt bei einem falschen Wert dagegen sehr wohl eine Absage: + Fuer ihn ist ein Tippfehler ein Fehler und kein Geheimnis. + + Leer heisst ausdruecklich `null` und nicht "unveraendert" -- sonst + liesse sich eine einmal gesetzte Kategorie nie wieder entfernen. */ + if (körper.kategorie !== undefined && kategorienFuer(person).length) { + const k = String(körper.kategorie ?? "").trim(); + if (!k) aus.kategorie = null; + else if (!KATEGORIEN.has(k)) fehler.push("Unbekannte Kategorie."); + else aus.kategorie = k; + } + /* Frei eingetragene Namen statt einer Person -- eine Agentur, eine Marke, jemand ohne Konto. Sie schlagen die Auswahl: Wer tippt, meint das Getippte, und die Verknuepfung faellt weg. */ @@ -496,7 +572,7 @@ aufgabenRouter.delete("/workspace/api/notizen/:id", gleicheHerkunft, (req, res) aufgabenRouter.post("/workspace/api/aufgaben", gleicheHerkunft, (req, res) => { try { - const { aus, fehler } = pruefeFelder(req.body || {}, { neu: true }); + const { aus, fehler } = pruefeFelder(req.body || {}, { neu: true, person: req.person }); if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") }); /* Wer nicht Management ist, darf ausschließlich für sich selbst @@ -515,12 +591,12 @@ aufgabenRouter.post("/workspace/api/aufgaben", gleicheHerkunft, (req, res) => { const { lastInsertRowid } = db().prepare(` INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id, - creator_extern, verantwortlich_extern, frist, erstellt, erstellt_von) - VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run( + creator_extern, verantwortlich_extern, frist, kategorie, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`).run( aus.titel, aus.beschreibung ?? null, aus.status ?? "offen", aus.prioritaet ?? "mittel", aus.creator_id ?? null, aus.verantwortlich_id ?? null, aus.creator_extern ?? null, aus.verantwortlich_extern ?? null, - aus.frist ?? null, jetzt(), req.person.id); + aus.frist ?? null, aus.kategorie ?? null, jetzt(), req.person.id); protokolliere("aufgabe_angelegt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), @@ -547,7 +623,7 @@ aufgabenRouter.patch("/workspace/api/aufgaben/:id", gleicheHerkunft, (req, res) if (!aufgabe) return res.status(404).json({ fehler: "nicht_gefunden" }); if (!darfAendern(req.person, aufgabe)) return res.status(403).json({ fehler: "nicht_erlaubt" }); - const { aus, fehler } = pruefeFelder(req.body || {}, { neu: false }); + const { aus, fehler } = pruefeFelder(req.body || {}, { neu: false, person: req.person }); if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") }); /* Zuordnungen darf nur das Management verschieben. */ diff --git a/server/workspace-zentrale.js b/server/workspace-zentrale.js index 1cd97e7d..07f799be 100644 --- a/server/workspace-zentrale.js +++ b/server/workspace-zentrale.js @@ -247,7 +247,23 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { zahlen: [ { wert: meine.length, schild: "heute" }, { wert: [...mitTermin].filter((s) => s >= stundeJetzt).length, schild: "kommt noch" }, - { wert: betreuteIds(zeigt).length, schild: "betreut" }, + /* "BETREUT" IST FUER EINEN MODI IMMER NULL (10.09.2026). + + Er betreut keine Creator -- das ist die Arbeit der Scouts und + Manager. Auf seiner Startseite stand deshalb dauerhaft "0 + betreut": eine Zahl, die nie etwas anderes sagen kann, und + damit schlimmer als keine. Wer sie sieht, sucht nach dem + Fehler. + + Stattdessen zaehlt sie, wie viele im Modi-Team sind. Das ist + die Zahl, die an dieser Stelle etwas bedeutet -- und sie + bekommt nur zu sehen, wer ohnehin weiss, dass es die Runde + gibt. */ + zeigt.rolle === "modi" + ? { wert: db().prepare( + "SELECT COUNT(*) AS n FROM personen WHERE rolle = 'modi' AND aktiv = 1").get().n, + schild: "im Team" } + : { wert: betreuteIds(zeigt).length, schild: "betreut" }, ], }); } catch (fehler) { diff --git a/server/workspace.js b/server/workspace.js index 2492d224..fafcddcd 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -186,6 +186,173 @@ export const ROLLEN_NAME = { modi: "Modi", }; +/* ===================================================================== + DIE KACHELN EINES MODIS (10.09.2026) + + Wo die anderen fuenf Rollen ihre Kacheln aus assets/js/bereiche.js + bekommen, kommen sie fuer einen Modi von hier. Der Grund ist derselbe + wie beim Anzeigenamen und bei der Rollenauswahl: bereiche.js wird an + JEDEN ausgeliefert, der die Seite oeffnet. Stuende dort auch nur + `rollen: [... , 'modi']`, waere die Rolle mit einem Blick in den + Quelltext gefunden -- und damit alles verraten. + + NICHT NUR EINE LISTE VON ZIELEN, sondern die ganze Kachel. Wer nur + die Ziele schickte und die Beschriftung aus bereiche.js nehmen liesse, + bekaeme unter "Dashboard" den Untertitel "Alle Creator auf einen + Blick" -- fuer jemanden, der keine Creator betreut. Die Worte gehoeren + zum Empfaenger, nicht zum Ziel. + + ZEICHEN, TON UND SZENE SIND SCHLUESSEL AUS bereiche.js, keine neuen. + Sie sagen nichts ueber Modis aus (jede andere Kachel benutzt dieselben) + und muessen deshalb nicht verborgen werden. Neue zu erfinden waere + genau falsch herum: Sie muessten in bereiche.js stehen, und dort + faellt jede unbenutzte Zeile irgendwann jemandem auf. Die Toene sind + dieselben wie an den entsprechenden Kacheln der anderen Rollen -- ein + Ton kennzeichnet die Kachel, nicht ihren Platz. + + WAS NICHT DABEI IST, und warum: Uebersicht, Calls, Content, Reports, + Scouting, Personen und Automationen. Sie drehen sich um betreute + Creator, um die Agentur oder um Rechte -- fuer einen Modi waeren sie + leer oder nicht seine Sache. Der Rollen-Rundgang (pruef-rollen) zeigt, + dass sie ihn nicht abstuerzen lassen; das ist der Grund, sie + wegzulassen, und nicht der Grund, sie mitzunehmen. + + ES IST EINE ROLLENFRAGE, KEINE PERSONENFRAGE: Alle Modis bekommen + dasselbe. Sollte das je auseinandergehen, gehoert die Auswahl an die + Person und nicht hierher -- dann aber bewusst und mit einer Stelle, + an der sie gepflegt wird. + ===================================================================== */ +const MODI_BEREICHE = [ + { gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", + name: "Aufgaben", unter: "Offen, in Arbeit, Review", zeichen: "aufgaben", + ton: 13, gross: true, ziel: "aufgaben.html", szene: "garage" }, + { gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", + name: "Chat", unter: "Nachrichten mit dem Team", zeichen: "chat", + ton: 4, ziel: "chat.html", szene: "lounge" }, + { gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", + name: "Kalender", unter: "Live-Zeiten und Termine", zeichen: "kalender", + ton: 8, ziel: "kalender.html", szene: "skyline" }, + { gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", + name: "Dateien", unter: "Ablage und Freigaben", zeichen: "dateien", + ton: 19, ziel: "dateien.html", szene: "arena" }, + + { 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" }, + { gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung", + name: "Community", unter: "Begrüßen, vermitteln, wiedererkennen", + zeichen: "community", ton: 6, ziel: "bereich.html?b=community", szene: "lounge" }, + { gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung", + name: "Technik", unter: "Ton, Bild, Verbindung", zeichen: "technik", + 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" }, + { gruppe: "Für dich", gruppeUnter: "Deine Seite im Team", + name: "Wissen", unter: "Nachschlagen statt nachfragen", zeichen: "wissen", + ton: 9, ziel: "wissen.html", szene: "arena" }, +]; + +/** + * Die Kacheln dieser Person -- oder `null`, wenn sie aus bereiche.js + * kommen sollen. + * + * `null` und nicht eine leere Liste: Die Oberflaeche unterscheidet + * "nimm deine eigene Liste" von "du bekommst keine Kacheln". Eine leere + * Liste hiesse das Zweite, und die fuenf bekannten Rollen haetten ab + * sofort eine leere Startseite. + */ +export function bereicheFuer(person) { + return person?.rolle === "modi" ? MODI_BEREICHE : null; +} + +/* 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. + + OHNE IHN STAND DA NUR DAS WORT "Modi" -- neben fuenf Rollen, die einen + ganzen Satz bekommen. Das sieht nicht verborgen aus, sondern + unfertig, und Filipe haette zu Recht gefragt, warum seine Seite + halbfertig wirkt. Gefunden hat das die Browserpruefung, nicht das + Nachdenken. + + AUF AUGENHOEHE FORMULIERT: "dein Bereich im Team", nicht "dein + Bereich unter DogFather". Ein Modi ist Teil des Teams, kein + Untergebener -- das gilt fuer jeden Text im Haus. */ +const MODI_ROLLENTEXT = "Modi · dein Bereich im Team"; + +/* DIE ZIERZEILE UEBER "DIE ZENTRALE". + + Sie nennt den BETRIEB, unter dem gearbeitet wird -- seit dem + 08.09.2026 auf Filipes Wunsch "Spicy Media" statt "Dogfather + Universe". Das steht fest in start.html, und fuer die fuenf bekannten + Rollen stimmt es auch. + + FUER EINEN MODI STIMMT ES NICHT. Ein Modi gehoert nicht zur Agentur, + er moderiert die Lives -- "Team Dogi" heisst diese Runde auch auf der + oeffentlichen Seite (team-modis.html). Bis hierher stand ueber seiner + Startseite die Marke eines Betriebs, mit dem er nichts zu tun hat. + Aufgefallen ist das nicht beim Nachdenken, sondern auf dem + Bildschirmfoto der Browserpruefung. + + Wie ueberall kommt der Text vom Server: Ein zweiter Markenname in + start.html waere zwar kein Verrat (er sagt nichts ueber Modis), aber + eine Zeile, die fuer fast jeden Leser tot dasteht -- und tote Zeilen + werden irgendwann falsch erklaert. */ +const MODI_MARKE = "Team Dogi"; + +/* ===================================================================== + DIE KATEGORIEN EINER MODI-AUFGABE (10.09.2026) + + Kapitel 6.1 des Anforderungsdokuments nennt sechs Beispiele, der + Aufgabenkatalog in Teil 2 benutzt dreizehn. Dreizehn Knoepfe in einer + Filterleiste sind auf einem Handy unbedienbar -- deshalb diese acht, + und hier steht, wohin die uebrigen fallen: + + Branding, Team, Kommunikation -> Planung & Organisation + Wachstum -> Community & Events + + Das ist eine Entscheidung und keine Uebersetzung. Wer sie spaeter + anders will, aendert sie hier, an einer Stelle. + + SIE STEHEN AUF DEM SERVER, nicht in assets/js/aufgaben.js. Nicht weil + die Woerter etwas verraten wuerden -- "Clipping" sagt nichts ueber + Modis --, sondern weil eine Liste, die im Browser jedes Menschen + liegt und nur bei einem einzigen benutzt wird, die Frage aufwirft, + fuer wen sie da ist. Genau diese Frage soll niemand stellen. */ +export const MODI_KATEGORIEN = [ + { wert: "chat", name: "Chat-Moderation" }, + { wert: "clipping", name: "Clipping & Schnitt" }, + { wert: "social", name: "Social Media" }, + { wert: "community", name: "Community & Events" }, + { wert: "technik", name: "Technik" }, + { wert: "planung", name: "Planung & Organisation" }, + { wert: "analyse", name: "Analyse & Zahlen" }, + { wert: "sonstiges", name: "Sonstiges" }, +]; + +/** + * Darf diese Person mit Kategorien arbeiten? + * + * Ein Modi (es sind seine Aufgaben) und die DogFather-Rolle (sie sieht + * seine Aufgaben und legt ihm welche an). Sonst niemand -- und "sonst + * niemand" heisst hier: Die Liste kommt gar nicht erst mit, statt + * mitzukommen und ausgeblendet zu werden. + */ +export function kategorienFuer(person) { + if (!person) return []; + return person.rolle === "modi" || person.rolle === "admin" ? MODI_KATEGORIEN : []; +} + +export function rollentextFuer(person) { + return person?.rolle === "modi" ? MODI_ROLLENTEXT : null; +} + +export function markeFuer(person) { + return person?.rolle === "modi" ? MODI_MARKE : null; +} + /* scrypt-Parameter. N=2^15 braucht auf diesem Server rund 150 ms — spürbar genug, um Rateversuche auszubremsen, und unauffällig für einen echten Anmeldevorgang. Die Werte werden je Datensatz mitgespeichert, damit sie @@ -614,6 +781,20 @@ function umstellungen(d) { nur, WEN man pruefen soll; ob der Code stimmt, sagt weiterhin allein scrypt. Ein Treffer hier allein laesst niemanden herein. */ ["personen", "code_kennung", "TEXT"], + + /* DIE KATEGORIE EINER AUFGABE (10.09.2026). + + Kapitel 6.1 des Anforderungsdokuments verlangt sie an jeder + Aufgabe. Filipes Entscheidung dazu: nur bei den Modis -- fuer + Creator, Scouts und Manager bleibt das Formular, wie es war. + + KEIN CHECK IN DER SPALTE, obwohl die Werte fest sind. Ein CHECK + laesst sich in SQLite nicht aendern; die Liste um eine Kategorie + zu erweitern hiesse jedes Mal, die ganze Tabelle neu zu bauen. + Geprueft wird stattdessen beim Schreiben (siehe KATEGORIEN in + workspace-aufgaben.js) -- an EINER Stelle, durch die alle + Schreibwege laufen. */ + ["aufgaben", "kategorie", "TEXT"], ]) { try { const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name); @@ -2439,6 +2620,20 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => { rolle_name: ROLLEN_NAME[person.rolle] ?? person.rolle, sicht, bild, + /* DIE KACHELN, WENN SIE NICHT IM BROWSER STEHEN DUERFEN. + + Fuer die fuenf bekannten Rollen steht hier `null`, und die + Oberflaeche nimmt wie bisher ihre eigene Liste -- der Ablauf + aendert sich fuer sie also nicht. + + NACH DER ANGESEHENEN PERSON, nicht nach der eigenen: Sieht sich + DogFather den Arbeitsplatz eines Modis an, soll er DESSEN Kacheln + sehen. Genau daran war beim Sicht-Umschalter schon einmal zu + erkennen, dass er nicht greift ("ich seh immer noch die Seite + genau wie meine"). */ + bereiche: bereicheFuer(angesehen || person), + rolle_text: rollentextFuer(angesehen || person), + marke: markeFuer(angesehen || person), }); }); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index a40f1d1c..f9928569 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=202609100010", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609100051", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609100010", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609100051", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100010", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100051", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100010", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100051", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index f8407c0e..19c14578 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -200,6 +200,23 @@ (faellig ? 'überfällig ' : 'bis ') + datumHuebsch(a.frist)); zeile.append(f); } + /* Die Kategorie als Wort in derselben Zeile wie Frist und Person. + Kein eigener Streifen und keine eigene Farbe: Sie ist eine + Einordnung, keine Warnung -- und eine Karte, auf der alles + hervorsticht, hebt nichts hervor. Der Name kommt aus der Liste + des Servers; steht die Aufgabe in einer Kategorie, die es nicht + mehr gibt, wird nichts angezeigt statt eines Schluessels. + + KEINE EIGENE CSS-KLASSE. Sie muesste in sieben gleichlautenden + Kopien der Modulliste gepflegt werden (pruef-css-klassen wacht + darueber, dass die sieben Zeichen fuer Zeichen gleich sind) -- + viel Aufwand und sieben Gelegenheiten fuer einen Unterschied, + fuer eine Zeile Text. Dieselbe schlichte Form wie "· Bereich X" + daneben tut es. */ + if (a.kategorie) { + const name = kategorien.find((k) => k.wert === a.kategorie)?.name; + if (name) zeile.append(el('span', null, '· ' + name)); + } if (a.verantwortlich_name) zeile.append(el('span', null, '· ' + a.verantwortlich_name)); if (LEITUNG.has(ich.rolle) && a.creator_name && a.creator_name !== a.verantwortlich_name) { zeile.append(el('span', null, '· Bereich ' + a.creator_name)); @@ -668,11 +685,71 @@ window.zielMarkieren?.(treffer, ZEIG_TEXT[was]); } + /* ===================================================================== + DIE KATEGORIE EINER AUFGABE (10.09.2026, Kapitel 6.1) + + Filipes Entscheidung: nur bei den Modis. Fuer Creator, Scouts und + Manager bleibt das Formular unveraendert -- sie bekommen das Feld + nicht ausgeblendet, sondern gar nicht: Der Server schickt ihnen + keine Kategorien, und ohne Kategorien wird nichts gebaut. + + WARUM DIE NAMEN NICHT HIER STEHEN: Diese Datei bekommt jeder + ausgeliefert, der die Seite oeffnet. Eine Liste, die im Browser + jedes Menschen liegt und nur bei einem einzigen benutzt wird, wirft + die Frage auf, fuer wen sie da ist. Genau die soll niemand stellen. + ===================================================================== */ + let kategorien = []; + /* Wer ein Modi ist -- gefuellt aus der Personenliste, die ohnehin + geladen wird. Sie enthaelt Modis nur fuer den, der sie sehen darf; + fuer alle anderen bleibt diese Menge leer. */ + const modis = new Set(); + + function kategorienEinsetzen() { + if (!kategorien.length) return; + for (const ziel of ['f-kategorie', 'b-kategorie']) { + const feld = $(ziel); + if (!feld || feld.dataset.fertig) continue; + feld.dataset.fertig = 'ja'; + for (const k of kategorien) { + const o = el('option', null, k.name); + o.value = k.wert; + feld.append(o); + } + } + } + + /* Gehoert diese Aufgabe zu einem Modi? + + Fuer einen Modi immer (es sind seine eigenen). Fuer die + DogFather-Rolle nur dann, wenn die gewaehlte Person eine ist -- + sonst stuende das Feld auch an jeder Creator-Aufgabe, und genau das + wollte Filipe nicht ("nur bei den Modis"). */ + function gehoertModi(creatorId, verantId) { + if (ich?.rolle === 'modi') return true; + return modis.has(Number(creatorId)) || modis.has(Number(verantId)); + } + + function kategorieFeldZeigen(praefix) { + const huelle = $(praefix === 'f' ? 'feld-kategorie' : 'b-feld-kategorie'); + if (!huelle) return; + const zeigen = kategorien.length + && gehoertModi($(`${praefix}-creator`)?.value, $(`${praefix}-verant`)?.value); + huelle.hidden = !zeigen; + /* Verborgen heisst auch: nichts mitschicken. Ein Wert, der in einem + unsichtbaren Feld stehenbleibt, wandert sonst beim naechsten + Speichern mit -- und niemand sieht, woher er kommt. */ + if (!zeigen && $(`${praefix}-kategorie`)) $(`${praefix}-kategorie`).value = ''; + } + async function laden() { try { const antwort = await hole('/workspace/api/aufgaben'); if (!antwort.ok) { melde('Aufgaben konnten nicht geladen werden.'); return; } - alleAufgaben = (await antwort.json()).aufgaben || []; + const daten = await antwort.json(); + alleAufgaben = daten.aufgaben || []; + kategorien = daten.kategorien || []; + kategorienEinsetzen(); + kategorieFeldZeigen('f'); lageZeigen(alleAufgaben); neuZeichnen(); /* Erst NACH dem Zeichnen -- vorher gibt es die Karten nicht, und @@ -713,6 +790,7 @@ daten.creator_extern = $('f-creator').dataset.extern || null; daten.verantwortlich_extern = $('f-verant').dataset.extern || null; } + if (!$('feld-kategorie')?.hidden) daten.kategorie = $('f-kategorie').value || ''; knopfEl.disabled = true; try { @@ -749,6 +827,8 @@ $('b-verant').value = a.verantwortlich_id ? String(a.verantwortlich_id) : ''; $('b-loeschen').hidden = false; } + kategorieFeldZeigen('b'); + if ($('b-kategorie')) $('b-kategorie').value = a.kategorie || ''; dialog.showModal(); $('b-titel').focus(); } @@ -774,6 +854,7 @@ daten.creator_extern = $('b-creator').dataset.extern || null; daten.verantwortlich_extern = $('b-verant').dataset.extern || null; } + if (!$('b-feld-kategorie')?.hidden) daten.kategorie = $('b-kategorie').value || ''; $('b-speichern').disabled = true; try { const a = await hole(`/workspace/api/aufgaben/${bearbeitet.id}`, { @@ -824,6 +905,11 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe try { const { personen } = await (await hole('/workspace/api/personen')).json(); for (const p of personen) { + /* Wer ein Modi ist, entscheidet spaeter, ob das Kategorie-Feld + ueberhaupt erscheint. Die Liste enthaelt Modis nur fuer den, + der sie sehen darf -- fuer alle anderen bleibt die Menge + leer, und das Feld bleibt weg. */ + if (p.rolle === 'modi') modis.add(Number(p.id)); for (const ziel of ['f-creator', 'f-verant', 'b-creator', 'b-verant']) { const o = el('option', null, `${p.name} (${p.rolle})`); o.value = String(p.id); @@ -849,6 +935,15 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe window.personenwahl?.(ziel, { hinweis: "Kein Konto im Workspace – der Name steht nur an diesem Eintrag." + ' Die Aufgabe steht dann auf keinem persönlichen Aufgabenbrett.' }); } + /* Das Kategorie-Feld folgt der gewaehlten Person. Auf `change` und + nicht einmalig beim Oeffnen: Wer erst den Titel tippt und dann + die Person aussucht, bekaeme das Feld sonst nie zu sehen. */ + for (const ziel of ['f-creator', 'f-verant']) { + $(ziel)?.addEventListener('change', () => kategorieFeldZeigen('f')); + } + for (const ziel of ['b-creator', 'b-verant']) { + $(ziel)?.addEventListener('change', () => kategorieFeldZeigen('b')); + } } await laden(); @@ -915,6 +1010,23 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe async function vorlagenbrettZeigen() { const ziel = $('vorlagenbrett'); if (!ziel || !ich) return; + /* NICHT FUER EINEN MODI (10.09.2026). + + Das Vorlagenbrett haelt 80 fertige Aufgaben in vier Bereichen und + vier Stufen bereit -- den Begleitplan fuer einen CREATOR, von + "Ton aufnehmen und anhoeren" bis "Monatsbericht mit drei festen + Zahlen". Fuer einen Modi ist davon nichts gedacht: Er moderiert, + er baut keinen Kanal auf. + + Aufgefallen ist das auf dem Bildschirmfoto der Browserpruefung, + nicht im Code -- die Kachel laedt fehlerfrei, sie zeigt nur das + Falsche. Ein Vorschlag, der nicht passt, ist schlimmer als + keiner: Man raeumt ihn nicht weg, man arbeitet um ihn herum. + + SEIN Katalog steht in Teil 2 des Anforderungsdokuments (die 60+ + Aufgaben aus Phase 1 bis 6). Bis der da ist, steht hier nichts -- + das ist ehrlicher als etwas Fremdes. */ + if (ich.rolle === 'modi') { ziel.hidden = true; return; } if (!vorlagen) { try { const a = await hole('/workspace/api/vorlagen'); diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index acf9b636..dac122ab 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -133,13 +133,52 @@ zahlen.set(kachel, bisher); } + /* ================================================================= + WELCHE KACHELN DIESE PERSON SIEHT (10.09.2026) + + ZWEI QUELLEN, und die Reihenfolge ist die Aussage: + + 1. Schickt der Server eine Liste, gilt sie. Das ist der Weg fuer + Rollen, die in bereiche.js gar nicht vorkommen duerfen -- + diese Datei bekommt jeder ausgeliefert, der die Seite + oeffnet, und ein Rollenname darin waere mit einem Blick in + den Quelltext gefunden. + 2. Sonst wie bisher: die Rollenliste an der Kachel selbst. + + Nach der ANGESEHENEN Rolle, nicht der eigenen -- sonst stuenden + bei der Sicht auf einen Creator weiterhin "Personen & Zugaenge" + und "Automationen" da, die er gar nicht hat. Genau daran war zu + sehen, dass die Sicht nicht greift. Der Server richtet sich beim + Bauen seiner Liste nach derselben Person. + + `null` heisst "nimm deine eigene Liste", eine LEERE Liste hiesse + "keine Kacheln". Die beiden auseinanderzuhalten ist kein + Feinschliff: Verwechselte man sie, haetten die fuenf bekannten + Rollen ab sofort eine leere Startseite. */ + function eigeneGruppen(wer) { + const vomServer = ich && ich.bereiche; + if (Array.isArray(vomServer)) { + /* Nach Gruppen buendeln, in der Reihenfolge, in der sie kommen + -- eine Map behaelt sie, ein Objekt sortierte Zahlenschluessel + um. Hier sind es Woerter, aber darauf zu bauen waere die Art + Annahme, die spaeter jemanden Zeit kostet. */ + const nach = new Map(); + for (const b of vomServer) { + if (!nach.has(b.gruppe)) { + nach.set(b.gruppe, { name: b.gruppe, unter: b.gruppeUnter || '', bereiche: [] }); + } + nach.get(b.gruppe).bereiche.push(b); + } + return [...nach.values()]; + } + return GRUPPEN + .map((g) => ({ ...g, bereiche: g.bereiche.filter((b) => b.rollen.includes(wer.rolle)) })) + .filter((g) => g.bereiche.length); + } + let laufNr = 0; - for (const gruppe of GRUPPEN) { - /* Nach der ANGESEHENEN Rolle -- sonst stuenden bei der Sicht auf - einen Creator weiterhin "Personen & Zugaenge" und "Automationen" - da, die er gar nicht hat. Genau daran war zu sehen, dass die - Sicht nicht greift. */ - const meine = gruppe.bereiche.filter((b) => b.rollen.includes(zeigt.rolle)); + for (const gruppe of eigeneGruppen(zeigt)) { + const meine = gruppe.bereiche; if (!meine.length) continue; const abschnitt = el('section', 'gruppe'); @@ -681,9 +720,34 @@ Liste (mit dem erklaerenden Zusatz dahinter), dann der Name vom Server, zuletzt der nackte Schluessel. So aendert sich fuer die fuenf bekannten Rollen nichts. */ + /* DIE ZIERZEILE UEBER DEM TITEL (10.09.2026). + + Sie nennt den Betrieb -- fest "Spicy Media" in start.html. Fuer + einen Modi stimmt das nicht, er gehoert zu Team Dogi; der Server + schickt dann den passenden Namen mit. + + ERSETZT WIRD NUR DER TEXTKNOTEN, nicht der ganze Inhalt: In dem + Element sitzen zwei als Zierrauten links und + rechts. Ein textContent darauf haette sie geloescht -- und zwar + lautlos, denn eine fehlende Raute sieht nicht nach Fehler aus, + sondern nach Absicht. Ein neues Element drumherum waere die + andere naheliegende Loesung und aendert die Flex-Aufteilung; das + hier aendert am Aufbau gar nichts. */ + if (ich.marke) { + const marke = document.querySelector('.zmarke'); + if (marke) { + for (const knoten of marke.childNodes) { + if (knoten.nodeType === 3 && knoten.textContent.trim()) { + knoten.textContent = ` ${ich.marke} `; + } + } + } + } + $('rollentext').textContent = ich.sicht - ? `Arbeitsplatz von ${ich.sicht.name} · ${ROLLENTEXT[zeigt.rolle] || zeigt.rolle}` - : (ROLLENTEXT[ich.rolle] || ich.rolle_name || ich.rolle); + ? `Arbeitsplatz von ${ich.sicht.name} · ` + + `${ROLLENTEXT[zeigt.rolle] || ich.sicht.rolle_name || zeigt.rolle}` + : (ROLLENTEXT[ich.rolle] || ich.rolle_text || ich.rolle_name || ich.rolle); /* Datum und Rolle in der Anrede -- wie im Vorbild, wo unter "Noch wach, Dogi" das Datum steht und daneben "ADMIN & PARTNER". diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 6f49a90e..d2dc01a4 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. --> - - - - - - + + + + + + @@ -97,6 +97,16 @@ + +
@@ -188,6 +198,10 @@
+ @@ -231,10 +245,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 82ab43da..c0d428f9 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 0d518758..7b655f8b 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 4a93bafc..fe6af1f3 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 7af3bc97..8734a6b4 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 3d142571..154a196b 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 72df973d..9ea98cca 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 36499d0c..822a0823 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 68f7b4d4..f054e2c3 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 1314e080..db2e9e59 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 26587650..0df13d5b 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 fc2dcc9f..f85459b1 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 6239f145..c8968446 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 d38bfd2d..545ab78b 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 eda1e546..e28252e4 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 56169443..3e9c365b 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 679eb149..69ad87e3 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 2647d762..27cff631 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 556571e5..f54c6aaf 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 2bf61a88..17b0e500 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 @@ - - - - - + + + + +