diff --git a/server/pruef-terminregel.mjs b/server/pruef-terminregel.mjs new file mode 100644 index 00000000..fc49bf6c --- /dev/null +++ b/server/pruef-terminregel.mjs @@ -0,0 +1,355 @@ +/* ===================================================================== + ZWEI WEGE, EINE ANTWORT (17.09.2026) + + „Wann hat ein Zettel einen Termin?" wird an zwei Stellen gefragt: + + Der KALENDER fragt „welche gehoeren hinein?" -- als SQL, ueber die + ganze Tabelle auf einmal. + Die BRETTKARTE fragt „stehe ICH drin?" -- als JavaScript, je Zeile. + + Beide muessen dasselbe antworten. Tun sie es nicht, behauptet die + Karte „steht im Kalender" und der Kalender zeigt nichts -- oder + umgekehrt. Eine Anzeige, die etwas Falsches sagt, ist schlimmer als + gar keine: Man glaubt ihr ja. + + Dass zwei Fassungen derselben Regel auseinanderlaufen, ist im Haus + keine Theorie. Es ist dreimal passiert: + 11.09. eine abgeschriebene Spaltenliste warf drei Spalten samt + Inhalt weg -- ohne Fehlermeldung, bei gleicher Zeilenzahl + im Kalender eine zweite Artenliste neben ARTNAME -- ein BigMatch + liess sich anlegen und war danach unsichtbar + 17.09. `.teilen` fuer zwei verschiedene Dinge + + Deshalb stehen beide Fassungen in EINER Datei (workspace-termin- + regel.js), und deshalb prueft diese Datei sie GEGENEINANDER -- nicht + jede fuer sich. Zwei Pruefungen, die je eine Seite bestaetigen, + finden genau diesen Fehler nicht. + + Aufrufen mit: node server/pruef-terminregel.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"; +import { tagLokal } from "./helfer-tag.mjs"; + +const PORT = await portMussFreiSein(4192, "pruef-terminregel"); +const ordner = mkdtempSync(join(tmpdir(), "ws-terminregel-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +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-terminregel"); +await new Promise((r) => setTimeout(r, 800)); + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); + +let fehler = 0, geprueft = 0, offen = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +const unklar = (t) => { offen++; console.log(" -- " + t); }; + +const HEUTE = tagLokal(0); +const MORGEN = tagLokal(1); +const IN5 = tagLokal(5); +const GESTERN = tagLokal(-1); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +const regel = await import("./workspace-termin-regel.js"); + +/* ======================================================================= + DIE FAELLE + + Jede Zeile beantwortet EINE Frage, und die Haelfte davon ist die + Frage, ob etwas ZU RECHT wegbleibt. Eine Pruefung nur mit Treffern + koennte auch dann gruen sein, wenn die Regel alles hereinlaesst. + ======================================================================= */ +const FAELLE = [ + // [name, bereich, datum, uhrzeit, geplant, ende, status, soll] + ["Zusage", "wunsch", HEUTE, null, IN5, null, "angenommen", true], + ["Zukunft", "content", IN5, null, null, null, "offen", true], + ["Mit Uhrzeit", "live", MORGEN, "20:00", null, null, "offen", true], + ["Zeitraum", "agentur", MORGEN, null, null, IN5, "offen", true], + ["Uhrzeit heute", "live", HEUTE, "21:00", null, null, "offen", true], + ["Nur heute", "ideen", HEUTE, null, null, null, "offen", false], + ["Von gestern", "ideen", GESTERN, null, null, null, "offen", false], + ["Erledigt", "content", IN5, null, null, null, "erledigt", false], + ["Abgelehnt", "wunsch", HEUTE, null, IN5, null, "abgelehnt", false], +]; + +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync("CODE-DOGI-0001", 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("Filipe", "admin", hash, salt, 32768, new Date().toISOString()); + for (const [name, bereich, datum, uhrzeit, geplant, ende, status] of FAELLE) { + d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, uhrzeit, geplant, + event_ende, status, dringlichkeit, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?,?,?,'mittel',?,1)`) + .run(bereich, "sonstiges", name, "Text", datum, uhrzeit, geplant, ende, + status, new Date().toISOString()); + } + d.close(); +} + +function roh(pfad, kopf = {}) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, method: "GET", + headers: { Host: "workspace.dogfather-universe.com", ...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); + a.end(); + }); +} +function post(pfad, koerper) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, method: "POST", + headers: { Host: "workspace.dogfather-universe.com", "Content-Type": "application/json" }, + }, (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); + }); + a.on("error", schief); + a.write(JSON.stringify(koerper)); + a.end(); + }); +} +const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } }; + +const an = await post("/workspace/api/anmelden", { rolle: "admin", code: "CODE-DOGI-0001" }); +const keks = [].concat(an.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); + +/* ======================================================================= + 1. DIE JS-FASSUNG FUER SICH + ======================================================================= */ +melde("=== 1. terminVon() ==="); +for (const [name, , datum, uhrzeit, geplant, ende, status, soll] of FAELLE) { + const r = regel.terminVon({ datum, uhrzeit, geplant, event_ende: ende, status }, HEUTE); + ok(!!r === soll, `${name.padEnd(14)} -> ${r ? `${r.grund} am ${r.tag}` : "kein Termin"}`); +} +{ + /* Die Rangfolge: Eine Zusage schlaegt alles andere. Wer „am 24." + sagt, meint den 24. -- auch wenn der Zettel am 3. entstand und + eine Uhrzeit traegt. */ + const r = regel.terminVon( + { datum: HEUTE, uhrzeit: "20:00", geplant: IN5, event_ende: null, status: "offen" }, HEUTE); + ok(r?.grund === "zugesagt" && r?.tag === IN5, + `die Zusage schlaegt Uhrzeit und Erfassungstag (${r?.grund} am ${r?.tag})`); +} + +/* ======================================================================= + 2. DER KALENDER (SQL) UND DIE KARTE (JS) SAGEN DASSELBE + + Das ist der eigentliche Zweck dieser Datei. + ======================================================================= */ +melde(""); +melde("=== 2. Beide Wege, verglichen ==="); + +const kal = json(await roh("/workspace/api/termine?tage=60", { cookie: keks })); +const imKalender = new Set((kal.brettsachen || []).map((b) => b.titel)); +ok(imKalender.size > 0, `der Kalender liefert ${imKalender.size} Zettel`); + +const bereiche = [...new Set(FAELLE.map((f) => f[1]))]; +const mitMarke = new Set(); +const tagLaut = new Map(); +for (const b of bereiche) { + const r = json(await roh(`/workspace/api/bereich/${b}`, { cookie: keks })); + for (const e of r.eintraege || []) { + if (e.termin) { mitMarke.add(e.titel); tagLaut.set(e.titel, e.termin.tag); } + } +} +ok(mitMarke.size > 0, `die Bretter melden ${mitMarke.size} Zettel mit Termin`); + +{ + const nurKalender = [...imKalender].filter((x) => !mitMarke.has(x)); + const nurKarte = [...mitMarke].filter((x) => !imKalender.has(x)); + ok(nurKalender.length === 0, + nurKalender.length ? `steht im Kalender, aber die Karte sagt nichts: ${nurKalender.join(", ")}` + : "kein Zettel steht im Kalender, ohne dass seine Karte es sagt"); + ok(nurKarte.length === 0, + nurKarte.length ? `die Karte behauptet einen Termin, der Kalender zeigt ihn nicht: ${nurKarte.join(", ")}` + : "und keine Karte behauptet einen Termin, den der Kalender nicht kennt"); +} + +/* UND DER TAG MUSS AUCH DERSELBE SEIN. Beide Seiten einig darueber, + DASS etwas einen Termin hat, und uneinig ueber WANN -- das waere die + unauffaelligste Art, falsch zu liegen. */ +{ + const uneinig = (kal.brettsachen || []) + .filter((b) => tagLaut.has(b.titel) && tagLaut.get(b.titel) !== b.wann) + .map((b) => `${b.titel}: Kalender ${b.wann}, Karte ${tagLaut.get(b.titel)}`); + ok(uneinig.length === 0, + uneinig.length ? `verschiedene Tage: ${uneinig.join(" | ")}` + : `beide nennen denselben Tag (${tagLaut.size} verglichen)`); +} + +/* ======================================================================= + 3. UND GEGEN DIE ERWARTUNG, NICHT NUR GEGENEINANDER + + Zwei Wege koennen sich einig sein und beide falsch liegen. Deshalb + zusaetzlich gegen die Fallliste oben. + ======================================================================= */ +melde(""); +melde("=== 3. Gegen die Erwartung ==="); +for (const [name, , , , , , , soll] of FAELLE) { + ok(imKalender.has(name) === soll, + soll ? `„${name}" steht im Kalender` : `„${name}" bleibt draussen`); +} + +/* ======================================================================= + 4. IM BROWSER + ======================================================================= */ +melde(""); +melde("=== 4. Was man sieht ==="); +{ + let chromium; + try { + ({ chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs")); + } catch { chromium = null; } + + if (!chromium) { + unklar("kein Playwright gefunden -- die Oberflaeche wurde nicht angesehen"); + } else { + const browser = await chromium.launch(); + try { + const sicht = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + await sicht.addCookies(keks.split("; ").map((c) => { + const [name, ...rest] = c.split("="); + return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await sicht.newPage(); + const konsole = []; + seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/bereich.html?b=content`, + { waitUntil: "domcontentloaded" }); + await seite.waitForTimeout(1600); + + /* SICHTBARKEIT, NICHT VORHANDENSEIN. `textContent` liefert auch, + was `display: none` ausblendet -- und am Bildschirmfoto fiel + auf, dass die Marke im Bild fehlte, obwohl die Zaehlung 1 + ergab. Ohne `offsetHeight` waere das nie aufgefallen. */ + const marken = await seite.evaluate(() => + [...document.querySelectorAll(".im-kalender")].map((a) => ({ + text: a.textContent.trim(), ziel: a.getAttribute("href"), + grund: a.dataset.grund, + sichtbar: a.offsetHeight > 0 && a.offsetWidth > 0, + wo: a.closest(".eintrag-karte")?.querySelector("h3, .eintrag-titel")?.textContent + || a.parentElement?.className || "?", + }))); + ok(marken.length > 0, `auf dem Brett steht ${marken.length}x „im Kalender"`); + ok(marken.length > 0 && marken.every((m) => m.sichtbar), + `und man sieht sie auch (${marken.map((m) => `${m.wo}: ${m.sichtbar}`).join(", ")})`); + ok(marken.every((m) => m.ziel === "kalender.html"), + "und jede fuehrt in den Kalender"); + /* GEGENPROBE: NICHT an jeder Karte. „Erledigt" und „von gestern" + liegen auf demselben Brett und duerfen die Marke nicht haben -- + sonst hiesse „steht im Kalender" nur, dass ueberall etwas steht. */ + const karten = await seite.locator(".eintrag-karte").count(); + ok(karten > marken.length, + `aber nicht an allen (${marken.length} von ${karten} Karten)`); + + /* ---- Der Hinweis unter dem Datumsfeld ---- */ + const hinweis = async () => seite.evaluate(() => ({ + text: document.getElementById("hinweis-datum")?.textContent?.trim() || "", + marke: document.getElementById("hinweis-datum")?.dataset.imkalender || "", + })); + /* Der Knopf heisst `neu-oeffnen`, nicht `neu-auf` -- beim ersten + Lauf stand hier der falsche Name samt `.catch(() => {})`, und + der Fehlgriff verschwand lautlos. Ein `.catch`, das einen + Selektorfehler schluckt, macht aus einer kaputten Pruefung eine + gruene: Danach stand im Hinweisfeld "" und die Zeile darunter + war trotzdem in Ordnung, weil "" nun einmal nicht "ja" ist. */ + await seite.click("#neu-oeffnen"); + await seite.waitForTimeout(500); + const h1 = await hinweis(); + ok(h1.text.length > 5, `beim Oeffnen steht ein Satz da ("${h1.text}")`); + /* Die Anzahl gehoert in die BEDINGUNG: Ohne `h1.text` waere diese + Zeile auch bei leerem Feld gruen. */ + ok(h1.text.length > 5 && h1.marke !== "ja", + "und er sagt NICHT, dass es im Kalender landet -- heute ist heute"); + + await seite.fill("#f-datum", IN5); + await seite.waitForTimeout(300); + const h2 = await hinweis(); + ok(h2.text.length > 5 && h2.marke === "ja" && /Kalender/.test(h2.text), + `mit einem Tag in der Zukunft aendert er sich ("${h2.text}")`); + + await seite.fill("#f-datum", HEUTE); + await seite.waitForTimeout(300); + const h3 = await hinweis(); + ok(h3.text.length > 5 && h3.marke !== "ja", + `und zurueck auf heute wieder ("${h3.text}")`); + + /* ---- UND AM HANDY (17.09.2026) ---------------------------------- + + Der Hinweis haengt seit heute AUSSERHALB des Feldes (absolut, + damit er die Eingaben nicht aus der Linie schiebt). Genau das + kann am schmalen Bildschirm das naechste Feld ueberdecken -- + dort stehen die Felder untereinander, und dann liegt der + schwebende Satz auf dem, was folgt. + + Gemessen wird die Ueberschneidung, nicht geschaetzt. */ + await seite.setViewportSize({ width: 390, height: 844 }); + await seite.waitForTimeout(500); + const eng = await seite.evaluate(() => { + const h = document.getElementById("hinweis-datum"); + if (!h || h.offsetHeight === 0) return { da: false }; + const a2 = h.getBoundingClientRect(); + const treffer = [...document.querySelectorAll(".neu__raster > *")] + .filter((f) => !f.contains(h)) + .map((f) => f.getBoundingClientRect()) + .filter((b) => a2.left < b.right && b.left < a2.right + && a2.top < b.bottom && b.top < a2.bottom); + return { da: true, ueberdeckt: treffer.length, hoehe: Math.round(a2.height) }; + }); + ok(eng.da, "am Handy steht der Hinweis auch da"); + ok(eng.da && eng.ueberdeckt === 0, + `und liegt auf keinem anderen Feld (${eng.ueberdeckt} Ueberschneidungen)`); + await seite.setViewportSize({ width: 1280, height: 900 }); + await seite.waitForTimeout(400); + + ok(konsole.length === 0, + konsole.length ? `Konsolenfehler: ${konsole[0]}` : "kein Konsolenfehler"); + + /* EIN AUSSCHNITT DER KARTE, nicht nur die ganze Seite. Auf einem + Vollbild von 2000 px Hoehe ist eine Pille von 28 px nicht zu + beurteilen -- ich habe sie beim ersten Hinsehen schlicht + uebersehen und fuer fehlend gehalten. Ein Bild, auf dem man + nichts erkennen kann, ist keine Kontrolle. */ + const karteMitMarke = seite.locator(".eintrag-karte").filter( + { has: seite.locator(".im-kalender") }).first(); + await karteMitMarke.screenshot({ path: "server/pruef-terminregel-marke.png" }) + .catch(() => {}); + await seite.screenshot({ path: "server/pruef-terminregel.png", fullPage: true }); + melde(" -- Bildschirmfoto: server/pruef-terminregel.png"); + } finally { + await browser.close(); + } + } +} + +/* ------------------------------------------------------------------- */ +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler` + + (offen ? `, ${offen} nicht nachsehbar` : "")); +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 31d6c67a..201e1d16 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -17,6 +17,13 @@ import express from "express"; import { TREFF_START } from "./workspace-treff-start.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 + sie jetzt zwei Verwender hat: Der Kalender fragt "welche gehoeren + hinein?", die Brettkarte fragt "stehe ICH drin?". Zwei Fassungen + derselben Regel laufen auseinander, und dann behauptet die Karte + etwas, das der Kalender nicht zeigt. */ +import { terminVon } from "./workspace-termin-regel.js"; /* EIN KATALOG AUS ZWEI QUELLEN (17.09.2026) @@ -40,6 +47,7 @@ import { externPruefen, externSql, ohneTeamDogi, ohneAgentur, siehtModis, fuehrtTeamDogi, MODI_BEREICHE_ERLAUBT, TEAM_DOGI_ROLLEN, TREFF_BRETTER, TREFF_ROLLEN, AUSSEN_ROLLEN, + heuteLokal, } from "./workspace.js"; import { nachrichtSchicken } from "./workspace-chat.js"; import { @@ -1222,6 +1230,28 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => { ORDER BY ${reihenfolge}`) .all(...(treffBrett ? [wer] : []), ...regel.werte, bereich); + /* STEHT DIESER ZETTEL IM KALENDER? (17.09.2026) + + Der Kalender zeigt seit heute, was auf den Brettern steht und + einen Termin hat. Die Verbindung war aber EINSEITIG: Vom + Kalender kam man aufs Brett, vom Brett erfuhr man nichts. Am + Kartenfuss stand bloss ein Datum -- und "24.09.2026" sieht + genauso aus wie "17.09.2026", obwohl das eine ein Termin ist + und das andere der Tag, an dem jemand getippt hat. + + DIE ANTWORT KOMMT VOM SERVER und wird nicht im Browser + nachgerechnet: Sonst gaebe es eine zweite Fassung der Regel, und + die Karte behauptete irgendwann etwas, das der Kalender nicht + zeigt. Eine Anzeige, die etwas Falsches sagt, ist schlimmer als + keine -- man glaubt ihr ja. + + Kostet keine zusaetzliche Abfrage: Alle vier Felder stehen + ohnehin schon in SPALTEN. */ + { + const heute = heuteLokal(); + for (const e of eintraege) e.termin = terminVon(e, heute); + } + /* DIE BILDER AN DEN BEITRAEGEN (17.09.2026) -- in EINER Abfrage fuer die ganze Liste. Vierzig Beitraege einzeln zu fragen waeren vierzig Anfragen, und die Seite ruckelte sichtbar beim Aufbau; diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index 0a40c5a8..5beb954c 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -30,6 +30,14 @@ import { Aufgabenfristen: nicht durch ein Loch, sondern durch eine zweite Regel, die "istLeitung" fragte statt der richtigen. */ import { sichtbarEintrag, BEREICHE } from "./workspace-bereiche.js"; +/* Die Regel, WANN ein Zettel einen Termin hat -- als eigenes Blatt. + Seit die Brettkarte dasselbe sagt ("steht im Kalender am 24."), + haette sie sonst eine zweite Fassung. Zwei Fassungen derselben Regel + laufen auseinander, und dann zeigt die Karte etwas anderes als der + Kalender. */ +import { + TERMIN_TAG_SQL, TERMIN_BEDINGUNG_SQL, TERMIN_GRUND_SQL, OHNE_ZUSTAENDE, +} from "./workspace-termin-regel.js"; export const kalenderRouter = express.Router(); @@ -448,31 +456,17 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { SELECT e.id, e.bereich, e.art, e.titel, e.text, e.status, e.dringlichkeit, e.datum, e.uhrzeit, e.geplant, e.event_ende, pe.name AS von_name, - /* WELCHER TAG ZAEHLT -- abgeleitet, nicht im Browser - nachgerechnet. Eine Zusage schlaegt das Erfassungsdatum: - Wer sagt "am 24.", meint den 24., auch wenn der Zettel - am 3. entstanden ist. */ - COALESCE(e.geplant, e.datum) AS wann, - CASE - WHEN e.geplant IS NOT NULL THEN 'zugesagt' - WHEN e.event_ende IS NOT NULL THEN 'zeitraum' - WHEN e.uhrzeit IS NOT NULL THEN 'uhrzeit' - ELSE 'termin' - END AS grund + ${TERMIN_TAG_SQL} AS wann, + ${TERMIN_GRUND_SQL} AS grund FROM eintraege e LEFT JOIN personen pe ON pe.id = e.erstellt_von WHERE ${eintragRegel.wo} - AND e.status NOT IN ('erledigt', 'abgelehnt') - AND ( - e.geplant IS NOT NULL - OR e.event_ende IS NOT NULL - OR e.uhrzeit IS NOT NULL - OR e.datum > ? - ) - AND COALESCE(e.geplant, e.datum) >= ? - AND COALESCE(e.geplant, e.datum) <= ? + AND e.status NOT IN (${OHNE_ZUSTAENDE.map(() => "?").join(", ")}) + AND ${TERMIN_BEDINGUNG_SQL} + AND ${TERMIN_TAG_SQL} >= ? + AND ${TERMIN_TAG_SQL} <= ? ORDER BY wann, e.uhrzeit, e.id`) - .all(...eintragRegel.werte, heuteLokal(), von, bis) : []; + .all(...eintragRegel.werte, ...OHNE_ZUSTAENDE, heuteLokal(), von, bis) : []; /* DER NAME DES BRETTS KOMMT MIT -- und zwar aus dem Katalog, nicht als abgeschriebene Liste im Browser. Ein Brett, das morgen diff --git a/server/workspace-termin-regel.js b/server/workspace-termin-regel.js new file mode 100644 index 00000000..96c36067 --- /dev/null +++ b/server/workspace-termin-regel.js @@ -0,0 +1,109 @@ +/* ===================================================================== + WANN EIN ZETTEL EINEN TERMIN HAT (17.09.2026) + + EINE Regel, zwei Verwender -- und deshalb eine eigene Datei. + + Seit heute zeigt der Kalender, was auf den Brettern steht und einen + Termin hat. Die Bedingung dafuer stand als SQL mitten in + workspace-kalender.js. Sobald die Brettkarte dasselbe sagen soll + ("steht im Kalender am 24."), braeuchte sie eine zweite Fassung -- + und zwei Fassungen derselben Regel laufen auseinander. Nicht sofort, + sondern beim naechsten Nachschaerfen, und dann zeigt die Karte etwas + anderes als der Kalender. Das ist schlimmer als beides wegzulassen: + Man glaubt der Anzeige ja. + + Dieselbe Krankheit gab es im Haus schon zweimal -- die abgeschriebene + Spaltenliste (11.09., drei Spalten samt Inhalt verloren) und die + zweite Artenliste neben ARTNAME (im Kalender, ein BigMatch liess + sich anlegen und war danach unsichtbar). + + DIE DATEI IMPORTIERT NICHTS. Muster wie treff-tabellen.js und + workspace-bewerbung-rollen.js: Ein Blatt ohne eigene Importe kann von + ueberall gelesen werden, ohne einen Ring zu bauen -- und ein Ring + aeussert sich in JavaScript nicht als Fehler, sondern als + `undefined` an einer Stelle, an der niemand damit rechnet. + + ------------------------------------------------------------------- + WARUM GENAU DIESE VIER FAELLE + + `eintraege.datum` ist ein PFLICHTFELD und faellt auf HEUTE zurueck, + wenn niemand eins waehlt. Alle Eintraege in den Kalender zu nehmen + hiesse also: jeder je getippte Zettel steht an dem Tag, an dem ihn + jemand getippt hat. Nach einem Monat waere der Kalender ein + Protokoll und kein Plan -- und einer, in dem alles steht, sagt + nichts mehr. + + geplant Eine Zusage: "Wird gemacht -- am 24.09." Das ist ein + Termin, den jemand jemandem gegeben hat. + event_ende Ein Zeitraum. Wer ein Ende eintraegt, meint eine + Veranstaltung, keine Notiz. + uhrzeit Wer eine Uhrzeit tippt, meint einen Zeitpunkt. + Niemand traegt aus Versehen 20:00 ein. + datum > heute Der Rueckfall ist IMMER heute oder frueher. Ein Datum + in der Zukunft kann nur Absicht sein. + + Der letzte Fall ist der wichtigste und der unauffaelligste: Er + braucht kein neues Feld und keine Umgewoehnung. + ===================================================================== */ + +/** Der massgebliche Tag eines Eintrags -- als SQL-Ausdruck. + * + * Eine Zusage schlaegt das Erfassungsdatum: Wer "am 24." sagt, meint + * den 24., auch wenn der Zettel am 3. entstanden ist. */ +export const TERMIN_TAG_SQL = "COALESCE(e.geplant, e.datum)"; + +/** Die Bedingung als SQL. Erwartet GENAU EINEN Platzhalter: den + * heutigen Tag in Ortszeit (`heuteLokal()`). + * + * UTC waere hier falsch: Zwischen Mitternacht und zwei Uhr morgens + * liefert es in Deutschland den Vortag, und ein Zettel von heute + * staende dann ploetzlich im Kalender. */ +export const TERMIN_BEDINGUNG_SQL = `( + e.geplant IS NOT NULL + OR e.event_ende IS NOT NULL + OR e.uhrzeit IS NOT NULL + OR e.datum > ? +)`; + +/** Warum er drinsteht -- als SQL-Ausdruck. Die Reihenfolge ist die + * Rangfolge: Eine Zusage ist mehr als ein Zeitraum, ein Zeitraum mehr + * als eine Uhrzeit. */ +export const TERMIN_GRUND_SQL = `CASE + WHEN e.geplant IS NOT NULL THEN 'zugesagt' + WHEN e.event_ende IS NOT NULL THEN 'zeitraum' + WHEN e.uhrzeit IS NOT NULL THEN 'uhrzeit' + ELSE 'termin' +END`; + +/** Welche Zustaende NICHT in den Kalender gehoeren. + * + * Ein Kalender zeigt, was ansteht. Was gemacht oder abgelehnt ist, + * gehoert auf sein Brett -- nicht in die Planung der naechsten Woche. */ +export const OHNE_ZUSTAENDE = ["erledigt", "abgelehnt"]; + +/** Dieselbe Frage in JavaScript -- fuer einen einzelnen Eintrag. + * + * Sie wird beim Ausliefern der Brettkarten gebraucht: Dort steht seit + * heute "im Kalender am 24.", und diese Auskunft MUSS dieselbe sein + * wie die des Kalenders. Beide Fassungen stehen deshalb in dieser + * Datei nebeneinander, damit eine Aenderung an der einen die andere + * ins Auge springen laesst -- und pruef-terminregel.mjs weist nach, + * dass sie fuer dieselben Zeilen dasselbe sagen. + * + * @param e Zeile aus `eintraege` (mit geplant, event_ende, uhrzeit, + * datum, status) + * @param heute Heutiger Tag in Ortszeit, "JJJJ-MM-TT" + * @returns {null|{tag, grund}} */ +export function terminVon(e, heute) { + if (!e) return null; + if (OHNE_ZUSTAENDE.includes(e.status)) return null; + + const grund = e.geplant ? "zugesagt" + : e.event_ende ? "zeitraum" + : e.uhrzeit ? "uhrzeit" + : (e.datum && heute && e.datum > heute) ? "termin" + : null; + if (!grund) return null; + + return { tag: e.geplant || e.datum, grund }; +} diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index abeaaa2f..b3ce9357 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609172248", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609172352", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609172248", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609172352", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609172248", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609172352", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609172248", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609172352", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css index 630f9b58..e0f4543f 100644 --- a/workspace/assets/css/aufgaben.css +++ b/workspace/assets/css/aufgaben.css @@ -40,6 +40,11 @@ die Beschriftung darueber ein- oder zweizeilig ist. Ohne das sitzt ein Feld mit langer Beschriftung tiefer als seine Nachbarn. */ .neu__raster { align-items: stretch; } +/* ZEILENABSTAND FUER DEN HINWEIS. Er haengt ausserhalb des Feldes und + braucht Luft nach unten -- sonst liegt er auf dem naechsten Feld, + und zwar genau dann, wenn die Felder untereinander stehen (schmaler + Bildschirm). Der Spaltenabstand bleibt unveraendert. */ +.neu__raster { row-gap: 30px; } /* Zwei Zeilen je Feld: Die Beschriftung oben DEHNT sich (1fr), das Eingabefeld unten hat feste Hoehe. Dadurch sitzen alle Eingabefelder auf exakt derselben Linie -- unabhaengig davon, ob die Beschriftung @@ -55,7 +60,32 @@ hoeher. Gemessen: Container beider Felder 652+71 identisch, Eingabe aber 676..720 gegen 679..723. Drei Pixel -- genau das, was man als "verzogen" sieht. */ -.neu__raster > * { display: grid; grid-template-rows: 1fr 44px; } +.neu__raster > * { display: grid; grid-template-rows: 1fr 44px; position: relative; } + +/* EIN HINWEIS UNTER EINEM FELD HAENGT UNTER DEM RASTER, NICHT DARIN + (17.09.2026). + + Die zwei Rasterzeilen darueber sind der ganze Grund, warum die + Eingaben auf einer Linie sitzen. Ein dritter Absatz im Feld erzeugt + eine dritte Zeile -- und weil das Raster alle Felder auf gleiche + Hoehe dehnt, rutscht dann ausgerechnet DIESE Eingabe nach oben. + Gemessen am 17.09.: 328..372 gegen 351..395 bei den Nachbarn. 23 px, + und genau das sieht man als "verzogen". + + Der Hinweis wird deshalb aus dem Fluss genommen. Er beschreibt das + Feld darueber und braucht dessen Breite -- deshalb absolut IM Feld + und nicht als eigenes Rasterkind, das in der naechsten Zeile ganz + links landen wuerde. + + `pointer-events: none`: Er ist Text, kein Ziel. Ohne das faengt er + Klicks ab, die dem Element darunter galten -- und niemand versteht, + warum ein Knopf manchmal nicht reagiert. */ +.neu__raster > * > .feld-hinweis { + position: absolute; + top: 100%; left: 0; right: 0; + margin: 4px 0 0; + pointer-events: none; +} .neu__raster > * > .feld-schild { align-self: end; margin-bottom: 6px; } .neu input, .neu select { height: 44px; box-sizing: border-box; } /* Die Zeile ganz ausfuellen -- sonst zentriert der Browser das Element diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index 5bcb0ec2..caaa1993 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -1327,3 +1327,53 @@ @media (prefers-reduced-motion: reduce) { .tl-brett { transition: none; } } + +/* ===================================================================== + „STEHT IM KALENDER" (17.09.2026) + + Die Verbindung zwischen Brett und Kalender war einseitig: Vom + Kalender kam man auf das Brett, zurueck nicht. Am Kartenfuss stand + bloss ein Datum -- und „24.09.2026" sieht aus wie „17.09.2026", + obwohl das eine ein Termin ist und das andere der Tag, an dem + jemand getippt hat. + + ES IST EIN LINK UND KEINE MARKE. Wer liest „steht im Kalender", will + als Naechstes wissen, was sonst noch an dem Tag ist. + ===================================================================== */ +.im-kalender { + display: inline-flex; align-items: center; gap: 7px; + margin-top: 10px; + padding: 5px 11px; + border: 1px solid color-mix(in srgb, var(--akzent) 34%, var(--rand)); + border-radius: 999px; + /* Undurchsichtig gemischt, nicht mit `transparent`: Sonst scheint das + Wasserzeichen der Seite durch den Text. Am 17.09. genau so an einer + vertraulichen Bewerbung aufgefallen. */ + background: color-mix(in srgb, var(--tinte) 90%, #ffffff 10%); + color: var(--text-leise); + font-size: .78rem; font-weight: 600; + text-decoration: none; + transition: border-color .16s ease, color .16s ease; +} +.im-kalender:hover { border-color: var(--akzent); color: var(--text); } +.im-kalender:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } + +.im-kalender__zeichen { font-size: .9rem; line-height: 1; } + +/* EINE ZUSAGE SIEHT ANDERS AUS ALS EIN EIGENER TERMIN. „Wird gemacht -- + am 24." ist ein Versprechen an jemanden; ein selbstgesetztes Datum + ist eine Notiz an sich selbst. Unterschieden wird ueber das WORT + („Zugesagt für") und zusaetzlich ueber den Rand -- eine Auskunft, + die nur aus einer Farbe besteht, kommt bei niemandem an, der Farben + schlecht unterscheidet. */ +.im-kalender[data-grund="zugesagt"] { + border-color: color-mix(in srgb, var(--gut, #79d1a2) 48%, var(--rand)); + color: color-mix(in srgb, var(--gut, #79d1a2) 30%, #ffffff); +} + +/* Der Hinweis unter dem Datumsfeld. Wenn er „steht damit im Kalender" + sagt, darf man das sehen -- sonst liest man ihn nicht. */ +#hinweis-datum[data-imkalender="ja"] { + color: color-mix(in srgb, var(--akzent) 42%, #ffffff); +} + diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index b100f716..3abd1a35 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -281,6 +281,40 @@ /* ---------- Zeichnen --------------------------------------------------- */ + /** Sagt unter dem Datumsfeld, ob dieser Tag im Kalender landet. + * + * DIESELBE REGEL WIE AUF DEM SERVER -- aber bewusst nur die eine + * Haelfte, die der Browser ueberhaupt beantworten kann: Liegt der + * Tag in der Zukunft? Ob am Ende `geplant`, `uhrzeit` oder + * `event_ende` gesetzt sind, entscheidet der Server, und die Karte + * zeigt danach sein Ergebnis. Der Hinweis hier ist eine Vorschau, + * keine zweite Wahrheit -- deshalb steht er im Konjunktiv-Ton + * („steht damit im Kalender") und nicht als Zusage. */ + function kalenderhinweisSetzen() { + const feld = $('f-datum'); + const ziel = $('hinweis-datum'); + if (!feld || !ziel) return; + const wert = feld.value; + /* Denselben Rueckfall wie an den zwei Stellen darueber: `heuteLokal` + kommt aus kopf.js, und wenn das noch nicht geladen ist, waere ein + direkter Aufruf ein Absturz mitten im Formularaufbau. */ + const heute = window.heuteLokal ? window.heuteLokal() + : new Date().toISOString().slice(0, 10); + if (!wert) { ziel.textContent = ''; return; } + if (wert > heute) { + ziel.textContent = 'Liegt in der Zukunft – steht damit im Kalender.'; + ziel.dataset.imkalender = 'ja'; + } else if ($('f-uhrzeit') && !$('feld-uhrzeit').hidden && $('f-uhrzeit').value) { + ziel.textContent = 'Mit Uhrzeit – steht damit im Kalender.'; + ziel.dataset.imkalender = 'ja'; + } else { + ziel.textContent = wert === heute + ? 'Heute – das ist der Tag der Notiz, kein Termin.' + : 'Liegt in der Vergangenheit – kein Termin.'; + delete ziel.dataset.imkalender; + } + } + function karte(e) { const k = el('article', 'eintrag-karte'); k.dataset.dringlich = e.dringlichkeit; @@ -507,10 +541,41 @@ } } + /* ---- STEHT IM KALENDER (17.09.2026) ------------------------------ + + Der Kalender zeigt seit heute, was auf den Brettern steht und + einen Termin hat. Die Verbindung war einseitig: vom Kalender + aufs Brett ja, zurueck nicht. Am Kartenfuss stand bloss ein + Datum -- und „24.09.2026" sieht aus wie „17.09.2026", obwohl das + eine ein Termin ist und das andere der Tag, an dem jemand + getippt hat. + + OB ER DRINSTEHT, SAGT DER SERVER (`e.termin`). Hier wird es + nicht nachgerechnet: Eine zweite Fassung der Regel behauptet + irgendwann etwas, das der Kalender nicht zeigt -- und eine + Anzeige, die etwas Falsches sagt, ist schlimmer als keine. + + ES IST EIN LINK, keine Marke. Wer liest „steht im Kalender", + will als Naechstes nachsehen, was noch an dem Tag ist. */ + if (e.termin) { + const zeile = el('a', 'im-kalender'); + zeile.dataset.grund = e.termin.grund; + zeile.href = 'kalender.html'; + zeile.append(el('span', 'im-kalender__zeichen', '\u{1F5D3}')); + zeile.append(el('span', 'im-kalender__text', + (e.termin.grund === 'zugesagt' ? 'Zugesagt für ' : 'Im Kalender am ') + + datum(e.termin.tag) + + (e.uhrzeit ? ' um ' + e.uhrzeit : ''))); + k.append(zeile); + } + const fuss = el('div', 'eintrag-fuss'); /* Beim Event steht der Zeitraum schon oben -- das Datum hier waere - dieselbe Zahl ein zweites Mal. */ - if (!stand) fuss.append(el('span', null, datum(e.datum))); + dieselbe Zahl ein zweites Mal. Und steht der Zettel im Kalender, + steht der Tag ebenfalls schon da; dann waere das Erfassungsdatum + hier eine ZWEITE Zahl neben einer wichtigeren -- und die + verwirrt mehr, als sie sagt. */ + if (!stand && !e.termin) fuss.append(el('span', null, datum(e.datum))); if (LEITUNG.has(ich.rolle) && e.creator_name) fuss.append(el('span', null, '· ' + e.creator_name)); if (e.erstellt_name) fuss.append(el('span', null, '· von ' + e.erstellt_name)); if (e.status === 'erledigt') fuss.append(el('span', null, '· erledigt')); @@ -1408,6 +1473,19 @@ if (einstellung?.vertraulich) $('feld-vertraulich').hidden = event; $('schild-datum').textContent = event ? 'Von' : 'Datum'; + /* WAS DAS DATUM BEWIRKT, STEHT DARUNTER (17.09.2026). + + Das Feld ist mit HEUTE vorbelegt und heisst schlicht „Datum". + Damit war die Kalenderfunktion zwar vorhanden, aber unauffindbar: + Niemand konnte wissen, dass ein Tag in der Zukunft den Zettel in + den Kalender bringt. Eine Funktion, die niemand findet, ist + keine. + + Der Satz aendert sich MIT DER EINGABE, statt allgemein zu + bleiben -- ein fester Hinweis („kann im Kalender erscheinen") + beantwortet die Frage nicht, die man gerade hat: „Steht der + jetzt drin oder nicht?" */ + kalenderhinweisSetzen(); $('schild-titel').textContent = event ? 'Titel des Events' : 'Überschrift'; /* textContent wuerde das mit erschlagen -- deshalb nur der erste Textknoten, das kleine "(optional)" bleibt @@ -1439,6 +1517,7 @@ }; $('neu-oeffnen').addEventListener('click', () => { $('f-datum').value = window.heuteLokal(); + kalenderhinweisSetzen(); formularAnpassen(); umschalten(true); }); @@ -1450,6 +1529,15 @@ am urspruenglichen + +

- + @@ -107,10 +107,10 @@ - - - - - + + + + + diff --git a/workspace/bewerbungen.html b/workspace/bewerbungen.html index f31140a9..5855b1ce 100644 --- a/workspace/bewerbungen.html +++ b/workspace/bewerbungen.html @@ -7,16 +7,16 @@ - - - - - - - - + + + + + + + + - + @@ -80,10 +80,10 @@ - - - - - + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 981c6c12..138f79ec 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 653c7f5d..a259ca6d 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 9d22353a..c1db5f51 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 be77c18d..11af162a 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 07eea0b3..0d5f40e4 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609172248", + "src": "/assets/img/app-symbole/crew-192.png?v=202609172352", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609172248", + "src": "/assets/img/app-symbole/crew-512.png?v=202609172352", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609172248", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609172352", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609172248", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609172352", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 4e64eeb1..d94a598d 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 079940b9..f062f18a 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 e04e7845..a093cd9d 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 bef86d9a..b9342f5e 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 71b94717..1e7f39e3 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 49f1488e..45105e8d 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 a3ec5bc9..a0292f5e 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 fef0366a..fba52113 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 1fcbd9e9..57cbf681 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 9610c72e..bb951278 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 af21ecc7..1ce0be22 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 e60f51ec..a53eded3 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 726525a7..db965b28 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 daa3a554..46f4897c 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 9a9bb053..b0a26483 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 b1c05b28..6afa7ec7 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/teilen.html b/workspace/teilen.html index f6ea830e..f243cad7 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 6f316c37..2fba8bcd 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 5a3489a7..a7c5782b 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 e227620f..18dd6b68 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 f6854038..3f369355 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 @@ - - - - - + + + + +