diff --git a/server/helfer-tag.mjs b/server/helfer-tag.mjs new file mode 100644 index 00000000..524dc113 --- /dev/null +++ b/server/helfer-tag.mjs @@ -0,0 +1,57 @@ +/* ===================================================================== + EIN TAGESDATUM IN ORTSZEIT — für Prüfungen (16.09.2026) + + `new Date().toISOString().slice(0, 10)` ist der bequemste Weg zu + einem Datum und der falsche. toISOString rechnet in UTC; Server und + Menschen stehen hier auf Europe/Berlin. Zwischen Mitternacht und + zwei Uhr morgens liefert die Rechnung deshalb den VORTAG. + + In einer Prüfung heißt das: Sie ist tagsüber grün und nachts rot, + ohne dass irgendetwas kaputt ist. Ein Fehlalarm, den man nicht + nachstellen kann, ist der sicherste Weg, eine Prüfung abzuschalten. + + --------------------------------------------------------------------- + WARUM NICHT AUS workspace.js + + Dort stehen `heuteLokal()` und `tagLokal()` schon, mit demselben + Kommentar — aber wer sie importiert, zieht das ganze Haus mit: die + Datenbank, die Umstellungen, die Zeitgeber. Eine Prüfung, die nur ein + Datum braucht, soll dafür keinen Server hochfahren. + + Dieselbe Rechnung an zwei Stellen ist hier also Absicht, und es ist + die Ausnahme, nicht die Regel: Es sind vier Zeilen ohne + Verzweigung, sie können nicht auseinanderlaufen, ohne dass + pruef-struktur es meldet. + + --------------------------------------------------------------------- + UND WANN toISOString RICHTIG IST + + Wenn die Uhrzeit fest auf Mittag steht. `Date.parse("…T12:00:00Z")` + plus oder minus ganze Tage bleibt in jeder Zeitzone am selben + Kalendertag — deshalb rechnet der Server mit UTC-Mittag, wenn er + Fristen VERGLEICHT. Nur beim ANZEIGEN und beim Erzeugen eines + Tagesdatums gilt die Ortszeit. + ===================================================================== */ + +const zwei = (n) => String(n).padStart(2, "0"); + +/** Der heutige Tag in Ortszeit, als `JJJJ-MM-TT`. */ +export function heuteLokal() { + const d = new Date(); + return `${d.getFullYear()}-${zwei(d.getMonth() + 1)}-${zwei(d.getDate())}`; +} + +/** Derselbe Tag, um `versatz` Tage verschoben. Negativ heißt zurück. */ +export function tagLokal(versatz = 0) { + const d = new Date(Date.now() + versatz * 86400_000); + return `${d.getFullYear()}-${zwei(d.getMonth() + 1)}-${zwei(d.getDate())}`; +} + +/** Ein beliebiger Zeitpunkt als Tagesdatum in Ortszeit. + * + * Für Prüfungen, die von einem FESTEN Zeitpunkt aus rechnen statt von + * jetzt — dort ist die Zeitzone genauso entscheidend. */ +export function tagVon(zeitpunkt, versatz = 0) { + const d = new Date(zeitpunkt + versatz * 86400_000); + return `${d.getFullYear()}-${zwei(d.getMonth() + 1)}-${zwei(d.getDate())}`; +} diff --git a/server/pruef-backstage-import.mjs b/server/pruef-backstage-import.mjs index e0acff0e..eb0dd951 100644 --- a/server/pruef-backstage-import.mjs +++ b/server/pruef-backstage-import.mjs @@ -26,6 +26,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { tagLokal } from "./helfer-tag.mjs"; import { spawn } from "node:child_process"; import { DatabaseSync } from "node:sqlite"; import { scryptSync, randomBytes } from "node:crypto"; @@ -52,11 +53,12 @@ const jetzt = new Date().toISOString(); (Shop, 06.09.2026): Die Wanduhr darf Eingabe sein, nie Annahme. ALLE DREI WERDEN AUS EINER STELLE ABGELEITET -- drei eigene Rechnungen waeren drei Gelegenheiten, wieder auseinanderzulaufen. */ -const tagLokal = (versatz) => { - const d = new Date(Date.now() + versatz * 86400000); - const p2 = (n) => String(n).padStart(2, "0"); - return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`; -}; +/* SEIT DEM 16.09.2026 AUS helfer-tag.mjs. Die Rechnung stand hier + richtig -- und wurde 1270 Zeilen weiter unten trotzdem nicht + benutzt: Dort stand wieder `toISOString().slice(0, 10)`. Ein Helfer, + den man beim naechsten Mal vergisst, ist eine halbe Loesung. Jetzt + gibt es ihn einmal fuer alle Pruefungen, und pruef-struktur meldet, + wer ihn nicht benutzt. */ const heute = tagLokal(0); const gestern = tagLokal(-1); const morgen = tagLokal(1); @@ -1324,7 +1326,7 @@ console.log("\n=== Die Seite ==="); /* EINE Person, ZWEI Tage -- dafuer ist dieser Weg da. Der erste Entwurf hatte hier zwei verschiedene Creator; dass die neue Sperre ihn abgewiesen hat, war ihr erster echter Treffer. */ - const vortag = new Date(Date.now() - 2 * 86400000).toISOString().slice(0, 10); + const vortag = tagLokal(-2); const xlsxDatei = xlsxBauen([ ["Datum", "Creator", "Diamanten", "LIVE-Dauer"], [gestern, "@Nova_Live", "12.480", "3Std. 4Min."], diff --git a/server/pruef-eskalation.mjs b/server/pruef-eskalation.mjs index 48b564a5..4fd0171c 100644 --- a/server/pruef-eskalation.mjs +++ b/server/pruef-eskalation.mjs @@ -30,6 +30,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { request as httpAnfrage } from "node:http"; import { portMussFreiSein } from "./helfer-port.mjs"; +import { heuteLokal, tagLokal, tagVon } from "./helfer-tag.mjs"; const PORT = await portMussFreiSein(4417, "pruef-eskalation"); @@ -67,7 +68,7 @@ const jetzt = new Date().toISOString(); als Annahme benutzt, misst irgendwann das Gegenteil -- der Kalender holt sie ein. Hier wird die Zeit uebergeben, also ist sie Eingabe. */ const NUN = Date.parse("2026-09-15T12:00:00+02:00"); -const tagVor = (n) => new Date(NUN - n * 86400000).toISOString().slice(0, 10); +const tagVor = (n) => tagVon(NUN, -n); let idDogi; { @@ -219,12 +220,12 @@ melde("=== 5. Über die Leitung ==="); const d = oeffne(); const rein = d.prepare(`INSERT INTO aufgaben (titel, status, prioritaet, frist, erstellt, erstellt_von) VALUES (?,?,'mittel',?,?,?)`); - const heute = new Date().toISOString().slice(0, 10); - const vor = (n) => new Date(Date.now() - n * 86400000).toISOString().slice(0, 10); + const heute = heuteLokal(); + const vor = (n) => tagLokal(-n); rein.run("Seit gestern faellig", "offen", vor(1), jetzt, idDogi); rein.run("Liegt seit fuenf Tagen", "offen", vor(5), jetzt, idDogi); rein.run("Haengt seit zwanzig", "offen", vor(20), jetzt, idDogi); - rein.run("Erst naechste Woche", "offen", new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10), jetzt, idDogi); + rein.run("Erst naechste Woche", "offen", tagLokal(7), jetzt, idDogi); rein.run("Heute faellig", "offen", heute, jetzt, idDogi); d.close(); diff --git a/server/pruef-modi-livecheck.mjs b/server/pruef-modi-livecheck.mjs index 75580f1f..521b215a 100644 --- a/server/pruef-modi-livecheck.mjs +++ b/server/pruef-modi-livecheck.mjs @@ -23,6 +23,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { tagLokal } from "./helfer-tag.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-live-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -75,7 +76,7 @@ void idMan; liegt in der Zukunft und wird RELATIV gerechnet -- ein festes Datum holt der Kalender irgendwann ein, und dann ist die Pruefung rot, ohne dass etwas kaputt waere. */ -const inTagen = (n) => new Date(Date.now() + n * 86400_000).toISOString().slice(0, 10); +const inTagen = (n) => tagLokal(n); const TAG1 = inTagen(3); const TAG2 = inTagen(10); diff --git a/server/pruef-struktur.mjs b/server/pruef-struktur.mjs index c2839fe7..42abb072 100644 --- a/server/pruef-struktur.mjs +++ b/server/pruef-struktur.mjs @@ -30,6 +30,7 @@ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs"; import { join, dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { brotliCompressSync, constants } from "node:zlib"; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO = join(__dirname, ".."); @@ -183,8 +184,33 @@ ok(nurPraefix > 0, `${nurPraefix} zusammengesetzte Aufrufe: nur Praefix pruefbar /* ---------- 4. Jede Seite ist erreichbar --------------------------------- */ console.log(`\n=== Erreichbarkeit ===`); +/* WER AUF EINE SEITE ZEIGT, IST NICHT NUR EIN href (16.09.2026). + + Diese Zeile meldete sechs Seiten als "von nirgendwo verlinkt": + crew-index, entwicklung, rechte, talente, teamlage und + treff-moderation. Keine davon war verwaist -- man kommt auf jede, + indem man eine KACHEL antippt. Die Kacheln stehen aber im Server + (`ziel: "talente.html"`), und der wurde hier nie durchsucht. + + Eine Pruefung, die seit Wochen sechs Fehler meldet, die keine sind, + ist schlimmer als keine: Man liest sie nicht mehr, und dann uebersieht + man die siebte, die echt ist. Der blinde Fleck lag in der Pruefung, + nicht im Haus. + + ABGELEITET, NICHT NACHGETRAGEN: Statt die sechs von Hand + auszunehmen, kommen die Dateien dazu, in denen die Ziele wirklich + stehen. Wer morgen eine Kachel anlegt, ist damit automatisch + abgedeckt -- und wer eine Seite anlegt, auf die nichts zeigt, faellt + weiterhin auf. */ +const zeigerQuellen = [ + ...quellenMitLinks, + /* Die Kachellisten -- dort steht `ziel: "talente.html"`. */ + { name: "server/workspace.js", pfad: join(SERVER, "workspace.js") }, + /* Die Zugangswaende -- crew-index.html wird dort ausgeliefert. */ + { name: "server/crew-adresse.js", pfad: join(SERVER, "crew-adresse.js") }, +]; const verlinkt = new Set(["index.html", "start.html"]); -for (const { pfad: quelle } of quellenMitLinks) { +for (const { pfad: quelle } of zeigerQuellen) { const text = readFileSync(quelle, "utf8"); for (const m of text.matchAll(/["'`](?:\/workspace\/)?([a-z0-9-]+\.html)/g)) { verlinkt.add(m[1]); @@ -239,6 +265,23 @@ ok(reste.length === 0, reste.length ? reste.join(" · ") : "keine Entwicklungsre /* ---------- 7. Groesse der Assets ---------------------------------------- */ console.log(`\n=== Dateigroessen ===`); +/** Ist diese Text-Datei zu schwer? Gibt den Grund oder `null`. + * + * ALS EIGENE FUNKTION, damit die Gegenprobe sie mit erfundenem Inhalt + * aufrufen kann. Eine Messung, die nur ueber die echten Dateien + * laeuft, ist genau so lange gruen, wie zufaellig keine zu gross ist + * -- und beweist nie, dass sie ueberhaupt anschlagen kann. */ +function textZuSchwer(roh) { + const gepackt = Math.round(brotliCompressSync(roh, { + params: { [constants.BROTLI_PARAM_QUALITY]: 4 }, + }).length / 1024); + if (gepackt > 200) return `${gepackt} KB uebertragen`; + const ohneKommentar = Math.round( + roh.toString("utf8").replace(/\/\*[\s\S]*?\*\//g, "").length / 1024); + if (ohneKommentar > 200) return `${ohneKommentar} KB ohne Kommentare`; + return null; +} + let schwer = []; for (const unter of ["assets/js", "assets/css", "assets/img"]) { const ordner = join(WS, unter); @@ -300,11 +343,81 @@ for (const unter of ["assets/js", "assets/css", "assets/img"]) { } continue; } + /* BEI TEXT MISST DIE ROHE GROESSE DAS FALSCHE (16.09.2026). + + Die Grenze soll verhindern, "dass eine Seite unnoetig viel + laedt" -- so steht es oben. Fuer Bilder ist die Datei genau das, + was geladen wird. Fuer CSS und JS nicht: Caddy komprimiert sie + unterwegs. Nachgemessen auf der echten Seite am 16.09.: + + start.css auf der Platte 346 KB uebertragen 110 KB (br) + chat.css 59 KB 16 KB + entwicklung.css 33 KB 8 KB + + Die Pruefung meldete start.css mit 347 KB -- davon sind 227 KB + KOMMENTAR (64 %). Sie bestrafte damit genau das, was dieses Haus + absichtlich tut: jede Entscheidung danebenschreiben. Und sie + haette eine Datei mit 199 KB dichtem, unkommentiertem CSS + durchgewunken. + + Dasselbe Argument steht drei Absaetze weiter oben schon einmal, + fuer die gestuften Bilder: "Bei einem Bild, das in mehreren + Groessen vorliegt, misst sie das Falsche." + + JETZT ZWEI ZAHLEN, weil es zwei verschiedene Fragen sind: + + 1. WAS DER BESUCHER LAEDT -- brotli, Stufe 4. Die Stufe ist + nicht geraten: Bei 4 liefert node 111 KB, Caddy liefert + 110. Jede andere Stufe waere eine Zahl, die mit der + Wirklichkeit nichts zu tun hat. + 2. WAS EIN MENSCH PFLEGEN MUSS -- die Datei ohne Kommentare. + Sonst koennte man die erste Zahl klein halten, indem man + immer mehr Prosa schreibt, und niemand merkt, dass das CSS + selbst ausgeufert ist. */ + if (/\.(css|js)$/.test(n)) { + const grund = textZuSchwer(readFileSync(p)); + if (grund) schwer.push(`${unter}/${n} ${grund}`); + continue; + } if (kb > 200) schwer.push(`${unter}/${n} ${kb} KB`); } } ok(schwer.length === 0, schwer.length ? `ueber 200 KB: ${schwer.join(", ")}` : "keine Datei ueber 200 KB"); +/* GEGENPROBE ZUR GROESSENMESSUNG. Sie ist seit dem 16.09.2026 kein + blosser Blick auf die Dateigroesse mehr, sondern packt den Inhalt + zusammen -- und eine Messung, die niemand je hat anschlagen sehen, + ist eine Behauptung. Drei Faelle, und jeder muss anders ausgehen. */ +{ + /* 1. Viel echtes CSS, das sich nicht wegpacken laesst. */ + let dicht = ""; + for (let i = 0; i < 9000; i++) { + dicht += `.k${i.toString(36)}-${(i * 7919).toString(36)} { color: #${ + (i * 2654435761 % 16777215).toString(16).padStart(6, "0")}; }\n`; + } + ok(textZuSchwer(Buffer.from(dicht)) !== null, + `dichtes CSS von ${Math.round(dicht.length / 1024)} KB faellt durch ` + + `(${textZuSchwer(Buffer.from(dicht))})`); + + /* 2. Dieselbe Menge Text, aber als Prosa im Kommentar -- die packt + sich auf ein Zehntel und darf durchgehen. Das ist der ganze + Grund fuer die Umstellung. */ + const prosa = "/* " + "Jede Entscheidung gehoert danebengeschrieben. ".repeat(9000) + " */\n" + + ".a { color: #fff; }\n"; + ok(textZuSchwer(Buffer.from(prosa)) === null, + `${Math.round(prosa.length / 1024)} KB Kommentar gehen durch — sie kosten ` + + "den Besucher fast nichts"); + + /* 3. UND DIE ZWEITE ZAHL MUSS AUCH ANSCHLAGEN: viel echtes CSS, das + sich zwar gut packt (weil es sich wiederholt), aber trotzdem + niemand mehr pflegen kann. Ohne diesen Fall koennte man die + erste Grenze mit Wiederholung unterlaufen. */ + const wiederholt = ".gleich { color: #ffffff; padding: 4px; }\n".repeat(6000); + ok(textZuSchwer(Buffer.from(wiederholt)) !== null, + `${Math.round(wiederholt.length / 1024)} KB sich wiederholendes CSS faellt ` + + `ebenfalls durch (${textZuSchwer(Buffer.from(wiederholt))})`); +} + /* ---------- 8. Totes CSS ------------------------------------------------- */ /* start.css wird auf JEDER Seite geladen und ist 126 KB gross. Was darin @@ -611,7 +724,19 @@ console.log(`\n=== Rechnen die Pruefungen mit Ortszeit? ===`); anzeigen -- eine Warnung, die immer kommt, und die liest bald niemand mehr. */ if (datei === "pruef-struktur.mjs") continue; - const text = readFileSync(join(SERVER, datei), "utf8").replace(/\/\*[\s\S]*?\*\//g, " "); + /* 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. */ + 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 diff --git a/server/pruef-treff-werkzeuge.mjs b/server/pruef-treff-werkzeuge.mjs index dad8dca9..ffd870db 100644 --- a/server/pruef-treff-werkzeuge.mjs +++ b/server/pruef-treff-werkzeuge.mjs @@ -39,6 +39,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { request as httpAnfrage } from "node:http"; +import { heuteLokal } from "./helfer-tag.mjs"; import { portMussFreiSein } from "./helfer-port.mjs"; const PORT = await portMussFreiSein(4402, "pruef-treff-werkzeuge"); @@ -75,7 +76,7 @@ const { BEREICHE } = await import("./workspace-bereiche.js"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const heute = jetzt.slice(0, 10); +const heute = heuteLokal(); const schluessel = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 5f48b5da..dffcaf8c 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609161723", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609161803", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609161723", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609161803", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609161723", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609161803", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609161723", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609161803", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css index a8c460e4..26a7e13c 100644 --- a/workspace/assets/css/gate.css +++ b/workspace/assets/css/gate.css @@ -654,18 +654,26 @@ body.gate { stroke: none; fill: color-mix(in srgb, var(--rf) 52%, transparent); } -.rolle__zeichen .auge { - stroke: none; - fill: color-mix(in srgb, var(--rf) 26%, #ffffff); -} -.rolle__zeichen .nase { - stroke: none; - fill: color-mix(in srgb, var(--rf) 45%, #06090f); -} -.rolle__zeichen .hell { - stroke: color-mix(in srgb, var(--rf) 16%, #ffffff); - stroke-width: 2; -} +/* HIER STANDEN DREI REGELN FUER EIN ZEICHEN, DAS ES NICHT MEHR GIBT + (16.09.2026). + + `.auge`, `.nase` und `.hell` gehoerten zu einem Rollenzeichen, das + aus SVG-Pfaden gezeichnet war. Heute ist es ein Bild plus drei + Kreise mit festen Farben (`#r-husky`) -- kein Element im Haus traegt + diese Klassen noch. Nachgemessen ueber alle HTML- und JS-Dateien: + als Klasse null Vorkommen. + + `.fuell` BLEIBT: die wird an einer Stelle wirklich benutzt. + + WARUM NUR EINE DAVON GEMELDET WURDE: Die Suche nach totem CSS + vergleicht den Klassennamen als Teilzeichenkette gegen den gesamten + Quelltext. „auge" steckt in „Auge", „hell" in „hell" und + „hellster" -- beide kamen im Fliesstext deutscher Kommentare + dutzendfach vor und galten damit als benutzt. Nur „nase" kam + nirgends vor. Die beiden anderen waren genauso tot, nur unsichtbar. + + Das ist eine offene Schwaeche der Pruefung, kein Befund gegen das + Haus -- sie steht hier notiert, damit sie nicht wieder verlorengeht. */ /* Gewaehlt: mehr Licht auf demselben Gegenstand -- kein anderer Gegenstand. */ .rolle[aria-checked="true"] .rolle__zeichen { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 79c417a1..01360d3d 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -282,10 +282,10 @@ - - - - - + + + + +