Filipe, 22.09.2026: "ersetze die zentrale durch, Die IrrenAnstalt.
genau so geschrieben bitte. und alles was treff heißt oder wo treff
steht soll durch Rudel ersetzt werden bitte."
Die Schreibweise "IrrenAnstalt" mit grossem A in der Mitte ist so
gewollt. Das steht als Hinweis daneben, damit sie beim naechsten Mal
niemand "korrigiert".
WAS UMBENANNT WURDE: alles, was jemand LIEST -- Kachelnamen,
Ueberschriften, Markenzeilen, Saetze, Meldungen, Aufgabenvorlagen.
116 Zeilen in 42 Dateien.
WAS BLEIBT: Adressen (treff-regeln.html), Bezeichner (TREFF_ROLLEN),
Datenbankwerte (bereich = 'treff'), Abfrageteile (b=treff). Eine
Adresse umzubenennen bricht jedes Lesezeichen, und ein Datenbankwert
umzuschreiben waere eine Umstellung ohne Gegenwert. Dieselbe
Entscheidung wie heute frueh bei Dogi-Media, wo material.html auch
material.html geblieben ist.
DREI GRAMMATIKFEHLER IM EIGENEN ENTWURF, alle vor dem Ausliefern
gefunden -- ein blindes Ersetzen reicht hier nicht:
1. "der Treff" ist maennlich, "das Rudel" saechlich. Ohne Tabelle
waere ueberall "Der Rudel" gestanden. (Und "Treffer" waere zu
"Rudeler" geworden, "Treffen" zu "Rudelen" -- deshalb greift die
Regel nur bei Wortgrenze und nie vor einem Kleinbuchstaben.)
2. BINDESTRICH-ZUSAMMENSETZUNGEN. In "der Treff-Chat" gehoert der
Artikel zu "Chat", nicht zu "Treff". Der erste Durchlauf machte
daraus "das Rudel-Chat", "das Rudel-Kacheln" und "ein Rudel-Raum".
Jetzt greift die Artikelregel nur, wenn das Wort allein steht.
3. GESCHUETZTE LEERZEICHEN. In den Markenzeilen steht
`Der Treff`, damit die zwei Woerter nicht umbrechen. Das
Muster hat daran vorbeigegriffen: "Der Rudel", auf fuenf
Seiten. Jetzt wird das Trennzeichen mitgefasst und unveraendert
wieder eingesetzt.
Gefunden wurden alle drei, weil jede einzelne der 116 Zeilen vor dem
Schreiben als ALT/NEU ausgegeben und gelesen wurde -- und danach
gezielt nach falschen Artikeln gesucht ("den Rudel", "der Rudel",
"einen Rudel"). Uebrig blieben sechs Treffer, und die sind alle
richtig: "einen Rudel-Raum", "der Rudel-Chat", "den Rudel-Regeln" --
Zusammensetzungen, bei denen der Artikel zum letzten Wort gehoert.
UND ZWEI PRUEFUNGEN, die die Umbenennung nicht mitbekommen haetten:
`/Treff/.test(...)` und `/Treff/i.test(...)`. Ein regulaerer Ausdruck
ist keine Zeichenkette -- sie haetten ab sofort nach einem Wort
gesucht, das die Seite nicht mehr sagt, und waeren still rot geworden,
ohne dass etwas kaputt ist.
Geprueft, alle gruen: pruef-treff, pruef-treffchat,
pruef-deutsche-texte, pruef-uebernahme, pruef-crew-adresse,
pruef-willkommen, pruef-wege-nach-draussen, pruef-vorlagen,
pruef-start-ansicht, pruef-struktur.
Co-Authored-By: Claude Opus 5 <[email protected]>
463 lines
21 KiB
JavaScript
463 lines
21 KiB
JavaScript
/* =====================================================================
|
|
pruef-willkommen.mjs — DIE SEITE, DIE ERKLAERT, WAS ES HIER GIBT
|
|
=====================================================================
|
|
|
|
Auftrag vom 21.09.2026, Abschnitte 7 und 8.
|
|
|
|
WAS HIER GEMESSEN WIRD — und warum gerade das:
|
|
|
|
(1) DASS DIE ERKLAERUNG ZU DEN RECHTEN PASST. Abschnitt 8 verlangt
|
|
woertlich, die Erklaerung solle sich "automatisch an die
|
|
tatsaechlich vorhandenen Rechte" anpassen. Der Fehler, den man
|
|
dabei macht, ist eine gepflegte Liste: Sie stimmt am ersten Tag
|
|
und erzaehlt am zweiten von einer Seite, die es fuer diesen
|
|
Menschen nie gab. Geprueft wird deshalb nicht "steht da etwas",
|
|
sondern: JEDE erklaerte Kachel muss diese Person auch oeffnen
|
|
duerfen — und zwar gemessen an derselben Rechtetafel, die der
|
|
Server durchsetzt.
|
|
|
|
(2) DIE GEGENPROBE DAZU. Eine Pruefung, die nur "alles erlaubt"
|
|
bestaetigt, bestaetigt nichts. Also wird ausdruecklich gezaehlt,
|
|
dass ein Modi die Seiten, die er NICHT hat (Personen, Hilfe,
|
|
Bewerbungen), auch nicht erklaert bekommt — und dass DogFather
|
|
mehr erklaert bekommt als er.
|
|
|
|
(3) DASS DIE ROLLEN WIRKLICH VERSCHIEDENE TEXTE BEKOMMEN. "Sie muss
|
|
vollstaendig rollenbasiert aufgebaut werden" ist nur erfuellt,
|
|
wenn sich die Texte auch unterscheiden. Vier gleiche Absaetze
|
|
mit vier verschiedenen Namen davor waeren keine Rollenseite.
|
|
|
|
(4) DASS DIE AUSGELIEFERTE DATEI KEINE ROLLENNAMEN TRAEGT. Sie geht
|
|
an jeden Browser. Steht die Aufteilung dort, ist sie lesbar und
|
|
veraltet ausserdem. Dieselbe Regel wie bei pruef-modi-wortleck.
|
|
|
|
(5) UND AM ECHTEN BILDSCHIRM, dass ein Mensch es lesen und antippen
|
|
kann. Ein richtiger Datensatz, den niemand sieht, ist kein
|
|
Erfolg.
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import http from "node:http";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-willk-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Willkommenspruefung");
|
|
|
|
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");
|
|
notbremse(300_000, "pruef-willkommen");
|
|
await new Promise((r) => setTimeout(r, 1200));
|
|
|
|
let fehler = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const HOST = "crew.dogfather-universe.com";
|
|
function rufe(pfad, wie = {}) {
|
|
return new Promise((fertig) => {
|
|
const daten = wie.body === undefined ? null : JSON.stringify(wie.body);
|
|
const a = http.request({
|
|
host: "127.0.0.1", port: PORT, path: pfad, method: wie.method || "GET",
|
|
headers: {
|
|
Host: HOST,
|
|
...(daten ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) } : {}),
|
|
...(wie.keks ? { Cookie: wie.keks } : {}),
|
|
Origin: `https://${HOST}`, Referer: `https://${HOST}/workspace/`,
|
|
},
|
|
}, (a2) => {
|
|
let t = "";
|
|
a2.on("data", (c) => (t += c));
|
|
a2.on("end", () => fertig({
|
|
code: a2.statusCode, kopf: a2.headers, text: t,
|
|
json: (() => { try { return JSON.parse(t); } catch { return null; } })(),
|
|
}));
|
|
});
|
|
a.on("error", (e) => fertig({ code: 0, text: e.message, json: null }));
|
|
if (daten) a.write(daten);
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
await rufe("/workspace/api/anmelden", { method: "POST", body: { rolle: "admin", code: "x" } });
|
|
for (let i = 0; i < 60; i++) {
|
|
try {
|
|
const t = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { t.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { t.close(); }
|
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
}
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
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;
|
|
}
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Funny", "hand", "CODE-HAND-0001");
|
|
anlegen("Lino", "linke", "CODE-LINK-0001");
|
|
anlegen("Kessi", "modi", "CODE-MOD1-0001");
|
|
/* Ein Gast muss sein Alter bestaetigt haben, sonst kommt er nicht durch. */
|
|
const idGast = anlegen("Robin", "gast", "CODE-GAST-0001");
|
|
d.prepare("UPDATE personen SET alter_bestaetigt_am = ? WHERE id = ?").run(jetzt, idGast);
|
|
d.close();
|
|
|
|
async function alsWer(rolle, code) {
|
|
const a = await rufe("/workspace/api/anmelden", { method: "POST", body: { rolle, code } });
|
|
return (a.kopf?.["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
1. DIE SCHRANKE
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Die Schranke ===");
|
|
{
|
|
const ohne = await rufe("/workspace/api/willkommen");
|
|
ok(ohne.code === 401, `unangemeldet gibt es nichts (${ohne.code})`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
2. JEDE ROLLE BEKOMMT IHRE EIGENE SEITE
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Jede Rolle bekommt ihre eigene Seite ===");
|
|
const LEUTE = [
|
|
{ rolle: "admin", code: "CODE-DOGI-0001", name: "Filipe" },
|
|
{ rolle: "hand", code: "CODE-HAND-0001", name: "Funny" },
|
|
{ rolle: "linke", code: "CODE-LINK-0001", name: "Lino" },
|
|
{ rolle: "modi", code: "CODE-MOD1-0001", name: "Kessi" },
|
|
{ rolle: "gast", code: "CODE-GAST-0001", name: "Robin" },
|
|
];
|
|
const gesehen = {};
|
|
for (const p of LEUTE) {
|
|
const keks = await alsWer(p.rolle, p.code);
|
|
const a = await rufe("/workspace/api/willkommen", { keks });
|
|
gesehen[p.rolle] = { keks, daten: a.json };
|
|
ok(a.code === 200, `${p.rolle}: die Seite antwortet (${a.code})`);
|
|
ok(a.json?.gruss === `Willkommen, ${p.name}!`,
|
|
` und begruesst mit Namen ("${a.json?.gruss}")`);
|
|
ok(!!a.json?.einleitung?.was && !!a.json?.einleitung?.dazu,
|
|
" mit einer Einleitung, die aus zwei Teilen besteht");
|
|
}
|
|
|
|
/* DIE EINLEITUNGEN MUESSEN SICH UNTERSCHEIDEN. Sonst ist es keine
|
|
rollenbasierte Seite, sondern eine mit Namensfeld. */
|
|
{
|
|
const saetze = LEUTE.map((p) => gesehen[p.rolle]?.daten?.einleitung?.was);
|
|
const verschieden = new Set(saetze).size;
|
|
ok(verschieden === LEUTE.length,
|
|
`${verschieden} von ${LEUTE.length} Einleitungen sind verschieden`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
3. DER KERN: ERKLAERT WIRD NUR, WAS MAN AUCH DARF
|
|
---------------------------------------------------------------------
|
|
|
|
Gemessen gegen die Rechtetafel selbst, nicht gegen eine Liste in
|
|
dieser Datei -- sonst pruefe ich meine Abschrift gegen seine. */
|
|
melde("\n=== Erklaert wird nur, was man auch oeffnen darf ===");
|
|
const { darfSeite } = await import("./rechte.js");
|
|
const { bereicheFuer, zusatzBereicheFuer } = await import("./workspace.js");
|
|
const { istCrewAdresse } = await import("./crew-adresse.js");
|
|
|
|
/* DAS HAUS GEHOERT DAZU, und es wird ABGELEITET wie beim Server.
|
|
Gemessen am 21.09.: DogFather hat auf der Crew-Adresse 30 Kacheln
|
|
und auf der Agenturadresse 1 (die Tuer hinueber) -- er ist die
|
|
einzige Rolle, die in beiden Haeusern lebt. Eine Vergleichsrechnung
|
|
ohne Haus misst deshalb ausgerechnet bei ihm das falsche Haus und
|
|
meldet einen Fehler, den es nicht gibt. */
|
|
const HAUS = istCrewAdresse(HOST) ? "crew" : "agentur";
|
|
const alsPerson = (rolle) => ({ rolle, haus: HAUS });
|
|
|
|
for (const p of LEUTE) {
|
|
const daten = gesehen[p.rolle]?.daten;
|
|
const kacheln = (daten?.gruppen || []).flatMap((g) => g.kacheln || []);
|
|
ok(kacheln.length > 0, `${p.rolle}: es wird ueberhaupt etwas erklaert (${kacheln.length})`);
|
|
|
|
const verboten = kacheln.filter((k) => {
|
|
const ziel = String(k.ziel || "").split("?")[0];
|
|
if (!ziel || /^https?:/.test(ziel)) return false;
|
|
return !darfSeite(alsPerson(p.rolle), "/workspace/" + ziel);
|
|
});
|
|
ok(verboten.length === 0, verboten.length === 0
|
|
? " keine einzige davon ist fuer ihn gesperrt"
|
|
: ` ERKLAERT GESPERRTE SEITEN: ${verboten.map((v) => v.ziel).join(", ")}`);
|
|
|
|
/* Und die andere Richtung: die Zahl muss zu seinen echten Kacheln
|
|
passen. Zu WENIG waere genauso falsch -- dann verschweigt die
|
|
Seite etwas, das er hat. */
|
|
const roh = [
|
|
...(bereicheFuer(alsPerson(p.rolle)) || []),
|
|
...(zusatzBereicheFuer(alsPerson(p.rolle)) || []),
|
|
];
|
|
const sollte = roh.filter((k) => {
|
|
if (k?.aussen) return true;
|
|
const ziel = String(k?.ziel || "");
|
|
if (!ziel) return false;
|
|
return darfSeite(alsPerson(p.rolle), "/workspace/" + ziel.split("?")[0]);
|
|
}).length;
|
|
ok(kacheln.length === sollte,
|
|
` und es sind genau seine ${sollte} Kacheln (erklaert: ${kacheln.length})`);
|
|
}
|
|
|
|
/* GEGENPROBE MIT ZAHLEN: Kann die Pruefung ueberhaupt Unterschiede
|
|
sehen? Wenn alle gleich viel bekaemen, waere oben nichts bewiesen. */
|
|
{
|
|
const zahl = (r) => (gesehen[r]?.daten?.gruppen || []).flatMap((g) => g.kacheln || []).length;
|
|
const chef = zahl("admin"), modi = zahl("modi"), gast = zahl("gast");
|
|
ok(chef > modi && modi > gast,
|
|
`DogFather ${chef} > Modi ${modi} > Community ${gast} -- die Seite unterscheidet wirklich`);
|
|
|
|
/* DIE DREI, DIE EIN MODI NICHT HAT. Namentlich, damit ein stiller
|
|
Durchrutscher auffaellt. */
|
|
const modiZiele = (gesehen.modi?.daten?.gruppen || [])
|
|
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
|
for (const seite of ["personen.html", "bewerbungen.html", "rechte.html"]) {
|
|
ok(!modiZiele.includes(seite), `einem Modi wird "${seite}" nicht erklaert`);
|
|
}
|
|
/* Und der Gegenbeweis, dass diese Namen ueberhaupt vorkommen koennen: */
|
|
const chefZiele = (gesehen.admin?.daten?.gruppen || [])
|
|
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
|
ok(chefZiele.includes("personen.html"),
|
|
"DogFather dagegen schon (sonst waere die Probe oben wertlos)");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
4. DIE LINKE HAND BEKOMMT IHRE GRENZEN GESAGT
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Niemand erfaehrt, was er NICHT hat ===");
|
|
{
|
|
/* UMGEDREHT AM 22.09.2026. Hier stand: "ihr wird gesagt, was sie
|
|
nicht hat -- einmal, klar, ohne Liste."
|
|
|
|
Filipe: "keiner soll wissen was er nicht sieht das geht keinen was
|
|
an. sie sehen die sachen ja nicht weil es sie nichts angeht und das
|
|
muss auch nicht erwaehnt werden. kontrollier das ueberall bitte."
|
|
|
|
Das "ueberall" ist der Grund, warum hier nicht eine Zeile
|
|
umgedreht, sondern JEDE ROLLE gemessen wird. Ein Satz, der
|
|
aufzaehlt, was fehlt, hebt ausserdem die Sorgfalt wieder auf, mit
|
|
der der Server ueberall 404 statt 403 antwortet -- damit ein "das
|
|
darfst du nicht" gar nicht erst verraet, dass es etwas gibt. */
|
|
const VERRAETERISCH = [
|
|
/nicht f(u|ue|\u00fc)r dich freigeschaltet/i,
|
|
/was du nicht siehst/i,
|
|
/was du nicht hast/i,
|
|
/hast du nicht/i,
|
|
/siehst du nicht/i,
|
|
/darfst du nicht/i,
|
|
/bleiben seine/i,
|
|
/kein zugriff/i,
|
|
];
|
|
const gefunden = [];
|
|
for (const pp of LEUTE) {
|
|
const d = gesehen[pp.rolle]?.daten;
|
|
const text = [
|
|
d?.einleitung?.was, d?.einleitung?.dazu,
|
|
...(d?.hinweise || []).flatMap((h) => [h.titel, h.text]),
|
|
].filter(Boolean).join(" ");
|
|
for (const m of VERRAETERISCH) {
|
|
if (m.test(text)) gefunden.push(pp.rolle + ": " + m.source);
|
|
}
|
|
}
|
|
ok(gefunden.length === 0, gefunden.length === 0
|
|
? `keine der ${LEUTE.length} Rollen bekommt gesagt, was ihr fehlt`
|
|
: `ZAEHLT AUF, WAS FEHLT: ${gefunden.join(" | ")}`);
|
|
|
|
/* GEGENPROBE: Wuerde dieses Muster so einen Satz ueberhaupt finden?
|
|
Ohne sie waere die Zeile oben gruen, sobald die Liste kaputt ist. */
|
|
ok(VERRAETERISCH.some((m) => m.test("Das ist nicht fuer dich freigeschaltet.")),
|
|
"und ein solcher Satz wuerde hier wirklich auffallen");
|
|
const ziele = (gesehen.linke?.daten?.gruppen || [])
|
|
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
|
ok(!ziele.includes("personen.html") && !ziele.includes("hilfe.html"),
|
|
"und Personen/Meldeweg stehen nicht in ihrer Erklaerung");
|
|
ok(ziele.includes("aufgaben.html"),
|
|
"die Aufgaben dagegen schon");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
5. DIE AUSGELIEFERTE DATEI TRAEGT KEINE ROLLENAUFTEILUNG
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Was im Browser landet ===");
|
|
{
|
|
const js = readFileSync(new URL("../workspace/assets/js/willkommen.js", import.meta.url), "utf8");
|
|
const html = readFileSync(new URL("../workspace/willkommen.html", import.meta.url), "utf8");
|
|
const ohneKommentar = js
|
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
|
|
for (const wort of ["admin", "linke", "gast"]) {
|
|
const treffer = new RegExp("[\"'`]" + wort + "[\"'`]").test(ohneKommentar);
|
|
ok(!treffer, `die Datei kennt "${wort}" nicht (sie kommt vom Server)`);
|
|
}
|
|
ok(!/aufgaben\.html|personen\.html/.test(ohneKommentar),
|
|
"und fuehrt auch keine eigene Kachelliste");
|
|
ok(html.includes("willkommen.css"), "die Seite laedt ihre Gestaltung");
|
|
ok(html.includes("meldung.js"), "und den gemeinsamen Meldeweg");
|
|
|
|
/* GEGENPROBE: Findet dieses Muster ueberhaupt etwas? Sonst waere die
|
|
Aussage oben nur die Bestaetigung eines kaputten Suchmusters. */
|
|
const probe = 'const x = "admin";';
|
|
ok(new RegExp("[\"'`]admin[\"'`]").test(probe),
|
|
"das Suchmuster wuerde einen Rollennamen finden");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
6. DIE KACHEL FUEHRT HIN
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Der Weg zur Seite ===");
|
|
{
|
|
const kacheln = [
|
|
...(bereicheFuer(alsPerson("gast")) || []),
|
|
...(zusatzBereicheFuer(alsPerson("gast")) || []),
|
|
];
|
|
const willk = kacheln.filter((k) => String(k.ziel || "").startsWith("willkommen.html"));
|
|
ok(willk.length === 1, `die Community hat genau eine Kachel dorthin (${willk.length})`);
|
|
ok(willk[0]?.name === "Willkommen", `und sie heisst so (${willk[0]?.name})`);
|
|
|
|
/* DAS TREFF-BRETT DARF NICHT STILL VERSCHWINDEN. Gemessen am
|
|
21.09.: null Eintraege, 29 Chatnachrichten daneben -- geredet wird
|
|
im Chat. Falls das Brett je wieder eine Kachel bekommt, soll diese
|
|
Zeile daran erinnern, dass hier einmal eine war. */
|
|
const brett = kacheln.filter((k) => String(k.ziel || "").includes("b=treff"));
|
|
ok(brett.length === 0,
|
|
`das leere Rudel-Brett hat keine eigene Kachel mehr (${brett.length})`);
|
|
const chat = kacheln.filter((k) => String(k.ziel || "").includes("chat.html"));
|
|
ok(chat.length >= 1, `der Rudel-Chat dagegen schon (${chat.length})`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
7. AM ECHTEN BILDSCHIRM
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Am echten Bildschirm ===");
|
|
{
|
|
const { execFileSync } = await import("node:child_process");
|
|
const { createServer: httpsServer } = await import("node:https");
|
|
const { request: httpAnfrage } = await import("node:http");
|
|
|
|
const schluessel = join(ordner, "pruef.key");
|
|
const zertifikat = join(ordner, "pruef.crt");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
"-keyout", schluessel, "-out", zertifikat, "-days", "2",
|
|
"-subj", `/CN=${HOST}`, "-addext", `subjectAltName=DNS:${HOST}`],
|
|
{ stdio: "ignore" });
|
|
|
|
const HTTPS_PORT = PORT + 1;
|
|
const vorbau = httpsServer({
|
|
key: readFileSync(schluessel), cert: readFileSync(zertifikat),
|
|
}, (von, nach) => {
|
|
const weiter = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
|
headers: { ...von.headers, host: `${HOST}:${HTTPS_PORT}` },
|
|
}, (antwort) => {
|
|
nach.writeHead(antwort.statusCode || 502, antwort.headers);
|
|
antwort.pipe(nach);
|
|
});
|
|
von.pipe(weiter);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
|
const SEITE = `https://${HOST}:${HTTPS_PORT}`;
|
|
|
|
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 ${HOST} 127.0.0.1`, "--ignore-certificate-errors"],
|
|
});
|
|
const seitenFehler = [];
|
|
|
|
async function alsSeite(rolle, code, breite = 1280) {
|
|
const ctx = await browser.newContext({ viewport: { width: breite, height: 950 } });
|
|
const s = await ctx.newPage();
|
|
s.on("pageerror", (e) => seitenFehler.push(`${rolle}: ${String(e).slice(0, 110)}`));
|
|
await s.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" });
|
|
await s.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await s.fill("#code", code);
|
|
await s.click("button[type=submit]");
|
|
await s.waitForTimeout(800);
|
|
await s.goto(`${SEITE}/workspace/willkommen.html`, { waitUntil: "networkidle" });
|
|
await s.waitForTimeout(900);
|
|
return { ctx, seite: s };
|
|
}
|
|
|
|
/* ---- Ein Modi, am Handy ---- */
|
|
const pModi = await alsSeite("modi", "CODE-MOD1-0001", 390);
|
|
try {
|
|
const bild = await pModi.seite.evaluate(() => {
|
|
const kacheln = [...document.querySelectorAll(".w-kachel")];
|
|
const breite = document.documentElement.scrollWidth;
|
|
return {
|
|
gruss: document.getElementById("gruss")?.textContent || "",
|
|
rolle: document.getElementById("rollentext")?.textContent || "",
|
|
was: document.getElementById("einleitung-was")?.textContent || "",
|
|
kacheln: kacheln.length,
|
|
mitLang: kacheln.filter((k) => k.querySelector(".w-kachel__lang")).length,
|
|
laedtWeg: document.getElementById("laedt")?.hidden === true,
|
|
busy: document.getElementById("haupt")?.getAttribute("aria-busy"),
|
|
breite,
|
|
ueberlauf: kacheln.filter((k) => k.getBoundingClientRect().right > breite + 1).length,
|
|
zuKlein: kacheln.filter((k) => k.getBoundingClientRect().height < 44).length,
|
|
ersteZiel: kacheln[0]?.getAttribute("href") || "",
|
|
};
|
|
});
|
|
ok(bild.gruss.includes("Kessi"), `der Name steht oben ("${bild.gruss}")`);
|
|
ok(bild.was.length > 20, "darunter ein Satz, der etwas sagt");
|
|
ok(bild.kacheln > 0, `${bild.kacheln} Kacheln werden erklaert`);
|
|
ok(bild.mitLang > 0, `davon ${bild.mitLang} mit ausfuehrlichem Satz`);
|
|
ok(bild.laedtWeg, "der Ladehinweis ist weg");
|
|
ok(bild.busy !== "true", "und die Seite meldet sich nicht mehr als ladend");
|
|
ok(bild.breite <= 390, `nichts sprengt das Handy (${bild.breite}px)`);
|
|
ok(bild.ueberlauf === 0, `keine Kachel ragt hinaus (${bild.ueberlauf})`);
|
|
ok(bild.zuKlein === 0, `jede ist mindestens 44px hoch (${bild.zuKlein} zu klein)`);
|
|
|
|
/* SIE MUSS AUCH HINFUEHREN. Eine Erklaerung ohne Weg ist ein
|
|
Prospekt. */
|
|
await pModi.seite.click(".w-kachel");
|
|
await pModi.seite.waitForTimeout(700);
|
|
const wohin = pModi.seite.url();
|
|
ok(!wohin.includes("willkommen.html"),
|
|
`ein Antippen fuehrt wirklich hin (${wohin.split("/").pop()})`);
|
|
} finally { await pModi.ctx.close(); }
|
|
|
|
/* ---- DogFather, am Rechner ---- */
|
|
const pChef = await alsSeite("admin", "CODE-DOGI-0001");
|
|
try {
|
|
const bild = await pChef.seite.evaluate(() => ({
|
|
gruss: document.getElementById("gruss")?.textContent || "",
|
|
kacheln: document.querySelectorAll(".w-kachel").length,
|
|
gruppen: [...document.querySelectorAll(".w-gruppe__titel")].map((t) => t.textContent),
|
|
hinweise: document.querySelectorAll(".w-hinweis").length,
|
|
}));
|
|
ok(bild.gruss.includes("Filipe"), `sein Name steht oben ("${bild.gruss}")`);
|
|
ok(bild.gruppen.length >= 2,
|
|
`nach Gruppen geordnet (${bild.gruppen.join(" · ")})`);
|
|
ok(bild.hinweise > 0, `mit ${bild.hinweise} Hinweisen zu seiner Rolle`);
|
|
ok(bild.kacheln > 10, `und allem, was er hat (${bild.kacheln})`);
|
|
} finally { await pChef.ctx.close(); }
|
|
|
|
ok(seitenFehler.length === 0, seitenFehler.length === 0
|
|
? "kein Skript ist unterwegs gestolpert"
|
|
: `Skriptfehler: ${seitenFehler.join(" | ")}`);
|
|
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
}
|
|
|
|
melde(fehler === 0
|
|
? "\nAlles in Ordnung.\n"
|
|
: `\n${fehler} FEHLER.\n`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|