/* ===================================================================== pruef-ladefehler.mjs — Eine Seite, die nicht lädt, sagt es. Filipe, 19.09.2026: „Ladezustände … Es darf keine Sackgassen geben. Keine kryptischen Fehlermeldungen, sondern klare Aussagen darüber, was passiert ist und wie man das Problem lösen kann." --------------------------------------------------------------------- WAS GEMESSEN WURDE (20.09.2026) Jede Seite beginnt mit demselben Block: ein Abruf auf `/api/ich`, und darunter `} catch { return; }`. Auf **13 von 21 Seiten** war dieses `catch` stumm. Faellt das Netz aus -- Aufzug, U-Bahn, ein Funkloch --, bricht der Block ab, und die Seite bleibt stehen. Dabei steht auf ihr meistens schon etwas: „Uebersicht wird geladen …", `aria-busy="true"`. Dieser Satz wurde nie ersetzt. Gemessen: **0 von 32** Stellen setzten `aria-busy` im Fehlerfall zurueck. Und es gab **keinen einzigen** „Nochmal versuchen" auf allen 21 Seiten. Dazu ein eigener Fund auf `start.html`: Ein Netzfehler warf den Menschen auf die ANMELDESEITE. Er glaubt dann, er sei rausgeworfen worden, und tippt seinen Zugangscode neu -- dabei ist seine Sitzung noch zwoelf Stunden gueltig. --------------------------------------------------------------------- WAS GEPRUEFT WIRD 1. Kein Startblock faengt einen Fehler mehr stumm ab. 2. `start.js` leitet bei einem NETZfehler nicht mehr um -- nur noch bei einer Antwort, die es sagt (401). 3. Am echten Bildschirm: Netz abgeklemmt, Seite geladen -- steht ein Satz da? Ein Knopf? Und ist `aria-busy` wirklich zurueckgesetzt, oder meldet ein Vorleseprogramm ewig „beschaeftigt"? 4. Und der Knopf tut wirklich etwas: Netz wieder an, geklickt, Inhalt da. Punkt 4 ist der, auf den es ankommt. Alles davor koennte gut aussehen und trotzdem in einer Sackgasse enden. AUFRUF: node server/pruef-ladefehler.mjs ===================================================================== */ import { mkdtempSync, rmSync, readdirSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const HIER = dirname(fileURLToPath(import.meta.url)); const JS = join(HIER, "..", "workspace", "assets", "js"); const ordner = mkdtempSync(join(tmpdir(), "ws-ladefehler-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); import { notbremse } from "./helfer-notbremse.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); const PORT = await portMussFreiSein(4339, "die Ladefehlerpruefung"); process.env.PORT = String(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-ladefehler"); await new Promise((r) => setTimeout(r, 1000)); const BASIS = `http://127.0.0.1:${PORT}`; process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); let fehler = 0, geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; console.log("\n=== Eine Seite, die nicht lädt, sagt es ===\n"); /* ---- 1. Kein stummes catch mehr im Startblock ---------------------- */ const ohneK = (s) => s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1"); const stumm = []; let mitLadefehler = 0; for (const f of readdirSync(JS).filter((x) => x.endsWith(".js"))) { const code = ohneK(readFileSync(join(JS, f), "utf8")); if (/window\.ladefehler\?\.\(/.test(code)) mitLadefehler++; /* Der Startblock: ein Abruf auf /api/ich, darunter binnen weniger Zeilen ein catch, das NUR zurueckkehrt. */ if (/api\/ich[^\n]*\n(?:[^\n]*\n){0,5}?\s*\}\s*catch\s*\{\s*return;\s*\}/.test(code)) { stumm.push(f); } } /* DIE ZAHL GEHOERT IN DIE BEDINGUNG: Faende die Pruefung gar keine Datei, waere „keine stumm" trivialerweise wahr. */ ok(mitLadefehler >= 15 && stumm.length === 0, `${mitLadefehler} Dateien melden einen Ladefehler, ${stumm.length} fangen ihn noch stumm ab` + (stumm.length ? ": " + stumm.join(", ") : "")); /* ---- 2. start.js wirft nicht mehr raus ----------------------------- */ const startJs = ohneK(readFileSync(join(JS, "start.js"), "utf8")); const wurf = /\}\s*catch\s*\{\s*location\.assign\('\/workspace\/'\)/.test(startJs); ok(!wurf, "ein Netzfehler auf der Startseite wirft nicht mehr auf die Anmeldung"); ok(/if \(!antwort\.ok\) \{ location\.assign/.test(startJs), "bei einer echten Absage (401) wird weiterhin umgeleitet — das ist richtig"); /* ---- 3+4. Am echten Bildschirm ------------------------------------- */ 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 pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); try { const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); 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 }); for (const seitenname of ["uebersicht.html", "dateien.html", "start.html"]) { console.log(`\n— ${seitenname}, ohne Netz —`); /* DAS NETZ ABKLEMMEN, ABER NUR FUER DIE DATEN. Die Seite selbst (HTML, CSS, JS) muss laden -- sonst prueft man einen leeren Browser statt einer Seite ohne Daten. */ await seite.route("**/workspace/api/**", (weg) => weg.abort("failed")); await seite.goto(BASIS + "/workspace/" + seitenname, { waitUntil: "domcontentloaded" }); await seite.waitForTimeout(1500); const lage = await seite.evaluate(() => { const k = document.querySelector(".ladefehler"); return { kasten: !!k, satz: k?.querySelector(".ladefehler__satz")?.textContent?.trim() || "", knopf: k?.querySelector(".ladefehler__knopf")?.textContent?.trim() || "", /* Behauptet irgendwo noch etwas, es wuerde geladen? */ nochBeschaeftigt: document.querySelectorAll('[aria-busy="true"]').length, laedtNochText: [...document.querySelectorAll("*")] .filter((e) => e.children.length === 0 && /wird geladen|einen Moment/i.test(e.textContent || "")).length, adresse: location.pathname, }; }); ok(lage.kasten, ' es steht ein Kasten da, statt ewig zu laden'); ok(lage.satz.length > 15, ` mit einem ganzen Satz („${lage.satz}")`); ok(/[Nn]ochmal/.test(lage.knopf), ` und einem Knopf („${lage.knopf}")`); /* DER TEIL, DEN NIEMAND SIEHT UND DER TROTZDEM ZAEHLT. */ ok(lage.nochBeschaeftigt === 0, ` nichts meldet sich noch als „beschäftigt" (${lage.nochBeschaeftigt})`); ok(lage.laedtNochText === 0, ` und nirgends steht noch „wird geladen" (${lage.laedtNochText})`); if (seitenname === "start.html") { ok(lage.adresse.includes("start.html"), ` ein Funkloch wirft NICHT auf die Anmeldung (${lage.adresse})`); } /* ---- Und der Knopf tut wirklich etwas -------------------------- */ await seite.unroute("**/workspace/api/**"); await seite.click(".ladefehler__knopf"); await seite.waitForTimeout(2500); const danach = await seite.evaluate(() => ({ kastenWeg: !document.querySelector(".ladefehler"), inhalt: document.body.textContent.replace(/\s+/g, " ").trim().length, })); ok(danach.kastenWeg, ' nach dem Wiederholen ist der Kasten weg'); ok(danach.inhalt > 200, ` und die Seite hat Inhalt (${danach.inhalt} Zeichen)`); } /* ---- DIE GEGENPROBE --------------------------------------------- */ console.log("\n --- Gegenprobe (muss anschlagen) ---"); await seite.goto(BASIS + "/workspace/uebersicht.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(1200); const ohneFehler = await seite.evaluate(() => ({ kasten: !!document.querySelector(".ladefehler"), busy: document.querySelectorAll('[aria-busy="true"]').length, })); ok(!ohneFehler.kasten, "mit Netz steht KEIN Fehlerkasten da — sonst käme er immer und niemand läse ihn"); await ctx.close(); } finally { await browser.close(); } console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler ? 1 : 0);