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 <[email protected]>
This commit is contained in:
2026-09-10 00:52:06 +02:00
co-authored by Claude Opus 5
parent 7be488e0ca
commit 114a00eed5
30 changed files with 1196 additions and 268 deletions
+305
View File
@@ -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);
+105 -2
View File
@@ -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);
+28 -3
View File
@@ -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(
+19 -1
View File
@@ -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).
+83 -7
View File
@@ -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. */
+17 -1
View File
@@ -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) {
+195
View File
@@ -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),
});
});