Filipe: "kannst du bitte das logo wenn man es installiert auf pc oder handy und oben rechts in der leiste noch perfektionnieren." Beide Stellen hatten denselben Fehler, und er war derselbe wie an mehreren Stellen davor: Der Husky lag als MASKE auf einer Farbflaeche. Von einer Maske zaehlt nur der Alphakanal, und die Vorlage ist rundum freigestellt -- uebrig blieb eine geschlossene Flaeche in Hundeform. Kein Auge, keine Schnauze, kein Ohrinneres. Ein Fleck. Jetzt liegt an beiden Stellen dieselbe Zeichnung im Mischmodus "screen" ueber einer stahlblauen Silhouette: Schwarz laesst das Fell dunkel, Weiss hebt Gesicht, Ohren und Auge heraus. Gleiche Farben, gleicher Daempfer (.88) -- ein Zeichen, zwei Orte. App-Symbol ausserdem: - Die Chili lief mit ihrem Stiel quer ueber den Fang. Sie ist jetzt gespiegelt, kleiner und liegt hinter dem Hals. - Der Hals endete in einer geraden Kante (die Vorlage ist unten angeschnitten). Eine zweite Maske blendet ihn aus, statt ihn abzuschneiden. - Der Grund war matschig: Rot und Blau trafen sich diagonal genau dort, wo der Kopf steht. Jetzt kaltes Licht oben, warmes unten. - Unter 64 px faellt die Gesichtszeichnung weg, die Chili aber NICHT -- klein erkennt man ein Zeichen zuerst an der Farbe. - Alle Masse haengen an einer Zahl (Groesse der Gruppe), nicht an sechs. Kopfleiste ausserdem: - Die Chili links hatte einen BLAUEN Schein -- aus der Zeit, als dort der Husky stand. Der Schein ist nie mitgewandert. Jetzt warm. - `filter` ersetzt, es ergaenzt nicht: Beim Ueberfahren wurde der Schein geloescht und das Zeichen dabei flacher statt heller. Behoben. Damit die Aenderung auch ankommt: - Der Stempel gilt jetzt auch fuer die App-Symbole und fuer das Manifest. Bilder werden einen Tag zwischengespeichert, das Symbol der INSTALLIERTEN App gar nicht neu geholt -- ohne Stempel haette niemand das neue Zeichen gesehen. - pruef-zwischenspeicher prueft das (18 -> 21 Pruefungen). Werkzeuge: - tools/ausschnitt.mjs (neu): schneidet aus einem Bildschirmfoto ein Stueck heraus und vergroessert die PNG-Datei. Noetig, weil eine Vergroesserung per CSS-transform den Browser NEU rechnen laesst -- ich habe damit 264 px beurteilt und geglaubt, es seien 22, und daraus den falschen Schluss gezogen, ein Gesicht trage bei 22 px nicht. - tools/ansicht.mjs: AUSSCHNITT=<selektor> nimmt nur ein Element auf. - workspace-symbol.mjs: SYMBOL_ZIEL lenkt die Ausgabe um (Entwuerfe ansehen, ohne die sechs echten Dateien zu ueberschreiben), und zwei Gegenproben pruefen jetzt, dass Gesicht und Chili wirklich zu sehen sind -- die alte Pruefung sagte nur "es steht etwas drauf" und haette den Fleck anstandslos durchgewunken. Co-Authored-By: Claude Opus 5 <[email protected]>
309 lines
14 KiB
JavaScript
309 lines
14 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 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 {
|
|
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);
|