Feste Checklisten statt Vorschlaege - in allen vier Bereichen

"Die neuen Sachen in der LIVE-Analyse soll der Creator FEST sehen, auch
schoen kategorisiert, und der Ansprechpartner soll anklicken koennen,
wenn er findet, dass der Creator was verbessern sollte."

DER UMBAU. Vorher waren die Punkte Vorschlaege zum Uebernehmen: Man
holte sie sich und bekam einen eigenen Eintrag. Das war falsch gedacht.
Eine Checkliste, die man erst anfordern muss, ist keine Checkliste --
und schlimmer: Zwei Creator haetten unterschiedliche Listen gehabt, je
nachdem wer sich was geholt hat. Genau das macht einen Vergleich
unmoeglich, und um Vergleich geht es bei einer Betreuung.

Jetzt stehen dieselben Punkte fuer JEDEN fest da, gruppiert:

  LIVE       21 Punkte -- vor, waehrend, nach der Sendung
  Community  14 Punkte -- Moderation vorbereiten, aufbauen, wenn es kippt
  Technik    12 Punkte -- Einrichtung, Ausfall, was geholfen hat
  Content    13 Ideen  -- nach Saeule (70/20/10) statt nach Ablauf

Jede Gruppe hat einen Satz, der erklaert, wofuer sie da ist. Eine
Ueberschrift allein sagt das nicht.

Was sich je Creator unterscheidet, ist nur der STAND -- und den setzt
die Betreuung: "Passt so" oder "Verbessern". Ein Creator kann sich nicht
selbst bewerten; koennte er es, stuende alles auf gruen. "Verbessern"
verlangt einen Satz, WAS zu verbessern ist -- eine Bewertung, mit der er
nichts anfangen kann, ist nicht streng, sondern nur entmutigend.

Der Creator sieht alles: die Stufe, den Grund, den Namen und den
Zeitpunkt. Und er kann an JEDEM Punkt antworten -- das ist der Kanal,
ueber den er ueberhaupt etwas sagen kann.

Oben steht eine Bilanz in einer Zeile: wie viele passen, wie viele sind
zu verbessern, wie viele hat noch niemand angesehen. Das ist die Frage,
die beide Seiten zuerst haben.

STABILE SCHLUESSEL statt Positionen. Ein Stand haengt am Schluessel des
Punktes ("ton-geprueft"), nicht an seiner Nummer. Haenge er an der
Position, waere beim Einfuegen eines Punktes in der Mitte jede Bewertung
dahinter am falschen Punkt -- und niemand wuerde es merken, weil beides
plausibel aussieht. 64 Punkte haben jetzt einen.

"Offen" loescht den Stand, statt ihn auf "offen" zu setzen: Ein
Datensatz, der nichts aussagt, ist Ballast.

ZWEI EIGENE FEHLER, VON DEN PRUEFUNGEN GEFUNDEN:

1. DAS CSS LAG IN DER FALSCHEN DATEI. Ich hatte es in content.css
   geschrieben -- bereich.html laedt die gar nicht. Die Punkte standen
   auf drei von vier Seiten nackt und ohne Flaeche da. Es ist derselbe
   Fehler wie im August bei .knopf-still, .schalter und .kopf-zeile, und
   es gibt seitdem eine Warnung dazu in start.css. Sie hat nichts
   genutzt, solange keine Pruefung sie nachhielt.

   Jetzt gibt es eine: Sie misst, ob eine Karte wirklich eine Kante und
   Polsterung hat -- nicht nur, ob das Element existiert. Gegenprobe
   gemacht: Klasse umbenannt, Pruefung meldet "STIL FEHLT".

2. Ein Betreuer sah beim Oeffnen keine Bewertungsknoepfe, weil noch kein
   Creator gewaehlt war -- und musste erst raten, dass er oben jemanden
   auswaehlen soll. Jetzt nimmt der Server den ersten betreuten Creator,
   wenn keiner angegeben ist.

Die alte Oberflaechenpruefung fuer den Vorschlaege-Block wurde entfernt
statt angepasst: Sie verlangte etwas, das es nicht mehr gibt. Eine
dauerhaft rote Pruefung ist schlimmer als keine -- man gewoehnt sich
daran, und beim naechsten echten Fehler sieht niemand hin.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-01 17:12:55 +02:00
co-authored by Claude Opus 5
parent 372a0eb95a
commit 9267797d1d
33 changed files with 1473 additions and 279 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 876 KiB

After

Width:  |  Height:  |  Size: 952 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 KiB

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 KiB

After

Width:  |  Height:  |  Size: 843 KiB

+2
View File
@@ -34,6 +34,7 @@ import { steckbriefRouter } from "./workspace-steckbrief.js";
import { vorlagenRouter } from "./workspace-vorlagen.js";
import { schulungRouter } from "./workspace-schulung.js";
import { ampelRouter } from "./workspace-ampel.js";
import { checklisteRouter } from "./workspace-checkliste.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SITE_DIR = join(__dirname, "..");
@@ -102,6 +103,7 @@ app.use(steckbriefRouter);
app.use(vorlagenRouter);
app.use(schulungRouter);
app.use(ampelRouter);
app.use(checklisteRouter);
app.use(aufgabenRouter);
app.use(personenRouter);
app.use(profilRouter);
+325
View File
@@ -0,0 +1,325 @@
/* Prueft die feste Checkliste.
Zwei Dinge sind hier wichtiger als alles andere:
1. DIE LISTE IST FUER JEDEN GLEICH. Das war der Grund fuer den Umbau:
Vorher musste man sich Punkte "holen", und zwei Creator haetten
unterschiedliche Listen gehabt. Damit ist kein Vergleich moeglich,
und um Vergleich geht es bei einer Betreuung.
2. DER SCHLUESSEL IST STABIL. Ein Stand haengt am Schluessel des
Punktes, nicht an seiner Position. Haenge er an der Position, waere
beim Einfuegen eines Punktes in der Mitte jede Bewertung dahinter
am falschen Punkt -- und niemand merkte es, weil beides plausibel
aussieht. Geprueft wird das, indem die gespeicherte Zeile direkt
angesehen wird: Dort muss Text stehen, keine Nummer. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-check-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4207";
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 }); };
await import("./index.js");
await new Promise((r) => setTimeout(r, 700));
const BASIS = "http://127.0.0.1:4207";
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.();
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
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;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
const idZwei = anlegen("Zweiter", "creator", "CODE-ZWEI-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.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idZwei, 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 kDogi = await keks("admin", "CODE-DOGI-0001");
const kScout = await keks("scout", "CODE-SCOU-0001");
const kTili = await keks("creator", "CODE-ANNA-0001");
const kZwei = await keks("creator", "CODE-ZWEI-0001");
const kFremd = await keks("creator", "CODE-FREM-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 || {}) });
/* ---------- Vier Bereiche, alle vorbereitet ------------------------------- */
console.log("\n=== Alle vier Bereiche haben eine Liste");
const listen = {};
for (const bereich of ["live", "community", "technik", "content"]) {
const a = await ruf(`/workspace/api/checkliste/${bereich}`, kTili);
const r = await a.json();
listen[bereich] = r;
const punkte = r.gruppen.reduce((s, g) => s + g.punkte.length, 0);
ok(a.ok && r.gruppen.length === 3 && punkte >= 6,
`${bereich}: ${r.gruppen.length} Gruppen, ${punkte} Punkte (${r.gruppen.map((g) => g.name).join(" · ")})`);
/* Jede Gruppe braucht einen erklaerenden Satz -- eine Ueberschrift
allein sagt nicht, wofuer die Punkte da sind. */
ok(r.gruppen.every((g) => g.text && g.text.length > 20),
` jede Gruppe erklaert sich (${r.gruppen.filter((g) => !g.text).length} ohne Text)`);
/* Jeder Punkt braucht einen stabilen Schluessel. */
const ohne = r.gruppen.flatMap((g) => g.punkte).filter((p) => !p.schluessel);
ok(ohne.length === 0, ` jeder Punkt hat einen Schluessel (${ohne.length} ohne)`);
}
/* Content wird nach Saeule gruppiert -- 70/20/10. */
ok(listen.content.gruppen[0].punkte.length >= listen.content.gruppen[1].punkte.length,
`Content: die Wert-Saeule ist die groesste (${listen.content.gruppen.map((g) => g.punkte.length).join("/")})`);
/* ---------- DIE LISTE IST FUER JEDEN GLEICH ------------------------------- */
console.log("\n=== Fuer jeden dieselbe Liste");
{
const a = await (await ruf("/workspace/api/checkliste/live", kTili)).json();
const b = await (await ruf("/workspace/api/checkliste/live", kZwei)).json();
const c = await (await ruf("/workspace/api/checkliste/live", kDogi)).json();
const schluessel = (r) => r.gruppen.flatMap((g) => g.punkte.map((p) => p.schluessel)).join(",");
ok(schluessel(a) === schluessel(b),
"zwei Creator sehen exakt dieselben Punkte");
ok(schluessel(a) === schluessel(c),
"und die Leitung ebenfalls -- sonst waere kein Vergleich moeglich");
}
/* ---------- Bewerten ------------------------------------------------------ */
console.log("\n=== Bewerten darf nur die Betreuung");
const punkt = listen.live.gruppen[0].punkte[0].schluessel;
{
const selbst = await senden(`/workspace/api/checkliste/live/${punkt}`, kTili, "PUT",
{ stufe: "gut" });
ok(selbst.status === 403, `ein Creator kann sich NICHT selbst bewerten (${selbst.status})`);
const ohneGrund = await senden(`/workspace/api/checkliste/live/${punkt}`, kScout, "PUT",
{ stufe: "verbessern", creator_id: idTili });
ok(ohneGrund.status === 400, `"verbessern" ohne Grund abgelehnt (${ohneGrund.status})`);
const mit = await senden(`/workspace/api/checkliste/live/${punkt}`, kScout, "PUT",
{ stufe: "verbessern", begruendung: "Der Ton rauscht noch.", creator_id: idTili });
ok(mit.ok, `mit Grund geht es (${mit.status})`);
const r = await mit.json();
ok(r.stand.stufe === "verbessern" && r.stand.begruendung === "Der Ton rauscht noch.",
"Stufe und Grund stehen");
ok(r.stand.von === idScout, `mit dem Namen des Bewertenden (${r.stand.von})`);
/* Ein Punkt, den es nicht gibt, wird abgewiesen -- sonst koennte man
Staende fuer erfundene Schluessel anlegen. */
const erfunden = await senden("/workspace/api/checkliste/live/gibt-es-nicht", kScout, "PUT",
{ stufe: "gut", creator_id: idTili });
ok(erfunden.status === 404, `ein erfundener Punkt wird abgewiesen (${erfunden.status})`);
/* Und ein Punkt aus einem ANDEREN Bereich auch nicht. */
const falscherBereich = await senden(
`/workspace/api/checkliste/technik/${punkt}`, kScout, "PUT",
{ stufe: "gut", creator_id: idTili });
ok(falscherBereich.status === 404,
`ein Punkt aus einem anderen Bereich ebenfalls (${falscherBereich.status})`);
}
/* ---------- DER SCHLUESSEL IST STABIL ------------------------------------- */
console.log("\n=== Der Stand haengt am Punkt, nicht an seiner Position");
{
const p = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
const z = p.prepare("SELECT * FROM punkt_stand WHERE creator_id = ?").get(idTili);
p.close();
ok(z && typeof z.schluessel === "string" && !/^\d+$/.test(z.schluessel),
`gespeichert wird der Schluessel, keine Nummer ("${z?.schluessel}")`);
ok(z?.schluessel === punkt, "und zwar genau der des Punktes");
}
/* ---------- Der Creator SIEHT es ------------------------------------------ */
console.log("\n=== Der Creator sieht, was er verbessern soll");
{
const r = await (await ruf("/workspace/api/checkliste/live", kTili)).json();
ok(r.stand[punkt]?.stufe === "verbessern", `er sieht die Stufe (${r.stand[punkt]?.stufe})`);
ok(r.stand[punkt]?.begruendung === "Der Ton rauscht noch.", "und den Grund");
ok(r.darf_bewerten === false, `aber darf nichts setzen (${r.darf_bewerten})`);
/* Der ANDERE Creator darf davon nichts sehen. */
const b = await (await ruf("/workspace/api/checkliste/live", kZwei)).json();
ok(!b.stand[punkt], "ein anderer Creator sieht diesen Stand NICHT");
}
/* ---------- Zurueck auf offen --------------------------------------------- */
console.log("\n=== Zurueck auf offen");
{
const a = await senden(`/workspace/api/checkliste/live/${punkt}`, kScout, "PUT",
{ stufe: "offen", creator_id: idTili });
ok(a.ok, `laesst sich zuruecknehmen (${a.status})`);
const r = await a.json();
ok(r.stand === null, "und der Stand ist weg statt auf 'offen' gesetzt");
const p = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
const n = p.prepare("SELECT COUNT(*) AS n FROM punkt_stand WHERE creator_id = ?").get(idTili).n;
p.close();
ok(n === 0, `kein Datensatz ohne Aussage bleibt liegen (${n})`);
}
/* ---------- Nachrichten je Punkt ------------------------------------------ */
console.log("\n=== Rueckmeldung an einem Punkt");
{
const a = await senden(`/workspace/api/checkliste/live/${punkt}/nachrichten`, kTili, "POST",
{ text: "Ich hab ein neues Mikro bestellt." });
ok(a.ok, `ein Creator darf antworten (${a.status})`);
const b = await senden(`/workspace/api/checkliste/live/${punkt}/nachrichten`, kScout, "POST",
{ text: "Super, melde dich wenn es da ist.", creator_id: idTili });
ok(b.ok, `die Betreuung auch (${b.status})`);
const { nachrichten } = await b.json();
ok(nachrichten.length === 2, `beide im Verlauf (${nachrichten.length})`);
/* Sie haengen am PUNKT -- ein anderer Punkt hat seinen eigenen Verlauf. */
const anderer = listen.live.gruppen[0].punkte[1].schluessel;
const leer = await (await ruf(
`/workspace/api/checkliste/live/${anderer}/nachrichten`, kTili)).json();
ok(leer.nachrichten.length === 0,
`ein anderer Punkt hat seinen eigenen Verlauf (${leer.nachrichten.length})`);
/* Nur die eigene zuruecknehmen. */
const meine = nachrichten.find((n) => n.von === idTili);
const fremdWeg = await ruf(`/workspace/api/checkliste/nachricht/${meine.id}`, kScout,
{ method: "DELETE" });
ok(fremdWeg.status === 403, `fremde Nachricht bleibt stehen (${fremdWeg.status})`);
const eigenWeg = await ruf(`/workspace/api/checkliste/nachricht/${meine.id}`, kTili,
{ method: "DELETE" });
ok(eigenWeg.ok, `die eigene laesst sich zuruecknehmen (${eigenWeg.status})`);
}
/* ---------- Fremdes bleibt fremd ------------------------------------------ */
console.log("\n=== Fremdes bleibt fremd");
{
const a = await senden(`/workspace/api/checkliste/live/${punkt}`, kScout, "PUT",
{ stufe: "gut", creator_id: idFremd });
ok(a.status === 404,
`ein Scout bewertet keinen fremden Creator -- und erfaehrt nicht, dass es ihn gibt (${a.status})`);
const b = await ruf(`/workspace/api/checkliste/live?creator_id=${idTili}`, kFremd);
const r = await b.json();
/* Bei einem Creator wird creator_id ignoriert -- er sieht immer nur
sich selbst. Der Stand von Tili darf da nicht auftauchen. */
ok(r.creator_id === idFremd,
`ein Creator sieht immer nur sich selbst (${r.creator_id} = ${idFremd})`);
}
/* ---------- Im Browser ----------------------------------------------------- */
console.log("\n=== Die Seiten");
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
/* Wieder eine Bewertung setzen, damit es etwas zu sehen gibt. */
await senden(`/workspace/api/checkliste/live/${punkt}`, kScout, "PUT",
{ stufe: "verbessern", begruendung: "Mikro naeher heranstellen.", creator_id: idTili });
for (const [rolle, code, name, darf] of [
["scout", "CODE-SCOU-0001", "Patrick", true],
["creator", "CODE-ANNA-0001", "Tili", false],
]) {
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1100 } });
const konsole = [];
const seite = await kontext.newPage();
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
await seite.click(`.rolle[data-rolle="${rolle}"]`);
await seite.fill("#code", code);
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 15000 });
for (const [pfad, bereich] of [
["bereich.html?b=live", "live"],
["bereich.html?b=community", "community"],
["bereich.html?b=technik", "technik"],
["content.html", "content"],
]) {
await seite.goto(BASIS + "/workspace/" + pfad, { waitUntil: "networkidle" });
await seite.waitForTimeout(1100);
const r = await seite.evaluate(() => ({
da: !document.getElementById("vorlagen")?.hidden,
gruppen: document.querySelectorAll(".fest-gruppe").length,
punkte: document.querySelectorAll(".fest-punkt").length,
bilanz: document.querySelectorAll(".bilanz-zahl").length,
stufenKnoepfe: document.querySelectorAll(".stufe-knopf").length,
marken: document.querySelectorAll(".fest-punkt__marke-stufe").length,
/* IST DER STIL AUCH ANGEKOMMEN? Beim ersten Anlauf lag das CSS in
content.css -- und bereich.html laedt die gar nicht. Die Punkte
standen nackt und ohne Flaeche da, und die Pruefung war trotzdem
gruen, weil sie nur ELEMENTE gezaehlt hat. Gemessen wird deshalb
der tatsaechliche Stil: eine Karte ohne Kante und ohne Polsterung
ist keine Karte. */
gestaltet: (() => {
const k = document.querySelector(".fest-punkt");
if (!k) return null;
const st = getComputedStyle(k);
return parseFloat(st.borderTopWidth) >= 1 && parseFloat(st.paddingLeft) >= 8;
})(),
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
}));
const gut = r.da && r.gruppen === 3 && r.punkte >= 6 && r.bilanz === 3 && r.gestaltet === true
&& r.marken === r.punkte && r.ueber <= 0
&& (darf ? r.stufenKnoepfe === r.punkte * 2 : r.stufenKnoepfe === 0);
if (!gut) fehler++;
console.log(` ${gut ? "ok " : "FEHL"} ${name} · ${bereich}: `
+ `${r.gruppen} Gruppen, ${r.punkte} Punkte, ${r.bilanz} Bilanzzahlen, `
+ `${r.stufenKnoepfe} Bewertungsknoepfe, ${r.marken} Marken, ${r.ueber}px über`
+ (r.gestaltet ? "" : " STIL FEHLT"));
}
ok(konsole.length === 0,
` ${name}: keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
if (rolle === "creator") {
/* Der Creator muss die Bewertung SEHEN -- das ist der ganze Punkt. */
await seite.goto(BASIS + "/workspace/bereich.html?b=live", { waitUntil: "networkidle" });
await seite.waitForTimeout(1000);
const s = await seite.evaluate(() => {
const p = document.querySelector('.fest-punkt[data-stufe="verbessern"]');
return p ? {
titel: p.querySelector(".fest-punkt__titel")?.textContent,
marke: p.querySelector(".fest-punkt__marke-stufe")?.textContent,
grund: p.querySelector(".fest-punkt__grund p")?.textContent,
} : null;
});
ok(!!s, "der Creator sieht den Punkt als 'zu verbessern' markiert");
ok(/Verbessern/i.test(s?.marke || ""), ` mit lesbarer Marke ("${s?.marke}")`);
ok(/Mikro/.test(s?.grund || ""), ` und dem Grund ("${s?.grund}")`);
await seite.screenshot({ path: "pruef-checkliste.png", fullPage: false });
}
await kontext.close();
}
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
process.exit(fehler ? 1 : 0);
+11 -66
View File
@@ -208,75 +208,20 @@ console.log("\n=== Hilfe anfordern");
ok(leer.status === 400, `ohne Thema wird abgelehnt (${leer.status})`);
}
/* ---------- Im Browser ---------------------------------------------------- */
console.log("\n=== Die Seite");
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: 1280, height: 1000 } });
const konsole = [];
const seite = await kontext.newPage();
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
/* ---------- Die Oberflaeche ----------------------------------------------
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: 15000 });
Der Browser-Teil stand hier und prueft den frueheren
"Vorschlaege"-Block. Den gibt es seit dem Umbau auf die feste
Checkliste nicht mehr: Die Punkte stehen jetzt fuer jeden gleich da,
statt einzeln geholt zu werden.
for (const [seiteName, art] of [["content.html", "Ideen"], ["bereich.html?b=live", "Punkte"]]) {
await seite.goto(BASIS + "/workspace/" + seiteName, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const r = await seite.evaluate(() => ({
da: !document.getElementById("vorlagen")?.hidden,
knopf: document.querySelector(".vorlagen__kopf .knopf")?.textContent?.trim(),
offen: document.querySelector(".vorlagen__raster")?.hidden === false,
anzahl: document.querySelectorAll(".vorlage").length,
}));
ok(r.da, `${seiteName}: der Vorlagenblock steht da`);
ok(/Vorschläge zeigen/.test(r.knopf || ""), ` mit Aufklappknopf ("${r.knopf}")`);
ok(r.offen === false, " und ist zunächst zu");
ok(r.anzahl > 0, ` ${r.anzahl} Vorschläge dahinter`);
Geprueft wird die Oberflaeche deshalb in pruef-checkliste.mjs. Die
Schnittstelle oben bleibt hier -- "Uebernehmen" gibt es weiterhin,
fuer eigene Ergaenzungen ueber die feste Liste hinaus.
/* Aufklappen muss wirklich etwas zeigen. */
await seite.click(".vorlagen__kopf .knopf");
await seite.waitForTimeout(350);
const auf = await seite.evaluate(() => ({
offen: document.querySelector(".vorlagen__raster")?.hidden === false,
sichtbar: [...document.querySelectorAll(".vorlage")].filter((v) => v.offsetParent !== null).length,
hooks: document.querySelectorAll(".vorlage__hook").length,
}));
ok(auf.offen && auf.sichtbar > 0, ` aufgeklappt sind ${auf.sichtbar} zu sehen`);
if (art === "Ideen") {
ok(auf.hooks === auf.sichtbar, ` und jede zeigt ihren Aufhänger (${auf.hooks})`);
}
}
/* Übernehmen über die Oberfläche -- nicht nur über die Schnittstelle. */
await seite.goto(BASIS + "/workspace/content.html", { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
await seite.click(".vorlagen__kopf .knopf");
await seite.waitForTimeout(300);
/* Die Karten heissen dort .video -- ein erster Anlauf suchte nach
".karte" und zaehlte deshalb immer null. */
const vorher = await seite.evaluate(() => document.querySelectorAll(".video").length);
await seite.click(".vorlage .schritt");
await seite.waitForTimeout(1200);
const nachher = await seite.evaluate(() => ({
karten: document.querySelectorAll(".video").length,
genommen: document.querySelectorAll('.vorlage[data-genommen="ja"]').length,
}));
ok(nachher.karten > vorher, `Übernehmen legt wirklich an (${vorher} → ${nachher.karten})`);
ok(nachher.genommen === 1, `und die Vorlage ist als übernommen markiert (${nachher.genommen})`);
ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
await seite.screenshot({ path: "pruef-vorlagen.png", fullPage: false });
await kontext.close();
}
await browser.close();
Eine Pruefung stehen zu lassen, die etwas Abgeschafftes verlangt,
waere schlimmer als keine: Sie ist dauerhaft rot, man gewoehnt sich
daran, und beim naechsten echten Fehler sieht niemand hin. */
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
process.exit(fehler ? 1 : 0);
+397
View File
@@ -0,0 +1,397 @@
/* =====================================================================
workspace-checkliste.js — feste Punkte, die immer dastehen.
Der Wunsch vom 01.09.2026: "diese neuen Sachen in der LIVE-Analyse
soll der Creator FEST sehen, auch schön kategorisiert, und der
Ansprechpartner soll dann anklicken können, wenn er findet, dass der
Creator was verbessern sollte -- so dass der Streamer es sieht und
Bescheid weiß."
WAS SICH DAMIT ÄNDERT.
Zuerst waren die Punkte Vorschläge zum Übernehmen: Man holte sie sich
und bekam einen eigenen Eintrag. Das war falsch gedacht. Eine
Checkliste, die man erst anfordern muss, ist keine Checkliste --
und schlimmer: Zwei Creator hätten unterschiedliche Listen, je
nachdem, wer sich was geholt hat. Genau das macht einen Vergleich
unmöglich, und um Vergleich geht es bei einer Betreuung.
Jetzt stehen dieselben Punkte für JEDEN fest da, gruppiert nach
Abschnitt. Was sich je Creator unterscheidet, ist nur der STAND --
und den setzt der Betreuer.
WARUM STABILE SCHLÜSSEL statt Positionen.
Ein Stand hängt am Schlüssel des Punktes ("ton-geprueft"), nicht an
seiner Nummer in der Liste. Würde er an der Position hängen, wäre beim
Einfügen eines Punktes in der Mitte plötzlich jede Bewertung dahinter
am falschen Punkt -- und niemand würde es merken, weil beides
plausibel aussieht.
DIE EIGENEN EINTRÄGE BLEIBEN.
Die Checkliste ersetzt nicht, was jemand selbst notiert. Sie steht
darüber: erst die feste Liste ("das gehört immer geprüft"), darunter
die eigenen Einträge ("das ist bei dir konkret passiert").
===================================================================== */
import express from "express";
import {
db, sitzungLesen, istLeitung, darfCreator, betreuteIds,
protokolliere, echteIp,
} from "./workspace.js";
import {
CONTENT_IDEEN, LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE,
} from "./workspace-vorlagen.js";
export const checklisteRouter = express.Router();
const jetzt = () => new Date().toISOString();
const STUFEN = new Set(["offen", "gut", "verbessern"]);
const GRUND_MAX = 2000;
/* ---------------------------------------------------------------------
Die Listen, gruppiert und beschriftet.
Die Gruppennamen stehen HIER und nicht in der Oberfläche: Sonst hiesse
derselbe Abschnitt auf zwei Seiten unterschiedlich, und niemand
wüsste, ob es dasselbe ist.
--------------------------------------------------------------------- */
export const LISTEN = {
live: {
name: "LIVE-Analyse",
gruppen: [
{ schluessel: "vorbereitung", name: "Vor der Sendung",
text: "Ein LIVE steht und fällt mit dem, was vorher passiert. "
+ "Diese Liste ist deshalb die längste.", punkte: LIVE_PUNKTE.vorbereitung },
{ schluessel: "mitschrift", name: "Während der Sendung",
text: "Was jetzt auffällt, ist morgen weg. In Sekunden anklicken, "
+ "nicht später aufschreiben.", punkte: LIVE_PUNKTE.mitschrift },
{ schluessel: "auswertung", name: "Nach der Sendung",
text: "Eine Sache für nächstes Mal. Wer sich fünf vornimmt, macht keine.",
punkte: LIVE_PUNKTE.auswertung },
],
},
community: {
name: "Community",
gruppen: [
{ schluessel: "moderation", name: "Moderation vorbereiten",
text: "Vor dem Ernstfall abhaken. Danach ist es zu spät.",
punkte: COMMUNITY_PUNKTE.moderation },
{ schluessel: "aktion", name: "Community aufbauen",
text: "Wiedererkannt zu werden ist der halbe Grund wiederzukommen.",
punkte: COMMUNITY_PUNKTE.aktion },
{ schluessel: "konflikt", name: "Wenn es kippt",
text: "Im Chat gewinnt niemand. Diese Punkte sind für den Moment danach.",
punkte: COMMUNITY_PUNKTE.konflikt },
],
},
technik: {
name: "Technik",
gruppen: [
{ schluessel: "setup", name: "Einrichtung",
text: "Ton zuerst – schlechter Ton ist der Grund Nummer eins, warum "
+ "Leute wieder gehen.", punkte: TECHNIK_PUNKTE.setup },
{ schluessel: "problem", name: "Wenn etwas ausfällt",
text: "Notieren, solange man es noch weiß. Am nächsten Tag fehlt die Hälfte.",
punkte: TECHNIK_PUNKTE.problem },
{ schluessel: "loesung", name: "Was geholfen hat",
text: "Genau die eine Änderung – nicht alles, was probiert wurde.",
punkte: TECHNIK_PUNKTE.loesung },
],
},
content: {
name: "Content-Ideen",
/* Content ist anders als die drei anderen: Es sind keine Punkte zum
Abhaken, sondern Ideen zum Umsetzen. Gruppiert wird deshalb nach
der SÄULE (70/20/10) und nicht nach einem Ablauf. Die Ampel
bedeutet hier: "die solltest du machen" bzw. "das hast du gut
umgesetzt". */
gruppen: [
{ schluessel: "wert", name: "Inhalte mit Wert",
text: "Rund 70 Prozent. Was jemand mitnimmt, auch wenn er dich nicht kennt.",
punkte: CONTENT_IDEEN.filter((v) => v.saeule === "Wert") },
{ schluessel: "community", name: "Für die Community",
text: "Rund 20 Prozent. Was nur die verstehen, die dabei sind.",
punkte: CONTENT_IDEEN.filter((v) => v.saeule === "Community") },
{ schluessel: "eigenwerbung", name: "In eigener Sache",
text: "Rund 10 Prozent. Mehr wirkt wie Werbung, weniger findet dich niemand.",
punkte: CONTENT_IDEEN.filter((v) => v.saeule === "Eigenwerbung") },
],
},
};
/* Alle gültigen Schlüssel je Bereich -- damit niemand einen Stand für
einen Punkt setzen kann, den es gar nicht gibt. */
const GUELTIG = {};
for (const [bereich, liste] of Object.entries(LISTEN)) {
GUELTIG[bereich] = new Set(liste.gruppen.flatMap((g) => g.punkte.map((p) => p.schluessel)));
}
let bereit = false;
function tabellen() {
if (bereit) return;
db().exec(`
/* Der Stand eines Punktes bei einem Creator.
Der Schluessel ist Text, keine Nummer: Er haengt am PUNKT, nicht
an seiner Position in der Liste. */
CREATE TABLE IF NOT EXISTS punkt_stand (
bereich TEXT NOT NULL,
schluessel TEXT NOT NULL,
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
stufe TEXT NOT NULL CHECK (stufe IN ('offen','gut','verbessern')),
begruendung TEXT,
von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
am TEXT NOT NULL,
PRIMARY KEY (bereich, schluessel, creator_id)
);
/* Rueckmeldungen zu einem Punkt -- der Kanal, ueber den ein Creator
antworten kann. */
CREATE TABLE IF NOT EXISTS punkt_nachricht (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bereich TEXT NOT NULL,
schluessel TEXT NOT NULL,
creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
text TEXT NOT NULL,
von INTEGER REFERENCES personen(id) ON DELETE SET NULL,
am TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_punkt_nachricht
ON punkt_nachricht (bereich, schluessel, creator_id);
`);
bereit = true;
}
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
try { tabellen(); } catch (f) {
console.error("[workspace] Checklisten-Tabellen:", f?.message);
return res.status(503).json({ fehler: "nicht_verfuegbar" });
}
next();
}
checklisteRouter.use("/workspace/api/checkliste", angemeldet);
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();
}
/** Um welchen Creator geht es -- und darf die Person das? */
function creatorAus(req) {
if (req.person.rolle === "creator") return req.person.id;
const id = Number(req.body?.creator_id || req.query?.creator_id) || 0;
if (!id) return 0;
return darfCreator(req.person, id) ? id : -1; /* -1 = verboten */
}
/* ---------- Die Liste ------------------------------------------------------ */
checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
try {
const bereich = String(req.params.bereich);
const liste = LISTEN[bereich];
if (!liste) return res.status(404).json({ fehler: "nicht_gefunden" });
let creatorId = creatorAus(req);
if (creatorId === -1) return res.status(404).json({ fehler: "nicht_gefunden" });
/* Wen darf die Person auswaehlen? */
let creatoren = [];
if (req.person.rolle !== "creator") {
const ids = istLeitung(req.person)
? db().prepare("SELECT id FROM personen WHERE rolle = 'creator' AND aktiv = 1")
.all().map((z) => z.id)
: betreuteIds(req.person);
creatoren = ids.length
? db().prepare(`SELECT id, name FROM personen
WHERE id IN (${ids.map(() => "?").join(",")}) ORDER BY name`).all(...ids)
: [];
/* Ohne ausdrueckliche Wahl den ERSTEN nehmen. Sonst sieht ein
Betreuer beim Oeffnen eine Liste ohne Bewertungsknoepfe und
ohne Bilanz -- und muss erst raten, dass er oben jemanden
auswaehlen soll. Genau das hat die Browserpruefung gemeldet
("0 Bewertungsknoepfe"). */
if (!creatorId && creatoren.length) creatorId = creatoren[0].id;
}
const stand = {};
const nachrichten = {};
if (creatorId > 0) {
for (const z of db().prepare(`
SELECT s.*, p.name AS von_name FROM punkt_stand s
LEFT JOIN personen p ON p.id = s.von
WHERE s.bereich = ? AND s.creator_id = ?`).all(bereich, creatorId)) {
stand[z.schluessel] = z;
}
for (const z of db().prepare(`
SELECT schluessel, COUNT(*) AS n FROM punkt_nachricht
WHERE bereich = ? AND creator_id = ? GROUP BY schluessel`).all(bereich, creatorId)) {
nachrichten[z.schluessel] = z.n;
}
}
res.json({
bereich, name: liste.name,
gruppen: liste.gruppen,
stand, nachrichten,
creatoren, creator_id: creatorId > 0 ? creatorId : null,
/* 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. */
darf_bewerten: req.person.rolle !== "creator",
ich: req.person.id,
});
} catch (fehler) {
console.error("[workspace] Checkliste lesen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Bewerten (NUR die Betreuung) ----------------------------------- */
checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHerkunft,
express.json({ limit: "8kb" }), (req, res) => {
try {
/* Ein Creator bewertet sich nicht selbst. Waere das moeglich,
stuende alles auf gruen und die Ampel waere wertlos. */
if (req.person.rolle === "creator") {
return res.status(403).json({
fehler: "Die Bewertung setzt deine Betreuung. Du kannst hier antworten.",
});
}
const bereich = String(req.params.bereich);
const schluessel = String(req.params.schluessel);
if (!GUELTIG[bereich]?.has(schluessel)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const creatorId = creatorAus(req);
if (creatorId <= 0) return res.status(404).json({ fehler: "nicht_gefunden" });
const stufe = String(req.body.stufe || "");
if (!STUFEN.has(stufe)) return res.status(400).json({ fehler: "Unbekannte Stufe." });
const grund = String(req.body.begruendung ?? "").trim().slice(0, GRUND_MAX);
/* "verbessern" ohne Grund ist fuer den Creator nicht umsetzbar --
nur entmutigend. */
if (stufe === "verbessern" && grund.length < 5) {
return res.status(400).json({
fehler: "Bitte kurz schreiben, was verbessert werden soll – sonst kann er nichts damit anfangen.",
});
}
/* "offen" heisst: zurueck auf Anfang. Dann wird der Stand
geloescht statt auf "offen" gesetzt -- ein Datensatz, der nichts
aussagt, ist Ballast. */
if (stufe === "offen") {
db().prepare("DELETE FROM punkt_stand WHERE bereich = ? AND schluessel = ? AND creator_id = ?")
.run(bereich, schluessel, creatorId);
} else {
db().prepare(`INSERT INTO punkt_stand
(bereich, schluessel, creator_id, stufe, begruendung, von, am)
VALUES (?,?,?,?,?,?,?)
ON CONFLICT(bereich, schluessel, creator_id) DO UPDATE SET
stufe = excluded.stufe, begruendung = excluded.begruendung,
von = excluded.von, am = excluded.am`)
.run(bereich, schluessel, creatorId, stufe, grund || null, req.person.id, jetzt());
}
protokolliere("punkt_bewertet", req.person, echteIp(req),
`${bereich}/${schluessel}: ${stufe}`);
const neu = db().prepare(`
SELECT s.*, p.name AS von_name FROM punkt_stand s
LEFT JOIN personen p ON p.id = s.von
WHERE s.bereich = ? AND s.schluessel = ? AND s.creator_id = ?`)
.get(bereich, schluessel, creatorId) || null;
res.json({ stand: neu });
} catch (fehler) {
console.error("[workspace] Punkt bewerten:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Nachrichten zu einem Punkt ------------------------------------- */
checklisteRouter.get("/workspace/api/checkliste/:bereich/:schluessel/nachrichten", (req, res) => {
try {
const bereich = String(req.params.bereich);
const schluessel = String(req.params.schluessel);
if (!GUELTIG[bereich]?.has(schluessel)) return res.status(404).json({ fehler: "nicht_gefunden" });
const creatorId = creatorAus(req);
if (creatorId <= 0) return res.status(404).json({ fehler: "nicht_gefunden" });
const nachrichten = db().prepare(`
SELECT n.*, p.name AS von_name, p.rolle AS von_rolle
FROM punkt_nachricht n LEFT JOIN personen p ON p.id = n.von
WHERE n.bereich = ? AND n.schluessel = ? AND n.creator_id = ?
ORDER BY n.id`).all(bereich, schluessel, creatorId);
res.json({ nachrichten, ich: req.person.id });
} catch (fehler) {
console.error("[workspace] Punktnachrichten:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
checklisteRouter.post("/workspace/api/checkliste/:bereich/:schluessel/nachrichten", gleicheHerkunft,
express.json({ limit: "8kb" }), (req, res) => {
try {
const bereich = String(req.params.bereich);
const schluessel = String(req.params.schluessel);
if (!GUELTIG[bereich]?.has(schluessel)) return res.status(404).json({ fehler: "nicht_gefunden" });
const creatorId = creatorAus(req);
if (creatorId <= 0) return res.status(404).json({ fehler: "nicht_gefunden" });
const text = String(req.body.text ?? "").trim();
if (text.length < 2) return res.status(400).json({ fehler: "Die Nachricht ist leer." });
if (text.length > GRUND_MAX) return res.status(400).json({ fehler: "Die Nachricht ist zu lang." });
db().prepare(`INSERT INTO punkt_nachricht
(bereich, schluessel, creator_id, text, von, am) VALUES (?,?,?,?,?,?)`)
.run(bereich, schluessel, creatorId, text, req.person.id, jetzt());
protokolliere("punkt_nachricht", req.person, echteIp(req), `${bereich}/${schluessel}`);
const alle = db().prepare(`
SELECT n.*, p.name AS von_name, p.rolle AS von_rolle
FROM punkt_nachricht n LEFT JOIN personen p ON p.id = n.von
WHERE n.bereich = ? AND n.schluessel = ? AND n.creator_id = ?
ORDER BY n.id`).all(bereich, schluessel, creatorId);
res.json({ nachrichten: alle });
} catch (fehler) {
console.error("[workspace] Punktnachricht:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* Nur die EIGENE zurücknehmen -- fremde zu löschen hiesse, ein Gespräch
nachträglich umzuschreiben. Auch für die Leitung nicht. */
checklisteRouter.delete("/workspace/api/checkliste/nachricht/:id(\\d+)", gleicheHerkunft,
(req, res) => {
try {
const n = db().prepare("SELECT * FROM punkt_nachricht WHERE id = ?").get(Number(req.params.id));
if (!n) return res.status(404).json({ fehler: "nicht_gefunden" });
/* Erst pruefen, ob die Person den Creator ueberhaupt sehen darf --
sonst verraet die Antwort, dass es die Nachricht gibt. */
const erlaubt = req.person.rolle === "creator"
? n.creator_id === req.person.id
: darfCreator(req.person, n.creator_id);
if (!erlaubt) return res.status(404).json({ fehler: "nicht_gefunden" });
if (n.von !== req.person.id) {
return res.status(403).json({
fehler: "Fremde Nachrichten bleiben stehen. Schreib eine neue, wenn etwas richtigzustellen ist.",
});
}
db().prepare("DELETE FROM punkt_nachricht WHERE id = ?").run(n.id);
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Punktnachricht löschen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
+64 -64
View File
@@ -59,65 +59,65 @@ const BEREICH_PUNKTE = {};
export const CONTENT_IDEEN = [
/* ---- Wert (70 %) ---------------------------------------------------- */
{ titel: "Das hätte ich am Anfang gern gewusst",
{ schluessel: "das-haette-ich-am-anfang-gern-gewusst", titel: "Das hätte ich am Anfang gern gewusst",
hook: "„Ich hab drei Monate gebraucht, um das zu kapieren – du brauchst 30 Sekunden.\"",
format: "Talking Head", saeule: "Wert",
text: "Ein Fehler, den du selbst gemacht hast, und was du heute anders machst. "
+ "Konkret bleiben: ein Fehler, eine Lösung. Wer drei Dinge sagt, sagt nichts." },
{ titel: "Mein Setup für unter 100 €",
{ schluessel: "mein-setup-fuer-unter-100", titel: "Mein Setup für unter 100 €",
hook: "„Alle sagen, du brauchst teure Technik. Hier ist meins für 87 Euro.\"",
format: "Zeigen & Erklären", saeule: "Wert",
text: "Was du wirklich benutzt, mit Preisen. Ehrlich sein, wo es hakt – "
+ "das ist der Teil, den niemand sonst zeigt." },
{ titel: "Drei Einstellungen, die sofort was bringen",
{ schluessel: "drei-einstellungen-die-sofort-was-bringe", titel: "Drei Einstellungen, die sofort was bringen",
hook: "„Wenn dein Stream unscharf aussieht, liegt es an einer davon.\"",
format: "Bildschirmaufnahme", saeule: "Wert",
text: "Nicht mehr als drei. Jede in unter 15 Sekunden erklärt, mit Vorher/Nachher." },
{ titel: "Was ich in einer Woche gelernt habe",
{ schluessel: "was-ich-in-einer-woche-gelernt-habe", titel: "Was ich in einer Woche gelernt habe",
hook: "„Sieben Tage, eine Erkenntnis – die hier hat alles verändert.\"",
format: "Talking Head", saeule: "Wert",
text: "Rückblick auf eine echte Woche. Funktioniert nur mit einer konkreten Zahl "
+ "oder einem konkreten Moment." },
{ titel: "Antwort auf die Frage, die immer kommt",
{ schluessel: "antwort-auf-die-frage-die-immer-kommt", titel: "Antwort auf die Frage, die immer kommt",
hook: "„Das werde ich jeden Tag gefragt – hier einmal in Ruhe.\"",
format: "Talking Head", saeule: "Wert",
text: "Aus dem Chat sammeln, welche Frage sich wiederholt. Die Antwort einmal "
+ "gut aufnehmen und immer wieder verwenden." },
{ titel: "So bereite ich einen Stream vor",
{ schluessel: "so-bereite-ich-einen-stream-vor", titel: "So bereite ich einen Stream vor",
hook: "„Zwei Stunden vor LIVE mache ich immer dasselbe.\"",
format: "Vlog", saeule: "Wert",
text: "Der Ablauf hinter der Sendung. Zeigt Verlässlichkeit und gibt Anfängern "
+ "eine Vorlage." },
{ titel: "Fehler, die ich bei anderen sehe",
{ schluessel: "fehler-die-ich-bei-anderen-sehe", titel: "Fehler, die ich bei anderen sehe",
hook: "„Bitte mach das nicht – ich sehe es jeden Tag.\"",
format: "Talking Head", saeule: "Wert",
text: "Niemanden bloßstellen. Das Verhalten beschreiben, nicht die Person." },
/* ---- Community (20 %) ------------------------------------------------ */
{ titel: "Der beste Moment aus dem letzten LIVE",
{ schluessel: "der-beste-moment-aus-dem-letzten-live", titel: "Der beste Moment aus dem letzten LIVE",
hook: "„Das ist gestern passiert, und ich rede immer noch davon.\"",
format: "Zusammenschnitt", saeule: "Community",
text: "Ein Ausschnitt, keine Zusammenfassung. Wer alles zeigt, zeigt nichts." },
{ titel: "Ich beantworte eure Kommentare",
{ schluessel: "ich-beantworte-eure-kommentare", titel: "Ich beantworte eure Kommentare",
hook: "„Ihr habt gefragt – jetzt rede ich.\"",
format: "Talking Head", saeule: "Community",
text: "Drei bis fünf echte Kommentare, sichtbar eingeblendet. Auch die kritischen." },
{ titel: "Danke an die, die immer da sind",
{ schluessel: "danke-an-die-die-immer-da-sind", titel: "Danke an die, die immer da sind",
hook: "„Ihr seid seit dem ersten Stream dabei. Das hier ist für euch.\"",
format: "Talking Head", saeule: "Community",
text: "Namen nennen, wenn die Leute einverstanden sind. Sonst allgemein bleiben – "
+ "aber ehrlich." },
{ titel: "Eine Entscheidung, bei der ihr mitredet",
{ schluessel: "eine-entscheidung-bei-der-ihr-mitredet", titel: "Eine Entscheidung, bei der ihr mitredet",
hook: "„Ich kann mich nicht entscheiden. Ihr habt 24 Stunden.\"",
format: "Umfrage", saeule: "Community",
text: "Echte Wahl, keine Scheinfrage. Das Ergebnis danach auch wirklich umsetzen." },
/* ---- Eigenwerbung (10 %) --------------------------------------------- */
{ titel: "Wann und wo ich streame",
{ schluessel: "wann-und-wo-ich-streame", titel: "Wann und wo ich streame",
hook: "„Immer dienstags und donnerstags – und ich fange pünktlich an.\"",
format: "Kurzclip", saeule: "Eigenwerbung",
text: "Fester Sendeplan als eigener Clip. Wiederholbar, sobald sich etwas ändert." },
{ titel: "Was diese Woche ansteht",
{ schluessel: "was-diese-woche-ansteht", titel: "Was diese Woche ansteht",
hook: "„Drei Sachen, und eine davon habe ich noch nie gemacht.\"",
format: "Kurzclip", saeule: "Eigenwerbung",
text: "Vorschau auf die Woche. Kurz halten, sonst wird es eine Ankündigung "
@@ -140,31 +140,31 @@ export const CONTENT_IDEEN = [
export const LIVE_PUNKTE = {
vorbereitung: [
{ titel: "Ton geprüft", text: "Eine Minute aufnehmen und anhören. Ton ist der Grund Nummer eins, warum Leute wieder gehen.", hilfe: true },
{ titel: "Licht sitzt", text: "Gesicht heller als der Hintergrund. Kein Fenster im Rücken.", hilfe: true },
{ titel: "Verbindung getestet", text: "Upload-Geschwindigkeit geprüft, Kabel statt WLAN wenn möglich.", hilfe: true },
{ titel: "Titel und Thema stehen", text: "Wer den Titel liest, weiß, was ihn erwartet – und ob es für ihn ist.", hilfe: false },
{ titel: "Ablauf grob im Kopf", text: "Drei Blöcke reichen. Ein Stream ohne roten Faden fühlt sich für Zuschauer wie Warten an.", hilfe: false },
{ titel: "Angekündigt", text: "Mindestens einmal vorher gesagt, dass es losgeht – Story, Post oder Nachricht.", hilfe: false },
{ titel: "Moderation geklärt", text: "Weiß jemand Bescheid, der eingreifen kann, wenn es im Chat kippt?", hilfe: true },
{ titel: "Technik-Ersatz bereit", text: "Was passiert, wenn der Stream abbricht? Handy als Notlösung bereitlegen.", hilfe: true },
{ schluessel: "ton-geprueft", titel: "Ton geprüft", text: "Eine Minute aufnehmen und anhören. Ton ist der Grund Nummer eins, warum Leute wieder gehen.", hilfe: true },
{ schluessel: "licht-sitzt", titel: "Licht sitzt", text: "Gesicht heller als der Hintergrund. Kein Fenster im Rücken.", hilfe: true },
{ schluessel: "verbindung-getestet", titel: "Verbindung getestet", text: "Upload-Geschwindigkeit geprüft, Kabel statt WLAN wenn möglich.", hilfe: true },
{ schluessel: "titel-und-thema-stehen", titel: "Titel und Thema stehen", text: "Wer den Titel liest, weiß, was ihn erwartet – und ob es für ihn ist.", hilfe: false },
{ schluessel: "ablauf-grob-im-kopf", titel: "Ablauf grob im Kopf", text: "Drei Blöcke reichen. Ein Stream ohne roten Faden fühlt sich für Zuschauer wie Warten an.", hilfe: false },
{ schluessel: "angekuendigt", titel: "Angekündigt", text: "Mindestens einmal vorher gesagt, dass es losgeht – Story, Post oder Nachricht.", hilfe: false },
{ schluessel: "moderation-geklaert", titel: "Moderation geklärt", text: "Weiß jemand Bescheid, der eingreifen kann, wenn es im Chat kippt?", hilfe: true },
{ schluessel: "technik-ersatz-bereit", titel: "Technik-Ersatz bereit", text: "Was passiert, wenn der Stream abbricht? Handy als Notlösung bereitlegen.", hilfe: true },
],
mitschrift: [
{ titel: "Guter Moment – aufheben", text: "Etwas ist gut gelaufen. Kurz notieren, woran es lag – daraus wird später ein Clip.", hilfe: false },
{ titel: "Hier sind Leute gegangen", text: "Zeitpunkt merken. Später nachsehen, was in dem Moment passiert ist.", hilfe: false },
{ titel: "Frage aus dem Chat, die wiederkommt", text: "Wird oft gefragt? Dann gehört sie in ein eigenes Video.", hilfe: false },
{ titel: "Technik hat gehakt", text: "Ton weg, Bild eingefroren, Verzögerung – aufschreiben, solange man es noch weiß.", hilfe: true },
{ titel: "Unangenehme Situation im Chat", text: "Was war es, wie ist es ausgegangen? Auch wenn es gut lief.", hilfe: true },
{ titel: "Neue Zuschauer aufgetaucht", text: "Woher kamen sie? Wurde jemand begrüßt oder ging es unter?", hilfe: false },
{ titel: "Ich wusste nicht weiter", text: "Stelle merken, an der es gestockt hat. Dafür gibt es Hilfe.", hilfe: true },
{ schluessel: "guter-moment-aufheben", titel: "Guter Moment – aufheben", text: "Etwas ist gut gelaufen. Kurz notieren, woran es lag – daraus wird später ein Clip.", hilfe: false },
{ schluessel: "hier-sind-leute-gegangen", titel: "Hier sind Leute gegangen", text: "Zeitpunkt merken. Später nachsehen, was in dem Moment passiert ist.", hilfe: false },
{ schluessel: "frage-aus-dem-chat-die-wiederkommt", titel: "Frage aus dem Chat, die wiederkommt", text: "Wird oft gefragt? Dann gehört sie in ein eigenes Video.", hilfe: false },
{ schluessel: "technik-hat-gehakt", titel: "Technik hat gehakt", text: "Ton weg, Bild eingefroren, Verzögerung – aufschreiben, solange man es noch weiß.", hilfe: true },
{ schluessel: "unangenehme-situation-im-chat", titel: "Unangenehme Situation im Chat", text: "Was war es, wie ist es ausgegangen? Auch wenn es gut lief.", hilfe: true },
{ schluessel: "neue-zuschauer-aufgetaucht", titel: "Neue Zuschauer aufgetaucht", text: "Woher kamen sie? Wurde jemand begrüßt oder ging es unter?", hilfe: false },
{ schluessel: "ich-wusste-nicht-weiter", titel: "Ich wusste nicht weiter", text: "Stelle merken, an der es gestockt hat. Dafür gibt es Hilfe.", hilfe: true },
],
auswertung: [
{ titel: "Was hat diesmal getragen?", text: "Ein Ding. Nicht drei. Das eine, das man wiederholen will.", hilfe: false },
{ titel: "Was war zäh?", text: "Ehrlich sein. Die zähen Stellen sind die, an denen man wächst.", hilfe: false },
{ titel: "Zahlen im Vergleich", text: "Zuschauer, Verweildauer, Kommentare – gegen das letzte Mal, nicht gegen andere.", hilfe: true },
{ titel: "Clips gesichert", text: "Die guten Momente rausgeschnitten und abgelegt, solange man sie noch findet.", hilfe: false },
{ titel: "Eine Sache für nächstes Mal", text: "Genau eine. Wer sich fünf Dinge vornimmt, macht keins davon.", hilfe: false },
{ titel: "Braucht Besprechung", text: "Etwas, das man nicht allein lösen kann – geht an die Betreuung.", hilfe: true },
{ schluessel: "was-hat-diesmal-getragen", titel: "Was hat diesmal getragen?", text: "Ein Ding. Nicht drei. Das eine, das man wiederholen will.", hilfe: false },
{ schluessel: "was-war-zaeh", titel: "Was war zäh?", text: "Ehrlich sein. Die zähen Stellen sind die, an denen man wächst.", hilfe: false },
{ schluessel: "zahlen-im-vergleich", titel: "Zahlen im Vergleich", text: "Zuschauer, Verweildauer, Kommentare – gegen das letzte Mal, nicht gegen andere.", hilfe: true },
{ schluessel: "clips-gesichert", titel: "Clips gesichert", text: "Die guten Momente rausgeschnitten und abgelegt, solange man sie noch findet.", hilfe: false },
{ schluessel: "eine-sache-fuer-naechstes-mal", titel: "Eine Sache für nächstes Mal", text: "Genau eine. Wer sich fünf Dinge vornimmt, macht keins davon.", hilfe: false },
{ schluessel: "braucht-besprechung", titel: "Braucht Besprechung", text: "Etwas, das man nicht allein lösen kann – geht an die Betreuung.", hilfe: true },
],
};
@@ -186,24 +186,24 @@ export const LIVE_PUNKTE = {
export const COMMUNITY_PUNKTE = {
moderation: [
{ titel: "Regeln stehen sichtbar", text: "Kurz, verstaendlich, an einer Stelle, die man findet. Drei Saetze reichen.", hilfe: false },
{ titel: "Jemand kann eingreifen", text: "Waehrend des Streams ist eine zweite Person mit Rechten da -- oder es ist klar, dass keine da ist.", hilfe: true },
{ titel: "Wortfilter eingerichtet", text: "Die offensichtlichen Sachen automatisch weg, damit man sich um den Rest kuemmern kann.", hilfe: true },
{ titel: "Was passiert bei einem Verstoss?", text: "Verwarnen, stummschalten, sperren -- vorher entscheiden, nicht im Moment.", hilfe: false },
{ titel: "Umgang mit Minderjaehrigen", text: "Wie erkennt man sie, und was gilt dann? Das ist keine Ermessensfrage.", hilfe: true },
{ schluessel: "regeln-stehen-sichtbar", titel: "Regeln stehen sichtbar", text: "Kurz, verstaendlich, an einer Stelle, die man findet. Drei Saetze reichen.", hilfe: false },
{ schluessel: "jemand-kann-eingreifen", titel: "Jemand kann eingreifen", text: "Waehrend des Streams ist eine zweite Person mit Rechten da -- oder es ist klar, dass keine da ist.", hilfe: true },
{ schluessel: "wortfilter-eingerichtet", titel: "Wortfilter eingerichtet", text: "Die offensichtlichen Sachen automatisch weg, damit man sich um den Rest kuemmern kann.", hilfe: true },
{ schluessel: "was-passiert-bei-einem-verstoss", titel: "Was passiert bei einem Verstoss?", text: "Verwarnen, stummschalten, sperren -- vorher entscheiden, nicht im Moment.", hilfe: false },
{ schluessel: "umgang-mit-minderjaehrigen", titel: "Umgang mit Minderjaehrigen", text: "Wie erkennt man sie, und was gilt dann? Das ist keine Ermessensfrage.", hilfe: true },
],
aktion: [
{ titel: "Neue begruessen", text: "Wer zum ersten Mal da ist, wird angesprochen. Kostet zwei Sekunden und entscheidet, ob jemand bleibt.", hilfe: false },
{ titel: "Stammleute erkennen", text: "Namen merken oder notieren. Wiedererkannt zu werden ist der halbe Grund wiederzukommen.", hilfe: false },
{ titel: "Etwas geben, das nur hier gibt", text: "Ein Insider, ein Ritual, ein Spitzname. Community entsteht aus Gemeinsamem.", hilfe: false },
{ titel: "Ausserhalb des Streams antworten", text: "Kommentare und Nachrichten -- die Community lebt auch zwischen den Sendungen.", hilfe: false },
{ schluessel: "neue-begruessen", titel: "Neue begruessen", text: "Wer zum ersten Mal da ist, wird angesprochen. Kostet zwei Sekunden und entscheidet, ob jemand bleibt.", hilfe: false },
{ schluessel: "stammleute-erkennen", titel: "Stammleute erkennen", text: "Namen merken oder notieren. Wiedererkannt zu werden ist der halbe Grund wiederzukommen.", hilfe: false },
{ 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.", hilfe: false },
{ schluessel: "ausserhalb-des-streams-antworten", titel: "Ausserhalb des Streams antworten", text: "Kommentare und Nachrichten -- die Community lebt auch zwischen den Sendungen.", hilfe: false },
],
konflikt: [
{ titel: "Ruhig bleiben und nicht oeffentlich streiten", text: "Im Chat gewinnt niemand. Kurz ansagen, dann privat klaeren.", hilfe: true },
{ titel: "Was ist genau passiert?", text: "Zeitpunkt, Beteiligte, Wortlaut. Ohne das ist jede Entscheidung angreifbar.", hilfe: false },
{ titel: "Beweise sichern", text: "Bildschirmfoto oder Ausschnitt, solange es noch da ist. Chats verschwinden.", hilfe: true },
{ titel: "Betreuung einbeziehen", text: "Bei allem, was groesser ist als ein blöder Kommentar. Dafuer ist sie da.", hilfe: true },
{ titel: "Danach: was hat gefehlt?", text: "Regel unklar, niemand zum Eingreifen da, zu spaet gemerkt -- daraus wird der naechste Punkt oben.", hilfe: false },
{ schluessel: "ruhig-bleiben-und-nicht-oeffentlich-stre", titel: "Ruhig bleiben und nicht oeffentlich streiten", text: "Im Chat gewinnt niemand. Kurz ansagen, dann privat klaeren.", hilfe: true },
{ schluessel: "was-ist-genau-passiert", titel: "Was ist genau passiert?", text: "Zeitpunkt, Beteiligte, Wortlaut. Ohne das ist jede Entscheidung angreifbar.", hilfe: false },
{ schluessel: "beweise-sichern", titel: "Beweise sichern", text: "Bildschirmfoto oder Ausschnitt, solange es noch da ist. Chats verschwinden.", hilfe: true },
{ schluessel: "betreuung-einbeziehen", titel: "Betreuung einbeziehen", text: "Bei allem, was groesser ist als ein blöder Kommentar. Dafuer ist sie da.", hilfe: true },
{ 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.", hilfe: false },
],
};
@@ -218,38 +218,38 @@ export const COMMUNITY_PUNKTE = {
export const TECHNIK_PUNKTE = {
setup: [
{ titel: "Mikrofon richtig eingestellt", text: "Nicht uebersteuert, nicht zu leise, kein Rauschen. Einmal aufnehmen und anhoeren.", hilfe: true },
{ titel: "Kamera scharf und gut belichtet", text: "Gesicht heller als der Hintergrund. Automatik oft abschalten -- sie pumpt.", hilfe: true },
{ titel: "Szenen im Streaming-Programm angelegt", text: "Start, Vollbild, Pause, Ende. Wer waehrend der Sendung baut, sendet Chaos.", hilfe: true },
{ titel: "Bitrate zur Leitung passend", text: "Lieber stabil und etwas weicher als scharf mit Aussetzern.", hilfe: true },
{ titel: "Alles einmal komplett getestet", text: "Eine Probesendung von fuenf Minuten, aufgezeichnet und angesehen.", hilfe: true },
{ schluessel: "mikrofon-richtig-eingestellt", titel: "Mikrofon richtig eingestellt", text: "Nicht uebersteuert, nicht zu leise, kein Rauschen. Einmal aufnehmen und anhoeren.", hilfe: true },
{ schluessel: "kamera-scharf-und-gut-belichtet", titel: "Kamera scharf und gut belichtet", text: "Gesicht heller als der Hintergrund. Automatik oft abschalten -- sie pumpt.", hilfe: true },
{ schluessel: "szenen-im-streaming-programm-angelegt", titel: "Szenen im Streaming-Programm angelegt", text: "Start, Vollbild, Pause, Ende. Wer waehrend der Sendung baut, sendet Chaos.", hilfe: true },
{ schluessel: "bitrate-zur-leitung-passend", titel: "Bitrate zur Leitung passend", text: "Lieber stabil und etwas weicher als scharf mit Aussetzern.", hilfe: true },
{ schluessel: "alles-einmal-komplett-getestet", titel: "Alles einmal komplett getestet", text: "Eine Probesendung von fuenf Minuten, aufgezeichnet und angesehen.", hilfe: true },
],
problem: [
{ titel: "Ton weg oder verzerrt", text: "Wann genau, welches Geraet, was lief gleichzeitig? Ohne das findet es niemand.", hilfe: true },
{ titel: "Bild ruckelt oder friert ein", text: "Auslastung, Leitung, Aufloesung -- eins nach dem anderen ausschliessen.", hilfe: true },
{ titel: "Stream bricht ab", text: "Zeitpunkt notieren und im Protokoll des Programms nachsehen.", hilfe: true },
{ titel: "Ton und Bild laufen auseinander", text: "Meist ein Puffer. Wann faellt es auf, sofort oder erst spaeter?", hilfe: true },
{ schluessel: "ton-weg-oder-verzerrt", titel: "Ton weg oder verzerrt", text: "Wann genau, welches Geraet, was lief gleichzeitig? Ohne das findet es niemand.", hilfe: true },
{ schluessel: "bild-ruckelt-oder-friert-ein", titel: "Bild ruckelt oder friert ein", text: "Auslastung, Leitung, Aufloesung -- eins nach dem anderen ausschliessen.", hilfe: true },
{ schluessel: "stream-bricht-ab", titel: "Stream bricht ab", text: "Zeitpunkt notieren und im Protokoll des Programms nachsehen.", hilfe: true },
{ schluessel: "ton-und-bild-laufen-auseinander", titel: "Ton und Bild laufen auseinander", text: "Meist ein Puffer. Wann faellt es auf, sofort oder erst spaeter?", hilfe: true },
],
loesung: [
{ titel: "Was hat es behoben?", text: "Genau die eine Aenderung aufschreiben, nicht alles was probiert wurde.", hilfe: false },
{ titel: "Kommt es wieder?", text: "Beim naechsten Mal nachsehen. Einmal weg ist nicht geloest.", hilfe: false },
{ titel: "Einstellung gesichert", text: "Damit sie nach einem Update nicht wieder weg ist.", hilfe: false },
{ schluessel: "was-hat-es-behoben", titel: "Was hat es behoben?", text: "Genau die eine Aenderung aufschreiben, nicht alles was probiert wurde.", hilfe: false },
{ schluessel: "kommt-es-wieder", titel: "Kommt es wieder?", text: "Beim naechsten Mal nachsehen. Einmal weg ist nicht geloest.", hilfe: false },
{ schluessel: "einstellung-gesichert", titel: "Einstellung gesichert", text: "Damit sie nach einem Update nicht wieder weg ist.", hilfe: false },
],
};
export const CALL_LEITFAEDEN = [
{ titel: "Erstgespräch",
{ schluessel: "erstgespraech", titel: "Erstgespräch",
text: "Wer bist du, was machst du, was willst du erreichen? · Was läuft schon gut? · "
+ "Was nervt dich gerade am meisten? · Wie viel Zeit hast du wirklich? · "
+ "Was erwartest du von uns – und was erwarten wir von dir?" },
{ titel: "Wochen-Call",
{ schluessel: "wochen-call", titel: "Wochen-Call",
text: "Was ist seit dem letzten Mal passiert? · Was ist liegen geblieben und warum? · "
+ "Zahlen kurz ansehen – eine Auffälligkeit reicht · Was steht diese Woche an? · "
+ "Brauchst du irgendwo Hilfe?" },
{ titel: "Wenn es gerade nicht läuft",
{ schluessel: "wenn-es-gerade-nicht-laeuft", titel: "Wenn es gerade nicht läuft",
text: "Zuerst zuhören, nicht lösen · Seit wann geht es so? · Was hat sich geändert? · "
+ "Was war das letzte Mal, als es gut war? · Eine kleine Sache vereinbaren, "
+ "nicht fünf große" },
{ titel: "Monats-Review",
{ schluessel: "monats-review", titel: "Monats-Review",
text: "Die drei Ziele vom letzten Mal durchgehen · Was ist erreicht, was nicht? · "
+ "Woran lag es – Zeit, Können oder Motivation? · Drei neue Ziele, verbindlich · "
+ "Termin für das nächste Review festhalten" },
+203
View File
@@ -1818,3 +1818,206 @@ body.start #protokoll {
text-transform: none; letter-spacing: normal;
font-weight: 400; color: var(--text-still);
}
/* Die Checkliste steht auf VIER Seiten (LIVE, Community, Technik,
Content-Ideen). Ihr Stil gehoert deshalb hierher und nicht in eine
Seitendatei -- genau diesen Fehler habe ich beim ersten Anlauf
gemacht: Der Block lag in content.css, und bereich.html laedt die
gar nicht. Die Punkte standen dort nackt und ohne Flaeche da.
Es ist derselbe Fehler wie im August bei .knopf-still, .schalter und
.kopf-zeile. Deshalb steht die Warnung weiter oben in dieser Datei --
und deshalb hat sie nichts genutzt, solange keine Pruefung sie
nachhaelt. Jetzt gibt es eine (pruef-checkliste.mjs misst, ob die
Punkte wirklich eine Kante haben). */
/* =====================================================================
DIE FESTE CHECKLISTE
Steht auf jeder der vier Seiten (LIVE, Community, Technik,
Content-Ideen) und sieht ueberall gleich aus -- es ist dieselbe Sache.
Drei Zustaende, drei Farben. "Verbessern" ist bewusst warnend und
nicht rot: Es ist eine Aufgabe, kein Fehler. Rot waere eine
Beschuldigung.
===================================================================== */
.liste-fest { margin: 20px 0 30px; }
.liste-fest__kopf {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 16px; flex-wrap: wrap; margin-bottom: 14px;
}
.liste-fest__unter {
margin: 4px 0 0; max-width: 70ch;
font-size: .87rem; line-height: 1.55; color: var(--text-leise);
}
.liste-fest__wahl { display: grid; gap: 4px; min-width: 0; }
/* ---------- Die Bilanz oben ---------------------------------------------
Wie viele Punkte passen, wie viele sind zu verbessern, wie viele hat
noch niemand angesehen. Das ist die Frage, die beide Seiten zuerst
haben -- deshalb steht sie vor der Liste und nicht darunter. */
.liste-fest__bilanz {
display: grid; gap: 9px;
grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
margin-bottom: 20px;
}
.bilanz-zahl {
--b-ton: #6f89a8;
display: flex; align-items: baseline; gap: 9px;
padding: 12px 15px;
border: 1px solid color-mix(in srgb, var(--b-ton) 26%, var(--rand));
border-radius: var(--radius-klein);
background:
linear-gradient(180deg, color-mix(in srgb, var(--b-ton) 9%, transparent), transparent 60%),
var(--flaeche);
}
.bilanz-zahl[data-stufe="gut"] { --b-ton: #4dab77; }
.bilanz-zahl[data-stufe="verbessern"] { --b-ton: #e0a33c; }
.bilanz-zahl[data-stufe="offen"] { --b-ton: #6f89a8; }
.bilanz-zahl__n {
font-size: 1.6rem; font-weight: 700; line-height: 1;
color: var(--b-ton); font-variant-numeric: tabular-nums;
}
.bilanz-zahl__text { font-size: .82rem; color: var(--text-leise); }
/* ---------- Die Gruppen -------------------------------------------------- */
.fest-gruppe { margin-bottom: 24px; }
.fest-gruppe__kopf { display: flex; align-items: baseline; gap: 10px; }
.fest-gruppe__name {
margin: 0; font-size: .96rem; font-weight: 650; color: var(--text);
}
.fest-gruppe__zahl {
padding: 1px 8px;
border: 1px solid var(--rand); border-radius: 999px;
font-size: .72rem; color: var(--text-still);
font-variant-numeric: tabular-nums;
}
.fest-gruppe__text {
margin: 4px 0 12px; max-width: 68ch;
font-size: .84rem; line-height: 1.5; color: var(--text-still);
}
.fest-punkte { display: grid; gap: 9px; }
/* ---------- Ein Punkt ---------------------------------------------------- */
.fest-punkt {
--p-ton: #6f89a8;
position: relative; overflow: hidden;
padding: 13px 15px 12px;
border: 1px solid color-mix(in srgb, var(--p-ton) 22%, var(--rand));
border-radius: var(--radius-klein);
background:
linear-gradient(90deg, color-mix(in srgb, var(--p-ton) 7%, transparent), transparent 52%),
var(--flaeche);
}
.fest-punkt::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
background: linear-gradient(180deg, var(--p-ton),
color-mix(in srgb, var(--p-ton) 30%, transparent));
}
.fest-punkt[data-stufe="gut"] { --p-ton: #4dab77; }
/* Warnend, nicht rot: Es ist eine Aufgabe, kein Fehler. Rot waere eine
Beschuldigung. */
.fest-punkt[data-stufe="verbessern"] { --p-ton: #e0a33c; }
.fest-punkt[data-stufe="offen"] { --p-ton: #6f89a8; }
.fest-punkt__kopf {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 13px;
}
.fest-punkt__text { min-width: 0; }
.fest-punkt__titel {
margin: 0 0 5px; font-size: .93rem; font-weight: 650; color: var(--text);
}
.fest-punkt__beschreibung {
margin: 0; font-size: .84rem; line-height: 1.5; color: var(--text-leise);
}
/* Der Aufhaenger bei Content-Ideen -- die ersten Sekunden entscheiden,
deshalb steht er hervorgehoben und nicht im Fliesstext. */
.fest-punkt__hook {
margin: 0 0 7px; padding: 8px 11px;
border-left: 2px solid var(--ton, var(--akzent));
border-radius: 0 6px 6px 0;
background: color-mix(in srgb, var(--ton, var(--akzent)) 9%, transparent);
font-size: .87rem; line-height: 1.45; color: var(--text);
}
.fest-punkt__hookschild {
display: block; margin-bottom: 2px;
font-size: .66rem; letter-spacing: .12em; text-transform: uppercase;
color: var(--ton, var(--akzent));
}
.fest-punkt__marke {
display: inline-block; margin-top: 8px; padding: 2px 9px;
border: 1px solid var(--rand); border-radius: 999px;
font-size: .72rem; color: var(--text-still);
}
.fest-punkt__marke-stufe {
flex: none; padding: 3px 11px;
border: 1px solid color-mix(in srgb, var(--p-ton) 36%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--p-ton) 12%, transparent);
color: var(--p-ton);
font-size: .73rem; font-weight: 600; white-space: nowrap;
}
/* Der Grund in voller Breite -- "verbessern" ohne Grund ist nicht
umsetzbar, nur entmutigend. Er darf nicht in eine Ecke. */
.fest-punkt__grund {
margin-top: 10px; padding: 9px 12px;
border-left: 2px solid var(--p-ton);
border-radius: 0 6px 6px 0;
background: color-mix(in srgb, var(--p-ton) 9%, transparent);
}
.fest-punkt__grund p { margin: 0; font-size: .86rem; line-height: 1.5; color: var(--text); }
.fest-punkt__von {
display: block; margin-top: 4px;
font-size: .74rem; color: var(--text-still);
}
.fest-punkt__fuss {
display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
margin-top: 11px;
}
.stufe-knopf {
padding: 6px 13px;
border: 1px solid var(--rand); border-radius: 999px;
background: rgba(0, 0, 0, .2);
color: var(--text-still);
font: inherit; font-size: .8rem; cursor: pointer;
transition: border-color .18s ease, color .18s ease, background .18s ease;
}
.stufe-knopf[data-stufe="gut"]:hover,
.stufe-knopf[data-stufe="gut"][data-an="ja"] {
border-color: rgba(77, 171, 119, .5); color: #6fbd95;
background: rgba(77, 171, 119, .13);
}
.stufe-knopf[data-stufe="verbessern"]:hover,
.stufe-knopf[data-stufe="verbessern"][data-an="ja"] {
border-color: rgba(224, 163, 60, .5); color: #e0b45c;
background: rgba(224, 163, 60, .13);
}
.fest-punkt__fuss .schritt { margin-left: auto; }
.punkt-gespraech {
margin-top: 11px; padding: 12px 13px;
border: 1px solid var(--rand); border-radius: var(--radius-klein);
background: rgba(0, 0, 0, .2);
}
@media (max-width: 560px) {
.liste-fest__kopf { flex-direction: column; align-items: stretch; }
.fest-punkt__kopf { flex-direction: column; }
.fest-punkt__marke-stufe { align-self: flex-start; }
.fest-punkt__fuss .schritt { margin-left: 0; }
.stufe-knopf { flex: 1 1 auto; text-align: center; }
}
/* Die neuen Klassen der festen Checkliste in der Mindestgroesse fuer
das Handy -- gemeldet von der Geraetepruefung (10,56 px beim
Aufhaenger-Schild). Wer kuenftig eine kleine Marke baut, faellt ihr
ebenfalls auf und landet hier. */
@media (max-width: 760px) {
body.start .fest-punkt__hookschild,
body.start .fest-punkt__marke,
body.start .fest-punkt__von,
body.start .fest-gruppe__zahl { font-size: .75rem; }
}
+13 -9
View File
@@ -205,17 +205,21 @@
Welcher Abschnitt gezeigt wird, richtet sich nach dem gewaehlten
Filter. Ohne Filter die Vorbereitung -- das ist der Teil, ohne den
die anderen beiden gar nicht erst sinnvoll sind. */
/* Die FESTE Liste -- nicht mehr Vorschlaege zum Uebernehmen.
Der Unterschied ist grundlegend: Eine Liste, die man sich erst
holen muss, ist keine Checkliste. Und zwei Creator haetten
unterschiedliche Listen gehabt, je nachdem wer sich was geholt
hat -- was einen Vergleich unmoeglich macht, und um Vergleich
geht es bei einer Betreuung.
Jetzt stehen dieselben Punkte fuer alle fest da, gruppiert. Was
sich unterscheidet, ist der Stand je Creator. */
async function vorlagenZeigen() {
const behaelter = $('vorlagen');
if (!behaelter || bereich !== 'live' || !window.vorlagenBlock) return;
const abschnitt = ['vorbereitung', 'mitschrift', 'auswertung'].includes(filter)
? filter : 'vorbereitung';
await window.vorlagenBlock(behaelter, {
art: 'live',
unterart: abschnitt,
creatorId: creatorWahl(),
danach: laden,
});
if (!behaelter || !window.checkliste) return;
if (!['live', 'community', 'technik'].includes(bereich)) return;
await window.checkliste(behaelter, bereich);
}
/* Fuer wen wird uebernommen? Ein Creator immer fuer sich (der Server
+318
View File
@@ -0,0 +1,318 @@
/* ===================================================================
Die feste Checkliste — steht immer da, für jeden gleich.
Ersetzt die vorherigen "Vorschläge zum Übernehmen". Der Unterschied
ist grundlegend: Eine Liste, die man sich erst holen muss, ist keine
Checkliste. Und zwei Creator hätten unterschiedliche Listen gehabt,
je nachdem, wer sich was geholt hat -- was einen Vergleich unmöglich
macht, und um Vergleich geht es bei einer Betreuung.
Jetzt: dieselben Punkte für alle, gruppiert. Was sich unterscheidet,
ist der STAND je Creator -- und den setzt die Betreuung.
Drei Zustände, drei Farben:
offen noch nicht angesehen
gut passt so
verbessern mit dem Satz, WAS zu verbessern ist
Der Creator sieht alles, kann aber nichts davon setzen. Antworten
kann er -- über die Nachrichten an jedem Punkt.
=================================================================== */
(() => {
'use strict';
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
async function hole(pfad, optionen = {}) {
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
return a;
}
const wann = (iso) => (iso
? new Date(iso).toLocaleString('de-DE',
{ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: '');
/* Welcher Creator wird gerade angesehen -- bleibt über Neuaufbauten
hinweg stehen, sonst springt die Auswahl bei jeder Bewertung zurück
auf den ersten. */
const gewaehlt = {};
window.checkliste = async function (behaelter, bereich) {
if (!behaelter) return;
let daten;
try {
const wahl = gewaehlt[bereich] ? `?creator_id=${gewaehlt[bereich]}` : '';
const a = await hole(`/workspace/api/checkliste/${bereich}${wahl}`);
if (!a.ok) return;
daten = await a.json();
} catch { return; }
behaelter.textContent = '';
const block = el('section', 'liste-fest');
/* ---- Kopf ---- */
const kopf = el('div', 'liste-fest__kopf');
const t = el('div', null);
t.append(el('p', 'feldschild', 'Feste Punkte'));
t.append(el('p', 'liste-fest__unter',
daten.darf_bewerten
? 'Diese Punkte gelten für alle. Markiere, was gut läuft und was der '
+ 'Creator noch verbessern sollte – er sieht es sofort.'
: 'Diese Punkte gelten für alle. Deine Betreuung markiert hier, was gut '
+ 'läuft und was du noch verbessern solltest. Antworten kannst du an '
+ 'jedem Punkt.'));
kopf.append(t);
if (daten.darf_bewerten && daten.creatoren.length) {
const feld = el('div', 'liste-fest__wahl');
feld.append(el('label', 'feld-schild', 'Creator'));
const s = el('select');
for (const c of daten.creatoren) s.append(new Option(c.name, String(c.id)));
if (daten.creator_id) s.value = String(daten.creator_id);
s.addEventListener('change', () => {
gewaehlt[bereich] = Number(s.value);
window.checkliste(behaelter, bereich);
});
feld.append(s);
kopf.append(feld);
if (!gewaehlt[bereich] && daten.creatoren.length) {
gewaehlt[bereich] = Number(s.value);
}
}
block.append(kopf);
/* ---- Fortschritt in einer Zeile ----
Wie viele Punkte sind angesehen, wie viele stehen auf "noch
verbessern"? Das ist die Frage, die beide zuerst haben. */
const alle = daten.gruppen.flatMap((g) => g.punkte);
const zuTun = alle.filter((p) => daten.stand[p.schluessel]?.stufe === 'verbessern').length;
const gut = alle.filter((p) => daten.stand[p.schluessel]?.stufe === 'gut').length;
if (daten.creator_id || !daten.darf_bewerten) {
const bilanz = el('div', 'liste-fest__bilanz');
bilanz.append(zahl(gut, 'passt so', 'gut'));
bilanz.append(zahl(zuTun, zuTun === 1 ? 'zu verbessern' : 'zu verbessern', 'verbessern'));
bilanz.append(zahl(alle.length - gut - zuTun, 'noch offen', 'offen'));
block.append(bilanz);
}
/* ---- Die Gruppen ---- */
for (const g of daten.gruppen) {
const abschnitt = el('section', 'fest-gruppe');
const gk = el('div', 'fest-gruppe__kopf');
gk.append(el('h3', 'fest-gruppe__name', g.name));
gk.append(el('span', 'fest-gruppe__zahl', String(g.punkte.length)));
abschnitt.append(gk);
if (g.text) abschnitt.append(el('p', 'fest-gruppe__text', g.text));
const liste = el('div', 'fest-punkte');
for (const p of g.punkte) {
liste.append(punktBauen(p, daten, bereich, behaelter));
}
abschnitt.append(liste);
block.append(abschnitt);
}
behaelter.append(block);
behaelter.hidden = false;
};
function zahl(n, text, stufe) {
const k = el('div', 'bilanz-zahl');
k.dataset.stufe = stufe;
k.append(el('span', 'bilanz-zahl__n', String(n)));
k.append(el('span', 'bilanz-zahl__text', text));
return k;
}
function punktBauen(p, daten, bereich, behaelter) {
const stand = daten.stand[p.schluessel];
const stufe = stand?.stufe || 'offen';
const k = el('article', 'fest-punkt');
k.dataset.stufe = stufe;
const kopf = el('div', 'fest-punkt__kopf');
const links = el('div', 'fest-punkt__text');
links.append(el('h4', 'fest-punkt__titel', p.titel));
/* Bei Content-Ideen ist der Aufhänger das Wichtigste -- die ersten
Sekunden entscheiden. Er steht deshalb hervorgehoben. */
if (p.hook) {
const h = el('p', 'fest-punkt__hook');
h.append(el('span', 'fest-punkt__hookschild', 'Aufhänger'));
h.append(document.createTextNode(p.hook));
links.append(h);
}
if (p.text) links.append(el('p', 'fest-punkt__beschreibung', p.text));
if (p.format) links.append(el('span', 'fest-punkt__marke', p.format));
kopf.append(links);
/* Die Marke rechts -- immer sichtbar, auch für den Creator, auch
wenn sie "noch offen" lautet. Er soll sehen, wo er steht. */
const marke = el('span', 'fest-punkt__marke-stufe',
stufe === 'gut' ? 'Passt so'
: stufe === 'verbessern' ? 'Verbessern' : 'Offen');
kopf.append(marke);
k.append(kopf);
/* Der Grund steht in voller Breite -- "verbessern" ohne Grund ist
nicht umsetzbar, nur entmutigend. */
if (stand?.begruendung) {
const g = el('div', 'fest-punkt__grund');
g.append(el('p', null, stand.begruendung));
g.append(el('span', 'fest-punkt__von',
`${stand.von_name || ''} · ${wann(stand.am)}`));
k.append(g);
}
/* ---- Was man tun kann ---- */
const fuss = el('div', 'fest-punkt__fuss');
if (daten.darf_bewerten && daten.creator_id) {
for (const [wert, name] of [['gut', 'Passt so'], ['verbessern', 'Verbessern']]) {
const b = el('button', 'stufe-knopf', name);
b.type = 'button';
b.dataset.stufe = wert;
if (stufe === wert) b.dataset.an = 'ja';
b.addEventListener('click', async () => {
/* Ein zweiter Klick auf dieselbe Stufe nimmt sie zurück --
sonst gäbe es keinen Weg zurück auf "offen". */
const neu = stufe === wert ? 'offen' : wert;
let grund = stand?.begruendung || '';
if (neu === 'verbessern') {
grund = prompt(`„${p.titel}“\n\nWas genau soll verbessert werden?\n\n`
+ 'Ohne diesen Satz kann er nichts damit anfangen.', grund) || '';
if (grund.trim().length < 5) return;
}
b.disabled = true;
try {
const a = await hole(`/workspace/api/checkliste/${bereich}/${p.schluessel}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stufe: neu, begruendung: grund,
creator_id: daten.creator_id }),
});
if (!a.ok) {
alert((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
b.disabled = false;
return;
}
window.checkliste(behaelter, bereich);
} catch { b.disabled = false; }
});
fuss.append(b);
}
}
/* Rückmeldungen -- für beide Seiten. */
const anzahl = daten.nachrichten[p.schluessel] || 0;
const reden = el('button', 'schritt',
anzahl ? `Rückmeldung (${anzahl})` : 'Rückmeldung');
reden.type = 'button';
reden.addEventListener('click', () => gespraech(k, bereich, p, daten, reden));
fuss.append(reden);
/* Bei Punkten, wo Hilfe angeboten wird, kann der Creator sie hier
anfordern -- daraus wird eine Aufgabe bei seinem Betreuer. */
if (p.hilfe && !daten.darf_bewerten && window.hilfeKnopf) {
fuss.append(window.hilfeKnopf(p.titel, daten.creator_id));
}
k.append(fuss);
return k;
}
/* ---------- Das Gespräch an einem Punkt -------------------------------- */
async function gespraech(karte, bereich, p, daten, knopf) {
const offen = karte.querySelector('.punkt-gespraech');
if (offen) { offen.remove(); return; }
let n;
try {
const a = await hole(
`/workspace/api/checkliste/${bereich}/${p.schluessel}/nachrichten`
+ (daten.creator_id ? `?creator_id=${daten.creator_id}` : ''));
if (!a.ok) return;
n = await a.json();
} catch { return; }
const kasten = el('div', 'punkt-gespraech');
const verlauf = el('div', 'nachrichten');
zeichne(verlauf, n.nachrichten, n.ich);
kasten.append(verlauf);
const form = el('form', 'nachricht-neu');
const feld = el('textarea');
feld.rows = 2;
feld.maxLength = 2000;
feld.placeholder = daten.darf_bewerten
? 'Rückmeldung an den Creator …'
: 'Frage oder Antwort an deine Betreuung …';
form.append(feld);
const senden = el('button', 'knopf knopf--klein', 'Senden');
senden.type = 'submit';
form.append(senden);
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!feld.value.trim()) return;
senden.disabled = true;
try {
const a = await hole(
`/workspace/api/checkliste/${bereich}/${p.schluessel}/nachrichten`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: feld.value, creator_id: daten.creator_id }),
});
if (!a.ok) { alert((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); return; }
const r = await a.json();
feld.value = '';
zeichne(verlauf, r.nachrichten, n.ich);
knopf.textContent = `Rückmeldung (${r.nachrichten.length})`;
} catch { /* umgeleitet */ } finally { senden.disabled = false; }
});
kasten.append(form);
karte.append(kasten);
feld.focus();
}
function zeichne(ziel, liste, ichId) {
ziel.textContent = '';
if (!liste.length) {
ziel.append(el('p', 'nachrichten__leer', 'Noch keine Rückmeldung zu diesem Punkt.'));
return;
}
for (const n of liste) {
const z = el('div', 'nachricht');
if (n.von === ichId) z.dataset.eigen = 'ja';
const kopf = el('div', 'nachricht__kopf');
kopf.append(el('span', 'nachricht__von', n.von_name || 'Unbekannt'));
kopf.append(el('span', 'nachricht__wann', wann(n.am)));
if (n.von === ichId) {
const weg = el('button', 'nachricht__weg', '×');
weg.type = 'button';
weg.title = 'Eigene Nachricht zurücknehmen';
weg.addEventListener('click', async () => {
if (!confirm('Diese Nachricht zurücknehmen?')) return;
try {
const a = await hole(`/workspace/api/checkliste/nachricht/${n.id}`,
{ method: 'DELETE' });
if (a.ok) z.remove();
} catch { /* umgeleitet */ }
});
kopf.append(weg);
}
z.append(kopf);
z.append(el('p', 'nachricht__text', n.text));
ziel.append(z);
}
}
})();
+5 -7
View File
@@ -443,14 +443,12 @@
/* Fertige Ideen zum Uebernehmen. Steht ueber der Strecke, damit
jemand mit leeren Spalten nicht erst daran vorbeiscrollen muss. */
/* Die feste Liste der Ideen -- nach Saeule gruppiert (70/20/10).
Sie steht fuer jeden gleich da; was sich unterscheidet, ist,
was die Betreuung dazu vermerkt hat. */
const behaelter = document.getElementById('vorlagen');
if (behaelter && window.vorlagenBlock) {
const wahl = document.getElementById('creator-wahl');
window.vorlagenBlock(behaelter, {
art: 'content',
creatorId: wahl && wahl.value ? Number(wahl.value) : null,
danach: laden,
});
if (behaelter && window.checkliste) {
window.checkliste(behaelter, 'content');
}
filterBauen();
+7 -7
View File
@@ -7,9 +7,9 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
</head>
<body class="start">
@@ -139,9 +139,9 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/aufgaben.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/aufgaben.js?v=202609011707" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609011707" />
</head>
<body class="start">
@@ -132,9 +132,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/automation.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/automation.js?v=202609011707" defer></script>
</body>
</html>
+12 -11
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609011707" />
</head>
<body class="start">
@@ -115,12 +115,13 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/ampel.js?v=202609011649" defer></script>
<script src="assets/js/schulung.js?v=202609011649" defer></script>
<script src="assets/js/vorlagen.js?v=202609011649" defer></script>
<script src="assets/js/bereich.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/checkliste.js?v=202609011707" defer></script>
<script src="assets/js/ampel.js?v=202609011707" defer></script>
<script src="assets/js/schulung.js?v=202609011707" defer></script>
<script src="assets/js/vorlagen.js?v=202609011707" defer></script>
<script src="assets/js/bereich.js?v=202609011707" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609011707" />
</head>
<body class="start">
@@ -54,10 +54,10 @@
</main>
<script src="assets/js/ki.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/calls.js?v=202609011649" defer></script>
<script src="assets/js/ki.js?v=202609011707" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/calls.js?v=202609011707" defer></script>
</body>
</html>
+10 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/content.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/content.css?v=202609011707" />
</head>
<body class="start">
@@ -162,10 +162,11 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/vorlagen.js?v=202609011649" defer></script>
<script src="assets/js/content.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/checkliste.js?v=202609011707" defer></script>
<script src="assets/js/vorlagen.js?v=202609011707" defer></script>
<script src="assets/js/content.js?v=202609011707" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609011707" />
</head>
<body class="start">
@@ -93,9 +93,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/dateien.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/dateien.js?v=202609011707" defer></script>
</body>
</html>
+2 -2
View File
@@ -25,7 +25,7 @@
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
</head>
<body class="gate">
@@ -143,6 +143,6 @@
</p>
</main>
<script src="assets/js/gate.js?v=202609011649" defer></script>
<script src="assets/js/gate.js?v=202609011707" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609011707" />
</head>
<body class="start">
@@ -147,9 +147,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/kalender.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/kalender.js?v=202609011707" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609011707" />
</head>
<body class="start">
@@ -121,9 +121,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/personen.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/personen.js?v=202609011707" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609011707" />
</head>
<body class="start">
@@ -175,10 +175,10 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/steckbrief.js?v=202609011649" defer></script>
<script src="assets/js/profil.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/steckbrief.js?v=202609011707" defer></script>
<script src="assets/js/profil.js?v=202609011707" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/report.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/report.css?v=202609011707" />
</head>
<body class="start">
@@ -91,10 +91,10 @@
</main>
<script src="assets/js/ki.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/report.js?v=202609011649" defer></script>
<script src="assets/js/ki.js?v=202609011707" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/report.js?v=202609011707" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609011707" />
</head>
<body class="start">
@@ -124,9 +124,9 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/scouting.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/scouting.js?v=202609011707" defer></script>
</body>
</html>
+6 -6
View File
@@ -7,8 +7,8 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
</head>
<body class="start">
@@ -99,9 +99,9 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/start.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/start.js?v=202609011707" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609011707" />
</head>
<body class="start">
@@ -70,10 +70,10 @@
</main>
<script src="assets/js/ki.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/startcheck.js?v=202609011649" defer></script>
<script src="assets/js/ki.js?v=202609011707" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/startcheck.js?v=202609011707" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609011707" />
</head>
<body class="start">
@@ -85,9 +85,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/steckbrief.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/steckbrief.js?v=202609011707" defer></script>
</body>
</html>
+6 -6
View File
@@ -7,9 +7,9 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609011707" />
</head>
<body class="start">
@@ -57,9 +57,9 @@
</main>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/uebersicht.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/uebersicht.js?v=202609011707" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609011649" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011707" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609011707" />
</head>
<body class="start">
@@ -175,9 +175,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011649" defer></script>
<script src="assets/js/bereiche.js?v=202609011649" defer></script>
<script src="assets/js/kopf.js?v=202609011649" defer></script>
<script src="assets/js/wissen.js?v=202609011649" defer></script>
<script src="assets/js/wahl.js?v=202609011707" defer></script>
<script src="assets/js/bereiche.js?v=202609011707" defer></script>
<script src="assets/js/kopf.js?v=202609011707" defer></script>
<script src="assets/js/wissen.js?v=202609011707" defer></script>
</body>
</html>