Modis verteilen Aufgaben statt sie zu bekommen -- und Angebote zur Entscheidung

Die drei Beurteilungs-Bereiche liefen bisher in die falsche Richtung: Sie
bewerteten die Modis. Filipe: "die modis sind ja da um mir zu helfen."
Also umgedreht.

WAS SICH GEDREHT HAT
- Alle 101 Punkte sind jetzt Beobachtungen ueber den Stream, nicht
  Pflichten des Modis ("Der Ton blieb verstaendlich" statt "Ton geprueft").
- Nur der Modi selbst drueckt auf seiner Liste. Wer sonst darauf zeigt,
  bekommt 403 und den Weg zur Team-Lage -- bewerten wird hier niemand.
- "Verbessern" landet als Eingang bei DogFather. Ein Klick macht daraus
  eine Aufgabe mit dem Satz des Modis im Text, oder eine Absage mit Grund.
  Beides schreibt eine Nachricht zurueck, damit der Modi sieht: angekommen.

ANGEBOTE
Neuer Bereich, in dem Modis planen und vorschlagen: Nutzen und Aufwand
statt Bewertung und Dringlichkeit, dazu ein Feld "was DogFather danach
tun muss". Wird ein Angebot angenommen, entstehen zwei Aufgaben -- eine
beim Modi zum Umsetzen, eine bei DogFather aus genau diesem Feld.

GEMESSEN
pruef-modi-checkliste  59 Pruefungen, 0 Fehler (neu geschrieben)
pruef-rollen          245 statt 244 -- der Zuwachs ist die neue
                      Angebote-Kachel, alle 11 Modi-Kacheln kommen an
pruef-modi-ideen       30, pruef-modi-verborgen 75, pruef-bereiche-lesend: gruen

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-10 12:52:45 +02:00
co-authored by Claude Opus 5
parent 6122ea6972
commit 05f262eb61
31 changed files with 1583 additions and 630 deletions
+162
View File
@@ -0,0 +1,162 @@
/* =====================================================================
Die eigene Adresse der Modi-App (10.09.2026)
crew.dogfather-universe.com
WARUM EINE EIGENE ADRESSE. Derselbe Grund, aus dem der Creator
Workspace am 06.09.2026 umgezogen ist (siehe workspace-umzug.js):
Auf einem Ursprung laesst sich genau EINE App installieren. Zwei
Manifeste mit verschachtelten Bereichen sind laut w3c/manifest
Nr. 1180 "strongly not recommended", und Chrome haelt sich daran --
der zweite Eintrag erscheint dann nur als Verknuepfung, nie als
eigene App. Filipe will fuer die Modis ausdruecklich eine eigene
App: "ich will dass es eine eigene app wird."
WAS SICH NICHT AENDERT: der Pfad /workspace/, der Dienst (4100), die
Datenbank, der Code. Getrennt wird ueber den HOSTNAMEN, nicht ueber
einen zweiten Prozess. Genau so machen es Hauptseite und Workspace
schon heute.
---------------------------------------------------------------------
DREI FALLEN, DIE MAN EINER BEDINGUNG NICHT ANSIEHT
---------------------------------------------------------------------
1. EIN MUSTER STATT EINER LISTE WAERE TOEDLICH.
/dogfather-universe\.com$/ passt auch auf
workspace.dogfather-universe.com -- und wuerde damit genau die
Adresse abschalten, die man schuetzen will. Deshalb hier zwei
feste Mengen und kein einziges Muster. (Dieselbe Ueberlegung
steht schon in workspace-umzug.js; sie hat dort einen Ausfall
verhindert und gilt hier unveraendert.)
2. "ALLES AUSSER crew." IST NICHT DASSELBE WIE "DIE ALTEN ADRESSEN".
Der naheliegende Satz waere: "Ein Modi darf ueberall rein, nur
nicht auf einer Adresse, die nicht crew heisst." Damit waeren
localhost, 127.0.0.1 und jede Pruefadresse eingeschlossen -- und
saemtliche Modi-Pruefungen wuerden ab sofort messen, dass ein
Modi nirgends hereinkommt, ohne dass irgendwo etwas rot wird.
Ein gruener Lauf, der nichts mehr prueft, ist schlimmer als ein
roter. Deshalb ist OHNE_MODI_HOSTS eine AUFZAEHLUNG der drei
echten Adressen. Wer eine vierte anlegt, traegt sie hier nach --
und pruef-crew-adresse.mjs sagt es ihm, wenn er es vergisst.
3. "FAENGT AN MIT /workspace" TRIFFT AUCH /workspaceXYZ.
Beim Workspace-Umzug waere das beinahe passiert. Hier gilt es
genauso, deshalb dieselbe Schreibweise: genau "/workspace" oder
alles unter "/workspace/", sonst nichts.
---------------------------------------------------------------------
WARUM DIE ALTE ADRESSE FUER MODIS ZUGEHT
---------------------------------------------------------------------
Ein Modi, der sich weiterhin auf workspace.dogfather-universe.com
anmelden koennte, haette zwei Anmeldungen, zwei Sitzungen und zwei
Apps auf dem Startbildschirm -- und der verborgene Zugang haette
damit eine zweite Spur genau dort, wo alle anderen arbeiten. Er
bekommt deshalb auf der alten Adresse KEINE Sitzung, sondern den
Hinweis auf die neue.
DASS DIESER HINWEIS DIE ADRESSE NENNT, IST KEIN LECK: Er erscheint
nur nach Eingabe eines gueltigen Modi-Codes. Wer den hat, ist Modi.
Fuer jeden anderen -- auch fuer jeden Manager mit gueltigem eigenen
Code -- veraendert sich an der alten Adresse ueberhaupt nichts.
---------------------------------------------------------------------
UND WARUM AUF DER NEUEN ADRESSE NUR MODIS HEREINKOMMEN
---------------------------------------------------------------------
Sonst waere crew. eine zweite, vollwertige Tuer in den Workspace --
und die erste Person, die sie zufaellig errät und ihren eigenen Code
eintippt, wuesste sofort, dass es hier noch etwas gibt. Ein fremder
Code bekommt dort deshalb dieselbe Antwort wie ein erfundener: die
Zugangswand sieht aus wie immer, und der Code stimmt nicht.
===================================================================== */
export const CREW_ADRESSE = "https://crew.dogfather-universe.com";
/** Die Adresse der Modi-App. Feste Menge, kein Muster -- Falle 1. */
const CREW_HOSTS = new Set(["crew.dogfather-universe.com"]);
/** Die echten Adressen, auf denen ein Modi nichts verloren hat.
* Aufzaehlung und ausdruecklich NICHT "alles ausser crew" -- Falle 2. */
const OHNE_MODI_HOSTS = new Set([
"dogfather-universe.com",
"www.dogfather-universe.com",
"workspace.dogfather-universe.com",
]);
/** Nur der Name, ohne Port und in Kleinschreibung.
* Hostnamen sind laut RFC 4343 gross/klein-unempfindlich; ein Browser
* darf "Workspace.Dogfather-Universe.com" schicken. */
function nurHost(hostKopfzeile) {
return String(hostKopfzeile || "").toLowerCase().split(":")[0];
}
/** Kommt die Anfrage ueber die Adresse der Modi-App? */
export function istCrewAdresse(hostKopfzeile) {
return CREW_HOSTS.has(nurHost(hostKopfzeile));
}
/** Ist das eine der Adressen, auf denen Modis nicht mehr arbeiten? */
export function istOhneModiAdresse(hostKopfzeile) {
return OHNE_MODI_HOSTS.has(nurHost(hostKopfzeile));
}
/** Gehoert dieser Pfad zum Workspace-Teil? Genau "/workspace" oder
* alles darunter -- aber NICHT /workspaceXYZ (Falle 3). */
export function istWorkspacePfad(pfad) {
const p = String(pfad || "");
return p === "/workspace" || p.startsWith("/workspace/");
}
/**
* Darf diese Person auf dieser Adresse angemeldet sein?
*
* Die eine Regel, aus der beide Richtungen folgen. Sie steht hier als
* eigene Funktion, damit sie ohne laufenden Server pruefbar ist -- und
* damit sie an GENAU EINER Stelle steht: sitzungLesen() in
* workspace.js. Jedes der 26 Fachmodule geht dort durch.
*
* @param {string} rolle Rolle der angemeldeten Person
* @param {string} hostKopfzeile Inhalt des Host-Kopffeldes
*/
export function sitzungPasstZurAdresse(rolle, hostKopfzeile) {
const modi = rolle === "modi";
if (istCrewAdresse(hostKopfzeile)) return modi; // crew: nur Modis
if (istOhneModiAdresse(hostKopfzeile)) return !modi; // echte alte Adressen: keine Modis
return true; // localhost, Pruefadressen: unberuehrt
}
/**
* Express-Zwischenschritt fuer die Adresse der Modi-App.
*
* Er macht zwei Dinge und ausdruecklich nichts mit Rechten -- die
* liegen in sitzungLesen():
*
* 1. NICHT INDIZIEREN. Die Adresse taucht heute in keinem oeffentlichen
* Zertifikatsprotokoll auf (nachgemessen am 10.09.2026: crt.sh
* kennt fuer dogfather-universe.com nur den Platzhalter-Eintrag).
* Eine Suchmaschine waere der zweite Weg, sie doch zu finden --
* der Kopf schliesst ihn. Er steht hier und nicht nur als
* <meta> im HTML, weil er dann auch fuer Bilder, Manifest und
* jede Antwort ohne HTML gilt.
*
* 2. NUR DER WORKSPACE. Auf crew. gibt es die Hauptseite nicht --
* sonst laege die komplette Dogfather-Website ein zweites Mal
* unter einer zweiten Adresse (doppelter Inhalt, und ein Weg,
* die Adresse ueber eine Suchmaschine zu finden). Alles ausserhalb
* von /workspace und /assets geht zur Zugangswand.
*
* Caddy tut Punkt 2 bereits mit derselben Weiche. Dass es hier ein
* zweites Mal steht, ist Absicht: Eine Schranke, die nur in einer
* Datei ausserhalb des Projekts existiert, faellt beim naechsten
* Serverumbau lautlos weg.
*/
export function crewWeiche(req, res, next) {
if (!istCrewAdresse(req.get("host"))) return next();
res.setHeader("X-Robots-Tag", "noindex, nofollow");
if (istWorkspacePfad(req.path) || req.path.startsWith("/assets/")) return next();
return res.redirect(302, "/workspace/");
}
+265 -126
View File
@@ -1,22 +1,24 @@
/* ===================================================================== /* =====================================================================
DIE CHECKLISTEN EINES MODIS DIE MODIS MELDEN -- und DogFather und VanVan antworten
Wunsch Filipe (10.09.2026): *"diese kategorien mussen auf dieser Wunsch Filipe (10.09.2026): *"diese 3 kategorien müssen so
seite auch noch auf die modis perfektionniert werden, da mussen eingestellt werden dass die modis mir quasi aufgaben verteilen, also
lauter sachen sein die vanvan und ich danach verteilen können und da das sie drücken können passt so oder verbesserungen. weil die modis
anpassen können ob gut oder nicht."* sind ja da um mir zu helfen."* Und dazu: *"Erst ein Vorschlag, den du
annimmst."*
VIER FRAGEN: DIE RICHTUNG WAR VORHER UMGEKEHRT, und diese Pruefung auch: Sie hat
geprueft, dass DogFather den Modi bewertet. Das war eine Note fuer
ihn statt einer Hilfe fuer DogFather -- und es widersprach der
Hausregel, nach der niemand ueber dem Team steht.
1. Bekommt ein Modi SEINEN Satz -- nicht den der Creator? FUENF FRAGEN:
2. Sieht er seine EIGENE Liste, ohne jemanden auswaehlen zu muessen?
3. Kann die DogFather-Rolle ihn auswaehlen und bewerten?
4. Bekommt Spicy Media ihn NICHT in die Auswahl?
Die vierte ist die heikelste. `darfCreator` sagt bei siehtAlles() 1. Beurteilt der Modi die SENDUNG -- und darf er druecken?
pauschal ja -- und darin steckt auch Spicy Media. Wer die 2. Duerfen DogFather und VanVan seinen Bericht NICHT ueberschreiben?
Modi-Auswahl daran haengt, oeffnet sie ihr nebenbei mit, ohne dass an 3. Landet ein "Verbessern" im Eingang der beiden?
der Stelle etwas davon steht. 4. Wird daraus eine Aufgabe -- und merkt der Modi, dass es ankam?
5. Bekommt Spicy Media von alledem nichts?
===================================================================== */ ===================================================================== */
import { mkdtempSync, rmSync } from "node:fs"; import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
@@ -69,7 +71,7 @@ const idSpicy = anlegen("Cigdem", "spicy", "CODE-SPIC-0001");
const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001"); const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001");
const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); const idModi = anlegen("Marina", "modi", "CODE-MODI-0001");
d.close(); d.close();
void idDogi; void idSpicy;
async function anmelden(rolle, code) { async function anmelden(rolle, code) {
const a = await fetch(BASIS + "/workspace/api/anmelden", { const a = await fetch(BASIS + "/workspace/api/anmelden", {
@@ -93,144 +95,281 @@ const vanAn = await anmelden("admin", "CODE-VAN-0001");
const spicyAn = await anmelden("spicy", "CODE-SPIC-0001"); const spicyAn = await anmelden("spicy", "CODE-SPIC-0001");
const creatAn = await anmelden("creator", "CODE-ANNA-0001"); const creatAn = await anmelden("creator", "CODE-ANNA-0001");
/* ---------- 1. Sein eigener Satz ------------------------------------- */ /* ---------- 1. Er beurteilt die Sendung ------------------------------ */
console.log("\n=== 1. Der Modi bekommt seinen eigenen Punktesatz ===\n"); console.log("\n=== 1. Der Modi beurteilt die Sendung ===\n");
const BEREICHE = ["live", "community", "technik"]; const BEREICHE = ["live", "community", "technik"];
let ersterPunkt = null; let ersterPunkt = null;
for (const b of BEREICHE) { for (const b of BEREICHE) {
const a = await hole(modiAn.keks, `/workspace/api/checkliste/${b}`); const a = await hole(modiAn.keks, `/workspace/api/checkliste/${b}`);
if (a.status !== 200) { unklar(`${b}: nicht lesbar (${a.status})`); continue; } if (a.status !== 200) { unklar(`${b}: nicht lesbar (${a.status})`); continue; }
const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []); const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []);
/* DIE ZAHL IN DIE BEDINGUNG: Eine leere Liste wuerde sonst als
"keine Creator-Punkte dabei" durchgehen. */
ok(punkte.length > 0 && punkte.every((p) => p.schluessel.startsWith("m-")), ok(punkte.length > 0 && punkte.every((p) => p.schluessel.startsWith("m-")),
`${b.padEnd(10)} ${punkte.length} Punkte, alle aus seinem Satz`); `${b.padEnd(10)} ${punkte.length} Punkte aus seinem Satz`);
if (b === "live" && punkte.length) ersterPunkt = punkte[0].schluessel; if (b === "live" && punkte.length) ersterPunkt = punkte[0].schluessel;
/* Und er sieht SEINE Liste, ohne jemanden auszuwaehlen. */ ok(a.daten?.creator_id === idModi, `${b.padEnd(10)} es ist seine eigene Liste`);
ok(a.daten?.creator_id === idModi, /* DAS IST DIE UMKEHRUNG: Er darf druecken. Vorher stand hier das
`${b.padEnd(10)} es ist seine eigene Liste (${a.daten?.creator_id})`); Gegenteil, und das war der Fehler. */
ok(a.daten?.darf_bewerten === false, ok(a.daten?.darf_bewerten === true,
`${b.padEnd(10)} bewerten darf er sich nicht selbst`); `${b.padEnd(10)} und er darf "Passt so" oder "Verbessern" drücken`);
} }
/* GEGENPROBE: Der Creator bekommt unveraendert seinen Satz -- und darf
/* GEGENPROBE: Ein Creator bekommt weiterhin SEINEN Satz -- sonst waere sich weiterhin NICHT selbst bewerten. Sonst waere oben auch gruen,
oben auch gruen, wenn ich allen denselben gegeben haette. */ wenn ich allen alles erlaubt haette. */
{ {
const a = await hole(creatAn.keks, "/workspace/api/checkliste/live"); const a = await hole(creatAn.keks, "/workspace/api/checkliste/live");
const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []); const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []);
ok(punkte.length > 0 && punkte.every((p) => !p.schluessel.startsWith("m-")), ok(punkte.length > 0 && punkte.every((p) => !p.schluessel.startsWith("m-")),
`der Creator bekommt unveraendert seinen Satz (${punkte.length} Punkte)`); `der Creator bekommt unverändert seinen Satz (${punkte.length} Punkte)`);
ok(a.daten?.darf_bewerten === false, "und darf sich weiterhin nicht selbst bewerten");
} }
/* ---------- 2. DogFather und VanVan waehlen ihn aus ------------------ */ /* ---------- 2. Sein Bericht gehört ihm ------------------------------- */
console.log("\n=== 2. DogFather und VanVan sehen ihn ===\n"); console.log("\n=== 2. Sein Bericht gehört ihm ===\n");
for (const [name, an] of [["DogFather", dogiAn], ["VanVan", vanAn]]) { for (const [name, an] of [["DogFather", dogiAn], ["VanVan", vanAn]]) {
const a = await hole(an.keks, "/workspace/api/checkliste/live"); const a = await hole(an.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`);
const drin = (a.daten?.creatoren || []).some((z) => z.id === idModi); const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []);
ok(drin, `${name.padEnd(10)} hat ihn in der Auswahl`); ok(punkte.length > 0, `${name.padEnd(10)} sieht seinen Bericht (${punkte.length} Punkte)`);
const b = await hole(an.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`); /* NICHT ueberschreiben. Duerften sie es, waere es kein Bericht mehr,
const punkte = (b.daten?.gruppen || []).flatMap((g) => g.punkte || []); sondern eine Abstimmung darueber, was er gesehen haben soll. */
ok(punkte.length > 0 && punkte.every((p) => p.schluessel.startsWith("m-")), ok(a.daten?.darf_bewerten === false,
`${name.padEnd(10)} bekommt SEINEN Satz, nicht den der Creator (${punkte.length})`); `${name.padEnd(10)} kann ihn aber nicht überschreiben`);
ok(b.daten?.darf_bewerten === true, `${name.padEnd(10)} darf bewerten`); const b = await hole(an.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, {
}
/* ---------- 3. Bewerten ---------------------------------------------- */
console.log("\n=== 3. Gut oder verbessern ===\n");
if (!ersterPunkt) {
unklar("ohne Punkt laesst sich das Bewerten nicht pruefen");
} else {
const a = await hole(dogiAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, {
method: "PUT", headers: jsonKopf,
body: JSON.stringify({ stufe: "gut", begruendung: "sauber gemacht", creator_id: idModi }),
});
ok(a.status === 200, `DogFather setzt "gut" (${a.status})`);
const b = await hole(modiAn.keks, "/workspace/api/checkliste/live");
ok(b.daten?.stand?.[ersterPunkt]?.stufe === "gut",
`der Modi sieht den Stand (${b.daten?.stand?.[ersterPunkt]?.stufe})`);
const c = await hole(vanAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, {
method: "PUT", headers: jsonKopf,
body: JSON.stringify({ stufe: "verbessern", begruendung: "beim nächsten Mal früher", creator_id: idModi }),
});
ok(c.status === 200, `VanVan setzt "verbessern" (${c.status})`);
/* Und er selbst kann es NICHT umstellen -- sonst waere die Bewertung
eine Selbsteinschaetzung. */
const e = await hole(modiAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, {
method: "PUT", headers: jsonKopf, body: JSON.stringify({ stufe: "gut" }),
});
ok(e.status === 403, `er selbst kann sie nicht umstellen (${e.status})`);
}
/* ---------- 4. Spicy Media geht das nichts an ------------------------ */
console.log("\n=== 4. Spicy Media bekommt ihn nicht ===\n");
{
const a = await hole(spicyAn.keks, "/workspace/api/checkliste/live");
const drin = (a.daten?.creatoren || []).some((z) => z.id === idModi);
ok(!drin,
`nicht in der Auswahl (${(a.daten?.creatoren || []).length} Personen: `
+ `${(a.daten?.creatoren || []).map((z) => z.name).join(", ")})`);
/* GEGENPROBE: Den Creator hat sie sehr wohl -- sonst waere der Haken
darueber auch dann gruen, wenn die Auswahl ueberhaupt leer bliebe. */
ok((a.daten?.creatoren || []).some((z) => z.id === idCreat),
"den Creator dagegen schon");
/* Und ueber die Adresse auch nicht. Eine Regel, die nur in der
Auswahlliste gilt, ist keine. */
const b = await hole(spicyAn.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`);
ok(b.status === 404, `und ueber die Adresse ebenfalls nicht (${b.status})`);
const c = await hole(spicyAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, {
method: "PUT", headers: jsonKopf, method: "PUT", headers: jsonKopf,
body: JSON.stringify({ stufe: "gut", creator_id: idModi }), body: JSON.stringify({ stufe: "gut", creator_id: idModi }),
}); });
ok(c.status === 404, `bewerten erst recht nicht (${c.status})`); ok(b.status === 403 && /Team-Lage/.test(b.daten?.fehler || ""),
`${name.padEnd(10)} und wird auf die Team-Lage verwiesen (${b.status})`);
} }
/* ---------- 3. Er meldet etwas --------------------------------------- */
/* ---------- 5. Die gemeinsame Lage ----------------------------------- */ console.log("\n=== 3. Er drückt „Verbessern“ ===\n");
console.log("\n=== 5. Die Seite für DogFather und VanVan ===\n");
{ {
const a = await hole(modiAn.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, {
method: "PUT", headers: jsonKopf,
body: JSON.stringify({ stufe: "verbessern", begruendung: "Kam erst zehn Minuten vorher" }),
});
ok(a.status === 200, `die Meldung geht durch (${a.status})`);
for (const [name, an] of [["DogFather", dogiAn], ["VanVan", vanAn]]) { for (const [name, an] of [["DogFather", dogiAn], ["VanVan", vanAn]]) {
const a = await hole(an.keks, "/workspace/api/teamlage"); const b = await hole(an.keks, "/workspace/api/teamlage");
if (a.status !== 200) { unklar(`${name}: nicht lesbar (${a.status})`); continue; } const drin = (b.daten?.eingang || []).find((e) => e.schluessel === ersterPunkt);
const eintrag = (a.daten?.modis || []).find((m) => m.id === idModi); ok(!!drin, `${name.padEnd(10)} hat sie im Eingang`);
ok(!!eintrag, `${name.padEnd(10)} sieht den Modi in der Lage`); ok(drin?.begruendung === "Kam erst zehn Minuten vorher",
/* Die Zahlen muessen zum Punktesatz passen -- eine Uebersicht, die `${name.padEnd(10)} mit seinem Satz dabei ("${drin?.begruendung || ""}")`);
andere Zahlen zeigt als die Seite dahinter, ist schlimmer als ok(!!drin?.titel && drin.titel !== ersterPunkt,
keine. */ `${name.padEnd(10)} und dem Titel, nicht dem Schlüssel ("${drin?.titel || ""}")`);
ok(eintrag?.bereiche?.live?.gesamt === 40,
`${name.padEnd(10)} und die Gesamtzahl stimmt (${eintrag?.bereiche?.live?.gesamt})`);
/* Ein Punkt wurde in Abschnitt 3 auf "verbessern" gesetzt. */
ok(eintrag?.bereiche?.live?.verbessern === 1
&& eintrag?.bereiche?.live?.offen === 39,
`${name.padEnd(10)} der bewertete Punkt ist abgezogen `
+ `(${eintrag?.bereiche?.live?.verbessern} verbessern, ${eintrag?.bereiche?.live?.offen} offen)`);
/* Und die Kachel dorthin bekommt sie auch. */
const b = await hole(an.keks, "/workspace/api/ich");
const kachel = (b.daten?.bereiche_zusatz || []).some((k) => k.ziel === "teamlage.html");
ok(kachel, `${name.padEnd(10)} hat die Kachel auf der Startseite`);
} }
/* Und "Passt so" landet NICHT im Eingang -- sonst waere er nach einer
Sendung voll mit Lob, und das Wichtige ginge unter. */
const zweiter = await hole(modiAn.keks, "/workspace/api/checkliste/live");
const p2 = (zweiter.daten?.gruppen || []).flatMap((g) => g.punkte || [])[1]?.schluessel;
await hole(modiAn.keks, `/workspace/api/checkliste/live/${p2}`, {
method: "PUT", headers: jsonKopf, body: JSON.stringify({ stufe: "gut" }),
});
const c = await hole(dogiAn.keks, "/workspace/api/teamlage");
ok(!(c.daten?.eingang || []).some((e) => e.schluessel === p2),
"„Passt so“ landet nicht im Eingang");
} }
/* ---------- 6. Für alle anderen gibt es sie nicht -------------------- */ /* ---------- 4. Daraus wird eine Aufgabe ------------------------------ */
console.log("\n=== 6. Und für alle anderen gibt es sie nicht ===\n"); console.log("\n=== 4. Daraus wird eine Aufgabe ===\n");
for (const [name, an] of [["Spicy Media", spicyAn], ["Creator", creatAn], ["Modi", modiAn]]) {
const a = await hole(an.keks, "/workspace/api/teamlage");
const b = await hole(an.keks, "/workspace/api/gibtesnicht");
ok(a.status === 404,
`${name.padEnd(12)} bekommt 404 -- wie bei einer Adresse, die es nicht gibt (${a.status}/${b.status})`);
const c = await hole(an.keks, "/workspace/api/ich");
ok((c.daten?.bereiche_zusatz || []).length === 0,
`${name.padEnd(12)} bekommt auch die Kachel nicht`);
}
/* GEGENPROBE ZUR KACHEL: Sie ist ueberhaupt vorhanden -- sonst waeren
die drei Haken darueber auch dann gruen, wenn es sie gar nicht gibt. */
{ {
const a = await hole(dogiAn.keks, "/workspace/api/ich"); const a = await hole(dogiAn.keks, "/workspace/api/teamlage/behandeln", {
ok((a.daten?.bereiche_zusatz || []).length === 1, method: "POST", headers: jsonKopf,
`bei DogFather ist genau eine Zusatzkachel da (${(a.daten?.bereiche_zusatz || []).length})`); body: JSON.stringify({
bereich: "live", schluessel: ersterPunkt, person_id: idModi, art: "aufgabe",
}),
});
ok(a.status === 200 && a.daten?.aufgabe_id, `angenommen (${a.status})`);
const b = await hole(dogiAn.keks, "/workspace/api/aufgaben");
const neu = (b.daten?.aufgaben || []).find((x) => x.id === a.daten?.aufgabe_id);
ok(neu?.verantwortlich_id === idDogi, "die Aufgabe gehört dem, der gedrückt hat");
ok(/zehn Minuten vorher/.test(neu?.beschreibung || ""),
"der Satz des Modis steht in der Aufgabe");
/* Aus dem Eingang verschwunden -- ein Eingang, der nie leer wird,
wird nicht mehr angesehen. */
const c = await hole(dogiAn.keks, "/workspace/api/teamlage");
ok(!(c.daten?.eingang || []).some((e) => e.schluessel === ersterPunkt),
"und ist aus dem Eingang verschwunden");
/* UND DER MODI MERKT ES. Eine Meldung, auf die nichts folgt, sieht
fuer ihn aus wie eine, die niemand gelesen hat. */
const e = await hole(modiAn.keks,
`/workspace/api/checkliste/live/${ersterPunkt}/nachrichten`);
const texte = (e.daten?.nachrichten || e.daten || []).map((z) => z.text || "");
ok(texte.some((t) => /Aufgabe geworden/.test(t)),
`er sieht die Antwort an seinem Punkt (${texte.length} Nachricht(en))`);
/* Ein zweites Mal geht nicht -- sonst entstuenden zwei Aufgaben aus
einer Meldung. */
const f = await hole(dogiAn.keks, "/workspace/api/teamlage/behandeln", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({
bereich: "live", schluessel: ersterPunkt, person_id: idModi, art: "aufgabe",
}),
});
ok(f.status === 404, `ein zweites Mal geht nicht (${f.status})`);
}
/* ---------- 5. Oder eine begründete Absage --------------------------- */
console.log("\n=== 5. Oder eine begründete Absage ===\n");
{
const liste = await hole(modiAn.keks, "/workspace/api/checkliste/community");
const punkt = (liste.daten?.gruppen || []).flatMap((g) => g.punkte || [])[0]?.schluessel;
await hole(modiAn.keks, `/workspace/api/checkliste/community/${punkt}`, {
method: "PUT", headers: jsonKopf,
body: JSON.stringify({ stufe: "verbessern", begruendung: "Regeln standen weit unten" }),
});
/* OHNE GRUND KEINE ABSAGE. Wer nichts dazu schreibt, laesst den Modi
genauso ratlos zurueck wie Schweigen -- nur ist die Meldung dann
auch noch aus dem Eingang weg. */
const ohne = await hole(vanAn.keks, "/workspace/api/teamlage/behandeln", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({
bereich: "community", schluessel: punkt, person_id: idModi, art: "abgelehnt",
}),
});
ok(ohne.status === 400, `eine Absage ohne Grund wird abgelehnt (${ohne.status})`);
const mit = await hole(vanAn.keks, "/workspace/api/teamlage/behandeln", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({
bereich: "community", schluessel: punkt, person_id: idModi,
art: "abgelehnt", grund: "Steht schon oben, wir lassen es so",
}),
});
ok(mit.status === 200 && !mit.daten?.aufgabe_id,
`mit Grund geht sie durch und legt keine Aufgabe an (${mit.status})`);
const e = await hole(modiAn.keks,
`/workspace/api/checkliste/community/${punkt}/nachrichten`);
const texte = (e.daten?.nachrichten || e.daten || []).map((z) => z.text || "");
ok(texte.some((t) => /lassen es so/.test(t)), "der Grund kommt beim Modi an");
}
/* ---------- 6. Spicy Media bekommt nichts davon ---------------------- */
console.log("\n=== 6. Spicy Media bekommt nichts davon ===\n");
{
const a = await hole(spicyAn.keks, "/workspace/api/checkliste/live");
const drin = (a.daten?.creatoren || []).some((z) => z.id === idModi);
ok(!drin, `nicht in der Auswahl (${(a.daten?.creatoren || []).map((z) => z.name).join(", ")})`);
ok((a.daten?.creatoren || []).some((z) => z.id === idCreat), "den Creator dagegen schon");
const b = await hole(spicyAn.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`);
ok(b.status === 404, `und über die Adresse ebenfalls nicht (${b.status})`);
const c = await hole(spicyAn.keks, "/workspace/api/teamlage");
ok(c.status === 404, `die Team-Lage bekommt sie nicht (${c.status})`);
const e = await hole(spicyAn.keks, "/workspace/api/teamlage/behandeln", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ bereich: "live", schluessel: ersterPunkt, person_id: idModi, art: "aufgabe" }),
});
ok(e.status === 404, `und behandeln erst recht nicht (${e.status})`);
}
/* ---------- 7. Die Kachel -------------------------------------------- */
console.log("\n=== 7. Die Kachel für euch beide ===\n");
for (const [name, an, soll] of [
["DogFather", dogiAn, 1], ["VanVan", vanAn, 1],
["Spicy Media", spicyAn, 0], ["Creator", creatAn, 0], ["Modi", modiAn, 0],
]) {
const a = await hole(an.keks, "/workspace/api/ich");
const n = (a.daten?.bereiche_zusatz || []).length;
ok(n === soll, `${name.padEnd(12)} bekommt ${n} Zusatzkachel(n), erwartet ${soll}`);
}
/* ---------- 8. Angebote ---------------------------------------------- */
console.log("\n=== 8. Ein Angebot an DogFather ===\n");
{
/* Der Modi schlaegt etwas vor -- mit Nutzen, Aufwand und dem, was
DogFather dafuer tun muesste. */
const a = await hole(modiAn.keks, "/workspace/api/bereich/angebote", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({
art: "aktion", titel: "Zuschauer-Challenge am Freitag",
text: "Drei Runden, Gewinner bekommt einen Shoutout.",
einsatz: "Im Live ansagen und die Regeln vorlesen",
bewertung: 4, dringlichkeit: "niedrig",
}),
});
ok(a.status === 201, `der Modi legt ein Angebot an (${a.status})`);
/* Es steht im Eingang von DogFather und VanVan -- mit dem, was sein
Teil waere. Ohne das muesste er entscheiden, ohne ihn zu kennen. */
const b = await hole(dogiAn.keks, "/workspace/api/teamlage");
const ang = (b.daten?.angebote || [])[0];
ok(!!ang, `es steht in der Team-Lage (${(b.daten?.angebote || []).length})`);
ok(ang?.einsatz === "Im Live ansagen und die Regeln vorlesen",
`mit seinem Teil dabei ("${ang?.einsatz || ""}")`);
ok(ang?.bewertung === 4 && ang?.dringlichkeit === "niedrig",
`mit Nutzen ${ang?.bewertung} und Aufwand ${ang?.dringlichkeit}`);
/* Und Spicy Media sieht davon nichts. */
const c = await hole(spicyAn.keks, "/workspace/api/bereich/angebote");
ok(c.status === 404, `Spicy Media kommt nicht an die Angebote (${c.status})`);
/* ANNEHMEN: zwei Aufgaben -- der Modi setzt um, DogFather bekommt
seinen Teil. Genau das war Filipes Wunsch. */
const e = await hole(dogiAn.keks, "/workspace/api/teamlage/angebot", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ id: ang.id, art: "angenommen" }),
});
ok(e.status === 200 && e.daten?.aufgaben === 2,
`angenommen, ${e.daten?.aufgaben} Aufgaben angelegt`);
const f = await hole(dogiAn.keks, "/workspace/api/aufgaben");
const alle = f.daten?.aufgaben || [];
ok(alle.some((x) => x.titel === "Umsetzen: Zuschauer-Challenge am Freitag"
&& x.verantwortlich_id === idModi), "der Modi setzt es um");
ok(alle.some((x) => x.titel === "Im Live ansagen und die Regeln vorlesen"
&& x.verantwortlich_id === idDogi), "und DogFather bekommt seinen Teil");
/* Aus dem Eingang verschwunden, und ein zweites Mal geht nicht. */
const g = await hole(dogiAn.keks, "/workspace/api/teamlage");
ok((g.daten?.angebote || []).length === 0, "aus dem Eingang verschwunden");
const h = await hole(dogiAn.keks, "/workspace/api/teamlage/angebot", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ id: ang.id, art: "angenommen" }),
});
ok(h.status === 404, `ein zweites Mal geht nicht (${h.status})`);
/* Und der Modi sieht die Antwort an seinem Angebot. */
const i = await hole(modiAn.keks, "/workspace/api/bereich/angebote");
const seins = (i.daten?.eintraege || i.daten?.liste || []).find((z) => z.id === ang.id);
ok(/Angenommen von/.test(seins?.text || ""),
"der Modi sieht, dass es angenommen wurde");
ok(seins?.status === "angenommen", `und der Zustand steht (${seins?.status})`);
}
/* ---------- 9. Ein Angebot ablehnen ---------------------------------- */
console.log("\n=== 9. Ein Angebot ablehnen ===\n");
{
const a = await hole(modiAn.keks, "/workspace/api/bereich/angebote", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ art: "format", titel: "Wöchentlicher Podcast", bewertung: 2 }),
});
const b = await hole(vanAn.keks, "/workspace/api/teamlage");
const ang = (b.daten?.angebote || [])[0];
if (!ang) { unklar("kein Angebot zum Ablehnen da"); }
else {
const ohne = await hole(vanAn.keks, "/workspace/api/teamlage/angebot", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ id: ang.id, art: "abgelehnt" }),
});
ok(ohne.status === 400, `ohne Grund geht es nicht (${ohne.status})`);
const mit = await hole(vanAn.keks, "/workspace/api/teamlage/angebot", {
method: "POST", headers: jsonKopf,
body: JSON.stringify({ id: ang.id, art: "abgelehnt", grund: "Zeitlich nicht drin" }),
});
ok(mit.status === 200 && mit.daten?.aufgaben === 0,
`mit Grund geht es, ohne Aufgaben (${mit.status})`);
const c = await hole(modiAn.keks, "/workspace/api/bereich/angebote");
const seins = (c.daten?.eintraege || c.daten?.liste || []).find((z) => z.id === ang.id);
ok(/Zeitlich nicht drin/.test(seins?.text || ""), "der Grund kommt beim Modi an");
}
void a;
} }
console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- ` console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- `
+ `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`); + `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
+63 -7
View File
@@ -169,6 +169,47 @@ export const BEREICHE = {
ohneCreatorBezug: true, ohneCreatorBezug: true,
}, },
/* =====================================================================
DIE ANGEBOTE (10.09.2026, Wunsch Filipe)
*"ich brauch auch noch kategorien wie, angebote, wo die modis
sachen planen und auch entscheiden was ich machen muss wenn das und
das erledigt wird."*
Ein Modi schlaegt etwas vor -- eine Aktion, ein Format, eine Regel
-- und schreibt dazu, was DogFather dafuer tun muesste. DogFather
oder VanVan nimmt an oder lehnt ab; bei Annahme entstehen die
Aufgaben. Entschieden wird in der Team-Lage, an derselben Stelle
wie die uebrigen Rueckmeldungen: EIN Eingang, nicht zwei.
DIE ZWEI ZAHLEN SIND UMBENANNT, nicht neu erfunden: `bewertung`
(1-5) heisst hier NUTZEN, `dringlichkeit` heisst AUFWAND. Beide
Felder gibt es laengst; sie mit neuen Namen zu belegen ist besser,
als zwei weitere Spalten anzulegen, die dasselbe koennen. Damit
die Oberflaeche nicht "Dringlichkeit" ueber den Aufwand schreibt,
stehen die Namen hier.
KEIN `fuerAlle`, wie beim Ideen-Board: Wer die Angebote sieht,
entscheidet dieselbe Regel wie ueberall. */
angebote: {
name: "Angebote",
arten: {
aktion: "Aktion",
format: "Format",
regel: "Regel",
technik: "Technik",
sonstiges: "Sonstiges",
},
ober: "Vorschläge",
bewertung: true,
bewertungName: "Nutzen",
dringlichkeit: true,
dringlichkeitName: "Aufwand",
einsatzFeld: true,
einsatzName: "Was DogFather dafür tun müsste",
ohneCreatorBezug: true,
},
agentur: { agentur: {
name: "Agentur", name: "Agentur",
arten: { arten: {
@@ -235,10 +276,12 @@ bereicheRouter.use("/workspace/api/bereich", angemeldet);
ZWEI SCHLOESSER, ABSICHTLICH: Hier haengt der ZUGANG zur Seite, ZWEI SCHLOESSER, ABSICHTLICH: Hier haengt der ZUGANG zur Seite,
sichtbar() haengt die Bedingung an die ZEILEN. Faellt eines weg, sichtbar() haengt die Bedingung an die ZEILEN. Faellt eines weg,
haelt das andere. */ haelt das andere. */
bereicheRouter.use("/workspace/api/bereich/ideen", (req, res, next) => { for (const geschuetzt of ["ideen", "angebote"]) {
if (siehtModis(req.person)) return next(); bereicheRouter.use(`/workspace/api/bereich/${geschuetzt}`, (req, res, next) => {
return res.status(404).json({ fehler: "nicht_gefunden" }); if (siehtModis(req.person)) return next();
}); return res.status(404).json({ fehler: "nicht_gefunden" });
});
}
/* --------------------------------------------------------------------- /* ---------------------------------------------------------------------
UND UMGEKEHRT: Ein Modi kommt nur in SEINE Bereiche. UND UMGEKEHRT: Ein Modi kommt nur in SEINE Bereiche.
@@ -457,6 +500,9 @@ const SPALTEN = `
e.hook, e.format, e.saeule_id, e.geplant, e.hook, e.format, e.saeule_id, e.geplant,
e.event_ende, e.event_aufgaben, e.event_regeln, e.event_ende, e.event_aufgaben, e.event_regeln,
e.creator_extern, e.creator_extern,
/* Was DogFather dafuer tun muesste -- nur bei den Angeboten
gefuellt, siehe BEREICHE.angebote. */
e.einsatz,
${externSql("pc.name", "e.creator_extern")} AS creator_name, ${externSql("pc.name", "e.creator_extern")} AS creator_name,
pe.name AS erstellt_name, pe.name AS erstellt_name,
(SELECT name FROM content_saeulen s WHERE s.id = e.saeule_id) AS saeule_name, (SELECT name FROM content_saeulen s WHERE s.id = e.saeule_id) AS saeule_name,
@@ -630,6 +676,15 @@ function pruefe(bereich, körper, { neu }) {
Konto; sichtbar bleibt er ueber erstellt_von (siehe sichtbar()). */ Konto; sichtbar bleibt er ueber erstellt_von (siehe sichtbar()). */
externPruefen(körper, aus, "creator", fehler); externPruefen(körper, aus, "creator", fehler);
/* Das Einsatz-Feld gibt es nur in Bereichen, die es kennen. Ein
Aufruf an der Oberflaeche vorbei soll nicht in jedem Bereich eine
Spalte fuellen koennen, die dort nichts bedeutet. */
if (einstellung.einsatzFeld && körper.einsatz !== undefined) {
const t = String(körper.einsatz ?? "").trim();
if (t.length > TEXT_MAX) fehler.push("Der Einsatz ist zu lang.");
else aus.einsatz = t || null;
}
/* Ein Agentur-Eintrag gehoert NIEMANDEM einzeln (07.09.2026). /* Ein Agentur-Eintrag gehoert NIEMANDEM einzeln (07.09.2026).
Er gilt fuer alle, und genau deshalb sieht ihn auch jeder (siehe Er gilt fuer alle, und genau deshalb sieht ihn auch jeder (siehe
@@ -704,13 +759,14 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
INSERT INTO eintraege INSERT INTO eintraege
(bereich, art, titel, text, datum, bewertung, dringlichkeit, status, (bereich, art, titel, text, datum, bewertung, dringlichkeit, status,
creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant, creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant,
event_ende, event_aufgaben, event_regeln) event_ende, event_aufgaben, event_regeln, einsatz)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
bereich, aus.art, aus.titel, aus.text ?? null, aus.datum, bereich, aus.art, aus.titel, aus.text ?? null, aus.datum,
aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen", aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen",
aus.creator_id ?? null, aus.creator_extern ?? null, jetzt(), req.person.id, aus.creator_id ?? null, aus.creator_extern ?? null, jetzt(), req.person.id,
aus.hook ?? null, aus.format ?? null, aus.saeule_id ?? null, aus.geplant ?? null, aus.hook ?? null, aus.format ?? null, aus.saeule_id ?? null, aus.geplant ?? null,
aus.event_ende ?? null, aus.event_aufgaben ?? null, aus.event_regeln ?? null); aus.event_ende ?? null, aus.event_aufgaben ?? null, aus.event_regeln ?? null,
aus.einsatz ?? null);
protokolliere("eintrag_angelegt", { protokolliere("eintrag_angelegt", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
+92 -8
View File
@@ -38,7 +38,7 @@ import {
protokolliere, echteIp, protokolliere, echteIp,
siehtModis, siehtModis,
} from "./workspace.js"; } from "./workspace.js";
import { MODI_PUNKTE } from "./workspace-modi-punkte.js"; import { MODI_PUNKTE, MODI_GRUPPENTEXTE } from "./workspace-modi-punkte.js";
import { import {
CONTENT_IDEEN, LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE, CONTENT_IDEEN, LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE,
} from "./workspace-vorlagen.js"; } from "./workspace-vorlagen.js";
@@ -147,8 +147,17 @@ function gruppenFuer(bereich, fuerModi) {
/* Eine Gruppe ohne Punkte faellt heraus statt leer dazustehen -- ein /* Eine Gruppe ohne Punkte faellt heraus statt leer dazustehen -- ein
Abschnitt mit einer Ueberschrift und nichts darunter sieht aus wie Abschnitt mit einer Ueberschrift und nichts darunter sieht aus wie
ein Fehler. */ ein Fehler. */
/* Auch der Satz UNTER der Ueberschrift wird getauscht. Der der
Creator-Fassung spricht von etwas anderem ("Vor dem Ernstfall
abhaken") -- und ein Satz, der etwas anderes verspricht als die
Liste darunter, ist schlimmer als gar keiner. */
const texte = MODI_GRUPPENTEXTE[bereich] || {};
return liste.gruppen return liste.gruppen
.map((g) => ({ ...g, punkte: satz[g.schluessel] || [] })) .map((g) => ({
...g,
text: texte[g.schluessel] || g.text,
punkte: satz[g.schluessel] || [],
}))
.filter((g) => g.punkte.length); .filter((g) => g.punkte.length);
} }
@@ -197,6 +206,38 @@ function tabellen() {
CREATE INDEX IF NOT EXISTS idx_punkt_nachricht CREATE INDEX IF NOT EXISTS idx_punkt_nachricht
ON punkt_nachricht (bereich, schluessel, creator_id); ON punkt_nachricht (bereich, schluessel, creator_id);
`); `);
/* =====================================================================
WURDE DIE RUECKMELDUNG SCHON BEHANDELT? (10.09.2026)
Seit die Modis melden statt bewertet zu werden, ist ein
"Verbessern" ein Eingang: DogFather oder VanVan macht eine Aufgabe
daraus oder sagt begruendet ab. Ohne diesen Vermerk stuende jede
Meldung ewig im Eingang -- und ein Eingang, der nie leer wird,
wird nicht mehr angesehen.
ALTER TABLE ADD COLUMN und kein Tabellenneubau: Es aendert sich
keine CHECK-Regel, und SQLite haengt eine Spalte anstandslos an.
`behandelt_art` statt eines Ja/Nein: "daraus wurde eine Aufgabe"
und "das ist nicht noetig" sind zwei verschiedene Antworten, und
der Modi soll beide unterscheiden koennen. */
for (const [spalte, typ] of [
["behandelt_am", "TEXT"],
["behandelt_von", "INTEGER"],
["behandelt_art", "TEXT"],
["aufgabe_id", "INTEGER"],
]) {
try {
const vorhanden = db().prepare("PRAGMA table_info(punkt_stand)").all().map((z) => z.name);
if (vorhanden.length && !vorhanden.includes(spalte)) {
db().exec(`ALTER TABLE punkt_stand ADD COLUMN ${spalte} ${typ}`);
console.log(`[workspace] Spalte '${spalte}' in punkt_stand ergaenzt.`);
}
} catch (fehler) {
console.error(`[workspace] Spalte '${spalte}':`, fehler?.message);
}
}
bereit = true; bereit = true;
} }
@@ -343,10 +384,29 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
/* Die Oberflaeche soll nicht raten muessen, wer was darf -- sonst /* Die Oberflaeche soll nicht raten muessen, wer was darf -- sonst
zeigt sie einem Creator einen Knopf, der beim Klicken 403 sagt. zeigt sie einem Creator einen Knopf, der beim Klicken 403 sagt.
Das ist schlechter als kein Knopf. */ Das ist schlechter als kein Knopf. */
/* Wer an der EIGENEN Liste arbeitet, bewertet sie nicht selbst -- /* =============================================================
weder Creator noch Modi. Die Stufe setzt die Betreuung WER DRUECKT, HAENGT DAVON AB, WESSEN LISTE ES IST
beziehungsweise die DogFather-Rolle. */ (umgedreht am 10.09.2026)
darf_bewerten: req.person.rolle !== "creator" && req.person.rolle !== "modi",
Wunsch Filipe: *"die modis mir quasi aufgaben verteilen, also
das sie drücken können passt so oder verbesserungen. weil die
modis sind ja da um mir zu helfen."*
BEI EINER CREATOR-LISTE wie bisher: Die Betreuung bewertet,
der Creator nicht sich selbst.
BEI EINER MODI-LISTE genau andersherum: NUR ER druecke. Es ist
SEIN Bericht ueber die Sendung -- was er gesehen und gehoert
hat. Duerfte DogFather ihn ueberschreiben, waere es nicht mehr
sein Bericht, sondern eine Abstimmung darueber, was er gesehen
haben soll.
DogFather und VanVan LESEN ihn und handeln in der Team-Lage:
Aus "Verbessern" wird dort mit einem Klick eine Aufgabe -- oder
eine begruendete Absage, die beim Modi ankommt. */
darf_bewerten: zielRolle === "modi"
? req.person.id === creatorId
: (req.person.rolle !== "creator" && req.person.rolle !== "modi"),
ich: req.person.id, ich: req.person.id,
}); });
} catch (fehler) { } catch (fehler) {
@@ -362,7 +422,23 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe
try { try {
/* Ein Creator bewertet sich nicht selbst. Waere das moeglich, /* Ein Creator bewertet sich nicht selbst. Waere das moeglich,
stuende alles auf gruen und die Ampel waere wertlos. */ stuende alles auf gruen und die Ampel waere wertlos. */
if (req.person.rolle === "creator" || req.person.rolle === "modi") { /* Wessen Liste ist es? Danach entscheidet sich, wer druecken
darf -- siehe die Begruendung beim Lesen weiter oben. */
const zielId = creatorAus(req);
const zielRolle = zielId > 0
? db().prepare("SELECT rolle FROM personen WHERE id = ?").get(zielId)?.rolle
: null;
const eigenerBericht = zielRolle === "modi" && req.person.id === zielId;
if (zielRolle === "modi" && !eigenerBericht) {
/* Auch DogFather nicht: Es ist der Bericht des Modis. Er
handelt in der Team-Lage, nicht hier. */
return res.status(403).json({
fehler: "Das ist die Rückmeldung des Modis. Sie wird nicht überschrieben, "
+ "sondern in der Team-Lage behandelt.",
});
}
if (!eigenerBericht
&& (req.person.rolle === "creator" || req.person.rolle === "modi")) {
return res.status(403).json({ return res.status(403).json({
fehler: "Die Bewertung setzt deine Betreuung. Du kannst hier antworten.", fehler: "Die Bewertung setzt deine Betreuung. Du kannst hier antworten.",
}); });
@@ -391,6 +467,8 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe
geloescht statt auf "offen" gesetzt -- ein Datensatz, der nichts geloescht statt auf "offen" gesetzt -- ein Datensatz, der nichts
aussagt, ist Ballast. */ aussagt, ist Ballast. */
if (stufe === "offen") { if (stufe === "offen") {
/* Auch der Vermerk faellt mit -- eine neu gesetzte Stufe ist
eine neue Meldung, keine schon behandelte. */
db().prepare("DELETE FROM punkt_stand WHERE bereich = ? AND schluessel = ? AND creator_id = ?") db().prepare("DELETE FROM punkt_stand WHERE bereich = ? AND schluessel = ? AND creator_id = ?")
.run(bereich, schluessel, creatorId); .run(bereich, schluessel, creatorId);
} else { } else {
@@ -399,7 +477,13 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe
VALUES (?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?)
ON CONFLICT(bereich, schluessel, creator_id) DO UPDATE SET ON CONFLICT(bereich, schluessel, creator_id) DO UPDATE SET
stufe = excluded.stufe, begruendung = excluded.begruendung, stufe = excluded.stufe, begruendung = excluded.begruendung,
von = excluded.von, am = excluded.am`) von = excluded.von, am = excluded.am,
/* Eine neu gesetzte Stufe ist eine NEUE Meldung -- der
alte Vermerk gehoert weg, sonst verschwindet sie
stillschweigend aus dem Eingang, obwohl sich etwas
geaendert hat. */
behandelt_am = NULL, behandelt_von = NULL,
behandelt_art = NULL, aufgabe_id = NULL`)
.run(bereich, schluessel, creatorId, stufe, grund || null, req.person.id, jetzt()); .run(bereich, schluessel, creatorId, stufe, grund || null, req.person.id, jetzt());
} }
+253 -229
View File
@@ -1,278 +1,302 @@
/* ===================================================================== /* =====================================================================
DIE CHECKLISTEN-PUNKTE EINES MODIS (10.09.2026) WAS DIE MODIS BEURTEILEN (10.09.2026, umgedreht am selben Tag)
Wunsch Filipe: *"diese kategorien mussen auf dieser seite auch noch Wunsch Filipe: *"diese 3 kategorien müssen so eingestellt werden dass
auf die modis perfektionniert werden, da mussen lauter sachen sein die modis mir quasi aufgaben verteilen, also das sie drücken können
die vanvan und ich danach verteilen können und da anpassen können ob passt so oder verbesserungen. weil die modis sind ja da um mir zu
gut oder nicht."* helfen."*
WARUM EIN ZWEITER SATZ UND NICHT EIN ERWEITERTER: Die vorhandenen DIE RICHTUNG WAR VORHER FALSCH HERUM. Beim ersten Anlauf standen hier
Punkte (LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE) sind fuer Punkte ueber die Arbeit des MODIS -- "Schicht bestaetigt", "Chat im
CREATOR geschrieben -- "Upload messen", "Sendeplan festlegen", Blick" -- und DogFather sollte sie bewerten. Das war eine Note fuer
"Verweildauer vergleichen". Ein Modi sendet nicht, er moderiert. Ihm ihn, keine Hilfe fuer DogFather. Und es widerspricht der Hausregel,
dieselbe Liste vorzulegen hiesse, ihn an Dingen zu messen, die nicht nach der niemand ueber dem Team steht.
seine Arbeit sind.
DIESELBEN DREI BEREICHE UND DIESELBEN GRUPPEN wie beim Creator -- JETZT BEURTEILT DER MODI DIE SENDUNG. Er sitzt im Chat, er sieht und
Vor/Waehrend/Nach, Moderation/Aktion/Konflikt, Setup/Problem/Loesung. hoert, was die Zuschauer sehen und hoeren -- frueher und ungefilterter
Das ist Absicht: Zwei verschiedene Einteilungen fuer dieselbe Sache als jeder, der gerade sendet. Er drueckt "Passt so" oder
waeren zwei Dinge zum Lernen statt einem, und die Oberflaeche kaeme "Verbessern", schreibt einen Satz dazu, und daraus wird ein Vorschlag
ohne Umbau nicht damit zurecht. fuer DogFather.
DIESELBEN VIER STUFEN (anfaenger, fort, profi, meister). Sie DIE PUNKTE SIND DESHALB BEOBACHTUNGEN, KEINE PFLICHTEN. Nicht "Ton
entscheiden, was jemandem zuerst gezeigt wird. Ein Modi in der pruefen", sondern "Der Ton war durchgehend verstaendlich". Der
Probezeit soll nicht an "Muster ueber mehrere Sendungen erkennen" Unterschied ist nicht sprachlich: Eine Pflicht kann man erfuellen
gemessen werden. oder versaeumen, eine Beobachtung kann man nur machen -- und
niemandem vorwerfen.
DIE SCHLUESSEL BEGINNEN MIT "m-". Der Stand haengt in `punkt_stand` WAS EIN MODI SELBST ZU TUN HAT, steht woanders: im Aufgabenkatalog
an (bereich, schluessel, person) -- ein Modi und ein Creator im aus Teil 2 des Anforderungsdokuments (60 Aufgaben, neun Etappen).
selben Bereich duerfen sich also nicht in die Quere kommen. Mit dem Das ist die richtige Stelle dafuer -- eine Aufgabe hat eine Frist und
Praefix kann das nicht passieren, auch wenn jemand spaeter einen einen Status, eine Beobachtung nicht.
Punkt gleich benennt.
BEWERTET WIRD NICHT VON IHM SELBST, sondern von der DogFather-Rolle DIE VIER STUFEN bleiben. Sie sagen hier, wie GENAU jemand hinsieht:
-- offen / gut / verbessern, wie beim Creator auch. Genau das war "Ton war da" sieht jeder, "es gab einen Grund wiederzukommen" sieht,
Filipes Wunsch: "verteilen ... und anpassen ob gut oder nicht". wer laenger dabei ist. Ein Modi in der Probezeit soll nicht mit den
Meister-Punkten anfangen muessen.
DIE SCHLUESSEL BEGINNEN MIT "m-". Der Stand haengt an (bereich,
schluessel, person) -- ohne Praefix koennte ein Modi-Punkt eines
Tages denselben Namen tragen wie ein Creator-Punkt und dessen
Bewertung erben.
===================================================================== */ ===================================================================== */
/* Die Ueberschriften und Saetze je Gruppe. Sie stehen hier, weil die
der Creator-Fassung von etwas anderem sprechen ("Vor dem Ernstfall
abhaken") -- und ein Satz, der etwas anderes verspricht als die
Liste darunter, ist schlimmer als gar keiner. */
export const MODI_GRUPPENTEXTE = {
live: {
vorbereitung: "Was du vor und beim Start gesehen hast. Der Anfang entscheidet, ob jemand bleibt.",
mitschrift: "Was währenddessen auffiel. Jetzt notieren – morgen ist es weg.",
auswertung: "Was danach passiert ist. Auch das gehört zur Sendung.",
},
community: {
moderation: "Wie die Regeln im Alltag wirken – nicht, wie sie gemeint sind.",
aktion: "Ob die Community etwas zurückbekommt.",
konflikt: "Wie es lief, als es eng wurde.",
},
technik: {
setup: "Was bei den Zuschauern ankommt. Du hörst und siehst es wie sie.",
problem: "Wenn etwas ausgefallen ist.",
loesung: "Was danach geblieben ist.",
},
};
export const MODI_LIVE_PUNKTE = { export const MODI_LIVE_PUNKTE = {
vorbereitung: [ vorbereitung: [
{ schluessel: "m-schicht-bestaetigt", titel: "Schicht bestätigt", stufe: "anfaenger", { schluessel: "m-ankuendigung-rechtzeitig", titel: "Die Ankündigung kam rechtzeitig", stufe: "anfaenger",
text: "Vor dem Tag gesagt, ob du kannst. Ein „ich schau mal“ ist keine Zusage – und das Team plant danach." }, text: "30 bis 60 Minuten vorher. Später erreicht sie die nicht mehr, die sich den Abend noch einteilen." },
{ schluessel: "m-ankuendigung-gesehen", titel: "Ankündigung gesehen", stufe: "anfaenger", { schluessel: "m-titel-sagt-worum", titel: "Der Titel sagte, worum es geht", stufe: "anfaenger",
text: "Du weißt, was draußen versprochen wurde. Sonst beantwortest du im Chat Fragen zu etwas, das du nicht kennst." }, text: "Wer ihn liest, muss wissen, ob es für ihn ist. Ein Titel, der neugierig macht und nichts sagt, holt die Falschen." },
{ schluessel: "m-ablauf-gelesen", titel: "Ablaufplan gelesen", stufe: "anfaenger", { schluessel: "m-startzeit-gehalten", titel: "Die Startzeit wurde gehalten", stufe: "anfaenger",
text: "Anfang, Mitte, Schluss – grob im Kopf. Wer den Ablauf kennt, kann den Chat darauf vorbereiten." }, text: "Wer zur angekündigten Zeit kommt und nichts vorfindet, kommt beim nächsten Mal später – oder nicht." },
{ schluessel: "m-regeln-griffbereit", titel: "Regeln griffbereit", stufe: "anfaenger", { schluessel: "m-ton-von-anfang-an", titel: "Der Ton war von Anfang an da", stufe: "anfaenger",
text: "Der Link oder der Text, den du hineinstellst, ohne zu suchen. Im Ernstfall zählen Sekunden." }, text: "Die ersten Sekunden ohne Ton kosten mehr Zuschauer als jede spätere Panne." },
{ schluessel: "m-eigene-technik", titel: "Eigene Technik geprüft", stufe: "anfaenger", { schluessel: "m-bild-hell-genug", titel: "Das Bild war hell genug", stufe: "anfaenger",
text: "Dein Gerät, deine Verbindung, dein Zugang. Ein Modi, der mittendrin herausfliegt, fehlt genau dann." }, text: "Auf dem Handy sieht es dunkler aus als am Rechner. Wenn du das Gesicht schlecht erkennst, erkennen es andere gar nicht." },
{ schluessel: "m-wer-ist-dabei", titel: "Wer moderiert heute noch?", stufe: "fort", { schluessel: "m-einstieg-vorhanden", titel: "Es gab einen Einstieg", stufe: "fort",
text: "Zu zweit teilt man sich auf – wer begrüßt, wer greift ein. Allein macht man beides halb." }, text: "Nicht mitten im Satz anfangen. Wer in den ersten Sekunden nicht weiß, wo er gelandet ist, geht wieder." },
{ schluessel: "m-ziel-bekannt", titel: "Ziel der Sendung bekannt", stufe: "fort", { schluessel: "m-thema-frueh-klar", titel: "Das Thema war früh klar", stufe: "fort",
text: "Eine Zahl oder eine Sache. Darauf kannst du im Chat hinweisen, ohne zu fragen." }, text: "Spätestens nach zwei Minuten. Alles danach ist Suchen, und Suchen macht man woanders." },
{ schluessel: "m-gaeste-bekannt", titel: "Gäste bekannt", stufe: "fort", { schluessel: "m-chat-wusste-bescheid", titel: "Der Chat wusste, was kommt", stufe: "fort",
text: "Wer kommt, wann, und wie heißt er richtig. Ein falsch geschriebener Name im Chat bleibt hängen." }, text: "Ein Satz zum Ablauf am Anfang spart dir zwanzig Fragen später." },
{ schluessel: "m-wortfilter-durchgesehen", titel: "Wortfilter durchgesehen", stufe: "fort", { schluessel: "m-technikcheck-war-durch", titel: "Der Technik-Check war vorher durch", stufe: "fort",
text: "Was letzte Woche durchrutschte, gehört heute hinein. Ein Filter, den niemand pflegt, veraltet." }, text: "Man merkt es daran, ob in den ersten Minuten noch geschraubt wird. Das sehen die Zuschauer auch." },
{ schluessel: "m-eskalationsweg", titel: "Eskalationsweg im Kopf", stufe: "fort", { schluessel: "m-gaeste-angekuendigt", titel: "Gäste waren angekündigt", stufe: "fort",
text: "Ab wann gibst du weiter, statt selbst zu entscheiden – und an wen. Vorher klären, nicht im Moment." }, text: "Wer im Chat sitzt, will wissen, wer da ist – und du willst den Namen richtig schreiben können." },
{ schluessel: "m-begruessung-bereit", titel: "Begrüßungssätze bereit", stufe: "profi", { schluessel: "m-einstieg-zog", titel: "Der Einstieg hat gezogen", stufe: "profi",
text: "Drei verschiedene, nicht einer dreimal. Wer merkt, dass ein Baustein antwortet, antwortet nicht zurück." }, text: "Blieben die ersten, die kamen? Über 70 Prozent entscheiden in den ersten Sekunden." },
{ schluessel: "m-umfrage-vorbereitet", titel: "Umfrage vorbereitet", stufe: "profi", { schluessel: "m-plan-erkennbar", titel: "Ein Plan war erkennbar", stufe: "profi",
text: "Höchstens drei Antworten, vorher aufgeschrieben. Mitten im Stream formuliert man keine gute Frage." }, text: "Nicht als Liste vorgelesen, aber spürbar. Ein Abend ohne Richtung fühlt sich für Zuschauer wie Warten an." },
{ schluessel: "m-vertretung-geklaert", titel: "Vertretung geklärt", stufe: "profi", { schluessel: "m-ankuendigung-passte", titel: "Die Ankündigung passte zum Inhalt", stufe: "profi",
text: "Wer übernimmt, wenn du wegmusst. Ein Chat ohne Aufsicht ist schlimmer als einer, der zu ist." }, text: "Wer wegen etwas anderem gekommen ist, bleibt nicht – und kommt beim nächsten Mal nicht wieder." },
{ schluessel: "m-uebergabe-vorbereitet", titel: "Übergabe an die nächste Schicht", stufe: "meister", { schluessel: "m-start-wiederholbar", titel: "Der Start war wiederholbar", stufe: "meister",
text: "Was die andere Person wissen muss, bevor du gehst. Zwei Zeilen reichen – aber sie müssen da sein." }, text: "So gut, dass man ihn genauso noch einmal machen könnte. Genau das macht aus einem guten Abend ein Format." },
], ],
mitschrift: [ mitschrift: [
{ schluessel: "m-chat-im-blick", titel: "Chat wirklich im Blick", stufe: "anfaenger", { schluessel: "m-ton-durchgehend", titel: "Der Ton blieb verständlich", stufe: "anfaenger",
text: "Nicht nebenbei. Wer mitschaut statt mitliest, sieht den Verstoß erst, wenn zehn Leute geantwortet haben." }, text: "Durchgehend, nicht überwiegend. Ein Aussetzer alle zehn Minuten reicht, damit jemand geht." },
{ schluessel: "m-neue-begruesst", titel: "Neue begrüßt", stufe: "anfaenger", { schluessel: "m-bild-ohne-ruckeln", titel: "Das Bild lief ohne Ruckeln", stufe: "anfaenger",
text: "Mit Namen, wenn es geht. Wer beim ersten Mal angesprochen wird, kommt deutlich häufiger wieder." }, text: "Wenn es bei dir ruckelt, ruckelt es bei vielen. Notiere die Uhrzeit – daran erkennt man die Ursache." },
{ schluessel: "m-spam-sofort-weg", titel: "Spam sofort weg", stufe: "anfaenger", { schluessel: "m-auf-chat-reagiert", titel: "Auf den Chat kam Reaktion", stufe: "anfaenger",
text: "Löschen, bevor jemand antwortet. Eine Diskussion über einen gelöschten Beitrag kostet mehr als der Beitrag." }, text: "Nicht auf alles, aber sichtbar. Ein Chat, der nie vorkommt, hört auf zu schreiben." },
{ schluessel: "m-fragen-gesammelt", titel: "Fragen gesammelt", stufe: "anfaenger", { schluessel: "m-namen-richtig", titel: "Namen wurden richtig gesagt", stufe: "anfaenger",
text: "An einer Stelle, nicht im Kopf. Was im Chat vorbeirauscht, ist nach zwei Minuten weg." }, text: "Wer falsch angesprochen wird, korrigiert einmal und schweigt dann." },
{ schluessel: "m-auf-ziel-hingewiesen", titel: "Auf das Ziel hingewiesen", stufe: "fort", { schluessel: "m-roter-faden", titel: "Der rote Faden war erkennbar", stufe: "fort",
text: "Lieber zweimal beiläufig als einmal ausführlich. Wer später dazukommt, hat es sonst nie gehört." }, text: "Auch für jemanden, der nach 40 Minuten dazukommt. Der weiß sonst nicht, worum es geht." },
{ schluessel: "m-interaktion-angestossen", titel: "Interaktion angestoßen", stufe: "fort", { schluessel: "m-fragen-beantwortet", titel: "Fragen bekamen eine Antwort", stufe: "fort",
text: "Eine Frage in den Chat, wenn es still wird. Stille im Chat sieht für Neue aus wie ein leerer Raum." }, text: "Oder wenigstens ein „später“. Eine übergangene Frage wird kein zweites Mal gestellt." },
{ schluessel: "m-clipmomente-markiert", titel: "Clip-Momente markiert", stufe: "fort", { schluessel: "m-ziel-erwaehnt", titel: "Das Ziel wurde erwähnt", stufe: "fort",
text: "Mit Uhrzeit, sofort. Am nächsten Tag findet niemand die Stelle wieder, an der alle gelacht haben." }, text: "Mindestens zweimal, beiläufig. Wer erst spät dazukommt, hat es sonst nie gehört." },
{ schluessel: "m-zeit-im-blick", titel: "Zeit im Blick behalten", stufe: "fort", { schluessel: "m-pausen-angesagt", titel: "Pausen waren angesagt", stufe: "fort",
text: "Ein kurzes Zeichen genügt. Wer sendet, hat kein Gefühl für die Uhr – das ist normal." }, text: "Ein stiller Bildschirm ohne Ansage sieht aus wie ein Absturz." },
{ schluessel: "m-verwarnen-vor-sperren", titel: "Erst verwarnen, dann sperren", stufe: "profi", { schluessel: "m-tempo-stimmte", titel: "Das Tempo stimmte", stufe: "profi",
text: "Außer bei den Sachen, die sofort gehen müssen. Eine Verwarnung, die jeder sieht, wirkt auf mehr als einen." }, text: "Nicht gehetzt, nicht zäh. Du merkst es am Chat: Wird er still, war es zu lang." },
{ schluessel: "m-stammzuschauer-angesprochen", titel: "Stammzuschauer angesprochen", stufe: "profi", { schluessel: "m-auf-stimmung-reagiert", titel: "Auf die Stimmung wurde reagiert", stufe: "profi",
text: "Wiedererkannt zu werden ist der halbe Grund wiederzukommen." }, text: "Wenn der Chat kippt oder einschläft, muss etwas passieren. Du siehst es zuerst." },
{ schluessel: "m-eskalation-weitergegeben", titel: "Eskalation weitergegeben", stufe: "profi", { schluessel: "m-gaeste-eingebunden", titel: "Gäste wurden eingebunden", stufe: "profi",
text: "Nach der Regel, nicht nach Gefühl. Allein zu entscheiden, wo die Regel etwas anderes sagt, bringt dich in die Schusslinie." }, text: "Ein Gast, der zwanzig Minuten zuhört, ist kein Gast. Das fällt im Chat sofort auf." },
{ schluessel: "m-nichts-oeffentlich-ausdiskutiert", titel: "Nichts öffentlich ausdiskutiert", stufe: "profi", { schluessel: "m-chat-nicht-ueberhoert", titel: "Der Chat wurde nicht überhört", stufe: "profi",
text: "Im Chat gewinnt niemand. Kurz sagen, was gilt, und weiter – der Rest gehört ins Team." }, text: "Gerade wenn es voll wird. Genau dann fühlen sich die Leisen übergangen." },
{ schluessel: "m-stimmung-gemeldet", titel: "Stimmung gemeldet", stufe: "meister", { schluessel: "m-abschluss-gesetzt", titel: "Der Abschluss war gesetzt", stufe: "meister",
text: "Wenn es kippt, bevor es kippt. Du siehst es früher als jeder, der gerade sendet." }, text: "Nicht abgebrochen. Ein Ende, das man kommen sieht, ist der Grund, beim nächsten Mal bis zum Schluss zu bleiben." },
{ schluessel: "m-ruhig-geblieben", titel: "Ruhig geblieben, als es eng wurde", stufe: "meister", { schluessel: "m-grund-wiederzukommen", titel: "Es gab einen Grund wiederzukommen", stufe: "meister",
text: "Der Ton eines Modis wird zum Ton des Chats. Das ist der schwerste Punkt auf dieser Liste." }, text: "Etwas Angekündigtes, etwas Offenes, etwas Verabredetes. Ohne das entscheidet der Zufall." },
], ],
auswertung: [ auswertung: [
{ schluessel: "m-auffaelligkeiten-notiert", titel: "Auffälligkeiten notiert", stufe: "anfaenger", { schluessel: "m-aufzeichnung-da", titel: "Die Aufzeichnung war da", stufe: "anfaenger",
text: "Solange du es noch weißt. Am nächsten Tag fehlt die Hälfte, und zwar die interessante." }, text: "Am selben Abend gesichert. Ohne sie gibt es keine Clips und keinen Beleg." },
{ schluessel: "m-clipmarken-weitergegeben", titel: "Clip-Marken weitergegeben", stufe: "anfaenger", { schluessel: "m-clips-kamen", titel: "Es kamen Clips", stufe: "anfaenger",
text: "An die, die schneiden. Eine Marke, die niemand bekommt, ist keine." }, text: "Mindestens einer, am nächsten Tag. Der Feed hat mehr Reichweite als der Stream." },
{ schluessel: "m-feedback-im-teamchat", titel: "Kurzes Feedback im Team-Chat", stufe: "anfaenger", { schluessel: "m-offene-fragen-nachgereicht", titel: "Offene Fragen wurden nachgereicht", stufe: "anfaenger",
text: "Eine Sache, die gut lief, eine, die man ändert. Wer sich fünf vornimmt, ändert keine." },
{ schluessel: "m-vorfaelle-festgehalten", titel: "Vorfälle festgehalten", stufe: "fort",
text: "Wer, wann, was – und was du getan hast. Beim zweiten Mal mit derselben Person zählt genau das." },
{ schluessel: "m-chatverlauf-gesichert", titel: "Chatverlauf gesichert", stufe: "fort",
text: "Vor dem Schließen. Er ist die einzige Quelle dafür, was wirklich gesagt wurde." },
{ schluessel: "m-offene-fragen-beantwortet", titel: "Offene Fragen nachgereicht", stufe: "fort",
text: "Was im Stream unterging, gehört danach beantwortet – sonst fragt niemand ein zweites Mal." }, text: "Was im Stream unterging, gehört danach beantwortet – sonst fragt niemand ein zweites Mal." },
{ schluessel: "m-eine-sache-fuers-naechste-mal", titel: "Eine Sache fürs nächste Mal", stufe: "fort", { schluessel: "m-team-bekam-rueckmeldung", titel: "Das Team bekam Rückmeldung", stufe: "fort",
text: "Aufgeschrieben, nicht vorgenommen. Ein Vorsatz ohne Zettel ist am Freitag weg." }, text: "Auch wenn alles gut lief. Wer nie hört, dass es gut war, weiß nicht, was er behalten soll." },
{ schluessel: "m-regel-vorgeschlagen", titel: "Regel vorgeschlagen, wo eine fehlte", stufe: "profi", { schluessel: "m-zahlen-geteilt", titel: "Zahlen wurden geteilt", stufe: "fort",
text: "Wenn du zweimal improvisieren musstest, fehlt eine Regel – nicht Erfahrung." }, text: "Nicht alle – die zwei, die etwas sagen. Ein Team, das die Wirkung nicht kennt, arbeitet blind." },
{ schluessel: "m-wiederkehrende-stoerer", titel: "Wiederkehrende Störer notiert", stufe: "profi", { schluessel: "m-angekuendigtes-gehalten", titel: "Angekündigtes wurde eingehalten", stufe: "fort",
text: "Dieselben Namen tauchen wieder auf. Eine Liste macht aus drei Einzelfällen ein Muster." }, text: "„Nächste Woche machen wir …“ – wenn das dreimal nicht kam, glaubt es niemand mehr." },
{ schluessel: "m-zahlen-angesehen", titel: "Zahlen angesehen", stufe: "profi", { schluessel: "m-aus-hinweisen-wurde-etwas", titel: "Aus Hinweisen wurde etwas", stufe: "profi",
text: "Wie viele kamen, wie lange blieben sie. Nicht um zu bewerten, sondern um zu wissen, wann der Chat kippte." }, text: "Was das Team gemeldet hat, taucht beim nächsten Mal auf – oder es kommt eine Absage. Beides ist in Ordnung, Schweigen nicht." },
{ schluessel: "m-uebergabe-geschrieben", titel: "Übergabe geschrieben", stufe: "meister", { schluessel: "m-naechster-termin-stand", titel: "Der nächste Termin stand", stufe: "profi",
text: "So, dass jemand anderes die nächste Sendung übernehmen könnte. Wer nicht ersetzbar ist, kann nicht krank werden." }, text: "Vor dem Ende gesagt und im Kalender. Zwei feste Tage schlagen fünf zufällige." },
{ schluessel: "m-muster-ueber-sendungen", titel: "Muster über mehrere Sendungen erkannt", stufe: "meister", { schluessel: "m-wiederkehrendes-angegangen", titel: "Wiederkehrendes wurde angegangen", stufe: "profi",
text: "Immer dieselbe Uhrzeit, immer dieselbe Sorte Kommentar. Das sieht nur, wer mehrere Abende nebeneinanderlegt." }, text: "Derselbe Aussetzer, dieselbe Frage, derselbe Störer – beim dritten Mal ist es kein Zufall mehr." },
{ schluessel: "m-geaendert-nicht-nur-besprochen", titel: "Es wurde geändert, nicht nur besprochen", stufe: "meister",
text: "Der Unterschied zwischen einer Nachbesprechung und einer Verbesserung." },
{ schluessel: "m-uebergabefaehig", titel: "Die Sendung war übergabefähig", stufe: "meister",
text: "So dokumentiert, dass jemand anderes sie fahren könnte. Wer nicht ersetzbar ist, kann nicht krank werden." },
{ schluessel: "m-team-genannt", titel: "Das Team wurde genannt", stufe: "meister",
text: "Öffentlich und namentlich. Wer nur im Hintergrund vorkommt, bleibt irgendwann ganz weg." },
], ],
}; };
export const MODI_COMMUNITY_PUNKTE = { export const MODI_COMMUNITY_PUNKTE = {
moderation: [ moderation: [
{ schluessel: "m-regeln-gelesen", titel: "Regeln gelesen und verstanden", stufe: "anfaenger", { schluessel: "m-regeln-auffindbar", titel: "Die Regeln waren auffindbar", stufe: "anfaenger",
text: "Nicht überflogen. Du wirst danach gefragt, und eine falsche Auskunft ist schwerer zurückzunehmen als keine." }, text: "An einer Stelle, die man ohne Suchen findet. Was man suchen muss, liest niemand." },
{ schluessel: "m-eigene-rechte-bekannt", titel: "Eigene Rechte bekannt", stufe: "anfaenger", { schluessel: "m-regeln-verstaendlich", titel: "Die Regeln waren verständlich", stufe: "anfaenger",
text: "Was du darfst: löschen, stummschalten, sperren – und was nicht. Wer es nicht weiß, tut zu viel oder zu wenig." }, text: "In eigenen Worten, nicht als Rechtstext. Regeln, die man erklären muss, werden nicht befolgt." },
{ schluessel: "m-tonfall", titel: "Tonfall gefunden", stufe: "anfaenger", { schluessel: "m-wortfilter-passte", titel: "Der Wortfilter passte noch", stufe: "anfaenger",
text: "Freundlich und kurz. Ein Modi, der sich rechtfertigt, verliert; einer, der herrisch wird, auch." }, text: "Was diese Woche durchrutschte, gehört hinein. Ein Filter, den niemand pflegt, veraltet." },
{ schluessel: "m-haeufige-fragen", titel: "Häufige Fragen im Kopf", stufe: "anfaenger", { schluessel: "m-klar-wer-eingreift", titel: "Es war klar, wer eingreifen darf", stufe: "anfaenger",
text: "Die fünf, die jeden Abend kommen. Wer sie kennt, muss nicht jedes Mal nachsehen." }, text: "Für dich und für den Chat. Unklarheit darüber kostet im Ernstfall die entscheidenden Sekunden." },
{ schluessel: "m-warnung-vor-sperre", titel: "Reihenfolge klar: Hinweis, Warnung, Sperre", stufe: "fort", { schluessel: "m-eskalationsregel-bekannt", titel: "Die Eskalationsregel war bekannt", stufe: "fort",
text: "Vorher entscheiden, nicht im Moment. Wer im Ärger sperrt, sperrt irgendwann falsch." }, text: "Ab wann gibt man weiter statt selbst zu entscheiden. Wer das erst im Ernstfall klärt, klärt es zu spät." },
{ schluessel: "m-minderjaehrige", titel: "Umgang mit Minderjährigen", stufe: "fort", { schluessel: "m-regeln-angewandt", titel: "Die Regeln wurden auch angewandt", stufe: "fort",
text: "Wie erkennt man sie, was gilt dann. Das ist keine Ermessensfrage." }, text: "Eine Regel, die zweimal übergangen wird, gilt nicht mehr – auch für die, die sich daran halten." },
{ schluessel: "m-privat-vs-oeffentlich", titel: "Privat statt öffentlich", stufe: "fort", { schluessel: "m-entscheidungen-nachvollziehbar", titel: "Entscheidungen waren nachvollziehbar", stufe: "fort",
text: "Was eine Person betrifft, klärt man mit ihr – nicht vor zweihundert Leuten." }, text: "Nicht für jeden im Chat, aber für dich. Eine Sperre, die niemand erklären kann, wird zurückgenommen." },
{ schluessel: "m-keine-versprechen", titel: "Keine Versprechen im Namen anderer", stufe: "fort", { schluessel: "m-alle-gleich-behandelt", titel: "Alle wurden gleich behandelt", stufe: "fort",
text: "\"Das machen wir\" bindet das ganze Team. \"Ich gebe es weiter\" bindet nur dich." },
{ schluessel: "m-entscheidung-begruenden", titel: "Entscheidung in einem Satz begründen können", stufe: "profi",
text: "Nicht im Chat – aber wenn DogFather fragt. Eine Sperre, die niemand erklären kann, wird zurückgenommen." },
{ schluessel: "m-eigene-grenze", titel: "Eigene Grenze kennen", stufe: "profi",
text: "Wann du aussteigst und jemanden holst. Weiterzumachen, wenn es dich mitnimmt, hilft niemandem." },
{ schluessel: "m-gleich-behandeln", titel: "Alle gleich behandeln", stufe: "profi",
text: "Auch die, die man mag. Eine Ausnahme sehen mehr Leute, als man denkt." }, text: "Auch die, die man mag. Eine Ausnahme sehen mehr Leute, als man denkt." },
{ schluessel: "m-neue-modis-eingearbeitet", titel: "Neue Modis eingearbeitet", stufe: "meister", { schluessel: "m-regeln-passen-noch", titel: "Die Regeln passen noch zur Community", stufe: "profi",
text: "Einen Abend nebenher laufen lassen und danach besprechen. Zusehen lernt mehr als jede Regelliste." }, text: "Eine Community von heute ist nicht die von vor einem Jahr. Regeln altern mit." },
{ schluessel: "m-wiederholungstaeter-erkannt", titel: "Wiederholungstäter wurden erkannt", stufe: "profi",
text: "Dieselben Namen tauchen wieder auf. Ohne Liste bleiben drei Einzelfälle drei Einzelfälle." },
{ schluessel: "m-rueckendeckung", titel: "Es gab Rückendeckung", stufe: "profi",
text: "Wenn ein Modi eingreift und danach allein dasteht, greift beim nächsten Mal keiner mehr ein." },
{ schluessel: "m-aus-vorfaellen-regeln", titel: "Aus Vorfällen wurden Regeln", stufe: "meister",
text: "Ein Vorfall, aus dem nichts folgt, kommt wieder. Der Ertrag ist die Regel, nicht die Sperre." },
], ],
aktion: [ aktion: [
{ schluessel: "m-willkommen-gepflegt", titel: "Willkommens-Routine gepflegt", stufe: "anfaenger", { schluessel: "m-neue-wurden-begruesst", titel: "Neue wurden begrüßt", stufe: "anfaenger",
text: "Ein Satz, der nicht nach Textbaustein klingt. Wer merkt, dass ein Automat schreibt, antwortet nicht." }, text: "Mit Namen. Wer beim ersten Mal angesprochen wird, kommt deutlich häufiger wieder." },
{ schluessel: "m-namen-gemerkt", titel: "Namen gemerkt", stufe: "anfaenger", { schluessel: "m-kommentare-beantwortet", titel: "Kommentare bekamen Antwort", stufe: "anfaenger",
text: "Die zehn, die immer da sind. Mehr braucht es nicht, damit sich eine Community erkannt fühlt." }, text: "Auch die kurzen, auch die späten. Eine Antwort nach drei Tagen liest sich wie keine." },
{ schluessel: "m-fragen-in-den-stream", titel: "Fragen in den Stream gegeben", stufe: "anfaenger", { schluessel: "m-stammzuschauer-erkannt", titel: "Stammzuschauer wurden erkannt", stufe: "anfaenger",
text: "Gesammelt, ausgewählt, weitergereicht. Nicht alles – die drei, die etwas bringen." }, text: "Wiedererkannt zu werden ist der halbe Grund wiederzukommen." },
{ schluessel: "m-umfrage-ausgewertet", titel: "Umfrage ausgewertet", stufe: "fort", { schluessel: "m-etwas-zum-mitmachen", titel: "Es gab etwas zum Mitmachen", stufe: "fort",
text: "Und das Ergebnis gesagt. Eine Abstimmung ohne Ergebnis fragt beim nächsten Mal niemand mehr mit." }, text: "Eine Umfrage, eine Frage, eine Challenge. Zuschauen allein bindet niemanden." },
{ schluessel: "m-aktion-vorgeschlagen", titel: "Eine Aktion vorgeschlagen", stufe: "fort", { schluessel: "m-umfragen-ausgewertet", titel: "Umfragen wurden ausgewertet", stufe: "fort",
text: "Challenge, Q&A, gemeinsamer Abend. Du hörst im Chat, was ankommt, früher als alle anderen." }, text: "Und das Ergebnis gesagt. Eine Abstimmung ohne Ergebnis macht beim nächsten Mal niemand mit." },
{ schluessel: "m-stille-belebt", titel: "Stille belebt statt ausgesessen", stufe: "fort", { schluessel: "m-wuensche-kamen-zurueck", titel: "Wünsche kamen zurück", stufe: "fort",
text: "Eine Frage, ein Bezug auf vorhin. Ein stiller Chat wirkt auf Neue wie ein leerer Raum." }, text: "Auch als Nein. Ein Wunschkanal ohne Antwort verstummt." },
{ schluessel: "m-rueckmeldung-eingesammelt", titel: "Rückmeldungen eingesammelt", stufe: "fort", { schluessel: "m-aktionen-angekuendigt", titel: "Aktionen waren angekündigt", stufe: "fort",
text: "Was Zuschauer sich wünschen, gehört weitergegeben – auch wenn es nicht umgesetzt wird." }, text: "Rechtzeitig genug, dass man sich den Termin freihalten kann." },
{ schluessel: "m-stammzuschauer-liste", titel: "Liste der Stammzuschauer gepflegt", stufe: "profi", { schluessel: "m-community-erwaehnt", titel: "Die Community wurde erwähnt", stufe: "profi",
text: "Wer seit Wochen da ist, verdient mehr als ein Herz. Ohne Liste vergisst man ihn." }, text: "Namentlich, nicht als „ihr da draußen“. Der Unterschied zwischen Publikum und Runde." },
{ schluessel: "m-neue-in-die-runde", titel: "Neue in die Runde geholt", stufe: "profi", { schluessel: "m-ideen-aufgegriffen", titel: "Ideen aus dem Chat wurden aufgegriffen", stufe: "profi",
text: "Sie einmal namentlich ins Gespräch ziehen. Der Unterschied zwischen Zuschauen und Dabeisein." }, text: "Und gesagt, woher sie kamen. Wer sich wiederfindet, bringt die nächste Idee." },
{ schluessel: "m-eigene-idee-eingetragen", titel: "Eigene Idee ins Ideen-Board", stufe: "profi", { schluessel: "m-grund-teil-zu-sein", titel: "Es gab einen Grund, dabei zu sein", stufe: "profi",
text: "Was dir im Chat auffällt, ist die beste Quelle für Inhalte – aber nur, wenn es aufgeschrieben wird." }, text: "Etwas, das man nur mitbekommt, wenn man da ist. Sonst reicht der Clip am nächsten Tag." },
{ schluessel: "m-aktion-durchgezogen", titel: "Eine Aktion von Anfang bis Ende begleitet", stufe: "meister", { schluessel: "m-aus-aktion-wurde-reihe", titel: "Aus einer Aktion wurde eine Reihe", stufe: "meister",
text: "Ankündigen, durchführen, hinterher sagen, was daraus wurde. Der letzte Teil wird fast immer vergessen." }, text: "Erst beim zweiten Mal sieht man, ob das Format trägt oder nur das Thema getragen hat." },
{ schluessel: "m-community-uebersetzt", titel: "Zwischen Community und Team übersetzt", stufe: "meister", { schluessel: "m-community-waechst-von-innen", titel: "Die Community wächst von innen", stufe: "meister",
text: "Was im Chat gemeint ist, und was im Team ankommt, sind oft zwei Dinge. Diese Lücke zu schließen ist die eigentliche Arbeit." }, text: "Neue kommen, weil jemand sie mitgebracht hat. Das ist die einzige Reichweite, die bleibt." },
], ],
konflikt: [ konflikt: [
{ schluessel: "m-erst-lesen", titel: "Erst lesen, dann handeln", stufe: "anfaenger", { schluessel: "m-frueh-eingegriffen", titel: "Es wurde früh eingegriffen", stufe: "anfaenger",
text: "Zwei Sätze zurückscrollen. Die Hälfte der Streitfälle sieht mit dem Satz davor anders aus." }, text: "Bevor zehn Leute geantwortet haben. Danach moderiert man nicht mehr, man räumt auf." },
{ schluessel: "m-nicht-mitstreiten", titel: "Nicht mitstreiten", stufe: "anfaenger", { schluessel: "m-ruhig-geblieben-live", titel: "Es blieb ruhig", stufe: "anfaenger",
text: "Du bist nicht Partei. In dem Moment, in dem du eine wirst, hast du deine Rolle verloren." }, text: "Der Ton oben wird zum Ton im Chat. Das ist der schwerste Punkt auf dieser Liste." },
{ schluessel: "m-kurz-und-klar", titel: "Kurz und klar sagen, was gilt", stufe: "anfaenger", { schluessel: "m-nicht-oeffentlich-ausdiskutiert", titel: "Nichts wurde öffentlich ausdiskutiert", stufe: "anfaenger",
text: "Ein Satz, keine Begründungskette. Je länger die Erklärung, desto mehr Angriffsfläche." }, text: "Im Chat gewinnt niemand. Kurz sagen, was gilt, und weiter." },
{ schluessel: "m-abkuehlen-lassen", titel: "Abkühlen lassen statt eskalieren", stufe: "fort", { schluessel: "m-entscheidung-begruendet", titel: "Die Entscheidung war begründet", stufe: "fort",
text: "Stummschalten auf Zeit ist oft besser als sperren. Man kann es zurücknehmen." }, text: "In einem Satz, nicht in einer Kette. Je länger die Erklärung, desto mehr Angriffsfläche." },
{ schluessel: "m-zweite-meinung", titel: "Zweite Meinung geholt", stufe: "fort", { schluessel: "m-rueckendeckung-im-fall", titel: "Es gab Rückendeckung", stufe: "fort",
text: "Bei allem, was größer ist als ein blöder Kommentar. Zu zweit entscheidet es sich leichter und besser." }, text: "Wer eingreift, muss wissen, dass er danach nicht allein dasteht." },
{ schluessel: "m-belege-gesichert", titel: "Belege gesichert", stufe: "fort", { schluessel: "m-belege-gesichert-fall", titel: "Belege wurden gesichert", stufe: "fort",
text: "Bildschirmfoto, bevor der Beitrag weg ist. Ohne Beleg steht später Aussage gegen Aussage." }, text: "Bildschirmfoto, bevor der Beitrag weg ist. Ohne Beleg steht später Aussage gegen Aussage." },
{ schluessel: "m-danach-gemeldet", titel: "Danach im Team gemeldet", stufe: "fort", { schluessel: "m-danach-besprochen", titel: "Danach wurde besprochen", stufe: "fort",
text: "Auch wenn es gut ausging. DogFather und die rechte Hand sollten es von dir erfahren, nicht von anderen." }, text: "Auch wenn es gut ausging. Sonst lernt nur der daraus, der dabei war." },
{ schluessel: "m-keine-drohungen", titel: "Keine Drohungen, nur Folgen", stufe: "profi", { schluessel: "m-keine-ausnahmen", titel: "Keine Ausnahmen für Bekannte", stufe: "profi",
text: "\"Noch einmal und du bist raus\" ist eine Ansage. Alles darüber hinaus ist Streit." }, text: "Das ist der Punkt, an dem Regeln ihre Glaubwürdigkeit verlieren – und er fällt allen auf." },
{ schluessel: "m-nach-der-sperre", titel: "Nach der Sperre nicht nachtreten", stufe: "profi", { schluessel: "m-fehler-zurueckgenommen", titel: "Fehler wurden zurückgenommen", stufe: "profi",
text: "Kein Kommentar hinterher, kein Witz darüber. Der Chat merkt sich das." }, text: "Falsch gesperrt? Einmal sagen und rückgängig machen. Das kostet weniger als recht zu behalten." },
{ schluessel: "m-eigene-fehler", titel: "Eigenen Fehler zugegeben", stufe: "profi", { schluessel: "m-ton-blieb", titel: "Der Ton blieb auch danach", stufe: "profi",
text: "Falsch gesperrt? Zurücknehmen und einmal sagen. Das kostet weniger als der Versuch, recht zu behalten." }, text: "Kein Nachtreten, kein Witz über den Gesperrten. Der Chat merkt sich das." },
{ schluessel: "m-ernstfall-geprobt", titel: "Ernstfall einmal durchgesprochen", stufe: "meister", { schluessel: "m-fall-fuehrte-zu-regel", titel: "Der Fall führte zu einer Regel", stufe: "meister",
text: "Wer tut was, wenn es wirklich kippt. Im Ernstfall zum ersten Mal darüber nachzudenken ist zu spät." }, text: "Wenn zweimal improvisiert werden musste, fehlt eine Regel – nicht Erfahrung." },
{ schluessel: "m-daraus-gelernt", titel: "Aus dem Fall eine Regel gemacht", stufe: "meister", { schluessel: "m-kam-nicht-wieder", titel: "Es kam nicht wieder", stufe: "meister",
text: "Ein Vorfall, aus dem nichts folgt, kommt wieder. Der Ertrag ist die Regel, nicht die Sperre." }, text: "Der einzige Beweis, dass die Regel gewirkt hat." },
], ],
}; };
export const MODI_TECHNIK_PUNKTE = { export const MODI_TECHNIK_PUNKTE = {
setup: [ setup: [
{ schluessel: "m-zugaenge-funktionieren", titel: "Zugänge funktionieren", stufe: "anfaenger", { schluessel: "m-ton-ohne-rauschen", titel: "Ton ohne Rauschen", stufe: "anfaenger",
text: "Einmal vor der Sendung anmelden, nicht währenddessen. Ein abgelaufener Zugang fällt sonst im schlechtesten Moment auf." }, text: "Du hörst es wie die Zuschauer. Schlechter Ton ist der Grund Nummer eins, warum Leute wieder gehen." },
{ schluessel: "m-benachrichtigungen-an", titel: "Benachrichtigungen an", stufe: "anfaenger", { schluessel: "m-ton-nicht-uebersteuert", titel: "Ton nicht übersteuert", stufe: "anfaenger",
text: "Für Aufgaben und Team-Nachrichten. Ein Hinweis, den niemand bekommt, ist keiner." }, text: "Kratzt es bei lauten Stellen? Das merkt man erst, wenn jemand lacht – und dann ist es unangenehm laut." },
{ schluessel: "m-zweites-geraet", titel: "Zweites Gerät griffbereit", stufe: "anfaenger", { schluessel: "m-bild-scharf", titel: "Das Bild war scharf", stufe: "anfaenger",
text: "Handy neben dem Rechner. Wenn eines hängt, moderierst du mit dem anderen weiter." }, text: "Nicht weich, nicht verpixelt. Auf dem Handy fällt beides stärker auf als am Rechner." },
{ schluessel: "m-verbindung-stabil", titel: "Verbindung stabil", stufe: "anfaenger", { schluessel: "m-licht-ausreichend", titel: "Das Licht reichte", stufe: "anfaenger",
text: "Kabel schlägt WLAN. Ein Modi, der alle zehn Minuten neu lädt, sieht die Hälfte nicht." }, text: "Gesicht heller als der Hintergrund, kein Fenster im Rücken." },
{ schluessel: "m-chatfenster-eingerichtet", titel: "Chatfenster richtig eingerichtet", stufe: "fort", { schluessel: "m-verbindung-stabil-live", titel: "Die Verbindung war stabil", stufe: "anfaenger",
text: "Groß genug, ohne anderes darüber. Wer scrollen muss, um mitzulesen, liest nicht mit." }, text: "Wenn es bei dir hängt, hängt es bei vielen. Uhrzeit notieren – daran erkennt man die Ursache." },
{ schluessel: "m-werkzeuge-kennen", titel: "Moderations-Werkzeuge kennen", stufe: "fort", { schluessel: "m-szenenwechsel-sauber", titel: "Szenenwechsel waren sauber", stufe: "fort",
text: "Wo Löschen, Stummschalten, Sperren sitzt – ohne zu suchen. Im Ernstfall zählen Sekunden." }, text: "Kein schwarzes Bild dazwischen, kein Ton, der wegbricht." },
{ schluessel: "m-automod-verstanden", titel: "Automatische Filter verstanden", stufe: "fort", { schluessel: "m-musik-nicht-zu-laut", titel: "Die Musik war nicht zu laut", stufe: "fort",
text: "Was sie wegnehmen und was nicht. Wer sich darauf verlässt, ohne es zu wissen, übersieht genau die Lücke." }, text: "Sie darf die Stimme nicht überdecken. Am Rechner klingt das anders als im Kopfhörer." },
{ schluessel: "m-app-installiert", titel: "Die App wirklich installiert", stufe: "fort", { schluessel: "m-overlays-lesbar", titel: "Einblendungen waren lesbar", stufe: "fort",
text: "Nicht als Lesezeichen. Nur so kommen Erinnerungen an, auch wenn der Browser zu ist." }, text: "Auch auf einem Handy. Was du zusammenkneifen musst, liest niemand." },
{ schluessel: "m-tastenkuerzel", titel: "Tastenkürzel eingerichtet", stufe: "profi", { schluessel: "m-text-auf-handy", titel: "Text war auf dem Handy lesbar", stufe: "fort",
text: "Die drei, die du am häufigsten brauchst. Aus zwei Sekunden werden über einen Abend Minuten." }, text: "Der größte Teil schaut auf dem Handy. Was dort nicht ankommt, kommt gar nicht an." },
{ schluessel: "m-ruhe-eingestellt", titel: "Ruhezeiten eingestellt", stufe: "profi", { schluessel: "m-kein-nachhall", titel: "Kein Hall im Raum", stufe: "profi",
text: "Damit die App nachts still ist. Wer um drei geweckt wird, schaltet sie irgendwann ganz ab." }, text: "Klingt es nach Badezimmer, hört man nach zehn Minuten weg – ohne zu wissen, warum." },
{ schluessel: "m-ersatzweg", titel: "Ersatzweg ins Netz", stufe: "meister", { schluessel: "m-ausweichweg-vorhanden", titel: "Ein Ausweichweg war vorhanden", stufe: "meister",
text: "Mobilfunk, einmal wirklich benutzt. Ein Ersatz, den man erst im Ausfall einrichtet, ist keiner." }, text: "Zweites Gerät, zweite Leitung. Ein Ersatz, den man erst im Ausfall einrichtet, ist keiner." },
], ],
problem: [ problem: [
{ schluessel: "m-was-genau-faellt-aus", titel: "Was genau fällt aus?", stufe: "anfaenger", { schluessel: "m-ausfall-bemerkt", titel: "Der Ausfall wurde bemerkt", stufe: "anfaenger",
text: "Chat, Bild, dein Zugang oder das ganze Netz – das sind vier verschiedene Probleme mit vier Lösungen." }, text: "Und zwar oben, nicht nur im Chat. Minutenlang ins Leere zu senden ist das Schlimmste, was passieren kann." },
{ schluessel: "m-sofort-gesagt", titel: "Sofort gesagt, dass etwas ist", stufe: "anfaenger", { schluessel: "m-chat-informiert", titel: "Der Chat wurde informiert", stufe: "anfaenger",
text: "Lieber einmal zu viel im Team-Chat. Ein stiller Modi sieht aus wie ein anwesender." }, text: "Ein Satz genügt. Stille sieht aus wie Absturz, und dann gehen die Leute." },
{ schluessel: "m-notiert-solange-frisch", titel: "Notiert, solange es frisch ist", stufe: "anfaenger", { schluessel: "m-ansage-wie-lange", titel: "Es gab eine Ansage, wie lange", stufe: "fort",
text: "Uhrzeit, Fehlermeldung, was du zuletzt getan hast. Am nächsten Tag fehlt genau das." }, text: "Auch „ich weiß es nicht“ ist eine Ansage. Wer keine Zeit genannt bekommt, wartet nicht." },
{ schluessel: "m-eine-aenderung", titel: "Nur eine Änderung auf einmal", stufe: "fort", { schluessel: "m-jemand-uebernahm", titel: "Jemand hat übernommen", stufe: "fort",
text: "Wer drei Dinge gleichzeitig ändert und es geht wieder, weiß nicht warum – und steht beim nächsten Mal am Anfang." }, text: "Chat weiterführen, während oben repariert wird. Ohne das zerfällt die Runde in fünf Minuten." },
{ schluessel: "m-uebergeben-statt-kaempfen", titel: "Übergeben statt kämpfen", stufe: "fort", { schluessel: "m-fehler-festgehalten", titel: "Der Fehler wurde festgehalten", stufe: "fort",
text: "Nach fünf Minuten Basteln moderiert jemand anderes. Der Chat wartet nicht auf deine Technik." }, text: "Uhrzeit, Meldung, was zuletzt anders war. Am nächsten Tag fehlt genau das." },
{ schluessel: "m-bildschirmfoto", titel: "Bildschirmfoto gemacht", stufe: "fort", { schluessel: "m-nicht-live-gebastelt", titel: "Es wurde nicht live gebastelt", stufe: "profi",
text: "Eine Fehlermeldung, die man nachher beschreibt, ist eine andere als die, die dastand." }, text: "Nach zwei Minuten Herumprobieren schaut man dabei zu, wie jemand arbeitet. Dafür ist niemand gekommen." },
{ schluessel: "m-wiederholbar", titel: "Geprüft, ob es wiederkommt", stufe: "profi", { schluessel: "m-ausfall-wiederholte-sich-nicht", titel: "Der Ausfall kam nicht wieder", stufe: "profi",
text: "Einmal weg ist nicht gelöst. Beim nächsten Mal nachsehen gehört dazu." }, text: "Einmal weg ist nicht gelöst. Beim nächsten Mal nachsehen gehört dazu." },
{ schluessel: "m-nicht-alleine-suchen", titel: "Nicht allein stundenlang suchen", stufe: "profi", { schluessel: "m-plan-b", titel: "Es gab einen Plan B", stufe: "meister",
text: "Nach zwanzig Minuten jemanden dazuholen. Zu zweit findet man es in fünf." }, text: "Und er wurde vorher einmal geprobt. Ein ungeprobter Ausweichweg ist keiner." },
], ],
loesung: [ loesung: [
{ schluessel: "m-was-geholfen-hat", titel: "Was geholfen hat", stufe: "anfaenger", { schluessel: "m-loesung-aufgeschrieben", titel: "Die Lösung wurde aufgeschrieben", stufe: "anfaenger",
text: "Genau die eine Änderung – nicht alles, was probiert wurde. Sonst ist es beim nächsten Mal wieder Raten." }, text: "Genau die eine Änderung – nicht alles, was probiert wurde." },
{ schluessel: "m-fuer-alle-aufgeschrieben", titel: "Für alle aufgeschrieben", stufe: "anfaenger", { schluessel: "m-loesung-verstaendlich", titel: "Sie war verständlich", stufe: "fort",
text: "In die Bibliothek, nicht in den Chat. Was im Chat steht, ist nach zwei Tagen unauffindbar." }, text: "Auch für jemanden, der nicht dabei war. Was man selbst geschrieben hat, versteht man immer." },
{ schluessel: "m-anleitung-verstaendlich", titel: "Anleitung verständlich", stufe: "fort", { schluessel: "m-loesung-hielt", titel: "Sie hat gehalten", stufe: "fort",
text: "Jemanden gegenlesen lassen, der es nicht erlebt hat. Was du selbst geschrieben hast, verstehst du immer." }, text: "Beim nächsten Mal nachgesehen. Ein Pflaster ist in Ordnung – aber es gehört dabeigeschrieben." },
{ schluessel: "m-dauerhaft-oder-pflaster", titel: "Dauerhaft oder Pflaster?", stufe: "fort", { schluessel: "m-loesung-geteilt", titel: "Sie wurde geteilt", stufe: "profi",
text: "Beides ist in Ordnung – aber es gehört dabeigeschrieben, damit niemand darauf baut." }, text: "In der Bibliothek, nicht im Chat. Was im Chat steht, ist nach zwei Tagen unauffindbar." },
{ schluessel: "m-vorbeugen-vorgeschlagen", titel: "Vorbeugen vorgeschlagen", stufe: "profi", { schluessel: "m-vorbeugung", titel: "Es wurde vorgebeugt", stufe: "profi",
text: "Wenn es zweimal passiert ist, ist es kein Zufall mehr, sondern eine fehlende Einstellung." }, text: "Wenn es zweimal passiert ist, fehlt eine Einstellung – nicht Glück." },
{ schluessel: "m-wissen-weitergegeben", titel: "Wissen weitergegeben", stufe: "meister", { schluessel: "m-wissen-nicht-bei-einem", titel: "Das Wissen liegt nicht bei einer Person", stufe: "meister",
text: "Einmal im Team erklärt, statt es dreimal allein zu lösen. Ein Modi, der als Einziger etwas kann, ist ein Engpass." }, text: "Wer als Einziger etwas kann, ist ein Engpass – und darf nicht krank werden." },
], ],
}; };
/* Alle drei unter den Bereichsnamen, unter denen sie gebraucht werden --
damit die Auswahl an einer Stelle steht und nicht in jeder Abfrage
neu zusammengesucht wird. */
export const MODI_PUNKTE = { export const MODI_PUNKTE = {
live: MODI_LIVE_PUNKTE, live: MODI_LIVE_PUNKTE,
community: MODI_COMMUNITY_PUNKTE, community: MODI_COMMUNITY_PUNKTE,
+266 -2
View File
@@ -26,10 +26,33 @@
===================================================================== */ ===================================================================== */
import express from "express"; import express from "express";
import { import {
db, sitzungLesen, istDogFather, heuteLokal, db, sitzungLesen, istDogFather, heuteLokal, protokolliere, echteIp,
} from "./workspace.js"; } from "./workspace.js";
import { MODI_PUNKTE } from "./workspace-modi-punkte.js"; import { MODI_PUNKTE } from "./workspace-modi-punkte.js";
/* Titel und Bereich zu einem Schluessel -- damit im Eingang steht,
WORUM es geht, und nicht nur ein Schluessel. */
const PUNKT = new Map();
for (const [bereich, gruppen] of Object.entries(MODI_PUNKTE)) {
for (const punkte of Object.values(gruppen)) {
for (const p of punkte) PUNKT.set(`${bereich}|${p.schluessel}`, p);
}
}
/* Die Kategorie, unter der eine daraus entstandene Aufgabe einsortiert
wird. Der Bereich sagt es schon -- eine zweite Zuordnung waere eine
zweite Gelegenheit, dass beide auseinanderlaufen. */
const KATEGORIE = { live: "technik", community: "community", technik: "technik" };
function gleicheHerkunft(req, res, next) {
const herkunft = req.get("origin");
if (!herkunft) return next();
let erlaubt;
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
next();
}
export const teamlageRouter = express.Router(); export const teamlageRouter = express.Router();
/* Wie viele Punkte es je Bereich ueberhaupt gibt. Einmal gerechnet -- /* Wie viele Punkte es je Bereich ueberhaupt gibt. Einmal gerechnet --
@@ -49,7 +72,11 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
WHERE rolle = 'modi' AND aktiv = 1 ORDER BY name`).all(); WHERE rolle = 'modi' AND aktiv = 1 ORDER BY name`).all();
if (!modis.length) { if (!modis.length) {
return res.json({ modis: [], bereiche: GESAMT, heute: heuteLokal() }); /* `eingang` auch hier -- die Oberflaeche darf nicht raten
muessen, ob das Feld fehlt oder leer ist. */
return res.json({
modis: [], eingang: [], angebote: [], bereiche: GESAMT, heute: heuteLokal(),
});
} }
const platz = modis.map(() => "?").join(","); const platz = modis.map(() => "?").join(",");
const nummern = modis.map((m) => m.id); const nummern = modis.map((m) => m.id);
@@ -88,6 +115,46 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
ideen.set(z.p, z.n); ideen.set(z.p, z.n);
} }
/* =================================================================
DER EINGANG: was die Modis gemeldet haben (10.09.2026)
Filipes Entscheidung: erst ein Vorschlag, dann eine Aufgabe.
Hier steht, was noch nicht behandelt ist -- offen heisst: Ihr
habt noch nicht darauf geantwortet. Was zur Aufgabe wurde oder
begruendet abgelehnt ist, faellt heraus; ein Eingang, der nie
leer wird, wird nicht mehr angesehen. */
const eingang = [];
for (const z of db().prepare(
`SELECT s.bereich, s.schluessel, s.creator_id, s.begruendung, s.am, p.name AS von_name
FROM punkt_stand s LEFT JOIN personen p ON p.id = s.von
WHERE s.creator_id IN (${platz})
AND s.stufe = 'verbessern' AND s.behandelt_am IS NULL
ORDER BY s.am DESC`).all(...nummern)) {
const punkt = PUNKT.get(`${z.bereich}|${z.schluessel}`);
/* Ein Punkt, den es nicht mehr gibt, wird uebersprungen -- aber
gezaehlt waere er sonst und niemand koennte ihn behandeln. */
if (!punkt) continue;
eingang.push({
bereich: z.bereich, schluessel: z.schluessel, person_id: z.creator_id,
titel: punkt.titel, hinweis: punkt.text,
begruendung: z.begruendung || "", am: z.am, von: z.von_name || null,
});
}
/* =================================================================
DIE ANGEBOTE (10.09.2026)
Vorschlaege der Modis, ueber die hier entschieden wird. Sie
stehen im selben Eingang wie die Rueckmeldungen -- EIN Ort, an
dem etwas auf eine Antwort wartet, nicht zwei. Wer zwei Eingaenge
hat, sieht irgendwann in den falschen. */
const angebote = db().prepare(
`SELECT e.id, e.art, e.titel, e.text, e.einsatz, e.bewertung, e.dringlichkeit,
e.datum, e.erstellt_von, p.name AS von_name
FROM eintraege e LEFT JOIN personen p ON p.id = e.erstellt_von
WHERE e.bereich = 'angebote' AND e.status = 'offen'
ORDER BY e.bewertung DESC, e.datum DESC`).all();
/* Wie viele Rueckmeldungen an seinen Punkten haengen -- das ist /* Wie viele Rueckmeldungen an seinen Punkten haengen -- das ist
das Gespraech, nicht die Note. */ das Gespraech, nicht die Note. */
const nachrichten = new Map(); const nachrichten = new Map();
@@ -100,6 +167,8 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
res.json({ res.json({
heute, heute,
bereiche: GESAMT, bereiche: GESAMT,
eingang,
angebote,
modis: modis.map((m) => { modis: modis.map((m) => {
const s = stand.get(m.id) || {}; const s = stand.get(m.id) || {};
const je = {}; const je = {};
@@ -125,3 +194,198 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
res.status(503).json({ fehler: "nicht_verfuegbar" }); res.status(503).json({ fehler: "nicht_verfuegbar" });
} }
}); });
/* =====================================================================
EINE MELDUNG BEHANDELN (10.09.2026)
Filipes Entscheidung: *"Erst ein Vorschlag, den du annimmst."* Zwei
Antworten, und beide kommen beim Modi an:
annehmen -> daraus wird eine Aufgabe (fuer den, der drueckt)
ablehnen -> mit Begruendung, ohne dass etwas entsteht
WARUM BEIDE EINE NACHRICHT SCHREIBEN: Eine Meldung, auf die nichts
folgt, sieht fuer den Modi aus wie eine, die niemand gelesen hat --
und beim dritten Mal meldet er nichts mehr. Die Nachricht haengt am
Punkt (punkt_nachricht), also genau dort, wo er sie beim naechsten
Mal wiederfindet.
DIE AUFGABE GEHOERT DEM, DER DRUECKT. Filipe oder VanVan -- wer
annimmt, nimmt sie sich. Sie einem Dritten zuzuweisen waere eine
Entscheidung ueber jemand anderen, ohne ihn zu fragen.
===================================================================== */
teamlageRouter.post("/workspace/api/teamlage/behandeln", gleicheHerkunft,
express.json({ limit: "16kb" }), (req, res) => {
try {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
const bereich = String(req.body?.bereich || "");
const schluessel = String(req.body?.schluessel || "");
const personId = Number(req.body?.person_id) || 0;
const art = String(req.body?.art || "");
const grund = String(req.body?.grund ?? "").trim().slice(0, 2000);
if (!["aufgabe", "abgelehnt"].includes(art)) {
return res.status(400).json({ fehler: "Unbekannte Art." });
}
if (!PUNKT.has(`${bereich}|${schluessel}`) || !personId) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
/* EINE ABSAGE OHNE GRUND IST KEINE ANTWORT. Wer nichts dazu
schreibt, laesst den Modi genauso ratlos zurueck wie
Schweigen -- nur mit dem Unterschied, dass die Meldung jetzt
auch noch aus dem Eingang verschwunden ist. */
if (art === "abgelehnt" && grund.length < 3) {
return res.status(400).json({ fehler: "Bitte kurz sagen, warum nicht." });
}
const meldung = db().prepare(
`SELECT begruendung FROM punkt_stand
WHERE bereich = ? AND schluessel = ? AND creator_id = ?
AND stufe = 'verbessern' AND behandelt_am IS NULL`)
.get(bereich, schluessel, personId);
/* Schon behandelt oder gar nicht gemeldet -- beides ist "gibt es
hier nicht". Zwei Antworten waeren zwei Faelle, die die
Oberflaeche unterscheiden muesste, ohne dass es hilft. */
if (!meldung) return res.status(404).json({ fehler: "nicht_gefunden" });
const punkt = PUNKT.get(`${bereich}|${schluessel}`);
const jetzt = new Date().toISOString();
let aufgabeId = null;
if (art === "aufgabe") {
const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben
(titel, beschreibung, status, prioritaet, verantwortlich_id,
kategorie, erstellt, erstellt_von)
VALUES (?,?,'offen','mittel',?,?,?,?)`).run(
punkt.titel,
/* Der Satz des Modis steht IN der Aufgabe -- ohne ihn muesste
man zurueckblaettern, um zu wissen, worum es ging. */
meldung.begruendung
? `Gemeldet aus „${punkt.titel}“:
${meldung.begruendung}`
: `Gemeldet aus „${punkt.titel}“.`,
person.id, KATEGORIE[bereich] || "sonstiges", jetzt, person.id);
aufgabeId = Number(lastInsertRowid);
}
db().prepare(
`UPDATE punkt_stand
SET behandelt_am = ?, behandelt_von = ?, behandelt_art = ?, aufgabe_id = ?
WHERE bereich = ? AND schluessel = ? AND creator_id = ?`)
.run(jetzt, person.id, art, aufgabeId, bereich, schluessel, personId);
db().prepare(`INSERT INTO punkt_nachricht
(bereich, schluessel, creator_id, text, von, am) VALUES (?,?,?,?,?,?)`)
.run(bereich, schluessel, personId,
art === "aufgabe"
? `Danke – daraus ist eine Aufgabe geworden.${grund ? " " + grund : ""}`
: `Danke fürs Melden. Das machen wir nicht: ${grund}`,
person.id, jetzt);
protokolliere("meldung_behandelt", {
personId: person.id, rolle: person.rolle, ip: echteIp(req),
detail: `${bereich}/${schluessel} -> ${art}`.slice(0, 120),
});
res.json({ ok: true, art, aufgabe_id: aufgabeId });
} catch (fehler) {
console.error("[workspace] Meldung behandeln:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* =====================================================================
UEBER EIN ANGEBOT ENTSCHEIDEN (10.09.2026)
Filipes Wahl: *"Angebote an dich zur Entscheidung ... Bei Annahme
entstehen die Aufgaben fuer alle Beteiligten auf einmal."*
BEI ANNAHME ZWEI AUFGABEN, nicht eine:
* der Modi, der es vorgeschlagen hat, setzt es um
* und wenn im Angebot steht, was DogFather dafuer tun muesste,
bekommt der, der annimmt, genau das als eigene Aufgabe
Die zweite ist der Kern von Filipes Wunsch ("entscheiden was ich
machen muss"). Ohne sie waere eine Annahme ein Nicken, aus dem nichts
folgt -- und beim dritten Mal schlaegt niemand mehr etwas vor.
ABGELEHNT BRAUCHT EINEN GRUND, wie bei den Rueckmeldungen: Eine
Absage ohne Begruendung ist fuer den, der sie bekommt, dasselbe wie
Schweigen -- nur endgueltiger.
===================================================================== */
teamlageRouter.post("/workspace/api/teamlage/angebot", gleicheHerkunft,
express.json({ limit: "16kb" }), (req, res) => {
try {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
const id = Number(req.body?.id) || 0;
const art = String(req.body?.art || "");
const grund = String(req.body?.grund ?? "").trim().slice(0, 2000);
if (!["angenommen", "abgelehnt"].includes(art)) {
return res.status(400).json({ fehler: "Unbekannte Art." });
}
if (art === "abgelehnt" && grund.length < 3) {
return res.status(400).json({ fehler: "Bitte kurz sagen, warum nicht." });
}
const angebot = db().prepare(
`SELECT id, titel, text, einsatz, erstellt_von FROM eintraege
WHERE id = ? AND bereich = 'angebote' AND status = 'offen'`).get(id);
/* Schon entschieden oder gar keins -- beides ist "gibt es hier
nicht". Zwei Antworten waeren zwei Faelle, die die Oberflaeche
unterscheiden muesste, ohne dass es hilft. */
if (!angebot) return res.status(404).json({ fehler: "nicht_gefunden" });
const jetzt = new Date().toISOString();
const angelegt = [];
if (art === "angenommen") {
if (angebot.erstellt_von) {
const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben
(titel, beschreibung, status, prioritaet, verantwortlich_id,
kategorie, erstellt, erstellt_von)
VALUES (?,?,'offen','mittel',?,'planung',?,?)`).run(
`Umsetzen: ${angebot.titel}`, angebot.text || null,
angebot.erstellt_von, jetzt, person.id);
angelegt.push({ fuer: angebot.erstellt_von, id: Number(lastInsertRowid) });
}
if (angebot.einsatz) {
const { lastInsertRowid } = db().prepare(`INSERT INTO aufgaben
(titel, beschreibung, status, prioritaet, verantwortlich_id,
kategorie, erstellt, erstellt_von)
VALUES (?,?,'offen','mittel',?,'planung',?,?)`).run(
angebot.einsatz, `Aus dem Angebot „${angebot.titel}“.`,
person.id, jetzt, person.id);
angelegt.push({ fuer: person.id, id: Number(lastInsertRowid) });
}
}
db().prepare(
"UPDATE eintraege SET status = ?, geaendert = ? WHERE id = ?")
.run(art, jetzt, angebot.id);
/* Die Antwort gehoert an das Angebot, damit der Modi sie dort
wiederfindet, wo er nachsieht. `text` ist das einzige Feld
dafuer -- angehaengt, nicht ersetzt. */
const antwort = art === "angenommen"
? `
— Angenommen von ${person.name}${grund ? ": " + grund : "."}`
: `
— Nicht umgesetzt (${person.name}): ${grund}`;
db().prepare("UPDATE eintraege SET text = COALESCE(text, '') || ? WHERE id = ?")
.run(antwort, angebot.id);
protokolliere("angebot_entschieden", {
personId: person.id, rolle: person.rolle, ip: echteIp(req),
detail: `#${angebot.id} ${art}`.slice(0, 120),
});
res.json({ ok: true, art, aufgaben: angelegt.length });
} catch (fehler) {
console.error("[workspace] Angebot entscheiden:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
+26
View File
@@ -284,6 +284,9 @@ const MODI_BEREICHE = [
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst", { gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
name: "Ideen-Board", unter: "Gesammelt und nach Dringlichkeit sortiert", name: "Ideen-Board", unter: "Gesammelt und nach Dringlichkeit sortiert",
zeichen: "content", ton: 23, ziel: "bereich.html?b=ideen", szene: "portal" }, zeichen: "content", ton: 23, ziel: "bereich.html?b=ideen", szene: "portal" },
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
name: "Angebote", unter: "Vorschläge, über die DogFather entscheidet",
zeichen: "agentur", ton: 17, ziel: "bereich.html?b=angebote", szene: "halle" },
{ gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung", { gruppe: "Rund ums Live", gruppeUnter: "Vor, während und nach der Sendung",
name: "Live-Ablauf", unter: "Checkliste für vorher, mittendrin und danach", name: "Live-Ablauf", unter: "Checkliste für vorher, mittendrin und danach",
@@ -948,6 +951,17 @@ function umstellungen(d) {
workspace-aufgaben.js) -- an EINER Stelle, durch die alle workspace-aufgaben.js) -- an EINER Stelle, durch die alle
Schreibwege laufen. */ Schreibwege laufen. */
["aufgaben", "kategorie", "TEXT"], ["aufgaben", "kategorie", "TEXT"],
/* WAS DOGFATHER DAZU TUN MUESSTE (10.09.2026).
Filipe: *"auch entscheiden was ich machen muss wenn das und das
erledigt wird."* Der Modi schreibt es beim Angebot dazu; wird
das Angebot angenommen, wird genau daraus seine Aufgabe.
Ein eigenes Feld und nicht im Fliesstext: Aus einem Absatz laesst
sich keine Aufgabe machen, ohne zu raten, welcher Satz gemeint
war. */
["eintraege", "einsatz", "TEXT"],
]) { ]) {
try { try {
const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name); const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name);
@@ -1498,6 +1512,18 @@ function umstellungen(d) {
waere die vierte Gelegenheit dazu. */ waere die vierte Gelegenheit dazu. */
checkListeErweitern(d, "eintraege", "bereich", "ideen", checkListeErweitern(d, "eintraege", "bereich", "ideen",
["live", "content", "technik", "community", "schutz", "agentur", "ideen"], jetztStempel); ["live", "content", "technik", "community", "schutz", "agentur", "ideen"], jetztStempel);
/* Die Angebote (10.09.2026) -- Vorschlaege der Modis, ueber die
DogFather und VanVan entscheiden. */
checkListeErweitern(d, "eintraege", "bereich", "angebote",
["live", "content", "technik", "community", "schutz", "agentur", "ideen", "angebote"],
jetztStempel);
/* ZWEI NEUE ZUSTAENDE. Bisher kannte ein Eintrag nur offen/erledigt.
Ein Angebot hat aber eine ENTSCHEIDUNG: angenommen oder abgelehnt
-- und das ist etwas anderes als "erledigt". Wer beides in einen
Topf wirft, kann hinterher nicht mehr sagen, was aus einem
Vorschlag geworden ist. */
checkListeErweitern(d, "eintraege", "status", "angenommen",
["offen", "erledigt", "angenommen", "abgelehnt"], jetztStempel);
} }
export function db() { export function db() {
+4 -4
View File
@@ -13,25 +13,25 @@
"theme_color": "#06090f", "theme_color": "#06090f",
"icons": [ "icons": [
{ {
"src": "/assets/img/app-symbole/workspace-192.png?v=202609100303", "src": "/assets/img/app-symbole/workspace-192.png?v=202609100327",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "any" "purpose": "any"
}, },
{ {
"src": "/assets/img/app-symbole/workspace-512.png?v=202609100303", "src": "/assets/img/app-symbole/workspace-512.png?v=202609100327",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "any" "purpose": "any"
}, },
{ {
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100303", "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100327",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "maskable" "purpose": "maskable"
}, },
{ {
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100303", "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100327",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "maskable" "purpose": "maskable"
+27
View File
@@ -414,6 +414,10 @@
}; };
const event = istEventArt(daten.art); const event = istEventArt(daten.art);
if (einstellung.dringlichkeit && !event) daten.dringlichkeit = $('f-dringlich').value; if (einstellung.dringlichkeit && !event) daten.dringlichkeit = $('f-dringlich').value;
/* Verborgen heisst auch: nichts mitschicken. Ein Wert, der in einem
unsichtbaren Feld stehenbleibt, wandert sonst beim naechsten
Speichern mit -- und niemand sieht, woher er kommt. */
if (einstellung.einsatzFeld && $('f-einsatz')) daten.einsatz = $('f-einsatz').value.trim();
if (einstellung.bewertung) daten.bewertung = $('f-bewertung').value || null; if (einstellung.bewertung) daten.bewertung = $('f-bewertung').value || null;
if (event) { if (event) {
daten.event_ende = $('f-ende').value || null; daten.event_ende = $('f-ende').value || null;
@@ -506,6 +510,29 @@
$('f-art').append(o); $('f-art').append(o);
} }
$('feld-dringlich').hidden = !einstellung.dringlichkeit; $('feld-dringlich').hidden = !einstellung.dringlichkeit;
/* =================================================================
ZWEI ZAHLEN, ANDERE NAMEN (10.09.2026)
Bei den Angeboten heisst `bewertung` NUTZEN und `dringlichkeit`
AUFWAND. Die Felder gibt es laengst -- sie mit neuen Namen zu
belegen ist besser, als zwei weitere Spalten anzulegen, die
dasselbe koennen. Aber ein Feld, ueber dem "Dringlichkeit" steht
und das den Aufwand meint, ist falsch beschriftet, und das faellt
beim Ausfuellen niemandem auf -- erst beim Lesen.
Die Namen kommen aus den Bereichsangaben; steht dort keiner,
bleibt es wie bisher. */
if (einstellung.dringlichkeitName && $('dringlich-schild')) {
$('dringlich-schild').textContent = einstellung.dringlichkeitName;
}
if (einstellung.bewertungName && $('bewertung-schild')) {
$('bewertung-schild').textContent = `${einstellung.bewertungName} (1–5)`;
}
/* Das Einsatz-Feld nur, wo der Bereich es kennt. */
if ($('feld-einsatz')) $('feld-einsatz').hidden = !einstellung.einsatzFeld;
if (einstellung.einsatzName && $('einsatz-schild')) {
$('einsatz-schild').textContent = einstellung.einsatzName;
}
$('feld-bewertung').hidden = !einstellung.bewertung; $('feld-bewertung').hidden = !einstellung.bewertung;
if (LEITUNG.has(ich.rolle)) { if (LEITUNG.has(ich.rolle)) {
+164 -1
View File
@@ -114,6 +114,165 @@
return k; return k;
} }
/* =====================================================================
DER EINGANG -- was die Modis gemeldet haben.
Er steht OBEN, vor den Karten: Das ist das, was eine Antwort
braucht. Die Zahlen darunter kann man sich ansehen; hier wartet
jemand.
ZWEI ANTWORTEN, und beide kommen beim Modi an. Eine Meldung, auf
die nichts folgt, sieht fuer ihn aus wie eine, die niemand gelesen
hat -- und beim dritten Mal meldet er nichts mehr.
===================================================================== */
function eingangBauen(liste) {
const k = el('section', 'tperson');
k.append(el('p', 'feldschild', 'Rückmeldungen aus dem Team'));
if (!liste.length) {
k.append(el('p', 'tperson__zuletzt',
'Nichts offen – auf alles, was gemeldet wurde, habt ihr geantwortet.'));
return k;
}
for (const m of liste) {
const zeile = el('div', 'tpipe');
zeile.append(el('p', 'tperson__namen', m.titel));
if (m.begruendung) zeile.append(el('p', 'tperson__unter', m.begruendung));
zeile.append(el('span', 'tperson__zuletzt',
`${m.von || 'jemand'} · ${(m.am || '').slice(0, 10)}`));
const knoepfe = el('div', 'tpipe__reihe');
const melden = (t) => { melde(t); };
const ja = el('button', 'schritt', 'Daraus wird eine Aufgabe');
ja.type = 'button';
ja.addEventListener('click', async () => {
ja.disabled = true;
try {
const a = await hole('/workspace/api/teamlage/behandeln', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bereich: m.bereich, schluessel: m.schluessel,
person_id: m.person_id, art: 'aufgabe',
}),
});
if (!a.ok) {
melden((await a.json().catch(() => ({}))).fehler || 'Das hat nicht geklappt.');
ja.disabled = false; return;
}
melden('');
await laden();
} catch { ja.disabled = false; }
});
knoepfe.append(ja);
const nein = el('button', 'abmelden', 'Nicht nötig');
nein.type = 'button';
nein.addEventListener('click', async () => {
/* EINE ABSAGE OHNE GRUND IST KEINE ANTWORT. Deshalb wird
gefragt -- und ein leeres Feld bricht ab, statt still nichts
zu tun. */
const grund = window.prompt(
`Warum machen wir das nicht?
„${m.titel}“`, '');
if (grund === null) return;
if (!grund.trim()) { melden('Ohne Grund bleibt die Meldung stehen.'); return; }
nein.disabled = true;
try {
const a = await hole('/workspace/api/teamlage/behandeln', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bereich: m.bereich, schluessel: m.schluessel,
person_id: m.person_id, art: 'abgelehnt', grund: grund.trim(),
}),
});
if (!a.ok) {
melden((await a.json().catch(() => ({}))).fehler || 'Das hat nicht geklappt.');
nein.disabled = false; return;
}
melden('');
await laden();
} catch { nein.disabled = false; }
});
knoepfe.append(nein);
zeile.append(knoepfe);
k.append(zeile);
}
return k;
}
/* =====================================================================
DIE ANGEBOTE -- Vorschlaege, ueber die ihr entscheidet.
Sie stehen im selben Eingang wie die Rueckmeldungen: EIN Ort, an
dem etwas auf eine Antwort wartet. Wer zwei Eingaenge hat, sieht
irgendwann in den falschen.
===================================================================== */
const AUFWAND = { hoch: 'viel Aufwand', mittel: 'mittlerer Aufwand', niedrig: 'wenig Aufwand' };
async function entscheiden(knopf, koerper) {
knopf.disabled = true;
try {
const a = await hole('/workspace/api/teamlage/angebot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(koerper),
});
if (!a.ok) {
melde((await a.json().catch(() => ({}))).fehler || 'Das hat nicht geklappt.');
knopf.disabled = false; return;
}
const d = await a.json().catch(() => ({}));
melde(d.aufgaben ? `${d.aufgaben} Aufgabe(n) angelegt.` : '');
await laden();
} catch { knopf.disabled = false; }
}
function angeboteBauen(liste) {
const k = el('section', 'tperson');
k.append(el('p', 'feldschild', 'Angebote aus dem Team'));
if (!liste.length) {
k.append(el('p', 'tperson__zuletzt', 'Gerade liegt kein Vorschlag zur Entscheidung.'));
return k;
}
for (const a of liste) {
const zeile = el('div', 'tpipe');
zeile.append(el('p', 'tperson__namen', a.titel));
if (a.text) zeile.append(el('p', 'tperson__unter', a.text));
/* WAS ES FUER DICH HEISST, steht direkt dabei -- sonst muss man
die Entscheidung treffen, ohne den eigenen Teil zu kennen. */
if (a.einsatz) {
zeile.append(el('p', 'tperson__unter', `Dein Teil: ${a.einsatz}`));
}
zeile.append(el('span', 'tperson__zuletzt',
`${a.von_name || 'jemand'} · Nutzen ${a.bewertung ?? '–'}/5 · `
+ `${AUFWAND[a.dringlichkeit] || a.dringlichkeit}`));
const knoepfe = el('div', 'tpipe__reihe');
const ja = el('button', 'schritt', 'Annehmen');
ja.type = 'button';
ja.addEventListener('click', () => entscheiden(ja, { id: a.id, art: 'angenommen' }));
knoepfe.append(ja);
const nein = el('button', 'abmelden', 'Nicht umsetzen');
nein.type = 'button';
nein.addEventListener('click', () => {
const grund = window.prompt(`Warum nicht?
„${a.titel}“`, '');
if (grund === null) return;
if (!grund.trim()) { melde('Ohne Grund bleibt das Angebot stehen.'); return; }
entscheiden(nein, { id: a.id, art: 'abgelehnt', grund: grund.trim() });
});
knoepfe.append(nein);
zeile.append(knoepfe);
k.append(zeile);
}
return k;
}
async function laden() { async function laden() {
const ziel = $('lage'); const ziel = $('lage');
try { try {
@@ -128,11 +287,15 @@
s + Object.values(m.bereiche).reduce((x, b) => x + b.offen, 0), 0); s + Object.values(m.bereiche).reduce((x, b) => x + b.offen, 0), 0);
/* Eine leere Seite ohne Erklaerung sieht aus wie ein Fehler -- /* Eine leere Seite ohne Erklaerung sieht aus wie ein Fehler --
deshalb steht hier auch, was zu tun waere. */ deshalb steht hier auch, was zu tun waere. */
const wartend = (daten.eingang || []).length + (daten.angebote || []).length;
$('kopf-unter').textContent = leute.length $('kopf-unter').textContent = leute.length
? `${leute.length} ${leute.length === 1 ? 'Person' : 'Personen'} · ` ? `${leute.length} ${leute.length === 1 ? 'Person' : 'Personen'} · `
+ `${offenGesamt} Punkte noch nicht besprochen` + `${wartend} ${wartend === 1 ? 'Rückmeldung wartet' : 'Rückmeldungen warten'} · `
+ `${offenGesamt} Punkte noch nicht beurteilt`
: 'Noch niemand im Team. Zugänge legst du unter „Personen & Zugänge“ an.'; : 'Noch niemand im Team. Zugänge legst du unter „Personen & Zugänge“ an.';
ziel.append(eingangBauen(daten.eingang || []));
ziel.append(angeboteBauen(daten.angebote || []));
for (const m of leute) ziel.append(karte(m)); for (const m of leute) ziel.append(karte(m));
} catch { /* umgeleitet */ } } catch { /* umgeleitet */ }
} }
+11 -11
View File
@@ -12,12 +12,12 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -245,10 +245,10 @@
</form> </form>
</dialog> </dialog>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/aufgaben.js?v=202609100303" defer></script> <script src="assets/js/aufgaben.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/automation.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -140,10 +140,10 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/automation.js?v=202609100303" defer></script> <script src="assets/js/automation.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+26 -18
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/bereich.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -75,8 +75,16 @@
<label class="feld-schild" for="f-ende">Bis</label> <label class="feld-schild" for="f-ende">Bis</label>
<input id="f-ende" type="date" /> <input id="f-ende" type="date" />
</div> </div>
<!-- Was DogFather dafuer tun muesste. Nur in Bereichen, die das
Feld kennen (siehe BEREICHE.angebote) -- sonst bleibt es
verborgen und wird auch nicht mitgeschickt. -->
<div id="feld-einsatz" hidden>
<label class="feld-schild" for="f-einsatz" id="einsatz-schild">Einsatz</label>
<input id="f-einsatz" type="text" maxlength="300"
placeholder="z. B. Termin ansagen und den Gast einladen" />
</div>
<div id="feld-dringlich" hidden> <div id="feld-dringlich" hidden>
<label class="feld-schild" for="f-dringlich">Dringlichkeit</label> <label class="feld-schild" for="f-dringlich" id="dringlich-schild">Dringlichkeit</label>
<select id="f-dringlich"> <select id="f-dringlich">
<option value="hoch">hoch</option> <option value="hoch">hoch</option>
<option value="mittel" selected>mittel</option> <option value="mittel" selected>mittel</option>
@@ -84,7 +92,7 @@
</select> </select>
</div> </div>
<div id="feld-bewertung" hidden> <div id="feld-bewertung" hidden>
<label class="feld-schild" for="f-bewertung">Bewertung (1–5)</label> <label class="feld-schild" for="f-bewertung" id="bewertung-schild">Bewertung (1–5)</label>
<select id="f-bewertung"> <select id="f-bewertung">
<option value="">—</option> <option value="">—</option>
<option value="1">1 – schwach</option> <option value="1">1 – schwach</option>
@@ -149,14 +157,14 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/checkliste.js?v=202609100303" defer></script> <script src="assets/js/checkliste.js?v=202609100327" defer></script>
<script src="assets/js/ampel.js?v=202609100303" defer></script> <script src="assets/js/ampel.js?v=202609100327" defer></script>
<script src="assets/js/schulung.js?v=202609100303" defer></script> <script src="assets/js/schulung.js?v=202609100327" defer></script>
<script src="assets/js/vorlagen.js?v=202609100303" defer></script> <script src="assets/js/vorlagen.js?v=202609100327" defer></script>
<script src="assets/js/bereich.js?v=202609100303" defer></script> <script src="assets/js/bereich.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/calls.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -82,11 +82,11 @@
</main> </main>
<script src="assets/js/ki.js?v=202609100303" defer></script> <script src="assets/js/ki.js?v=202609100327" defer></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/calls.js?v=202609100303" defer></script> <script src="assets/js/calls.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,19 +12,19 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog, <!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem .neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html. zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. --> Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/chat.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -247,10 +247,10 @@
</form> </form>
</dialog> </dialog>
<script src="assets/js/bereiche.js?v=202609100303"></script> <script src="assets/js/bereiche.js?v=202609100327"></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303"></script> <script src="assets/js/kopf.js?v=202609100327"></script>
<script src="assets/js/glocke.js?v=202609100303"></script> <script src="assets/js/glocke.js?v=202609100327"></script>
<script src="assets/js/chat.js?v=202609100303"></script> <script src="assets/js/chat.js?v=202609100327"></script>
</body> </body>
</html> </html>
+14 -14
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/content.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/content.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -172,12 +172,12 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/checkliste.js?v=202609100303" defer></script> <script src="assets/js/checkliste.js?v=202609100327" defer></script>
<script src="assets/js/vorlagen.js?v=202609100303" defer></script> <script src="assets/js/vorlagen.js?v=202609100327" defer></script>
<script src="assets/js/content.js?v=202609100303" defer></script> <script src="assets/js/content.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/dateien.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -101,10 +101,10 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/dateien.js?v=202609100303" defer></script> <script src="assets/js/dateien.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+4 -4
View File
@@ -30,9 +30,9 @@
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp" <link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" /> type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
</head> </head>
<body class="gate"> <body class="gate">
@@ -401,6 +401,6 @@
</main> </main>
</div> </div>
<script src="assets/js/gate.js?v=202609100303" defer></script> <script src="assets/js/gate.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/kalender.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -370,11 +370,11 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/kalender.js?v=202609100303" defer></script> <script src="assets/js/kalender.js?v=202609100327" defer></script>
<script src="assets/js/kalender-abo.js?v=202609100303" defer></script> <script src="assets/js/kalender-abo.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/leistung.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -217,10 +217,10 @@
</form> </form>
</dialog> </dialog>
<script src="assets/js/wahl.js?v=202609100303"></script> <script src="assets/js/wahl.js?v=202609100327"></script>
<script src="assets/js/bereiche.js?v=202609100303"></script> <script src="assets/js/bereiche.js?v=202609100327"></script>
<script src="assets/js/kopf.js?v=202609100303"></script> <script src="assets/js/kopf.js?v=202609100327"></script>
<script src="assets/js/glocke.js?v=202609100303"></script> <script src="assets/js/glocke.js?v=202609100327"></script>
<script src="assets/js/leistung.js?v=202609100303"></script> <script src="assets/js/leistung.js?v=202609100327"></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/personen.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -335,10 +335,10 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/personen.js?v=202609100303" defer></script> <script src="assets/js/personen.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/profil.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -207,11 +207,11 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/steckbrief.js?v=202609100303" defer></script> <script src="assets/js/steckbrief.js?v=202609100327" defer></script>
<script src="assets/js/profil.js?v=202609100303" defer></script> <script src="assets/js/profil.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/report.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/report.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -99,11 +99,11 @@
</main> </main>
<script src="assets/js/ki.js?v=202609100303" defer></script> <script src="assets/js/ki.js?v=202609100327" defer></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/report.js?v=202609100303" defer></script> <script src="assets/js/report.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/scouting.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -132,10 +132,10 @@
</div> </div>
</div> </div>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/scouting.js?v=202609100303" defer></script> <script src="assets/js/scouting.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+11 -11
View File
@@ -12,14 +12,14 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe <!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. --> den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
<link rel="stylesheet" href="assets/css/heim.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/heim.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -667,10 +667,10 @@
</p> </p>
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/start.js?v=202609100303" defer></script> <script src="assets/js/start.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/startcheck.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -78,11 +78,11 @@
</main> </main>
<script src="assets/js/ki.js?v=202609100303" defer></script> <script src="assets/js/ki.js?v=202609100327" defer></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/startcheck.js?v=202609100303" defer></script> <script src="assets/js/startcheck.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/profil.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -119,10 +119,10 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/steckbrief.js?v=202609100303" defer></script> <script src="assets/js/steckbrief.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" /> <link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/team.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/team.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -103,10 +103,10 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/team.js?v=202609100303" defer></script> <script src="assets/js/team.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+10 -10
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" /> <link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/team.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/team.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -66,8 +66,8 @@
</main> </main>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/teamlage.js?v=202609100303" defer></script> <script src="assets/js/teamlage.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,15 +12,15 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der <!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". --> Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/uebersicht.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -142,11 +142,11 @@
</main> </main>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/uebersicht.js?v=202609100303" defer></script> <script src="assets/js/uebersicht.js?v=202609100327" defer></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. --> eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" /> <meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" /> <link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100327" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" /> <link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100327" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/gate.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/start.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/wissen.css?v=202609100327" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" /> <link rel="stylesheet" href="assets/css/module.css?v=202609100327" />
</head> </head>
<body class="start"> <body class="start">
@@ -183,10 +183,10 @@
</main> </main>
<script src="assets/js/wahl.js?v=202609100303" defer></script> <script src="assets/js/wahl.js?v=202609100327" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script> <script src="assets/js/bereiche.js?v=202609100327" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script> <script src="assets/js/kopf.js?v=202609100327" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script> <script src="assets/js/glocke.js?v=202609100327" defer></script>
<script src="assets/js/wissen.js?v=202609100303" defer></script> <script src="assets/js/wissen.js?v=202609100327" defer></script>
</body> </body>
</html> </html>