/* =================================================================== Die Handy-Pruefung. Auf einem Telefon geht anderes kaputt als am Rechner, und man sieht es beim Entwickeln nie: Ein Knopf steht 3 px aus dem Bild, eine Ueberschrift ist auf halbem Wort abgeschnitten, ein Feld ist so schmal, dass man es nicht trifft. Alles davon faellt erst auf, wenn jemand das Geraet in der Hand hat. Geprueft wird auf DREI echten Geraetegroessen, jede Seite einzeln: 1. Nichts steht ueber waagerechtes Scrollen ist auf einem Handy der schlimmste Fehler -- die Seite ruckelt und man verliert den Rand 2. Kein Text abgeschnitten gemessen wird die tatsaechliche Textbreite gegen die verfuegbare, nicht "sieht ganz aus" 3. Beruehrziele gross genug 24x24 CSS-Pixel ist das Mindestmass (WCAG 2.2, Kriterium 2.5.8). Darunter trifft man mit dem Daumen daneben 4. Nichts ueberlappt zwei Bedienelemente duerfen sich nicht ueberdecken -- man traefe das falsche 5. Keine Fehler in der Konsole =================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-handy-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); /* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026). Ohne diese Zeile startet der eigene Server bei belegtem Port STILL nicht -- und alles Folgende misst gegen einen fremden Stand. Das ist der dritte Ausgang: belegt ist etwas anderes als kaputt. */ const PORT = await eigenerPort(import.meta, "pruef-handy"); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; 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 }); }; import { notbremse } from "./helfer-notbremse.mjs"; import { eigenerPort } from "./helfer-port.mjs"; 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-handy"); await new Promise((r) => setTimeout(r, 700)); const BASIS = `http://127.0.0.1:${PORT}`; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 400_000).unref?.(); /* ORTSZEIT, nicht UTC. Die Anwendung rechnet "heute" nach der Uhr des Benutzers; toISOString() liefert dagegen UTC und damit zwischen Mitternacht und 2 Uhr den VORTAG. In diesem Fenster schlug diese Pruefung fehl, ohne dass etwas kaputt war. */ const tagText = (d = new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` + `-${String(d.getDate()).padStart(2, "0")}`; let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); 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)); } } /* Echte Daten statt leerer Seiten: Eine leere Liste kann nicht ueberlaufen. Geprueft wird mit LANGEN Namen und vollen Listen -- genau dort bricht es. */ const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const heute = tagText(); const gestern = tagText(new Date(Date.now() - 86400_000)); 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idScout = anlegen("Patrick-Maximilian", "scout", "CODE-SCOU-0001"); const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001"); /* Die beiden fehlenden Rollen -- gebraucht fuer die Mittenpruefung ganz unten. Filipe: "das soll bei jedem und jeder rolle verbessert werden", und eine Pruefung an drei von fuenf Rollen haette genau diese Aussage nicht belegen koennen. */ const idSpicy = anlegen("Cigdem", "spicy", "CODE-SPIC-0001"); const idMgr = anlegen("Mara", "manager", "CODE-MANA-0001"); d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idScout, jetzt); const LANG = "Ein sehr langer Eintrag ohne Leerzeichenumbruch fuer die Handypruefung"; for (const [status, frist] of [["offen", gestern], ["offen", heute], ["arbeit", null], ["review", null], ["erledigt", null]]) { d.prepare(`INSERT INTO aufgaben (titel, status, creator_id, verantwortlich_id, frist, prioritaet, erstellt, erstellt_von) VALUES (?,?,?,?,?,'hoch',?,?)`) .run(LANG, status, idTili, idTili, frist, jetzt, idDogi); } for (const [art, stunde] of [["call", "10:00"], ["termin", "14:30"], ["review", "18:00"]]) { d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, teilnehmer_id, erledigt, erstellt, erstellt_von) VALUES (?,?,?,60,?,?,0,?,?)`) .run(LANG, art, heute + "T" + stunde, idTili, idTili, jetzt, idDogi); } d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von) VALUES ('technik','problem',?,?,'hoch','offen',?,?,?)`) .run(LANG, heute, idTili, jetzt, idDogi); d.close(); const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await chromium.launch(); /* ---------- WELCHE SEITEN? ALLE. ----------------------------------------- Hier stand bis zum 06.09.2026 eine Liste von Hand -- und sie war veraltet, ohne dass es jemandem auffiel: chat.html, leistung.html und steckbrief.html fehlten darin, also DREI von neunzehn Seiten waren nie auf einem Handy gemessen worden. Der Chat ausgerechnet ist die Seite, die man am ehesten unterwegs benutzt. Das ist genau die Sorte Fehler, die eine Liste macht: Sie altert in dem Moment, in dem jemand weiterarbeitet, und sie sagt nichts dazu. Der Lauf blieb gruen, die Zahl der Pruefungen stimmte -- gemessen wurde trotzdem zu wenig. Deshalb wird die Liste jetzt aus dem VERZEICHNIS gelesen. Eine neue Seite ist damit von selbst dabei, so wie sie durch die Schranke in workspace.js von selbst geschuetzt ist -- derselbe Gedanke: Der sichere Rueckfall muss der VORGABEwert sein, nicht der, an den man denken muss. Ausgenommen ist einzig index.html -- das ist die Anmeldeseite, sie wird weiter unten eigens geprueft (vor der Anmeldung, denn danach leitet sie weiter). */ const { readdirSync } = await import("node:fs"); const { dirname } = await import("node:path"); const { fileURLToPath } = await import("node:url"); /* NICHT process.cwd(): Diese Datei wird mal aus dem Wurzelverzeichnis gestartet (tools/alles-pruefen.mjs) und mal aus server/ von Hand. Im zweiten Fall zeigte cwd auf server/, dort gibt es kein workspace/, und readdirSync wuerfe -- oder faende, schlimmer, ein anderes Verzeichnis. Der Ort DIESER Datei steht dagegen fest. */ const SEITEN_ORDNER = join(dirname(fileURLToPath(import.meta.url)), "..", "workspace"); const ALLE_SEITEN = readdirSync(SEITEN_ORDNER) .filter((n) => n.endsWith(".html") && n !== "index.html") .sort(); /* Ein paar Seiten brauchen eine Abfrage, sonst zeigen sie nur ihre leere Huelle. Fuer die steht hier, WOMIT sie aufgerufen werden. */ const MIT_ABFRAGE = { "bereich.html": "?b=live" }; const SEITEN = ALLE_SEITEN.map((n) => n + (MIT_ABFRAGE[n] || "")); /* Und die Zahl gehoert in die BEDINGUNG, nicht nur in den Meldetext: Faende readdirSync nichts (falsches Arbeitsverzeichnis), liefe die Schleife unten null Mal -- und der Lauf waere gruen, ohne eine einzige Seite angesehen zu haben. Genau der Fehler, den ich schon zweimal hatte. */ if (SEITEN.length < 15) { console.log(` FEHL nur ${SEITEN.length} Seiten in ${SEITEN_ORDNER} gefunden ` + "-- die Handypruefung kann so nichts messen"); process.exit(1); } console.log(`${SEITEN.length} Workspace-Seiten werden gemessen: ${SEITEN.join(", ")}`); /* Drei echte Geraete, nicht "irgendwas Schmales": 320 iPhone SE (1. Gen) -- das schmalste noch benutzte Geraet 390 iPhone 14/15, das haeufigste Mass 412 Pixel 8 / Galaxy S24, typisches Android */ const GERAETE = [[320, 568, "sehr schmal"], [390, 844, "iPhone"], [412, 915, "Android"]]; const messen = () => ({ /* 1. Waagerechtes Scrollen. Der schlimmste Fehler auf einem Handy. */ ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, schuld: [...document.querySelectorAll("body *")] .filter((e) => { const r = e.getBoundingClientRect(); /* Nur was wirklich sichtbar ist. Ein Wasserzeichen hinter overflow:hidden meldet ein Rechteck, das nie zu sehen war -- das hat frueher schon einmal falschen Alarm ausgeloest. */ if (r.width < 1 || r.height < 1) return false; const st = getComputedStyle(e); if (st.visibility === "hidden" || st.display === "none") return false; /* Steckt es in einem Vorfahren mit overflow:hidden? Dann ist es abgeschnitten und steht nicht wirklich ueber. */ for (let v = e.parentElement; v; v = v.parentElement) { const vs = getComputedStyle(v); if (vs.overflow !== "visible" || vs.overflowX !== "visible") return false; } return r.right > window.innerWidth + 1; }) .map((e) => `${e.tagName}.${String(e.className).slice(0, 26)}(+${Math.round(e.getBoundingClientRect().right - window.innerWidth)}px)`) .slice(0, 4), /* 2. Abgeschnittener Text. Gemessen wird die tatsaechliche Breite gegen die verfuegbare -- nicht "sieht ganz aus". Elemente, die ausdruecklich kuerzen duerfen (ellipsis), sind ausgenommen: Dort ist das Kuerzen die Absicht und der volle Text steht woanders. */ abgeschnitten: [...document.querySelectorAll("h1, h2, h3, p, span, a, label, button, li, td, th")] .filter((e) => { if (e.children.length) return false; const t = e.textContent.trim(); if (t.length < 3) return false; const st = getComputedStyle(e); if (st.display === "none" || st.visibility === "hidden") return false; if (st.textOverflow === "ellipsis") return false; if (st.overflow === "hidden" && st.whiteSpace === "nowrap") return false; /* ==== NUR WO DIE MESSUNG UEBERHAUPT ETWAS BEDEUTET (25.09.2026) `scrollWidth` und `clientWidth` gibt es nur fuer Elemente mit einem eigenen Kasten. Bei `display: inline` liefert Chromium fuer `clientWidth` glatt NULL -- und damit ist jeder Text breiter als sein Kasten. GEMELDET WURDE dadurch auf team.html: „Mara" (18>0), „Noch niemand zugeteilt" (14>0), „Patrick-Maximilian" (18>15). Alle drei stehen vollstaendig da; sie sind nur inline gesetzt. Ein Inline-Element SCHNEIDET auch nichts ab -- es bricht um. Abgeschnitten wird es erst durch einen Vorfahren mit `overflow: hidden`, und dann greift die Zeile darueber. DER RICHTIGE UMGANG MIT EINER UNGUELTIGEN MESSUNG ist, sie nicht zu machen -- nicht, ihr Ergebnis zu glauben. */ if (st.display === "inline") return false; /* ==== UND WAS FUERS AUGE WEGGENOMMEN IST (25.09.2026) ======== Fuer Vorleseprogramme steht auf mehreren Seiten Text da, den man nicht sieht: ein echtes `