DIE LETZTEN BEIDEN SEITEN ANGESEHEN, die einem Mitglied offenstehen.
Sie standen bisher nur in der Messung ("keine toten Wege, keine fremde
Sprache") -- angesehen hatte ich sie nie.
1. ZWEIMAL "REGELN & HILFE". Die Seite mit den fuenf Regeln hiess
genauso wie das BRETT, auf das sie verweist. Auf dem Brett stand
damit ein Link "Regeln & Hilfe", der auf eine Seite namens "Regeln
& Hilfe" fuehrte -- wer ihn drueckte, glaubte im Kreis gelaufen zu
sein. Und dieser Link steht auf JEDEM Brett des Treffs.
Es sind zwei verschiedene Dinge: Hier die fuenf Regeln, kurz und
fest. Dort ein Brett, das daneben haeufige Fragen, Hilfe und die
Folgen sammelt. Die Seite heisst jetzt "Die Regeln".
2. "GESPEICHERT WIRD ERST, WENN DU ABSCHICKST" -- daneben stand "du
kannst zwischendurch aufhoeren und spaeter weitermachen". Beides
zusammen kann nicht stimmen, und nachgemessen stimmt der zweite
Satz: bewerben.js legt einen Entwurf im Browser ab (localStorage).
Der alte Satz war also nicht falsch, aber missverstaendlich -- er
meinte "an uns uebermittelt wird erst beim Abschicken" und klang
nach "nichts wird gespeichert". Bei einem Formular mit sechzehn
Feldern ist das der Unterschied zwischen "ich mache spaeter weiter"
und "ich fange lieber gar nicht erst an". Und es ist eine
Datenschutzauskunft: Wer an einem fremden Geraet sitzt, sollte
wissen, dass die Antworten dort liegen bleiben.
Die Bewerbungsseite selbst ist im Uebrigen vorbildlich: jedes Feld mit
"noetig"/"freiwillig" markiert, Hilfetexte, die ehrlich sind ("Ehrlich
ist besser als beeindruckend"), und am Ende steht, wer es liest.
pruef-community-sicht: 7 Pruefungen, 0 Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
377 lines
19 KiB
JavaScript
377 lines
19 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"];
|
|
/* Die beiden anderen Seiten, die einem Mitglied offenstehen. Sie
|
|
standen bisher nur in der Messung, angesehen hatte ich sie nie. */
|
|
const WEITERE = ["treff-regeln", "bewerben"];
|
|
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 });
|
|
}
|
|
|
|
/* ---- Die zwei uebrigen Seiten ------------------------------------ */
|
|
console.log("");
|
|
console.log("=== Die anderen beiden Seiten ===");
|
|
for (const d of WEITERE) {
|
|
await seite.goto(`${BASIS}/workspace/${d}.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1200);
|
|
const z = await seite.evaluate(() => ({
|
|
titel: document.querySelector("h1")?.textContent?.trim().slice(0, 40) || "(keine)",
|
|
hoehe: document.body.scrollHeight,
|
|
knoepfe: document.querySelectorAll("button, a.schritt, .knopf").length,
|
|
felder: document.querySelectorAll("input, textarea, select").length,
|
|
}));
|
|
console.log(`${d.padEnd(14)} „${z.titel}" · ${z.hoehe} px · ${z.knoepfe} Knoepfe · ${z.felder} Felder`);
|
|
await seite.screenshot({
|
|
path: `C:/Users/qciga/AppData/Local/Temp/comm-${d}${BREITE < 700 ? "-handy" : ""}.png`,
|
|
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 */ }
|