Files
dogfather-universe/server/pruef-push-hinweis.mjs
T
DogFatherGitandClaude Opus 5 cc716a9c1c 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 <[email protected]>
2026-10-03 13:36:51 +02:00

297 lines
13 KiB
JavaScript

/* =====================================================================
"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);