Im Code stand die Loesung schon -- fuer jemand anderen. Am 10.09.2026 wurde dort vermerkt: "BETREUT" IST FUER EINEN MODI IMMER NULL. Er betreut keine Creator. Auf seiner Startseite stand deshalb dauerhaft "0 betreut": eine Zahl, die nie etwas anderes sagen kann, und damit schlimmer als keine. Wer sie sieht, sucht nach dem Fehler. Fuer ein Mitglied der Community gilt das erst recht -- und dort blieb es stehen. Dieselbe Zahl, derselbe Grund, dieselbe Wirkung. Jetzt steht dort, was die Community wirklich betrifft: WUENSCHE OFFEN. Der Ort, an dem sie mitentscheidet, was als Naechstes passiert -- eine Zahl, die zum Hingehen einlaedt, statt eine, die nichts sagt. WORAN DIE ENTSCHEIDUNG HAENGT: `freigabeBedingung` gibt nur fuer Leute von aussen eine SQL-Bedingung zurueck, sonst null. Damit ist die Frage "ist das jemand aus der Community" schon beantwortet, ohne dass hier ein Rollenname steht -- und dieselbe Bedingung sorgt dafuer, dass nur gezaehlt wird, was diese Person auch sehen darf. GEMESSEN, beide Seiten: Community : "4 Wuensche offen" DogFather : "2 im Team" (unveraendert) pruef-zentrale-ring: 14 Pruefungen, 0 Fehler. Co-Authored-By: Claude Opus 5 <[email protected]>
356 lines
18 KiB
JavaScript
356 lines
18 KiB
JavaScript
/* =====================================================================
|
|
DIE SIEBEN COMMUNITY-BRETTER MIT DEN AUGEN EINES MITGLIEDS
|
|
|
|
Aufruf: node tools/community-blick.mjs
|
|
BREITE=412 node tools/community-blick.mjs (Handy)
|
|
|
|
WARUM ALS GAST UND NICHT ALS ADMIN. Als DogFather sieht man auf
|
|
jedem Brett Knoepfe, Vorschlaege und Felder, die ein Mitglied nie zu
|
|
Gesicht bekommt. Wer die Bretter so beurteilt, beurteilt eine Seite,
|
|
die es fuer die Community nicht gibt -- und uebersieht genau das,
|
|
was ihr fehlt.
|
|
|
|
Der Startkatalog wird vorher vom Team uebernommen, damit die Bretter
|
|
nicht leer sind. Leere Bretter zu fotografieren beantwortet nur die
|
|
Frage, ob sie leer sind.
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawn } from "node:child_process";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { scryptSync, randomBytes } from "node:crypto";
|
|
import { request as anfrage } from "node:http";
|
|
|
|
const HIER = join(dirname(fileURLToPath(import.meta.url)), "..", "server");
|
|
const PORT = Number(process.env.PORT_BLICK || 4364);
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-comm-"));
|
|
const DB = join(ordner, "workspace.db");
|
|
const BRETTER = ["treff", "anschlag", "ansteht", "wunsch", "highlight", "regeln", "mitmachen"];
|
|
let kind = null;
|
|
|
|
async function start() {
|
|
kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
|
env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT),
|
|
SITE_ACCESS_SECRET: "lokaler-test",
|
|
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
|
|
stdio: ["ignore", "pipe", "pipe"] });
|
|
let aus = "";
|
|
kind.stdout.on("data", (d) => { aus += d; });
|
|
kind.stderr.on("data", (d) => { aus += d; });
|
|
for (let i = 0; i < 80; i++) {
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
if (kind.exitCode !== null) break;
|
|
try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
|
if (a.status < 500) return; } catch { /* noch nicht oben */ }
|
|
}
|
|
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
|
}
|
|
|
|
function personAnlegen(name, rolle, code) {
|
|
const d = new DatabaseSync(DB);
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salz, 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, salz, 32768, new Date().toISOString());
|
|
d.close();
|
|
}
|
|
|
|
await start();
|
|
kind.kill();
|
|
await new Promise((r) => setTimeout(r, 600));
|
|
personAnlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
personAnlegen("Mara", "gast", "CODE-GAST-0001");
|
|
/* Ein ZWEITES Mitglied. Den eigenen Beitrag meldet niemand -- er
|
|
laesst sich loeschen, das ist der kuerzere Weg. Ohne dieses zweite
|
|
Konto misst die Gegenprobe unten also nicht den Meldeweg, sondern
|
|
diese Regel. */
|
|
personAnlegen("Jonas", "gast", "CODE-GAST-0002");
|
|
await start();
|
|
|
|
/** Meldet an -- mit Host-Kopfzeile, weil die Rolle davon abhaengt.
|
|
*
|
|
* „gast" gibt es nur im Haus der Community. Der Host entscheidet
|
|
* darueber, und ein `fetch` auf 127.0.0.1 schickt eben 127.0.0.1.
|
|
* Deshalb hier ueber node:http, wo sich der Kopf setzen laesst.
|
|
*
|
|
* ALTERSBESTAETIGUNG: Wer sich zum ersten Mal in der Community
|
|
* anmeldet, muss bestaetigen, dass er 18 ist -- sonst antwortet der
|
|
* Server mit 400 „alter_offen". */
|
|
function rohAnfrage(pfad, host, koerper) {
|
|
return new Promise((f, s2) => {
|
|
const a = anfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad, method: "POST",
|
|
headers: { Host: host, "Content-Type": "application/json" },
|
|
}, (an) => {
|
|
let t = "";
|
|
an.on("data", (c) => { t += c; });
|
|
an.on("end", () => f({ code: an.statusCode, text: t, kopf: an.headers }));
|
|
});
|
|
a.on("error", s2);
|
|
a.write(JSON.stringify(koerper));
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
async function anmelden(rolle, code, host = "127.0.0.1") {
|
|
let a = await rohAnfrage("/workspace/api/anmelden", host, { rolle, code });
|
|
if (a.code === 400 && a.text.includes("alter_offen")) {
|
|
a = await rohAnfrage("/workspace/api/anmelden", host,
|
|
{ rolle, code, alter_ok: true });
|
|
}
|
|
if (a.code !== 200) {
|
|
throw new Error(`Anmeldung ${rolle}@${host} -> ${a.code} ${a.text.slice(0, 120)}`);
|
|
}
|
|
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
|
|
/* ---- Das Team uebernimmt die Vorschlaege, damit nichts leer ist ---- */
|
|
const kAdmin = await anmelden("admin", "CODE-DOGI-0001");
|
|
/* LEER=ja zeigt die Bretter, wie sie am ersten Tag aussehen.
|
|
Die Texte dafuer gibt es seit Langem -- gesehen hat sie noch
|
|
niemand, weil jede Messung vorher den Startkatalog uebernimmt. */
|
|
const LEER = process.env.LEER === "ja";
|
|
console.log(LEER ? "=== Startkatalog NICHT uebernehmen (leere Bretter) ===" : "=== Startkatalog uebernehmen ===");
|
|
for (const b of LEER ? [] : BRETTER) {
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/${b}/start`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", cookie: kAdmin, origin: BASIS },
|
|
body: JSON.stringify({}),
|
|
});
|
|
const r = await a.json().catch(() => ({}));
|
|
console.log(` ${b.padEnd(11)} ${a.status} ${r.angelegt ?? r.anzahl ?? ""}`);
|
|
}
|
|
|
|
/* ---- EIN BEITRAG VON EINEM MITGLIED --------------------------------
|
|
|
|
GEGENPROBE ZUM MELDEWEG. „melden" steht seit dem 18.09.2026 nicht
|
|
mehr unter Beitraegen des Teams -- unter einer Regel ergab der Knopf
|
|
keinen Sinn. Die Gefahr dabei: dass er dadurch auch dort
|
|
verschwindet, wo er gebraucht wird.
|
|
|
|
Deshalb bekommt der Treff hier einen Beitrag, der von einem
|
|
MITGLIED stammt. Unter ihm muss der Knopf stehen. Ohne diesen
|
|
Eintrag saehe eine abgeschaffte Meldefunktion genauso aus wie eine
|
|
funktionierende: Im Test schreibt sonst nur das Team.
|
|
|
|
Direkt in die Datenbank und nicht ueber die Schnittstelle: Ein
|
|
frisches Mitglied darf sieben Tage lang nicht schreiben, und diese
|
|
Regel soll gelten bleiben. */
|
|
if (!LEER) {
|
|
const d = new DatabaseSync(DB);
|
|
const wer = d.prepare("SELECT id FROM personen WHERE rolle = 'gast' AND name = 'Jonas'").get();
|
|
/* GEGENPROBE ZU „NUR OFFENE" (18.09.2026). Der Knopf erscheint
|
|
seit heute nur, wenn es etwas zu filtern gibt -- sonst stand er
|
|
auf fuenf Brettern, die gar keine Zustaende kennen, und tat dort
|
|
nichts. Die Gefahr dabei: dass er auch dort verschwindet, wo er
|
|
gebraucht wird. Ein erledigter Wunsch beweist das Gegenteil. */
|
|
d.prepare("UPDATE eintraege SET status = 'erledigt' WHERE bereich = 'wunsch'"
|
|
+ " AND id = (SELECT MIN(id) FROM eintraege WHERE bereich = 'wunsch')").run();
|
|
d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von)
|
|
VALUES ('treff','frage',?,?,?, 'offen', ?, ?)`)
|
|
.run("Kommt Casper auch mal in den Stream?",
|
|
"Ich schaue meistens abends und habe ihn noch nie live gesehen.",
|
|
new Date().toISOString().slice(0, 10), new Date().toISOString(), wer.id);
|
|
d.close();
|
|
}
|
|
|
|
/* ---- Und jetzt als Mitglied hinsehen ------------------------------ */
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
/* DIE ECHTE ADRESSE, nicht 127.0.0.1.
|
|
Der Server entscheidet ueber den Host-Kopf, ob er im Haus der
|
|
Community steht („crew.dogfather-universe.com") oder in dem der
|
|
Agentur. Auf 127.0.0.1 ist er in der Agentur -- und dort gibt es die
|
|
Rolle „gast" nicht. Der erste Versuch hat deshalb sieben Mal die
|
|
ANMELDESEITE fotografiert, mit „0 Karten" und ohne Schreibfeld, und
|
|
das sah aus wie ein Befund ueber die Bretter.
|
|
Chromium loest den Namen auf Wunsch selbst auf. */
|
|
const browser = await pw.chromium.launch();
|
|
const BREITE = Number(process.env.BREITE || 1500);
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: BREITE, height: BREITE < 700 ? 915 : 1100 },
|
|
deviceScaleFactor: 1, isMobile: BREITE < 700, hasTouch: BREITE < 700 });
|
|
const seite = await ctx.newPage();
|
|
const fehlerAufSeite = [];
|
|
seite.on("console", (m) => { if (m.type() === "error") fehlerAufSeite.push(m.text()); });
|
|
/* Welche Dateien kommen NICHT an? Ohne diese Zeile sieht man nur eine
|
|
nackte Seite und raet, woran es liegt. */
|
|
const nichtGeladen = [];
|
|
seite.on("requestfailed", (r) => nichtGeladen.push(`${r.url().slice(-60)} (${r.failure()?.errorText})`));
|
|
seite.on("response", (r) => { if (r.status() >= 400) nichtGeladen.push(`${r.url().slice(-60)} -> ${r.status()}`); });
|
|
|
|
/* DIE SITZUNG EINES GASTES IN DEN BROWSER LEGEN.
|
|
|
|
Direkt im Browser anzumelden ginge nur ueber die echte Adresse --
|
|
und dort schickt der Server `Strict-Transport-Security`, woraufhin
|
|
Chromium alle Unterdateien auf https umbiegt. Der Testserver spricht
|
|
aber nur http: Ergebnis war eine nackte Seite ohne Stil und ohne
|
|
Skripte, auf der kein Knopf mehr etwas tat. Die Kopfzeile ist
|
|
richtig so und bleibt.
|
|
|
|
Also: anmelden, wo es geht (ueber die Kopfzeile), und das
|
|
Sitzungsplaetzchen in den Browser legen. Gemessen wird danach das,
|
|
was ein Mitglied wirklich sieht. */
|
|
const kGast = await anmelden("gast", "CODE-GAST-0001", "crew.dogfather-universe.com");
|
|
await ctx.addCookies(kGast.split("; ").filter(Boolean).map((z) => {
|
|
const [name, ...rest] = z.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
|
|
console.log("");
|
|
/* ---- ZUERST DIE STARTSEITE ---------------------------------------
|
|
Sie ist das Erste, was ein Mitglied sieht -- und der einzige Weg zu
|
|
den sieben Brettern. Bisher habe ich sie nie mit Gast-Augen
|
|
angesehen; gemessen wurden immer nur die Bretter selbst. */
|
|
{
|
|
await seite.goto(`${BASIS}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1600);
|
|
const z = await seite.evaluate(() => {
|
|
const kacheln = [...document.querySelectorAll(".kachel")];
|
|
return {
|
|
anzahl: kacheln.length,
|
|
namen: kacheln.map((k) => (k.querySelector(".kachel__name, h2, h3")?.textContent || "").trim()),
|
|
ohneUnterzeile: kacheln.filter((k) =>
|
|
!(k.querySelector(".kachel__unter, .kachel p")?.textContent || "").trim()).length,
|
|
hoehen: [...new Set(kacheln.map((k) => Math.round(k.getBoundingClientRect().height)))].length,
|
|
ueberschrift: document.querySelector("h1")?.textContent?.trim() || "",
|
|
seitenhoehe: document.body.scrollHeight,
|
|
aufgabenBlock: !document.getElementById("zahlen-block")?.hidden,
|
|
arbeitswoerter: [...document.body.innerText.matchAll(/Vorarbeiten|goenn dir was|gönn dir was|Betreut|Review|Abgebrochen/g)].map((m) => m[0]),
|
|
};
|
|
});
|
|
console.log("");
|
|
console.log("=== Die Startseite mit Gast-Augen ===");
|
|
console.log(` Ueberschrift: „${z.ueberschrift}"`);
|
|
console.log(` ${z.anzahl} Kacheln, ${z.hoehen} verschiedene Hoehen, ${z.ohneUnterzeile} ohne Unterzeile`);
|
|
console.log(` ${z.namen.join(" · ")}`);
|
|
console.log(` Seitenhoehe ${z.seitenhoehe} px · Aufgabenblock: ${z.aufgabenBlock ? "DA" : "weg"}`);
|
|
console.log(` Arbeitswoerter auf der Seite: ${z.arbeitswoerter.join(", ") || "(keine)"}`);
|
|
|
|
/* FUEHREN DIE ZAHLEN IRGENDWOHIN? Sieben Links auf aufgaben.html --
|
|
und die Rechtetabelle nennt dort „ALLE", was die Rolle „gast"
|
|
ausdruecklich NICHT einschliesst. Also nachsehen statt annehmen. */
|
|
const zahlenlinks = await seite.evaluate(() =>
|
|
[...document.querySelectorAll("#zahlen a")].map((a2) => a2.getAttribute("href")));
|
|
console.log(` Zahlen-Links: ${zahlenlinks.length}`);
|
|
|
|
/* DIE ZENTRALE OBEN: Ring, drei Zaehler, Uhr. Was davon betrifft
|
|
ein Mitglied ueberhaupt? */
|
|
const zentrale = await seite.evaluate(() => {
|
|
const txt = (w) => (document.querySelector(w)?.textContent || "").replace(/\s+/g, " ").trim();
|
|
return {
|
|
ring: txt(".ring, .ringblock, [class*='ring']"),
|
|
zaehler: [...document.querySelectorAll(".standzahl, .stand, [class*='stand']")]
|
|
.map((e) => e.textContent.replace(/\s+/g, " ").trim()).filter(Boolean).slice(0, 6),
|
|
kopfzeilen: [...document.querySelectorAll(".zentrale *")]
|
|
.map((e) => e.children.length ? "" : (e.textContent || "").replace(/\s+/g, " ").trim())
|
|
.filter((t) => t && t.length < 60).slice(0, 14),
|
|
};
|
|
});
|
|
console.log(` Ring: „${zentrale.ring.slice(0, 60)}"`);
|
|
console.log(` Zaehler: ${zentrale.zaehler.join(" | ") || "(nicht gefunden)"}`);
|
|
console.log(` Texte oben: ${zentrale.kopfzeilen.join(" · ")}`);
|
|
if (zahlenlinks.length) {
|
|
const ziel = new URL(zahlenlinks[0], `${BASIS}/workspace/`).href;
|
|
const antwort = await seite.goto(ziel, { waitUntil: "domcontentloaded" });
|
|
console.log(` Klick auf die erste Zahl -> ${antwort.status()} ${seite.url().split("/").pop()}`);
|
|
await seite.goto(`${BASIS}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
}
|
|
await seite.screenshot({
|
|
path: `C:/Users/qciga/AppData/Local/Temp/comm-start${BREITE < 700 ? "-handy" : ""}.png`,
|
|
fullPage: true });
|
|
}
|
|
|
|
console.log("");
|
|
console.log("=== Was ein Mitglied sieht ===");
|
|
for (const b of BRETTER) {
|
|
await seite.goto(`${BASIS}/workspace/bereich.html?b=${b}`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1500);
|
|
const z = await seite.evaluate(() => ({
|
|
titel: document.querySelector("h1")?.textContent?.trim() || "",
|
|
karten: document.querySelectorAll('.eintrag-karte').length,
|
|
arten: [...new Set([...document.querySelectorAll('.eintrag-art, .art')].map((e) => e.textContent.trim()))].length,
|
|
knoepfeJeKarte: document.querySelector('.eintrag-karte')
|
|
? document.querySelector('.eintrag-karte').querySelectorAll('button, a.schritt').length : 0,
|
|
/* Steht „melden" dort, wo es hingehoert -- und nur dort? */
|
|
meldenGesamt: [...document.querySelectorAll('.eintrag-karte button')]
|
|
.filter((b) => b.textContent.trim() === 'melden').length,
|
|
schreibfeld: !!document.querySelector('#neu-form, .neu-form, form#neu'),
|
|
hoeheJeKarte: document.querySelector('.eintrag-karte')
|
|
? Math.round(document.querySelector('.eintrag-karte').getBoundingClientRect().height) : 0,
|
|
startkatalog: !!document.querySelector("#startkatalog")?.children?.length,
|
|
leerText: document.querySelector(".leer, .leer-hinweis, .hinweis")?.textContent?.trim().slice(0, 80) || "",
|
|
hoehe: document.body.scrollHeight,
|
|
/* Wie viele Filterknoepfe, und sind die Karten gleich hoch? */
|
|
filterZahl: document.querySelectorAll('#filter button').length,
|
|
kartenHoehen: [...new Set([...document.querySelectorAll('.eintrag-karte')]
|
|
.map((k) => Math.round(k.getBoundingClientRect().height)))].length,
|
|
/* Der Kopf des Bretts: Was steht da, und wohin fuehrt der Link? */
|
|
ober: document.querySelector('.brettkopf__ober, .ober')?.textContent?.trim() || '',
|
|
unter: document.querySelector('.brettkopf__unter, .unterzeile, .brettkopf p')?.textContent?.trim() || '',
|
|
kopflink: (() => {
|
|
const a2 = document.querySelector('.brettkopf a, .kopfkasten a');
|
|
return a2 ? a2.textContent.trim() + ' -> ' + (a2.getAttribute('href') || '') : '(keiner)';
|
|
})(),
|
|
/* Sichtbare Sternchenpaare -- gemeint war Fettschrift. */
|
|
sternchen: (document.body.innerText.match(/\*\*/g) || []).length,
|
|
}));
|
|
console.log(`${b.padEnd(11)} „${z.titel}" · ${z.karten} Karten`
|
|
+ ` · Schreibfeld: ${z.schreibfeld ? "ja" : "NEIN"}`
|
|
+ ` · Karte ${z.hoeheJeKarte}px · melden: ${z.meldenGesamt}`
|
|
+ ` · Filter: ${z.filterZahl} · Kartenhoehen: ${z.kartenHoehen}`
|
|
+ `
|
|
Kopf: „${z.ober}" / „${z.unter}" · Link: ${z.kopflink}`
|
|
+ (z.sternchen ? ` · STERNCHEN: ${z.sternchen}` : "")
|
|
+ (z.leerText ? ` · „${z.leerText}"` : ""));
|
|
const pfad = `C:/Users/qciga/AppData/Local/Temp/comm-${b}${BREITE < 700 ? "-handy" : ""}.png`;
|
|
await seite.screenshot({ path: pfad, fullPage: true });
|
|
}
|
|
|
|
console.log("");
|
|
console.log(fehlerAufSeite.length
|
|
? `ACHTUNG: ${fehlerAufSeite.length} Konsolenfehler: ${fehlerAufSeite.slice(0, 3).join(" | ")}`
|
|
: "Keine Konsolenfehler.");
|
|
|
|
/* ---- GEGENPROBE: SIEHT DAS TEAM SEINEN ARBEITSPLATZ NOCH? --------
|
|
|
|
„Deine Aufgaben" verschwindet seit dem 18.09.2026 fuer alle, die
|
|
keine Aufgaben-Kachel haben. Die Gefahr dabei ist immer dieselbe:
|
|
dass es auch dort verschwindet, wo es hingehoert. Ohne diese
|
|
Messung saehe eine abgeschaffte Funktion genauso aus wie eine, die
|
|
richtig entscheidet. */
|
|
{
|
|
const ctx2 = await browser.newContext({ viewport: { width: BREITE, height: 1100 } });
|
|
const seite2 = await ctx2.newPage();
|
|
await seite2.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await seite2.click('.rolle[data-rolle="admin"]');
|
|
await seite2.fill("#code", "CODE-DOGI-0001");
|
|
await seite2.click("button[type=submit]", { noWaitAfter: true });
|
|
await seite2.waitForURL("**/start.html", { timeout: 25000 });
|
|
await seite2.waitForTimeout(1600);
|
|
const z = await seite2.evaluate(() => ({
|
|
block: !document.getElementById("zahlen-block")?.hidden,
|
|
links: document.querySelectorAll("#zahlen a").length,
|
|
satz: document.body.innerText.includes("Vorarbeiten"),
|
|
/* Behaelt DogFather seine dritte Zahl? „Wuensche offen" waere fuer
|
|
ihn die falsche -- er sieht ohnehin alles. */
|
|
drittezahl: [...document.querySelectorAll(".standzahl, .stand, [class*='stand']")]
|
|
.map((e) => e.textContent.replace(/\s+/g, " ").trim())
|
|
.find((t) => /betreut|im Team|Wünsche offen/.test(t)) || "(keine)",
|
|
}));
|
|
console.log("");
|
|
console.log("=== Gegenprobe: DogFather auf derselben Seite ===");
|
|
console.log(` Aufgabenblock: ${z.block ? "DA" : "FEHLT"} · Zahlen-Links: ${z.links}`
|
|
+ ` · „Vorarbeiten": ${z.satz ? "ja" : "nein"}`);
|
|
console.log(` Dritte Zahl: ${z.drittezahl}`);
|
|
await ctx2.close();
|
|
}
|
|
|
|
await browser.close();
|
|
kind.kill();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }
|