From 7dc5356c806c1e1099b04d3cd15e40352ec4c330 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 1 Oct 2026 01:45:16 +0200 Subject: [PATCH] Datum: der Tag kommt aus der Ortszeit, nicht aus UTC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GEFUNDEN UM 01:22, von pruef-kreislauf -- und nur, weil nachts gearbeitet wurde. Die Pruefung legt einen Wunsch mit dem HEUTIGEN Datum an und macht daraus einen Termin. Gemessen: geschickt: datum = 2026-10-01 (heuteLokal auf dem Server) gespeichert: datum = 2026-09-30 Der Termin lag also in der Vergangenheit. Auf „Was ansteht" sortiert er sich damit in den Abschnitt „Vorbei" ein -- 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 (im Winter eine), genau in den Stunden, in denen hier gearbeitet wird. URSACHE const jetzt = () => new Date().toISOString(); // UTC ... jetzt().slice(0, 10) ... // UTC-Tag ES WAR NICHT EINE STELLE. Nachgemessen: SECHZEHN in vierzehn Modulen, davon ZEHN, die den falschen Tag in die Datenbank schreiben -- Termine, Wuensche, Highlights, Talente, Leads, Videos, Vorlagen -- und sechs, die „heute" vergleichen (ueberfaellige Aufgaben, Berichte, die Frist einer Entwicklungsaufgabe). NICHT ANGEFASST, WEIL RICHTIG: Rechnungen auf einem Datumstext mit fester Uhrzeit (`Date.parse(tag + "T12:00:00Z") + n * 86400000`). Die bleiben in jeder Zeitzone am selben Kalendertag -- kalender, teamlage und serien machen es so, und das bleibt. WARUM ES NIEMAND GEMERKT HAT pruef-struktur sucht dieses Muster seit dem 06.09.2026. Aber: - sie sah NUR in die `pruef-*.mjs`, nie in die Anwendung - sie kannte die Schreibweise ueber eine FUNKTION nicht (`const jetzt = () => ...` statt `const jetzt = ...`) Die Wache stand vor den Pruefungen, nicht vor dem Haus -- derselbe Fehler wie heute Nacht bei den Messports: eine Sicherung, die nur die halbe Menge kennt, faellt in der anderen Haelfte aus, und zwar lautlos, denn sie meldet ja „nichts gefunden". Jetzt sieht sie in beides und kennt beide Schreibweisen. Beim ersten scharfen Lauf fand sie sofort 23 weitere Stellen in den Pruefdateien selbst -- dieselben Zeitbomben, gegen die sie gebaut worden war. EINE ZWEITE WACHE, WEIL ICH SELBST HINEINGELAUFEN BIN Mein Umbauwerkzeug hat in elf Modulen `heuteLokal()` eingesetzt und die Einfuhr weggelassen: Es hat erst ersetzt und DANN gefragt, ob der Name schon in der Datei steht -- da stand er, mein eigener Aufruf. `node --check` sagt dazu nichts, „Laedt jedes Server-Modul?" auch nicht: Die Datei ist syntaktisch tadellos. Erst der Aufruf faellt um mit `ReferenceError: heuteLokal is not defined`. Gefunden hat es pruef-video, zufaellig. Die anderen zehn waeren durchgerutscht. Deshalb neu: „Ruft ein Modul etwas, das es nie eingefuehrt hat?" -- die Namen des Hauses aus den export-Zeilen gelesen, nicht aufgezaehlt. 460 Aufrufe in 350 Dateien, alle mit Einfuhr. pruef-kreislauf STELLT JETZT DIE RICHTIGE FRAGE Sie war rot und hat den Fehler dabei nur gestreift: „`.kette` wird nicht sichtbar", Zeitsperre nach 15 s. Das klingt nach der Anzeige und schickt einen zur falschen Stelle. Neu: - eine Zeile fragt das DATUM (ohne Browser, nennt den Fehler beim Namen) - der Browserteil klappt zu, was zu ist, und misst dann die Kette; „gar nicht da" wird von „da und unsichtbar" unterschieden NEBENBEFUND IN pruef-ics Die Probe „fast richtig" war `echt.slice(0, -1) + "A"`. Der Schluessel ist base64url; sein letztes Zeichen ist eines von sechzehn. Endet er auf „A", IST die Probe der echte Schluessel, der Server antwortet zu Recht mit 200, und die Pruefung meldet ein Loch, das es nicht gibt -- einmal je sechzehn Laeufe. Heute Nacht zweimal hintereinander, und die Suche ging eine halbe Stunde in eine Aenderung, die damit nichts zu tun hatte. GEPRUEFT pruef-struktur 44 -> 59 Pruefungen, 0 Fehler pruef-ics 37 -> 38, 0 Fehler pruef-kreislauf Absturz bei Nr. 17 -> 25 Pruefungen, 0 Fehler und gruen geblieben: treff 85, arten 28, video 74, uebernahme 39, entwicklung 79, content 45, vorlagen 24, zuteilung 75, scout-zuteilung 37, unterstuetzung 70, aufbewahrung 45, bewerbung 91, treff-start 42, uebergang 65, nachwuchs 262, auskunft 46, modi-ideen 30, neue-seiten 109, spicy 85, wege-nach-draussen 67, aufgabenbrett 49, agentur 62, bereiche-lesend 37 -- beide Haeuser GEGENPROBEN, DIE WIRKLICH ROT WERDEN - den UTC-Tag im `daraus`-Weg wieder eingebaut: pruef-kreislauf meldet „er liegt HEUTE, nicht gestern (2026-09-30, heute ist 2026-10-01)", 25 Pruefungen, 1 Fehler -- und der Browserteil bleibt gruen, weil er jetzt aufklappt. Jede Frage bei ihrer eigenen Pruefung. - eine Einfuhr aus workspace-video.js entfernt: die neue Wache meldet „workspace-video.js: heuteLokal() (aus helfer-tag.mjs)" - beide Erkennungen je gegen einen gebauten Rueckschritt geprueft (Funktion, Variable, zwei Schritte, Date.now-Rechnung) und gegen das, was NICHT anschlagen darf (UTC-Mittag, voller Zeitstempel, fremdes Date, Name im Kommentar, Eigenschaft am Objekt) EIN FEHLER BEIM UMBAU, HIER FESTGEHALTEN: Mein erster Lauf ueber die Pruefdateien hat stumpf ersetzt und dabei KOMMENTARE umgeschrieben -- in sieben Dateien stand die alte Schreibweise als Beleg in der Begruendung, und daraus wurde das Gegenteil. Bemerkt hat es der Vergleich der Zahlen (34 Stellen statt der gemessenen 24), nicht die Absicht. Zurueckgenommen und mit Schutz fuer Kommentare und Zeichenketten wiederholt. Datenbank vorher gesichert. Keine Schemaaenderung. Co-Authored-By: Claude Opus 5 --- server/pruef-auskunft.mjs | 3 +- server/pruef-ics.mjs | 20 +- server/pruef-kreislauf.mjs | 61 +++++- server/pruef-modi-ideen.mjs | 5 +- server/pruef-nachwuchs.mjs | 3 +- server/pruef-neue-seiten.mjs | 5 +- server/pruef-spicy.mjs | 3 +- server/pruef-struktur.mjs | 287 +++++++++++++++++++++++----- server/pruef-treff-start.mjs | 3 +- server/pruef-treff.mjs | 15 +- server/pruef-uebergang.mjs | 3 +- server/pruef-uebernahme.mjs | 9 +- server/pruef-video.mjs | 3 +- server/pruef-wege-nach-draussen.mjs | 7 +- server/workspace-aufbewahrung.js | 7 +- server/workspace-aufgaben.js | 5 +- server/workspace-bereiche.js | 22 ++- server/workspace-bewerbung.js | 3 +- server/workspace-content.js | 6 +- server/workspace-entwicklung.js | 8 +- server/workspace-ics.js | 11 +- server/workspace-reports.js | 13 +- server/workspace-scouts.js | 5 +- server/workspace-treff.js | 4 +- server/workspace-unterstuetzung.js | 8 +- server/workspace-video.js | 9 +- server/workspace-vorlagen.js | 6 +- server/workspace-zuteilung.js | 7 +- 28 files changed, 444 insertions(+), 97 deletions(-) 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) {