/* 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)`); } 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);