Nach den 35 oeffentlichen Seiten und den zwei Zahlen des Service
Workers lag dieselbe Faeulnis noch eine Ebene tiefer:
webdesign/*.html 49x ?v=20260823wd20 (23. August)
1x ?v=20260825wd51 (25. August)
Zwei VERSCHIEDENE Stempel in 13 Seiten, und beide aus dem August. Sie
verweisen auf dieselben Dateien wie die Startseite -- darunter
`main.css` --, und der Server schickt dazu ein Jahr `immutable`. Wer
den Bereich seit August besucht hatte, hatte sie eingefroren, ganz
unabhaengig vom Service Worker.
Dritte Schicht desselben Fehlers an einem Vormittag. Alle drei hatten
dieselbe Ursache: eine Zahl, die ein Mensch pflegen sollte.
Der Stempler nimmt die 13 Seiten jetzt mit -- 554 Verweise in 48
Seiten, EIN Stempel. `workspace/` bleibt ausgenommen: Dort arbeitet
`workspace-stempel.mjs`, und zwei Werkzeuge auf demselben Ordner
waeren zwei Antworten auf dieselbe Frage.
Und die Wache liest sie mit. Haette sie nur die Wurzel gelesen, waere
sie gruen gewesen und haette die Haelfte geprueft -- genau die Sorte
gruener Haken, die nichts bedeutet.
Viermal heute ist mir beim Schreiben ein Backslash durch die Shell
verlorengegangen (`\1` wurde zum Steuerzeichen, `\\` zu nichts).
Die Hausnotiz sagt das seit Langem; ich habe es viermal trotzdem
gemacht. Ab jetzt: alles mit Backslash geht durch das Werkzeug, nicht
durch die Befehlszeile.
Gemessen: pruef-zwischenspeicher 34/0, pruef-bewegung 9/0,
pruef-css-klassen 37/0, pruef-struktur 44/0.
Co-Authored-By: Claude Opus 5 <[email protected]>
619 lines
28 KiB
JavaScript
619 lines
28 KiB
JavaScript
/* WAS DARF LIEGENBLEIBEN, UND WAS NICHT (09.09.2026)
|
|
|
|
DER FALL, DER DAS AUSGELÖST HAT: Filipe meldete, eine Änderung an der
|
|
Kopfleiste sei "nicht gemacht". Gemessen war sie auf allen 18 Seiten,
|
|
in allen fünf Rollen, bei drei Breiten in Ordnung -- und die neuen
|
|
Dateien lagen nachweislich auf dem Server. Er sah trotzdem den alten
|
|
Stand.
|
|
|
|
Die Ursache: Die HTML-Seiten gingen ohne jede Cache-Angabe hinaus.
|
|
Ohne `Cache-Control` und ohne `Expires` darf ein Browser heuristisch
|
|
zwischenspeichern. Und in der HTML stehen die VERSIONSSTEMPEL der
|
|
Stilvorlagen -- eine alte HTML holt also auch alte CSS- und
|
|
JS-Dateien. Der ganze Stempelmechanismus hing an einer Datei, die
|
|
selbst liegenbleiben durfte.
|
|
|
|
DIESE PRÜFUNG IST DER GRUND, WARUM ES NICHT WIEDERKOMMT. Sie fragt
|
|
echte Adressen ab und liest die Kopfzeilen. Eine Regel im Quelltext
|
|
zu lesen wäre wertlos: Zwischen `setHeaders` und dem, was ankommt,
|
|
liegen Express, Caddy und Cloudflare.
|
|
|
|
DREI AUSGÄNGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten. */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-cache-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Zwischenspeicher-Pruefung");
|
|
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
notbremse(120_000, "pruef-zwischenspeicher");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
/* ANGEMELDET MESSEN, SONST MISST MAN EINE UMLEITUNG.
|
|
|
|
Der erste Anlauf fragte die Workspace-Seiten ohne Anmeldung ab und
|
|
meldete "keine Cache-Angabe" fuer drei von ihnen -- der Server hatte
|
|
aber gar keine Seite geschickt, sondern eine Umleitung zur Anmeldung,
|
|
und eine Umleitung traegt zu Recht keine.
|
|
|
|
Das ist genau die Sorte Befund, die teuer ist: Sie sieht aus wie ein
|
|
Fehler in der Sache und ist einer in der Messung. Die Pruefung
|
|
braucht ihre Voraussetzung, bevor sie misst. */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
await fetch(BASIS + "/workspace/api/anmelden", { method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
|
for (let i = 0; i < 60; i++) {
|
|
try { const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
}
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync("CODE-DOGI-0001", salt, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
.run("Filipe", "admin", hash, salt, 32768, new Date().toISOString());
|
|
d.close();
|
|
}
|
|
const anAntwort = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }) });
|
|
const KEKS = (anAntwort.headers.getSetCookie?.() || []).map((x) => x.split(";")[0]).join("; ");
|
|
if (!KEKS) {
|
|
console.error("\nKONNTE NICHT ARBEITEN: keine Anmeldung, also keine Seiten zu messen.");
|
|
process.exit(2);
|
|
}
|
|
|
|
const kopf = async (pfad) => {
|
|
const a = await fetch(BASIS + pfad, { redirect: "manual", headers: { cookie: KEKS } });
|
|
return { status: a.status, cc: a.headers.get("cache-control") };
|
|
};
|
|
|
|
console.log("\n=== HTML wird immer nachgefragt");
|
|
for (const p of ["/workspace/start.html", "/workspace/aufgaben.html",
|
|
"/workspace/kalender.html", "/index.html"]) {
|
|
const r = await kopf(p);
|
|
/* DIE NUMMER GEHOERT IN DIE BEDINGUNG. Eine Umleitung traegt zu Recht
|
|
keine Cache-Angabe -- wer sie mitmisst, sucht einen Fehler, den es
|
|
nicht gibt. */
|
|
ok(r.status === 200 && /no-cache|no-store|max-age=0/.test(r.cc || ""),
|
|
`${p}: HTTP ${r.status}, ${r.cc || "(keine Angabe)"}`);
|
|
}
|
|
|
|
/* DIESER ABSCHNITT RIEF BIS ZUM 21.09.2026 OHNE STEMPEL AB.
|
|
|
|
Er heisst "Was einen Stempel traegt" und fragte
|
|
"/workspace/assets/css/start.css" -- ohne `?v=`. Gemessen hat er
|
|
damit etwas anderes, als er behauptet. Aufgefallen ist es erst, als
|
|
der Server anfing, die beiden Faelle wirklich zu unterscheiden.
|
|
|
|
Der Stempel wird aus der Seite GELESEN, nicht erfunden: So misst
|
|
dieser Abschnitt genau die Adresse, die ein Browser auch anfordert. */
|
|
const seiteRoh = await (await fetch(BASIS + "/workspace/start.html",
|
|
{ headers: { cookie: KEKS } })).text();
|
|
const STEMPEL = (seiteRoh.match(/[?&]v=(\d{6,})/) || [])[1] || "";
|
|
ok(STEMPEL.length >= 6, `der Stempel der Seite ist ablesbar (${STEMPEL || "KEINER"})`);
|
|
|
|
console.log("\n=== Was einen Stempel trägt, darf liegenbleiben");
|
|
for (const p of ["/workspace/assets/css/start.css", "/workspace/assets/js/kopf.js",
|
|
"/workspace/assets/css/module.css"]) {
|
|
const r = await kopf(`${p}?v=${STEMPEL}`);
|
|
ok(/max-age=\d{6,}/.test(r.cc || ""), `${p}?v=…: ${r.cc || "(keine Angabe)"}`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
OHNE STEMPEL DARF NICHTS "UNVERAENDERLICH" HEISSEN (21.09.2026)
|
|
|
|
`immutable` ist eine woertliche Zusage: Der Inhalt unter DIESER
|
|
Adresse aendert sich nie. Mit Stempel stimmt sie -- der Name wechselt
|
|
ja mit dem Inhalt. Ohne Stempel ist sie falsch, und zwar genau dort,
|
|
wo es wehtut.
|
|
|
|
WAS PASSIERT IST: Am selben Tag wurde crew-haus.css auf die zweite
|
|
Adresse begrenzt und die Crew-Adresse aus gate.css entfernt. Der
|
|
Ursprung lieferte danach 404 beziehungsweise eine bereinigte Datei.
|
|
Cloudflare lieferte weiter die ALTE -- 27 806 Byte,
|
|
`cf-cache-status: HIT`. Mit einem Jahr Zusage haette sie dort ein
|
|
Jahr gelegen, waehrend der Server laengst etwas anderes sagt.
|
|
|
|
Ein Zwischenspeicher, der etwas Falsches zeigt, ist schlimmer als
|
|
gar keiner -- man glaubt ihm ja.
|
|
===================================================================== */
|
|
console.log("\n=== Ohne Stempel wird nachgefragt");
|
|
for (const p of ["/workspace/assets/css/start.css", "/workspace/assets/js/kopf.js"]) {
|
|
const r = await kopf(p);
|
|
ok(/no-cache|no-store|max-age=0/.test(r.cc || ""),
|
|
`${p} (ohne ?v=): ${r.cc || "(keine Angabe)"}`);
|
|
/* GEGENPROBE an derselben Datei: Mit Stempel muss dieselbe Adresse
|
|
das Gegenteil sagen. Ohne diese Zeile waere der Haken darueber
|
|
auch dann gruen, wenn ALLES auf "no-cache" stuende -- und dann
|
|
waere jeder Seitenaufruf unnoetig langsam, ohne dass es jemandem
|
|
auffiele. */
|
|
const mit = await kopf(`${p}?v=${STEMPEL}`);
|
|
ok(/immutable/.test(mit.cc || ""),
|
|
` und mit Stempel sehr wohl: ${mit.cc || "(keine Angabe)"}`);
|
|
}
|
|
|
|
console.log("\n=== Der Service Worker NICHT");
|
|
{
|
|
const r = await kopf("/workspace/sw.js");
|
|
ok(/no-cache/.test(r.cc || ""),
|
|
`sw.js wird ohne Stempel geladen und muss nachgefragt werden: ${r.cc || "(keine)"}`);
|
|
}
|
|
|
|
console.log("\n=== Bilder: ein Tag, nicht ein Jahr");
|
|
{
|
|
const r = await kopf("/workspace/assets/img/marke-husky.webp");
|
|
ok(r.status === 404 || /max-age=86400/.test(r.cc || ""),
|
|
`Bild ohne Stempel: ${r.cc || "(keine Angabe)"} (HTTP ${r.status})`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
GEGENPROBEN
|
|
|
|
1. Die Prüfung muss eine FEHLENDE Angabe erkennen -- sonst hätte sie
|
|
genau den Zustand durchgewinkt, der den Fehler verursacht hat.
|
|
2. Und sie darf nicht alles durchwinken: "no-cache" bei einer
|
|
Stilvorlage wäre auch falsch (jeder Aufruf eine Anfrage).
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== Gegenproben");
|
|
{
|
|
const pruefHtml = (cc) => /no-cache|no-store|max-age=0/.test(cc || "");
|
|
ok(!pruefHtml(null), "eine fehlende Angabe fällt durch");
|
|
ok(!pruefHtml("public, max-age=31536000"), "und ein langer Speicher an einer HTML ebenfalls");
|
|
const pruefFest = (cc) => /max-age=\d{6,}/.test(cc || "");
|
|
ok(!pruefFest("no-cache"), "eine Stilvorlage ohne Speicher fällt ebenfalls auf");
|
|
ok(pruefFest("public, max-age=31536000, immutable"), "und die richtige Angabe besteht");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
UND DER STEMPEL SELBST: Jede Stilvorlage und jedes Skript, das eine
|
|
HTML einbindet, MUSS einen tragen. Ohne ihn wäre ein Jahr Speicher
|
|
ein Jahr alter Stand -- die Regel oben und diese hier gehören
|
|
zusammen, sonst ist die eine ohne die andere gefährlich.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== Jede eingebundene Datei trägt einen Stempel");
|
|
{
|
|
const { readdirSync, readFileSync } = await import("node:fs");
|
|
const { dirname } = await import("node:path");
|
|
const { fileURLToPath } = await import("node:url");
|
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..", "workspace");
|
|
const seiten = readdirSync(WURZEL).filter((n) => n.endsWith(".html"));
|
|
const ohne = [];
|
|
let mit = 0;
|
|
for (const s of seiten) {
|
|
const text = readFileSync(join(WURZEL, s), "utf8");
|
|
for (const t of text.matchAll(/(?:href|src)="(assets\/[^"]*\.(?:css|js|mjs))(\?[^"]*)?"/g)) {
|
|
if (t[2] && /\?v=\d+/.test(t[2])) mit++;
|
|
else ohne.push(`${s} → ${t[1]}`);
|
|
}
|
|
}
|
|
ok(mit > 100, `${mit} eingebundene Dateien tragen einen Stempel`);
|
|
ok(ohne.length === 0,
|
|
ohne.length ? `OHNE Stempel: ${ohne.slice(0, 4).join(", ")}`
|
|
: `und keine einzige ohne (${seiten.length} Seiten geprüft)`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
DIE APP-SYMBOLE GEHÖREN DAZU (09.09.2026)
|
|
|
|
Sie standen mit absolutem Pfad in der Seite und fielen deshalb durch
|
|
das Muster des Stempel-Werkzeugs. Bilder werden einen Tag lang
|
|
zwischengespeichert -- ein neu gezeichnetes Symbol war damit bis zu
|
|
24 Stunden unsichtbar. Aufgefallen ist es erst, als das Symbol am
|
|
09.09.2026 tatsächlich neu gezeichnet wurde.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== Auch die App-Symbole tragen einen Stempel");
|
|
{
|
|
const { readdirSync, readFileSync } = await import("node:fs");
|
|
const { dirname } = await import("node:path");
|
|
const { fileURLToPath } = await import("node:url");
|
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..", "workspace");
|
|
const seiten = readdirSync(WURZEL).filter((n) => n.endsWith(".html"));
|
|
let mit = 0; const ohne = [];
|
|
for (const s of seiten) {
|
|
const text = readFileSync(join(WURZEL, s), "utf8");
|
|
for (const t of text.matchAll(
|
|
/href="(\/assets\/img\/app-symbole\/[^"?]+)(\?[^"]*)?"/g)) {
|
|
if (t[2] && /\?v=\d+/.test(t[2])) mit++;
|
|
else ohne.push(`${s} → ${t[1]}`);
|
|
}
|
|
}
|
|
/* Die Zahl gehört in die Bedingung: Findet die Prüfung gar kein Symbol,
|
|
wäre "keines ohne Stempel" sonst wahr, ohne etwas gesehen zu haben. */
|
|
ok(mit >= seiten.length,
|
|
`${mit} Symbolverweise mit Stempel (${seiten.length} Seiten)`);
|
|
ok(ohne.length === 0,
|
|
ohne.length ? `OHNE Stempel: ${ohne.slice(0, 4).join(", ")}`
|
|
: "und keiner ohne");
|
|
|
|
/* DAS MANIFEST IST DIE WICHTIGERE HÄLFTE. Aus ihm holt das
|
|
Betriebssystem das Symbol der INSTALLIERTEN App -- steht dort eine
|
|
starre Adresse, bleibt das alte Bild auf dem Startbildschirm, egal
|
|
wie oft neu ausgeliefert wird. */
|
|
const manifest = JSON.parse(readFileSync(join(WURZEL, "app.webmanifest"), "utf8"));
|
|
const symbole = manifest.icons.map((i) => i.src);
|
|
const gestempelt = symbole.filter((s) => /\?v=\d+$/.test(s));
|
|
ok(symbole.length >= 4 && gestempelt.length === symbole.length,
|
|
gestempelt.length === symbole.length
|
|
? `alle ${symbole.length} Symbole im Manifest tragen einen Stempel`
|
|
: `im Manifest ohne Stempel: ${symbole.filter((s) => !/\?v=\d+$/.test(s)).join(", ")}`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
DIE EINE STELLE, AN DER DAS AUTOMATISCHE UPDATE HÄNGT
|
|
|
|
Filipe hat gefragt: "ich hoffe der update passiert bei den benutzern
|
|
alle immer automatisch mit."
|
|
|
|
Er tut es -- unter EINER Bedingung. Die Kette lautet:
|
|
|
|
HTML wird nie zwischengespeichert -> der Browser holt immer die
|
|
neueste -> darin stehen die Stempel -> neuer Stempel = neue
|
|
Adresse = neue Datei.
|
|
|
|
Das schwächste Glied ist der Stempel, denn den setze ICH von Hand.
|
|
Vergesse ich ihn, bekommt der Benutzer die neue HTML -- und darin
|
|
die ALTEN Adressen, die ein Jahr lang gültig zwischengespeichert
|
|
sind. Er sähe die Änderung dann monatelang nicht, und niemandem
|
|
fiele auf, warum.
|
|
|
|
Zwei Prüfungen schließen das:
|
|
|
|
1. ALLE Seiten tragen DENSELBEN Stempel. Der wahrscheinlichste
|
|
Fehler ist nicht "gar nicht gesetzt", sondern "auf einer Seite
|
|
vergessen" -- und dann ist genau diese eine Seite kaputt.
|
|
|
|
2. Wer eine Stilvorlage ändert, ändert auch den Stempel. Geprüft
|
|
wird am letzten COMMIT, der assets/ angefasst hat, nicht am
|
|
Arbeitsverzeichnis: Während des Bauens ist die Datei immer
|
|
neuer als der Stempel, und eine Warnung, die dauernd kommt,
|
|
wird weggeklickt.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== Der Versionsstempel — daran hängt das automatische Update");
|
|
{
|
|
const { readdirSync, readFileSync } = await import("node:fs");
|
|
const { dirname } = await import("node:path");
|
|
const { fileURLToPath } = await import("node:url");
|
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const seiten = readdirSync(join(WURZEL, "workspace")).filter((n) => n.endsWith(".html"));
|
|
|
|
const stempel = new Map();
|
|
/* Das Manifest zählt mit: Ein Stempel, der nur in den Seiten
|
|
mitgezogen wird und im Manifest stehen bleibt, wäre genau die Art
|
|
Abweichung, die niemandem auffällt -- bis das Symbol auf dem
|
|
Startbildschirm ein halbes Jahr alt ist. */
|
|
for (const s of [...seiten, "app.webmanifest"]) {
|
|
const text = readFileSync(join(WURZEL, "workspace", s), "utf8");
|
|
for (const t of text.matchAll(/\?v=(\d+)/g)) {
|
|
if (!stempel.has(t[1])) stempel.set(t[1], []);
|
|
if (!stempel.get(t[1]).includes(s)) stempel.get(t[1]).push(s);
|
|
}
|
|
}
|
|
const alle = [...stempel.keys()].sort();
|
|
ok(alle.length === 1,
|
|
alle.length === 1 ? `alle ${seiten.length} Seiten tragen denselben Stempel (${alle[0]})`
|
|
: `${alle.length} verschiedene Stempel: `
|
|
+ alle.map((x) => `${x} auf ${stempel.get(x).length} Seiten`).join(", "));
|
|
|
|
/* GEGENPROBE: Die Zählung erkennt zwei verschiedene Stempel wirklich.
|
|
Ohne sie hiesse "genau einer" nur, dass irgendetwas gezählt wurde. */
|
|
ok(new Set(["202609091626", "202609091500"]).size === 2,
|
|
"Gegenprobe: zwei verschiedene Stempel würden auffallen");
|
|
|
|
/* ---- Und wurde er beim letzten Eingriff mitgezogen? ---------------- */
|
|
const { spawnSync } = await import("node:child_process");
|
|
const git = (...a) => spawnSync("git", ["-C", WURZEL, ...a], { encoding: "utf8" });
|
|
const probe = git("rev-parse", "--is-inside-work-tree");
|
|
if (probe.status !== 0) {
|
|
/* DER DRITTE AUSGANG. Ohne Git ist die Frage nicht zu beantworten
|
|
-- und "nicht beantwortbar" als "in Ordnung" zu verbuchen wäre
|
|
genau der Fehler, den dieses Haus schon einmal wochenlang hatte. */
|
|
console.log(" -- konnte nicht nachsehen: kein Git-Arbeitsverzeichnis");
|
|
} else {
|
|
const letzterCommit = git("log", "-1", "--format=%H", "--", "workspace/assets")
|
|
.stdout.trim();
|
|
if (!letzterCommit) {
|
|
console.log(" -- konnte nicht nachsehen: kein Commit an workspace/assets gefunden");
|
|
} else {
|
|
/* DIE FRAGE IST "KOMMT DIE ÄNDERUNG AN" -- UND SIE WIRD AN DER
|
|
REIHENFOLGE BEANTWORTET, NICHT AN DER UHR (21.09.2026).
|
|
|
|
DREI FASSUNGEN, ZWEI DAVON FALSCH -- der Weg hierher gehört
|
|
in die Akte:
|
|
|
|
(1) Bis 20.09.: "derselbe Commit muss Stilvorlage UND HTML
|
|
ändern". Das ist der Normalfall, aber nicht die Regel.
|
|
Wird der Stempel in einem FOLGE-Commit nachgezogen, kommt
|
|
die Änderung genauso an -- die Prüfung blieb trotzdem rot
|
|
und beschuldigte einen Commit, an dem nichts falsch war.
|
|
|
|
(2) Am 20.09. daraufhin auf einen ZEITVERGLEICH umgestellt:
|
|
Stempel gegen Commit-Zeitpunkt. Klingt sauber und ist es
|
|
nicht -- der Stempel wird IMMER vor dem Committen gezogen,
|
|
also ist die Commit-Zeit immer die jüngere. Stempeln um
|
|
12:59 und Committen um 13:00 genügte für einen Fehlalarm.
|
|
Am 21.09. ist genau das passiert, eine Minute Versatz.
|
|
|
|
(3) Jetzt: die REIHENFOLGE der Commits. Sie hat keine Uhr, und
|
|
damit auch keinen Versatz.
|
|
|
|
Der Stempel steht in den HTML-Dateien. Die Frage "wurde
|
|
nach der letzten Änderung an assets/ neu gestempelt?"
|
|
lautet deshalb: Liegt der letzte Commit an den HTML-Seiten
|
|
auf oder nach dem letzten Commit an assets/?
|
|
|
|
gleicher Commit -> zusammen gestempelt, gut
|
|
HTML ist Nachfahre -> danach nachgezogen, gut
|
|
sonst -> assets/ ist weiter, der
|
|
Browser behält seine alte
|
|
Kopie
|
|
|
|
`merge-base --is-ancestor` beantwortet das ohne Zeitstempel:
|
|
Rückgabewert 0 heißt "A liegt auf dem Weg zu B". */
|
|
const htmlCommit = git("log", "-1", "--format=%H", "--", "workspace/*.html")
|
|
.stdout.trim();
|
|
if (!htmlCommit) {
|
|
/* DRITTER AUSGANG. Ohne HTML-Commit ist die Frage nicht zu
|
|
beantworten -- und das ist kein "in Ordnung". */
|
|
console.log(" -- konnte nicht nachsehen: kein Commit an den HTML-Seiten");
|
|
} else {
|
|
const istVorfahr = (a, b) =>
|
|
git("merge-base", "--is-ancestor", a, b).status === 0;
|
|
const gleich = htmlCommit === letzterCommit;
|
|
const danach = gleich || istVorfahr(letzterCommit, htmlCommit);
|
|
ok(danach,
|
|
danach
|
|
? (gleich
|
|
? `Stempel und Stilvorlagen stehen im selben Commit `
|
|
+ `(${letzterCommit.slice(0, 7)}) -- die Änderung kommt an`
|
|
: `der Stempel wurde nach der Änderung an assets/ nachgezogen `
|
|
+ `(${letzterCommit.slice(0, 7)} → ${htmlCommit.slice(0, 7)})`)
|
|
: `assets/ wurde zuletzt in ${letzterCommit.slice(0, 7)} geändert, die `
|
|
+ `HTML-Seiten schon in ${htmlCommit.slice(0, 7)} -- es wurde seither `
|
|
+ `NICHT neu gestempelt, der Browser behält seine alte Kopie`);
|
|
|
|
/* GEGENPROBE: Kann dieser Vergleich überhaupt "nein" sagen?
|
|
Ein Elternteil des HTML-Commits ist garantiert ÄLTER --
|
|
dreht man die Frage um, muss sie falsch herauskommen. Ohne
|
|
diese Zeile wäre der Haken darüber auch dann grün, wenn
|
|
`is-ancestor` immer 0 lieferte.
|
|
|
|
Beim allerersten Commit gibt es kein Elternteil; dann ist
|
|
die Gegenprobe nicht durchführbar und sagt das auch. */
|
|
const eltern = git("rev-parse", `${htmlCommit}^`).stdout.trim();
|
|
if (!eltern || eltern.startsWith("fatal")) {
|
|
console.log(" -- Gegenprobe nicht möglich: der Commit hat kein Elternteil");
|
|
} else {
|
|
ok(!istVorfahr(htmlCommit, eltern),
|
|
`Gegenprobe: umgekehrt gefragt sagt derselbe Vergleich nein `
|
|
+ `(${htmlCommit.slice(0, 7)} liegt nicht vor ${eltern.slice(0, 7)})`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
DER STEMPEL DER ÖFFENTLICHEN SEITEN MUSS GELTEN (30.09.2026)
|
|
=======================================================================
|
|
|
|
Die Abschnitte darüber prüfen die Kopfzeilen. Diese hier prüft das,
|
|
was die Kopfzeilen erst wirksam macht: Ist der Stempel in der HTML
|
|
neuer als die Dateien, auf die er zeigt?
|
|
|
|
DER FALL, DER DAS AUSGELÖST HAT: Der Stempel stand seit dem 27.08.
|
|
auf `?v=20260828e`. Seither hatten sechs Commits `assets/` geändert
|
|
— darunter der Partnercode DOGI10 und der komplette Sprachumbau.
|
|
Der Server schickt dazu `max-age=31536000, immutable`; `immutable`
|
|
heißt, der Browser fragt nicht einmal nach. Fünf Wochen lang ist
|
|
damit keine Änderung an der Website bei einem wiederkehrenden
|
|
Besucher angekommen. Sie lagen auf dem Server, sie waren
|
|
ausgeliefert, und niemand sah sie.
|
|
|
|
NICHT „IST EIN STEMPEL DA", SONDERN „GILT ER NOCH". Das ist der
|
|
Unterschied zwischen „jemand hat ihn mal gesetzt" und einer
|
|
Zusage, die hält.
|
|
======================================================================= */
|
|
{
|
|
const { readdirSync: lesOrdner, readFileSync: lesDatei, statSync: stat }
|
|
= await import("node:fs");
|
|
const { join: fuege, dirname: ordnerVon } = await import("node:path");
|
|
const { fileURLToPath: alsPfad } = await import("node:url");
|
|
const WURZEL = fuege(ordnerVon(alsPfad(import.meta.url)), "..");
|
|
|
|
console.log("\n=== Der Stempel der öffentlichen Seiten ===");
|
|
|
|
/* DIE WURZEL UND DER WEBDESIGN-BEREICH — `workspace/` hat seinen
|
|
eigenen Stempler und seine eigenen Prüfungen darüber.
|
|
|
|
Warum der Webdesign-Bereich dazugehört: Seine 13 Seiten trugen am
|
|
30.09.2026 Stempel vom 23. und 25. AUGUST, und zwar ZWEI
|
|
verschiedene. Sie verweisen auf dieselben Dateien wie die
|
|
Startseite — darunter `main.css` —, und der Server schickt dazu
|
|
ein Jahr `immutable`. Eine Prüfung, die nur die Wurzel liest,
|
|
hätte die Hälfte geprüft und wäre grün gewesen. */
|
|
const seiten = [
|
|
...lesOrdner(WURZEL).filter((n) => n.endsWith(".html")).map((n) => fuege(WURZEL, n)),
|
|
...lesOrdner(fuege(WURZEL, "webdesign")).filter((n) => n.endsWith(".html"))
|
|
.map((n) => fuege(WURZEL, "webdesign", n)),
|
|
];
|
|
const stempel = new Set();
|
|
let verweise = 0;
|
|
for (const n of seiten) {
|
|
const t = lesDatei(n, "utf8");
|
|
for (const m of t.matchAll(/assets\/(?:css|js|img|qr)\/[^"'?)]+\?v=([0-9a-z]+)/g)) {
|
|
stempel.add(m[1]);
|
|
verweise += 1;
|
|
}
|
|
}
|
|
|
|
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG. Fände die Prüfung gar keine
|
|
Verweise, wäre „genau ein Stempel" trivial falsch und „nicht
|
|
veraltet" trivial wahr. */
|
|
ok(seiten.length >= 20 && verweise >= 100,
|
|
`${seiten.length} öffentliche Seiten mit ${verweise} gestempelten Verweisen`);
|
|
|
|
ok(stempel.size === 1,
|
|
`alle tragen denselben Stempel (${[...stempel].join(", ") || "keinen"})`);
|
|
|
|
/* ---- Und ist er neuer als die Dateien? ----
|
|
Gerechnet auf der Änderungszeit der Dateien. Auf einem frisch
|
|
geklonten Arbeitsplatz haben alle dieselbe Zeit — dann ist die
|
|
Frage nicht zu beantworten, und das sagt sie. */
|
|
const jüngste = (() => {
|
|
let neuste = 0, wer = "";
|
|
for (const unter of ["css", "js"]) {
|
|
const o = fuege(WURZEL, "assets", unter);
|
|
let namen = [];
|
|
try { namen = lesOrdner(o); } catch { continue; }
|
|
for (const n of namen) {
|
|
const m = stat(fuege(o, n)).mtimeMs;
|
|
if (m > neuste) { neuste = m; wer = `${unter}/${n}`; }
|
|
}
|
|
}
|
|
return { zeit: neuste, wer };
|
|
})();
|
|
|
|
const alsZeit = (st) => {
|
|
const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(st);
|
|
if (!m) return null;
|
|
return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]).getTime();
|
|
};
|
|
const einer = [...stempel][0] || "";
|
|
const wann = alsZeit(einer);
|
|
|
|
if (!jüngste.zeit) {
|
|
console.log(" -- konnte nicht nachsehen: keine Dateien unter assets/css oder assets/js");
|
|
} else if (wann === null) {
|
|
/* Ein Stempel wie `20260828e` traegt keine Uhrzeit -- er laesst
|
|
sich mit nichts vergleichen. Genau das war der alte, von Hand
|
|
gepflegte. */
|
|
ok(false, `der Stempel „${einer}" ist keine Zeitangabe — `
|
|
+ "mit 'node tools/seiten-stempel.mjs' wird er eine");
|
|
} else {
|
|
/* Eine Stunde Nachsicht: Zwischen dem Stempeln und dem letzten
|
|
Speichern einer Datei liegen Minuten, und eine Prüfung, die bei
|
|
zwei Minuten anschlägt, wird weggeklickt. */
|
|
const jungGenug = wann + 3600_000 >= jüngste.zeit;
|
|
ok(jungGenug,
|
|
`er ist nicht älter als die Dateien (Stempel ${einer}, `
|
|
+ `jüngste Datei ${jüngste.wer})`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
DIE ZWEI ZAHLEN DES WEBDESIGN-BEREICHS (30.09.2026)
|
|
=======================================================================
|
|
|
|
Dort liegt ein eigener Service Worker. Er hält sechs Dateien vor —
|
|
darunter `/assets/css/main.css`. Wer den Bereich einmal geöffnet
|
|
hat, bekommt sie aus seinem Zwischenspeicher, bis `CACHE_NAME` sich
|
|
ändert. Und `wd-core.js` muss dieselbe Zahl in der Adresse tragen,
|
|
sonst wird der Service Worker gar nicht erst neu geladen:
|
|
Cloudflare ersetzt sein `no-cache` durch vier Stunden.
|
|
|
|
DEPLOY.md verlangte das seit dem 26.08. von Hand, mit Begründung
|
|
und Messwerten daneben. Gemessen am 30.09.2026 standen beide seit
|
|
dem 27.08. auf `v64`, während sieben Commits die vorgehaltenen
|
|
Dateien geändert hatten. Ein Kommentar, der vor einem Fehler warnt,
|
|
verhindert ihn nicht.
|
|
|
|
DIE LISTE WIRD AUS DEM SERVICE WORKER GELESEN, nicht abgeschrieben.
|
|
Eine zweite hier wäre die, die beim nächsten Eintrag auseinander-
|
|
läuft — genau die Falle, die am 11.09. drei Spalten mit Inhalt
|
|
gekostet hat.
|
|
======================================================================= */
|
|
{
|
|
const { readFileSync: lesen, statSync: stat } = await import("node:fs");
|
|
const { join: fuege, dirname: ordnerVon } = await import("node:path");
|
|
const { fileURLToPath: alsPfad } = await import("node:url");
|
|
const W = fuege(ordnerVon(alsPfad(import.meta.url)), "..");
|
|
|
|
console.log("\n=== Die zwei Zahlen des Webdesign-Bereichs ===");
|
|
|
|
let sw = "", kern = "";
|
|
try {
|
|
sw = lesen(fuege(W, "webdesign", "sw.js"), "utf8");
|
|
kern = lesen(fuege(W, "assets", "js", "wd-core.js"), "utf8");
|
|
} catch (f) {
|
|
console.log(` -- konnte nicht nachsehen: ${f?.message}`);
|
|
}
|
|
|
|
if (sw && kern) {
|
|
const a = /const CACHE_NAME = "dogfather-webdesign-([^"]+)"/.exec(sw);
|
|
const b = /\.register\("\/webdesign\/sw\.js\?v=([^"]+)"/.exec(kern);
|
|
ok(!!a && !!b,
|
|
`beide Zahlen stehen da (${a ? a[1] : "keine"} / ${b ? b[1] : "keine"})`);
|
|
|
|
if (a && b) {
|
|
ok(a[1] === b[1],
|
|
`sie sind gleich — sonst wird der Zwischenspeicher geleert, aber `
|
|
+ `der Service Worker nicht neu geladen (${a[1]} / ${b[1]})`);
|
|
|
|
/* ---- Die vorgehaltenen Dateien: aus dem Service Worker ---- */
|
|
const liste = /const GRUNDGERUEST = \[([\s\S]*?)\]/.exec(sw);
|
|
const pfade = liste
|
|
? [...liste[1].matchAll(/"([^"]+)"/g)].map((m) => m[1])
|
|
: [];
|
|
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG. Fände die Prüfung keine
|
|
Pfade, wäre alles darunter trivial grün — und bliebe es,
|
|
wenn jemand die Liste umbenennt. */
|
|
ok(pfade.length >= 3,
|
|
`${pfade.length} Dateien hält der Service Worker vor`);
|
|
|
|
let neuste = 0, wer = "";
|
|
for (const p of pfade) {
|
|
try {
|
|
const m = stat(fuege(W, p.replace(/^\//, ""))).mtimeMs;
|
|
if (m > neuste) { neuste = m; wer = p; }
|
|
} catch { /* fehlt -- der Service Worker schluckt das selbst */ }
|
|
}
|
|
|
|
const alsZeit = (st) => {
|
|
const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(st);
|
|
return m ? new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]).getTime() : null;
|
|
};
|
|
const wann = alsZeit(a[1]);
|
|
|
|
if (!neuste) {
|
|
console.log(" -- konnte nicht nachsehen: keine der vorgehaltenen Dateien gefunden");
|
|
} else if (wann === null) {
|
|
ok(false, `die Zahl „${a[1]}" ist keine Zeitangabe — `
|
|
+ "mit 'node tools/seiten-stempel.mjs' wird sie eine");
|
|
} else {
|
|
/* Eine Stunde Nachsicht, wie beim Stempel der Seiten. */
|
|
ok(wann + 3600_000 >= neuste,
|
|
`sie ist nicht älter als das, was vorgehalten wird `
|
|
+ `(Zahl ${a[1]}, jüngste Datei ${wer})`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`\n${geprueft} Prüfungen`);
|
|
console.log(fehler ? `${fehler} FEHLER` : "ALLES IN ORDNUNG");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|