Knopf war unlesbar, und der Code wird jetzt bei jedem Schliessen neu verlangt

Zwei Rueckmeldungen vom 22.08.2026, beide behoben und mit Tests abgesichert.

1) "das sieht nicht gut aus" (Bildschirmfoto des Hauptknopfs)
   Ursache: ".wd a" ist Klasse+Element und damit spezifischer als die reine
   Knopfklasse ".wd-btn--haupt". Die Textfarbe des Knopfs wurde dadurch
   ueberstimmt -- hellblaue Schrift auf hellblauem Grund, praktisch
   unlesbar. Derselbe Spezifitaetsfehler wie zuvor bei den Namen auf der
   Zugangswand. Fix: ".wd a:not(.wd-btn)" plus zweistufig geschriebene
   Knopfregeln, damit das nicht wieder passieren kann.

2) "das sieht lang gezogen aus"
   Die Pillenform (border-radius 999px) laesst breite Knoepfe
   auseinandergezogen wirken, weil der Radius optisch mit der Breite
   mitwaechst. Jetzt fester Radius von 14px -- gleiche Form bei jeder
   Breite, ruhiger und hochwertiger.

3) "ich will das ich jedes mal den code gefragt werde wenn man die seite
   zu macht"
   Das Sitzungs-Cookie allein reicht dafuer nicht: Browser stellen genau
   solche Cookies beim Wiederherstellen von Tabs zurueck ("Dort
   fortfahren, wo du aufgehoert hast"), man landet dann ohne Codeabfrage
   wieder mitten in der Seite. Zusaetzlich jetzt eine Sitzungsmarke im
   sessionStorage, die beim echten Schliessen verschwindet. Fehlt sie bei
   vorhandenem Cookie, wird die Sitzung serverseitig beendet und zur
   Zugangswand geleitet. Token-Notbremse von 24 auf 8 Stunden gesenkt.
   13/13 Tests im echten Browser, inklusive Schutz vor Endlosschleife.

DEPLOY.md: zwei Checkouts auf dem Server dokumentiert (/home/dogiweb und
/home/dogiintern) und der Vorfall, dass /webdesign nach dem Pull kurz ohne
Zugangsschutz erreichbar war, weil der Dienstneustart fehlte.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-22 18:20:34 +02:00
co-authored by Claude Opus 5
parent 4b3ec450d5
commit 356bb974b8
6 changed files with 304 additions and 18 deletions
+45 -3
View File
@@ -36,11 +36,53 @@ ssh dogfather-server "cd /home/dogiweb/dogfather-universe && git pull --ff-only
Statische Dateien (HTML/CSS/JS/Bilder) sind damit **sofort** live — der Express-Dienst liefert
das Verzeichnis direkt aus, ein Neustart ist dafür **nicht** nötig.
Nur wenn Server-Code geändert wurde (`server/`, `server-internal/`):
## ⚠️ ES GIBT ZWEI CHECKOUTS AUF DEM SERVER, NICHT EINEN
Am 22.08.2026 beim Ausrollen des Webdesign-Bereichs gefunden — diese Datei
beschrieb vorher nur den ersten und war damit unvollständig:
| Dienst | Checkout | Liefert |
|---|---|---|
| `dogiweb.service` (Port 4100) | `/home/dogiweb/dogfather-universe/` | die öffentliche Website + `server/` |
| `dogiintern.service` (Port 4200) | `/home/dogiintern/dogfather-universe/` | die API unter `postfach.dogfather-universe.com` + `server-internal/` |
**Ein `git pull` in `/home/dogiweb` ändert an `server-internal/` also gar nichts.**
Wer nur dort zieht und danach `dogiintern.service` neu startet, startet den
Dienst mit unverändertem Code neu und wundert sich, warum nichts passiert.
`claudian` hat auf `/home/dogiintern/` **keinen Zugriff** (weder lesend noch
über den begrenzten sudo). Änderungen an `server-internal/` muss deshalb
Filipe selbst ausrollen:
```bash
ssh dogfather-server "sudo systemctl restart dogiweb.service" # öffentliche Seite
ssh dogfather-server "sudo systemctl restart dogiintern.service" # interner Bereich
sudo -u dogiintern git -C /home/dogiintern/dogfather-universe pull --ff-only origin main
sudo systemctl restart dogiintern.service
```
## Server-Code geändert? Dann ist ein Neustart PFLICHT
Statische Dateien sind nach dem Pull sofort live. Server-Code **nicht** —
der läuft weiter mit dem alten Stand, bis der Dienst neu startet.
```bash
ssh dogfather-server "sudo systemctl restart dogiweb.service" # nach Änderungen in server/
ssh dogfather-server "sudo systemctl restart dogiintern.service" # nach Änderungen in server-internal/
```
**Warum das hier besonders steht (Vorfall 22.08.2026):** Nach dem Pull des
Webdesign-Bereichs war `/webdesign/` für einige Minuten **ohne Zugangsschutz
öffentlich erreichbar** (HTTP 200 statt der Umleitung zur Zugangswand). Die
HTML-Dateien waren durch den Pull sofort da — die Schranke in
`server/webdesign-gate.js` lief aber erst nach dem Neustart von
`dogiweb.service`. Bei einem Bereich, der ausdrücklich nicht öffentlich sein
soll, ist genau dieses Zeitfenster der gefährliche Teil eines Deploys.
Merksatz: **Erst neu starten, dann „ist live" melden** — und danach mit
`curl -I` prüfen, dass die Schranke wirklich greift:
```bash
curl -sI https://dogfather-universe.com/webdesign/ | grep -iE "^location|x-robots-tag"
# erwartet: location: /webdesign/zugang.html?next=... und x-robots-tag: noindex, ...
```
**Achtung bei Zweig-Namen:** lokal heißt der Zweig `master`, auf dem Server und in Gitea `main`.
+19 -7
View File
@@ -153,8 +153,14 @@ body.wd::after {
.wd .wd-lead { font-size: clamp(1.08rem, 2vw, 1.32rem); color: var(--wd-text); line-height: 1.65; }
.wd .wd-klein { font-size: .92rem; }
.wd .wd-leise { color: var(--wd-text-soft); }
.wd a { color: var(--wd-blau); text-decoration: none; }
.wd a:hover { color: var(--wd-blau-hell); }
/* :not(.wd-btn) ist hier PFLICHT, kein Feinschliff.
Ohne das gewinnt ".wd a" (Klasse + Element = spezifischer als die reine
Knopf-Klasse ".wd-btn--haupt") und faerbt die Schrift auf dem hellblauen
Hauptknopf ebenfalls hellblau -- blau auf blau, praktisch unlesbar.
Genau so live gemeldet am 22.08.2026 per Bildschirmfoto. */
.wd a:not(.wd-btn) { color: var(--wd-blau); text-decoration: none; }
.wd a:not(.wd-btn):hover { color: var(--wd-blau-hell); }
.wd a.wd-btn { text-decoration: none; }
.wd strong { color: var(--wd-text); font-weight: 700; }
.wd-eyebrow {
@@ -272,8 +278,14 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
/* min-height 48px = Touch-Ziel-Mindestmaß (Masterplan S.15). */
min-height: 48px;
padding: .85rem 1.6rem;
border-radius: 999px;
padding: .85rem 1.7rem;
/* Abgerundetes Rechteck statt Pillenform (999px).
Rueckmeldung 22.08.2026: "das sieht lang gezogen aus". Stimmt — bei
einer Pille waechst der Radius optisch mit der Breite mit, ein breiter
Knopf wirkt dadurch auseinandergezogen wie eine Tablette. Ein fester
Radius haelt die Form bei jeder Breite gleich und wirkt ruhiger und
hochwertiger. */
border-radius: 14px;
border: 1px solid transparent;
font-family: var(--font-head);
font-weight: 700;
@@ -289,7 +301,7 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
.wd-btn:active { transform: translateY(1px) scale(.985); }
.wd-btn[disabled], .wd-btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; transform: none; }
.wd-btn--haupt {
.wd .wd-btn--haupt, .wd-btn--haupt {
/* Dunkler Text auf hellem Babyblau: 11.7:1 — der einzige Weg, einen
leuchtenden Knopf zu bauen, ohne die Lesbarkeit zu opfern. */
background: linear-gradient(135deg, var(--wd-blau-hell), var(--wd-blau) 55%, #63bcd8);
@@ -298,14 +310,14 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
}
.wd-btn--haupt:hover { box-shadow: 0 16px 44px -14px rgba(127, 208, 232, .95); transform: translateY(-2px); }
.wd-btn--zweit {
.wd .wd-btn--zweit, .wd-btn--zweit {
background: rgba(127, 208, 232, .07);
border-color: var(--wd-rand-stark);
color: var(--wd-blau-hell);
}
.wd-btn--zweit:hover { background: rgba(127, 208, 232, .14); border-color: var(--wd-blau); }
.wd-btn--still { background: transparent; border-color: var(--wd-rand); color: var(--wd-text-mid); }
.wd .wd-btn--still, .wd-btn--still { background: transparent; border-color: var(--wd-rand); color: var(--wd-text-mid); }
.wd-btn--still:hover { border-color: var(--wd-rand-stark); color: var(--wd-text); }
.wd-btn--breit { width: 100%; }
+61 -1
View File
@@ -761,10 +761,70 @@
}
/* ===================================================================
8. START
8. ZUGANG BEIM SCHLIESSEN DER SEITE BEENDEN
Wunsch vom 22.08.2026: "ich will das ich jedes mal den code gefragt
werde wenn man die seite zu macht."
Warum das Sitzungs-Cookie allein dafür NICHT reicht:
Ein Cookie ohne Ablaufdatum wirft der Browser beim Schließen zwar weg
— aber "Tabs wiederherstellen" (Chrome: "Dort fortfahren, wo du
aufgehört hast", Android beim erneuten Öffnen, iOS beim Zurückholen
einer App aus dem Speicher) stellt genau diese Cookies wieder her.
Man landet dann ohne Codeabfrage wieder mitten in der Seite.
Deshalb zusätzlich eine Kennung im sessionStorage. Der ist an die
Browser-Sitzung des Tabs gebunden und ist beim echten Schließen weg.
Fehlt die Kennung, obwohl ein Cookie da ist, war die Seite zu →
Sitzung serverseitig beenden und zurück zur Zugangswand.
Ehrliche Einschränkung: Wer die Adresse in einem NEUEN Tab von Hand
eingibt, hat dort ebenfalls einen leeren sessionStorage und wird
erneut gefragt. Das ist der Preis dafür, dass es beim Schließen
zuverlässig greift — und entspricht dem, was gewünscht war.
=================================================================== */
var SITZUNGS_MARKE = "wd-sitzung-aktiv";
function sitzungsMarkeSetzen() {
try { sessionStorage.setItem(SITZUNGS_MARKE, "1"); } catch (e) { /* egal */ }
}
function hatRollenCookie() {
return document.cookie.split(";").some(function (s) {
return s.trim().indexOf("dogiwd_rolle=") === 0;
});
}
function zugangPruefen() {
// Auf der Zugangswand selbst gibt es nichts zu beenden.
if (location.pathname.indexOf("/webdesign/zugang.html") === 0) return;
var marke = null;
try { marke = sessionStorage.getItem(SITZUNGS_MARKE); } catch (e) { return; }
if (marke) return; // laufende Sitzung, alles gut
/* Kein Rollen-Cookie -> es gab hier noch nie eine Anmeldung in diesem
Browser (oder der Bereich ist freigegeben). Dann NICHT abmelden:
sonst würde eine gerade erst begonnene Sitzung sofort beendet. */
if (!hatRollenCookie()) { sitzungsMarkeSetzen(); return; }
/* Cookie da, Marke weg -> die Seite war zwischendurch zu. */
fetch("/webdesign/zugang-beenden", { method: "POST" })
.catch(function () { /* auch ohne Antwort weiterleiten */ })
.then(function () {
location.replace(
"/webdesign/zugang.html?next=" + encodeURIComponent(location.pathname + location.search)
);
});
}
/* ===================================================================
9. START
=================================================================== */
function start() {
document.body.classList.add("wd");
zugangPruefen();
sitzungsMarkeSetzen();
uebersetzeSeite();
zeichneKopf();
zeichneFuss();
+157
View File
@@ -0,0 +1,157 @@
/* =====================================================================
test-webdesign-sitzung.mjs — "Beim Schließen der Seite wieder fragen"
Wunsch 22.08.2026: "ich will das ich jedes mal den code gefragt werde
wenn man die seite zu macht."
Das laesst sich nur in einem echten Browser pruefen, weil genau der
Punkt der schwierige ist: Browser stellen Sitzungs-Cookies beim
Wiederherstellen von Tabs zurueck. Playwright bildet das nach:
- neue Seite im GLEICHEN Kontext = neuer Tab, Cookie noch da,
sessionStorage leer -> muss erneut fragen
- gleiche Seite weiternavigieren = laufende Sitzung -> darf NICHT fragen
Voraussetzung: lokaler Testserver auf 4199 laeuft.
===================================================================== */
import { chromium } from "playwright";
const BASIS = "http://127.0.0.1:4199";
const CODE = "DOGI-TEST-1";
let ok = 0, schlecht = 0;
const pruefe = (name, b, zusatz = "") => {
if (b) { ok++; console.log(" ok " + name); }
else { schlecht++; console.log(" FEHLT " + name + " " + zusatz); }
};
const browser = await chromium.launch();
console.log("\n=== Zugang bei jedem Schliessen ===\n");
/* ---------- 1. Anmelden und normal weiterklicken ---------- */
console.log("1. Laufende Sitzung");
const kontext = await browser.newContext();
const seite = await kontext.newPage();
await seite.goto(BASIS + "/webdesign/zugang.html", { waitUntil: "networkidle" });
await seite.fill("#zg-code-dogfather", CODE);
await Promise.all([
seite.waitForURL((u) => !u.pathname.endsWith("zugang.html"), { timeout: 15000 }),
seite.click("#zg-form-dogfather button[type=submit]"),
]);
pruefe("Anmeldung fuehrt in den Bereich", seite.url().endsWith("/webdesign/"), seite.url());
await seite.goto(BASIS + "/webdesign/preise.html", { waitUntil: "networkidle" });
await seite.waitForTimeout(400);
pruefe("Weiterklicken fragt NICHT erneut", !seite.url().includes("zugang.html"), seite.url());
await seite.reload({ waitUntil: "networkidle" });
await seite.waitForTimeout(400);
pruefe("Neuladen fragt NICHT erneut", !seite.url().includes("zugang.html"), seite.url());
const markeDa = await seite.evaluate(() => sessionStorage.getItem("wd-sitzung-aktiv"));
pruefe("Sitzungsmarke ist gesetzt", markeDa === "1");
/* ---------- 2. Seite zu, Cookie bleibt (Tab-Wiederherstellung) ---------- */
console.log("\n2. Seite geschlossen, Cookie vom Browser wiederhergestellt");
{
const kekse = await kontext.cookies();
const sitzungsKeks = kekse.find((c) => c.name === "dogiwd_session");
pruefe("Sitzungs-Cookie hat KEIN Ablaufdatum",
!sitzungsKeks || sitzungsKeks.expires === -1,
"-> der Browser wirft es beim Schliessen selbst weg");
/* Neuer Tab im selben Kontext: Cookies bleiben (wie bei
"Tabs wiederherstellen"), sessionStorage ist aber leer. Genau der
Fall, in dem das Cookie allein NICHT reichen wuerde. */
const neuerTab = await kontext.newPage();
await neuerTab.goto(BASIS + "/webdesign/preise.html", { waitUntil: "networkidle" });
await neuerTab.waitForTimeout(1200);
pruefe("neuer Tab verlangt den Code erneut",
neuerTab.url().includes("zugang.html"),
"war " + neuerTab.url());
const nachher = await kontext.cookies();
const nochDa = nachher.find((c) => c.name === "dogiwd_session");
pruefe("die alte Sitzung wurde serverseitig beendet", !nochDa,
"-> nicht nur weggeklickt, sondern wirklich abgemeldet");
await neuerTab.close();
}
/* ---------- 3. Komplett neuer Browser ---------- */
console.log("\n3. Browser komplett neu gestartet");
{
const frisch = await browser.newContext();
const s = await frisch.newPage();
await s.goto(BASIS + "/webdesign/", { waitUntil: "networkidle" });
pruefe("frischer Browser landet auf der Zugangswand", s.url().includes("zugang.html"), s.url());
await frisch.close();
}
/* ---------- 4. Keine Endlosschleife ---------- */
console.log("\n4. Keine Endlosschleife nach dem Anmelden");
{
const k = await browser.newContext();
const s = await k.newPage();
let umleitungen = 0;
s.on("framenavigated", (f) => { if (f === s.mainFrame()) umleitungen++; });
await s.goto(BASIS + "/webdesign/zugang.html", { waitUntil: "networkidle" });
await s.fill("#zg-code-dogfather", CODE);
await Promise.all([
s.waitForURL((u) => !u.pathname.endsWith("zugang.html"), { timeout: 15000 }),
s.click("#zg-form-dogfather button[type=submit]"),
]);
await s.waitForTimeout(1500);
pruefe("bleibt nach der Anmeldung im Bereich", !s.url().includes("zugang.html"), s.url());
pruefe("keine auffaellige Umleitungsschleife", umleitungen < 8, umleitungen + " Navigationen");
await k.close();
}
/* ---------- 5. Knopf-Lesbarkeit (der gemeldete Fehler) ---------- */
console.log("\n5. Lesbarkeit des Hauptknopfs");
{
/* Frischer Kontext mit eigener Anmeldung: Schritt 2 hat die Sitzung von
`seite` absichtlich beendet -- dort wuerde jetzt die Zugangswand
stehen und der Knopf gar nicht existieren. */
const k5 = await browser.newContext();
const seite5 = await k5.newPage();
await seite5.goto(BASIS + "/webdesign/zugang.html", { waitUntil: "networkidle" });
await seite5.fill("#zg-code-dogfather", CODE);
await Promise.all([
seite5.waitForURL((u) => !u.pathname.endsWith("zugang.html"), { timeout: 15000 }),
seite5.click("#zg-form-dogfather button[type=submit]"),
]);
await seite5.waitForLoadState("networkidle");
const skip = seite5.locator("#wd-intro-skip");
if (await skip.isVisible().catch(() => false)) await skip.click();
await seite5.waitForTimeout(300);
const befund = await seite5.evaluate(() => {
const knopf = document.querySelector(".wd-hero .wd-btn--haupt");
if (!knopf) return null;
const s = getComputedStyle(knopf);
const zahl = (f) => (f.match(/[\d.]+/g) || []).slice(0, 3).map(Number);
return { farbe: zahl(s.color), radius: s.borderRadius, text: knopf.textContent.trim() };
});
pruefe("Hauptknopf gefunden", !!befund);
if (befund) {
/* Relative Helligkeit nach WCAG. Der Knopfgrund ist hellblau (~0.55) --
eine helle Schrift darauf waere der gemeldete Fehler. */
const [r, g, b] = befund.farbe;
const lin = (v) => { const x = v / 255; return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); };
const hell = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
pruefe("Schrift auf dem Hauptknopf ist DUNKEL", hell < 0.1,
`Helligkeit ${hell.toFixed(3)}, Farbe rgb(${befund.farbe.join(",")})`);
pruefe("Knopf ist kein Pillen-Oval mehr",
!/999/.test(befund.radius) && parseInt(befund.radius, 10) <= 20,
befund.radius);
}
await k5.close();
}
await browser.close();
console.log(`\n=== ${ok} bestanden, ${schlecht} gescheitert ===\n`);
process.exit(schlecht === 0 ? 0 : 1);
+16 -7
View File
@@ -31,13 +31,22 @@ import { createHmac, timingSafeEqual as cryptoTimingSafeEqual, randomUUID } from
export const WD_COOKIE = "dogiwd_session";
export const WD_ROLLE_COOKIE = "dogiwd_rolle";
/* Sitzungsdauer. Bewusst länger als die 12 Stunden im Universe: hier wird
inhaltlich gearbeitet (Anfragen prüfen, Texte pflegen), und ein
Rauswurf mitten in einer halb ausgefüllten Verwaltungsmaske wäre
teurer als das kleine zusätzliche Risiko. Das Cookie selbst bleibt ein
reines Sitzungs-Cookie ohne maxAge — beim Schließen des Browsers ist
der Zugang trotzdem weg. */
export const WD_SESSION_STUNDEN = 24;
/* Sitzungsdauer als NOTBREMSE, nicht als eigentlicher Ablauf.
Der Zugang soll bei jedem Schließen der Seite neu abgefragt werden
(Wunsch 22.08.2026: "ich will das ich jedes mal den code gefragt werde
wenn man die seite zu macht"). Dafür sorgen zwei Dinge zusammen:
1. Das Cookie ist ein reines Sitzungs-Cookie (kein maxAge/expires) —
der Browser wirft es beim Schließen selbst weg.
2. Eine Sitzungskennung im Browser-Sitzungsspeicher, die beim
Schließen ebenfalls verschwindet (siehe wd-core.js). Nötig, weil
viele Browser mit "Tabs wiederherstellen" auch Sitzungs-Cookies
zurückholen und Punkt 1 allein dann NICHT greift.
Diese Stundenzahl ist nur der Fall, dass ein Browser tagelang offen
bleibt. Von 24 auf 8 Stunden gesenkt: bleibt ein Gerät unbeaufsichtigt
offen liegen, soll der Zugang spätestens nach einem Arbeitstag zu sein. */
export const WD_SESSION_STUNDEN = 8;
const WD_PRAEFIX = "/webdesign";
+6
View File
@@ -251,6 +251,12 @@
return;
}
/* Sitzungsmarke setzen, BEVOR weitergeleitet wird. wd-core.js
prüft sie auf der Zielseite und würde sonst annehmen, die Seite
sei zwischendurch geschlossen worden — und einen direkt wieder
hierher zurückwerfen. Endlosschleife. */
try { sessionStorage.setItem("wd-sitzung-aktiv", "1"); } catch (e) { /* egal */ }
/* Erfolg: Knopf bewusst gesperrt lassen — die Seite wechselt
gleich, ein weiterer Tap wäre nur verwirrend. */
knopf.textContent = window.WD.t("zg_willkommen");