Files
dogfather-universe/server/pruef-push-hinweis.mjs
T
DogFatherGitandClaude Opus 5 e00a905ce8 Dopplung weg: "Bei dir klingelt nichts" stand zweimal auf einem Schirm
Der neue Kasten von heute Mittag hat die alte Tagesblick-Zeile nicht
ersetzt, sondern sich darueber gesetzt. Beide sagten dasselbe,
untereinander, auf derselben Seite.

GEFUNDEN HAT DAS NUR EIN BILDSCHIRMFOTO. Beide Teile waren fuer sich
gruen: pruef-glocke verlangte die Zeile, pruef-push-hinweis den
Kasten, und keine der beiden konnte sehen, dass die andere existiert.
Zahlen zeigen so etwas nie. Deshalb macht pruef-push-hinweis jetzt
bei jedem Lauf ein Foto -- und scrollt vorher hin, weil der erste
Versuch brav die Begruessungskachel zeigte und den Kasten gar nicht
im Bild hatte. Ein Beweisbild ohne das, was es beweisen soll, ist
schlimmer als keins: Man glaubt ja, hingesehen zu haben.

Entfernt wurde die aeltere der beiden (19.09.2026). Sie war gut
gebaut, server-seitig und nicht wegklickbar -- aber sie hat das
Problem nicht geloest: Am 03.10. nachgemessen hatten immer noch
12 von 20 kein Geraet, darunter die linke Hand und ein Modi. Sie SAGT
es und man kann nichts tun; sie fuehrt nur auf eine andere Seite. Der
neue Kasten hat einen Knopf und erklaert den iPhone-Fall, in dem
Web-Push ohne Home-Bildschirm gar nicht geht.

Entscheidung von Filipe am 03.10. Offen gesagt, was damit aufgegeben
ist: Wer "Nicht jetzt" tippt, bekommt keine zweite Erinnerung mehr.
Gegensicherung ist die Personenliste -- dort steht seit heute bei
jeder Person, die nichts bekommt, genau das.

pruef-glocke 36 -> 33, und die Zahl ist erklaert: 5 Pruefungen zur
alten Zeile entfernt, 2 neue dafuer. Die neuen pruefen das
GEGENTEIL von vorher -- dass die Zeile nicht unbemerkt
zurueckkommt -- mit der Zahl der geprueften Hinweise in der
Bedingung, damit eine leere Antwort der Schnittstelle nicht als
"steht nicht drin" durchgeht.

Die alte Gegenprobe ("mit angemeldetem Geraet ist der Hinweis weg")
ist ersatzlos entfallen, nicht aus Bequemlichkeit: Sie waere ab jetzt
IMMER gruen, egal was das Haus tut. Eine Pruefung, die nichts mehr
widerlegen kann, taeuscht nur Deckung vor. Was sie geprueft hat,
prueft pruef-push-hinweis am neuen Kasten, mit einer echten
Anmeldung.

OHNE_ZAHL in start.js bleibt als leere Liste stehen -- der naechste
Zustandshinweis braucht sie wieder, und als Liste ist sie die eine
Stelle dafuer.

Gegengemessen: pruef-tagesblick 23, pruef-start-ansicht 160,
pruef-push-hinweis 16 -- alle 0 Fehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-03 13:47:50 +02:00

312 lines
14 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)`);
/* EIN BILD DAZU. Zahlen sagen, dass er passt -- nicht, ob er
aussieht wie etwas, das man anfassen moechte.
ERST HINSCROLLEN. Der erste Versuch zeigte brav die
Begruessungskachel: Der Hinweis steht darunter und war im Bild
gar nicht drin. Ein Beweisbild ohne das, was es beweisen soll,
ist schlimmer als keins -- man glaubt ja, hingesehen zu haben. */
await seite.evaluate(() => document.querySelector("#push-hinweis")
.scrollIntoView({ block: "center" }));
await seite.waitForTimeout(400);
await seite.screenshot({
path: new URL("./pruef-push-hinweis.png", import.meta.url).pathname
.replace(/^\/([A-Za-z]:)/, "$1"),
});
}
/* =====================================================================
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);