Files
dogfather-universe/server/pruef-browser.mjs
T
DogFatherGitandClaude Opus 5 6e46a08543 Portnummern werden abgeleitet, nicht mehr vergeben
Gemessen: 165 Pruefdateien, 129 verschiedene Nummern -- NEUNZEHN
doppelt, vier davon dreifach. Niemand hatte das gewollt; jede neue
Pruefung wurde von einer vorhandenen abgeschrieben, und die Nummer kam
mit. Meine eigene Notiz sagte "sieben" -- auch eine Bestandsliste
altert.

Der Waechter faengt den Schaden ab, aber er kann nur melden, was schon
passiert ist: Zwei Pruefungen mit derselben Nummer koennen nie
gleichzeitig laufen, und ein liegengebliebener Prozess der einen laesst
die andere abbrechen mit einer Meldung, die wie ein Befund aussieht.
Genau das ist mir am 20.09. zweimal passiert.

Jetzt leitet jede Datei ihre Nummer aus ihrer STELLE IM ALPHABET ab
(eigenerPort in helfer-port.mjs), zwei je Datei. Nicht ueber eine
Pruefsumme: Bei 165 Namen in 4900 Nummern waeren nach dem
Geburtstagsproblem rund DREI Zusammenstoesse zu erwarten -- ein Hash
tauscht eine sichtbare Doppelung gegen eine unsichtbare. Die Stelle im
Alphabet ist eindeutig von der Bauart her.

--- ZWEI FEHLER AUF DEM WEG, BEIDE LEHRREICH -------------------------

1. DER ERSTE VERSUCH WAR GRUEN UND KAPUTT. Ersetzt wurde mit einem
   Muster: "([^"]*4231[^"]*)". Das hielt

     { host: "127.0.0.1", port: 4231, path: "/404.html" }

   fuer eine Zeichenkette -- ein Muster kann eine oeffnende nicht von
   einer schliessenden Anfuehrung unterscheiden. Heraus kam

     { host: "127.0.0.1`, port: ${PORT}, path: `/404.html" }

   also GUELTIGER Code ohne port-Feld. `node --check` sagte gruen fuer
   alle 164 Dateien. Aufgefallen ist es erst, weil ich vier Vertreter
   gegen eine vorher gemessene Grundlinie laufen liess: pruef-schranke
   39/0 vorher, 38/1 nachher.

   Alles zurueckgenommen und mit einem Zerleger neu gemacht, der weiss,
   ob eine Stelle Code, Zeichenkette, Vorlage, Kommentar oder
   regulaerer Ausdruck ist. In pruef-ics stand die Nummer in einem
   regulaeren Ausdruck -- der wird jetzt gebaut statt hingeschrieben.

2. EIN MODUL, DAS BEIM IMPORTIEREN ARBEITET, IST EINE FALLE. Der
   zweite Durchgang importierte den ersten, um seine Mechanik zu
   benutzen -- und fuehrte dessen Hauptlauf gleich mit aus. Die
   zweiten Nummern wurden dadurch als erste behandelt, zwei Aufrufe
   bekamen dieselbe Nummer, und in pruef-content stand `const PORT`
   zweimal.

--- WAS DAS DAUERHAFT HAELT -----------------------------------------

pruef-portnummern.mjs (neu, 8 Pruefungen) fragt nicht "welche Nummern
sind doppelt", sondern "wer traegt ueberhaupt noch eine von Hand ein"
und "wer startet einen Server, ohne seine Nummer abzuleiten". Die
zweite Frage hat sofort etwas gefunden, das in KEINER Doppelungsliste
stand: pruef-push-weg belegte 4341 und 4342, rief den Waechter aber
gar nicht auf -- dieselbe Nummer wie pruef-agentur. Eine Liste zeigt
nur, was auf ihr steht.

Mit Gegenprobe: Eine unbekannte Datei bekommt keine geratene Nummer,
sondern einen Abbruch, und eine dritte Nummer je Datei gibt es nicht.

--- NACHGEMESSEN ----------------------------------------------------

Sechzehn Pruefungen gegen ihre vorher gemessene Grundlinie, je eine
Vertreterin jeder umgestellten Bauweise (eine Nummer, zwei Nummern,
Nummer in einer Zeichenkette, in einer Vorlage, in einem regulaeren
Ausdruck, dynamische Einfuhr, Nachtlauf mit zwei Laeufen):

  crew-adresse 132/0 · schranke 39/0 · content 45/0 · entwicklung 46/0
  anruf 127/0 · ics 37/0 · push-weg 20/0 · arten 28/0 · video 67/0
  kanalzeile 14/0 · agentur 62/0 · spicy 83/0 · creator-anlegen 50/0
  push-ziel 10/0 · portnummern 8/0

Alle exakt wie vorher. Zwei waren schon vorher rot und sind es
unveraendert geblieben (crew-wand-bild 41/4, teilen 14/3) -- per
`git stash` belegt, nicht angenommen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 22:51:21 +02:00

306 lines
14 KiB
JavaScript

/* DREI RENDERING-MASCHINEN, NICHT EINE.
Auftrag vom 04.09.2026 (vollstaendiger Audit), Abschnitt 5:
Chrome, Edge, Firefox, Safari, Mobile Safari, mobiles Chrome.
WARUM DAS NOETIG IST. Alle bisherigen Pruefungen dieses Hauses laufen
in Chromium -- und damit deckt jede einzelne nur Chrome und Edge ab
(beide bauen darauf auf). Firefox (Gecko) und Safari (WebKit) sind
voellig andere Maschinen. Genau dort zeigen sich:
* CSS, das nur eine Maschine kennt (color-mix, :has, mask-composite,
aspect-ratio, backdrop-filter -- dieses Haus benutzt alles davon)
* JavaScript, das eine Maschine noch nicht mitbringt
* unterschiedliche Voreinstellungen bei Formularen und Schriften
Safari ist der wichtigste Fall: Wer den Workspace auf dem iPhone
benutzt, benutzt WebKit -- auch dann, wenn "Chrome" auf dem
Bildschirm steht. Auf iOS gibt es keine andere Maschine.
GEMESSEN WIRD JE MASCHINE:
1. Kommt die Seite an (echter Inhalt, nicht nur Geruest)?
2. Meldet die Konsole einen Fehler?
3. Steht etwas seitlich ueber?
4. Kommt man ueberhaupt hinein (Anmeldung, Weiterleitung)?
DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Faellt eine Maschine aus (nicht
installiert), darf das nicht als "bestanden" durchgehen -- dafuer gibt
es den dritten Ausgang: nicht in Ordnung ist etwas anderes als nicht
nachgesehen.
GEGENPROBE am Ende: In jeder Maschine muss ein absichtlicher Fehler
gefunden werden. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { heuteLokal } from "./helfer-zeit.mjs";
const ordner = mkdtempSync(join(tmpdir(), "ws-browser-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
/* Der Port muss frei sein -- sonst wird gegen einen fremden Server
gemessen. Begruendung in helfer-port.mjs. */
import { notbremse } from "./helfer-notbremse.mjs";
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "die Browserpruefung");
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-browser");
await new Promise((r) => setTimeout(r, 900));
const BASIS = `http://127.0.0.1:${PORT}`;
/* LAUFEND MITSCHREIBEN, nicht erst am Ende.
Node puffert stdout, sobald es in eine Datei umgeleitet wird -- bei
einem Lauf, der drei Browser startet und eine Viertelstunde braucht,
steht bis zum Schluss NICHTS in der Datei. Beim ersten Versuch wurde
der Lauf abgeraeumt, bevor er fertig war, und hinterliess null Bytes:
eine Viertelstunde Rechenzeit ohne ein einziges Ergebnis.
Deshalb geht jede Zeile zusaetzlich sofort auf die Platte. Wird der
Lauf abgebrochen, ist wenigstens alles da, was bis dahin gemessen
wurde. */
const { appendFileSync } = await import("node:fs");
const PROTOKOLL = join(process.cwd(), "pruef-browser-lauf.txt");
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
const melde = (zeile) => {
console.log(zeile);
try { appendFileSync(PROTOKOLL, zeile + "\n"); } catch { /* Protokoll ist Zugabe */ }
};
let fehler = 0;
const ok = (b, t) => { melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* ---- Bestand ------------------------------------------------------------ */
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 jetzt = new Date().toISOString();
/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. Mit
`jetzt.slice(0, 10)` lag dieser Wert zwischen Mitternacht und
zwei Uhr einen Tag daneben. */
const heute = heuteLokal();
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 Fernandes Ribeiro", "admin", "CODE-DOGI-0001");
const idPat = anlegen("Patrick von Hohenstein", "scout", "CODE-SCOU-0001");
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
for (const [t, st] of [["Eine offene Aufgabe", "offen"], ["In Arbeit", "arbeit"],
["Im Review", "review"], ["Erledigt", "erledigt"]]) {
d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, frist,
creator_id, verantwortlich_id, erstellt, erstellt_von) VALUES (?,?,?,'hoch',?,?,?,?,?)`)
.run(t, "Beschreibung.", st, heute, idLuna, idPat, jetzt, idDogi);
}
d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von)
VALUES ('Ein Call','call',?,30,?,?,?)`).run(heute + "T10:00", idLuna, jetzt, idDogi);
for (const b of ["live", "content", "technik", "community", "schutz"]) {
d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit,
datum, creator_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?,?)`)
.run(b, b === "content" ? "idee" : "vorbereitung", "Ein Eintrag", "Text.",
"offen", "hoch", heute, idLuna, jetzt, idPat);
}
d.close();
/* ---- Die drei Maschinen -------------------------------------------------- */
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const SEITEN = [
"start.html", "uebersicht.html", "aufgaben.html", "kalender.html", "calls.html",
"dateien.html", "content.html", "bereich.html?b=live", "profil.html",
"startcheck.html", "report.html", "scouting.html", "personen.html",
"wissen.html", "automation.html", "steckbrief.html",
];
/* Handy-Format fuer WebKit dazu -- das ist der Fall, der auf dem iPhone
wirklich vorkommt. */
const MASCHINEN = [
["chromium", "Chromium (Chrome, Edge)", 1280, 900],
["firefox", "Firefox (Gecko)", 1280, 900],
["webkit", "WebKit (Safari)", 1280, 900],
["webkit", "WebKit auf dem Handy (iPhone)", 390, 844],
];
const messen = () => {
const sichtbar = (e) => {
const r = e.getBoundingClientRect();
if (r.width <= 0 || r.height <= 0) return false;
const st = getComputedStyle(e);
return st.visibility !== "hidden" && st.display !== "none";
};
const alle = [...document.querySelectorAll("body *")].filter(sichtbar);
const breite = document.documentElement.clientWidth;
return {
elemente: alle.length,
inhalt: (document.body.innerText || "").trim().length,
ueber: Math.max(0, Math.round(document.documentElement.scrollWidth - breite)),
schuld: alle.filter((e) => {
const r = e.getBoundingClientRect();
return r.right > breite + 2 && r.width <= breite * 1.5;
}).slice(0, 2).map((e) => `${e.tagName}.${(typeof e.className === "string" ? e.className : "").split(" ")[0]}`),
bedienbar: alle.filter((e) =>
["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA"].includes(e.tagName)).length,
};
};
let seitenGesamt = 0, elementeGesamt = 0;
let maschinenGelaufen = 0;
const nichtNachgesehen = [];
for (const [motor, wie, breite, hoehe] of MASCHINEN) {
let browser;
try {
browser = await pw[motor].launch();
} catch (f) {
/* DER DRITTE AUSGANG. Eine Maschine, die nicht startet, ist nicht
"in Ordnung" -- sie ist nicht nachgesehen. Das muss dastehen und
nicht in einem gruenen Haken verschwinden. */
nichtNachgesehen.push(`${wie}: ${String(f?.message || f).slice(0, 80)}`);
melde(` ???? ${wie.padEnd(30)} NICHT NACHGESEHEN — ${String(f?.message || f).slice(0, 60)}`);
continue;
}
maschinenGelaufen++;
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text().slice(0, 90)); });
seite.on("pageerror", (e) => konsole.push("pageerror: " + String(e.message).slice(0, 90)));
const details = [];
let schlecht = 0;
/* Erst einmal: Kommt man ueberhaupt hinein? */
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
await seite.click('.rolle[data-rolle="admin"]');
await seite.fill("#code", "CODE-DOGI-0001");
await seite.click("button[type=submit]");
let drin = true;
try {
await seite.waitForURL("**/start.html", { timeout: 25000 });
/* DIE SEITE ERST FERTIG LADEN LASSEN, bevor der Rundgang beginnt.
`waitForURL` wartet nur darauf, dass sich die ADRESSE aendert --
start.html holt danach noch ihre Daten (/api/ich, /api/personen,
/api/hinweise). Ging der Rundgang sofort weiter, brach das
naechste `goto` diese laufenden Anfragen ab, und WebKit meldet
einen Abbruch als "pageerror: … due to access control checks".
Das sah nach einem Sicherheitsproblem in Safari aus und war der
Abbruch durch die Pruefung selbst -- in Ruhe nachgemessen: null
Ereignisse, alle 18 Kacheln da. Chromium und Firefox schweigen
bei einem Abbruch, WebKit nicht; deshalb fiel es nur dort auf. */
await seite.waitForLoadState("networkidle").catch(() => {});
await seite.waitForTimeout(400);
} catch {
drin = false;
details.push(` ANMELDUNG GESCHEITERT (${konsole.slice(0, 2).join(" | ") || "ohne Konsolenfehler"})`);
schlecht++;
}
if (drin) {
for (const datei of SEITEN) {
konsole.length = 0;
await seite.goto(BASIS + "/workspace/" + datei, { waitUntil: "networkidle" });
await seite.waitForTimeout(400);
const r = await seite.evaluate(messen);
seitenGesamt++;
elementeGesamt += r.elemente;
const m = [];
if (r.inhalt < 40) m.push(`fast kein Inhalt (${r.inhalt})`);
if (r.bedienbar < 3) m.push(`fast nichts bedienbar (${r.bedienbar})`);
if (r.ueber > 0) m.push(`${r.ueber}px Ueberstand [${r.schuld.join(" | ")}]`);
if (konsole.length) m.push(`Konsole: ${konsole.slice(0, 2).join(" | ")}`);
if (m.length) { schlecht++; details.push(` ${datei.padEnd(22)} ${m.join("; ")}`); }
}
}
const gut = schlecht === 0;
if (!gut) fehler++;
melde(` ${gut ? "ok " : "FEHL"} ${wie.padEnd(30)} ${drin ? SEITEN.length : 0} Seiten`
+ (gut ? "" : ` — ${schlecht} beanstandet`));
for (const z of details.slice(0, 8)) melde(z);
await kontext.close();
await browser.close();
}
console.log(`\n=== Umfang ===`);
ok(maschinenGelaufen === MASCHINEN.length,
`${maschinenGelaufen} von ${MASCHINEN.length} Maschinen wirklich gestartet`);
ok(nichtNachgesehen.length === 0,
nichtNachgesehen.length ? `nicht nachgesehen: ${nichtNachgesehen.join(" · ")}`
: "jede Maschine konnte gestartet werden");
ok(seitenGesamt >= SEITEN.length * 3, `${seitenGesamt} Seitenaufrufe ueber alle Maschinen`);
ok(elementeGesamt > 5000, `${elementeGesamt} Elemente vermessen`);
/* =======================================================================
GEGENPROBE — findet die Messung in JEDER Maschine etwas?
======================================================================= */
console.log(`\n=== Gegenprobe (muss in jeder Maschine anschlagen) ===`);
for (const [motor, wie] of [["chromium", "Chromium"], ["firefox", "Firefox"], ["webkit", "WebKit"]]) {
let browser;
try { browser = await pw[motor].launch(); } catch { continue; }
const kontext = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const seite = await kontext.newPage();
const konsole = [];
seite.on("pageerror", (e) => konsole.push(String(e.message).slice(0, 60)));
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
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(600);
const vorher = await seite.evaluate(messen);
ok(vorher.ueber === 0 && vorher.inhalt > 40,
`${wie}: die Startseite ist unauffaellig (${vorher.ueber}px, ${vorher.inhalt} Zeichen)`);
await seite.evaluate(() => {
const k = document.createElement("div");
k.style.cssText = "width:3000px;height:24px;background:#333";
document.body.append(k);
});
await seite.waitForTimeout(220);
const nachher = await seite.evaluate(messen);
ok(nachher.ueber > 0, `${wie}: zu breites Element wird gefunden (${nachher.ueber}px)`);
/* Und: Wird ein echter Skriptfehler gemeldet? */
konsole.length = 0;
await seite.evaluate(() => { setTimeout(() => { throw new Error("Absichtlicher Testfehler"); }, 0); });
await seite.waitForTimeout(400);
ok(konsole.some((z) => z.includes("Absichtlicher Testfehler")),
`${wie}: ein Skriptfehler wird bemerkt (${konsole.slice(0, 1).join("") || "nichts"})`);
await kontext.close();
await browser.close();
}
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);