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