diff --git a/server/pruef-push-hinweis.mjs b/server/pruef-push-hinweis.mjs new file mode 100644 index 00000000..e08a3bc3 --- /dev/null +++ b/server/pruef-push-hinweis.mjs @@ -0,0 +1,296 @@ +/* ===================================================================== + "DU BEKOMMST GERADE NICHTS" -- der Hinweis auf der Startseite. + + WARUM ES IHN GIBT (03.10.2026). + + Am 03.10. am laufenden System nachgemessen: 20 aktive Personen, 8 + mit einer Push-Anmeldung. Zwoelf bekamen keine einzige + Benachrichtigung -- darunter die linke Hand und ein Modi. Keiner + von ihnen wusste es: Die Glocke sagt es nur dem, der sie anschaut. + + WAS HIER WIRKLICH AUF DEM SPIEL STEHT, ist nicht "erscheint der + Hinweis". Das ist der leichte Teil. Die Gefahr liegt auf der + anderen Seite: Ein Hinweis, der auch denen erscheint, die laengst + alles angeschaltet haben, oder der nach dem Wegklicken + wiederkommt, ist eine Warnung, die immer kommt -- und die liest + nach dem zweiten Mal niemand mehr. Dann haette ich die Glocke + entwertet, die heute funktioniert. + + Deshalb misst diese Pruefung VIER Abwesenheiten und nur eine + Anwesenheit: + + * weg, sobald jemand "Nicht jetzt" tippt + * weg geblieben nach dem Neuladen (sonst ist es kein "einmalig") + * gar nicht erst da auf einer anderen Seite (er haengt an 39 + Seiten mit, wenn man es falsch baut) + * gar nicht erst da, wenn die Benachrichtigungen AN sind + + Die letzte ist die teuerste -- sie braucht ein echtes Fenster und + eine echte Anmeldung -- und sie ist die wichtigste. + + MIT FENSTER, UND ZWAR NOTWENDIG: Ein kopfloser Chromium verweigert + Benachrichtigungen, egal wie man die Erlaubnis erteilt (am + 02.10.2026 in fuenf Aufbauten gemessen, siehe pruef-push-zu). Ohne + erteilte Erlaubnis kommt `lage()` nie auf "an", und die + entscheidende Gegenprobe waere nicht messbar. + + DREI AUSGAENGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht nachsehen. + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-hinweis-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +import { notbremse } from "./helfer-notbremse.mjs"; +const { eigenerPort } = await import("./helfer-port.mjs"); +const PORT = await eigenerPort(import.meta, "pruef-push-hinweis"); +process.env.PORT = `${PORT}`; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +await import("./index.js"); +notbremse(300_000, "pruef-push-hinweis"); +await new Promise((r) => setTimeout(r, 900)); +const BASIS = `http://127.0.0.1:${PORT}`; + +let fehler = 0, geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }), +}).catch(() => {}); +for (let i = 0; i < 60; i++) { + try { + const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +{ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync("CODE-DOGI-0001", salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run("Filipe", "admin", hash, salt, 32768, new Date().toISOString()); + d.close(); +} + +const { chromium } = await import("playwright"); +const browser = await chromium.launch({ headless: false }); + +const abbruch = async (grund) => { + console.log(`\n KONNTE NICHT NACHSEHEN: ${grund}`); + try { await browser.close(); } catch { /* egal */ } + try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + process.exit(2); +}; + +/** Meldet sich an und landet auf der Startseite. */ +async function anmelden(kontext) { + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 20000 }); + await seite.waitForTimeout(700); + return seite; +} + +/** Was steht gerade im Hinweis -- und wie gross ist es? */ +const hinweisLesen = (seite) => seite.evaluate(() => { + const k = document.querySelector("#push-hinweis .push-hinweis"); + if (!k) return { da: false }; + const t = k.querySelector(".push-hinweis__text"); + const knoepfe = [...k.querySelectorAll(".push-hinweis__knopf")].map((b) => ({ + text: b.textContent.trim(), + hoch: Math.round(b.getBoundingClientRect().height), + breit: Math.round(b.getBoundingClientRect().width), + })); + const stil = getComputedStyle(t); + return { + da: true, + text: t.textContent.replace(/\s+/g, " ").trim(), + schrift: parseFloat(stil.fontSize), + farbe: stil.color, + knoepfe, + }; +}); + +/* ===================================================================== + 1. Ohne Anmeldung: der Hinweis steht da + ===================================================================== */ +console.log("\n=== Wer nichts bekommt, erfaehrt es ==="); +const kontext = await browser.newContext({ + viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true, +}); +const seite = await anmelden(kontext); + +{ + const erlaubnis = await seite.evaluate(() => Notification.permission); + if (erlaubnis === "granted") { + await abbruch("der Browser hat Benachrichtigungen schon erlaubt -- dann " + + "laesst sich der Fall \"noch nicht angeschaltet\" hier nicht messen"); + } + const h = await hinweisLesen(seite); + if (!h.da) { + await abbruch("auf der Startseite steht gar kein Hinweis -- ohne ihn " + + "ist nichts von dem unten Messbares messbar"); + } + ok(/keine Benachrichtigungen/.test(h.text), + `er sagt, worum es geht ("${h.text.slice(0, 64)}…")`); + ok(h.knoepfe.length === 2, `zwei Knoepfe: ${h.knoepfe.map((b) => b.text).join(" / ")}`); + ok(h.knoepfe.some((b) => /Anschalten/.test(b.text)), + "einer davon schaltet es an -- der Hinweis ist nicht nur eine Mitteilung"); + + /* Lesbarkeit: im Haus gilt keine Schrift unter 11,5 px, und ein + Ziel, das man mit dem Daumen trifft, ist mindestens 44 px hoch. */ + ok(h.schrift >= 11.5, `Schriftgroesse ${h.schrift} px (mindestens 11,5)`); + const klein = h.knoepfe.filter((b) => b.hoch < 44); + ok(klein.length === 0, + klein.length ? `zu flache Knoepfe: ${klein.map((b) => b.text + " " + b.hoch + "px")}` + : `beide Knoepfe mindestens 44 px hoch (${h.knoepfe.map((b) => b.hoch).join(", ")})`); + /* Auf 390 px darf nichts ueber den Rand stehen. */ + const ueber = await seite.evaluate(() => { + const k = document.querySelector("#push-hinweis .push-hinweis"); + return Math.round(k.getBoundingClientRect().right - document.documentElement.clientWidth); + }); + ok(ueber <= 0, `nichts steht ueber (${ueber} px)`); +} + +/* ===================================================================== + 2. Auf anderen Seiten NICHT -- er haengt an 39 Seiten mit + ===================================================================== */ +console.log("\n=== Nur auf der Startseite ==="); +{ + await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(600); + const h = await hinweisLesen(seite); + ok(!h.da, "auf der Aufgabenseite steht er nicht"); + /* Gegenprobe zur Gegenprobe: Die Glocke ist dort sehr wohl da -- + sonst wuerde oben nur gemessen, dass glocke.js gar nicht laeuft. */ + const glocke = await seite.evaluate(() => !!document.getElementById("glocke")); + ok(glocke, "die Glocke selbst schon -- glocke.js laeuft dort also"); + await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(600); + ok((await hinweisLesen(seite)).da, "und auf der Startseite wieder"); +} + +/* ===================================================================== + 3. "Nicht jetzt" heisst nicht jetzt und auch nicht morgen + ===================================================================== */ +console.log("\n=== Einmal weggetippt, bleibt weg ==="); +{ + await seite.click('.push-hinweis__knopf--spaeter'); + await seite.waitForTimeout(300); + ok(!(await hinweisLesen(seite)).da, "nach dem Tippen ist er weg"); + + await seite.reload({ waitUntil: "networkidle" }); + await seite.waitForTimeout(800); + ok(!(await hinweisLesen(seite)).da, + "und nach dem Neuladen immer noch -- sonst waere es keine einmalige Sache"); +} + +/* ===================================================================== + 4. DIE WICHTIGSTE: Wer sie AN hat, wird nicht behelligt + ===================================================================== */ +console.log("\n=== Wer sie an hat, sieht nichts davon ==="); +{ + /* ==== EIN ECHTES PROFIL, KEIN INKOGNITO-FENSTER ================= + + Erst gemessen, dann verstanden (03.10.2026): `browser.newContext()` + gibt ein Inkognito-Fenster, und dort meldete die Glocke hartnaeckig + "aus". Der Browser hat es im Klartext gesagt, sobald die Pruefung + ihn gefragt hat: + + "Chrome currently does not support the Push API in incognito + mode. There is deliberately no way to feature-detect this, + since incognito mode needs to be undetectable by websites." + + Also: kein Befund am Haus, sondern eine Eigenschaft des Fensters + -- und einer, den man NICHT abfragen kann. Ohne das Mitschreiben + der Browsermeldungen haette hier eine Stunde Suche im eigenen + Code gestanden. + + `launchPersistentContext` mit einem eigenen Ordner ist ein + richtiges Profil. Der Ordner wird am Ende weggeraeumt. */ + const profil = mkdtempSync(join(tmpdir(), "ws-hinweis-profil-")); + const k2 = await chromium.launchPersistentContext(profil, { + headless: false, + viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true, + permissions: ["notifications"], + }); + await k2.grantPermissions(["notifications"], { origin: BASIS }); + const s2 = await anmelden(k2); + + const erlaubnis = await s2.evaluate(() => Notification.permission); + if (erlaubnis !== "granted") { + await abbruch("der Browser erlaubt keine Benachrichtigungen " + + `(Notification.permission = "${erlaubnis}") -- ein kopfloser Chromium ` + + "sagt immer „denied“. Diese Pruefung braucht ein Fenster."); + } + + /* Vor dem Anschalten muss er da sein -- sonst beweist sein Fehlen + danach gar nichts (er koennte aus einem ganz anderen Grund + fehlen, etwa weil der neue Kontext nie fertig geladen hat). */ + ok((await hinweisLesen(s2)).da, + "im frischen Browser steht er zuerst da (sonst beweist sein Fehlen nichts)"); + + /* Mitschreiben, was der Browser sagt -- scheitert das Anmelden, + steht der Grund hier und nicht in einer Vermutung. */ + const klagen = []; + s2.on("console", (m) => { if (m.type() === "error") klagen.push(m.text()); }); + s2.on("pageerror", (f) => klagen.push(String(f?.message || f))); + + await s2.click(".push-hinweis__knopf--ja"); + + /* AUF DEN ZUSTAND WARTEN, NICHT AUF DIE UHR. Ein fester Schlaf ist + entweder zu kurz (dann ist es ein Fehlalarm) oder zu lang (dann + wartet jeder Lauf umsonst). `pushManager.subscribe` spricht mit + einem echten Push-Dienst im Netz -- wie lange das dauert, weiss + nur er. */ + let lage = null; + for (let i = 0; i < 60; i++) { + lage = await s2.evaluate(() => document.getElementById("glocke")?.dataset.lage); + if (lage === "an") break; + await s2.waitForTimeout(500); + } + if (lage !== "an") { + await abbruch(`das Anschalten hat nicht geklappt (Glocke steht auf "${lage}") ` + + "-- ohne eine echte Anmeldung ist die Gegenprobe nicht messbar." + + (klagen.length ? `\n Der Browser sagt dazu: ${klagen.slice(0, 3).join(" | ")}` + : "\n Der Browser hat sich dazu nicht geaeussert. Moeglich ist, dass " + + "pushManager.subscribe() keinen echten Push-Dienst erreicht " + + "(dafuer braucht dieser Rechner eine Verbindung ins Netz).")); + } + ok(lage === "an", "nach dem Antippen steht die Glocke auf „an“"); + ok(!(await hinweisLesen(s2)).da, + "und der Hinweis ist weg -- ohne dass jemand ihn wegklicken musste"); + + await s2.reload({ waitUntil: "networkidle" }); + await s2.waitForTimeout(1200); + ok(!(await hinweisLesen(s2)).da, "auch nach dem Neuladen bleibt er weg"); + + /* Und das Angemeldetsein ist echt: Es steht in der Datenbank. */ + const d = new DatabaseSync(process.env.WORKSPACE_DB); + const n = d.prepare("SELECT COUNT(*) AS n FROM push_anmeldungen").get().n; + d.close(); + ok(n === 1, `die Anmeldung steht wirklich in der Datenbank (${n})`); + await k2.close(); + try { rmSync(profil, { recursive: true, force: true }); } catch { /* egal */ } +} + +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(`\n${geprueft} Pruefungen, ${fehler} Fehler`); +console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); +process.exit(fehler ? 1 : 0); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 36dc6236..7c498df1 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +