Datum: der Tag kommt aus der Ortszeit, nicht aus UTC

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 <[email protected]>
This commit is contained in:
2026-10-01 01:45:16 +02:00
co-authored by Claude Opus 5
parent f6225d7341
commit 7dc5356c80
28 changed files with 444 additions and 97 deletions
+2 -1
View File
@@ -45,6 +45,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(180_000, "pruef-auskunft"); notbremse(180_000, "pruef-auskunft");
await new Promise((r) => setTimeout(r, 700)); await new Promise((r) => setTimeout(r, 700));
@@ -91,7 +92,7 @@ let idDogi, idRieke, idGast;
treff_entfernt.autor_id (ebenfalls ohne). */ treff_entfernt.autor_id (ebenfalls ohne). */
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)" d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)"
+ " VALUES ('treff','hallo','KESSI-EINTRAG','Hallo zusammen',?,?,?)") + " VALUES ('treff','hallo','KESSI-EINTRAG','Hallo zusammen',?,?,?)")
.run(jetzt.slice(0, 10), jetzt, idGast); .run(heuteLokal(), jetzt, idGast);
const raum = (() => { const raum = (() => {
d.prepare("INSERT INTO chat_raeume (art, name, erstellt, erstellt_von) VALUES ('gruppe','Testraum',?,?)") d.prepare("INSERT INTO chat_raeume (art, name, erstellt, erstellt_von) VALUES ('gruppe','Testraum',?,?)")
.run(jetzt, idDogi); .run(jetzt, idDogi);
+19 -1
View File
@@ -341,10 +341,28 @@ melde("\n=== Sichtbarkeit — der Weg ohne Anmeldung darf nicht mehr zeigen ==="
======================================================================= */ ======================================================================= */
melde("\n=== Falsche Schluessel ==="); 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 = [ const proben = [
["kurz", "abc"], ["kurz", "abc"],
["leer", ""], ["leer", ""],
["fast richtig", String(linkLuna).split("/").pop().replace(".ics", "").slice(0, -1) + "A"], ["fast richtig", fastRichtig],
["mit Punkten", "../../workspace/start"], ["mit Punkten", "../../workspace/start"],
["SQL-Versuch", "' OR '1'='1"], ["SQL-Versuch", "' OR '1'='1"],
]; ];
+60 -1
View File
@@ -149,6 +149,27 @@ let terminId = 0;
ok(t?.aus?.bereich === "wunsch", `und er weiss, woher er kam (${t?.aus?.bereich})`); ok(t?.aus?.bereich === "wunsch", `und er weiss, woher er kam (${t?.aus?.bereich})`);
ok(t?.aus?.von_name === "Lena", ok(t?.aus?.von_name === "Lena",
`mit dem Namen dessen, der den Wunsch schrieb ("${t?.aus?.von_name}")`); `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 = []; const konsole = [];
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); }); 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.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); 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(() => ({ const m = await seite.evaluate(() => ({
her: [...document.querySelectorAll(".kette--her")].map((z) => z.textContent.trim()), her: [...document.querySelectorAll(".kette--her")].map((z) => z.textContent.trim()),
+3 -2
View File
@@ -80,12 +80,12 @@ console.log("\n=== 1. Eine bestehende Datenbank ohne den neuen Bereich ===\n");
);`); );`);
for (let i = 1; i <= 7; i++) { for (let i = 1; i <= 7; i++) {
d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, erstellt) VALUES (?,?,?,?,?)") 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 = (() => { const abgelehnt = (() => {
try { try {
d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, erstellt) VALUES (?,?,?,?,?)") 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; return false;
} catch { return true; } } 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 }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { eigenerPort } from "./helfer-port.mjs"; import { eigenerPort } from "./helfer-port.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(180_000, "pruef-modi-ideen"); notbremse(180_000, "pruef-modi-ideen");
await new Promise((r) => setTimeout(r, 900)); await new Promise((r) => setTimeout(r, 900));
+2 -1
View File
@@ -39,6 +39,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(240_000, "pruef-nachwuchs"); notbremse(240_000, "pruef-nachwuchs");
await new Promise((r) => setTimeout(r, 700)); await new Promise((r) => setTimeout(r, 700));
@@ -106,7 +107,7 @@ anlegen("Mara", "linke", "CODE-LINK-0001");
Trichter ein gruener Haken ueber der Null. */ Trichter ein gruener Haken ueber der Null. */
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)" 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.',?,?,?,?)") + " 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; const idTalent = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.close(); d.close();
+3 -2
View File
@@ -49,6 +49,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(240_000, "pruef-neue-seiten"); notbremse(240_000, "pruef-neue-seiten");
await new Promise((r) => setTimeout(r, 700)); 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. */ eine Meldung und eine Maßnahme gibt. */
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)" 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!',?,?,?)") + " 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; const beitrag = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.close(); d.close();
@@ -156,7 +157,7 @@ let idHand = null;
const talent = (titel, text) => { const talent = (titel, text) => {
dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)" dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
+ " VALUES ('talente','ruhig',?,?,?,?,?,?)") + " 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; return dd.prepare("SELECT last_insert_rowid() AS id").get().id;
}; };
talent("Kessi aus dem Chat", "Faellt seit Wochen auf."); talent("Kessi aus dem Chat", "Faellt seit Wochen auf.");
+2 -1
View File
@@ -34,6 +34,7 @@ import { spawn } from "node:child_process";
import { DatabaseSync } from "node:sqlite"; import { DatabaseSync } from "node:sqlite";
import { scryptSync, randomBytes } from "node:crypto"; import { scryptSync, randomBytes } from "node:crypto";
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
notbremse(300_000, "die Spicy-Pruefung"); notbremse(300_000, "die Spicy-Pruefung");
@@ -137,7 +138,7 @@ let idDogi, idMax, idLuna, spaltenVorher, personenVorher;
.run(idLuna, idMax, jetzt); .run(idLuna, idMax, jetzt);
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, creator_id, erstellt, erstellt_von) d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, creator_id, erstellt, erstellt_von)
VALUES ('technik','problem','Ton bricht ab',?,?,?,?)`) 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); spaltenVorher = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
personenVorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n; personenVorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n;
+238 -49
View File
@@ -772,74 +772,263 @@ console.log(`\n=== Laedt jedes Server-Modul? ===`);
Deshalb wird es hier gesucht, statt darauf zu warten, dass es wieder Deshalb wird es hier gesucht, statt darauf zu warten, dass es wieder
jemanden trifft. Gemeint ist nur das TAGESDATUM; ein Zeitstempel jemanden trifft. Gemeint ist nur das TAGESDATUM; ein Zeitstempel
(`erstellt`) darf und soll UTC sein. */ (`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")); /** Kommentare und Zeichenketten weg, Zeilen behalten. Sonst zaehlt
ok(dateien.length >= 30, `${dateien.length} Pruefdateien durchgesehen`); * 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 = []; const verdaechtig = [];
for (const datei of dateien) { for (const datei of dateien) {
/* Die eigene Datei bleibt aussen vor: Die Gegenprobe unten MUSS /* Die eigene Datei bleibt aussen vor: Die Gegenproben unten
das gesuchte Muster als Text enthalten, sonst könnte sie nicht MUESSEN das gesuchte Muster als Text enthalten, sonst koennten
beweisen, dass sie es erkennt. Sie würde sich sonst selbst sie nicht beweisen, dass sie es erkennen. Sie wuerde sich sonst
anzeigen -- eine Warnung, die immer kommt, und die liest bald selbst anzeigen -- eine Warnung, die immer kommt. */
niemand mehr. */
if (datei === "pruef-struktur.mjs") continue; if (datei === "pruef-struktur.mjs") continue;
/* KOMMENTARE RAUS, ZEILEN BEHALTEN (16.09.2026). /* KOMMENTARE RAUS, ZEILEN BEHALTEN (16.09.2026).
Hier stand `replace(…, " ")` -- ein Blockkommentar ueber zwanzig Hier stand `replace(…, " ")` -- ein Blockkommentar ueber zwanzig
Zeilen wurde also zu einem Leerzeichen, und ab da zaehlte Zeilen wurde also zu einem Leerzeichen, und ab da zaehlte
`split("\n").length` falsch. Gemeldet wurden Zeilennummern, die `split("\n").length` falsch. Gemeldet wurden Zeilennummern, die
auf voellig anderen Code zeigten: pruef-eskalation.mjs:43 ist auf voellig anderen Code zeigten. Jetzt bleibt je Zeile eine
eine Zeile ueber Kekse. Zeile stehen. */
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. */
const text = readFileSync(join(SERVER, datei), "utf8") const text = readFileSync(join(SERVER, datei), "utf8")
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")); .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
/* Das Muster ist eng gefasst: NUR toISOString() gefolgt von einem .replace(/^\s*\/\/.*$/gm, "");
Schnitt auf zehn Zeichen -- also genau die Bildung eines for (const z of utcTage(text)) verdaechtig.push(`${datei}:${z}`);
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})`);
}
}
} }
ok(verdaechtig.length === 0, ok(verdaechtig.length === 0,
verdaechtig.length verdaechtig.length
? `bilden ihr Tagesdatum aus UTC statt Ortszeit — nachts zwischen ` ? `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 ") + 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 /* ---- GEGENPROBEN -------------------------------------------------
Nachweis wäre "0 Treffer" auch dann grün, wenn der reguläre Eine Erkennung, die nichts findet, sieht genauso aus wie eine, die
Ausdruck gar nichts erkennt. */ nichts finden KANN. Heute Nacht war sie genau das: Die Schreibweise
const probe = "const jetzt = new Date().toISOString();\nconst heute = jetzt.slice(0, 10);"; mit einer Funktion kannte sie nicht, und die Anwendung sah sie gar
const erkanntA = /toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/ nicht an. Beides steht jetzt hier als Probe. */
.test("new Date().toISOString().slice(0, 10)"); const probe = (code) => utcTage(code).length;
const erkanntB = /=\s*(\w+)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/.test(probe) ok(probe('const h = new Date().toISOString().slice(0, 10);') === 1,
&& /(const|let)\s+jetzt\s*=\s*new Date\(\)\.toISOString\(\)/.test(probe); "Gegenprobe: die direkte Schreibweise faellt auf");
ok(erkanntA && erkanntB, ok(probe('const jetzt = () => new Date().toISOString();\nconst h = jetzt().slice(0, 10);') === 1,
"Gegenprobe: beide Schreibweisen werden erkannt (direkt und über eine Variable)"); " 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 ok(probe('const jetzt = new Date().toISOString();') === 0,
die Prüfung eine Warnung, die immer kommt. */ "Gegenprobe: ein voller Zeitstempel ohne Schnitt schlaegt NICHT an");
const harmlos = /toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/ ok(probe('const b = new Date(Date.parse(tag + "T12:00:00Z") + n * 86400000)'
.test("const jetzt = new Date().toISOString();"); + '.toISOString().slice(0, 10);') === 0,
ok(!harmlos, "Gegenprobe: ein voller Zeitstempel (ohne Schnitt) schlägt nicht an"); " 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");
} }
/* ======================================================================= /* =======================================================================
+2 -1
View File
@@ -49,6 +49,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(180_000, "pruef-treff-start"); notbremse(180_000, "pruef-treff-start");
await new Promise((r) => setTimeout(r, 700)); 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`, { const r = await fetch(`${BASIS}/workspace/api/bereich/treff`, {
method: "POST", headers: kopf(dogi), method: "POST", headers: kopf(dogi),
body: JSON.stringify({ art: "frage", titel: `Frage Nummer ${i}`, body: JSON.stringify({ art: "frage", titel: `Frage Nummer ${i}`,
datum: jetzt.slice(0, 10) }), datum: heuteLokal() }),
}); });
letzte = r.status; letzte = r.status;
if (r.status === 429) break; if (r.status === 429) break;
+8 -7
View File
@@ -61,6 +61,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(240_000, "pruef-treff"); notbremse(240_000, "pruef-treff");
await new Promise((r) => setTimeout(r, 700)); await new Promise((r) => setTimeout(r, 700));
@@ -126,9 +127,9 @@ const BRETTER = ["anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln"
vergessen. */ vergessen. */
const PERSOENLICH = ["Vertraulich melden", "Mein Steckbrief", "Support"]; 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], 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. */ /* 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(); d.close();
/* Die Methode ist ein eigenes Argument, seit es Wege gibt, die PUT /* 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 Seiten der Grenze gemessen, sonst beweist es nichts: Eine
Schreibsperre, die immer sperrt, ist keine Stufe. */ Schreibsperre, die immer sperrt, ist keine Stufe. */
const neu = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, 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})`); 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); const lage0 = JSON.parse((await roh("/workspace/api/treff/lage", CREW, { cookie: gast.keks })).text);
@@ -867,7 +868,7 @@ melde("=== 6. Schreiben ===");
dd.close(); dd.close();
const a = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, 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})`); ok(a.code === 200 || a.code === 201, `nach acht Tagen schon (${a.code})`);
/* UND SECHS TAGE REICHEN NICHT. Die andere Seite derselben Grenze -- /* 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); .run(new Date(Date.now() - 6 * 86400000).toISOString(), idGast);
dd2.close(); dd2.close();
const knapp = await roh("/workspace/api/bereich/treff", CREW, { cookie: gast.keks }, 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})`); ok(knapp.code === 403, `sechs Tage reichen nicht (${knapp.code})`);
const dd3 = new DatabaseSync(process.env.WORKSPACE_DB); const dd3 = new DatabaseSync(process.env.WORKSPACE_DB);
dd3.prepare("UPDATE personen SET erstellt = ? WHERE id = ?") 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. */ /* Und auf ein Brett des Teams kommt er auch als "Dabei" nicht. */
const brettDesTeams = await roh("/workspace/api/bereich/anschlag", CREW, { cookie: gast.keks }, const brettDesTeams = await roh("/workspace/api/bereich/anschlag", CREW, { cookie: gast.keks },
{ art: Object.keys(BEREICHE.anschlag.arten)[0], titel: "GAST-AM-ANSCHLAG-0001", { 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})`); ok(brettDesTeams.code === 403, `aufs Anschlagbrett schreibt nur das Team (${brettDesTeams.code})`);
const beiDogi = await brett("treff", HAUS, kDogi); const beiDogi = await brett("treff", HAUS, kDogi);
@@ -901,7 +902,7 @@ melde("=== 6. Schreiben ===");
{ {
/* UND NICHT IN EIN BRETT DES HAUSES. */ /* UND NICHT IN EIN BRETT DES HAUSES. */
const a = await roh("/workspace/api/bereich/ideen", CREW, { cookie: gast.keks }, 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})`); ok(a.code !== 200 && a.code !== 201, `aber nicht in ein Brett des Hauses (${a.code})`);
} }
+2 -1
View File
@@ -43,6 +43,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(240_000, "pruef-uebergang"); notbremse(240_000, "pruef-uebergang");
await new Promise((r) => setTimeout(r, 700)); await new Promise((r) => setTimeout(r, 700));
@@ -100,7 +101,7 @@ let idDogi, idHand, idFrida, idMara, idNina, idTalent, idZweit;
const neuTalent = (titel) => { const neuTalent = (titel) => {
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)" d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
+ " VALUES ('talente','ruhig',?,?,?,?,?,?)") + " 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; return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}; };
idTalent = neuTalent("Kessi aus dem Chat"); idTalent = neuTalent("Kessi aus dem Chat");
+5 -4
View File
@@ -42,6 +42,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(180_000, "pruef-uebernahme"); notbremse(180_000, "pruef-uebernahme");
await new Promise((r) => setTimeout(r, 700)); 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) (bereich, art, titel, text, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von)
VALUES (?,?,?,?,?,?,'offen',?,?,?)`); VALUES (?,?,?,?,?,?,'offen',?,?,?)`);
rein.run("ideen", "content", "Reihe über Technik", "Drei Teile, je fünf Minuten.", 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; idIdee = d.prepare("SELECT last_insert_rowid() AS id").get().id;
rein.run("ideen", "content", "Dringende Idee", "Muss diese Woche.", 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; idIdeeHoch = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von) d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)
VALUES ('treff','hallo','Beitrag aus dem Rudel','Hallo zusammen',?,?,?)`) 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; idTreff = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.close(); d.close();
} }
@@ -314,7 +315,7 @@ melde("=== 8. Im Browser ===");
d.prepare(`INSERT INTO eintraege d.prepare(`INSERT INTO eintraege
(bereich, art, titel, text, datum, dringlichkeit, status, erstellt, erstellt_von) (bereich, art, titel, text, datum, dringlichkeit, status, erstellt, erstellt_von)
VALUES ('ideen','content','BILDSCHIRM-IDEE','Zum Anklicken.',?,'mittel','offen',?,?)`) VALUES ('ideen','content','BILDSCHIRM-IDEE','Zum Anklicken.',?,'mittel','offen',?,?)`)
.run(jetzt.slice(0, 10), jetzt, idDogi); .run(heuteLokal(), jetzt, idDogi);
d.close(); d.close();
await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" }); await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" });
+2 -1
View File
@@ -125,6 +125,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(180_000, "pruef-video"); notbremse(180_000, "pruef-video");
await new Promise((r) => setTimeout(r, 700)); await new Promise((r) => setTimeout(r, 700));
@@ -503,7 +504,7 @@ melde("=== 5c. Aus einem Wunsch wird ein Video ===");
const { lastInsertRowid } = db3.prepare(` const { lastInsertRowid } = db3.prepare(`
INSERT INTO eintraege (bereich, art, titel, datum, status, erstellt, erstellt_von) INSERT INTO eintraege (bereich, art, titel, datum, status, erstellt, erstellt_von)
VALUES (?,?,?,?, 'offen', ?, (SELECT id FROM personen LIMIT 1))`) 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); return Number(lastInsertRowid);
}; };
const wunschId = anlegen("wunsch", "wunsch", "Bitte mal Casper beim Baden zeigen"); const wunschId = anlegen("wunsch", "wunsch", "Bitte mal Casper beim Baden zeigen");
+4 -3
View File
@@ -51,6 +51,7 @@ const express = (await import("express")).default;
const ec = express.response.cookie; const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs"; import { notbremse } from "./helfer-notbremse.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js"); await import("./index.js");
notbremse(150_000, "pruef-wege-nach-draussen"); notbremse(150_000, "pruef-wege-nach-draussen");
await new Promise((r) => setTimeout(r, 700)); 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," d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von,"
+ " quelle_url, quelle_kanal) VALUES (?,?,?,?,?,?,?,?,?,?)") + " quelle_url, quelle_kanal) VALUES (?,?,?,?,?,?,?,?,?,?)")
.run("highlight", "clip", "Wer ist euer Komet", "Schreibt es in die Kommentare", .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 */ /* (b) ohne Video -- darf NICHT klickbar sein */
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von)" d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von)"
+ " VALUES (?,?,?,?,?,?,?,?)") + " VALUES (?,?,?,?,?,?,?,?)")
.run("highlight", "clip", "Nur ein Bild", "Ohne Quelle", .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 /* (c) Video bei TikTok geloescht -- darf NICHT klickbar sein, sonst
fuehrt der Klick auf eine Fehlerseite und WIR sehen kaputt aus */ 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," d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von,"
+ " quelle_url, quelle_kanal, quelle_fehlt_seit) VALUES (?,?,?,?,?,?,?,?,?,?,?)") + " quelle_url, quelle_kanal, quelle_fehlt_seit) VALUES (?,?,?,?,?,?,?,?,?,?,?)")
.run("highlight", "clip", "Nicht mehr da", "Wurde geloescht", .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. /* EIN ECHTES BILD AN DIE ERSTE KARTE.
+6 -1
View File
@@ -56,6 +56,7 @@ import { unterstuetzungAufraeumen } from "./workspace-unterstuetzung.js";
import { import {
db, sitzungLesen, istDogFather, protokolliere, echteIp, db, sitzungLesen, istDogFather, protokolliere, echteIp,
einstellung, einstellungSetzen, einstellung, einstellungSetzen,
tagLokal,
} from "./workspace.js"; } from "./workspace.js";
export const aufbewahrungRouter = express.Router(); export const aufbewahrungRouter = express.Router();
@@ -392,7 +393,11 @@ export const FRISTEN = [
+ "gezählt wird je Weg und Tag, nicht je Mensch.", + "gezählt wird je Weg und Tag, nicht je Mensch.",
offen: (d) => zaehle(d, offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM unterstuetzung_striche WHERE tag < ?", "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(), raeumen: () => unterstuetzungAufraeumen(),
}, },
{ {
+4 -1
View File
@@ -547,7 +547,10 @@ aufgabenRouter.get("/workspace/api/uebersicht", (req, res) => {
.prepare(`SELECT COUNT(*) AS n ${VERBUND} WHERE ${wo}${zusatz}`) .prepare(`SELECT COUNT(*) AS n ${VERBUND} WHERE ${wo}${zusatz}`)
.get(...werte, ...extra).n; .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). /* ABGEBROCHENE SIND NICHT "NOCH ZU TUN" (08.09.2026).
+19 -3
View File
@@ -1775,7 +1775,13 @@ function pruefe(bereich, körper, { neu }) {
} }
if (neu || körper.datum !== undefined) { if (neu || körper.datum !== undefined) {
const d = String(körper.datum ?? "").trim(); 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))) { else if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || Number.isNaN(Date.parse(d))) {
fehler.push("Datum ist ungültig."); fehler.push("Datum ist ungültig.");
} else aus.datum = d; } else aus.datum = d;
@@ -2130,7 +2136,10 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (r
erstellt_von, haus) erstellt_von, haus)
VALUES (?,?,?,?,?, 'offen', ?, ?, ?)`) VALUES (?,?,?,?,?, 'offen', ?, ?, ?)`)
.run(bereich, v.art, v.titel, v.text ?? null, .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 /* UND SOFORT FREIGEBEN (17.09.2026) -- der Fehler, den Filipe
auf seinem Bildschirm gesehen hat. auf seinem Bildschirm gesehen hat.
@@ -2243,7 +2252,14 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/:id/daraus",
VALUES (?,?,?,?,?, 'offen', ?,?,?,?)`) VALUES (?,?,?,?,?, 'offen', ?,?,?,?)`)
.run(nach, art, quelle.titel, .run(nach, art, quelle.titel,
String(req.body?.text ?? "").trim().slice(0, TEXT_MAX) || null, 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); req.person.haus || null);
protokolliere("bereich_daraus", { protokolliere("bereich_daraus", {
+2 -1
View File
@@ -548,7 +548,8 @@ bewerbungRouter.put("/workspace/api/bewerbung/:id/weg", express.json({ limit: "8
.run(b.nenn_name, .run(b.nenn_name,
`Hat sich selbst gemeldet (Anfrage #${id}).` `Hat sich selbst gemeldet (Anfrage #${id}).`
+ (antworten.get("warum") ? `\n\nWarum: ${antworten.get("warum")}` : ""), + (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); eintragId = Number(lastInsertRowid);
/* AUF „ANGESPROCHEN" und nicht auf „aufgefallen": Die ersten /* AUF „ANGESPROCHEN" und nicht auf „aufgefallen": Die ersten
+5 -1
View File
@@ -27,6 +27,7 @@ import express from "express";
import { import {
db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds,
istLeitung, istDogFather, siehtAlles, tagLokal, istLeitung, istDogFather, siehtAlles, tagLokal,
heuteLokal,
} from "./workspace.js"; } from "./workspace.js";
export const contentRouter = express.Router(); export const contentRouter = express.Router();
@@ -39,7 +40,10 @@ const NAME_MAX = 40;
const SAEULEN_MAX = 5; const SAEULEN_MAX = 5;
const jetzt = () => new Date().toISOString(); 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) { function angemeldet(req, res, next) {
const person = sitzungLesen(req); const person = sitzungLesen(req);
+7 -1
View File
@@ -62,6 +62,7 @@ import express from "express";
import { import {
db, sitzungLesen, protokolliere, echteIp, fuehrtTeamDogi, ROLLEN_NAME, db, sitzungLesen, protokolliere, echteIp, fuehrtTeamDogi, ROLLEN_NAME,
ROLLEN_SORTIERUNG, MODI_KATEGORIEN, kategorienFuer, darfKatalogBekommen, ROLLEN_SORTIERUNG, MODI_KATEGORIEN, kategorienFuer, darfKatalogBekommen,
tagLokal,
} from "./workspace.js"; } from "./workspace.js";
/* Die Aufgaben, die ein Neuer bekommen kann -- dieselbe Liste, aus der /* Die Aufgaben, die ein Neuer bekommen kann -- dieselbe Liste, aus der
auch die Aufgabenseite ihre Vorschlaege baut. Eine zweite waere die, 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); AND status NOT IN ('erledigt','abgebrochen')`).get(id, schluessel);
if (offen) return res.status(400).json({ fehler: "schon_da", aufgabe_id: offen.id }); 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(); const jetztIso = jetzt();
db().prepare(`INSERT INTO aufgaben db().prepare(`INSERT INTO aufgaben
(titel, beschreibung, status, prioritaet, verantwortlich_id, (titel, beschreibung, status, prioritaet, verantwortlich_id,
+8 -3
View File
@@ -54,6 +54,7 @@ import express from "express";
import { randomBytes, timingSafeEqual } from "node:crypto"; import { randomBytes, timingSafeEqual } from "node:crypto";
import { import {
db, protokolliere, echteIp, sitzungLesen, termineSichtbar, externSql, db, protokolliere, echteIp, sitzungLesen, termineSichtbar, externSql,
tagLokal,
} from "./workspace.js"; } from "./workspace.js";
export const icsRouter = express.Router(); export const icsRouter = express.Router();
@@ -335,9 +336,13 @@ icsRouter.get("/workspace/kalender/:datei", (req, res) => {
/* Ein Jahr rueckwaerts, ein Jahr vorwaerts. Alles auszuliefern /* Ein Jahr rueckwaerts, ein Jahr vorwaerts. Alles auszuliefern
waere bei jedem Abruf die ganze Geschichte; ein Kalender braucht waere bei jedem Abruf die ganze Geschichte; ein Kalender braucht
sie nicht, und die Datei waechst sonst ohne Ende. */ sie nicht, und die Datei waechst sonst ohne Ende. */
const heute = new Date(); /* ORTSZEIT (01.10.2026). Der Schaden waere hier klein -- ein
const vor = new Date(heute.getTime() - 365 * 86400000).toISOString().slice(0, 10); Fenster von zwei Jahren verschiebt sich um einen Tag --, aber
const nach = new Date(heute.getTime() + 365 * 86400000).toISOString().slice(0, 10); 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(` const termine = db().prepare(`
SELECT ${SPALTEN} ${VERBUND} SELECT ${SPALTEN} ${VERBUND}
+10 -3
View File
@@ -21,6 +21,7 @@
import express from "express"; import express from "express";
import { import {
db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, siehtAlles, sichtbareCreatorIds, db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, siehtAlles, sichtbareCreatorIds,
heuteLokal, tagLokal,
} from "./workspace.js"; } from "./workspace.js";
import { verteilen } from "./workspace-womit.js"; import { verteilen } from "./workspace-womit.js";
import { BEREICHE } from "./workspace-bereiche.js"; import { BEREICHE } from "./workspace-bereiche.js";
@@ -29,6 +30,10 @@ export const reportRouter = express.Router();
const jetzt = () => new Date().toISOString(); const jetzt = () => new Date().toISOString();
const tagOffset = (tage) => new Date(Date.now() - tage * 86400_000).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) { function angemeldet(req, res, next) {
const person = sitzungLesen(req); const person = sitzungLesen(req);
@@ -101,11 +106,13 @@ reportRouter.get("/workspace/api/report", (req, res) => {
const creator = zielCreator(req); const creator = zielCreator(req);
const jetztIso = jetzt(); 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 abIso = tagOffset(tage);
const davorIso = tagOffset(tage * 2); const davorIso = tagOffset(tage * 2);
const ab = abIso.slice(0, 10); const ab = tagOffsetTag(tage);
const davor = davorIso.slice(0, 10); const davor = tagOffsetTag(tage * 2);
/* Einschraenkung auf einen Creator, je Tabelle einmal formuliert. */ /* Einschraenkung auf einen Creator, je Tabelle einmal formuliert. */
const nurA = creator ? " AND (a.creator_id = ? OR a.verantwortlich_id = ?)" : ""; const nurA = creator ? " AND (a.creator_id = ? OR a.verantwortlich_id = ?)" : "";
+4 -1
View File
@@ -18,6 +18,7 @@
import express from "express"; import express from "express";
import { import {
db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, siehtAlles, pipelineIds, db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, siehtAlles, pipelineIds,
heuteLokal,
} from "./workspace.js"; } from "./workspace.js";
export const scoutRouter = express.Router(); export const scoutRouter = express.Router();
@@ -131,7 +132,9 @@ const VERBUND = `
scoutRouter.get("/workspace/api/leads", (req, res) => { scoutRouter.get("/workspace/api/leads", (req, res) => {
try { try {
const { wo, werte } = sichtbar(req.sicht || req.person); 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({ res.json({
stufen: STUFEN, stufen: STUFEN,
/* Die Auswahllisten kommen MIT, damit die Oberflaeche keine /* Die Auswahllisten kommen MIT, damit die Oberflaeche keine
+3 -1
View File
@@ -75,6 +75,7 @@ import express from "express";
import { import {
db, sitzungLesen, protokolliere, echteIp, db, sitzungLesen, protokolliere, echteIp,
TREFF_BRETTER, TREFF_ROLLEN, ROLLEN_NAME, istHand, TREFF_BRETTER, TREFF_ROLLEN, ROLLEN_NAME, istHand,
heuteLokal,
} from "./workspace.js"; } from "./workspace.js";
/* Die Tabellen und die Sperrfrage stehen in einem Blatt ohne eigene /* Die Tabellen und die Sperrfrage stehen in einem Blatt ohne eigene
Importe -- workspace.js braucht beides ebenfalls, und ein Importkreis Importe -- workspace.js braucht beides ebenfalls, und ein Importkreis
@@ -1023,7 +1024,8 @@ treffRouter.post("/workspace/api/treff/mitmachen/:id/uebernehmen", gleicheHerkun
.run( .run(
`${wer?.name ?? "Jemand"} hat sich im Rudel gemeldet`, `${wer?.name ?? "Jemand"} hat sich im Rudel gemeldet`,
`Aus „Mitmachen": ${String(e.titel ?? "")}\n\n${String(e.text ?? "")}`.trim(), `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), protokolliere("treff_talent", { personId: req.person.id, ip: echteIp(req),
detail: `aus Eintrag ${id}` }); detail: `aus Eintrag ${id}` });
res.json({ ok: true, talent_id: Number(r.lastInsertRowid) }); res.json({ ok: true, talent_id: Number(r.lastInsertRowid) });
+6 -2
View File
@@ -39,6 +39,7 @@
import express from "express"; import express from "express";
import { import {
db, protokolliere, echteIp, istDogFather, heuteLokal, db, protokolliere, echteIp, istDogFather, heuteLokal,
tagLokal,
} from "./workspace.js"; } from "./workspace.js";
import { UNTERSTUETZUNG_ARTEN, ZAEHLUNG_TAGE } from "./unterstuetzung-tabellen.js"; import { UNTERSTUETZUNG_ARTEN, ZAEHLUNG_TAGE } from "./unterstuetzung-tabellen.js";
@@ -358,8 +359,11 @@ unterstuetzungRouter.delete("/workspace/api/unterstuetzung/:id(\\d+)",
* hinsieht. */ * hinsieht. */
export function unterstuetzungAufraeumen() { export function unterstuetzungAufraeumen() {
try { try {
const grenze = new Date(Date.now() - ZAEHLUNG_TAGE * 86400000) /* ORTSZEIT (01.10.2026): Die Tage in dieser Tabelle stehen in
.toISOString().slice(0, 10); 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); const r = db().prepare("DELETE FROM unterstuetzung_striche WHERE tag < ?").run(grenze);
return r.changes || 0; return r.changes || 0;
} catch { return 0; } } catch { return 0; }
+7 -2
View File
@@ -47,6 +47,7 @@ import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
import { import {
db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER,
KANAELE, kanalVonHandle, TREFF_BRETTER, KANAELE, kanalVonHandle, TREFF_BRETTER,
heuteLokal,
} from "./workspace.js"; } from "./workspace.js";
import { darfSchreiben, sofortFreigeben } from "./workspace-treff.js"; import { darfSchreiben, sofortFreigeben } from "./workspace-treff.js";
import { sichtbar } from "./workspace-bereiche.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, (bereich, art, titel, datum, status, erstellt, erstellt_von,
quelle_url, quelle_kanal, quelle_geholt_am, aus_eintrag_id) quelle_url, quelle_kanal, quelle_geholt_am, aus_eintrag_id)
VALUES (?,?,?,?, 'offen', ?,?,?,?,?,?)`) 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); jetzt, req.person.id, url, ablage, jetzt, ausQuelle ? aus : null);
eintragId = Number(lastInsertRowid); eintragId = Number(lastInsertRowid);
} }
@@ -434,7 +438,8 @@ export async function videoNochDa(url) {
export async function videoWacheLauf() { export async function videoWacheLauf() {
const d = db(); const d = db();
const grenze = new Date(Date.now() - WACHE_ABSTAND_TAGE * 86400_000).toISOString(); 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 /* WELCHE ZUERST: die mit Fehlversuchen (die sind verdaechtig und
sollen schnell geklaert sein), dann die am laengsten ungesehenen. sollen schnell geklaert sein), dann die am laengsten ungesehenen.
+5 -1
View File
@@ -55,6 +55,7 @@ import {
darfSchreibenMit, darfSchreibenMit,
KATALOG_EMPFAENGER, KATALOG_EMPFAENGER,
KATALOG_EMPFAENGER_SQL, KATALOG_EMPFAENGER_SQL,
heuteLokal,
} from "./workspace.js"; } from "./workspace.js";
/* Dieselbe Meldung wie beim anderen Bewerbungsweg -- eine Stelle, /* 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, (bereich, art, titel, text, datum, status, creator_id, hook, format,
erstellt, erstellt_von) erstellt, erstellt_von)
VALUES (?,?,?,?,?,?,?,?,?,?,?)`) 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, creatorId, zusatz.hook || null, zusatz.format || null,
jetzt(), req.person.id); jetzt(), req.person.id);
angelegt.push(titel); angelegt.push(titel);
+6 -1
View File
@@ -56,6 +56,7 @@ import {
db, protokolliere, echteIp, darfAufgabenVerteilen, entscheidetUeberAufgaben, db, protokolliere, echteIp, darfAufgabenVerteilen, entscheidetUeberAufgaben,
istDogFather, istHand, istDogFather, istHand,
sitzungLesen, sitzungLesen,
heuteLokal,
} from "./workspace.js"; } from "./workspace.js";
import { TEAM_DOGI_ROLLEN } from "./crew-adresse.js"; import { TEAM_DOGI_ROLLEN } from "./crew-adresse.js";
/* WER ES ERFAEHRT -- an einer Stelle fuer beide Bewerbungswege. /* 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 }); 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 }; const zaehl = { offen: 0, angenommen: 0, arbeit: 0, erledigt: 0, abgelehnt: 0 };
let ueberfaellig = 0, gelobt = 0, zuVerbessern = 0; let ueberfaellig = 0, gelobt = 0, zuVerbessern = 0;
for (const z of zeilen) { for (const z of zeilen) {