diff --git a/server/mess-sichtbarkeit.mjs b/server/mess-sichtbarkeit.mjs new file mode 100644 index 00000000..91d60cfc --- /dev/null +++ b/server/mess-sichtbarkeit.mjs @@ -0,0 +1,157 @@ +/* MESSUNG, KEINE PRUEFUNG (17.09.2026) + + Filipe: "die community und alle anderen sollen die neuen sachen in + allen kategorien auch sehen bitte. in allen die sie sehen soll auch + jede aenderung mit gehen, immer nur in denen die ihnen erlaubt ist + zu sehen." + + Die Frage: Was sieht eigentlich wer? Nicht geraten -- gemessen. + + ZWEI DINGE, DIE ICH BEIM ERSTEN VERSUCH FALSCH HATTE und die jede + Zahl wertlos gemacht haben: + + 1. Die Community lebt auf crew., nicht auf der Agenturadresse. + Ohne den richtigen Host-Kopf kam der Gast mit 401 "ungueltig" + heraus -- und das sah aus wie ein Befund, war aber meine eigene + falsche Adresse. + 2. Ein Gast bestaetigt beim Anmelden sein Alter (`alter_ok`). + + Aufrufen mit: node server/mess-sichtbarkeit.mjs +*/ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4496, "mess-sichtbarkeit"); +const ordner = mkdtempSync(join(tmpdir(), "ws-mess-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "mess-sichtbarkeit"); +await new Promise((r) => setTimeout(r, 700)); + +const CREW = "crew.dogfather-universe.com"; +const AGENTUR = "workspace.dogfather-universe.com"; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const { TREFF_BRETTER } = await import("./workspace.js"); +const { TREFF_FREIGABE_BRETTER } = await import("./workspace-treff.js"); +const { BEREICHE } = await import("./workspace-bereiche.js"); +const ALLE_BEREICHE = Object.keys(BEREICHE); + +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const sch = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", sch, jetzt); + +const ROLLEN = ["admin", "hand", "modi", "gast"]; +const codes = {}; +for (const r of ROLLEN) { + const code = `CODE-${r.toUpperCase()}-0001`; + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(r, r, hash, salt, 32768, + createHmac("sha256", sch).update(code).digest("hex"), jetzt); + codes[r] = code; +} +d.close(); + +function roh(pfad, host, kopf = {}, koerper = null, methode = null) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, + method: methode || (koerper ? "POST" : "GET"), + headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf }, + }, (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); + }); +} +async function anmelden(rolle, host) { + const r = await roh("/workspace/api/anmelden", host, {}, + rolle === "gast" ? { rolle, code: codes[rolle], alter_ok: true } : { rolle, code: codes[rolle] }); + const kekse = [].concat(r.kopf?.["set-cookie"] || []); + return { keks: kekse.map((c) => c.split(";")[0]).join("; "), code: r.code }; +} + +/* Jede Rolle auf der Wand, auf der sie hingehoert. */ +const WAND = { admin: CREW, hand: CREW, modi: CREW, gast: CREW }; +const kekse = {}; +console.log("\n=== Anmeldung ==="); +for (const r of ROLLEN) { + const a = await anmelden(r, WAND[r]); + kekse[r] = a.keks; + console.log(` ${r.padEnd(8)} auf ${WAND[r]} -> ${a.code}`); +} + +/* ---- DogFather uebernimmt ueberall den Startkatalog ------------------ */ +console.log("\n=== DogFather uebernimmt die Vorschlaege ==="); +for (const b of ALLE_BEREICHE) { + const a = await roh(`/workspace/api/bereich/${b}/start`, CREW, + { cookie: kekse.admin, origin: `https://${CREW}` }, {}); + const j = JSON.parse(a.text || "{}"); + console.log(` ${b.padEnd(12)} ${a.code} angelegt: ${j?.angelegt ?? "-"}`); +} + +/* ---- Und jetzt: wer sieht wie viel? --------------------------------- */ +console.log("\n=== Was jede Rolle danach sieht ==="); +console.log(" Brett " + ROLLEN.map((r) => r.padEnd(8)).join("") + "Freigabe?"); +for (const b of TREFF_BRETTER) { + const zahlen = []; + for (const r of ROLLEN) { + const a = await roh(`/workspace/api/bereich/${b}`, WAND[r], { cookie: kekse[r] }); + let j = null; try { j = JSON.parse(a.text); } catch { /* keine JSON-Antwort */ } + zahlen.push(a.code === 200 ? String((j?.eintraege || []).length) : `(${a.code})`); + } + const frei = TREFF_FREIGABE_BRETTER.includes(b) ? "JA" : "-"; + console.log(` ${b.padEnd(14)}` + zahlen.map((z) => z.padEnd(8)).join("") + frei); +} + +console.log("\n=== Wer bekommt den Vorschlagskasten? ==="); +for (const b of TREFF_BRETTER) { + const zeile = []; + for (const r of ROLLEN) { + const a = await roh(`/workspace/api/bereich/${b}/start`, WAND[r], { cookie: kekse[r] }); + let j = null; try { j = JSON.parse(a.text); } catch { /* keine JSON-Antwort */ } + zeile.push(a.code === 200 ? `${(j?.vorschlaege || []).length}` : `(${a.code})`); + } + console.log(` ${b.padEnd(14)}` + zeile.map((z) => z.padEnd(8)).join("")); +} + +console.log("\n=== Und die uebrigen Kategorien (ausserhalb des Treffs) ==="); +const andere = Object.keys(BEREICHE).filter((b) => !TREFF_BRETTER.includes(b)); +console.log(" Bereich " + ROLLEN.map((r) => r.padEnd(8)).join("")); +for (const b of andere) { + const zeile = []; + for (const r of ROLLEN) { + const a = await roh(`/workspace/api/bereich/${b}`, WAND[r], { cookie: kekse[r] }); + let j = null; try { j = JSON.parse(a.text); } catch { /* keine JSON-Antwort */ } + zeile.push(a.code === 200 ? String((j?.eintraege || []).length) : `(${a.code})`); + } + console.log(` ${b.padEnd(14)}` + zeile.map((z) => z.padEnd(8)).join("")); +} + +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(0); diff --git a/server/pruef-alle-sehen-es.mjs b/server/pruef-alle-sehen-es.mjs new file mode 100644 index 00000000..a9156378 --- /dev/null +++ b/server/pruef-alle-sehen-es.mjs @@ -0,0 +1,258 @@ +/* ===================================================================== + WAS DOGFATHER EINTRÄGT, SEHEN AUCH DIE ANDEREN (17.09.2026) + + Filipe: „die community und alle anderen sollen die neuen sachen in + allen kategorien auch sehen bitte. in allen die sie sehen soll auch + jede änderung mit gehen, immer nur in denen die ihnen erlaubt ist + zu sehen." + + --------------------------------------------------------------------- + ZWEI FEHLER, DIE NIEMAND GEMELDET HÄTTE + + Beide sahen auf DogFathers eigenem Bildschirm vollkommen richtig + aus. Das ist das Tückische daran: Ein leeres Brett sieht nicht nach + Fehler aus, sondern nach „da ist eben noch nichts". + + 1. FREIGABE. `ansteht` und `highlight` zeigen der Community nur, was + freigegeben ist. Ein aus dem Vorschlagskasten übernommener + Eintrag hatte keine Freigabe -- gemessen sah die Community 0 von + 4 und 0 von 3, während im Team alle drei Rollen alles sahen. + + 2. TEAM_DOGI_ROLLEN ist {hand, modi} -- OHNE "admin". Die + Sichtbarkeitsregel für Team Dogi lautete damit wörtlich + „Einträge von hand oder modi", und alles, was DogFather in Live, + Technik, Community, Ideen, Angebote oder Rückmeldung schrieb, war + für sein eigenes Team unsichtbar. + + --------------------------------------------------------------------- + UND DIE ANDERE HÄLFTE DES SATZES WIRD AUCH GEPRÜFT + + „immer nur in denen die ihnen erlaubt ist zu sehen." Eine Prüfung, + die nur zählt, ob alle alles sehen, wäre grün, wenn man sämtliche + Schranken entfernte. Deshalb steht in Abschnitt 4 die Gegenprobe: + Was zu ist, muss zu bleiben. + + Aufrufen mit: node server/pruef-alle-sehen-es.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4495, "pruef-alle-sehen-es"); +const ordner = mkdtempSync(join(tmpdir(), "ws-sicht-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-alle-sehen-es"); +await new Promise((r) => setTimeout(r, 700)); + +const CREW = "crew.dogfather-universe.com"; + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const { TREFF_BRETTER } = await import("./workspace.js"); +const { TREFF_FREIGABE_BRETTER } = await import("./workspace-treff.js"); +const { BEREICHE } = await import("./workspace-bereiche.js"); +const { ARBEIT_START } = await import("./workspace-arbeit-start.js"); +const { TREFF_START } = await import("./workspace-treff-start.js"); + +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const sch = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", sch, jetzt); +const ROLLEN = ["admin", "hand", "modi", "gast"]; +const codes = {}; +for (const r of ROLLEN) { + const code = `CODE-${r.toUpperCase()}-0001`; + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(r, r, hash, salt, 32768, + createHmac("sha256", sch).update(code).digest("hex"), jetzt); + codes[r] = code; +} +d.close(); + +function roh(pfad, kopf = {}, koerper = null, methode = null) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, + method: methode || (koerper ? "POST" : "GET"), + headers: { Host: CREW, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf }, + }, (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); + }); +} +const kekse = {}; +for (const r of ROLLEN) { + const antwort = await new Promise((fertig) => { + const q = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST", + headers: { Host: CREW, "Content-Type": "application/json" }, + }, (w) => { let t = ""; w.on("data", (x) => { t += x; }); w.on("end", () => fertig(w)); }); + q.write(JSON.stringify(r === "gast" + ? { rolle: r, code: codes[r], alter_ok: true } : { rolle: r, code: codes[r] })); + q.end(); + }); + kekse[r] = [].concat(antwort.headers["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); +} + +async function liste(bereich, rolle) { + const a = await roh(`/workspace/api/bereich/${bereich}`, { cookie: kekse[rolle] }); + let j = null; try { j = JSON.parse(a.text); } catch { /* keine JSON-Antwort */ } + return { code: a.code, n: (j?.eintraege || []).length }; +} + +/* ======================================================================= + 1. DOGFATHER FÜLLT ALLES + ======================================================================= */ +melde(""); +melde("=== 1. Der Vorschlagskasten ==="); +const KATALOG = { ...ARBEIT_START, ...TREFF_START }; +const gefuellt = []; +{ + let summe = 0; + for (const b of Object.keys(BEREICHE)) { + const a = await roh(`/workspace/api/bereich/${b}/start`, + { cookie: kekse.admin, origin: `https://${CREW}` }, {}); + let j = null; try { j = JSON.parse(a.text); } catch { /* keine JSON-Antwort */ } + if (a.code === 201 && j?.angelegt) { gefuellt.push(b); summe += j.angelegt; } + } + /* DIE ZAHL STEHT IN DER BEDINGUNG, nicht nur im Text. Ohne das wäre + „alle sehen gleich viel" auch dann grün, wenn es null wäre. */ + ok(summe > 50, `DogFather legt ${summe} fertige Einträge in ${gefuellt.length} Kategorien an`); + ok(gefuellt.length >= 15, + `und zwar in fast allen (${gefuellt.length} von ${Object.keys(BEREICHE).length})`); + + /* Die beiden personenbezogenen bleiben mit Absicht leer. */ + ok(!KATALOG.talente && !KATALOG.entwicklung, + "„talente\" und „entwicklung\" haben bewusst keinen Katalog – dort stünden erfundene Menschen"); +} + +/* ======================================================================= + 2. UND DIE COMMUNITY SIEHT ES AUCH ← Fehler 1 + ======================================================================= */ +melde(""); +melde("=== 2. Was die Community sieht ==="); +{ + for (const b of TREFF_BRETTER) { + const a = await liste(b, "admin"); + const g = await liste(b, "gast"); + const noetig = TREFF_FREIGABE_BRETTER.includes(b); + ok(a.n > 0 && g.n === a.n, + `${b.padEnd(10)} DogFather ${a.n}, Community ${g.n}` + + (noetig ? " (Brett mit Freigabe – genau hier war der Fehler)" : "")); + } +} + +/* ======================================================================= + 3. UND SEIN TEAM AUCH ← Fehler 2 + ======================================================================= */ +melde(""); +melde("=== 3. Was das Team sieht ==="); +{ + let geprueftHier = 0; + for (const b of gefuellt) { + const a = await liste(b, "admin"); + if (!a.n) continue; + for (const r of ["hand", "modi"]) { + const x = await liste(b, r); + /* 404 heißt: Diese Rolle darf den Bereich gar nicht -- das ist + kein Fehler, sondern die andere Hälfte von Filipes Satz. Sie + wird in Abschnitt 4 geprüft, hier übersprungen. */ + if (x.code === 404) continue; + geprueftHier++; + ok(x.n === a.n, `${b.padEnd(12)} ${r.padEnd(5)} sieht ${x.n} von ${a.n}`); + } + } + ok(geprueftHier >= 10, + `es wurden ${geprueftHier} Kombinationen wirklich geprüft – nicht alle übersprungen`); +} + +/* ======================================================================= + 4. ABER NUR DA, WO SIE DÜRFEN ← die Gegenprobe + ======================================================================= */ +melde(""); +melde("=== 4. Und was zu bleibt ==="); +{ + /* OHNE DIESEN ABSCHNITT wäre die ganze Prüfung grün, wenn man jede + Schranke entfernte. „Alle sehen alles" ist nicht das Ziel -- + Filipe sagt ausdrücklich „immer nur in denen die ihnen erlaubt + ist zu sehen". */ + const zu = []; + for (const b of Object.keys(BEREICHE)) { + for (const r of ["hand", "modi", "gast"]) { + const x = await liste(b, r); + if (x.code === 404) zu.push(`${b}/${r}`); + } + } + ok(zu.length >= 10, + `${zu.length} Bereich-Rollen-Paare bleiben verschlossen (404)`); + + /* Und die Community kommt an keinen einzigen Arbeitsbereich. */ + const arbeit = Object.keys(BEREICHE).filter((b) => !TREFF_BRETTER.includes(b)); + const offenFuerGast = []; + for (const b of arbeit) { + const x = await liste(b, "gast"); + if (x.code !== 404) offenFuerGast.push(`${b} (${x.code})`); + } + ok(offenFuerGast.length === 0, + offenFuerGast.length + ? `die Community kommt an: ${offenFuerGast.join(", ")}` + : `die Community sieht keinen der ${arbeit.length} Arbeitsbereiche`); +} + +/* ======================================================================= + 5. UND DIE REGEL STEHT AN EINER STELLE + ======================================================================= */ +melde(""); +melde("=== 5. Eine Quelle, nicht zwei ==="); +{ + const { readFileSync } = await import("node:fs"); + const quelle = readFileSync(new URL("./workspace-bereiche.js", import.meta.url), "utf8") + .replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\r\n]*/g, " "); + ok(/\[\.\.\.TEAM_DOGI_ROLLEN,\s*"admin"\]/.test(quelle), + "die Team-Sicht schließt DogFather ein"); + + /* UND TEAM_DOGI_ROLLEN SELBST BLEIBT UNBERÜHRT. Stünde "admin" + dort drin, wäre er stillschweigend ins andere Haus umgezogen -- + diese Menge entscheidet auch darüber. */ + const { TEAM_DOGI_ROLLEN } = await import("./workspace.js"); + ok(!TEAM_DOGI_ROLLEN.has("admin"), + `TEAM_DOGI_ROLLEN bleibt {${[...TEAM_DOGI_ROLLEN].join(", ")}} – sonst zöge DogFather ins Haus der Crew`); + + const treff = readFileSync(new URL("./workspace-bereiche.js", import.meta.url), "utf8"); + ok(/INSERT OR IGNORE INTO treff_freigaben/.test(treff), + "und ein übernommener Vorschlag wird gleich freigegeben"); +} + +melde(""); +melde(`${geprueft} Prüfungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-deutsche-texte.mjs b/server/pruef-deutsche-texte.mjs index 690edb18..54d89e0a 100644 --- a/server/pruef-deutsche-texte.mjs +++ b/server/pruef-deutsche-texte.mjs @@ -83,8 +83,16 @@ const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); ======================================================================= */ melde(""); melde("=== 1. Was in den Katalogen steht ==="); -const [w, v] = await Promise.all([ +const [w, v, as, ts, ep] = await Promise.all([ import("./workspace.js"), import("./workspace-vorlagen.js"), + /* SEIT DEM 17.09.2026 AUCH DIESE DREI. Sie kamen an einem Tag dazu + -- 113 fertige Einträge, 40 Entwicklungspunkte, 6 Befindensfragen + -- und diese Prüfung war die ganze Zeit grün, ohne eine Zeile + davon gelesen zu haben. Ein Katalog, den die Sprachprüfung nicht + kennt, ist ein Katalog ohne Sprachprüfung. */ + import("./workspace-arbeit-start.js"), + import("./workspace-treff-start.js"), + import("./workspace-entwicklungs-punkte.js"), ]); const texte = []; @@ -93,11 +101,28 @@ for (const s of w.MODI_STUFEN) texte.push([`Stufe ${s.wert}`, `${s.name} ${s.tex for (const g of v.MODI_GRUPPEN) texte.push([`Gruppe ${g.schluessel}`, `${g.name} ${g.text}`]); for (const a of v.MODI_AUFGABEN_FLACH) texte.push([`Modi ${a.schluessel}`, `${a.titel} ${a.text}`]); for (const a of v.MODI_KATALOG) texte.push([`Agentur ${a.schluessel}`, `${a.titel} ${a.text}`]); +/* Die beiden Startkataloge -- abgeleitet aus ihren eigenen Schlüsseln, + nicht aus einer Liste hier. Kommt ein Bereich dazu, ist er dabei. */ +for (const [bereich, liste] of Object.entries(as.ARBEIT_START)) { + for (const e of liste) texte.push([`Start ${bereich}`, `${e.titel} ${e.text ?? ""}`]); +} +for (const [bereich, liste] of Object.entries(ts.TREFF_START)) { + for (const e of liste) texte.push([`Treff ${bereich}`, `${e.titel} ${e.text ?? ""}`]); +} +/* Und der Entwicklungskatalog samt seiner Blöcke und Stufen. */ +for (const b of ep.ENTWICKLUNG_BLOECKE) texte.push([`Block ${b.schluessel}`, `${b.name} ${b.text}`]); +for (const e of ep.ENTWICKLUNG_ERWARTUNG) texte.push([`Erwartung ${e.schluessel}`, `${e.name} ${e.text}`]); +for (const pt of ep.ENTWICKLUNG_ALLE) texte.push([`Punkt ${pt.schluessel}`, `${pt.titel} ${pt.text}`]); /* DIE ANZAHL GEHOERT IN DIE BEDINGUNG, nicht nur in den Meldetext. Waere der Katalog leer, haette die Schleife darunter nichts zu tun und wuerde ein sattes Gruen melden. */ -ok(texte.length >= 150, `${texte.length} ausgelieferte Katalogtexte gefunden`); +/* 350 und nicht 400: Gemessen sind es am 17.09.2026 exakt 391. Die + Grenze ist eine SICHERUNG gegen "es wird gar nichts mehr geprueft", + keine Behauptung ueber die genaue Zahl -- deshalb mit Luft nach + unten und ohne Anspruch, gepflegt zu werden. Meine erste Fassung + stand auf 400 und war damit sofort rot: geraten statt gemessen. */ +ok(texte.length >= 350, `${texte.length} ausgelieferte Katalogtexte gefunden`); const schmutzig = texte.filter(([, t]) => ersatzschreibungen(t).length); for (const [wo, t] of schmutzig) { diff --git a/server/workspace-arbeit-start.js b/server/workspace-arbeit-start.js new file mode 100644 index 00000000..6e720e6d --- /dev/null +++ b/server/workspace-arbeit-start.js @@ -0,0 +1,346 @@ +/* ===================================================================== + FERTIGE INHALTE FÜR DIE ARBEITSBEREICHE (17.09.2026) + + Filipe, mit dem Bildschirmfoto der leeren Talente-Seite: „bitte mach + das viel geiler … mach dass da fertige sachen sind, aber nicht basic + sondern hochwertig, professionell, bis zum letzten punkt verbunden, + vereinfacht, perfektioniert." + + --------------------------------------------------------------------- + WAS GEMESSEN WURDE, BEVOR HIER ETWAS STAND + + `node server/mess-sichtbarkeit.mjs` am 17.09.2026, alle Bereiche, + alle vier Rollen, nachdem DogFather überall übernommen hatte: + + Treff-Bretter 7 · 5 · 4 · 5 · 3 · 17 · 11 Einträge + live 0 + content 0 + technik 0 + community 0 + schutz 0 + entwicklung 0 + talente 0 + rueckmeldung 0 + ideen 0 + angebote 0 + agentur 0 + + Elf Bereiche, null Einträge — und zwar für JEDE Rolle, auch für + DogFather selbst. Der Startkatalog kannte nur die sieben Bretter des + Treffs. Die Oberfläche war die ganze Zeit bereit: `bereich.js` fragt + bei jedem Bereich nach Vorschlägen und blendet den Kasten aus, wenn + keine kommen. Es fehlten nur die Texte. + + --------------------------------------------------------------------- + ZWEI BEREICHE BEKOMMEN HIER ABSICHTLICH NICHTS + + `talente` und `entwicklung` sind personenbezogen. Ein „fertiger + Inhalt" wäre dort ein erfundener Mensch — ein Talent, das niemand + kennt, oder eine Beobachtung über jemanden, die nie jemand gemacht + hat. Das ist keine Starthilfe, das sind Testdaten in einem System, + das echte Menschen bewertet. Filipes Regel dazu ist eindeutig + („keine scheiss teste mehr reinknallen, verboten“), und sie gilt + hier erst recht. + + Diese beiden Bereiche bekommen ihre Hilfe deshalb anders: als + Leitfaden auf der Seite selbst, ohne einen einzigen Datensatz. + + --------------------------------------------------------------------- + WIE DIE TEXTE GESCHRIEBEN SIND + + Jeder Eintrag ist etwas, das man SOFORT benutzen kann — kein + Platzhalter, keine Überschrift ohne Inhalt. Wer „Übernehmen“ drückt, + hat danach eine brauchbare Zeile in seiner Liste, keine Hausaufgabe. + + Und sie sind auf DogFather bezogen (seine Anweisung vom 17.09.2026: + „ich, dogfather bin der mittelpunkt immer“): seine Streams, seine + drei Kanäle, Casper. Wo das Team vorkommt, steht es auf Augenhöhe — + die Regel vom 18.08.2026 gilt unverändert. + ===================================================================== */ + +export const ARBEIT_START = { + + /* =================================================================== + LIVE-ANALYSE — was vor, während und nach einem Stream passiert + =================================================================== */ + live: [ + { art: "vorbereitung", titel: "Startcheck: Ton, Bild, Licht, Chat", + text: "Fünf Minuten vor dem Start: Mikro auf Pegel, Kamera scharf, " + + "Licht nicht gegen das Fenster, Chat offen, Modi da. Die häufigste " + + "Ursache für einen schlechten Stream ist nichts Dramatisches, " + + "sondern etwas, das vorher niemand angesehen hat." }, + { art: "vorbereitung", titel: "Ein Satz, mit dem der Stream anfängt", + text: "Nicht „so, hallo“ — sondern der Grund, warum heute jemand " + + "dableiben soll. Was passiert gleich? Warum lohnt es sich? " + + "Die ersten dreißig Sekunden entscheiden über die nächste Stunde." }, + { art: "vorbereitung", titel: "Was heute im Stream vorkommen soll", + text: "Drei Punkte reichen. Ein Stream ohne Plan wird nicht lockerer, " + + "er wird nur länger. Und die Wünsche aus dem Treff gehören hier " + + "hinein — dort stehen sie bereits sortiert." }, + { art: "mitschrift", titel: "Der Moment, der ein Clip wird", + text: "Sobald etwas passiert, das gut war: Uhrzeit hier notieren. " + + "Nach dem Stream findet man es sonst nicht wieder — und genau " + + "daran scheitern die meisten Clips, nicht am Schneiden." }, + { art: "mitschrift", titel: "Was der Chat gefragt hat und offen blieb", + text: "Fragen, die im Trubel untergegangen sind. Sie sind der beste " + + "Anfang für den nächsten Stream: Man kommt wieder und die Frage " + + "von letztem Mal wird beantwortet." }, + { art: "auswertung", titel: "Wann sind die Leute gegangen?", + text: "Nicht wie viele — WANN. Ein Einbruch immer an derselben Stelle " + + "(nach dem Intro, in einer Pause, beim Spielwechsel) ist eine " + + "Stellschraube, keine Zufälligkeit." }, + { art: "auswertung", titel: "Was hat den Chat wirklich bewegt?", + text: "Der Abschnitt mit den meisten Nachrichten, nicht der lauteste. " + + "Wo mitgeschrieben wird, wird mitgefiebert — und das ist die " + + "Sorte Moment, die als Clip auch außerhalb funktioniert." }, + { art: "auswertung", titel: "Ein Satz an Casper", + text: "Kommt er vor, bleiben die Leute. Nach jedem Stream eine Zeile: " + + "Was hat er gemacht, hat es jemand gesehen, taugt es für HasiDog?" }, + ], + + /* =================================================================== + CONTENT-IDEEN — vom Einfall bis zum veröffentlichten Video + =================================================================== */ + content: [ + { art: "idee", titel: "Casper reagiert auf … (Serie)", + text: "Wiederkehrendes Format für HasiDog: Casper bekommt etwas vorgesetzt " + + "— Geräusch, Spielzeug, neuer Napf — und die Kamera läuft schon. " + + "Serien schlagen Einzelvideos, weil die Leute wiederkommen, ohne " + + "dass man sie darum bitten muss." }, + { art: "idee", titel: "„Das hat der Chat entschieden“", + text: "Im Stream abstimmen lassen, im Clip das Ergebnis zeigen. " + + "Verbindet DogFather und DogFather Clips: Wer den Clip sieht, " + + "merkt, dass er live hätte mitentscheiden können." }, + { art: "idee", titel: "Der Moment, in dem etwas schiefging", + text: "Nicht wegschneiden — genau das ist der Clip. Gelungenes sieht " + + "man überall; misslungenes mit ehrlicher Reaktion sieht man selten." }, + { art: "skript", titel: "Die ersten drei Sekunden festlegen", + text: "Hook zuerst schreiben, nicht zuletzt. Wer erst erklärt und dann " + + "zeigt, hat schon verloren. Regel für alle drei Kanäle: Das " + + "Interessanteste steht am Anfang, nicht in der Mitte." }, + { art: "skript", titel: "Ein Video, eine Aussage", + text: "Wenn sich ein Clip nicht in einem Satz zusammenfassen lässt, " + + "sind es zwei Clips. Das ist keine Kürzung, sondern doppelt " + + "so viel Material." }, + { art: "gedreht", titel: "Material liegt — Ton geprüft?", + text: "Bevor etwas in den Schnitt geht: einmal mit Kopfhörer anhören. " + + "Ein gutes Bild mit schlechtem Ton wird weggewischt, umgekehrt nicht." }, + { art: "fertig", titel: "Cover und Text vor dem Hochladen", + text: "Das Cover entscheidet, ob jemand tippt. Gesicht oder Casper, " + + "großer Text, kein Schriftzug am unteren Rand (dort liegt die " + + "Bedienleiste). Der Text darunter ist kein Titel, sondern eine Frage." }, + { art: "fertig", titel: "Auf welchem Kanal — und warum dort?", + text: "DogFather für Streams und Persönliches, DogFather Clips für " + + "Ausschnitte, HasiDog für Casper. Ein Video auf dem falschen Kanal " + + "ist nicht falsch, es findet nur niemanden." }, + { art: "veroeffentlicht", titel: "Nach 24 Stunden noch einmal ansehen", + text: "Nicht die Zahl — die Kommentare. Was wurde gefragt, was " + + "missverstanden? Das ist die Vorlage fürs nächste Video, und sie " + + "kostet fünf Minuten." }, + ], + + /* =================================================================== + TECHNIK — Setups, Probleme, Lösungen + =================================================================== */ + technik: [ + { art: "setup", titel: "Das laufende Setup aufschreiben", + text: "Mischpult, Mikro, Kamera, Licht, Programme — mit Einstellungen. " + + "Klingt überflüssig, bis etwas kaputtgeht oder umgezogen wird. " + + "Dann ist es das Einzige, was zählt." }, + { art: "setup", titel: "Windows auf Höchstleistung, nicht Ausbalanciert", + text: "Am 14.09.2026 nachgemessen: Bei voller CPU bekommt der Ton-Encoder " + + "keine Rechenzeit und verwirft Blöcke — das klingt für die Zuschauer " + + "roboterhaft. Zwei Tage Fehlersuche gingen in die falsche Richtung, " + + "die Messung dauerte 30 Sekunden." }, + { art: "setup", titel: "60 Bilder pro Sekunde sind selten nötig", + text: "1080×1920 mit 60 fps hat die CPU auf 98 % gebracht. 30 fps sehen " + + "auf dem Handy praktisch gleich aus und lassen Luft für den Ton. " + + "Ton schlägt Bild — immer." }, + { art: "problem", titel: "„Ich höre mich selbst doppelt“", + text: "Mix-Minus ist beim RØDECaster Duo ab Werk nur auf USB Chat und " + + "USB 2 aktiv, nicht auf USB 1. Steht auf Seite 30 des deutschen " + + "Handbuchs. An einem laufenden Stream wird dafür nichts „mal " + + "ausprobiert“ — entweder man weiß, was eine Einstellung tut, oder " + + "man fasst sie nicht an." }, + { art: "problem", titel: "Zuschauer hören es, DogFather nicht", + text: "Dann liegt es nie am Eingang. Der Kopfhörer am Mischpult ist " + + "Hardware und vom Rechner unabhängig — ist der sauber, ist der " + + "Eingang bewiesenermaßen in Ordnung, und nur der Weg nach draußen " + + "bleibt verdächtig. Diese Zweiteilung spart Tage." }, + { art: "loesung", titel: "Vor jeder Fehlersuche: CPU, GPU, Netz messen", + text: "Dreißig Sekunden. Steht die CPU bei 98 %, ist jede weitere " + + "Vermutung über Kabel, Treiber und Latenzen verschwendete Zeit." }, + { art: "anleitung", titel: "Wenn der Stream abbricht — die ersten drei Schritte", + text: "1. Läuft das Internet? (Handy ohne WLAN prüfen) 2. Läuft das " + + "Programm noch? 3. Erst dann Einstellungen. Wer bei Schritt 3 " + + "anfängt, macht aus einer Störung ein zweites Problem." }, + { art: "anleitung", titel: "Aufnahme mitlaufen lassen — immer", + text: "Eine lokale Aufnahme kostet nichts und rettet jeden Stream, der " + + "schiefgeht. Ohne sie ist ein abgebrochener Stream für Clips " + + "vollständig verloren." }, + ], + + /* =================================================================== + COMMUNITY — Moderation, Aktionen, Konflikte + =================================================================== */ + community: [ + { art: "moderation", titel: "Wer ist heute im Chat zuständig?", + text: "Vor dem Stream festlegen, nicht währenddessen. „Irgendjemand " + + "macht das schon“ heißt in der Praxis: niemand, und dann drei " + + "gleichzeitig." }, + { art: "moderation", titel: "Neue begrüßen, bevor sie fragen", + text: "Der erste Satz an jemanden, der zum ersten Mal da ist, entscheidet, " + + "ob er wiederkommt. Kostet nichts und passiert trotzdem selten." }, + { art: "moderation", titel: "Eingreifen ohne Bühne", + text: "Eine Verwarnung mit Publikum erzeugt Publikum. Kurz, sachlich, " + + "weiter — und danach nicht mehr darauf zurückkommen." }, + { art: "aktion", titel: "Wunschstream aus dem Treff", + text: "Die Wunschliste im Treff sortiert sich nach Stimmen. Einmal im " + + "Monat der oberste Wunsch — und danach der Clip zurück ins Brett, " + + "mit dem Namen dessen, der ihn geschrieben hat. Das ist der " + + "Unterschied zwischen einem Briefkasten und einer Community." }, + { art: "aktion", titel: "Casper-Tag", + text: "Ein Stream, in dem Casper im Mittelpunkt steht — angekündigt auf " + + "HasiDog, gestreamt auf DogFather. Verbindet zwei Kanäle ohne " + + "zusätzlichen Aufwand." }, + { art: "aktion", titel: "Zuschauerabend: Ihr spielt, DogFather schaut zu", + text: "Einmal umgedreht. Kostet keine Vorbereitung, bindet die Leute " + + "stärker als jedes Gewinnspiel — und liefert nebenbei Material." }, + { art: "konflikt", titel: "Streit im Chat: erst trennen, dann klären", + text: "Im Chat gewinnt niemand. Die Sache wird angehalten, das Gespräch " + + "findet danach statt — mit beiden, nicht öffentlich." }, + { art: "konflikt", titel: "Wenn es gegen DogFather persönlich geht", + text: "Nicht diskutieren, nicht rechtfertigen. Regel anwenden, notieren, " + + "weitermachen. Eine Antwort ist genau das, was gesucht wurde." }, + ], + + /* =================================================================== + SCHUTZ & REGELN — Richtlinien, Vorfälle, Eskalation + =================================================================== */ + schutz: [ + { art: "richtlinie", titel: "Was sofort entfernt wird — ohne Diskussion", + text: "Hetze, Bedrohungen, sexuelle Inhalte mit Minderjährigen, private " + + "Daten anderer. Kein Verwarnen, kein Erklären, keine zweite Chance. " + + "Alles andere ist verhandelbar, das hier nicht." }, + { art: "richtlinie", titel: "Drei Stufen, und jede wird notiert", + text: "Hinweis → Auszeit → Ausschluss. Wer notiert, kann beim dritten Mal " + + "belegen, dass es das dritte Mal ist. Wer nicht notiert, diskutiert." }, + { art: "richtlinie", titel: "Gleiche Sache, gleiche Antwort", + text: "Ein Chat merkt sich vor allem eins: ob dasselbe heute anders " + + "behandelt wird als gestern. Ungleichbehandlung kostet mehr " + + "Vertrauen als jede zu harte Entscheidung." }, + { art: "richtlinie", titel: "Niemand moderiert allein gegen sich selbst", + text: "Wer betroffen ist, entscheidet nicht. Das schützt beide Seiten — " + + "und es ist der Grund, warum es ein Team gibt und keinen " + + "Alleinentscheider." }, + { art: "eskalation", titel: "Wann DogFather geholt wird", + text: "Bei allem, was über den Chat hinausgeht: Drohungen, rechtliche " + + "Fragen, Presse, Vorfälle mit Minderjährigen. Kein Modi muss das " + + "allein tragen, und keiner soll es." }, + { art: "eskalation", titel: "Vorfall mit einer minderjährigen Person", + text: "Sofort an DogFather, Inhalt sichern (Bildschirmfoto mit Uhrzeit), " + + "nichts löschen, bevor es gesichert ist. Gelöschtes lässt sich " + + "später nicht mehr melden." }, + { art: "gelernt", titel: "Nach jedem Vorfall: drei Zeilen", + text: "Was ist passiert, was wurde getan, was machen wir nächstes Mal " + + "anders. Ohne diesen Schritt passiert dasselbe in zwei Monaten " + + "wieder, mit denselben Überraschungen." }, + ], + + /* =================================================================== + IDEEN-BOARD — alles, was noch keinen Platz hat + =================================================================== */ + ideen: [ + { art: "content", titel: "Ein Kanal-Trailer für neue Zuschauer", + text: "Dreißig Sekunden: wer DogFather ist, was hier passiert, warum " + + "Casper mitspielt. Läuft auf allen drei Kanälen und beantwortet " + + "die Frage, die sonst jeder einzeln stellt." }, + { art: "live", titel: "Feste Sendezeiten statt „wenn es passt“", + text: "Wiederkehrende Zeiten schlagen spontane Streams, auch bei " + + "weniger Stunden. Die Frage „Wann seid ihr da?“ steht im Treff — " + + "die Antwort gehört von dort hierher." }, + { art: "community", titel: "Ein Willkommenstext, der automatisch kommt", + text: "Wer zum ersten Mal schreibt, bekommt einen Satz und einen Weg: " + + "Regeln, Treff, nächster Stream. Danach fragt niemand mehr, " + + "wie das hier läuft." }, + { art: "community", titel: "Highlights der Woche, jeden Sonntag", + text: "Drei Clips, drei Zeilen, ein Beitrag. Macht aus verstreuten " + + "Momenten eine Gewohnheit — und aus Zuschauern Leute, die sonntags " + + "vorbeischauen." }, + { art: "sonstiges", titel: "Was wir dieses Jahr nicht mehr anfangen", + text: "Genauso wichtig wie eine Ideenliste: eine Liste dessen, was " + + "bewusst liegen bleibt. Sonst steht alles gleichzeitig halb da." }, + ], + + /* =================================================================== + RÜCKMELDUNG — was das Team sagt + =================================================================== */ + rueckmeldung: [ + { art: "gut", titel: "Was in den letzten Wochen richtig gut lief", + text: "Eine Zeile reicht. Was hier steht, wird beibehalten — und man " + + "merkt erstaunlich oft erst beim Aufschreiben, dass etwas gut lief." }, + { art: "haken", titel: "Wo es gerade hakt — ohne Schuldige", + text: "Nicht „wer“, sondern „was“. Ein Ablauf, der nicht funktioniert, " + + "ist ein Ablauf-Problem, auch wenn ein Mensch daneben steht." }, + { art: "behalten", titel: "Das eine, das auf keinen Fall wegfallen darf", + text: "Bei jeder Umstellung geht etwas verloren, das niemand als wichtig " + + "angemeldet hatte. Hier ist die Stelle, es anzumelden." }, + { art: "dogfather", titel: "Was DogFather anders machen könnte", + text: "Direkt an ihn, ohne Umweg. Deutliche Kritik ist ausdrücklich " + + "erwünscht — sie ist der Grund, warum es diesen Bereich gibt." }, + { art: "team", titel: "Was dem Team fehlt, um besser zu arbeiten", + text: "Werkzeug, Absprachen, Zeiten, Zuständigkeiten. Was hier nicht " + + "steht, kann niemand ändern." }, + { art: "ablauf", titel: "Ein Ablauf, der zu viele Schritte hat", + text: "Wo dauert etwas länger, als es müsste? Das sind die Stellen, an " + + "denen man Zeit gewinnt, ohne dass irgendjemand schneller arbeitet." }, + { art: "zukunft", titel: "Wo soll das Ganze in einem Jahr stehen?", + text: "Ohne diese Antwort ist jede Einzelentscheidung Geschmackssache. " + + "Mit ihr sortiert sie sich fast von selbst." }, + ], + + /* =================================================================== + ANGEBOTE — was die Community DogFather anbietet + =================================================================== */ + angebote: [ + { art: "aktion", titel: "Gemeinsamer Community-Abend im Discord", + text: "Vorgeschlagen und organisiert aus der Community. DogFather kommt " + + "dazu, muss aber nichts vorbereiten — das ist der Punkt." }, + { art: "format", titel: "Ein Zuschauer stellt die Fragen", + text: "Jemand aus der Community führt einmal durch eine Fragerunde. " + + "Ungewohnt, macht aber aus Zuschauern Beteiligte." }, + { art: "technik", titel: "Hilfe beim Schneiden von Clips", + text: "Wer schneiden kann und Lust hat, meldet sich hier. Für DogFather " + + "Clips ist Schnittzeit der Engpass, nicht Material." }, + { art: "technik", titel: "Untertitel für die Videos", + text: "Viele schauen ohne Ton. Untertitel sind die billigste " + + "Reichweitenmaßnahme, die es gibt — und Fleißarbeit, die man teilen " + + "kann." }, + { art: "regel", titel: "Eine Regel, die aus der Community kommt", + text: "Wer mitliest, sieht Dinge, die von vorn niemand sieht. Regeln, " + + "die von unten kommen, werden auch eingehalten." }, + ], + + /* =================================================================== + AGENTUR — Spicy Media + =================================================================== */ + agentur: [ + { art: "kampagne", titel: "Was in den nächsten Wochen ansteht", + text: "Termine, Aktionen, Abgabefristen der Agentur an einer Stelle. " + + "Was hier nicht steht, wird vergessen — und zwar von allen " + + "gleichzeitig." }, + { art: "schulung", titel: "Einarbeitung für neue Modis", + text: "Was jemand am ersten Tag wissen muss: Regeln, Werkzeuge, wen man " + + "fragt. Einmal aufgeschrieben, spart es bei jedem Neuen dieselbe " + + "Stunde." }, + { art: "anliegen", titel: "Was wir von der Agentur brauchen", + text: "Offene Fragen an Spicy Media, gesammelt statt einzeln gestellt. " + + "Gebündelt bekommt man Antworten, verstreut bekommt man Rückfragen." }, + { art: "zustaendig", titel: "Wer ist wofür ansprechbar?", + text: "Namen zu Themen. Die häufigste Verzögerung im Alltag ist nicht " + + "fehlende Zeit, sondern die Frage, wen man eigentlich fragt." }, + ], +}; + +/** Alle Bereiche, für die es hier Vorschläge gibt. */ +export const ARBEIT_START_BEREICHE = Object.keys(ARBEIT_START); diff --git a/server/workspace-befinden.js b/server/workspace-befinden.js index 88463e01..c157d2db 100644 --- a/server/workspace-befinden.js +++ b/server/workspace-befinden.js @@ -141,8 +141,15 @@ function block() { return ENTWICKLUNG_BLOECKE.find((b) => b.nurSelbst) || null; } -/** Wann war die letzte vollständige Runde — und ist wieder Zeit? */ -function rhythmus(personId) { +/** Wann war die letzte vollständige Runde — und ist wieder Zeit? + * + * EXPORTIERT SEIT DEM 17.09.2026, damit die automatische Erinnerung + * (workspace-push.js) DIESELBE Rechnung benutzt. Eine zweite Formel + * daneben ("älter als 14 Tage") wäre die Falle, die in diesem Haus + * schon mehrfach zugeschlagen hat: Sie stimmt am Tag des Schreibens + * und läuft danach auseinander -- und zwar still, weil beide Seiten + * plausible Zahlen liefern. */ +export function rhythmus(personId) { const b = block(); const gesamt = (ENTWICKLUNG_PUNKTE[b.schluessel] || []).length; const letzte = db().prepare( @@ -341,6 +348,56 @@ export const MUSTER_AB = 2; * und stammt aus den Quellen oben. Ein Satz, der zu allen sechs passen * wuerde, gehoert hier nicht hinein. */ export const EINORDNUNG = { + /* ---- Die sechs vom 17.09.2026 ------------------------------------ */ + "e-ich-abschalten": { + welt: "Nicht die Menge der Arbeit sagt am besten voraus, ob jemand aufhört, " + + "sondern ob die Erholung dazwischen trägt. Wer abschalten kann, hält " + + "deutlich mehr aus als jemand mit der halben Schicht und ohne Pause.", + hilft: "Ein klares Ende ist wirksamer als eine kürzere Schicht: Meldungen aus, " + + "Chat zu, und niemand erwartet danach noch eine Antwort. Wer das nicht " + + "für sich selbst festlegen kann, braucht jemanden, der es festlegt.", + }, + "e-ich-sinn": { + welt: "Das Gefühl, etwas zu bewirken, gilt in Untersuchungen zu Ehrenamt als " + + "der stärkste Grund zu bleiben – stärker als Anerkennung und stärker " + + "als gute Stimmung im Team.", + hilft: "Es reicht oft, das Ergebnis überhaupt zu zeigen: Was ist durch die " + + "Arbeit der letzten Wochen tatsächlich anders geworden? Wer nur " + + "Aufgaben sieht und nie ein Ergebnis, arbeitet ins Leere.", + }, + "e-ich-klar-was-erwartet": { + welt: "Unklare Erwartungen sind einer der am besten belegten Auslöser für " + + "Erschöpfung – noch vor der Arbeitsmenge. Sie wirken leise, weil " + + "niemand sie als Problem benennt.", + hilft: "Drei Sätze reichen meistens: Was gehört dir, was ausdrücklich nicht, " + + "und wann ist es gut genug. Fehlt einer davon, füllt man ihn selbst – " + + "und zwar immer mit zu viel.", + }, + "e-ich-nicht-angefeindet": { + welt: "Anfeindungen außerhalb der Plattform sind bei Moderatoren verbreiteter " + + "als in der Öffentlichkeit bekannt, und sie sind der Punkt, an dem aus " + + "Belastung eine Gefahr wird.", + hilft: "Das ist kein Punkt für eine Umfrage, sondern für ein Gespräch – " + + "sofort. Nachrichten sichern (Bildschirmfoto mit Uhrzeit), nichts " + + "löschen, und niemand trägt das allein.", + }, + "e-ich-lerne-dazu": { + welt: "Ehrenamtliche bleiben deutlich länger, wenn die Tätigkeit ihnen " + + "erkennbar etwas mitgibt – eine Fähigkeit, eine Erfahrung, etwas, das " + + "auch außerhalb zählt.", + hilft: "Das entsteht selten von allein. Eine neue Aufgabe, ein Bereich zum " + + "Ausprobieren, ein Schnitt- oder Technikthema, das jemand übernimmt: " + + "Es kostet nichts und wirkt lange.", + }, + "e-ich-naechste-monate": { + welt: "Die Frage nach der eigenen Zukunft im Team ist der beste einzelne " + + "Frühindikator, den Befindensabfragen kennen. Sie schlägt an, bevor " + + "die anderen Fragen es tun.", + hilft: "Wenn hier etwas hakt, ist die Antwort nie eine Maßnahme, sondern ein " + + "Gespräch – und zwar eines, das fragt statt überzeugt. Wer schon " + + "innerlich gegangen ist, lässt sich nicht zurückargumentieren.", + }, + "e-ich-menge-passt": { welt: "„Zu wenig Zeit“ ist in Untersuchungen zu freiwilligen Moderatoren der " + "meistgenannte Grund fürs Aufhören – noch vor den Inhalten selbst.", diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 2367e64f..933d83ad 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -16,6 +16,25 @@ import express from "express"; import { TREFF_START } from "./workspace-treff-start.js"; +import { ARBEIT_START } from "./workspace-arbeit-start.js"; + +/* EIN KATALOG AUS ZWEI QUELLEN (17.09.2026) + + Die sieben Bretter des Treffs stehen in workspace-treff-start.js, + die elf Arbeitsbereiche in workspace-arbeit-start.js. Getrennte + Dateien, weil die Texte fuer voellig verschiedene Leser geschrieben + sind -- die einen fuer die Community, die anderen fuer das Team. + + HIER WERDEN SIE EINMAL ZUSAMMENGEFUEHRT und nicht an jeder der + beiden Abfragestellen einzeln. Zwei Stellen mit "das eine ODER das + andere" waeren zwei Stellen, an denen man beim naechsten Katalog + eine vergisst -- und die vergessene faellt nicht auf, weil sie + einfach nichts anzeigt. + + Ueberschneiden duerfen sich die Schluessel nicht; sollte es je + passieren, gewinnt der Treff (er ist oeffentlich, dort waere ein + falscher Text teurer). */ +const STARTKATALOG = { ...ARBEIT_START, ...TREFF_START }; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, externPruefen, externSql, @@ -25,7 +44,7 @@ import { import { nachrichtSchicken } from "./workspace-chat.js"; import { darfSchreiben, freigabeBedingung, entfernenVorbereiten, TREFF_TEAM_ROLLEN, - mitRollenname, + TREFF_FREIGABE_BRETTER, mitRollenname, } from "./workspace-treff.js"; export const bereicheRouter = express.Router(); @@ -906,7 +925,44 @@ function sichtbarRoh(person) { niemand: Ein leeres Brett sieht nicht nach Fehler aus. Genau so ist es am 01.09. dem Manager und am 09.09. dem Modi ergangen. */ if (TEAM_DOGI_ROLLEN.has(person.rolle)) { - const liste = [...TEAM_DOGI_ROLLEN].map((r) => `'${r}'`).join(", "); + /* UND DOGFATHER GEHOERT DAZU (17.09.2026). + + Filipe: "die community und alle anderen sollen die neuen sachen + in allen kategorien auch sehen bitte. in allen die sie sehen + soll auch jede aenderung mit gehen, immer nur in denen die ihnen + erlaubt ist zu sehen." + + GEMESSEN, und der Befund war groesser als der Anlass: + TEAM_DOGI_ROLLEN ist {hand, modi} -- ohne "admin". Die Bedingung + hier lautete damit woertlich "Eintraege von hand oder modi", und + DogFather fiel heraus. Folge: ALLES, was er in Live, Technik, + Community, Ideen, Angebote oder Rueckmeldung schreibt, war fuer + sein eigenes Team unsichtbar. + + Bereich DogFather rechte Hand Modi + live 8 0 0 + technik 8 0 0 + community 8 0 0 + ideen 5 0 0 + + Das ist nicht erst seit dem Startkatalog so -- der hat es nur + sichtbar gemacht, weil vorher ueberall Null stand und Null gleich + Null aussieht, egal aus welchem Grund. + + WARUM NICHT "admin" IN TEAM_DOGI_ROLLEN AUFNEHMEN: Diese Menge + entscheidet an anderer Stelle, WELCHES HAUS jemand bewohnt + (crew statt agentur) und was Spicy Media NICHT sehen darf. Ein + Eintrag dort haette DogFather stillschweigend ins andere Haus + verschoben. Die Liste wird deshalb genau hier erweitert, wo es + um Sichtbarkeit von Eintraegen geht -- und nirgends sonst. + + UND ES OEFFNET NICHTS: Welche Bereiche ein Modi ueberhaupt + aufrufen darf, entscheidet MODI_BEREICHE_ERLAUBT weiter oben + (content, schutz, agentur, entwicklung, talente bleiben 404). + Die Agentur-Trennung haengt an ohneAgentur() und AGENTUR_ROLLEN + -- dort steht "admin" nicht drin, an ihr aendert sich also + ebenfalls nichts. */ + const liste = [...TEAM_DOGI_ROLLEN, "admin"].map((r) => `'${r}'`).join(", "); return { wo: `(e.erstellt_von IN (SELECT id FROM personen WHERE rolle IN (${liste}))` + ` OR e.creator_id IN (SELECT id FROM personen WHERE rolle IN (${liste})))`, werte: [] }; } @@ -1582,7 +1638,7 @@ bereicheRouter.get("/workspace/api/bereich/:bereich/start", (req, res) => { /* Wer hier nicht schreiben darf, bekommt den Katalog gar nicht -- statt ihn zu bekommen und den Knopf ausgeblendet zu sehen. */ if (darfSchreiben(req.person, bereich)) return res.status(404).json({ fehler: "nicht_gefunden" }); - const liste = TREFF_START[bereich] || []; + const liste = STARTKATALOG[bereich] || []; /* Was schon dasteht, kommt nicht noch einmal. Verglichen wird ueber den Titel: Eine eigene Kennung an der Zeile waere eine zweite Wahrheit, und beim Umformulieren liefe sie auseinander. */ @@ -1606,7 +1662,7 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (r const warum = darfSchreiben(req.person, bereich); if (warum) return res.status(403).json({ fehler: warum }); - const liste = TREFF_START[bereich] || []; + const liste = STARTKATALOG[bereich] || []; if (!liste.length) return res.status(404).json({ fehler: "nicht_gefunden" }); const nr = req.body?.nr; @@ -1626,11 +1682,47 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (r Katalog eine Art in die Datenbank, die es dort nicht gibt, und die Zeile laesst sich hinterher nicht mehr aendern. */ if (!Object.hasOwn(einstellung.arten, v.art)) continue; - db().prepare(` + const { lastInsertRowid } = db().prepare(` INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von) VALUES (?,?,?,?,?, 'offen', ?, ?)`) .run(bereich, v.art, v.titel, v.text ?? null, jetzt().slice(0, 10), jetzt(), req.person.id); + + /* UND SOFORT FREIGEBEN (17.09.2026) -- der Fehler, den Filipe + auf seinem Bildschirm gesehen hat. + + Gemessen, BEVOR etwas gebaut wurde: DogFather uebernimmt bei + "Was ansteht" vier Vorschlaege und bei den Highlights drei. + Auf seinem Bildschirm stehen sie. Die Community sah: + + Brett DogFather rechte Hand Modi Community + ansteht 4 4 4 0 + highlight 3 3 3 0 + + Beide Bretter verlangen eine Freigabe, bevor jemand von + aussen etwas sieht -- und ein uebernommener Vorschlag hatte + keine. Filipe konnte also fuellen, so viel er wollte; fuer die + Community blieben genau die zwei Bretter leer, auf die es + ankommt. Und es waere niemandem aufgefallen, weil es auf + jedem Team-Bildschirm richtig aussieht. + + WARUM DAS KEINE UMGEHUNG DER FREIGABE IST: Die Freigabe fragt + "hat das Team diesen Text gesehen?". Bei einem uebernommenen + Vorschlag lautet die Antwort ja -- jemand aus dem Team hat ihn + ausgesucht und auf "Uebernehmen" gedrueckt. Das IST die + Entscheidung. Eine zweite daneben waere keine Sicherheit, + sondern ein Klick. + + NUR FUER TEAM DOGI, und die Bedingung fragt die ROLLE, nicht + den Weg. Duerfte eines Tages jemand von aussen den Katalog + uebernehmen, ginge sein Eintrag wie jeder andere zuerst durch + die Freigabe. */ + if (TREFF_FREIGABE_BRETTER.includes(bereich) + && TREFF_TEAM_ROLLEN.has(req.person?.rolle)) { + db().prepare( + "INSERT OR IGNORE INTO treff_freigaben (eintrag_id, zeitpunkt, von) VALUES (?,?,?)") + .run(Number(lastInsertRowid), jetzt(), req.person.id); + } angelegt++; } protokolliere("bereich_start_uebernommen", { diff --git a/server/workspace-entwicklungs-punkte.js b/server/workspace-entwicklungs-punkte.js index bc9582fa..66646075 100644 --- a/server/workspace-entwicklungs-punkte.js +++ b/server/workspace-entwicklungs-punkte.js @@ -78,7 +78,18 @@ export const ENTWICKLUNG_ERWARTUNG = [ text: "Das gilt für alle, auch in der Probezeit." }, { schluessel: "standard", name: "Nach ein paar Monaten", rang: 2, text: "Das ist normal, wenn jemand angekommen ist." }, - { schluessel: "senior", name: "Wofür man jemanden fragt", rang: 3, + /* DIE VIERTE STUFE (17.09.2026). Filipe: „wieso ist da immer noch + nicht perfektionniert wie bei den anderen mit fortgeschritten und + so". Die Checkliste nebenan hat Anfänger/Fortgeschritten/Profi/ + Meister; hier fehlte genau die Mitte — zwischen „läuft seit ein + paar Monaten" und „dafür fragt man jemanden" lag nichts. + + SIE IST KEINE NOTE, wie die anderen drei auch nicht. Es gibt + weiterhin keine Punktzahl, keinen Durchschnitt, keine Rangliste — + das bleibt der wichtigste Einzelentscheid dieses Bereichs. */ + { schluessel: "fort", name: "Fortgeschritten", rang: 3, + text: "Wer länger dabei ist, macht das von selbst – ohne dass es jemand anstößt." }, + { schluessel: "senior", name: "Wofür man jemanden fragt", rang: 4, text: "Das kann nicht jeder – und es wird auch nicht von jedem erwartet." }, ]; @@ -97,8 +108,21 @@ export const ENTWICKLUNG_BLOECKE = [ { schluessel: "selbst", name: "Wie viel von selbst", text: "Der Unterschied zwischen Ausführen und Mitdenken. Und – weiter unten – " + "zwischen Mitdenken und Überlastung." }, + /* ZWEI BLOECKE, DIE FILIPE AUSDRUECKLICH WOLLTE (17.09.2026): + „ich will auch dass es fortgeschrittene aufgaben gibt und + individuelle auch für kategorien wie videos schneide, + kommentieren, markierungen". Das war bisher nirgends abgebildet — + die vier alten Blöcke beschreiben Moderation, nicht Mitarbeit an + den Inhalten. */ + { schluessel: "inhalte", name: "Clips, Schnitt und Kommentare", + text: "Die Arbeit an den Videos selbst — für DogFather, DogFather Clips " + + "und HasiDog. Hier entscheidet sich, ob aus einem Stream etwas " + + "übrig bleibt." }, + { schluessel: "stream", name: "Während der Stream läuft", + text: "Was im Livestream passiert, lässt sich nicht nachholen. Dieser " + + "Block beschreibt die Arbeit, die genau dann stattfinden muss." }, { schluessel: "befinden", name: "Wie geht es dir damit?", nurSelbst: true, - text: "Diese sechs Fragen beantwortest nur du. Niemand aus dem Team sieht " + text: "Diese Fragen beantwortest nur du. Niemand aus dem Team sieht " + "deine Antworten – auch DogFather nicht. Sichtbar wird nur, ob es im " + "Team gerade jemandem zu viel wird, ohne Namen." }, ]; @@ -134,6 +158,29 @@ export const ENTWICKLUNG_PUNKTE = { { schluessel: "e-chat-weitergereicht", stufe: "senior", titel: "Neue werden weitergereicht, nicht nur bedient", text: "Aus einem beantworteten Neuen wird ein Mitglied, sobald er jemanden kennt." }, + { schluessel: "e-chat-ton-beim-zehnten-mal", stufe: "probe", + titel: "Dieselbe Frage wird auch beim zehnten Mal freundlich beantwortet", + text: "Für den, der fragt, ist es das erste Mal. Das ist der ganze Punkt." }, + { schluessel: "e-chat-niemand-blossgestellt", stufe: "probe", + titel: "Niemand wird im Chat bloßgestellt", + text: "Auch nicht jemand, der es verdient hätte. Wer zuschaut, merkt sich " + + "nicht den Anlass, sondern dass es passieren kann." }, + { schluessel: "e-chat-stammzuschauer-namen", stufe: "fort", + titel: "Stammzuschauer werden mit Namen angesprochen", + text: "Der Unterschied zwischen Publikum und Community — und er kostet " + + "nichts außer Aufmerksamkeit." }, + { schluessel: "e-chat-witz-erkannt", stufe: "fort", + titel: "Ein Witz wird als Witz erkannt, bevor daraus ein Verstoß wird", + text: "Die häufigste unnötige Eskalation. Wer den Chat kennt, hört den " + + "Unterschied — wer nur die Regel liest, nicht." }, + { schluessel: "e-chat-ohne-dogfather", stufe: "fort", + titel: "Der Chat läuft weiter, wenn DogFather kurz weg ist", + text: "Pause, Technik, Telefon. Was in diesen Minuten passiert, " + + "entscheidet, ob die Leute bleiben." }, + { schluessel: "e-chat-reguliert-sich", stufe: "senior", + titel: "Der Chat reguliert sich zunehmend selbst", + text: "Stammleute weisen einander zurecht, bevor ein Modi es tun muss. " + + "Das ist kein Zufall, das ist das Ergebnis von allem darüber." }, ], /* ---- Block 2: Wenn es eng wird ------------------------------------ */ @@ -162,6 +209,26 @@ export const ENTWICKLUNG_PUNKTE = { { schluessel: "e-druck-unpopulaer-getragen", stufe: "senior", titel: "Eine unpopuläre Entscheidung wird getragen, auch öffentlich", text: "Wer eine Regel nur durchsetzt, solange Applaus kommt, setzt sie nicht durch." }, + { schluessel: "e-druck-welle-ruhig", stufe: "standard", + titel: "Eine plötzliche Welle wird ruhig abgearbeitet", + text: "Ein Raid, ein viraler Clip, hundert Neue auf einmal. Nicht " + + "schneller werden — der Reihe nach bleiben." }, + { schluessel: "e-druck-erklaert-ohne-verteidigen", stufe: "fort", + titel: "Eine Entscheidung wird erklärt, ohne sie zu verteidigen", + text: "Erklären beendet die Sache, Verteidigen eröffnet sie. Der " + + "Unterschied liegt in einem Satz." }, + { schluessel: "e-druck-reihenfolge-nachvollziehbar", stufe: "fort", + titel: "Auch im Andrang bleibt nachvollziehbar, wer warum dran war", + text: "Wer sich übergangen fühlt, kommt nicht wieder — und sagt vorher " + + "nichts." }, + { schluessel: "e-druck-nachverfolgt", stufe: "fort", + titel: "Was eskaliert wurde, wird auch nachverfolgt", + text: "Eine Eskalation ohne Rückmeldung fühlt sich an wie Ignoriertwerden. " + + "Für beide Seiten." }, + { schluessel: "e-druck-team-aufgefangen", stufe: "senior", + titel: "Nach einem schweren Vorfall wird das Team aufgefangen", + text: "Wer Hässliches wegmoderiert, nimmt es mit nach Hause. Jemand muss " + + "danach fragen — von allein sagt es niemand." }, ], /* ---- Block 3: Im Team --------------------------------------------- */ @@ -191,6 +258,25 @@ export const ENTWICKLUNG_PUNKTE = { titel: "Widerspruch kommt auch mir gegenüber", text: "Ausdrücklich so formuliert: Ein Team, das dem Sprecher nicht " + "widerspricht, hat aufgehört zu denken. Hier steht niemand über dem anderen." }, + { schluessel: "e-team-lob-weitergegeben", stufe: "standard", + titel: "Lob wird weitergegeben, nicht nur Kritik", + text: "Kritik findet von allein ihren Weg nach oben. Lob nicht." }, + { schluessel: "e-team-schichten-getauscht", stufe: "fort", + titel: "Schichten werden untereinander getauscht, ohne dass jemand es organisieren muss", + text: "Ein Team, das das kann, braucht keine Einsatzplanung — nur einen " + + "Kalender." }, + { schluessel: "e-team-uebergabe-vollstaendig", stufe: "fort", + titel: "Eine Übergabe enthält, was die nächste Schicht wissen muss", + text: "Wer war auffällig, was steht offen, was ist gerade heikel. Drei " + + "Zeilen sparen der nächsten Schicht eine Stunde." }, + { schluessel: "e-team-uneinigkeit-entschieden", stufe: "fort", + titel: "Uneinigkeit endet mit einer Entscheidung, nicht mit Schweigen", + text: "Ein ungelöster Streit verschwindet nicht, er zieht um — in " + + "Privatnachrichten." }, + { schluessel: "e-team-regeln-mitentwickelt", stufe: "senior", + titel: "Regeln werden mitentwickelt, nicht nur befolgt", + text: "Wer mitliest, sieht Dinge, die von vorn niemand sieht. Regeln, die " + + "von dort kommen, werden auch eingehalten." }, ], /* ---- Block 4: Wie viel von selbst ---------------------------------- */ @@ -216,9 +302,115 @@ export const ENTWICKLUNG_PUNKTE = { text: "Steht bewusst ganz oben statt als Mangel: Nein sagen zu können ist " + "die schwerste Fähigkeit im Ehrenamt – und sie verhindert genau den " + "Ausfall, der sonst kommt." }, + { schluessel: "e-selbst-schicht-eingetragen", stufe: "probe", + titel: "Die eigene Schicht steht im Kalender, ohne Nachfrage", + text: "Ein Kalender, in dem die Hälfte fehlt, ist schlimmer als keiner — " + + "man plant dann auf falscher Grundlage." }, + { schluessel: "e-selbst-vorlage-gemacht", stufe: "fort", + titel: "Wiederkehrendes wird zur Vorlage, statt jedes Mal neu gemacht", + text: "Das Dritte Mal ist die Grenze. Was dreimal gleich war, wird beim " + + "vierten Mal nicht besser, nur länger." }, + { schluessel: "e-selbst-grenze-benannt", stufe: "fort", + titel: "Eine eigene Grenze wird benannt, bevor sie gerissen wird", + text: "Danach ist es keine Grenze mehr, sondern eine Erklärung." }, + { schluessel: "e-selbst-nicht-umgangen", stufe: "fort", + titel: "Was nicht funktioniert, wird angesprochen statt umgangen", + text: "Jeder Umweg wird irgendwann zum Weg — und dann fragt niemand mehr, " + + "warum er so lang ist." }, + { schluessel: "e-selbst-jemand-eingearbeitet", stufe: "senior", + titel: "Jemand wurde eingearbeitet, der es danach allein kann", + text: "Nicht gezeigt — übergeben. Das ist der Unterschied, und er zeigt " + + "sich erst, wenn man selbst nicht da ist." }, ], - /* ---- Block 5: Wie geht es dir damit? (nur die Person selbst) ------- */ + /* ---- Block 5: Clips, Schnitt und Kommentare ------------------------ + Filipes Wunschkategorien vom 17.09.2026, woertlich: „videos + schneide, kommentieren, markierungen". */ + inhalte: [ + { schluessel: "e-inhalt-momente-markiert", stufe: "probe", + titel: "Gute Momente werden markiert, während sie passieren", + text: "Uhrzeit notieren, fertig. Wer nachher sucht, findet nichts — " + + "daran scheitern die meisten Clips, nicht am Schneiden." }, + { schluessel: "e-inhalt-kommentare-gelesen", stufe: "probe", + titel: "Die Kommentare unter den Videos werden gelesen", + text: "Dort stehen die Fragen, die im Stream niemand gestellt hat — und " + + "die Themen des nächsten Videos." }, + { schluessel: "e-inhalt-geantwortet", stufe: "probe", + titel: "Auf Kommentare wird geantwortet, nicht nur reagiert", + text: "Ein Herz ist kein Gespräch. Eine Antwort bringt denselben Menschen " + + "beim nächsten Video wieder." }, + { schluessel: "e-inhalt-clip-ohne-anstoss", stufe: "standard", + titel: "Ein Clip entsteht, ohne dass DogFather ihn anstoßen muss", + text: "Das Material liegt ohnehin da. Was fehlt, ist meistens nur der " + + "Anfang." }, + { schluessel: "e-inhalt-richtiger-kanal", stufe: "standard", + titel: "Der Clip landet auf dem richtigen Kanal", + text: "DogFather für Streams, DogFather Clips für Ausschnitte, HasiDog " + + "für Casper. Auf dem falschen Kanal ist ein Video nicht falsch — es " + + "findet nur niemanden." }, + { schluessel: "e-inhalt-erste-sekunden", stufe: "fort", + titel: "Die ersten drei Sekunden eines Clips sitzen", + text: "Wer erst erklärt und dann zeigt, hat verloren. Das Interessanteste " + + "steht am Anfang, nicht in der Mitte." }, + { schluessel: "e-inhalt-cover-mitgeliefert", stufe: "fort", + titel: "Cover und Text kommen mit, nicht hinterher", + text: "Ein fertiger Clip ohne Cover ist kein fertiger Clip. Das Cover " + + "entscheidet, ob jemand überhaupt tippt." }, + { schluessel: "e-inhalt-kommentare-draussen", stufe: "fort", + titel: "Kommentare unter fremden Videos bringen Leute mit", + text: "Nicht Werbung — etwas Sinnvolles zum Thema. Das ist mühsam, " + + "wirkt aber besser als jeder Aufruf im eigenen Stream." }, + { schluessel: "e-inhalt-mehrere-clips", stufe: "senior", + titel: "Aus einem Stream entstehen mehrere Clips, ohne Rückfrage", + text: "Wenn sich ein Clip nicht in einem Satz zusammenfassen lässt, sind " + + "es zwei. Das ist keine Kürzung, sondern doppelt so viel." }, + { schluessel: "e-inhalt-gemessen-wiederholt", stufe: "senior", + titel: "Was gut lief, wird gemessen und wiederholt", + text: "Nicht das Gefühl — die Zahl daneben. Ein Format, das zweimal " + + "funktioniert hat, ist kein Zufall mehr." }, + ], + + /* ---- Block 6: Während der Stream läuft ----------------------------- */ + stream: [ + { schluessel: "e-stream-vorher-da", stufe: "probe", + titel: "Vor dem Start ist jemand im Chat", + text: "Die ersten Minuten entscheiden über die Stimmung der ganzen " + + "Sendung. Ein leerer Chat beim Start füllt sich schwer." }, + { schluessel: "e-stream-technik-gemeldet", stufe: "probe", + titel: "Technische Probleme werden sofort gemeldet", + text: "DogFather hört sich selbst nicht so, wie die Zuschauer ihn hören. " + + "Wer es merkt und nichts sagt, lässt die Sendung laufen." }, + { schluessel: "e-stream-fragen-gesammelt", stufe: "standard", + titel: "Fragen aus dem Chat werden gesammelt, damit sie drankommen", + text: "Im Trubel geht die Hälfte unter. Gesammelt sind sie der beste " + + "Anfang für den nächsten Stream." }, + { schluessel: "e-stream-mitgeschrieben", stufe: "standard", + titel: "Was auffällt, wird währenddessen notiert", + text: "Am nächsten Tag fehlt die Hälfte. Eine Zeile von gestern schlägt " + + "ein gutes Gedächtnis." }, + { schluessel: "e-stream-gelenkt", stufe: "fort", + titel: "Der Chat wird zurückgeholt, wenn er abdriftet", + text: "Nicht abwürgen — umlenken. Eine Frage an die Runde wirkt besser " + + "als eine Ermahnung." }, + { schluessel: "e-stream-wunsch-eingebracht", stufe: "fort", + titel: "Ein Wunsch aus dem Treff wird im Stream eingebracht", + text: "Die Wunschliste sortiert sich nach Stimmen. Wer den obersten " + + "einbringt, schließt den Kreis für den, der ihn geschrieben hat." }, + { schluessel: "e-stream-auswertung", stufe: "fort", + titel: "Nach dem Stream steht eine kurze Auswertung da", + text: "Wann sind Leute gegangen, was hat den Chat bewegt, was wird ein " + + "Clip. Fünf Minuten, und der nächste Stream fängt besser an." }, + { schluessel: "e-stream-laeuft-ohne", stufe: "senior", + titel: "Der Stream trägt auch eine Pause", + text: "Technik, Telefon, Casper. Wenn diese Minuten funktionieren, " + + "bleiben die Leute — sonst nicht." }, + { schluessel: "e-stream-format-entstanden", stufe: "senior", + titel: "Aus wiederkehrenden Fragen wird ein Format", + text: "Dieselbe Frage zum dritten Mal ist kein Ärgernis, sondern ein " + + "Themenvorschlag, den die Zuschauer selbst gemacht haben." }, + ], + + /* ---- Block 7: Wie geht es dir damit? (nur die Person selbst) ------- */ befinden: [ { schluessel: "e-ich-menge-passt", stufe: "probe", titel: "Die Menge passt gerade", @@ -240,6 +432,37 @@ export const ENTWICKLUNG_PUNKTE = { titel: "Ich habe das Gefühl, dass es auffällt, wenn ich da bin", text: "Fehlende Anerkennung ist der am häufigsten genannte emotionale Preis – " + "und der billigste zu beheben." }, + /* SECHS WEITERE (17.09.2026). Sie decken die Luecken, die die + ersten sechs offen lassen: Erholung, Sinn, Klarheit, Sicherheit + vor Anfeindung, Entwicklung und der Blick nach vorn. + + ALLE AUF "probe": Diese Fragen sind keine Stufenleiter. Sie + gelten am ersten Tag genauso wie im dritten Jahr -- ein + Befinden hat keine Erwartungsstufe. */ + { schluessel: "e-ich-abschalten", stufe: "probe", + titel: "Nach einer Schicht kann ich abschalten", + text: "Nicht ob du müde bist – ob es aufhört, wenn du aufhörst. " + + "Das ist der Unterschied zwischen anstrengend und zehrend." }, + { schluessel: "e-ich-sinn", stufe: "probe", + titel: "Ich habe das Gefühl, dass meine Arbeit hier etwas bewirkt", + text: "Wer nicht sieht, was seine Arbeit bewirkt, hört auf – auch wenn " + + "alles andere stimmt. Es ist der stillste Grund von allen." }, + { schluessel: "e-ich-klar-was-erwartet", stufe: "probe", + titel: "Mir ist klar, was von mir erwartet wird", + text: "Unklarheit fühlt sich nicht wie Unklarheit an, sondern wie " + + "das Gefühl, es nie ganz richtig zu machen." }, + { schluessel: "e-ich-nicht-angefeindet", stufe: "probe", + titel: "Ich werde außerhalb des Streams in Ruhe gelassen", + text: "Privatnachrichten, andere Plattformen, das Privatleben. Wer hier " + + "„da hakt es“ setzt, braucht keine Umfrage, sondern ein Gespräch." }, + { schluessel: "e-ich-lerne-dazu", stufe: "probe", + titel: "Ich lerne hier etwas, das mir auch sonst nützt", + text: "Ehrenamt hält, wenn es etwas zurückgibt. Das muss kein Geld sein – " + + "aber es muss etwas sein." }, + { schluessel: "e-ich-naechste-monate", stufe: "probe", + titel: "Ich kann mir gut vorstellen, in ein paar Monaten noch dabei zu sein", + text: "Die ehrlichste Frage der Liste. Wer hier zögert, hat es meistens " + + "vorher schon gewusst – und niemand hat gefragt." }, ], }; diff --git a/server/workspace-push.js b/server/workspace-push.js index 010cdb1a..d33eee35 100644 --- a/server/workspace-push.js +++ b/server/workspace-push.js @@ -58,6 +58,8 @@ import { gehoertAufDieseAdresse, TEAM_DOGI_ROLLEN, } from "./workspace.js"; import { schluesselErzeugen, schicken } from "./workspace-push-krypto.js"; +import { rhythmus as befindenRhythmus } from "./workspace-befinden.js"; +import { SEITEN } from "./rechte.js"; export const pushRouter = express.Router(); @@ -100,6 +102,20 @@ export const ARTEN = [ aufsteht. */ { schluessel: "tagesruf", name: "Einmal am Tag: was noch offen ist", vorgabe: false, wertVorgabe: "09:00", wertArt: "uhrzeit" }, + /* DIE BEFINDENS-ANFRAGE (17.09.2026). Filipe: "mach da mehr so eine + automatisierte anfrage regelmaessig." + + MIT `vorgabe: true`, anders als der Tagesruf: Sie antwortet nicht + auf ein Ereignis, aber sie kommt auch nicht taeglich -- alle 14 + Tage, und nur, wenn wirklich etwas offen ist. Und sie ist der + einzige Weg, auf dem jemand ueberhaupt erfaehrt, dass wieder + gefragt wird. Waere sie zum Einschalten, bekaeme sie genau der + nicht, der sie am noetigsten braucht. + + ABSCHALTEN GEHT NATUERLICH TROTZDEM -- wie bei jeder Art, in den + Einstellungen. Das ist der Unterschied zwischen einer Vorgabe und + einem Zwang. */ + { schluessel: "befinden", name: "Alle zwei Wochen: wie geht es dir?", vorgabe: true }, { schluessel: "test", name: "Probe-Benachrichtigung", vorgabe: true }, ]; const ARTEN_SCHLUESSEL = new Set(ARTEN.map((a) => a.schluessel)); @@ -555,6 +571,52 @@ export async function laufen() { }); } + /* --- WIE GEHT ES DIR? Alle zwei Wochen, von selbst --------------- + + Filipe (17.09.2026): "mach da mehr so eine automatisierte + anfrage regelmaessig." + + WER GEFRAGT WIRD, steht nicht hier: Es sind genau die Rollen, + die befinden.html ueberhaupt aufrufen duerfen -- und die stehen + in rechte.js. Eine eigene Liste hier waere eine zweite, und die + eine vergessene waere jemand, der nie gefragt wird, ohne dass es + auffaellt. (Genau so ist am 11.09. eine Rolle durch den Rost + gefallen.) + + OB FAELLIG, ENTSCHEIDET rhythmus() aus workspace-befinden.js -- + dieselbe Funktion, die auch die Seite selbst fragt. Nicht "aelter + als 14 Tage" nachgerechnet: Die echte Regel ist "alt genug ODER + nie fertig geworden", und die haette ich hier vergessen. + + HOECHSTENS EINMAL IN SIEBEN TAGEN, ueber das Merkmal mit der + Kalenderwoche. Ohne das kaeme die Erinnerung ab Tag 14 JEDEN Tag, + weil "faellig" faellig bleibt, bis jemand antwortet -- und eine + Erinnerung, die jeden Tag kommt, ist nach drei Tagen keine + Erinnerung mehr, sondern Laerm, den man abschaltet. */ + const befindenRollen = (SEITEN["/workspace/befinden.html"] || []); + if (befindenRollen.length) { + const platz = befindenRollen.map(() => "?").join(", "); + /* Die Woche als Nummer -- gerechnet aus dem Datum, nicht aus + einer Bibliothek: Gebraucht wird "hoechstens einmal je sieben + Tage", nicht die kalendarisch richtige Wochennummer. */ + const woche = Math.floor(Date.parse(heute + "T00:00:00Z") / (7 * 86400_000)); + for (const person of d.prepare( + `SELECT id, name FROM personen WHERE aktiv = 1 AND rolle IN (${platz})`) + .all(...befindenRollen)) { + let lage; + try { lage = befindenRhythmus(person.id); } catch { continue; } + if (!lage?.faellig) continue; + await senden(person.id, "befinden", { + titel: "Wie geht es dir?", + text: lage.letzte + ? "Die zwei Wochen sind um – es dauert zwei Minuten." + : "Zwölf Fragen über dich. Die Antworten sieht niemand außer dir.", + ziel: "/workspace/befinden.html", + merkmal: `befinden:${person.id}:${woche}`, + }); + } + } + /* Alte Merkmale aufraeumen -- nach 60 Tagen braucht sie niemand mehr, und die Tabelle soll nicht ewig wachsen. */ const grenze = new Date(Date.now() - 60 * 86400_000).toISOString(); diff --git a/server/workspace.js b/server/workspace.js index 9827c534..9e4a423f 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -849,7 +849,7 @@ const RECHTE_KACHEL = { einfacher -- sie tut nur noch eine Sache. */ const BEFINDEN_KACHEL = { gruppe: "Für dich", gruppeUnter: "Dein Platz im Team", - name: "Wie geht's dir?", unter: "Sechs Fragen – die Antworten sieht nur du", + name: "Wie geht's dir?", unter: "Ein paar Fragen – die Antworten sieht nur du", zeichen: "steckbrief", ton: 37, ziel: "befinden.html", szene: "lounge", }; diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index de3724f5..6a00991b 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609171249", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609171318", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609171249", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609171318", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171249", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171318", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171249", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171318", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index 89d7dc8d..04d5978e 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -203,6 +203,73 @@ .e-bloecke__alle:hover { color: var(--text); background: rgba(255, 255, 255, .05); } .e-bloecke__alle:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } +/* --------------------------------------------------------------------- + DIE STUFENLEITER (17.09.2026) + + Filipe: „wieso ist da immer noch nicht perfektionniert wie bei den + anderen mit fortgeschritten und so." + + GEDECKT UND OHNE AMPEL, aus demselben Grund, aus dem die Stufe am + einzelnen Punkt gedeckt ist (siehe .e-punkt__erwartung weiter + unten): Sie sagt, ab WANN etwas erwartet wird -- nicht, wie gut + jemand ist. Ein Farbverlauf von grün nach rot würde daraus eine + Note machen, und keine Note über einem Menschen ist der wichtigste + Einzelentscheid dieses ganzen Bereichs. + + Die gewählte Stufe bekommt deshalb mehr GEWICHT, nicht mehr Farbe. + ------------------------------------------------------------------- */ +.e-stufen { + display: flex; flex-wrap: wrap; align-items: center; gap: 6px; + /* Umgebrochen wird, wenn der Platz nicht reicht -- keine feste + Schwelle. Genau daran ist die Kopfleiste am 06.09.2026 zweimal + gescheitert: Eine Zahl, die zum sechsten Element nicht mehr + passte, und niemand hat sie nachgerechnet. */ + margin-inline-start: auto; +} +.e-stufen__knopf { + display: inline-flex; align-items: center; gap: 7px; + font: inherit; font-size: .78rem; font-weight: 600; + padding: 6px 12px; border-radius: 999px; cursor: pointer; + color: var(--text-still); + background: transparent; + border: 1px solid var(--rand); + transition: background .16s ease, color .16s ease, border-color .16s ease; +} +.e-stufen__knopf:hover { + color: var(--text-leise); + background: rgba(255, 255, 255, .04); + border-color: var(--rand-hell); +} +.e-stufen__knopf:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } +/* Die gewählte Stufe: heller Text, kräftigerer Rand -- kein Farbwechsel. */ +.e-stufen__knopf[aria-pressed="true"] { + color: var(--text); + border-color: var(--text-still); + background: rgba(255, 255, 255, .06); +} +.e-stufen__zahl { + /* .72rem = 11,5 px -- die Grenze, die pruef-css-klassen bewacht. + Darunter liest man Ziffern auf einem Handy nicht mehr, man ahnt + sie. Und diese Zahl ist der Grund, warum man überhaupt klickt: + Ein Filter, der auf eine leere Liste führt, sieht aus wie ein + Fehler. */ + font-size: .72rem; font-weight: 700; + min-width: 1.6em; padding: 1px 6px; + border-radius: 999px; text-align: center; + color: var(--text-still); + background: rgba(255, 255, 255, .06); +} +.e-stufen__knopf[aria-pressed="true"] .e-stufen__zahl { + color: var(--text-leise); + background: rgba(255, 255, 255, .1); +} + +/* Auf dem Handy gehört die Leiste unter den Knopf, nicht neben ihn -- + sonst quetschen sich fünf Knöpfe auf 40 % der Breite. */ +@media (max-width: 640px) { + .e-stufen { margin-inline-start: 0; width: 100%; } +} + .e-block { margin: 0 0 10px; border: 1px solid var(--rand); @@ -489,6 +556,64 @@ .t-leer p { margin: 0 0 12px; max-width: 54ch; margin-inline: auto; font-size: .88rem; line-height: 1.6; } .t-leer .schritt { text-decoration: none; } +/* --------------------------------------------------------------------- + DER LEERE ZUSTAND ARBEITET (17.09.2026) + + Filipe: „mach dass da fertige sachen sind, aber nicht basich sonder + hochwertig, profissionel bis zum letzten punkt verbunden." + + LINKSBÜNDIG STATT ZENTRIERT, sobald etwas zu lesen da ist: Eine + zentrierte Liste liest sich schlecht -- das Auge sucht bei jeder + Zeile den Anfang neu. Zentriert war richtig, solange dort ein + einziger Satz stand; jetzt steht dort eine Aufzählung. + ------------------------------------------------------------------- */ +.t-leer:has(.t-leer__merkmale) { text-align: start; } +.t-leer:has(.t-leer__merkmale) p { margin-inline: 0; } + +.t-leer__titel { + margin: 0 0 8px; + font-size: .95rem; font-weight: 700; letter-spacing: .01em; + color: var(--text); +} + +.t-leer__merkmale { + list-style: none; margin: 14px 0; padding: 0; + display: flex; flex-wrap: wrap; gap: 8px; +} +.t-leer__merkmal { + display: inline-flex; align-items: baseline; gap: 8px; + padding: 7px 13px; border-radius: 999px; + border: 1px solid var(--rand-hell); + background: rgba(255, 255, 255, .03); +} +.t-leer__merkmal-name { + font-size: .84rem; font-weight: 600; color: var(--text-leise); +} +.t-leer__merkmal-zahl { + /* .72rem = 11,5 px, die Grenze, die pruef-css-klassen bewacht. + Darunter erkennt man Ziffern nur noch wieder, statt sie zu + lesen. */ + font-size: .72rem; color: var(--text-still); +} + +.t-leer__weg { + /* Etwas abgesetzt: Das hier ist der Weg, nicht die Aufzählung. */ + padding-top: 12px; border-top: 1px solid var(--rand); +} + +.t-leer__knoepfe { + display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; +} +/* Der zweite Weg ist der leisere -- „Im Treff umsehen" ist ein + Angebot, „Jemanden eintragen" der Schritt. Zwei gleich laute Knöpfe + nebeneinander stellen eine Frage, statt einen Weg zu zeigen. */ +.schritt--leise { + color: var(--text-still); + border-color: var(--rand); + background: transparent; +} +.schritt--leise:hover { color: var(--text-leise); border-color: var(--rand-hell); } + /* DIE FUSSZEILE BRAUCHT DENSELBEN GRUND WIE ALLES ANDERE. Sie stand als einziger Text der Seite direkt auf der Bühne — dem diff --git a/workspace/assets/js/befinden.js b/workspace/assets/js/befinden.js index 82231b3c..c87111df 100644 --- a/workspace/assets/js/befinden.js +++ b/workspace/assets/js/befinden.js @@ -70,13 +70,16 @@ satz.textContent = offen ? `Noch ${offen} von ${alle} Fragen offen. Wenn du durch bist, halte den Stand fest – ` + `dann siehst du beim nächsten Mal, was sich verändert hat.` - : 'Alle sechs beantwortet. Halte den Stand fest, dann hast du beim nächsten Mal ' - + 'einen Vergleich.'; + /* GEZÄHLT STATT GESCHRIEBEN (17.09.2026): Der Block ist von sechs + auf zwölf Fragen gewachsen, und dieser Satz stand danach falsch + da. `alle` ist dieselbe Zahl, die eine Zeile höher schon steht. */ + : `Alle ${alle} beantwortet. Halte den Stand fest, dann hast du beim nächsten Mal ` + + `einen Vergleich.`; kasten.dataset.lage = offen ? 'offen' : 'bereit'; } else if (r.faellig) { satz.textContent = `Deine letzte Runde ist ${r.tage} Tage her. ` + (offen ? `Noch ${offen} von ${alle} Fragen offen.` - : 'Geh die sechs Fragen noch einmal durch – es dauert zwei Minuten.'); + : `Geh die ${alle} Fragen noch einmal durch – es dauert zwei Minuten.`); kasten.dataset.lage = 'faellig'; } else { const rest = daten.rhythmus_tage - r.tage; diff --git a/workspace/assets/js/entwicklung.js b/workspace/assets/js/entwicklung.js index 9f360cc0..05e473e5 100644 --- a/workspace/assets/js/entwicklung.js +++ b/workspace/assets/js/entwicklung.js @@ -40,6 +40,17 @@ } let stufen = []; + /* Welche Erwartungsstufe gerade gefiltert ist ('' = alle). + + ER LIEGT AUSSERHALB VON karte(), wie die Klappzustände: Beim + Neuzeichnen soll er nicht zurückspringen. Wer „Fortgeschritten" + gewählt hat und einen Punkt setzt, will danach immer noch + „Fortgeschritten" sehen. + + DIE VORGABE IST "ALLE". Ein Filter, der beim Öffnen schon etwas + versteckt, lässt einen Punkte suchen, die gestern noch da waren. + Wer filtert, hat es selbst getan und weiß es. */ + let stufenFilter = ''; /* WELCHE KATEGORIEN OFFEN STEHEN (15.09.2026). @@ -113,15 +124,23 @@ for (const b of r.bloecke) { const abschnitt = document.querySelector(`.e-block[data-block="${b.schluessel}"]`); if (!abschnitt) continue; - const n = b.punkte.filter((pt) => pt.meins).length; - const haktHier = b.punkte.filter((pt) => pt.meins?.stufe === 'hakt').length; + /* MIT DEMSELBEN FILTER WIE BEIM ZEICHNEN (17.09.2026). Ohne + diese drei Zeilen zieht der Zaehler nach jedem Klick auf die + UNGEFILTERTE Zahl zurueck -- der Kopf sagte dann "3 / 14", + waehrend vier Zeilen darunter stehen. Und man zweifelt an der + Zahl, nicht am Filter. */ + const punkte = stufenFilter + ? b.punkte.filter((pt) => pt.stufe === stufenFilter) + : b.punkte; + const n = punkte.filter((pt) => pt.meins).length; + const haktHier = punkte.filter((pt) => pt.meins?.stufe === 'hakt').length; const stand = abschnitt.querySelector('.e-block__stand'); if (stand) { - stand.textContent = `${n} / ${b.punkte.length}`; - if (n === b.punkte.length) stand.dataset.fertig = 'ja'; + stand.textContent = `${n} / ${punkte.length}`; + if (n === punkte.length) stand.dataset.fertig = 'ja'; else delete stand.dataset.fertig; } - if (n === b.punkte.length) abschnitt.dataset.fertig = 'ja'; + if (n === punkte.length) abschnitt.dataset.fertig = 'ja'; else delete abschnitt.dataset.fertig; const hakt = abschnitt.querySelector('.e-block__hakt'); if (hakt) { @@ -165,7 +184,7 @@ ziel.append(el('p', 'e-ampel__satz', r.sagbar ? r.text : r.grund)); if (r.sagbar) { ziel.append(el('p', 'e-ampel__zusatz', - `${r.geantwortet} von ${r.team} im Team haben die sechs Fragen schon beantwortet. ` + `${r.geantwortet} von ${r.team} im Team haben die Fragen schon beantwortet. ` + 'Namen stehen hier nicht – auch für dich nicht.')); } } @@ -360,19 +379,66 @@ einem Bildschirm laesst einen ueberlegen, ob es zwei verschiedene Zahlen sind. Gesehen habe ich das im Bildschirmfoto, nicht im Code. */ + /* DIE STUFENLEITER (17.09.2026). + + Sie wird aus r.erwartung GEBAUT und nicht hier aufgezählt -- + eine zweite Liste wäre die Falle, die in diesem Haus schon + mehrfach zugeschlagen hat: Kommt eine fünfte Stufe dazu, fehlt + sie genau an der Stelle, an der niemand nachsieht. + + Und jede Stufe sagt, wie viele Punkte sie hat. Ein Filter, der + auf eine leere Liste führt, sieht aus wie ein Fehler. */ + const stufenLeiste = el('div', 'e-stufen'); + stufenLeiste.setAttribute('role', 'group'); + stufenLeiste.setAttribute('aria-label', 'Nach Stufe filtern'); + const allePunkte = r.bloecke.flatMap((b) => b.punkte); + const stufenWahl = [{ schluessel: '', name: 'Alle', text: '' }, ...(r.erwartung || [])]; + for (const e of stufenWahl) { + const wieViele = e.schluessel + ? allePunkte.filter((pt) => pt.stufe === e.schluessel).length + : allePunkte.length; + const k = el('button', 'e-stufen__knopf'); + k.type = 'button'; + k.dataset.erwartung = e.schluessel; + k.setAttribute('aria-pressed', String(stufenFilter === e.schluessel)); + if (e.text) k.title = e.text; + k.append(el('span', 'e-stufen__name', e.name)); + k.append(el('span', 'e-stufen__zahl', String(wieViele))); + k.addEventListener('click', () => { + /* Ein zweiter Klick auf dieselbe Stufe hebt den Filter auf. + Sonst sucht man den Weg zurück zu „Alle" und findet ihn + nicht, weil man nicht mehr weiß, dass man gefiltert hat. */ + stufenFilter = (stufenFilter === e.schluessel) ? '' : e.schluessel; + karte(id, false); + }); + stufenLeiste.append(k); + } + leiste.append(stufenLeiste); ziel.append(leiste); for (const b of r.bloecke) { - const gesetzt = b.punkte.filter((pt) => pt.meins).length; + /* GEFILTERT WIRD HIER, EINMAL -- und alles darunter rechnet mit + derselben Liste. Stünde der Filter nur beim Zeichnen der + Zeilen, zeigte der Kopf weiterhin „3 / 14", während vier + Punkte darunter stehen. Genau diese Sorte Unstimmigkeit + lässt einen an der Zahl zweifeln statt am Filter. */ + const punkte = stufenFilter + ? b.punkte.filter((pt) => pt.stufe === stufenFilter) + : b.punkte; + /* EINE KATEGORIE OHNE TREFFER WIRD GANZ AUSGELASSEN. Ein + Abschnitt mit „0 / 0" ist kein Ergebnis, sondern ein + Hindernis zwischen den echten. */ + if (!punkte.length) continue; + const gesetzt = punkte.filter((pt) => pt.meins).length; const offen = offeneBloecke.has(b.schluessel); - const haktHier = b.punkte.filter((pt) => pt.meins?.stufe === 'hakt').length; + const haktHier = punkte.filter((pt) => pt.meins?.stufe === 'hakt').length; const abschnitt = el('section', 'e-block'); /* Der Schlüssel am Element -- daran findet zaehlerNachziehen() den richtigen Kopf wieder, ohne eine zweite Liste zu führen. */ abschnitt.dataset.block = b.schluessel; abschnitt.dataset.offen = offen ? 'ja' : 'nein'; - if (gesetzt === b.punkte.length) abschnitt.dataset.fertig = 'ja'; + if (gesetzt === punkte.length) abschnitt.dataset.fertig = 'ja'; const koerper = el('div', 'e-block__koerper'); koerper.id = `block-${b.schluessel}`; @@ -388,8 +454,8 @@ /* DER STAND STEHT IM KOPF, damit man ihn ZUGEKLAPPT sieht. Eine zugeklappte Kategorie, die nicht sagt, wie weit sie ist, klappt man einmal auf und danach nie wieder zu. */ - const stand = el('span', 'e-block__stand', `${gesetzt} / ${b.punkte.length}`); - if (gesetzt === b.punkte.length) stand.dataset.fertig = 'ja'; + const stand = el('span', 'e-block__stand', `${gesetzt} / ${punkte.length}`); + if (gesetzt === punkte.length) stand.dataset.fertig = 'ja'; schalter.append(stand); /* DER HAKT-HINWEIS STEHT IMMER DA, nur meistens verborgen: So kann er nachgezogen werden, ohne den Kopf neu zu bauen -- und @@ -422,7 +488,7 @@ /* Der erklärende Satz gehört in den Körper, nicht in den Kopf: Zugeklappt will man die Überschrift lesen, nicht einen Absatz. */ koerper.append(el('p', 'e-block__satz', b.text)); - for (const pt of b.punkte) koerper.append(punktZeile(id, pt, r.erwartung)); + for (const pt of punkte) koerper.append(punktZeile(id, pt, r.erwartung)); abschnitt.append(koerper); ziel.append(abschnitt); } diff --git a/workspace/assets/js/talente.js b/workspace/assets/js/talente.js index 07957b3a..b8e1f974 100644 --- a/workspace/assets/js/talente.js +++ b/workspace/assets/js/talente.js @@ -555,14 +555,55 @@ ziel.textContent = ''; ziel.setAttribute('aria-busy', 'false'); if (!lage.kandidaten.length) { + /* DER LEERE ZUSTAND ARBEITET (17.09.2026). + + Vorher stand hier ein Satz, der sagte, dass nichts da ist, + und ein Knopf, der woanders hinführt. Das ist keine + Auskunft, sondern eine Absage -- und genau der Eindruck, + den Filipe im Bildschirmfoto beschrieben hat. + + Jetzt steht da, WORAUF man achtet: die fünf Gruppen aus + lage.gruppen mit der Zahl ihrer Anzeichen -- 21 Stück, die + es längst gibt und die auf der leeren Seite nie jemand zu + sehen bekam. Abgeleitet, nicht abgeschrieben: Kommt eine + sechste Gruppe dazu, steht sie hier von selbst. + + UND ER BLEIBT EHRLICH: Es werden keine erfundenen Namen in + den Trichter gesetzt, damit er voll aussieht. Ein Talent ist + ein Mensch, kein Inhalt. */ const leer = el('div', 't-leer'); + leer.append(el('h3', 't-leer__titel', 'Wonach du suchst')); leer.append(el('p', null, - 'Noch niemand auf der Liste. Ein Talent entsteht als Eintrag im Brett ' - + '„Talente" – dort schreibst du auf, wem du etwas zutraust, und hier ' - + 'wird daraus ein Weg.')); - const a = el('a', 'schritt', 'Zum Brett „Talente"'); + 'Noch niemand auf der Liste – das ist der Normalzustand am Anfang. ' + + 'Ein Talent fällt zuerst im Chat auf, nicht in einer Bewerbung. ' + + 'Das hier sind die fünf Dinge, die zählen:')); + const merkmale = el('ul', 't-leer__merkmale'); + for (const g of lage.gruppen || []) { + const li = el('li', 't-leer__merkmal'); + li.dataset.art = g.schluessel; + li.append(el('span', 't-leer__merkmal-name', g.name)); + if (g.merkmale?.length) { + li.append(el('span', 't-leer__merkmal-zahl', + `${g.merkmale.length} Anzeichen`)); + } + merkmale.append(li); + } + if (merkmale.children.length) leer.append(merkmale); + leer.append(el('p', 't-leer__weg', + 'Wer dir auffällt, kommt als Eintrag ins Brett „Talente“. ' + + 'Von dort wird hier ein Weg daraus: aufgefallen, beobachtet, ' + + 'angesprochen, auf Probe – und am Ende im Team.')); + const knoepfe = el('div', 't-leer__knoepfe'); + const a = el('a', 'schritt', 'Jemanden eintragen'); a.href = 'bereich.html?b=talente'; - leer.append(a); + knoepfe.append(a); + /* DER WEG ZURÜCK ZUM TREFF. Dort fällt jemand auf -- das ist + der Anfang der Kette, und ohne ihn beginnt diese Seite im + Nichts. */ + const t = el('a', 'schritt schritt--leise', 'Im Treff umsehen'); + t.href = 'bereich.html?b=treff'; + knoepfe.append(t); + leer.append(knoepfe); ziel.append(leer); return; } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 603a931d..711249ba 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -291,10 +291,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 52dc5a49..a48160a6 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/befinden.html b/workspace/befinden.html index b8ac16c4..d19ced53 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -54,7 +54,7 @@

Für dich

Wie geht's dir?

- Sechs Fragen über dich. Niemand aus dem Team sieht deine Antworten – + Ein paar Fragen über dich. Niemand aus dem Team sieht deine Antworten – auch DogFather nicht.

@@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 73c513e2..9a9da10c 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -224,14 +224,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 081fde31..6e7d8217 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 8c0c1524..62a0bef3 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 40dc3156..1ccdc909 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index c0daca53..63a21f96 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 25ebb318..157068a7 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609171249", + "src": "/assets/img/app-symbole/crew-192.png?v=202609171318", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609171249", + "src": "/assets/img/app-symbole/crew-512.png?v=202609171318", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171249", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171318", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171249", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171318", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 57011101..16020f8c 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index 35cafd0c..8b7bbf76 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -95,16 +95,16 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index fa296ebb..6706b875 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index b63813ad..be770312 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index b00e5cd7..679b5c3e 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 69a4f6c5..e51e04a3 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index bcd1a2ca..372f5b11 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index a79494be..3c0290f2 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index d6f06cee..bda75ae0 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 5e8846f8..b0767097 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 6a1ea25f..de9cb8fe 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 57622f68..a1330fb0 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 603d6f79..be984230 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index 8fb5ec59..6ca94e87 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index bc44ce52..1fd6d919 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 2ed8bc6c..201fa783 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index e7488a4b..55be80ca 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -126,9 +126,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index dd0c63e0..199da40c 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -211,10 +211,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index b9f46828..657d3d3c 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 482ea970..39b72dcf 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +