Files
DogFatherGitandClaude Opus 5 782a9116f2 Zwei Pruefungen rechneten in UTC und schlugen abends falschen Alarm
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]>
2026-09-21 11:44:55 +02:00

226 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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);