diff --git a/server/pruef-struktur.mjs b/server/pruef-struktur.mjs index cf0c60c0..53da122f 100644 --- a/server/pruef-struktur.mjs +++ b/server/pruef-struktur.mjs @@ -1245,9 +1245,14 @@ console.log(`\n=== Wird der TAG in Ortszeit gebildet -- ueberall? ===`); const treffer = []; const zeile = (i) => text.slice(0, i).split("\n").length; - /* (1) Direkt: new Date(...).toISOString().slice(0, 10) */ + /* (1) Direkt: new Date(...).toISOString() und dann auf den Tag + gekuerzt. DREI SCHREIBWEISEN, nicht eine (02.10.2026): Den + UTC-Tag bekommt man mit `.slice(0, 10)`, mit `.split("T")[0]` + und mit `.substring(0, 10)`. Gemessen schreibt heute niemand + die anderen beiden -- aber „niemand schreibt es so" ist kein + Schutz, sondern Glueck. */ for (const t of text.matchAll( - /new Date\(([^;]*?)\)\s*\.\s*toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) { + /new Date\(([^;]*?)\)\s*\.\s*toISOString\(\)\s*\.\s*(?:slice\(\s*0\s*,\s*10\s*\)|substring\(\s*0\s*,\s*10\s*\)|split\(\s*["'`]T["'`]\s*\)\s*\[\s*0\s*\])/g)) { if (festeUhrzeit.test(t[1])) continue; treffer.push(zeile(t.index)); } @@ -1273,19 +1278,45 @@ console.log(`\n=== Wird der TAG in Ortszeit gebildet -- ueberall? ===`); if (utcNamen.has(t[2])) utcNamen.add(t[1]); } } - for (const t of text.matchAll(/(\w+)(\(\))?\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) { + for (const t of text.matchAll( + /(\w+)(\(\))?\s*\.\s*(?:slice\(\s*0\s*,\s*10\s*\)|substring\(\s*0\s*,\s*10\s*\)|split\(\s*["'`]T["'`]\s*\)\s*\[\s*0\s*\])/g)) { if (utcNamen.has(t[1])) treffer.push(zeile(t.index)); } return treffer; } + /* ==== DIE ZWEI DATEIEN, DIE DAS MUSTER ENTHALTEN MUESSEN ======== + + pruef-struktur.mjs: Die Gegenproben weiter unten MUESSEN es als + Text enthalten, sonst koennten sie nicht beweisen, dass die + Erkennung greift. + pruef-tagesgrenze.mjs: Sie stellt den falschen Weg dem richtigen + gegenueber und zeigt, dass der Unterschied auffaellt. + + EINE AUSNAHME, DIE NIEMAND NACHPRUEFT, IST DER ANFANG EINER + LISTE, DIE NIEMAND PFLEGT. Deshalb wird nachgesehen, dass jede + ausgenommene Datei das Muster WIRKLICH enthaelt — raeumt es + jemand weg, ist die Ausnahme veraltet und faellt auf. */ + const AUSNAHMEN_UTC = new Set(["pruef-struktur.mjs", "pruef-tagesgrenze.mjs"]); + { + const schal = [...AUSNAHMEN_UTC].filter((n) => { + const t = readFileSync(join(SERVER, n), "utf8"); + return !/toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/.test(t); + }); + ok(schal.length === 0, + schal.length + ? `diese Ausnahmen brauchen keine mehr zu sein: ${schal.join(", ")}` + : `${AUSNAHMEN_UTC.size} Dateien sind ausgenommen und enthalten das Muster` + + " auch wirklich"); + } + const verdaechtig = []; for (const datei of dateien) { /* Die eigene Datei bleibt aussen vor: Die Gegenproben unten MUESSEN das gesuchte Muster als Text enthalten, sonst koennten sie nicht beweisen, dass sie es erkennen. Sie wuerde sich sonst selbst anzeigen -- eine Warnung, die immer kommt. */ - if (datei === "pruef-struktur.mjs") continue; + if (AUSNAHMEN_UTC.has(datei)) continue; /* KOMMENTARE RAUS, ZEILEN BEHALTEN (16.09.2026). Hier stand `replace(…, " ")` -- ein Blockkommentar ueber zwanzig @@ -1323,6 +1354,13 @@ console.log(`\n=== Wird der TAG in Ortszeit gebildet -- ueberall? ===`); " auch ueber zwei Schritte"); ok(probe('const g = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);') === 1, " und eine Rechnung auf Date.now()"); + ok(probe('const h = new Date().toISOString().split("T")[0];') === 1, + " auch die Schreibweise mit split(\"T\")[0]"); + ok(probe('const h = new Date().toISOString().substring(0, 10);') === 1, + " und die mit substring(0, 10)"); + ok(probe('const jetzt = () => new Date().toISOString();\n' + + 'const h = jetzt().split("T")[0];') === 1, + " und beides ueber einen Namen"); ok(probe('const jetzt = new Date().toISOString();') === 0, "Gegenprobe: ein voller Zeitstempel ohne Schnitt schlaegt NICHT an"); diff --git a/server/pruef-tagesgrenze.mjs b/server/pruef-tagesgrenze.mjs new file mode 100644 index 00000000..70e7407b --- /dev/null +++ b/server/pruef-tagesgrenze.mjs @@ -0,0 +1,198 @@ +/* ===================================================================== + pruef-tagesgrenze.mjs — der Tageswechsel, zu jeder Uhrzeit prüfbar + + --------------------------------------------------------------------- + DAS PROBLEM MIT DEN PRÜFUNGEN VON GESTERN + + Am 01.10.2026 wurde ein Fehler gefunden, der sechzehn Stellen im + Haus betraf: Ein Kalendertag wurde aus UTC gebildet statt aus der + Ortszeit. Zwischen 00:00 und 02:00 (im Winter 00:00 bis 01:00) + liegt der UTC-Tag einen zurück — ein Termin, nachts angelegt, trug + gestern und landete auf dem Brett unter „Vorbei", wo er + zugeklappt ist. + + Repariert, und mit `pruef-kreislauf` abgesichert: + + ok und er liegt HEUTE, nicht gestern (…) + + ABER: Diese Zeile kann den Fehler nur ZWEI STUNDEN AM TAG + überhaupt sehen. Den Rest des Tages sind Ortszeit und UTC-Tag + derselbe, und die Behauptung ist wahr, ohne irgendetwas zu + beweisen. Genau die Sorte grüner Haken, vor der dieses Haus an + mehreren Stellen warnt: Sie läuft, sie ist grün, und sie ist + wertlos. + + Gemerkt habe ich es, weil ich die Reparatur am 01.10. zwischen + 10 und 16 Uhr geprüft habe — also zu einer Zeit, in der der + Fehler gar nicht auftreten KONNTE. Erst um 00:09 des 02.10. war + sie wirklich bewiesen. + + --------------------------------------------------------------------- + WAS DIESE PRÜFUNG ANDERS MACHT + + Sie wartet nicht auf Mitternacht, sie verschiebt die Uhr. Node + übernimmt eine zur Laufzeit gesetzte `TZ` (nachgemessen: Stunde 0 + wird zu Stunde 12 unter `Etc/GMT-14`). + + WELCHE ZEITZONE, HÄNGT VON DER UHRZEIT AB. Es gibt keine, in der + sich Orts- und UTC-Tag IMMER unterscheiden — dafür bräuchte es + 24 Stunden Versatz. Also wird die passende gewählt: + + UTC-Stunde ab 10 Uhr -> Etc/GMT-14 (UTC+14, Ortstag voraus) + UTC-Stunde vor 10 Uhr -> Etc/GMT+11 (UTC−11, Ortstag zurück) + + Bei 10 Uhr UTC gehen beide; die Grenze ist deshalb kein scharfer + Rand, an dem etwas kippen kann. + + UND DAS WIRD NACHGESEHEN, NICHT ANGENOMMEN: Unterscheiden sich + die zwei Tage in diesem Lauf wider Erwarten nicht, bricht die + Prüfung mit dem dritten Ausgang ab. Eine Prüfung, die ihre eigene + Voraussetzung nicht hat, darf nicht grün melden — daran ist am + 03.09.2026 die Gitea-Pflichtprüfung wochenlang vorbeigelaufen. + + Aufrufen mit: node server/pruef-tagesgrenze.mjs + ===================================================================== */ + +/* ==== DIE ZEITZONE MUSS VOR ALLEM ANDEREN STEHEN ==================== + Sie wirkt auf jedes `new Date()` danach — auch auf die im Server, + der gleich geladen wird. Stünde sie weiter unten, hätte der + Server schon seine Vorstellung vom heutigen Tag. */ +const UTC_STUNDE = new Date().getUTCHours(); +const ZONE = UTC_STUNDE >= 10 ? "Etc/GMT-14" : "Etc/GMT+11"; +process.env.TZ = ZONE; + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { eigenerPort } from "./helfer-port.mjs"; +import { heuteLokal } from "./helfer-tag.mjs"; + +let fehler = 0, geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +/* ---- Die Voraussetzung, bevor irgendetwas anderes passiert -------- */ +console.log(`\n=== Die verschobene Uhr (${ZONE}) ===`); +const ORTSTAG = heuteLokal(); +const UTC_TAG = new Date().toISOString().slice(0, 10); +console.log(` Ortstag ${ORTSTAG} · UTC-Tag ${UTC_TAG} · UTC-Stunde ${UTC_STUNDE}`); + +if (ORTSTAG === UTC_TAG) { + /* DRITTER AUSGANG. Ohne Unterschied prüft diese Datei nichts, und + das muss sie sagen statt grün zu melden. */ + console.log(" -- KONNTE NICHT NACHSEHEN: Orts- und UTC-Tag sind in dieser"); + console.log(` Zeitzone gerade gleich (${ZONE}, UTC-Stunde ${UTC_STUNDE}).`); + console.log(" Die Wahl der Zone stimmt dann nicht mehr — sie steht im"); + console.log(" Kopf dieser Datei und gehoert nachgerechnet."); + process.exit(3); +} +ok(true, `Orts- und UTC-Tag unterscheiden sich (${ORTSTAG} gegen ${UTC_TAG})`); + +/* ---- Der Server, mit derselben verschobenen Uhr ------------------- */ +const PORT = await eigenerPort(import.meta, "pruef-tagesgrenze"); +const ordner = mkdtempSync(join(tmpdir(), "ws-tagesgrenze-")); +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(120_000, "pruef-tagesgrenze"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = `http://127.0.0.1:${PORT}`; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetztIso = 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, jetztIso); +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"), jetztIso); +} +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Lena", "modi", "CODE-LENA-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 lena = await anmelden("modi", "CODE-LENA-0001"); +const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS }); +const liste = async (bereich, keks = dogi) => + (await (await fetch(`${BASIS}/workspace/api/bereich/${bereich}`, + { headers: { cookie: keks } })).json().catch(() => null))?.eintraege || []; + +console.log("\n=== 1. Ein Eintrag OHNE Datum bekommt den ORTSTAG ==="); +/* Die Stelle in workspace-bereiche.js, an der bis zum 01.10. stand: + if (!d) aus.datum = jetzt().slice(0, 10); // UTC */ +{ + const a = await fetch(`${BASIS}/workspace/api/bereich/wunsch`, { + method: "POST", headers: kopf(lena), + body: JSON.stringify({ art: "gemeinsam", titel: "Ohne Datum angelegt" }), + }); + ok(a.status === 201, `der Wunsch wird angenommen (${a.status})`); + const e = (await liste("wunsch", lena)).find((x) => x.titel === "Ohne Datum angelegt"); + ok(e?.datum === ORTSTAG, + `sein Datum ist der Ortstag ${ORTSTAG}, nicht der UTC-Tag ${UTC_TAG}` + + ` (steht: ${e?.datum})`); +} + +console.log("\n=== 2. Und die Kette, an der es aufgefallen ist ==="); +/* `daraus` legte den neuen Eintrag mit `jetzt().slice(0, 10)` an. + Genau hier stand am 01.10. um 01:22 ein Tag zu frueh — und der + Termin landete auf „Was ansteht" im zugeklappten Abschnitt + „Vorbei". */ +{ + const w = await fetch(`${BASIS}/workspace/api/bereich/wunsch`, { + method: "POST", headers: kopf(lena), + body: JSON.stringify({ art: "gemeinsam", titel: "Daraus wird ein Termin", datum: ORTSTAG }), + }); + const wunschId = (await w.json().catch(() => null))?.id || 0; + ok(wunschId > 0, `der Wunsch steht (#${wunschId})`); + + const a = await fetch(`${BASIS}/workspace/api/bereich/wunsch/${wunschId}/daraus`, { + method: "POST", headers: kopf(dogi), body: JSON.stringify({ nach: "ansteht" }), + }); + const terminId = (await a.json().catch(() => null))?.id || 0; + ok(a.status === 201 && terminId > 0, `daraus wird ein Termin (${a.status})`); + + const t = (await liste("ansteht")).find((x) => x.id === terminId); + ok(t?.datum === ORTSTAG, + `und er liegt auf dem Ortstag ${ORTSTAG}, nicht auf ${UTC_TAG} (steht: ${t?.datum})`); +} + +console.log("\n=== 3. Gegenprobe: wuerde der alte Weg auffallen? ==="); +/* Eine Pruefung, die immer bestaetigt, bestaetigt nichts. Hier wird + nachgebaut, was bis zum 01.10. im Code stand — und gezeigt, dass + die Behauptungen oben daran scheitern wuerden. */ +{ + const alterWeg = new Date().toISOString().slice(0, 10); + ok(alterWeg === UTC_TAG && alterWeg !== ORTSTAG, + `der alte Weg (toISOString) liefert ${alterWeg} — das waere der falsche Tag`); + ok(heuteLokal() === ORTSTAG, + `heuteLokal liefert ${heuteLokal()} — der richtige`); +} + +console.log(`\n${geprueft} Pruefungen, ${fehler} Fehler`); +console.log(fehler ? "NICHT IN ORDNUNG\n" : "ALLES IN ORDNUNG\n"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0);