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:
@@ -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);
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user