From 114a00eed5021633f0707386cb35b6a1e7c4a24c Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 10 Sep 2026 00:52:06 +0200 Subject: [PATCH] Der Modi bekommt seine eigene Seite -- und sein Brett zurueck DER WICHTIGSTE FUND, und er kam nicht aus dem Nachdenken: Ein Modi sah auf dem Aufgabenbrett GAR NICHTS -- nicht einmal seine eigenen Aufgaben. sichtbar() endet mit `default: 0=1`, und 'modi' stand nicht darin. Genau dieser Fehler ist am 01.09.2026 schon dem Manager passiert; der Kommentar zwei Zeilen darueber warnt woertlich davor ("Ein leeres Brett sieht aus wie 'nichts zu tun', nicht wie ein Fehler; deshalb ist das vermutlich lange niemandem aufgefallen"). Der Rollen-Rundgang meldete fuer den Modi trotzdem brav "aufgaben.html ok" -- die Seite laedt ja, sie war nur leer. Gefunden hat es erst eine Pruefung, die eine Aufgabe ANLEGT und sie danach wiederzufinden versucht. Ein Modi sieht jetzt die Aufgaben des ganzen Modi-Teams (Filipes Entscheidung "sie sind untereinander ein Team"), aendern darf er weiterhin nur seine eigenen. Die Nummern werden bei jeder Abfrage frisch gelesen -- eine beim Serverstart gebaute Liste waere ab dem naechsten neuen Modi falsch, und niemand wuesste warum. DIE STARTSEITE. Ein Modi hatte keine einzige Kachel: Jede traegt eine feste Rollenliste, und 'modi' darf dort nicht stehen -- bereiche.js bekommt jeder ausgeliefert, der die Seite oeffnet. Die Kacheln kommen deshalb vom Server (MODI_BEREICHE), samt Beschriftung. Nur die Ziele zu schicken haette nicht gereicht: Unter "Dashboard" stuende sonst "Alle Creator auf einen Blick" -- fuer jemanden ohne Creator. Die Worte gehoeren zum Empfaenger, nicht zum Ziel. Neun Kacheln in drei Gruppen: Aufgaben, Chat, Kalender, Dateien / Live-Ablauf, Community, Technik / Profil, Wissen. Nichts aus der Agentur -- diese Seiten drehen sich um betreute Creator oder um Rechte. `null` heisst "nimm deine eigene Liste", eine LEERE Liste hiesse "keine Kacheln". Verwechselte man die beiden, haetten die fuenf bekannten Rollen ab sofort eine leere Startseite. ZWEI DINGE HAT DAS BILDSCHIRMFOTO GEZEIGT, NICHT DER CODE: * Ueber der Modi-Startseite stand "Spicy Media" -- die Marke einer Agentur, mit der er nichts zu tun hat. Jetzt "Team Dogi", wie auf der oeffentlichen Seite. Ersetzt wird nur der Textknoten: In dem Element sitzen zwei Zierrauten, ein textContent haette sie lautlos geloescht. * Auf seinem Aufgabenbrett stand das Creator-Vorlagenbrett, 80 Aufgaben fuer den Aufbau eines Kanals. Fuer einen Moderator ist davon nichts gedacht. Ausgeblendet, bis sein Katalog aus Teil 2 des Anforderungsdokuments da ist -- nichts ist ehrlicher als etwas Fremdes. Dazu: "0 betreut" stand dauerhaft auf seiner Startseite, eine Zahl, die nie etwas anderes sagen kann. Jetzt zaehlt sie, wie viele im Modi-Team sind. Und der Satz unter der Begruessung war nur das Wort "Modi", neben fuenf Rollen mit einem ganzen Satz -- das sah nicht verborgen aus, sondern unfertig. KATEGORIEN (Kapitel 6.1), nach Filipes Entscheidung nur bei den Modis. Acht Stueck; hier steht, wohin die dreizehn aus Teil 2 fallen (Branding/Team/Kommunikation -> Planung, Wachstum -> Community). Der heikelste Fall ist nicht das Setzen, sondern das SCHICKEN durch jemanden, der es nicht darf: Eine Absage ("Unbekannte Kategorie") waere die Auskunft, dass es das Feld gibt. Also faellt der Wert lautlos weg und die Aufgabe entsteht ganz normal. Wer die Kategorien benutzen darf, bekommt bei einem Tippfehler dagegen sehr wohl eine Absage. Das Feld erscheint nur, wenn die Aufgabe wirklich zu einem Modi gehoert -- bei DogFather also erst, wenn er einen als Person auswaehlt. Sonst stuende es auch an jeder Creator-Aufgabe. Verborgen heisst dabei auch "nichts mitschicken": Ein Wert in einem unsichtbaren Feld wandert sonst beim naechsten Speichern mit. KEINE NEUE CSS-KLASSE fuer die Kategorie auf der Karte. Sie muesste in sieben gleichlautenden Kopien der Modulliste gepflegt werden -- sieben Gelegenheiten fuer einen Unterschied, fuer eine Zeile Text. AUSSERDEM BERICHTIGT, UND ES WAR SCHON VORHER ROT: pruef-start-ansicht erwartete drei Kachelgruppen. Seit b45de94 gibt es vier ("Rund um das Team"). Bevor ich die Zahl angefasst habe, habe ich meine Aenderungen beiseitegelegt und den Lauf wiederholt -- schon auf dem unveraenderten Stand rot, also nicht von mir. Geprueft werden jetzt die NAMEN: Vier Gruppen koennten auch drei richtige und eine doppelte sein. GEPRUEFT: pruef-modi-verborgen (57), pruef-modi-kategorien (22, neu), pruef-rollen (113 statt 97 -- der Modi laeuft jetzt ueber jede der 16 Seiten), pruef-start-ansicht, pruef-aufgabenbrett, pruef-sicht, pruef-verborgen, pruef-personen-formular (24), pruef-personen-liste, pruef-css-klassen, pruef-spicy (60). Co-Authored-By: Claude Opus 5 --- server/pruef-modi-kategorien.mjs | 305 +++++++++++++++++++++++++++++++ server/pruef-modi-verborgen.mjs | 107 ++++++++++- server/pruef-rollen.mjs | 31 +++- server/pruef-start-ansicht.mjs | 20 +- server/workspace-aufgaben.js | 90 ++++++++- server/workspace-zentrale.js | 18 +- server/workspace.js | 195 ++++++++++++++++++++ workspace/app.webmanifest | 8 +- workspace/assets/js/aufgaben.js | 114 +++++++++++- workspace/assets/js/start.js | 80 +++++++- workspace/aufgaben.html | 36 ++-- workspace/automation.html | 24 +-- workspace/bereich.html | 32 ++-- workspace/calls.html | 26 +-- workspace/chat.html | 24 +-- workspace/content.html | 28 +-- workspace/dateien.html | 24 +-- workspace/index.html | 8 +- workspace/kalender.html | 26 +-- workspace/leistung.html | 24 +-- workspace/personen.html | 24 +-- workspace/profil.html | 26 +-- workspace/report.html | 26 +-- workspace/scouting.html | 24 +-- workspace/start.html | 22 +-- workspace/startcheck.html | 26 +-- workspace/steckbrief.html | 24 +-- workspace/team.html | 24 +-- workspace/uebersicht.html | 24 +-- workspace/wissen.html | 24 +-- 30 files changed, 1196 insertions(+), 268 deletions(-) create mode 100644 server/pruef-modi-kategorien.mjs 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 @@ - - - - - + + + + +