diff --git a/server/pruef-auskunft.mjs b/server/pruef-auskunft.mjs index 44a58b53..6e0f426d 100644 --- a/server/pruef-auskunft.mjs +++ b/server/pruef-auskunft.mjs @@ -45,6 +45,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(180_000, "pruef-auskunft"); await new Promise((r) => setTimeout(r, 700)); @@ -91,7 +92,7 @@ let idDogi, idRieke, idGast; treff_entfernt.autor_id (ebenfalls ohne). */ d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)" + " VALUES ('treff','hallo','KESSI-EINTRAG','Hallo zusammen',?,?,?)") - .run(jetzt.slice(0, 10), jetzt, idGast); + .run(heuteLokal(), jetzt, idGast); const raum = (() => { d.prepare("INSERT INTO chat_raeume (art, name, erstellt, erstellt_von) VALUES ('gruppe','Testraum',?,?)") .run(jetzt, idDogi); diff --git a/server/pruef-ics.mjs b/server/pruef-ics.mjs index 7fb73a00..c8e87fd7 100644 --- a/server/pruef-ics.mjs +++ b/server/pruef-ics.mjs @@ -341,10 +341,28 @@ melde("\n=== Sichtbarkeit — der Weg ohne Anmeldung darf nicht mehr zeigen ===" ======================================================================= */ melde("\n=== Falsche Schluessel ==="); { + /* DIE PROBE DARF NICHT DER ECHTE SCHLUESSEL SEIN (01.10.2026). + + Hier stand `echt.slice(0, -1) + "A"`. Der Schluessel ist base64url + aus 32 Bytes; sein letztes Zeichen kodiert vier Bit und ist eines + von sechzehn. Endet er auf „A", IST die Probe der echte + Schluessel -- der Server antwortet zu Recht mit 200, und diese + Pruefung meldet ein Loch, das es nicht gibt. Einmal je sechzehn + Laeufe, also mehrmals im Monat; heute Nacht zweimal + hintereinander, und die Suche ging eine halbe Stunde in eine + Aenderung, die damit nichts zu tun hatte. + + Ein Fehlalarm, den man nicht nachstellen kann, ist der sicherste + Weg, eine Pruefung abzuschalten. */ + const echt = String(linkLuna).split("/").pop().replace(".ics", ""); + const fastRichtig = echt.slice(0, -1) + (echt.at(-1) === "A" ? "B" : "A"); + ok(fastRichtig !== echt && fastRichtig.length === echt.length, + `die Probe unterscheidet sich vom echten Schluessel (…${echt.slice(-4)} gegen …${fastRichtig.slice(-4)})`); + const proben = [ ["kurz", "abc"], ["leer", ""], - ["fast richtig", String(linkLuna).split("/").pop().replace(".ics", "").slice(0, -1) + "A"], + ["fast richtig", fastRichtig], ["mit Punkten", "../../workspace/start"], ["SQL-Versuch", "' OR '1'='1"], ]; diff --git a/server/pruef-kreislauf.mjs b/server/pruef-kreislauf.mjs index 4de97b99..cf8f6fd1 100644 --- a/server/pruef-kreislauf.mjs +++ b/server/pruef-kreislauf.mjs @@ -149,6 +149,27 @@ let terminId = 0; ok(t?.aus?.bereich === "wunsch", `und er weiss, woher er kam (${t?.aus?.bereich})`); ok(t?.aus?.von_name === "Lena", `mit dem Namen dessen, der den Wunsch schrieb ("${t?.aus?.von_name}")`); + + /* ---- UND AN WELCHEM TAG? (01.10.2026) -------------------------- + + DIE FRAGE HAT HIER GEFEHLT, und der Fehler stand trotzdem vor + der Tuer: Der Termin bekam sein Datum aus `jetzt().slice(0, 10)` + -- dem UTC-Tag. Um 01:22 Ortszeit war das der 30.09., nicht der + 01.10. + + WAS DAS ANRICHTET: Auf „Was ansteht" sortiert sich der Termin + nach Datum in einen Zeitstrahl ein. Ein Tag in der Vergangenheit + heisst Abschnitt „Vorbei" -- und der ist mit Absicht zugeklappt. + Beim Wunsch stand „Daraus wurde ein Termin", auf dem Brett war + er nicht zu sehen. Zwei Stunden jede Nacht, genau die Stunden, + in denen hier gearbeitet wird. + + GEFUNDEN HAT ES DER BROWSERTEIL weiter unten -- aber nur als + Folge („`.kette` wird nicht sichtbar"), und das schickt einen zur + falschen Stelle. Diese eine Zeile nennt den Fehler beim Namen und + braucht dafuer keinen Browser. */ + ok(t?.datum === heuteLokal(), + `und er liegt HEUTE, nicht gestern (${t?.datum}, heute ist ${heuteLokal()})`); } /* ======================================================================= @@ -237,8 +258,46 @@ melde("=== 5. Was man sieht ==="); const konsole = []; seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); }); await seite.goto(`${BASIS}/workspace/bereich.html?b=ansteht`, { waitUntil: "networkidle" }); - await seite.waitForSelector(".kette", { timeout: 15_000 }); + + /* ---- ERST AUFKLAPPEN, DANN HINSEHEN (01.10.2026) ------------- + + Hier stand nur `waitForSelector(".kette")`. Das wartet auf + SICHTBARKEIT -- und am 01.10. um 01:22 lag der Termin wegen + eines falschen Datums im Abschnitt „Vorbei", der von sich aus + zugeklappt ist. Die Meldung lautete „Zeitsperre nach 15 s", mit + dem Zusatz „33 x locator resolved to 2 elements": Das Element + WAR da. Wer die erste Zeile liest, sucht in der Anzeige; die + Ursache lag im Datum. + + Diese Pruefung misst die KETTE, nicht die Zeiteinteilung. Also + macht sie auf, was zu ist -- wie ein Mensch, der einen + Abschnitt anklickt. Dass der Tag stimmt, fragt jetzt eine + eigene Zeile weiter oben; so bleibt jede Frage bei einer + Pruefung, und ein Fehler zeigt sich dort, wo er hingehoert. */ + await seite.waitForSelector(".kette", { state: "attached", timeout: 15_000 }); + const aufgeklappt = await seite.evaluate(() => { + let n = 0; + for (const schild of document.querySelectorAll(".zeitstrahl__schild")) { + const kasten = schild.nextElementSibling; + if (kasten?.hidden) { schild.click(); n++; } + } + return n; + }); await seite.waitForTimeout(400); + if (aufgeklappt) melde(` -- ${aufgeklappt} zugeklappte(n) Abschnitt(e) aufgemacht`); + + /* DRITTER AUSGANG: „gar nicht da" ist etwas anderes als „da und + unsichtbar". Ohne diese Unterscheidung heisst beides + „Zeitsperre", und man sucht zweimal an der falschen Stelle. */ + const lage = await seite.evaluate(() => { + const alle = [...document.querySelectorAll(".kette")]; + return { + da: alle.length, + sichtbar: alle.filter((z) => z.getBoundingClientRect().height > 0).length, + }; + }); + ok(lage.da >= 2 && lage.sichtbar === lage.da, + `beide Kettenzeilen stehen sichtbar auf der Seite (${lage.sichtbar} von ${lage.da})`); const m = await seite.evaluate(() => ({ her: [...document.querySelectorAll(".kette--her")].map((z) => z.textContent.trim()), diff --git a/server/pruef-modi-ideen.mjs b/server/pruef-modi-ideen.mjs index d3dc9744..df36ba67 100644 --- a/server/pruef-modi-ideen.mjs +++ b/server/pruef-modi-ideen.mjs @@ -80,12 +80,12 @@ console.log("\n=== 1. Eine bestehende Datenbank ohne den neuen Bereich ===\n"); );`); for (let i = 1; i <= 7; i++) { d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, erstellt) VALUES (?,?,?,?,?)") - .run("live", "vorbereitung", `Altbestand ${i}`, jetzt.slice(0, 10), jetzt); + .run("live", "vorbereitung", `Altbestand ${i}`, heuteLokal(), jetzt); } const abgelehnt = (() => { try { d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, erstellt) VALUES (?,?,?,?,?)") - .run("ideen", "content", "geht noch nicht", jetzt.slice(0, 10), jetzt); + .run("ideen", "content", "geht noch nicht", heuteLokal(), jetzt); return false; } catch { return true; } })(); @@ -103,6 +103,7 @@ 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"; import { eigenerPort } from "./helfer-port.mjs"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(180_000, "pruef-modi-ideen"); await new Promise((r) => setTimeout(r, 900)); diff --git a/server/pruef-nachwuchs.mjs b/server/pruef-nachwuchs.mjs index 0cb40bb8..e39c2a2f 100644 --- a/server/pruef-nachwuchs.mjs +++ b/server/pruef-nachwuchs.mjs @@ -39,6 +39,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(240_000, "pruef-nachwuchs"); await new Promise((r) => setTimeout(r, 700)); @@ -106,7 +107,7 @@ anlegen("Mara", "linke", "CODE-LINK-0001"); Trichter ein gruener Haken ueber der Null. */ d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)" + " VALUES ('talente','ruhig','Kessi aus dem Chat','Faellt seit Wochen auf.',?,?,?,?)") - .run(jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag"); + .run(heuteLokal(), jetzt, idDogi, "Im Stream am Dienstag"); const idTalent = d.prepare("SELECT last_insert_rowid() AS id").get().id; d.close(); diff --git a/server/pruef-neue-seiten.mjs b/server/pruef-neue-seiten.mjs index 14005bc1..1ca01f92 100644 --- a/server/pruef-neue-seiten.mjs +++ b/server/pruef-neue-seiten.mjs @@ -49,6 +49,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(240_000, "pruef-neue-seiten"); await new Promise((r) => setTimeout(r, 700)); @@ -87,7 +88,7 @@ const idKessi = anlegen("Kessi", "gast", "CODE-GAST-0001", ALT); eine Meldung und eine Maßnahme gibt. */ d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)" + " VALUES ('treff','hallo','Hallo, ich bin die Kessi','Freue mich, hier zu sein!',?,?,?)") - .run(jetzt.slice(0, 10), jetzt, idKessi); + .run(heuteLokal(), jetzt, idKessi); const beitrag = d.prepare("SELECT last_insert_rowid() AS id").get().id; d.close(); @@ -156,7 +157,7 @@ let idHand = null; const talent = (titel, text) => { dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)" + " VALUES ('talente','ruhig',?,?,?,?,?,?)") - .run(titel, text, jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag"); + .run(titel, text, heuteLokal(), jetzt, idDogi, "Im Stream am Dienstag"); return dd.prepare("SELECT last_insert_rowid() AS id").get().id; }; talent("Kessi aus dem Chat", "Faellt seit Wochen auf."); diff --git a/server/pruef-spicy.mjs b/server/pruef-spicy.mjs index 68e27df7..b1c888f6 100644 --- a/server/pruef-spicy.mjs +++ b/server/pruef-spicy.mjs @@ -34,6 +34,7 @@ import { spawn } from "node:child_process"; import { DatabaseSync } from "node:sqlite"; import { scryptSync, randomBytes } from "node:crypto"; import { notbremse } from "./helfer-notbremse.mjs"; +import { heuteLokal } from "./helfer-tag.mjs"; notbremse(300_000, "die Spicy-Pruefung"); @@ -137,7 +138,7 @@ let idDogi, idMax, idLuna, spaltenVorher, personenVorher; .run(idLuna, idMax, jetzt); d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, creator_id, erstellt, erstellt_von) VALUES ('technik','problem','Ton bricht ab',?,?,?,?)`) - .run(jetzt.slice(0, 10), idLuna, jetzt, idMax); + .run(heuteLokal(), idLuna, jetzt, idMax); spaltenVorher = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name); personenVorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; diff --git a/server/pruef-struktur.mjs b/server/pruef-struktur.mjs index 134d489c..ed85bf68 100644 --- a/server/pruef-struktur.mjs +++ b/server/pruef-struktur.mjs @@ -772,74 +772,263 @@ console.log(`\n=== Laedt jedes Server-Modul? ===`); Deshalb wird es hier gesucht, statt darauf zu warten, dass es wieder jemanden trifft. Gemeint ist nur das TAGESDATUM; ein Zeitstempel (`erstellt`) darf und soll UTC sein. */ -console.log(`\n=== Rechnen die Pruefungen mit Ortszeit? ===`); +console.log(`\n=== Ruft ein Modul etwas, das es nie eingefuehrt hat? ===`); +/* =================================================================== + AM 01.10.2026 SELBST GEBAUT UND FAST AUSGELIEFERT. + + Ein Werkzeug von mir hat in elf Modulen `heuteLokal()` eingesetzt + und die Einfuhr weggelassen. `node --check` sagt dazu nichts, und + „Laedt jedes Server-Modul?" ein Stueck weiter oben auch nicht: Die + Datei ist syntaktisch tadellos. Erst der AUFRUF faellt um: + + ReferenceError: heuteLokal is not defined + + Gefunden hat es pruef-video -- zufaellig, weil dort genau diese + Zeile geprueft wird. Die anderen zehn Module waeren durchgerutscht, + und bei einer Zeile, die nur nachts laeuft, faellt das erst Wochen + spaeter jemandem auf. + + DAS IST EINE DRITTE SORTE FEHLER an dieser Stelle: nicht „die Datei + ist kaputt" und nicht „die Datei tut das Falsche", sondern „die + Datei faellt erst um, wenn jemand genau diese Zeile braucht". + + ABGELEITET, NICHT AUFGEZAEHLT: Die Namen des Hauses kommen aus den + `export`-Zeilen von workspace.js und der helfer-Module. Eine + gepflegte Liste waere hier genau die, die beim naechsten Export + nicht mitwaechst -- derselbe Fehler, an dem dieses Haus schon + viermal haengengeblieben ist. + =================================================================== */ { - const dateien = readdirSync(SERVER).filter((n) => n.startsWith("pruef-") && n.endsWith(".mjs")); - ok(dateien.length >= 30, `${dateien.length} Pruefdateien durchgesehen`); + /** Kommentare und Zeichenketten weg, Zeilen behalten. Sonst zaehlt + * ein Name in einem SQL-Text oder in einer Begruendung als Aufruf. */ + const nurCode = (text) => text + .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")) + .replace(/^\s*\/\/.*$/gm, "") + .replace(/`(?:[^`\\]|\\.)*`/g, (m) => m.replace(/[^\n]/g, " ")) + .replace(/"(?:[^"\\\n]|\\.)*"/g, "\"\"") + .replace(/'(?:[^'\\\n]|\\.)*'/g, "''"); + + /** Was eine Datei nach aussen gibt -- aus ihren eigenen Zeilen. */ + const exporte = (text) => { + const raus = new Set(); + for (const t of text.matchAll( + /^export\s+(?:async\s+)?(?:function|const|let|class)\s+(\w+)/gm)) raus.add(t[1]); + return raus; + }; + + const quellen = ["workspace.js", "helfer-zeit.mjs", "helfer-tag.mjs"]; + const hausNamen = new Map(); // Name -> woher + for (const q of quellen) { + for (const n of exporte(readFileSync(join(SERVER, q), "utf8"))) hausNamen.set(n, q); + } + ok(hausNamen.size >= 30, + `${hausNamen.size} ausgeleitete Namen aus ${quellen.length} Quellen gelesen`); + + /** Welche Namen kennt diese Datei? Eingefuehrte, selbst erklaerte + * und aus einer Zerlegung entnommene -- alles, was nicht zu einem + * ReferenceError fuehren kann. */ + const bekannt = (code) => { + const kennt = new Set(); + for (const t of code.matchAll(/import\s*\{([^}]*)\}\s*from/g)) { + for (const stueck of t[1].split(",")) { + const n = stueck.trim().split(/\s+as\s+/).pop().trim(); + if (n) kennt.add(n); + } + } + for (const t of code.matchAll(/(?:const|let|var)\s*\{([^}]*)\}\s*=/g)) { + for (const stueck of t[1].split(",")) { + const n = stueck.trim().split(":").pop().trim(); + if (n) kennt.add(n); + } + } + for (const t of code.matchAll( + /(?:^|\s)(?:const|let|var|function|class)\s+(\w+)/g)) kennt.add(t[1]); + for (const t of code.matchAll(/import\s+(\w+)\s+from/g)) kennt.add(t[1]); + return kennt; + }; + + const dateien = readdirSync(SERVER) + .filter((n) => (n.endsWith(".js") || n.endsWith(".mjs")) && !quellen.includes(n)); + const fehlt = []; + let gesucht = 0; + for (const datei of dateien) { + if (datei === "pruef-struktur.mjs") continue; /* siehe Gegenprobe */ + const code = nurCode(readFileSync(join(SERVER, datei), "utf8")); + const kennt = bekannt(code); + for (const [name, woher] of hausNamen) { + /* Nur ein AUFRUF zaehlt. Ein Name in einer Eigenschaft + (`x.heuteLokal`) ist etwas anderes und gehoert nicht hierher. */ + const ruft = new RegExp(`(^|[^\\w.])${name}\\s*\\(`, "m").test(code); + if (!ruft) continue; + gesucht++; + if (!kennt.has(name)) fehlt.push(`${datei}: ${name}() (aus ${woher})`); + } + } + ok(gesucht > 50, `${gesucht} Aufrufe von Hausnamen in ${dateien.length} Dateien angesehen`); + ok(fehlt.length === 0, + fehlt.length + ? `rufen etwas, das sie nicht eingefuehrt haben — das faellt erst beim` + + ` Aufruf um, nicht beim Laden:\n ` + fehlt.slice(0, 12).join("\n ") + : "jeder Aufruf eines Hausnamens hat auch seine Einfuhr"); + + /* ---- GEGENPROBEN -------------------------------------------------- + Die Zeile darueber meldet „in Ordnung", solange sie nichts findet. + Heute Nacht haette sie elf Dateien gefunden -- also einmal zeigen, + dass sie das auch kann. */ + { + const probeFehlt = (code, name) => { + const c = nurCode(code); + const ruft = new RegExp(`(^|[^\\w.])${name}\\s*\\(`, "m").test(c); + return ruft && !bekannt(c).has(name); + }; + ok(probeFehlt('const t = heuteLokal();', "heuteLokal"), + "Gegenprobe: ein Aufruf ohne Einfuhr faellt auf"); + ok(!probeFehlt('import { heuteLokal } from "./workspace.js";\nconst t = heuteLokal();', + "heuteLokal"), " mit Einfuhr NICHT"); + ok(!probeFehlt('const { heuteLokal } = await import("./workspace.js");\n' + + 'const t = heuteLokal();', "heuteLokal"), " und ueber eine Zerlegung auch nicht"); + ok(!probeFehlt('function heuteLokal() { return 1; }\nconst t = heuteLokal();', + "heuteLokal"), " wer ihn selbst erklaert, ebenfalls nicht"); + ok(!probeFehlt('/* heuteLokal() waere hier falsch */\nconst t = 1;', "heuteLokal"), + " und ein Name im Kommentar ist kein Aufruf"); + ok(!probeFehlt('const t = uhr.heuteLokal();', "heuteLokal"), + " eine Eigenschaft an einem Objekt auch nicht"); + } +} + +console.log(`\n=== Wird der TAG in Ortszeit gebildet -- ueberall? ===`); +/* =================================================================== + AM 01.10.2026 UM 01:22 GEWACHSEN, weil diese Wache versagt hatte. + + Sie sah seit dem 06.09. nur in die `pruef-*.mjs`. In der ANWENDUNG + standen zur selben Zeit SECHZEHN Stellen, die einen Kalendertag aus + UTC bilden -- zehn davon schrieben ihn in die Datenbank. + + Aufgefallen ist es nicht hier, sondern in pruef-kreislauf: Ein + Wunsch mit dem heutigen Datum wurde zu einem Termin, und der trug + `2026-09-30` statt `2026-10-01`. Auf dem Brett „Was ansteht" landete + er damit unter „Vorbei" -- einem Abschnitt, der von sich aus + zugeklappt ist. Der Termin war da und war nicht zu sehen. + + Eine Sicherung, die nur die halbe Menge kennt, faellt in der + anderen Haelfte aus -- lautlos, denn sie meldet ja „nichts + gefunden". Deshalb sieht sie jetzt in BEIDES: in die Anwendung und + in die Pruefungen. + =================================================================== */ +{ + const pruefungen = readdirSync(SERVER) + .filter((n) => (n.startsWith("pruef-") || n.startsWith("mess-")) && n.endsWith(".mjs")); + /* DIE ANWENDUNG -- abgeleitet, nicht aufgezaehlt: alles, was der + Dienst laedt, liegt als `.js` im Serverordner. Eine Liste von + Dateinamen waere die naechste, die beim uebernaechsten Modul + nicht mitwaechst. */ + const anwendung = readdirSync(SERVER).filter((n) => n.endsWith(".js")); + const dateien = [...anwendung, ...pruefungen]; + ok(anwendung.length >= 30 && pruefungen.length >= 30, + `${anwendung.length} Dateien der Anwendung und ${pruefungen.length} Pruef-/Messdateien durchgesehen`); + + /* EINE UHRZEIT IM AUSDRUCK HEISST: DER TAG STEHT SCHON FEST. + `Date.parse(tag + "T12:00:00Z") + n * 86400000` bleibt in jeder + Zeitzone am selben Kalendertag -- das ist der dokumentierte Weg + des Hauses (workspace-kalender, workspace-teamlage, + workspace-serien). Erkannt wird das am Ausdruck selbst; eine + Liste erlaubter Ausnahmen braucht es dadurch nicht, und eine + Liste, die niemand pflegt, waere die naechste Stelle, an der es + auseinanderlaeuft. */ + const festeUhrzeit = /T\d{2}:\d{2}:\d{2}Z/; + + /** Alle Stellen einer Datei, an denen aus einem ZEITPUNKT ein + * Kalendertag wird. Gibt Zeilennummern zurueck. */ + function utcTage(text) { + const treffer = []; + const zeile = (i) => text.slice(0, i).split("\n").length; + + /* (1) Direkt: new Date(...).toISOString().slice(0, 10) */ + for (const t of text.matchAll( + /new Date\(([^;]*?)\)\s*\.\s*toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) { + if (festeUhrzeit.test(t[1])) continue; + treffer.push(zeile(t.index)); + } + + /* (2) Ueber einen NAMEN. Zuerst sammeln, welche Namen einen + UTC-Zeitstempel liefern -- als Funktion ODER als Wert. Genau + diese Schreibweise fehlte bis heute: + + const jetzt = () => new Date().toISOString(); + ... jetzt().slice(0, 10) ... + + Und in zwei Schritten kommt sie auch vor: + + const jetztIso = jetzt(); + ... jetztIso.slice(0, 10) ... */ + const utcNamen = new Set(); + for (const t of text.matchAll( + /(?:const|let)\s+(\w+)\s*=\s*(?:\(\)\s*=>\s*)?new Date\(\)\.toISOString\(\)/g)) { + utcNamen.add(t[1]); + } + for (let runde = 0; runde < 2; runde++) { + for (const t of text.matchAll(/(?:const|let)\s+(\w+)\s*=\s*(\w+)\(\)\s*;/g)) { + 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)) { + if (utcNamen.has(t[1])) treffer.push(zeile(t.index)); + } + return treffer; + } const verdaechtig = []; for (const datei of dateien) { - /* Die eigene Datei bleibt aussen vor: Die Gegenprobe unten MUSS - das gesuchte Muster als Text enthalten, sonst könnte sie nicht - beweisen, dass sie es erkennt. Sie würde sich sonst selbst - anzeigen -- eine Warnung, die immer kommt, und die liest bald - niemand mehr. */ + /* 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; /* KOMMENTARE RAUS, ZEILEN BEHALTEN (16.09.2026). Hier stand `replace(…, " ")` -- ein Blockkommentar ueber zwanzig Zeilen wurde also zu einem Leerzeichen, und ab da zaehlte `split("\n").length` falsch. Gemeldet wurden Zeilennummern, die - auf voellig anderen Code zeigten: pruef-eskalation.mjs:43 ist - eine Zeile ueber Kekse. - - Wer einer solchen Meldung nachgeht, findet dort nichts, haelt - die Pruefung fuer kaputt und sieht beim naechsten Mal gar nicht - mehr nach. Jetzt bleibt je Zeile eine Zeile stehen. */ + auf voellig anderen Code zeigten. Jetzt bleibt je Zeile eine + Zeile stehen. */ const text = readFileSync(join(SERVER, datei), "utf8") - .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")); - /* Das Muster ist eng gefasst: NUR toISOString() gefolgt von einem - Schnitt auf zehn Zeichen -- also genau die Bildung eines - Tagesdatums. `new Date().toISOString()` allein (ein voller - Zeitstempel) ist voellig in Ordnung und faellt nicht darunter. */ - for (const treffer of text.matchAll(/toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) { - const zeile = text.slice(0, treffer.index).split("\n").length; - verdaechtig.push(`${datei}:${zeile}`); - } - /* Und die zweite Schreibweise: erst in eine Variable, dann geschnitten. */ - for (const treffer of text.matchAll(/=\s*(\w+)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) { - const name = treffer[1]; - /* Nur melden, wenn diese Variable aus toISOString() stammt. */ - if (new RegExp(`(const|let)\\s+${name}\\s*=\\s*new Date\\(\\)\\.toISOString\\(\\)`).test(text)) { - const zeile = text.slice(0, treffer.index).split("\n").length; - verdaechtig.push(`${datei}:${zeile} (über ${name})`); - } - } + .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")) + .replace(/^\s*\/\/.*$/gm, ""); + for (const z of utcTage(text)) verdaechtig.push(`${datei}:${z}`); } ok(verdaechtig.length === 0, verdaechtig.length ? `bilden ihr Tagesdatum aus UTC statt Ortszeit — nachts zwischen ` - + `00:00 und 02:00 schlagen sie fälschlich an:\n ` + + `00:00 und 02:00 liegt es einen Tag zurueck:\n ` + verdaechtig.join("\n ") - : `keine Prüfung bildet ihr Tagesdatum aus UTC (alle nutzen helfer-zeit.mjs)`); + : `keine der ${dateien.length} Dateien bildet einen Kalendertag aus UTC`); - /* GEGENPROBE: Findet das Muster überhaupt etwas? Ohne diesen - Nachweis wäre "0 Treffer" auch dann grün, wenn der reguläre - Ausdruck gar nichts erkennt. */ - const probe = "const jetzt = new Date().toISOString();\nconst heute = jetzt.slice(0, 10);"; - const erkanntA = /toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/ - .test("new Date().toISOString().slice(0, 10)"); - const erkanntB = /=\s*(\w+)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/.test(probe) - && /(const|let)\s+jetzt\s*=\s*new Date\(\)\.toISOString\(\)/.test(probe); - ok(erkanntA && erkanntB, - "Gegenprobe: beide Schreibweisen werden erkannt (direkt und über eine Variable)"); + /* ---- GEGENPROBEN ------------------------------------------------- + Eine Erkennung, die nichts findet, sieht genauso aus wie eine, die + nichts finden KANN. Heute Nacht war sie genau das: Die Schreibweise + mit einer Funktion kannte sie nicht, und die Anwendung sah sie gar + nicht an. Beides steht jetzt hier als Probe. */ + const probe = (code) => utcTage(code).length; + ok(probe('const h = new Date().toISOString().slice(0, 10);') === 1, + "Gegenprobe: die direkte Schreibweise faellt auf"); + ok(probe('const jetzt = () => new Date().toISOString();\nconst h = jetzt().slice(0, 10);') === 1, + " die ueber eine FUNKTION ebenfalls — genau die, die heute Nacht durchrutschte"); + ok(probe('const jetzt = new Date().toISOString();\nconst h = jetzt.slice(0, 10);') === 1, + " und die ueber eine Variable"); + ok(probe('const jetzt = () => new Date().toISOString();\n' + + 'const iso = jetzt();\nconst h = iso.slice(0, 10);') === 1, + " 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()"); - /* Und ein harmloser Zeitstempel darf NICHT anschlagen -- sonst wäre - die Prüfung eine Warnung, die immer kommt. */ - const harmlos = /toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/ - .test("const jetzt = new Date().toISOString();"); - ok(!harmlos, "Gegenprobe: ein voller Zeitstempel (ohne Schnitt) schlägt nicht an"); + ok(probe('const jetzt = new Date().toISOString();') === 0, + "Gegenprobe: ein voller Zeitstempel ohne Schnitt schlaegt NICHT an"); + ok(probe('const b = new Date(Date.parse(tag + "T12:00:00Z") + n * 86400000)' + + '.toISOString().slice(0, 10);') === 0, + " und die Rechnung auf UTC-Mittag ebenfalls nicht — die ist richtig so"); + ok(probe('const alsText = (d) => d.toISOString().slice(0, 10);') === 0, + " und ein Date, das von aussen kommt, auch nicht"); } /* ======================================================================= diff --git a/server/pruef-treff-start.mjs b/server/pruef-treff-start.mjs index 28711b8b..52cae6b2 100644 --- a/server/pruef-treff-start.mjs +++ b/server/pruef-treff-start.mjs @@ -49,6 +49,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(180_000, "pruef-treff-start"); await new Promise((r) => setTimeout(r, 700)); @@ -301,7 +302,7 @@ melde("=== 3. Fuenfzehn Regeln auf einmal ==="); const r = await fetch(`${BASIS}/workspace/api/bereich/treff`, { method: "POST", headers: kopf(dogi), body: JSON.stringify({ art: "frage", titel: `Frage Nummer ${i}`, - datum: jetzt.slice(0, 10) }), + datum: heuteLokal() }), }); letzte = r.status; if (r.status === 429) break; diff --git a/server/pruef-treff.mjs b/server/pruef-treff.mjs index 87261127..33075d09 100644 --- a/server/pruef-treff.mjs +++ b/server/pruef-treff.mjs @@ -61,6 +61,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(240_000, "pruef-treff"); await new Promise((r) => setTimeout(r, 700)); @@ -126,9 +127,9 @@ const BRETTER = ["anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln" vergessen. */ const PERSOENLICH = ["Vertraulich melden", "Mein Steckbrief", "Support"]; for (const b of BRETTER) rein.run(b, Object.keys((await import("./workspace-bereiche.js")).BEREICHE[b].arten)[0], - `CREW-PROBE-${b.toUpperCase()}`, "", jetzt.slice(0, 10), jetzt, idDogi); + `CREW-PROBE-${b.toUpperCase()}`, "", heuteLokal(), jetzt, idDogi); /* Und einer im Haus, den die Community NICHT sehen darf. */ -rein.run("ideen", "content", "HAUS-GEHEIM-4711", "", jetzt.slice(0, 10), jetzt, idDogi); +rein.run("ideen", "content", "HAUS-GEHEIM-4711", "", heuteLokal(), jetzt, idDogi); d.close(); /* Die Methode ist ein eigenes Argument, seit es Wege gibt, die PUT @@ -851,7 +852,7 @@ melde("=== 6. Schreiben ==="); Seiten der Grenze gemessen, sonst beweist es nichts: Eine Schreibsperre, die immer sperrt, ist keine Stufe. */ const neu = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, - { art: "hallo", titel: "GAST-ZU-FRUEH-0001", text: "Hallo zusammen.", datum: jetzt.slice(0, 10) }); + { art: "hallo", titel: "GAST-ZU-FRUEH-0001", text: "Hallo zusammen.", datum: heuteLokal() }); ok(neu.code === 403, `wer neu ist, schreibt noch nicht (${neu.code})`); const lage0 = JSON.parse((await roh("/workspace/api/treff/lage", CREW, { cookie: gast.keks })).text); @@ -867,7 +868,7 @@ melde("=== 6. Schreiben ==="); dd.close(); const a = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, - { art: "hallo", titel: "GAST-SCHREIBT-8899", text: "Hallo zusammen.", datum: jetzt.slice(0, 10) }); + { art: "hallo", titel: "GAST-SCHREIBT-8899", text: "Hallo zusammen.", datum: heuteLokal() }); ok(a.code === 200 || a.code === 201, `nach acht Tagen schon (${a.code})`); /* UND SECHS TAGE REICHEN NICHT. Die andere Seite derselben Grenze -- @@ -878,7 +879,7 @@ melde("=== 6. Schreiben ==="); .run(new Date(Date.now() - 6 * 86400000).toISOString(), idGast); dd2.close(); const knapp = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, - { art: "hallo", titel: "GAST-ZU-FRUEH-0002", text: "Und?", datum: jetzt.slice(0, 10) }); + { art: "hallo", titel: "GAST-ZU-FRUEH-0002", text: "Und?", datum: heuteLokal() }); ok(knapp.code === 403, `sechs Tage reichen nicht (${knapp.code})`); const dd3 = new DatabaseSync(process.env.WORKSPACE_DB); dd3.prepare("UPDATE personen SET erstellt = ? WHERE id = ?") @@ -888,7 +889,7 @@ melde("=== 6. Schreiben ==="); /* Und auf ein Brett des Teams kommt er auch als "Dabei" nicht. */ const brettDesTeams = await roh("/workspace/api/bereich/anschlag", CREW, { cookie: gast.keks }, { art: Object.keys(BEREICHE.anschlag.arten)[0], titel: "GAST-AM-ANSCHLAG-0001", - text: "Ansage", datum: jetzt.slice(0, 10) }); + text: "Ansage", datum: heuteLokal() }); ok(brettDesTeams.code === 403, `aufs Anschlagbrett schreibt nur das Team (${brettDesTeams.code})`); const beiDogi = await brett("treff", HAUS, kDogi); @@ -901,7 +902,7 @@ melde("=== 6. Schreiben ==="); { /* UND NICHT IN EIN BRETT DES HAUSES. */ const a = await roh("/workspace/api/bereich/ideen", CREW, { cookie: gast.keks }, - { art: "content", titel: "GAST-IM-HAUS-0000", text: "", datum: jetzt.slice(0, 10) }); + { art: "content", titel: "GAST-IM-HAUS-0000", text: "", datum: heuteLokal() }); ok(a.code !== 200 && a.code !== 201, `aber nicht in ein Brett des Hauses (${a.code})`); } diff --git a/server/pruef-uebergang.mjs b/server/pruef-uebergang.mjs index b248f8dc..d22ea2d5 100644 --- a/server/pruef-uebergang.mjs +++ b/server/pruef-uebergang.mjs @@ -43,6 +43,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(240_000, "pruef-uebergang"); await new Promise((r) => setTimeout(r, 700)); @@ -100,7 +101,7 @@ let idDogi, idHand, idFrida, idMara, idNina, idTalent, idZweit; const neuTalent = (titel) => { d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)" + " VALUES ('talente','ruhig',?,?,?,?,?,?)") - .run(titel, "Faellt seit Wochen auf.", jetzt.slice(0, 10), jetzt, idDogi, "Dienstag im Stream"); + .run(titel, "Faellt seit Wochen auf.", heuteLokal(), jetzt, idDogi, "Dienstag im Stream"); return d.prepare("SELECT last_insert_rowid() AS id").get().id; }; idTalent = neuTalent("Kessi aus dem Chat"); diff --git a/server/pruef-uebernahme.mjs b/server/pruef-uebernahme.mjs index dbb919ee..cf3361c9 100644 --- a/server/pruef-uebernahme.mjs +++ b/server/pruef-uebernahme.mjs @@ -42,6 +42,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(180_000, "pruef-uebernahme"); await new Promise((r) => setTimeout(r, 700)); @@ -92,14 +93,14 @@ let idDogi, idLuna, idIdee, idTreff, idIdeeHoch; (bereich, art, titel, text, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,'offen',?,?,?)`); rein.run("ideen", "content", "Reihe über Technik", "Drei Teile, je fünf Minuten.", - jetzt.slice(0, 10), "mittel", idLuna, jetzt, idDogi); + heuteLokal(), "mittel", idLuna, jetzt, idDogi); idIdee = d.prepare("SELECT last_insert_rowid() AS id").get().id; rein.run("ideen", "content", "Dringende Idee", "Muss diese Woche.", - jetzt.slice(0, 10), "hoch", null, jetzt, idDogi); + heuteLokal(), "hoch", null, jetzt, idDogi); idIdeeHoch = d.prepare("SELECT last_insert_rowid() AS id").get().id; d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von) VALUES ('treff','hallo','Beitrag aus dem Rudel','Hallo zusammen',?,?,?)`) - .run(jetzt.slice(0, 10), jetzt, idDogi); + .run(heuteLokal(), jetzt, idDogi); idTreff = d.prepare("SELECT last_insert_rowid() AS id").get().id; d.close(); } @@ -314,7 +315,7 @@ melde("=== 8. Im Browser ==="); d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, dringlichkeit, status, erstellt, erstellt_von) VALUES ('ideen','content','BILDSCHIRM-IDEE','Zum Anklicken.',?,'mittel','offen',?,?)`) - .run(jetzt.slice(0, 10), jetzt, idDogi); + .run(heuteLokal(), jetzt, idDogi); d.close(); await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" }); diff --git a/server/pruef-video.mjs b/server/pruef-video.mjs index f19a9649..8e021708 100644 --- a/server/pruef-video.mjs +++ b/server/pruef-video.mjs @@ -125,6 +125,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(180_000, "pruef-video"); await new Promise((r) => setTimeout(r, 700)); @@ -503,7 +504,7 @@ melde("=== 5c. Aus einem Wunsch wird ein Video ==="); const { lastInsertRowid } = db3.prepare(` INSERT INTO eintraege (bereich, art, titel, datum, status, erstellt, erstellt_von) VALUES (?,?,?,?, 'offen', ?, (SELECT id FROM personen LIMIT 1))`) - .run(bereich, art, titel, nun.slice(0, 10), nun); + .run(bereich, art, titel, heuteLokal(), nun); return Number(lastInsertRowid); }; const wunschId = anlegen("wunsch", "wunsch", "Bitte mal Casper beim Baden zeigen"); diff --git a/server/pruef-wege-nach-draussen.mjs b/server/pruef-wege-nach-draussen.mjs index b7375482..ac543884 100644 --- a/server/pruef-wege-nach-draussen.mjs +++ b/server/pruef-wege-nach-draussen.mjs @@ -51,6 +51,7 @@ 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"; +import { heuteLokal } from "./helfer-tag.mjs"; await import("./index.js"); notbremse(150_000, "pruef-wege-nach-draussen"); await new Promise((r) => setTimeout(r, 700)); @@ -308,20 +309,20 @@ const VIDEO = "https://www.tiktok.com/@dogfather0804/video/7500000000000000000"; d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von," + " quelle_url, quelle_kanal) VALUES (?,?,?,?,?,?,?,?,?,?)") .run("highlight", "clip", "Wer ist euer Komet", "Schreibt es in die Kommentare", - jetzt.slice(0, 10), "offen", jetzt, wer, VIDEO, "tiktok"); + heuteLokal(), "offen", jetzt, wer, VIDEO, "tiktok"); /* (b) ohne Video -- darf NICHT klickbar sein */ d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von)" + " VALUES (?,?,?,?,?,?,?,?)") .run("highlight", "clip", "Nur ein Bild", "Ohne Quelle", - jetzt.slice(0, 10), "offen", jetzt, wer); + heuteLokal(), "offen", jetzt, wer); /* (c) Video bei TikTok geloescht -- darf NICHT klickbar sein, sonst fuehrt der Klick auf eine Fehlerseite und WIR sehen kaputt aus */ d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von," + " quelle_url, quelle_kanal, quelle_fehlt_seit) VALUES (?,?,?,?,?,?,?,?,?,?,?)") .run("highlight", "clip", "Nicht mehr da", "Wurde geloescht", - jetzt.slice(0, 10), "offen", jetzt, wer, VIDEO, "tiktok", jetzt); + heuteLokal(), "offen", jetzt, wer, VIDEO, "tiktok", jetzt); /* EIN ECHTES BILD AN DIE ERSTE KARTE. diff --git a/server/workspace-aufbewahrung.js b/server/workspace-aufbewahrung.js index 2db9b836..1cb9b8c5 100644 --- a/server/workspace-aufbewahrung.js +++ b/server/workspace-aufbewahrung.js @@ -56,6 +56,7 @@ import { unterstuetzungAufraeumen } from "./workspace-unterstuetzung.js"; import { db, sitzungLesen, istDogFather, protokolliere, echteIp, einstellung, einstellungSetzen, + tagLokal, } from "./workspace.js"; export const aufbewahrungRouter = express.Router(); @@ -392,7 +393,11 @@ export const FRISTEN = [ + "gezählt wird je Weg und Tag, nicht je Mensch.", offen: (d) => zaehle(d, "SELECT COUNT(*) AS n FROM unterstuetzung_striche WHERE tag < ?", - vorTagen(ZAEHLUNG_TAGE).slice(0, 10)), + /* Dieselbe Grenze wie in unterstuetzungAufraeumen -- und + dieselbe Rechnung, in Ortszeit (01.10.2026). Stuenden hier + zwei verschiedene, zeigte die Vorschau eine andere Zahl an, + als das Raeumen danach loescht. */ + tagLokal(-ZAEHLUNG_TAGE)), raeumen: () => unterstuetzungAufraeumen(), }, { diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index e7801120..93975e4f 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -547,7 +547,10 @@ aufgabenRouter.get("/workspace/api/uebersicht", (req, res) => { .prepare(`SELECT COUNT(*) AS n ${VERBUND} WHERE ${wo}${zusatz}`) .get(...werte, ...extra).n; - const heute = jetzt().slice(0, 10); + /* ORTSZEIT (01.10.2026): `jetzt().slice(0, 10)` waere der UTC-Tag. + Nachts zwischen 00:00 und 02:00 haette eine Aufgabe mit Frist + „gestern" noch als nicht ueberfaellig gegolten. */ + const heute = heuteLokal(); /* ABGEBROCHENE SIND NICHT "NOCH ZU TUN" (08.09.2026). diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 72960e72..fffa218f 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -1775,7 +1775,13 @@ function pruefe(bereich, körper, { neu }) { } if (neu || körper.datum !== undefined) { const d = String(körper.datum ?? "").trim(); - if (!d) aus.datum = jetzt().slice(0, 10); + /* ORTSZEIT, NICHT UTC (01.10.2026). Hier stand + `jetzt().slice(0, 10)` -- der UTC-Tag. Zwischen 00:00 und 02:00 + Ortszeit ist das der VORTAG: Ein Eintrag ohne Datum, nachts + angelegt, trug gestern. Bei „Was ansteht" landet er damit im + Abschnitt „Vorbei", und der ist von sich aus zugeklappt -- der + Eintrag ist also da und trotzdem nicht zu sehen. */ + if (!d) aus.datum = heuteLokal(); else if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || Number.isNaN(Date.parse(d))) { fehler.push("Datum ist ungültig."); } else aus.datum = d; @@ -2130,7 +2136,10 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (r erstellt_von, haus) VALUES (?,?,?,?,?, 'offen', ?, ?, ?)`) .run(bereich, v.art, v.titel, v.text ?? null, - jetzt().slice(0, 10), jetzt(), req.person.id, req.person.haus || null); + /* Der TAG in Ortszeit, der ZEITSTEMPEL in UTC -- das ist kein + Widerspruch: Ein Zeitpunkt ist ueberall derselbe, ein + Kalendertag nicht. */ + heuteLokal(), jetzt(), req.person.id, req.person.haus || null); /* UND SOFORT FREIGEBEN (17.09.2026) -- der Fehler, den Filipe auf seinem Bildschirm gesehen hat. @@ -2243,7 +2252,14 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/:id/daraus", VALUES (?,?,?,?,?, 'offen', ?,?,?,?)`) .run(nach, art, quelle.titel, String(req.body?.text ?? "").trim().slice(0, TEXT_MAX) || null, - jetzt().slice(0, 10), jetzt(), req.person.id, quelle.id, + /* DIE STELLE, AN DER ES AUFGEFALLEN IST (01.10.2026). + pruef-kreislauf legt einen Wunsch mit dem heutigen Datum an + und macht daraus einen Termin; gemessen um 01:22 kam + `2026-09-30` heraus statt `2026-10-01`. Der Termin lag + damit in der Vergangenheit, stand auf dem Brett unter + „Vorbei" und war zugeklappt -- waehrend beim Wunsch + „Daraus wurde ein Termin" zu lesen war. */ + heuteLokal(), jetzt(), req.person.id, quelle.id, req.person.haus || null); protokolliere("bereich_daraus", { diff --git a/server/workspace-bewerbung.js b/server/workspace-bewerbung.js index de854fad..47b33e9b 100644 --- a/server/workspace-bewerbung.js +++ b/server/workspace-bewerbung.js @@ -548,7 +548,8 @@ bewerbungRouter.put("/workspace/api/bewerbung/:id/weg", express.json({ limit: "8 .run(b.nenn_name, `Hat sich selbst gemeldet (Anfrage #${id}).` + (antworten.get("warum") ? `\n\nWarum: ${antworten.get("warum")}` : ""), - t.slice(0, 10), t, req.person.id, einsatz); + /* Der TAG in Ortszeit (01.10.2026), der Zeitstempel in UTC. */ + heuteLokal(), t, req.person.id, einsatz); eintragId = Number(lastInsertRowid); /* AUF „ANGESPROCHEN" und nicht auf „aufgefallen": Die ersten diff --git a/server/workspace-content.js b/server/workspace-content.js index fcc8a347..466063d0 100644 --- a/server/workspace-content.js +++ b/server/workspace-content.js @@ -27,6 +27,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, tagLokal, + heuteLokal, } from "./workspace.js"; export const contentRouter = express.Router(); @@ -39,7 +40,10 @@ const NAME_MAX = 40; const SAEULEN_MAX = 5; const jetzt = () => new Date().toISOString(); -const heute = () => jetzt().slice(0, 10); +/* ORTSZEIT (01.10.2026). Stand hier als `jetzt().slice(0, 10)` -- + der UTC-Tag, also nachts der Vortag. Dieses `heute()` schreibt das + Datum neuer Eintraege, der Fehler waere dauerhaft in den Daten. */ +const heute = () => heuteLokal(); function angemeldet(req, res, next) { const person = sitzungLesen(req); diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index 61f9deb7..87b57448 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -62,6 +62,7 @@ import express from "express"; import { db, sitzungLesen, protokolliere, echteIp, fuehrtTeamDogi, ROLLEN_NAME, ROLLEN_SORTIERUNG, MODI_KATEGORIEN, kategorienFuer, darfKatalogBekommen, + tagLokal, } from "./workspace.js"; /* Die Aufgaben, die ein Neuer bekommen kann -- dieselbe Liste, aus der auch die Aufgabenseite ihre Vorschlaege baut. Eine zweite waere die, @@ -1145,7 +1146,12 @@ entwicklungRouter.post("/workspace/api/entwicklung/person/:id/:schluessel/schrit AND status NOT IN ('erledigt','abgebrochen')`).get(id, schluessel); if (offen) return res.status(400).json({ fehler: "schon_da", aufgabe_id: offen.id }); - const frist = new Date(Date.now() + SCHRITT_TAGE * 86400000).toISOString().slice(0, 10); + /* ORTSZEIT (01.10.2026). Mit dem UTC-Tag lag die Frist nachts + zwischen 00:00 und 02:00 einen Tag zu frueh -- derselbe + Fehler, der am 30.09. schon einmal bei „morgen" gefunden + wurde. Eine Frist, die einen Tag zu frueh steht, macht eine + Aufgabe ueberfaellig, die es nicht ist. */ + const frist = tagLokal(SCHRITT_TAGE); const jetztIso = jetzt(); db().prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, verantwortlich_id, diff --git a/server/workspace-ics.js b/server/workspace-ics.js index ffbb4f5b..66ccc9e8 100644 --- a/server/workspace-ics.js +++ b/server/workspace-ics.js @@ -54,6 +54,7 @@ import express from "express"; import { randomBytes, timingSafeEqual } from "node:crypto"; import { db, protokolliere, echteIp, sitzungLesen, termineSichtbar, externSql, + tagLokal, } from "./workspace.js"; export const icsRouter = express.Router(); @@ -335,9 +336,13 @@ icsRouter.get("/workspace/kalender/:datei", (req, res) => { /* Ein Jahr rueckwaerts, ein Jahr vorwaerts. Alles auszuliefern waere bei jedem Abruf die ganze Geschichte; ein Kalender braucht sie nicht, und die Datei waechst sonst ohne Ende. */ - const heute = new Date(); - const vor = new Date(heute.getTime() - 365 * 86400000).toISOString().slice(0, 10); - const nach = new Date(heute.getTime() + 365 * 86400000).toISOString().slice(0, 10); + /* ORTSZEIT (01.10.2026). Der Schaden waere hier klein -- ein + Fenster von zwei Jahren verschiebt sich um einen Tag --, aber + eine Ausnahme von der Regel ist teurer als die Aenderung: Wer + spaeter eine Liste der erlaubten Ausnahmen pflegen muss, pflegt + sie nicht. */ + const vor = tagLokal(-365); + const nach = tagLokal(365); const termine = db().prepare(` SELECT ${SPALTEN} ${VERBUND} diff --git a/server/workspace-reports.js b/server/workspace-reports.js index 966f4161..90cd3c1c 100644 --- a/server/workspace-reports.js +++ b/server/workspace-reports.js @@ -21,6 +21,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, siehtAlles, sichtbareCreatorIds, + heuteLokal, tagLokal, } from "./workspace.js"; import { verteilen } from "./workspace-womit.js"; import { BEREICHE } from "./workspace-bereiche.js"; @@ -29,6 +30,10 @@ export const reportRouter = express.Router(); const jetzt = () => new Date().toISOString(); const tagOffset = (tage) => new Date(Date.now() - tage * 86400_000).toISOString(); +/* Derselbe Versatz als TAG -- in Ortszeit (01.10.2026). `tagOffset` + bleibt, wo ein voller Zeitstempel gebraucht wird; wo ein + Kalendertag gemeint ist, zaehlt die Ortszeit. */ +const tagOffsetTag = (tage) => tagLokal(-tage); function angemeldet(req, res, next) { const person = sitzungLesen(req); @@ -101,11 +106,13 @@ reportRouter.get("/workspace/api/report", (req, res) => { const creator = zielCreator(req); const jetztIso = jetzt(); - const heute = jetztIso.slice(0, 10); + /* ORTSZEIT (01.10.2026), nicht der UTC-Tag -- sonst faengt der + Zeitraum eines Berichts nachts einen Tag zu frueh an. */ + const heute = heuteLokal(); const abIso = tagOffset(tage); const davorIso = tagOffset(tage * 2); - const ab = abIso.slice(0, 10); - const davor = davorIso.slice(0, 10); + const ab = tagOffsetTag(tage); + const davor = tagOffsetTag(tage * 2); /* Einschraenkung auf einen Creator, je Tabelle einmal formuliert. */ const nurA = creator ? " AND (a.creator_id = ? OR a.verantwortlich_id = ?)" : ""; diff --git a/server/workspace-scouts.js b/server/workspace-scouts.js index 02889ead..2154f6f3 100644 --- a/server/workspace-scouts.js +++ b/server/workspace-scouts.js @@ -18,6 +18,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, siehtAlles, pipelineIds, + heuteLokal, } from "./workspace.js"; export const scoutRouter = express.Router(); @@ -131,7 +132,9 @@ const VERBUND = ` scoutRouter.get("/workspace/api/leads", (req, res) => { try { const { wo, werte } = sichtbar(req.sicht || req.person); - const heute = jetzt().slice(0, 10); + /* ORTSZEIT (01.10.2026), nicht der UTC-Tag: Sonst gilt ein Lead + nachts zwischen 00:00 und 02:00 als einen Tag juenger. */ + const heute = heuteLokal(); res.json({ stufen: STUFEN, /* Die Auswahllisten kommen MIT, damit die Oberflaeche keine diff --git a/server/workspace-treff.js b/server/workspace-treff.js index d2de5902..b9e9190e 100644 --- a/server/workspace-treff.js +++ b/server/workspace-treff.js @@ -75,6 +75,7 @@ import express from "express"; import { db, sitzungLesen, protokolliere, echteIp, TREFF_BRETTER, TREFF_ROLLEN, ROLLEN_NAME, istHand, + heuteLokal, } from "./workspace.js"; /* Die Tabellen und die Sperrfrage stehen in einem Blatt ohne eigene Importe -- workspace.js braucht beides ebenfalls, und ein Importkreis @@ -1023,7 +1024,8 @@ treffRouter.post("/workspace/api/treff/mitmachen/:id/uebernehmen", gleicheHerkun .run( `${wer?.name ?? "Jemand"} hat sich im Rudel gemeldet`, `Aus „Mitmachen": ${String(e.titel ?? "")}\n\n${String(e.text ?? "")}`.trim(), - nun.slice(0, 10), e.erstellt_von, nun, req.person.id, "Das Rudel"); + /* Der TAG in Ortszeit (01.10.2026), der Zeitstempel in UTC. */ + heuteLokal(), e.erstellt_von, nun, req.person.id, "Das Rudel"); protokolliere("treff_talent", { personId: req.person.id, ip: echteIp(req), detail: `aus Eintrag ${id}` }); res.json({ ok: true, talent_id: Number(r.lastInsertRowid) }); diff --git a/server/workspace-unterstuetzung.js b/server/workspace-unterstuetzung.js index ae10f72b..6add499e 100644 --- a/server/workspace-unterstuetzung.js +++ b/server/workspace-unterstuetzung.js @@ -39,6 +39,7 @@ import express from "express"; import { db, protokolliere, echteIp, istDogFather, heuteLokal, + tagLokal, } from "./workspace.js"; import { UNTERSTUETZUNG_ARTEN, ZAEHLUNG_TAGE } from "./unterstuetzung-tabellen.js"; @@ -358,8 +359,11 @@ unterstuetzungRouter.delete("/workspace/api/unterstuetzung/:id(\\d+)", * hinsieht. */ export function unterstuetzungAufraeumen() { try { - const grenze = new Date(Date.now() - ZAEHLUNG_TAGE * 86400000) - .toISOString().slice(0, 10); + /* ORTSZEIT (01.10.2026): Die Tage in dieser Tabelle stehen in + Ortszeit, die Grenze stand in UTC. Nachts wurde dadurch ein Tag + zu wenig geraeumt -- kein Schaden, aber zwei Rechnungen fuer + denselben Begriff, und eine davon laeuft irgendwann weg. */ + const grenze = tagLokal(-ZAEHLUNG_TAGE); const r = db().prepare("DELETE FROM unterstuetzung_striche WHERE tag < ?").run(grenze); return r.changes || 0; } catch { return 0; } diff --git a/server/workspace-video.js b/server/workspace-video.js index 7bbb3cd2..563cd87c 100644 --- a/server/workspace-video.js +++ b/server/workspace-video.js @@ -47,6 +47,7 @@ import { mkdirSync, writeFileSync, unlinkSync } from "node:fs"; import { db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, KANAELE, kanalVonHandle, TREFF_BRETTER, + heuteLokal, } from "./workspace.js"; import { darfSchreiben, sofortFreigeben } from "./workspace-treff.js"; import { sichtbar } from "./workspace-bereiche.js"; @@ -282,7 +283,10 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { (bereich, art, titel, datum, status, erstellt, erstellt_von, quelle_url, quelle_kanal, quelle_geholt_am, aus_eintrag_id) VALUES (?,?,?,?, 'offen', ?,?,?,?,?,?)`) - .run(bereich, arten[bereich] || "clip", auskunft.titel, jetzt.slice(0, 10), + /* Der TAG in Ortszeit (01.10.2026), der Zeitstempel daneben in + UTC -- ein Zeitpunkt ist ueberall derselbe, ein Kalendertag + nicht. */ + .run(bereich, arten[bereich] || "clip", auskunft.titel, heuteLokal(), jetzt, req.person.id, url, ablage, jetzt, ausQuelle ? aus : null); eintragId = Number(lastInsertRowid); } @@ -434,7 +438,8 @@ export async function videoNochDa(url) { export async function videoWacheLauf() { const d = db(); const grenze = new Date(Date.now() - WACHE_ABSTAND_TAGE * 86400_000).toISOString(); - const heute = new Date().toISOString().slice(0, 10); + /* ORTSZEIT (01.10.2026) -- nicht der UTC-Tag. */ + const heute = heuteLokal(); /* WELCHE ZUERST: die mit Fehlversuchen (die sind verdaechtig und sollen schnell geklaert sein), dann die am laengsten ungesehenen. diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index 273f9836..cdc17e79 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -55,6 +55,7 @@ import { darfSchreibenMit, KATALOG_EMPFAENGER, KATALOG_EMPFAENGER_SQL, + heuteLokal, } from "./workspace.js"; /* Dieselbe Meldung wie beim anderen Bewerbungsweg -- eine Stelle, @@ -2036,7 +2037,10 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, (bereich, art, titel, text, datum, status, creator_id, hook, format, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?,?,?)`) - .run(bereich, unterart, titel, text, jetzt().slice(0, 10), "offen", + /* Der TAG in Ortszeit (01.10.2026), der Zeitstempel weiter + unten in UTC. Aus dem Startkatalog angelegte Eintraege + trugen sonst nachts das Datum von gestern. */ + .run(bereich, unterart, titel, text, heuteLokal(), "offen", creatorId, zusatz.hook || null, zusatz.format || null, jetzt(), req.person.id); angelegt.push(titel); diff --git a/server/workspace-zuteilung.js b/server/workspace-zuteilung.js index 870d7671..03559037 100644 --- a/server/workspace-zuteilung.js +++ b/server/workspace-zuteilung.js @@ -56,6 +56,7 @@ import { db, protokolliere, echteIp, darfAufgabenVerteilen, entscheidetUeberAufgaben, istDogFather, istHand, sitzungLesen, + heuteLokal, } from "./workspace.js"; import { TEAM_DOGI_ROLLEN } from "./crew-adresse.js"; /* WER ES ERFAEHRT -- an einer Stelle fuer beide Bewerbungswege. @@ -920,7 +921,11 @@ export function resuemeeFuer(personId) { zeilen.push({ zustand, bewertung: null, frist: a.frist, aufgabe_status: a.status }); } - const heute = new Date().toISOString().slice(0, 10); + /* ORTSZEIT (01.10.2026): Mit dem UTC-Tag galt eine Aufgabe, deren + Frist gestern ablief, nachts zwischen 00:00 und 02:00 noch als + puenktlich -- und die Zahl „ueberfaellig" war zwei Stunden lang + zu niedrig. */ + const heute = heuteLokal(); const zaehl = { offen: 0, angenommen: 0, arbeit: 0, erledigt: 0, abgelehnt: 0 }; let ueberfaellig = 0, gelobt = 0, zuVerbessern = 0; for (const z of zeilen) {