Filipe, mit zwei Bildschirmfotos: "die kachel auf screen 2 soll zwischen
den kacheln auf screen3 sein bitte."
ZWEITER FEHLER, DEN ER NICHT GEMELDET HAT -- und der groessere: Die
Support-Kachel hatte GAR KEINE Gruppe. Sie stand deshalb bei JEDER
Rolle in einem eigenen Abschnitt OHNE UEBERSCHRIFT, allein, mit einem
Aufklapp-Kopf, auf dem nur "1" stand. Gebaut habe ich das heute frueh;
auf seinem Bildschirmfoto war es zu sehen, und mir ist es nicht
aufgefallen, weil ich auf die Kachel geschaut habe und nicht auf das,
was um sie herum steht.
Jetzt gehoert sie zu "Fuer dich" und wird VOR den vertraulichen
Meldeweg eingeschoben statt ans Ende gehaengt. Gesucht wird der NACHBAR
ueber sein Ziel, nicht eine Position -- eine feste Zahl waere beim
naechsten Umbau still falsch. Findet sich der Nachbar nicht (ein Modi
hat den Meldeweg nicht), steht sie am Ende der Gruppe.
GEMESSEN, NICHT GERECHNET (1280 px, angemeldet, je Rolle):
admin/hand Fuer dich: Steckbrief | Wissen | Personen & Zugaenge
Wie geht's dir? | Wer sieht was | Support
Vertraulich melden
modi Fuer dich: Steckbrief | Wissen | Wie geht's dir?
Support
gast Fuer dich: Support | Vertraulich melden | Steckbrief
Bei DogFather und der rechten Hand bricht "Vertraulich melden" in eine
eigene Zeile um: Die Gruppe hat jetzt sieben Kacheln, das Raster drei
Spalten, und sieben geht durch drei nicht auf. Die Luecke faellt ans
ENDE -- das ist die bessere der beiden Moeglichkeiten und dieselbe
Regel, die im Kommentar zur Treff-Reihe steht: "eine Luecke in der
Mitte sieht kaputt aus, eine am Ende sieht grosszuegig aus."
NEU: server/mess-kachelreihen.mjs. Die Reihenfolge im Quelltext ist
NICHT die auf dem Bildschirm -- "Willkommen" belegt zwei Spalten,
gruppiert wird nach dem ersten Auftreten einer Gruppe, und eine Kachel
ohne `gruppe` bekommt einen eigenen namenlosen Abschnitt. Genau daran
ist dieser Fehler entstanden. Die Datei misst es im Browser, je Rolle,
und meldet einen Abschnitt ohne Ueberschrift ausdruecklich als Befund.
Sie prueft nichts.
Gemessen: pruef-support 45, pruef-unterstuetzung 70,
pruef-start-ansicht -- gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
247 lines
11 KiB
JavaScript
247 lines
11 KiB
JavaScript
/* =====================================================================
|
|
WIE STEHEN DIE KACHELN — je Rolle, Gruppe für Gruppe (24.09.2026)
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM ES DIESE DATEI GIBT
|
|
|
|
Filipe schickt seine Wünsche zur Startseite fast immer als
|
|
Bildschirmfoto: „die kachel auf screen 2 soll zwischen den kacheln
|
|
auf screen3 sein", „wechsel bitte die reihenfolge, links soll
|
|
unterstützen sein". Beides sind Fragen nach einer REIHE, nicht nach
|
|
einer Liste.
|
|
|
|
Und genau da liegt die Falle: Die Reihenfolge im Quelltext ist NICHT
|
|
die Reihenfolge auf dem Bildschirm.
|
|
|
|
* Das Raster hat drei Spalten, „Willkommen" belegt zwei — alles
|
|
danach verschiebt sich um eins.
|
|
* Gruppiert wird nach dem ERSTEN Auftreten einer Gruppe im Feld;
|
|
eine Kachel am Ende der Liste kann dadurch mitten auf der Seite
|
|
landen.
|
|
* Und eine Kachel ohne `gruppe` bekommt einen eigenen Abschnitt
|
|
ohne Überschrift — genau so stand der Support einen halben Tag
|
|
lang allein in einem namenlosen Kasten, und beim Lesen des
|
|
Quelltextes sieht man das nicht.
|
|
|
|
Wer das im Kopf ausrechnet, rechnet früher oder später falsch.
|
|
Deshalb wird es hier GEMESSEN: angemeldet, im Browser, je Rolle.
|
|
|
|
Sie prüft NICHTS und meldet keinen Fehlschlag. Sie beantwortet eine
|
|
Frage, die man vor jeder Umsortierung stellen muss.
|
|
|
|
Sie heißt `mess-` und nicht `pruef-`: Die Portvergabe leitet sich
|
|
aus den `pruef-`-Namen ab, und eine Messdatei darf die Nummern der
|
|
Prüfungen nicht verschieben.
|
|
|
|
Aufrufen mit: node server/mess-kachelreihen.mjs
|
|
Nur eine Rolle: ROLLE=modi node server/mess-kachelreihen.mjs
|
|
Bild: SCHIRM=1 node server/mess-kachelreihen.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { createServer as httpsServer } from "node:https";
|
|
|
|
const PORT = 5387, HP = 5388;
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-kachelreihen-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
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 });
|
|
};
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(240_000, "mess-kachelreihen");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const sch = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", sch, jetzt);
|
|
|
|
const anlegen = (name, rolle, code) => {
|
|
const s = randomBytes(16).toString("hex");
|
|
const h = scryptSync(code, s, 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, code_kennung, aktiv, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, h, s, 32768,
|
|
createHmac("sha256", sch).update(code).digest("hex"), jetzt);
|
|
};
|
|
/* Die fünf Rollen, die auf der Crew-Adresse eine Startseite haben. */
|
|
const ROLLEN = [
|
|
["Filipe", "admin", "CODE-DOGI-0001"],
|
|
["VanVan", "hand", "CODE-HAND-0001"],
|
|
["Kessi", "linke", "CODE-LINKE-0001"],
|
|
["Diene", "modi", "CODE-MODI-0001"],
|
|
["Zuschauer", "gast", "CODE-GAST-0001"],
|
|
];
|
|
for (const [name, rolle, code] of ROLLEN) anlegen(name, rolle, code);
|
|
d.close();
|
|
|
|
/* ÜBER DIE CREW-ADRESSE, nicht über 127.0.0.1: Welches Haus gemeint
|
|
ist, leitet der Server aus dem `Host`-Kopf ab. Über die Zahladresse
|
|
landet man im Agenturhaus, und dort gibt es weder `gast` noch
|
|
`modi` — man misst dann eine Seite, die niemand so sieht. */
|
|
const anfrage = (pfad, art, koerper) => new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad, method: art,
|
|
headers: { Host: CREW, "Content-Type": "application/json" },
|
|
}, (r) => {
|
|
let t = ""; r.on("data", (x) => { t += x; });
|
|
r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t }));
|
|
});
|
|
a.on("error", schief);
|
|
if (koerper) a.write(JSON.stringify(koerper));
|
|
a.end();
|
|
});
|
|
|
|
const kekse = {};
|
|
for (const [, rolle, code] of ROLLEN) {
|
|
const an = await anfrage("/workspace/api/anmelden", "POST",
|
|
rolle === "gast" ? { rolle, code, alter_ok: true } : { rolle, code });
|
|
if (an.code !== 200) {
|
|
console.log(` ABBRUCH: ${rolle} ließ sich nicht anmelden (HTTP ${an.code}). `
|
|
+ "Ohne Anmeldung gibt es keine Startseite zu messen.");
|
|
process.exit(2);
|
|
}
|
|
kekse[rolle] = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
|
|
}
|
|
|
|
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl,
|
|
"-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
|
|
{ stdio: "ignore" });
|
|
const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) },
|
|
(von, nach) => {
|
|
const w = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
|
headers: { ...von.headers, host: `${CREW}:${HP}` },
|
|
}, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
|
|
von.pipe(w);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
|
|
|
|
const pw = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
|
|
});
|
|
try {
|
|
const nur = process.env.ROLLE;
|
|
for (const [, rolle] of ROLLEN) {
|
|
if (nur && nur !== rolle) continue;
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1800 } });
|
|
await kontext.addCookies(kekse[rolle].split("; ").map((k) => {
|
|
const [n, ...r] = k.split("=");
|
|
return { name: n, value: r.join("="), domain: CREW, path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (e) => {
|
|
if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(e.text())) {
|
|
konsole.push(e.text().slice(0, 110));
|
|
}
|
|
});
|
|
await seite.goto(`https://${CREW}:${HP}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(800);
|
|
|
|
const gruppen = await seite.evaluate(() =>
|
|
[...document.querySelectorAll("section")].map((s) => {
|
|
const titel = s.querySelector("h2, .gruppe__titel")?.textContent?.trim() || "";
|
|
const k = [...s.querySelectorAll(".kachel")].map((x) => {
|
|
const r = x.getBoundingClientRect();
|
|
return {
|
|
n: x.querySelector(".kachel__name")?.textContent,
|
|
x: Math.round(r.x), y: Math.round(r.y), b: Math.round(r.width),
|
|
};
|
|
});
|
|
return k.length ? { titel, k } : null;
|
|
}).filter(Boolean));
|
|
|
|
console.log("");
|
|
console.log(`=== ${rolle} ===`);
|
|
for (const g of gruppen) {
|
|
/* NACH ZEILEN SORTIEREN, nicht nach Feldreihenfolge -- das ist ja
|
|
gerade die Frage. Gerundet auf 20 px, damit zwei Kacheln
|
|
derselben Zeile mit leicht verschiedener Oberkante
|
|
zusammenbleiben. */
|
|
const zeilen = new Map();
|
|
for (const k of g.k) {
|
|
const y = Math.round(k.y / 20) * 20;
|
|
if (!zeilen.has(y)) zeilen.set(y, []);
|
|
zeilen.get(y).push(k);
|
|
}
|
|
const sortiert = [...zeilen.entries()].sort((a, b) => a[0] - b[0]);
|
|
const name = g.titel || "!! OHNE ÜBERSCHRIFT !!";
|
|
console.log(` [${name}]`);
|
|
for (const [, ks] of sortiert) {
|
|
console.log(" " + ks.sort((a, b) => a.x - b.x)
|
|
.map((k) => `${k.n} (${k.b}px)`).join(" | "));
|
|
}
|
|
}
|
|
/* EIN ABSCHNITT OHNE ÜBERSCHRIFT IST EIN BEFUND, kein Zufall:
|
|
Er entsteht, wenn eine Kachel keine `gruppe` hat. */
|
|
const ohne = gruppen.filter((g) => !g.titel);
|
|
if (ohne.length) {
|
|
console.log(` !! ${ohne.length} Abschnitt(e) OHNE Überschrift: `
|
|
+ ohne.map((g) => g.k.map((k) => k.n).join(", ")).join(" / "));
|
|
}
|
|
if (konsole.length) console.log(" !! Konsole: " + konsole[0]);
|
|
if (process.env.SCHIRM) {
|
|
/* MIT `GRUPPE="Für dich"` nur diesen einen Abschnitt -- ein Bild
|
|
der ganzen Seite ist 1280 mal viertausend Pixel, und darauf
|
|
sucht man die drei Kacheln, um die es gerade geht. */
|
|
const nurGruppe = process.env.GRUPPE;
|
|
const window0 = await seite.evaluate(() => window.scrollY);
|
|
const feld = nurGruppe ? await seite.evaluate((titel) => {
|
|
const s2 = [...document.querySelectorAll("section")]
|
|
.find((x) => (x.querySelector("h2, .gruppe__titel")?.textContent || "").trim() === titel);
|
|
return s2 ? s2.getBoundingClientRect().toJSON() : null;
|
|
}, nurGruppe) : null;
|
|
const datei = `server/mess-kachelreihen-${rolle}.png`;
|
|
try {
|
|
if (feld) {
|
|
/* MIT `fullPage`, auch beim Ausschnitt: Die Gruppe liegt
|
|
meist unterhalb des Fensters, und ohne fullPage kennt
|
|
Playwright dort keine Koordinaten -- der Aufruf scheitert.
|
|
Gemessen am 24.09.2026, als genau das passierte. */
|
|
await seite.screenshot({ path: datei, fullPage: true, clip: {
|
|
x: Math.max(0, feld.x - 10), y: Math.max(0, feld.y + window0 - 10),
|
|
width: Math.min(1280, feld.width + 20), height: feld.height + 20 } });
|
|
} else {
|
|
await seite.screenshot({ path: datei, fullPage: true });
|
|
}
|
|
console.log(` -- Bild: ${datei}`);
|
|
} catch (f) {
|
|
/* NICHT STILL SCHEITERN. Ein Lauf, der erfolgreich aussieht und
|
|
kein Bild hinterlaesst, kostet drei Wiederholungen. */
|
|
console.log(` !! Bild ging nicht: ${String(f?.message || f).slice(0, 120)}`);
|
|
}
|
|
}
|
|
await kontext.close();
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
try { vorbau.close(); } catch { /* egal */ }
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(0);
|
|
}
|