diff --git a/server/pruef-modi-katalog.mjs b/server/pruef-modi-katalog.mjs new file mode 100644 index 00000000..2d38bea9 --- /dev/null +++ b/server/pruef-modi-katalog.mjs @@ -0,0 +1,314 @@ +/* ===================================================================== + DER AUFGABENKATALOG AUS TEIL 2 DES ANFORDERUNGSDOKUMENTS + + Kapitel 14: "Alle Aufgaben dieses Katalogs koennen 1:1 als Vorlagen in + die App importiert werden -- inklusive Kategorie, empfohlener Rolle + und Frequenz. So ist das Aufgaben-Board ab dem ersten Tag vollstaendig + befuellt, statt leer zu starten." + + GEPRUEFT WIRD NICHT NUR, DASS ER DA IST. Ein Katalog, den man ansehen, + aber nicht uebernehmen kann, ist eine Speisekarte ohne Kueche. Und ein + uebernommener Eintrag, bei dem die Kategorie unterwegs verlorengeht, + ist die Haelfte wert -- dann sortiert man 60-mal von Hand ein, was im + Dokument bereits danebensteht. + + DAZU DIE VERBORGENHEIT: Wer den Katalog nicht bekommt, darf auch nicht + erfahren, dass es ihn gibt. Eine Absage muss deshalb wortgleich mit + der fuer eine erfundene Art sein. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-kat2-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4363"; +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-katalog"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4363"; + +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 idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001"); +const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); +d.close(); +void idDogi; +void idMan; + +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" }; + +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"); + +/* ---------- 1. Ist er vollstaendig da? ------------------------------- */ +console.log("\n=== 1. Der Katalog, wie ihn das Dokument beschreibt ===\n"); + +let katalog = null; +{ + const a = await hole(modiAn.keks, "/workspace/api/vorlagen"); + katalog = a.daten?.katalog || null; + /* SECHZIG, fest hingeschrieben. Das Dokument wirbt mit "60+ konkrete + Aufgaben"; faellt eine heraus, muss das auffallen. Eine Zahl, die + sich selbst nachzaehlt, kann das nicht. */ + ok(katalog && katalog.aufgaben.length === 60, + `${katalog?.aufgaben?.length ?? 0} Aufgaben (erwartet 60)`); + ok(katalog && katalog.gruppen.length === 9, + `${katalog?.gruppen?.length ?? 0} Etappen (erwartet 9)`); + /* Die Verteilung aus Teil 2, Phase fuer Phase. Sie steht hier, weil + eine reine Gesamtzahl auch dann stimmt, wenn zwoelf Aufgaben in der + falschen Etappe liegen. */ + const soll = { aufbau: 12, alltag: 9, vor: 5, waehrend: 5, nach: 4, + content: 8, woche: 5, monat: 5, community: 7 }; + const ist = {}; + for (const v of katalog?.aufgaben || []) ist[v.gruppe] = (ist[v.gruppe] || 0) + 1; + ok(JSON.stringify(ist) === JSON.stringify(soll), + `die Verteilung stimmt (${Object.entries(ist).map(([k, n]) => `${k}:${n}`).join(" ")})`); + ok((katalog?.aufgaben || []).every((v) => v.titel && v.text && v.kategorie && v.rolle && v.tage), + "jede Aufgabe hat Titel, Text, Kategorie, empfohlene Rolle und Frist"); +} + +/* Jede Kategorie im Katalog muss es auch wirklich geben -- sonst nimmt + der Server sie beim Uebernehmen an und lehnt sie beim Aendern ab. */ +{ + const a = await hole(modiAn.keks, "/workspace/api/aufgaben"); + const gueltig = new Set((a.daten?.kategorien || []).map((k) => k.wert)); + const unbekannt = [...new Set((katalog?.aufgaben || []).map((v) => v.kategorie))] + .filter((k) => !gueltig.has(k)); + ok(gueltig.size > 0 && unbekannt.length === 0, + unbekannt.length + ? `unbekannte Kategorie(n) im Katalog: ${unbekannt.join(", ")}` + : `alle ${gueltig.size} Kategorien passen zum Katalog`); +} + +/* ---------- 2. Wer bekommt ihn nicht? -------------------------------- */ +console.log("\n=== 2. Und wer ihn nicht bekommt ===\n"); +for (const [name, an] of [["Manager", manAn], ["Creator", creatAn]]) { + const a = await hole(an.keks, "/workspace/api/vorlagen"); + ok(a.status === 200 && a.daten?.katalog === null, + `${name}: kein Katalog (${JSON.stringify(a.daten?.katalog)})`); + /* Und der ANDERE Katalog ist bei ihnen sehr wohl da -- sonst waere + oben auch gruen, wenn die ganze Antwort leer kaeme. */ + ok(a.daten?.aufgaben && Object.keys(a.daten.aufgaben).length > 0, + `${name}: der Creator-Katalog dagegen schon`); +} +{ + const a = await hole(modiAn.keks, "/workspace/api/vorlagen"); + ok(a.daten?.aufgaben === null, + "und der Modi bekommt umgekehrt den Creator-Katalog nicht"); +} + +/* ---------- 3. Uebernehmen ------------------------------------------- */ +console.log("\n=== 3. Aus der Vorlage wird eine echte Aufgabe ===\n"); +{ + const nr = katalog.aufgaben.findIndex((v) => v.gruppe === "waehrend"); + const vorlage = katalog.aufgaben[nr]; + const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr, gruppe: "waehrend" }), + }); + ok(a.status === 200 && a.daten?.angelegt === 1, `übernommen (${a.status})`); + + const b = await hole(modiAn.keks, "/workspace/api/aufgaben"); + const neu = (b.daten?.aufgaben || []).find((x) => x.vorlage === vorlage.schluessel); + ok(!!neu, `sie liegt auf seinem Brett (${neu?.titel || "nicht gefunden"})`); + /* DIE KATEGORIE IST DER HALBE WERT DER VORLAGE. Ginge sie unterwegs + verloren, muesste man 60-mal von Hand einsortieren, was im Dokument + danebensteht -- und niemand merkte es, weil die Aufgabe ja da ist. */ + ok(neu?.kategorie === vorlage.kategorie, + `mit ihrer Kategorie (${neu?.kategorie} / erwartet ${vorlage.kategorie})`); + ok(!!neu?.frist, `und mit Frist (${neu?.frist})`); + ok(neu?.verantwortlich_id === idModi, "und er selbst ist verantwortlich"); +} + +/* Eine ganze Etappe auf einmal. */ +{ + const wieViele = katalog.aufgaben.filter((v) => v.gruppe === "nach").length; + const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog_alle", gruppe: "nach" }), + }); + ok(a.status === 200 && a.daten?.angelegt === wieViele, + `eine ganze Etappe auf einmal: ${a.daten?.angelegt} von ${wieViele}`); +} + +/* ---------- 4. DogFather traegt fuer jemanden ein -------------------- */ +console.log("\n=== 4. DogFather trägt für einen Modi ein ===\n"); +{ + const nr = katalog.aufgaben.findIndex((v) => v.gruppe === "monat"); + const a = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr, gruppe: "monat", person_id: idModi }), + }); + ok(a.status === 200 && a.daten?.angelegt === 1, `für den Modi geht es (${a.status})`); + + /* Aber nicht an einen Creator -- sonst laege eine Aufgabe mit + Modi-Kategorie an jemandem, der die Kategorien gar nicht kennt, und + die Trennung waere von innen aufgeweicht. */ + const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr, gruppe: "monat", person_id: idCreat }), + }); + ok(b.status === 404, `an einen Creator dagegen nicht (${b.status})`); + + /* Und ohne Person gar nicht -- eine Aufgabe ohne Adressaten ist ein + Zettel. */ + const c = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr, gruppe: "monat" }), + }); + ok(c.status === 400 && /Person/i.test(c.daten?.fehler || ""), + `ohne Person wird gefragt (${c.status} ${c.daten?.fehler || ""})`); +} + +/* ---------- 5. Der heikelste Fall ------------------------------------ */ +console.log("\n=== 5. Ein Manager versucht es trotzdem ===\n"); +{ + const a = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr: 0, gruppe: "aufbau", person_id: idModi }), + }); + const b = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "gibtesnicht", nr: 0 }), + }); + /* WORTGLEICH. Ein eigener Fehlertext waere die Auskunft, dass es die + Art gibt -- und damit, dass es den Katalog gibt. */ + ok(a.status === b.status && JSON.stringify(a.daten) === JSON.stringify(b.daten), + `klingt wie eine erfundene Art (${a.status} ${JSON.stringify(a.daten)})`); +} + +/* ---------- 6. Im Browser -------------------------------------------- */ +console.log("\n=== 6. Das Brett 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)); + 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 }); + await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + + /* Aufklappen, falls es zugeklappt startet. */ + const zu = await seite.evaluate(() => + document.getElementById("vorlagenbrett")?.dataset.auf !== "ja"); + if (zu) { + await seite.click(".vorlagenbrett__kopf").catch(() => {}); + await seite.waitForTimeout(700); + } + + const bild = await seite.evaluate(() => ({ + sichtbar: !document.getElementById("vorlagenbrett")?.hidden, + etappen: [...document.querySelectorAll(".stufenleiter__knopf .stufenleiter__name")] + .map((z) => z.textContent.trim()), + karten: document.querySelectorAll(".vorlagenbrett__karte").length, + text: document.body.innerText, + kopf: (document.querySelector(".kopfleiste .marke__text") || {}).textContent || "", + })); + ok(bild.sichtbar, "das Brett steht da"); + ok(bild.etappen.length === 9, `mit neun Etappen (${bild.etappen.join(" · ")})`); + ok(bild.karten > 0, `und ${bild.karten} Karten in der offenen Etappe`); + /* Der Katalog des ANDEREN darf nirgends durchschlagen. */ + ok(!/vier Bereichen und vier Stufen/.test(bild.text), + "kein Wort vom Creator-Katalog"); + /* DIE KOPFLEISTE. Auf jeder Seite steht dort fest "Creator + Workspace" -- fuer jemanden, der moderiert statt einen Kanal + aufzubauen, ist das schlicht der falsche Name. Aufgefallen auf + dem Bildschirmfoto, wie schon die Zierzeile auf der Startseite. */ + ok(bild.kopf.includes("Team") && !bild.kopf.includes("Creator"), + `die Kopfleiste nennt seinen Betrieb ("${bild.kopf.trim()}")`); + /* Und der Seitenname dahinter steht noch da -- ihn beim Austausch + zu verlieren waere lautlos passiert. */ + ok(/Aufgaben/.test(bild.kopf), "der Seitenname dahinter blieb erhalten"); + ok(konsole.length === 0, + konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); + await seite.screenshot({ path: "pruef-modi-katalog.png", fullPage: false }); + console.log(" -- Bildschirmfoto: server/pruef-modi-katalog.png"); + await kontext.close(); + } finally { + await browser.close(); + } +} + +/* ---------- 7. Gegenprobe -------------------------------------------- */ +console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n"); +{ + /* Eine Vorlagennummer, die es nicht gibt. Kaeme dafuer ein 200, + wuerden die Haken oben nichts beweisen -- dann sagt der Server zu + allem ja. */ + const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr: 9999, gruppe: "aufbau" }), + }); + ok(a.status === 400, `eine erfundene Vorlagennummer wird abgelehnt (${a.status})`); + const b = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog_alle", gruppe: "gibtesnicht" }), + }); + ok(b.status === 400, `und eine erfundene Etappe ebenso (${b.status})`); +} + +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-kategorien.mjs b/server/pruef-modi-kategorien.mjs index 82efc979..60443448 100644 --- a/server/pruef-modi-kategorien.mjs +++ b/server/pruef-modi-kategorien.mjs @@ -242,17 +242,21 @@ console.log("\n=== 5. Das Feld im echten Formular ===\n"); 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. */ + /* DAS VORLAGENBRETT (berichtigt 10.09.2026). + + Hier stand "und kein Creator-Vorlagenbrett" -- richtig, solange + es fuer einen Modi ueberhaupt keinen Katalog gab. Seit der + Katalog aus Teil 2 da ist, steht das Brett wieder da, nur mit + SEINEM Inhalt. Geprueft wird deshalb ab jetzt beides: dass es + da ist, und dass der fremde nicht durchschlaegt. Was der + Katalog selbst enthaelt, prueft pruef-modi-katalog. */ 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(vorlagen.da, "das Vorlagenbrett steht da -- mit seinem eigenen Katalog"); ok(!/vier Bereichen und vier Stufen/.test(vorlagen.text), - "auch nicht als Text auf der Seite"); + "und kein Wort vom Creator-Katalog"); 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 }); @@ -277,8 +281,15 @@ console.log("\n=== 5. Das Feld im echten Formular ===\n"); 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"); + /* DIE NAMEN NICHT ABSCHREIBEN, SONDERN FRAGEN (berichtigt + 10.09.2026). Hier stand "Chat-Moderation|Clipping & Schnitt" + woertlich -- und beim Umbenennen einer Kategorie suchte die + Pruefung ploetzlich nach etwas, das es nicht mehr gibt. Sie war + damit gruen, ohne etwas zu pruefen: genau die Sorte Haken, die + nichts wert ist. Jetzt kommt die Liste aus derselben Quelle wie + die Oberflaeche. */ + ok(kategorien.length > 0 && !kategorien.some((k) => z.text.includes(k.name)), + `keiner der ${kategorien.length} Kategorienamen steht auf seiner Seite`); ok(konsole.length === 0, konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); await kontext.close(); @@ -328,8 +339,8 @@ console.log("\n=== 5. Das Feld im echten Formular ===\n"); { 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"); + ok(kategorien.length > 0 && kategorien.some((k) => text.includes(k.name)), + "beim Modi findet dieselbe Suche die Namen sehr wohl"); await kontext.close(); } } finally { diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index 3675e119..32b8f584 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -36,7 +36,7 @@ import express from "express"; import { - db, sitzungLesen, istLeitung, darfCreator, protokolliere, echteIp, + db, sitzungLesen, istLeitung, darfCreator, protokolliere, echteIp, kategorienFuer, } from "./workspace.js"; export const vorlagenRouter = express.Router(); @@ -796,6 +796,261 @@ function angemeldet(req, res, next) { Eskalation in vier Stufen. ===================================================================== */ +/* ===================================================================== + DER AUFGABENKATALOG AUS TEIL 2 DES ANFORDERUNGSDOKUMENTS (10.09.2026) + + 60 fertig ausformulierte Aufgaben, sortiert nach den sechs Etappen des + Dokuments. Kapitel 14 sagt dazu: "Alle Aufgaben dieses Katalogs koennen + 1:1 als Vorlagen in die App importiert werden -- inklusive Kategorie, + empfohlener Rolle und Frequenz. So ist das Aufgaben-Board ab dem + ersten Tag vollstaendig befuellt, statt leer zu starten." + + NEUN GRUPPEN, NICHT SECHS. Das Dokument hat sechs Phasen, aber Phase 3 + zerfaellt in Vor/Waehrend/Nach und Phase 5 in Woche/Monat -- und das + sind fuer den, der davorsitzt, verschiedene Momente. "Waehrend des + Lives" sucht man nicht in derselben Liste wie "einmal im Monat". + + DIE KATEGORIEN SIND DIE DES DOKUMENTS, nicht meine. Beim ersten Anlauf + hatte ich sie auf acht zusammengefasst, mit der Begruendung, dreizehn + Knoepfe seien auf einem Handy unbedienbar. Das war eine plausible + Erklaerung fuer etwas Falsches: Gebaut ist ein AUSWAHLFELD, keine + Knopfleiste, und darin sind vierzehn Eintraege voellig unauffaellig. + Jetzt passt jede Aufgabe genau auf die Kategorie, die im Dokument + danebensteht -- ohne Uebersetzung, die spaeter niemand nachvollziehen + kann. + + DIE TEXTE SIND NEU. Das Dokument nennt nur die Titel; ein Titel allein + ("Eskalationsregeln definieren") sagt nicht, woran man erkennt, dass + man fertig ist. Jeder Satz hier nennt deshalb das eine, was zaehlt -- + nicht drei, denn wer sich fuenf Dinge vornimmt, macht keines. + + EINIGE AUFGABEN AUS PHASE 1 SIND LAENGST ERLEDIGT (die Subdomain + steht, das App-Symbol auch). Sie bleiben trotzdem drin: Der Katalog + ist eine Speisekarte, kein Auftrag. Wer ihn spaeter fuer ein zweites + Team benutzt, braucht sie wieder. + ===================================================================== */ + +export const MODI_GRUPPEN = [ + { schluessel: "aufbau", name: "Aufbau & Einrichtung", + text: "Einmaliges, bevor das Team startklar ist." }, + { schluessel: "alltag", name: "Täglicher Betrieb", + text: "Was an (fast) jedem Tag anfällt." }, + { schluessel: "vor", name: "Vor dem Live", + text: "Die letzten 30 bis 60 Minuten davor." }, + { schluessel: "waehrend", name: "Während des Lives", + text: "Was jetzt passiert, lässt sich später nicht nachholen." }, + { schluessel: "nach", name: "Nach dem Live", + text: "Solange man es noch weiß." }, + { schluessel: "content", name: "Content & Reichweite", + text: "Aus einer Sendung werden viele Videos." }, + { schluessel: "woche", name: "Jede Woche", + text: "Der Rhythmus, der das Team zusammenhält." }, + { schluessel: "monat", name: "Jeden Monat", + text: "Der Blick, den der Alltag nicht hergibt." }, + { schluessel: "community", name: "Community & Wachstum", + text: "Wiedererkannt zu werden ist der halbe Grund wiederzukommen." }, +]; + +export const MODI_KATALOG = [ + /* ---------- Phase 1 — Aufbau & Einrichtung ------------------------- */ + { schluessel: "mk-domain", gruppe: "aufbau", kategorie: "technik", rolle: "DogFather", tage: 14, + titel: "Domain und Subdomain für die App einrichten", + text: "Eine eigene Subdomain, nicht ein Unterordner. Nur so lässt sich die App auf dem Handy wirklich installieren statt nur als Verknüpfung abzulegen." }, + { schluessel: "mk-symbol", gruppe: "aufbau", kategorie: "branding", rolle: "DogFather", tage: 14, + titel: "App-Symbol, Startbild und Farben aus dem Logo ableiten", + text: "Einmal auf einem echten Handy ansehen, nicht nur am großen Bildschirm. Ein Symbol, das bei 48 Pixeln zu Matsch wird, fällt sonst erst nach der Installation auf." }, + { schluessel: "mk-kategorien", gruppe: "aufbau", kategorie: "organisation", rolle: "DogFather", tage: 7, + titel: "Grundkategorien im Aufgaben-Board anlegen", + text: "Chat-Moderation, Clipping, Social Media, Events, Technik. Lieber mit wenigen anfangen und ergänzen als mit zwanzig, von denen zwölf leer bleiben." }, + { schluessel: "mk-rechte-hand", gruppe: "aufbau", kategorie: "team", rolle: "DogFather", tage: 7, + titel: "Die rechte Hand festlegen und einladen", + text: "Eine Person, nicht zwei. Wer vertreten soll, muss im Zweifel allein entscheiden dürfen – sonst ist es keine Vertretung." }, + { schluessel: "mk-erste-modis", gruppe: "aufbau", kategorie: "team", rolle: "DogFather & rechte Hand", tage: 14, + titel: "Den ersten Kreis an Modis einladen (Probezeit)", + text: "Lieber drei, die man kennt, als zehn, die man einschätzen muss. Erweitern geht immer, zurücknehmen kostet Vertrauen." }, + { schluessel: "mk-rulebook", gruppe: "aufbau", kategorie: "organisation", rolle: "DogFather & rechte Hand", tage: 14, + titel: "Team-Regeln und Erwartungen aufschreiben", + text: "Was schriftlich steht, muss man nicht dreimal erklären – und niemand muss raten, was gemeint war. Eine Seite reicht." }, + { schluessel: "mk-eskalation", gruppe: "aufbau", kategorie: "organisation", rolle: "DogFather & rechte Hand", tage: 7, + titel: "Eskalationsregeln festlegen", + text: "Ab wann entscheidet ein Modi nicht mehr allein? Wer das erst im Ernstfall klärt, klärt es zu spät und vor Publikum." }, + { schluessel: "mk-kanaele", gruppe: "aufbau", kategorie: "kommunikation", rolle: "DogFather", tage: 3, + titel: "Team-Gruppenchat und Kategorie-Kanäle anlegen", + text: "Ein Kanal je Zuständigkeit, nicht je Person. Wer wechselt, nimmt sonst das Gespräch mit." }, + { schluessel: "mk-kalender-start", gruppe: "aufbau", kategorie: "planung", rolle: "rechte Hand", tage: 3, + titel: "Team-Kalender mit den ersten Live-Terminen füllen", + text: "Zwei feste Tage schlagen fünf zufällige. Ein Termin, der im Kalender steht, lässt sich einteilen – einer im Kopf nicht." }, + { schluessel: "mk-automod", gruppe: "aufbau", kategorie: "chat", rolle: "Modi (Senior)", tage: 7, + titel: "Automatische Wortfilter für den Live-Chat einrichten", + text: "Erst die Liste, dann der Stream. Was der Filter wegnimmt, muss niemand von Hand wegnehmen – und niemand liest es." }, + { schluessel: "mk-checkliste-vorlage", gruppe: "aufbau", kategorie: "organisation", rolle: "DogFather & rechte Hand", tage: 7, + titel: "Vorlage für die Vor-/Während-/Nach-Live-Checkliste anlegen", + text: "Einmal geschrieben, bei jedem Live benutzt. Nach drei Sendungen anpassen – dann weiß man, was wirklich draufgehört." }, + { schluessel: "mk-social-verknuepfen", gruppe: "aufbau", kategorie: "social", rolle: "Modi (Social Media)", tage: 14, + titel: "Social-Media-Kanäle mit der App verknüpfen", + text: "TikTok, Instagram, YouTube. Ohne Verknüpfung trägt jemand die Zahlen von Hand ab – und irgendwann nicht mehr." }, + + /* ---------- Phase 2 — Täglicher Betrieb ---------------------------- */ + { schluessel: "mk-teamchat-lesen", gruppe: "alltag", kategorie: "kommunikation", rolle: "rechte Hand", tage: 1, + titel: "Team-Chat auf offene Fragen durchsehen", + text: "Einmal am Tag reicht, aber es muss wirklich einmal sein. Eine Frage, die zwei Tage steht, wird nicht mehr gestellt." }, + { schluessel: "mk-board-sichten", gruppe: "alltag", kategorie: "organisation", rolle: "DogFather & rechte Hand", tage: 1, + titel: "Aufgaben-Board sichten: offen, überfällig, erledigt", + text: "Die überfälligen zuerst. Eine Aufgabe, die dauerhaft rot steht, ist entweder zu groß oder gehört abgebrochen." }, + { schluessel: "mk-termin-bestaetigen", gruppe: "alltag", kategorie: "planung", rolle: "rechte Hand", tage: 1, + titel: "Den heutigen Live-Termin bestätigen", + text: "Bestätigen oder absagen – beides früh. Wer nichts sagt, lässt das halbe Team auf Verdacht bereitstehen." }, + { schluessel: "mk-ankuendigung", gruppe: "alltag", kategorie: "social", rolle: "Modi (Social Media)", tage: 1, + titel: "Ankündigung für das heutige Live posten", + text: "30 bis 60 Minuten vorher. Sie bündelt die, die ohnehin kommen wollten – und ein voller Start zieht neue nach." }, + { schluessel: "mk-kommentare", gruppe: "alltag", kategorie: "community", rolle: "Modi (Community)", tage: 1, + titel: "Kommentare und Nachrichten beantworten", + text: "Auch die kurzen. Eine Antwort nach drei Tagen liest sich wie keine." }, + { schluessel: "mk-begruessen", gruppe: "alltag", kategorie: "community", rolle: "Modi", tage: 1, + titel: "Neue Mitglieder im Chat begrüßen", + text: "Mit Namen. Wer beim ersten Mal angesprochen wird, kommt deutlich häufiger wieder." }, + { schluessel: "mk-technikcheck", gruppe: "alltag", kategorie: "technik", rolle: "DogFather", tage: 1, + titel: "Technik-Kurzcheck vor dem Live", + text: "Ton, Bild, Verbindung – in dieser Reihenfolge. Schlechter Ton ist der Grund Nummer eins, warum Leute wieder gehen." }, + { schluessel: "mk-clip-taeglich", gruppe: "alltag", kategorie: "clipping", rolle: "Modi (Clipping)", tage: 1, + titel: "Mindestens einen Clip vom Live des Vortags veröffentlichen", + text: "Einer täglich schlägt fünf am Sonntag. Der Feed hat mehr Reichweite als der Stream – er ist der Weg dorthin." }, + { schluessel: "mk-trial-beobachten", gruppe: "alltag", kategorie: "team", rolle: "rechte Hand", tage: 1, + titel: "Stand der Modis in der Probezeit kurz festhalten", + text: "Zwei Zeilen genügen. Am Ende der Probezeit will man nicht aus der Erinnerung entscheiden müssen." }, + + /* ---------- Phase 3 — Vor dem Live --------------------------------- */ + { schluessel: "mk-vor-technik", gruppe: "vor", kategorie: "technik", rolle: "DogFather", tage: 1, + titel: "Technik-Check: Ton, Bild, Verbindung", + text: "Eine Minute aufnehmen und mit Kopfhörern anhören. Während der Sendung hört man sich selbst nie." }, + { schluessel: "mk-vor-ankuendigung", gruppe: "vor", kategorie: "social", rolle: "Modi (Social Media)", tage: 1, + titel: "Ankündigung auf Social Media und in den Community-Kanälen", + text: "Überall dasselbe Bild, überall dieselbe Uhrzeit. Zwei verschiedene Zeiten kosten mehr Zuschauer als gar keine Ankündigung." }, + { schluessel: "mk-vor-ablauf", gruppe: "vor", kategorie: "planung", rolle: "DogFather & rechte Hand", tage: 1, + titel: "Themen- und Ablaufplan abstimmen", + text: "Anfang, Mitte, Schluss – je zwei Stichworte. Mehr hält beim Senden ohnehin niemand im Kopf." }, + { schluessel: "mk-vor-schicht", gruppe: "vor", kategorie: "team", rolle: "rechte Hand", tage: 1, + titel: "Moderatoren für die heutige Schicht einteilen", + text: "Namentlich, nicht \"wer Zeit hat\". Zuständig ist sonst niemand, und im Ernstfall schaut man sich gegenseitig an." }, + { schluessel: "mk-vor-ziel", gruppe: "vor", kategorie: "planung", rolle: "DogFather", tage: 1, + titel: "Ein Ziel für die heutige Sendung festlegen", + text: "Eine einzige Zahl. Zwei Ziele sind keines – man verfehlt beide und weiß hinterher nicht, woran es lag." }, + + /* ---------- Phase 3 — Während des Lives ---------------------------- */ + { schluessel: "mk-live-chat", gruppe: "waehrend", kategorie: "chat", rolle: "Modi (Chat-Moderation)", tage: 1, + titel: "Chat moderieren und neue Zuschauer begrüßen", + text: "Spam weg, bevor jemand antwortet. Eine Diskussion über einen gelöschten Beitrag kostet mehr als der Beitrag." }, + { schluessel: "mk-live-zeit", gruppe: "waehrend", kategorie: "planung", rolle: "rechte Hand", tage: 1, + titel: "Ablaufplan und Zeit im Blick behalten", + text: "Ein kurzes Zeichen genügt. Wer sendet, hat kein Gefühl für die Uhr – das ist normal und braucht jemanden daneben." }, + { schluessel: "mk-live-interaktion", gruppe: "waehrend", kategorie: "community", rolle: "Modi", tage: 1, + titel: "Umfragen anstoßen und auf das Ziel hinweisen", + text: "Lieber zweimal beiläufig als einmal ausführlich. Wer erst in Minute 40 dazukommt, hat den Hinweis sonst nie gehört." }, + { schluessel: "mk-live-clips-merken", gruppe: "waehrend", kategorie: "clipping", rolle: "Modi (Clipping)", tage: 1, + titel: "Besondere Momente für Clips markieren", + text: "Mit Uhrzeit, sofort. Am nächsten Tag findet niemand die Stelle wieder, an der alle gelacht haben." }, + { schluessel: "mk-live-eskalation", gruppe: "waehrend", kategorie: "chat", rolle: "Modi (Senior)", tage: 1, + titel: "Regelverstöße nach der Eskalationsregel behandeln", + text: "Nach der Regel, nicht nach Gefühl. Im Chat gewinnt niemand – und eine begründete Sperre diskutiert man hinterher." }, + + /* ---------- Phase 3 — Nach dem Live -------------------------------- */ + { schluessel: "mk-nach-aufzeichnung", gruppe: "nach", kategorie: "technik", rolle: "DogFather", tage: 1, + titel: "Aufzeichnung sichern und ablegen", + text: "Noch am selben Abend. Eine Aufzeichnung, die nur auf dem Sendegerät liegt, ist keine Sicherung." }, + { schluessel: "mk-nach-clips", gruppe: "nach", kategorie: "clipping", rolle: "Modi (Clipping)", tage: 2, + titel: "Clips schneiden und veröffentlichen", + text: "Unter 60 Sekunden, Untertitel drauf. Der größte Teil wird ohne Ton geschaut – was dann nicht ankommt, kommt gar nicht an." }, + { schluessel: "mk-nach-feedback", gruppe: "nach", kategorie: "kommunikation", rolle: "alle", tage: 1, + titel: "Kurzes Team-Feedback im Team-Chat", + text: "Eine Sache, die gut lief, eine, die man ändert. Wer sich fünf vornimmt, ändert keine." }, + { schluessel: "mk-nach-zahlen", gruppe: "nach", kategorie: "analyse", rolle: "DogFather & rechte Hand", tage: 2, + titel: "Statistiken und Reichweite durchsehen", + text: "Vor allem die Verweildauer. Über 15 Minuten heißt: Die Stimmung trägt. Darunter liegt es fast nie am Thema, sondern am Anfang." }, + + /* ---------- Phase 4 — Content & Reichweite ------------------------- */ + { schluessel: "mk-momente-sichten", gruppe: "content", kategorie: "clipping", rolle: "Modi (Clipping)", tage: 1, + titel: "Beste Live-Momente sichten und auswählen", + text: "Nach jedem Live, solange man noch weiß, wo es gut war. Drei Kandidaten reichen." }, + { schluessel: "mk-clips-schneiden", gruppe: "content", kategorie: "clipping", rolle: "Modi (Clipping)", tage: 3, + titel: "Clips schneiden, Untertitel und Cover hinzufügen", + text: "Der erste Satz entscheidet. Über 70 Prozent steigen in den ersten drei Sekunden aus oder bleiben." }, + { schluessel: "mk-redaktionsplan", gruppe: "content", kategorie: "planung", rolle: "rechte Hand", tage: 7, + titel: "Redaktionsplan für die Woche befüllen", + text: "Thema, Kanal, Zeitpunkt – je Eintrag eine Zeile. Eine Idee ohne Termin ist eine Notiz." }, + { schluessel: "mk-veroeffentlichung-planen", gruppe: "content", kategorie: "social", rolle: "Modi (Social Media)", tage: 3, + titel: "Veröffentlichung auf den Plattformen planen", + text: "TikTok, Instagram, YouTube Shorts – nicht alle zur selben Minute. Dieselbe Sache dreimal gleichzeitig konkurriert mit sich selbst." }, + { schluessel: "mk-kennzahlen", gruppe: "content", kategorie: "analyse", rolle: "rechte Hand", tage: 7, + titel: "Reichweiten-Kennzahlen je Plattform erfassen", + text: "Immer dieselben Zahlen, immer am selben Wochentag. Wechselnde Zahlen ergeben eine Erzählung, keinen Vergleich." }, + { schluessel: "mk-bestformate", gruppe: "content", kategorie: "analyse", rolle: "DogFather & rechte Hand", tage: 30, + titel: "Bestlaufende Formate und Zeiten heraussuchen", + text: "Die drei besten und die drei schlechtesten nebeneinanderlegen. Der Unterschied steht meistens in den ersten Sekunden." }, + { schluessel: "mk-ideen-uebernehmen", gruppe: "content", kategorie: "planung", rolle: "rechte Hand", tage: 7, + titel: "Ideen aus dem Ideen-Board in den Redaktionsplan überführen", + text: "Wöchentlich zwei. Ein Ideen-Board, aus dem nie etwas herausgeht, ist ein Friedhof." }, + { schluessel: "mk-crosspromo", gruppe: "content", kategorie: "social", rolle: "Modi (Social Media)", tage: 30, + titel: "Cross-Promotion zwischen den Kanälen prüfen", + text: "Von TikTok zu YouTube, nicht umgekehrt. Wer kurz schaut, klickt weiter; wer lang schaut, ist schon da." }, + + /* ---------- Phase 5 — Jede Woche ----------------------------------- */ + { schluessel: "mk-woche-kalender", gruppe: "woche", kategorie: "planung", rolle: "rechte Hand", tage: 7, + titel: "Wochenübersicht der Live-Termine aktualisieren", + text: "Am selben Wochentag, bevor die Woche anfängt. Ein Kalender, der der Woche hinterherläuft, wird nicht mehr gelesen." }, + { schluessel: "mk-woche-meeting", gruppe: "woche", kategorie: "team", rolle: "DogFather & rechte Hand", tage: 7, + titel: "Kurze Abstimmung mit allen aktiven Modis", + text: "Fünfzehn Minuten, feste Uhrzeit. Ein Termin, der jede Woche verschoben wird, findet nicht statt." }, + { schluessel: "mk-woche-offene", gruppe: "woche", kategorie: "organisation", rolle: "rechte Hand", tage: 7, + titel: "Offene Aufgaben aus der Vorwoche nachverfolgen", + text: "Erledigen, verschieben oder abbrechen – aber entscheiden. Eine Aufgabe, die dreimal mitwandert, wird nie gemacht." }, + { schluessel: "mk-woche-trial", gruppe: "woche", kategorie: "team", rolle: "DogFather & rechte Hand", tage: 7, + titel: "Modis in der Probezeit bewerten", + text: "Verlängern, aufnehmen oder absagen. Eine Probezeit ohne Ende ist keine, und beide Seiten wissen es." }, + { schluessel: "mk-woche-redaktion", gruppe: "woche", kategorie: "planung", rolle: "rechte Hand", tage: 7, + titel: "Redaktionsplan für die kommende Woche abschließen", + text: "Vor dem Wochenende fertig. Wer montags plant, dreht mittwochs." }, + + /* ---------- Phase 5 — Jeden Monat ---------------------------------- */ + { schluessel: "mk-monat-bericht", gruppe: "monat", kategorie: "analyse", rolle: "DogFather & rechte Hand", tage: 30, + titel: "Reichweiten- und Wachstumsbericht erstellen", + text: "Follower, Aufrufe, Beteiligung – dieselben drei, jeden Monat am selben Tag. Ein Bericht mit wechselnden Zahlen ist eine Erzählung." }, + { schluessel: "mk-monat-feedback", gruppe: "monat", kategorie: "team", rolle: "DogFather", tage: 30, + titel: "Rückmeldung der Modis einholen", + text: "Einzeln fragen, nicht in der Runde. In der Runde sagt niemand, was ihn stört." }, + { schluessel: "mk-monat-regeln", gruppe: "monat", kategorie: "organisation", rolle: "DogFather", tage: 30, + titel: "Regeln und Eskalationsregeln nachziehen", + text: "Nur, wenn ein Fall dazu Anlass gab. Regeln, die sich monatlich ändern, kennt niemand auswendig." }, + { schluessel: "mk-monat-shoutouts", gruppe: "monat", kategorie: "team", rolle: "DogFather", tage: 30, + titel: "Wertschätzung für besonders aktive Modis planen", + text: "Öffentlich und konkret. \"Danke euch allen\" erreicht niemanden." }, + { schluessel: "mk-monat-formate", gruppe: "monat", kategorie: "planung", rolle: "DogFather & rechte Hand", tage: 30, + titel: "Große Content-Formate für den nächsten Monat planen", + text: "Eines, nicht drei. Ein großes Format, das stattfindet, schlägt drei, die verschoben werden." }, + + /* ---------- Phase 6 — Community & Wachstum ------------------------- */ + { schluessel: "mk-umfrage", gruppe: "community", kategorie: "community", rolle: "Modi (Community)", tage: 30, + titel: "Community-Umfrage zu neuen Formaten starten", + text: "Höchstens drei Antwortmöglichkeiten. Bei acht entscheidet die Reihenfolge, nicht die Meinung." }, + { schluessel: "mk-event", gruppe: "community", kategorie: "events", rolle: "rechte Hand", tage: 30, + titel: "Community-Event planen", + text: "Zuschauer-Challenge, Q&A-Live, gemeinsamer Abend. Termin zuerst, Inhalt danach – sonst wird nie einer daraus." }, + { schluessel: "mk-stammzuschauer", gruppe: "community", kategorie: "community", rolle: "Modi", tage: 7, + titel: "Stammzuschauer erkennen und wertschätzen", + text: "Eine kurze Liste führen. Wiedererkannt zu werden ist der halbe Grund wiederzukommen." }, + { schluessel: "mk-willkommen", gruppe: "community", kategorie: "community", rolle: "Modi", tage: 7, + titel: "Willkommens-Routine für neue Mitglieder pflegen", + text: "Ein Satz, der nicht nach Textbaustein klingt. Wer merkt, dass ein Automat schreibt, antwortet nicht." }, + { schluessel: "mk-feedbackkanal", gruppe: "community", kategorie: "community", rolle: "rechte Hand", tage: 7, + titel: "Feedback-Kanal für Community-Wünsche auswerten", + text: "Und zurückmelden, was daraus wurde – auch ein Nein. Ein Wunschkanal ohne Antwort verstummt." }, + { schluessel: "mk-kooperation", gruppe: "community", kategorie: "wachstum", rolle: "DogFather", tage: 30, + titel: "Zusammenarbeit mit anderen Creators prüfen", + text: "Ähnliche Größe, andere Community. Wer viel größer ist, bringt Zuschauer, die nicht bleiben." }, + { schluessel: "mk-richtlinien", gruppe: "community", kategorie: "community", rolle: "Modi (Senior)", tage: 7, + titel: "Community-Richtlinien verständlich kommunizieren", + text: "In eigenen Worten, nicht als Rechtstext. Regeln, die man erklären muss, werden nicht befolgt." }, +]; + export const AUFGABEN_VORLAGEN = { live: { anfaenger: [ @@ -1127,6 +1382,20 @@ vorlagenRouter.get("/workspace/api/vorlagen", (req, res) => { technik: TECHNIK_PUNKTE, calls: CALL_LEITFAEDEN, aufgaben: ohneKatalog ? null : AUFGABEN_VORLAGEN, + /* DER KATALOG AUS TEIL 2 DES ANFORDERUNGSDOKUMENTS. + + "katalog" und nicht der Rollenname -- weder hier noch im Browser + darf stehen, fuer wen er gedacht ist. Wer ihn nicht bekommt, sieht + `null`, und die Oberflaeche baut dann nichts. Ein leeres Feld sagt + nichts; ein Feld namens "modi_katalog" saehe jeder im + Netzwerk-Protokoll seines eigenen Browsers. + + An denselben Personenkreis wie die Kategorien -- es ist dieselbe + Frage, und zwei Abfragen dafuer waeren zwei Gelegenheiten, dass + sie auseinanderlaufen. */ + katalog: kategorienFuer(req.person).length + ? { gruppen: MODI_GRUPPEN, aufgaben: MODI_KATALOG } + : null, }); }); @@ -1245,6 +1514,64 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, angelegt.push(v.titel); } + } else if (art === "katalog" || art === "katalog_alle") { + /* ============================================================= + DER KATALOG AUS TEIL 2 (10.09.2026) + + Wie der Zweig darueber legt er echte Aufgaben an, nicht + Eintraege. Zwei Unterschiede, und beide haben einen Grund: + + WER DIE AUFGABE BEKOMMT. Bei den Creator-Vorlagen ist es der + gewaehlte Creator. Hier ist es die Person selbst, wenn sie an + eigenen Aufgaben arbeitet -- sonst muesste sie sich in einer + Auswahl selbst heraussuchen, um sich etwas vorzunehmen. Traegt + jemand fuer einen anderen ein (das kann nur die + DogFather-Rolle), muss dieser andere ausdruecklich genannt + werden. + + DIE KATEGORIE WANDERT MIT. Sie steht am Katalogeintrag und ist + der halbe Wert der Vorlage: Ohne sie muesste man 60-mal von + Hand einsortieren, was im Dokument bereits danebensteht. */ + if (!kategorienFuer(req.person).length) { + /* Wortgleich mit einer erfundenen Art -- wer den Katalog nicht + bekommt, soll nicht erfahren, dass es ihn gibt. */ + return res.status(400).json({ fehler: "Unbekannte Art." }); + } + const zielId = req.person.rolle === "modi" + ? req.person.id + : Number(req.body.person_id) || null; + if (!zielId) return res.status(400).json({ fehler: "Bitte zuerst eine Person wählen." }); + /* Nur an jemanden, fuer den die Kategorien ueberhaupt gelten -- + sonst landete eine Modi-Aufgabe samt Kategorie an einem + Creator, und die Trennung waere von innen aufgeweicht. */ + const zielPerson = db().prepare( + "SELECT id, rolle FROM personen WHERE id = ? AND aktiv = 1").get(zielId); + if (!zielPerson || zielPerson.rolle !== "modi") { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + + const gruppe = String(req.body.gruppe || ""); + const auswahl = art === "katalog_alle" + ? MODI_KATALOG.filter((v) => v.gruppe === gruppe) + : [MODI_KATALOG[nr]]; + if (!auswahl.length || auswahl.some((v) => !v)) { + return res.status(400).json({ fehler: "Unbekannte Vorlage." }); + } + for (const v of auswahl) { + /* Frist in ORTSZEIT gerechnet. Ueber toISOString kaeme + zwischen Mitternacht und zwei Uhr der Vortag heraus. */ + const d = new Date(Date.now() + (v.tage || 7) * 86400_000); + const frist = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` + + `-${String(d.getDate()).padStart(2, "0")}`; + db().prepare(`INSERT INTO aufgaben + (titel, beschreibung, status, prioritaet, verantwortlich_id, frist, + kategorie, vorlage, erstellt, erstellt_von) + VALUES (?,?,'offen',?,?,?,?,?,?,?)`) + .run(v.titel, v.text, v.tage <= 1 ? "hoch" : "mittel", + zielPerson.id, frist, v.kategorie, v.schluessel, jetzt(), req.person.id); + angelegt.push(v.titel); + } + } else { return res.status(400).json({ fehler: "Unbekannte Art." }); } diff --git a/server/workspace.js b/server/workspace.js index a2804c32..3c3877e3 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -305,33 +305,44 @@ 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: + Es sind die des Anforderungsdokuments -- alle dreizehn, die der + Aufgabenkatalog in Teil 2 tatsaechlich benutzt, dazu "Sonstiges" aus + Kapitel 6.1. - Branding, Team, Kommunikation -> Planung & Organisation - Wachstum -> Community & Events + BEIM ERSTEN ANLAUF WAREN ES ACHT, mit der Begruendung, dreizehn + Knoepfe seien auf einem Handy unbedienbar. Das war eine plausible + Erklaerung fuer etwas Falsches: Gebaut ist ein AUSWAHLFELD, keine + Knopfleiste -- vierzehn Eintraege darin sind voellig unauffaellig. + Die Zusammenfassung haette eine Uebersetzungstabelle noetig gemacht + ("Branding gehoert zu Planung"), die spaeter niemand nachvollziehen + kann. Jetzt steht an jeder Aufgabe die Kategorie, die im Dokument + danebensteht. - Das ist eine Entscheidung und keine Uebersetzung. Wer sie spaeter - anders will, aendert sie hier, an einer Stelle. + DIE REIHENFOLGE IST DIE DER HAEUFIGKEIT, nicht die des Dokuments: + Was taeglich vorkommt, steht oben. Wer eine Kategorie sucht, soll + nicht scrollen muessen. 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. */ + die Woerter etwas verraten wuerden -- "Clipping" sagt nichts --, + 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: "community", name: "Community" }, + { wert: "events", name: "Events" }, + { wert: "clipping", name: "Clipping" }, { 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: "planung", name: "Planung" }, + { wert: "organisation", name: "Organisation" }, + { wert: "team", name: "Team" }, + { wert: "kommunikation", name: "Kommunikation" }, + { wert: "analyse", name: "Analyse" }, + { wert: "wachstum", name: "Wachstum" }, + { wert: "branding", name: "Branding" }, { wert: "sonstiges", name: "Sonstiges" }, ]; - /** * Darf diese Person mit Kategorien arbeiten? * diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 5855f0cb..4d46125f 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=202609100105", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609100119", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609100105", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609100119", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100105", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100119", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100105", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100119", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index 549e91ca..8934c4ae 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -994,6 +994,15 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe hilfe: 'Was auch ohne dich weiterläuft — Vorlage, Vertretung, Ablage.' }, ]; let vorlagen = null; + /* Der zweite Katalog -- welcher es ist, sagt der Server. Hier steht + bewusst nur "katalog": Im Browser darf nichts stehen, woraus sich + schliessen laesst, fuer wen er gedacht ist. */ + let katalog = null; + let kGruppe = null; + /* Getrennt vom Inhalt, weil "nichts bekommen" ein gueltiges Ergebnis + ist. Ohne dieses Merkmal fragte die Seite bei jedem Neuzeichnen + erneut nach -- und bekaeme jedes Mal wieder nichts. */ + let vorlagenGeladen = false; let vBereich = 'live'; let vStufe = 'anfaenger'; @@ -1027,13 +1036,24 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe nichts, und `if (!vorlagen) return` weiter unten laesst das Brett verborgen. Dieselbe Wirkung, ohne eine Zeile, die verraet, fuer wen sie gilt. */ - if (!vorlagen) { + if (!vorlagenGeladen) { try { const a = await hole('/workspace/api/vorlagen'); if (!a.ok) return; - vorlagen = (await a.json()).aufgaben || null; + const d = await a.json(); + vorlagen = d.aufgaben || null; + /* Ein Katalog mit null Eintraegen ist keiner. Ohne diese Pruefung + haette ein leeres `{gruppen:[],aufgaben:[]}` einen Block mit + Ueberschrift und nichts darin ergeben -- das sieht aus wie ein + Fehler, und es waere auch einer. */ + katalog = d.katalog && Array.isArray(d.katalog.aufgaben) && d.katalog.aufgaben.length + ? d.katalog : null; + vorlagenGeladen = true; } catch { return; } } + /* Zwei Kataloge, einer je Person -- nie beide. Wer den einen + bekommt, bekommt den anderen nicht; das entscheidet der Server. */ + if (katalog) { katalogBrett(ziel); return; } if (!vorlagen) return; ziel.hidden = false; ziel.textContent = ''; @@ -1194,6 +1214,146 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe } } + + /* ===================================================================== + DAS ZWEITE VORLAGENBRETT (10.09.2026) + + Derselbe Block, derselbe Kopf, dieselben Karten -- nur eine andere + Einteilung: neun Etappen statt vier Bereichen mal vier Stufen. Die + Bausteine sind bewusst dieselben (vorlagenbrett__karte, + stufenleiter, ...): Ein zweites Aussehen fuer dieselbe Sache waere + zwei Dinge zum Lernen statt einem -- und eine zweite Stelle, an der + jemand eine neue CSS-Klasse in sieben Kopien pflegen muesste. + + WAS DRINSTEHT, KOMMT VOM SERVER, samt Gruppennamen und Texten. In + dieser Datei steht kein Wort darueber, fuer wen dieser Katalog ist. + ===================================================================== */ + function katalogBrett(ziel) { + ziel.hidden = false; + ziel.textContent = ''; + if (!kGruppe) kGruppe = (katalog.gruppen[0] || {}).schluessel || null; + + const kopf = el('button', 'vorlagenbrett__kopf'); + kopf.type = 'button'; + kopf.setAttribute('aria-expanded', String(vOffen)); + kopf.setAttribute('aria-controls', 'vorlagenbrett-koerper'); + const kText = el('span', 'vorlagenbrett__kopftext'); + kText.append(el('span', 'feldschild', 'Aufgaben aus der Vorlage')); + kText.append(el('span', 'vorlagenbrett__unter', vOffen + ? 'Nach Etappen sortiert. Übernommen wird als ganz normale Aufgabe ' + + '– mit Frist und Kategorie, veränderbar, auf dem Brett darunter.' + : `${katalog.aufgaben.length} fertige Aufgaben in ` + + `${katalog.gruppen.length} Etappen – zum Übernehmen.`)); + kopf.append(kText); + const pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + pfeil.setAttribute('viewBox', '0 0 24 24'); + pfeil.setAttribute('class', 'klapp-pfeil'); + pfeil.setAttribute('aria-hidden', 'true'); + const pfad = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + pfad.setAttribute('d', 'm6 9.5 6 6 6-6'); + pfeil.append(pfad); + kopf.append(pfeil); + kopf.addEventListener('click', () => { + vOffen = !vOffen; + try { localStorage.setItem(VORLAGEN_OFFEN, vOffen ? 'ja' : 'nein'); } catch { /* egal */ } + vorlagenbrettZeigen(); + }); + ziel.append(kopf); + ziel.dataset.auf = vOffen ? 'ja' : 'nein'; + /* Zugeklappt wird der Rest gar nicht erst gebaut -- sonst lägen bei + jedem Neuzeichnen sechzig Karten da, die niemand sieht. */ + if (!vOffen) return; + + const koerper = el('div', 'vorlagenbrett__koerper'); + koerper.id = 'vorlagenbrett-koerper'; + ziel.append(koerper); + + /* Die Etappen als Reihe -- mit der Zahl darin, damit man sieht, wo + etwas liegt, bevor man klickt. */ + const reihe = el('div', 'stufenleiter'); + reihe.setAttribute('role', 'group'); + reihe.setAttribute('aria-label', 'Etappe wählen'); + for (const g of katalog.gruppen) { + const n = katalog.aufgaben.filter((v) => v.gruppe === g.schluessel).length; + if (!n) continue; + const k = el('button', 'stufenleiter__knopf'); + k.type = 'button'; + k.dataset.stufe = g.schluessel; + k.setAttribute('aria-pressed', String(kGruppe === g.schluessel)); + k.append(el('span', 'stufenleiter__name', g.name)); + k.append(el('span', 'stufenleiter__zahl', String(n))); + k.addEventListener('click', () => { kGruppe = g.schluessel; vorlagenbrettZeigen(); }); + reihe.append(k); + } + koerper.append(reihe); + + const gruppe = katalog.gruppen.find((g) => g.schluessel === kGruppe); + if (gruppe?.text) koerper.append(el('p', 'vorlagenbrett__stand', gruppe.text)); + + /* Was schon auf dem Brett liegt -- aus den Aufgaben, die ohnehin + geladen sind. Verglichen wird die Kennung der Vorlage und nicht + der Titel: Der darf geändert werden. */ + const schonDa = new Map(); + for (const a of alleAufgaben) if (a.vorlage) schonDa.set(a.vorlage, a); + + const liste = katalog.aufgaben + .map((v, nr) => ({ ...v, nr })) + .filter((v) => v.gruppe === kGruppe); + const box = el('div', 'vorlagenbrett__liste'); + for (const v of liste) { + const karte = el('div', 'vorlagenbrett__karte'); + const schon = schonDa.get(v.schluessel); + if (schon) karte.dataset.schon = 'ja'; + karte.append(el('h3', 'vorlagenbrett__titel', v.titel)); + karte.append(el('p', 'vorlagenbrett__text', v.text)); + const fuss = el('div', 'vorlagenbrett__fuss'); + fuss.append(el('span', 'vorlagenbrett__frist', + v.tage === 1 ? 'Frist: morgen' : `Frist: in ${v.tage} Tagen`)); + /* Die empfohlene Rolle steht im Dokument neben jeder Aufgabe und + ist die halbe Auskunft: "Technik-Check" heisst etwas anderes, + je nachdem, wer ihn macht. */ + if (v.rolle) fuss.append(el('span', 'vorlagenbrett__frist', `für: ${v.rolle}`)); + if (schon) { + fuss.append(el('span', 'vorlagenbrett__schon', + `schon übernommen · ${STATUS_NAME[schon.status] || schon.status}`)); + } + const k = el('button', 'knopf knopf--klein', schon ? 'Nochmal' : 'Übernehmen'); + k.type = 'button'; + if (schon) k.title = 'Diese Vorlage liegt schon auf dem Brett – noch einmal übernehmen?'; + k.addEventListener('click', () => uebernehmen(k, + { art: 'katalog', nr: v.nr, gruppe: kGruppe, person_id: katalogZiel() })); + fuss.append(k); + karte.append(fuss); + box.append(karte); + } + + if (liste.length) { + const offen = liste.filter((v) => !schonDa.has(v.schluessel)).length; + koerper.append(el('p', 'vorlagenbrett__stand', offen + ? `${offen} von ${liste.length} noch nicht übernommen.` + : `Alle ${liste.length} dieser Etappe sind übernommen.`)); + } + koerper.append(box); + + if (liste.length > 1) { + const alle = el('button', 'abmelden', `Alle ${liste.length} übernehmen`); + alle.title = 'Legt alle Aufgaben dieser Etappe an – auch die, die schon auf dem Brett liegen.'; + alle.type = 'button'; + alle.style.marginTop = '12px'; + alle.addEventListener('click', () => uebernehmen(alle, + { art: 'katalog_alle', gruppe: kGruppe, person_id: katalogZiel() })); + koerper.append(alle); + } + } + + /* FUER WEN wird uebernommen? Wer nur eigene Aufgaben hat, meint sich + selbst -- der Server setzt das dann ein und braucht hier nichts. + Wer fuer andere eintragen darf, muss die Person oben ausgewaehlt + haben; sonst sagt der Server es. */ + function katalogZiel() { + return $('f-verant')?.value || $('f-creator')?.value || null; + } + /* Ein Weg fuer beide Knoepfe -- einzeln und alle. Zwei fast gleiche Funktionen waeren der sichere Weg dazu, dass die eine irgendwann anders reagiert als die andere. */ diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index de262b82..ed1a6be7 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -38,6 +38,27 @@ mit, auch was es heute noch gar nicht gibt. */ if (ich.rolle) document.documentElement.dataset.rolle = ich.rolle; + /* DER NAME IN DER KOPFLEISTE (10.09.2026). + + Auf jeder Seite steht dort fest "Creator Workspace · ". + Fuer wen das nicht stimmt, schickt der Server einen anderen Namen + mit -- WER das ist, steht ausdruecklich nicht in dieser Datei: + Sie bekommt jeder, der die Seite oeffnet. + + Nur der Verweis wird umgeschrieben, nicht die ganze Zeile: Hinter + ihm steht noch " · Aufgaben" bzw. der Name der jeweiligen Seite, + und der bleibt richtig. + + MIT GESCHUETZTEM LEERZEICHEN, wie im HTML. Die Kopfleiste rechnet + damit, dass dieser Name nicht umbricht -- ein gewoehnliches + Leerzeichen liesse ihn auf schmalen Handys in zwei Zeilen fallen + und die Leiste mitwachsen. */ + if (ich.marke) { + for (const verweis of document.querySelectorAll('.kopfleiste .marke__text .zurueck')) { + verweis.textContent = String(ich.marke).replace(/ /g, ' '); + } + } + /* Das eigene Bild, wenn es eines gibt -- sonst der Anfangsbuchstabe. Der Buchstabe bleibt als Unterlage LIEGEN und wird nicht ersetzt: Laedt das Bild nicht (Verbindung weg, Datei geloescht), steht dort diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 71db8113..02598c58 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 ad8f867c..bedb2eb9 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 240a9b2c..fa7e490a 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 8a8658ce..81db71b7 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 c96800de..ff8058e7 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 41ecc6c8..a1f309e1 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 5bac82e6..94240fc0 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 29f678af..ef07cef5 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 a322e4bf..cdc65df0 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 f0eebd53..140b28e4 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 cccd1002..bec02eda 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 dd891c55..849d3429 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 a5c25bde..5bc9ebb5 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 81708418..9a089d75 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 9da8af96..f91d4de0 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 6445e49b..11c5a59f 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 a5c31ee2..4748065c 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 c96517e8..796ba81a 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 cb376c81..e7e29d92 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 ee067569..1eb19f21 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 @@ - - - - - + + + + +