Files
dogfather-universe/server/pruef-zwischenspeicher.mjs
T
DogFatherGitandClaude Opus 5 5881f9ce2d Der Versionsstempel wird jetzt geprueft -- daran haengt das Update
Filipe: "ich hoffe der update passiert bei den benutzern alle immer
automatisch mit."

Nachgesehen statt behauptet. Die Kette lautet:

  HTML geht mit `Cache-Control: no-cache` hinaus (live bestaetigt,
  Cloudflare laesst sie durch: cf-cache-status DYNAMIC)
    -> der Browser holt bei jedem Aufruf die neueste HTML
    -> darin stehen die Versionsstempel der Stilvorlagen
    -> neuer Stempel = neue Adresse = neue Datei.

Das funktioniert. Es haengt aber an EINEM Glied, und das setze ich von
Hand: dem Stempel. Vergesse ich ihn, bekommt der Benutzer die neue HTML
mit den ALTEN Adressen -- und die sind seit heute Mittag ein Jahr lang
gueltig zwischengespeichert. Er saehe die Aenderung monatelang nicht,
und niemandem fiele auf, warum.

--- WIE ERNST DAS IST, HAT SICH HEUTE GEAENDERT ---

Nachgezaehlt in den letzten vierzig Commits an workspace/assets: DREI
haben Stilvorlagen geaendert, ohne eine HTML anzufassen -- 4c0ff5c,
0a0dbff und 7c00e76, letzterer von heute Frueh.

Sie waren harmlos, und zwar aus einem Grund, den ich heute selbst
beseitigt habe: Bis Mittag bekam JEDE Datei `no-cache`, auch jede
Stilvorlage. Ein vergessener Stempel fiel damit nicht auf, weil der
Browser ohnehin bei jedem Aufruf nachfragte.

Seit die Stilvorlagen ein Jahr liegenbleiben duerfen (was richtig ist
-- sie tragen ja einen Stempel), ist derselbe Fehler kein Schoenheits-
fehler mehr, sondern ein stiller Ausfall. Wer eine Sicherung
wegnimmt, muss die Bedingung absichern, unter der sie ueberfluessig
war. Genau das fehlte.

--- DIE BEIDEN NEUEN PRUEFUNGEN ---

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, waehrend alles andere
   stimmt. Mit Gegenprobe.

2. Wer eine Stilvorlage aendert, aendert auch den Stempel. Geprueft am
   letzten COMMIT an assets/, nicht am Arbeitsverzeichnis: Waehrend des
   Bauens ist die Datei immer neuer als der Stempel, und eine Warnung,
   die dauernd kommt, wird weggeklickt.

   Mit dem DRITTEN AUSGANG: Ohne Git-Arbeitsverzeichnis ist die Frage
   nicht zu beantworten, und das wird als "konnte nicht nachsehen"
   gemeldet statt als "in Ordnung" verbucht.

18 Pruefungen in pruef-zwischenspeicher, alle gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 17:36:45 +02:00

259 lines
12 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 { portMussFreiSein } = await import("./helfer-port.mjs");
await portMussFreiSein(4343, "die Zwischenspeicher-Pruefung");
process.env.PORT = "4343";
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:4343";
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)"}`);
}
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);
ok(/max-age=\d{6,}/.test(r.cc || ""), `${p}: ${r.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 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();
for (const s of seiten) {
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 {
const dateien = git("show", "--name-only", "--format=", letzterCommit)
.stdout.split(/\r?\n/).filter(Boolean);
const hatAssets = dateien.some((f) => /^workspace\/assets\/.*\.(css|js|mjs)$/.test(f));
const hatSeiten = dateien.some((f) => /^workspace\/.*\.html$/.test(f));
ok(!hatAssets || hatSeiten,
hatAssets && !hatSeiten
? `Commit ${letzterCommit.slice(0, 7)} hat Stilvorlagen geändert, aber keine HTML — `
+ "der Stempel wurde nicht mitgezogen, die Änderung käme bei niemandem an"
: `im letzten Commit an assets/ (${letzterCommit.slice(0, 7)}) wurde der Stempel mitgezogen`);
}
}
}
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);