Tagesgrenze: der Fehler von gestern ist jetzt rund um die Uhr pruefbar
UM 00:09 DES 02.10. WAR DAS FENSTER OFFEN — Ortszeit 2026-10-02,
UTC noch 2026-10-01. Genau die zwei Stunden, in denen der Fehler von
gestern zuschlug.
Ich habe die Reparatur am 01.10. zwischen 10 und 16 Uhr geprueft,
also zu einer Zeit, in der der Fehler GAR NICHT AUFTRETEN KONNTE.
Jetzt nachgeholt — vierzehn Dateien, 1083 Pruefungen, 0 Fehler. Die
entscheidende Zeile in pruef-kreislauf:
ok und er liegt HEUTE, nicht gestern (2026-10-02, heute ist 2026-10-02)
Gestern um dieselbe Uhrzeit stand dort 2026-09-30 gegen 2026-10-01.
DABEI IST MIR DAS EIGENTLICHE PROBLEM AUFGEFALLEN
Diese Zeile kann den Fehler nur ZWEI STUNDEN AM TAG ueberhaupt
sehen. Den Rest des Tages sind Ortszeit und UTC-Tag derselbe, und
sie ist wahr, ohne irgendetwas zu beweisen — ein gruener Haken ohne
Aussage, 22 Stunden lang.
pruef-tagesgrenze.mjs wartet deshalb nicht auf Mitternacht, sondern
verschiebt die Uhr. Node uebernimmt eine zur Laufzeit gesetzte TZ
(nachgemessen: Stunde 0 wird zu Stunde 12 unter Etc/GMT-14), und sie
steht VOR allem anderen — sonst haette der Server schon seine
Vorstellung vom heutigen Tag.
Welche Zone, haengt von der Uhrzeit ab: Es gibt keine, in der sich
die zwei Tage IMMER unterscheiden, dafuer braeuchte es 24 Stunden
Versatz. Ab 10 Uhr UTC also Etc/GMT-14, davor Etc/GMT+11; bei 10 Uhr
gehen beide, die Grenze ist kein scharfer Rand.
UND DAS WIRD NACHGESEHEN: Unterscheiden sich die Tage wider Erwarten
nicht, bricht sie mit dem dritten Ausgang ab (3) statt gruen zu
melden. Eine Pruefung ohne ihre Voraussetzung darf nicht bestaetigen
— daran ist am 03.09. die Gitea-Pflichtpruefung wochenlang
vorbeigelaufen.
Geprueft werden die zwei Stellen, die es wirklich getroffen hat: ein
Eintrag OHNE Datum, und die Kette Wunsch -> Termin.
GEGENPROBE: Den alten Weg in workspace-bereiche.js wieder eingebaut
(beide Stellen) -> 8 Pruefungen, 2 Fehler, beide mit dem falschen
Tag im Klartext. Die Zahl bleibt bei 8, nichts wird still
uebersprungen.
ZWEI DINGE AM DETEKTOR IN pruef-struktur
1. EINE AUSNAHME, DIE SICH SELBST PRUEFT. pruef-tagesgrenze benutzt
den falschen Weg absichtlich und wurde deshalb zu Recht und
trotzdem falsch gemeldet. Sie ist jetzt ausgenommen — aber nicht
als stille Liste: Es wird nachgesehen, dass jede ausgenommene
Datei das Muster WIRKLICH enthaelt. Raeumt es jemand weg, ist die
Ausnahme veraltet und faellt auf.
2. EINE LUECKE, DIE MIR DABEI AUFFIEL. Der Detektor kannte nur
`.slice(0, 10)`. Denselben UTC-Tag bekommt man mit
`.split("T")[0]` und `.substring(0, 10)`. Gemessen schreibt das
heute niemand so — aber „niemand schreibt es so" ist kein Schutz,
sondern Glueck. Beide zaehlen jetzt mit, mit eigenen Gegenproben.
GEPRUEFT: pruef-struktur 75 -> 79, pruef-tagesgrenze 8, beide 0
Fehler. Ports nach der neuen Datei: bis 5417, 464 Nummern, 0
Kollisionen. pruef-arbeitsschloss 48/0, pruef-fingermass 5/0.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user