diff --git a/server/pruef-treff-start.mjs b/server/pruef-treff-start.mjs new file mode 100644 index 00000000..240946f2 --- /dev/null +++ b/server/pruef-treff-start.mjs @@ -0,0 +1,338 @@ +/* ===================================================================== + FERTIGE INHALTE STATT LEERER BRETTER (17.09.2026) + + Filipe, mit drei Bildschirmfotos nebeneinander -- Der Treff, das + Anschlagbrett, Was ansteht, alle drei mit demselben grauen Kasten + "Noch kein Eintrag in diesem Bereich": + + "ich meine alle kategorien sehen einfach so scheisse und leer aus, + ich will fertige sachen schon drin wo auch die community und alle + anderen benutzen koennen" + + Er hatte recht, und es stand sogar im Plan (§5.1): "Acht leere Seiten + sind schlimmer als keine." Ein Bereich, der nur aus Filterknoepfen + und einem grauen Kasten besteht, sieht nicht neu aus, sondern + aufgegeben -- und dreimal derselbe Satz sieht aus wie ein Defekt. + + --------------------------------------------------------------------- + WAS HIER GEPRUEFT WIRD + + 1. Der Katalog selbst: Passt jede Art zu ihrem Bereich? Ist jeder + Text mehr als eine Ueberschrift? + 2. Das Uebernehmen: einzeln, alle auf einmal -- und zweimal + uebernehmen darf nichts verdoppeln. + 3. UND DER FALL, DEN ICH MIR SELBST GEBAUT HABE: Die Bremse aus + Stufe 4 zaehlt Beitraege pro Stunde. Wer fuenfzehn Regeln + uebernimmt, haette nach der zehnten eine halb gefuellte Seite + und die Meldung, er habe zu viel geschrieben. + 4. Die Verborgenheit: Wer hier nicht schreiben darf, bekommt den + Katalog gar nicht. + + Aufrufen mit: node server/pruef-treff-start.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { portMussFreiSein } from "./helfer-port.mjs"; +import { ersatzschreibungen } from "./helfer-deutsch.mjs"; + +const PORT = await portMussFreiSein(4489, "pruef-treff-start"); +const ordner = mkdtempSync(join(tmpdir(), "ws-start-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +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-treff-start"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = `http://127.0.0.1:${PORT}`; + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { TREFF_START } = await import("./workspace-treff-start.js"); +const { BEREICHE, BEITRAEGE_JE_STUNDE } = await import("./workspace-bereiche.js"); + +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); +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, + createHmac("sha256", schluessel).update(code).digest("hex"), jetzt); +} +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Tili", "creator", "CODE-CREA-0001"); +d.close(); + +async function anmelden(kachel, code) { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: kachel, code }), + }); + return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); +} +const dogi = await anmelden("admin", "CODE-DOGI-0001"); +const crea = await anmelden("creator", "CODE-CREA-0001"); +const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS }); + +/* ======================================================================= + 1. DER KATALOG SELBST + ======================================================================= */ +melde(""); +melde("=== 1. Was bereitliegt ==="); +{ + const gesamt = Object.values(TREFF_START).reduce((n, l) => n + l.length, 0); + ok(gesamt >= 40, `${gesamt} fertige Eintraege in ${Object.keys(TREFF_START).length} Bereichen`); + + /* JEDE ART MUSS ES IM BEREICH GEBEN. Sonst landet beim Uebernehmen + eine Art in der Datenbank, die das Formular nicht kennt -- und die + Zeile laesst sich hinterher nicht mehr aendern. */ + const falsch = []; + for (const [bereich, liste] of Object.entries(TREFF_START)) { + const arten = BEREICHE[bereich]?.arten || {}; + for (const v of liste) if (!Object.hasOwn(arten, v.art)) falsch.push(`${bereich}/${v.art}`); + } + ok(falsch.length === 0, + falsch.length ? `unbekannte Arten: ${falsch.join(", ")}` : "jede Art gibt es in ihrem Bereich"); + + /* EIN TEXT, DER NUR DEN TITEL WIEDERHOLT, ist keiner. Dieselbe Regel + wie beim Aufgabenkatalog der Modis. */ + const duenn = []; + for (const [bereich, liste] of Object.entries(TREFF_START)) { + for (const v of liste) { + if (!v.text || v.text.length < v.titel.length + 25) duenn.push(`${bereich}/${v.titel}`); + } + } + ok(duenn.length === 0, + duenn.length ? `zu duenn: ${duenn[0]}` : "jeder Text sagt mehr als sein Titel"); + + /* KEINE DOPPELTEN TITEL innerhalb eines Bereichs -- das Uebernehmen + erkennt "schon da" am Titel. */ + let doppelt = 0; + for (const liste of Object.values(TREFF_START)) { + doppelt += liste.length - new Set(liste.map((v) => v.titel)).size; + } + ok(doppelt === 0, `kein Titel doppelt (${doppelt})`); + + /* UND SIE TRAGEN UMLAUTE. Dieselbe Falle wie bei den Kategorien und + den Bereichsnamen -- beide Male auf einem Bildschirmfoto gefunden. */ + const ascii = []; + for (const [bereich, liste] of Object.entries(TREFF_START)) { + for (const v of liste) { + const t = ersatzschreibungen(`${v.titel} ${v.text}`); + if (t.length) ascii.push(`${bereich}/${v.titel} [${t.join(",")}]`); + } + } + ok(ascii.length === 0, + ascii.length ? `Ersatzschreibungen: ${ascii[0]}` : "alle Texte tragen echte Umlaute"); + + /* DIE BEIDEN NACHSCHLAGEWERKE muessen ordentlich gefuellt sein. Ein + leeres Regelwerk ist schlimmer als gar keins: Im Streitfall hat + dann niemand etwas in der Hand. */ + ok((TREFF_START.regeln || []).length >= 12, + `Regeln & Hilfe hat ${(TREFF_START.regeln || []).length} Eintraege`); + ok((TREFF_START.mitmachen || []).length >= 8, + `Mitmachen hat ${(TREFF_START.mitmachen || []).length}`); + /* UND DIE HILFE NENNT EINE ECHTE NUMMER. Ein Community-Bereich fuer + junge Leute ohne Notrufnummer ist eine vertane Gelegenheit. */ + ok((TREFF_START.regeln || []).some((v) => /0800 111 0 111|116 111/.test(v.text || "")), + "und die Hilfe nennt die Telefonseelsorge"); +} + +/* ======================================================================= + 2. UEBERNEHMEN + ======================================================================= */ +melde(""); +melde("=== 2. Uebernehmen ==="); +{ + const a = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, { headers: { cookie: dogi } }); + const j = await a.json().catch(() => null); + ok(a.status === 200 && (j?.vorschlaege || []).length > 0, + `der Katalog kommt (${j?.offen} offen)`); + + const e = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, { + method: "POST", headers: kopf(dogi), body: JSON.stringify({ nr: 0 }), + }); + const ej = await e.json().catch(() => null); + ok(e.status === 201 && ej?.angelegt === 1, `einer einzeln (${ej?.angelegt})`); + + /* ZWEIMAL DARF NICHTS VERDOPPELN. Wer den Knopf zweimal drueckt -- + oder zwei Leute gleichzeitig --, soll dieselbe Seite bekommen. */ + const z = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, { + method: "POST", headers: kopf(dogi), body: JSON.stringify({ nr: 0 }), + }); + const zj = await z.json().catch(() => null); + ok(zj?.angelegt === 0 && zj?.schon === 1, `zweimal legt nichts nach (${zj?.angelegt})`); +} + +/* ======================================================================= + 3. DIE BREMSE DARF DAS NICHT VERHINDERN + ======================================================================= */ +melde(""); +melde("=== 3. Fuenfzehn Regeln auf einmal ==="); +{ + const wie_viele = (TREFF_START.regeln || []).length; + ok(wie_viele > BEITRAEGE_JE_STUNDE, + `es sind ${wie_viele} Regeln -- mehr als die Bremse je Stunde zulaesst (${BEITRAEGE_JE_STUNDE})`); + + const a = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, { + method: "POST", headers: kopf(dogi), body: "{}", + }); + const j = await a.json().catch(() => null); + /* DAS IST DER PUNKT. Ginge der Weg durch die Bremse, stuenden hier + zehn Regeln und eine Fehlermeldung -- eine halb gefuellte Seite + mit dem Vorwurf, man habe zu viel geschrieben. Genau die Sorte + Sicherung, die das richtige Verhalten bestraft. */ + ok(a.status === 201 && j?.angelegt === wie_viele, + `alle ${wie_viele} auf einmal (${j?.angelegt})`); + + const l = await fetch(`${BASIS}/workspace/api/bereich/regeln`, { headers: { cookie: dogi } }); + const lj = await l.json().catch(() => null); + ok((lj?.eintraege || []).length === wie_viele, + `und sie stehen wirklich da (${(lj?.eintraege || []).length})`); + + /* GEGENPROBE: Die Bremse ist deshalb nicht abgeschaltet. Ein + gewoehnlicher Beitrag danach muss sie spueren. */ + let letzte = 0; + for (let i = 0; i < BEITRAEGE_JE_STUNDE + 2; i++) { + const r = await fetch(`${BASIS}/workspace/api/bereich/treff`, { + method: "POST", headers: kopf(dogi), + body: JSON.stringify({ art: "frage", titel: `Frage Nummer ${i}`, + datum: jetzt.slice(0, 10) }), + }); + letzte = r.status; + if (r.status === 429) break; + } + ok(letzte === 429, `die Bremse greift trotzdem noch (${letzte})`); +} + +/* ======================================================================= + 4. WER DEN KATALOG NICHT BEKOMMT + ======================================================================= */ +melde(""); +melde("=== 4. Die Verborgenheit ==="); +{ + const a = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, { headers: { cookie: crea } }); + ok(a.status === 404, `ein Creator bekommt ihn nicht (${a.status})`); + const b = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, { + method: "POST", headers: kopf(crea), body: "{}", + }); + ok(b.status === 403 || b.status === 404, + `und kann auch nichts uebernehmen (${b.status})`); +} + +/* ======================================================================= + 5. JEDER BEREICH SAGT ETWAS ANDERES, WENN ER LEER IST + ======================================================================= */ +melde(""); +melde("=== 5. Die leeren Bretter ==="); +{ + /* DREI BILDSCHIRMFOTOS MIT DEMSELBEN SATZ sahen aus wie ein Defekt. + Jetzt sagt jeder Bereich in seinen eigenen Worten, was hier + hingehoert. */ + const saetze = Object.entries(BEREICHE) + .filter(([k]) => TREFF_START[k]) + .map(([k, e]) => [k, e.leer || ""]); + ok(saetze.every(([, t]) => t.length > 25), + `alle ${saetze.length} Community-Bereiche haben einen eigenen Leertext`); + ok(new Set(saetze.map(([, t]) => t)).size === saetze.length, + "und keiner davon ist derselbe"); + ok(saetze.every(([, t]) => !/Noch kein Eintrag in diesem Bereich/.test(t)), + "der alte Einheitssatz kommt nicht mehr vor"); +} + +/* ======================================================================= + 6. IM BROWSER: VORHER UND NACHHER + ======================================================================= */ +melde(""); +melde("=== 6. Wie es aussieht ==="); +{ + const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await chromium.launch(); + try { + const kontext = await browser.newContext({ viewport: { width: 1100, height: 950 } }); + await kontext.addCookies(dogi.split("; ").map((k) => { + const [name, ...rest] = k.split("="); + return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); }); + + /* "mitmachen" ist noch unberuehrt -- dort ist der Katalog voll. */ + await seite.goto(`${BASIS}/workspace/bereich.html?b=mitmachen`, { waitUntil: "networkidle" }); + await seite.waitForSelector("#startkatalog:not([hidden])", { timeout: 15_000 }); + await seite.waitForTimeout(400); + + const vorher = await seite.evaluate(() => ({ + karten: document.querySelectorAll(".startkatalog__karte").length, + alleKnopf: document.querySelector(".startkatalog__alle")?.textContent?.trim() || "", + leer: document.querySelector(".leer-hinweis")?.textContent?.trim() || "", + })); + ok(vorher.karten >= 8, `${vorher.karten} Vorschlaege stehen bereit`); + ok(/Alle \d+ übernehmen/.test(vorher.alleKnopf), `mit einem Knopf ("${vorher.alleKnopf}")`); + /* DER LEERTEXT IST DER DES BEREICHS, nicht der Einheitssatz. */ + ok(vorher.leer.includes("dazugehören"), + `und der leere Bereich sagt seinen eigenen Satz ("${vorher.leer}")`); + + if (process.env.SCHIRM) { + const { fileURLToPath } = await import("node:url"); + await seite.screenshot({ path: fileURLToPath(new URL("pruef-treff-start-vorher.png", + import.meta.url)), fullPage: false }); + } + + await seite.click(".startkatalog__alle"); + await seite.waitForTimeout(1500); + const nachher = await seite.evaluate(() => ({ + katalogWeg: !!document.getElementById("startkatalog")?.hidden, + eintraege: document.querySelectorAll(".eintrag-karte").length, + leerWeg: !document.querySelector(".leer-hinweis"), + })); + ok(nachher.eintraege >= 8, `nach einem Klick stehen ${nachher.eintraege} Eintraege da`); + ok(nachher.leerWeg, "der leere Kasten ist weg"); + /* UND DER KATALOG VERSCHWINDET, wenn nichts mehr zu holen ist -- + ein Angebot, das leer weiterhin dasteht, ist eine Sackgasse. */ + ok(nachher.katalogWeg, "und der Katalog verschwindet, weil nichts mehr offen ist"); + ok(konsole.length === 0, + konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); + + if (process.env.SCHIRM) { + const { fileURLToPath } = await import("node:url"); + await seite.screenshot({ path: fileURLToPath(new URL("pruef-treff-start-nachher.png", + import.meta.url)), fullPage: false }); + melde(" -- Bilder: server/pruef-treff-start-vorher.png und -nachher.png"); + } + await kontext.close(); + } finally { + await browser.close(); + } +} + +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index e97217ac..3af6de42 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -15,6 +15,7 @@ ===================================================================== */ import express from "express"; +import { TREFF_START } from "./workspace-treff-start.js"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, externPruefen, externSql, @@ -55,6 +56,9 @@ export const BEREICHE = { anschlag: { name: "Anschlagbrett", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer. */ + leer: "Gerade gilt nichts Besonderes. Das ist eine gute Nachricht.", ober: "Vom Team", arten: { ansage: "Ansage", @@ -70,6 +74,9 @@ export const BEREICHE = { ansteht: { name: "Was ansteht", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer. */ + leer: "Noch kein Termin eingetragen. Der nächste Stream steht hier, sobald er feststeht.", ober: "Termine", /* AUSDRUECKLICH NICHT AN DEN ECHTEN KALENDER GEKOPPELT. @@ -93,6 +100,9 @@ export const BEREICHE = { treff: { name: "Der Treff", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer. */ + leer: "Noch hat niemand Hallo gesagt. Sei der Erste — ein Satz reicht.", ober: "Euer Raum", /* DIE REIHENFOLGE IST ABSICHT. Das erste Feld bestimmt, was geschrieben wird -- wer ein Formular oeffnet, dessen erste @@ -117,6 +127,9 @@ export const BEREICHE = { wunsch: { name: "Wunschliste", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer. */ + leer: "Noch kein Wunsch. Was würdest du im nächsten Stream gern sehen?", ober: "Was ihr euch wünscht", /* DER WEG EINES WUNSCHES (17.09.2026). @@ -165,6 +178,9 @@ export const BEREICHE = { highlight: { name: "Highlights", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer. */ + leer: "Hier kommen eure Clips und Bilder hin. Der erste darf ruhig wackelig sein.", ober: "Von euch", arten: { clip: "Clip", @@ -180,6 +196,9 @@ export const BEREICHE = { regeln: { name: "Regeln & Hilfe", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer. */ + leer: "Hier stehen die Regeln des Treffs — sobald sie eingetragen sind.", ober: "Wie es hier läuft", /* REGELN IN EINFACHER SPRACHE UND AN ECHTEN FAELLEN. Vage Regeln erzeugen Grenzfaelle, und die muss dann jeder Modi einzeln @@ -199,6 +218,9 @@ export const BEREICHE = { mitmachen: { name: "Mitmachen", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer. */ + leer: "Hier steht, wie man dazugehören kann — sobald es eingetragen ist.", ober: "Nachwuchs", /* DIE BRUECKE ZU DEN TALENTEN. Wer sich hier meldet, ist ein Kandidat -- und Kandidaten gehoeren in die Kategorie, die es @@ -1467,6 +1489,96 @@ function pruefe(bereich, körper, { neu }) { return { aus, fehler }; } +/* ===================================================================== + DER STARTKATALOG (17.09.2026) + + Filipe, mit drei Bildschirmfotos leerer Bretter: "ich will fertige + sachen schon drin wo auch die community und alle anderen benutzen + koennen". Es stand sogar im Plan (§5.1): "Acht leere Seiten sind + schlimmer als keine." + + NICHT AUTOMATISCH BEIM START. Ein Text, den niemand gelesen hat, + stuende sonst als Ansage des Teams auf einer oeffentlichen Seite. + "Bleibt fair" klingt harmlos, bis jemand fragt, was das im + Streitfall heisst -- und dann muss dahinterstehen, WER es gemeint + hat. Uebernehmen ist eine Entscheidung; automatisch fuellen waere + eine Behauptung. + + UND DIE BREMSE GILT HIER NICHT. Sie zaehlt Beitraege pro Stunde und + wuerde beim Uebernehmen von fuenfzehn Regeln nach dem zehnten + abbrechen -- mit einer halb gefuellten Seite und der Meldung, man + habe zu viel geschrieben. Das ist genau die Sorte Sicherung, die das + richtige Verhalten bestraft und deshalb abgeschaltet wird. Der Weg + hier ist eine EINZELNE Handlung des Teams, kein Schreibrausch. */ +bereicheRouter.get("/workspace/api/bereich/:bereich/start", (req, res) => { + try { + const bereich = String(req.params.bereich); + if (!BEREICHE[bereich]) return res.status(404).json({ fehler: "nicht_gefunden" }); + /* Wer hier nicht schreiben darf, bekommt den Katalog gar nicht -- + statt ihn zu bekommen und den Knopf ausgeblendet zu sehen. */ + if (darfSchreiben(req.person, bereich)) return res.status(404).json({ fehler: "nicht_gefunden" }); + const liste = TREFF_START[bereich] || []; + /* Was schon dasteht, kommt nicht noch einmal. Verglichen wird ueber + den Titel: Eine eigene Kennung an der Zeile waere eine zweite + Wahrheit, und beim Umformulieren liefe sie auseinander. */ + const da = new Set(db().prepare( + "SELECT titel FROM eintraege WHERE bereich = ?").all(bereich).map((z) => z.titel)); + res.json({ + vorschlaege: liste.map((v, nr) => ({ nr, ...v, schon: da.has(v.titel) })), + offen: liste.filter((v) => !da.has(v.titel)).length, + }); + } catch (fehler) { + console.error("[workspace] Startkatalog:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + +bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (req, res) => { + try { + const bereich = String(req.params.bereich); + const einstellung = BEREICHE[bereich]; + if (!einstellung) return res.status(404).json({ fehler: "nicht_gefunden" }); + const warum = darfSchreiben(req.person, bereich); + if (warum) return res.status(403).json({ fehler: warum }); + + const liste = TREFF_START[bereich] || []; + if (!liste.length) return res.status(404).json({ fehler: "nicht_gefunden" }); + + const nr = req.body?.nr; + const gewaehlt = nr === undefined || nr === null || nr === "" + ? liste + : [liste[Number(nr)]].filter(Boolean); + if (!gewaehlt.length) return res.status(400).json({ fehler: "Unbekannter Vorschlag." }); + + const da = new Set(db().prepare( + "SELECT titel FROM eintraege WHERE bereich = ?").all(bereich).map((z) => z.titel)); + const neu = gewaehlt.filter((v) => !da.has(v.titel)); + + let angelegt = 0; + for (const v of neu) { + /* DIE ART MUSS ZUM BEREICH PASSEN. Sie steht im Katalog, aber + geprueft wird sie trotzdem -- sonst traegt ein Tippfehler im + Katalog eine Art in die Datenbank, die es dort nicht gibt, und + die Zeile laesst sich hinterher nicht mehr aendern. */ + if (!Object.hasOwn(einstellung.arten, v.art)) continue; + db().prepare(` + INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von) + VALUES (?,?,?,?,?, 'offen', ?, ?)`) + .run(bereich, v.art, v.titel, v.text ?? null, + jetzt().slice(0, 10), jetzt(), req.person.id); + angelegt++; + } + protokolliere("bereich_start_uebernommen", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `${bereich} ${angelegt} von ${gewaehlt.length}`, + }); + res.status(201).json({ angelegt, schon: gewaehlt.length - angelegt }); + } catch (fehler) { + console.error("[workspace] Startkatalog uebernehmen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + /* ---------- Anlegen ------------------------------------------------------ */ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, res) => { diff --git a/server/workspace-treff-start.js b/server/workspace-treff-start.js new file mode 100644 index 00000000..a71c0406 --- /dev/null +++ b/server/workspace-treff-start.js @@ -0,0 +1,290 @@ +/* ===================================================================== + FERTIGE INHALTE FÜR DIE COMMUNITY-BEREICHE (17.09.2026) + + Filipe, mit drei Bildschirmfotos: „ich meine alle kategorien sehen + einfach so scheisse und leer aus, ich will fertige sachen schon drin + wo auch die community und alle anderen benutzen können". + + Er hat recht, und es stand sogar im Plan (§5.1): „Acht leere Seiten + sind schlimmer als keine." Ein Bereich, der nur aus Filterknöpfen und + dem Satz „Noch kein Eintrag" besteht, sieht nicht neu aus, sondern + aufgegeben. + + --------------------------------------------------------------------- + WARUM EIN KATALOG UND KEIN AUTOMATISCHES BEFÜLLEN + + Diese Texte werden NICHT beim Start in die Datenbank geschrieben. + Sie liegen bereit, und jemand aus dem Team übernimmt sie — genau wie + beim Aufgabenkatalog der Modis. + + Der Grund ist derselbe wie dort: Ein Text, den niemand gelesen hat, + steht sonst als Ansage des Teams auf einer öffentlichen Seite. + „Bleibt fair" klingt harmlos, bis jemand fragt, was das im Streitfall + heißt — und dann muss dahinterstehen, wer es gemeint hat. Übernehmen + ist eine Entscheidung; automatisch füllen wäre eine Behauptung. + + Und es hält den zweiten Weg offen: Jeder Text lässt sich vor dem + Übernehmen ändern. Was hier steht, ist ein guter Anfang, kein Gesetz. + + --------------------------------------------------------------------- + WIE DIE TEXTE GESCHRIEBEN SIND + + Für Zuschauer, die von TikTok kommen — überwiegend jung, auf dem + Handy, mit wenig Geduld für Behördendeutsch. Also: + + - kurze Sätze, direkte Ansprache, „du" und „ihr" + - keine Drohungen, sondern Gründe („damit …", „sonst …") + - jede Regel sagt, was PASSIERT, nicht nur was verboten ist + - nichts, was DogFather über sein Team stellt + + Zwei Bereiche sind bewusst KEIN Katalog, sondern fertige Seiten: + „Regeln & Hilfe" und „Mitmachen". Sie sind Nachschlagewerke, keine + Feeds — ein leeres Regelwerk ist schlimmer als gar keins, weil im + Streitfall niemand etwas in der Hand hat. + ===================================================================== */ + +/** Ein Eintrag: { art, titel, text }. `art` muss zu den Arten des + * Bereichs passen — pruef-treff-start rechnet das nach. */ +export const TREFF_START = { + + /* =================================================================== + REGELN & HILFE — das Nachschlagewerk + Zuerst, weil es das Einzige ist, das im Streitfall zählt. Die + Reihenfolge ist Absicht: erst was gilt, dann was passiert, dann + wie man Hilfe bekommt. + =================================================================== */ + regeln: [ + { art: "regel", titel: "Rede mit Menschen, nicht über sie", + text: "Hier sitzen echte Leute. Kritik ist in Ordnung, auch deutliche — " + + "aber sie geht an die Sache, nicht an die Person. Der Unterschied ist " + + "einfach zu merken: „Das Video war langweilig“ ist Kritik. " + + "„Du bist langweilig“ ist keine." }, + { art: "regel", titel: "Keine Beleidigungen, keine Hetze", + text: "Gegen niemanden, auch nicht als Witz, auch nicht „ironisch“. " + + "Rassismus, Sexismus, Hetze gegen Herkunft, Religion, Behinderung oder " + + "sexuelle Orientierung fliegen sofort raus — ohne Diskussion und ohne " + + "zweite Chance." }, + { art: "regel", titel: "Was privat ist, bleibt privat", + text: "Keine Klarnamen, Adressen, Telefonnummern, Schulen oder Arbeitsplätze — " + + "weder deine eigenen noch die von anderen. Auch nicht Screenshots aus " + + "privaten Chats. Was einmal draußen ist, holt niemand zurück." }, + { art: "regel", titel: "Kein Streit von woanders hierher tragen", + text: "Wenn es auf einer anderen Plattform gekracht hat, bleibt es dort. " + + "Hier fängt jeder wieder bei null an — sonst schleppt der Treff jeden " + + "alten Streit mit sich herum." }, + { art: "regel", titel: "Keine Werbung für deinen eigenen Kanal", + text: "Ein Hinweis in einem Gespräch, in das er passt: klar. Unter jedem " + + "Beitrag derselbe Link: nein. Wer nur zum Verlinken kommt, merkt man " + + "nach drei Beiträgen." }, + { art: "regel", titel: "Achtzehn plus bleibt draußen", + text: "Keine Pornografie, keine Gewaltdarstellungen, keine Drogenwerbung. " + + "Hier lesen Minderjährige mit — und für einen Teil der Inhalte haften " + + "wir." }, + + { art: "folge", titel: "Was passiert, wenn jemand die Regeln bricht", + text: "Drei Stufen, in dieser Reihenfolge: **Hinweis** (jemand sagt dir, was " + + "nicht ging), **Pause** (du kannst ein paar Tage nicht schreiben), " + + "**Ausschluss**. Bei den schweren Sachen oben — Hetze, private Daten, " + + "18+ — fällt die erste Stufe weg." }, + { art: "folge", titel: "Was passiert, wenn du dich falsch behandelt fühlst", + text: "Sag es. Jede Maßnahme wird festgehalten, mit Grund und mit Namen " + + "dessen, der sie verhängt hat — nicht damit du dich rechtfertigen musst, " + + "sondern damit wir es nachlesen können. Wenn wir daneben lagen, nehmen " + + "wir es zurück." }, + { art: "folge", titel: "Warum manches gelöscht wird, ohne dass etwas passiert", + text: "Nicht jede Entfernung ist eine Strafe. Doppelte Beiträge, Sachen im " + + "falschen Bereich, ein Bild, das nicht lädt — das räumen wir auf. Wenn " + + "es dich betrifft und du nicht weißt warum, frag nach." }, + + { art: "hilfe", titel: "Wie du jemanden erreichst", + text: "Im Treff eine Frage stellen — dort liest immer jemand mit. Wenn es " + + "nicht öffentlich sein soll: im Bereich „Etwas stimmt nicht“ melden. " + + "Das sehen nur die Leute, die hier moderieren." }, + { art: "hilfe", titel: "Wie du etwas meldest", + text: "An jedem Beitrag gibt es „melden“. Schreib in einem Satz dazu, was " + + "los ist — „unangebracht“ hilft niemandem weiter, „er postet seit " + + "einer Stunde denselben Link“ schon. Melden ist kein Petzen; es ist " + + "der Weg, wie so ein Raum sauber bleibt." }, + { art: "hilfe", titel: "Wenn du selbst gerade nicht klarkommst", + text: "Wenn es dir wirklich schlecht geht, ist ein Community-Chat der falsche " + + "Ort — und das ist nicht gegen dich gesagt. Telefonseelsorge: 0800 111 0 111 " + + "oder 0800 111 0 222, rund um die Uhr, kostenlos und anonym. " + + "Nummer gegen Kummer für Jüngere: 116 111." }, + + { art: "frage", titel: "Wer moderiert hier eigentlich?", + text: "Ein Team aus Leuten, die selbst hier angefangen haben. Sie machen das " + + "neben ihrem Alltag und sind nicht rund um die Uhr da — wenn etwas ein " + + "paar Stunden stehen bleibt, heißt das nicht, dass es in Ordnung ist." }, + { art: "frage", titel: "Kann ich meinen Beitrag wieder löschen?", + text: "Deinen eigenen ja. Was andere darauf geantwortet haben, bleibt — " + + "sonst würden Gespräche Löcher bekommen." }, + { art: "frage", titel: "Warum sehe ich manche Bereiche nicht?", + text: "Manches ist nur für das Team, zum Beispiel die Liste der Meldungen. " + + "Das ist keine Geheimniskrämerei: Dort stehen Namen von Leuten, über die " + + "gerade entschieden wird." }, + ], + + /* =================================================================== + MITMACHEN — der Weg hinein, in Schritten + =================================================================== */ + mitmachen: [ + { art: "gesucht", titel: "Schritt 1: Sei einfach ein paar Streams dabei", + text: "Kein Bewerbungsschreiben, kein Formular. Wer mitmachen will, fängt " + + "damit an, da zu sein — im Chat, im Treff, bei den Wünschen. Fast alle, " + + "die heute moderieren, sind so angefangen." }, + { art: "gesucht", titel: "Schritt 2: Sag im Treff, dass du Lust hast", + text: "Ein Satz reicht: was du gern machen würdest und wann du ungefähr " + + "Zeit hast. Du musst nichts können — alles, was hier gebraucht wird, " + + "lernt man in ein paar Wochen nebenher." }, + { art: "gesucht", titel: "Schritt 3: Eine Schicht zum Reinschnuppern", + text: "Du sitzt eine Sendung neben jemandem, der es schon macht. Danach " + + "entscheidet ihr beide — du auch. Es ist völlig in Ordnung zu sagen: " + + "doch nichts für mich." }, + + { art: "gesucht", titel: "Chat-Moderation", + text: "Während der Sendung im Chat mitlesen: Neue begrüßen, Fragen " + + "einsammeln, eingreifen wenn es kippt. Am wichtigsten ist nicht " + + "Härte, sondern Ruhe." }, + { art: "gesucht", titel: "Clips schneiden", + text: "Aus einer Sendung werden Ausschnitte. Wer ein Schnittprogramm " + + "aufbekommt und Geduld für die ersten drei Sekunden hat, kann das — " + + "den Rest zeigen wir." }, + { art: "gesucht", titel: "Community betreuen", + text: "Wer war lange nicht da, wer ist neu, wer hat etwas gefragt und keine " + + "Antwort bekommen. Die Arbeit, die niemand sieht und alle merken." }, + { art: "gesucht", titel: "Technik", + text: "Ton, Bild, Verbindung, Geräte. Meistens unsichtbar — und sofort " + + "sichtbar, wenn es fehlt." }, + + { art: "frage", titel: "Was habe ich davon?", + text: "Ehrlich: kein Geld. Du lernst etwas, das anderswo bezahlt wird, du " + + "hast Leute um dich, mit denen du etwas aufbaust, und du entscheidest " + + "mit, wie dieser Ort aussieht. Wenn du auf schnelles Geld aus bist, ist " + + "das hier nichts." }, + { art: "frage", titel: "Wie viel Zeit muss ich haben?", + text: "Zwei Stunden in der Woche reichen für den Anfang. Wichtiger als viel " + + "ist verlässlich: Eine Schicht, auf die man sich verlassen kann, ist mehr " + + "wert als fünf spontane." }, + { art: "frage", titel: "Muss ich mein Gesicht zeigen?", + text: "Nein. Die meisten hier moderieren mit Namen und ohne Kamera." }, + { art: "frage", titel: "Ich bin unter 18 — geht das?", + text: "Für vieles ja, für manches nicht. Sag einfach dazu, wie alt du bist, " + + "dann sagen wir dir, was geht. Ausschlüsse und Meldungen entscheidet " + + "nie jemand allein." }, + ], + + /* =================================================================== + ANSCHLAGBRETT — was gerade gilt + Wenige, große Aushänge. Deshalb bewusst kurz gehalten. + =================================================================== */ + anschlag: [ + { art: "ansage", titel: "Willkommen im Treff", + text: "Das hier ist der Raum neben dem Stream. Fragen stellen, Hallo sagen, " + + "Wünsche eintragen, eure Clips zeigen. Wer neu ist: Fang im Treff an, " + + "ein Satz reicht." }, + { art: "ansage", titel: "So läuft eine Sendung ab", + text: "Wir fangen pünktlich an und sagen vorher Bescheid, wenn es anders " + + "kommt. Während der Sendung liest jemand im Chat mit — wenn du etwas " + + "wissen willst, frag einfach." }, + { art: "hinweis", titel: "Wo was hingehört", + text: "Termine unter „Was ansteht“, Wünsche in die Wunschliste, Clips zu " + + "den Highlights, alles andere in den Treff. Wenn du unsicher bist: " + + "Treff. Wir sortieren das dann." }, + { art: "hinweis", titel: "Wenn mal nichts passiert", + text: "Keine Sendung heißt nicht, dass hier nichts los ist. Die Wunschliste " + + "und die Highlights leben auch zwischendurch." }, + { art: "danke", titel: "Danke, dass ihr da seid", + text: "Ohne die Leute, die jede Woche wiederkommen, wäre das hier ein leerer " + + "Raum mit guter Beleuchtung. Das ist keine Floskel — man sieht den " + + "Unterschied an jedem Abend." }, + ], + + /* =================================================================== + DER TREFF — Gesprächsanfänge + Kein Brett bleibt so sicher leer wie eins, auf dem niemand anfangen + will. Deshalb: Fragen, auf die man in einem Satz antworten kann. + =================================================================== */ + treff: [ + { art: "hallo", titel: "Sag Hallo — wie bist du hier gelandet?", + text: "Ein Satz reicht. Über TikTok, über jemanden, zufällig? Uns " + + "interessiert es wirklich — und es hilft uns zu verstehen, wo wir " + + "gefunden werden." }, + { art: "frage", titel: "Was sollen wir im nächsten Stream unbedingt machen?", + text: "Schreib es hier hin oder — noch besser — trag es in die Wunschliste " + + "ein. Dort sieht man, wie viele dasselbe wollen." }, + { art: "frage", titel: "Welches Spiel sollten wir mal ausprobieren?", + text: "Gern auch etwas, das nicht jeder kennt. Die besten Abende waren " + + "meistens die, bei denen vorher niemand wusste, was passiert." }, + { art: "lob", titel: "Was war dein bester Moment bisher?", + text: "Irgendeine Szene, die dir hängengeblieben ist. Wenn wir den Clip " + + "finden, landet er bei den Highlights." }, + { art: "frage", titel: "Wann seid ihr da? Sagt uns eure Zeiten", + text: "Wir planen die Sendezeiten nach euch — aber dafür müssen wir wissen, " + + "wann ihr überhaupt könnt. Wochentags spät? Am Wochenende früher?" }, + { art: "hallo", titel: "Zeig uns dein Setup", + text: "Handy auf dem Sofa, drei Bildschirme, Fernseher in der Küche? " + + "Erzähl kurz, wie du zuschaust." }, + ], + + /* =================================================================== + WUNSCHLISTE — Beispiele, die zeigen, wie ein guter Wunsch aussieht + =================================================================== */ + wunsch: [ + { art: "gemeinsam", titel: "Ein Abend, an dem wir mit euch zusammen zocken", + text: "Nicht wir vorne und ihr im Chat, sondern zusammen in einer Runde. " + + "Braucht Vorlauf, damit es nicht im Chaos endet — aber es steht seit " + + "Langem ganz oben auf der Liste." }, + { art: "stream", titel: "Ein Stream, in dem ihr Fragen stellen könnt", + text: "Eine Sendung, in der es nur um eure Fragen geht — gesammelt vorher, " + + "beantwortet live, auch die unangenehmen." }, + { art: "technik", titel: "Besserer Ton, wenn mehrere reden", + text: "Wenn zwei gleichzeitig sprechen, versteht man beide schlecht. Steht " + + "auf der Liste; wer sich mit Mischpulten auskennt, meldet sich gern." }, + { art: "sonstiges", titel: "Wie ein guter Wunsch aussieht", + text: "Sag WAS du willst und WARUM. „Mehr Technik-Streams“ ist ein " + + "Anfang, „Mehr Technik-Streams, weil ich beim Aufnehmen nicht " + + "weiterkomme“ ist einer, mit dem wir etwas anfangen können. " + + "Und: Drück bei Wünschen anderer auf „Will ich auch“ — danach wird " + + "sortiert." }, + ], + + /* =================================================================== + HIGHLIGHTS — was hier hingehört + =================================================================== */ + highlight: [ + { art: "moment", titel: "Was hier hingehört", + text: "Clips, Bilder, Zeichnungen, Bildschirmfotos von Momenten, die euch " + + "geblieben sind. Der erste darf ruhig wackelig sein — es geht nicht um " + + "Produktion, sondern um den Moment." }, + { art: "fanart", titel: "Fanart ist ausdrücklich erwünscht", + text: "Gezeichnet, gebastelt, am Handy zusammengeschoben — egal. Schreib " + + "dazu, ob wir es im Stream zeigen dürfen." }, + ], + + /* =================================================================== + WAS ANSTEHT — Vorlagen für Termine + Ohne Datum, das setzt beim Übernehmen, wer den Termin kennt. + =================================================================== */ + ansteht: [ + { art: "stream", titel: "Der nächste Stream", + text: "Trag Datum und Uhrzeit ein — dann steht oben auf der Seite ein " + + "Countdown, und alle sehen auf einen Blick, wann es losgeht." }, + { art: "event", titel: "Community-Abend", + text: "Ein Abend, an dem die Zuschauer mitspielen statt zuzusehen. Schreib " + + "dazu, wie man mitmacht und wie viele Plätze es gibt." }, + { art: "turnier", titel: "Turnier", + text: "Mit Anmeldung, Regeln und einem Ende. Schreib dazu, bis wann man sich " + + "melden kann — sonst fragen es fünfzehn Leute einzeln." }, + { art: "pause", titel: "Keine Sendung", + text: "Auch das gehört hier hin. Wer nicht weiß, dass ausfällt, wartet " + + "umsonst — und kommt beim nächsten Mal vielleicht nicht wieder." }, + ], +}; + +/** Wie viele Vorschläge gibt es je Bereich? Für die Oberfläche, damit + * sie den Knopf nur zeigt, wenn es etwas zu holen gibt. */ +export function startZahlen() { + const aus = {}; + for (const [bereich, liste] of Object.entries(TREFF_START)) aus[bereich] = liste.length; + return aus; +} diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 75fd453b..eb99865b 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609171139", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609171148", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609171139", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609171148", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171139", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171148", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171139", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171148", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index 594012c3..786ac962 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -897,3 +897,57 @@ .bildstreifen__bild { transition: none; } .bildstreifen__feld:hover .bildstreifen__bild { transform: none; } } + +/* ===================================================================== + DER STARTKATALOG (17.09.2026) + + Filipe, mit drei Bildschirmfotos leerer Bretter: „ich will fertige + sachen schon drin". Im Plan stand es sogar (§5.1): „Acht leere Seiten + sind schlimmer als keine." + + DER KASTEN SIEHT ANDERS AUS ALS DIE EINTRÄGE DARUNTER — und das ist + der Punkt: Was hier steht, ist noch nichts. Sähe er aus wie ein + Beitrag, würde man ihn für einen halten und sich fragen, warum er + sich nicht anfassen lässt. Deshalb: gestrichelter Rand statt + durchgezogener. Das Zeichen für „Platzhalter" ist alt und wird + sofort verstanden. + ------------------------------------------------------------------- */ +.startkatalog { + margin: 0 0 24px; padding: 18px 20px; + border: 1px dashed color-mix(in srgb, var(--akzent) 34%, var(--rand)); + border-radius: var(--radius); + background: color-mix(in srgb, var(--akzent) 5%, transparent); +} +.startkatalog__kopf { + display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 14px; + margin-bottom: 14px; +} +.startkatalog__titel { + margin: 0; font-size: 1.02rem; font-weight: 650; color: var(--text); +} +.startkatalog__unter { + margin: 0; flex: 1 1 260px; + font-size: .85rem; color: var(--text-leise); +} +.startkatalog__alle { flex: 0 0 auto; } + +/* Auto-fill: Auf dem Handy eine Spalte, am Rechner drei — gemessen, + nicht gerechnet. */ +.startkatalog__liste { + display: grid; gap: 12px; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); +} +.startkatalog__karte { + display: flex; flex-direction: column; align-items: flex-start; gap: 7px; + padding: 13px 14px; + border: 1px solid var(--rand); + border-radius: var(--radius-klein); + background: color-mix(in srgb, var(--tinte) 92%, #ffffff 8%); +} +.startkatalog__name { + margin: 0; font-size: .95rem; font-weight: 640; color: var(--text); +} +.startkatalog__text { + margin: 0; flex: 1; + font-size: .84rem; line-height: 1.5; color: var(--text-leise); +} diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index 8a8c12aa..8ecd767e 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -766,6 +766,68 @@ } catch { uebernahmen = {}; uebernahmeMoeglich = false; } } + /* ---- DER STARTKATALOG ------------------------------------------------ + Fertige Texte, die jemand aus dem Team uebernimmt. Nicht + automatisch: Ein Text, den niemand gelesen hat, stuende sonst als + Ansage des Teams auf einer oeffentlichen Seite. */ + async function startkatalogZeigen() { + const ziel = $('startkatalog'); + if (!ziel) return; + if (!darfEintragen()) { ziel.hidden = true; return; } + let daten = null; + try { + const a = await fetch(`/workspace/api/bereich/${bereich}/start`, + { credentials: 'same-origin' }); + if (!a.ok) { ziel.hidden = true; return; } + daten = await a.json(); + } catch { ziel.hidden = true; return; } + if (!daten?.offen) { ziel.hidden = true; return; } + + ziel.hidden = false; + ziel.textContent = ''; + const kopf = el('div', 'startkatalog__kopf'); + kopf.append(el('h2', 'startkatalog__titel', 'Fertige Vorschläge')); + kopf.append(el('p', 'startkatalog__unter', + `${daten.offen} ${daten.offen === 1 ? 'Vorschlag ist' : 'Vorschläge sind'} ` + + 'vorbereitet. Übernehmen, was passt – ändern kannst du danach alles.')); + const alle = el('button', 'schritt startkatalog__alle', + `Alle ${daten.offen} übernehmen`); + alle.type = 'button'; + alle.addEventListener('click', async () => { + alle.disabled = true; + const a = await hole(`/workspace/api/bereich/${bereich}/start`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', + }); + if (!a.ok) { melde('Ging nicht.'); alle.disabled = false; return; } + await laden(); + }); + kopf.append(alle); + ziel.append(kopf); + + const liste = el('div', 'startkatalog__liste'); + for (const v of daten.vorschlaege) { + if (v.schon) continue; + const k = el('div', 'startkatalog__karte'); + k.append(el('span', 'marke-art', einstellung?.arten?.[v.art] || v.art)); + k.append(el('h3', 'startkatalog__name', v.titel)); + if (v.text) k.append(el('p', 'startkatalog__text', v.text)); + const b = el('button', 'schritt', 'Übernehmen'); + b.type = 'button'; + b.addEventListener('click', async () => { + b.disabled = true; + const a = await hole(`/workspace/api/bereich/${bereich}/start`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nr: v.nr }), + }); + if (!a.ok) { melde('Ging nicht.'); b.disabled = false; return; } + await laden(); + }); + k.append(b); + liste.append(k); + } + ziel.append(liste); + } + async function laden() { try { await uebernahmenLaden(); @@ -797,13 +859,22 @@ offene" stellt, will trotzdem wissen, wann der naechste Stream ist. */ countdownZeigen(daten.eintraege); + /* NACH der Liste, nicht davor: Der Katalog verschwindet, sobald + alles uebernommen ist -- und das haengt an dem, was gerade + geladen wurde. */ + startkatalogZeigen(); maxStimmen = (daten.eintraege || []) .reduce((n, e) => Math.max(n, Number(e.stimmen) || 0), 0); if (!sichtbar.length) { + /* DER SATZ GEHOERT ZUM BEREICH (17.09.2026). "Noch kein Eintrag + in diesem Bereich" stand auf allen acht gleich -- und drei + Bildschirmfotos davon nebeneinander sahen aus wie ein defektes + System. Jeder Bereich sagt jetzt in seinen eigenen Worten, + was hier hingehoert. */ ziel.append(el('p', 'leer-hinweis', daten.eintraege.length ? 'Zu dieser Auswahl gibt es nichts.' - : 'Noch kein Eintrag in diesem Bereich.')); + : (einstellung?.leer || 'Noch kein Eintrag in diesem Bereich.'))); /* AUCH HIER springen -- und das ist kein Schoenheitsfehler, sondern der wichtigere Fall: Wer auf "3 dringende Punkte" klickt und auf einer leeren Seite landet, glaubt, er sei diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index d94be48f..59408ed0 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -291,10 +291,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 387c0d09..4c8b653e 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/befinden.html b/workspace/befinden.html index 475c5f27..54dcab68 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 1db333e3..31122ffe 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -189,19 +189,24 @@ + +

Einträge werden geladen …

- - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index b21ee3c9..a3f2acba 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 84d982e5..3ffa57ae 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 53160cd0..c9c2b4fc 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index 5e3b746b..c793f626 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 563f8931..022e5c26 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609171139", + "src": "/assets/img/app-symbole/crew-192.png?v=202609171148", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609171139", + "src": "/assets/img/app-symbole/crew-512.png?v=202609171148", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171139", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171148", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171139", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171148", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index e0bd01fb..8dd145b5 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index 79de1909..2848fcda 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -102,9 +102,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 077f79dd..9f90a10e 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index e09f78f5..757b1ea2 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 6e04071f..00c7d380 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 5a69ab63..2c5255ab 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 0115b82e..92badc5b 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index 25d33dd0..34d6f8ca 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index 978e9b1a..9bfba68a 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 1ee75bca..d077ccda 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 61e6245d..0b4b7fea 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index a82c1259..b366fd62 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 5019984d..ee8026f5 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index 99919769..383f6782 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index 077701ef..55729912 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index b2747cac..0321a7f5 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index 8529e63f..585a85b2 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -126,9 +126,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index 07922c5d..29b5c360 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -211,10 +211,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 678173b1..b28500ff 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 06bfc19c..5c06ce1f 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +