Files
dogfather-universe/webdesign/sw.js
T
DogFatherGitandClaude Opus 5 0ef64ba222 Systemabnahme: fuenf Altlasten geschlossen
Vollstaendiger Durchlauf ueber alle Pruefungen. Dabei kamen fuenf Dinge
ans Licht, die teils seit Wochen offen standen.

1. WCAG-KONTRAST -- der einzige Punkt, der echte Besucher betraf

Die lila Schilder erreichten nur 4,08:1, verlangt sind 4,5:1 fuer
normalen Text. 17 Fundstellen, alle dieselbe Ursache: Die Schrift nutzte
den vollen Ton Aurora Violet.

Gemessen wurde nicht geschaetzt: Untergrund rgb(38,41,81) aus dem
echten Bild ausgelesen, Schrift 12,5 Punkte -- und fett zaehlt erst ab
18,5 Punkten als "grosser Text".

Neu ist --wd-lila-hell (#C197FF, 6,02:1). Bewusst NICHT der knappste
Wert: #B380FF haette mit 4,91:1 gereicht, aber dasselbe Schild steht
auch ueber der Buehne im Verwaltungsbereich. Ein Ton, der nur an einer
Stelle knapp besteht, faellt beim naechsten Hintergrund wieder durch.
Dasselbe Muster wie --wd-blau-hell, das genau deshalb existiert.

2. GEHEIMNIS-TEST -- der Test war veraltet, nicht der Code

Er verlangte ".env hat Vorrang", die Umsetzung macht das Gegenteil. Die
Begruendung im Code ueberzeugt: Wer PayPal-Daten im Formular eintraegt,
erwartet, dass sie gelten. Andernfalls koennte ein alter Wert in der
.env sie stumm ueberstimmen -- und man sucht stundenlang.

Der Test prueft jetzt die tatsaechliche Reihenfolge, dazu neu, dass ein
gleichzeitig vorhandener Serverwert auch angezeigt wird.

Ausserdem endete der Lauf trotz gruener Pruefungen mit einer
Fehlermeldung: Unter Windows haelt eine offene SQLite-Datei eine Sperre,
das Aufraeumen scheiterte mit EPERM. Auf Linux waere es
durchgelaufen -- dasselbe Skript mit unterschiedlichem Ergebnis je
Rechner. Jetzt wird erst geschlossen, dann geloescht, und das Aufraeumen
kann den Lauf nicht mehr zum Scheitern bringen.

3. PORTAL-TEST -- ebenfalls veraltet

Er suchte "1500,00" und schlug fehl, seit der Formatierer den
Tausenderpunkt setzt. "1.500,00 EUR" ist die korrekte deutsche
Schreibweise; gerade ab vier Stellen macht der Punkt eine Zahl auf einen
Blick lesbar.

4. ZWEI SEITEN OHNE WOERTERBUCH -- eine Entscheidung, keine Luecke

"diagnose" ist ein Betriebswerkzeug. "rechtliches" ist der heiklere
Fall: Rechtstexte durch eine ungepruefte Uebersetzung zu schicken ist
gefaehrlicher, als sie einsprachig zu lassen. Ein Fehler in einer
Widerrufsbelehrung wirkt gegen den Verfasser.

Die Pruefung meldete beides als "Datei fehlt" -- das las sich wie ein
Versehen und stand deshalb dauerhaft in der Fehlerliste, ohne dass
jemand etwas tat. Jetzt sind beide benannt und begruendet.

5. TAG-UNGLEICHGEWICHT -- eine Fehlmessung

Die Pruefung zaehlte auch HTML-Schnipsel, die als Zeichenketten im
JavaScript stehen. Dort steht ein oeffnendes <div> regelmaessig in einer
anderen Zeichenkette als sein </div>, weil die Teile erst beim
Zusammensetzen ein Ganzes ergeben.

Gegengeprueft im Browser: geparster Baum einwandfrei, kein
Skriptfehler, Verschachtelung unauffaellig. Es war nie ein
Strukturfehler. Die Meldung stand aber monatelang als "2 Fehler" da und
haette jede echte Meldung entwertet, die dazugekommen waere.

STAND

  Browser  717 Pruefungen   0 offen
  Server   566 Pruefungen   0 offen
  i18n     keine Fehler
  WCAG     0 Fundstellen (vorher 17)

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 17:13:42 +02:00

192 lines
7.9 KiB
JavaScript

/* =====================================================================
sw.js — Service Worker NUR für /webdesign/
Macht den Bereich auf Handy und Computer als App installierbar und
sorgt dafür, dass die Oberfläche auch bei wackeliger Verbindung
sofort da ist.
⚠️ SICHERHEITSENTSCHEIDUNG, DIE HIER DEN AUSSCHLAG GIBT
Der Webdesign-Bereich ist zugangsgeschützt (server/webdesign-gate.js).
Ein Service Worker, der HTML-Seiten zwischenspeichert, würde diesen
Schutz aushebeln: Nach dem Abmelden — oder nachdem jemand anders das
Gerät in die Hand nimmt — lieferte der Zwischenspeicher die geschützten
Seiten samt Preisen, Anfragen und Kundendaten weiterhin aus, ohne dass
der Server je gefragt wird. Genau das ist der klassische Fehler bei
PWAs hinter einer Anmeldung.
Deshalb gilt hier strikt:
- HTML-Seiten werden NIEMALS zwischengespeichert. Immer Netz.
Ist kein Netz da, kommt eine schlichte Offline-Meldung — nicht die
zuletzt gesehene, möglicherweise vertrauliche Seite.
- Zwischengespeichert werden ausschließlich die neutralen Bausteine
unter /assets/ (Stilvorlagen, Skripte, Logo, Icons). Die enthalten
keine Kunden- oder Projektdaten und liegen ohnehin außerhalb der
Zugangsschranke.
- Antworten der API (postfach.dogfather-universe.com) werden
ebenfalls nie zwischengespeichert.
Das kostet Offline-Komfort. Der ist hier aber weniger wert als die
Zusicherung, dass nach dem Schließen der Sitzung wirklich nichts mehr
einsehbar ist.
===================================================================== */
/* Version im Namen: eine Änderung dieser Zeichenkette wirft beim nächsten
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */
const CACHE_NAME = "dogfather-webdesign-v49";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
const GRUNDGERUEST = [
"/assets/css/main.css",
"/assets/css/webdesign.css",
"/assets/js/wd-core.js",
"/assets/img/logo-dogfather-transparent.png",
"/assets/img/wd-app-192.png",
"/assets/img/wd-app-512.png",
];
self.addEventListener("install", (ereignis) => {
ereignis.waitUntil(
caches.open(CACHE_NAME).then((speicher) =>
/* addAll bricht komplett ab, wenn EINE Datei fehlt — dann wäre der
Service Worker gar nicht erst installiert und die App nicht
installierbar. Deshalb jede Datei einzeln und Fehler schlucken. */
Promise.all(
GRUNDGERUEST.map((pfad) =>
speicher.add(new Request(pfad, { cache: "reload" })).catch(() => {})
)
)
)
);
/* Sofort übernehmen statt auf das Schließen aller Tabs zu warten —
sonst bleibt nach einem Deploy tagelang die alte Fassung aktiv. */
self.skipWaiting();
});
self.addEventListener("activate", (ereignis) => {
ereignis.waitUntil(
caches.keys()
.then((namen) => Promise.all(
namen.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n))
))
.then(() => self.clients.claim())
);
});
/* Schlichte Offline-Meldung. Bewusst als erzeugte Antwort statt als
Datei — so kann sie nie mit echtem Seiteninhalt verwechselt werden.
Fünf Sprachen wären hier Überinstrumentierung: der Text erscheint nur
ohne Internet und ist absichtlich sehr kurz gehalten. */
function offlineSeite() {
return new Response(
`<!DOCTYPE html><html lang="de"><head><meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Keine Verbindung</title>
<style>
body{margin:0;min-height:100svh;display:flex;align-items:center;justify-content:center;
background:#05070b;color:#eef4fb;font-family:system-ui,sans-serif;text-align:center;padding:2rem}
.k{max-width:34ch}
h1{font-size:1.4rem;margin:0 0 .6rem}
p{color:#b3c2d4;line-height:1.6;margin:0 0 1.4rem}
button{min-height:48px;padding:.8rem 1.6rem;border:none;border-radius:999px;
background:#7fd0e8;color:#04070c;font-weight:700;font-size:1rem;cursor:pointer}
</style></head><body>
<div class="k">
<h1>Keine Verbindung</h1>
<p>Dogfather Webdesign braucht eine Internetverbindung. Sobald du wieder online bist, geht es normal weiter.</p>
<button onclick="location.reload()">Erneut versuchen</button>
</div></body></html>`,
{ status: 503, headers: { "Content-Type": "text/html; charset=utf-8" } }
);
}
self.addEventListener("fetch", (ereignis) => {
const anfrage = ereignis.request;
// Nur einfache Abrufe. Absendungen (POST) gehen immer direkt ans Netz.
if (anfrage.method !== "GET") return;
const url = new URL(anfrage.url);
// Fremde Server (API, externe Bilder) nie anfassen.
if (url.origin !== self.location.origin) return;
/* --- HTML: immer Netz, niemals zwischenspeichern (siehe Kopf) --- */
const willHtml = anfrage.mode === "navigate" ||
(anfrage.headers.get("accept") || "").includes("text/html");
if (willHtml) {
ereignis.respondWith(
fetch(anfrage).catch(() => offlineSeite())
);
return;
}
/* --- Zugangs-Endpunkte niemals zwischenspeichern --- */
if (url.pathname.startsWith("/webdesign/zugang-")) return;
/* --- Neutrale Bausteine unter /assets/ ---
⚠️ HIER WAR EIN ECHTER FEHLER (gemeldet 22.08.2026: "immer noch so").
Vorher lief das als "stale-while-revalidate": die gespeicherte Fassung
ging sofort raus, im Hintergrund wurde eine frische geholt. Das ist
schnell — bedeutet aber, dass jede Korrektur an CSS oder JavaScript
beim ersten Aufruf UNSICHTBAR bleibt und erst beim zweiten wirkt.
Genau das ist passiert: die Korrektur am gestreckten Logo war auf dem
Server längst live, der Browser hat aber die alte Stilvorlage aus
meinem eigenen Zwischenspeicher angezeigt. Ein Fehler, der wie ein
nicht behobener Fehler aussieht — die unangenehmste Sorte.
Jetzt: erst Netz, Zwischenspeicher nur als Notfallnetz.
Für Stilvorlagen und Skripte ist Richtigkeit wichtiger als die
eingesparten Millisekunden — eine falsche Darstellung kostet mehr
als ein kurzer Ladevorgang. Bilder und Symbole bleiben beim schnellen
Weg: die ändern sich praktisch nie, und wenn doch, bekommen sie einen
neuen Dateinamen. */
if (url.pathname.startsWith("/assets/")) {
const istCodeOderStil = /\.(css|js|mjs)$/i.test(url.pathname);
if (istCodeOderStil) {
// Netz zuerst
ereignis.respondWith(
fetch(anfrage)
.then((antwort) => {
if (antwort && antwort.status === 200 && antwort.type === "basic") {
const kopie = antwort.clone();
caches.open(CACHE_NAME).then((speicher) => speicher.put(anfrage, kopie));
}
return antwort;
})
.catch(() => caches.match(anfrage).then((g) => g || Response.error()))
);
return;
}
// Bilder, Symbole, Schriften: Zwischenspeicher zuerst
ereignis.respondWith(
caches.open(CACHE_NAME).then(async (speicher) => {
const gespeichert = await speicher.match(anfrage);
const ausDemNetz = fetch(anfrage)
.then((antwort) => {
/* Nur vollständige, eigene Antworten ablegen. Teilantworten
(206, z.B. bei Video-Sprüngen) lassen sich nicht sinnvoll
zwischenspeichern und würden den Speicher beschädigen. */
if (antwort && antwort.status === 200 && antwort.type === "basic") {
speicher.put(anfrage, antwort.clone());
}
return antwort;
})
.catch(() => null);
return gespeichert || (await ausDemNetz) || Response.error();
})
);
return;
}
/* Alles Übrige: kein respondWith -> der Browser macht es ganz normal
selbst. Das ist die sicherste Voreinstellung. */
});