Was DogFather eintraegt, sehen jetzt auch die anderen -- und 113 fertige Inhalte

Vier Bildschirmfotos, vier Auftraege. Vor dem Bauen gemessen
(server/mess-sichtbarkeit.mjs): alle Bereiche, alle vier Rollen.

ZWEI FEHLER, DIE NIEMAND GEMELDET HAETTE
Beide sahen auf DogFathers eigenem Bildschirm vollkommen richtig aus --
und das ist das Tueckische: Ein leeres Brett sieht nicht nach Fehler
aus, sondern nach "da ist eben noch nichts".

1. DIE FREIGABE. "Was ansteht" und "Highlights" zeigen der Community
   nur, was freigegeben ist. Ein aus dem Vorschlagskasten uebernommener
   Eintrag hatte keine Freigabe. Gemessen:

       Brett      DogFather  rechte Hand  Modi  Community
       ansteht        4           4         4        0
       highlight      3           3         3        0

   Filipe konnte fuellen, so viel er wollte -- fuer die Community
   blieben genau die zwei Bretter leer, auf die es ankommt. Wer aus dem
   Team uebernimmt, gibt jetzt zugleich frei: Die Freigabe fragt "hat
   das Team diesen Text gesehen?", und die Antwort ist beim Uebernehmen
   ja. Eine zweite Entscheidung daneben waere keine Sicherheit, sondern
   ein Klick.

2. TEAM_DOGI_ROLLEN IST {hand, modi} -- OHNE "admin". Die
   Sichtbarkeitsregel fuer Team Dogi lautete damit woertlich "Eintraege
   von hand oder modi". Folge: ALLES, was DogFather in Live, Technik,
   Community, Ideen, Angebote oder Rueckmeldung schreibt, war fuer sein
   eigenes Team unsichtbar. 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.

   Die Liste wird genau dort erweitert, wo es um Sichtbarkeit geht --
   NICHT in TEAM_DOGI_ROLLEN selbst: Diese Menge entscheidet auch,
   welches Haus jemand bewohnt. Ein Eintrag dort haette DogFather
   stillschweigend zur Crew umgezogen.

113 FERTIGE INHALTE IN DEN LEEREN KATEGORIEN
Gemessen waren elf Bereiche ausserhalb des Treffs komplett leer -- fuer
JEDE Rolle. Der Startkatalog kannte nur die sieben Treff-Bretter; die
Oberflaeche war die ganze Zeit bereit, es fehlten nur die Texte.
workspace-arbeit-start.js fuellt neun davon: Live, Content, Technik,
Community, Schutz, Ideen, Rueckmeldung, Angebote, Agentur.

"talente" und "entwicklung" bekommen ABSICHTLICH nichts: Dort waere ein
"fertiger Inhalt" ein erfundener Mensch -- ein Testdatensatz in einem
System, das echte Menschen bewertet. Die Talente-Seite bekommt ihre
Hilfe anders (siehe unten).

DER ENTWICKLUNGSKATALOG: 28 -> 68 PUNKTE, VIERTE STUFE
Filipe: "wieso ist da immer noch nicht perfektionniert wie bei den
anderen mit fortgeschritten und so, und VIIIIIEEEELLLLLL mehr aufgaben."
  * Neue Stufe "Fortgeschritten" zwischen "nach ein paar Monaten" und
    "wofuer man jemanden fragt" -- genau die Mitte fehlte.
  * Stufenleiter als FILTER ueber den Kategorien, wie die Checkliste es
    seit dem 10.09. vormacht. Aus r.erwartung gebaut, nicht aufgezaehlt.
  * Zwei neue Bloecke, die Filipe ausdruecklich wollte: "Clips, Schnitt
    und Kommentare" und "Waehrend der Stream laeuft" -- seine
    Wunschkategorien vom 17.09. ("videos schneide, kommentieren,
    markierungen").
  * Keine Note, keine Punktzahl, keine Rangliste. Unveraendert.

DAS BEFINDEN: 6 -> 12 FRAGEN, UND SIE MELDET SICH VON SELBST
Den 14-Tage-Rhythmus gab es schon -- er wirkte aber nur, WENN jemand die
Seite aufmachte. Wer sie vergisst, wurde nie wieder gefragt: Das war
eine Anzeige, keine Anfrage. Jetzt kommt sie ueber dasselbe Push-System
wie faellige Aufgaben, hoechstens einmal je sieben Tage, und sie fragt
rhythmus() aus workspace-befinden.js -- dieselbe Funktion wie die Seite,
keine zweite Formel daneben.
Sechs neue Fragen zu Erholung, Sinn, Klarheit, Anfeindung, Entwicklung
und dem Blick nach vorn, jede mit ihrer Einordnung.
Und an acht Stellen stand "sechs Fragen" ueber zwoelf Fragen -- wo ein
Skript den Text baut, wird jetzt gezaehlt; in den festen Seiten steht
gar keine Zahl mehr.

DIE TALENTE-SEITE, WENN NIEMAND DARAUF STEHT
Statt "Noch niemand auf der Liste" stehen dort jetzt die fuenf Gruppen
mit ihren 21 Anzeichen -- die gab es laengst, sie waren auf der leeren
Seite nur nie zu sehen. Dazu der Weg in beide Richtungen: woher ein
Talent kommt (der Treff) und wo es endet (die Entwicklung).

DIE PRUEFUNG (server/pruef-alle-sehen-es.mjs, 42 Pruefungen)
Sie prueft BEIDE Haelften von Filipes Satz. Nur zu zaehlen, ob alle
alles sehen, waere gruen, wenn man saemtliche Schranken entfernte --
Abschnitt 4 belegt, dass 19 Bereich-Rollen-Paare zu bleiben und die
Community an keinen der elf Arbeitsbereiche kommt.

ZWEI GEGENPROBEN, weil 42 von 42 im ersten Lauf kein Beweis ist:
  a) "admin" aus der Team-Sicht entfernen  -> 13 Pruefungen rot
  b) Freigabe beim Uebernehmen abschalten  ->  2 rot, genau ansteht
     und highlight
Jede trifft ihr Ziel und nicht alles.

UND EINE LUECKE IN EINER ALTEN PRUEFUNG
pruef-deutsche-texte war gruen -- und hatte die 113 neuen Texte, die 40
neuen Entwicklungspunkte und die 6 neuen Fragen nie gesehen: Sie stehen
in Dateien, die sie nicht kannte. Jetzt 391 statt 174 geprueften
Texten. (Meine erste Untergrenze stand auf 400 und war sofort rot --
geraten statt gemessen.)

Stempel 202609171318.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 13:18:45 +02:00
co-authored by Claude Opus 5
parent f05783ebc8
commit 8f0a184b9f
43 changed files with 1850 additions and 395 deletions
+157
View File
@@ -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);
+258
View File
@@ -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);
+27 -2
View File
@@ -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) {
+346
View File
@@ -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);
+59 -2
View File
@@ -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.",
+97 -5
View File
@@ -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", {
+226 -3
View File
@@ -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." },
],
};
+62
View File
@@ -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();
+1 -1
View File
@@ -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",
};