/* ===================================================================== pruef-zeitraum.mjs — Ein Termin von–bis belegt jeden Tag dazwischen Filipe, 20.09.2026: „wenn ich termine eintrage mit von bis, will ich dass die ganze zeit markiert ist und nicht nur der anfang." --------------------------------------------------------------------- ZWEI FEHLER, NICHT EINER Der sichtbare: Die Kachel stand nur am Anfangstag. Wer im Monatsraster auf den Mittwoch sah, sah nichts — obwohl die Aktion von Montag bis Freitag lief. Der unsichtbare, und der ist der schlimmere: Der SERVER suchte Einträge, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion vom 28.09. bis zum 05.10. kam im Oktober deshalb überhaupt nicht an — nicht „nur am ersten Tag markiert", sondern gar nicht da. Wer im Oktober plante, sah eine freie Woche, die belegt war. Deshalb wird hier BEIDES gemessen, und der zweite Fall zuerst. AUFRUF: node server/pruef-zeitraum.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { portMussFreiSein } from "./helfer-port.mjs"; import { notbremse } from "./helfer-notbremse.mjs"; const PORT = await portMussFreiSein(4437, "pruef-zeitraum"); const ordner = mkdtempSync(join(tmpdir(), "ws-zeitraum-")); 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 }); }; await import("./index.js"); notbremse(230_000, "pruef-zeitraum"); await new Promise((r) => setTimeout(r, 900)); 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 ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; console.log("\n=== Termine über mehrere Tage ===\n"); /* ---- Daten anlegen ------------------------------------------------ RELATIV ZU HEUTE, nie mit einem festen Datum: Ein fest eingetragener Termin wird vom Kalender eingeholt, und dann schlägt die Prüfung fehl, ohne dass etwas kaputt ist (Hausregel, 06.09.2026). */ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); 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()); const tagPlus = (n) => { const x = new Date(); x.setHours(12, 0, 0, 0); x.setDate(x.getDate() + n); return x.toISOString().slice(0, 10); }; /* Eine Aktion über fünf Tage, die GESTERN angefangen hat — genau der Fall, den der Server vorher verschluckt hat. */ const LAUF_START = tagPlus(-1), LAUF_ENDE = tagPlus(3); /* Und ein Punkttermin als Gegenprobe. */ const EINZEL = tagPlus(5); const spalten = new Set(d.prepare("PRAGMA table_info(eintraege)").all().map((z) => z.name)); ok(spalten.has("event_ende") && spalten.has("geplant"), "die Tabelle kennt Anfang und Ende (event_ende, geplant)"); const einf = (titel, von, bis) => d.prepare( "INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit," + " datum, geplant, event_ende, erstellt_von, erstellt)" + " VALUES (?,?,?,?,?,?,?,?,?,?,?)") .run("live", "idee", titel, "", "offen", "mittel", von, von, bis, 1, new Date().toISOString()); einf("Wochenaktion quer", LAUF_START, LAUF_ENDE); einf("Nur ein Tag", EINZEL, null); d.close(); const an = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json", Origin: BASIS }, body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }), }); const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); ok(an.ok && !!keks, "angemeldet"); /* ---- 1. DER SERVER: Überschneidung statt Anfang ------------------- */ const hole = async (von, tage) => { const a = await fetch(`${BASIS}/workspace/api/termine?von=${von}&tage=${tage}`, { headers: { Cookie: keks } }); return a.ok ? a.json() : null; }; const titel = (j) => (j?.brettsachen || []).map((b) => b.titel); /* Ein Fenster, das MITTEN im Zeitraum liegt — der Anfang ist davor. */ const mitten = await hole(tagPlus(1), 2); ok(titel(mitten).includes("Wochenaktion quer"), "ein laufender Zeitraum kommt mit, auch wenn sein Anfang vor dem Fenster liegt"); console.log(` Fenster ${tagPlus(1)} + 2 Tage, Aktion läuft ${LAUF_START}–${LAUF_ENDE}`); /* GEGENPROBE: Ein Fenster NACH dem Ende darf ihn nicht bringen. Ohne diese Probe würde auch ein Server bestehen, der einfach alles schickt — und dann stünde jeder je getippte Zettel im Kalender. */ const danach = await hole(tagPlus(10), 5); ok(!titel(danach).includes("Wochenaktion quer"), "Gegenprobe: nach dem Ende kommt er nicht mehr — es wird nicht einfach alles geschickt"); const davor = await hole(tagPlus(-30), 20); ok(!titel(davor).includes("Wochenaktion quer"), "Gegenprobe: und vor dem Anfang auch nicht"); /* Der Punkttermin muss sich genauso verhalten wie vorher. */ const beimEinzel = await hole(EINZEL, 1); ok(titel(beimEinzel).includes("Nur ein Tag"), "der Punkttermin kommt weiterhin an seinem Tag"); /* ---- 2. DER BILDSCHIRM: jeder Tag ist markiert -------------------- */ const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); try { const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } }); const s = await ctx.newPage(); await s.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); await s.click('.rolle[data-rolle="admin"]'); await s.fill("#code", "CODE-DOGI-0001"); await s.click("button[type=submit]"); await s.waitForURL("**/start.html", { timeout: 25000 }); await s.goto(BASIS + "/workspace/kalender.html", { waitUntil: "networkidle" }); await s.waitForTimeout(1800); /* Wie viele Tageszellen tragen die Aktion? */ const lage = await s.evaluate(() => { const zellen = [...document.querySelectorAll(".k-tag")]; const treffer = []; for (const z of zellen) { for (const p of z.querySelectorAll(".k-pille")) { if (/Wochenaktion quer/.test(p.textContent)) { treffer.push({ zahl: z.querySelector(".k-tag__zahl")?.textContent || "?", spanne: p.dataset.spanne || null, text: p.textContent.replace(/\s+/g, " ").trim(), breit: Math.round(p.getBoundingClientRect().width), }); } } } const einzel = [...document.querySelectorAll(".k-pille")] .filter((p) => /Nur ein Tag/.test(p.textContent)); return { zellen: zellen.length, treffer, einzel: einzel.length, einzelSpanne: einzel[0]?.dataset.spanne ?? null }; }); ok(lage.zellen > 0, `das Monatsraster steht (${lage.zellen} Tageszellen)`); ok(lage.treffer.length === 5, `die Aktion ist an allen fünf Tagen markiert (${lage.treffer.length})`); console.log(" " + lage.treffer.map((t) => `${t.zahl}.[${t.spanne}]`).join(" ")); const pos = lage.treffer.map((t) => t.spanne); ok(pos[0] === "start" && pos.at(-1) === "ende", `Anfang und Ende sind als solche erkennbar (${pos[0]} … ${pos.at(-1)})`); ok(pos.slice(1, -1).every((p) => p === "mitte"), "dazwischen steht überall 'mitte' — die Form sagt, dass es weitergeht"); /* Auf dem ersten Tag steht die Uhrzeit-Spalte, danach die Lage. Eine Uhrzeit am Mittwoch zu wiederholen behauptet, es ginge am Mittwoch wieder los. */ ok(/Tag 2\/5/.test(lage.treffer[1]?.text || ""), `der zweite Tag sagt, der wievielte er ist ("${lage.treffer[1]?.text.slice(0, 34)}")`); ok(!/Tag \d+\/\d+/.test(lage.treffer[0]?.text || ""), "am ersten Tag steht das NICHT — dort gehört die Startzeit hin"); /* GEGENPROBE AM BILDSCHIRM: Der Punkttermin darf sich nicht angesteckt haben. Ohne sie bestünde auch eine Fassung, die jede Kachel über den halben Monat zieht. */ ok(lage.einzel === 1, `Gegenprobe: der Punkttermin steht an genau einem Tag (${lage.einzel})`); ok(lage.einzelSpanne === null, "Gegenprobe: und trägt keine Spannen-Kennzeichnung"); /* ---- 3. Der Tagesdialog widerspricht dem Raster nicht ----------- */ /* Vorher filterte er selbst auf den Anfangstag: Im Raster war der Mittwoch markiert, tippte man ihn an, stand da „An diesem Tag steht nichts." */ const mitteTag = lage.treffer[2]?.zahl; await s.evaluate((zahl) => { for (const z of document.querySelectorAll(".k-tag")) { if (z.querySelector(".k-tag__zahl")?.textContent === zahl) { z.click(); return; } } }, mitteTag); await s.waitForTimeout(700); const dialog = await s.evaluate(() => { const dl = document.querySelector("#tagdialog[open]"); return dl ? dl.textContent.replace(/\s+/g, " ").trim() : null; }); ok(!!dialog && /Wochenaktion quer/.test(dialog), `der Tagesdialog am ${mitteTag}. zeigt die Aktion auch`); ok(!/An diesem Tag steht nichts/.test(dialog || ""), " und sagt nicht mehr 'An diesem Tag steht nichts'"); await ctx.close(); } finally { await browser.close(); } console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler ? 1 : 0);