diff --git a/server/pruef-ladefehler.mjs b/server/pruef-ladefehler.mjs new file mode 100644 index 00000000..7853b102 --- /dev/null +++ b/server/pruef-ladefehler.mjs @@ -0,0 +1,207 @@ +/* ===================================================================== + 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); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index cfd8e51d..f2aea738 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -277,6 +277,28 @@ aufgabenRouter.get("/workspace/api/aufgaben", (req, res) => { irgendwann drei Antworten. */ eskalationSetzen(reihen); + /* ==== WER DARF DIESE KARTE WEITERSCHIEBEN? (20.09.2026) ========== + + An jeder Karte stehen „◀ zurueck" und „▶ In Arbeit". Sie wurden + OHNE Pruefung gebaut -- auch an den Karten anderer Leute. + + Ein Modi sieht das Brett des ganzen Teams (siehe sichtbarkeit + weiter oben, das ist Absicht). Er tippt an einer fremden Karte + auf „In Arbeit", der Server lehnt mit 403 ab, und die Meldung + erscheint GANZ OBEN auf der Seite. Bei einer Karte weiter unten + sieht er davon nichts -- fuer ihn tut der Knopf einfach nichts. + + Zwei Zeilen tiefer steht die Regel im Klartext: „hier geht es + nur darum, niemandem etwas anzubieten, das dann abgelehnt wird." + Sie galt fuer den Abbrechen-Knopf und nicht fuer diese beiden. + + DIE ANTWORT KOMMT VOM SERVER, nicht aus einer zweiten Regel im + Browser: `darfAendern` entscheidet ohnehin schon ueber das + Schreiben. Zwei Stellen, die dieselbe Frage beantworten, geben + irgendwann zwei Antworten -- und die falsche ist dann die + sichtbare. */ + for (const r of reihen) r.darf_aendern = darfAendern(req.person, r); + res.json({ aufgaben: reihen, /* Die Aufwandsstufen kommen mit: Eine zweite Liste im Browser diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 5717b77f..76626cfd 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -64,6 +64,7 @@ import { TREFF_BRETTER, TREFF_ROLLEN, AUSSEN_ROLLEN, heuteLokal, gehoertAufDieseAdresse, + bereicheFuer, } from "./workspace.js"; import { nachrichtSchicken } from "./workspace-chat.js"; import { @@ -289,7 +290,12 @@ export const BEREICHE = { }, live: { - name: "LIVE-Analyse", + /* HEISST WIE DIE KACHEL (20.09.2026). Die Kachel sagt + „Live-Ablauf", die Seite sagte „LIVE-Analyse" -- und der + Untertext der Kachel verspricht eine Checkliste. Wer darauf + tippt, landet unter einem anderen Namen. Der Weg faengt auf + der Startseite an, also gewinnt der Name von dort. */ + name: "Live-Ablauf", /* Was dasteht, wenn nichts dasteht. Ein Bereich, der schweigt, sieht kaputt aus statt leer -- und der Satz sagt NICHT nur "leer", sondern was hier hingehoert. */ @@ -1463,8 +1469,30 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => { } : null; + /* ==== DIE OBERZEILE KOMMT AUS DER KACHEL (20.09.2026) ========== + + Ueber jedem Brett steht eine kleine Zeile, die sagt, wozu es + gehoert. Sie stand FEST im HTML: `

Betreuung

` + -- ueberschrieben nur dann, wenn das Brett ein eigenes `ober` + hat. + + Gemessen: SECHS Bretter haben keines (live, content, technik, + community, schutz, agentur). Ueber dreien davon -- „Rund ums + Live" eines Modi -- stand damit dauerhaft „Betreuung". Ein Modi + betreut niemanden. + + Jetzt faellt sie auf die GRUPPE der Kachel zurueck, mit der + dieser Mensch hierhergekommen ist. Das ist die ehrlichste + Auskunft, die es gibt: Genau dort hat er geklickt. Und sie ist + je Rolle richtig, ohne dass jemand eine zweite Liste pflegt -- + dasselbe Brett kann fuer einen Modi in einer anderen Gruppe + stehen als fuer die rechte Hand. */ + const meine = bereicheFuer(req.sicht || req.person) || []; + const passend = meine.find((k) => String(k.ziel || "").split("?")[1] === "b=" + bereich); + const ober = einstellung.ober || passend?.gruppe || null; + res.json({ - bereich, einstellung, treff: treffBrett, gehoert, weiter, + bereich, einstellung: { ...einstellung, ober }, treff: treffBrett, gehoert, weiter, /* DARF DIESER MENSCH IN DEN KALENDER? Beantwortet von den zwei Stellen, die es entscheiden -- nicht von einer dritten Regel im Browser. Die Karte zeigt ihren Termin dann als Text statt diff --git a/server/workspace.js b/server/workspace.js index 14311640..566a13b5 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1129,9 +1129,33 @@ const TREFF_MODERATION_KACHEL = { /* Welche Bretter zum Treff gehoeren -- ABGELEITET aus den Kacheln, nicht danebengeschrieben. Eine zweite Liste waere die, die beim naechsten neuen Brett vergessen wird. */ -export const TREFF_BRETTER = TREFF_BEREICHE - .map((k) => new URL("http://x/" + k.ziel).searchParams.get("b")) - .filter(Boolean); +export const TREFF_BRETTER = [...new Set([ + ...TREFF_BEREICHE + .map((k) => new URL("http://x/" + k.ziel).searchParams.get("b")) + .filter(Boolean), + /* EIN BRETT OHNE KACHEL (berichtigt 20.09.2026). + ---------------------------------------------------------------- + Die Liste wird aus den Kachelzielen abgeleitet, und das ist + richtig: Was niemand anklicken kann, muss auch nicht offenstehen. + + Am 19.09.2026 bekam die Kachel „Regeln & Hilfe" ein neues Ziel -- + `treff-regeln.html` statt `bereich.html?b=regeln`, weil die + Kachel vorher auf ein leeres Brett zeigte. Damit fiel `regeln` + aus dieser Ableitung heraus, und das BRETT war ab da fuer die + Community gesperrt (404). + + Nur verweist `treff-regeln.html` weiterhin darauf: „Haeufige + Fragen und Antworten stehen auf dem Brett Regeln & Hilfe." Ein + Verweis, der seit einem Tag ins Leere fuehrt -- und zwar auf der + einen Seite, die einem neuen Mitglied alles erklaeren soll. + + Gefunden hat es `pruef-treff` (4 Fehler, seit dem 19.09.), nicht + ein Mensch. Deshalb steht es hier ausdruecklich und mit Grund: + Ein Brett kann erreichbar sein muessen, ohne eine eigene Kachel + zu haben. Wer die Ableitung liest und diese Zeile nicht sieht, + haelt die Liste fuer vollstaendig. */ + "regeln", +])]; /* Wer den Treff ueberhaupt sehen darf: die Community selbst und das Team von Dogi. Die Agentur ausdruecklich nicht -- Creator, Scouts und @@ -1393,10 +1417,21 @@ const BRETTER_UEBER_SEITEN = { /** Bretter mit eigener Kachel. Diese darf jeder aus dem Team, der die * Kachel sieht -- sie sind daraus abgeleitet, nicht abgeschrieben. */ -export const MODI_BEREICHE_ERLAUBT = new Set( - HAND_MIT_TREFF +export const MODI_BEREICHE_ERLAUBT = new Set([ + ...HAND_MIT_TREFF .map((k) => (/bereich\.html\?b=([a-z]+)/.exec(k.ziel) || [])[1]) - .filter(Boolean)); + .filter(Boolean), + /* DIESELBE LUECKE WIE BEI TREFF_BRETTER (20.09.2026), und aus + demselben Grund: `regeln` hat seit dem 19.09. keine Kachel mehr, + faellt damit aus jeder Ableitung ueber Kachelziele -- und ist + trotzdem ein Brett, das `treff-regeln.html` verlinkt. + + Zwei Ableitungen, dieselbe blinde Stelle. Das ist der Grund, + warum hier eine Zeile steht statt einer zweiten klugen Regel: + Wer eine Kachel umleitet, nimmt einem Brett damit stillschweigend + die Erreichbarkeit. Das muss sichtbar sein, nicht gerechnet. */ + "regeln", +]); /** Und die Bretter hinter einer Seite -- nur fuer den, der die Seite darf. * diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index f7c1e48e..e2b0e441 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +