pruef-zeitraum und pruef-highlights bildeten ihren Tagesstempel mit toISOString(). Das ist UTC. Zwischen 22:00 und Mitternacht deutscher Zeit ist das schon der naechste Tag -- die Pruefungen verglichen dann einen Eintrag von heute mit dem Datum von morgen und meldeten einen Fehler, wo keiner war. Der Zeitraum-Filter im Haus rechnet richtig; er benutzt tagLokal() aus helfer-zeit.mjs. Nur die Pruefungen darueber hatten ihre eigene, falsche Rechnung. Jetzt benutzen sie denselben Helfer wie der Code, den sie pruefen -- damit koennen die beiden gar nicht mehr auseinanderlaufen. Co-Authored-By: Claude Opus 5 <[email protected]>
226 lines
10 KiB
JavaScript
226 lines
10 KiB
JavaScript
/* =====================================================================
|
||
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 { tagLokal } from "./helfer-zeit.mjs";
|
||
import { eigenerPort } from "./helfer-port.mjs";
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
|
||
const PORT = await eigenerPort(import.meta, "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());
|
||
|
||
/* DER HAUSSCHLUESSEL STATT EIGENER RECHNUNG (20.09.2026).
|
||
|
||
Hier stand `x.setHours(12,...)` und danach `toISOString().slice(0,10)`.
|
||
Das ist RICHTIG -- die Verankerung um zwoelf Uhr sorgt dafuer, dass
|
||
der UTC-Tag und der Ortstag zusammenfallen. Nur sieht man das der
|
||
Zeile nicht an, und pruef-struktur meldete sie als UTC-Rechnung.
|
||
|
||
Ein Fehlalarm ist schlimmer als keine Pruefung: Wer ihn zweimal
|
||
wegklickt, ueberliest beim dritten Mal den echten. Deshalb hier die
|
||
Form, die das Haus ohnehin kennt -- `tagLokal` rechnet ohne Umweg
|
||
ueber UTC und braucht keine Verankerung, die man erklaeren muss. */
|
||
const tagPlus = (n) => tagLokal(n);
|
||
/* 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);
|