diff --git a/server/pruef-vorlagen.mjs b/server/pruef-vorlagen.mjs index d4e9bcbf..e56883c3 100644 --- a/server/pruef-vorlagen.mjs +++ b/server/pruef-vorlagen.mjs @@ -1,185 +1,272 @@ -/* Prueft die fertigen Vorlagen und was daraus wird. +/* ===================================================================== + DIE STARTHILFE FÜR DIE COMMUNITY -- 18.09.2026 - 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. + Filipe: „ich will auch dass du fertige und beispiele machst für die + community damit die auch start hilfe haben überall." - 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. */ + DIE WICHTIGSTE PRÜFUNG HIER IST DIE ERSTE: Steht in jeder Vorlage + eine Art, die es auf diesem Brett wirklich gibt? + + Eine Vorlage mit unbekannter Art fällt nicht auf. Der Knopf + erscheint, man klickt ihn, das Auswahlfeld springt NICHT um -- + und der Beitrag landet in der ersten Art der Liste. Ein Wunsch für + den Stream steht dann unter „Technik", und niemand weiß, warum. + + Aufrufen mit: node server/pruef-vorlagen.mjs + ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; +const PORT = await portMussFreiSein(4197, "pruef-vorlagen"); const ordner = mkdtempSync(join(tmpdir(), "ws-vorl-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); -process.env.PORT = "4204"; +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(120_000, "pruef-vorlagen"); 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++; }; +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); -await fetch(BASIS + "/workspace/api/anmelden", { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); +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 CREW = "crew.dogfather-universe.com"; +const { TREFF_VORLAGEN, vorlagenFuer } = await import("./workspace-treff-vorlagen.js"); +const { BEREICHE } = await import("./workspace-bereiche.js"); + +/* ======================================================================= + 1. JEDE ART MUSS ES GEBEN + ======================================================================= */ +melde(""); +melde("=== 1. Passen die Arten zu den Brettern? ==="); +{ + let gepruefteVorlagen = 0, unbekannt = 0; + for (const [brett, liste] of Object.entries(TREFF_VORLAGEN)) { + const arten = Object.keys(BEREICHE[brett]?.arten || {}); + ok(arten.length > 0, `„${brett}" gibt es, mit ${arten.length} Arten`); + for (const v of liste) { + gepruefteVorlagen++; + if (!arten.includes(v.art)) { + unbekannt++; + console.log(` -> „${v.art}" gibt es auf ${brett} nicht (${arten.join(", ")})`); + } + } + } + /* DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Waere die Liste leer, waere + „0 unbekannte Arten" wahr, ohne dass etwas geprueft wurde. */ + ok(gepruefteVorlagen >= 8 && unbekannt === 0, + `alle ${gepruefteVorlagen} Vorlagen nennen eine Art, die es gibt (${unbekannt} unbekannt)`); +} + +/* ======================================================================= + 2. WAS EINE VORLAGE HABEN MUSS + ======================================================================= */ +melde(""); +melde("=== 2. Taugen die Texte? ==="); +{ + let ohneLuecke = 0, zuKurz = 0, anzahl = 0; + for (const liste of Object.values(TREFF_VORLAGEN)) { + for (const v of liste) { + anzahl++; + /* JEDE VORLAGE BRAUCHT EINE LUECKE. Eine, die man absenden KANN, + wird abgesendet -- und dann steht zehnmal derselbe Satz da. */ + if (!`${v.titel} ${v.text}`.includes("…")) ohneLuecke++; + if (!v.titel || v.titel.length < 8 || !v.text || v.text.length < 25) zuKurz++; + } + } + ok(anzahl >= 8 && ohneLuecke === 0, + `jede der ${anzahl} Vorlagen hat eine Luecke zum Ausfuellen (${ohneLuecke} ohne)`); + ok(anzahl >= 8 && zuKurz === 0, + `und alle haben Ueberschrift und Text (${zuKurz} zu duenn)`); +} + +/* ======================================================================= + 3. NUR DIE BRETTER, AUF DENEN DIE COMMUNITY SCHREIBT + ======================================================================= */ +melde(""); +melde("=== 3. Auf welchen Brettern? ==="); +{ + const drin = Object.keys(TREFF_VORLAGEN); + for (const b of ["anschlag", "ansteht", "regeln"]) { + ok(!drin.includes(b), + `„${b}" hat keine Vorlagen -- dort schreibt die Community nicht`); + } + ok(drin.includes("treff") && drin.includes("wunsch"), + `Treff und Wunschliste haben welche (${drin.join(", ")})`); + ok(vorlagenFuer("gibtsnicht").length === 0, + "ein unbekanntes Brett liefert eine leere Liste, kein undefined"); +} + +/* ======================================================================= + 4. UND ÜBER DIE SCHNITTSTELLE + ======================================================================= */ +melde(""); +melde("=== 4. Wer bekommt sie zu sehen? ==="); + +function roh(pfad, host, kopf = {}, koerper = null) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, + method: koerper ? "POST" : "GET", + headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf }, + }, (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); + }); +} +const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } }; const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); -for (let i = 0; i < 60; i++) { +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + for (const [name, rolle, code] of [ + ["Filipe", "admin", "CODE-DOGI-0001"], + ["Mara", "gast", "CODE-GAST-0001"], + ]) { + const salz = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salz, 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, salz, 32768, new Date().toISOString()); + } + d.close(); +} + +async function anmelden(rolle, code, host) { + let a = await roh("/workspace/api/anmelden", host, {}, { rolle, code }); + if (a.code === 400 && a.text.includes("alter_offen")) { + a = await roh("/workspace/api/anmelden", host, {}, { rolle, code, alter_ok: true }); + } + return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); +} + +const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW); +ok(!!kDogi, "DogFather ist angemeldet"); +{ + const a = json(await roh("/workspace/api/bereich/treff/vorlagen", CREW, { cookie: kDogi })); + ok(Array.isArray(a.vorlagen) && a.vorlagen.length >= 3, + `er bekommt die Vorlagen des Treffs (${a.vorlagen?.length})`); + const b = json(await roh("/workspace/api/bereich/regeln/vorlagen", CREW, { cookie: kDogi })); + ok(Array.isArray(b.vorlagen) && b.vorlagen.length === 0, + `fuer „Regeln & Hilfe" kommt eine leere Liste (${b.vorlagen?.length})`); +} +{ + /* OHNE ANMELDUNG GAR NICHTS. Der Router verlangt sie -- diese Zeile + misst, dass die neue Route nicht daran vorbeigebaut wurde. */ + const a = await roh("/workspace/api/bereich/treff/vorlagen", CREW); + ok(a.code === 401, `ohne Anmeldung: abgewiesen (${a.code})`); +} +{ + const a = await roh("/workspace/api/bereich/gibtsnicht/vorlagen", CREW, { cookie: kDogi }); + ok(a.code === 404, `ein unbekanntes Brett: 404 (${a.code})`); +} + +/* ======================================================================= + 5. IM BROWSER -- STEHEN DIE KNOEPFE WIRKLICH DA? + + DIESER ABSCHNITT IST DER GRUND, WARUM ES IHN GIBT. Die Starthilfe + war fertig, die Route lieferte die Vorlagen, alle Prüfungen darüber + waren grün -- und im Formular erschien nichts. + + Ursache: `vorlagenZeigen` und `#vorlagen` gab es im Haus bereits + (die Checkliste für Creator). Die spätere Funktionsdeklaration + gewinnt, und zwei Elemente trugen dieselbe Kennung. Es gab keine + Fehlermeldung, keine rote Prüfung, nichts in der Konsole. + + Gefunden hat es nur ein Blick auf das Bild. Damit das nicht vom + Zufall abhängt, misst diese Prüfung jetzt das Sichtbare. + ======================================================================= */ +melde(""); +melde("=== 5. Im Browser ==="); +{ + let pw = null; 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(); + pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + } catch { /* kein Playwright -- dritter Ausgang unten */ } -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 || {}) } }); + if (!pw) { + melde(" -- Playwright nicht da -- im Browser nicht nachgesehen"); + } else { + const browser = await pw.chromium.launch(); + try { + const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } }); + const seite = await ctx.newPage(); + const konsole = []; + seite.on("pageerror", (e) => konsole.push(String(e).slice(0, 120))); -/* ---------- 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})`); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "networkidle" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]", { noWaitAfter: true }); + await seite.waitForURL("**/start.html", { timeout: 25000 }); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/bereich.html?b=treff`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); - /* 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)`); + /* KEINE DOPPELTEN KENNUNGEN. Das war die Ursache, und es ist + die Sorte Fehler, die man nur sieht, wenn man danach sucht. */ + const doppelt = await seite.evaluate(() => { + const zaehler = {}; + for (const e of document.querySelectorAll("[id]")) { + zaehler[e.id] = (zaehler[e.id] || 0) + 1; + } + return Object.entries(zaehler).filter(([, n]) => n > 1).map(([k]) => k); + }); + ok(doppelt.length === 0, + `keine Kennung kommt zweimal vor${doppelt.length ? ": " + doppelt.join(", ") : ""}`); - /* 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)})`); + await seite.click("#neu-oeffnen"); + await seite.waitForTimeout(900); + const z = await seite.evaluate(() => ({ + sichtbar: !document.getElementById("starthilfe")?.hidden, + knoepfe: [...document.querySelectorAll(".starthilfe__beispiel")].map((b) => b.textContent.trim()), + })); + ok(z.sichtbar && z.knoepfe.length === 4, + `die Starthilfe steht im Formular (${z.knoepfe.length} Beispiele: ${z.knoepfe.join(", ")})`); + /* EIN KLICK MUSS DIE FELDER FUELLEN -- alles andere ist ein + Knopf, der nichts tut. */ + await seite.click(".starthilfe__beispiel"); + await seite.waitForTimeout(300); + const g = await seite.evaluate(() => ({ + art: document.getElementById("f-art")?.value || "", + titel: document.getElementById("f-titel")?.value || "", + text: document.getElementById("f-text")?.value || "", + wegDanach: !!document.getElementById("starthilfe")?.hidden, + })); + ok(g.titel.length > 5 && g.text.length > 20, + `ein Klick fuellt Ueberschrift und Text („${g.titel.slice(0, 40)}")`); + ok(g.art === "hallo", `und stellt die Art passend um (${g.art})`); + ok(g.text.includes("…"), "der Text hat Luecken, die noch zu fuellen sind"); + ok(g.wegDanach, "danach ist die Starthilfe weg -- sie hat ihre Arbeit getan"); + ok(konsole.length === 0, + `kein Skriptfehler${konsole.length ? ": " + konsole[0] : ""}`); + } finally { + await browser.close(); + } + } } -/* ---------- 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; -} - -/* Der Abschnitt "Hilfe anfordern" stand hier. Der Weg ist am - 01.09.2026 entfallen -- geblieben ist die Rückmeldung an jedem Punkt, - geprüft in pruef-checkliste.mjs. */ - -/* ---------- Die Oberflaeche ---------------------------------------------- - - Der Browser-Teil stand hier und prueft den frueheren - "Vorschlaege"-Block. Den gibt es seit dem Umbau auf die feste - Checkliste nicht mehr: Die Punkte stehen jetzt fuer jeden gleich da, - statt einzeln geholt zu werden. - - Geprueft wird die Oberflaeche deshalb in pruef-checkliste.mjs. Die - Schnittstelle oben bleibt hier -- "Uebernehmen" gibt es weiterhin, - fuer eigene Ergaenzungen ueber die feste Liste hinaus. - - Eine Pruefung stehen zu lassen, die etwas Abgeschafftes verlangt, - waere schlimmer als keine: Sie ist dauerhaft rot, man gewoehnt sich - daran, und beim naechsten echten Fehler sieht niemand hin. */ -try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } -console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung."); +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ } process.exit(fehler ? 1 : 0); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 49f3388a..d20e78a7 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -16,6 +16,7 @@ import express from "express"; import { TREFF_START } from "./workspace-treff-start.js"; +import { vorlagenFuer } from "./workspace-treff-vorlagen.js"; import { ARBEIT_START } from "./workspace-arbeit-start.js"; /* Wann ein Zettel einen Termin hat -- DIESELBE Regel, aus der auch der Kalender seine Auswahl baut. Sie steht in einer eigenen Datei, weil @@ -1703,6 +1704,34 @@ function pruefe(bereich, körper, { neu }) { 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. */ +/* ===================================================================== + VORLAGEN FUER DIE COMMUNITY (18.09.2026) + + Filipe: „ich will auch dass du fertige und beispiele machst fuer die + community damit die auch start hilfe haben ueberall." + + NICHT ZU VERWECHSELN MIT DEM STARTKATALOG darueber. Der ist fuer das + Team: fertige Beitraege, die jemand uebernimmt, damit kein Brett + leer dasteht. Hier geht es um das andere Ende -- ein Mitglied steht + vor einem leeren Feld und weiss nicht, was hineingehoert. + + NUR WER SCHREIBEN DARF. Ein Mitglied, das die Sieben-Tage-Frist noch + nicht hinter sich hat, sieht das Formular gar nicht; Vorlagen dazu + waeren ein Angebot fuer eine Tuer, die zu ist. Dieselbe Regel wie + beim Startkatalog, nur mit umgekehrtem Vorzeichen: dort „wer + schreiben darf, bekommt NICHTS", hier „nur wer schreiben darf". */ +bereicheRouter.get("/workspace/api/bereich/:bereich/vorlagen", (req, res) => { + try { + const bereich = String(req.params.bereich); + if (!BEREICHE[bereich]) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (darfSchreiben(req.person, bereich)) return res.json({ vorlagen: [] }); + res.json({ vorlagen: vorlagenFuer(bereich) }); + } catch (fehler) { + console.error("[workspace] Vorlagen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + bereicheRouter.get("/workspace/api/bereich/:bereich/start", (req, res) => { try { const bereich = String(req.params.bereich); diff --git a/server/workspace-treff-vorlagen.js b/server/workspace-treff-vorlagen.js new file mode 100644 index 00000000..9e41d456 --- /dev/null +++ b/server/workspace-treff-vorlagen.js @@ -0,0 +1,132 @@ +/* ===================================================================== + VORLAGEN FÜR DIE COMMUNITY (18.09.2026) + + Filipe: „ich will auch dass du fertige und beispiele machst für die + community damit die auch start hilfe haben überall." + + --------------------------------------------------------------------- + DAS IST NICHT DER STARTKATALOG + + Den gibt es seit dem 17.09. — und er ist für das TEAM: fertige + Beiträge, die jemand aus dem Team übernimmt, damit kein Brett leer + dasteht. Ein Mitglied bekommt ihn nie zu sehen (`darfSchreiben` + entscheidet), und das ist richtig: Es sind Ansagen des Teams. + + Hier geht es um das andere Ende. Ein Mitglied steht vor einem leeren + Feld und weiß nicht, was hineingehört — und schreibt deshalb gar + nichts. Das ist der häufigste Grund, warum ein Brett still bleibt, + obwohl viele mitlesen. + + --------------------------------------------------------------------- + WARUM DIE TEXTE LÜCKEN HABEN + + Eine Vorlage, die man absenden KANN, wird abgesendet. Dann stehen + dort zehnmal dieselben Sätze, und der elfte Mensch merkt, dass hier + niemand wirklich schreibt. + + Jede Vorlage hat deshalb mindestens eine Stelle, die nur der + Schreibende füllen kann — meist mit „…" markiert. Wer sie stehen + lässt, merkt es beim Lesen, bevor er auf Absenden drückt. + + --------------------------------------------------------------------- + UND WARUM NUR VIER BRETTER + + Auf `anschlag`, `ansteht` und `regeln` schreibt die Community nicht. + Vorlagen dort wären ein Angebot für eine Tür, die zu ist. + ===================================================================== */ + +/** @typedef {{art: string, titel: string, text: string}} Vorlage */ + +/** Vorlagen je Brett. Die `art` muss zu den Arten des Bretts passen — + * `pruef-vorlagen.mjs` misst das, damit hier keine Art steht, die es + * nicht gibt: Eine Vorlage mit unbekannter Art landet beim Absenden + * in der ersten Art der Liste, und niemand merkt es. */ +export const TREFF_VORLAGEN = { + treff: [ + { + art: "hallo", + titel: "Hallo, ich bin neu hier", + text: "Ich schaue seit … und bin über … hergekommen. " + + "Am liebsten mag ich … an den Streams.", + }, + { + art: "frage", + titel: "Frage zu …", + text: "Mir ist aufgefallen, dass … Weiß jemand, wie das gemeint ist?", + }, + { + art: "lob", + titel: "Der Moment mit … war stark", + text: "Im Stream vom … ging es um … — das ist mir hängengeblieben, weil …", + }, + { + art: "problem", + titel: "Etwas stimmt nicht: …", + text: "Was passiert ist: … Wann: … " + + "Ich schreibe es hier, weil es mir wichtig ist, dass es aufhört.", + }, + ], + + wunsch: [ + { + art: "stream", + titel: "Ich wünsche mir … im Stream", + text: "Was: … Warum: … " + + "Für mich wäre das gut, weil …", + }, + { + art: "gemeinsam", + titel: "Etwas, das wir zusammen machen könnten", + text: "Meine Idee: … Dafür bräuchte es … " + + "Ich wäre auf jeden Fall dabei.", + }, + { + art: "technik", + titel: "Eine Kleinigkeit an der Technik", + text: "Mir fällt auf, dass … Bei mir ist es … " + + "(Gerät, Browser oder App: …)", + }, + ], + + highlight: [ + { + art: "moment", + titel: "Der Moment, als …", + text: "Wann: … Was passiert ist: … " + + "Ich musste danach noch eine Weile lachen.", + }, + { + art: "fanart", + titel: "Ich habe etwas gemalt", + text: "Gemacht mit … Gedauert hat es … " + + "Wer es weiterverwenden will: bitte mit Namen.", + }, + ], + + mitmachen: [ + { + art: "lust", + titel: "Ich hätte Lust bei … mitzumachen", + text: "Das kann ich schon: … Zeit hätte ich … " + + "Erfahrung habe ich damit … (auch ohne ist es eine Antwort).", + }, + { + art: "frage", + titel: "Frage dazu: …", + text: "Bevor ich mich melde, würde ich gern wissen, ob …", + }, + ], +}; + +/** Alle Bretter, für die es Vorlagen gibt. */ +export const VORLAGEN_BRETTER = Object.keys(TREFF_VORLAGEN); + +/** Die Vorlagen eines Bretts — oder eine leere Liste. + * + * Als Funktion und nicht als direkter Zugriff, damit an einer Stelle + * entschieden ist, was bei einem unbekannten Brett passiert: eine + * leere Liste, kein `undefined`. Ein `undefined` hier wäre ein + * Absturz dort, wo jemand `.length` liest. */ +export function vorlagenFuer(bereich) { + return TREFF_VORLAGEN[String(bereich)] || []; +} diff --git a/tools/community-blick.mjs b/tools/community-blick.mjs index bf3e36a1..914ac208 100644 --- a/tools/community-blick.mjs +++ b/tools/community-blick.mjs @@ -65,6 +65,11 @@ kind.kill(); await new Promise((r) => setTimeout(r, 600)); personAnlegen("Filipe", "admin", "CODE-DOGI-0001"); personAnlegen("Mara", "gast", "CODE-GAST-0001"); +/* Ein ZWEITES Mitglied. Den eigenen Beitrag meldet niemand -- er + laesst sich loeschen, das ist der kuerzere Weg. Ohne dieses zweite + Konto misst die Gegenprobe unten also nicht den Meldeweg, sondern + diese Regel. */ +personAnlegen("Jonas", "gast", "CODE-GAST-0002"); await start(); /** Meldet an -- mit Host-Kopfzeile, weil die Rolle davon abhaengt. @@ -117,6 +122,32 @@ for (const b of BRETTER) { console.log(` ${b.padEnd(11)} ${a.status} ${r.angelegt ?? r.anzahl ?? ""}`); } +/* ---- EIN BEITRAG VON EINEM MITGLIED -------------------------------- + + GEGENPROBE ZUM MELDEWEG. „melden" steht seit dem 18.09.2026 nicht + mehr unter Beitraegen des Teams -- unter einer Regel ergab der Knopf + keinen Sinn. Die Gefahr dabei: dass er dadurch auch dort + verschwindet, wo er gebraucht wird. + + Deshalb bekommt der Treff hier einen Beitrag, der von einem + MITGLIED stammt. Unter ihm muss der Knopf stehen. Ohne diesen + Eintrag saehe eine abgeschaffte Meldefunktion genauso aus wie eine + funktionierende: Im Test schreibt sonst nur das Team. + + Direkt in die Datenbank und nicht ueber die Schnittstelle: Ein + frisches Mitglied darf sieben Tage lang nicht schreiben, und diese + Regel soll gelten bleiben. */ +{ + const d = new DatabaseSync(DB); + const wer = d.prepare("SELECT id FROM personen WHERE rolle = 'gast' AND name = 'Jonas'").get(); + d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von) + VALUES ('treff','frage',?,?,?, 'offen', ?, ?)`) + .run("Kommt Casper auch mal in den Stream?", + "Ich schaue meistens abends und habe ihn noch nie live gesehen.", + new Date().toISOString().slice(0, 10), new Date().toISOString(), wer.id); + d.close(); +} + /* ---- Und jetzt als Mitglied hinsehen ------------------------------ */ const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); /* DIE ECHTE ADRESSE, nicht 127.0.0.1. @@ -170,6 +201,9 @@ for (const b of BRETTER) { arten: [...new Set([...document.querySelectorAll('.eintrag-art, .art')].map((e) => e.textContent.trim()))].length, knoepfeJeKarte: document.querySelector('.eintrag-karte') ? document.querySelector('.eintrag-karte').querySelectorAll('button, a.schritt').length : 0, + /* Steht „melden" dort, wo es hingehoert -- und nur dort? */ + meldenGesamt: [...document.querySelectorAll('.eintrag-karte button')] + .filter((b) => b.textContent.trim() === 'melden').length, schreibfeld: !!document.querySelector('#neu-form, .neu-form, form#neu'), hoeheJeKarte: document.querySelector('.eintrag-karte') ? Math.round(document.querySelector('.eintrag-karte').getBoundingClientRect().height) : 0, @@ -181,7 +215,7 @@ for (const b of BRETTER) { })); console.log(`${b.padEnd(11)} „${z.titel}" · ${z.karten} Karten` + ` · Schreibfeld: ${z.schreibfeld ? "ja" : "NEIN"}` - + ` · Arten: ${z.arten} · Karte ${z.hoeheJeKarte}px/${z.knoepfeJeKarte} Knöpfe` + + ` · Karte ${z.hoeheJeKarte}px · melden: ${z.meldenGesamt}` + (z.sternchen ? ` · STERNCHEN: ${z.sternchen}` : "") + (z.leerText ? ` · „${z.leerText}"` : "")); const pfad = `C:/Users/qciga/AppData/Local/Temp/comm-${b}${BREITE < 700 ? "-handy" : ""}.png`; diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 60a37fb8..4ab004d7 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609181833", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609181858", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609181833", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609181858", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609181833", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609181858", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609181833", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609181858", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css index e0f4543f..70f6d8c0 100644 --- a/workspace/assets/css/aufgaben.css +++ b/workspace/assets/css/aufgaben.css @@ -112,6 +112,31 @@ @media (max-width: 560px) { .neu__raster > .feld, .neu__raster > .feld--halb { grid-column: 1 / -1; } } + +/* DER HINWEIS GEHOERT AUF DEM HANDY WIEDER IN DEN FLUSS (18.09.2026). + + Er haengt sonst absolut unter dem Feld, und darunter stehen genau + 30 px Luft (row-gap). Das reicht fuer EINE Zeile. Der Satz bei „Gilt + fuer" -- „Gehoert allen hier - wer Der Treff sieht, sieht auch + diesen Eintrag." -- braucht auf 412 px ZWEI, und lag damit quer + ueber der naechsten Beschriftung. Im Bildschirmfoto stand + „UEBERSCHRIFT" halb unter dem Hinweis; gemeldet hat das keine Zahl. + + Die ganze absolute Konstruktion gibt es nur, damit die Eingaben + MEHRERER Felder nebeneinander auf einer Linie sitzen. Unter 560 px + steht ohnehin nur eins je Zeile -- dort gibt es nichts auszurichten, + und der Hinweis darf einfach dort stehen, wo er hingehoert. + + Das ist die bessere Antwort als „row-gap groesser machen": Die + naechste Zeile Text waere sonst wieder eine zu viel. */ +@media (max-width: 560px) { + .neu__raster { row-gap: 16px; } + .neu__raster > * { grid-template-rows: auto auto; } + .neu__raster > * > .feld-hinweis { + position: static; + margin-top: 6px; + } +} .neu__knoepfe { display: flex; gap: 10px; align-items: center; margin-top: 12px; } .feld-schild { diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index ad2c5290..2bf69abe 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -1065,20 +1065,51 @@ /* Die Sprungmarken — der Unterschied zwischen einem Dokument und einer langen Seite. */ +/* WARUM DAS HIER KEINE PILLEN MEHR SIND (18.09.2026). + + Sie sahen aus wie die Filterknöpfe darüber: gleiche Form, gleiche + Größe, gleiche Wörter. Auf „Regeln & Hilfe" standen damit zwei + identisch wirkende Reihen untereinander, von denen die eine filterte + und die andere sprang. Niemand kann das erraten. + + Jetzt sind es Links, und sie sehen aus wie Links: unterstrichen, + ohne Rahmen, mit „Springe zu" davor und der Anzahl dahinter. Die + Filterreihe entfällt auf diesen Brettern ganz. */ .brettmarken { - display: flex; flex-wrap: wrap; gap: 6px; + display: flex; flex-wrap: wrap; gap: 4px 14px; + align-items: baseline; margin: 0 0 18px; padding-bottom: 14px; border-bottom: 1px solid var(--rand); } -.brettmarken__weg { - padding: 5px 12px; border-radius: 999px; - font-size: .8rem; font-weight: 600; text-decoration: none; - color: var(--text-leise); - border: 1px solid var(--rand-hell); - transition: color var(--tempo), border-color var(--tempo), background var(--tempo); +.brettmarken__wort { + font-size: .78rem; + color: var(--text-still); + margin-inline-end: 2px; } -.brettmarken__weg:hover { - color: var(--text); background: rgba(255, 255, 255, .04); +.brettmarken__weg { + display: inline-flex; align-items: baseline; gap: 5px; + font-size: .84rem; font-weight: 600; + color: var(--text-leise); + text-decoration: underline; + text-decoration-color: var(--rand-hell); + text-underline-offset: 3px; + /* Die Fläche zum Tippen bleibt groß genug, obwohl nichts umrandet + ist -- sonst trifft man auf dem Handy den Nachbarn. */ + padding: 6px 2px; + transition: color var(--tempo), text-decoration-color var(--tempo); +} +.brettmarken__weg:hover, +.brettmarken__weg:focus-visible { + color: var(--text); + text-decoration-color: currentColor; +} +/* Die Anzahl sagt, ob sich der Sprung lohnt -- und dass hier nicht + gefiltert wird. */ +.brettmarken__zahl { + font-size: .72rem; font-weight: 700; + color: var(--text-still); + padding: 1px 6px; border-radius: 999px; + background: rgba(255, 255, 255, .06); } /* Die Abschnittsüberschrift. scroll-margin, damit die Überschrift beim @@ -1433,3 +1464,43 @@ color: #9fb3c9; font-size: .84rem; } + +/* ---- STARTHILFE BEIM SCHREIBEN (18.09.2026) ----------------------- + Ein leeres Feld ist der häufigste Grund, warum ein Brett still + bleibt, obwohl viele mitlesen. Die Zeile steht ganz oben im + Formular — vor Art und Datum, weil sie beides mit ausfüllt. + + Gedämpft gehalten: Es ist ein Angebot, keine Aufforderung. Wer weiß, + was er schreiben will, soll daran vorbeilesen können. */ +.starthilfe { + margin: 0 0 16px; + padding: 12px 14px; + border: 1px dashed var(--rand-hell); + border-radius: 10px; + background: rgba(255, 255, 255, .02); +} +.starthilfe__wort { + margin: 0 0 9px; + font-size: .84rem; + color: var(--text-leise); +} +.starthilfe__reihe { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.starthilfe__beispiel { + min-height: 38px; + padding: 7px 13px; + border: 1px solid var(--rand-hell); + border-radius: 999px; + background: color-mix(in srgb, var(--tinte) 86%, #ffffff 14%); + color: var(--text); + font: inherit; + font-size: .82rem; + font-weight: 600; + cursor: pointer; + transition: border-color var(--tempo), background var(--tempo); +} +.starthilfe__beispiel:hover { border-color: var(--akzent); } +.starthilfe__beispiel:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index e7b7e503..751793f7 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -106,6 +106,16 @@ im Browser kann man aendern. */ let treffBrett = false; let lage = null; + /* WELCHE BRETTER NACH ART GEGLIEDERT WERDEN. + Stand frueher mitten in der Zeichenfunktion -- dort konnte + `filterBauen` sie nicht sehen, und deshalb baute die Filterreihe + auf genau diesen Brettern eine ZWEITE Leiste mit denselben + Woertern wie die Sprungmarken darunter. */ + const NACH_ART = { + regeln: ['regel', 'folge', 'hilfe', 'frage'], + mitmachen: ['gesucht', 'lust', 'frage'], + }; + let filter = ''; /* WELCHER KANAL -- eine ZWEITE Dimension neben `filter`, nicht derselbe Wert. @@ -672,7 +682,22 @@ den eigenen kann man loeschen, das ist der kuerzere Weg. Der Grund ist Pflicht (DSA Art. 16); der Server prueft ihn noch einmal, die Abfrage hier erspart nur den Fehlschlag. */ - if (!lage.team && e.erstellt_von !== ich.id) { + /* NICHT AN BEITRAEGEN DES TEAMS (18.09.2026). + + Unter jeder Regel stand „melden" -- auch unter „Achtzehn plus + bleibt draussen". Man meldet keine Regel: Sie ist der Text des + Teams selbst, und ein Knopf darunter legt nahe, das Team + moderiere sich gegenseitig. Auf „Regeln & Hilfe" standen so + siebzehn Melden-Knoepfe, von denen keiner einen Sinn hatte. + + Wer mit dem Team reden will, findet den Weg unter „Hilfe" -- + dort steht, wie man DogFather und die Moderation erreicht. + + IM ZWEIFEL BLEIBT DER KNOPF: Fehlt die Rollenangabe (alte + Eintraege), gilt der Beitrag nicht als Team-Beitrag. Lieber + ein Knopf zu viel als ein fehlender Meldeweg. */ + const vomTeam = !!e.erstellt_rolle && e.erstellt_rolle !== 'gast'; + if (!lage.team && e.erstellt_von !== ich.id && !vomTeam) { zeile.append(tuKnopf('melden', async () => { const grund = prompt('Was ist daran nicht in Ordnung?\n\n' + 'Bitte schreib es in einem Satz – sonst können wir es nicht prüfen.'); @@ -1089,9 +1114,95 @@ return stueck; } + /* ---- STARTHILFE BEIM SCHREIBEN (18.09.2026) --------------------- + + Filipe: „ich will auch dass du fertige und beispiele machst fuer + die community damit die auch start hilfe haben ueberall." + + Wer zum ersten Mal schreibt, steht vor einem leeren Feld. Die + Vorlagen geben einen Anfang -- mit LUECKEN, die nur der + Schreibende fuellen kann. Eine Vorlage, die man absenden KANN, + wird abgesendet; dann steht dort zehnmal derselbe Satz. + + SIE VERSCHWINDEN, SOBALD JEMAND TIPPT. Hilfe, die stehen bleibt, + wenn man sie nicht mehr braucht, ist keine Hilfe mehr, sondern + etwas, das man wegklickt -- und beim naechsten Mal gleich + ignoriert. */ + /* HEISST `starthilfe` UND NICHT `vorlagen`. + + `vorlagenZeigen` und `#vorlagen` gab es hier schon: die Checkliste + fuer Creator auf live/community/technik. Meine gleichnamige + Funktion wurde davon ueberschrieben -- die spaetere Deklaration + gewinnt --, und im HTML standen zwei Elemente mit derselben + Kennung. Die Starthilfe erschien deshalb NIE, ohne eine einzige + Fehlermeldung. Aufgefallen ist es nur, weil ich nachgesehen habe, + ob die Knoepfe wirklich dastehen. */ + let starthilfeGeholt = ''; + + async function starthilfeZeigen() { + const kasten = $('starthilfe'); + const reihe = $('starthilfe-reihe'); + if (!kasten || !reihe) return; + if (starthilfeGeholt === bereich) return; + starthilfeGeholt = bereich; + reihe.textContent = ''; + kasten.hidden = true; + + let liste = []; + try { + const a = await hole(`/workspace/api/bereich/${bereich}/vorlagen`); + if (!a.ok) return; + liste = (await a.json())?.vorlagen || []; + } catch { return; } + if (!liste.length) return; + + for (const v of liste) { + const b = el('button', 'starthilfe__beispiel', einstellung?.arten?.[v.art] || v.art); + b.type = 'button'; + /* Der volle Text als Titel-Attribut: Wer wissen will, was ihn + erwartet, muss nicht erst klicken. */ + b.title = `${v.titel}\n\n${v.text}`; + b.addEventListener('click', () => { + const art = $('f-art'); + if (art && [...art.options].some((o) => o.value === v.art)) { + art.value = v.art; + art.dispatchEvent(new Event('change')); + } + $('f-titel').value = v.titel; + $('f-text').value = v.text; + kasten.hidden = true; + /* Der Fokus wandert in die Ueberschrift und markiert sie: Der + erste Handgriff ist, den Platzhalter zu ersetzen. */ + $('f-titel').focus(); + $('f-titel').select(); + }); + reihe.append(b); + } + kasten.hidden = false; + + /* Sobald jemand selbst schreibt, sind die Beispiele erledigt. */ + for (const id of ['f-titel', 'f-text']) { + $(id)?.addEventListener('input', () => { kasten.hidden = true; }, { once: true }); + } + } + function filterBauen(eintraege = []) { const ziel = $('filter'); ziel.textContent = ''; + + /* ---- EIN NACHSCHLAGEWERK BRAUCHT KEINEN FILTER (18.09.2026) ---- + + Auf „Regeln & Hilfe" standen zwei Knopfreihen direkt + untereinander -- „Regel · Häufige Frage · Hilfe · Was passiert, + wenn" und darunter dieselben vier Wörter noch einmal. Die eine + filterte, die andere sprang zur Überschrift. Von außen sahen sie + gleich aus, und niemand konnte erraten, was der Unterschied ist. + + Dort, wo nach Art gegliedert wird, ist der Filter der + überflüssige von beiden: Die Gliederung zeigt ohnehin schon + alles, sortiert und mit Überschriften. Die Sprungmarken bleiben + -- sie sind das Inhaltsverzeichnis. */ + if (NACH_ART[bereich]) { filter = ''; return; } const knopf = (wert, text) => { const b = el('button', 'schritt', text); b.type = 'button'; @@ -1436,10 +1547,6 @@ "Mitmachen" ist ein WEG: erst die drei Schritte, dann die Gebiete, dann die Fragen. Eine Liste beantwortet "was gibt es"; die Frage lautet aber "was tue ich als Erstes". */ - const NACH_ART = { - regeln: ['regel', 'folge', 'hilfe', 'frage'], - mitmachen: ['gesucht', 'lust', 'frage'], - }; if (NACH_ART[bereich] && !filter) { const reihe = NACH_ART[bereich]; const faecher = new Map(reihe.map((a) => [a, []])); @@ -1451,13 +1558,22 @@ einem Dokument und einer langen Seite. */ const marken = el('nav', 'brettmarken'); marken.setAttribute('aria-label', 'Zu einem Abschnitt springen'); + /* „Springe zu" davor: Ohne diese zwei Wörter sieht eine Reihe + von Wörtern aus wie eine Reihe von Filtern -- genau die + Verwechslung, die hier behoben wird. Mit ihnen ist es ein + Inhaltsverzeichnis, und das ist es auch. */ + marken.append(el('span', 'brettmarken__wort', 'Springe zu')); for (const a of faecher.keys()) { - if (!faecher.get(a).length) continue; + const anzahl = faecher.get(a).length; + if (!anzahl) continue; const m = el('a', 'brettmarken__weg', einstellung?.arten?.[a] || a); + /* Die Anzahl gehört dazu: Sie sagt, ob sich der Sprung lohnt + -- und sie verrät, dass hier nicht gefiltert wird. */ + m.append(el('span', 'brettmarken__zahl', String(anzahl))); m.href = `#abschnitt-${a}`; marken.append(m); } - if (marken.childNodes.length > 1) ziel.append(marken); + if (marken.childNodes.length > 2) ziel.append(marken); for (const a of faecher.keys()) { const fach = faecher.get(a); @@ -1662,6 +1778,7 @@ ist. */ if (!darfEintragen()) { $('neu').hidden = true; $('neu-oeffnen').hidden = true; return; } $('neu').hidden = !auf; $('neu-oeffnen').hidden = auf; + if (auf) starthilfeZeigen(); if (auf) $('f-titel').focus(); }; $('neu-oeffnen').addEventListener('click', () => { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index b516b3ba..71cb20f2 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 62cd8b76..1841414f 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 72a1eb75..70460843 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 3b7e115e..3343ba62 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,24 +12,24 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - - + + - + @@ -70,6 +70,14 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index ffbfec8b..093da072 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 61c49095..1914099c 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 7243985c..477c0863 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 5d5a3cf5..e1761c4d 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 dbd8635c..62ea8f17 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 35219d51..fa90c1ac 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 d0ddb492..8d028e0c 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 f8c080c7..4321ec84 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 9efa422f..d3c95338 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 ca078907..ce1a53cc 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 0a884216..2864dd42 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,18 +7,18 @@ - - - - - - + + + + + + - - - + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index aca2cb05..5e5c5a18 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 e69413f5..a94e56f1 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/teilen.html b/workspace/teilen.html index 31b4c5fa..c138d85a 100644 --- a/workspace/teilen.html +++ b/workspace/teilen.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -112,9 +112,9 @@ - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index 85c84be8..4a7af4bf 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 2ed4f990..097c0929 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 ddc25d62..de245537 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 669e2f8f..e6438a40 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 @@ - - - - - + + + + +