ZWEI SACHEN, EINE DAVON EIN ECHTER BEFUND. 1) DIE KARTEN KAMEN AN DER RECHTETAFEL VORBEI Der Bauplan sagt in Kapitel 04 und noch einmal in der Abnahme: "Leite die sichtbaren Karten aus derselben Rechte-Tafel ab, die rechte.html verwendet. Eine Rolle sieht nur Karten zu Seiten, die sie oeffnen darf; pruefe das SERVERSEITIG." Ich habe dafuer eine Pruefung geschrieben, die die Seite ueber die ECHTE Schnittstelle sperrt (nicht mit einem UPDATE in der Datenbank -- nur so laeuft tafelLaden() mit). Sie wurde sofort rot: Nach dem Sperren von bereich.html fuer einen Modi lieferte der Server weiterhin ALLE ELF Bereichs-Karten. Im Browser fiel das nicht auf, und das ist der heikle Teil: Die Seite verbindet jede Karte mit einer Kachel, und die Kachelliste ist gefiltert -- die Karten verschwanden also auf dem Bildschirm. Die TEXTE gingen trotzdem hinaus. Eine Oberflaeche, die etwas nicht anzeigt, ist keine Schranke; genau deshalb steht im Bauplan "serverseitig". Jetzt siebt `darfKarte()` an der Quelle. Der Pfad wird aus dem Schluessel GERECHNET (`bereich:highlight` -> /workspace/bereich.html), nicht aus einer Zuordnungsliste -- das ist die Umkehrung von `schluesselVonZiel`, und eine gepflegte Tabelle daneben waere die, die beim naechsten neuen Brett fehlt. Entschieden wird mit `darfSeite` selbst; hier wird nichts davon nachgebaut. Gegenprobe in derselben Pruefung: zuruecksperren -> die Karten sind wieder da (29 von 29). Und eine Zeile dazwischen, die zaehlt, dass NUR sie verschwunden sind -- waere mit der Sperre die halbe Anleitung weggefallen, waere die erste Zeile auch gruen gewesen. 2) „AB DABEI" UND „AB STAMM" AN DER KARTE (Kapitel 04) "Kann ein Community-Mitglied etwas wegen seiner Stufe noch nicht, steht das an der Karte." Auch das abgeleitet: aus TREFF_SCHREIBEN, derselben Tafel, die das Schreiben ENTSCHEIDET. `null` darin heisst "von aussen schreibt hier niemand" -- das ist keine Stufe, die man erreicht, und bekommt deshalb keine Marke. "ab Stamm" an einem Anschlagbrett waere ein Versprechen, das nie eingeloest wird. Als Wort, nicht als Schloss-Symbol: Ein Schloss sagt "du darfst nicht" und laesst offen, ob jemals. "ab Dabei" sagt, dass es kommt -- und wann, steht mit Datum in der Karte "Deine Stufe" darueber. Ruhig gestaltet, nicht rot: Es ist keine Sperre, sondern eine Auskunft ueber die Zeit. GEPRUEFT (pruef-anleitung 166 -> 173) Die wichtigste der neuen Zeilen ist die, dass die Marke nach dem Aufstieg VERSCHWINDET. Eine Marke, die bleibt, ist schlimmer als keine: Sie sagt jemandem, er duerfe etwas nicht, das er laengst darf, und er probiert es gar nicht erst. Gemessen an drei Karten mit drei verschiedenen Antworten (Chat: Dabei, Highlights: Stamm, Anschlagbrett: gar nichts) -- waere die Rechnung grob falsch, traefe sie alle drei gleich. Dazu die Gegenprobe, dass eine Teamrolle keine einzige solche Marke bekommt. Mitgelaufen: pruef-rechtetafel 19, pruef-rechte-umstellen 56, pruef-css-klassen, pruef-tippziele 13 -- alle gruen. Die Abnahmezahlen aus Kapitel 13 (14/29/29/34 mit ihrer Aufteilung) sind nach dem neuen Sieb unveraendert; das ist der Beleg, dass es im Normalfall nichts wegnimmt. Im Browser nachgemessen (390 px, echte Anmeldung): Community sieht vier Marken (Rudel-Chat, Wunschliste, Mitmachen „ab Dabei", Highlights „ab Stamm"), die rechte Hand keine einzige. Co-Authored-By: Claude Opus 5 <[email protected]>
383 lines
17 KiB
JavaScript
383 lines
17 KiB
JavaScript
/* =====================================================================
|
|
WAS SEHEN DIE VIER CREW-ROLLEN HEUTE AUF willkommen.html?
|
|
|
|
Ausgangsmessung vom 06.10.2026, bevor der Bauplan auf der
|
|
Crew-Seite umgesetzt wird. Ohne sie waere hinterher nicht zu
|
|
sagen, was die Arbeit bewirkt hat -- und ob die Seite vorher
|
|
wirklich leer war oder nur anders.
|
|
|
|
Gemessen wird am HOSTNAMEN, nicht an der Rolle: Erst der
|
|
Hostname macht `person.haus` zu "crew", und genau daran haengt
|
|
die Schranke im Servermodul.
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request, request as httpAnfrage } from "node:http";
|
|
import { createServer as httpsServer } from "node:https";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-anlcrew-"));
|
|
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, "mess-anleitung-crew");
|
|
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(120_000, "mess-anleitung-crew");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
|
|
/** Ein Aufruf mit frei waehlbarem Host-Kopf -- `fetch` laesst `Host`
|
|
* nicht setzen, deshalb node:http. */
|
|
function ruf(pfad, { keks = "", methode = "GET", rumpf = null } = {}) {
|
|
return new Promise((fertig) => {
|
|
const kopf = { Host: CREW };
|
|
if (keks) kopf.Cookie = keks;
|
|
if (rumpf) { kopf["Content-Type"] = "application/json"; }
|
|
const a = request({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, headers: kopf },
|
|
(r) => {
|
|
let t = "";
|
|
r.on("data", (c) => { t += c; });
|
|
r.on("end", () => fertig({
|
|
status: r.statusCode,
|
|
keks: (r.headers["set-cookie"] || []).map((k) => k.split(";")[0]).join("; "),
|
|
text: t,
|
|
json: (() => { try { return JSON.parse(t); } catch { return null; } })(),
|
|
}));
|
|
});
|
|
a.on("error", () => fertig({ status: 0, text: "", json: null, keks: "" }));
|
|
if (rumpf) a.write(rumpf);
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
await ruf("/workspace/api/anmelden", { methode: "POST", rumpf: JSON.stringify({ rolle: "admin", code: "x" }) });
|
|
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 ROLLEN = [["gast", "Community"], ["modi", "Modi"], ["linke", "Linke Hand"], ["hand", "Rechte Hand"]];
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
for (const [rolle] of ROLLEN) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const code = `CODE-${rolle.toUpperCase()}-0001`;
|
|
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(rolle, rolle, hash, salt, 32768, new Date().toISOString());
|
|
}
|
|
d.close();
|
|
}
|
|
|
|
console.log("\n=== Was die Crew-Rollen heute auf willkommen.html bekommen ===");
|
|
for (const [rolle, wie] of ROLLEN) {
|
|
/* DIE ALTERSBESTAETIGUNG IST EIN EIGENER SCHRITT -- nur fuer den
|
|
Gast. Ohne sie antwortet der Server mit 400, und die Messung
|
|
haette "Community kommt nicht herein" gemeldet, ohne dass etwas
|
|
kaputt waere. */
|
|
const an = await ruf("/workspace/api/anmelden", {
|
|
methode: "POST",
|
|
rumpf: JSON.stringify({ rolle, code: `CODE-${rolle.toUpperCase()}-0001`, alter_ok: true }),
|
|
});
|
|
if (an.status !== 200) { console.log(` ${wie.padEnd(12)} kommt nicht herein (${an.status})`); continue; }
|
|
const seite = await ruf("/workspace/willkommen.html", { keks: an.keks });
|
|
const api = await ruf("/workspace/api/anleitung", { keks: an.keks });
|
|
const karten = api.json?.karten?.length ?? "-";
|
|
console.log(` ${wie.padEnd(12)} Seite ${seite.status} · Schnittstelle ${api.status}`
|
|
+ ` · Karten ${karten}`
|
|
+ (api.json?.fehler ? ` · "${api.json.fehler}"` : ""));
|
|
}
|
|
|
|
/* ==== FUEHREN DIE KACHELN DER LINKEN HAND INS LEERE? ================
|
|
|
|
Der Bauplan spricht der Linken Hand vier Kacheln ab (Talente,
|
|
Personen & Zugaenge, Wer sieht was, Vertraulich melden). Die
|
|
Kachelliste gibt sie ihr trotzdem. Entweder irrt der Plan -- oder
|
|
sie sieht vier Kacheln, die auf Seiten fuehren, die sie nicht
|
|
oeffnen darf. Gefragt wird der laufende Server, nicht die Tabelle. */
|
|
{
|
|
console.log("\n=== Linke Hand: fuehren ihre Kacheln irgendwohin? ===");
|
|
const an = await ruf("/workspace/api/anmelden", {
|
|
methode: "POST", rumpf: JSON.stringify({ rolle: "linke", code: "CODE-LINKE-0001" }),
|
|
});
|
|
for (const pfad of ["/workspace/talente.html", "/workspace/personen.html",
|
|
"/workspace/rechte.html", "/workspace/hilfe.html", "/workspace/aufgaben.html"]) {
|
|
const a2 = await ruf(pfad, { keks: an.keks });
|
|
const wohin = a2.status >= 300 && a2.status < 400 ? " -> umgeleitet" : "";
|
|
console.log(` ${pfad.replace("/workspace/", "").padEnd(18)} ${a2.status}${wohin}`);
|
|
}
|
|
}
|
|
|
|
/* Und was steht in der Datenbank -- je Haus. */
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
console.log("\n=== Inhalte in der Datenbank ===");
|
|
for (const r of d.prepare(
|
|
"SELECT haus, rolle, COUNT(*) n FROM anl_karte GROUP BY haus, rolle ORDER BY haus, rolle").all()) {
|
|
console.log(` ${r.haus.padEnd(8)} ${r.rolle.padEnd(10)} ${r.n} Karten`);
|
|
}
|
|
const crew = d.prepare("SELECT COUNT(*) n FROM anl_karte WHERE haus = 'crew'").get().n;
|
|
console.log(` -> fuer die Crew: ${crew} Karten`);
|
|
d.close();
|
|
}
|
|
|
|
/* =====================================================================
|
|
UND WIE SIEHT ES AUS?
|
|
|
|
Zahlen sagen, dass die Karten da sind -- nicht, ob die Seite lesbar
|
|
ist. Der Bauplan verlangt "Handy zuerst: Karten untereinander,
|
|
grosse Tippflaechen, kein Querscrollen". Das misst man nur im
|
|
Browser.
|
|
===================================================================== */
|
|
{
|
|
/* ==== WARUM EIN HTTPS-VORBAU (06.10.2026) =====================
|
|
|
|
Ueber `http://crew.dogfather-universe.com:PORT` laedt das Dokument,
|
|
aber KEINE einzige Unterressource -- gemessen: alle CSS- und
|
|
JS-Dateien melden "requestfailed", und die Seite steht nackt da.
|
|
|
|
Der Grund ist HSTS: Der Name steht in der Vorladeliste von
|
|
Chromium, und der Browser erzwingt deshalb https:// fuer alles,
|
|
was die Seite nachlaedt. Auf einem Klartext-Port findet er dort
|
|
nichts.
|
|
|
|
Deshalb derselbe Weg wie in pruef-chat-neu-stelle: ein kleiner
|
|
HTTPS-Vorbau mit eigenem Zertifikat, der an den echten Server
|
|
weiterreicht. Der Hostname bleibt damit `crew.` -- und nur daran
|
|
erkennt der Server das Haus. */
|
|
const { zertifikatBauen } = await import("./helfer-openssl.mjs");
|
|
const HP = PORT + 1;
|
|
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
|
|
zertifikatBauen(schl, zert, CREW);
|
|
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 },
|
|
}, (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 { chromium } = await import("playwright");
|
|
const browser = await chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
|
|
ignoreHTTPSErrors: true,
|
|
});
|
|
for (const [rolle, wie, datei] of [
|
|
["gast", "Community", "bild-anleitung-crew-community.png"],
|
|
["hand", "Rechte Hand", "bild-anleitung-crew-hand.png"],
|
|
]) {
|
|
/* ANGEMELDET WIRD UEBER DIE SCHNITTSTELLE, nicht ueber die Wand.
|
|
Auf der Crew-Wand liegt bei 390 px das Buehnenbild ueber den
|
|
Knoepfen und faengt jeden Klick ab. Gemessen werden soll hier
|
|
die ANLEITUNG, nicht die Anmeldung -- die hat ihre eigenen
|
|
Pruefungen. Ein Keks aus der Schnittstelle ist derselbe Keks. */
|
|
const an = await ruf("/workspace/api/anmelden", {
|
|
methode: "POST",
|
|
rumpf: JSON.stringify({ rolle, code: `CODE-${rolle.toUpperCase()}-0001`, alter_ok: true }),
|
|
});
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true,
|
|
});
|
|
await kontext.addCookies(an.keks.split("; ").filter(Boolean).map((k) => {
|
|
const [x, ...r] = k.split("=");
|
|
return { name: x, value: r.join("="), domain: CREW, path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const klagen = [];
|
|
seite.on("pageerror", (f) => klagen.push(String(f?.message || f)));
|
|
const schlecht = [];
|
|
seite.on("response", (r) => {
|
|
if (r.status() >= 300) schlecht.push(`${r.status()} ${r.url().split("/workspace/")[1] || r.url()}`);
|
|
});
|
|
seite.on("requestfailed", (r) => schlecht.push(`weg: ${r.url().split("/workspace/")[1]}`));
|
|
await seite.goto(`https://${CREW}:${HP}/workspace/anleitung.html`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1500);
|
|
|
|
const lage = await seite.evaluate(() => {
|
|
const z = (id) => document.getElementById(id);
|
|
const karten = document.querySelectorAll(".an-karte").length;
|
|
const breit = document.documentElement.scrollWidth
|
|
> document.documentElement.clientWidth + 1;
|
|
const klein = [...document.querySelectorAll("button, a")]
|
|
.filter((e) => { const r = e.getBoundingClientRect();
|
|
return r.width > 0 && r.height > 0 && r.height < 44; }).length;
|
|
/* „ab Dabei" / „ab Stamm" -- der Bauplan will sie an der Karte
|
|
sehen, nicht nur im Datenstrom. */
|
|
const marken = [...document.querySelectorAll(".an-karte__abstufe")]
|
|
.map((m) => `${m.closest(".an-karte")?.querySelector(".an-karte__name")
|
|
?.textContent || "?"}: ${m.textContent}`);
|
|
return {
|
|
karten, quer: breit, klein, marken,
|
|
vorweg: z("vorweg-block") && !z("vorweg-block").hidden,
|
|
stufe: z("stufe-block") && !z("stufe-block").hidden,
|
|
stufeText: z("stufe-jetzt")?.textContent || "",
|
|
titel: z("seiten-titel")?.textContent || "",
|
|
};
|
|
});
|
|
console.log(`
|
|
=== ${wie} im Browser (390 px) ===`);
|
|
console.log(` Titel : "${lage.titel}"`);
|
|
console.log(` Karten : ${lage.karten}`);
|
|
console.log(` Drei Dinge vorweg : ${lage.vorweg}`);
|
|
console.log(` Deine Stufe : ${lage.stufe} ${lage.stufeText}`);
|
|
console.log(` Querscrollen : ${lage.quer ? "JA -- Befund" : "nein"}`);
|
|
console.log(` Tippziele unter 44 : ${lage.klein}`);
|
|
console.log(` Stufen-Marken : ${lage.marken.join(" | ") || "keine"}`);
|
|
console.log(` Browsermeldungen : ${klagen.length ? klagen.join(" | ") : "keine"}`);
|
|
console.log(` Fehlgeschlagen : ${schlecht.length ? schlecht.slice(0, 6).join(" | ") : "nichts"}`);
|
|
|
|
/* ==== DER RUNDGANG BEIM ERSTEN ANMELDEN (Bauplan Kapitel 10) ====
|
|
|
|
Er ist selbstanpassend gebaut ("Wird ein Ziel nicht gefunden,
|
|
faellt der Schritt weg") -- daraus sollen sich die drei Schritte
|
|
der Community und die fuenf der Teamrollen von selbst ergeben.
|
|
Geglaubt wird das nicht, es wird gezaehlt. */
|
|
const start = await kontext.newPage();
|
|
await start.goto(`https://${CREW}:${HP}/workspace/start.html`,
|
|
{ waitUntil: "networkidle" });
|
|
await start.waitForTimeout(1800);
|
|
const gruss = await start.evaluate(() => {
|
|
const g = document.querySelector(".rg-gruss");
|
|
return { da: !!g, text: (g?.textContent || "").replace(/\s+/g, " ").trim().slice(0, 70) };
|
|
});
|
|
let stationen = 0;
|
|
if (gruss.da) {
|
|
await start.evaluate(() => {
|
|
const k = [...document.querySelectorAll(".rg-gruss button")]
|
|
.find((b) => /Los/i.test(b.textContent || ""));
|
|
k?.click();
|
|
});
|
|
await start.waitForTimeout(700);
|
|
/* Durchklicken und zaehlen, wie viele Stationen es wirklich gibt. */
|
|
for (let i = 0; i < 9; i++) {
|
|
const da = await start.evaluate(() => !!document.querySelector(".rg__kasten"));
|
|
if (!da) break;
|
|
stationen++;
|
|
const weiter = await start.evaluate(() => {
|
|
const b = [...document.querySelectorAll(".rg__knoepfe button")]
|
|
.find((x) => /Weiter|Fertig|Los/i.test(x.textContent || ""));
|
|
if (!b) return false;
|
|
b.click(); return true;
|
|
});
|
|
if (!weiter) break;
|
|
await start.waitForTimeout(400);
|
|
}
|
|
}
|
|
console.log(` Begruessung : ${gruss.da} "${gruss.text}"`);
|
|
console.log(` Stationen gezaehlt : ${stationen}`);
|
|
await start.close();
|
|
|
|
await seite.screenshot({ path: new URL("./" + datei, import.meta.url).pathname
|
|
.replace(/^\/([A-Za-z]:)/, "$1"), fullPage: true });
|
|
await kontext.close();
|
|
}
|
|
/* ==== „NEU FUER DICH" BEIM AUFSTIEG (Bauplan Kapitel 10) ==========
|
|
|
|
Gemessen wird der ganze Weg: Beitrittstag zurueckdatieren ->
|
|
Zentrale aufrufen -> steht die Karte da? -> auf einen ihrer Wege
|
|
tippen -> landet man auf der Anleitung BEI DER RICHTIGEN KARTE,
|
|
auch wenn sie in einem zugeklappten Abschnitt liegt?
|
|
|
|
Der letzte Punkt ist der, den eine Schnittstellenpruefung nicht
|
|
beantworten kann: Ob ein Sprung ankommt, haengt daran, dass das
|
|
<details> vorher aufgeht. Sonst hat die Karte die Hoehe null, und
|
|
man landet irgendwo. */
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
d.prepare("UPDATE personen SET erstellt = ? WHERE rolle = 'gast'")
|
|
.run(new Date(Date.now() - 10 * 86400_000).toISOString());
|
|
d.close();
|
|
|
|
const an = await ruf("/workspace/api/anmelden", {
|
|
methode: "POST",
|
|
rumpf: JSON.stringify({ rolle: "gast", code: "CODE-GAST-0001", alter_ok: true }),
|
|
});
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true,
|
|
});
|
|
await kontext.addCookies(an.keks.split("; ").filter(Boolean).map((k) => {
|
|
const [x, ...r] = k.split("=");
|
|
return { name: x, value: r.join("="), domain: CREW, path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const klagen = [];
|
|
seite.on("pageerror", (f) => klagen.push(String(f?.message || f)));
|
|
await seite.goto(`https://${CREW}:${HP}/workspace/start.html`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1600);
|
|
|
|
const karte = await seite.evaluate(() => {
|
|
const k = document.querySelector(".aufstieg");
|
|
const wege = [...document.querySelectorAll(".aufstieg__weg")];
|
|
return {
|
|
da: !!k,
|
|
satz: (k?.querySelector(".aufstieg__satz")?.textContent || "").trim(),
|
|
wege: wege.map((a) => `${a.textContent.trim()} -> ${a.getAttribute("href")}`),
|
|
breite: k ? Math.round(k.getBoundingClientRect().width) : 0,
|
|
klein: wege.filter((a) => a.getBoundingClientRect().height < 44).length,
|
|
};
|
|
});
|
|
console.log(`
|
|
=== „Neu für dich" auf der Zentrale (Community, 10 Tage dabei) ===`);
|
|
console.log(` Karte da : ${karte.da}`);
|
|
console.log(` Satz : "${karte.satz}"`);
|
|
console.log(` Wege : ${karte.wege.join(" | ") || "keine"}`);
|
|
console.log(` Breite / zu klein : ${karte.breite} px / ${karte.klein}`);
|
|
|
|
let gelandet = { pfad: "-", markiert: false, sichtbar: false, offen: false };
|
|
if (karte.da && karte.wege.length) {
|
|
await seite.click(".aufstieg__weg");
|
|
await seite.waitForTimeout(1800);
|
|
gelandet = await seite.evaluate(() => {
|
|
const k = document.querySelector('.an-karte[data-gesucht="ja"]');
|
|
const r = k?.getBoundingClientRect();
|
|
return {
|
|
pfad: location.pathname.split("/").pop() + location.search,
|
|
markiert: !!k,
|
|
/* „Sichtbar" heisst hier: im Fenster, nicht nur im Baum. */
|
|
sichtbar: !!r && r.height > 0 && r.top < innerHeight && r.bottom > 0,
|
|
offen: !!k?.closest("details")?.open,
|
|
};
|
|
});
|
|
}
|
|
console.log(` Gelandet auf : ${gelandet.pfad}`);
|
|
console.log(` Karte markiert : ${gelandet.markiert}`);
|
|
console.log(` …aufgeklappt : ${gelandet.offen}`);
|
|
console.log(` …im Bild : ${gelandet.sichtbar}`);
|
|
|
|
/* Kommt sie ein zweites Mal? Sie darf nicht. */
|
|
await seite.goto(`https://${CREW}:${HP}/workspace/start.html`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1600);
|
|
const nochmal = await seite.evaluate(() => !!document.querySelector(".aufstieg"));
|
|
console.log(` Beim zweiten Mal : ${nochmal ? "JA -- Befund" : "weg"}`);
|
|
console.log(` Browsermeldungen : ${klagen.length ? klagen.join(" | ") : "keine"}`);
|
|
await kontext.close();
|
|
}
|
|
|
|
await browser.close();
|
|
vorbau.close();
|
|
}
|
|
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(0);
|