From a116b286cbd82ada955a7ef26f4baa566d57551d Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 9 Sep 2026 11:21:54 +0200 Subject: [PATCH] Aufgaben-Vorlagen: vier Bereiche mal vier Stufen, 48 fertige Aufgaben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filipe: "ich will dass du die vier klassen ueberall mit aufgaben perfektionnierst ... ich will dass die 4 kategorien ueberall perfekt angepasst sind, mit perfekt kategorien und aufgaben." WAS GEFEHLT HAT, NACHGEMESSEN Die Tabelle `aufgaben` kannte weder Bereich noch Stufe, und Aufgabenvorlagen gab es keine. Die Checkliste sagt, WAS sitzen muss -- sie sagt nicht, was jemand als Naechstes TUT. Zwischen "Ton ist der Grund Nummer eins" und einem Abend, an dem der Ton wirklich besser wird, liegt eine Aufgabe mit Frist und Verantwortlichem. Die musste bisher jemand von Hand tippen. WAS JETZT DASTEHT live 12 Aufgaben · 3 je Stufe content 12 Aufgaben · 3 je Stufe technik 12 Aufgaben · 3 je Stufe community 12 Aufgaben · 3 je Stufe zusammen 48 Drei je Feld und nicht zehn: Wer zehn Aufgaben bekommt, macht keine. Drei sind ein Abend, eine Woche, ein Monat -- und die Fristen sind entsprechend gestaffelt. Jede Aufgabe bringt ihre Frist als `tage` mit; eine Aufgabe ohne Frist ist ein Wunsch. Die Sachaussagen stammen aus derselben Recherche wie die Checklistenpunkte: Lautheit minus 20 bis minus 24 LUFS, Tonspur 160 kbit/s, Bitrate hoechstens 75 Prozent des Uploads, Ankuendigung 30 bis 60 Minuten vorher, Verweildauer ueber 15 Minuten, Eskalation in vier Stufen, Intro-Retention ab 70 Prozent. DER WEG BIS ZUR ECHTEN AUFGABE `/workspace/api/vorlagen` liefert sie aus, `/uebernehmen` legt sie an -- als Zeile in `aufgaben`, nicht als weiterer Eintrag in `eintraege`. Eine Aufgabe hat einen Verantwortlichen, eine Frist und einen Status, der sich bewegt; das ist etwas anderes als eine Notiz. Zwei Orte fuer dieselbe Sache waeren der Anfang davon, dass sie auseinanderlaufen. Ohne Creator wird abgelehnt statt herrenlos angelegt. Ein Creator legt sie immer fuer SICH an, auch wenn er jemand anderen angibt. AUF DER SEITE Ein Block ueber dem Aufgabenbrett: zwei Knopfreihen (Bereich, Stufe), drei Karten mit Titel, Erklaerung, Frist und einem Knopf. Die Knopfreihen benutzen `.stufenleiter` aus der Checkliste -- dieselbe Einteilung, dieselbe Darstellung. Eine zweite Optik fuer dieselben vier Bereiche waeren zwei Sachen zum Lernen statt einer. ZWEI EIGENE FEHLER, BEIDE VON PRUEFUNGEN GEFUNDEN 1. Ich hatte verlangt, dass die Frist mit der Stufe waechst. Die Pruefung schlug fehl: Profi 6,7 Tage gegen Fortgeschritten 7,4. Beim Nachsehen war die ERWARTUNG falsch. Eine Profi-Aufgabe ist meist eine Messung -- anspruchsvoll, aber an einem Abend erledigt; eine Fortgeschrittenen-Aufgabe wie "Saeulen anlegen und drei Wochen halten" braucht zwangslaeufig Wochen. Die Stufe sagt, was man KOENNEN muss, nicht wie lange es dauert. Geprueft wird jetzt, was wirklich gilt: keine Anfaengeraufgabe ueber fuenf Tage, keine Meisteraufgabe unter einer Woche. 2. Der Block war zu durchsichtig. pruef-buehne mass den Untergrund als rgb(84, 69, 70) -- das Buehnenfoto -- und die Ueberschrift kam auf 3,61:1 statt 4,5:1. Ein durchsichtiger Kasten hat keinen Untergrund, sondern den, der gerade dahinterliegt. Jetzt eine deckende Flaeche mit Weichzeichner: 5,21:1. GEPRUEFT Neu: server/pruef-aufgaben-vorlagen.mjs, 30 Pruefungen -- Vollstaendigkeit, der Weg bis zur Aufgabe auf dem Brett des Creators, und sechs Gegenproben (unbekannte Stufe, unbekannter Bereich, Nummer ohne Vorlage, ohne Creator, fremder Creator, und ein Creator, der jemand anderen angibt). Dazu ein Browserteil, der klickt statt nur zu zaehlen. Ausserdem gruen: pruef-aufgabenbrett, pruef-buehne, pruef-css-klassen, pruef-handy. Co-Authored-By: Claude Opus 5 --- server/pruef-aufgaben-vorlagen.mjs | 277 +++++++++++++++++++++++++++++ server/workspace-vorlagen.js | 260 +++++++++++++++++++++++++-- workspace/assets/css/aufgaben.css | 80 +++++++++ workspace/assets/js/aufgaben.js | 162 +++++++++++++++++ workspace/aufgaben.html | 14 ++ 5 files changed, 775 insertions(+), 18 deletions(-) create mode 100644 server/pruef-aufgaben-vorlagen.mjs diff --git a/server/pruef-aufgaben-vorlagen.mjs b/server/pruef-aufgaben-vorlagen.mjs new file mode 100644 index 00000000..8c4b230f --- /dev/null +++ b/server/pruef-aufgaben-vorlagen.mjs @@ -0,0 +1,277 @@ +/* Prueft die Aufgaben-Vorlagen: vier Bereiche, vier Stufen, und den + Weg von der Vorlage zur echten Aufgabe. + + WARUM DAS EINE EIGENE PRUEFUNG IST. Die Vorlagen sind Daten -- dass + sie vollstaendig sind, laesst sich in Sekunden zaehlen. Interessant + ist der Weg DANACH: Aus einer Vorlage muss eine Zeile in `aufgaben` + werden, mit Creator, Frist und Status, und sie muss in der + Aufgabenliste des Creators wirklich auftauchen. Zwischen "die Vorlage + gibt es" und "der Creator sieht die Aufgabe" liegen vier Schichten, + und jede kann still danebengehen. + + DREI SORTEN GEGENPROBE, alle drei noetig: + - eine unbekannte Stufe MUSS abgelehnt werden, + - eine Uebernahme ohne Creator MUSS abgelehnt werden (eine Aufgabe + ohne Adressaten ist ein Zettel), + - ein Betreuer darf sie NICHT fuer einen fremden Creator anlegen. */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-aufv-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4211"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(240_000, "pruef-aufgaben-vorlagen"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4211"; + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +for (let i = 0; i < 60; i++) { + try { const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001"); +const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001"); +const idFremd = anlegen("Fremd", "creator", "CODE-FREM-0001"); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idScout, jetzt); +d.close(); + +const keks = async (rolle, code) => { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code }) }); + return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); +}; +const kScout = await keks("scout", "CODE-SCOU-0001"); +const kTili = await keks("creator", "CODE-ANNA-0001"); +const ruf = (pfad, k, o = {}) => fetch(BASIS + pfad, { ...o, headers: { Cookie: k, ...(o.headers || {}) } }); +const senden = (pfad, k, art, koerper) => ruf(pfad, k, { + method: art, headers: { "Content-Type": "application/json" }, body: JSON.stringify(koerper || {}) }); + +/* ---------- 1. Die Vorlagen sind vollstaendig ---------------------------- */ +console.log("\n=== Vier Bereiche, vier Stufen, drei Aufgaben"); +const alles = await (await ruf("/workspace/api/vorlagen", kScout)).json(); +const V = alles.aufgaben; +ok(!!V, "die Aufgabenvorlagen werden ausgeliefert"); +const BEREICHE = ["live", "content", "technik", "community"]; +const STUFEN = ["anfaenger", "fort", "profi", "meister"]; +let gesamt = 0; +for (const b of BEREICHE) { + const fehlend = STUFEN.filter((st) => !(V[b] || {})[st] || !V[b][st].length); + const n = STUFEN.reduce((s, st) => s + ((V[b] || {})[st] || []).length, 0); + gesamt += n; + ok(fehlend.length === 0 && n >= 8, + `${b}: ${n} Aufgaben über ${STUFEN.length - fehlend.length} von 4 Stufen` + + (fehlend.length ? ` — fehlt: ${fehlend.join(", ")}` : "")); +} +ok(gesamt === 48, `zusammen ${gesamt} Aufgaben (erwartet 48)`); + +/* Jede Aufgabe braucht Titel, Text, Frist und einen eindeutigen + Schluessel -- eine Aufgabe ohne Frist ist ein Wunsch. */ +{ + const flach = BEREICHE.flatMap((b) => STUFEN.flatMap((st) => (V[b][st] || []))); + const unvollstaendig = flach.filter((v) => !v.titel || !v.text || !v.tage || !v.schluessel); + ok(unvollstaendig.length === 0, + `jede Aufgabe hat Titel, Text, Frist und Schlüssel (${flach.length - unvollstaendig.length} von ${flach.length})`); + const schluessel = flach.map((v) => v.schluessel); + const doppelt = schluessel.filter((x, i) => schluessel.indexOf(x) !== i); + ok(doppelt.length === 0, `alle Schlüssel eindeutig${doppelt.length ? ": " + doppelt.join(", ") : ""}`); + /* DIE FRIST WAECHST NICHT DURCHGEHEND MIT DER STUFE -- und das ist + kein Mangel. + + Der erste Anlauf verlangte anfaenger < fort < profi < meister und + schlug fehl: Profi kam auf 6,7 Tage, Fortgeschritten auf 7,4. Beim + Nachsehen war die Erwartung falsch, nicht die Zahl. Eine + Profi-Aufgabe ist meist eine MESSUNG -- "Upload messen und Bitrate + setzen", "Lautheit auf minus 20 bis minus 24 bringen": anspruchsvoll, + aber an einem Abend erledigt. Eine Fortgeschrittenen-Aufgabe wie + "Content-Saeulen anlegen und drei Wochen halten" braucht + zwangslaeufig Wochen, weil sie aus Wiederholung besteht. + + Die Stufe sagt, was man KOENNEN muss, nicht wie lange es dauert. + + Was dagegen wirklich gelten muss: Eine Anfaengeraufgabe ist an einem + Abend zu schaffen (sonst faengt niemand an), und eine Meisteraufgabe + ist es nie (sonst ist sie keine). */ + const schnitt = (st) => { + const l = BEREICHE.flatMap((b) => V[b][st] || []); + return l.reduce((s, v) => s + v.tage, 0) / l.length; + }; + const [sa, sf, sp, sm] = STUFEN.map(schnitt); + const anfListe = BEREICHE.flatMap((b) => V[b].anfaenger || []); + const meiListe = BEREICHE.flatMap((b) => V[b].meister || []); + const anfZuLang = anfListe.filter((v) => v.tage > 5); + const meiZuKurz = meiListe.filter((v) => v.tage < 7); + ok(anfZuLang.length === 0, + `jede Anfängeraufgabe ist in höchstens fünf Tagen zu schaffen` + + (anfZuLang.length ? ` — zu lang: ${anfZuLang.map((v) => v.schluessel).join(", ")}` : "")); + ok(meiZuKurz.length === 0, + `keine Meisteraufgabe unter einer Woche` + + (meiZuKurz.length ? ` — zu kurz: ${meiZuKurz.map((v) => v.schluessel).join(", ")}` : "")); + ok(sa < sm, + `Anfänger deutlich kürzer als Meister (${sa.toFixed(1)} gegen ${sm.toFixed(1)} Tage;` + + ` fort ${sf.toFixed(1)}, profi ${sp.toFixed(1)} — dazwischen gilt keine Reihenfolge)`); + /* GEGENPROBE: Eine erfundene Anfaengeraufgabe mit 30 Tagen MUSS + auffallen -- sonst prueft die Regel oben nichts. */ + ok([...anfListe, { schluessel: "test", tage: 30 }].filter((v) => v.tage > 5).length === 1, + `Gegenprobe: eine Anfängeraufgabe mit 30 Tagen fällt auf`); +} + +/* ---------- 2. Aus der Vorlage wird eine echte Aufgabe -------------------- */ +console.log("\n=== Von der Vorlage zur Aufgabe"); +{ + const vorher = (await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben?.length ?? 0; + const a = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST", + { art: "aufgabe", bereich: "technik", stufe: "anfaenger", nr: 0, creator_id: idTili }); + ok(a.ok, `der Scout kann eine Aufgabe für seinen Creator übernehmen (${a.status})`); + + const liste = (await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben || []; + ok(liste.length === vorher + 1, `sie steht in der Liste des Creators (${vorher} → ${liste.length})`); + const neu = liste.find((x) => x.titel === V.technik.anfaenger[0].titel); + ok(!!neu, `mit dem richtigen Titel ("${V.technik.anfaenger[0].titel}")`); + ok(!!neu && neu.status === "offen", `als offen angelegt (${neu?.status})`); + ok(!!neu && !!neu.frist, `und mit einer Frist (${neu?.frist})`); + /* Die Frist muss in der Zukunft liegen -- und zwar nach ORTSZEIT + gerechnet, nicht nach UTC. */ + if (neu?.frist) { + const heute = new Date(); + const h = `${heute.getFullYear()}-${String(heute.getMonth() + 1).padStart(2, "0")}` + + `-${String(heute.getDate()).padStart(2, "0")}`; + ok(neu.frist > h, `die Frist liegt in der Zukunft (heute ${h}, Frist ${neu.frist})`); + } +} + +/* Alle drei einer Stufe auf einmal. */ +{ + const vorher = ((await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben || []).length; + const a = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST", + { art: "aufgaben_alle", bereich: "community", stufe: "meister", creator_id: idTili }); + const nachher = ((await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben || []).length; + ok(a.ok && nachher === vorher + 3, + `alle drei einer Stufe auf einmal (${vorher} → ${nachher})`); +} + +/* ---------- 3. Gegenproben ------------------------------------------------ */ +console.log("\n=== Gegenproben"); +{ + const a = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST", + { art: "aufgabe", bereich: "technik", stufe: "gibt-es-nicht", nr: 0, creator_id: idTili }); + ok(a.status === 400, `eine unbekannte Stufe wird abgelehnt (${a.status})`); + + const b = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST", + { art: "aufgabe", bereich: "erfunden", stufe: "anfaenger", nr: 0, creator_id: idTili }); + ok(b.status === 400, `ein unbekannter Bereich wird abgelehnt (${b.status})`); + + const c = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST", + { art: "aufgabe", bereich: "technik", stufe: "anfaenger", nr: 99, creator_id: idTili }); + ok(c.status === 400, `eine Nummer, die es nicht gibt, wird abgelehnt (${c.status})`); + + const e = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST", + { art: "aufgabe", bereich: "technik", stufe: "anfaenger", nr: 0 }); + ok(e.status === 400, `ohne Creator wird abgelehnt — eine Aufgabe ohne Adressaten ist ein Zettel (${e.status})`); + + const f = await senden("/workspace/api/vorlagen/uebernehmen", kScout, "POST", + { art: "aufgabe", bereich: "technik", stufe: "anfaenger", nr: 0, creator_id: idFremd }); + ok(f.status === 404, `für einen fremden Creator wird abgelehnt (${f.status})`); + + /* Und der Creator selbst legt sie fuer sich an -- ohne creator_id, + der Server setzt sie. Sonst koennte er sie jemand anderem geben. */ + const g = await senden("/workspace/api/vorlagen/uebernehmen", kTili, "POST", + { art: "aufgabe", bereich: "live", stufe: "anfaenger", nr: 0, creator_id: idFremd }); + const beiFremd = ((await (await ruf("/workspace/api/aufgaben", kTili)).json()).aufgaben || []) + .filter((x) => x.titel === V.live.anfaenger[0].titel).length; + ok(g.ok && beiFremd === 1, + `ein Creator legt sie für SICH an, auch wenn er jemand anderen angibt (${g.status})`); +} + +/* ---------- 4. Im Browser: sieht man es, und geht es? -------------------- */ +console.log("\n=== Auf der Aufgabenseite"); +{ + const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await chromium.launch(); + const kontext = await browser.newContext({ viewport: { width: 1200, height: 1400 } }); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("pageerror", (f) => konsole.push(f.message)); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="creator"]'); + await seite.fill("#code", "CODE-ANNA-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 20000 }); + await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "domcontentloaded" }); + await seite.waitForTimeout(2500); + + const d = await seite.evaluate(() => { + const b = document.getElementById("vorlagenbrett"); + return { + sichtbar: !!b && !b.hidden, + reihen: document.querySelectorAll("#vorlagenbrett .stufenleiter").length, + karten: document.querySelectorAll("#vorlagenbrett .vorlagenbrett__karte").length, + knoepfe: document.querySelectorAll("#vorlagenbrett .vorlagenbrett__fuss .knopf").length, + /* Gestaltet oder nackt? Eine Karte ohne Kante ist keine Karte -- + derselbe Fehler wie damals bei der Checkliste, wo das CSS in + der falschen Datei lag und die Pruefung trotzdem gruen war, + weil sie nur Elemente gezaehlt hat. */ + gestaltet: (() => { + const k = document.querySelector("#vorlagenbrett .vorlagenbrett__karte"); + if (!k) return null; + const st = getComputedStyle(k); + return parseFloat(st.borderTopWidth) >= 1 && parseFloat(st.paddingLeft) >= 8; + })(), + ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, + }; + }); + ok(d.sichtbar, "der Block ist da"); + ok(d.reihen === 2, `zwei Knopfreihen: Bereich und Stufe (${d.reihen})`); + ok(d.karten === 3, `drei Aufgaben je Feld (${d.karten})`); + ok(d.knoepfe === 3, `jede hat einen Übernehmen-Knopf (${d.knoepfe})`); + ok(d.gestaltet === true, "die Karten sind gestaltet, nicht nackt"); + ok(d.ueber <= 0, `nichts steht seitlich über (${d.ueber}px)`); + + /* Der Bereich laesst sich wechseln, und die Karten wechseln mit. */ + const vorher = await seite.textContent("#vorlagenbrett .vorlagenbrett__titel"); + await seite.click("#vorlagenbrett .stufenleiter >> nth=0 >> .stufenleiter__knopf >> nth=2"); + await seite.waitForTimeout(500); + const nachher = await seite.textContent("#vorlagenbrett .vorlagenbrett__titel"); + ok(vorher !== nachher, `ein anderer Bereich zeigt andere Aufgaben ("${vorher}" → "${nachher}")`); + + /* Und der Weg bis zum Brett. */ + const zaehle = () => seite.evaluate(() => document.querySelectorAll("#brett .karte").length); + const brettVorher = await zaehle(); + await seite.click("#vorlagenbrett .vorlagenbrett__fuss .knopf"); + await seite.waitForTimeout(2500); + const brettNachher = await zaehle(); + ok(brettNachher === brettVorher + 1, + `Übernehmen legt eine Aufgabe auf dem Brett an (${brettVorher} → ${brettNachher})`); + ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? ": " + konsole[0] : ""}`); + await browser.close(); +} + +console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung."); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index 2dd7ce3e..4f8a9b69 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -250,10 +250,11 @@ export const LIVE_PUNKTE = { { schluessel: "die-ersten-dreissig-sekunden-stehen", titel: "Die ersten dreissig Sekunden stehen", text: "Wortwoertlich vorbereitet. In dieser halben Minute entscheidet sich, ob jemand bleibt, und genau dort improvisiert man am schlechtesten.", stufe: "profi" }, { schluessel: "ausweichweg-laeuft-mit", titel: "Ausweichweg laeuft mit", text: "Zweites Geraet bereit und angemeldet, nicht nur daneben. Ein Ersatz, den man erst einrichten muss, ist im Moment des Ausfalls keiner.", stufe: "meister" }, { schluessel: "ablauf-als-vorlage-nicht-als-zettel", titel: "Ablauf als Vorlage, nicht als Zettel", text: "Derselbe Aufbau jedes Mal, als Vorlage gespeichert. Was jedes Mal neu erfunden wird, wird jedes Mal anders gut.", stufe: "meister" }, - { schluessel: "ein-ziel-in-einer-zahl", titel: "Ein Ziel in einer Zahl", text: "Vorher festlegen, woran diese Sendung gemessen wird -- Verweildauer, Wiederkehrer, Kommentare. Ohne Zahl vorher ist die Auswertung danach eine Meinung.", stufe: "meister" }, /* --- Aus der Recherche vom 09.09.2026 --- */ + { schluessel: "ein-ziel-in-einer-zahl", titel: "Ein Ziel in einer Zahl", text: "Vorher festlegen, woran diese Sendung gemessen wird -- Verweildauer, Wiederkehrer, Kommentare. Ohne Zahl vorher ist die Auswertung danach eine Meinung.", stufe: "meister" }, + + /* --- Aus der Recherche vom 09.09.2026 --- */ { schluessel: "ankuendigung-kurz-vorher", titel: "Ankündigung kurz vorher", text: "Ein kurzes Video 30 bis 60 Minuten vor dem Start. Es bündelt die, die ohnehin kommen wollten – und ein voller Start zieht neue nach.", stufe: "fort" }, { schluessel: "sendeplan-steht-fest", titel: "Sendeplan steht fest", text: "Zwei feste Tage schlagen fünf zufällige. Wer weiß, wann du sendest, kommt wieder – und Wiederkehr ist das Signal, das man nicht fälschen kann.", stufe: "fort" }, - ], mitschrift: [ { schluessel: "guter-moment-aufheben", titel: "Guter Moment – aufheben", text: "Etwas ist gut gelaufen. Kurz notieren, woran es lag – daraus wird später ein Clip.", stufe: "anfaenger" }, @@ -264,9 +265,9 @@ export const LIVE_PUNKTE = { { schluessel: "neue-zuschauer-aufgetaucht", titel: "Neue Zuschauer aufgetaucht", text: "Woher kamen sie? Wurde jemand begrüßt oder ging es unter?", stufe: "fort" }, { schluessel: "ich-wusste-nicht-weiter", titel: "Ich wusste nicht weiter", text: "Stelle merken, an der es gestockt hat. Dafür gibt es Hilfe.", stufe: "anfaenger" }, { schluessel: "zeitstempel-statt-gefuehl", titel: "Zeitstempel statt Gefuehl", text: "Die Minute notieren, nicht 'irgendwann in der Mitte'. Nur mit der Minute findet man die Stelle in der Aufzeichnung wieder.", stufe: "profi" }, - { schluessel: "chatverlauf-gesichert", titel: "Chatverlauf gesichert", text: "Vor dem Schliessen speichern. Er ist die einzige Quelle dafuer, was wirklich gesagt wurde -- und bei einem Vorfall die einzige, die zaehlt.", stufe: "profi" }, { schluessel: "namen-vorgelesen", titel: "Namen vorgelesen", text: "Neue in den ersten Minuten begrüßen, nicht erst wenn Zeit ist. Wer beim Namen genannt wird, bleibt spürbar länger.", stufe: "anfaenger" }, + { schluessel: "chatverlauf-gesichert", titel: "Chatverlauf gesichert", text: "Vor dem Schliessen speichern. Er ist die einzige Quelle dafuer, was wirklich gesagt wurde -- und bei einem Vorfall die einzige, die zaehlt.", stufe: "profi" }, + { schluessel: "namen-vorgelesen", titel: "Namen vorgelesen", text: "Neue in den ersten Minuten begrüßen, nicht erst wenn Zeit ist. Wer beim Namen genannt wird, bleibt spürbar länger.", stufe: "anfaenger" }, { schluessel: "interaktion-eingefordert", titel: "Interaktion eingefordert", text: "Frage, Umfrage, Abstimmung. Ein Kommentar zählt mehr als stilles Zuschauen – und zieht den Stream zu neuen Leuten.", stufe: "fort" }, - ], auswertung: [ { schluessel: "was-hat-diesmal-getragen", titel: "Was hat diesmal getragen?", text: "Ein Ding. Nicht drei. Das eine, das man wiederholen will.", stufe: "anfaenger" }, @@ -277,10 +278,10 @@ export const LIVE_PUNKTE = { { schluessel: "braucht-besprechung", titel: "Braucht Besprechung", text: "Etwas, das man nicht allein lösen kann – geht an die Betreuung.", stufe: "anfaenger" }, { schluessel: "verweildauer-gegen-die-letzten-fuenf", titel: "Verweildauer gegen die letzten fuenf", text: "Nicht gegen das letzte Mal -- ein einzelner Vergleich ist Zufall. Fuenf Sendungen zeigen eine Richtung.", stufe: "profi" }, { schluessel: "ein-versuch-eine-aenderung", titel: "Ein Versuch, eine Aenderung", text: "Wer Titel, Uhrzeit und Aufbau zugleich aendert, weiss hinterher nicht, was gewirkt hat. Eine Sache je Sendung.", stufe: "profi" }, - { schluessel: "erkenntnis-zurueck-in-die-vorlage", titel: "Erkenntnis zurueck in die Vorlage", text: "Was sich bewaehrt hat, wird Teil des Ablaufs -- sonst muss man es sich jedes Mal neu merken.", stufe: "meister" }, { schluessel: "verweildauer-abgelesen", titel: "Verweildauer abgelesen", text: "Über 15 Minuten im Schnitt heißt: Die Stimmung trägt. Darunter liegt es fast nie am Thema, sondern am Anfang.", stufe: "profi" }, + { schluessel: "erkenntnis-zurueck-in-die-vorlage", titel: "Erkenntnis zurueck in die Vorlage", text: "Was sich bewaehrt hat, wird Teil des Ablaufs -- sonst muss man es sich jedes Mal neu merken.", stufe: "meister" }, + { schluessel: "verweildauer-abgelesen", titel: "Verweildauer abgelesen", text: "Über 15 Minuten im Schnitt heißt: Die Stimmung trägt. Darunter liegt es fast nie am Thema, sondern am Anfang.", stufe: "profi" }, { schluessel: "anteil-neuer-zuschauer", titel: "Anteil neuer Zuschauer", text: "Wie viele waren noch nie da? Ohne neue wächst nichts, ohne alte trägt nichts. Beide Zahlen gehören nebeneinander.", stufe: "profi" }, { schluessel: "wiederkehrer-ueber-vier-wochen", titel: "Wiederkehrer über vier Wochen", text: "Wie viele kommen ein zweites Mal? Diese eine Zahl sagt mehr über den Kanal als jeder Spitzenwert.", stufe: "meister" }, - ], }; @@ -311,10 +312,11 @@ export const COMMUNITY_PUNKTE = { { schluessel: "automatikfilter-stufe-begruendet", titel: "Automatikfilter: Stufe gewaehlt und begruendet", text: "Die Stufe des automatischen Filters ist eine Abwaegung, keine Voreinstellung: zu scharf loescht Harmloses, zu lasch laesst durch. Die Wahl gehoert aufgeschrieben.", stufe: "profi" }, { schluessel: "wer-eskaliert-an-wen", titel: "Wer eskaliert an wen", text: "Ab wann entscheidet die Moderation nicht mehr selbst? Bei Bedrohung, Rechtsverstoss oder Unsicherheit geht es weiter -- und es muss klar sein, an wen.", stufe: "profi" }, { schluessel: "moderatoren-eingewiesen-nicht-nur-ernannt", titel: "Moderatoren eingewiesen, nicht nur ernannt", text: "Rechte vergeben ist der kleinere Teil. Dazu gehoeren: die Regeln schriftlich, die Eskalationsleiter, die Werkzeuge einmal gezeigt.", stufe: "meister" }, - { schluessel: "ernstfall-einmal-geprobt", titel: "Ernstfall einmal geprobt", text: "Einmal durchspielen, wer was tut, wenn es kippt. Im Ernstfall zum ersten Mal darueber nachzudenken ist zu spaet.", stufe: "meister" }, /* --- Aus der Recherche vom 09.09.2026 --- */ + { schluessel: "ernstfall-einmal-geprobt", titel: "Ernstfall einmal geprobt", text: "Einmal durchspielen, wer was tut, wenn es kippt. Im Ernstfall zum ersten Mal darueber nachzudenken ist zu spaet.", stufe: "meister" }, + + /* --- Aus der Recherche vom 09.09.2026 --- */ { schluessel: "erlaubte-woerter-gepflegt", titel: "Erlaubte Wörter gepflegt", text: "Der Filter fängt auch Wörter, die bei euch harmlos sind. Eine kurze Ausnahmeliste verhindert, dass die Stammleute verstummen.", stufe: "fort" }, { schluessel: "eigener-kanal-fuers-mod-team", titel: "Eigener Kanal fürs Mod-Team", text: "Absprachen gehören nicht in den öffentlichen Chat. Ein eigener Raum, in dem gefragt werden darf, ohne dass alle mitlesen.", stufe: "profi" }, - ], aktion: [ { schluessel: "neue-begruessen", titel: "Neue begruessen", text: "Wer zum ersten Mal da ist, wird angesprochen. Kostet zwei Sekunden und entscheidet, ob jemand bleibt.", stufe: "anfaenger" }, @@ -322,9 +324,9 @@ export const COMMUNITY_PUNKTE = { { schluessel: "etwas-geben-das-nur-hier-gibt", titel: "Etwas geben, das nur hier gibt", text: "Ein Insider, ein Ritual, ein Spitzname. Community entsteht aus Gemeinsamem.", stufe: "fort" }, { schluessel: "ausserhalb-des-streams-antworten", titel: "Ausserhalb des Streams antworten", text: "Kommentare und Nachrichten -- die Community lebt auch zwischen den Sendungen.", stufe: "fort" }, { schluessel: "wiederkehrende-rubrik-mit-festem-termin", titel: "Wiederkehrende Rubrik mit festem Termin", text: "Etwas, das immer am selben Tag stattfindet. Ein Termin, auf den man sich freuen kann, bindet staerker als guter Einzelinhalt.", stufe: "profi" }, - { schluessel: "uebergabe-an-einen-zweiten-geregelt", titel: "Uebergabe an einen Zweiten geregelt", text: "Was passiert mit der Community, wenn man zwei Wochen ausfaellt? Wer nicht ersetzbar ist, kann nicht krank werden.", stufe: "meister" }, { schluessel: "begruessung-in-den-ersten-minuten", titel: "Begrüßung in den ersten Minuten", text: "Nicht wenn Zeit ist, sondern sofort. Wer in der ersten Minute angesprochen wird, bleibt deutlich länger.", stufe: "anfaenger" }, + { schluessel: "uebergabe-an-einen-zweiten-geregelt", titel: "Uebergabe an einen Zweiten geregelt", text: "Was passiert mit der Community, wenn man zwei Wochen ausfaellt? Wer nicht ersetzbar ist, kann nicht krank werden.", stufe: "meister" }, + { schluessel: "begruessung-in-den-ersten-minuten", titel: "Begrüßung in den ersten Minuten", text: "Nicht wenn Zeit ist, sondern sofort. Wer in der ersten Minute angesprochen wird, bleibt deutlich länger.", stufe: "anfaenger" }, { schluessel: "ein-ritual-das-jeder-kennt", titel: "Ein Ritual, das jeder kennt", text: "Derselbe Einstieg, dieselbe Verabschiedung. Wiedererkennung ist der halbe Grund wiederzukommen.", stufe: "fort" }, - ], konflikt: [ { schluessel: "ruhig-bleiben-und-nicht-oeffentlich-stre", titel: "Ruhig bleiben und nicht oeffentlich streiten", text: "Im Chat gewinnt niemand. Kurz ansagen, dann privat klaeren.", stufe: "anfaenger" }, @@ -333,8 +335,8 @@ export const COMMUNITY_PUNKTE = { { schluessel: "betreuung-einbeziehen", titel: "Betreuung einbeziehen", text: "Bei allem, was groesser ist als ein blöder Kommentar. Dafuer ist sie da.", stufe: "anfaenger" }, { schluessel: "danach-was-hat-gefehlt", titel: "Danach: was hat gefehlt?", text: "Regel unklar, niemand zum Eingreifen da, zu spaet gemerkt -- daraus wird der naechste Punkt oben.", stufe: "fort" }, { schluessel: "entscheidung-dokumentiert", titel: "Entscheidung dokumentiert, nicht nur getroffen", text: "Wer, wann, warum, welche Stufe. Beim zweiten Mal mit derselben Person ist das der Unterschied zwischen 'wir haben da was' und 'ich glaube'.", stufe: "profi" }, - { schluessel: "regel-nachgezogen", titel: "Regel nachgezogen, damit es nicht wiederkommt", text: "Ein Vorfall, der von keiner Regel gedeckt war, ist eine Luecke -- kein Einzelfall. Die Regel wird ergaenzt, nicht der Fall vergessen.", stufe: "meister" }, { schluessel: "gleich-behandelt-nicht-nach-sympathie", titel: "Gleich behandelt, nicht nach Sympathie", text: "Dieselbe Regel für Stammgast und Fremden. Eine Ausnahme kostet mehr Vertrauen als zehn Sperren.", stufe: "fort" }, - + { schluessel: "regel-nachgezogen", titel: "Regel nachgezogen, damit es nicht wiederkommt", text: "Ein Vorfall, der von keiner Regel gedeckt war, ist eine Luecke -- kein Einzelfall. Die Regel wird ergaenzt, nicht der Fall vergessen.", stufe: "meister" }, + { schluessel: "gleich-behandelt-nicht-nach-sympathie", titel: "Gleich behandelt, nicht nach Sympathie", text: "Dieselbe Regel für Stammgast und Fremden. Eine Ausnahme kostet mehr Vertrauen als zehn Sperren.", stufe: "fort" }, ], }; @@ -356,12 +358,13 @@ export const TECHNIK_PUNKTE = { { schluessel: "alles-einmal-komplett-getestet", titel: "Alles einmal komplett getestet", text: "Eine Probesendung von fuenf Minuten, aufgezeichnet und angesehen.", stufe: "anfaenger" }, { schluessel: "tonfilter-in-der-richtigen-reihenfolge", titel: "Tonfilter in der richtigen Reihenfolge", text: "Erst Rauschunterdrueckung, dann Kompressor, dann Rauschsperre. Die Reihenfolge ist nicht beliebig: Ein Kompressor vor der Rauschunterdrueckung hebt das Rauschen mit an.", stufe: "profi" }, { schluessel: "hardware-encoder-statt-prozessor", titel: "Hardware-Encoder statt Prozessor", text: "Wenn die Grafikkarte einen eigenen Encoder hat, uebernimmt der das Kodieren -- der Prozessor bleibt frei fuer alles andere. Das ist der groesste Einzelgewinn an Stabilitaet.", stufe: "profi" }, - { schluessel: "szenen-und-filter-gesichert", titel: "Szenen und Filter gesichert", text: "Die ganze Einrichtung als Sammlung exportiert und abgelegt. Nach einem Rechnerwechsel ist das der Unterschied zwischen zehn Minuten und einem Abend.", stufe: "meister" }, /* --- Aus der Recherche vom 09.09.2026 --- */ + { schluessel: "szenen-und-filter-gesichert", titel: "Szenen und Filter gesichert", text: "Die ganze Einrichtung als Sammlung exportiert und abgelegt. Nach einem Rechnerwechsel ist das der Unterschied zwischen zehn Minuten und einem Abend.", stufe: "meister" }, + + /* --- Aus der Recherche vom 09.09.2026 --- */ { schluessel: "mikrofon-abstand-und-winkel", titel: "Abstand und Winkel des Mikrofons", text: "Fünf bis zehn Zentimeter, leicht seitlich gesprochen. Näher dran und leiser aufgedreht ist die beste Verbesserung, die nichts kostet.", stufe: "anfaenger" }, { schluessel: "tonspur-mit-160-kbit", titel: "Tonspur mit 160 kbit/s", text: "Darunter klingt Sprache dumpf, darüber nimmt es dem Bild die Bandbreite. 160 ist der übliche Wert für Stimme.", stufe: "fort" }, { schluessel: "bildrate-im-spiel-begrenzt", titel: "Bildrate im Spiel begrenzt", text: "Ein ungebremstes Spiel nimmt dem Encoder die Luft. Ein Deckel bei 60 oder 120 behebt die meisten Ruckler.", stufe: "fort" }, { schluessel: "lautheit-gemessen", titel: "Lautheit gemessen, nicht geschätzt", text: "Zielbereich minus 20 bis minus 24 LUFS, Begrenzer bei minus 3. Wer nach Gefühl aussteuert, ist entweder leiser als alle anderen oder übersteuert.", stufe: "profi" }, - ], problem: [ { schluessel: "ton-weg-oder-verzerrt", titel: "Ton weg oder verzerrt", text: "Wann genau, welches Geraet, was lief gleichzeitig? Ohne das findet es niemand.", stufe: "anfaenger" }, @@ -369,17 +372,17 @@ export const TECHNIK_PUNKTE = { { schluessel: "stream-bricht-ab", titel: "Stream bricht ab", text: "Zeitpunkt notieren und im Protokoll des Programms nachsehen.", stufe: "fort" }, { schluessel: "ton-und-bild-laufen-auseinander", titel: "Ton und Bild laufen auseinander", text: "Meist ein Puffer. Wann faellt es auf, sofort oder erst spaeter?", stufe: "fort" }, { schluessel: "verworfene-bilder-abgelesen", titel: "Verworfene Bilder abgelesen, nicht geraten", text: "Das Programm zeigt, ob Bilder wegen der Leitung oder wegen des Kodierens verloren gehen. Das sind zwei verschiedene Fehler mit zwei verschiedenen Loesungen.", stufe: "profi" }, - { schluessel: "ausfall-mit-datum-festgehalten", titel: "Ausfall mit Datum festgehalten", text: "Jeder Ausfall bekommt einen Eintrag. Erst die Liste zeigt, ob es dasselbe Problem ist oder drei verschiedene.", stufe: "meister" }, { schluessel: "statistikfenster-offen-gehabt", titel: "Statistikfenster offen gehabt", text: "Verworfene Bilder und Renderverzögerung stehen dort in Echtzeit. Hinterher raten kostet mehr Zeit als vorher hinsehen.", stufe: "fort" }, + { schluessel: "ausfall-mit-datum-festgehalten", titel: "Ausfall mit Datum festgehalten", text: "Jeder Ausfall bekommt einen Eintrag. Erst die Liste zeigt, ob es dasselbe Problem ist oder drei verschiedene.", stufe: "meister" }, + { schluessel: "statistikfenster-offen-gehabt", titel: "Statistikfenster offen gehabt", text: "Verworfene Bilder und Renderverzögerung stehen dort in Echtzeit. Hinterher raten kostet mehr Zeit als vorher hinsehen.", stufe: "fort" }, { schluessel: "netz-oder-rechner-unterschieden", titel: "Netz oder Rechner unterschieden", text: "Verworfene Bilder kommen vom Netz, ausgelassene vom Encoder. Verschiedene Ursachen, verschiedene Lösungen – erst trennen, dann handeln.", stufe: "profi" }, - ], loesung: [ { schluessel: "was-hat-es-behoben", titel: "Was hat es behoben?", text: "Genau die eine Aenderung aufschreiben, nicht alles was probiert wurde.", stufe: "anfaenger" }, { schluessel: "kommt-es-wieder", titel: "Kommt es wieder?", text: "Beim naechsten Mal nachsehen. Einmal weg ist nicht geloest.", stufe: "fort" }, { schluessel: "einstellung-gesichert", titel: "Einstellung gesichert", text: "Damit sie nach einem Update nicht wieder weg ist.", stufe: "fort" }, { schluessel: "genau-eine-aenderung-je-versuch", titel: "Genau eine Aenderung je Versuch", text: "Wer drei Dinge gleichzeitig aendert und es geht wieder, weiss nicht warum -- und beim naechsten Mal steht man wieder am Anfang.", stufe: "profi" }, - { schluessel: "loesung-als-anleitung-abgelegt", titel: "Loesung als Anleitung abgelegt", text: "So aufgeschrieben, dass jemand anderes sie nachvollziehen kann. Eine Loesung im Kopf einer Person ist keine Loesung des Teams.", stufe: "meister" }, { schluessel: "profile-ausserhalb-gesichert", titel: "Profile außerhalb des Rechners gesichert", text: "Szenen, Filter und Profile auf einen zweiten Datenträger. Ein Rechnerschaden kostet sonst zwei Abende Wiederaufbau.", stufe: "meister" }, - + { schluessel: "loesung-als-anleitung-abgelegt", titel: "Loesung als Anleitung abgelegt", text: "So aufgeschrieben, dass jemand anderes sie nachvollziehen kann. Eine Loesung im Kopf einer Person ist keine Loesung des Teams.", stufe: "meister" }, + { schluessel: "profile-ausserhalb-gesichert", titel: "Profile außerhalb des Rechners gesichert", text: "Szenen, Filter und Profile auf einen zweiten Datenträger. Ein Rechnerschaden kostet sonst zwei Abende Wiederaufbau.", stufe: "meister" }, ], }; export const CALL_LEITFAEDEN = [ @@ -415,6 +418,190 @@ function angemeldet(req, res, next) { req.person = person; next(); } + +/* ===================================================================== + AUFGABEN-VORLAGEN — vier Bereiche, vier Stufen, drei Aufgaben je Feld + + Filipe (09.09.2026): "ich will dass du die vier klassen ueberall mit + aufgaben perfektionnierst ... die 4 kategorien ueberall perfekt + angepasst, mit perfekt kategorien und aufgaben." + + WARUM ES DAS BRAUCHT. Die Checkliste sagt, WAS sitzen muss. Sie sagt + nicht, was jemand als Naechstes TUT. Zwischen "Ton ist der Grund + Nummer eins" und einem Abend, an dem der Ton wirklich besser wird, + liegt eine Aufgabe mit Frist und Verantwortlichem -- und die gab es + bisher nur, wenn jemand sie von Hand tippte. + + WARUM DREI JE FELD UND NICHT ZEHN. Wer zehn Aufgaben bekommt, macht + keine. Drei sind ein Abend, eine Woche, ein Monat -- in dieser + Reihenfolge, und die Fristen sind entsprechend gestaffelt. + + WIE DIE STUFEN GEMEINT SIND (Dreyfus-Modell, wie in der Checkliste): + anfaenger Was ohne Weiteres nicht geht. Meist an einem Abend zu tun. + fort Aus Zufall wird Routine. Braucht Wiederholung, also Wochen. + profi Gemessen statt geschaetzt: ein Wert, ein Versuch, ein Beleg. + meister Was ohne dich weiterlaeuft: Vorlage, Vertretung, Ablage. + + `tage` ist der Vorschlag fuer die Frist, gerechnet ab dem Tag der + Uebernahme. Er ist kein Zierrat: Eine Aufgabe ohne Frist ist ein + Wunsch. Die Zahlen folgen der Stufe -- Anfaengeraufgaben in Tagen, + Meisteraufgaben in Wochen. + + Die Sachaussagen stammen aus derselben Recherche wie die + Checklistenpunkte (siehe dort): Lautheit -20 bis -24 LUFS, + Tonspur 160 kbit/s, Ankuendigung 30 bis 60 Minuten vorher, + Verweildauer ueber 15 Minuten als Zeichen, dass die Stimmung traegt, + Eskalation in vier Stufen. + ===================================================================== */ + +export const AUFGABEN_VORLAGEN = { + live: { + anfaenger: [ + { schluessel: "live-ton-aufnehmen-und-anhoeren", titel: "Ton aufnehmen und anhören", tage: 2, + text: "Eine Minute mit dem echten Aufbau aufnehmen und mit Kopfhörern anhören. Nicht während des Streams – da hört man sich selbst nie." }, + { schluessel: "live-licht-einmal-richtig-stellen", titel: "Licht einmal richtig stellen", tage: 2, + text: "Gesicht heller als der Hintergrund, kein Fenster im Rücken. Ein Foto vorher und nachher machen – der Unterschied ist größer, als man denkt." }, + { schluessel: "live-titel-und-thema-vorher", titel: "Titel und Thema vorher aufschreiben", tage: 3, + text: "Für die nächsten drei Streams je eine Zeile. Wer den Titel liest, muss wissen, ob es für ihn ist." }, + ], + fort: [ + { schluessel: "live-sendeplan-vier-wochen", titel: "Sendeplan für vier Wochen festlegen", tage: 7, + text: "Zwei feste Tage schlagen fünf zufällige. In den Kalender eintragen, damit die Betreuung mitplanen kann." }, + { schluessel: "live-ankuendigung-zur-routine", titel: "Ankündigung zur Routine machen", tage: 7, + text: "30 bis 60 Minuten vor jedem Stream ein kurzes Video. Es bündelt die, die ohnehin kommen wollten – und ein voller Start zieht neue nach." }, + { schluessel: "live-ablauf-in-drei-bloecken", titel: "Ablauf in drei Blöcken", tage: 5, + text: "Anfang, Mitte, Schluss – je zwei Stichworte. Einmal danach senden und hinterher notieren, wo es gehakt hat." }, + ], + profi: [ + { schluessel: "live-upload-messen-bitrate-setzen", titel: "Upload messen, Bitrate setzen", tage: 3, + text: "Upload messen und höchstens 75 Prozent davon als Bitrate einstellen. Darüber verwirft die Leitung Bilder, sobald sonst noch etwas läuft." }, + { schluessel: "live-erste-dreissig-sekunden", titel: "Die ersten dreißig Sekunden ausformulieren", tage: 5, + text: "Wörtlich aufschreiben, nicht stichwortartig. In den ersten Sekunden entscheidet sich, ob jemand bleibt – das ist der einzige Teil, den man auswendig können sollte." }, + { schluessel: "live-verweildauer-vergleichen", titel: "Verweildauer der letzten fünf Streams vergleichen", tage: 7, + text: "Durchschnittliche Verweildauer nebeneinanderlegen. Über 15 Minuten heißt: Die Stimmung trägt. Darunter liegt es fast nie am Thema, sondern am Anfang." }, + ], + meister: [ + { schluessel: "live-ablauf-als-vorlage", titel: "Ablauf als Vorlage ablegen", tage: 14, + text: "So aufschreiben, dass eine Vertretung den Stream fahren könnte. Wer nicht ersetzbar ist, kann nicht krank werden." }, + { schluessel: "live-ein-ziel-vier-wochen", titel: "Ein Ziel in einer Zahl, vier Wochen verfolgen", tage: 28, + text: "Eine einzige Zahl je Stream – Verweildauer, neue Zuschauer oder Wiederkehrer. Vier Wochen dieselbe, sonst vergleicht man nichts." }, + { schluessel: "live-ausweichweg-proben", titel: "Ausweichweg einrichten und proben", tage: 14, + text: "Zweites Gerät, zweite Leitung oder ein Ersatzstream. Einmal wirklich umschalten – ein ungeprobter Ausweichweg ist keiner." }, + ], + }, + + content: { + anfaenger: [ + { schluessel: "content-drei-ideen-mit-termin", titel: "Drei Ideen übernehmen und terminieren", tage: 3, + text: "Aus der Ideenliste drei aussuchen und jeder ein Datum geben. Eine Idee ohne Termin ist eine Notiz." }, + { schluessel: "content-ein-video-mit-haken", titel: "Ein Video mit Haken in Sekunde eins", tage: 5, + text: "Der erste Satz ist eine Frage, eine Behauptung oder eine Zahl – kein „Hey Leute“. Über 70 Prozent entscheiden in den ersten drei Sekunden." }, + { schluessel: "content-bester-moment-als-clip", titel: "Den besten Moment als Clip schneiden", tage: 3, + text: "Aus dem letzten LIVE einen Ausschnitt unter 60 Sekunden. Der Feed hat mehr Reichweite als der Stream – er ist der Weg dorthin." }, + ], + fort: [ + { schluessel: "content-saeulen-anlegen", titel: "Content-Säulen anlegen und drei Wochen halten", tage: 14, + text: "Rund 70 Prozent Wert, 20 Prozent Community, 10 Prozent eigene Sache. Drei Wochen danach planen und dann nachsehen, was es verändert hat." }, + { schluessel: "content-fuenf-haken-vorher", titel: "Fünf Haken schreiben, bevor gedreht wird", tage: 5, + text: "Erst die ersten Sätze, dann die Kamera. Wer beim Drehen nach dem Einstieg sucht, findet ihn nicht." }, + { schluessel: "content-format-zweimal-wiederholen", titel: "Ein Format zweimal wiederholen", tage: 14, + text: "Dasselbe Format, andere Sache. Erst beim zweiten Mal sieht man, ob das Format trägt oder nur das Thema getragen hat." }, + ], + profi: [ + { schluessel: "content-intro-retention-ablesen", titel: "Intro-Retention der letzten zehn Videos ablesen", tage: 7, + text: "Wie viele bleiben über die ersten drei Sekunden hinaus? Gute Werte liegen bei 70 Prozent und mehr. Die drei schlechtesten nebeneinanderlegen und den Haken vergleichen." }, + { schluessel: "content-zwei-haken-gegeneinander", titel: "Zwei Haken gegeneinander testen", tage: 10, + text: "Gleiches Video, gleicher Schnitt, gleiche Zeit – nur der erste Satz anders. Ein Versuch, eine Änderung, sonst weiß man hinterher nichts." }, + { schluessel: "content-untertitel-ohne-ton", titel: "Untertitel prüfen: ohne Ton lesbar", tage: 5, + text: "Alle Videos einmal stumm durchsehen. Der größte Teil wird ohne Ton geschaut – was dann nicht ankommt, kommt gar nicht an." }, + ], + meister: [ + { schluessel: "content-redaktionsplan-vier-wochen", titel: "Redaktionsplan für vier Wochen", tage: 21, + text: "So geschrieben, dass jemand anderes ihn fahren könnte: Thema, Format, Säule, Termin. Das ist der Unterschied zwischen einem Plan und einer Liste." }, + { schluessel: "content-vorlagensammlung", titel: "Vorlagensammlung anlegen", tage: 21, + text: "Haken, Schnittfolge, Endkarte, Beschreibungstext. Einmal gut gemacht und danach zehnmal benutzt – das ist der ganze Unterschied zwischen Fleiß und System." }, + { schluessel: "content-zweiten-einweisen", titel: "Einen Zweiten einweisen", tage: 28, + text: "Jemand, der Clips schneiden kann, ohne jedes Mal zu fragen. Dafür braucht es die Vorlagensammlung – deshalb steht sie davor." }, + ], + }, + + technik: { + anfaenger: [ + { schluessel: "technik-mikrofon-abstand", titel: "Mikrofon auf fünf bis zehn Zentimeter", tage: 1, + text: "Näher heran und die Verstärkung herunter, leicht seitlich gesprochen. Das ist die wirksamste Verbesserung, die nichts kostet." }, + { schluessel: "technik-kamera-einmal-einstellen", titel: "Kamera scharf stellen und belichten", tage: 2, + text: "Automatik aus, einmal von Hand einstellen. Eine Kamera, die während des Streams nachregelt, pumpt im Bild." }, + { schluessel: "technik-kompletter-durchlauf", titel: "Einen kompletten Durchlauf testen", tage: 3, + text: "Ton, Bild, Verbindung, Szenenwechsel – zehn Minuten privat aufnehmen und ansehen. Nicht kurz vor dem Stream." }, + ], + fort: [ + { schluessel: "technik-szenen-anlegen", titel: "Vier Szenen anlegen", tage: 5, + text: "Start, Haupt, Pause, Ende. Wer während des Streams Fenster sucht, ist für zwei Minuten weg – und die Zuschauer auch." }, + { schluessel: "technik-tonspur-160", titel: "Tonspur auf 160 kbit/s stellen", tage: 2, + text: "Darunter klingt Sprache dumpf, darüber nimmt es dem Bild die Bandbreite. 160 ist der übliche Wert für Stimme." }, + { schluessel: "technik-bildrate-begrenzen", titel: "Bildrate im Spiel begrenzen", tage: 2, + text: "Deckel bei 60 oder 120 Bildern. Ein ungebremstes Spiel nimmt dem Encoder die Luft – das behebt die meisten Ruckler." }, + ], + profi: [ + { schluessel: "technik-lautheit-messen", titel: "Lautheit messen und einstellen", tage: 5, + text: "Zielbereich minus 20 bis minus 24 LUFS, Begrenzer bei minus 3. Wer nach Gefühl aussteuert, ist entweder leiser als alle anderen oder übersteuert." }, + { schluessel: "technik-filterkette-pruefen", titel: "Filterkette prüfen", tage: 5, + text: "Reihenfolge: Rauschsperre, Entrauschen, Kompressor, Begrenzer. Falsch sortiert arbeiten sie gegeneinander – und man hört nur, dass etwas nicht stimmt." }, + { schluessel: "technik-statistik-eine-woche", titel: "Eine Woche Statistikfenster mitlaufen lassen", tage: 7, + text: "Verworfene Bilder und Renderverzögerung nach jedem Stream notieren. Verworfen kommt vom Netz, ausgelassen vom Encoder – verschiedene Ursachen, verschiedene Lösungen." }, + ], + meister: [ + { schluessel: "technik-profile-sichern", titel: "Szenen, Filter und Profile sichern", tage: 7, + text: "Auf einen zweiten Datenträger, nicht auf denselben Rechner. Ein Rechnerschaden kostet sonst zwei Abende Wiederaufbau." }, + { schluessel: "technik-ersatzweg-einrichten", titel: "Ersatzweg einrichten", tage: 21, + text: "Zweites Gerät oder zweite Leitung, die übernehmen kann. Einmal wirklich umschalten – sonst weiß man nicht, ob es geht." }, + { schluessel: "technik-stoerungsliste", titel: "Störungsliste anlegen", tage: 14, + text: "Symptom, Ursache, Lösung – drei Spalten. Beim zweiten Mal ist derselbe Ausfall dann kein Abend mehr, sondern fünf Minuten." }, + ], + }, + + community: { + anfaenger: [ + { schluessel: "community-drei-regeln", titel: "Drei Regeln aufschreiben und sichtbar machen", tage: 2, + text: "Kurz, verständlich, an einer Stelle, die man findet. Drei Sätze reichen – wer zehn Regeln schreibt, hat keine." }, + { schluessel: "community-erste-minute-begruessen", titel: "In der ersten Minute begrüßen", tage: 1, + text: "Neue beim Namen ansprechen, sobald sie da sind – nicht wenn Zeit ist. Wer angesprochen wird, bleibt deutlich länger." }, + { schluessel: "community-wer-greift-ein", titel: "Klären, wer eingreifen kann", tage: 3, + text: "Eine zweite Person mit Rechten während des Streams – oder die ausdrückliche Feststellung, dass keine da ist. Beides ist besser als Unklarheit." }, + ], + fort: [ + { schluessel: "community-wortfilter-einrichten", titel: "Wortfilter einrichten und beobachten", tage: 7, + text: "Mittlere Stufe, eine Woche mitlesen, dann nachjustieren. Zu scharf löscht Harmloses, zu lasch lässt durch – beides merkt man erst im Betrieb." }, + { schluessel: "community-erlaubte-woerter", titel: "Ausnahmeliste erlaubter Wörter pflegen", tage: 7, + text: "Der Filter fängt auch Wörter, die bei euch harmlos sind. Ohne Ausnahmeliste verstummen ausgerechnet die Stammleute." }, + { schluessel: "community-feste-rubrik", titel: "Eine feste Rubrik mit Termin einführen", tage: 14, + text: "Etwas, das immer am selben Tag stattfindet. Ein Termin, auf den man sich freuen kann, bindet stärker als guter Einzelinhalt." }, + ], + profi: [ + { schluessel: "community-eskalationsleiter", titel: "Eskalationsleiter schriftlich festlegen", tage: 7, + text: "Vier Stufen in dieser Reihenfolge: ansprechen, Nachricht löschen, kurze Auszeit, Sperre. Wer sie erst im Moment festlegt, entscheidet ungleich." }, + { schluessel: "community-mod-kanal", titel: "Eigenen Kanal fürs Mod-Team einrichten", tage: 5, + text: "Absprachen gehören nicht in den öffentlichen Chat. Ein Raum, in dem gefragt werden darf, ohne dass alle mitlesen." }, + { schluessel: "community-entscheidungen-dokumentieren", titel: "Entscheidungen dokumentieren", tage: 14, + text: "Wer, wann, warum, welche Stufe. Beim zweiten Mal mit derselben Person ist das der Unterschied zwischen „wir haben da was“ und „ich glaube“." }, + ], + meister: [ + { schluessel: "community-moderatoren-einweisen", titel: "Moderatoren einweisen", tage: 14, + text: "Regeln schriftlich, Eskalationsleiter, Werkzeuge einmal gezeigt. Rechte zu vergeben ist der kleinere Teil." }, + { schluessel: "community-ernstfall-proben", titel: "Ernstfall einmal durchspielen", tage: 21, + text: "Wer tut was, wenn es kippt? Im Ernstfall zum ersten Mal darüber nachzudenken ist zu spät." }, + { schluessel: "community-uebergabe-regeln", titel: "Übergabe regeln", tage: 21, + text: "Wer führt die Community, wenn du zwei Wochen ausfällst? Schriftlich, mit Zugängen – sonst ist es kein Plan, sondern eine Hoffnung." }, + ], + }, +}; + +/* Alle Aufgabenvorlagen in einer flachen Liste -- fuer Pruefungen und + fuer das Nachschlagen eines einzelnen Schluessels. */ +export const AUFGABEN_VORLAGEN_FLACH = Object.entries(AUFGABEN_VORLAGEN) + .flatMap(([bereich, stufen]) => Object.entries(stufen) + .flatMap(([stufe, liste]) => liste.map((v) => ({ ...v, bereich, stufe })))); + vorlagenRouter.use("/workspace/api/vorlagen", angemeldet); vorlagenRouter.get("/workspace/api/vorlagen", (req, res) => { @@ -424,6 +611,7 @@ vorlagenRouter.get("/workspace/api/vorlagen", (req, res) => { community: COMMUNITY_PUNKTE, technik: TECHNIK_PUNKTE, calls: CALL_LEITFAEDEN, + aufgaben: AUFGABEN_VORLAGEN, }); }); @@ -502,6 +690,42 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, if (!liste) return res.status(400).json({ fehler: "Unbekannter Abschnitt." }); for (const v of liste) einfuegen(bereich, unterart, v.titel, v.text); + } else if (art === "aufgabe" || art === "aufgaben_alle") { + /* ECHTE AUFGABEN, KEINE EINTRAEGE. + + Alles darueber legt Eintraege an (Ideen, Notizen, Befunde). + Eine Aufgabe ist etwas anderes: Sie hat einen + Verantwortlichen, eine Frist und einen Status, der sich + bewegt. Deshalb eine eigene Zeile in `aufgaben` und nicht + noch eine Art in `eintraege` -- sonst stuende dieselbe Sache + an zwei Orten, und die beiden liefen auseinander. + + OHNE CREATOR KEINE AUFGABE. Eine Aufgabe ohne Adressaten ist + ein Zettel. Der Server lehnt sie deshalb ab, statt sie + herrenlos anzulegen. */ + const bereich = String(req.body.bereich || ""); + const stufe = String(req.body.stufe || ""); + const feld = (AUFGABEN_VORLAGEN[bereich] || {})[stufe]; + if (!feld) return res.status(400).json({ fehler: "Unbekannter Bereich oder unbekannte Stufe." }); + if (!creatorId) return res.status(400).json({ fehler: "Bitte zuerst einen Creator wählen." }); + const auswahl = art === "aufgaben_alle" ? feld : [feld[nr]]; + if (auswahl.some((v) => !v)) return res.status(400).json({ fehler: "Unbekannte Vorlage." }); + for (const v of auswahl) { + /* Die Frist wird aus `tage` gerechnet, in ORTSZEIT. Ueber + toISOString kaeme zwischen Mitternacht und zwei Uhr der + Vortag heraus -- derselbe Fehler, der im Haus schon einmal + eine Pruefung falsch rot gemacht hat. */ + const d = new Date(Date.now() + (v.tage || 7) * 86400_000); + const frist = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` + + `-${String(d.getDate()).padStart(2, "0")}`; + db().prepare(`INSERT INTO aufgaben + (titel, beschreibung, status, prioritaet, creator_id, frist, erstellt, erstellt_von) + VALUES (?,?,'offen',?,?,?,?,?)`) + .run(v.titel, v.text, stufe === "anfaenger" ? "hoch" : "mittel", + creatorId, frist, jetzt(), req.person.id); + angelegt.push(v.titel); + } + } else { return res.status(400).json({ fehler: "Unbekannte Art." }); } diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css index 268a6c29..16ce8673 100644 --- a/workspace/assets/css/aufgaben.css +++ b/workspace/assets/css/aufgaben.css @@ -835,3 +835,83 @@ font-size: .76rem; line-height: 1.45; color: var(--text-still); } + + +/* ===================================================================== + AUFGABEN AUS DER VORLAGE (09.09.2026) + + Der Block sitzt über dem Brett und muss sich als VORRAT lesen, nicht + als weitere Spalte: eigene Fläche, ruhigere Farbe, deutlich weniger + Kontrast als die Karten darunter. Wer das Brett voll hat, soll ihn + überblättern können, ohne ihn wegklicken zu müssen. + + Die beiden Knopfreihen benutzen `.stufenleiter` aus start.css -- + dieselbe Einteilung, dieselbe Darstellung. Eine zweite Optik für + dieselben vier Bereiche wäre zwei Sachen zum Lernen statt einer. + ===================================================================== */ +.vorlagenbrett { + margin: 0 0 22px; + padding: 20px 20px 22px; + border-radius: var(--radius-klein, 14px); + /* EINE EIGENE FLAECHE, KEIN SCHLEIER. + + Erster Anlauf: nur ein Hauch Weiss ueber dem Buehnenbild. pruef-buehne + hat das zu Recht abgelehnt -- der Untergrund war gemessen + rgb(84, 69, 70), also das Foto, und die Ueberschrift kam auf 3,61:1 + statt der noetigen 4,5:1. Ein durchsichtiger Kasten hat keinen + Untergrund, sondern den, der gerade dahinterliegt. + + Jetzt eine deckende dunkle Flaeche mit einem Hauch Struktur darauf. + Der Weichzeichner dahinter macht aus dem Foto einen ruhigen Grund, + statt es einfach zuzudecken -- dieselbe Behandlung wie bei den + anderen freistehenden Bloecken im Haus. */ + background: + linear-gradient(168deg, rgba(255, 255, 255, .05), rgba(255, 255, 255, .02)), + rgba(6, 9, 16, .84); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(160, 190, 225, .16); +} +.vorlagenbrett__kopf { margin-bottom: 14px; } +.vorlagenbrett__unter { + margin-top: 6px; font-size: .86rem; line-height: 1.5; + color: var(--text-leise); + max-width: 62ch; +} +.vorlagenbrett__hinweis { + margin: 12px 0 0; font-size: .86rem; font-weight: 600; + color: var(--warn, #ff9aa2); +} +.vorlagenbrett__liste { + display: grid; gap: 12px; margin-top: 16px; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} +.vorlagenbrett__karte { + display: flex; flex-direction: column; gap: 8px; + padding: 16px 18px; + border-radius: 12px; + background: rgba(4, 7, 12, .72); + border: 1px solid rgba(160, 190, 225, .14); +} +.vorlagenbrett__titel { + margin: 0; font-size: 1rem; font-weight: 700; line-height: 1.3; + color: var(--text); +} +.vorlagenbrett__text { + margin: 0; font-size: .86rem; line-height: 1.5; color: var(--text-leise); +} +/* Der Fuß trägt Frist und Knopf. `margin-top: auto` schiebt ihn nach + unten, damit die Knöpfe aller Karten auf einer Linie stehen -- auch + wenn die Texte verschieden lang sind. */ +.vorlagenbrett__fuss { + display: flex; align-items: center; justify-content: space-between; + gap: 12px; margin-top: auto; padding-top: 6px; +} +.vorlagenbrett__frist { + font-size: .78rem; font-weight: 700; letter-spacing: .04em; + color: var(--text-still); +} +@media (max-width: 620px) { + .vorlagenbrett { padding: 16px 14px 18px; } + .vorlagenbrett__liste { grid-template-columns: 1fr; } +} diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index b6ad0882..f8297205 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -841,5 +841,167 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe } await laden(); + await vorlagenbrettZeigen(); })(); + + /* ===================================================================== + AUFGABEN AUS DER VORLAGE + + Vier Bereiche, vier Stufen, drei Aufgaben je Feld. Der Block ist + bewusst KEINE zweite Aufgabenverwaltung: Er legt eine ganz normale + Aufgabe an, mit Frist und Verantwortlichem, und verschwindet danach + aus dem Weg. Alles Weitere macht das Brett darunter. + + WARUM DIE BEIDEN REIHEN DIESELBEN KNOEPFE SIND WIE IN DER + CHECKLISTE: Es sind dieselben vier Bereiche und dieselben vier + Stufen. Zwei verschiedene Darstellungen fuer dieselbe Einteilung + waeren zwei Sachen zum Lernen statt einer. + + OHNE CREATOR KEINE AUFGABE. Ein Creator legt sie fuer sich an, ein + Betreuer fuer den, den er oben gewaehlt hat. Ist keiner gewaehlt, + steht das da -- statt eines Knopfes, der beim Druecken einen Fehler + zeigt. + ===================================================================== */ + const BEREICHE_VORLAGE = [ + { schluessel: 'live', name: 'LIVE-Analyse' }, + { schluessel: 'content', name: 'Content-Ideen' }, + { schluessel: 'technik', name: 'Technik' }, + { schluessel: 'community', name: 'Community' }, + ]; + const STUFEN_VORLAGE = [ + { schluessel: 'anfaenger', name: 'Anfänger', + hilfe: 'Was von Anfang an sitzen muss. Meist an einem Abend zu schaffen.' }, + { schluessel: 'fort', name: 'Fortgeschritten', + hilfe: 'Aus Zufall wird Routine. Braucht Wiederholung, also Wochen.' }, + { schluessel: 'profi', name: 'Profi', + hilfe: 'Gemessen statt geschätzt: ein Wert, ein Versuch, ein Beleg.' }, + { schluessel: 'meister', name: 'Meister', + hilfe: 'Was auch ohne dich weiterläuft — Vorlage, Vertretung, Ablage.' }, + ]; + let vorlagen = null; + let vBereich = 'live'; + let vStufe = 'anfaenger'; + + async function vorlagenbrettZeigen() { + const ziel = $('vorlagenbrett'); + if (!ziel || !ich) return; + if (!vorlagen) { + try { + const a = await hole('/workspace/api/vorlagen'); + if (!a.ok) return; + vorlagen = (await a.json()).aufgaben || null; + } catch { return; } + } + if (!vorlagen) return; + ziel.hidden = false; + ziel.textContent = ''; + + const kopf = el('div', 'vorlagenbrett__kopf'); + kopf.append(el('h2', 'feldschild', 'Aufgaben aus der Vorlage')); + kopf.append(el('p', 'vorlagenbrett__unter', + 'Vier Bereiche, vier Stufen. Übernommen wird als ganz normale Aufgabe ' + + '– mit Frist, veränderbar, auf dem Brett darunter.')); + ziel.append(kopf); + + /* Reihe 1: der Bereich. */ + const r1 = el('div', 'stufenleiter'); + r1.setAttribute('role', 'group'); + r1.setAttribute('aria-label', 'Bereich wählen'); + for (const b of BEREICHE_VORLAGE) { + const k = el('button', 'stufenleiter__knopf'); + k.type = 'button'; + k.dataset.stufe = b.schluessel; + k.setAttribute('aria-pressed', String(vBereich === b.schluessel)); + k.append(el('span', 'stufenleiter__name', b.name)); + k.addEventListener('click', () => { vBereich = b.schluessel; vorlagenbrettZeigen(); }); + r1.append(k); + } + ziel.append(r1); + + /* Reihe 2: die Stufe, mit der Zahl der Aufgaben darin. */ + const r2 = el('div', 'stufenleiter'); + r2.setAttribute('role', 'group'); + r2.setAttribute('aria-label', 'Könnensstufe wählen'); + for (const st of STUFEN_VORLAGE) { + const n = ((vorlagen[vBereich] || {})[st.schluessel] || []).length; + if (!n) continue; + const k = el('button', 'stufenleiter__knopf'); + k.type = 'button'; + k.dataset.stufe = st.schluessel; + k.setAttribute('aria-pressed', String(vStufe === st.schluessel)); + k.title = st.hilfe; + k.append(el('span', 'stufenleiter__name', st.name)); + k.append(el('span', 'stufenleiter__zahl', String(n))); + k.addEventListener('click', () => { vStufe = st.schluessel; vorlagenbrettZeigen(); }); + r2.append(k); + } + ziel.append(r2); + const hilfe = STUFEN_VORLAGE.find((x) => x.schluessel === vStufe)?.hilfe; + if (hilfe) ziel.append(el('p', 'stufenleiter__hilfe', hilfe)); + + /* Für wen? */ + const fuer = ich.rolle === 'creator' ? ich.id : nurCreator; + if (!fuer) { + ziel.append(el('p', 'vorlagenbrett__hinweis', + 'Wähle oben einen Creator, dann lassen sich diese Aufgaben übernehmen.')); + } + + const liste = (vorlagen[vBereich] || {})[vStufe] || []; + const box = el('div', 'vorlagenbrett__liste'); + for (const [nr, v] of liste.entries()) { + const karte = el('div', 'vorlagenbrett__karte'); + karte.append(el('h3', 'vorlagenbrett__titel', v.titel)); + karte.append(el('p', 'vorlagenbrett__text', v.text)); + const fuss = el('div', 'vorlagenbrett__fuss'); + fuss.append(el('span', 'vorlagenbrett__frist', + v.tage === 1 ? 'Frist: morgen' : `Frist: in ${v.tage} Tagen`)); + if (fuer) { + const k = el('button', 'knopf knopf--klein', 'Übernehmen'); + k.type = 'button'; + k.addEventListener('click', () => uebernehmen(k, { art: 'aufgabe', nr }, fuer)); + fuss.append(k); + } + karte.append(fuss); + box.append(karte); + } + ziel.append(box); + + if (fuer && liste.length > 1) { + const alle = el('button', 'abmelden', `Alle ${liste.length} übernehmen`); + alle.type = 'button'; + alle.style.marginTop = '12px'; + alle.addEventListener('click', () => uebernehmen(alle, { art: 'aufgaben_alle' }, fuer)); + ziel.append(alle); + } + } + + /* Ein Weg fuer beide Knoepfe -- einzeln und alle. Zwei fast gleiche + Funktionen waeren der sichere Weg dazu, dass die eine irgendwann + anders reagiert als die andere. */ + async function uebernehmen(knopf, was, fuer) { + knopf.disabled = true; + const vorher = knopf.textContent; + knopf.textContent = 'Wird übernommen …'; + try { + const a = await hole('/workspace/api/vorlagen/uebernehmen', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...was, bereich: vBereich, stufe: vStufe, creator_id: fuer }), + }); + if (!a.ok) { + const f = await a.json().catch(() => ({})); + melde(f.fehler || 'Das hat nicht geklappt.'); + knopf.disabled = false; knopf.textContent = vorher; + return; + } + melde(''); + /* Erst das Brett neu laden, dann den Block -- so sieht man die + neue Aufgabe sofort dort, wo sie ab jetzt lebt. */ + await laden(); + await vorlagenbrettZeigen(); + } catch { + melde('Das hat nicht geklappt.'); + knopf.disabled = false; knopf.textContent = vorher; + } + } })(); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 9ea74f49..5c1f739a 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -107,6 +107,20 @@ + + +

Aufgaben werden geladen …