diff --git a/server/index.js b/server/index.js index a6be4e86..56773b1f 100644 --- a/server/index.js +++ b/server/index.js @@ -430,10 +430,37 @@ app.use(express.static(SITE_DIR, { res.setHeader("Cache-Control", "no-cache"); return; } - /* Mit Versionsstempel in der Adresse: Der Name ist der Beweis. - `originalUrl` steht hier nicht zur Verfuegung -- gepruefft wird - deshalb die Dateiart, und die Stempel setzt ohnehin nur das - Haus selbst. */ + /* "UNVERAENDERLICH" NUR, WENN DIE ADRESSE DAS AUCH HERGIBT + (berichtigt 21.09.2026). + + Hier stand: "`originalUrl` steht hier nicht zur Verfuegung -- + geprueft wird deshalb die Dateiart." Nachgemessen stimmt das + nicht: `res.req.originalUrl` ist da und liefert + "/start.html?v=123". Die Annahme war nie geprueft worden, und + aus ihr folgte eine Zusage, die das Haus nicht halten kann. + + WAS DARAUS WURDE: Jede CSS- und JS-Datei ging mit + `max-age=31536000, immutable` hinaus -- auch unter ihrer + Adresse OHNE Stempel. "immutable" heisst woertlich: Der Inhalt + unter dieser Adresse aendert sich nie. Fuer `gate.css?v=...` + stimmt das; fuer `gate.css` ist es falsch, denn genau dort + aendert er sich bei jeder Auslieferung. + + UND ES WAR NICHT THEORETISCH. Am 21.09. wurde crew-haus.css auf + die zweite Adresse begrenzt und die Crew-Adresse aus gate.css + entfernt. Der Ursprung lieferte danach korrekt 404 und eine + bereinigte Datei -- Cloudflare aber weiterhin die ALTE Fassung + aus seinem Zwischenspeicher, 27 806 Byte, `cf-cache-status: + HIT`. Mit einem Jahr Zusage haette sie dort ein Jahr gelegen. + Ein Zwischenspeicher, der etwas Falsches zeigt, ist schlimmer + als gar keiner -- man glaubt ihm ja. + + Mit Stempel bleibt alles wie bisher: Die Seiten rufen ohnehin + immer `?v=...` auf, dort kostet die Aenderung nichts. Ohne + Stempel wird nachgefragt (`no-cache` heisst nicht "nicht + speichern", sondern "vorher rueckfragen"). */ + const adresse = String(res.req?.originalUrl || ""); + const mitStempel = /[?&]v=/.test(adresse); /* AUSNAHME: sw.js. Der Service Worker wird OHNE Stempel geladen (glocke.js: register('/workspace/sw.js')). Ein Jahr unveraenderlich hiesse: Ein Fehler darin bliebe ein Jahr stehen. Browser @@ -444,7 +471,9 @@ app.use(express.static(SITE_DIR, { return; } if (/\.(css|js|mjs)$/.test(pfad)) { - res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); + res.setHeader("Cache-Control", mitStempel + ? "public, max-age=31536000, immutable" + : "no-cache"); return; } if (/\.(webp|png|jpe?g|svg|gif|ico|woff2?)$/.test(pfad)) { diff --git a/server/pruef-zwischenspeicher.mjs b/server/pruef-zwischenspeicher.mjs index 50c04d56..55befb97 100644 --- a/server/pruef-zwischenspeicher.mjs +++ b/server/pruef-zwischenspeicher.mjs @@ -100,11 +100,58 @@ for (const p of ["/workspace/start.html", "/workspace/aufgaben.html", `${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(/max-age=\d{6,}/.test(r.cc || ""), `${p}: ${r.cc || "(keine Angabe)"}`); + 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"); @@ -289,55 +336,80 @@ console.log("\n=== Der Versionsstempel — daran hängt das automatische Update" if (!letzterCommit) { console.log(" -- konnte nicht nachsehen: kein Commit an workspace/assets gefunden"); } else { - /* DIE FRAGE IST NICHT "STEHT BEIDES IM SELBEN COMMIT", SONDERN - "KOMMT DIE ÄNDERUNG AN" (geschärft 20.09.2026). + /* DIE FRAGE IST "KOMMT DIE ÄNDERUNG AN" -- UND SIE WIRD AN DER + REIHENFOLGE BEANTWORTET, NICHT AN DER UHR (21.09.2026). - Vorher verlangte diese Stelle, dass derselbe Commit die - Stilvorlage UND die HTML ändert. Das ist der Normalfall (der - Stempel wird vor dem Committen gezogen), 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. + DREI FASSUNGEN, ZWEI DAVON FALSCH -- der Weg hierher gehört + in die Akte: - Verglichen wird deshalb der Stempel selbst mit dem Zeitpunkt - der letzten Änderung an assets/. Ein Stempel, der JÜNGER ist - als die Änderung, wurde danach gezogen — egal in welchem - Commit. Einer, der älter ist, kann sie nicht enthalten. + (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. - Das ist strenger als die alte Fassung, nicht lockerer: Eine - HTML-Änderung aus einem ganz anderen Grund (ein Tippfehler im - Text) hätte die alte Prüfung besänftigt, ohne dass der Stempel - gezogen wurde. Die Zahl lügt nicht. */ - const wannRoh = git("log", "-1", "--format=%cI", letzterCommit).stdout.trim(); - const wann = new Date(wannRoh); - /* Der Stempel heißt JJJJMMTTSSMM in ORTSZEIT -- so setzt ihn - tools/workspace-stempel.mjs. Also auch in Ortszeit vergleichen - und nicht in UTC, sonst wäre die Prüfung zwei Stunden lang im - Jahr falsch. */ - const z = (n) => String(n).padStart(2, "0"); - const alsStempel = (dd) => Number(`${dd.getFullYear()}${z(dd.getMonth() + 1)}` - + `${z(dd.getDate())}${z(dd.getHours())}${z(dd.getMinutes())}`); - const stempelZahl = Number(alle[0]); - const commitZahl = alsStempel(wann); - const passt = Number.isFinite(stempelZahl) && Number.isFinite(commitZahl) - && stempelZahl >= commitZahl; - ok(passt, - passt - ? `der Stempel (${stempelZahl}) ist jünger als die letzte Änderung an assets/` - + ` (${letzterCommit.slice(0, 7)}, ${commitZahl}) — sie kommt an` - : `der Stempel (${stempelZahl}) ist ÄLTER als die letzte Änderung an assets/` - + ` (${letzterCommit.slice(0, 7)}, ${commitZahl}) — der Browser behält seine` - + " alte Kopie, die Änderung käme bei niemandem an"); + (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. - /* GEGENPROBE: Diese Zeile ist ein Zahlenvergleich, und ein - Zahlenvergleich bestaetigt gern. Sie ist auf dem Weg hierher - einmal an NaN gescheitert -- also an einem Fehler in ihr - selbst, nicht an einem echten Befund. Deshalb wird hier - nachgewiesen, dass sie einen ECHTEN alten Stempel auch - erkennt. */ - const alterStempel = commitZahl - 1; - ok(!(alterStempel >= commitZahl) && stempelZahl >= commitZahl, - `Gegenprobe: ein Stempel von ${alterStempel} wuerde durchfallen, ${stempelZahl} nicht`); + (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)})`); + } + } } } }