/* DER GROSSCHECK — jede Seite, jede Rolle, Handy und Rechner. Auftrag vom 02.09.2026: "ich will, dass du jetzt einmal einen grossen Check machst, ob alles klappt, auf dem Handy und PC." WAS DIESE PRUEFUNG ANDERS MACHT ALS DIE ANDEREN. Die uebrigen dreissig Pruefungen fragen jeweils "stimmt diese eine Sache?". Diese hier fragt "faellt IRGENDWO etwas auseinander?" -- und geht dafuer stumpf ueber alles: sechzehn Workspace-Seiten mal vier Rollen mal zwei Bildschirmgroessen, dazu jede oeffentliche Seite. Solche Rundgaenge finden andere Fehler als gezielte Pruefungen: nicht den falsch gerechneten Wert, sondern die Seite, die bei genau einer Rolle leer bleibt, oder die auf dem Handy zwei Pixel ueberlaeuft. GEMESSEN WIRD, WAS EIN MENSCH MERKEN WUERDE: 1. Kommt die Seite ueberhaupt an (Inhalt da, nicht nur Geruest)? 2. Steht etwas seitlich ueber? (Auf dem Handy der schlimmste Fehler.) 3. Meldet der Browser einen Fehler? 4. Ist Text abgeschnitten? 5. Sind Beruehrziele auf dem Handy gross genug (24 px, WCAG 2.5.8)? 6. Liegt etwas Bedienbares unter etwas anderem? 7. Ist Schrift zu klein zum Lesen? DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Eine Seite, die gar nichts enthaelt, waere sonst die sauberste von allen -- kein Ueberstand, keine zu kleine Schrift, kein Fehler. Deshalb wird gezaehlt, WIE VIEL auf jeder Seite ueberhaupt angesehen wurde, und "nichts gefunden" gilt als Fehler. Am Ende steht eine Gegenprobe: eine absichtlich zu breite Kachel muss gemeldet werden. Eine Pruefung, die immer bestaetigt, bestaetigt nichts. */ import { mkdtempSync, rmSync, readdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; /* Der Repo-Ordner, vom Ort DIESER Datei aus -- nicht vom Arbeits- verzeichnis. Siehe die Begruendung weiter unten bei OEFFENTLICH. */ const REPO = join(dirname(fileURLToPath(import.meta.url)), ".."); const ordner = mkdtempSync(join(tmpdir(), "ws-gross-")); 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-grosscheck"); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-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-grosscheck"); await new Promise((r) => setTimeout(r, 800)); const BASIS = `http://127.0.0.1:${PORT}`; setTimeout(() => { console.log("ABBRUCH — zu lange gelaufen"); process.exit(1); }, 900_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)); } } /* ---- Ein Bestand, der die Seiten wirklich fuellt ---------------------- Leere Seiten pruefen nichts. Jede Rolle bekommt Daten, damit auf jeder Seite tatsaechlich etwas steht -- sonst misst der Rundgang leere Geruesten und meldet sie als tadellos. */ 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)); const morgen = 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; } /* Lange Namen -- kurze wuerden Ueberlauf-Fehler verstecken. */ const idDogi = anlegen("Filipe Fernandes Ribeiro", "admin", "CODE-DOGI-0001"); const idMara = anlegen("Mara Schmidt-Hohenberg", "manager", "CODE-MANA-0001"); const idPat = anlegen("Patrick von Hohenstein", "scout", "CODE-SCOU-0001"); const idTili = anlegen("SpongBobSchwammKopf", "creator", "CODE-ANNA-0001"); const idLuna = anlegen("Luna", "creator", "CODE-LUNA-0001"); d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idPat, jetzt); d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idMara, jetzt); d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idPat, idMara, jetzt); const LANG = "Ein absichtlich sehr langer Titel ohne Leerzeichenumbrueche " + "damit ein Ueberlauf auffaellt statt sich zu verstecken"; for (const [titel, st, frist, cr, ve] of [ [LANG, "offen", gestern, idTili, idPat], ["Kurz", "arbeit", heute, idTili, idPat], ["Review-Sache", "review", morgen, idLuna, idMara], ["Fertig", "erledigt", null, idLuna, idMara], ]) { d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, frist, creator_id, verantwortlich_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?)`) .run(titel, "Beschreibung mit etwas Text.", st, "hoch", frist, cr, ve, jetzt, idDogi); } for (const [t, art, beginn] of [ ["Wochen-Call mit einem sehr langen Namen", "call", heute + "T10:00"], ["Review", "review", morgen + "T14:00"], ]) { d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?)`).run(t, art, beginn, 30, idTili, jetzt, idPat); } for (const b of ["live", "community", "technik", "schutz", "content"]) { d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit, datum, creator_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?,?)`) .run(b, b === "content" ? "idee" : "vorbereitung", LANG, "Text dazu.", "offen", "hoch", heute, idTili, jetzt, idPat); } d.prepare(`INSERT INTO leads (name, plattform, handle, status, prioritaet, scout_id, erstellt, naechster_followup) VALUES (?,?,?,?,?,?,?,?)`) .run("Ein Lead mit langem Namen", "tiktok", "@langername", "angesprochen", "hoch", idPat, jetzt, gestern); try { d.prepare(`INSERT INTO wissen (titel, beschreibung, kategorie, stufe, geraet, name_original, name_datei, groesse, wichtig, veroeffentlicht, hochgeladen, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`) .run(LANG, "Eine Anleitung.", "hilfe", "einsteiger", "egal", "a.pdf", "x1.pdf", 1000, 1, heute, jetzt, idDogi); } catch { /* Tabelle evtl. anders benannt -- der Rundgang laeuft trotzdem */ } d.close(); /* ---------------------------------------------------------------- */ const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await chromium.launch(); /* Die Messung laeuft IM Browser -- nur dort weiss man, was wirklich gezeichnet wurde. */ const messen = () => { const sichtbar = (e) => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0; }; const name = (e) => (e.tagName + "." + String(e.className || "").split(" ")[0]).slice(0, 28) + (e.textContent ? ` "${e.textContent.trim().slice(0, 14)}"` : ""); const alle = [...document.querySelectorAll("body *")]; /* ABSICHTLICH UNSICHTBAR IST KEIN FEHLER. wahl.js ersetzt jedes Auswahlfeld durch einen eigenen Knopf; das echte