Fertige Vorlagen zum Uebernehmen - Content-Ideen und LIVE-Punkte

Der Wunsch stand an fuenf Stellen gleichlautend: "ich will dass da schon
fertige Sachen stehen". Eine leere Seite mit einem Knopf "Neuer Eintrag"
verlangt vom Creator genau das, was er noch nicht kann -- zu wissen, was
ueberhaupt hineingehoert.

WARUM VORLAGEN UND KEINE VORAUSGEFUELLTEN DATEN. Man koennte beim
Anlegen eines Creators dreissig Eintraege in seine Datenbank schreiben.
Das waere falsch: Sie waeren ab dem ersten Tag "seine" Eintraege und
damit Altlast; aendert Filipe spaeter eine Formulierung, gilt sie nur
fuer neue Creator; und die Liste saehe voll aus, obwohl noch nichts
geschehen ist -- das Gegenteil einer ehrlichen Uebersicht.

Vorlagen bleiben deshalb VORSCHLAEGE, bis jemand sie uebernimmt. Sie
stehen im Code, gelten fuer alle sofort, und werden erst dann zu Daten,
wenn sie gebraucht werden. Was uebernommen ist, ist ein ganz normaler
Eintrag -- aenderbar, loeschbar, und von spaeteren Aenderungen an der
Vorlage unberuehrt.

INHALTE, fachlich begruendet:
  13 Content-Ideen, jede mit AUSFORMULIERTEM Aufhaenger und Format. Die
  ersten ein bis drei Sekunden entscheiden ueber die Verbreitung --
  "mach was Persoenliches" hilft niemandem, "Das haette ich am Anfang
  gern gewusst" kann man sagen. Verteilt nach 70/20/10 (Wert, Community,
  Eigenwerbung).

  21 LIVE-Punkte in drei Abschnitten. Der mittlere ist der wichtigste
  und gibt es sonst nirgends: Was WAEHREND der Sendung auffaellt, ist am
  naechsten Tag weg. Diese Punkte sind so formuliert, dass man sie in
  einem Moment anklicken kann, in dem man eigentlich keine Zeit hat.

  Die Vorbereitung ist die laengste Liste, weil ein LIVE dort steht und
  faellt. Ton zuerst -- der Grund Nummer eins, warum Leute wieder gehen.

"ICH BRAUCHE HILFE" wird eine AUFGABE, keine Nachricht. Eine Nachricht
ist gelesen und dann weg; eine Aufgabe bleibt stehen, bis sie jemand
erledigt, geht an den zustaendigen Betreuer (ohne Betreuer an die
Leitung -- eine Bitte um Hilfe darf nicht ins Leere laufen), traegt hohe
Prioritaet und eine Frist von drei Tagen. Ohne Frist bleibt sie liegen;
das ist der Unterschied zwischen einer Aufgabe und einem Zettel.

"CONTENT-PLANUNG" HEISST JETZT "CONTENT-IDEEN". "Planung" klang nach
Terminen und Tabellen; was dort wirklich passiert, ist das Sammeln und
Weiterentwickeln von Ideen. Der Kalender daneben plant.

EIN CREATOR DARF SEINE EIGENEN EINTRAEGE AENDERN -- aber nur im Bereich
Content. Wenn er sich eine Idee uebernimmt, ist das SEINE Idee; sie
danach nicht umbenennen zu duerfen waere absurd. LIVE, Technik,
Community und Schutz bleiben die Betreuungsakte, dort aendert er nichts.
Ein erster Anlauf hatte die Ausnahme fuer alle Bereiche erlaubt -- die
Pruefung pruef-bereiche-lesend hat das sofort gemeldet.

DREI EIGENE FEHLER, VON DEN PRUEFUNGEN GEFUNDEN:
- Der Vorlagenblock vergass nach dem Uebernehmen, was schon geholt war:
  Die Liste wird neu geladen, der Block neu gebaut, und die Markierung
  am Element war jedes Mal weg. Jetzt merkt sich das Modul die Auswahl.
- Die Regel fuer eigene Eintraege war zu breit (siehe oben).
- Die Marken im Vorlagenblock waren auf dem Handy 10,2 px klein.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-01 16:27:30 +02:00
co-authored by Claude Opus 5
parent 6996c5957d
commit ca10479909
28 changed files with 1199 additions and 136 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+2
View File
@@ -31,6 +31,7 @@ import { kiRouter } from "./workspace-ki.js";
import { contentRouter } from "./workspace-content.js";
import { sicherungRouter, sicherungStarten } from "./workspace-sicherung.js";
import { steckbriefRouter } from "./workspace-steckbrief.js";
import { vorlagenRouter } from "./workspace-vorlagen.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SITE_DIR = join(__dirname, "..");
@@ -96,6 +97,7 @@ app.use(sicherungRouter);
JSON in einem <img> ergibt nur ein kaputtes Bild ohne jeden Hinweis.
Der Steckbrief bringt seine eigene Schranke mit. */
app.use(steckbriefRouter);
app.use(vorlagenRouter);
app.use(aufgabenRouter);
app.use(personenRouter);
app.use(profilRouter);
+282
View File
@@ -0,0 +1,282 @@
/* Prueft die fertigen Vorlagen und was daraus wird.
Der Kern ist nicht "es gibt Vorlagen", sondern was beim UEBERNEHMEN
passiert: Aus einem Vorschlag muss ein ganz normaler Eintrag werden --
aenderbar, loeschbar, und ab dann unabhaengig von der Vorlage.
Und "Hilfe anfordern" muss eine AUFGABE erzeugen, keine Nachricht.
Eine Nachricht ist gelesen und dann weg; eine Aufgabe bleibt stehen,
bis sie jemand erledigt. Geprueft wird deshalb, dass sie beim
richtigen Betreuer landet und eine Frist hat. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-vorl-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4204";
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 }); };
await import("./index.js");
await new Promise((r) => setTimeout(r, 700));
const BASIS = "http://127.0.0.1:4204";
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_000).unref?.();
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idScout, jetzt);
d.close();
const keks = async (rolle, code) => {
const a = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle, code }) });
return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
};
const kDogi = await keks("admin", "CODE-DOGI-0001");
const kTili = await keks("creator", "CODE-ANNA-0001");
const hole = (pfad, k, o = {}) => fetch(BASIS + pfad, { ...o, headers: { Cookie: k, ...(o.headers || {}) } });
/* ---------- Was es gibt --------------------------------------------------- */
console.log("\n=== Die Vorlagen");
let vorlagen;
{
const a = await hole("/workspace/api/vorlagen", kTili);
vorlagen = await a.json();
ok(a.ok, "die Vorlagen sind abrufbar");
ok(vorlagen.content.length >= 12, `genug Content-Ideen (${vorlagen.content.length})`);
ok(vorlagen.live.vorbereitung.length >= 6, `Vorbereitungspunkte (${vorlagen.live.vorbereitung.length})`);
ok(vorlagen.live.mitschrift.length >= 5, `Punkte fuer waehrend LIVE (${vorlagen.live.mitschrift.length})`);
ok(vorlagen.live.auswertung.length >= 5, `Auswertungspunkte (${vorlagen.live.auswertung.length})`);
/* Jede Content-Idee braucht einen ausformulierten Aufhaenger -- die
ersten Sekunden entscheiden, und "mach was Persoenliches" hilft
niemandem. */
const ohneHook = vorlagen.content.filter((v) => !v.hook || v.hook.length < 15);
ok(ohneHook.length === 0,
`jede Idee hat einen ausformulierten Aufhaenger (${ohneHook.length} ohne)`);
const ohneFormat = vorlagen.content.filter((v) => !v.format);
ok(ohneFormat.length === 0, `und ein Format (${ohneFormat.length} ohne)`);
/* 70/20/10: der groesste Teil muss Inhalte mit Wert sein. */
const zaehl = {};
for (const v of vorlagen.content) zaehl[v.saeule] = (zaehl[v.saeule] || 0) + 1;
const anteilWert = zaehl.Wert / vorlagen.content.length;
ok(anteilWert >= 0.45,
`der groesste Teil sind Inhalte mit Wert (${Math.round(anteilWert * 100)} %: ${JSON.stringify(zaehl)})`);
/* Punkte, bei denen man Hilfe braucht, muessen als solche erkennbar
sein -- sonst weiss niemand, wo er fragen darf. */
const mitHilfe = Object.values(vorlagen.live).flat().filter((v) => v.hilfe);
ok(mitHilfe.length >= 5, `Punkte mit Hilfe-Angebot (${mitHilfe.length})`);
}
/* ---------- Uebernehmen --------------------------------------------------- */
console.log("\n=== Uebernehmen");
{
/* Ein Creator uebernimmt fuer sich selbst -- ohne auf jemanden zu warten. */
const a = await hole("/workspace/api/vorlagen/uebernehmen", kTili, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ art: "content", nr: 0 }) });
const r = await a.json();
ok(a.ok && r.angelegt === 1, `ein Creator darf selbst uebernehmen (${r.angelegt})`);
const liste = await (await hole("/workspace/api/bereich/content", kTili)).json();
const drin = liste.eintraege.find((e) => e.titel === vorlagen.content[0].titel);
ok(!!drin, `die Idee steht jetzt in seiner Liste ("${drin?.titel}")`);
/* Der Aufhaenger muss MITKOMMEN -- sonst ist die halbe Vorlage weg. */
ok(drin?.hook === vorlagen.content[0].hook, `mit dem Aufhaenger (${drin?.hook?.slice(0, 30)}…)`);
ok(drin?.format === vorlagen.content[0].format, `und dem Format (${drin?.format})`);
ok(drin?.art === "idee", `und beginnt bei "Idee" (${drin?.art})`);
/* Sie ist ab jetzt ein NORMALER Eintrag: aenderbar. Das ist der
eigentliche Punkt an Vorlagen gegenueber fest eingebauten Daten. */
const p = await hole(`/workspace/api/bereich/content/${drin.id}`, kTili, {
method: "PATCH", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ titel: "Selbst umbenannt" }) });
ok(p.ok, `und laesst sich aendern (${p.status})`);
/* Alle auf einmal. */
const b = await hole("/workspace/api/vorlagen/uebernehmen", kTili, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ art: "live_alle", unterart: "vorbereitung" }) });
const rb = await b.json();
ok(b.ok && rb.angelegt === vorlagen.live.vorbereitung.length,
`alle Vorbereitungspunkte auf einmal (${rb.angelegt})`);
const live = await (await hole("/workspace/api/bereich/live", kTili)).json();
ok(live.eintraege.filter((e) => e.art === "vorbereitung").length === rb.angelegt,
"und sie stehen alle im Bereich LIVE");
}
/* Ein Betreuer uebernimmt fuer SEINEN Creator -- und nur fuer den. */
console.log("\n=== Wer fuer wen");
{
const fremd = anlegenFremd();
const a = await hole("/workspace/api/vorlagen/uebernehmen", kDogi, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ art: "content", nr: 1, creator_id: idTili }) });
ok(a.ok, `DogFather darf fuer Tili uebernehmen (${a.status})`);
const kScout = await keks("scout", "CODE-SCOU-0001");
const b = await hole("/workspace/api/vorlagen/uebernehmen", kScout, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ art: "content", nr: 2, creator_id: fremd }) });
ok(b.status === 404,
`ein Scout NICHT fuer einen fremden Creator -- und erfaehrt nicht, dass es ihn gibt (${b.status})`);
}
function anlegenFremd() {
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-FREM-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d2.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run("Fremd", "creator", hash, salt, 32768, jetzt);
const id = d2.prepare("SELECT last_insert_rowid() AS id").get().id;
d2.close();
return id;
}
/* ---------- Hilfe anfordern ----------------------------------------------- */
console.log("\n=== Hilfe anfordern");
{
const a = await hole("/workspace/api/vorlagen/hilfe", kTili, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ thema: "Ton geprüft", notiz: "Mein Mikro rauscht." }) });
ok(a.ok, `ein Creator kann Hilfe anfordern (${a.status})`);
/* Es muss eine AUFGABE geworden sein, nicht eine Nachricht. */
const d3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
const auf = d3.prepare(
"SELECT * FROM aufgaben WHERE titel LIKE 'Hilfe gewünscht%' ORDER BY id DESC LIMIT 1").get();
d3.close();
ok(!!auf, "daraus wird eine Aufgabe");
ok(auf?.verantwortlich_id === idScout,
`sie geht an seinen Betreuer Patrick (${auf?.verantwortlich_id} = ${idScout})`);
ok(auf?.creator_id === idTili, "und haengt an ihm");
ok(auf?.prioritaet === "hoch", `mit hoher Prioritaet (${auf?.prioritaet})`);
/* Ohne Frist bleibt sie liegen -- das ist der Unterschied zwischen
einer Aufgabe und einem Zettel. */
ok(!!auf?.frist, `und einer Frist (${auf?.frist})`);
ok(String(auf?.beschreibung || "").includes("Mein Mikro rauscht"),
"die Notiz steht mit drin");
/* Der Betreuer sieht sie auf seiner Startseite. */
const kScout = await keks("scout", "CODE-SCOU-0001");
/* Sie steht in SEINER Aufgabenliste. Als Hinweis auf der Startseite
erscheint sie erst, wenn die Frist naeher rueckt -- eine Aufgabe mit
drei Tagen Zeit ist weder ueberfaellig noch heute faellig, und das
ist richtig so. Ein erster Anlauf hat genau das geprueft und war
rot, obwohl das Verhalten stimmte. */
const seine = await (await hole("/workspace/api/aufgaben", kScout)).json();
const dabei = (seine.aufgaben || []).some((x) => String(x.titel).startsWith("Hilfe gewünscht"));
ok(dabei, `und steht in seiner Aufgabenliste (${(seine.aufgaben || []).length} Aufgaben)`);
/* Ohne Thema geht nichts -- eine leere Bitte hilft niemandem. */
const leer = await hole("/workspace/api/vorlagen/hilfe", kTili, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ thema: "" }) });
ok(leer.status === 400, `ohne Thema wird abgelehnt (${leer.status})`);
}
/* ---------- Im Browser ---------------------------------------------------- */
console.log("\n=== Die Seite");
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
{
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
const konsole = [];
const seite = await kontext.newPage();
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
await seite.click('.rolle[data-rolle="creator"]');
await seite.fill("#code", "CODE-ANNA-0001");
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 15000 });
for (const [seiteName, art] of [["content.html", "Ideen"], ["bereich.html?b=live", "Punkte"]]) {
await seite.goto(BASIS + "/workspace/" + seiteName, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const r = await seite.evaluate(() => ({
da: !document.getElementById("vorlagen")?.hidden,
knopf: document.querySelector(".vorlagen__kopf .knopf")?.textContent?.trim(),
offen: document.querySelector(".vorlagen__raster")?.hidden === false,
anzahl: document.querySelectorAll(".vorlage").length,
}));
ok(r.da, `${seiteName}: der Vorlagenblock steht da`);
ok(/Vorschläge zeigen/.test(r.knopf || ""), ` mit Aufklappknopf ("${r.knopf}")`);
ok(r.offen === false, " und ist zunächst zu");
ok(r.anzahl > 0, ` ${r.anzahl} Vorschläge dahinter`);
/* Aufklappen muss wirklich etwas zeigen. */
await seite.click(".vorlagen__kopf .knopf");
await seite.waitForTimeout(350);
const auf = await seite.evaluate(() => ({
offen: document.querySelector(".vorlagen__raster")?.hidden === false,
sichtbar: [...document.querySelectorAll(".vorlage")].filter((v) => v.offsetParent !== null).length,
hooks: document.querySelectorAll(".vorlage__hook").length,
}));
ok(auf.offen && auf.sichtbar > 0, ` aufgeklappt sind ${auf.sichtbar} zu sehen`);
if (art === "Ideen") {
ok(auf.hooks === auf.sichtbar, ` und jede zeigt ihren Aufhänger (${auf.hooks})`);
}
}
/* Übernehmen über die Oberfläche -- nicht nur über die Schnittstelle. */
await seite.goto(BASIS + "/workspace/content.html", { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
await seite.click(".vorlagen__kopf .knopf");
await seite.waitForTimeout(300);
/* Die Karten heissen dort .video -- ein erster Anlauf suchte nach
".karte" und zaehlte deshalb immer null. */
const vorher = await seite.evaluate(() => document.querySelectorAll(".video").length);
await seite.click(".vorlage .schritt");
await seite.waitForTimeout(1200);
const nachher = await seite.evaluate(() => ({
karten: document.querySelectorAll(".video").length,
genommen: document.querySelectorAll('.vorlage[data-genommen="ja"]').length,
}));
ok(nachher.karten > vorher, `Übernehmen legt wirklich an (${vorher} → ${nachher.karten})`);
ok(nachher.genommen === 1, `und die Vorlage ist als übernommen markiert (${nachher.genommen})`);
ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
await seite.screenshot({ path: "pruef-vorlagen.png", fullPage: false });
await kontext.close();
}
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
process.exit(fehler ? 1 : 0);
+49 -3
View File
@@ -41,7 +41,10 @@ export const BEREICHE = {
vorher alles, was den Unterschied macht. Und nicht mehr als fuenf --
jede weitere Spalte ist eine, die auf dem Handy nicht mehr passt. */
content: {
name: "Content-Planung",
/* Umbenannt am 01.09.2026: "Planung" klang nach Terminen und
Tabellen. Was hier wirklich passiert, ist das Sammeln und
Weiterentwickeln von IDEEN -- der Kalender daneben plant. */
name: "Content-Ideen",
arten: {
idee: "Idee",
skript: "Hook & Skript",
@@ -122,13 +125,56 @@ bereicheRouter.use("/workspace/api/bereich", angemeldet);
-- in jeder der drei Stellen einzeln pruefen -- ist genau die Bauweise,
durch die am 31.08.2026 schon einmal ein Loch entstanden ist: Zwei von
drei Stellen waren abgesichert, die dritte vergessen. */
/* Ein Creator schreibt hier nicht -- MIT EINER AUSNAHME, die am
01.09.2026 dazukam: Was er SELBST angelegt hat, darf er auch
aendern und loeschen.
Die urspruengliche Regel ("hier tragen deine Betreuer ein") bleibt
damit unangetastet: Er kann weiterhin nichts an dem aendern, was
ueber ihn geschrieben wurde -- keine Bewertung, keine Notiz, keinen
Eintrag seines Managers.
Noetig wurde die Ausnahme durch die fertigen Vorlagen: Wenn ein
Creator sich eine Content-Idee uebernimmt, ist das SEINE Idee. Sie
danach nicht umbenennen zu duerfen waere absurd -- er haette einen
Eintrag erzeugt, den nur ein anderer bearbeiten kann.
Entscheidend ist erstellt_von, nicht creator_id: Ein Eintrag, den
sein Betreuer ueber ihn angelegt hat, traegt zwar seine creator_id,
gehoert ihm aber nicht. */
function nichtSchreibendFuerCreator(req, res, next) {
if (req.person?.rolle === "creator") {
if (req.person?.rolle !== "creator") return next();
/* Neu anlegen bleibt gesperrt -- der Weg dafuer ist "Vorlage
uebernehmen", und der prueft eigene Regeln. */
/* NUR im Bereich content. Das ist der Bereich, in dem der Creator
selbst arbeitet -- seine Ideen, sein Material. LIVE, Technik,
Community und Schutz sind die BETREUUNGSAKTE: Dort schreiben die
Betreuer ueber ihn, und daran aendert er nichts, auch nicht an
etwas, das frueher einmal von ihm stammte. Das war am 31.08.2026
ausdruecklich so entschieden und bleibt so.
Ein erster Anlauf hatte die Ausnahme fuer ALLE Bereiche erlaubt --
die Pruefung pruef-bereiche-lesend hat das sofort gemeldet. */
const bereich = String(req.path).split("/")[4] || "";
if (bereich !== "content") {
return res.status(403).json({
fehler: "Hier tragen deine Betreuer ein. Du siehst alles, was zu dir gehört.",
});
}
next();
const id = Number(String(req.path).split("/").pop());
if (Number.isInteger(id) && id > 0) {
try {
const e = db().prepare("SELECT erstellt_von FROM eintraege WHERE id = ?").get(id);
if (e && e.erstellt_von === req.person.id) return next();
} catch { /* faellt unten auf die Sperre zurueck */ }
}
return res.status(403).json({
fehler: "Das hat deine Betreuung eingetragen – ändern können sie es. "
+ "Was du selbst angelegt hast, kannst du bearbeiten.",
});
}
/* Express 4 -- dort ist der Platzhalter `*`, nicht `*name` wie in
+353
View File
@@ -0,0 +1,353 @@
/* =====================================================================
workspace-vorlagen.js — fertige Inhalte, die man übernehmen kann.
Der Wunsch vom 01.09.2026, an fünf Stellen gleichlautend: "ich will
dass da schon fertige Sachen stehen". Eine leere Seite mit einem
Knopf "Neuer Eintrag" verlangt vom Creator genau das, was er noch
nicht kann — zu wissen, was überhaupt hineingehört.
WARUM VORLAGEN UND KEINE VORAUSGEFÜLLTEN DATEN.
Man könnte beim Anlegen eines Creators dreißig Einträge in seine
Datenbank schreiben. Das wäre falsch:
* Sie wären ab dem ersten Tag "seine" Einträge -- und damit
Altlast, sobald sich etwas ändert.
* Ändert Filipe später eine Formulierung, gilt sie nur für neue
Creator. Die alten behalten die alte für immer.
* Und die Liste sähe voll aus, obwohl noch nichts geschehen ist --
genau das Gegenteil einer ehrlichen Übersicht.
Vorlagen bleiben deshalb VORSCHLÄGE, bis jemand sie übernimmt. Sie
stehen im Code, gelten für alle sofort, und werden erst dann zu Daten,
wenn sie tatsächlich gebraucht werden. Wer sie übernimmt, bekommt
einen ganz normalen Eintrag, den er ändern und löschen kann.
FACHLICHE GRUNDLAGE der Inhalte:
* Die ersten ein bis drei Sekunden eines Kurzvideos entscheiden über
die Verbreitung -- deshalb hat jede Content-Vorlage einen Hook,
und zwar als eigenes Feld.
* Ein LIVE steht und fällt mit dem, was VOR der Sendung passiert.
Deshalb sind die Vorbereitungspunkte die längste Liste.
* Was während der Sendung auffällt, ist am nächsten Tag vergessen.
Deshalb gibt es Punkte für "während LIVE", die man in Sekunden
anklicken kann, statt sie zu tippen.
* 70/20/10: etwa 70 % Inhalte mit Wert, 20 % Community, 10 %
Eigenwerbung. Die Content-Vorlagen sind danach verteilt.
===================================================================== */
import express from "express";
import {
db, sitzungLesen, istLeitung, darfCreator, protokolliere, echteIp,
} from "./workspace.js";
export const vorlagenRouter = express.Router();
const jetzt = () => new Date().toISOString();
/* =====================================================================
CONTENT-IDEEN
Jede Vorlage bringt mit, was ein Video braucht, bevor man es dreht:
einen Aufhänger, ein Format und die Säule, zu der es gehört. Der
Aufhänger ist ausformuliert, nicht beschrieben -- "mach was
Persönliches" hilft niemandem, "Das hätte ich am Anfang gern gewusst"
kann man sagen.
===================================================================== */
export const CONTENT_IDEEN = [
/* ---- Wert (70 %) ---------------------------------------------------- */
{ titel: "Das hätte ich am Anfang gern gewusst",
hook: "„Ich hab drei Monate gebraucht, um das zu kapieren – du brauchst 30 Sekunden.\"",
format: "Talking Head", saeule: "Wert",
text: "Ein Fehler, den du selbst gemacht hast, und was du heute anders machst. "
+ "Konkret bleiben: ein Fehler, eine Lösung. Wer drei Dinge sagt, sagt nichts." },
{ titel: "Mein Setup für unter 100 €",
hook: "„Alle sagen, du brauchst teure Technik. Hier ist meins für 87 Euro.\"",
format: "Zeigen & Erklären", saeule: "Wert",
text: "Was du wirklich benutzt, mit Preisen. Ehrlich sein, wo es hakt – "
+ "das ist der Teil, den niemand sonst zeigt." },
{ titel: "Drei Einstellungen, die sofort was bringen",
hook: "„Wenn dein Stream unscharf aussieht, liegt es an einer davon.\"",
format: "Bildschirmaufnahme", saeule: "Wert",
text: "Nicht mehr als drei. Jede in unter 15 Sekunden erklärt, mit Vorher/Nachher." },
{ titel: "Was ich in einer Woche gelernt habe",
hook: "„Sieben Tage, eine Erkenntnis – die hier hat alles verändert.\"",
format: "Talking Head", saeule: "Wert",
text: "Rückblick auf eine echte Woche. Funktioniert nur mit einer konkreten Zahl "
+ "oder einem konkreten Moment." },
{ titel: "Antwort auf die Frage, die immer kommt",
hook: "„Das werde ich jeden Tag gefragt – hier einmal in Ruhe.\"",
format: "Talking Head", saeule: "Wert",
text: "Aus dem Chat sammeln, welche Frage sich wiederholt. Die Antwort einmal "
+ "gut aufnehmen und immer wieder verwenden." },
{ titel: "So bereite ich einen Stream vor",
hook: "„Zwei Stunden vor LIVE mache ich immer dasselbe.\"",
format: "Vlog", saeule: "Wert",
text: "Der Ablauf hinter der Sendung. Zeigt Verlässlichkeit und gibt Anfängern "
+ "eine Vorlage." },
{ titel: "Fehler, die ich bei anderen sehe",
hook: "„Bitte mach das nicht – ich sehe es jeden Tag.\"",
format: "Talking Head", saeule: "Wert",
text: "Niemanden bloßstellen. Das Verhalten beschreiben, nicht die Person." },
/* ---- Community (20 %) ------------------------------------------------ */
{ titel: "Der beste Moment aus dem letzten LIVE",
hook: "„Das ist gestern passiert, und ich rede immer noch davon.\"",
format: "Zusammenschnitt", saeule: "Community",
text: "Ein Ausschnitt, keine Zusammenfassung. Wer alles zeigt, zeigt nichts." },
{ titel: "Ich beantworte eure Kommentare",
hook: "„Ihr habt gefragt – jetzt rede ich.\"",
format: "Talking Head", saeule: "Community",
text: "Drei bis fünf echte Kommentare, sichtbar eingeblendet. Auch die kritischen." },
{ titel: "Danke an die, die immer da sind",
hook: "„Ihr seid seit dem ersten Stream dabei. Das hier ist für euch.\"",
format: "Talking Head", saeule: "Community",
text: "Namen nennen, wenn die Leute einverstanden sind. Sonst allgemein bleiben – "
+ "aber ehrlich." },
{ titel: "Eine Entscheidung, bei der ihr mitredet",
hook: "„Ich kann mich nicht entscheiden. Ihr habt 24 Stunden.\"",
format: "Umfrage", saeule: "Community",
text: "Echte Wahl, keine Scheinfrage. Das Ergebnis danach auch wirklich umsetzen." },
/* ---- Eigenwerbung (10 %) --------------------------------------------- */
{ titel: "Wann und wo ich streame",
hook: "„Immer dienstags und donnerstags – und ich fange pünktlich an.\"",
format: "Kurzclip", saeule: "Eigenwerbung",
text: "Fester Sendeplan als eigener Clip. Wiederholbar, sobald sich etwas ändert." },
{ titel: "Was diese Woche ansteht",
hook: "„Drei Sachen, und eine davon habe ich noch nie gemacht.\"",
format: "Kurzclip", saeule: "Eigenwerbung",
text: "Vorschau auf die Woche. Kurz halten, sonst wird es eine Ankündigung "
+ "statt einer Einladung." },
];
/* =====================================================================
LIVE-PUNKTE
Drei Listen entlang der Sendung. Die mittlere ist die wichtigste und
zugleich die, die es sonst nirgends gibt: Was während des Streams
auffällt, ist am nächsten Tag weg. Deshalb sind diese Punkte so
formuliert, dass man sie in einem Moment anklicken kann, in dem man
eigentlich keine Zeit hat.
`hilfe: true` heißt: Der Creator kann das anklicken und sagen, dass
er dabei Unterstützung braucht -- daraus wird eine Aufgabe für seinen
Betreuer.
===================================================================== */
export const LIVE_PUNKTE = {
vorbereitung: [
{ titel: "Ton geprüft", text: "Eine Minute aufnehmen und anhören. Ton ist der Grund Nummer eins, warum Leute wieder gehen.", hilfe: true },
{ titel: "Licht sitzt", text: "Gesicht heller als der Hintergrund. Kein Fenster im Rücken.", hilfe: true },
{ titel: "Verbindung getestet", text: "Upload-Geschwindigkeit geprüft, Kabel statt WLAN wenn möglich.", hilfe: true },
{ titel: "Titel und Thema stehen", text: "Wer den Titel liest, weiß, was ihn erwartet – und ob es für ihn ist.", hilfe: false },
{ titel: "Ablauf grob im Kopf", text: "Drei Blöcke reichen. Ein Stream ohne roten Faden fühlt sich für Zuschauer wie Warten an.", hilfe: false },
{ titel: "Angekündigt", text: "Mindestens einmal vorher gesagt, dass es losgeht – Story, Post oder Nachricht.", hilfe: false },
{ titel: "Moderation geklärt", text: "Weiß jemand Bescheid, der eingreifen kann, wenn es im Chat kippt?", hilfe: true },
{ titel: "Technik-Ersatz bereit", text: "Was passiert, wenn der Stream abbricht? Handy als Notlösung bereitlegen.", hilfe: true },
],
mitschrift: [
{ titel: "Guter Moment – aufheben", text: "Etwas ist gut gelaufen. Kurz notieren, woran es lag – daraus wird später ein Clip.", hilfe: false },
{ titel: "Hier sind Leute gegangen", text: "Zeitpunkt merken. Später nachsehen, was in dem Moment passiert ist.", hilfe: false },
{ titel: "Frage aus dem Chat, die wiederkommt", text: "Wird oft gefragt? Dann gehört sie in ein eigenes Video.", hilfe: false },
{ titel: "Technik hat gehakt", text: "Ton weg, Bild eingefroren, Verzögerung – aufschreiben, solange man es noch weiß.", hilfe: true },
{ titel: "Unangenehme Situation im Chat", text: "Was war es, wie ist es ausgegangen? Auch wenn es gut lief.", hilfe: true },
{ titel: "Neue Zuschauer aufgetaucht", text: "Woher kamen sie? Wurde jemand begrüßt oder ging es unter?", hilfe: false },
{ titel: "Ich wusste nicht weiter", text: "Stelle merken, an der es gestockt hat. Dafür gibt es Hilfe.", hilfe: true },
],
auswertung: [
{ titel: "Was hat diesmal getragen?", text: "Ein Ding. Nicht drei. Das eine, das man wiederholen will.", hilfe: false },
{ titel: "Was war zäh?", text: "Ehrlich sein. Die zähen Stellen sind die, an denen man wächst.", hilfe: false },
{ titel: "Zahlen im Vergleich", text: "Zuschauer, Verweildauer, Kommentare – gegen das letzte Mal, nicht gegen andere.", hilfe: true },
{ titel: "Clips gesichert", text: "Die guten Momente rausgeschnitten und abgelegt, solange man sie noch findet.", hilfe: false },
{ titel: "Eine Sache für nächstes Mal", text: "Genau eine. Wer sich fünf Dinge vornimmt, macht keins davon.", hilfe: false },
{ titel: "Braucht Besprechung", text: "Etwas, das man nicht allein lösen kann – geht an die Betreuung.", hilfe: true },
],
};
/* =====================================================================
CALL-LEITFÄDEN
Jeder Call endet mit To-dos -- das steht schon auf der Seite. Was
fehlte, ist der Weg dorthin. Diese Leitfäden sind bewusst kurz: Ein
Gespräch nach Fragebogen ist ein Verhör, kein Gespräch.
===================================================================== */
export const CALL_LEITFAEDEN = [
{ titel: "Erstgespräch",
text: "Wer bist du, was machst du, was willst du erreichen? · Was läuft schon gut? · "
+ "Was nervt dich gerade am meisten? · Wie viel Zeit hast du wirklich? · "
+ "Was erwartest du von uns – und was erwarten wir von dir?" },
{ titel: "Wochen-Call",
text: "Was ist seit dem letzten Mal passiert? · Was ist liegen geblieben und warum? · "
+ "Zahlen kurz ansehen – eine Auffälligkeit reicht · Was steht diese Woche an? · "
+ "Brauchst du irgendwo Hilfe?" },
{ titel: "Wenn es gerade nicht läuft",
text: "Zuerst zuhören, nicht lösen · Seit wann geht es so? · Was hat sich geändert? · "
+ "Was war das letzte Mal, als es gut war? · Eine kleine Sache vereinbaren, "
+ "nicht fünf große" },
{ titel: "Monats-Review",
text: "Die drei Ziele vom letzten Mal durchgehen · Was ist erreicht, was nicht? · "
+ "Woran lag es – Zeit, Können oder Motivation? · Drei neue Ziele, verbindlich · "
+ "Termin für das nächste Review festhalten" },
];
/* ---------- Schnittstelle ------------------------------------------------ */
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
next();
}
vorlagenRouter.use("/workspace/api/vorlagen", angemeldet);
vorlagenRouter.get("/workspace/api/vorlagen", (req, res) => {
res.json({
content: CONTENT_IDEEN,
live: LIVE_PUNKTE,
calls: CALL_LEITFAEDEN,
});
});
/* ---------- Übernehmen ---------------------------------------------------
Aus einer Vorlage wird ein ganz normaler Eintrag: änderbar, löschbar,
und ab dann unabhängig von der Vorlage. Wer die Vorlage später
ändert, ändert damit NICHT, was schon übernommen wurde -- das wäre
eine böse Überraschung.
Creator dürfen ausdrücklich selbst übernehmen. Es ist ihre Arbeit;
sie sollen nicht darauf warten müssen, dass ein Betreuer für sie eine
Idee anlegt. */
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();
}
vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft,
express.json({ limit: "16kb" }), (req, res) => {
try {
const art = String(req.body.art || "");
const nr = Number(req.body.nr);
/* Für wen? Ein Creator immer für sich; Betreuer für den Creator,
den sie gerade ansehen. */
let creatorId = req.person.rolle === "creator"
? req.person.id
: Number(req.body.creator_id) || null;
if (req.person.rolle !== "creator" && creatorId && !darfCreator(req.person, creatorId)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const angelegt = [];
const einfuegen = (bereich, unterart, titel, text, zusatz = {}) => {
db().prepare(`INSERT INTO eintraege
(bereich, art, titel, text, datum, status, creator_id, hook, format,
erstellt, erstellt_von)
VALUES (?,?,?,?,?,?,?,?,?,?,?)`)
.run(bereich, unterart, titel, text, jetzt().slice(0, 10), "offen",
creatorId, zusatz.hook || null, zusatz.format || null,
jetzt(), req.person.id);
angelegt.push(titel);
};
if (art === "content") {
const v = CONTENT_IDEEN[nr];
if (!v) return res.status(400).json({ fehler: "Unbekannte Vorlage." });
einfuegen("content", "idee", v.titel, v.text, { hook: v.hook, format: v.format });
} else if (art === "content_alle") {
for (const v of CONTENT_IDEEN) {
einfuegen("content", "idee", v.titel, v.text, { hook: v.hook, format: v.format });
}
} else if (art === "live") {
const unterart = String(req.body.unterart || "");
const liste = LIVE_PUNKTE[unterart];
if (!liste) return res.status(400).json({ fehler: "Unbekannter Abschnitt." });
const v = liste[nr];
if (!v) return res.status(400).json({ fehler: "Unbekannte Vorlage." });
einfuegen("live", unterart, v.titel, v.text);
} else if (art === "live_alle") {
const unterart = String(req.body.unterart || "");
const liste = LIVE_PUNKTE[unterart];
if (!liste) return res.status(400).json({ fehler: "Unbekannter Abschnitt." });
for (const v of liste) einfuegen("live", unterart, v.titel, v.text);
} else {
return res.status(400).json({ fehler: "Unbekannte Art." });
}
protokolliere("vorlage_uebernommen", req.person, echteIp(req),
`${art}: ${angelegt.length} Eintrag/Einträge`);
res.json({ angelegt: angelegt.length, titel: angelegt });
} catch (fehler) {
console.error("[workspace] Vorlage übernehmen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- "Ich brauche Hilfe" ------------------------------------------
Ein Creator klickt an einem LIVE-Punkt auf "Hilfe". Daraus wird eine
Aufgabe für seinen Betreuer -- nicht eine Nachricht, die untergeht,
sondern ein Eintrag im Aufgabenbrett mit Frist.
Warum eine Aufgabe und keine Benachrichtigung: Eine Benachrichtigung
ist gelesen und dann weg. Eine Aufgabe bleibt stehen, bis sie jemand
erledigt -- und taucht auf der Startseite des Betreuers auf, wenn sie
überfällig wird. */
vorlagenRouter.post("/workspace/api/vorlagen/hilfe", gleicheHerkunft,
express.json({ limit: "8kb" }), (req, res) => {
try {
const thema = String(req.body.thema || "").trim().slice(0, 160);
if (thema.length < 3) return res.status(400).json({ fehler: "Worum geht es?" });
const notiz = String(req.body.notiz || "").trim().slice(0, 2000);
const creatorId = req.person.rolle === "creator" ? req.person.id
: Number(req.body.creator_id) || null;
if (!creatorId) return res.status(400).json({ fehler: "Kein Creator angegeben." });
if (req.person.rolle !== "creator" && !darfCreator(req.person, creatorId)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
/* An wen? An den Betreuer. Gibt es keinen, an die Leitung -- eine
Bitte um Hilfe darf nicht ins Leere laufen, nur weil die
Zuordnung fehlt. */
const betreuer = db().prepare(
"SELECT betreuer_id FROM betreuung WHERE creator_id = ? LIMIT 1").get(creatorId);
const ziel = betreuer?.betreuer_id
|| db().prepare(
"SELECT id FROM personen WHERE rolle = 'admin' AND aktiv = 1 ORDER BY id LIMIT 1")
.get()?.id
|| null;
const name = db().prepare("SELECT name FROM personen WHERE id = ?").get(creatorId)?.name
|| "Creator";
/* Frist: in drei Tagen. Eine Bitte um Hilfe ohne Frist ist eine
Bitte, die liegen bleibt. */
const frist = new Date(Date.now() + 3 * 86400_000).toISOString().slice(0, 10);
db().prepare(`INSERT INTO aufgaben
(titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
frist, erstellt, erstellt_von)
VALUES (?,?,'offen','hoch',?,?,?,?,?)`)
.run(`Hilfe gewünscht: ${thema}`,
`${name} hat um Unterstützung gebeten.\n\n${notiz || "(keine weitere Angabe)"}`,
creatorId, ziel, frist, jetzt(), req.person.id);
protokolliere("hilfe_angefragt", req.person, echteIp(req), thema);
res.json({ ok: true, an: ziel });
} catch (fehler) {
console.error("[workspace] Hilfe anfragen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
+116
View File
@@ -313,3 +313,119 @@ body.start {
.spalte__mehr:hover { color: var(--akzent); border-color: rgba(63, 189, 245, .35); }
.spalte__mehr:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .spalte__mehr { transition: none; } }
/* =====================================================================
VORLAGEN — fertige Vorschlaege zum Uebernehmen
Sie sehen bewusst ANDERS aus als eigene Eintraege: heller Rand,
gestrichelte Kante, keine Statusfarbe. Man soll auf den ersten Blick
sehen, dass hier noch nichts "gehoert" -- es sind Angebote, keine
Arbeit. Sobald einer uebernommen ist, wird die Karte ruhig und traegt
einen Haken.
===================================================================== */
.vorlagen { margin: 22px 0 28px; }
.vorlagen__kopf {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 16px; flex-wrap: wrap;
margin-bottom: 14px;
}
.vorlagen__unter {
margin: 4px 0 0;
font-size: .86rem; color: var(--text-leise);
}
.vorlagen__raster {
display: grid; gap: 11px;
grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
}
.vorlagen__raster[hidden] { display: none; }
.vorlage {
position: relative;
padding: 15px 16px 13px;
border: 1px dashed color-mix(in srgb, var(--ton, var(--akzent)) 30%, var(--rand));
border-radius: var(--radius-klein);
background:
linear-gradient(180deg, color-mix(in srgb, var(--ton, var(--akzent)) 6%, transparent), transparent 55%),
var(--flaeche);
transition: border-color .18s ease;
}
.vorlage:hover { border-color: color-mix(in srgb, var(--ton, var(--akzent)) 50%, var(--rand)); }
/* Uebernommen: durchgezogene Kante statt gestrichelter -- aus einem
Angebot ist etwas Wirkliches geworden. */
.vorlage[data-genommen="ja"] {
border-style: solid;
border-color: rgba(80, 167, 130, .4);
opacity: .72;
}
.vorlage[data-genommen="ja"]::after {
content: "✓";
position: absolute; top: 12px; right: 14px;
color: #6fbd95; font-weight: 700;
}
.vorlage__titel {
margin: 0 0 8px;
font-size: .96rem; font-weight: 650; line-height: 1.3; color: var(--text);
}
/* Der Aufhaenger ist der wichtigste Teil einer Content-Idee -- die
ersten ein bis drei Sekunden entscheiden ueber die Verbreitung.
Deshalb steht er hervorgehoben und nicht im Fliesstext. */
.vorlage__hook {
margin: 0 0 9px; padding: 9px 11px;
border-left: 2px solid var(--ton, var(--akzent));
border-radius: 0 6px 6px 0;
background: color-mix(in srgb, var(--ton, var(--akzent)) 9%, transparent);
font-size: .88rem; line-height: 1.5; color: var(--text);
}
.vorlage__hookschild {
display: block; margin-bottom: 3px;
font-size: .64rem; letter-spacing: .12em; text-transform: uppercase;
color: var(--ton, var(--akzent));
}
.vorlage__text {
margin: 0 0 12px;
font-size: .85rem; line-height: 1.55; color: var(--text-leise);
}
.vorlage__fuss { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.vorlage__marke {
padding: 2px 9px;
border: 1px solid var(--rand); border-radius: 999px;
font-size: .7rem; color: var(--text-still);
}
.vorlage__marke--saeule {
border-color: color-mix(in srgb, var(--ton, var(--akzent)) 32%, transparent);
color: var(--ton, var(--akzent));
}
.vorlage__marke--hilfe { border-color: rgba(216, 161, 58, .34); color: #d8a13a; }
.vorlage__fuss .schritt { margin-left: auto; }
.vorlagen__alle {
grid-column: 1 / -1;
display: flex; justify-content: center;
padding-top: 4px;
}
/* Der Hilfe-Knopf ist bewusst auffaelliger als die uebrigen: Wer ihn
braucht, soll ihn finden, ohne zu suchen. */
.schritt--hilfe {
border-color: rgba(216, 161, 58, .4);
color: #e0b45c;
}
.schritt--hilfe[data-fertig="ja"] {
border-color: rgba(80, 167, 130, .45);
color: #6fbd95;
}
@media (max-width: 560px) {
.vorlagen__kopf { flex-direction: column; align-items: stretch; }
.vorlagen__kopf .knopf { width: 100%; }
}
/* Auf dem Handy sind die kleinen Marken im Vorlagenblock zu klein --
gemeldet von der Geraetepruefung (10,24 px beim Aufhaenger-Schild,
11,2 px bei Format und Saeule). body.start davor, weil content.css
NACH start.css geladen wird und dort dieselbe Regel steht. */
@media (max-width: 760px) {
body.start .vorlage__hookschild { font-size: .72rem; }
body.start .vorlage__marke { font-size: .75rem; }
}
+28
View File
@@ -156,6 +156,34 @@
}
for (const e of sichtbar) ziel.append(karte(e));
} catch { /* umgeleitet */ }
await vorlagenZeigen();
}
/* Fertige Punkte zum Uebernehmen -- derzeit nur fuer die LIVE-Analyse.
Sie stehen ueber der Liste: Wer noch nichts hat, soll nicht erst an
einem leeren Kasten vorbeiscrollen.
Welcher Abschnitt gezeigt wird, richtet sich nach dem gewaehlten
Filter. Ohne Filter die Vorbereitung -- das ist der Teil, ohne den
die anderen beiden gar nicht erst sinnvoll sind. */
async function vorlagenZeigen() {
const behaelter = $('vorlagen');
if (!behaelter || bereich !== 'live' || !window.vorlagenBlock) return;
const abschnitt = ['vorbereitung', 'mitschrift', 'auswertung'].includes(filter)
? filter : 'vorbereitung';
await window.vorlagenBlock(behaelter, {
art: 'live',
unterart: abschnitt,
creatorId: creatorWahl(),
danach: laden,
});
}
/* Fuer wen wird uebernommen? Ein Creator immer fuer sich (der Server
setzt das ohnehin), ein Betreuer fuer den gewaehlten Creator. */
function creatorWahl() {
const w = document.getElementById('f-creator');
return w && w.value ? Number(w.value) : null;
}
/* ---------- Neuer Eintrag ---------------------------------------------- */
+1 -1
View File
@@ -125,7 +125,7 @@
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' },
{ name: 'LIVE-Analyse', unter: 'Vorher, während, danach', zeichen: 'live', ton: 7,
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=live', szene: 'portal' },
{ name: 'Content-Planung', unter: 'Idee bis Upload', zeichen: 'content', ton: 13,
{ name: 'Content-Ideen', unter: 'Idee bis Upload', zeichen: 'content', ton: 13,
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'content.html', szene: 'portal' },
{ name: 'Community', unter: 'Moderation und Konflikte', zeichen: 'community', ton: 6,
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=community', szene: 'lounge' },
+13 -1
View File
@@ -1,5 +1,5 @@
/* ===================================================================
Content-Planung.
Content-Ideen.
Bekommt als einziger der sechs Bereiche eine eigene Seite. Grund: Die
anderen fuenf sind Sammlungen -- Eintraege zu einem Thema. Content ist
@@ -441,6 +441,18 @@
saeulen = sA.ok ? (await sA.json()).saeulen || [] : [];
} catch { return; }
/* Fertige Ideen zum Uebernehmen. Steht ueber der Strecke, damit
jemand mit leeren Spalten nicht erst daran vorbeiscrollen muss. */
const behaelter = document.getElementById('vorlagen');
if (behaelter && window.vorlagenBlock) {
const wahl = document.getElementById('creator-wahl');
window.vorlagenBlock(behaelter, {
art: 'content',
creatorId: wahl && wahl.value ? Number(wahl.value) : null,
danach: laden,
});
}
filterBauen();
streckeZeigen();
saeulenZeigen();
+214
View File
@@ -0,0 +1,214 @@
/* ===================================================================
Vorlagen — fertige Inhalte, die man übernehmen kann.
EIN Bauteil für mehrere Seiten (Content-Ideen, LIVE-Analyse). Es
hängt sich an einen Behälter mit `data-vorlagen="<art>"` und baut
dort eine aufklappbare Liste von Vorschlägen.
Warum ausgeklappt-per-Knopf und nicht offen: Wer schon zwanzig eigene
Einträge hat, braucht die Vorschläge nicht mehr im Weg. Wer noch
keinen hat, sieht den Knopf sofort -- und er ist dann das Auffälligste
auf der Seite, weil sonst nichts da ist.
Übernehmen macht aus dem Vorschlag einen ganz normalen Eintrag. Ab
dann ist er unabhängig von der Vorlage: änderbar, löschbar, und
spätere Änderungen an der Vorlage berühren ihn nicht.
=================================================================== */
(() => {
'use strict';
const el = (tag, klasse, text) => {
const k = document.createElement(tag);
if (klasse) k.className = klasse;
if (text !== undefined) k.textContent = text;
return k;
};
async function hole(pfad, optionen = {}) {
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
return a;
}
let vorlagen = null;
/* Was in dieser Sitzung schon uebernommen wurde. Muss AUSSERHALB des
Bauvorgangs stehen: Nach dem Uebernehmen wird die Liste neu geladen
und der Block neu gebaut -- eine Markierung am Element allein waere
dabei jedes Mal verloren. Genau das hat die Pruefung gemeldet
("als uebernommen markiert: 0").
Bewusst nur fuer diese Sitzung und nicht dauerhaft gespeichert: Wer
die Seite neu laedt, hat seine Eintraege ja in der Liste stehen --
dort gehoeren sie hin, nicht in eine zweite Buchfuehrung. */
const genommen = new Set();
/** Baut den Vorlagenblock in einen Behälter.
* art: 'content' | 'live'
* unterart: bei live der Abschnitt (vorbereitung | mitschrift | auswertung)
* creatorId: für wen übernommen wird (Betreuer); bei Creator egal
* danach: wird nach dem Übernehmen aufgerufen (Liste neu laden) */
window.vorlagenBlock = async function (behaelter, { art, unterart, creatorId, danach }) {
if (!behaelter) return;
if (!vorlagen) {
try {
const a = await hole('/workspace/api/vorlagen');
if (!a.ok) return;
vorlagen = await a.json();
} catch { return; }
}
const liste = art === 'content' ? vorlagen.content
: (vorlagen.live?.[unterart] || []);
if (!liste.length) return;
behaelter.textContent = '';
const block = el('section', 'vorlagen');
/* ---- Kopf mit Aufklappknopf ---- */
const kopf = el('div', 'vorlagen__kopf');
const titel = el('div', null);
titel.append(el('p', 'feldschild', 'Fertige Vorschläge'));
titel.append(el('p', 'vorlagen__unter',
art === 'content'
? `${liste.length} Ideen mit Aufhänger und Format – zum Übernehmen und Anpassen.`
: `${liste.length} Punkte, die sich bewährt haben. Übernehmen und abhaken.`));
kopf.append(titel);
const auf = el('button', 'knopf knopf--klein');
auf.type = 'button';
auf.setAttribute('aria-expanded', 'false');
auf.textContent = 'Vorschläge zeigen';
kopf.append(auf);
block.append(kopf);
/* ---- Die Liste ---- */
const raster = el('div', 'vorlagen__raster');
raster.hidden = true;
for (const [nr, v] of liste.entries()) {
const k = el('article', 'vorlage');
const schluessel = art + ':' + (unterart || '') + ':' + nr;
if (genommen.has(schluessel)) k.dataset.genommen = 'ja';
if (v.saeule) k.dataset.saeule = v.saeule;
k.append(el('h3', 'vorlage__titel', v.titel));
/* Der Aufhänger ist der wichtigste Teil einer Content-Idee -- die
ersten Sekunden entscheiden über die Verbreitung. Deshalb steht
er hervorgehoben und nicht im Fließtext. */
if (v.hook) {
const h = el('p', 'vorlage__hook');
h.append(el('span', 'vorlage__hookschild', 'Aufhänger'));
h.append(document.createTextNode(v.hook));
k.append(h);
}
if (v.text) k.append(el('p', 'vorlage__text', v.text));
const fuss = el('div', 'vorlage__fuss');
if (v.format) fuss.append(el('span', 'vorlage__marke', v.format));
if (v.saeule) fuss.append(el('span', 'vorlage__marke vorlage__marke--saeule', v.saeule));
/* Punkte, bei denen man Hilfe anfordern kann, sagen das auch. */
if (v.hilfe) fuss.append(el('span', 'vorlage__marke vorlage__marke--hilfe', 'Hilfe möglich'));
const nehmen = el('button', 'schritt', 'Übernehmen');
nehmen.type = 'button';
nehmen.addEventListener('click', async () => {
nehmen.disabled = true;
nehmen.textContent = 'wird übernommen …';
try {
const a = await hole('/workspace/api/vorlagen/uebernehmen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ art, unterart, nr, creator_id: creatorId }),
});
if (!a.ok) {
const f = await a.json().catch(() => ({}));
nehmen.textContent = f.fehler || 'ging nicht';
nehmen.disabled = false;
return;
}
/* Übernommen bleibt sichtbar markiert -- man soll sehen, was
man schon geholt hat, ohne die Liste zu verlassen. */
genommen.add(schluessel);
k.dataset.genommen = 'ja';
nehmen.textContent = 'übernommen';
danach?.();
} catch { nehmen.disabled = false; nehmen.textContent = 'Übernehmen'; }
});
fuss.append(nehmen);
k.append(fuss);
raster.append(k);
}
/* ---- Alle auf einmal ---- */
const alle = el('div', 'vorlagen__alle');
const allesNehmen = el('button', 'knopf-still',
art === 'content' ? `Alle ${liste.length} Ideen übernehmen`
: `Alle ${liste.length} Punkte übernehmen`);
allesNehmen.type = 'button';
allesNehmen.addEventListener('click', async () => {
if (!confirm(`Wirklich alle ${liste.length} übernehmen? `
+ 'Sie landen als eigene Einträge in der Liste und lassen sich einzeln löschen.')) return;
allesNehmen.disabled = true;
allesNehmen.textContent = 'wird übernommen …';
try {
const a = await hole('/workspace/api/vorlagen/uebernehmen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ art: art + '_alle', unterart, creator_id: creatorId }),
});
if (!a.ok) { allesNehmen.textContent = 'ging nicht'; allesNehmen.disabled = false; return; }
const { angelegt } = await a.json();
/* Alle auf einmal: dann sind auch alle markiert. */
for (let i = 0; i < liste.length; i++) genommen.add(art + ':' + (unterart || '') + ':' + i);
allesNehmen.textContent = `${angelegt} übernommen`;
danach?.();
} catch { allesNehmen.disabled = false; }
});
alle.append(allesNehmen);
raster.append(alle);
block.append(raster);
auf.addEventListener('click', () => {
const offen = auf.getAttribute('aria-expanded') === 'true';
auf.setAttribute('aria-expanded', offen ? 'false' : 'true');
raster.hidden = offen;
auf.textContent = offen ? 'Vorschläge zeigen' : 'Vorschläge ausblenden';
});
behaelter.append(block);
behaelter.hidden = false;
};
/* ---------- "Ich brauche Hilfe" ---------------------------------------
Ein Knopf, der aus einer Bitte eine AUFGABE macht -- keine
Nachricht. Eine Nachricht ist gelesen und dann weg; eine Aufgabe
bleibt stehen, bis sie jemand erledigt, und taucht beim Betreuer auf
der Startseite auf, wenn sie überfällig wird. */
window.hilfeKnopf = function (thema, creatorId) {
const b = el('button', 'schritt schritt--hilfe', 'Hilfe anfordern');
b.type = 'button';
b.addEventListener('click', async () => {
const notiz = prompt(`Hilfe zu „${thema}“.\n\n`
+ 'Was genau brauchst du? (Ein Satz reicht – kann auch leer bleiben.)');
if (notiz === null) return; /* abgebrochen */
b.disabled = true;
b.textContent = 'wird gemeldet …';
try {
const a = await hole('/workspace/api/vorlagen/hilfe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ thema, notiz, creator_id: creatorId }),
});
if (!a.ok) { b.textContent = 'ging nicht'; b.disabled = false; return; }
b.textContent = 'Hilfe angefordert';
b.dataset.fertig = 'ja';
} catch { b.disabled = false; b.textContent = 'Hilfe anfordern'; }
});
return b;
};
})();
+7 -7
View File
@@ -7,9 +7,9 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
</head>
<body class="start">
@@ -139,9 +139,9 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/aufgaben.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/aufgaben.js?v=202609011625" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609011625" />
</head>
<body class="start">
@@ -132,9 +132,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/automation.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/automation.js?v=202609011625" defer></script>
</body>
</html>
+13 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609011625" />
</head>
<body class="start">
@@ -103,13 +103,18 @@
<div class="filter" id="filter"></div>
<!-- Fertige Punkte zum Uebernehmen -- nur in Bereichen, fuer die es
welche gibt (derzeit LIVE-Analyse). -->
<div id="vorlagen" hidden></div>
<div id="liste" aria-busy="true"><p class="leise">Einträge werden geladen …</p></div>
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/bereich.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/vorlagen.js?v=202609011625" defer></script>
<script src="assets/js/bereich.js?v=202609011625" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609011625" />
</head>
<body class="start">
@@ -54,10 +54,10 @@
</main>
<script src="assets/js/ki.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/calls.js?v=202609011609" defer></script>
<script src="assets/js/ki.js?v=202609011625" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/calls.js?v=202609011625" defer></script>
</body>
</html>
+15 -10
View File
@@ -3,14 +3,14 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Content-Planung · Creator Workspace</title>
<title>Content-Ideen · Creator Workspace</title>
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/content.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/content.css?v=202609011625" />
</head>
<body class="start">
@@ -39,7 +39,7 @@
<section class="kopf-zeile">
<div>
<p class="marke">Betreuung</p>
<h1 class="titel">Content-Planung</h1>
<h1 class="titel">Content-Ideen</h1>
<p class="unterzeile" id="unterzeile">
Von der Idee bis zum Upload – eine Strecke, kein Terminkalender.
</p>
@@ -152,15 +152,20 @@
<div class="filterzeile" id="filterzeile"></div>
<!-- Die Strecke. Fünf Spalten, auf dem Handy untereinander. -->
<!-- Fertige Ideen zum Uebernehmen. Steht VOR der Strecke: Wer noch
nichts hat, soll nicht erst an leeren Spalten vorbeiscrollen. -->
<div id="vorlagen" hidden></div>
<div class="strecke" id="strecke" aria-busy="true">
<p class="leise">Wird geladen …</p>
</div>
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/content.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/vorlagen.js?v=202609011625" defer></script>
<script src="assets/js/content.js?v=202609011625" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609011625" />
</head>
<body class="start">
@@ -93,9 +93,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/dateien.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/dateien.js?v=202609011625" defer></script>
</body>
</html>
+2 -2
View File
@@ -25,7 +25,7 @@
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
</head>
<body class="gate">
@@ -143,6 +143,6 @@
</p>
</main>
<script src="assets/js/gate.js?v=202609011609" defer></script>
<script src="assets/js/gate.js?v=202609011625" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609011625" />
</head>
<body class="start">
@@ -147,9 +147,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/kalender.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/kalender.js?v=202609011625" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609011625" />
</head>
<body class="start">
@@ -121,9 +121,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/personen.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/personen.js?v=202609011625" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609011625" />
</head>
<body class="start">
@@ -175,10 +175,10 @@
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/steckbrief.js?v=202609011609" defer></script>
<script src="assets/js/profil.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/steckbrief.js?v=202609011625" defer></script>
<script src="assets/js/profil.js?v=202609011625" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/report.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/report.css?v=202609011625" />
</head>
<body class="start">
@@ -91,10 +91,10 @@
</main>
<script src="assets/js/ki.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/report.js?v=202609011609" defer></script>
<script src="assets/js/ki.js?v=202609011625" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/report.js?v=202609011625" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609011625" />
</head>
<body class="start">
@@ -124,9 +124,9 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/scouting.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/scouting.js?v=202609011625" defer></script>
</body>
</html>
+6 -6
View File
@@ -7,8 +7,8 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
</head>
<body class="start">
@@ -99,9 +99,9 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/start.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/start.js?v=202609011625" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609011625" />
</head>
<body class="start">
@@ -70,10 +70,10 @@
</main>
<script src="assets/js/ki.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/startcheck.js?v=202609011609" defer></script>
<script src="assets/js/ki.js?v=202609011625" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/startcheck.js?v=202609011625" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609011625" />
</head>
<body class="start">
@@ -85,9 +85,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/steckbrief.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/steckbrief.js?v=202609011625" defer></script>
</body>
</html>
+6 -6
View File
@@ -7,9 +7,9 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609011625" />
</head>
<body class="start">
@@ -57,9 +57,9 @@
</main>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/uebersicht.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/uebersicht.js?v=202609011625" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609011609" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/start.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609011625" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609011625" />
</head>
<body class="start">
@@ -175,9 +175,9 @@
</main>
<script src="assets/js/wahl.js?v=202609011609" defer></script>
<script src="assets/js/bereiche.js?v=202609011609" defer></script>
<script src="assets/js/kopf.js?v=202609011609" defer></script>
<script src="assets/js/wissen.js?v=202609011609" defer></script>
<script src="assets/js/wahl.js?v=202609011625" defer></script>
<script src="assets/js/bereiche.js?v=202609011625" defer></script>
<script src="assets/js/kopf.js?v=202609011625" defer></script>
<script src="assets/js/wissen.js?v=202609011625" defer></script>
</body>
</html>