Files
dogfather-universe/webdesign/sw.js
T
DogFatherGit e92daed103 CRYONOVA Schritt 2: Bildwelt, Materialien, Rangsystem, Status
Setzt die restlichen Kapitel des Design-Systems um (Seiten 7, 14, 16,
19, 24-29). Wie Schritt 1 ausschliesslich Farbe, Material und Licht --
Aufbau, Navigation, Texte und Funktionen bleiben unangetastet.

BILDWELT (Seiten 24-29)
Die Motive sind jetzt echte Seitenhintergruende: Ring auf Startseite
und Portal, Kristallwelt an der Zugangswand. Ueber jedem Motiv liegt
eine Navy-Glasflaeche -- das System fordert auf Seite 31 ausdruecklich,
dass bei Bedarf "eine Navy- oder Frost-Glasflaeche zwischen Bild und
Inhalt" liegt. Ohne sie waere Text auf den hellen Kristallspitzen nicht
sicher lesbar, und genau dort macht ein schoenes Bild eine Seite
unbrauchbar.

Von 2,5 MB auf 96-229 KB (WebP), auf dem Handy 30-73 KB. Das System
nennt fuer Mobile ausdruecklich Ladezeit als Kriterium.

DAS LOGO IM BILD -- eine Entscheidung gegen die woertliche Vorgabe
Zwei der drei Motive zeigen das Dogfather-Logo gross in der Mitte. Als
Seitenhintergrund waere das ein zweites Logo neben dem echten in der
Kopfzeile: zwei Marken, die um dieselbe Aufmerksamkeit ringen. Beim
ersten Versuch schien es an der Zugangswand hinter den Karten durch
(gemessen 15,9 % helle Flaeche im Logobereich). Die Zugangswand nutzt
deshalb jetzt einen Ausschnitt der linken Bildhaelfte -- dieselbe
Kristallwelt, dasselbe Licht, nur ohne Wappen. Die Logo-Motive bleiben
als Blickfang-Klasse erhalten, dort wo sie fuer sich stehen duerfen.

MATERIALIEN (Seite 7)
Optic Glass, Frosted Ice, Prism Edge und Void Navy als Klassen. "Hell"
heisst auf einer dunklen Seite aufgehelltes Navy, nicht Weiss -- echtes
Weiss waere ein Loch im Bildschirm, und das System verbietet weisse
Vollflaechen ausdruecklich.

RANGSYSTEM (Seite 14)
Drei Stufen fehlten: Premium (Indigo), VIP (Champagne), Gefahr (Coral).
Wenn jeder Knopf gleich laut ist, ist keiner mehr laut.

Zwei bewusste Abweichungen von der naheliegenden Loesung:
- Indigo steht als FLAECHE unter hellem Text, nie als Schriftfarbe. Es
  erreicht als Text nirgends 4,5:1 (gemessen 3,25-4,29). Der Test haelt
  das dauerhaft fest.
- Gefahr ist NICHT vollflaechig rot. Ein voll gefuellter roter Knopf
  zieht den Blick staerker an als die Hauptaktion und wird dadurch
  versehentlich gedrueckt. Kritische Aktionen sollen auffindbar sein,
  nicht verlockend.

FOKUSRING (Seite 19)
Doppelter Aqua-Ring: 2 px Aqua aussen, dunkler Innenring. Kein Schmuck
-- ein einfacher heller Ring verschwindet auf hellen Flaechen, ein
dunkler auf dunklen. Die Kombination ist auf JEDEM Untergrund sichtbar.

STATUS-SPEKTRUM (Seite 16)
Acht Zustaende. Die Klasse faerbt nur -- den Text liefert immer das
Markup. Es gibt bewusst keine Variante, die nur einen farbigen Punkt
zeigt: Wer Farben nicht unterscheiden kann, saehe dann gar nichts. Der
Test prueft, dass keine Statusmarke ohne Text existiert.

Dabei einen eigenen Fehler gefunden und behoben: Ich hatte beim Bauen
des Premium-Knopfes selbst einen losen Hex-Wert eingesetzt -- genau
das, was Token-Regel 05 verbietet und was der Test dann meldete.

Geprueft: 43 (CRYONOVA, von 24 erweitert) + 0 Fundstellen (WCAG ueber
12 Seiten x 5 Sprachen, jetzt MIT Bildhintergruenden) + 55 + 40 + 69 +
54 + 42 + 16 + 5 + 20 + 64 + 58 -- alles gruen.
2026-08-24 22:43:58 +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-v32";
/* 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. */
});