Von den sechzehn Stellen vom 01.10. SCHREIBEN zehn einen Tag in die
Datenbank — die decken die zwei Abschnitte von vorhin ab. Sechs
VERGLEICHEN gegen „heute": ist diese Frist vorbei, faellt dieser
Lead in den Zeitraum, ist dieser Bericht aktuell.
Davon findet ein Schreibtest keinen einzigen. Die Spalte sieht
richtig aus; falsch ist die Zahl, die jemand auf dem Bildschirm
liest.
ZWEI AUFGABEN GENUEGEN. Die Regel im Haus ist `frist < heute`:
Frist = gestern (Ortszeit) -> muss ueberfaellig sein
Frist = heute (Ortszeit) -> darf es nicht sein
Mit dem alten UTC-Tag geht das in BEIDEN Zweigen der verschobenen
Uhr schief:
Etc/GMT-14 (Ortstag = UTC+1): heute_alt waere gestern -> 0 statt 1
Etc/GMT+11 (Ortstag = UTC-1): heute_alt waere morgen -> 2 statt 1
GEGENPROBE AM ECHTEN CODE: In workspace-aufgaben.js den UTC-Tag
wieder eingebaut -> „genau eine ist ueberfaellig (0)". Zurueckgedreht
und wieder gruen.
UND EIN BEFUND AN MEINER EIGENEN PRUEFUNG, bei genau dieser
Gegenprobe: Die zweite Zahl (`heute`) blieb 1 — aber sie zaehlte die
FALSCHE Aufgabe. Mit dem UTC-Tag galt die von gestern als heute
faellig. Mit zwei Zahlen allein ist das nicht zu trennen; in beiden
Zweigen bleibt sie 1.
Sie steht jetzt ausdruecklich als BEGLEITPROBE da: Sie zeigt, dass
ueberhaupt gezaehlt wird, und der Unterschied kommt aus der Zeile
darueber. Eine Zahl, die aus dem falschen Grund stimmt, soll nicht
aussehen wie ein Beweis.
Dazu im Lauf eine ausgerechnete Zeile, die sagt, WARUM die Zahlen
etwas beweisen: „mit dem UTC-Tag waeren es 0 statt 1".
IM FENSTER NACHGEMESSEN (00:09 bis 00:40 Ortszeit, UTC noch der
Vortag) — die restlichen Pruefungen, die ich gestern zur falschen
Tageszeit geprueft hatte: zuteilung 75, aufgabenbrett 49,
content 45, vorlagen 24, bewerbung 91, scout-zuteilung 37,
unterstuetzung 70, aufbewahrung 45, ics 38, entwicklung 79 — alle 0
Fehler. Zusammen mit den vierzehn von vorhin sind das 24 Dateien,
die diesmal im richtigen Zeitfenster gemessen wurden.
pruef-tagesgrenze: 8 -> 12 Pruefungen, 0 Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
255 lines
12 KiB
JavaScript
255 lines
12 KiB
JavaScript
/* =====================================================================
|
||
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, tagLokal } 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. Und die VERGLEICHE: was ist ueberfaellig? ===");
|
||
/* Von den sechzehn Stellen vom 01.10. SCHREIBEN zehn einen Tag; die
|
||
zwei Abschnitte oben decken sie ab. Sechs VERGLEICHEN gegen
|
||
„heute" — und davon findet ein Schreibtest keinen einzigen. Die
|
||
Spalte sieht richtig aus; falsch ist die Zahl, die jemand auf dem
|
||
Bildschirm liest.
|
||
|
||
Die Regel im Haus ist `frist < heute`. Zwei Aufgaben genuegen, und
|
||
sie unterscheiden in BEIDEN Zweigen der verschobenen Uhr:
|
||
|
||
Etc/GMT-14 (Ortstag = UTC+1): heute_alt waere gestern
|
||
-> „gestern" nicht mehr kleiner als heute_alt -> 0 statt 1
|
||
Etc/GMT+11 (Ortstag = UTC-1): heute_alt waere morgen
|
||
-> „heute" kleiner als heute_alt -> 2 statt 1 */
|
||
{
|
||
const GESTERN = tagLokal(-1);
|
||
const neueAufgabe = (titel, frist) => fetch(`${BASIS}/workspace/api/aufgaben`, {
|
||
method: "POST", headers: kopf(dogi),
|
||
body: JSON.stringify({ titel, frist, prioritaet: "mittel" }),
|
||
});
|
||
|
||
const a1 = await neueAufgabe("Frist war gestern", GESTERN);
|
||
const a2 = await neueAufgabe("Frist ist heute", ORTSTAG);
|
||
ok(a1.status === 201 && a2.status === 201,
|
||
`zwei Aufgaben angelegt, Fristen ${GESTERN} und ${ORTSTAG}`
|
||
+ ` (${a1.status}/${a2.status})`);
|
||
|
||
const u = await (await fetch(`${BASIS}/workspace/api/uebersicht`,
|
||
{ headers: { cookie: dogi } })).json().catch(() => ({}));
|
||
|
||
ok(u.ueberfaellig === 1,
|
||
`genau eine ist ueberfaellig — die von gestern (${u.ueberfaellig})`);
|
||
/* DIESE ZAHL UNTERSCHEIDET NICHT, und das gehoert dazugeschrieben.
|
||
Gemessen beim Rueckbau: Mit dem UTC-Tag blieb sie 1 — nur zaehlte
|
||
sie die FALSCHE Aufgabe (die von gestern galt dann als heute
|
||
faellig). Mit zwei Zahlen allein ist das nicht zu trennen; in
|
||
beiden Zweigen der verschobenen Uhr bleibt sie 1.
|
||
|
||
Sie steht hier als Begleitprobe: Sie zeigt, dass ueberhaupt
|
||
gezaehlt wird. Der Unterschied kommt aus der Zeile darueber. Eine
|
||
Zahl, die aus dem falschen Grund stimmt, soll nicht aussehen wie
|
||
ein Beweis. */
|
||
ok(u.heute === 1,
|
||
`und eine ist fuer heute faellig (${u.heute}) — Begleitprobe,`
|
||
+ " sie unterscheidet nicht");
|
||
|
||
/* WAS DER ALTE WEG GESAGT HAETTE. Nicht nachgebaut, sondern
|
||
ausgerechnet: Damit steht im Lauf, warum diese zwei Zahlen etwas
|
||
beweisen und nicht nur zufaellig stimmen. */
|
||
const alt = UTC_TAG;
|
||
const waere = [GESTERN, ORTSTAG].filter((f) => f < alt).length;
|
||
ok(waere !== 1,
|
||
`mit dem UTC-Tag (${alt}) waeren es ${waere} statt 1 — die Zeile`
|
||
+ " ueber dieser unterscheidet also wirklich");
|
||
}
|
||
|
||
console.log("\n=== 4. 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);
|