Compare commits

..
2 Commits
Author SHA1 Message Date
DogFatherGitandClaude Opus 5 3cda64568d Die Modi-App bekommt ihre eigene Adresse: crew.dogfather-universe.com
Filipe: "ich will dass es eine eigene app wird" -- und dann "subdomain
machen jetzt sofort". Name von ihm gewaehlt: crew. (unauffaellig).

Auf einem Ursprung laesst sich genau EINE App installieren (w3c/manifest
Nr. 1180). Derselbe Grund, aus dem der Workspace am 06.09. umgezogen ist.
Getrennt wird ueber den HOSTNAMEN: ein Dienst, eine Datenbank, ein
Verzeichnis wie bisher.

DIE REGEL STEHT AN EINER STELLE
crew-adresse.js beantwortet: Darf diese Rolle auf dieser Adresse
angemeldet sein? Eingehaengt in sitzungLesen() -- durch die Funktion geht
jedes der 26 Fachmodule und jede Seitenschranke. Eine Middleware daneben
kann man in einem neuen Modul vergessen, und ein vergessener Rechteschutz
faellt nicht auf, weil dann alles geht.

  crew.      nur Modis; jeder andere bekommt 401 wie bei einem Tippfehler
  workspace. keine Modis mehr; sie bekommen den Weg zur neuen Adresse
  localhost  UNVERAENDERT

Die dritte Zeile ist der Kern: Die Regel ist eine AUFZAEHLUNG der drei
echten Adressen, nicht "alles ausser crew". Sonst wuerden sieben andere
Pruefdateien ab sofort messen, dass ein Modi nirgends hereinkommt -- gruen,
weil sie nichts mehr finden.

ZWEI APPS, ZWEI NAMEN
Beide Adressen liefern dieselben HTML-Dateien. Auf crew. wird
/workspace/app.webmanifest serverseitig auf crew.webmanifest umgebogen --
so braucht keine der 30 Seiten eine zweite Zeile, die man bei der 31.
vergisst. "Team Dogi" statt "Creator Workspace", eigenes Zeichen:
derselbe Husky, aber ohne Chili (die gehoert zu Spicy Media, nicht zu
ihnen) und im Modi-Ton #5f8a9f.

tools/crew-symbol.mjs erzeugt die sechs Symbole und bricht ab, wenn sie
sich zu weniger als 10 % vom Workspace-Symbol unterscheiden. Gemessen:
43 bis 53 %.

GEMESSEN
pruef-crew-adresse    74 Pruefungen, 0 Fehler -- mit Gegenprobe: der Modi
                      bekommt in der Datenbank die Rolle 'manager', danach
                      MUSS dieselbe Sitzung auf crew. ins Leere laufen
pruef-rollen          245, unveraendert (die Regel ist lokal wirkungslos)
pruef-workspace-umzug 31 Faelle
pruef-zwischenspeicher 21 -- ein Stempel im Haus, jetzt ueber 23 Dateien

Der Server kennt die Adresse damit. DNS und Caddy bleiben Filipes Schritt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:07:12 +02:00
DogFatherGitandClaude Opus 5 05f262eb61 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]>
2026-09-10 12:52:45 +02:00
42 changed files with 2340 additions and 638 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

+184
View File
@@ -0,0 +1,184 @@
/* =====================================================================
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");
/* DAS MANIFEST MACHT DIE APP (10.09.2026).
Beide Adressen liefern dieselben HTML-Dateien aus -- in jeder steht
<link rel="manifest" href="/workspace/app.webmanifest">. Auf der
Modi-Adresse soll dahinter aber ein anderes Manifest stehen: eigener
Name ("Team Dogi"), eigenes Symbol. Sonst hiesse die App auf dem
Startbildschirm eines Modis "Creator Workspace" und traege das
Zeichen mit der Chili -- die zu Spicy Media gehoert, nicht zu ihnen.
WARUM DIE ADRESSE UMGEBOGEN WIRD, STATT IN 30 HTML-DATEIEN EINE
ZWEITE ZEILE ZU SETZEN: Eine zweite Zeile muesste jemand bei jeder
neuen Seite mitschreiben, und wer sie vergisst, bekommt eine Seite,
die still zur falschen App gehoert. Hier steht es einmal.
ZWISCHENSPEICHER: Cloudflare nimmt den Hostnamen in den Schluessel
auf, die beiden Antworten koennen sich also nicht vermischen. Der
Browser ohnehin nicht -- fuer ihn sind es zwei Ursprunge. */
if (req.path === "/workspace/app.webmanifest") {
req.url = "/workspace/crew.webmanifest";
return next();
}
if (istWorkspacePfad(req.path) || req.path.startsWith("/assets/")) return next();
return res.redirect(302, "/workspace/");
}
+15
View File
@@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
import { gateMiddleware } from "./gate.js";
import { workspaceUmzug } from "./workspace-umzug.js";
import { crewWeiche } from "./crew-adresse.js";
import { webdesignGate } from "./webdesign-gate.js";
import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js";
import { workspaceRouter, sichtSetzen } from "./workspace.js";
@@ -112,6 +113,20 @@ app.use((req, res, next) => {
unter der alten Adresse nicht mehr erscheint. */
app.use(workspaceUmzug);
/* Die eigene Adresse der Modi-App (10.09.2026): crew.dogfather-universe.com.
Begruendung und die drei Fallstricke stehen in crew-adresse.js -- sie sind
dort als eigene Funktionen pruefbar (pruef-crew-adresse.mjs).
Steht direkt neben dem Workspace-Umzug und aus demselben Grund FRUEH: Der
Kopf "nicht indizieren" soll auch fuer Manifest, Symbole und die
Zugangswand gelten, nicht erst ab der ersten Schranke.
WICHTIG: Diese Weiche vergibt KEINE Rechte und nimmt keine. Wer auf
welcher Adresse angemeldet sein darf, entscheidet sitzungLesen() in
workspace.js -- an genau einer Stelle, durch die alle 26 Fachmodule
gehen. Zwei Orte fuer dieselbe Regel waeren einer zu viel. */
app.use(crewWeiche);
/* Inhaltsrichtlinie (26.08.2026). Steht bewusst HIER -- vor jeder
Schranke und vor express.static: Auch die Zugangswand und die
404-Seite sollen sie bekommen, denn gerade dort werden Eingaben
+335
View File
@@ -0,0 +1,335 @@
/* =====================================================================
DIE EIGENE ADRESSE DER MODI-APP
Geprueft wird nicht "die neue Adresse geht", sondern die vier Fragen,
an denen dieser Umbau scheitern kann:
1. KOMMT EIN MODI AUF crew. HEREIN -- und sonst niemand?
2. IST DER WORKSPACE UNVERAENDERT? Derselbe Dienst bedient beide
Adressen. Wer nicht auf den Hostnamen prueft, sperrt mit einer
einzigen Zeile alle Creator, Scouts und Manager aus, sofort nach
dem Neustart und ohne Vorwarnung.
3. BLEIBEN DIE PRUEFADRESSEN UNBERUEHRT? Waere die Regel "alles
ausser crew", dann wuerden ab sofort sieben andere Pruefdateien
messen, dass ein Modi nirgends hereinkommt -- gruen, weil sie
nichts mehr finden. Ein Lauf, der nichts mehr prueft, ist
schlimmer als ein roter.
4. HEISST DIE APP AUF DEM HANDY DES MODIS ANDERS? Sonst war der
ganze Umzug umsonst.
MIT GEGENPROBE. Ganz unten bekommt der Modi in der Datenbank eine
andere Rolle -- danach MUSS ihn dieselbe Sitzung auf crew. nicht mehr
hereinlassen. Ohne diesen Teil waere nicht bewiesen, dass die
Pruefung ueberhaupt anschlagen kann.
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request } from "node:http";
const ordner = mkdtempSync(join(tmpdir(), "ws-crew-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4371";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(180_000, "pruef-crew-adresse");
await new Promise((r) => setTimeout(r, 700));
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
let offen = 0;
const unklar = (t) => { offen++; console.log(" -- " + t); };
/* ---------------------------------------------------------------------
Ein Aufruf mit frei waehlbarem Host-Kopf.
Bewusst node:http und nicht fetch: `Host` gilt in der Fetch-Norm als
verbotener Kopf, und ob undici das durchlaesst, ist eine Annahme, auf
die diese ganze Pruefung nicht gebaut sein soll. Hier wird der Kopf
nachweislich gesetzt.
------------------------------------------------------------------ */
function hole(pfad, { host, methode = "GET", rumpf = null, keks = "" } = {}) {
return new Promise((fertig, schief) => {
const kopf = { Host: host };
if (rumpf) { kopf["Content-Type"] = "application/json"; kopf["Content-Length"] = Buffer.byteLength(rumpf); }
if (keks) kopf.Cookie = keks;
const a = request({ host: "127.0.0.1", port: 4371, path: pfad, method: methode, headers: kopf },
(antwort) => {
let text = "";
antwort.setEncoding("utf8");
antwort.on("data", (t) => { text += t; });
antwort.on("end", () => fertig({
status: antwort.statusCode,
kopf: antwort.headers,
text,
keks: (antwort.headers["set-cookie"] || [])
.map((z) => z.split(";")[0]).join("; "),
}));
});
a.on("error", schief);
a.setTimeout(15_000, () => { a.destroy(new Error("Zeitueberschreitung")); });
if (rumpf) a.write(rumpf);
a.end();
});
}
const anmelden = (host, rolle, code) =>
hole("/workspace/api/anmelden", { host, methode: "POST", rumpf: JSON.stringify({ rolle, code }) });
/* ---------- Personen anlegen ---------------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
const kennung = (code) => createHmac("sha256", schluessel).update(code).digest("hex");
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen"
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, kennung(code), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Schulle", "manager", "CODE-MAN-0001");
const idModi = anlegen("Marina", "modi", "CODE-MODI-0001");
const CREW = "crew.dogfather-universe.com";
const WS = "workspace.dogfather-universe.com";
const HAUPT = "dogfather-universe.com";
const LOKAL = "127.0.0.1:4371";
/* =====================================================================
TEIL A -- DIE REGELN, OHNE SERVER
Sie stehen als eigene Funktionen in crew-adresse.js, damit genau das
hier moeglich ist: die Entscheidung pruefen, nicht ihre Auswirkung.
===================================================================== */
console.log("\n--- Die Regeln selbst ---");
const { istCrewAdresse, istOhneModiAdresse, istWorkspacePfad, sitzungPasstZurAdresse, CREW_ADRESSE }
= await import("./crew-adresse.js");
ok(istCrewAdresse(CREW) === true, "crew.dogfather-universe.com ist die Modi-Adresse");
ok(istCrewAdresse("CREW.Dogfather-Universe.COM") === true, "Gross/klein macht keinen Unterschied");
ok(istCrewAdresse(CREW + ":443") === true, "ein Port angehaengt macht keinen Unterschied");
ok(istCrewAdresse(WS) === false, "workspace. ist NICHT die Modi-Adresse");
ok(istCrewAdresse(HAUPT) === false, "die Hauptseite ist NICHT die Modi-Adresse");
ok(istCrewAdresse("crew.dogfather-universe.com.boese.de") === false,
"eine fremde Domain mit dem Namen darin zaehlt nicht");
ok(istCrewAdresse("") === false && istCrewAdresse(null) === false,
"leer und fehlend sind nicht die Modi-Adresse");
ok(istOhneModiAdresse(WS) === true, "auf workspace. haben Modis nichts verloren");
ok(istOhneModiAdresse(HAUPT) === true, "auf der Hauptseite ebenfalls nicht");
ok(istOhneModiAdresse("www." + HAUPT) === true, "auf www. ebenfalls nicht");
ok(istOhneModiAdresse(CREW) === false, "crew. gehoert NICHT dazu (Falle 1)");
ok(istOhneModiAdresse(LOKAL) === false, "127.0.0.1 gehoert NICHT dazu (Falle 2)");
ok(istOhneModiAdresse("localhost:4100") === false, "localhost gehoert NICHT dazu (Falle 2)");
/* Die beiden Mengen duerfen sich nirgends beruehren. Ein Muster statt
einer Liste haette hier sofort angeschlagen. */
const alleAdressen = [CREW, WS, HAUPT, "www." + HAUPT, LOKAL, "localhost:4100", "git." + HAUPT];
ok(alleAdressen.every((h) => !(istCrewAdresse(h) && istOhneModiAdresse(h))),
"keine Adresse liegt in beiden Mengen");
ok(istWorkspacePfad("/workspace") === true, "/workspace gehoert dazu");
ok(istWorkspacePfad("/workspace/start.html") === true, "/workspace/start.html gehoert dazu");
ok(istWorkspacePfad("/workspaceXYZ") === false, "/workspaceXYZ gehoert NICHT dazu (Falle 3)");
ok(istWorkspacePfad("/workspace-alt") === false, "/workspace-alt gehoert NICHT dazu (Falle 3)");
ok(istWorkspacePfad("/assets/img/x.png") === false, "/assets liegt ausserhalb");
const ROLLEN = ["admin", "spicy", "manager", "scout", "creator", "modi"];
let passtWahr = 0, passtFalsch = 0;
for (const r of ROLLEN) {
const aufCrew = sitzungPasstZurAdresse(r, CREW);
const aufWs = sitzungPasstZurAdresse(r, WS);
const aufLokal = sitzungPasstZurAdresse(r, LOKAL);
ok(aufCrew === (r === "modi"), `crew.: ${r} ${r === "modi" ? "darf" : "darf nicht"}`);
ok(aufWs === (r !== "modi"), `workspace.: ${r} ${r !== "modi" ? "darf" : "darf nicht"}`);
ok(aufLokal === true, `127.0.0.1: ${r} unveraendert erlaubt`);
for (const w of [aufCrew, aufWs, aufLokal]) { if (w) passtWahr++; else passtFalsch++; }
}
/* Die Gegenprobe zur Regel selbst: Eine Funktion, die immer `true`
liefert, haette alle 18 Zeilen darueber ebenfalls teilweise bestanden.
Beide Antworten muessen vorkommen. */
ok(passtWahr > 0 && passtFalsch > 0,
`die Regel unterscheidet wirklich (${passtWahr}x ja, ${passtFalsch}x nein)`);
/* =====================================================================
TEIL B -- AM LAUFENDEN SERVER
===================================================================== */
console.log("\n--- Anmeldung je Adresse ---");
const modiAufCrew = await anmelden(CREW, "modi", "CODE-MODI-0001");
ok(modiAufCrew.status === 200, "crew.: der Modi kommt herein");
let zielModi = {};
try { zielModi = JSON.parse(modiAufCrew.text); } catch { /* bleibt leer */ }
ok(zielModi.weiter === "/workspace/start.html", "crew.: er landet auf seiner Startseite");
ok(modiAufCrew.keks.includes("dfw_sitzung"), "crew.: er bekommt eine Sitzung");
/* Der Code des Modis wird bewusst mit der Kachel 'creator' geschickt --
genau so kommt er auch aus der Zugangswand, denn eine Modi-Kachel
gibt es nicht. */
const modiOhneKachel = await anmelden(CREW, "creator", "CODE-MODI-0001");
ok(modiOhneKachel.status === 200, "crew.: die gewaehlte Kachel ist egal (stiller Zugang)");
const managerAufCrew = await anmelden(CREW, "manager", "CODE-MAN-0001");
ok(managerAufCrew.status === 401, "crew.: ein Manager mit gueltigem Code kommt NICHT herein");
ok(/ungueltig/.test(managerAufCrew.text),
"crew.: er bekommt dieselbe Antwort wie bei einem Tippfehler");
ok(!managerAufCrew.keks.includes("dfw_sitzung"), "crew.: und keine Sitzung");
const adminAufCrew = await anmelden(CREW, "admin", "CODE-DOGI-0001");
ok(adminAufCrew.status === 401, "crew.: auch DogFather selbst kommt dort nicht herein");
const modiAufWs = await anmelden(WS, "creator", "CODE-MODI-0001");
ok(modiAufWs.status === 200, "workspace.: der Modi bekommt eine Antwort");
let zielWs = {};
try { zielWs = JSON.parse(modiAufWs.text); } catch { /* bleibt leer */ }
ok(zielWs.weiter === `${CREW_ADRESSE}/workspace/start.html`,
"workspace.: sie zeigt ihm die neue Adresse");
ok(!modiAufWs.keks.includes("dfw_sitzung"),
"workspace.: aber KEINE Sitzung -- er meldet sich nur noch auf crew. an");
/* DER WICHTIGSTE PUNKT DIESER DATEI. Derselbe Dienst bedient beide
Adressen; ein Denkfehler hier sperrt das ganze Team aus. */
const managerAufWs = await anmelden(WS, "manager", "CODE-MAN-0001");
ok(managerAufWs.status === 200 && managerAufWs.keks.includes("dfw_sitzung"),
"workspace.: der Manager kommt herein wie immer -- der Workspace ist unberuehrt");
const managerLokal = await anmelden(LOKAL, "manager", "CODE-MAN-0001");
const modiLokal = await anmelden(LOKAL, "creator", "CODE-MODI-0001");
ok(managerLokal.status === 200 && managerLokal.keks.includes("dfw_sitzung"),
"127.0.0.1: der Manager kommt herein (Pruefadressen unberuehrt)");
ok(modiLokal.status === 200 && modiLokal.keks.includes("dfw_sitzung"),
"127.0.0.1: der Modi ebenfalls -- die sieben anderen Modi-Pruefungen messen weiter");
/* ---------------------------------------------------------------------
Eine Sitzung gehoert zu genau einer Adresse.
------------------------------------------------------------------ */
console.log("\n--- Eine Sitzung, eine Adresse ---");
const keksModi = modiAufCrew.keks;
const keksManager = managerAufWs.keks;
const modiDaheim = await hole("/workspace/api/ich", { host: CREW, keks: keksModi });
ok(modiDaheim.status === 200, "crew.: der angemeldete Modi wird erkannt");
ok(/Marina/.test(modiDaheim.text), "crew.: und zwar als er selbst");
const modiFremd = await hole("/workspace/api/ich", { host: WS, keks: keksModi });
ok(modiFremd.status === 401, "workspace.: dieselbe Sitzung zaehlt dort nicht");
const managerDaheim = await hole("/workspace/api/ich", { host: WS, keks: keksManager });
ok(managerDaheim.status === 200, "workspace.: der Manager wird erkannt");
const managerFremd = await hole("/workspace/api/ich", { host: CREW, keks: keksManager });
ok(managerFremd.status === 401, "crew.: seine Sitzung zaehlt dort nicht");
/* Auch die Seitenschranke, nicht nur die Schnittstelle: Eine Seite, die
ausgeliefert wird, waere ein Loch, selbst wenn die Daten dahinter
leer blieben. */
const seiteFremd = await hole("/workspace/start.html", { host: CREW, keks: keksManager });
ok(seiteFremd.status === 302, "crew.: die Startseite schickt den Manager zur Zugangswand");
const seiteDaheim = await hole("/workspace/start.html", { host: CREW, keks: keksModi });
ok(seiteDaheim.status === 200, "crew.: dem Modi liefert sie die Seite");
/* ---------------------------------------------------------------------
Die Weiche: was es auf crew. ueberhaupt gibt.
------------------------------------------------------------------ */
console.log("\n--- Die Weiche ---");
const wurzel = await hole("/", { host: CREW });
ok(wurzel.status === 302 && wurzel.kopf.location === "/workspace/",
"crew.: die Hauptseite gibt es dort nicht, es geht zur Zugangswand");
const wurzelWs = await hole("/", { host: HAUPT });
ok(wurzelWs.status === 200, "die Hauptseite selbst ist unveraendert erreichbar");
const fastWorkspace = await hole("/workspaceXYZ", { host: CREW });
ok(fastWorkspace.status === 302, "crew.: /workspaceXYZ gehoert nicht dazu (Falle 3)");
const robots = await hole("/workspace/", { host: CREW });
ok(String(robots.kopf["x-robots-tag"] || "").includes("noindex"),
"crew.: nicht indizieren -- die Adresse soll in keiner Suchmaschine stehen");
const robotsWs = await hole("/workspace/", { host: WS });
ok(!robotsWs.kopf["x-robots-tag"],
"workspace.: dort steht der Kopf NICHT (Gegenprobe zur Weiche)");
/* ---------------------------------------------------------------------
Die App heisst anders.
------------------------------------------------------------------ */
console.log("\n--- Zwei Apps, zwei Namen ---");
const manifestCrew = await hole("/workspace/app.webmanifest", { host: CREW });
const manifestWs = await hole("/workspace/app.webmanifest", { host: WS });
let mc = {}, mw = {};
try { mc = JSON.parse(manifestCrew.text); } catch { /* bleibt leer */ }
try { mw = JSON.parse(manifestWs.text); } catch { /* bleibt leer */ }
ok(manifestCrew.status === 200 && manifestWs.status === 200,
"beide Adressen liefern ein Manifest");
ok(mc.name === "Team Dogi", `crew.: die App heisst "Team Dogi" (gelesen: ${mc.name || "?"})`);
ok(mw.name === "Creator Workspace — Spicy & Dogi",
`workspace.: dort heisst sie unveraendert (gelesen: ${mw.name || "?"})`);
ok(Array.isArray(mc.icons) && mc.icons.length > 0
&& mc.icons.every((s) => /crew-/.test(s.src)),
`crew.: alle ${(mc.icons || []).length} Symbole sind die eigenen`);
ok(Array.isArray(mw.icons) && mw.icons.length > 0
&& mw.icons.every((s) => /workspace-/.test(s.src)),
"workspace.: dort weiterhin die alten");
ok(mc.start_url === "/workspace/start.html" && mc.scope === "/workspace/",
"crew.: Bereich und Startseite stimmen");
/* Die Symboldateien muessen es auch wirklich geben -- ein Manifest, das
auf nichts zeigt, faellt sonst erst auf dem Startbildschirm auf. */
let symboleDa = 0;
for (const s of mc.icons || []) {
const pfad = String(s.src).split("?")[0];
const a = await hole(pfad, { host: CREW });
if (a.status === 200) symboleDa++;
}
ok((mc.icons || []).length > 0 && symboleDa === (mc.icons || []).length,
`crew.: alle ${symboleDa} Symbole werden auch ausgeliefert`);
/* =====================================================================
DIE GEGENPROBE
Bis hierhin koennte alles auch dann gruen sein, wenn die Regel gar
nicht auf die Rolle sieht. Deshalb wird sie jetzt ausgehebelt: Der
Modi bekommt in der Datenbank die Rolle 'manager'. Seine Sitzung
bleibt dieselbe -- aber auf crew. darf sie ab jetzt NICHT mehr
gelten.
===================================================================== */
console.log("\n--- Gegenprobe: die Regel ausgehebelt ---");
d.prepare("UPDATE personen SET rolle = 'manager' WHERE id = ?").run(idModi);
const nachher = await hole("/workspace/api/ich", { host: CREW, keks: keksModi });
ok(nachher.status === 401,
"aus dem Modi wurde ein Manager -- seine Sitzung gilt auf crew. nicht mehr");
const nachherWs = await hole("/workspace/api/ich", { host: WS, keks: keksModi });
ok(nachherWs.status === 200,
"und dieselbe Sitzung gilt jetzt auf workspace. -- die Regel sieht wirklich auf die Rolle");
d.prepare("UPDATE personen SET rolle = 'modi' WHERE id = ?").run(idModi);
const zurueck = await hole("/workspace/api/ich", { host: CREW, keks: keksModi });
ok(zurueck.status === 200, "zurueckgedreht gilt sie wieder");
/* ---------- Ende ------------------------------------------------------ */
d.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt die Datei manchmal */ }
console.log(`\n${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}`
+ ` -- ${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
process.exit(fehler === 0 ? 0 : 1);
+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
seite auch noch auf die modis perfektionniert werden, da mussen
lauter sachen sein die vanvan und ich danach verteilen können und da
anpassen können ob gut oder nicht."*
Wunsch Filipe (10.09.2026): *"diese 3 kategorien müssen so
eingestellt werden dass 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."* 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?
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?
FUENF FRAGEN:
Die vierte ist die heikelste. `darfCreator` sagt bei siehtAlles()
pauschal ja -- und darin steckt auch Spicy Media. Wer die
Modi-Auswahl daran haengt, oeffnet sie ihr nebenbei mit, ohne dass an
der Stelle etwas davon steht.
1. Beurteilt der Modi die SENDUNG -- und darf er druecken?
2. Duerfen DogFather und VanVan seinen Bericht NICHT ueberschreiben?
3. Landet ein "Verbessern" im Eingang der beiden?
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 { 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 idModi = anlegen("Marina", "modi", "CODE-MODI-0001");
d.close();
void idDogi;
void idSpicy;
async function anmelden(rolle, code) {
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 creatAn = await anmelden("creator", "CODE-ANNA-0001");
/* ---------- 1. Sein eigener Satz ------------------------------------- */
console.log("\n=== 1. Der Modi bekommt seinen eigenen Punktesatz ===\n");
/* ---------- 1. Er beurteilt die Sendung ------------------------------ */
console.log("\n=== 1. Der Modi beurteilt die Sendung ===\n");
const BEREICHE = ["live", "community", "technik"];
let ersterPunkt = null;
for (const b of BEREICHE) {
const a = await hole(modiAn.keks, `/workspace/api/checkliste/${b}`);
if (a.status !== 200) { unklar(`${b}: nicht lesbar (${a.status})`); continue; }
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-")),
`${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;
/* Und er sieht SEINE Liste, ohne jemanden auszuwaehlen. */
ok(a.daten?.creator_id === idModi,
`${b.padEnd(10)} es ist seine eigene Liste (${a.daten?.creator_id})`);
ok(a.daten?.darf_bewerten === false,
`${b.padEnd(10)} bewerten darf er sich nicht selbst`);
ok(a.daten?.creator_id === idModi, `${b.padEnd(10)} es ist seine eigene Liste`);
/* DAS IST DIE UMKEHRUNG: Er darf druecken. Vorher stand hier das
Gegenteil, und das war der Fehler. */
ok(a.daten?.darf_bewerten === true,
`${b.padEnd(10)} und er darf "Passt so" oder "Verbessern" drücken`);
}
/* GEGENPROBE: Ein Creator bekommt weiterhin SEINEN Satz -- sonst waere
oben auch gruen, wenn ich allen denselben gegeben haette. */
/* GEGENPROBE: Der Creator bekommt unveraendert seinen Satz -- und darf
sich weiterhin NICHT selbst bewerten. Sonst waere oben auch gruen,
wenn ich allen alles erlaubt haette. */
{
const a = await hole(creatAn.keks, "/workspace/api/checkliste/live");
const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []);
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 ------------------ */
console.log("\n=== 2. DogFather und VanVan sehen ihn ===\n");
/* ---------- 2. Sein Bericht gehört ihm ------------------------------- */
console.log("\n=== 2. Sein Bericht gehört ihm ===\n");
for (const [name, an] of [["DogFather", dogiAn], ["VanVan", vanAn]]) {
const a = await hole(an.keks, "/workspace/api/checkliste/live");
const drin = (a.daten?.creatoren || []).some((z) => z.id === idModi);
ok(drin, `${name.padEnd(10)} hat ihn in der Auswahl`);
const b = await hole(an.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`);
const punkte = (b.daten?.gruppen || []).flatMap((g) => g.punkte || []);
ok(punkte.length > 0 && punkte.every((p) => p.schluessel.startsWith("m-")),
`${name.padEnd(10)} bekommt SEINEN Satz, nicht den der Creator (${punkte.length})`);
ok(b.daten?.darf_bewerten === true, `${name.padEnd(10)} darf bewerten`);
}
/* ---------- 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}`, {
const a = await hole(an.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`);
const punkte = (a.daten?.gruppen || []).flatMap((g) => g.punkte || []);
ok(punkte.length > 0, `${name.padEnd(10)} sieht seinen Bericht (${punkte.length} Punkte)`);
/* NICHT ueberschreiben. Duerften sie es, waere es kein Bericht mehr,
sondern eine Abstimmung darueber, was er gesehen haben soll. */
ok(a.daten?.darf_bewerten === false,
`${name.padEnd(10)} kann ihn aber nicht überschreiben`);
const b = await hole(an.keks, `/workspace/api/checkliste/live/${ersterPunkt}`, {
method: "PUT", headers: jsonKopf,
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})`);
}
/* ---------- 5. Die gemeinsame Lage ----------------------------------- */
console.log("\n=== 5. Die Seite für DogFather und VanVan ===\n");
/* ---------- 3. Er meldet etwas --------------------------------------- */
console.log("\n=== 3. Er drückt „Verbessern“ ===\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]]) {
const a = await hole(an.keks, "/workspace/api/teamlage");
if (a.status !== 200) { unklar(`${name}: nicht lesbar (${a.status})`); continue; }
const eintrag = (a.daten?.modis || []).find((m) => m.id === idModi);
ok(!!eintrag, `${name.padEnd(10)} sieht den Modi in der Lage`);
/* Die Zahlen muessen zum Punktesatz passen -- eine Uebersicht, die
andere Zahlen zeigt als die Seite dahinter, ist schlimmer als
keine. */
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`);
const b = await hole(an.keks, "/workspace/api/teamlage");
const drin = (b.daten?.eingang || []).find((e) => e.schluessel === ersterPunkt);
ok(!!drin, `${name.padEnd(10)} hat sie im Eingang`);
ok(drin?.begruendung === "Kam erst zehn Minuten vorher",
`${name.padEnd(10)} mit seinem Satz dabei ("${drin?.begruendung || ""}")`);
ok(!!drin?.titel && drin.titel !== ersterPunkt,
`${name.padEnd(10)} und dem Titel, nicht dem Schlüssel ("${drin?.titel || ""}")`);
}
/* 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 -------------------- */
console.log("\n=== 6. Und für alle anderen gibt es sie nicht ===\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. */
/* ---------- 4. Daraus wird eine Aufgabe ------------------------------ */
console.log("\n=== 4. Daraus wird eine Aufgabe ===\n");
{
const a = await hole(dogiAn.keks, "/workspace/api/ich");
ok((a.daten?.bereiche_zusatz || []).length === 1,
`bei DogFather ist genau eine Zusatzkachel da (${(a.daten?.bereiche_zusatz || []).length})`);
const a = 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(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"} -- `
+ `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
+61 -5
View File
@@ -169,6 +169,47 @@ export const BEREICHE = {
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: {
name: "Agentur",
arten: {
@@ -235,10 +276,12 @@ bereicheRouter.use("/workspace/api/bereich", angemeldet);
ZWEI SCHLOESSER, ABSICHTLICH: Hier haengt der ZUGANG zur Seite,
sichtbar() haengt die Bedingung an die ZEILEN. Faellt eines weg,
haelt das andere. */
bereicheRouter.use("/workspace/api/bereich/ideen", (req, res, next) => {
for (const geschuetzt of ["ideen", "angebote"]) {
bereicheRouter.use(`/workspace/api/bereich/${geschuetzt}`, (req, res, next) => {
if (siehtModis(req.person)) return next();
return res.status(404).json({ fehler: "nicht_gefunden" });
});
});
}
/* ---------------------------------------------------------------------
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.event_ende, e.event_aufgaben, e.event_regeln,
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,
pe.name AS erstellt_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()). */
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).
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
(bereich, art, titel, text, datum, bewertung, dringlichkeit, status,
creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant,
event_ende, event_aufgaben, event_regeln)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
event_ende, event_aufgaben, event_regeln, einsatz)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
bereich, aus.art, aus.titel, aus.text ?? null, aus.datum,
aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen",
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.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", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
+92 -8
View File
@@ -38,7 +38,7 @@ import {
protokolliere, echteIp,
siehtModis,
} from "./workspace.js";
import { MODI_PUNKTE } from "./workspace-modi-punkte.js";
import { MODI_PUNKTE, MODI_GRUPPENTEXTE } from "./workspace-modi-punkte.js";
import {
CONTENT_IDEEN, LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE,
} from "./workspace-vorlagen.js";
@@ -147,8 +147,17 @@ function gruppenFuer(bereich, fuerModi) {
/* Eine Gruppe ohne Punkte faellt heraus statt leer dazustehen -- ein
Abschnitt mit einer Ueberschrift und nichts darunter sieht aus wie
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
.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);
}
@@ -197,6 +206,38 @@ function tabellen() {
CREATE INDEX IF NOT EXISTS idx_punkt_nachricht
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;
}
@@ -343,10 +384,29 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => {
/* Die Oberflaeche soll nicht raten muessen, wer was darf -- sonst
zeigt sie einem Creator einen Knopf, der beim Klicken 403 sagt.
Das ist schlechter als kein Knopf. */
/* Wer an der EIGENEN Liste arbeitet, bewertet sie nicht selbst --
weder Creator noch Modi. Die Stufe setzt die Betreuung
beziehungsweise die DogFather-Rolle. */
darf_bewerten: req.person.rolle !== "creator" && req.person.rolle !== "modi",
/* =============================================================
WER DRUECKT, HAENGT DAVON AB, WESSEN LISTE ES IST
(umgedreht am 10.09.2026)
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,
});
} catch (fehler) {
@@ -362,7 +422,23 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe
try {
/* Ein Creator bewertet sich nicht selbst. Waere das moeglich,
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({
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
aussagt, ist Ballast. */
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 = ?")
.run(bereich, schluessel, creatorId);
} else {
@@ -399,7 +477,13 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe
VALUES (?,?,?,?,?,?,?)
ON CONFLICT(bereich, schluessel, creator_id) DO UPDATE SET
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());
}
+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
auf die modis perfektionniert werden, da mussen lauter sachen sein
die vanvan und ich danach verteilen können und da anpassen können ob
gut oder nicht."*
Wunsch Filipe: *"diese 3 kategorien müssen so eingestellt werden dass
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."*
WARUM EIN ZWEITER SATZ UND NICHT EIN ERWEITERTER: Die vorhandenen
Punkte (LIVE_PUNKTE, COMMUNITY_PUNKTE, TECHNIK_PUNKTE) sind fuer
CREATOR geschrieben -- "Upload messen", "Sendeplan festlegen",
"Verweildauer vergleichen". Ein Modi sendet nicht, er moderiert. Ihm
dieselbe Liste vorzulegen hiesse, ihn an Dingen zu messen, die nicht
seine Arbeit sind.
DIE RICHTUNG WAR VORHER FALSCH HERUM. Beim ersten Anlauf standen hier
Punkte ueber die Arbeit des MODIS -- "Schicht bestaetigt", "Chat im
Blick" -- und DogFather sollte sie bewerten. Das war eine Note fuer
ihn, keine Hilfe fuer DogFather. Und es widerspricht der Hausregel,
nach der niemand ueber dem Team steht.
DIESELBEN DREI BEREICHE UND DIESELBEN GRUPPEN wie beim Creator --
Vor/Waehrend/Nach, Moderation/Aktion/Konflikt, Setup/Problem/Loesung.
Das ist Absicht: Zwei verschiedene Einteilungen fuer dieselbe Sache
waeren zwei Dinge zum Lernen statt einem, und die Oberflaeche kaeme
ohne Umbau nicht damit zurecht.
JETZT BEURTEILT DER MODI DIE SENDUNG. Er sitzt im Chat, er sieht und
hoert, was die Zuschauer sehen und hoeren -- frueher und ungefilterter
als jeder, der gerade sendet. Er drueckt "Passt so" oder
"Verbessern", schreibt einen Satz dazu, und daraus wird ein Vorschlag
fuer DogFather.
DIESELBEN VIER STUFEN (anfaenger, fort, profi, meister). Sie
entscheiden, was jemandem zuerst gezeigt wird. Ein Modi in der
Probezeit soll nicht an "Muster ueber mehrere Sendungen erkennen"
gemessen werden.
DIE PUNKTE SIND DESHALB BEOBACHTUNGEN, KEINE PFLICHTEN. Nicht "Ton
pruefen", sondern "Der Ton war durchgehend verstaendlich". Der
Unterschied ist nicht sprachlich: Eine Pflicht kann man erfuellen
oder versaeumen, eine Beobachtung kann man nur machen -- und
niemandem vorwerfen.
DIE SCHLUESSEL BEGINNEN MIT "m-". Der Stand haengt in `punkt_stand`
an (bereich, schluessel, person) -- ein Modi und ein Creator im
selben Bereich duerfen sich also nicht in die Quere kommen. Mit dem
Praefix kann das nicht passieren, auch wenn jemand spaeter einen
Punkt gleich benennt.
WAS EIN MODI SELBST ZU TUN HAT, steht woanders: im Aufgabenkatalog
aus Teil 2 des Anforderungsdokuments (60 Aufgaben, neun Etappen).
Das ist die richtige Stelle dafuer -- eine Aufgabe hat eine Frist und
einen Status, eine Beobachtung nicht.
BEWERTET WIRD NICHT VON IHM SELBST, sondern von der DogFather-Rolle
-- offen / gut / verbessern, wie beim Creator auch. Genau das war
Filipes Wunsch: "verteilen ... und anpassen ob gut oder nicht".
DIE VIER STUFEN bleiben. Sie sagen hier, wie GENAU jemand hinsieht:
"Ton war da" sieht jeder, "es gab einen Grund wiederzukommen" sieht,
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 = {
vorbereitung: [
{ schluessel: "m-schicht-bestaetigt", titel: "Schicht bestätigt", stufe: "anfaenger",
text: "Vor dem Tag gesagt, ob du kannst. Ein „ich schau mal“ ist keine Zusage – und das Team plant danach." },
{ schluessel: "m-ankuendigung-gesehen", titel: "Ankündigung gesehen", stufe: "anfaenger",
text: "Du weißt, was draußen versprochen wurde. Sonst beantwortest du im Chat Fragen zu etwas, das du nicht kennst." },
{ schluessel: "m-ablauf-gelesen", titel: "Ablaufplan gelesen", stufe: "anfaenger",
text: "Anfang, Mitte, Schluss – grob im Kopf. Wer den Ablauf kennt, kann den Chat darauf vorbereiten." },
{ schluessel: "m-regeln-griffbereit", titel: "Regeln griffbereit", stufe: "anfaenger",
text: "Der Link oder der Text, den du hineinstellst, ohne zu suchen. Im Ernstfall zählen Sekunden." },
{ schluessel: "m-eigene-technik", titel: "Eigene Technik geprüft", stufe: "anfaenger",
text: "Dein Gerät, deine Verbindung, dein Zugang. Ein Modi, der mittendrin herausfliegt, fehlt genau dann." },
{ schluessel: "m-wer-ist-dabei", titel: "Wer moderiert heute noch?", stufe: "fort",
text: "Zu zweit teilt man sich auf – wer begrüßt, wer greift ein. Allein macht man beides halb." },
{ schluessel: "m-ziel-bekannt", titel: "Ziel der Sendung bekannt", stufe: "fort",
text: "Eine Zahl oder eine Sache. Darauf kannst du im Chat hinweisen, ohne zu fragen." },
{ schluessel: "m-gaeste-bekannt", titel: "Gäste bekannt", stufe: "fort",
text: "Wer kommt, wann, und wie heißt er richtig. Ein falsch geschriebener Name im Chat bleibt hängen." },
{ schluessel: "m-wortfilter-durchgesehen", titel: "Wortfilter durchgesehen", stufe: "fort",
text: "Was letzte Woche durchrutschte, gehört heute hinein. Ein Filter, den niemand pflegt, veraltet." },
{ schluessel: "m-eskalationsweg", titel: "Eskalationsweg im Kopf", stufe: "fort",
text: "Ab wann gibst du weiter, statt selbst zu entscheiden – und an wen. Vorher klären, nicht im Moment." },
{ schluessel: "m-begruessung-bereit", titel: "Begrüßungssätze bereit", stufe: "profi",
text: "Drei verschiedene, nicht einer dreimal. Wer merkt, dass ein Baustein antwortet, antwortet nicht zurück." },
{ schluessel: "m-umfrage-vorbereitet", titel: "Umfrage vorbereitet", stufe: "profi",
text: "Höchstens drei Antworten, vorher aufgeschrieben. Mitten im Stream formuliert man keine gute Frage." },
{ schluessel: "m-vertretung-geklaert", titel: "Vertretung geklärt", stufe: "profi",
text: "Wer übernimmt, wenn du wegmusst. Ein Chat ohne Aufsicht ist schlimmer als einer, der zu ist." },
{ schluessel: "m-uebergabe-vorbereitet", titel: "Übergabe an die nächste Schicht", stufe: "meister",
text: "Was die andere Person wissen muss, bevor du gehst. Zwei Zeilen reichen – aber sie müssen da sein." },
{ schluessel: "m-ankuendigung-rechtzeitig", titel: "Die Ankündigung kam rechtzeitig", stufe: "anfaenger",
text: "30 bis 60 Minuten vorher. Später erreicht sie die nicht mehr, die sich den Abend noch einteilen." },
{ schluessel: "m-titel-sagt-worum", titel: "Der Titel sagte, worum es geht", stufe: "anfaenger",
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-startzeit-gehalten", titel: "Die Startzeit wurde gehalten", stufe: "anfaenger",
text: "Wer zur angekündigten Zeit kommt und nichts vorfindet, kommt beim nächsten Mal später – oder nicht." },
{ schluessel: "m-ton-von-anfang-an", titel: "Der Ton war von Anfang an da", stufe: "anfaenger",
text: "Die ersten Sekunden ohne Ton kosten mehr Zuschauer als jede spätere Panne." },
{ schluessel: "m-bild-hell-genug", titel: "Das Bild war hell genug", stufe: "anfaenger",
text: "Auf dem Handy sieht es dunkler aus als am Rechner. Wenn du das Gesicht schlecht erkennst, erkennen es andere gar nicht." },
{ schluessel: "m-einstieg-vorhanden", titel: "Es gab einen Einstieg", stufe: "fort",
text: "Nicht mitten im Satz anfangen. Wer in den ersten Sekunden nicht weiß, wo er gelandet ist, geht wieder." },
{ schluessel: "m-thema-frueh-klar", titel: "Das Thema war früh klar", stufe: "fort",
text: "Spätestens nach zwei Minuten. Alles danach ist Suchen, und Suchen macht man woanders." },
{ schluessel: "m-chat-wusste-bescheid", titel: "Der Chat wusste, was kommt", stufe: "fort",
text: "Ein Satz zum Ablauf am Anfang spart dir zwanzig Fragen später." },
{ schluessel: "m-technikcheck-war-durch", titel: "Der Technik-Check war vorher durch", stufe: "fort",
text: "Man merkt es daran, ob in den ersten Minuten noch geschraubt wird. Das sehen die Zuschauer auch." },
{ schluessel: "m-gaeste-angekuendigt", titel: "Gäste waren angekündigt", stufe: "fort",
text: "Wer im Chat sitzt, will wissen, wer da ist – und du willst den Namen richtig schreiben können." },
{ schluessel: "m-einstieg-zog", titel: "Der Einstieg hat gezogen", stufe: "profi",
text: "Blieben die ersten, die kamen? Über 70 Prozent entscheiden in den ersten Sekunden." },
{ schluessel: "m-plan-erkennbar", titel: "Ein Plan war erkennbar", stufe: "profi",
text: "Nicht als Liste vorgelesen, aber spürbar. Ein Abend ohne Richtung fühlt sich für Zuschauer wie Warten an." },
{ schluessel: "m-ankuendigung-passte", titel: "Die Ankündigung passte zum Inhalt", stufe: "profi",
text: "Wer wegen etwas anderem gekommen ist, bleibt nicht – und kommt beim nächsten Mal nicht wieder." },
{ schluessel: "m-start-wiederholbar", titel: "Der Start war wiederholbar", stufe: "meister",
text: "So gut, dass man ihn genauso noch einmal machen könnte. Genau das macht aus einem guten Abend ein Format." },
],
mitschrift: [
{ schluessel: "m-chat-im-blick", titel: "Chat wirklich im Blick", stufe: "anfaenger",
text: "Nicht nebenbei. Wer mitschaut statt mitliest, sieht den Verstoß erst, wenn zehn Leute geantwortet haben." },
{ schluessel: "m-neue-begruesst", titel: "Neue begrüßt", stufe: "anfaenger",
text: "Mit Namen, wenn es geht. Wer beim ersten Mal angesprochen wird, kommt deutlich häufiger wieder." },
{ schluessel: "m-spam-sofort-weg", titel: "Spam sofort weg", stufe: "anfaenger",
text: "Löschen, bevor jemand antwortet. Eine Diskussion über einen gelöschten Beitrag kostet mehr als der Beitrag." },
{ schluessel: "m-fragen-gesammelt", titel: "Fragen gesammelt", stufe: "anfaenger",
text: "An einer Stelle, nicht im Kopf. Was im Chat vorbeirauscht, ist nach zwei Minuten weg." },
{ schluessel: "m-auf-ziel-hingewiesen", titel: "Auf das Ziel hingewiesen", stufe: "fort",
text: "Lieber zweimal beiläufig als einmal ausführlich. Wer später dazukommt, hat es sonst nie gehört." },
{ schluessel: "m-interaktion-angestossen", titel: "Interaktion angestoßen", stufe: "fort",
text: "Eine Frage in den Chat, wenn es still wird. Stille im Chat sieht für Neue aus wie ein leerer Raum." },
{ schluessel: "m-clipmomente-markiert", titel: "Clip-Momente markiert", stufe: "fort",
text: "Mit Uhrzeit, sofort. Am nächsten Tag findet niemand die Stelle wieder, an der alle gelacht haben." },
{ schluessel: "m-zeit-im-blick", titel: "Zeit im Blick behalten", stufe: "fort",
text: "Ein kurzes Zeichen genügt. Wer sendet, hat kein Gefühl für die Uhr – das ist normal." },
{ schluessel: "m-verwarnen-vor-sperren", titel: "Erst verwarnen, dann sperren", stufe: "profi",
text: "Außer bei den Sachen, die sofort gehen müssen. Eine Verwarnung, die jeder sieht, wirkt auf mehr als einen." },
{ schluessel: "m-stammzuschauer-angesprochen", titel: "Stammzuschauer angesprochen", stufe: "profi",
text: "Wiedererkannt zu werden ist der halbe Grund wiederzukommen." },
{ schluessel: "m-eskalation-weitergegeben", titel: "Eskalation weitergegeben", stufe: "profi",
text: "Nach der Regel, nicht nach Gefühl. Allein zu entscheiden, wo die Regel etwas anderes sagt, bringt dich in die Schusslinie." },
{ schluessel: "m-nichts-oeffentlich-ausdiskutiert", titel: "Nichts öffentlich ausdiskutiert", stufe: "profi",
text: "Im Chat gewinnt niemand. Kurz sagen, was gilt, und weiter – der Rest gehört ins Team." },
{ schluessel: "m-stimmung-gemeldet", titel: "Stimmung gemeldet", stufe: "meister",
text: "Wenn es kippt, bevor es kippt. Du siehst es früher als jeder, der gerade sendet." },
{ schluessel: "m-ruhig-geblieben", titel: "Ruhig geblieben, als es eng wurde", stufe: "meister",
text: "Der Ton eines Modis wird zum Ton des Chats. Das ist der schwerste Punkt auf dieser Liste." },
{ schluessel: "m-ton-durchgehend", titel: "Der Ton blieb verständlich", stufe: "anfaenger",
text: "Durchgehend, nicht überwiegend. Ein Aussetzer alle zehn Minuten reicht, damit jemand geht." },
{ schluessel: "m-bild-ohne-ruckeln", titel: "Das Bild lief ohne Ruckeln", stufe: "anfaenger",
text: "Wenn es bei dir ruckelt, ruckelt es bei vielen. Notiere die Uhrzeit – daran erkennt man die Ursache." },
{ schluessel: "m-auf-chat-reagiert", titel: "Auf den Chat kam Reaktion", stufe: "anfaenger",
text: "Nicht auf alles, aber sichtbar. Ein Chat, der nie vorkommt, hört auf zu schreiben." },
{ schluessel: "m-namen-richtig", titel: "Namen wurden richtig gesagt", stufe: "anfaenger",
text: "Wer falsch angesprochen wird, korrigiert einmal und schweigt dann." },
{ schluessel: "m-roter-faden", titel: "Der rote Faden war erkennbar", stufe: "fort",
text: "Auch für jemanden, der nach 40 Minuten dazukommt. Der weiß sonst nicht, worum es geht." },
{ schluessel: "m-fragen-beantwortet", titel: "Fragen bekamen eine Antwort", stufe: "fort",
text: "Oder wenigstens ein „später“. Eine übergangene Frage wird kein zweites Mal gestellt." },
{ schluessel: "m-ziel-erwaehnt", titel: "Das Ziel wurde erwähnt", stufe: "fort",
text: "Mindestens zweimal, beiläufig. Wer erst spät dazukommt, hat es sonst nie gehört." },
{ schluessel: "m-pausen-angesagt", titel: "Pausen waren angesagt", stufe: "fort",
text: "Ein stiller Bildschirm ohne Ansage sieht aus wie ein Absturz." },
{ schluessel: "m-tempo-stimmte", titel: "Das Tempo stimmte", stufe: "profi",
text: "Nicht gehetzt, nicht zäh. Du merkst es am Chat: Wird er still, war es zu lang." },
{ schluessel: "m-auf-stimmung-reagiert", titel: "Auf die Stimmung wurde reagiert", stufe: "profi",
text: "Wenn der Chat kippt oder einschläft, muss etwas passieren. Du siehst es zuerst." },
{ schluessel: "m-gaeste-eingebunden", titel: "Gäste wurden eingebunden", stufe: "profi",
text: "Ein Gast, der zwanzig Minuten zuhört, ist kein Gast. Das fällt im Chat sofort auf." },
{ schluessel: "m-chat-nicht-ueberhoert", titel: "Der Chat wurde nicht überhört", stufe: "profi",
text: "Gerade wenn es voll wird. Genau dann fühlen sich die Leisen übergangen." },
{ schluessel: "m-abschluss-gesetzt", titel: "Der Abschluss war gesetzt", stufe: "meister",
text: "Nicht abgebrochen. Ein Ende, das man kommen sieht, ist der Grund, beim nächsten Mal bis zum Schluss zu bleiben." },
{ schluessel: "m-grund-wiederzukommen", titel: "Es gab einen Grund wiederzukommen", stufe: "meister",
text: "Etwas Angekündigtes, etwas Offenes, etwas Verabredetes. Ohne das entscheidet der Zufall." },
],
auswertung: [
{ schluessel: "m-auffaelligkeiten-notiert", titel: "Auffälligkeiten notiert", stufe: "anfaenger",
text: "Solange du es noch weißt. Am nächsten Tag fehlt die Hälfte, und zwar die interessante." },
{ schluessel: "m-clipmarken-weitergegeben", titel: "Clip-Marken weitergegeben", stufe: "anfaenger",
text: "An die, die schneiden. Eine Marke, die niemand bekommt, ist keine." },
{ schluessel: "m-feedback-im-teamchat", titel: "Kurzes Feedback im Team-Chat", 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",
{ schluessel: "m-aufzeichnung-da", titel: "Die Aufzeichnung war da", stufe: "anfaenger",
text: "Am selben Abend gesichert. Ohne sie gibt es keine Clips und keinen Beleg." },
{ schluessel: "m-clips-kamen", titel: "Es kamen Clips", stufe: "anfaenger",
text: "Mindestens einer, am nächsten Tag. Der Feed hat mehr Reichweite als der Stream." },
{ schluessel: "m-offene-fragen-nachgereicht", titel: "Offene Fragen wurden nachgereicht", stufe: "anfaenger",
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",
text: "Aufgeschrieben, nicht vorgenommen. Ein Vorsatz ohne Zettel ist am Freitag weg." },
{ schluessel: "m-regel-vorgeschlagen", titel: "Regel vorgeschlagen, wo eine fehlte", stufe: "profi",
text: "Wenn du zweimal improvisieren musstest, fehlt eine Regel – nicht Erfahrung." },
{ schluessel: "m-wiederkehrende-stoerer", titel: "Wiederkehrende Störer notiert", stufe: "profi",
text: "Dieselben Namen tauchen wieder auf. Eine Liste macht aus drei Einzelfällen ein Muster." },
{ schluessel: "m-zahlen-angesehen", titel: "Zahlen angesehen", stufe: "profi",
text: "Wie viele kamen, wie lange blieben sie. Nicht um zu bewerten, sondern um zu wissen, wann der Chat kippte." },
{ schluessel: "m-uebergabe-geschrieben", titel: "Übergabe geschrieben", stufe: "meister",
text: "So, dass jemand anderes die nächste Sendung übernehmen könnte. Wer nicht ersetzbar ist, kann nicht krank werden." },
{ schluessel: "m-muster-ueber-sendungen", titel: "Muster über mehrere Sendungen erkannt", stufe: "meister",
text: "Immer dieselbe Uhrzeit, immer dieselbe Sorte Kommentar. Das sieht nur, wer mehrere Abende nebeneinanderlegt." },
{ schluessel: "m-team-bekam-rueckmeldung", titel: "Das Team bekam Rückmeldung", stufe: "fort",
text: "Auch wenn alles gut lief. Wer nie hört, dass es gut war, weiß nicht, was er behalten soll." },
{ schluessel: "m-zahlen-geteilt", titel: "Zahlen wurden geteilt", stufe: "fort",
text: "Nicht alle – die zwei, die etwas sagen. Ein Team, das die Wirkung nicht kennt, arbeitet blind." },
{ schluessel: "m-angekuendigtes-gehalten", titel: "Angekündigtes wurde eingehalten", stufe: "fort",
text: "„Nächste Woche machen wir …“ – wenn das dreimal nicht kam, glaubt es niemand mehr." },
{ schluessel: "m-aus-hinweisen-wurde-etwas", titel: "Aus Hinweisen wurde etwas", stufe: "profi",
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-naechster-termin-stand", titel: "Der nächste Termin stand", stufe: "profi",
text: "Vor dem Ende gesagt und im Kalender. Zwei feste Tage schlagen fünf zufällige." },
{ schluessel: "m-wiederkehrendes-angegangen", titel: "Wiederkehrendes wurde angegangen", stufe: "profi",
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 = {
moderation: [
{ schluessel: "m-regeln-gelesen", titel: "Regeln gelesen und verstanden", stufe: "anfaenger",
text: "Nicht überflogen. Du wirst danach gefragt, und eine falsche Auskunft ist schwerer zurückzunehmen als keine." },
{ schluessel: "m-eigene-rechte-bekannt", titel: "Eigene Rechte bekannt", stufe: "anfaenger",
text: "Was du darfst: löschen, stummschalten, sperren – und was nicht. Wer es nicht weiß, tut zu viel oder zu wenig." },
{ schluessel: "m-tonfall", titel: "Tonfall gefunden", stufe: "anfaenger",
text: "Freundlich und kurz. Ein Modi, der sich rechtfertigt, verliert; einer, der herrisch wird, auch." },
{ schluessel: "m-haeufige-fragen", titel: "Häufige Fragen im Kopf", stufe: "anfaenger",
text: "Die fünf, die jeden Abend kommen. Wer sie kennt, muss nicht jedes Mal nachsehen." },
{ schluessel: "m-warnung-vor-sperre", titel: "Reihenfolge klar: Hinweis, Warnung, Sperre", stufe: "fort",
text: "Vorher entscheiden, nicht im Moment. Wer im Ärger sperrt, sperrt irgendwann falsch." },
{ schluessel: "m-minderjaehrige", titel: "Umgang mit Minderjährigen", stufe: "fort",
text: "Wie erkennt man sie, was gilt dann. Das ist keine Ermessensfrage." },
{ schluessel: "m-privat-vs-oeffentlich", titel: "Privat statt öffentlich", stufe: "fort",
text: "Was eine Person betrifft, klärt man mit ihr – nicht vor zweihundert Leuten." },
{ schluessel: "m-keine-versprechen", titel: "Keine Versprechen im Namen anderer", 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",
{ schluessel: "m-regeln-auffindbar", titel: "Die Regeln waren auffindbar", stufe: "anfaenger",
text: "An einer Stelle, die man ohne Suchen findet. Was man suchen muss, liest niemand." },
{ schluessel: "m-regeln-verstaendlich", titel: "Die Regeln waren verständlich", stufe: "anfaenger",
text: "In eigenen Worten, nicht als Rechtstext. Regeln, die man erklären muss, werden nicht befolgt." },
{ schluessel: "m-wortfilter-passte", titel: "Der Wortfilter passte noch", stufe: "anfaenger",
text: "Was diese Woche durchrutschte, gehört hinein. Ein Filter, den niemand pflegt, veraltet." },
{ schluessel: "m-klar-wer-eingreift", titel: "Es war klar, wer eingreifen darf", stufe: "anfaenger",
text: "Für dich und für den Chat. Unklarheit darüber kostet im Ernstfall die entscheidenden Sekunden." },
{ schluessel: "m-eskalationsregel-bekannt", titel: "Die Eskalationsregel war bekannt", stufe: "fort",
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-regeln-angewandt", titel: "Die Regeln wurden auch angewandt", stufe: "fort",
text: "Eine Regel, die zweimal übergangen wird, gilt nicht mehr – auch für die, die sich daran halten." },
{ schluessel: "m-entscheidungen-nachvollziehbar", titel: "Entscheidungen waren nachvollziehbar", stufe: "fort",
text: "Nicht für jeden im Chat, aber für dich. Eine Sperre, die niemand erklären kann, wird zurückgenommen." },
{ schluessel: "m-alle-gleich-behandelt", titel: "Alle wurden gleich behandelt", stufe: "fort",
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",
text: "Einen Abend nebenher laufen lassen und danach besprechen. Zusehen lernt mehr als jede Regelliste." },
{ schluessel: "m-regeln-passen-noch", titel: "Die Regeln passen noch zur Community", stufe: "profi",
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: [
{ schluessel: "m-willkommen-gepflegt", titel: "Willkommens-Routine gepflegt", stufe: "anfaenger",
text: "Ein Satz, der nicht nach Textbaustein klingt. Wer merkt, dass ein Automat schreibt, antwortet nicht." },
{ schluessel: "m-namen-gemerkt", titel: "Namen gemerkt", stufe: "anfaenger",
text: "Die zehn, die immer da sind. Mehr braucht es nicht, damit sich eine Community erkannt fühlt." },
{ schluessel: "m-fragen-in-den-stream", titel: "Fragen in den Stream gegeben", stufe: "anfaenger",
text: "Gesammelt, ausgewählt, weitergereicht. Nicht alles – die drei, die etwas bringen." },
{ schluessel: "m-umfrage-ausgewertet", titel: "Umfrage ausgewertet", stufe: "fort",
text: "Und das Ergebnis gesagt. Eine Abstimmung ohne Ergebnis fragt beim nächsten Mal niemand mehr mit." },
{ schluessel: "m-aktion-vorgeschlagen", titel: "Eine Aktion vorgeschlagen", stufe: "fort",
text: "Challenge, Q&A, gemeinsamer Abend. Du hörst im Chat, was ankommt, früher als alle anderen." },
{ schluessel: "m-stille-belebt", titel: "Stille belebt statt ausgesessen", stufe: "fort",
text: "Eine Frage, ein Bezug auf vorhin. Ein stiller Chat wirkt auf Neue wie ein leerer Raum." },
{ schluessel: "m-rueckmeldung-eingesammelt", titel: "Rückmeldungen eingesammelt", stufe: "fort",
text: "Was Zuschauer sich wünschen, gehört weitergegeben – auch wenn es nicht umgesetzt wird." },
{ schluessel: "m-stammzuschauer-liste", titel: "Liste der Stammzuschauer gepflegt", stufe: "profi",
text: "Wer seit Wochen da ist, verdient mehr als ein Herz. Ohne Liste vergisst man ihn." },
{ schluessel: "m-neue-in-die-runde", titel: "Neue in die Runde geholt", stufe: "profi",
text: "Sie einmal namentlich ins Gespräch ziehen. Der Unterschied zwischen Zuschauen und Dabeisein." },
{ schluessel: "m-eigene-idee-eingetragen", titel: "Eigene Idee ins Ideen-Board", stufe: "profi",
text: "Was dir im Chat auffällt, ist die beste Quelle für Inhalte – aber nur, wenn es aufgeschrieben wird." },
{ schluessel: "m-aktion-durchgezogen", titel: "Eine Aktion von Anfang bis Ende begleitet", stufe: "meister",
text: "Ankündigen, durchführen, hinterher sagen, was daraus wurde. Der letzte Teil wird fast immer vergessen." },
{ schluessel: "m-community-uebersetzt", titel: "Zwischen Community und Team übersetzt", 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." },
{ schluessel: "m-neue-wurden-begruesst", titel: "Neue wurden begrüßt", stufe: "anfaenger",
text: "Mit Namen. Wer beim ersten Mal angesprochen wird, kommt deutlich häufiger wieder." },
{ schluessel: "m-kommentare-beantwortet", titel: "Kommentare bekamen Antwort", stufe: "anfaenger",
text: "Auch die kurzen, auch die späten. Eine Antwort nach drei Tagen liest sich wie keine." },
{ schluessel: "m-stammzuschauer-erkannt", titel: "Stammzuschauer wurden erkannt", stufe: "anfaenger",
text: "Wiedererkannt zu werden ist der halbe Grund wiederzukommen." },
{ schluessel: "m-etwas-zum-mitmachen", titel: "Es gab etwas zum Mitmachen", stufe: "fort",
text: "Eine Umfrage, eine Frage, eine Challenge. Zuschauen allein bindet niemanden." },
{ schluessel: "m-umfragen-ausgewertet", titel: "Umfragen wurden ausgewertet", stufe: "fort",
text: "Und das Ergebnis gesagt. Eine Abstimmung ohne Ergebnis macht beim nächsten Mal niemand mit." },
{ schluessel: "m-wuensche-kamen-zurueck", titel: "Wünsche kamen zurück", stufe: "fort",
text: "Auch als Nein. Ein Wunschkanal ohne Antwort verstummt." },
{ schluessel: "m-aktionen-angekuendigt", titel: "Aktionen waren angekündigt", stufe: "fort",
text: "Rechtzeitig genug, dass man sich den Termin freihalten kann." },
{ schluessel: "m-community-erwaehnt", titel: "Die Community wurde erwähnt", stufe: "profi",
text: "Namentlich, nicht als „ihr da draußen“. Der Unterschied zwischen Publikum und Runde." },
{ schluessel: "m-ideen-aufgegriffen", titel: "Ideen aus dem Chat wurden aufgegriffen", stufe: "profi",
text: "Und gesagt, woher sie kamen. Wer sich wiederfindet, bringt die nächste Idee." },
{ schluessel: "m-grund-teil-zu-sein", titel: "Es gab einen Grund, dabei zu sein", stufe: "profi",
text: "Etwas, das man nur mitbekommt, wenn man da ist. Sonst reicht der Clip am nächsten Tag." },
{ schluessel: "m-aus-aktion-wurde-reihe", titel: "Aus einer Aktion wurde eine Reihe", stufe: "meister",
text: "Erst beim zweiten Mal sieht man, ob das Format trägt oder nur das Thema getragen hat." },
{ schluessel: "m-community-waechst-von-innen", titel: "Die Community wächst von innen", stufe: "meister",
text: "Neue kommen, weil jemand sie mitgebracht hat. Das ist die einzige Reichweite, die bleibt." },
],
konflikt: [
{ schluessel: "m-erst-lesen", titel: "Erst lesen, dann handeln", stufe: "anfaenger",
text: "Zwei Sätze zurückscrollen. Die Hälfte der Streitfälle sieht mit dem Satz davor anders aus." },
{ schluessel: "m-nicht-mitstreiten", titel: "Nicht mitstreiten", stufe: "anfaenger",
text: "Du bist nicht Partei. In dem Moment, in dem du eine wirst, hast du deine Rolle verloren." },
{ schluessel: "m-kurz-und-klar", titel: "Kurz und klar sagen, was gilt", stufe: "anfaenger",
text: "Ein Satz, keine Begründungskette. Je länger die Erklärung, desto mehr Angriffsfläche." },
{ schluessel: "m-abkuehlen-lassen", titel: "Abkühlen lassen statt eskalieren", stufe: "fort",
text: "Stummschalten auf Zeit ist oft besser als sperren. Man kann es zurücknehmen." },
{ schluessel: "m-zweite-meinung", titel: "Zweite Meinung geholt", stufe: "fort",
text: "Bei allem, was größer ist als ein blöder Kommentar. Zu zweit entscheidet es sich leichter und besser." },
{ schluessel: "m-belege-gesichert", titel: "Belege gesichert", stufe: "fort",
{ schluessel: "m-frueh-eingegriffen", titel: "Es wurde früh eingegriffen", stufe: "anfaenger",
text: "Bevor zehn Leute geantwortet haben. Danach moderiert man nicht mehr, man räumt auf." },
{ schluessel: "m-ruhig-geblieben-live", titel: "Es blieb ruhig", stufe: "anfaenger",
text: "Der Ton oben wird zum Ton im Chat. Das ist der schwerste Punkt auf dieser Liste." },
{ schluessel: "m-nicht-oeffentlich-ausdiskutiert", titel: "Nichts wurde öffentlich ausdiskutiert", stufe: "anfaenger",
text: "Im Chat gewinnt niemand. Kurz sagen, was gilt, und weiter." },
{ schluessel: "m-entscheidung-begruendet", titel: "Die Entscheidung war begründet", stufe: "fort",
text: "In einem Satz, nicht in einer Kette. Je länger die Erklärung, desto mehr Angriffsfläche." },
{ schluessel: "m-rueckendeckung-im-fall", titel: "Es gab Rückendeckung", stufe: "fort",
text: "Wer eingreift, muss wissen, dass er danach nicht allein dasteht." },
{ 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." },
{ schluessel: "m-danach-gemeldet", titel: "Danach im Team gemeldet", stufe: "fort",
text: "Auch wenn es gut ausging. DogFather und die rechte Hand sollten es von dir erfahren, nicht von anderen." },
{ schluessel: "m-keine-drohungen", titel: "Keine Drohungen, nur Folgen", stufe: "profi",
text: "\"Noch einmal und du bist raus\" ist eine Ansage. Alles darüber hinaus ist Streit." },
{ schluessel: "m-nach-der-sperre", titel: "Nach der Sperre nicht nachtreten", stufe: "profi",
text: "Kein Kommentar hinterher, kein Witz darüber. Der Chat merkt sich das." },
{ schluessel: "m-eigene-fehler", titel: "Eigenen Fehler zugegeben", stufe: "profi",
text: "Falsch gesperrt? Zurücknehmen und einmal sagen. Das kostet weniger als der Versuch, recht zu behalten." },
{ schluessel: "m-ernstfall-geprobt", titel: "Ernstfall einmal durchgesprochen", stufe: "meister",
text: "Wer tut was, wenn es wirklich kippt. Im Ernstfall zum ersten Mal darüber nachzudenken ist zu spät." },
{ schluessel: "m-daraus-gelernt", titel: "Aus dem Fall eine Regel gemacht", stufe: "meister",
text: "Ein Vorfall, aus dem nichts folgt, kommt wieder. Der Ertrag ist die Regel, nicht die Sperre." },
{ schluessel: "m-danach-besprochen", titel: "Danach wurde besprochen", stufe: "fort",
text: "Auch wenn es gut ausging. Sonst lernt nur der daraus, der dabei war." },
{ schluessel: "m-keine-ausnahmen", titel: "Keine Ausnahmen für Bekannte", stufe: "profi",
text: "Das ist der Punkt, an dem Regeln ihre Glaubwürdigkeit verlieren – und er fällt allen auf." },
{ schluessel: "m-fehler-zurueckgenommen", titel: "Fehler wurden zurückgenommen", stufe: "profi",
text: "Falsch gesperrt? Einmal sagen und rückgängig machen. Das kostet weniger als recht zu behalten." },
{ schluessel: "m-ton-blieb", titel: "Der Ton blieb auch danach", stufe: "profi",
text: "Kein Nachtreten, kein Witz über den Gesperrten. Der Chat merkt sich das." },
{ schluessel: "m-fall-fuehrte-zu-regel", titel: "Der Fall führte zu einer Regel", stufe: "meister",
text: "Wenn zweimal improvisiert werden musste, fehlt eine Regel – nicht Erfahrung." },
{ schluessel: "m-kam-nicht-wieder", titel: "Es kam nicht wieder", stufe: "meister",
text: "Der einzige Beweis, dass die Regel gewirkt hat." },
],
};
export const MODI_TECHNIK_PUNKTE = {
setup: [
{ schluessel: "m-zugaenge-funktionieren", titel: "Zugänge funktionieren", stufe: "anfaenger",
text: "Einmal vor der Sendung anmelden, nicht währenddessen. Ein abgelaufener Zugang fällt sonst im schlechtesten Moment auf." },
{ schluessel: "m-benachrichtigungen-an", titel: "Benachrichtigungen an", stufe: "anfaenger",
text: "Für Aufgaben und Team-Nachrichten. Ein Hinweis, den niemand bekommt, ist keiner." },
{ schluessel: "m-zweites-geraet", titel: "Zweites Gerät griffbereit", stufe: "anfaenger",
text: "Handy neben dem Rechner. Wenn eines hängt, moderierst du mit dem anderen weiter." },
{ schluessel: "m-verbindung-stabil", titel: "Verbindung stabil", stufe: "anfaenger",
text: "Kabel schlägt WLAN. Ein Modi, der alle zehn Minuten neu lädt, sieht die Hälfte nicht." },
{ schluessel: "m-chatfenster-eingerichtet", titel: "Chatfenster richtig eingerichtet", stufe: "fort",
text: "Groß genug, ohne anderes darüber. Wer scrollen muss, um mitzulesen, liest nicht mit." },
{ schluessel: "m-werkzeuge-kennen", titel: "Moderations-Werkzeuge kennen", stufe: "fort",
text: "Wo Löschen, Stummschalten, Sperren sitzt – ohne zu suchen. Im Ernstfall zählen Sekunden." },
{ schluessel: "m-automod-verstanden", titel: "Automatische Filter verstanden", stufe: "fort",
text: "Was sie wegnehmen und was nicht. Wer sich darauf verlässt, ohne es zu wissen, übersieht genau die Lücke." },
{ schluessel: "m-app-installiert", titel: "Die App wirklich installiert", stufe: "fort",
text: "Nicht als Lesezeichen. Nur so kommen Erinnerungen an, auch wenn der Browser zu ist." },
{ schluessel: "m-tastenkuerzel", titel: "Tastenkürzel eingerichtet", stufe: "profi",
text: "Die drei, die du am häufigsten brauchst. Aus zwei Sekunden werden über einen Abend Minuten." },
{ schluessel: "m-ruhe-eingestellt", titel: "Ruhezeiten eingestellt", stufe: "profi",
text: "Damit die App nachts still ist. Wer um drei geweckt wird, schaltet sie irgendwann ganz ab." },
{ schluessel: "m-ersatzweg", titel: "Ersatzweg ins Netz", stufe: "meister",
text: "Mobilfunk, einmal wirklich benutzt. Ein Ersatz, den man erst im Ausfall einrichtet, ist keiner." },
{ schluessel: "m-ton-ohne-rauschen", titel: "Ton ohne Rauschen", stufe: "anfaenger",
text: "Du hörst es wie die Zuschauer. Schlechter Ton ist der Grund Nummer eins, warum Leute wieder gehen." },
{ schluessel: "m-ton-nicht-uebersteuert", titel: "Ton nicht übersteuert", stufe: "anfaenger",
text: "Kratzt es bei lauten Stellen? Das merkt man erst, wenn jemand lacht – und dann ist es unangenehm laut." },
{ schluessel: "m-bild-scharf", titel: "Das Bild war scharf", stufe: "anfaenger",
text: "Nicht weich, nicht verpixelt. Auf dem Handy fällt beides stärker auf als am Rechner." },
{ schluessel: "m-licht-ausreichend", titel: "Das Licht reichte", stufe: "anfaenger",
text: "Gesicht heller als der Hintergrund, kein Fenster im Rücken." },
{ schluessel: "m-verbindung-stabil-live", titel: "Die Verbindung war stabil", stufe: "anfaenger",
text: "Wenn es bei dir hängt, hängt es bei vielen. Uhrzeit notieren – daran erkennt man die Ursache." },
{ schluessel: "m-szenenwechsel-sauber", titel: "Szenenwechsel waren sauber", stufe: "fort",
text: "Kein schwarzes Bild dazwischen, kein Ton, der wegbricht." },
{ schluessel: "m-musik-nicht-zu-laut", titel: "Die Musik war nicht zu laut", stufe: "fort",
text: "Sie darf die Stimme nicht überdecken. Am Rechner klingt das anders als im Kopfhörer." },
{ schluessel: "m-overlays-lesbar", titel: "Einblendungen waren lesbar", stufe: "fort",
text: "Auch auf einem Handy. Was du zusammenkneifen musst, liest niemand." },
{ schluessel: "m-text-auf-handy", titel: "Text war auf dem Handy lesbar", stufe: "fort",
text: "Der größte Teil schaut auf dem Handy. Was dort nicht ankommt, kommt gar nicht an." },
{ schluessel: "m-kein-nachhall", titel: "Kein Hall im Raum", stufe: "profi",
text: "Klingt es nach Badezimmer, hört man nach zehn Minuten weg – ohne zu wissen, warum." },
{ schluessel: "m-ausweichweg-vorhanden", titel: "Ein Ausweichweg war vorhanden", stufe: "meister",
text: "Zweites Gerät, zweite Leitung. Ein Ersatz, den man erst im Ausfall einrichtet, ist keiner." },
],
problem: [
{ schluessel: "m-was-genau-faellt-aus", titel: "Was genau fällt aus?", stufe: "anfaenger",
text: "Chat, Bild, dein Zugang oder das ganze Netz – das sind vier verschiedene Probleme mit vier Lösungen." },
{ schluessel: "m-sofort-gesagt", titel: "Sofort gesagt, dass etwas ist", stufe: "anfaenger",
text: "Lieber einmal zu viel im Team-Chat. Ein stiller Modi sieht aus wie ein anwesender." },
{ schluessel: "m-notiert-solange-frisch", titel: "Notiert, solange es frisch ist", stufe: "anfaenger",
text: "Uhrzeit, Fehlermeldung, was du zuletzt getan hast. Am nächsten Tag fehlt genau das." },
{ schluessel: "m-eine-aenderung", titel: "Nur eine Änderung auf einmal", stufe: "fort",
text: "Wer drei Dinge gleichzeitig ändert und es geht wieder, weiß nicht warum – und steht beim nächsten Mal am Anfang." },
{ schluessel: "m-uebergeben-statt-kaempfen", titel: "Übergeben statt kämpfen", stufe: "fort",
text: "Nach fünf Minuten Basteln moderiert jemand anderes. Der Chat wartet nicht auf deine Technik." },
{ schluessel: "m-bildschirmfoto", titel: "Bildschirmfoto gemacht", stufe: "fort",
text: "Eine Fehlermeldung, die man nachher beschreibt, ist eine andere als die, die dastand." },
{ schluessel: "m-wiederholbar", titel: "Geprüft, ob es wiederkommt", stufe: "profi",
{ schluessel: "m-ausfall-bemerkt", titel: "Der Ausfall wurde bemerkt", stufe: "anfaenger",
text: "Und zwar oben, nicht nur im Chat. Minutenlang ins Leere zu senden ist das Schlimmste, was passieren kann." },
{ schluessel: "m-chat-informiert", titel: "Der Chat wurde informiert", stufe: "anfaenger",
text: "Ein Satz genügt. Stille sieht aus wie Absturz, und dann gehen die Leute." },
{ schluessel: "m-ansage-wie-lange", titel: "Es gab eine Ansage, wie lange", stufe: "fort",
text: "Auch „ich weiß es nicht“ ist eine Ansage. Wer keine Zeit genannt bekommt, wartet nicht." },
{ schluessel: "m-jemand-uebernahm", titel: "Jemand hat übernommen", stufe: "fort",
text: "Chat weiterführen, während oben repariert wird. Ohne das zerfällt die Runde in fünf Minuten." },
{ schluessel: "m-fehler-festgehalten", titel: "Der Fehler wurde festgehalten", stufe: "fort",
text: "Uhrzeit, Meldung, was zuletzt anders war. Am nächsten Tag fehlt genau das." },
{ schluessel: "m-nicht-live-gebastelt", titel: "Es wurde nicht live gebastelt", stufe: "profi",
text: "Nach zwei Minuten Herumprobieren schaut man dabei zu, wie jemand arbeitet. Dafür ist niemand gekommen." },
{ 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." },
{ schluessel: "m-nicht-alleine-suchen", titel: "Nicht allein stundenlang suchen", stufe: "profi",
text: "Nach zwanzig Minuten jemanden dazuholen. Zu zweit findet man es in fünf." },
{ schluessel: "m-plan-b", titel: "Es gab einen Plan B", stufe: "meister",
text: "Und er wurde vorher einmal geprobt. Ein ungeprobter Ausweichweg ist keiner." },
],
loesung: [
{ schluessel: "m-was-geholfen-hat", titel: "Was geholfen hat", stufe: "anfaenger",
text: "Genau die eine Änderung – nicht alles, was probiert wurde. Sonst ist es beim nächsten Mal wieder Raten." },
{ schluessel: "m-fuer-alle-aufgeschrieben", titel: "Für alle aufgeschrieben", stufe: "anfaenger",
text: "In die Bibliothek, nicht in den Chat. Was im Chat steht, ist nach zwei Tagen unauffindbar." },
{ schluessel: "m-anleitung-verstaendlich", titel: "Anleitung verständlich", stufe: "fort",
text: "Jemanden gegenlesen lassen, der es nicht erlebt hat. Was du selbst geschrieben hast, verstehst du immer." },
{ schluessel: "m-dauerhaft-oder-pflaster", titel: "Dauerhaft oder Pflaster?", stufe: "fort",
text: "Beides ist in Ordnung – aber es gehört dabeigeschrieben, damit niemand darauf baut." },
{ schluessel: "m-vorbeugen-vorgeschlagen", titel: "Vorbeugen vorgeschlagen", stufe: "profi",
text: "Wenn es zweimal passiert ist, ist es kein Zufall mehr, sondern eine fehlende Einstellung." },
{ schluessel: "m-wissen-weitergegeben", titel: "Wissen weitergegeben", 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." },
{ schluessel: "m-loesung-aufgeschrieben", titel: "Die Lösung wurde aufgeschrieben", stufe: "anfaenger",
text: "Genau die eine Änderung – nicht alles, was probiert wurde." },
{ schluessel: "m-loesung-verstaendlich", titel: "Sie war verständlich", stufe: "fort",
text: "Auch für jemanden, der nicht dabei war. Was man selbst geschrieben hat, versteht man immer." },
{ schluessel: "m-loesung-hielt", titel: "Sie hat gehalten", stufe: "fort",
text: "Beim nächsten Mal nachgesehen. Ein Pflaster ist in Ordnung – aber es gehört dabeigeschrieben." },
{ schluessel: "m-loesung-geteilt", titel: "Sie wurde geteilt", stufe: "profi",
text: "In der Bibliothek, nicht im Chat. Was im Chat steht, ist nach zwei Tagen unauffindbar." },
{ schluessel: "m-vorbeugung", titel: "Es wurde vorgebeugt", stufe: "profi",
text: "Wenn es zweimal passiert ist, fehlt eine Einstellung – nicht Glück." },
{ schluessel: "m-wissen-nicht-bei-einem", titel: "Das Wissen liegt nicht bei einer Person", stufe: "meister",
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 = {
live: MODI_LIVE_PUNKTE,
community: MODI_COMMUNITY_PUNKTE,
+266 -2
View File
@@ -26,10 +26,33 @@
===================================================================== */
import express from "express";
import {
db, sitzungLesen, istDogFather, heuteLokal,
db, sitzungLesen, istDogFather, heuteLokal, protokolliere, echteIp,
} from "./workspace.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();
/* 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();
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 nummern = modis.map((m) => m.id);
@@ -88,6 +115,46 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
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
das Gespraech, nicht die Note. */
const nachrichten = new Map();
@@ -100,6 +167,8 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
res.json({
heute,
bereiche: GESAMT,
eingang,
angebote,
modis: modis.map((m) => {
const s = stand.get(m.id) || {};
const je = {};
@@ -125,3 +194,198 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
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" });
}
});
+91 -1
View File
@@ -27,6 +27,8 @@ import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import { sitzungPasstZurAdresse, CREW_ADRESSE, istOhneModiAdresse, istCrewAdresse }
from "./crew-adresse.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -284,6 +286,9 @@ const MODI_BEREICHE = [
{ gruppe: "Täglich", gruppeUnter: "Was du sowieso jeden Tag aufmachst",
name: "Ideen-Board", unter: "Gesammelt und nach Dringlichkeit sortiert",
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",
name: "Live-Ablauf", unter: "Checkliste für vorher, mittendrin und danach",
@@ -948,6 +953,17 @@ function umstellungen(d) {
workspace-aufgaben.js) -- an EINER Stelle, durch die alle
Schreibwege laufen. */
["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 {
const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name);
@@ -1498,6 +1514,18 @@ function umstellungen(d) {
waere die vierte Gelegenheit dazu. */
checkListeErweitern(d, "eintraege", "bereich", "ideen",
["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() {
@@ -2371,6 +2399,24 @@ export function sitzungLesen(req) {
db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token));
return null;
}
/* DIE ADRESSE ENTSCHEIDET MIT (10.09.2026).
Seit die Modi-App unter crew.dogfather-universe.com laeuft, gehoert
jede Sitzung zu genau EINER Adresse: Modis dorthin, alle anderen auf
den Workspace. Die Regel steht in crew-adresse.js und ist dort ohne
laufenden Server pruefbar.
WARUM HIER UND NICHT IN DEN MODULEN: Durch diese Funktion geht jedes
der 26 Fachmodule und jede Seitenschranke. Eine Middleware daneben
koennte man in einem neuen Modul vergessen -- und ein vergessener
Rechteschutz faellt nicht auf, weil dann alles geht.
`null` heisst hier dasselbe wie bei einer abgelaufenen Sitzung:
nicht angemeldet. Die Seitenschranke schickt zur Zugangswand, die
Schnittstellen antworten mit 401. Beides ist genau richtig -- auf
dieser Adresse ist diese Person schlicht niemand. */
if (!sitzungPasstZurAdresse(reihe.rolle, req.get?.("host"))) return null;
return { id: reihe.id, name: reihe.name, rolle: reihe.rolle };
} catch { return null; }
}
@@ -2671,6 +2717,33 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
WEN man pruefen soll -- danach entscheidet scrypt wie ueberall. */
const still = codeBrauchbar ? stillerZugang(code) : null;
if (still) {
/* AUF DER ALTEN ADRESSE GIBT ES KEINE SITZUNG MEHR (10.09.2026).
Die Modi-App hat seit heute eine eigene Adresse. Wer seinen Code
noch auf workspace.dogfather-universe.com eintippt, bekommt hier
den Weg dorthin -- und ausdruecklich KEINE Anmeldung: zwei
Sitzungen auf zwei Adressen waeren zwei Apps auf dem
Startbildschirm und eine zweite Spur genau dort, wo alle anderen
arbeiten.
DASS DIE ANTWORT DIE NEUE ADRESSE NENNT, IST KEIN LECK. Sie
kommt nur nach Eingabe eines gueltigen Modi-Codes; wer den hat,
ist Modi. Fuer jeden anderen -- auch fuer einen Manager mit
gueltigem eigenen Code -- veraendert sich hier gar nichts.
Kein Eintrag in `versuche`: Der Code war richtig. Ein Modi, der
die alte Verknuepfung auf dem Handy hat, wuerde sich sonst mit
jedem Versuch selbst aussperren. */
if (istOhneModiAdresse(req.get("host"))) {
protokolliere("anmeldung_alte_adresse", {
personId: still.id, rolle: still.rolle, ip, detail: still.name,
});
return res.json({
weiter: `${CREW_ADRESSE}/workspace/start.html`,
name: still.name, rolle: still.rolle, umgezogen: true,
});
}
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), still.id);
sitzungSetzen(res, still, req);
@@ -2685,7 +2758,24 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
/* Ab hier der gewohnte Weg -- unveraendert. 'modi' ist keine
Kachel-Rolle und faellt deshalb in dieselbe Antwort wie eine
erfundene. */
if (!ROLLEN_KACHEL.has(rolle) || !codeBrauchbar) {
/* AUF DER MODI-ADRESSE ENDET DER GEWOHNTE WEG HIER (10.09.2026).
crew.dogfather-universe.com ist nicht die zweite Tuer in den
Workspace. Wer sie errät und seinen eigenen Code eintippt, soll
genau das erleben, was er bei einem Tippfehler erlebt: Die
Zugangswand sieht aus wie immer, der Code stimmt nicht. Wuerde er
stattdessen hereinkommen, wuesste er im selben Moment, dass es
hier noch etwas gibt.
BEWUSST IN DIESELBE BEDINGUNG GESCHRIEBEN und nicht als eigener
Block davor: So ist es nicht nur dieselbe ANTWORT, sondern
derselbe Weg -- ein Eintrag in `versuche`, kein scrypt-Durchlauf,
401 "ungueltig". Ein eigener Block waere messbar schneller oder
langsamer gewesen als der Weg daneben, und genau daran erkennt
man eine Sonderbehandlung. */
const fremdAufCrew = istCrewAdresse(req.get("host"));
if (!ROLLEN_KACHEL.has(rolle) || !codeBrauchbar || fremdAufCrew) {
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
return res.status(401).json({ fehler: "ungueltig" });
}
+260
View File
@@ -0,0 +1,260 @@
/* ===================================================================
Das App-Symbol der Modi-App ("Team Dogi", 10.09.2026).
DER HUSKY IST DERSELBE. Filipe am 09.09.2026 zum Rollenbild:
"das modi symbol soll das gleiche sein wie bei dogfather." Das gilt
hier genauso -- es ist dieselbe Vorlage, dieselbe Technik.
DIE CHILI IST ES NICHT, UND ZWAR ABSICHTLICH. Sie steht fuer Spicy
Media. Die Modis gehoeren nicht dorthin; ihre Marke heisst "Team
Dogi". Eine Chili auf ihrem Startbildschirm waere kein Schmuck,
sondern eine falsche Aussage. An ihre Stelle tritt der Ton, der auch
ihre Kacheln traegt: das gedeckte Stahlblau #5f8a9f.
WARUM EINE EIGENE DATEI UND KEIN SCHALTER IN workspace-symbol.mjs:
Jenes Werkzeug PRUEFT, dass die Chili in jeder Fassung sichtbar ist.
Ein Schalter haette diese Pruefung fuer den halben Aufruf abschalten
muessen -- und eine Pruefung, die man abschalten kann, ist bald
abgeschaltet. Zwei Bilder mit zwei Regeln bekommen zwei Werkzeuge,
die jeweils ihre eigenen Regeln durchsetzen.
DIE DRITTE GEGENPROBE, DIE ES DORT NICHT GIBT: Am Ende wird das
erzeugte Symbol gegen das des Workspace gehalten. Waeren beide
gleich, haette der ganze Umzug seinen Sinn verloren -- zwei Apps mit
demselben Zeichen sind auf einem Startbildschirm dasselbe Symbol
zweimal. Gemessen wird das, nicht behauptet.
Aufrufen mit: node tools/crew-symbol.mjs
=================================================================== */
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
const HUSKY = join(WURZEL, "workspace", "assets", "img", "marke-husky.webp");
/* Umlenkbar wie beim Schwesterwerkzeug: Ein Symbol beurteilt man, indem
man es ansieht -- und dafuer darf man die echten Dateien nicht bei
jedem Versuch ueberschreiben. */
const ZIEL = process.env.SYMBOL_ZIEL || join(WURZEL, "assets", "img", "app-symbole");
/* Zum Vergleichen -- immer die echten Symbole, auch wenn das Ziel
umgelenkt ist. Fehlt eines, ist die dritte Gegenprobe NICHT
durchfuehrbar, und das ist ein Fehler, kein Ueberspringen. */
const VERGLEICH = join(WURZEL, "assets", "img", "app-symbole");
if (!existsSync(HUSKY)) { console.error(`Fehlt: ${HUSKY}`); process.exit(1); }
const alsDaten = (p) => "data:image/webp;base64," + readFileSync(p).toString("base64");
const GROESSEN = [
{ n: 512, endung: "512" },
{ n: 192, endung: "192" },
{ n: 512, endung: "512-maskable", maskable: true },
{ n: 192, endung: "192-maskable", maskable: true },
{ n: 180, endung: "180" },
{ n: 32, endung: "32" },
];
function html(n, maskable, husky, weglassen = "") {
/* KLEIN = alles unter 64 px. Dort faellt die Gesichtszeichnung weg:
achtfach vergroessert verglichen ist der Kopf mit Zeichnung bei
32 px ein dunkler Fleck, als reine Silhouette ein Hund. Dieselbe
Messung wie beim Workspace-Symbol, dasselbe Ergebnis. */
const klein = n < 64;
const rand = Math.round(n * 0.22);
/* ALLES IN EINER GRUPPE, damit fuer die maskable-Fassung genau EINE
Zahl kleiner wird und nicht mehrere auseinanderlaufen. 0,74 haelt
alles im 80-%-Sicherheitskreis, den Android als einziges garantiert.
Ohne Chili darf der Kopf etwas groesser stehen -- er ist hier allein
und muss die Flaeche selbst tragen. */
const g = Math.round(n * (maskable ? 0.74 : 0.96));
const ecke = maskable ? "" : `border-radius:${rand}px;`;
return `<!doctype html><html><head><meta charset="utf-8"><style>
html,body{margin:0;width:${n}px;height:${n}px;overflow:hidden}
.feld{position:relative;width:${n}px;height:${n}px;${ecke}
/* DER GRUND. Dieselbe Nacht wie beim Workspace -- es ist dasselbe
Haus --, aber das warme Licht unten ist durch den Modi-Ton
ersetzt. Beide Lichter sind damit kalt, das Zeichen wirkt
ruhiger und ist trotzdem sofort ein anderes.
Gedeckt, nicht leuchtend: augenschonend gilt auch fuer ein
Symbol, das auf einem hellen Startbildschirm liegt. */
background:
radial-gradient(76% 56% at 50% 24%, rgba(95,189,255,.20) 0%, transparent 68%),
radial-gradient(90% 56% at 50% 104%, rgba(95,138,159,.46) 0%, transparent 70%),
linear-gradient(180deg,#1a2f47 0%,#0d1a29 52%,#060c14 100%);
box-shadow:inset 0 ${Math.max(1, Math.round(n * .004))}px 0 rgba(255,255,255,.24),
inset 0 0 ${Math.round(n * .06)}px rgba(0,0,0,.38);
display:grid;place-items:center}
.gruppe{position:relative;width:${g}px;height:${g}px}
.gruppe > i{position:absolute;left:50%;transform:translate(-50%,-50%)}
/* DER KOPF -- zwei Lagen, und der Grund dafuer ist teuer bezahlt:
Als blosse MASKE zaehlt nur der Alphakanal, und die Vorlage ist
rundum freigestellt. Uebrig bliebe eine geschlossene Flaeche in
Hundeform -- ohne Auge, ohne Schnauze, ohne Ohrinneres. Genau das
war am 09.09.2026 der Fehler beim Workspace-Symbol.
Jetzt unten die Silhouette, darueber die Originalzeichnung im
Mischmodus "screen": Screen laesst Schwarz stehen (das Fell
bleibt) und hebt Weiss heraus (Gesicht, Ohrinneres, Auge).
"mask-mode: luminance" kann Chromium zwar, aber eine
Kurzschreibweise in einer spezifischeren Regel setzt es
stillschweigend auf "alpha" zurueck -- der Mischmodus hat diese
Falle nicht.
DIE ZWEITE MASKE blendet den Hals nach unten aus. Die Vorlage ist
dort angeschnitten; als Symbol sah die gerade Kante nach Fehler
aus. Bei 32 px faellt der Auslauf weg, weil er dann gerade die
Schnauze frisst -- die Kante ist dort drei Bildpunkte hoch. */
.gruppe > i.kopf{width:${klein ? 78 : 90}%;height:${klein ? 78 : 90}%;
top:${klein ? 46 : 45}%;
z-index:2;isolation:isolate;
background:linear-gradient(162deg,#6f9fbe 0%,#3a6a91 44%,#14344f 100%);
${klein
? `-webkit-mask:url("${husky}") center/contain no-repeat;
mask:url("${husky}") center/contain no-repeat;`
: `-webkit-mask:url("${husky}") center/contain no-repeat,
linear-gradient(180deg,#000 58%, transparent 86%);
-webkit-mask-composite:source-in;
mask:url("${husky}") center/contain no-repeat,
linear-gradient(180deg,#000 58%, transparent 86%);
mask-composite:intersect;`}
filter:drop-shadow(0 ${Math.round(n * .006)}px ${Math.round(n * .016)}px rgba(0,0,0,.7))
drop-shadow(0 0 ${Math.round(n * .024)}px rgba(95,138,159,.44))}
.gruppe > i.kopf .zeichnung{position:absolute;inset:0;display:block;
background:url("${husky}") center/contain no-repeat;
mix-blend-mode:screen;
/* Ohne Daempfer wird das Weiss der Vorlage zu reinem Weiss und
blendet auf dunklem Grund. Hell genug zum Erkennen, nicht
heller. */
filter:brightness(.88)}
.gruppe > i.kopf.flach{background:linear-gradient(168deg,#ffffff 4%,#e2edf5 38%,#a8bfcd 100%)}
.gruppe > i.kopf.flach .zeichnung{display:none}
${weglassen === "zeichnung" ? ".gruppe > i.kopf .zeichnung{display:none}" : ""}
</style></head><body><div class="feld"><div class="gruppe">
<i class="kopf${klein ? " flach" : ""}"><b class="zeichnung"></b></i>
</div></div></body></html>`;
}
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
const seite = await (await browser.newContext({ deviceScaleFactor: 1 })).newPage();
if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true });
const husky = alsDaten(HUSKY);
let gebaut = 0;
const maengel = [];
async function aufnehmen(n, maskable, weglassen = "") {
await seite.setViewportSize({ width: n, height: n });
await seite.setContent(html(n, maskable, husky, weglassen), { waitUntil: "networkidle" });
return await seite.screenshot({ omitBackground: false });
}
/* Streuung der Helligkeit. Ein leeres Feld hat keine -- das beweist,
dass ueberhaupt etwas draufsteht, mehr nicht. Die Bytezahl beweist
auch das nicht: ein einfarbiges PNG ist genauso gross wie ein volles. */
async function streuungVon(bild) {
return await seite.evaluate(async (b64) => {
const b = new Image(); b.src = "data:image/png;base64," + b64; await b.decode();
const c = document.createElement("canvas");
c.width = b.width; c.height = b.height;
const g2 = c.getContext("2d"); g2.drawImage(b, 0, 0);
const d = g2.getImageData(0, 0, c.width, c.height).data;
let s = 0, s2 = 0, m = 0;
for (let i = 0; i < d.length; i += 4) {
const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
s += h; s2 += h * h; m++;
}
return Math.sqrt(s2 / m - (s / m) ** 2);
}, bild.toString("base64"));
}
/* Anteil der Bildpunkte, die sich sichtbar unterscheiden. Acht Stufen
Abstand, damit Kantenglaettung allein den Wert nicht hochtreibt.
-1 heisst "nicht vergleichbar" (verschiedene Groessen) und ist
ausdruecklich etwas anderes als 0. */
async function unterschied(a, b) {
return await seite.evaluate(async ([x, y]) => {
const laden = async (b64) => {
const b = new Image(); b.src = "data:image/png;base64," + b64; await b.decode();
const c = document.createElement("canvas");
c.width = b.width; c.height = b.height;
c.getContext("2d").drawImage(b, 0, 0);
return c.getContext("2d").getImageData(0, 0, c.width, c.height).data;
};
const [p, q] = [await laden(x), await laden(y)];
if (p.length !== q.length) return -1;
let anders = 0, m = 0;
for (let i = 0; i < p.length; i += 4) {
const w = Math.max(Math.abs(p[i] - q[i]), Math.abs(p[i + 1] - q[i + 1]),
Math.abs(p[i + 2] - q[i + 2]), Math.abs(p[i + 3] - q[i + 3]));
if (w > 8) anders++;
m++;
}
return anders / m;
}, [a.toString("base64"), b.toString("base64")]);
}
let verglichen = 0;
for (const g of GROESSEN) {
const bild = await aufnehmen(g.n, !!g.maskable);
writeFileSync(join(ZIEL, `crew-${g.endung}.png`), bild);
gebaut++;
const streuung = await streuungVon(bild);
if (streuung < 12) maengel.push(`${g.endung}: kaum Zeichnung (Streuung ${streuung.toFixed(1)})`);
const klein = g.n < 64;
const ohneZeichnung = await unterschied(bild, await aufnehmen(g.n, !!g.maskable, "zeichnung"));
if (klein) {
/* Unter 64 px ist die schlichte Silhouette Absicht. Geprueft wird,
dass sie es auch bleibt -- sonst waere die Regel bloss ein
Kommentar, der beim naechsten Umbau stillschweigend kippt. */
if (ohneZeichnung > 0.001) maengel.push(`${g.endung}: klein soll ohne Gesichtszeichnung sein`);
} else if (ohneZeichnung < 0.04) {
maengel.push(`${g.endung}: das Gesicht kommt nicht durch (${(ohneZeichnung * 100).toFixed(1)} %)`);
}
/* DRITTE GEGENPROBE: anders als das Workspace-Symbol. */
const gegen = join(VERGLEICH, `workspace-${g.endung}.png`);
let anders = -2;
if (!existsSync(gegen)) {
maengel.push(`${g.endung}: Vergleichsbild workspace-${g.endung}.png fehlt `
+ `-- nicht nachsehbar, ob sich die Symbole unterscheiden`);
} else {
anders = await unterschied(bild, readFileSync(gegen));
verglichen++;
if (anders < 0) {
maengel.push(`${g.endung}: Vergleichsbild hat eine andere Groesse -- nicht vergleichbar`);
} else if (anders < 0.10) {
maengel.push(`${g.endung}: kaum vom Workspace-Symbol zu unterscheiden `
+ `(${(anders * 100).toFixed(1)} %)`);
}
}
console.log(` crew-${g.endung}.png`.padEnd(30)
+ `${(bild.length / 1024).toFixed(1)} KB · Streuung ${streuung.toFixed(1)}`
+ ` · Gesicht ${(ohneZeichnung * 100).toFixed(1)} %`
+ ` · anders als Workspace ${anders < 0 ? "?" : (anders * 100).toFixed(1) + " %"}`);
}
await browser.close();
/* Die Zahlen gehoeren in die Bedingung, nicht nur in den Bericht. */
if (gebaut !== GROESSEN.length) {
console.error(`\nFEHLER: ${gebaut} von ${GROESSEN.length} erzeugt.`); process.exit(1);
}
if (verglichen !== GROESSEN.length) {
console.error(`\nFEHLER: nur ${verglichen} von ${GROESSEN.length} gegen das `
+ `Workspace-Symbol gehalten.`); process.exit(1);
}
if (maengel.length) {
console.error("\nFEHLER:\n " + maengel.join("\n ")); process.exit(1);
}
console.log(`\n${gebaut} Symbole erzeugt. Der Husky ist derselbe wie bei DogFather,`
+ ` das Gesicht ist in jeder grossen Fassung nachweisbar sichtbar, und alle`
+ ` ${verglichen} unterscheiden sich messbar vom Workspace-Symbol.`);
+20 -7
View File
@@ -20,7 +20,7 @@
Gehoert vor jeden Deploy, der JS oder CSS im Workspace anfasst.
===================================================================== */
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
@@ -88,11 +88,24 @@ for (const name of readdirSync(ORDNER)) {
Derselbe Stempel wie in den Seiten -- `pruef-zwischenspeicher`
verlangt im ganzen Haus genau eine Zahl und wuerde eine zweite sofort
melden. */
const MANIFEST = join(ORDNER, "app.webmanifest");
/* ES SIND ZWEI MANIFESTE (10.09.2026). Seit die Modi-App unter ihrer
eigenen Adresse laeuft, gibt es neben app.webmanifest auch
crew.webmanifest -- dasselbe Haus, anderer Name, andere Symbole.
ALS LISTE UND NICHT ALS ZWEITER BLOCK: Ein kopierter Block waere beim
dritten Manifest ein dritter, und irgendwann bleibt einer stehen. Ein
fehlendes Manifest ist hier ausserdem ein FEHLER und kein
Ueberspringen -- sonst faellt das Ausbleiben nicht auf. */
const MANIFESTE = ["app.webmanifest", "crew.webmanifest"];
const MUSTER_MANIFEST =
/("src":\s*")(\/assets\/img\/app-symbole\/[A-Za-z0-9._-]+)(?:\?v=\d+)?(")/g;
{
const alt = readFileSync(MANIFEST, "utf8");
for (const name of MANIFESTE) {
const pfad = join(ORDNER, name);
if (!existsSync(pfad)) {
console.error(`FEHLER: ${name} fehlt -- erwartet in ${ORDNER}.`);
process.exit(1);
}
const alt = readFileSync(pfad, "utf8");
let n = 0;
const neu = alt.replace(MUSTER_MANIFEST, (_, vor, datei, nach) => {
n += 1;
@@ -102,11 +115,11 @@ const MUSTER_MANIFEST =
Dann hat jemand das Manifest umgebaut, und ab da laeuft das Symbol
still auf dem alten Stand weiter. */
if (n === 0) {
console.error("FEHLER: im Manifest keine Symboladresse gefunden -- Muster pruefen.");
console.error(`FEHLER: in ${name} keine Symboladresse gefunden -- Muster pruefen.`);
process.exit(1);
}
if (neu !== alt) { writeFileSync(MANIFEST, neu); dateien += 1; stellen += n; }
console.log(` app.webmanifest ${n} Verweise`);
if (neu !== alt) { writeFileSync(pfad, neu); dateien += 1; stellen += n; }
console.log(` ${name.padEnd(20)} ${n} Verweise`);
}
console.log(`\nStempel ${STEMPEL} auf ${stellen} Verweise in ${dateien} Dateien gesetzt.`);
+4 -4
View File
@@ -13,25 +13,25 @@
"theme_color": "#06090f",
"icons": [
{
"src": "/assets/img/app-symbole/workspace-192.png?v=202609100303",
"src": "/assets/img/app-symbole/workspace-192.png?v=202609101301",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-512.png?v=202609100303",
"src": "/assets/img/app-symbole/workspace-512.png?v=202609101301",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100303",
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609101301",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100303",
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609101301",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+27
View File
@@ -414,6 +414,10 @@
};
const event = istEventArt(daten.art);
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 (event) {
daten.event_ende = $('f-ende').value || null;
@@ -506,6 +510,29 @@
$('f-art').append(o);
}
$('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;
if (LEITUNG.has(ich.rolle)) {
+164 -1
View File
@@ -114,6 +114,165 @@
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() {
const ziel = $('lage');
try {
@@ -128,11 +287,15 @@
s + Object.values(m.bereiche).reduce((x, b) => x + b.offen, 0), 0);
/* Eine leere Seite ohne Erklaerung sieht aus wie ein Fehler --
deshalb steht hier auch, was zu tun waere. */
const wartend = (daten.eingang || []).length + (daten.angebote || []).length;
$('kopf-unter').textContent = leute.length
? `${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.';
ziel.append(eingangBauen(daten.eingang || []));
ziel.append(angeboteBauen(daten.angebote || []));
for (const m of leute) ziel.append(karte(m));
} catch { /* umgeleitet */ }
}
+11 -11
View File
@@ -12,12 +12,12 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -245,10 +245,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/aufgaben.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/aufgaben.js?v=202609101301" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -140,10 +140,10 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/automation.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/automation.js?v=202609101301" defer></script>
</body>
</html>
+26 -18
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -75,8 +75,16 @@
<label class="feld-schild" for="f-ende">Bis</label>
<input id="f-ende" type="date" />
</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>
<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">
<option value="hoch">hoch</option>
<option value="mittel" selected>mittel</option>
@@ -84,7 +92,7 @@
</select>
</div>
<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">
<option value="">—</option>
<option value="1">1 – schwach</option>
@@ -149,14 +157,14 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/checkliste.js?v=202609100303" defer></script>
<script src="assets/js/ampel.js?v=202609100303" defer></script>
<script src="assets/js/schulung.js?v=202609100303" defer></script>
<script src="assets/js/vorlagen.js?v=202609100303" defer></script>
<script src="assets/js/bereich.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/checkliste.js?v=202609101301" defer></script>
<script src="assets/js/ampel.js?v=202609101301" defer></script>
<script src="assets/js/schulung.js?v=202609101301" defer></script>
<script src="assets/js/vorlagen.js?v=202609101301" defer></script>
<script src="assets/js/bereich.js?v=202609101301" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -82,11 +82,11 @@
</main>
<script src="assets/js/ki.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/calls.js?v=202609100303" defer></script>
<script src="assets/js/ki.js?v=202609101301" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/calls.js?v=202609101301" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,19 +12,19 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -247,10 +247,10 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609100303"></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303"></script>
<script src="assets/js/glocke.js?v=202609100303"></script>
<script src="assets/js/chat.js?v=202609100303"></script>
<script src="assets/js/bereiche.js?v=202609101301"></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301"></script>
<script src="assets/js/glocke.js?v=202609101301"></script>
<script src="assets/js/chat.js?v=202609101301"></script>
</body>
</html>
+14 -14
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/content.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/content.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -172,12 +172,12 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/checkliste.js?v=202609100303" defer></script>
<script src="assets/js/vorlagen.js?v=202609100303" defer></script>
<script src="assets/js/content.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/checkliste.js?v=202609101301" defer></script>
<script src="assets/js/vorlagen.js?v=202609101301" defer></script>
<script src="assets/js/content.js?v=202609101301" defer></script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
{
"name": "Team Dogi",
"short_name": "Team Dogi",
"description": "Aufgaben, Termine, Ideen und Angebote an einem Ort. Nur mit persönlichem Zugangscode.",
"start_url": "/workspace/start.html",
"id": "/workspace/start.html",
"scope": "/workspace/",
"display": "standalone",
"orientation": "any",
"lang": "de",
"dir": "ltr",
"background_color": "#0a121e",
"theme_color": "#06090f",
"icons": [
{
"src": "/assets/img/app-symbole/crew-192.png?v=202609101301",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-512.png?v=202609101301",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609101301",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609101301",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -101,10 +101,10 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/dateien.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/dateien.js?v=202609101301" defer></script>
</body>
</html>
+4 -4
View File
@@ -30,9 +30,9 @@
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
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="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
</head>
<body class="gate">
@@ -401,6 +401,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609100303" defer></script>
<script src="assets/js/gate.js?v=202609101301" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -370,11 +370,11 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/kalender.js?v=202609100303" defer></script>
<script src="assets/js/kalender-abo.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/kalender.js?v=202609101301" defer></script>
<script src="assets/js/kalender-abo.js?v=202609101301" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -217,10 +217,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609100303"></script>
<script src="assets/js/bereiche.js?v=202609100303"></script>
<script src="assets/js/kopf.js?v=202609100303"></script>
<script src="assets/js/glocke.js?v=202609100303"></script>
<script src="assets/js/leistung.js?v=202609100303"></script>
<script src="assets/js/wahl.js?v=202609101301"></script>
<script src="assets/js/bereiche.js?v=202609101301"></script>
<script src="assets/js/kopf.js?v=202609101301"></script>
<script src="assets/js/glocke.js?v=202609101301"></script>
<script src="assets/js/leistung.js?v=202609101301"></script>
</body>
</html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -335,10 +335,10 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/personen.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/personen.js?v=202609101301" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -207,11 +207,11 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/steckbrief.js?v=202609100303" defer></script>
<script src="assets/js/profil.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/steckbrief.js?v=202609101301" defer></script>
<script src="assets/js/profil.js?v=202609101301" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/report.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/report.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -99,11 +99,11 @@
</main>
<script src="assets/js/ki.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/report.js?v=202609100303" defer></script>
<script src="assets/js/ki.js?v=202609101301" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/report.js?v=202609101301" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -132,10 +132,10 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/scouting.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/scouting.js?v=202609101301" defer></script>
</body>
</html>
+11 -11
View File
@@ -12,14 +12,14 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
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/module.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/heim.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -667,10 +667,10 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/start.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/start.js?v=202609101301" defer></script>
</body>
</html>
+13 -13
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -78,11 +78,11 @@
</main>
<script src="assets/js/ki.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/startcheck.js?v=202609100303" defer></script>
<script src="assets/js/ki.js?v=202609101301" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/startcheck.js?v=202609101301" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -119,10 +119,10 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/steckbrief.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/steckbrief.js?v=202609101301" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/team.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/team.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -103,10 +103,10 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/team.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/team.js?v=202609101301" defer></script>
</body>
</html>
+10 -10
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/team.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/team.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -66,8 +66,8 @@
</main>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/teamlage.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/teamlage.js?v=202609101301" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,15 +12,15 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -142,11 +142,11 @@
</main>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/uebersicht.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/uebersicht.js?v=202609101301" defer></script>
</body>
</html>
+12 -12
View File
@@ -12,13 +12,13 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100303" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100303" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/start.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609100303" />
<link rel="stylesheet" href="assets/css/module.css?v=202609100303" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609101301" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609101301" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/start.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609101301" />
<link rel="stylesheet" href="assets/css/module.css?v=202609101301" />
</head>
<body class="start">
@@ -183,10 +183,10 @@
</main>
<script src="assets/js/wahl.js?v=202609100303" defer></script>
<script src="assets/js/bereiche.js?v=202609100303" defer></script>
<script src="assets/js/kopf.js?v=202609100303" defer></script>
<script src="assets/js/glocke.js?v=202609100303" defer></script>
<script src="assets/js/wissen.js?v=202609100303" defer></script>
<script src="assets/js/wahl.js?v=202609101301" defer></script>
<script src="assets/js/bereiche.js?v=202609101301" defer></script>
<script src="assets/js/kopf.js?v=202609101301" defer></script>
<script src="assets/js/glocke.js?v=202609101301" defer></script>
<script src="assets/js/wissen.js?v=202609101301" defer></script>
</body>
</html>