From cc716a9c1c1d398b6d44f40ee2f882a37fbe06c7 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sat, 3 Oct 2026 13:36:51 +0200 Subject: [PATCH] Startseite sagt einmalig Bescheid, wenn jemand nichts bekommt Am 03.10.2026 nachgemessen: 20 aktive Personen, 8 mit einer Anmeldung. Zwoelf bekamen keine einzige Benachrichtigung -- darunter die linke Hand und ein Modi. Die heute frueh behobene Dringlichkeit hilft diesen zwoelf nichts: Ohne Anmeldung geht gar nichts hinaus. Belegt im neuen Sendeprotokoll, seit dem Deploy heute Mittag: 10 Chat-Meldungen zugestellt, 26 Versuche an "keine_geraete" gescheitert. Genau diese 26 sind der Grund. Keiner der zwoelf wusste es. Die Glocke sagt es nur dem, der sie anschaut -- und genau das hatten sie nie getan. Jetzt steht auf der Startseite eine ruhige Zeile mit zwei Knoepfen: Anschalten oder nicht jetzt. ABGELEITET AUS `lage()`, der Stelle, die es ohnehin weiss. Eine zweite Ableitung daneben waere die, die beim naechsten Umbau etwas anderes behauptet als die Glocke zwei Zentimeter weiter oben. NUR DORT, WO ES ETWAS ZU AENDERN GIBT. Bei "verboten" hilft kein Knopf (das muss man im Browser zuruecknehmen), bei "geht-nicht" erst recht nicht. Auf dem iPhone erklaert er stattdessen den Weg ueber den Home-Bildschirm -- ohne den gibt es dort gar kein Web-Push, und das weiss sonst niemand. NUR AUF DER STARTSEITE, obwohl glocke.js auf 39 Seiten laeuft: Die Seite stellt den Platz, das Skript fuellt ihn -- dasselbe Muster wie `#glocke-platz`. Auf jeder Seite waere er nach dem zweiten Mal Tapete. Ruhig, nicht alarmierend: Es ist kein Fehler, sondern eine Einstellung, die noch niemand getroffen hat. Rot waere hier falsch. pruef-push-hinweis.mjs (neu, 16 Pruefungen). Sie misst VIER Abwesenheiten und nur eine Anwesenheit, weil die Gefahr auf der anderen Seite liegt: weg nach "Nicht jetzt", weg geblieben nach dem Neuladen, nicht da auf anderen Seiten (mit der Gegenprobe, dass die Glocke dort sehr wohl steht -- sonst waere nur gemessen, dass das Skript gar nicht laeuft), und nicht da, wenn die Benachrichtigungen AN sind. Die letzte mit echter Anmeldung, die nachweislich in der Datenbank landet. Dabei gelernt, und es stand nicht im Code: `browser.newContext()` gibt ein Inkognito-Fenster, und Chrome unterstuetzt dort die Push-API nicht -- "deliberately no way to feature-detect this". Die Pruefung meldete zuerst den dritten Ausgang statt gruen, und weil sie die Browsermeldungen mitschreibt, stand die Ursache sofort da. Jetzt laeuft sie mit einem echten Profil. Gegengemessen, dass die zusaetzliche Zeile nichts verschiebt: pruef-glocke 36, pruef-start-ansicht 160, pruef-tagesblick 23, pruef-kachelraster 24, pruef-handy 189, pruef-breiten 23, pruef-lesbarkeit 14, pruef-ueberlappung (20 Breitenpaare) -- alle 0 Fehler. Co-Authored-By: Claude Opus 5 --- server/pruef-push-hinweis.mjs | 296 ++++++++++++++++++++++++++++++++ workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/start.css | 68 ++++++++ workspace/assets/js/glocke.js | 92 ++++++++++ workspace/aufgaben.html | 38 ++-- workspace/automation.html | 32 ++-- workspace/befinden.html | 34 ++-- workspace/bereich.html | 44 ++--- workspace/bewerben.html | 38 ++-- workspace/bewerbungen.html | 36 ++-- workspace/buehne.html | 6 +- workspace/calls.html | 34 ++-- workspace/chat.html | 40 ++--- workspace/content.html | 34 ++-- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 ++-- workspace/entwicklung.html | 36 ++-- workspace/hilfe.html | 30 ++-- workspace/index.html | 10 +- workspace/kalender.html | 34 ++-- workspace/leistung.html | 32 ++-- workspace/manager-ziele.html | 28 +-- workspace/material.html | 32 ++-- workspace/notizen.html | 28 +-- workspace/personen.html | 32 ++-- workspace/profil.html | 36 ++-- workspace/reaktion.html | 38 ++-- workspace/rechte.html | 34 ++-- workspace/report.html | 36 ++-- workspace/scouting.html | 32 ++-- workspace/start.html | 51 ++++-- workspace/startcheck.html | 34 ++-- workspace/steckbrief.html | 36 ++-- workspace/support.html | 28 +-- workspace/tafel.html | 6 +- workspace/talente.html | 36 ++-- workspace/team.html | 32 ++-- workspace/teamlage.html | 34 ++-- workspace/teilen.html | 30 ++-- workspace/treff-moderation.html | 32 ++-- workspace/treff-regeln.html | 32 ++-- workspace/uebersicht.html | 32 ++-- workspace/unsere-seiten.html | 30 ++-- workspace/unterstuetzen.html | 32 ++-- workspace/werdegang.html | 32 ++-- workspace/willkommen.html | 30 ++-- workspace/wissen.html | 34 ++-- 49 files changed, 1159 insertions(+), 684 deletions(-) create mode 100644 server/pruef-push-hinweis.mjs 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`. --> - - + + - +