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 @@ + +