From 7eec2edf3fc2e6e41b3e34153885ed06c95779f4 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 17 Sep 2026 11:28:37 +0200 Subject: [PATCH] Stufe 3: Die Wunschliste bekommt einen Rang und einen Weg Aus dem Plan im Vault. Eine Wunschliste ohne sichtbare Erfuellung ist ein Briefkasten ohne Postbote -- nach dem dritten unbeantworteten Wunsch schreibt niemand mehr. ZWEIMAL LAG MEIN PLAN DANEBEN, beide Male zu pessimistisch: - Die Sortierung nach Stimmen GAB es schon, seit dem 11.09. - Die Datenbank erlaubte 'angenommen' und 'abgelehnt' laengst in ihrer CHECK-Regel; nur die Pruefung im Server liess sie nicht durch. Die Tabelle war der Logik voraus. Auch ein Plan von gestern Nacht ist eine Bestandsliste und altert. WAS DAZUGEKOMMEN IST - Vier Zustaende JE BEREICH statt zwei global: Offen -> Wird gemacht -> Gemacht, dazu "Diesmal nicht". Eine gemeinsame Liste haette "Diesmal nicht" auch an einem Schutzvorfall erlaubt, und dort bedeutet es nichts. - Ein geplanter Tag dazu: "Wird gemacht" allein ist ein Versprechen, "Wird gemacht -- am 24.09." ist ein Termin. - Der Rangbalken. Eine Rangfolge sieht man erst, wenn der ABSTAND sichtbar ist: 12 Stimmen neben 14 sehen sonst aus wie 1 neben 40. Fuer ein Vorleseprogramm spricht er in Worten. - Entschiedenes rutscht nach unten, wird aber NICHT geloescht. Gerade der erfuellte Wunsch ist der Beweis, dass sich Schreiben lohnt. "DIESMAL NICHT" IST DER WICHTIGSTE DER VIER, und es ist bewusst nicht rot. Ein Nein ist eine Antwort, Schweigen ist keine -- aber wer ein rotes Schild an seinem Wunsch sieht, schreibt keinen zweiten. UND AUF DEM BILDSCHIRMFOTO STAND "WAS IHR EUCH WUENSCHT". Die Umlaut-Pruefung von gestern Nacht sah es nicht: Sie kannte die Kataloge und die festen Seitentexte, aber nicht die Bereichseinstellungen, aus denen JEDE Ueberschrift und jede Art-Beschriftung kommt. Sechs Stellen ("Fuer den Stream", "Wie es hier laeuft", "Haeufige Frage", "Ich haette Lust" ...). Gefunden hat sie kein Gedankengang, sondern ein Blick auf das fertige Bild. Die Wortliste hatte ausserdem Loecher -- "wuensch" fehlte schlicht; 23 Stuecke nachgetragen. pruef-deutsche-texte deckt jetzt auch die 120 Beschriftungen der Bereiche ab (9 -> 12 Pruefungen), mit einer Gegenprobe auf genau den Satz, der heute Morgen durchrutschte. NEU: pruef-wunschliste (30 Pruefungen) Der aelteste Wunsch bekommt die meisten Stimmen -- genau der Fall, der eine Sortierung nach Datum entlarvt. Dazu: alle vier Zustaende setzen, ein erfundener nicht, "abgelehnt" am Anschlagbrett ABGELEHNT, der geplante Tag, die Balkenbreiten (100/33/0 %), und dass der gemachte Wunsch weiter in der Liste steht -- nur nicht mehr oben. pruef-countdown 22, pruef-treff 66, pruef-bereiche-lesend, pruef-deutsche-texte 12: gruen. Co-Authored-By: Claude Opus 5 --- server/helfer-deutsch.mjs | 11 ++ server/pruef-deutsche-texte.mjs | 42 +++++ server/pruef-wunschliste.mjs | 301 +++++++++++++++++++++++++++++++ server/workspace-bereiche.js | 72 +++++++- workspace/app.webmanifest | 8 +- workspace/assets/css/bereich.css | 82 +++++++++ workspace/assets/js/bereich.js | 74 ++++++++ workspace/aufgaben.html | 24 +-- workspace/automation.html | 26 +-- workspace/befinden.html | 24 +-- workspace/bereich.html | 34 ++-- workspace/calls.html | 28 +-- workspace/chat.html | 26 +-- workspace/content.html | 30 +-- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 +-- workspace/entwicklung.html | 24 +-- workspace/index.html | 8 +- workspace/kalender.html | 28 +-- workspace/leistung.html | 26 +-- workspace/personen.html | 26 +-- workspace/profil.html | 28 +-- workspace/rechte.html | 24 +-- workspace/report.html | 28 +-- workspace/scouting.html | 26 +-- workspace/start.html | 24 +-- workspace/startcheck.html | 28 +-- workspace/steckbrief.html | 28 +-- workspace/talente.html | 24 +-- workspace/team.html | 26 +-- workspace/teamlage.html | 24 +-- workspace/treff-moderation.html | 24 +-- workspace/treff-regeln.html | 26 +-- workspace/uebersicht.html | 26 +-- workspace/wissen.html | 26 +-- 36 files changed, 933 insertions(+), 367 deletions(-) create mode 100644 server/pruef-wunschliste.mjs diff --git a/server/helfer-deutsch.mjs b/server/helfer-deutsch.mjs index 3246d320..76006e32 100644 --- a/server/helfer-deutsch.mjs +++ b/server/helfer-deutsch.mjs @@ -32,6 +32,17 @@ export const ERSATZSCHREIBUNGEN = [ "staerk", "stoer", "taegl", "taet", "traeg", "ueber", "uebung", "ungefaehr", "veraend", "verlaeuf", "vorschlaeg", "waehl", "waehr", "waere", "weiss", "woert", "zaehl", "zugaeng", "zurueck", + /* NACHGETRAGEN AM 17.09.2026, weil das Netz Loecher hatte: Auf einem + Bildschirmfoto der Wunschliste stand "WAS IHR EUCH WUENSCHT", und + die Liste schwieg -- "wuensch" fehlte schlicht. Ein Netz, das man + nicht nachzieht, faengt beim naechsten Mal wieder nichts. + + Deshalb steht hier auch, WIE man das findet: nicht durch Nachdenken, + sondern indem man sich das fertige Bild ansieht. */ + "ausfuehr", "duerf", "einfueh", "erklaer", "fuehl", "fuehr", "fuenf", + "gluec", "gruen", "guelt", "haeuf", "kuend", "muede", "natuerl", + "schuetz", "spuer", "stueck", "traeum", "tuer", "uebrig", + "unterstuetz", "wuensch", "wuerd", ]; /** Welche Ersatzschreibungen stecken in diesem Text? Leer heisst sauber. */ diff --git a/server/pruef-deutsche-texte.mjs b/server/pruef-deutsche-texte.mjs index 61ec402e..690edb18 100644 --- a/server/pruef-deutsche-texte.mjs +++ b/server/pruef-deutsche-texte.mjs @@ -142,6 +142,43 @@ melde("=== 2. Was fest auf den Seiten steht ==="); ok(stellen === 0, `ihr sichtbarer Text ist frei von Ersatzschreibungen (${stellen} Funde)`); } +/* ======================================================================= + 2b. DIE BESCHRIFTUNGEN DER COMMUNITY-BEREICHE + ======================================================================= */ +melde(""); +melde("=== 2b. Was an den Brettern steht ==="); +{ + /* NACHGETRAGEN AM 17.09.2026, und der Weg dorthin gehoert dazu: + Auf einem Bildschirmfoto der Wunschliste stand "WAS IHR EUCH + WUENSCHT" und "FUER DEN STREAM". Diese Pruefung sah es nicht -- + sie kannte die Kataloge und die festen Seitentexte, aber nicht die + Bereichseinstellungen, aus denen jede Ueberschrift und jede + Art-Beschriftung kommt. + + Sechs Stellen waren es. Gefunden hat sie kein Gedankengang, + sondern ein Blick auf das fertige Bild -- deshalb steht das hier. */ + const b = await import("./workspace-bereiche.js"); + const texte = []; + const sammeln = (wo, wert) => { + if (typeof wert === "string" && wert.length > 2) texte.push([wo, wert]); + }; + for (const [name, e] of Object.entries(b.BEREICHE || {})) { + for (const [feld, wert] of Object.entries(e)) { + sammeln(`${name}.${feld}`, wert); + if (wert && typeof wert === "object" && !Array.isArray(wert)) { + for (const [a, x] of Object.entries(wert)) sammeln(`${name}.${feld}.${a}`, x); + } + } + } + ok(texte.length >= 60, `${texte.length} Beschriftungen gefunden`); + const schmutzig = texte.filter(([, t]) => ersatzschreibungen(t).length); + for (const [wo, t] of schmutzig) { + melde(` ${wo}: [${ersatzschreibungen(t).join(", ")}] ${t}`); + } + ok(schmutzig.length === 0, + `keine schreibt Umlaute als ae/oe/ue/ss (${schmutzig.length} Funde)`); +} + /* ======================================================================= 3. GEGENPROBE -- KANN DIE PRUEFUNG UEBERHAUPT ROT WERDEN? ======================================================================= */ @@ -156,6 +193,11 @@ melde("=== 3. Die Gegenprobe ==="); const gefunden = ersatzschreibungen(alt); ok(gefunden.length >= 3, `der alte Kategorietext schlaegt an (${gefunden.join(", ")})`); + /* UND DER SATZ VON HEUTE, an dem das Netz ein Loch hatte. Ohne ihn + waere nicht bewiesen, dass die Liste nachgezogen wurde. */ + ok(ersatzschreibungen("Was ihr euch wuenscht").length > 0, + "auch \"wuenscht\" schlaegt an -- das tat es heute Morgen noch nicht"); + /* UND DIE ANDERE RICHTUNG, genauso wichtig: Der reparierte Text darf NICHT anschlagen. Sonst waere die Liste einfach zu breit. */ const neu = "Was im Livechat passiert, während gesendet wird. " diff --git a/server/pruef-wunschliste.mjs b/server/pruef-wunschliste.mjs new file mode 100644 index 00000000..37ff269d --- /dev/null +++ b/server/pruef-wunschliste.mjs @@ -0,0 +1,301 @@ +/* ===================================================================== + DIE WUNSCHLISTE IST EINE RANGFOLGE, KEINE LISTE (17.09.2026) + + Aus dem Plan im Vault, Stufe 3. + + Eine Wunschliste ohne sichtbare Erfüllung ist ein Briefkasten ohne + Postbote. Nach dem dritten unbeantworteten Wunsch schreibt niemand + mehr. + + --------------------------------------------------------------------- + ZWEI DINGE, DIE BEIM MESSEN ANDERS WAREN ALS IM PLAN + + 1. Die Sortierung nach Stimmen GAB es schon (seit 11.09.). Im Plan + stand "fehlt" -- eine Bestandsliste altert, auch die eigene von + gestern Nacht. + 2. Die Datenbank kannte 'angenommen' und 'abgelehnt' bereits in ihrer + CHECK-Regel; nur die Pruefung im Server liess sie nicht durch. Die + Tabelle war der Logik voraus. + + --------------------------------------------------------------------- + DIE VIER FRAGEN + + 1. Steht der meistgewuenschte oben -- auch wenn er der aelteste ist? + 2. Lassen sich alle vier Zustaende setzen, ein erfundener nicht? + 3. Gelten sie NUR hier? ("Diesmal nicht" an einem Schutzvorfall + bedeutet nichts.) + 4. Bleibt ein GEMACHTER Wunsch sichtbar -- nur nicht mehr oben? + + Die vierte ist die wichtigste. Loeschen waere die falsche Antwort: + Gerade der erfuellte Wunsch ist der Beweis, dass sich Schreiben + lohnt. + + Aufrufen mit: node server/pruef-wunschliste.mjs + Bild: SCHIRM=1 node server/pruef-wunschliste.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { portMussFreiSein } from "./helfer-port.mjs"; +import { heuteLokal, tagLokal } from "./helfer-tag.mjs"; + +const PORT = await portMussFreiSein(4483, "pruef-wunschliste"); +const ordner = mkdtempSync(join(tmpdir(), "ws-wunsch-")); +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-wunschliste"); +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 { 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("VanVan", "hand", "CODE-HAND-0001"); +anlegen("Marina", "modi", "CODE-MODI-0001"); +anlegen("Nane", "modi", "CODE-NANE-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 hand = await anmelden("creator", "CODE-HAND-0001"); +const modi = await anmelden("creator", "CODE-MODI-0001"); +const nane = await anmelden("creator", "CODE-NANE-0001"); + +const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS }); + +async function wunsch(titel, tage) { + const a = await fetch(BASIS + "/workspace/api/bereich/wunsch", { + method: "POST", headers: kopf(dogi), + body: JSON.stringify({ art: "stream", titel, datum: tagLokal(tage) }), + }); + return (await a.json().catch(() => null))?.id || 0; +} +async function willIch(id, keks) { + const a = await fetch(`${BASIS}/workspace/api/treff/eintrag/${id}/willich`, + { method: "PUT", headers: kopf(keks), body: "{}" }); + return a.status; +} +async function liste(keks = dogi) { + const a = await fetch(BASIS + "/workspace/api/bereich/wunsch", { headers: { cookie: keks } }); + return await a.json().catch(() => null); +} +async function setzen(id, feld, keks = dogi) { + const a = await fetch(`${BASIS}/workspace/api/bereich/wunsch/${id}`, { + method: "PATCH", headers: kopf(keks), body: JSON.stringify(feld), + }); + return { status: a.status, daten: await a.json().catch(() => null) }; +} + +/* ---------- Drei Wuensche, verschieden alt und verschieden beliebt --- */ +const alt = await wunsch("Ein Zockerabend mit der Community", -20); +const mittel = await wunsch("Mehr Technik-Streams", -8); +const neu = await wunsch("Ein Q&A mit Casper", -1); +/* DER AELTESTE bekommt die meisten Stimmen -- genau das ist der Fall, + der eine Sortierung nach Datum entlarvt. */ +for (const k of [hand, modi, nane]) await willIch(alt, k); +await willIch(mittel, hand); + +/* ======================================================================= + 1. DER MEISTGEWUENSCHTE STEHT OBEN + ======================================================================= */ +melde(""); +melde("=== 1. Die Rangfolge ==="); +{ + const j = await liste(); + const namen = (j?.eintraege || []).map((e) => e.titel); + ok((j?.eintraege || []).length === 3, `drei Wuensche (${namen.length})`); + ok(namen[0] === "Ein Zockerabend mit der Community", + `der meistgewuenschte steht oben, obwohl er der aelteste ist ("${namen[0]}")`); + const stimmen = (j?.eintraege || []).map((e) => Number(e.stimmen) || 0); + ok(stimmen.join(",") === "3,1,0", `und die Stimmen stimmen (${stimmen.join(", ")})`); + /* GEGENPROBE zur Sortierung: Waere sie nach Datum, stuende der + juengste oben. Er steht unten -- ohne Stimmen. */ + ok(namen[namen.length - 1] === "Ein Q&A mit Casper", + `der juengste ohne Stimmen steht unten ("${namen[namen.length - 1]}")`); +} + +/* ======================================================================= + 2. DIE VIER ZUSTAENDE + ======================================================================= */ +melde(""); +melde("=== 2. Der Weg eines Wunsches ==="); +{ + const j = await liste(); + const zust = j?.einstellung?.zustaende || {}; + ok(Object.keys(zust).length === 4, + `vier Zustaende kommen mit (${Object.values(zust).join(" · ")})`); + ok(!!zust.abgelehnt, + `darunter ein ausdrueckliches Nein ("${zust.abgelehnt}")`); + + for (const z of ["angenommen", "erledigt", "abgelehnt", "offen"]) { + const a = await setzen(mittel, { status: z }); + ok(a.status === 200, `"${z}" laesst sich setzen (${a.status} ${a.daten?.fehler || ""})`); + } + const x = await setzen(mittel, { status: "vielleicht" }); + ok(x.status === 400, `ein erfundener nicht (${x.status})`); + + /* DER GEPLANTE TAG: "Wird gemacht" allein ist ein Versprechen, + "Wird gemacht -- am 24.09." ist ein Termin. */ + const g = await setzen(mittel, { status: "angenommen", geplant: tagLokal(7) }); + ok(g.status === 200, `ein geplanter Tag dazu (${g.status})`); + const jj = await liste(); + const m = (jj?.eintraege || []).find((e) => e.id === mittel); + ok(m?.geplant === tagLokal(7), `und er kommt zurueck (${m?.geplant})`); + const gf = await setzen(mittel, { geplant: "übermorgen" }); + ok(gf.status === 400, `ein erfundener Tag nicht (${gf.status})`); +} + +/* ======================================================================= + 3. DIE ZUSTAENDE GELTEN NUR HIER + ======================================================================= */ +melde(""); +melde("=== 3. Nicht ueberall ==="); +{ + /* "Diesmal nicht" an einem Anschlag bedeutet nichts -- eine + gemeinsame Liste haette es trotzdem erlaubt. */ + const a = await fetch(BASIS + "/workspace/api/bereich/anschlag", { + method: "POST", headers: kopf(dogi), + body: JSON.stringify({ art: "regel", titel: "Eine Ansage", datum: heuteLokal() }), + }); + const id = (await a.json().catch(() => null))?.id; + if (!id) { melde(" -- kein Anschlag anlegbar -- Art unbekannt, Abschnitt uebersprungen"); } + else { + const b = await fetch(`${BASIS}/workspace/api/bereich/anschlag/${id}`, { + method: "PATCH", headers: kopf(dogi), body: JSON.stringify({ status: "abgelehnt" }), + }); + ok(b.status === 400, `"abgelehnt" gibt es am Anschlagbrett nicht (${b.status})`); + const c = await fetch(`${BASIS}/workspace/api/bereich/anschlag/${id}`, { + method: "PATCH", headers: kopf(dogi), body: JSON.stringify({ status: "erledigt" }), + }); + ok(c.status === 200, `"erledigt" dagegen schon (${c.status})`); + } +} + +/* ======================================================================= + 4. EIN GEMACHTER WUNSCH BLEIBT SICHTBAR + ======================================================================= */ +melde(""); +melde("=== 4. Erfuellt heisst nicht verschwunden ==="); +{ + /* Der MEISTGEWUENSCHTE wird gemacht. Er darf nicht verschwinden -- + gerade er ist der Beweis, dass sich Schreiben lohnt. Er soll nur + nicht mehr die offenen verdraengen. */ + await setzen(alt, { status: "erledigt" }); + const j = await liste(); + const namen = (j?.eintraege || []).map((e) => e.titel); + const drin = namen.includes("Ein Zockerabend mit der Community"); + ok(drin, "der gemachte Wunsch steht weiter in der Liste"); + ok(namen[0] !== "Ein Zockerabend mit der Community", + `aber nicht mehr oben (oben steht jetzt "${namen[0]}")`); + ok(namen[namen.length - 1] === "Ein Zockerabend mit der Community", + "sondern unten bei den entschiedenen"); +} + +/* ======================================================================= + 5. IM BROWSER + ======================================================================= */ +melde(""); +melde("=== 5. Was man sieht ==="); +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: 390, height: 844 } }); + 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()); }); + await seite.goto(`${BASIS}/workspace/bereich.html?b=wunsch`, { waitUntil: "networkidle" }); + await seite.waitForSelector(".wunsch-rang", { timeout: 15_000 }); + await seite.waitForTimeout(500); + + const m = await seite.evaluate(() => ({ + balken: [...document.querySelectorAll(".wunsch-rang__fuellung")] + .map((b) => b.style.width), + spitze: document.querySelectorAll('.wunsch-rang[data-spitze="ja"]').length, + staende: [...document.querySelectorAll(".wunsch-stand")] + .map((z) => `${z.dataset.stand}:${z.textContent.trim()}`), + wahlKnoepfe: document.querySelectorAll(".wunsch-wahl__knopf").length, + gedrueckt: document.querySelectorAll('.wunsch-wahl__knopf[aria-pressed="true"]').length, + sprache: document.querySelector(".wunsch-rang")?.getAttribute("aria-label") || "", + })); + + ok(m.balken.length === 3, `drei Rangbalken (${m.balken.join(", ")})`); + /* DER ABSTAND IST DIE AUSSAGE: drei Stimmen = voll, eine = ein + Drittel, keine = leer. Ohne diese Zeile waere ein Balken, der + immer 100 % zeigt, genauso gruen. */ + ok(m.balken[0] === "100%" || m.balken.includes("100%"), + `der meistgewuenschte ist voll (${m.balken.join(", ")})`); + ok(m.balken.includes("33%"), `einer bei einem Drittel (${m.balken.join(", ")})`); + ok(m.balken.includes("0%"), "und einer leer"); + ok(m.spitze === 1, `genau einer ist als Spitze markiert (${m.spitze})`); + ok(m.staende.length === 3, `jeder Wunsch zeigt seinen Zustand (${m.staende.join(" | ")})`); + ok(m.staende.some((z) => z.startsWith("erledigt")), "darunter der gemachte"); + /* VIER KNOEPFE JE WUNSCH, und genau einer ist der aktuelle. */ + ok(m.wahlKnoepfe === 12, `vier Knoepfe je Wunsch (${m.wahlKnoepfe})`); + ok(m.gedrueckt === 3, `und je einer ist der aktuelle (${m.gedrueckt})`); + /* EIN BALKEN IST FUER EIN VORLESEPROGRAMM NICHTS -- ausser man sagt + es in Worten. */ + ok(/wollen das auch|will das auch|niemand/.test(m.sprache), + `der Balken spricht auch ("${m.sprache}")`); + ok(konsole.length === 0, + konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); + + if (process.env.SCHIRM) { + const pfad = fileURLToPath(new URL("pruef-wunschliste.png", import.meta.url)); + await seite.screenshot({ path: pfad, fullPage: false }); + melde(" -- Bildschirmfoto: server/pruef-wunschliste.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 6bfecde2..b69971f5 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -117,11 +117,39 @@ export const BEREICHE = { wunsch: { name: "Wunschliste", - ober: "Was ihr euch wuenscht", + ober: "Was ihr euch wünscht", + /* DER WEG EINES WUNSCHES (17.09.2026). + + Bis heute kannte ein Eintrag zwei Zustaende: offen und erledigt. + Fuer eine Wunschliste ist das zu wenig -- und der Mangel ist + kein Schoenheitsfehler, sondern der Grund, warum solche Listen + sterben: + + Eine Wunschliste ohne sichtbare Erfuellung ist ein Briefkasten + ohne Postbote. Nach dem dritten unbeantworteten Wunsch + schreibt niemand mehr. + + "Diesmal nicht" gehoert ausdruecklich dazu. Ein Nein ist eine + Antwort; Schweigen ist keine. Wer sieht, dass entschieden wurde, + schreibt beim naechsten Mal wieder -- auch wenn die Entscheidung + gegen ihn ausfiel. + + DIE DATENBANK KONNTE DAS SCHON. Ihre CHECK-Regel erlaubt seit + laengerem 'angenommen' und 'abgelehnt'; nur die Pruefung im + Server liess sie nicht durch. Die Tabelle war der Logik voraus. */ + zustaende: { + offen: "Offen", + angenommen: "Wird gemacht", + erledigt: "Gemacht", + abgelehnt: "Diesmal nicht", + }, + /* Dazu ein Datum: "Wird gemacht" allein ist ein Versprechen, + "Wird gemacht -- am 24.09." ist ein Termin. */ + mitGeplant: true, arten: { - stream: "Fuer den Stream", + stream: "Für den Stream", technik: "Technik", - gemeinsam: "Fuer die Community", + gemeinsam: "Für die Community", sonstiges: "Sonstiges", }, bewertung: false, @@ -152,14 +180,14 @@ export const BEREICHE = { regeln: { name: "Regeln & Hilfe", - ober: "Wie es hier laeuft", + 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 entscheiden -- das ist der Weg, auf dem ein Team uneinheitlich wird und die Community es merkt. */ arten: { regel: "Regel", - frage: "Haeufige Frage", + frage: "Häufige Frage", hilfe: "Hilfe", folge: "Was passiert, wenn", }, @@ -182,7 +210,7 @@ export const BEREICHE = { weil es stimmt -- und weil es die richtige Erwartung setzt. */ arten: { gesucht: "Wir suchen", - lust: "Ich haette Lust", + lust: "Ich hätte Lust", frage: "Frage dazu", }, bewertung: false, @@ -1072,7 +1100,18 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => { const reihenfolge = bereich === "anschlag" ? "angeheftet DESC, e.datum DESC, e.id DESC" : bereich === "wunsch" - ? "stimmen DESC, e.datum DESC, e.id DESC" + /* ENTSCHIEDENES NACH UNTEN -- ABER NICHT WEG (17.09.2026). + + Die Sortierung nach Stimmen gibt es seit dem 11.09. Seit es + vier Zustaende gibt, reicht sie nicht mehr: Ein gemachter + Wunsch mit vielen Stimmen stuende dauerhaft ganz oben und + verdraengte die offenen, ueber die noch zu entscheiden ist. + + Loeschen waere die falsche Antwort. Gerade der ERFUELLTE + Wunsch ist der Beweis, dass sich Schreiben lohnt -- er + gehoert gesehen, nur nicht an der ersten Stelle. */ + ? `CASE e.status WHEN 'offen' THEN 0 WHEN 'angenommen' THEN 0 ELSE 1 END, + stimmen DESC, e.datum DESC, e.id DESC` : `CASE e.status WHEN 'offen' THEN 0 ELSE 1 END, CASE e.dringlichkeit WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END, e.datum DESC, e.id DESC`; @@ -1244,9 +1283,26 @@ function pruefe(bereich, körper, { neu }) { else aus.uhrzeit = u; } if (körper.status !== undefined) { - if (!STATUS.includes(körper.status)) fehler.push("Unbekannter Status."); + /* JE BEREICH, NICHT GLOBAL (17.09.2026). Die Wunschliste kennt + vier Zustaende, alle anderen zwei. Eine gemeinsame Liste haette + "Diesmal nicht" auch an einem Schutzvorfall erlaubt -- und dort + bedeutet es nichts. */ + const erlaubt = einstellung.zustaende + ? Object.keys(einstellung.zustaende) : STATUS; + if (!erlaubt.includes(körper.status)) fehler.push("Unbekannter Status."); else aus.status = körper.status; } + /* DER GEPLANTE TERMIN eines Wunsches. Bisher gab es das Feld nur in + der Content-Planung; hier traegt es dieselbe Bedeutung und darf + deshalb dasselbe Format haben. Leer heisst "noch kein Termin" und + nicht "unveraendert". */ + if (einstellung.mitGeplant && körper.geplant !== undefined) { + const g = String(körper.geplant ?? "").trim(); + if (!g) aus.geplant = null; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(g) || Number.isNaN(Date.parse(g))) { + fehler.push("Der geplante Tag ist ungültig."); + } else aus.geplant = g; + } /* Bewertung und Dringlichkeit gibt es nur dort, wo der Bereich sie vorsieht -- sonst werden sie stillschweigend verworfen. */ if (einstellung.bewertung && körper.bewertung !== undefined) { diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 7e63d168..4932c13e 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=202609171113", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609171125", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609171113", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609171125", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171113", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171125", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171113", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171125", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index 9c89d7bb..cf5a89ce 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -763,3 +763,85 @@ font-size: .74rem; letter-spacing: normal; color: var(--text-still); font-variant-numeric: tabular-nums; } + +/* ===================================================================== + DIE WUNSCHLISTE ALS RANGFOLGE (17.09.2026) + + Aus dem Plan im Vault, Stufe 3. Eine Wunschliste ist keine Liste — + sie ist eine Rangfolge mit einem Zustand. Ohne beides ist sie ein + Briefkasten ohne Postbote: Nach dem dritten unbeantworteten Wunsch + schreibt niemand mehr. + ------------------------------------------------------------------- */ + +/* DER RANGBALKEN. Er zeigt den ABSTAND zum meistgewünschten, nicht die + absolute Zahl — die steht ohnehin auf dem Knopf daneben. Ein Wunsch + mit 12 Stimmen neben einem mit 14 sieht sonst aus wie einer mit 1 + neben einem mit 40. + + Drei Pixel hoch und ohne Beschriftung: Er ist ein Hinweis, kein + Diagramm. Wer die Zahl will, liest sie. */ +.wunsch-rang { + height: 3px; margin: 10px 0 2px; + border-radius: 999px; + background: rgba(255, 255, 255, .05); + overflow: hidden; +} +.wunsch-rang__fuellung { + height: 100%; border-radius: inherit; + background: linear-gradient(90deg, + color-mix(in srgb, var(--akzent) 70%, transparent), + color-mix(in srgb, var(--akzent) 30%, transparent)); + transition: width .3s ease; +} +/* Der meistgewünschte bekommt etwas mehr — aber nur etwas. Er ist der + erste unter Gleichen, nicht der Gewinner. */ +.wunsch-rang[data-spitze="ja"] .wunsch-rang__fuellung { + background: linear-gradient(90deg, var(--akzent), + color-mix(in srgb, var(--akzent) 45%, transparent)); +} +@media (prefers-reduced-motion: reduce) { + .wunsch-rang__fuellung { transition: none; } +} + +/* DER ZUSTAND. Er steht im Kopf neben dem Titel, weil er das ist, + weswegen jemand wiederkommt. + + „Diesmal nicht" ist bewusst NICHT rot. Ein Nein ist eine Antwort und + keine Strafe — und wer ein rotes Schild an seinem Wunsch sieht, + schreibt keinen zweiten. */ +.wunsch-stand { + --w-ton: #6f89a8; + display: inline-flex; align-items: center; + padding: 2px 9px; border-radius: 999px; + font-size: .74rem; font-weight: 600; white-space: nowrap; + color: color-mix(in srgb, var(--w-ton) 72%, #ffffff); + background: color-mix(in srgb, var(--w-ton) 14%, transparent); + border: 1px solid color-mix(in srgb, var(--w-ton) 32%, transparent); +} +.wunsch-stand[data-stand="offen"] { --w-ton: #6f89a8; } +.wunsch-stand[data-stand="angenommen"] { --w-ton: #5f93c4; } +.wunsch-stand[data-stand="erledigt"] { --w-ton: #4dab77; } +.wunsch-stand[data-stand="abgelehnt"] { --w-ton: #8e8577; } + +/* DIE ENTSCHEIDUNG — vier Knöpfe statt eines Umschalters. Der aktuelle + Zustand ist abgeschaltet: Ein Knopf, der nichts tut, gehört nicht + anfassbar auszusehen. */ +.wunsch-wahl { display: flex; flex-wrap: wrap; gap: 6px; } +.wunsch-wahl__knopf { + font: inherit; font-size: .78rem; font-weight: 600; + padding: 6px 12px; border-radius: 999px; cursor: pointer; + color: var(--text-leise); + background: transparent; + border: 1px solid var(--rand-hell); + transition: color var(--tempo), border-color var(--tempo), background var(--tempo); +} +.wunsch-wahl__knopf:hover:not(:disabled) { + color: var(--text); border-color: var(--rand); + background: rgba(255, 255, 255, .04); +} +.wunsch-wahl__knopf[aria-pressed="true"] { + color: var(--text); + border-color: color-mix(in srgb, var(--akzent) 50%, transparent); + background: color-mix(in srgb, var(--akzent) 12%, transparent); + cursor: default; +} diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index d4b57003..62d4384b 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -47,6 +47,12 @@ return true; }; + /* DIE HOECHSTE STIMMENZAHL DER LISTE. Sie ist der Massstab fuer den + Balken: Ein Wunsch mit 12 Stimmen neben einem mit 14 sieht sonst + genauso aus wie einer mit 1 neben einem mit 40. Der Balken zeigt + den ABSTAND, nicht die absolute Zahl -- die steht ohnehin daneben. */ + let maxStimmen = 0; + const $ = (id) => document.getElementById(id); const UNTERZEILE = { live: 'Vorbereitung, Mitschrift und Auswertung – LIVE systematisch verbessern.', @@ -299,6 +305,17 @@ } kopf.append(el('h3', 'eintrag-titel', e.titel)); + /* DER ZUSTAND EINES WUNSCHES steht im Kopf, nicht im Kleingedruckten + (17.09.2026). Er ist das, weswegen jemand wiederkommt: Ein Wunsch, + bei dem nie etwas passiert, ist ein Briefkasten ohne Postbote. */ + if (einstellung?.zustaende && e.status) { + const name = einstellung.zustaende[e.status] || e.status; + const wann = e.geplant ? ` · ${datum(String(e.geplant).slice(0, 10))}` : ''; + const z = el('span', 'wunsch-stand', name + wann); + z.dataset.stand = e.status; + kopf.append(z); + } + if (einstellung.bewertung && e.bewertung) { const b = el('span', 'bewertung'); b.setAttribute('aria-label', `Bewertung ${e.bewertung} von 5`); @@ -311,6 +328,26 @@ } k.append(kopf); + /* DER RANGBALKEN. Eine Wunschliste ist eine Rangfolge, keine Liste + -- und eine Rangfolge sieht man erst, wenn der Abstand sichtbar + ist. Die Zahl allein beantwortet "wie viele", der Balken "wie + viel mehr als die anderen". */ + if (bereich === 'wunsch' && maxStimmen > 0) { + const n = Number(e.stimmen) || 0; + const balken = el('div', 'wunsch-rang'); + const fuell = el('div', 'wunsch-rang__fuellung'); + fuell.style.width = `${Math.round((n / maxStimmen) * 100)}%`; + if (n === maxStimmen && n > 0) balken.dataset.spitze = 'ja'; + balken.append(fuell); + /* Fuer Vorleseprogramme eine SPRACHE, kein Balken. */ + balken.setAttribute('role', 'img'); + balken.setAttribute('aria-label', + n === 0 ? 'Noch niemand will das auch' + : `${n} ${n === 1 ? 'Person will' : 'Personen wollen'} das auch` + + (n === maxStimmen ? ', das ist der meistgewünschte' : '')); + k.append(balken); + } + /* ---- Agentur-Event: Zeitraum, Aufgaben, Regeln ------------------- Die Reihenfolge ist nicht beliebig. Wer auf ein Event schaut, fragt in genau dieser Folge: Laeuft das ueberhaupt noch? Was @@ -525,6 +562,41 @@ if (!darfEintragen()) { k.append(fuss); return k; } const knoepfe = el('div', 'eintrag-knoepfe'); + + /* VIER ZUSTAENDE STATT EINES UMSCHALTERS (17.09.2026). + + "erledigt / wieder offen" reicht fuer einen Vorfall. Ein Wunsch + braucht mehr: Er wird angenommen, bekommt einen Tag, wird gemacht + -- oder eben nicht. "Diesmal nicht" gehoert ausdruecklich dazu: + Ein Nein ist eine Antwort, Schweigen ist keine. */ + if (einstellung?.zustaende) { + const reihe = el('div', 'wunsch-wahl'); + reihe.setAttribute('role', 'group'); + reihe.setAttribute('aria-label', 'Zustand setzen'); + for (const [wert, name] of Object.entries(einstellung.zustaende)) { + const b = el('button', 'wunsch-wahl__knopf', name); + b.type = 'button'; + b.dataset.stand = wert; + b.setAttribute('aria-pressed', String(e.status === wert)); + if (e.status === wert) b.disabled = true; + b.addEventListener('click', async () => { + b.disabled = true; + const a = await hole(`/workspace/api/bereich/${bereich}/${e.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: wert }), + }); + if (!a.ok) { melde('Ging nicht.'); b.disabled = false; return; } + await laden(); + }); + reihe.append(b); + } + knoepfe.append(reihe); + k.append(knoepfe); + k.append(fuss); + return k; + } + knoepfe.append(tuKnopf(e.status === 'offen' ? 'erledigt ▶' : '◀ wieder offen', async () => { const a = await hole(`/workspace/api/bereich/${bereich}/${e.id}`, { method: 'PATCH', @@ -681,6 +753,8 @@ offene" stellt, will trotzdem wissen, wann der naechste Stream ist. */ countdownZeigen(daten.eintraege); + maxStimmen = (daten.eintraege || []) + .reduce((n, e) => Math.max(n, Number(e.stimmen) || 0), 0); if (!sichtbar.length) { ziel.append(el('p', 'leer-hinweis', daten.eintraege.length diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index a078bbd7..6e4b04b2 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 3b382077..cfa8df7f 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 0956f5d7..72f1ac40 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 b36866fb..15050550 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. --> - - - - - - - + + + + + + + - + @@ -194,14 +194,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 58d16c41..decc1226 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 6febeaea..d4cf59ff 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 58c678e3..33390e7e 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 8af358d6..8afa6a10 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 dd638b15..3a9f12c7 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=202609171113", + "src": "/assets/img/app-symbole/crew-192.png?v=202609171125", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609171113", + "src": "/assets/img/app-symbole/crew-512.png?v=202609171125", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171113", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171125", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171113", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171125", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 547e0234..d0380af3 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 2dbb0c6b..8bc8a430 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 5d24ad61..84bddcb3 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 0df92ff8..58cd0abc 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 25fdd561..2ba1116f 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 1fc1f2ac..96d4efed 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 f6274ef8..09fb644e 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 dd1c38d5..3658d1c1 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 918c07fc..e8b49a87 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 81354fa3..553deffd 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 bc6b59db..60e8fc17 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 7b23d233..66bd600d 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 5c37dd28..83f6353b 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 ce651fe4..22d8b253 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 4ae1837f..936a430c 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 565a8613..c9d5580c 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 ae5b7678..26c461c4 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -110,9 +110,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index a01838f3..579868d3 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 7806a1c7..4ba7b687 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 05ae7d11..5157e9db 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 @@ - - - - - + + + + +