Files
DogFatherGitandClaude Opus 5 43264c0990 Startseite hochwertiger: Typografie, Weltfarben, Event-Karte
Wunsch 27.08.2026: "soll nur noch hochwertiger und professioneller
aussehen." Kein neues Design, sondern die Details, die den Unterschied
zwischen "gemacht" und "gesetzt" ausmachen:

- Ueberschrift enger gesetzt (-.022em, Zeilenabstand 1.08, text-wrap:
  balance). Bei 4rem wirkt normale Laufweite auseinandergefallen; die
  drei Zeilen verteilen sich jetzt gleichmaessig statt mit kurzer
  Restzeile.
- Jede Welt-Kachel traegt die Farbe IHRER Welt statt dreimal derselben:
  Babyblau, Silber, das gedeckte Rot von Spicy Media. Die Werte sind
  nicht erfunden, sondern die --accent-Toene der drei Themendateien.
- Plaketten ("WELT 1") klein, in Versalien, weit gesperrt -- liest sich
  als Kapitelmarke statt als Beschriftung.
- Event-Karte: Datum als gesperrte Versalzeile (ordnet sich dem Titel
  unter), "Mehr erfahren" als ruhiger Knopf statt nacktem Textlink,
  Haarlinie am Bild, Text auf 5 Zeilen begrenzt -- damit nicht die
  Textlaenge aus der Verwaltung das Aussehen der Startseite bestimmt und
  bei zwei Events beide Kacheln gleich hoch sind (geprueft: 572/572px).

ZWEI FUNDE BEIM PRUEFEN, BEIDE WICHTIGER ALS DER FEINSCHLIFF:

1. INHALTSRICHTLINIE UND ZEILENENDEN. Der Browser rechnet die Pruefsumme
   eines Inline-Skripts NICHT ueber die Bytes der Datei, sondern ueber
   den Text im Dokument -- und der HTML-Parser ersetzt beim Einlesen
   jedes CR LF durch LF (HTML-Spezifikation, "preprocessing the input
   stream"). Eine Datei mit Windows-Zeilenenden ergibt serverseitig also
   eine ANDERE Summe, und die Seite fuehrt ihr eigenes Skript nicht mehr
   aus: kein Live-Status, keine Events, nichts. Live war es unauffaellig
   (Linux-Auscheckung hat LF), auf dem Windows-Rechner sofort tot
   (core.autocrlf=true). inhaltsrichtlinie.js vereinheitlicht jetzt vor
   dem Rechnen auf LF -- das ist die Summe, die der Browser wirklich
   bildet, und macht die Richtlinie unabhaengig davon, mit welchem
   Werkzeug eine Datei zuletzt gespeichert wurde.

2. TESTS MIT FESTEM PORT KOENNEN LUEGEN. Drei Server aus frueheren
   Laeufen liefen noch. Neue Testlaeufe konnten ihren Port nicht belegen,
   starben still -- und massen weiter gegen den ALTEN Code. Ergebnis
   waren Fehlermeldungen zu einem laengst behobenen Fehler. pruef-musik,
   pruef-kasse und pruef-startseite suchen sich jetzt einen freien Port.

Neuer Test pruef-startseite.mjs (19 Pruefungen) mit FESTEN Event-Daten:
Beim Pruefen kam einmal nichts vom Server, der Abschnitt blieb leer und
der Test haette "kein Fehler" gemeldet, obwohl er nichts gesehen hat.
Geprueft werden Maske am Artwork, genau eine Kachel bei einem Event,
volle Breite, saubere Textkuerzung auf ganze Zeilen, eigene Weltfarben,
zwei gleich hohe Kacheln bei zwei Events, Handy ohne Ueberlauf.

Alles gruen: startseite 19, musik 17, kasse 15, inhaltsrichtlinie 22,
handy 56.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-27 16:06:09 +02:00

196 lines
9.5 KiB
JavaScript

/* =====================================================================
Kasse & Anmeldung gegen die Inhaltsrichtlinie (27.08.2026)
WORUM ES GEHT
Auf abonnieren.html kommen zwei Dinge von fremden Servern: die
PayPal-Kasse und die Google-Anmeldung. Beide werden erst NACH einer
Nutzerhandlung nachgeladen (Klick bzw. Anmeldung) und beide sind
derzeit nicht scharf geschaltet -- googleClientId und paypalClientId
stehen auf null. Ein Fehler dort fällt deshalb heute NICHT auf. Er
fällt erst an dem Tag auf, an dem Dogi seine echten Zugangsdaten
einträgt: Dann stünde an der Kasse ein leerer Bereich, ohne
Fehlermeldung, ohne Eintrag im Server-Protokoll.
Genau dieses Muster hat schon einmal zugeschlagen (Musik-Knopf,
26.-27.08.2026). Deshalb wird hier vorgezogen geprüft, statt zu warten.
WIE ECHT DIESER TEST IST
- Er startet den ECHTEN server/index.js, damit die echte Kopfzeile
ausgeliefert wird. Ein Datei-Server erzeugt sie gar nicht.
- Er lädt das ECHTE PayPal-SDK mit deren offizieller Testkennung
("client-id=test") und baut die Knöpfe wirklich auf.
- Er lädt die ECHTE Google-Bibliothek und rendert den Knopf wirklich.
- Verstöße werden nicht aus Konsolentext geraten, sondern über das
Ereignis "securitypolicyviolation" gezählt -- dort stehen Richtlinie
und blockierte Adresse maschinenlesbar drin.
Kein echter Zugangscode und kein echtes Geld sind beteiligt.
===================================================================== */
import { chromium } from "playwright";
import { spawn } from "node:child_process";
import { createServer } from "node:net";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const HIER = dirname(fileURLToPath(import.meta.url));
/* Freien Port suchen statt festen nehmen: Laeuft noch ein Server aus einem
frueheren Lauf, kann der neue seinen Port nicht belegen, stirbt still --
und der Test misst gegen den ALTEN Code. Genau das ist am 27.08.2026
passiert und hat fast zu einer Fehlersuche in die falsche Richtung
gefuehrt. */
const PORT = await new Promise((auf, ab) => {
const probe = createServer();
probe.on("error", ab);
probe.listen(0, "127.0.0.1", () => { const p = probe.address().port; probe.close(() => auf(p)); });
});
const BASIS = `http://127.0.0.1:${PORT}`;
let ok = 0, fehl = 0;
const funde = [];
const pruefe = (name, gut, info) => {
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); funde.push(name + " — " + info); }
};
const server = spawn(process.execPath, ["index.js"], {
cwd: join(HIER, "server"),
env: { ...process.env, PORT: String(PORT) },
stdio: "ignore",
});
const beenden = (code) => { try { server.kill(); } catch { /* schon weg */ } process.exit(code); };
/* Notbremse: PayPals render() wartet ohne echten Bezahlvorgang unbegrenzt.
Ohne diese Grenze bliebe der Test im Zweifel ewig hängen. */
const notbremse = setTimeout(() => { console.log("\nZEIT ABGELAUFEN — Testlauf abgebrochen."); beenden(1); }, 180000);
notbremse.unref();
async function warteAufServer() {
for (let i = 0; i < 40; i++) {
try {
const res = await fetch(BASIS + "/abonnieren.html");
if (res.ok) return res;
} catch { /* noch nicht oben */ }
await new Promise((r) => setTimeout(r, 250));
}
throw new Error("Server ist nicht hochgekommen");
}
(async () => {
const antwort = await warteAufServer();
const richtlinie = antwort.headers.get("content-security-policy") || "";
const regel = (name) => (richtlinie.split(";").find((t) => t.trim().startsWith(name + " ")) || "").trim();
console.log("\n== Was die Kopfzeile verspricht ==");
pruefe("script-src kennt PayPal", /www\.paypal\.com/.test(regel("script-src")), regel("script-src").slice(0, 120));
pruefe("script-src kennt Google", /accounts\.google\.com\/gsi\/client/.test(regel("script-src")), "");
pruefe("frame-src kennt PayPal (Betrieb + Übung)", /www\.paypal\.com/.test(regel("frame-src")) && /sandbox\.paypal\.com/.test(regel("frame-src")), regel("frame-src"));
pruefe("frame-src kennt Google", /accounts\.google\.com\/gsi\//.test(regel("frame-src")), "");
pruefe("style-src kennt Googles Stilvorlage", /accounts\.google\.com\/gsi\/style/.test(regel("style-src")), regel("style-src"));
pruefe("Musik bleibt erlaubt", /open\.spotify\.com/.test(regel("frame-src")), "");
pruefe("Kein Sternchen in frame-src", !/\*/.test(regel("frame-src")), regel("frame-src"));
pruefe("Kein 'unsafe-inline' bei Skripten", !/unsafe-inline/.test(regel("script-src")), "");
const browser = await chromium.launch();
const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } });
await seite.goto(BASIS + "/abonnieren.html", { waitUntil: "domcontentloaded" });
await seite.evaluate(() => {
window.__verstoesse = [];
document.addEventListener("securitypolicyviolation", (e) =>
window.__verstoesse.push({ regel: e.violatedDirective, blockiert: e.blockedURI || "(inline)" })
);
});
/* ---------- PayPal ---------- */
console.log("\n== PayPal-Kasse (offizielle Testkennung) ==");
const paypal = await seite.evaluate(async () => {
const geladen = await new Promise((res) => {
const sk = document.createElement("script");
sk.src = "https://www.paypal.com/sdk/js?client-id=test&currency=EUR";
sk.onload = () => res(true);
sk.onerror = () => res(false);
document.body.appendChild(sk);
setTimeout(() => res(false), 20000);
});
if (!geladen || !window.paypal) return { geladen, knoepfe: false };
const ziel = document.createElement("div");
ziel.id = "kassen-test";
ziel.style.width = "320px";
document.body.appendChild(ziel);
/* Absichtlich NICHT abwarten: render() loest ohne echten Bezahlvorgang
nie auf. Es geht nur darum, ob der Rahmen entstehen darf. */
try { window.paypal.Buttons({}).render("#kassen-test").catch(() => {}); } catch { /* egal */ }
await new Promise((r) => setTimeout(r, 9000));
const rahmen = ziel.querySelector("iframe");
return { geladen, knoepfe: !!rahmen, hoehe: rahmen ? Math.round(rahmen.getBoundingClientRect().height) : 0 };
});
pruefe("PayPal-SDK wird geladen", paypal.geladen, paypal.geladen ? "geladen" : "BLOCKIERT");
pruefe("Bezahlknöpfe werden aufgebaut", paypal.knoepfe, paypal.knoepfe ? paypal.hoehe + "px hoch" : "kein Rahmen entstanden");
/* ---------- Google ---------- */
console.log("\n== Google-Anmeldung ==");
const google = await seite.evaluate(async () => {
const geladen = await new Promise((res) => {
const sk = document.createElement("script");
sk.src = "https://accounts.google.com/gsi/client";
sk.onload = () => res(true);
sk.onerror = () => res(false);
document.head.appendChild(sk);
setTimeout(() => res(false), 20000);
});
if (!geladen || !window.google) return { geladen, knopf: false };
const ziel = document.createElement("div");
ziel.id = "anmelde-test";
document.body.appendChild(ziel);
try {
window.google.accounts.id.initialize({
client_id: "000000000000-testtesttesttesttesttest.apps.googleusercontent.com",
callback: () => {},
});
window.google.accounts.id.renderButton(ziel, { type: "standard" });
} catch { return { geladen, knopf: false }; }
await new Promise((r) => setTimeout(r, 5000));
return { geladen, knopf: ziel.children.length > 0 };
});
pruefe("Google-Bibliothek wird geladen", google.geladen, google.geladen ? "geladen" : "BLOCKIERT");
pruefe("Anmelde-Knopf wird aufgebaut", google.knopf, google.knopf ? "gerendert" : "leer geblieben");
const googleRahmen = seite.frames().filter((f) => f.url().startsWith("https://accounts.google.com/gsi/")).length;
pruefe("Googles Rahmen darf laden", googleRahmen >= 1, googleRahmen + " Rahmen");
/* ---------- Das eigentliche Urteil ---------- */
console.log("\n== Verstöße gegen die Inhaltsrichtlinie ==");
const verstoesse = await seite.evaluate(() => window.__verstoesse);
const gezaehlt = {};
for (const v of verstoesse) {
const schluessel = v.regel + " <- " + v.blockiert.split("?")[0];
gezaehlt[schluessel] = (gezaehlt[schluessel] || 0) + 1;
}
Object.entries(gezaehlt).forEach(([k, n]) => console.log(" " + n + "x " + k));
pruefe("Kein einziger Verstoß", verstoesse.length === 0, verstoesse.length + " Verstöße");
const fehlerRahmen = seite.frames().filter((f) => f.url().startsWith("chrome-error://")).length;
pruefe("Kein Rahmen endet als Fehlerseite", fehlerRahmen === 0, fehlerRahmen + " Fehlerseiten");
/* Bilder von genau den beiden Testelementen.
WICHTIG: Die Elemente werden dafür NICHT im Seitenbaum verschoben.
PayPal baut seine Knöpfe wieder ab, sobald ihr Rahmen umgehängt wird
— ein Bild davon zeigte dann fälschlich eine leere Kasse, obwohl sie
kurz zuvor sauber stand. */
for (const [wahl, datei] of [["#kassen-test", "bild-kasse-paypal.png"], ["#anmelde-test", "bild-kasse-google.png"]]) {
try {
await seite.locator(wahl).scrollIntoViewIfNeeded();
await seite.waitForTimeout(600);
await seite.locator(wahl).screenshot({ path: datei });
} catch { /* Bild ist Beiwerk, kein Prüfkriterium */ }
}
await browser.close();
clearTimeout(notbremse);
console.log("\n===== " + ok + " ok, " + fehl + " Fehler =====");
if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); }
beenden(fehl ? 1 : 0);
})().catch((e) => {
console.error("Testlauf abgebrochen:", e.message);
beenden(1);
});