Weiter durch die rote Liste. Drei der vier Befunde zeigten auf
gesunden Code; die Pruefungen selbst waren seit einem Umbau nicht
nachgezogen worden.
pruef-schritt (2 rot -> 71 gruen statt 47)
Meldete "1 von 6 Kategorien". Die Karte geht seit dem 01.10.2026
gefiltert auf ("zugetragen"), Bloecke ohne solche Punkte werden gar
nicht gezeichnet. Die Pruefung zaehlte danach alle. Schlimmer: Weil
dann kein Block mehr zugeklappt war, lief der Klick auf einen
zugeklappten 30 Sekunden in die Frist und riss den Lauf mit -- 24 von
71 Pruefungen liefen nie. Jetzt wird erst die Vorgabe geprueft (sie
ist eine Zusage), dann auf "Alle" gestellt.
pruef-glocke (1 rot -> 34 gruen)
Meldete "auf der Anmeldeseite steht der Knopf". Gemessen landete sie
auf start.html: Dieselbe Seite war eine Zeile zuvor angemeldet durch
zwanzig Arbeitsseiten gelaufen, und angemeldet fuehrt /workspace/
nicht zur Wand. Jetzt eigener Kontext ohne Keks -- und davor die
Zeile, dass die Wand wirklich erreicht wurde.
pruef-zeichen (1 rot -> 8 gruen)
Meldete content: "·" als Kodierungstruemmer. Die Bytes sind C2 B7,
also korrektes UTF-8; der Mittelpunkt ist ein begruendeter Trennpunkt
(seit 03.10.). Die Regel stammt aus einer Zeit, in der sie selbst
festhielt: "KEINE einzige benutzt ein Zeichen aus diesem Block."
Jetzt eine benannte Ausnahme MIT Veraltungsschutz: Jedes Zeichen
darin muss wirklich vorkommen, sonst wird die Ausnahme rot.
pruef-tempo-workspace (1 rot -> 9 gruen) -- DER ECHTE BEFUND
uebersicht.html 959 KB, bereich.html?b=live 1094 KB bei einer Grenze
von 900. Die Pruefung sagt jetzt auch, WORAN es liegt, und die
Antwort war eindeutig: ZWEI Buehnenbilder auf einer Seite, die eines
zeigt (281 KB studio + 262 KB portal).
Ursache: start.css traegt an body.start::before einen Rueckfall
(studio), und kopf.js setzt `data-buehne` erst, wenn die Seite steht.
Bis dahin ist der Rueckfall geholt -- auf 43 von 45 Seiten.
Jetzt steht die Szene fest im <body>, erzeugt aus bereiche.js
(tools/buehne-festschreiben.mjs, 20 Seiten). bereich.html bedient
fuenf Bretter und kann das nicht; sie bekommt ein kleines Skript
gleich hinter <body>, das die Szene aus `?b=` setzt, bevor gezeichnet
wird. Gemessen: 1094 -> 879 KB, und die Grenze haelt wieder.
DIE ZUORDNUNG STEHT DAMIT AN ZWEI STELLEN. Das ist nur vertretbar,
weil sie nicht auseinanderlaufen kann: pruef-buehnen-fest.mjs (neu,
13 Pruefungen) haelt HTML gegen bereiche.js, prueft die Lage des
Skripts und dass jede genannte Szene eine Regel hat.
Dabei gefunden und berichtigt -- zweimal an mir selbst: Der erste
Entwurf verglich Szenen mit Dateinamen und meldete teamlage.html
("eingang") als Fehler; die Szene gibt es, ihre Regel zeigt nur auf
ein crew-Bild. Und der Lage-Anker suchte `data-buehne`, das Skript
schreibt `dataset.buehne` -- indexOf gab -1, die Rechnung wurde
negativ und meldete "-2276 Zeichen dahinter".
Gegenproben gefahren: Szene verschoben -> rot mit Abstand; Ausnahme
verwaist -> rot; erfundene Szene -> erkannt.
Dazu gruen: css-klassen, workspace-seiten, neue-seiten, haus-trennung,
kopfleiste-farbe.
Co-Authored-By: Claude Opus 5 <[email protected]>
698 lines
31 KiB
JavaScript
698 lines
31 KiB
JavaScript
/* DER BENACHRICHTIGUNGS-KNOPF, IM ECHTEN BROWSER.
|
|
|
|
Wunsch Filipe, 05.09.2026, mit dem Bild eines "Benachrichtigungen
|
|
aus"-Knopfes: *"ich will auch sowas und dass es perfekt funktioniert
|
|
auf der seite für jeden!!!!"*
|
|
|
|
---------------------------------------------------------------------
|
|
WAS HIER NICHT GEMESSEN WERDEN KANN — und warum das dasteht
|
|
|
|
`pushManager.subscribe()` braucht einen echten Push-Dienst. Chromium
|
|
spricht dafür mit Googles FCM, Firefox mit Mozillas autopush. Ein
|
|
Browser, der in einer Prüfung startet, hat diese Verbindung nicht --
|
|
der Aufruf scheitert, und daran ist nichts zu ändern.
|
|
|
|
Das ist der DRITTE AUSGANG aus der Projektnotiz: nicht in Ordnung ist
|
|
etwas anderes als nicht nachgesehen. Diese Prüfung tut deshalb
|
|
zweierlei:
|
|
|
|
1. Sie MISST ZUERST, ob echtes subscribe hier geht. Geht es, wird
|
|
der echte Weg geprüft. Geht es nicht, wird das ausdrücklich
|
|
gemeldet -- nicht verschwiegen und nicht als "bestanden".
|
|
|
|
2. Für alles andere tritt ein Test-Doppel an die Stelle des
|
|
Browser-Kanals: Der PushManager wird ersetzt, alles davor und
|
|
dahinter bleibt echt. Damit sind Knopf, Zustände, Server-Aufrufe
|
|
und Speicherung wirklich geprüft -- nur die Strecke zwischen
|
|
Browser und Google nicht.
|
|
|
|
Was die Rechnung selbst angeht, ist die Lage besser als es klingt:
|
|
pruef-push.mjs prüft sie gegen die Testvektoren aus RFC 8291 und
|
|
8292, pruef-push-weg.mjs schickt eine Nachricht durch einen
|
|
nachgebauten Dienst und macht sie wieder auf. Was hier fehlt, ist
|
|
allein der Schritt "Browser holt sich eine Adresse bei Google".
|
|
|
|
---------------------------------------------------------------------
|
|
GEMESSEN WIRD
|
|
|
|
* Steht der Knopf auf JEDER Seite (außer der Anmeldeseite)?
|
|
* Sagt er in jeder Lage die Wahrheit -- auch wenn sie unbequem ist?
|
|
(abgelehnt, iPhone im Reiter, alter Browser)
|
|
* Legt ein Klick wirklich eine Anmeldung an, und nimmt ein zweiter
|
|
sie wieder weg?
|
|
* Bekommt jede ROLLE ihn -- "für jeden" war der Wunsch.
|
|
|
|
GEGENPROBEN durchgehend. */
|
|
|
|
import { mkdtempSync, rmSync, appendFileSync, readdirSync, readFileSync }
|
|
from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
/* WO DIE SEITEN LIEGEN. Abgeleitet vom eigenen Ort, nicht als
|
|
fester Pfad -- damit die Pruefung auch laeuft, wenn jemand das
|
|
Verzeichnis woandershin legt. */
|
|
const SEITEN_ORDNER = join(dirname(fileURLToPath(import.meta.url)),
|
|
"..", "workspace");
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-glocke-"));
|
|
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, "die Glockenpruefung");
|
|
|
|
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");
|
|
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
|
|
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
|
|
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
|
|
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
|
|
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
|
|
notbremse(480_000, "pruef-glocke");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
const PROTOKOLL = join(process.cwd(), "pruef-glocke-lauf.txt");
|
|
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
|
const melde = (z) => {
|
|
console.log(z);
|
|
try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ }
|
|
};
|
|
let fehler = 0;
|
|
const ok = (b, t) => { melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const hinweis = (t) => melde(" -- " + t);
|
|
|
|
/* ---- Bestand ------------------------------------------------------------ */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = 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 jetzt = new Date().toISOString();
|
|
/* DER SUCHSCHLUESSEL FUER DEN STILLEN ZUGANG (24.09.2026).
|
|
|
|
Rechte Hand, linke Hand und Modi haben keine eigene Kachel; der
|
|
Server findet sie ueber `code_kennung`, einen HMAC des Codes.
|
|
Fehlt diese Spalte, kommen sie gar nicht herein -- und das sah in
|
|
dieser Pruefung aus wie ein Fehler an der Glocke, war aber einer
|
|
am Aufbau der Testdaten. */
|
|
const kennSchluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", kennSchluessel, jetzt);
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, 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,"
|
|
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", kennSchluessel).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Cigdem", "manager", "CODE-MANA-0001");
|
|
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
|
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
|
/* DIE UEBRIGEN VIER ROLLEN (24.09.2026).
|
|
Sie fehlten, und deshalb fehlten sie auch im Abschnitt "Jede Rolle
|
|
bekommt ihn" weiter unten -- der pruefte vier von acht und hiess
|
|
trotzdem "jede". */
|
|
anlegen("VanVan", "hand", "CODE-HAND-0001");
|
|
anlegen("Funny", "linke", "CODE-LINK-0001");
|
|
anlegen("Miss", "modi", "CODE-MODI-0001");
|
|
anlegen("Spicy Media", "spicy", "CODE-SPIC-0001");
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idScout, jetzt);
|
|
d.close();
|
|
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
|
|
/** Ein Test-Doppel für den Browser-Push-Kanal.
|
|
*
|
|
* Ersetzt NUR pushManager -- Berechtigung, Service Worker, Knopf,
|
|
* fetch und Server bleiben echt. Der Endpunkt sieht aus wie einer von
|
|
* Google und ist https, weil der Server das verlangt (und verlangen
|
|
* soll: eine http-Zustelladresse wäre im Klartext erreichbar). */
|
|
const DOPPEL = `
|
|
(() => {
|
|
const abo = {
|
|
endpoint: 'https://fcm.googleapis.com/fcm/send/PRUEF-' + Math.random().toString(36).slice(2),
|
|
getKey: (art) => new Uint8Array(art === 'auth' ? 16 : 65).fill(7),
|
|
toJSON: () => ({ keys: {
|
|
p256dh: 'BQ'.padEnd(87, 'A'), auth: 'AQ'.padEnd(22, 'A'),
|
|
} }),
|
|
unsubscribe: async () => true,
|
|
};
|
|
let angemeldet = null;
|
|
const manager = {
|
|
subscribe: async () => { angemeldet = abo; return abo; },
|
|
getSubscription: async () => angemeldet,
|
|
};
|
|
/* Auch die Registrierung wird abgefangen: Der Service Worker
|
|
selbst wird hier nicht gebraucht, und ein fehlgeschlagenes
|
|
register() würde den ganzen Ablauf abbrechen. */
|
|
const reg = { pushManager: manager, showNotification: async () => {} };
|
|
Object.defineProperty(navigator, 'serviceWorker', {
|
|
configurable: true,
|
|
value: {
|
|
register: async () => reg,
|
|
getRegistration: async () => reg,
|
|
ready: Promise.resolve(reg),
|
|
addEventListener: () => {},
|
|
},
|
|
});
|
|
window.__doppel = true;
|
|
})();
|
|
`;
|
|
|
|
/* DEN BERECHTIGUNGSSTAND SETZEN — und warum das nötig ist.
|
|
|
|
Ein Chromium, der in einer Prüfung startet, meldet
|
|
`Notification.permission === "denied"`. Ein echter Browser meldet
|
|
beim ersten Besuch "default" und fragt erst, wenn man klickt.
|
|
|
|
Ohne diese Zeile misst die Prüfung also überall den Zustand
|
|
"schon einmal abgelehnt" -- und der Knopf sagt dann völlig zu Recht
|
|
"Vom Browser blockiert". Beim ersten Lauf standen deshalb acht
|
|
Fehler da, von denen keiner ein Fehler war: Der Knopf tat genau das
|
|
Richtige, nur zur falschen Frage.
|
|
|
|
`grantPermissions` allein genügt nicht -- es setzt die Berechtigung
|
|
im Browser, aber der Ausgangszustand "noch nicht gefragt" lässt sich
|
|
damit nicht herstellen. Deshalb wird beides gesetzt: der Stand und,
|
|
wo nötig, die Antwort auf die Nachfrage. */
|
|
function berechtigung(stand) {
|
|
return `
|
|
(() => {
|
|
Object.defineProperty(Notification, 'permission',
|
|
{ value: '${stand}', configurable: true });
|
|
Notification.requestPermission = async () => '${stand === "default" ? "granted" : stand}';
|
|
})();
|
|
`;
|
|
}
|
|
|
|
async function anmelden(ctx, rolle, code) {
|
|
const seite = await ctx.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
/* DER STILLE ZUGANG (09.09.2026, hier nachgezogen am 24.09.2026).
|
|
|
|
Rechte Hand, linke Hand und Modi haben ABSICHTLICH keine eigene
|
|
Kachel auf der Anmeldeseite -- Filipes Wunsch: "damit die von der
|
|
workspace auch nicht mal sehen dass die modis von mir einen
|
|
eigenen zugang haben." Sie tippen auf irgendeine vorhandene
|
|
Kachel, und der Code allein entscheidet.
|
|
|
|
Solange diese Pruefung nur vier Rollen ansah, fiel es nicht auf.
|
|
Seit sie alle acht ansieht, waere `click` auf eine Kachel, die
|
|
es nicht gibt, ein Abbruch -- und der saehe aus wie ein Fehler
|
|
an der Glocke, obwohl er einer an der Pruefung waere. */
|
|
/* ==== DIE RICHTIGE ANMELDESEITE (01.10.2026) =====================
|
|
|
|
Es gibt ZWEI, und sie gehoeren zu den zwei Haeusern:
|
|
|
|
workspace/index.html spicy, admin, manager, scout, creator
|
|
workspace/crew-index.html admin, hand, linke, modi, gast
|
|
|
|
Auf 127.0.0.1 kommt die Agenturseite. Wer sich als `hand`,
|
|
`linke` oder `modi` anmelden will, findet seine Kachel dort
|
|
nicht.
|
|
|
|
HIER STAND EIN NOTNAGEL:
|
|
|
|
... || await seite.$(".rolle");
|
|
|
|
Er sollte verhindern, dass ein Klick auf eine fehlende Kachel
|
|
abbricht. Seit die Kachel am 30.09. BINDEND ist, macht er aus
|
|
„diese Kachel gibt es hier nicht" etwas Schlimmeres: Er klickt
|
|
irgendeine, der Server weist die Anmeldung zu Recht ab (401),
|
|
und heraus kommt eine Zeitsperre nach 25 Sekunden, die wie ein
|
|
Fehler an der Glocke aussieht. Drei von acht Rollen waren so
|
|
unpruefbar, ohne dass es jemandem auffiel.
|
|
|
|
Gemessen: Dieselbe Anmeldung ueber die Schnittstelle antwortet
|
|
mit 200. Es lag nie am Server.
|
|
|
|
Die Seite wird PROBIERT, nicht aus einer Liste gelesen -- eine
|
|
Liste von Crew-Rollen waere die naechste, die beim sechsten
|
|
Eintrag nicht mitwaechst. */
|
|
let kachel = await seite.$(`.rolle[data-rolle="${rolle}"]`);
|
|
if (!kachel) {
|
|
await seite.goto(`${BASIS}/workspace/crew-index.html`,
|
|
{ waitUntil: "domcontentloaded" });
|
|
kachel = await seite.$(`.rolle[data-rolle="${rolle}"]`);
|
|
}
|
|
if (!kachel) {
|
|
/* LAUT ABBRECHEN STATT IRGENDEINE NEHMEN. Diese Meldung nennt
|
|
die Rolle; die Zeitsperre von vorher nannte nur eine Adresse,
|
|
auf die man nicht wartet. */
|
|
const da = await seite.$$eval(".rolle", (ks) => ks.map((k) => k.dataset.rolle));
|
|
throw new Error(`Kachel "${rolle}" gibt es auf keiner der beiden`
|
|
+ ` Anmeldeseiten. Hier stehen: ${da.join(", ") || "keine"}`);
|
|
}
|
|
await kachel.click();
|
|
await seite.fill("#code", code);
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
|
return seite;
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. Geht ECHTES subscribe in dieser Umgebung?
|
|
======================================================================= */
|
|
melde("\n=== Lässt sich der echte Browser-Kanal hier messen? ===");
|
|
let echtGeht = false;
|
|
{
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ permissions: ["notifications"] });
|
|
const seite = await anmelden(ctx, "admin", "CODE-DOGI-0001");
|
|
|
|
const ergebnis = await seite.evaluate(async () => {
|
|
try {
|
|
const reg = await navigator.serviceWorker.register('/workspace/sw.js', { scope: '/workspace/' });
|
|
await navigator.serviceWorker.ready;
|
|
const a = await fetch('/workspace/api/push/schluessel', { credentials: 'same-origin' });
|
|
const { schluessel } = await a.json();
|
|
const roh = atob(schluessel.replace(/-/g, '+').replace(/_/g, '/')
|
|
+ '='.repeat((4 - schluessel.length % 4) % 4));
|
|
const abo = await reg.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: Uint8Array.from(roh, (c) => c.charCodeAt(0)),
|
|
});
|
|
return { ok: true, endpunkt: abo.endpoint.slice(0, 40) };
|
|
} catch (f) { return { ok: false, grund: String(f?.message || f).slice(0, 80) }; }
|
|
});
|
|
|
|
echtGeht = ergebnis.ok;
|
|
if (echtGeht) {
|
|
ok(true, `echtes subscribe GEHT hier — geprüft wird der echte Weg (${ergebnis.endpunkt}…)`);
|
|
} else {
|
|
/* KEIN FEHLER, aber auch kein Haken: der dritte Ausgang. */
|
|
hinweis(`echtes subscribe geht hier NICHT: ${ergebnis.grund}`);
|
|
hinweis(` Grund: ein Prüf-Browser hat keine Verbindung zu Googles Push-Dienst.`);
|
|
hinweis(` Die Verschlüsselung selbst ist in pruef-push.mjs gegen die`);
|
|
hinweis(` RFC-Testvektoren geprüft, die Zustellung in pruef-push-weg.mjs.`);
|
|
hinweis(` Hier wird der PushManager ersetzt; alles davor und dahinter ist echt.`);
|
|
}
|
|
await browser.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. Steht der Knopf da — auf jeder Seite, bei jeder Rolle?
|
|
======================================================================= */
|
|
melde("\n=== Der Knopf ist überall ===");
|
|
{
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext();
|
|
await ctx.addInitScript(berechtigung("default"));
|
|
const seite = await anmelden(ctx, "admin", "CODE-DOGI-0001");
|
|
|
|
/* DIE LISTE WIRD ABGELEITET, NICHT ABGESCHRIEBEN (24.09.2026).
|
|
|
|
Hier standen acht Namen -- und der Abschnitt darueber heisst
|
|
"Der Knopf ist ueberall". Er war gruen, weil alle acht ihn
|
|
hatten. Gemessen am selben Tag: VIERZEHN WEITERE Seiten haben
|
|
eine Kopfleiste und hatten keinen Knopf, darunter befinden.html,
|
|
werdegang.html und teilen.html -- genau die Seiten, auf denen
|
|
eine Creator ihre Zeit verbringt, und talente.html und
|
|
bewerbungen.html, wo ein Scout sitzt. Gemeldet hat es Filipe
|
|
("dieser button soll bei jeder rolle perfekt funktionieren"),
|
|
nicht diese Pruefung.
|
|
|
|
DIE LISTE KOMMT JETZT AUS DEM VERZEICHNIS: Jede Seite, die eine
|
|
Kopfleiste hat, muss den Knopf haben. Eine Seite, die morgen
|
|
dazukommt, ist damit von selbst dabei -- eine Liste, die niemand
|
|
pflegt, kann nicht veralten. */
|
|
const alleSeiten = readdirSync(SEITEN_ORDNER)
|
|
.filter((f) => f.endsWith(".html"))
|
|
.filter((f) => readFileSync(join(SEITEN_ORDNER, f), "utf8")
|
|
.includes("kopfleiste__rechts"))
|
|
.map((f) => f.replace(/\.html$/, ""));
|
|
/* EINE BEGRUENDETE AUSNAHME, und nur eine: anruf-probe.html ist
|
|
eine eigenstaendige Diagnoseseite ohne kopf.js, und
|
|
anruf-probe.js verweist ausdruecklich auf "im Chat oben auf die
|
|
Glocke tippen". Sie steht hier NAMENTLICH -- eine stille
|
|
Ausnahme waere ein Loch mit Zustimmung. */
|
|
const AUSNAHMEN = new Set(["anruf-probe"]);
|
|
const seitenNamen = alleSeiten.filter((x) => !AUSNAHMEN.has(x));
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG: Faende das Verzeichnis morgen
|
|
keine Seite mehr, waere "auf allen 0 geprueften" sonst gruen. */
|
|
ok(seitenNamen.length >= 20,
|
|
`${seitenNamen.length} Seiten mit Kopfleiste gefunden `
|
|
+ `(ausgenommen: ${[...AUSNAHMEN].join(", ")})`);
|
|
let gefunden = 0;
|
|
const ohne = [];
|
|
for (const n of seitenNamen) {
|
|
await seite.goto(`${BASIS}/workspace/${n}.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(250);
|
|
if (await seite.$("#glocke")) gefunden++; else ohne.push(n);
|
|
}
|
|
ok(gefunden === seitenNamen.length && gefunden > 0,
|
|
ohne.length ? `fehlt auf: ${ohne.join(", ")}`
|
|
: `auf allen ${gefunden} Seiten mit Kopfleiste`);
|
|
|
|
/* Und NICHT auf der Anmeldeseite -- dort ist niemand angemeldet,
|
|
den man benachrichtigen könnte.
|
|
|
|
==== IN EINER FRISCHEN SITZUNG (berichtigt 07.10.2026) ==========
|
|
|
|
Hier stand `seite.goto(BASIS + "/workspace/")` auf DERSELBEN
|
|
Seite, die eine Zeile darüber noch angemeldet durch zwanzig
|
|
Arbeitsseiten gelaufen ist. Angemeldet führt `/workspace/` aber
|
|
nicht zur Wand, sondern weiter auf `start.html` -- gemessen:
|
|
„gelandet auf .../workspace/start.html, Titel: Creator Workspace".
|
|
Dort IST eine Glocke, und zwar zu Recht.
|
|
|
|
Die Prüfung meldete also seit dem Umbau der Anmeldung einen
|
|
Fehler an einer Seite, die sie nie zu Gesicht bekam. Ein
|
|
Phantombefund ist schlimmer als keiner: Er zeigt auf etwas
|
|
Gesundes, und man sucht dort.
|
|
|
|
EIGENER KONTEXT OHNE KEKS -- Kekse hängen am Kontext, nicht an
|
|
der Seite; ein `goto` räumt sie nicht weg. Und danach wird
|
|
NACHGESEHEN, ob die Wand wirklich erreicht wurde: Eine Prüfung,
|
|
die ihre Voraussetzung nicht herstellen kann, muss das sagen
|
|
können, statt eine Antwort über die falsche Seite zu geben. */
|
|
const wandKontext = await browser.newContext();
|
|
const wandSeite = await wandKontext.newPage();
|
|
await wandSeite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await wandSeite.waitForTimeout(250);
|
|
const aufDerWand = /\/workspace\/(index\.html)?(\?|$)/.test(wandSeite.url())
|
|
&& !!(await wandSeite.$(".rolle"));
|
|
ok(aufDerWand,
|
|
`die Anmeldewand wurde wirklich erreicht (${wandSeite.url()})`);
|
|
ok(aufDerWand && !(await wandSeite.$("#glocke")),
|
|
"auf der Anmeldeseite steht er nicht");
|
|
await wandKontext.close();
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
melde("\n=== Jede Rolle bekommt ihn ===");
|
|
{
|
|
const browser = await pw.chromium.launch();
|
|
for (const [name, rolle, code] of [
|
|
/* ALLE ACHT (24.09.2026). Hier standen vier -- unter der
|
|
Ueberschrift "Jede Rolle bekommt ihn". Die rechte Hand, die
|
|
linke Hand, die Modis und Spicy Media fehlten; ausgerechnet
|
|
die Modis sind die groesste Gruppe im Haus. */
|
|
["DogFather", "admin", "CODE-DOGI-0001"], ["Manager", "manager", "CODE-MANA-0001"],
|
|
["Scout", "scout", "CODE-SCOU-0001"], ["Creator", "creator", "CODE-CREA-0001"],
|
|
["Rechte Hand", "hand", "CODE-HAND-0001"], ["Linke Hand", "linke", "CODE-LINK-0001"],
|
|
["Modi", "modi", "CODE-MODI-0001"], ["Spicy Media", "spicy", "CODE-SPIC-0001"],
|
|
]) {
|
|
const ctx = await browser.newContext();
|
|
await ctx.addInitScript(berechtigung("default"));
|
|
const seite = await anmelden(ctx, rolle, code);
|
|
await seite.waitForTimeout(400);
|
|
const da = await seite.$("#glocke");
|
|
const text = da ? await seite.$eval("#glocke .glocke__text", (e) => e.textContent) : "";
|
|
ok(!!da, `${name}: ${da ? `Knopf da ("${text}")` : "FEHLT"}`);
|
|
await ctx.close();
|
|
}
|
|
await browser.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. Sagt er in jeder Lage die Wahrheit?
|
|
======================================================================= */
|
|
melde("\n=== Die Lagen ===");
|
|
{
|
|
const browser = await pw.chromium.launch();
|
|
|
|
/* AUS -- noch nie gefragt worden. Das ist der Zustand, in dem jeder
|
|
zum ersten Mal auf die Seite kommt. */
|
|
{
|
|
const ctx = await browser.newContext();
|
|
await ctx.addInitScript(berechtigung("default"));
|
|
const seite = await anmelden(ctx, "admin", "CODE-DOGI-0001");
|
|
await seite.waitForTimeout(400);
|
|
const lage = await seite.$eval("#glocke", (e) => e.dataset.lage);
|
|
const text = await seite.$eval("#glocke .glocke__text", (e) => e.textContent);
|
|
ok(lage === "aus" && /aus/i.test(text), `ohne Berechtigung: "${text}" (${lage})`);
|
|
|
|
/* Der Strich muss sichtbar sein -- er sagt es ohne Lesen. */
|
|
const strich = await seite.$eval(".glocke__strich", (e) => getComputedStyle(e).opacity);
|
|
ok(Number(strich) > 0.5, `die Glocke ist durchgestrichen (${strich})`);
|
|
await ctx.close();
|
|
}
|
|
|
|
/* VERBOTEN -- schon einmal abgelehnt. Der wichtigste Fall: Per
|
|
JavaScript lässt sich das NICHT zurücknehmen, also muss der Knopf
|
|
sagen, wo man es tut. Ein Knopf, der hier nur nichts tut, ist der
|
|
sichere Weg zu "das funktioniert nicht". */
|
|
{
|
|
const ctx = await browser.newContext();
|
|
await ctx.grantPermissions([]); // alles verweigert
|
|
const seite = await anmelden(ctx, "admin", "CODE-DOGI-0001");
|
|
await seite.evaluate(() => {
|
|
Object.defineProperty(Notification, "permission", { value: "denied", configurable: true });
|
|
});
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.evaluate(() => {
|
|
Object.defineProperty(Notification, "permission", { value: "denied", configurable: true });
|
|
});
|
|
await seite.waitForTimeout(400);
|
|
const lage = await seite.evaluate(() => {
|
|
document.getElementById("glocke")?.click();
|
|
return document.getElementById("glocke")?.dataset.lage;
|
|
});
|
|
const titel = await seite.$eval("#glocke", (e) => e.title);
|
|
ok(lage === "verboten", `abgelehnt: Lage "${lage}"`);
|
|
ok(/Browsereinstellungen/i.test(titel),
|
|
`und er sagt, WO man es ändert: "${titel.slice(0, 80)}…"`);
|
|
await ctx.close();
|
|
}
|
|
|
|
/* GEHT NICHT -- ein Browser ohne Push. */
|
|
{
|
|
const ctx = await browser.newContext();
|
|
const seite = await anmelden(ctx, "admin", "CODE-DOGI-0001");
|
|
await seite.addInitScript(() => {
|
|
delete window.PushManager;
|
|
});
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(400);
|
|
const lage = await seite.$eval("#glocke", (e) => e.dataset.lage);
|
|
const aus = await seite.$eval("#glocke", (e) => e.disabled);
|
|
ok(lage === "geht-nicht" && aus,
|
|
`Browser ohne Push: Lage "${lage}", Knopf abgeschaltet (${aus})`);
|
|
await ctx.close();
|
|
}
|
|
|
|
/* IPHONE im Reiter -- dort gibt es die Berechtigung überhaupt erst
|
|
nach "Zum Home-Bildschirm". Das muss dastehen, sonst tippt jemand
|
|
zehnmal und hält es für kaputt. */
|
|
{
|
|
const ctx = await browser.newContext({
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
|
|
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
|
});
|
|
const seite = await anmelden(ctx, "admin", "CODE-DOGI-0001");
|
|
await seite.waitForTimeout(400);
|
|
const lage = await seite.$eval("#glocke", (e) => e.dataset.lage);
|
|
const titel = await seite.$eval("#glocke", (e) => e.title);
|
|
ok(lage === "iphone", `iPhone im Reiter: Lage "${lage}"`);
|
|
ok(/Home-Bildschirm/i.test(titel), `mit Anleitung: "${titel.slice(0, 70)}…"`);
|
|
await ctx.close();
|
|
}
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. An- und abmelden — mit dem Test-Doppel
|
|
======================================================================= */
|
|
melde("\n=== Anmelden und wieder abmelden ===");
|
|
{
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ permissions: ["notifications"] });
|
|
await ctx.addInitScript(berechtigung("granted"));
|
|
await ctx.addInitScript(DOPPEL);
|
|
const seite = await ctx.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
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: 25000 });
|
|
await seite.waitForTimeout(500);
|
|
|
|
/* Steht das Doppel überhaupt? Ohne diese Prüfung liefe alles
|
|
Folgende womöglich gegen den echten (hier kaputten) Kanal und
|
|
meldete Fehler, die nichts bedeuten. */
|
|
ok(await seite.evaluate(() => window.__doppel === true),
|
|
"das Test-Doppel steht (der Rest der Kette ist echt)");
|
|
|
|
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const zahl = () => p.prepare("SELECT COUNT(*) AS n FROM push_anmeldungen").get().n;
|
|
ok(zahl() === 0, `vorher ${zahl()} Anmeldungen in der Datenbank`);
|
|
|
|
/* ==== "BEI DIR KLINGELT NICHTS" IST HIER WEG (03.10.2026) ========
|
|
|
|
Bis heute stand dieser Hinweis in "Was ist dran?", eingefuehrt am
|
|
19.09., als sieben von elf Zugaengen kein Geraet hatten. Er hat
|
|
das Problem nicht geloest: Am 03.10. nachgemessen waren es immer
|
|
noch 12 von 20, darunter die linke Hand und ein Modi. Er SAGT es,
|
|
aber man kann nichts tun -- er fuehrt nur auf eine andere Seite.
|
|
|
|
Ersetzt durch einen Kasten mit "Anschalten"-Knopf, direkt ueber
|
|
dieser Liste (gemessen in pruef-push-hinweis.mjs, 16 Pruefungen).
|
|
|
|
WAS HIER JETZT GEPRUEFT WIRD, ist das Gegenteil von vorher, und
|
|
zwar aus einem Grund: Aufgefallen ist die Dopplung erst auf einem
|
|
Bildschirmfoto -- beide Saetze standen untereinander auf
|
|
demselben Schirm. Zahlen haetten das nie gezeigt, beide Teile
|
|
waren fuer sich genommen gruen. Diese Pruefung sorgt dafuer, dass
|
|
die Zeile nicht unbemerkt zurueckkommt. */
|
|
const hinweise = async () => (await seite.evaluate(async () => {
|
|
const a = await fetch("/workspace/api/hinweise", { credentials: "same-origin" });
|
|
return a.json();
|
|
})).hinweise || [];
|
|
|
|
{
|
|
const offen = await hinweise();
|
|
/* Die Zahl gehoert in die Bedingung: Kaeme die Schnittstelle gar
|
|
nicht zustande, waere die Liste leer -- und "kein_geraet steht
|
|
nicht drin" waere gruen, ohne etwas gesehen zu haben. */
|
|
ok(Array.isArray(offen) && !offen.some((x) => x.art === "kein_geraet"),
|
|
`"Bei dir klingelt nichts" steht nicht mehr in der Liste `
|
|
+ `(${offen.length} Hinweise geprueft) -- es stuende sonst zweimal auf einem Schirm`);
|
|
const aufDerSeite = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".dran__punkt")]
|
|
.filter((li) => /klingelt nichts/.test(li.textContent)).length);
|
|
ok(aufDerSeite === 0, `und auch nicht auf der Seite (${aufDerSeite})`);
|
|
}
|
|
|
|
await seite.click("#glocke");
|
|
await seite.waitForTimeout(1200);
|
|
|
|
ok(zahl() === 1, `nach dem Klick: ${zahl()} Anmeldung`);
|
|
|
|
/* Hier stand bis zum 03.10.2026 die Gegenprobe "mit angemeldetem
|
|
Geraet ist der Hinweis weg". Sie ist ersatzlos entfallen -- nicht
|
|
aus Bequemlichkeit, sondern weil sie seit dem Entfernen des
|
|
Hinweises IMMER gruen waere, egal was das Haus tut. Eine
|
|
Pruefung, die nichts mehr widerlegen kann, bestaetigt nichts;
|
|
sie taeuscht nur Deckung vor.
|
|
|
|
Was sie geprueft hat ("verschwindet von selbst, sobald ein Geraet
|
|
da ist"), prueft jetzt pruef-push-hinweis.mjs am neuen Kasten --
|
|
dort mit einer echten Anmeldung, die nachweislich in der
|
|
Datenbank landet. */
|
|
|
|
const eintrag = p.prepare("SELECT * FROM push_anmeldungen").get();
|
|
ok(eintrag?.person_id === idDogi && /^https:\/\//.test(eintrag?.endpunkt || ""),
|
|
`sie gehört der richtigen Person und hat eine https-Adresse`);
|
|
/* Der Gerätename hilft beim Wiedererkennen in der Liste. */
|
|
ok(/Chrome|Edge|Browser/.test(eintrag?.geraet || ""),
|
|
`Gerät erkannt: "${eintrag?.geraet}"`);
|
|
|
|
const lage = await seite.$eval("#glocke", (e) => e.dataset.lage);
|
|
const text = await seite.$eval("#glocke .glocke__text", (e) => e.textContent);
|
|
ok(lage === "an" && /an/i.test(text), `der Knopf steht jetzt auf "${text}" (${lage})`);
|
|
|
|
const strich = await seite.$eval(".glocke__strich", (e) => getComputedStyle(e).opacity);
|
|
ok(Number(strich) < 0.2, `der Strich ist weg (${strich})`);
|
|
|
|
/* NOCH EINMAL KLICKEN darf nicht verdoppeln -- INSERT OR REPLACE auf
|
|
dem Endpunkt. Sonst käme jede Nachricht mehrfach an. */
|
|
await seite.click("#glocke"); // aus
|
|
await seite.waitForTimeout(900);
|
|
ok(zahl() === 0, `nach dem Abmelden: ${zahl()} Anmeldungen`);
|
|
|
|
await seite.click("#glocke"); // wieder an
|
|
await seite.waitForTimeout(1200);
|
|
await seite.click("#glocke"); // aus
|
|
await seite.waitForTimeout(900);
|
|
await seite.click("#glocke"); // an
|
|
await seite.waitForTimeout(1200);
|
|
ok(zahl() === 1, `nach mehrfachem Hin und Her: genau ${zahl()} Anmeldung, keine Dubletten`);
|
|
|
|
/* Und der Stand-Weg zeigt sie -- ohne den vollen Endpunkt zu
|
|
verraten, der die Zustelladresse dieses Geräts ist. */
|
|
const stand = await seite.evaluate(async () => {
|
|
const a = await fetch("/workspace/api/push/stand", { credentials: "same-origin" });
|
|
return a.json();
|
|
});
|
|
ok(stand.geraete?.length === 1 && !stand.geraete[0].endpunkt,
|
|
`der Stand nennt ${stand.geraete?.length} Gerät, aber nicht dessen volle Adresse`);
|
|
ok(Array.isArray(stand.arten) && stand.arten.length >= 3,
|
|
`${stand.arten?.length} Arten von Benachrichtigungen einstellbar`);
|
|
|
|
p.close();
|
|
await browser.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
GEGENPROBE — misst das oben überhaupt etwas?
|
|
======================================================================= */
|
|
melde("\n=== Gegenprobe ===");
|
|
{
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ permissions: ["notifications"] });
|
|
await ctx.addInitScript(berechtigung("granted"));
|
|
const seite = await ctx.newPage();
|
|
/* Ein Doppel, dessen subscribe SCHEITERT. Der Knopf darf dann NICHT
|
|
auf "an" springen -- täte er es, wäre die Anzeige eine Lüge, und
|
|
alle Haken oben wären wertlos. */
|
|
await seite.addInitScript(`
|
|
(() => {
|
|
const reg = { pushManager: {
|
|
subscribe: async () => { throw new Error('Registration failed'); },
|
|
getSubscription: async () => null,
|
|
} };
|
|
Object.defineProperty(navigator, 'serviceWorker', {
|
|
configurable: true,
|
|
value: { register: async () => reg, getRegistration: async () => reg,
|
|
ready: Promise.resolve(reg), addEventListener: () => {} },
|
|
});
|
|
})();
|
|
`);
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
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: 25000 });
|
|
await seite.waitForTimeout(500);
|
|
|
|
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const vorher = p.prepare("SELECT COUNT(*) AS n FROM push_anmeldungen").get().n;
|
|
await seite.click("#glocke");
|
|
await seite.waitForTimeout(1200);
|
|
const nachher = p.prepare("SELECT COUNT(*) AS n FROM push_anmeldungen").get().n;
|
|
const lage = await seite.$eval("#glocke", (e) => e.dataset.lage);
|
|
|
|
ok(lage !== "an" && nachher === vorher,
|
|
`wenn subscribe scheitert: Knopf bleibt auf "${lage}", `
|
|
+ `Anmeldungen ${vorher} → ${nachher} (keine erfundene)`);
|
|
|
|
/* Und die Seite darf daran nicht zerbrechen. */
|
|
const lebt = await seite.evaluate(() => !!document.getElementById("glocke"));
|
|
ok(lebt, "die Seite läuft weiter, der Knopf ist noch da");
|
|
|
|
p.close();
|
|
await browser.close();
|
|
}
|
|
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
|
|
melde(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
|
|
if (!echtGeht) {
|
|
melde(`HINWEIS: Der Schritt "Browser holt eine Adresse beim Push-Dienst" war`);
|
|
melde(`hier nicht messbar (kein Zugang zu FCM). Alles andere wurde geprüft.`);
|
|
}
|
|
process.exit(fehler ? 1 : 0);
|