From 05f262eb6117e99e91c6d1fba323ee23181ebd24 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 10 Sep 2026 12:52:45 +0200 Subject: [PATCH] Modis verteilen Aufgaben statt sie zu bekommen -- und Angebote zur Entscheidung Die drei Beurteilungs-Bereiche liefen bisher in die falsche Richtung: Sie bewerteten die Modis. Filipe: "die modis sind ja da um mir zu helfen." Also umgedreht. WAS SICH GEDREHT HAT - Alle 101 Punkte sind jetzt Beobachtungen ueber den Stream, nicht Pflichten des Modis ("Der Ton blieb verstaendlich" statt "Ton geprueft"). - Nur der Modi selbst drueckt auf seiner Liste. Wer sonst darauf zeigt, bekommt 403 und den Weg zur Team-Lage -- bewerten wird hier niemand. - "Verbessern" landet als Eingang bei DogFather. Ein Klick macht daraus eine Aufgabe mit dem Satz des Modis im Text, oder eine Absage mit Grund. Beides schreibt eine Nachricht zurueck, damit der Modi sieht: angekommen. ANGEBOTE Neuer Bereich, in dem Modis planen und vorschlagen: Nutzen und Aufwand statt Bewertung und Dringlichkeit, dazu ein Feld "was DogFather danach tun muss". Wird ein Angebot angenommen, entstehen zwei Aufgaben -- eine beim Modi zum Umsetzen, eine bei DogFather aus genau diesem Feld. GEMESSEN pruef-modi-checkliste 59 Pruefungen, 0 Fehler (neu geschrieben) pruef-rollen 245 statt 244 -- der Zuwachs ist die neue Angebote-Kachel, alle 11 Modi-Kacheln kommen an pruef-modi-ideen 30, pruef-modi-verborgen 75, pruef-bereiche-lesend: gruen Co-Authored-By: Claude Opus 5 --- server/crew-adresse.js | 162 +++++++++++ server/pruef-modi-checkliste.mjs | 391 +++++++++++++++++-------- server/workspace-bereiche.js | 70 ++++- server/workspace-checkliste.js | 100 ++++++- server/workspace-modi-punkte.js | 482 ++++++++++++++++--------------- server/workspace-teamlage.js | 268 ++++++++++++++++- server/workspace.js | 26 ++ workspace/app.webmanifest | 8 +- workspace/assets/js/bereich.js | 27 ++ workspace/assets/js/teamlage.js | 165 ++++++++++- workspace/aufgaben.html | 22 +- workspace/automation.html | 24 +- workspace/bereich.html | 44 +-- workspace/calls.html | 26 +- workspace/chat.html | 24 +- workspace/content.html | 28 +- workspace/dateien.html | 24 +- workspace/index.html | 8 +- workspace/kalender.html | 26 +- workspace/leistung.html | 24 +- workspace/personen.html | 24 +- workspace/profil.html | 26 +- workspace/report.html | 26 +- workspace/scouting.html | 24 +- workspace/start.html | 22 +- workspace/startcheck.html | 26 +- workspace/steckbrief.html | 24 +- workspace/team.html | 24 +- workspace/teamlage.html | 20 +- workspace/uebersicht.html | 24 +- workspace/wissen.html | 24 +- 31 files changed, 1583 insertions(+), 630 deletions(-) create mode 100644 server/crew-adresse.js diff --git a/server/crew-adresse.js b/server/crew-adresse.js new file mode 100644 index 00000000..6453dea8 --- /dev/null +++ b/server/crew-adresse.js @@ -0,0 +1,162 @@ +/* ===================================================================== + Die eigene Adresse der Modi-App (10.09.2026) + + crew.dogfather-universe.com + + WARUM EINE EIGENE ADRESSE. Derselbe Grund, aus dem der Creator + Workspace am 06.09.2026 umgezogen ist (siehe workspace-umzug.js): + Auf einem Ursprung laesst sich genau EINE App installieren. Zwei + Manifeste mit verschachtelten Bereichen sind laut w3c/manifest + Nr. 1180 "strongly not recommended", und Chrome haelt sich daran -- + der zweite Eintrag erscheint dann nur als Verknuepfung, nie als + eigene App. Filipe will fuer die Modis ausdruecklich eine eigene + App: "ich will dass es eine eigene app wird." + + WAS SICH NICHT AENDERT: der Pfad /workspace/, der Dienst (4100), die + Datenbank, der Code. Getrennt wird ueber den HOSTNAMEN, nicht ueber + einen zweiten Prozess. Genau so machen es Hauptseite und Workspace + schon heute. + + --------------------------------------------------------------------- + DREI FALLEN, DIE MAN EINER BEDINGUNG NICHT ANSIEHT + --------------------------------------------------------------------- + + 1. EIN MUSTER STATT EINER LISTE WAERE TOEDLICH. + /dogfather-universe\.com$/ passt auch auf + workspace.dogfather-universe.com -- und wuerde damit genau die + Adresse abschalten, die man schuetzen will. Deshalb hier zwei + feste Mengen und kein einziges Muster. (Dieselbe Ueberlegung + steht schon in workspace-umzug.js; sie hat dort einen Ausfall + verhindert und gilt hier unveraendert.) + + 2. "ALLES AUSSER crew." IST NICHT DASSELBE WIE "DIE ALTEN ADRESSEN". + Der naheliegende Satz waere: "Ein Modi darf ueberall rein, nur + nicht auf einer Adresse, die nicht crew heisst." Damit waeren + localhost, 127.0.0.1 und jede Pruefadresse eingeschlossen -- und + saemtliche Modi-Pruefungen wuerden ab sofort messen, dass ein + Modi nirgends hereinkommt, ohne dass irgendwo etwas rot wird. + Ein gruener Lauf, der nichts mehr prueft, ist schlimmer als ein + roter. Deshalb ist OHNE_MODI_HOSTS eine AUFZAEHLUNG der drei + echten Adressen. Wer eine vierte anlegt, traegt sie hier nach -- + und pruef-crew-adresse.mjs sagt es ihm, wenn er es vergisst. + + 3. "FAENGT AN MIT /workspace" TRIFFT AUCH /workspaceXYZ. + Beim Workspace-Umzug waere das beinahe passiert. Hier gilt es + genauso, deshalb dieselbe Schreibweise: genau "/workspace" oder + alles unter "/workspace/", sonst nichts. + + --------------------------------------------------------------------- + WARUM DIE ALTE ADRESSE FUER MODIS ZUGEHT + --------------------------------------------------------------------- + + Ein Modi, der sich weiterhin auf workspace.dogfather-universe.com + anmelden koennte, haette zwei Anmeldungen, zwei Sitzungen und zwei + Apps auf dem Startbildschirm -- und der verborgene Zugang haette + damit eine zweite Spur genau dort, wo alle anderen arbeiten. Er + bekommt deshalb auf der alten Adresse KEINE Sitzung, sondern den + Hinweis auf die neue. + + DASS DIESER HINWEIS DIE ADRESSE NENNT, IST KEIN LECK: Er erscheint + nur nach Eingabe eines gueltigen Modi-Codes. Wer den hat, ist Modi. + Fuer jeden anderen -- auch fuer jeden Manager mit gueltigem eigenen + Code -- veraendert sich an der alten Adresse ueberhaupt nichts. + + --------------------------------------------------------------------- + UND WARUM AUF DER NEUEN ADRESSE NUR MODIS HEREINKOMMEN + --------------------------------------------------------------------- + + Sonst waere crew. eine zweite, vollwertige Tuer in den Workspace -- + und die erste Person, die sie zufaellig errät und ihren eigenen Code + eintippt, wuesste sofort, dass es hier noch etwas gibt. Ein fremder + Code bekommt dort deshalb dieselbe Antwort wie ein erfundener: die + Zugangswand sieht aus wie immer, und der Code stimmt nicht. + ===================================================================== */ + +export const CREW_ADRESSE = "https://crew.dogfather-universe.com"; + +/** Die Adresse der Modi-App. Feste Menge, kein Muster -- Falle 1. */ +const CREW_HOSTS = new Set(["crew.dogfather-universe.com"]); + +/** Die echten Adressen, auf denen ein Modi nichts verloren hat. + * Aufzaehlung und ausdruecklich NICHT "alles ausser crew" -- Falle 2. */ +const OHNE_MODI_HOSTS = new Set([ + "dogfather-universe.com", + "www.dogfather-universe.com", + "workspace.dogfather-universe.com", +]); + +/** Nur der Name, ohne Port und in Kleinschreibung. + * Hostnamen sind laut RFC 4343 gross/klein-unempfindlich; ein Browser + * darf "Workspace.Dogfather-Universe.com" schicken. */ +function nurHost(hostKopfzeile) { + return String(hostKopfzeile || "").toLowerCase().split(":")[0]; +} + +/** Kommt die Anfrage ueber die Adresse der Modi-App? */ +export function istCrewAdresse(hostKopfzeile) { + return CREW_HOSTS.has(nurHost(hostKopfzeile)); +} + +/** Ist das eine der Adressen, auf denen Modis nicht mehr arbeiten? */ +export function istOhneModiAdresse(hostKopfzeile) { + return OHNE_MODI_HOSTS.has(nurHost(hostKopfzeile)); +} + +/** Gehoert dieser Pfad zum Workspace-Teil? Genau "/workspace" oder + * alles darunter -- aber NICHT /workspaceXYZ (Falle 3). */ +export function istWorkspacePfad(pfad) { + const p = String(pfad || ""); + return p === "/workspace" || p.startsWith("/workspace/"); +} + +/** + * Darf diese Person auf dieser Adresse angemeldet sein? + * + * Die eine Regel, aus der beide Richtungen folgen. Sie steht hier als + * eigene Funktion, damit sie ohne laufenden Server pruefbar ist -- und + * damit sie an GENAU EINER Stelle steht: sitzungLesen() in + * workspace.js. Jedes der 26 Fachmodule geht dort durch. + * + * @param {string} rolle Rolle der angemeldeten Person + * @param {string} hostKopfzeile Inhalt des Host-Kopffeldes + */ +export function sitzungPasstZurAdresse(rolle, hostKopfzeile) { + const modi = rolle === "modi"; + if (istCrewAdresse(hostKopfzeile)) return modi; // crew: nur Modis + if (istOhneModiAdresse(hostKopfzeile)) return !modi; // echte alte Adressen: keine Modis + return true; // localhost, Pruefadressen: unberuehrt +} + +/** + * Express-Zwischenschritt fuer die Adresse der Modi-App. + * + * Er macht zwei Dinge und ausdruecklich nichts mit Rechten -- die + * liegen in sitzungLesen(): + * + * 1. NICHT INDIZIEREN. Die Adresse taucht heute in keinem oeffentlichen + * Zertifikatsprotokoll auf (nachgemessen am 10.09.2026: crt.sh + * kennt fuer dogfather-universe.com nur den Platzhalter-Eintrag). + * Eine Suchmaschine waere der zweite Weg, sie doch zu finden -- + * der Kopf schliesst ihn. Er steht hier und nicht nur als + * im HTML, weil er dann auch fuer Bilder, Manifest und + * jede Antwort ohne HTML gilt. + * + * 2. NUR DER WORKSPACE. Auf crew. gibt es die Hauptseite nicht -- + * sonst laege die komplette Dogfather-Website ein zweites Mal + * unter einer zweiten Adresse (doppelter Inhalt, und ein Weg, + * die Adresse ueber eine Suchmaschine zu finden). Alles ausserhalb + * von /workspace und /assets geht zur Zugangswand. + * + * Caddy tut Punkt 2 bereits mit derselben Weiche. Dass es hier ein + * zweites Mal steht, ist Absicht: Eine Schranke, die nur in einer + * Datei ausserhalb des Projekts existiert, faellt beim naechsten + * Serverumbau lautlos weg. + */ +export function crewWeiche(req, res, next) { + if (!istCrewAdresse(req.get("host"))) return next(); + + res.setHeader("X-Robots-Tag", "noindex, nofollow"); + + if (istWorkspacePfad(req.path) || req.path.startsWith("/assets/")) return next(); + return res.redirect(302, "/workspace/"); +} diff --git a/server/pruef-modi-checkliste.mjs b/server/pruef-modi-checkliste.mjs index d2860e06..3fd79f7e 100644 --- a/server/pruef-modi-checkliste.mjs +++ b/server/pruef-modi-checkliste.mjs @@ -1,22 +1,24 @@ /* ===================================================================== - DIE CHECKLISTEN EINES MODIS + DIE MODIS MELDEN -- und DogFather und VanVan antworten - Wunsch Filipe (10.09.2026): *"diese kategorien mussen auf dieser - seite auch noch auf die modis perfektionniert werden, da mussen - lauter sachen sein die vanvan und ich danach verteilen können und da - anpassen können ob gut oder nicht."* + Wunsch Filipe (10.09.2026): *"diese 3 kategorien müssen so + eingestellt werden dass die modis mir quasi aufgaben verteilen, also + das sie drücken können passt so oder verbesserungen. weil die modis + sind ja da um mir zu helfen."* Und dazu: *"Erst ein Vorschlag, den du + annimmst."* - VIER FRAGEN: + DIE RICHTUNG WAR VORHER UMGEKEHRT, und diese Pruefung auch: Sie hat + geprueft, dass DogFather den Modi bewertet. Das war eine Note fuer + ihn statt einer Hilfe fuer DogFather -- und es widersprach der + Hausregel, nach der niemand ueber dem Team steht. - 1. Bekommt ein Modi SEINEN Satz -- nicht den der Creator? - 2. Sieht er seine EIGENE Liste, ohne jemanden auswaehlen zu muessen? - 3. Kann die DogFather-Rolle ihn auswaehlen und bewerten? - 4. Bekommt Spicy Media ihn NICHT in die Auswahl? + FUENF FRAGEN: - Die vierte ist die heikelste. `darfCreator` sagt bei siehtAlles() - pauschal ja -- und darin steckt auch Spicy Media. Wer die - Modi-Auswahl daran haengt, oeffnet sie ihr nebenbei mit, ohne dass an - der Stelle etwas davon steht. + 1. Beurteilt der Modi die SENDUNG -- und darf er druecken? + 2. Duerfen DogFather und VanVan seinen Bericht NICHT ueberschreiben? + 3. Landet ein "Verbessern" im Eingang der beiden? + 4. Wird daraus eine Aufgabe -- und merkt der Modi, dass es ankam? + 5. Bekommt Spicy Media von alledem nichts? ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -69,7 +71,7 @@ const idSpicy = anlegen("Cigdem", "spicy", "CODE-SPIC-0001"); const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001"); const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); d.close(); -void idDogi; +void idSpicy; async function anmelden(rolle, code) { const a = await fetch(BASIS + "/workspace/api/anmelden", { @@ -93,144 +95,281 @@ const vanAn = await anmelden("admin", "CODE-VAN-0001"); const spicyAn = await anmelden("spicy", "CODE-SPIC-0001"); const creatAn = await anmelden("creator", "CODE-ANNA-0001"); -/* ---------- 1. Sein eigener Satz ------------------------------------- */ -console.log("\n=== 1. Der Modi bekommt seinen eigenen Punktesatz ===\n"); +/* ---------- 1. Er beurteilt die Sendung ------------------------------ */ +console.log("\n=== 1. Der Modi beurteilt die Sendung ===\n"); const BEREICHE = ["live", "community", "technik"]; let ersterPunkt = null; for (const b of BEREICHE) { const a = await hole(modiAn.keks, `/workspace/api/checkliste/${b}`); if (a.status !== 200) { unklar(`${b}: nicht lesbar (${a.status})`); continue; } const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []); - /* DIE ZAHL IN DIE BEDINGUNG: Eine leere Liste wuerde sonst als - "keine Creator-Punkte dabei" durchgehen. */ ok(punkte.length > 0 && punkte.every((p) => p.schluessel.startsWith("m-")), - `${b.padEnd(10)} ${punkte.length} Punkte, alle aus seinem Satz`); + `${b.padEnd(10)} ${punkte.length} Punkte aus seinem Satz`); if (b === "live" && punkte.length) ersterPunkt = punkte[0].schluessel; - /* Und er sieht SEINE Liste, ohne jemanden auszuwaehlen. */ - ok(a.daten?.creator_id === idModi, - `${b.padEnd(10)} es ist seine eigene Liste (${a.daten?.creator_id})`); - ok(a.daten?.darf_bewerten === false, - `${b.padEnd(10)} bewerten darf er sich nicht selbst`); + ok(a.daten?.creator_id === idModi, `${b.padEnd(10)} es ist seine eigene Liste`); + /* DAS IST DIE UMKEHRUNG: Er darf druecken. Vorher stand hier das + Gegenteil, und das war der Fehler. */ + ok(a.daten?.darf_bewerten === true, + `${b.padEnd(10)} und er darf "Passt so" oder "Verbessern" drücken`); } - -/* GEGENPROBE: Ein Creator bekommt weiterhin SEINEN Satz -- sonst waere - oben auch gruen, wenn ich allen denselben gegeben haette. */ +/* GEGENPROBE: Der Creator bekommt unveraendert seinen Satz -- und darf + sich weiterhin NICHT selbst bewerten. Sonst waere oben auch gruen, + wenn ich allen alles erlaubt haette. */ { const a = await hole(creatAn.keks, "/workspace/api/checkliste/live"); const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []); ok(punkte.length > 0 && punkte.every((p) => !p.schluessel.startsWith("m-")), - `der Creator bekommt unveraendert seinen Satz (${punkte.length} Punkte)`); + `der Creator bekommt unverändert seinen Satz (${punkte.length} Punkte)`); + ok(a.daten?.darf_bewerten === false, "und darf sich weiterhin nicht selbst bewerten"); } -/* ---------- 2. DogFather und VanVan waehlen ihn aus ------------------ */ -console.log("\n=== 2. DogFather und VanVan sehen ihn ===\n"); +/* ---------- 2. Sein Bericht gehört ihm ------------------------------- */ +console.log("\n=== 2. Sein Bericht gehört ihm ===\n"); for (const [name, an] of [["DogFather", dogiAn], ["VanVan", vanAn]]) { - const a = await hole(an.keks, "/workspace/api/checkliste/live"); - const drin = (a.daten?.creatoren || []).some((z) => z.id === idModi); - ok(drin, `${name.padEnd(10)} hat ihn in der Auswahl`); - const b = await hole(an.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`); - const punkte = (b.daten?.gruppen || []).flatMap((g) => g.punkte || []); - ok(punkte.length > 0 && punkte.every((p) => p.schluessel.startsWith("m-")), - `${name.padEnd(10)} bekommt SEINEN Satz, nicht den der Creator (${punkte.length})`); - ok(b.daten?.darf_bewerten === true, `${name.padEnd(10)} darf bewerten`); -} - -/* ---------- 3. Bewerten ---------------------------------------------- */ -console.log("\n=== 3. Gut oder verbessern ===\n"); -if (!ersterPunkt) { - unklar("ohne Punkt laesst sich das Bewerten nicht pruefen"); -} else { - const a = await hole(dogiAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, { - method: "PUT", headers: jsonKopf, - body: JSON.stringify({ stufe: "gut", begruendung: "sauber gemacht", creator_id: idModi }), - }); - ok(a.status === 200, `DogFather setzt "gut" (${a.status})`); - - const b = await hole(modiAn.keks, "/workspace/api/checkliste/live"); - ok(b.daten?.stand?.[ersterPunkt]?.stufe === "gut", - `der Modi sieht den Stand (${b.daten?.stand?.[ersterPunkt]?.stufe})`); - - const c = await hole(vanAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, { - method: "PUT", headers: jsonKopf, - body: JSON.stringify({ stufe: "verbessern", begruendung: "beim nächsten Mal früher", creator_id: idModi }), - }); - ok(c.status === 200, `VanVan setzt "verbessern" (${c.status})`); - - /* Und er selbst kann es NICHT umstellen -- sonst waere die Bewertung - eine Selbsteinschaetzung. */ - const e = await hole(modiAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, { - method: "PUT", headers: jsonKopf, body: JSON.stringify({ stufe: "gut" }), - }); - ok(e.status === 403, `er selbst kann sie nicht umstellen (${e.status})`); -} - -/* ---------- 4. Spicy Media geht das nichts an ------------------------ */ -console.log("\n=== 4. Spicy Media bekommt ihn nicht ===\n"); -{ - const a = await hole(spicyAn.keks, "/workspace/api/checkliste/live"); - const drin = (a.daten?.creatoren || []).some((z) => z.id === idModi); - ok(!drin, - `nicht in der Auswahl (${(a.daten?.creatoren || []).length} Personen: ` - + `${(a.daten?.creatoren || []).map((z) => z.name).join(", ")})`); - /* GEGENPROBE: Den Creator hat sie sehr wohl -- sonst waere der Haken - darueber auch dann gruen, wenn die Auswahl ueberhaupt leer bliebe. */ - ok((a.daten?.creatoren || []).some((z) => z.id === idCreat), - "den Creator dagegen schon"); - /* Und ueber die Adresse auch nicht. Eine Regel, die nur in der - Auswahlliste gilt, ist keine. */ - const b = await hole(spicyAn.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`); - ok(b.status === 404, `und ueber die Adresse ebenfalls nicht (${b.status})`); - const c = await hole(spicyAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, { + const a = await hole(an.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`); + const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []); + ok(punkte.length > 0, `${name.padEnd(10)} sieht seinen Bericht (${punkte.length} Punkte)`); + /* NICHT ueberschreiben. Duerften sie es, waere es kein Bericht mehr, + sondern eine Abstimmung darueber, was er gesehen haben soll. */ + ok(a.daten?.darf_bewerten === false, + `${name.padEnd(10)} kann ihn aber nicht überschreiben`); + const b = await hole(an.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, { method: "PUT", headers: jsonKopf, body: JSON.stringify({ stufe: "gut", creator_id: idModi }), }); - ok(c.status === 404, `bewerten erst recht nicht (${c.status})`); + ok(b.status === 403 && /Team-Lage/.test(b.daten?.fehler || ""), + `${name.padEnd(10)} und wird auf die Team-Lage verwiesen (${b.status})`); } - -/* ---------- 5. Die gemeinsame Lage ----------------------------------- */ -console.log("\n=== 5. Die Seite für DogFather und VanVan ===\n"); +/* ---------- 3. Er meldet etwas --------------------------------------- */ +console.log("\n=== 3. Er drückt „Verbessern“ ===\n"); { + const a = await hole(modiAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, { + method: "PUT", headers: jsonKopf, + body: JSON.stringify({ stufe: "verbessern", begruendung: "Kam erst zehn Minuten vorher" }), + }); + ok(a.status === 200, `die Meldung geht durch (${a.status})`); + for (const [name, an] of [["DogFather", dogiAn], ["VanVan", vanAn]]) { - const a = await hole(an.keks, "/workspace/api/teamlage"); - if (a.status !== 200) { unklar(`${name}: nicht lesbar (${a.status})`); continue; } - const eintrag = (a.daten?.modis || []).find((m) => m.id === idModi); - ok(!!eintrag, `${name.padEnd(10)} sieht den Modi in der Lage`); - /* Die Zahlen muessen zum Punktesatz passen -- eine Uebersicht, die - andere Zahlen zeigt als die Seite dahinter, ist schlimmer als - keine. */ - ok(eintrag?.bereiche?.live?.gesamt === 40, - `${name.padEnd(10)} und die Gesamtzahl stimmt (${eintrag?.bereiche?.live?.gesamt})`); - /* Ein Punkt wurde in Abschnitt 3 auf "verbessern" gesetzt. */ - ok(eintrag?.bereiche?.live?.verbessern === 1 - && eintrag?.bereiche?.live?.offen === 39, - `${name.padEnd(10)} der bewertete Punkt ist abgezogen ` - + `(${eintrag?.bereiche?.live?.verbessern} verbessern, ${eintrag?.bereiche?.live?.offen} offen)`); - /* Und die Kachel dorthin bekommt sie auch. */ - const b = await hole(an.keks, "/workspace/api/ich"); - const kachel = (b.daten?.bereiche_zusatz || []).some((k) => k.ziel === "teamlage.html"); - ok(kachel, `${name.padEnd(10)} hat die Kachel auf der Startseite`); + const b = await hole(an.keks, "/workspace/api/teamlage"); + const drin = (b.daten?.eingang || []).find((e) => e.schluessel === ersterPunkt); + ok(!!drin, `${name.padEnd(10)} hat sie im Eingang`); + ok(drin?.begruendung === "Kam erst zehn Minuten vorher", + `${name.padEnd(10)} mit seinem Satz dabei ("${drin?.begruendung || ""}")`); + ok(!!drin?.titel && drin.titel !== ersterPunkt, + `${name.padEnd(10)} und dem Titel, nicht dem Schlüssel ("${drin?.titel || ""}")`); } + /* Und "Passt so" landet NICHT im Eingang -- sonst waere er nach einer + Sendung voll mit Lob, und das Wichtige ginge unter. */ + const zweiter = await hole(modiAn.keks, "/workspace/api/checkliste/live"); + const p2 = (zweiter.daten?.gruppen || []).flatMap((g) => g.punkte || [])[1]?.schluessel; + await hole(modiAn.keks, `/workspace/api/checkliste/live/${p2}`, { + method: "PUT", headers: jsonKopf, body: JSON.stringify({ stufe: "gut" }), + }); + const c = await hole(dogiAn.keks, "/workspace/api/teamlage"); + ok(!(c.daten?.eingang || []).some((e) => e.schluessel === p2), + "„Passt so“ landet nicht im Eingang"); } -/* ---------- 6. Für alle anderen gibt es sie nicht -------------------- */ -console.log("\n=== 6. Und für alle anderen gibt es sie nicht ===\n"); -for (const [name, an] of [["Spicy Media", spicyAn], ["Creator", creatAn], ["Modi", modiAn]]) { - const a = await hole(an.keks, "/workspace/api/teamlage"); - const b = await hole(an.keks, "/workspace/api/gibtesnicht"); - ok(a.status === 404, - `${name.padEnd(12)} bekommt 404 -- wie bei einer Adresse, die es nicht gibt (${a.status}/${b.status})`); - const c = await hole(an.keks, "/workspace/api/ich"); - ok((c.daten?.bereiche_zusatz || []).length === 0, - `${name.padEnd(12)} bekommt auch die Kachel nicht`); -} - -/* GEGENPROBE ZUR KACHEL: Sie ist ueberhaupt vorhanden -- sonst waeren - die drei Haken darueber auch dann gruen, wenn es sie gar nicht gibt. */ +/* ---------- 4. Daraus wird eine Aufgabe ------------------------------ */ +console.log("\n=== 4. Daraus wird eine Aufgabe ===\n"); { - const a = await hole(dogiAn.keks, "/workspace/api/ich"); - ok((a.daten?.bereiche_zusatz || []).length === 1, - `bei DogFather ist genau eine Zusatzkachel da (${(a.daten?.bereiche_zusatz || []).length})`); + const a = await hole(dogiAn.keks, "/workspace/api/teamlage/behandeln", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ + bereich: "live", schluessel: ersterPunkt, person_id: idModi, art: "aufgabe", + }), + }); + ok(a.status === 200 && a.daten?.aufgabe_id, `angenommen (${a.status})`); + + const b = await hole(dogiAn.keks, "/workspace/api/aufgaben"); + const neu = (b.daten?.aufgaben || []).find((x) => x.id === a.daten?.aufgabe_id); + ok(neu?.verantwortlich_id === idDogi, "die Aufgabe gehört dem, der gedrückt hat"); + ok(/zehn Minuten vorher/.test(neu?.beschreibung || ""), + "der Satz des Modis steht in der Aufgabe"); + + /* Aus dem Eingang verschwunden -- ein Eingang, der nie leer wird, + wird nicht mehr angesehen. */ + const c = await hole(dogiAn.keks, "/workspace/api/teamlage"); + ok(!(c.daten?.eingang || []).some((e) => e.schluessel === ersterPunkt), + "und ist aus dem Eingang verschwunden"); + + /* UND DER MODI MERKT ES. Eine Meldung, auf die nichts folgt, sieht + fuer ihn aus wie eine, die niemand gelesen hat. */ + const e = await hole(modiAn.keks, + `/workspace/api/checkliste/live/${ersterPunkt}/nachrichten`); + const texte = (e.daten?.nachrichten || e.daten || []).map((z) => z.text || ""); + ok(texte.some((t) => /Aufgabe geworden/.test(t)), + `er sieht die Antwort an seinem Punkt (${texte.length} Nachricht(en))`); + + /* Ein zweites Mal geht nicht -- sonst entstuenden zwei Aufgaben aus + einer Meldung. */ + const f = await hole(dogiAn.keks, "/workspace/api/teamlage/behandeln", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ + bereich: "live", schluessel: ersterPunkt, person_id: idModi, art: "aufgabe", + }), + }); + ok(f.status === 404, `ein zweites Mal geht nicht (${f.status})`); +} + +/* ---------- 5. Oder eine begründete Absage --------------------------- */ +console.log("\n=== 5. Oder eine begründete Absage ===\n"); +{ + const liste = await hole(modiAn.keks, "/workspace/api/checkliste/community"); + const punkt = (liste.daten?.gruppen || []).flatMap((g) => g.punkte || [])[0]?.schluessel; + await hole(modiAn.keks, `/workspace/api/checkliste/community/${punkt}`, { + method: "PUT", headers: jsonKopf, + body: JSON.stringify({ stufe: "verbessern", begruendung: "Regeln standen weit unten" }), + }); + + /* OHNE GRUND KEINE ABSAGE. Wer nichts dazu schreibt, laesst den Modi + genauso ratlos zurueck wie Schweigen -- nur ist die Meldung dann + auch noch aus dem Eingang weg. */ + const ohne = await hole(vanAn.keks, "/workspace/api/teamlage/behandeln", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ + bereich: "community", schluessel: punkt, person_id: idModi, art: "abgelehnt", + }), + }); + ok(ohne.status === 400, `eine Absage ohne Grund wird abgelehnt (${ohne.status})`); + + const mit = await hole(vanAn.keks, "/workspace/api/teamlage/behandeln", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ + bereich: "community", schluessel: punkt, person_id: idModi, + art: "abgelehnt", grund: "Steht schon oben, wir lassen es so", + }), + }); + ok(mit.status === 200 && !mit.daten?.aufgabe_id, + `mit Grund geht sie durch und legt keine Aufgabe an (${mit.status})`); + + const e = await hole(modiAn.keks, + `/workspace/api/checkliste/community/${punkt}/nachrichten`); + const texte = (e.daten?.nachrichten || e.daten || []).map((z) => z.text || ""); + ok(texte.some((t) => /lassen es so/.test(t)), "der Grund kommt beim Modi an"); +} + +/* ---------- 6. Spicy Media bekommt nichts davon ---------------------- */ +console.log("\n=== 6. Spicy Media bekommt nichts davon ===\n"); +{ + const a = await hole(spicyAn.keks, "/workspace/api/checkliste/live"); + const drin = (a.daten?.creatoren || []).some((z) => z.id === idModi); + ok(!drin, `nicht in der Auswahl (${(a.daten?.creatoren || []).map((z) => z.name).join(", ")})`); + ok((a.daten?.creatoren || []).some((z) => z.id === idCreat), "den Creator dagegen schon"); + const b = await hole(spicyAn.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`); + ok(b.status === 404, `und über die Adresse ebenfalls nicht (${b.status})`); + const c = await hole(spicyAn.keks, "/workspace/api/teamlage"); + ok(c.status === 404, `die Team-Lage bekommt sie nicht (${c.status})`); + const e = await hole(spicyAn.keks, "/workspace/api/teamlage/behandeln", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ bereich: "live", schluessel: ersterPunkt, person_id: idModi, art: "aufgabe" }), + }); + ok(e.status === 404, `und behandeln erst recht nicht (${e.status})`); +} + +/* ---------- 7. Die Kachel -------------------------------------------- */ +console.log("\n=== 7. Die Kachel für euch beide ===\n"); +for (const [name, an, soll] of [ + ["DogFather", dogiAn, 1], ["VanVan", vanAn, 1], + ["Spicy Media", spicyAn, 0], ["Creator", creatAn, 0], ["Modi", modiAn, 0], +]) { + const a = await hole(an.keks, "/workspace/api/ich"); + const n = (a.daten?.bereiche_zusatz || []).length; + ok(n === soll, `${name.padEnd(12)} bekommt ${n} Zusatzkachel(n), erwartet ${soll}`); +} + + +/* ---------- 8. Angebote ---------------------------------------------- */ +console.log("\n=== 8. Ein Angebot an DogFather ===\n"); +{ + /* Der Modi schlaegt etwas vor -- mit Nutzen, Aufwand und dem, was + DogFather dafuer tun muesste. */ + const a = await hole(modiAn.keks, "/workspace/api/bereich/angebote", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ + art: "aktion", titel: "Zuschauer-Challenge am Freitag", + text: "Drei Runden, Gewinner bekommt einen Shoutout.", + einsatz: "Im Live ansagen und die Regeln vorlesen", + bewertung: 4, dringlichkeit: "niedrig", + }), + }); + ok(a.status === 201, `der Modi legt ein Angebot an (${a.status})`); + + /* Es steht im Eingang von DogFather und VanVan -- mit dem, was sein + Teil waere. Ohne das muesste er entscheiden, ohne ihn zu kennen. */ + const b = await hole(dogiAn.keks, "/workspace/api/teamlage"); + const ang = (b.daten?.angebote || [])[0]; + ok(!!ang, `es steht in der Team-Lage (${(b.daten?.angebote || []).length})`); + ok(ang?.einsatz === "Im Live ansagen und die Regeln vorlesen", + `mit seinem Teil dabei ("${ang?.einsatz || ""}")`); + ok(ang?.bewertung === 4 && ang?.dringlichkeit === "niedrig", + `mit Nutzen ${ang?.bewertung} und Aufwand ${ang?.dringlichkeit}`); + + /* Und Spicy Media sieht davon nichts. */ + const c = await hole(spicyAn.keks, "/workspace/api/bereich/angebote"); + ok(c.status === 404, `Spicy Media kommt nicht an die Angebote (${c.status})`); + + /* ANNEHMEN: zwei Aufgaben -- der Modi setzt um, DogFather bekommt + seinen Teil. Genau das war Filipes Wunsch. */ + const e = await hole(dogiAn.keks, "/workspace/api/teamlage/angebot", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: ang.id, art: "angenommen" }), + }); + ok(e.status === 200 && e.daten?.aufgaben === 2, + `angenommen, ${e.daten?.aufgaben} Aufgaben angelegt`); + + const f = await hole(dogiAn.keks, "/workspace/api/aufgaben"); + const alle = f.daten?.aufgaben || []; + ok(alle.some((x) => x.titel === "Umsetzen: Zuschauer-Challenge am Freitag" + && x.verantwortlich_id === idModi), "der Modi setzt es um"); + ok(alle.some((x) => x.titel === "Im Live ansagen und die Regeln vorlesen" + && x.verantwortlich_id === idDogi), "und DogFather bekommt seinen Teil"); + + /* Aus dem Eingang verschwunden, und ein zweites Mal geht nicht. */ + const g = await hole(dogiAn.keks, "/workspace/api/teamlage"); + ok((g.daten?.angebote || []).length === 0, "aus dem Eingang verschwunden"); + const h = await hole(dogiAn.keks, "/workspace/api/teamlage/angebot", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: ang.id, art: "angenommen" }), + }); + ok(h.status === 404, `ein zweites Mal geht nicht (${h.status})`); + + /* Und der Modi sieht die Antwort an seinem Angebot. */ + const i = await hole(modiAn.keks, "/workspace/api/bereich/angebote"); + const seins = (i.daten?.eintraege || i.daten?.liste || []).find((z) => z.id === ang.id); + ok(/Angenommen von/.test(seins?.text || ""), + "der Modi sieht, dass es angenommen wurde"); + ok(seins?.status === "angenommen", `und der Zustand steht (${seins?.status})`); +} + +/* ---------- 9. Ein Angebot ablehnen ---------------------------------- */ +console.log("\n=== 9. Ein Angebot ablehnen ===\n"); +{ + const a = await hole(modiAn.keks, "/workspace/api/bereich/angebote", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "format", titel: "Wöchentlicher Podcast", bewertung: 2 }), + }); + const b = await hole(vanAn.keks, "/workspace/api/teamlage"); + const ang = (b.daten?.angebote || [])[0]; + if (!ang) { unklar("kein Angebot zum Ablehnen da"); } + else { + const ohne = await hole(vanAn.keks, "/workspace/api/teamlage/angebot", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: ang.id, art: "abgelehnt" }), + }); + ok(ohne.status === 400, `ohne Grund geht es nicht (${ohne.status})`); + const mit = await hole(vanAn.keks, "/workspace/api/teamlage/angebot", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ id: ang.id, art: "abgelehnt", grund: "Zeitlich nicht drin" }), + }); + ok(mit.status === 200 && mit.daten?.aufgaben === 0, + `mit Grund geht es, ohne Aufgaben (${mit.status})`); + const c = await hole(modiAn.keks, "/workspace/api/bereich/angebote"); + const seins = (c.daten?.eintraege || c.daten?.liste || []).find((z) => z.id === ang.id); + ok(/Zeitlich nicht drin/.test(seins?.text || ""), "der Grund kommt beim Modi an"); + } + void a; } console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- ` + `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 4131ebea..e243f229 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -169,6 +169,47 @@ export const BEREICHE = { ohneCreatorBezug: true, }, + /* ===================================================================== + DIE ANGEBOTE (10.09.2026, Wunsch Filipe) + + *"ich brauch auch noch kategorien wie, angebote, wo die modis + sachen planen und auch entscheiden was ich machen muss wenn das und + das erledigt wird."* + + Ein Modi schlaegt etwas vor -- eine Aktion, ein Format, eine Regel + -- und schreibt dazu, was DogFather dafuer tun muesste. DogFather + oder VanVan nimmt an oder lehnt ab; bei Annahme entstehen die + Aufgaben. Entschieden wird in der Team-Lage, an derselben Stelle + wie die uebrigen Rueckmeldungen: EIN Eingang, nicht zwei. + + DIE ZWEI ZAHLEN SIND UMBENANNT, nicht neu erfunden: `bewertung` + (1-5) heisst hier NUTZEN, `dringlichkeit` heisst AUFWAND. Beide + Felder gibt es laengst; sie mit neuen Namen zu belegen ist besser, + als zwei weitere Spalten anzulegen, die dasselbe koennen. Damit + die Oberflaeche nicht "Dringlichkeit" ueber den Aufwand schreibt, + stehen die Namen hier. + + KEIN `fuerAlle`, wie beim Ideen-Board: Wer die Angebote sieht, + entscheidet dieselbe Regel wie ueberall. */ + angebote: { + name: "Angebote", + arten: { + aktion: "Aktion", + format: "Format", + regel: "Regel", + technik: "Technik", + sonstiges: "Sonstiges", + }, + ober: "Vorschläge", + bewertung: true, + bewertungName: "Nutzen", + dringlichkeit: true, + dringlichkeitName: "Aufwand", + einsatzFeld: true, + einsatzName: "Was DogFather dafür tun müsste", + ohneCreatorBezug: true, + }, + agentur: { name: "Agentur", arten: { @@ -235,10 +276,12 @@ bereicheRouter.use("/workspace/api/bereich", angemeldet); ZWEI SCHLOESSER, ABSICHTLICH: Hier haengt der ZUGANG zur Seite, sichtbar() haengt die Bedingung an die ZEILEN. Faellt eines weg, haelt das andere. */ -bereicheRouter.use("/workspace/api/bereich/ideen", (req, res, next) => { - if (siehtModis(req.person)) return next(); - return res.status(404).json({ fehler: "nicht_gefunden" }); -}); +for (const geschuetzt of ["ideen", "angebote"]) { + bereicheRouter.use(`/workspace/api/bereich/${geschuetzt}`, (req, res, next) => { + if (siehtModis(req.person)) return next(); + return res.status(404).json({ fehler: "nicht_gefunden" }); + }); +} /* --------------------------------------------------------------------- UND UMGEKEHRT: Ein Modi kommt nur in SEINE Bereiche. @@ -457,6 +500,9 @@ const SPALTEN = ` e.hook, e.format, e.saeule_id, e.geplant, e.event_ende, e.event_aufgaben, e.event_regeln, e.creator_extern, + /* Was DogFather dafuer tun muesste -- nur bei den Angeboten + gefuellt, siehe BEREICHE.angebote. */ + e.einsatz, ${externSql("pc.name", "e.creator_extern")} AS creator_name, pe.name AS erstellt_name, (SELECT name FROM content_saeulen s WHERE s.id = e.saeule_id) AS saeule_name, @@ -630,6 +676,15 @@ function pruefe(bereich, körper, { neu }) { Konto; sichtbar bleibt er ueber erstellt_von (siehe sichtbar()). */ externPruefen(körper, aus, "creator", fehler); + /* Das Einsatz-Feld gibt es nur in Bereichen, die es kennen. Ein + Aufruf an der Oberflaeche vorbei soll nicht in jedem Bereich eine + Spalte fuellen koennen, die dort nichts bedeutet. */ + if (einstellung.einsatzFeld && körper.einsatz !== undefined) { + const t = String(körper.einsatz ?? "").trim(); + if (t.length > TEXT_MAX) fehler.push("Der Einsatz ist zu lang."); + else aus.einsatz = t || null; + } + /* Ein Agentur-Eintrag gehoert NIEMANDEM einzeln (07.09.2026). Er gilt fuer alle, und genau deshalb sieht ihn auch jeder (siehe @@ -704,13 +759,14 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re INSERT INTO eintraege (bereich, art, titel, text, datum, bewertung, dringlichkeit, status, creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant, - event_ende, event_aufgaben, event_regeln) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( + event_ende, event_aufgaben, event_regeln, einsatz) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( bereich, aus.art, aus.titel, aus.text ?? null, aus.datum, aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen", aus.creator_id ?? null, aus.creator_extern ?? null, jetzt(), req.person.id, aus.hook ?? null, aus.format ?? null, aus.saeule_id ?? null, aus.geplant ?? null, - aus.event_ende ?? null, aus.event_aufgaben ?? null, aus.event_regeln ?? null); + aus.event_ende ?? null, aus.event_aufgaben ?? null, aus.event_regeln ?? null, + aus.einsatz ?? null); protokolliere("eintrag_angelegt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), diff --git a/server/workspace-checkliste.js b/server/workspace-checkliste.js index 6c72a269..28dc16c7 100644 --- a/server/workspace-checkliste.js +++ b/server/workspace-checkliste.js @@ -38,7 +38,7 @@ import { protokolliere, echteIp, siehtModis, } from "./workspace.js"; -import { MODI_PUNKTE } from "./workspace-modi-punkte.js"; +import { MODI_PUNKTE, MODI_GRUPPENTEXTE } from "./workspace-modi-punkte.js"; import { CONTENT_IDEEN, LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE, } from "./workspace-vorlagen.js"; @@ -147,8 +147,17 @@ function gruppenFuer(bereich, fuerModi) { /* Eine Gruppe ohne Punkte faellt heraus statt leer dazustehen -- ein Abschnitt mit einer Ueberschrift und nichts darunter sieht aus wie ein Fehler. */ + /* Auch der Satz UNTER der Ueberschrift wird getauscht. Der der + Creator-Fassung spricht von etwas anderem ("Vor dem Ernstfall + abhaken") -- und ein Satz, der etwas anderes verspricht als die + Liste darunter, ist schlimmer als gar keiner. */ + const texte = MODI_GRUPPENTEXTE[bereich] || {}; return liste.gruppen - .map((g) => ({ ...g, punkte: satz[g.schluessel] || [] })) + .map((g) => ({ + ...g, + text: texte[g.schluessel] || g.text, + punkte: satz[g.schluessel] || [], + })) .filter((g) => g.punkte.length); } @@ -197,6 +206,38 @@ function tabellen() { CREATE INDEX IF NOT EXISTS idx_punkt_nachricht ON punkt_nachricht (bereich, schluessel, creator_id); `); + + /* ===================================================================== + WURDE DIE RUECKMELDUNG SCHON BEHANDELT? (10.09.2026) + + Seit die Modis melden statt bewertet zu werden, ist ein + "Verbessern" ein Eingang: DogFather oder VanVan macht eine Aufgabe + daraus oder sagt begruendet ab. Ohne diesen Vermerk stuende jede + Meldung ewig im Eingang -- und ein Eingang, der nie leer wird, + wird nicht mehr angesehen. + + ALTER TABLE ADD COLUMN und kein Tabellenneubau: Es aendert sich + keine CHECK-Regel, und SQLite haengt eine Spalte anstandslos an. + + `behandelt_art` statt eines Ja/Nein: "daraus wurde eine Aufgabe" + und "das ist nicht noetig" sind zwei verschiedene Antworten, und + der Modi soll beide unterscheiden koennen. */ + for (const [spalte, typ] of [ + ["behandelt_am", "TEXT"], + ["behandelt_von", "INTEGER"], + ["behandelt_art", "TEXT"], + ["aufgabe_id", "INTEGER"], + ]) { + try { + const vorhanden = db().prepare("PRAGMA table_info(punkt_stand)").all().map((z) => z.name); + if (vorhanden.length && !vorhanden.includes(spalte)) { + db().exec(`ALTER TABLE punkt_stand ADD COLUMN ${spalte} ${typ}`); + console.log(`[workspace] Spalte '${spalte}' in punkt_stand ergaenzt.`); + } + } catch (fehler) { + console.error(`[workspace] Spalte '${spalte}':`, fehler?.message); + } + } bereit = true; } @@ -343,10 +384,29 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => { /* Die Oberflaeche soll nicht raten muessen, wer was darf -- sonst zeigt sie einem Creator einen Knopf, der beim Klicken 403 sagt. Das ist schlechter als kein Knopf. */ - /* Wer an der EIGENEN Liste arbeitet, bewertet sie nicht selbst -- - weder Creator noch Modi. Die Stufe setzt die Betreuung - beziehungsweise die DogFather-Rolle. */ - darf_bewerten: req.person.rolle !== "creator" && req.person.rolle !== "modi", + /* ============================================================= + WER DRUECKT, HAENGT DAVON AB, WESSEN LISTE ES IST + (umgedreht am 10.09.2026) + + Wunsch Filipe: *"die modis mir quasi aufgaben verteilen, also + das sie drücken können passt so oder verbesserungen. weil die + modis sind ja da um mir zu helfen."* + + BEI EINER CREATOR-LISTE wie bisher: Die Betreuung bewertet, + der Creator nicht sich selbst. + + BEI EINER MODI-LISTE genau andersherum: NUR ER druecke. Es ist + SEIN Bericht ueber die Sendung -- was er gesehen und gehoert + hat. Duerfte DogFather ihn ueberschreiben, waere es nicht mehr + sein Bericht, sondern eine Abstimmung darueber, was er gesehen + haben soll. + + DogFather und VanVan LESEN ihn und handeln in der Team-Lage: + Aus "Verbessern" wird dort mit einem Klick eine Aufgabe -- oder + eine begruendete Absage, die beim Modi ankommt. */ + darf_bewerten: zielRolle === "modi" + ? req.person.id === creatorId + : (req.person.rolle !== "creator" && req.person.rolle !== "modi"), ich: req.person.id, }); } catch (fehler) { @@ -362,7 +422,23 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe try { /* Ein Creator bewertet sich nicht selbst. Waere das moeglich, stuende alles auf gruen und die Ampel waere wertlos. */ - if (req.person.rolle === "creator" || req.person.rolle === "modi") { + /* Wessen Liste ist es? Danach entscheidet sich, wer druecken + darf -- siehe die Begruendung beim Lesen weiter oben. */ + const zielId = creatorAus(req); + const zielRolle = zielId > 0 + ? db().prepare("SELECT rolle FROM personen WHERE id = ?").get(zielId)?.rolle + : null; + const eigenerBericht = zielRolle === "modi" && req.person.id === zielId; + if (zielRolle === "modi" && !eigenerBericht) { + /* Auch DogFather nicht: Es ist der Bericht des Modis. Er + handelt in der Team-Lage, nicht hier. */ + return res.status(403).json({ + fehler: "Das ist die Rückmeldung des Modis. Sie wird nicht überschrieben, " + + "sondern in der Team-Lage behandelt.", + }); + } + if (!eigenerBericht + && (req.person.rolle === "creator" || req.person.rolle === "modi")) { return res.status(403).json({ fehler: "Die Bewertung setzt deine Betreuung. Du kannst hier antworten.", }); @@ -391,6 +467,8 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe geloescht statt auf "offen" gesetzt -- ein Datensatz, der nichts aussagt, ist Ballast. */ if (stufe === "offen") { + /* Auch der Vermerk faellt mit -- eine neu gesetzte Stufe ist + eine neue Meldung, keine schon behandelte. */ db().prepare("DELETE FROM punkt_stand WHERE bereich = ? AND schluessel = ? AND creator_id = ?") .run(bereich, schluessel, creatorId); } else { @@ -399,7 +477,13 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe VALUES (?,?,?,?,?,?,?) ON CONFLICT(bereich, schluessel, creator_id) DO UPDATE SET stufe = excluded.stufe, begruendung = excluded.begruendung, - von = excluded.von, am = excluded.am`) + von = excluded.von, am = excluded.am, + /* Eine neu gesetzte Stufe ist eine NEUE Meldung -- der + alte Vermerk gehoert weg, sonst verschwindet sie + stillschweigend aus dem Eingang, obwohl sich etwas + geaendert hat. */ + behandelt_am = NULL, behandelt_von = NULL, + behandelt_art = NULL, aufgabe_id = NULL`) .run(bereich, schluessel, creatorId, stufe, grund || null, req.person.id, jetzt()); } diff --git a/server/workspace-modi-punkte.js b/server/workspace-modi-punkte.js index f35d26c6..c1985258 100644 --- a/server/workspace-modi-punkte.js +++ b/server/workspace-modi-punkte.js @@ -1,278 +1,302 @@ /* ===================================================================== - DIE CHECKLISTEN-PUNKTE EINES MODIS (10.09.2026) + WAS DIE MODIS BEURTEILEN (10.09.2026, umgedreht am selben Tag) - Wunsch Filipe: *"diese kategorien mussen auf dieser seite auch noch - auf die modis perfektionniert werden, da mussen lauter sachen sein - die vanvan und ich danach verteilen können und da anpassen können ob - gut oder nicht."* + Wunsch Filipe: *"diese 3 kategorien müssen so eingestellt werden dass + die modis mir quasi aufgaben verteilen, also das sie drücken können + passt so oder verbesserungen. weil die modis sind ja da um mir zu + helfen."* - WARUM EIN ZWEITER SATZ UND NICHT EIN ERWEITERTER: Die vorhandenen - Punkte (LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE) sind fuer - CREATOR geschrieben -- "Upload messen", "Sendeplan festlegen", - "Verweildauer vergleichen". Ein Modi sendet nicht, er moderiert. Ihm - dieselbe Liste vorzulegen hiesse, ihn an Dingen zu messen, die nicht - seine Arbeit sind. + DIE RICHTUNG WAR VORHER FALSCH HERUM. Beim ersten Anlauf standen hier + Punkte ueber die Arbeit des MODIS -- "Schicht bestaetigt", "Chat im + Blick" -- und DogFather sollte sie bewerten. Das war eine Note fuer + ihn, keine Hilfe fuer DogFather. Und es widerspricht der Hausregel, + nach der niemand ueber dem Team steht. - DIESELBEN DREI BEREICHE UND DIESELBEN GRUPPEN wie beim Creator -- - Vor/Waehrend/Nach, Moderation/Aktion/Konflikt, Setup/Problem/Loesung. - Das ist Absicht: Zwei verschiedene Einteilungen fuer dieselbe Sache - waeren zwei Dinge zum Lernen statt einem, und die Oberflaeche kaeme - ohne Umbau nicht damit zurecht. + JETZT BEURTEILT DER MODI DIE SENDUNG. Er sitzt im Chat, er sieht und + hoert, was die Zuschauer sehen und hoeren -- frueher und ungefilterter + als jeder, der gerade sendet. Er drueckt "Passt so" oder + "Verbessern", schreibt einen Satz dazu, und daraus wird ein Vorschlag + fuer DogFather. - DIESELBEN VIER STUFEN (anfaenger, fort, profi, meister). Sie - entscheiden, was jemandem zuerst gezeigt wird. Ein Modi in der - Probezeit soll nicht an "Muster ueber mehrere Sendungen erkennen" - gemessen werden. + DIE PUNKTE SIND DESHALB BEOBACHTUNGEN, KEINE PFLICHTEN. Nicht "Ton + pruefen", sondern "Der Ton war durchgehend verstaendlich". Der + Unterschied ist nicht sprachlich: Eine Pflicht kann man erfuellen + oder versaeumen, eine Beobachtung kann man nur machen -- und + niemandem vorwerfen. - DIE SCHLUESSEL BEGINNEN MIT "m-". Der Stand haengt in `punkt_stand` - an (bereich, schluessel, person) -- ein Modi und ein Creator im - selben Bereich duerfen sich also nicht in die Quere kommen. Mit dem - Praefix kann das nicht passieren, auch wenn jemand spaeter einen - Punkt gleich benennt. + WAS EIN MODI SELBST ZU TUN HAT, steht woanders: im Aufgabenkatalog + aus Teil 2 des Anforderungsdokuments (60 Aufgaben, neun Etappen). + Das ist die richtige Stelle dafuer -- eine Aufgabe hat eine Frist und + einen Status, eine Beobachtung nicht. - BEWERTET WIRD NICHT VON IHM SELBST, sondern von der DogFather-Rolle - -- offen / gut / verbessern, wie beim Creator auch. Genau das war - Filipes Wunsch: "verteilen ... und anpassen ob gut oder nicht". + DIE VIER STUFEN bleiben. Sie sagen hier, wie GENAU jemand hinsieht: + "Ton war da" sieht jeder, "es gab einen Grund wiederzukommen" sieht, + wer laenger dabei ist. Ein Modi in der Probezeit soll nicht mit den + Meister-Punkten anfangen muessen. + + DIE SCHLUESSEL BEGINNEN MIT "m-". Der Stand haengt an (bereich, + schluessel, person) -- ohne Praefix koennte ein Modi-Punkt eines + Tages denselben Namen tragen wie ein Creator-Punkt und dessen + Bewertung erben. ===================================================================== */ +/* Die Ueberschriften und Saetze je Gruppe. Sie stehen hier, weil die + der Creator-Fassung von etwas anderem sprechen ("Vor dem Ernstfall + abhaken") -- und ein Satz, der etwas anderes verspricht als die + Liste darunter, ist schlimmer als gar keiner. */ +export const MODI_GRUPPENTEXTE = { + live: { + vorbereitung: "Was du vor und beim Start gesehen hast. Der Anfang entscheidet, ob jemand bleibt.", + mitschrift: "Was währenddessen auffiel. Jetzt notieren – morgen ist es weg.", + auswertung: "Was danach passiert ist. Auch das gehört zur Sendung.", + }, + community: { + moderation: "Wie die Regeln im Alltag wirken – nicht, wie sie gemeint sind.", + aktion: "Ob die Community etwas zurückbekommt.", + konflikt: "Wie es lief, als es eng wurde.", + }, + technik: { + setup: "Was bei den Zuschauern ankommt. Du hörst und siehst es wie sie.", + problem: "Wenn etwas ausgefallen ist.", + loesung: "Was danach geblieben ist.", + }, +}; + export const MODI_LIVE_PUNKTE = { vorbereitung: [ - { schluessel: "m-schicht-bestaetigt", titel: "Schicht bestätigt", stufe: "anfaenger", - text: "Vor dem Tag gesagt, ob du kannst. Ein „ich schau mal“ ist keine Zusage – und das Team plant danach." }, - { schluessel: "m-ankuendigung-gesehen", titel: "Ankündigung gesehen", stufe: "anfaenger", - text: "Du weißt, was draußen versprochen wurde. Sonst beantwortest du im Chat Fragen zu etwas, das du nicht kennst." }, - { schluessel: "m-ablauf-gelesen", titel: "Ablaufplan gelesen", stufe: "anfaenger", - text: "Anfang, Mitte, Schluss – grob im Kopf. Wer den Ablauf kennt, kann den Chat darauf vorbereiten." }, - { schluessel: "m-regeln-griffbereit", titel: "Regeln griffbereit", stufe: "anfaenger", - text: "Der Link oder der Text, den du hineinstellst, ohne zu suchen. Im Ernstfall zählen Sekunden." }, - { schluessel: "m-eigene-technik", titel: "Eigene Technik geprüft", stufe: "anfaenger", - text: "Dein Gerät, deine Verbindung, dein Zugang. Ein Modi, der mittendrin herausfliegt, fehlt genau dann." }, - { schluessel: "m-wer-ist-dabei", titel: "Wer moderiert heute noch?", stufe: "fort", - text: "Zu zweit teilt man sich auf – wer begrüßt, wer greift ein. Allein macht man beides halb." }, - { schluessel: "m-ziel-bekannt", titel: "Ziel der Sendung bekannt", stufe: "fort", - text: "Eine Zahl oder eine Sache. Darauf kannst du im Chat hinweisen, ohne zu fragen." }, - { schluessel: "m-gaeste-bekannt", titel: "Gäste bekannt", stufe: "fort", - text: "Wer kommt, wann, und wie heißt er richtig. Ein falsch geschriebener Name im Chat bleibt hängen." }, - { schluessel: "m-wortfilter-durchgesehen", titel: "Wortfilter durchgesehen", stufe: "fort", - text: "Was letzte Woche durchrutschte, gehört heute hinein. Ein Filter, den niemand pflegt, veraltet." }, - { schluessel: "m-eskalationsweg", titel: "Eskalationsweg im Kopf", stufe: "fort", - text: "Ab wann gibst du weiter, statt selbst zu entscheiden – und an wen. Vorher klären, nicht im Moment." }, - { schluessel: "m-begruessung-bereit", titel: "Begrüßungssätze bereit", stufe: "profi", - text: "Drei verschiedene, nicht einer dreimal. Wer merkt, dass ein Baustein antwortet, antwortet nicht zurück." }, - { schluessel: "m-umfrage-vorbereitet", titel: "Umfrage vorbereitet", stufe: "profi", - text: "Höchstens drei Antworten, vorher aufgeschrieben. Mitten im Stream formuliert man keine gute Frage." }, - { schluessel: "m-vertretung-geklaert", titel: "Vertretung geklärt", stufe: "profi", - text: "Wer übernimmt, wenn du wegmusst. Ein Chat ohne Aufsicht ist schlimmer als einer, der zu ist." }, - { schluessel: "m-uebergabe-vorbereitet", titel: "Übergabe an die nächste Schicht", stufe: "meister", - text: "Was die andere Person wissen muss, bevor du gehst. Zwei Zeilen reichen – aber sie müssen da sein." }, + { schluessel: "m-ankuendigung-rechtzeitig", titel: "Die Ankündigung kam rechtzeitig", stufe: "anfaenger", + text: "30 bis 60 Minuten vorher. Später erreicht sie die nicht mehr, die sich den Abend noch einteilen." }, + { schluessel: "m-titel-sagt-worum", titel: "Der Titel sagte, worum es geht", stufe: "anfaenger", + text: "Wer ihn liest, muss wissen, ob es für ihn ist. Ein Titel, der neugierig macht und nichts sagt, holt die Falschen." }, + { schluessel: "m-startzeit-gehalten", titel: "Die Startzeit wurde gehalten", stufe: "anfaenger", + text: "Wer zur angekündigten Zeit kommt und nichts vorfindet, kommt beim nächsten Mal später – oder nicht." }, + { schluessel: "m-ton-von-anfang-an", titel: "Der Ton war von Anfang an da", stufe: "anfaenger", + text: "Die ersten Sekunden ohne Ton kosten mehr Zuschauer als jede spätere Panne." }, + { schluessel: "m-bild-hell-genug", titel: "Das Bild war hell genug", stufe: "anfaenger", + text: "Auf dem Handy sieht es dunkler aus als am Rechner. Wenn du das Gesicht schlecht erkennst, erkennen es andere gar nicht." }, + { schluessel: "m-einstieg-vorhanden", titel: "Es gab einen Einstieg", stufe: "fort", + text: "Nicht mitten im Satz anfangen. Wer in den ersten Sekunden nicht weiß, wo er gelandet ist, geht wieder." }, + { schluessel: "m-thema-frueh-klar", titel: "Das Thema war früh klar", stufe: "fort", + text: "Spätestens nach zwei Minuten. Alles danach ist Suchen, und Suchen macht man woanders." }, + { schluessel: "m-chat-wusste-bescheid", titel: "Der Chat wusste, was kommt", stufe: "fort", + text: "Ein Satz zum Ablauf am Anfang spart dir zwanzig Fragen später." }, + { schluessel: "m-technikcheck-war-durch", titel: "Der Technik-Check war vorher durch", stufe: "fort", + text: "Man merkt es daran, ob in den ersten Minuten noch geschraubt wird. Das sehen die Zuschauer auch." }, + { schluessel: "m-gaeste-angekuendigt", titel: "Gäste waren angekündigt", stufe: "fort", + text: "Wer im Chat sitzt, will wissen, wer da ist – und du willst den Namen richtig schreiben können." }, + { schluessel: "m-einstieg-zog", titel: "Der Einstieg hat gezogen", stufe: "profi", + text: "Blieben die ersten, die kamen? Über 70 Prozent entscheiden in den ersten Sekunden." }, + { schluessel: "m-plan-erkennbar", titel: "Ein Plan war erkennbar", stufe: "profi", + text: "Nicht als Liste vorgelesen, aber spürbar. Ein Abend ohne Richtung fühlt sich für Zuschauer wie Warten an." }, + { schluessel: "m-ankuendigung-passte", titel: "Die Ankündigung passte zum Inhalt", stufe: "profi", + text: "Wer wegen etwas anderem gekommen ist, bleibt nicht – und kommt beim nächsten Mal nicht wieder." }, + { schluessel: "m-start-wiederholbar", titel: "Der Start war wiederholbar", stufe: "meister", + text: "So gut, dass man ihn genauso noch einmal machen könnte. Genau das macht aus einem guten Abend ein Format." }, ], mitschrift: [ - { schluessel: "m-chat-im-blick", titel: "Chat wirklich im Blick", stufe: "anfaenger", - text: "Nicht nebenbei. Wer mitschaut statt mitliest, sieht den Verstoß erst, wenn zehn Leute geantwortet haben." }, - { schluessel: "m-neue-begruesst", titel: "Neue begrüßt", stufe: "anfaenger", - text: "Mit Namen, wenn es geht. Wer beim ersten Mal angesprochen wird, kommt deutlich häufiger wieder." }, - { schluessel: "m-spam-sofort-weg", titel: "Spam sofort weg", stufe: "anfaenger", - text: "Löschen, bevor jemand antwortet. Eine Diskussion über einen gelöschten Beitrag kostet mehr als der Beitrag." }, - { schluessel: "m-fragen-gesammelt", titel: "Fragen gesammelt", stufe: "anfaenger", - text: "An einer Stelle, nicht im Kopf. Was im Chat vorbeirauscht, ist nach zwei Minuten weg." }, - { schluessel: "m-auf-ziel-hingewiesen", titel: "Auf das Ziel hingewiesen", stufe: "fort", - text: "Lieber zweimal beiläufig als einmal ausführlich. Wer später dazukommt, hat es sonst nie gehört." }, - { schluessel: "m-interaktion-angestossen", titel: "Interaktion angestoßen", stufe: "fort", - text: "Eine Frage in den Chat, wenn es still wird. Stille im Chat sieht für Neue aus wie ein leerer Raum." }, - { schluessel: "m-clipmomente-markiert", titel: "Clip-Momente markiert", stufe: "fort", - text: "Mit Uhrzeit, sofort. Am nächsten Tag findet niemand die Stelle wieder, an der alle gelacht haben." }, - { schluessel: "m-zeit-im-blick", titel: "Zeit im Blick behalten", stufe: "fort", - text: "Ein kurzes Zeichen genügt. Wer sendet, hat kein Gefühl für die Uhr – das ist normal." }, - { schluessel: "m-verwarnen-vor-sperren", titel: "Erst verwarnen, dann sperren", stufe: "profi", - text: "Außer bei den Sachen, die sofort gehen müssen. Eine Verwarnung, die jeder sieht, wirkt auf mehr als einen." }, - { schluessel: "m-stammzuschauer-angesprochen", titel: "Stammzuschauer angesprochen", stufe: "profi", - text: "Wiedererkannt zu werden ist der halbe Grund wiederzukommen." }, - { schluessel: "m-eskalation-weitergegeben", titel: "Eskalation weitergegeben", stufe: "profi", - text: "Nach der Regel, nicht nach Gefühl. Allein zu entscheiden, wo die Regel etwas anderes sagt, bringt dich in die Schusslinie." }, - { schluessel: "m-nichts-oeffentlich-ausdiskutiert", titel: "Nichts öffentlich ausdiskutiert", stufe: "profi", - text: "Im Chat gewinnt niemand. Kurz sagen, was gilt, und weiter – der Rest gehört ins Team." }, - { schluessel: "m-stimmung-gemeldet", titel: "Stimmung gemeldet", stufe: "meister", - text: "Wenn es kippt, bevor es kippt. Du siehst es früher als jeder, der gerade sendet." }, - { schluessel: "m-ruhig-geblieben", titel: "Ruhig geblieben, als es eng wurde", stufe: "meister", - text: "Der Ton eines Modis wird zum Ton des Chats. Das ist der schwerste Punkt auf dieser Liste." }, + { schluessel: "m-ton-durchgehend", titel: "Der Ton blieb verständlich", stufe: "anfaenger", + text: "Durchgehend, nicht überwiegend. Ein Aussetzer alle zehn Minuten reicht, damit jemand geht." }, + { schluessel: "m-bild-ohne-ruckeln", titel: "Das Bild lief ohne Ruckeln", stufe: "anfaenger", + text: "Wenn es bei dir ruckelt, ruckelt es bei vielen. Notiere die Uhrzeit – daran erkennt man die Ursache." }, + { schluessel: "m-auf-chat-reagiert", titel: "Auf den Chat kam Reaktion", stufe: "anfaenger", + text: "Nicht auf alles, aber sichtbar. Ein Chat, der nie vorkommt, hört auf zu schreiben." }, + { schluessel: "m-namen-richtig", titel: "Namen wurden richtig gesagt", stufe: "anfaenger", + text: "Wer falsch angesprochen wird, korrigiert einmal und schweigt dann." }, + { schluessel: "m-roter-faden", titel: "Der rote Faden war erkennbar", stufe: "fort", + text: "Auch für jemanden, der nach 40 Minuten dazukommt. Der weiß sonst nicht, worum es geht." }, + { schluessel: "m-fragen-beantwortet", titel: "Fragen bekamen eine Antwort", stufe: "fort", + text: "Oder wenigstens ein „später“. Eine übergangene Frage wird kein zweites Mal gestellt." }, + { schluessel: "m-ziel-erwaehnt", titel: "Das Ziel wurde erwähnt", stufe: "fort", + text: "Mindestens zweimal, beiläufig. Wer erst spät dazukommt, hat es sonst nie gehört." }, + { schluessel: "m-pausen-angesagt", titel: "Pausen waren angesagt", stufe: "fort", + text: "Ein stiller Bildschirm ohne Ansage sieht aus wie ein Absturz." }, + { schluessel: "m-tempo-stimmte", titel: "Das Tempo stimmte", stufe: "profi", + text: "Nicht gehetzt, nicht zäh. Du merkst es am Chat: Wird er still, war es zu lang." }, + { schluessel: "m-auf-stimmung-reagiert", titel: "Auf die Stimmung wurde reagiert", stufe: "profi", + text: "Wenn der Chat kippt oder einschläft, muss etwas passieren. Du siehst es zuerst." }, + { schluessel: "m-gaeste-eingebunden", titel: "Gäste wurden eingebunden", stufe: "profi", + text: "Ein Gast, der zwanzig Minuten zuhört, ist kein Gast. Das fällt im Chat sofort auf." }, + { schluessel: "m-chat-nicht-ueberhoert", titel: "Der Chat wurde nicht überhört", stufe: "profi", + text: "Gerade wenn es voll wird. Genau dann fühlen sich die Leisen übergangen." }, + { schluessel: "m-abschluss-gesetzt", titel: "Der Abschluss war gesetzt", stufe: "meister", + text: "Nicht abgebrochen. Ein Ende, das man kommen sieht, ist der Grund, beim nächsten Mal bis zum Schluss zu bleiben." }, + { schluessel: "m-grund-wiederzukommen", titel: "Es gab einen Grund wiederzukommen", stufe: "meister", + text: "Etwas Angekündigtes, etwas Offenes, etwas Verabredetes. Ohne das entscheidet der Zufall." }, ], auswertung: [ - { schluessel: "m-auffaelligkeiten-notiert", titel: "Auffälligkeiten notiert", stufe: "anfaenger", - text: "Solange du es noch weißt. Am nächsten Tag fehlt die Hälfte, und zwar die interessante." }, - { schluessel: "m-clipmarken-weitergegeben", titel: "Clip-Marken weitergegeben", stufe: "anfaenger", - text: "An die, die schneiden. Eine Marke, die niemand bekommt, ist keine." }, - { schluessel: "m-feedback-im-teamchat", titel: "Kurzes Feedback im Team-Chat", stufe: "anfaenger", - text: "Eine Sache, die gut lief, eine, die man ändert. Wer sich fünf vornimmt, ändert keine." }, - { schluessel: "m-vorfaelle-festgehalten", titel: "Vorfälle festgehalten", stufe: "fort", - text: "Wer, wann, was – und was du getan hast. Beim zweiten Mal mit derselben Person zählt genau das." }, - { schluessel: "m-chatverlauf-gesichert", titel: "Chatverlauf gesichert", stufe: "fort", - text: "Vor dem Schließen. Er ist die einzige Quelle dafür, was wirklich gesagt wurde." }, - { schluessel: "m-offene-fragen-beantwortet", titel: "Offene Fragen nachgereicht", stufe: "fort", + { schluessel: "m-aufzeichnung-da", titel: "Die Aufzeichnung war da", stufe: "anfaenger", + text: "Am selben Abend gesichert. Ohne sie gibt es keine Clips und keinen Beleg." }, + { schluessel: "m-clips-kamen", titel: "Es kamen Clips", stufe: "anfaenger", + text: "Mindestens einer, am nächsten Tag. Der Feed hat mehr Reichweite als der Stream." }, + { schluessel: "m-offene-fragen-nachgereicht", titel: "Offene Fragen wurden nachgereicht", stufe: "anfaenger", text: "Was im Stream unterging, gehört danach beantwortet – sonst fragt niemand ein zweites Mal." }, - { schluessel: "m-eine-sache-fuers-naechste-mal", titel: "Eine Sache fürs nächste Mal", stufe: "fort", - text: "Aufgeschrieben, nicht vorgenommen. Ein Vorsatz ohne Zettel ist am Freitag weg." }, - { schluessel: "m-regel-vorgeschlagen", titel: "Regel vorgeschlagen, wo eine fehlte", stufe: "profi", - text: "Wenn du zweimal improvisieren musstest, fehlt eine Regel – nicht Erfahrung." }, - { schluessel: "m-wiederkehrende-stoerer", titel: "Wiederkehrende Störer notiert", stufe: "profi", - text: "Dieselben Namen tauchen wieder auf. Eine Liste macht aus drei Einzelfällen ein Muster." }, - { schluessel: "m-zahlen-angesehen", titel: "Zahlen angesehen", stufe: "profi", - text: "Wie viele kamen, wie lange blieben sie. Nicht um zu bewerten, sondern um zu wissen, wann der Chat kippte." }, - { schluessel: "m-uebergabe-geschrieben", titel: "Übergabe geschrieben", stufe: "meister", - text: "So, dass jemand anderes die nächste Sendung übernehmen könnte. Wer nicht ersetzbar ist, kann nicht krank werden." }, - { schluessel: "m-muster-ueber-sendungen", titel: "Muster über mehrere Sendungen erkannt", stufe: "meister", - text: "Immer dieselbe Uhrzeit, immer dieselbe Sorte Kommentar. Das sieht nur, wer mehrere Abende nebeneinanderlegt." }, + { schluessel: "m-team-bekam-rueckmeldung", titel: "Das Team bekam Rückmeldung", stufe: "fort", + text: "Auch wenn alles gut lief. Wer nie hört, dass es gut war, weiß nicht, was er behalten soll." }, + { schluessel: "m-zahlen-geteilt", titel: "Zahlen wurden geteilt", stufe: "fort", + text: "Nicht alle – die zwei, die etwas sagen. Ein Team, das die Wirkung nicht kennt, arbeitet blind." }, + { schluessel: "m-angekuendigtes-gehalten", titel: "Angekündigtes wurde eingehalten", stufe: "fort", + text: "„Nächste Woche machen wir …“ – wenn das dreimal nicht kam, glaubt es niemand mehr." }, + { schluessel: "m-aus-hinweisen-wurde-etwas", titel: "Aus Hinweisen wurde etwas", stufe: "profi", + text: "Was das Team gemeldet hat, taucht beim nächsten Mal auf – oder es kommt eine Absage. Beides ist in Ordnung, Schweigen nicht." }, + { schluessel: "m-naechster-termin-stand", titel: "Der nächste Termin stand", stufe: "profi", + text: "Vor dem Ende gesagt und im Kalender. Zwei feste Tage schlagen fünf zufällige." }, + { schluessel: "m-wiederkehrendes-angegangen", titel: "Wiederkehrendes wurde angegangen", stufe: "profi", + text: "Derselbe Aussetzer, dieselbe Frage, derselbe Störer – beim dritten Mal ist es kein Zufall mehr." }, + { schluessel: "m-geaendert-nicht-nur-besprochen", titel: "Es wurde geändert, nicht nur besprochen", stufe: "meister", + text: "Der Unterschied zwischen einer Nachbesprechung und einer Verbesserung." }, + { schluessel: "m-uebergabefaehig", titel: "Die Sendung war übergabefähig", stufe: "meister", + text: "So dokumentiert, dass jemand anderes sie fahren könnte. Wer nicht ersetzbar ist, kann nicht krank werden." }, + { schluessel: "m-team-genannt", titel: "Das Team wurde genannt", stufe: "meister", + text: "Öffentlich und namentlich. Wer nur im Hintergrund vorkommt, bleibt irgendwann ganz weg." }, ], }; export const MODI_COMMUNITY_PUNKTE = { moderation: [ - { schluessel: "m-regeln-gelesen", titel: "Regeln gelesen und verstanden", stufe: "anfaenger", - text: "Nicht überflogen. Du wirst danach gefragt, und eine falsche Auskunft ist schwerer zurückzunehmen als keine." }, - { schluessel: "m-eigene-rechte-bekannt", titel: "Eigene Rechte bekannt", stufe: "anfaenger", - text: "Was du darfst: löschen, stummschalten, sperren – und was nicht. Wer es nicht weiß, tut zu viel oder zu wenig." }, - { schluessel: "m-tonfall", titel: "Tonfall gefunden", stufe: "anfaenger", - text: "Freundlich und kurz. Ein Modi, der sich rechtfertigt, verliert; einer, der herrisch wird, auch." }, - { schluessel: "m-haeufige-fragen", titel: "Häufige Fragen im Kopf", stufe: "anfaenger", - text: "Die fünf, die jeden Abend kommen. Wer sie kennt, muss nicht jedes Mal nachsehen." }, - { schluessel: "m-warnung-vor-sperre", titel: "Reihenfolge klar: Hinweis, Warnung, Sperre", stufe: "fort", - text: "Vorher entscheiden, nicht im Moment. Wer im Ärger sperrt, sperrt irgendwann falsch." }, - { schluessel: "m-minderjaehrige", titel: "Umgang mit Minderjährigen", stufe: "fort", - text: "Wie erkennt man sie, was gilt dann. Das ist keine Ermessensfrage." }, - { schluessel: "m-privat-vs-oeffentlich", titel: "Privat statt öffentlich", stufe: "fort", - text: "Was eine Person betrifft, klärt man mit ihr – nicht vor zweihundert Leuten." }, - { schluessel: "m-keine-versprechen", titel: "Keine Versprechen im Namen anderer", stufe: "fort", - text: "\"Das machen wir\" bindet das ganze Team. \"Ich gebe es weiter\" bindet nur dich." }, - { schluessel: "m-entscheidung-begruenden", titel: "Entscheidung in einem Satz begründen können", stufe: "profi", - text: "Nicht im Chat – aber wenn DogFather fragt. Eine Sperre, die niemand erklären kann, wird zurückgenommen." }, - { schluessel: "m-eigene-grenze", titel: "Eigene Grenze kennen", stufe: "profi", - text: "Wann du aussteigst und jemanden holst. Weiterzumachen, wenn es dich mitnimmt, hilft niemandem." }, - { schluessel: "m-gleich-behandeln", titel: "Alle gleich behandeln", stufe: "profi", + { schluessel: "m-regeln-auffindbar", titel: "Die Regeln waren auffindbar", stufe: "anfaenger", + text: "An einer Stelle, die man ohne Suchen findet. Was man suchen muss, liest niemand." }, + { schluessel: "m-regeln-verstaendlich", titel: "Die Regeln waren verständlich", stufe: "anfaenger", + text: "In eigenen Worten, nicht als Rechtstext. Regeln, die man erklären muss, werden nicht befolgt." }, + { schluessel: "m-wortfilter-passte", titel: "Der Wortfilter passte noch", stufe: "anfaenger", + text: "Was diese Woche durchrutschte, gehört hinein. Ein Filter, den niemand pflegt, veraltet." }, + { schluessel: "m-klar-wer-eingreift", titel: "Es war klar, wer eingreifen darf", stufe: "anfaenger", + text: "Für dich und für den Chat. Unklarheit darüber kostet im Ernstfall die entscheidenden Sekunden." }, + { schluessel: "m-eskalationsregel-bekannt", titel: "Die Eskalationsregel war bekannt", stufe: "fort", + text: "Ab wann gibt man weiter statt selbst zu entscheiden. Wer das erst im Ernstfall klärt, klärt es zu spät." }, + { schluessel: "m-regeln-angewandt", titel: "Die Regeln wurden auch angewandt", stufe: "fort", + text: "Eine Regel, die zweimal übergangen wird, gilt nicht mehr – auch für die, die sich daran halten." }, + { schluessel: "m-entscheidungen-nachvollziehbar", titel: "Entscheidungen waren nachvollziehbar", stufe: "fort", + text: "Nicht für jeden im Chat, aber für dich. Eine Sperre, die niemand erklären kann, wird zurückgenommen." }, + { schluessel: "m-alle-gleich-behandelt", titel: "Alle wurden gleich behandelt", stufe: "fort", text: "Auch die, die man mag. Eine Ausnahme sehen mehr Leute, als man denkt." }, - { schluessel: "m-neue-modis-eingearbeitet", titel: "Neue Modis eingearbeitet", stufe: "meister", - text: "Einen Abend nebenher laufen lassen und danach besprechen. Zusehen lernt mehr als jede Regelliste." }, + { schluessel: "m-regeln-passen-noch", titel: "Die Regeln passen noch zur Community", stufe: "profi", + text: "Eine Community von heute ist nicht die von vor einem Jahr. Regeln altern mit." }, + { schluessel: "m-wiederholungstaeter-erkannt", titel: "Wiederholungstäter wurden erkannt", stufe: "profi", + text: "Dieselben Namen tauchen wieder auf. Ohne Liste bleiben drei Einzelfälle drei Einzelfälle." }, + { schluessel: "m-rueckendeckung", titel: "Es gab Rückendeckung", stufe: "profi", + text: "Wenn ein Modi eingreift und danach allein dasteht, greift beim nächsten Mal keiner mehr ein." }, + { schluessel: "m-aus-vorfaellen-regeln", titel: "Aus Vorfällen wurden Regeln", stufe: "meister", + text: "Ein Vorfall, aus dem nichts folgt, kommt wieder. Der Ertrag ist die Regel, nicht die Sperre." }, ], aktion: [ - { schluessel: "m-willkommen-gepflegt", titel: "Willkommens-Routine gepflegt", stufe: "anfaenger", - text: "Ein Satz, der nicht nach Textbaustein klingt. Wer merkt, dass ein Automat schreibt, antwortet nicht." }, - { schluessel: "m-namen-gemerkt", titel: "Namen gemerkt", stufe: "anfaenger", - text: "Die zehn, die immer da sind. Mehr braucht es nicht, damit sich eine Community erkannt fühlt." }, - { schluessel: "m-fragen-in-den-stream", titel: "Fragen in den Stream gegeben", stufe: "anfaenger", - text: "Gesammelt, ausgewählt, weitergereicht. Nicht alles – die drei, die etwas bringen." }, - { schluessel: "m-umfrage-ausgewertet", titel: "Umfrage ausgewertet", stufe: "fort", - text: "Und das Ergebnis gesagt. Eine Abstimmung ohne Ergebnis fragt beim nächsten Mal niemand mehr mit." }, - { schluessel: "m-aktion-vorgeschlagen", titel: "Eine Aktion vorgeschlagen", stufe: "fort", - text: "Challenge, Q&A, gemeinsamer Abend. Du hörst im Chat, was ankommt, früher als alle anderen." }, - { schluessel: "m-stille-belebt", titel: "Stille belebt statt ausgesessen", stufe: "fort", - text: "Eine Frage, ein Bezug auf vorhin. Ein stiller Chat wirkt auf Neue wie ein leerer Raum." }, - { schluessel: "m-rueckmeldung-eingesammelt", titel: "Rückmeldungen eingesammelt", stufe: "fort", - text: "Was Zuschauer sich wünschen, gehört weitergegeben – auch wenn es nicht umgesetzt wird." }, - { schluessel: "m-stammzuschauer-liste", titel: "Liste der Stammzuschauer gepflegt", stufe: "profi", - text: "Wer seit Wochen da ist, verdient mehr als ein Herz. Ohne Liste vergisst man ihn." }, - { schluessel: "m-neue-in-die-runde", titel: "Neue in die Runde geholt", stufe: "profi", - text: "Sie einmal namentlich ins Gespräch ziehen. Der Unterschied zwischen Zuschauen und Dabeisein." }, - { schluessel: "m-eigene-idee-eingetragen", titel: "Eigene Idee ins Ideen-Board", stufe: "profi", - text: "Was dir im Chat auffällt, ist die beste Quelle für Inhalte – aber nur, wenn es aufgeschrieben wird." }, - { schluessel: "m-aktion-durchgezogen", titel: "Eine Aktion von Anfang bis Ende begleitet", stufe: "meister", - text: "Ankündigen, durchführen, hinterher sagen, was daraus wurde. Der letzte Teil wird fast immer vergessen." }, - { schluessel: "m-community-uebersetzt", titel: "Zwischen Community und Team übersetzt", stufe: "meister", - text: "Was im Chat gemeint ist, und was im Team ankommt, sind oft zwei Dinge. Diese Lücke zu schließen ist die eigentliche Arbeit." }, + { schluessel: "m-neue-wurden-begruesst", titel: "Neue wurden begrüßt", stufe: "anfaenger", + text: "Mit Namen. Wer beim ersten Mal angesprochen wird, kommt deutlich häufiger wieder." }, + { schluessel: "m-kommentare-beantwortet", titel: "Kommentare bekamen Antwort", stufe: "anfaenger", + text: "Auch die kurzen, auch die späten. Eine Antwort nach drei Tagen liest sich wie keine." }, + { schluessel: "m-stammzuschauer-erkannt", titel: "Stammzuschauer wurden erkannt", stufe: "anfaenger", + text: "Wiedererkannt zu werden ist der halbe Grund wiederzukommen." }, + { schluessel: "m-etwas-zum-mitmachen", titel: "Es gab etwas zum Mitmachen", stufe: "fort", + text: "Eine Umfrage, eine Frage, eine Challenge. Zuschauen allein bindet niemanden." }, + { schluessel: "m-umfragen-ausgewertet", titel: "Umfragen wurden ausgewertet", stufe: "fort", + text: "Und das Ergebnis gesagt. Eine Abstimmung ohne Ergebnis macht beim nächsten Mal niemand mit." }, + { schluessel: "m-wuensche-kamen-zurueck", titel: "Wünsche kamen zurück", stufe: "fort", + text: "Auch als Nein. Ein Wunschkanal ohne Antwort verstummt." }, + { schluessel: "m-aktionen-angekuendigt", titel: "Aktionen waren angekündigt", stufe: "fort", + text: "Rechtzeitig genug, dass man sich den Termin freihalten kann." }, + { schluessel: "m-community-erwaehnt", titel: "Die Community wurde erwähnt", stufe: "profi", + text: "Namentlich, nicht als „ihr da draußen“. Der Unterschied zwischen Publikum und Runde." }, + { schluessel: "m-ideen-aufgegriffen", titel: "Ideen aus dem Chat wurden aufgegriffen", stufe: "profi", + text: "Und gesagt, woher sie kamen. Wer sich wiederfindet, bringt die nächste Idee." }, + { schluessel: "m-grund-teil-zu-sein", titel: "Es gab einen Grund, dabei zu sein", stufe: "profi", + text: "Etwas, das man nur mitbekommt, wenn man da ist. Sonst reicht der Clip am nächsten Tag." }, + { schluessel: "m-aus-aktion-wurde-reihe", titel: "Aus einer Aktion wurde eine Reihe", stufe: "meister", + text: "Erst beim zweiten Mal sieht man, ob das Format trägt oder nur das Thema getragen hat." }, + { schluessel: "m-community-waechst-von-innen", titel: "Die Community wächst von innen", stufe: "meister", + text: "Neue kommen, weil jemand sie mitgebracht hat. Das ist die einzige Reichweite, die bleibt." }, ], konflikt: [ - { schluessel: "m-erst-lesen", titel: "Erst lesen, dann handeln", stufe: "anfaenger", - text: "Zwei Sätze zurückscrollen. Die Hälfte der Streitfälle sieht mit dem Satz davor anders aus." }, - { schluessel: "m-nicht-mitstreiten", titel: "Nicht mitstreiten", stufe: "anfaenger", - text: "Du bist nicht Partei. In dem Moment, in dem du eine wirst, hast du deine Rolle verloren." }, - { schluessel: "m-kurz-und-klar", titel: "Kurz und klar sagen, was gilt", stufe: "anfaenger", - text: "Ein Satz, keine Begründungskette. Je länger die Erklärung, desto mehr Angriffsfläche." }, - { schluessel: "m-abkuehlen-lassen", titel: "Abkühlen lassen statt eskalieren", stufe: "fort", - text: "Stummschalten auf Zeit ist oft besser als sperren. Man kann es zurücknehmen." }, - { schluessel: "m-zweite-meinung", titel: "Zweite Meinung geholt", stufe: "fort", - text: "Bei allem, was größer ist als ein blöder Kommentar. Zu zweit entscheidet es sich leichter und besser." }, - { schluessel: "m-belege-gesichert", titel: "Belege gesichert", stufe: "fort", + { schluessel: "m-frueh-eingegriffen", titel: "Es wurde früh eingegriffen", stufe: "anfaenger", + text: "Bevor zehn Leute geantwortet haben. Danach moderiert man nicht mehr, man räumt auf." }, + { schluessel: "m-ruhig-geblieben-live", titel: "Es blieb ruhig", stufe: "anfaenger", + text: "Der Ton oben wird zum Ton im Chat. Das ist der schwerste Punkt auf dieser Liste." }, + { schluessel: "m-nicht-oeffentlich-ausdiskutiert", titel: "Nichts wurde öffentlich ausdiskutiert", stufe: "anfaenger", + text: "Im Chat gewinnt niemand. Kurz sagen, was gilt, und weiter." }, + { schluessel: "m-entscheidung-begruendet", titel: "Die Entscheidung war begründet", stufe: "fort", + text: "In einem Satz, nicht in einer Kette. Je länger die Erklärung, desto mehr Angriffsfläche." }, + { schluessel: "m-rueckendeckung-im-fall", titel: "Es gab Rückendeckung", stufe: "fort", + text: "Wer eingreift, muss wissen, dass er danach nicht allein dasteht." }, + { schluessel: "m-belege-gesichert-fall", titel: "Belege wurden gesichert", stufe: "fort", text: "Bildschirmfoto, bevor der Beitrag weg ist. Ohne Beleg steht später Aussage gegen Aussage." }, - { schluessel: "m-danach-gemeldet", titel: "Danach im Team gemeldet", stufe: "fort", - text: "Auch wenn es gut ausging. DogFather und die rechte Hand sollten es von dir erfahren, nicht von anderen." }, - { schluessel: "m-keine-drohungen", titel: "Keine Drohungen, nur Folgen", stufe: "profi", - text: "\"Noch einmal und du bist raus\" ist eine Ansage. Alles darüber hinaus ist Streit." }, - { schluessel: "m-nach-der-sperre", titel: "Nach der Sperre nicht nachtreten", stufe: "profi", - text: "Kein Kommentar hinterher, kein Witz darüber. Der Chat merkt sich das." }, - { schluessel: "m-eigene-fehler", titel: "Eigenen Fehler zugegeben", stufe: "profi", - text: "Falsch gesperrt? Zurücknehmen und einmal sagen. Das kostet weniger als der Versuch, recht zu behalten." }, - { schluessel: "m-ernstfall-geprobt", titel: "Ernstfall einmal durchgesprochen", stufe: "meister", - text: "Wer tut was, wenn es wirklich kippt. Im Ernstfall zum ersten Mal darüber nachzudenken ist zu spät." }, - { schluessel: "m-daraus-gelernt", titel: "Aus dem Fall eine Regel gemacht", stufe: "meister", - text: "Ein Vorfall, aus dem nichts folgt, kommt wieder. Der Ertrag ist die Regel, nicht die Sperre." }, + { schluessel: "m-danach-besprochen", titel: "Danach wurde besprochen", stufe: "fort", + text: "Auch wenn es gut ausging. Sonst lernt nur der daraus, der dabei war." }, + { schluessel: "m-keine-ausnahmen", titel: "Keine Ausnahmen für Bekannte", stufe: "profi", + text: "Das ist der Punkt, an dem Regeln ihre Glaubwürdigkeit verlieren – und er fällt allen auf." }, + { schluessel: "m-fehler-zurueckgenommen", titel: "Fehler wurden zurückgenommen", stufe: "profi", + text: "Falsch gesperrt? Einmal sagen und rückgängig machen. Das kostet weniger als recht zu behalten." }, + { schluessel: "m-ton-blieb", titel: "Der Ton blieb auch danach", stufe: "profi", + text: "Kein Nachtreten, kein Witz über den Gesperrten. Der Chat merkt sich das." }, + { schluessel: "m-fall-fuehrte-zu-regel", titel: "Der Fall führte zu einer Regel", stufe: "meister", + text: "Wenn zweimal improvisiert werden musste, fehlt eine Regel – nicht Erfahrung." }, + { schluessel: "m-kam-nicht-wieder", titel: "Es kam nicht wieder", stufe: "meister", + text: "Der einzige Beweis, dass die Regel gewirkt hat." }, ], }; export const MODI_TECHNIK_PUNKTE = { setup: [ - { schluessel: "m-zugaenge-funktionieren", titel: "Zugänge funktionieren", stufe: "anfaenger", - text: "Einmal vor der Sendung anmelden, nicht währenddessen. Ein abgelaufener Zugang fällt sonst im schlechtesten Moment auf." }, - { schluessel: "m-benachrichtigungen-an", titel: "Benachrichtigungen an", stufe: "anfaenger", - text: "Für Aufgaben und Team-Nachrichten. Ein Hinweis, den niemand bekommt, ist keiner." }, - { schluessel: "m-zweites-geraet", titel: "Zweites Gerät griffbereit", stufe: "anfaenger", - text: "Handy neben dem Rechner. Wenn eines hängt, moderierst du mit dem anderen weiter." }, - { schluessel: "m-verbindung-stabil", titel: "Verbindung stabil", stufe: "anfaenger", - text: "Kabel schlägt WLAN. Ein Modi, der alle zehn Minuten neu lädt, sieht die Hälfte nicht." }, - { schluessel: "m-chatfenster-eingerichtet", titel: "Chatfenster richtig eingerichtet", stufe: "fort", - text: "Groß genug, ohne anderes darüber. Wer scrollen muss, um mitzulesen, liest nicht mit." }, - { schluessel: "m-werkzeuge-kennen", titel: "Moderations-Werkzeuge kennen", stufe: "fort", - text: "Wo Löschen, Stummschalten, Sperren sitzt – ohne zu suchen. Im Ernstfall zählen Sekunden." }, - { schluessel: "m-automod-verstanden", titel: "Automatische Filter verstanden", stufe: "fort", - text: "Was sie wegnehmen und was nicht. Wer sich darauf verlässt, ohne es zu wissen, übersieht genau die Lücke." }, - { schluessel: "m-app-installiert", titel: "Die App wirklich installiert", stufe: "fort", - text: "Nicht als Lesezeichen. Nur so kommen Erinnerungen an, auch wenn der Browser zu ist." }, - { schluessel: "m-tastenkuerzel", titel: "Tastenkürzel eingerichtet", stufe: "profi", - text: "Die drei, die du am häufigsten brauchst. Aus zwei Sekunden werden über einen Abend Minuten." }, - { schluessel: "m-ruhe-eingestellt", titel: "Ruhezeiten eingestellt", stufe: "profi", - text: "Damit die App nachts still ist. Wer um drei geweckt wird, schaltet sie irgendwann ganz ab." }, - { schluessel: "m-ersatzweg", titel: "Ersatzweg ins Netz", stufe: "meister", - text: "Mobilfunk, einmal wirklich benutzt. Ein Ersatz, den man erst im Ausfall einrichtet, ist keiner." }, + { schluessel: "m-ton-ohne-rauschen", titel: "Ton ohne Rauschen", stufe: "anfaenger", + text: "Du hörst es wie die Zuschauer. Schlechter Ton ist der Grund Nummer eins, warum Leute wieder gehen." }, + { schluessel: "m-ton-nicht-uebersteuert", titel: "Ton nicht übersteuert", stufe: "anfaenger", + text: "Kratzt es bei lauten Stellen? Das merkt man erst, wenn jemand lacht – und dann ist es unangenehm laut." }, + { schluessel: "m-bild-scharf", titel: "Das Bild war scharf", stufe: "anfaenger", + text: "Nicht weich, nicht verpixelt. Auf dem Handy fällt beides stärker auf als am Rechner." }, + { schluessel: "m-licht-ausreichend", titel: "Das Licht reichte", stufe: "anfaenger", + text: "Gesicht heller als der Hintergrund, kein Fenster im Rücken." }, + { schluessel: "m-verbindung-stabil-live", titel: "Die Verbindung war stabil", stufe: "anfaenger", + text: "Wenn es bei dir hängt, hängt es bei vielen. Uhrzeit notieren – daran erkennt man die Ursache." }, + { schluessel: "m-szenenwechsel-sauber", titel: "Szenenwechsel waren sauber", stufe: "fort", + text: "Kein schwarzes Bild dazwischen, kein Ton, der wegbricht." }, + { schluessel: "m-musik-nicht-zu-laut", titel: "Die Musik war nicht zu laut", stufe: "fort", + text: "Sie darf die Stimme nicht überdecken. Am Rechner klingt das anders als im Kopfhörer." }, + { schluessel: "m-overlays-lesbar", titel: "Einblendungen waren lesbar", stufe: "fort", + text: "Auch auf einem Handy. Was du zusammenkneifen musst, liest niemand." }, + { schluessel: "m-text-auf-handy", titel: "Text war auf dem Handy lesbar", stufe: "fort", + text: "Der größte Teil schaut auf dem Handy. Was dort nicht ankommt, kommt gar nicht an." }, + { schluessel: "m-kein-nachhall", titel: "Kein Hall im Raum", stufe: "profi", + text: "Klingt es nach Badezimmer, hört man nach zehn Minuten weg – ohne zu wissen, warum." }, + { schluessel: "m-ausweichweg-vorhanden", titel: "Ein Ausweichweg war vorhanden", stufe: "meister", + text: "Zweites Gerät, zweite Leitung. Ein Ersatz, den man erst im Ausfall einrichtet, ist keiner." }, ], problem: [ - { schluessel: "m-was-genau-faellt-aus", titel: "Was genau fällt aus?", stufe: "anfaenger", - text: "Chat, Bild, dein Zugang oder das ganze Netz – das sind vier verschiedene Probleme mit vier Lösungen." }, - { schluessel: "m-sofort-gesagt", titel: "Sofort gesagt, dass etwas ist", stufe: "anfaenger", - text: "Lieber einmal zu viel im Team-Chat. Ein stiller Modi sieht aus wie ein anwesender." }, - { schluessel: "m-notiert-solange-frisch", titel: "Notiert, solange es frisch ist", stufe: "anfaenger", - text: "Uhrzeit, Fehlermeldung, was du zuletzt getan hast. Am nächsten Tag fehlt genau das." }, - { schluessel: "m-eine-aenderung", titel: "Nur eine Änderung auf einmal", stufe: "fort", - text: "Wer drei Dinge gleichzeitig ändert und es geht wieder, weiß nicht warum – und steht beim nächsten Mal am Anfang." }, - { schluessel: "m-uebergeben-statt-kaempfen", titel: "Übergeben statt kämpfen", stufe: "fort", - text: "Nach fünf Minuten Basteln moderiert jemand anderes. Der Chat wartet nicht auf deine Technik." }, - { schluessel: "m-bildschirmfoto", titel: "Bildschirmfoto gemacht", stufe: "fort", - text: "Eine Fehlermeldung, die man nachher beschreibt, ist eine andere als die, die dastand." }, - { schluessel: "m-wiederholbar", titel: "Geprüft, ob es wiederkommt", stufe: "profi", + { schluessel: "m-ausfall-bemerkt", titel: "Der Ausfall wurde bemerkt", stufe: "anfaenger", + text: "Und zwar oben, nicht nur im Chat. Minutenlang ins Leere zu senden ist das Schlimmste, was passieren kann." }, + { schluessel: "m-chat-informiert", titel: "Der Chat wurde informiert", stufe: "anfaenger", + text: "Ein Satz genügt. Stille sieht aus wie Absturz, und dann gehen die Leute." }, + { schluessel: "m-ansage-wie-lange", titel: "Es gab eine Ansage, wie lange", stufe: "fort", + text: "Auch „ich weiß es nicht“ ist eine Ansage. Wer keine Zeit genannt bekommt, wartet nicht." }, + { schluessel: "m-jemand-uebernahm", titel: "Jemand hat übernommen", stufe: "fort", + text: "Chat weiterführen, während oben repariert wird. Ohne das zerfällt die Runde in fünf Minuten." }, + { schluessel: "m-fehler-festgehalten", titel: "Der Fehler wurde festgehalten", stufe: "fort", + text: "Uhrzeit, Meldung, was zuletzt anders war. Am nächsten Tag fehlt genau das." }, + { schluessel: "m-nicht-live-gebastelt", titel: "Es wurde nicht live gebastelt", stufe: "profi", + text: "Nach zwei Minuten Herumprobieren schaut man dabei zu, wie jemand arbeitet. Dafür ist niemand gekommen." }, + { schluessel: "m-ausfall-wiederholte-sich-nicht", titel: "Der Ausfall kam nicht wieder", stufe: "profi", text: "Einmal weg ist nicht gelöst. Beim nächsten Mal nachsehen gehört dazu." }, - { schluessel: "m-nicht-alleine-suchen", titel: "Nicht allein stundenlang suchen", stufe: "profi", - text: "Nach zwanzig Minuten jemanden dazuholen. Zu zweit findet man es in fünf." }, + { schluessel: "m-plan-b", titel: "Es gab einen Plan B", stufe: "meister", + text: "Und er wurde vorher einmal geprobt. Ein ungeprobter Ausweichweg ist keiner." }, ], loesung: [ - { schluessel: "m-was-geholfen-hat", titel: "Was geholfen hat", stufe: "anfaenger", - text: "Genau die eine Änderung – nicht alles, was probiert wurde. Sonst ist es beim nächsten Mal wieder Raten." }, - { schluessel: "m-fuer-alle-aufgeschrieben", titel: "Für alle aufgeschrieben", stufe: "anfaenger", - text: "In die Bibliothek, nicht in den Chat. Was im Chat steht, ist nach zwei Tagen unauffindbar." }, - { schluessel: "m-anleitung-verstaendlich", titel: "Anleitung verständlich", stufe: "fort", - text: "Jemanden gegenlesen lassen, der es nicht erlebt hat. Was du selbst geschrieben hast, verstehst du immer." }, - { schluessel: "m-dauerhaft-oder-pflaster", titel: "Dauerhaft oder Pflaster?", stufe: "fort", - text: "Beides ist in Ordnung – aber es gehört dabeigeschrieben, damit niemand darauf baut." }, - { schluessel: "m-vorbeugen-vorgeschlagen", titel: "Vorbeugen vorgeschlagen", stufe: "profi", - text: "Wenn es zweimal passiert ist, ist es kein Zufall mehr, sondern eine fehlende Einstellung." }, - { schluessel: "m-wissen-weitergegeben", titel: "Wissen weitergegeben", stufe: "meister", - text: "Einmal im Team erklärt, statt es dreimal allein zu lösen. Ein Modi, der als Einziger etwas kann, ist ein Engpass." }, + { schluessel: "m-loesung-aufgeschrieben", titel: "Die Lösung wurde aufgeschrieben", stufe: "anfaenger", + text: "Genau die eine Änderung – nicht alles, was probiert wurde." }, + { schluessel: "m-loesung-verstaendlich", titel: "Sie war verständlich", stufe: "fort", + text: "Auch für jemanden, der nicht dabei war. Was man selbst geschrieben hat, versteht man immer." }, + { schluessel: "m-loesung-hielt", titel: "Sie hat gehalten", stufe: "fort", + text: "Beim nächsten Mal nachgesehen. Ein Pflaster ist in Ordnung – aber es gehört dabeigeschrieben." }, + { schluessel: "m-loesung-geteilt", titel: "Sie wurde geteilt", stufe: "profi", + text: "In der Bibliothek, nicht im Chat. Was im Chat steht, ist nach zwei Tagen unauffindbar." }, + { schluessel: "m-vorbeugung", titel: "Es wurde vorgebeugt", stufe: "profi", + text: "Wenn es zweimal passiert ist, fehlt eine Einstellung – nicht Glück." }, + { schluessel: "m-wissen-nicht-bei-einem", titel: "Das Wissen liegt nicht bei einer Person", stufe: "meister", + text: "Wer als Einziger etwas kann, ist ein Engpass – und darf nicht krank werden." }, ], }; -/* Alle drei unter den Bereichsnamen, unter denen sie gebraucht werden -- - damit die Auswahl an einer Stelle steht und nicht in jeder Abfrage - neu zusammengesucht wird. */ export const MODI_PUNKTE = { live: MODI_LIVE_PUNKTE, community: MODI_COMMUNITY_PUNKTE, diff --git a/server/workspace-teamlage.js b/server/workspace-teamlage.js index 8fd894bc..2d063355 100644 --- a/server/workspace-teamlage.js +++ b/server/workspace-teamlage.js @@ -26,10 +26,33 @@ ===================================================================== */ import express from "express"; import { - db, sitzungLesen, istDogFather, heuteLokal, + db, sitzungLesen, istDogFather, heuteLokal, protokolliere, echteIp, } from "./workspace.js"; import { MODI_PUNKTE } from "./workspace-modi-punkte.js"; +/* Titel und Bereich zu einem Schluessel -- damit im Eingang steht, + WORUM es geht, und nicht nur ein Schluessel. */ +const PUNKT = new Map(); +for (const [bereich, gruppen] of Object.entries(MODI_PUNKTE)) { + for (const punkte of Object.values(gruppen)) { + for (const p of punkte) PUNKT.set(`${bereich}|${p.schluessel}`, p); + } +} + +/* Die Kategorie, unter der eine daraus entstandene Aufgabe einsortiert + wird. Der Bereich sagt es schon -- eine zweite Zuordnung waere eine + zweite Gelegenheit, dass beide auseinanderlaufen. */ +const KATEGORIE = { live: "technik", community: "community", technik: "technik" }; + +function gleicheHerkunft(req, res, next) { + const herkunft = req.get("origin"); + if (!herkunft) return next(); + let erlaubt; + try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; } + if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" }); + next(); +} + export const teamlageRouter = express.Router(); /* Wie viele Punkte es je Bereich ueberhaupt gibt. Einmal gerechnet -- @@ -49,7 +72,11 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => { WHERE rolle = 'modi' AND aktiv = 1 ORDER BY name`).all(); if (!modis.length) { - return res.json({ modis: [], bereiche: GESAMT, heute: heuteLokal() }); + /* `eingang` auch hier -- die Oberflaeche darf nicht raten + muessen, ob das Feld fehlt oder leer ist. */ + return res.json({ + modis: [], eingang: [], angebote: [], bereiche: GESAMT, heute: heuteLokal(), + }); } const platz = modis.map(() => "?").join(","); const nummern = modis.map((m) => m.id); @@ -88,6 +115,46 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => { ideen.set(z.p, z.n); } + /* ================================================================= + DER EINGANG: was die Modis gemeldet haben (10.09.2026) + + Filipes Entscheidung: erst ein Vorschlag, dann eine Aufgabe. + Hier steht, was noch nicht behandelt ist -- offen heisst: Ihr + habt noch nicht darauf geantwortet. Was zur Aufgabe wurde oder + begruendet abgelehnt ist, faellt heraus; ein Eingang, der nie + leer wird, wird nicht mehr angesehen. */ + const eingang = []; + for (const z of db().prepare( + `SELECT s.bereich, s.schluessel, s.creator_id, s.begruendung, s.am, p.name AS von_name + FROM punkt_stand s LEFT JOIN personen p ON p.id = s.von + WHERE s.creator_id IN (${platz}) + AND s.stufe = 'verbessern' AND s.behandelt_am IS NULL + ORDER BY s.am DESC`).all(...nummern)) { + const punkt = PUNKT.get(`${z.bereich}|${z.schluessel}`); + /* Ein Punkt, den es nicht mehr gibt, wird uebersprungen -- aber + gezaehlt waere er sonst und niemand koennte ihn behandeln. */ + if (!punkt) continue; + eingang.push({ + bereich: z.bereich, schluessel: z.schluessel, person_id: z.creator_id, + titel: punkt.titel, hinweis: punkt.text, + begruendung: z.begruendung || "", am: z.am, von: z.von_name || null, + }); + } + + /* ================================================================= + DIE ANGEBOTE (10.09.2026) + + Vorschlaege der Modis, ueber die hier entschieden wird. Sie + stehen im selben Eingang wie die Rueckmeldungen -- EIN Ort, an + dem etwas auf eine Antwort wartet, nicht zwei. Wer zwei Eingaenge + hat, sieht irgendwann in den falschen. */ + const angebote = db().prepare( + `SELECT e.id, e.art, e.titel, e.text, e.einsatz, e.bewertung, e.dringlichkeit, + e.datum, e.erstellt_von, p.name AS von_name + FROM eintraege e LEFT JOIN personen p ON p.id = e.erstellt_von + WHERE e.bereich = 'angebote' AND e.status = 'offen' + ORDER BY e.bewertung DESC, e.datum DESC`).all(); + /* Wie viele Rueckmeldungen an seinen Punkten haengen -- das ist das Gespraech, nicht die Note. */ const nachrichten = new Map(); @@ -100,6 +167,8 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => { res.json({ heute, bereiche: GESAMT, + eingang, + angebote, modis: modis.map((m) => { const s = stand.get(m.id) || {}; const je = {}; @@ -125,3 +194,198 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => { res.status(503).json({ fehler: "nicht_verfuegbar" }); } }); + +/* ===================================================================== + EINE MELDUNG BEHANDELN (10.09.2026) + + Filipes Entscheidung: *"Erst ein Vorschlag, den du annimmst."* Zwei + Antworten, und beide kommen beim Modi an: + + annehmen -> daraus wird eine Aufgabe (fuer den, der drueckt) + ablehnen -> mit Begruendung, ohne dass etwas entsteht + + WARUM BEIDE EINE NACHRICHT SCHREIBEN: Eine Meldung, auf die nichts + folgt, sieht fuer den Modi aus wie eine, die niemand gelesen hat -- + und beim dritten Mal meldet er nichts mehr. Die Nachricht haengt am + Punkt (punkt_nachricht), also genau dort, wo er sie beim naechsten + Mal wiederfindet. + + DIE AUFGABE GEHOERT DEM, DER DRUECKT. Filipe oder VanVan -- wer + annimmt, nimmt sie sich. Sie einem Dritten zuzuweisen waere eine + Entscheidung ueber jemand anderen, ohne ihn zu fragen. + ===================================================================== */ +teamlageRouter.post("/workspace/api/teamlage/behandeln", gleicheHerkunft, + express.json({ limit: "16kb" }), (req, res) => { + try { + const person = sitzungLesen(req); + if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); + if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + + const bereich = String(req.body?.bereich || ""); + const schluessel = String(req.body?.schluessel || ""); + const personId = Number(req.body?.person_id) || 0; + const art = String(req.body?.art || ""); + const grund = String(req.body?.grund ?? "").trim().slice(0, 2000); + if (!["aufgabe", "abgelehnt"].includes(art)) { + return res.status(400).json({ fehler: "Unbekannte Art." }); + } + if (!PUNKT.has(`${bereich}|${schluessel}`) || !personId) { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + /* EINE ABSAGE OHNE GRUND IST KEINE ANTWORT. Wer nichts dazu + schreibt, laesst den Modi genauso ratlos zurueck wie + Schweigen -- nur mit dem Unterschied, dass die Meldung jetzt + auch noch aus dem Eingang verschwunden ist. */ + if (art === "abgelehnt" && grund.length < 3) { + return res.status(400).json({ fehler: "Bitte kurz sagen, warum nicht." }); + } + + const meldung = db().prepare( + `SELECT begruendung FROM punkt_stand + WHERE bereich = ? AND schluessel = ? AND creator_id = ? + AND stufe = 'verbessern' AND behandelt_am IS NULL`) + .get(bereich, schluessel, personId); + /* Schon behandelt oder gar nicht gemeldet -- beides ist "gibt es + hier nicht". Zwei Antworten waeren zwei Faelle, die die + Oberflaeche unterscheiden muesste, ohne dass es hilft. */ + if (!meldung) return res.status(404).json({ fehler: "nicht_gefunden" }); + + const punkt = PUNKT.get(`${bereich}|${schluessel}`); + const jetzt = new Date().toISOString(); + let aufgabeId = null; + + if (art === "aufgabe") { + const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben + (titel, beschreibung, status, prioritaet, verantwortlich_id, + kategorie, erstellt, erstellt_von) + VALUES (?,?,'offen','mittel',?,?,?,?)`).run( + punkt.titel, + /* Der Satz des Modis steht IN der Aufgabe -- ohne ihn muesste + man zurueckblaettern, um zu wissen, worum es ging. */ + meldung.begruendung + ? `Gemeldet aus „${punkt.titel}“: +${meldung.begruendung}` + : `Gemeldet aus „${punkt.titel}“.`, + person.id, KATEGORIE[bereich] || "sonstiges", jetzt, person.id); + aufgabeId = Number(lastInsertRowid); + } + + db().prepare( + `UPDATE punkt_stand + SET behandelt_am = ?, behandelt_von = ?, behandelt_art = ?, aufgabe_id = ? + WHERE bereich = ? AND schluessel = ? AND creator_id = ?`) + .run(jetzt, person.id, art, aufgabeId, bereich, schluessel, personId); + + db().prepare(`INSERT INTO punkt_nachricht + (bereich, schluessel, creator_id, text, von, am) VALUES (?,?,?,?,?,?)`) + .run(bereich, schluessel, personId, + art === "aufgabe" + ? `Danke – daraus ist eine Aufgabe geworden.${grund ? " " + grund : ""}` + : `Danke fürs Melden. Das machen wir nicht: ${grund}`, + person.id, jetzt); + + protokolliere("meldung_behandelt", { + personId: person.id, rolle: person.rolle, ip: echteIp(req), + detail: `${bereich}/${schluessel} -> ${art}`.slice(0, 120), + }); + res.json({ ok: true, art, aufgabe_id: aufgabeId }); + } catch (fehler) { + console.error("[workspace] Meldung behandeln:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + +/* ===================================================================== + UEBER EIN ANGEBOT ENTSCHEIDEN (10.09.2026) + + Filipes Wahl: *"Angebote an dich zur Entscheidung ... Bei Annahme + entstehen die Aufgaben fuer alle Beteiligten auf einmal."* + + BEI ANNAHME ZWEI AUFGABEN, nicht eine: + * der Modi, der es vorgeschlagen hat, setzt es um + * und wenn im Angebot steht, was DogFather dafuer tun muesste, + bekommt der, der annimmt, genau das als eigene Aufgabe + + Die zweite ist der Kern von Filipes Wunsch ("entscheiden was ich + machen muss"). Ohne sie waere eine Annahme ein Nicken, aus dem nichts + folgt -- und beim dritten Mal schlaegt niemand mehr etwas vor. + + ABGELEHNT BRAUCHT EINEN GRUND, wie bei den Rueckmeldungen: Eine + Absage ohne Begruendung ist fuer den, der sie bekommt, dasselbe wie + Schweigen -- nur endgueltiger. + ===================================================================== */ +teamlageRouter.post("/workspace/api/teamlage/angebot", gleicheHerkunft, + express.json({ limit: "16kb" }), (req, res) => { + try { + const person = sitzungLesen(req); + if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); + if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + + const id = Number(req.body?.id) || 0; + const art = String(req.body?.art || ""); + const grund = String(req.body?.grund ?? "").trim().slice(0, 2000); + if (!["angenommen", "abgelehnt"].includes(art)) { + return res.status(400).json({ fehler: "Unbekannte Art." }); + } + if (art === "abgelehnt" && grund.length < 3) { + return res.status(400).json({ fehler: "Bitte kurz sagen, warum nicht." }); + } + + const angebot = db().prepare( + `SELECT id, titel, text, einsatz, erstellt_von FROM eintraege + WHERE id = ? AND bereich = 'angebote' AND status = 'offen'`).get(id); + /* Schon entschieden oder gar keins -- beides ist "gibt es hier + nicht". Zwei Antworten waeren zwei Faelle, die die Oberflaeche + unterscheiden muesste, ohne dass es hilft. */ + if (!angebot) return res.status(404).json({ fehler: "nicht_gefunden" }); + + const jetzt = new Date().toISOString(); + const angelegt = []; + if (art === "angenommen") { + if (angebot.erstellt_von) { + const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben + (titel, beschreibung, status, prioritaet, verantwortlich_id, + kategorie, erstellt, erstellt_von) + VALUES (?,?,'offen','mittel',?,'planung',?,?)`).run( + `Umsetzen: ${angebot.titel}`, angebot.text || null, + angebot.erstellt_von, jetzt, person.id); + angelegt.push({ fuer: angebot.erstellt_von, id: Number(lastInsertRowid) }); + } + if (angebot.einsatz) { + const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben + (titel, beschreibung, status, prioritaet, verantwortlich_id, + kategorie, erstellt, erstellt_von) + VALUES (?,?,'offen','mittel',?,'planung',?,?)`).run( + angebot.einsatz, `Aus dem Angebot „${angebot.titel}“.`, + person.id, jetzt, person.id); + angelegt.push({ fuer: person.id, id: Number(lastInsertRowid) }); + } + } + + db().prepare( + "UPDATE eintraege SET status = ?, geaendert = ? WHERE id = ?") + .run(art, jetzt, angebot.id); + + /* Die Antwort gehoert an das Angebot, damit der Modi sie dort + wiederfindet, wo er nachsieht. `text` ist das einzige Feld + dafuer -- angehaengt, nicht ersetzt. */ + const antwort = art === "angenommen" + ? ` + +— Angenommen von ${person.name}${grund ? ": " + grund : "."}` + : ` + +— Nicht umgesetzt (${person.name}): ${grund}`; + db().prepare("UPDATE eintraege SET text = COALESCE(text, '') || ? WHERE id = ?") + .run(antwort, angebot.id); + + protokolliere("angebot_entschieden", { + personId: person.id, rolle: person.rolle, ip: echteIp(req), + detail: `#${angebot.id} ${art}`.slice(0, 120), + }); + res.json({ ok: true, art, aufgaben: angelegt.length }); + } catch (fehler) { + console.error("[workspace] Angebot entscheiden:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); diff --git a/server/workspace.js b/server/workspace.js index fd55f36c..90019855 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -284,6 +284,9 @@ const MODI_BEREICHE = [ { gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", name: "Ideen-Board", unter: "Gesammelt und nach Dringlichkeit sortiert", zeichen: "content", ton: 23, ziel: "bereich.html?b=ideen", szene: "portal" }, + { gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", + name: "Angebote", unter: "Vorschläge, über die DogFather entscheidet", + zeichen: "agentur", ton: 17, ziel: "bereich.html?b=angebote", szene: "halle" }, { gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung", name: "Live-Ablauf", unter: "Checkliste für vorher, mittendrin und danach", @@ -948,6 +951,17 @@ function umstellungen(d) { workspace-aufgaben.js) -- an EINER Stelle, durch die alle Schreibwege laufen. */ ["aufgaben", "kategorie", "TEXT"], + + /* WAS DOGFATHER DAZU TUN MUESSTE (10.09.2026). + + Filipe: *"auch entscheiden was ich machen muss wenn das und das + erledigt wird."* Der Modi schreibt es beim Angebot dazu; wird + das Angebot angenommen, wird genau daraus seine Aufgabe. + + Ein eigenes Feld und nicht im Fliesstext: Aus einem Absatz laesst + sich keine Aufgabe machen, ohne zu raten, welcher Satz gemeint + war. */ + ["eintraege", "einsatz", "TEXT"], ]) { try { const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name); @@ -1498,6 +1512,18 @@ function umstellungen(d) { waere die vierte Gelegenheit dazu. */ checkListeErweitern(d, "eintraege", "bereich", "ideen", ["live", "content", "technik", "community", "schutz", "agentur", "ideen"], jetztStempel); + /* Die Angebote (10.09.2026) -- Vorschlaege der Modis, ueber die + DogFather und VanVan entscheiden. */ + checkListeErweitern(d, "eintraege", "bereich", "angebote", + ["live", "content", "technik", "community", "schutz", "agentur", "ideen", "angebote"], + jetztStempel); + /* ZWEI NEUE ZUSTAENDE. Bisher kannte ein Eintrag nur offen/erledigt. + Ein Angebot hat aber eine ENTSCHEIDUNG: angenommen oder abgelehnt + -- und das ist etwas anderes als "erledigt". Wer beides in einen + Topf wirft, kann hinterher nicht mehr sagen, was aus einem + Vorschlag geworden ist. */ + checkListeErweitern(d, "eintraege", "status", "angenommen", + ["offen", "erledigt", "angenommen", "abgelehnt"], jetztStempel); } export function db() { diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index b4d3e323..993efac8 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609100303", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609100327", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609100303", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609100327", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100303", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100327", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100303", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100327", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index c9dfdf2e..cd79524d 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -414,6 +414,10 @@ }; const event = istEventArt(daten.art); if (einstellung.dringlichkeit && !event) daten.dringlichkeit = $('f-dringlich').value; + /* Verborgen heisst auch: nichts mitschicken. Ein Wert, der in einem + unsichtbaren Feld stehenbleibt, wandert sonst beim naechsten + Speichern mit -- und niemand sieht, woher er kommt. */ + if (einstellung.einsatzFeld && $('f-einsatz')) daten.einsatz = $('f-einsatz').value.trim(); if (einstellung.bewertung) daten.bewertung = $('f-bewertung').value || null; if (event) { daten.event_ende = $('f-ende').value || null; @@ -506,6 +510,29 @@ $('f-art').append(o); } $('feld-dringlich').hidden = !einstellung.dringlichkeit; + /* ================================================================= + ZWEI ZAHLEN, ANDERE NAMEN (10.09.2026) + + Bei den Angeboten heisst `bewertung` NUTZEN und `dringlichkeit` + AUFWAND. Die Felder gibt es laengst -- sie mit neuen Namen zu + belegen ist besser, als zwei weitere Spalten anzulegen, die + dasselbe koennen. Aber ein Feld, ueber dem "Dringlichkeit" steht + und das den Aufwand meint, ist falsch beschriftet, und das faellt + beim Ausfuellen niemandem auf -- erst beim Lesen. + + Die Namen kommen aus den Bereichsangaben; steht dort keiner, + bleibt es wie bisher. */ + if (einstellung.dringlichkeitName && $('dringlich-schild')) { + $('dringlich-schild').textContent = einstellung.dringlichkeitName; + } + if (einstellung.bewertungName && $('bewertung-schild')) { + $('bewertung-schild').textContent = `${einstellung.bewertungName} (1–5)`; + } + /* Das Einsatz-Feld nur, wo der Bereich es kennt. */ + if ($('feld-einsatz')) $('feld-einsatz').hidden = !einstellung.einsatzFeld; + if (einstellung.einsatzName && $('einsatz-schild')) { + $('einsatz-schild').textContent = einstellung.einsatzName; + } $('feld-bewertung').hidden = !einstellung.bewertung; if (LEITUNG.has(ich.rolle)) { diff --git a/workspace/assets/js/teamlage.js b/workspace/assets/js/teamlage.js index b7a5c755..2b96b12c 100644 --- a/workspace/assets/js/teamlage.js +++ b/workspace/assets/js/teamlage.js @@ -114,6 +114,165 @@ return k; } + /* ===================================================================== + DER EINGANG -- was die Modis gemeldet haben. + + Er steht OBEN, vor den Karten: Das ist das, was eine Antwort + braucht. Die Zahlen darunter kann man sich ansehen; hier wartet + jemand. + + ZWEI ANTWORTEN, und beide kommen beim Modi an. Eine Meldung, auf + die nichts folgt, sieht fuer ihn aus wie eine, die niemand gelesen + hat -- und beim dritten Mal meldet er nichts mehr. + ===================================================================== */ + function eingangBauen(liste) { + const k = el('section', 'tperson'); + k.append(el('p', 'feldschild', 'Rückmeldungen aus dem Team')); + if (!liste.length) { + k.append(el('p', 'tperson__zuletzt', + 'Nichts offen – auf alles, was gemeldet wurde, habt ihr geantwortet.')); + return k; + } + for (const m of liste) { + const zeile = el('div', 'tpipe'); + zeile.append(el('p', 'tperson__namen', m.titel)); + if (m.begruendung) zeile.append(el('p', 'tperson__unter', m.begruendung)); + zeile.append(el('span', 'tperson__zuletzt', + `${m.von || 'jemand'} · ${(m.am || '').slice(0, 10)}`)); + + const knoepfe = el('div', 'tpipe__reihe'); + const melden = (t) => { melde(t); }; + + const ja = el('button', 'schritt', 'Daraus wird eine Aufgabe'); + ja.type = 'button'; + ja.addEventListener('click', async () => { + ja.disabled = true; + try { + const a = await hole('/workspace/api/teamlage/behandeln', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + bereich: m.bereich, schluessel: m.schluessel, + person_id: m.person_id, art: 'aufgabe', + }), + }); + if (!a.ok) { + melden((await a.json().catch(() => ({}))).fehler || 'Das hat nicht geklappt.'); + ja.disabled = false; return; + } + melden(''); + await laden(); + } catch { ja.disabled = false; } + }); + knoepfe.append(ja); + + const nein = el('button', 'abmelden', 'Nicht nötig'); + nein.type = 'button'; + nein.addEventListener('click', async () => { + /* EINE ABSAGE OHNE GRUND IST KEINE ANTWORT. Deshalb wird + gefragt -- und ein leeres Feld bricht ab, statt still nichts + zu tun. */ + const grund = window.prompt( + `Warum machen wir das nicht? + +„${m.titel}“`, ''); + if (grund === null) return; + if (!grund.trim()) { melden('Ohne Grund bleibt die Meldung stehen.'); return; } + nein.disabled = true; + try { + const a = await hole('/workspace/api/teamlage/behandeln', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + bereich: m.bereich, schluessel: m.schluessel, + person_id: m.person_id, art: 'abgelehnt', grund: grund.trim(), + }), + }); + if (!a.ok) { + melden((await a.json().catch(() => ({}))).fehler || 'Das hat nicht geklappt.'); + nein.disabled = false; return; + } + melden(''); + await laden(); + } catch { nein.disabled = false; } + }); + knoepfe.append(nein); + zeile.append(knoepfe); + k.append(zeile); + } + return k; + } + + /* ===================================================================== + DIE ANGEBOTE -- Vorschlaege, ueber die ihr entscheidet. + + Sie stehen im selben Eingang wie die Rueckmeldungen: EIN Ort, an + dem etwas auf eine Antwort wartet. Wer zwei Eingaenge hat, sieht + irgendwann in den falschen. + ===================================================================== */ + const AUFWAND = { hoch: 'viel Aufwand', mittel: 'mittlerer Aufwand', niedrig: 'wenig Aufwand' }; + + async function entscheiden(knopf, koerper) { + knopf.disabled = true; + try { + const a = await hole('/workspace/api/teamlage/angebot', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(koerper), + }); + if (!a.ok) { + melde((await a.json().catch(() => ({}))).fehler || 'Das hat nicht geklappt.'); + knopf.disabled = false; return; + } + const d = await a.json().catch(() => ({})); + melde(d.aufgaben ? `${d.aufgaben} Aufgabe(n) angelegt.` : ''); + await laden(); + } catch { knopf.disabled = false; } + } + + function angeboteBauen(liste) { + const k = el('section', 'tperson'); + k.append(el('p', 'feldschild', 'Angebote aus dem Team')); + if (!liste.length) { + k.append(el('p', 'tperson__zuletzt', 'Gerade liegt kein Vorschlag zur Entscheidung.')); + return k; + } + for (const a of liste) { + const zeile = el('div', 'tpipe'); + zeile.append(el('p', 'tperson__namen', a.titel)); + if (a.text) zeile.append(el('p', 'tperson__unter', a.text)); + /* WAS ES FUER DICH HEISST, steht direkt dabei -- sonst muss man + die Entscheidung treffen, ohne den eigenen Teil zu kennen. */ + if (a.einsatz) { + zeile.append(el('p', 'tperson__unter', `Dein Teil: ${a.einsatz}`)); + } + zeile.append(el('span', 'tperson__zuletzt', + `${a.von_name || 'jemand'} · Nutzen ${a.bewertung ?? '–'}/5 · ` + + `${AUFWAND[a.dringlichkeit] || a.dringlichkeit}`)); + + const knoepfe = el('div', 'tpipe__reihe'); + const ja = el('button', 'schritt', 'Annehmen'); + ja.type = 'button'; + ja.addEventListener('click', () => entscheiden(ja, { id: a.id, art: 'angenommen' })); + knoepfe.append(ja); + + const nein = el('button', 'abmelden', 'Nicht umsetzen'); + nein.type = 'button'; + nein.addEventListener('click', () => { + const grund = window.prompt(`Warum nicht? + +„${a.titel}“`, ''); + if (grund === null) return; + if (!grund.trim()) { melde('Ohne Grund bleibt das Angebot stehen.'); return; } + entscheiden(nein, { id: a.id, art: 'abgelehnt', grund: grund.trim() }); + }); + knoepfe.append(nein); + zeile.append(knoepfe); + k.append(zeile); + } + return k; + } + async function laden() { const ziel = $('lage'); try { @@ -128,11 +287,15 @@ s + Object.values(m.bereiche).reduce((x, b) => x + b.offen, 0), 0); /* Eine leere Seite ohne Erklaerung sieht aus wie ein Fehler -- deshalb steht hier auch, was zu tun waere. */ + const wartend = (daten.eingang || []).length + (daten.angebote || []).length; $('kopf-unter').textContent = leute.length ? `${leute.length} ${leute.length === 1 ? 'Person' : 'Personen'} · ` - + `${offenGesamt} Punkte noch nicht besprochen` + + `${wartend} ${wartend === 1 ? 'Rückmeldung wartet' : 'Rückmeldungen warten'} · ` + + `${offenGesamt} Punkte noch nicht beurteilt` : 'Noch niemand im Team. Zugänge legst du unter „Personen & Zugänge“ an.'; + ziel.append(eingangBauen(daten.eingang || [])); + ziel.append(angeboteBauen(daten.angebote || [])); for (const m of leute) ziel.append(karte(m)); } catch { /* umgeleitet */ } } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 9ba3bc2b..9ae75548 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,12 +12,12 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + @@ -245,10 +245,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 1ff10d88..e1de7a1c 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -140,10 +140,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index daa4790b..6952e80b 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -75,8 +75,16 @@ + +