From 1d99da8ef8c9462edd89f42d003ec3568c99f97e Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 27 Aug 2026 13:52:36 +0200 Subject: [PATCH] Musik-Knopf repariert: Inhaltsrichtlinie blockierte die Spotify-Player Gemeldet am 27.08.2026 ("unten rechts laeuft was nicht richtig mit der Musik"). Ursache lag nicht in der Seite, sondern in der am 26.08.2026 eingefuehrten Inhaltsrichtlinie: "frame-src 'none'" verbietet dem Dokument jede Einbettung -- und die beiden Player (Hasidog, Van-Van) sind genau das. Der Knopf reagierte, das Feld ging auf, die Player blieben leer. Kein Absturz, keine Fehlermeldung auf der Seite; die Begruendung stand nur in der Browser-Konsole. - frame-src erlaubt jetzt genau eine Quelle: https://open.spotify.com. Kein Sternchen, kein 'unsafe-*'. Was im Spotify-Rahmen passiert, regelt Spotifys eigene Richtlinie. - Neuer Test pruef-musik.mjs (17 Pruefungen). Er startet bewusst den ECHTEN server/index.js statt eines Datei-Servers -- ein einfacher Datei-Server erzeugt die Kopfzeile gar nicht und haette den Fehler nie gesehen. Geprueft wird im echten Browser, ob die Rahmen wirklich von open.spotify.com laden (statt einer Fehlerseite), dazu Tippziel, Position, Schliessen per Knopf und Escape, Handy-Layout. Beim Nachsehen aufgefallen und NOCH OFFEN: PayPal-Kasse und Google-Anmeldung auf abonnieren.html laden ihre Skripte und Rahmen ebenfalls von fremden Adressen. Beide sind derzeit nicht scharf (googleClientId/paypalClientId sind null), wuerden aber mit derselben Richtlinie an derselben Stelle still ausfallen, sobald Dogi seine Zugangsdaten eintraegt. Wird gesondert mit ihm besprochen. Co-Authored-By: Claude Opus 5 --- pruef-musik.mjs | 154 ++++++++++++++++++++++++++++++++++++ server/inhaltsrichtlinie.js | 26 +++++- 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 pruef-musik.mjs diff --git a/pruef-musik.mjs b/pruef-musik.mjs new file mode 100644 index 00000000..dddac3d7 --- /dev/null +++ b/pruef-musik.mjs @@ -0,0 +1,154 @@ +/* ===================================================================== + Musik-Knopf unten rechts — läuft gegen den ECHTEN Server + + WARUM NICHT GEGEN EINEN SIMPLEN DATEI-SERVER + + Der Fehler, um den es hier geht, stand in keiner HTML-Datei und in + keiner Zeile JavaScript: Die Seite war fehlerfrei, aber der Server + schickte "frame-src 'none'" mit — und der Browser warf die beiden + Spotify-Player still weg (gemeldet 27.08.2026). Ein Test, der die + Dateien nur ausliefert, hätte das nie gesehen, weil er die Kopfzeile + gar nicht erst erzeugt. + + Deshalb startet dieser Test server/index.js selbst auf einem eigenen + Port und prüft im echten Browser, ob die Player wirklich ankommen. + + Geprüft wird: + 1. Der Knopf ist da und groß genug zum Antippen. + 2. Klick klappt das Feld auf, beide Player werden nachgeladen. + 3. Die Rahmen laden WIRKLICH von open.spotify.com (nicht Fehlerseite). + 4. Kein einziger Verstoß gegen die Inhaltsrichtlinie in der Konsole. + 5. Die Richtlinie bleibt eng: nur open.spotify.com, kein Sternchen. + 6. Schließen über ✕ und über Escape. + 7. Am Handy passt das Feld in den Bildschirm. + ===================================================================== */ +import { chromium } from "playwright"; +import { spawn } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const PORT = 4433; +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", "pipe", "pipe"], +}); + +async function warteAufServer() { + for (let i = 0; i < 40; i++) { + try { + const res = await fetch(BASIS + "/index.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") || ""; + + console.log("\n== Inhaltsrichtlinie =="); + const frameRegel = (richtlinie.split(";").find((t) => t.trim().startsWith("frame-src")) || "").trim(); + pruefe("frame-src erlaubt Spotify", /https:\/\/open\.spotify\.com/.test(frameRegel), frameRegel || "(keine frame-src-Regel)"); + pruefe("frame-src bleibt eng (kein *)", !/[\s:]\*/.test(frameRegel) && !/'unsafe/.test(frameRegel), frameRegel); + + const browser = await chromium.launch(); + + /* ---------- Computer ---------- */ + console.log("\n== Computer =="); + const seite = await browser.newPage({ viewport: { width: 1366, height: 800 } }); + const verstoesse = new Set(); + seite.on("console", (m) => { + const text = m.text(); + if (/Content Security Policy|violates the following/i.test(text)) verstoesse.add(text.replace(/\s+/g, " ").slice(0, 160)); + }); + await seite.goto(BASIS + "/index.html", { waitUntil: "domcontentloaded" }); + await seite.waitForSelector("#music-toggle", { timeout: 10000 }); + + const knopf = await seite.evaluate(() => { + const r = document.getElementById("music-toggle").getBoundingClientRect(); + return { breite: Math.round(r.width), hoehe: Math.round(r.height), rechts: Math.round(innerWidth - r.right), unten: Math.round(innerHeight - r.bottom) }; + }); + pruefe("Knopf ist groß genug zum Antippen", knopf.breite >= 44 && knopf.hoehe >= 44, knopf.breite + "x" + knopf.hoehe + "px"); + pruefe("Knopf sitzt unten rechts", knopf.rechts >= 0 && knopf.rechts < 80 && knopf.unten >= 0 && knopf.unten < 80, "Abstand rechts " + knopf.rechts + "px, unten " + knopf.unten + "px"); + + pruefe("Feld ist zu Beginn zu", await seite.evaluate(() => document.getElementById("music-panel").hidden), ""); + + await seite.click("#music-toggle"); + await seite.waitForTimeout(500); + const nachKlick = await seite.evaluate(() => { + const p = document.getElementById("music-panel"); + return { + offen: !p.hidden, + quellen: [...p.querySelectorAll("iframe")].map((f) => f.getAttribute("src") || ""), + nochUngeladen: p.querySelectorAll("iframe[data-src]").length, + }; + }); + pruefe("Feld klappt auf", nachKlick.offen, ""); + pruefe("Beide Player werden nachgeladen", nachKlick.quellen.length === 2 && nachKlick.quellen.every((q) => q.startsWith("https://open.spotify.com/embed/artist/")), nachKlick.quellen.length + " Player"); + pruefe("Kein Player bleibt ungeladen", nachKlick.nochUngeladen === 0, nachKlick.nochUngeladen + " ohne src"); + + /* Der eigentliche Punkt: Kommt der Rahmen wirklich an? Wird er von der + Richtlinie geblockt, steht in seiner Adresse chrome-error://. */ + await seite.waitForTimeout(6000); + const rahmen = seite.frames().map((f) => f.url()); + const spotifyRahmen = rahmen.filter((u) => u.startsWith("https://open.spotify.com/")); + const kaputteRahmen = rahmen.filter((u) => u.startsWith("chrome-error://")); + pruefe("Beide Spotify-Rahmen sind wirklich geladen", spotifyRahmen.length === 2, spotifyRahmen.length + " geladen: " + spotifyRahmen.map((u) => u.slice(38, 60)).join(", ")); + pruefe("Kein Rahmen ist eine Fehlerseite", kaputteRahmen.length === 0, kaputteRahmen.length + " Fehlerseiten"); + pruefe("Kein Verstoß gegen die Inhaltsrichtlinie", verstoesse.size === 0, [...verstoesse].join(" | ") || "keine"); + + await seite.screenshot({ path: "bild-musik-computer.png" }); + + /* ---------- Schließen ---------- */ + console.log("\n== Schließen =="); + await seite.click("#music-close"); + await seite.waitForTimeout(300); + pruefe("✕ schließt das Feld", await seite.evaluate(() => document.getElementById("music-panel").hidden), ""); + await seite.click("#music-toggle"); + await seite.waitForTimeout(300); + await seite.keyboard.press("Escape"); + await seite.waitForTimeout(300); + pruefe("Escape schließt das Feld", await seite.evaluate(() => document.getElementById("music-panel").hidden), ""); + + /* ---------- Handy ---------- */ + console.log("\n== Handy (390x844) =="); + const handy = await browser.newPage({ viewport: { width: 390, height: 844 } }); + await handy.goto(BASIS + "/index.html", { waitUntil: "domcontentloaded" }); + await handy.waitForSelector("#music-toggle", { timeout: 10000 }); + await handy.click("#music-toggle"); + await handy.waitForTimeout(4000); + const h = await handy.evaluate(() => { + const r = document.getElementById("music-panel").getBoundingClientRect(); + return { links: Math.round(r.left), rechts: Math.round(innerWidth - r.right), oben: Math.round(r.top), unten: Math.round(r.bottom), + fensterHoehe: innerHeight, ueberlauf: document.documentElement.scrollWidth - document.documentElement.clientWidth }; + }); + pruefe("Feld passt seitlich in den Bildschirm", h.links >= 0 && h.rechts >= 0, "links " + h.links + "px, rechts " + h.rechts + "px"); + pruefe("Feld ragt nicht oben heraus", h.oben >= 0, "oben " + h.oben + "px"); + pruefe("Seite läuft nicht seitlich über", h.ueberlauf <= 1, h.ueberlauf + "px"); + const handyRahmen = handy.frames().filter((f) => f.url().startsWith("https://open.spotify.com/")).length; + pruefe("Am Handy laden die Player auch", handyRahmen === 2, handyRahmen + " geladen"); + await handy.screenshot({ path: "bild-musik-handy.png" }); + + await browser.close(); + server.kill(); + console.log("\n===== " + ok + " ok, " + fehl + " Fehler ====="); + if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); } + process.exit(fehl ? 1 : 0); +})().catch((e) => { + console.error("Testlauf abgebrochen:", e.message); + server.kill(); + process.exit(1); +}); diff --git a/server/inhaltsrichtlinie.js b/server/inhaltsrichtlinie.js index 69f60230..9979ca51 100644 --- a/server/inhaltsrichtlinie.js +++ b/server/inhaltsrichtlinie.js @@ -186,7 +186,31 @@ export function inhaltsrichtlinie(wurzel) { "form-action 'self'", /* Ersetzt X-Frame-Options; wirkt gegen Klickfallen. */ "frame-ancestors 'self'", - "frame-src 'none'", + /* ⚠️ HIER STAND "frame-src 'none'" — DAS HAT DEN MUSIK-KNOPF + STILLGELEGT (gemeldet 27.08.2026). + + Der Knopf unten rechts klappt ein Feld mit zwei Spotify-Playern + auf (Hasidog und Van-Van, siehe main.js). Genau die sind +