/* ===================================================================== DIE MATERIAL-SEITE MIT DREI PAAR AUGEN Aufruf: node tools/material-blick.mjs BREITE=412 node tools/material-blick.mjs (Handy) WARUM DREI SICHTEN UND NICHT EINE. Auf dieser Seite sieht jede Rolle etwas anderes, und die Unterschiede sind der ganze Sinn: DogFather sieht den Kasten zum Bereitstellen UND die Namen derer, die sich etwas genommen haben. ein Modi sieht den Kasten, aber KEINE Namen. die Community sieht weder noch -- nur das, was sie holen darf. Wer die Seite nur als DogFather ansieht, beurteilt eine Seite, die es fuer die Community nicht gibt. Genau dieser Fehler ist am 11.09. beim Community-Blick schon einmal passiert. UND ES LIEGT ECHTES MATERIAL DARIN. Ein leeres Raster zu fotografieren beantwortet nur die Frage, ob es leer ist -- nicht, ob die Vorschau passt, ob die Karten gleich hoch sind oder ob der Text unter einem Bild lesbar bleibt. ===================================================================== */ import { mkdtempSync, rmSync, readFileSync } 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 WURZEL = join(dirname(fileURLToPath(import.meta.url)), ".."); const HIER = join(WURZEL, "server"); const PORT = Number(process.env.PORT_BLICK || 4371); const BASIS = `http://127.0.0.1:${PORT}`; const CREW = "crew.dogfather-universe.com"; const ordner = mkdtempSync(join(tmpdir(), "ws-mat-blick-")); const DB = join(ordner, "workspace.db"); 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("Rieke", "hand", "CODE-HAND-0001"); personAnlegen("Frida", "modi", "CODE-MODI-0001"); personAnlegen("Mara", "gast", "CODE-GAST-0001"); personAnlegen("Jonas", "gast", "CODE-GAST-0002"); await start(); function rohAnfrage(pfad, host, koerper, kopf = {}, methode = "POST") { return new Promise((f, s2) => { const daten = Buffer.isBuffer(koerper) ? koerper : (koerper == null ? null : Buffer.from(JSON.stringify(koerper))); const a = anfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, headers: { Host: host, ...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}), ...kopf, }, }, (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); if (daten) a.write(daten); a.end(); }); } async function anmelden(rolle, code) { let a = await rohAnfrage("/workspace/api/anmelden", CREW, { rolle, code }); if (a.code === 400 && a.text.includes("alter_offen")) { a = await rohAnfrage("/workspace/api/anmelden", CREW, { rolle, code, alter_ok: true }); } if (a.code !== 200) throw new Error(`Anmeldung ${rolle} -> ${a.code} ${a.text.slice(0, 160)}`); return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); } /* ---- ECHTES MATERIAL HINEINLEGEN ----------------------------------- Die Buehnenbilder des Hauses. Sie sind echte Fotos in echter Groesse -- ein 1x1-Pixel-Platzhalter wuerde jede Frage nach dem Seitenverhaeltnis und der Ladezeit unbeantwortet lassen. */ const kDogi = await anmelden("admin", "CODE-DOGI-0001"); const kHand = await anmelden("hand", "CODE-HAND-0001"); const kModi = await anmelden("modi", "CODE-MODI-0001"); const kGast = await anmelden("gast", "CODE-GAST-0001"); const kJonas = await anmelden("gast", "CODE-GAST-0002"); const BILDER = [ ["crew-gate-960.webp", "Stream-Ausschnitt gestern Abend.webp", "Für TikTok – passt zum Clip von gestern, gern mit Musik drunter."], ["crew-gate-hoch-580.webp", "Hochformat für Insta Story.webp", "Schon im 9:16-Format, geht direkt als Story raus."], ["gate-1280.webp", "Ankündigung Freitag.webp", ""], ["crew-gate-1280.webp", "Dogi im Studio.webp", "Bitte nur mit dem Text „Freitag 20 Uhr“ posten."], ["gate-960.webp", "Thumbnail Variante B.webp", "Falls A nicht zieht."], ]; console.log("=== Material einstellen ==="); let nr = 0; for (const [datei, name, text] of BILDER) { const rohBild = readFileSync(join(WURZEL, "workspace/assets/img/buehne", datei)); const wer = nr % 2 === 0 ? kDogi : kHand; const a = await rohAnfrage("/workspace/api/material", CREW, rohBild, { Cookie: wer, "Content-Type": "image/webp", "Content-Length": rohBild.length, "X-Datei-Name": encodeURIComponent(name), "X-Text": encodeURIComponent(text), Origin: `https://${CREW}`, }); console.log(` ${String(a.code).padEnd(4)} ${name} (${Math.round(rohBild.length / 1024)} KB)`); nr++; } /* ZWEI STUECKE SIND SCHON WEG. Ohne sie waere „Schon benutzt" leer, und genau dieser Block ist der, den Filipe sehen will. */ const alle = JSON.parse((await rohAnfrage("/workspace/api/material", CREW, null, { Cookie: kDogi }, "GET")).text).material; for (const [i, keks] of [[3, kGast], [4, kJonas]]) { const a = await rohAnfrage(`/workspace/api/material/${alle[i].id}/nehmen`, CREW, null, { Cookie: keks, Origin: `https://${CREW}` }); console.log(` genommen: ${alle[i].name} -> ${a.code}`); } /* ---- Und jetzt hinsehen -------------------------------------------- */ const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); const BREITE = Number(process.env.BREITE || 1500); const ANHANG = BREITE < 700 ? "-handy" : ""; async function hinsehen(titel, keks, marke) { const ctx = await browser.newContext({ viewport: { width: BREITE, height: BREITE < 700 ? 915 : 1100 }, deviceScaleFactor: 1, isMobile: BREITE < 700, hasTouch: BREITE < 700 }); await ctx.addCookies(keks.split("; ").filter(Boolean).map((z) => { const [name, ...rest] = z.split("="); return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; })); const seite = await ctx.newPage(); const schlimm = []; seite.on("console", (m) => { if (m.type() === "error") schlimm.push(m.text().slice(0, 120)); }); seite.on("requestfailed", (r) => schlimm.push(`${r.url().slice(-50)} (${r.failure()?.errorText})`)); seite.on("response", (r) => { if (r.status() >= 400) schlimm.push(`${r.url().slice(-50)} -> ${r.status()}`); }); await seite.goto(`${BASIS}/workspace/material.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1800); const z = await seite.evaluate(() => { const karten = [...document.querySelectorAll(".m-karte")]; const bilder = [...document.querySelectorAll(".m-karte__bild")]; const kasten = document.getElementById("neu-block"); const wegBlock = document.getElementById("weg-block"); /* Kontrast messen, wo es zaehlt: der Text unter dem Bild. */ const probe = document.querySelector(".m-karte__text, .m-karte__name"); const farbe = probe ? getComputedStyle(probe).color : ""; let hinter = ""; for (let e = probe; e; e = e.parentElement) { const h = getComputedStyle(e).backgroundColor; if (h && !/rgba\(0, 0, 0, 0\)/.test(h)) { hinter = h; break; } } return { titel: document.querySelector("h1")?.textContent?.trim() || "", unterzeile: (document.querySelector(".unterzeile")?.textContent || "") .replace(/\s+/g, " ").trim(), karten: karten.length, frei: document.querySelectorAll("#frei .m-karte").length, weg: document.querySelectorAll("#weg .m-karte").length, wegBlockDa: !!wegBlock && !wegBlock.hidden, kastenDa: !!kasten && !kasten.hidden, /* Wie viele Karten nennen einen NAMEN dessen, der es hat? */ mitNamen: [...document.querySelectorAll(".m-karte__stand")] .filter((e) => /hat es genommen/.test(e.textContent)).length, staende: [...new Set([...document.querySelectorAll(".m-karte__stand")] .map((e) => e.textContent.replace(/\s+/g, " ").trim()))], knoepfe: [...new Set([...document.querySelectorAll(".m-karte button")] .map((b) => b.textContent.trim()))], /* Sind die Karten gleich hoch? Verschiedene Hoehen in einem Raster sehen nach Versehen aus. */ hoehen: [...new Set(karten.map((k) => Math.round(k.getBoundingClientRect().height)))], /* UND STEHEN DIE KNOEPFE AUF EINER LINIE? Gleich hohe Karten genuegen dafuer nicht -- bei unterschiedlich langem Text bleibt unten unterschiedlich viel Luft, und die Knoepfe rutschen auseinander. Genau das war am 22.09. der Fall. */ knopfLinien: (() => { const erste = [...document.querySelectorAll("#frei .m-karte")] .map((k) => k.getBoundingClientRect().top); const oben = erste.length ? erste[0] : 0; return [...new Set([...document.querySelectorAll("#frei .m-karte")] .filter((k) => Math.abs(k.getBoundingClientRect().top - oben) < 2) .map((k) => { const b = k.querySelector("button, a.knopf"); return b ? Math.round(b.getBoundingClientRect().top) : null; }).filter((v) => v !== null))]; })(), wegHoehen: [...new Set([...document.querySelectorAll("#weg .m-karte")] .map((k) => Math.round(k.getBoundingClientRect().height)))], bilderGeladen: bilder.filter((b) => b.naturalWidth > 0).length, bilderGesamt: bilder.length, /* Laeuft etwas ueber den Rand hinaus? */ zuBreit: karten.filter((k) => k.getBoundingClientRect().right > document.documentElement.clientWidth + 1).length, spalten: (() => { const oben = karten.map((k) => Math.round(k.getBoundingClientRect().top)); return oben.length ? oben.filter((t) => t === oben[0]).length : 0; })(), seitenhoehe: document.body.scrollHeight, farbe, hinter, kopfSatz: (document.getElementById("frei-satz")?.textContent || "").trim(), wegSatz: (document.getElementById("weg-satz")?.textContent || "").trim(), }; }); console.log(""); console.log(`=== ${titel} ===`); console.log(` „${z.titel}" · ${z.unterzeile.slice(0, 90)}`); console.log(` Kasten zum Bereitstellen: ${z.kastenDa ? "DA" : "weg"}`); console.log(` ${z.frei} frei, ${z.weg} vergeben (Block „Schon benutzt": ${z.wegBlockDa ? "da" : "weg"})`); console.log(` Karten mit Namen: ${z.mitNamen} ${marke}`); console.log(` Zustaende: ${z.staende.join(" | ") || "(keine)"}`); console.log(` Knoepfe: ${z.knoepfe.join(" | ") || "(keine)"}`); console.log(` Vorschau geladen: ${z.bilderGeladen}/${z.bilderGesamt}`); console.log(` Raster: ${z.spalten} Spalten, ${z.hoehen.length} verschiedene Hoehen (${z.hoehen.join(", ")})`); console.log(` Knoepfe der ersten Zeile auf ${z.knopfLinien.length} Hoehe(n): ${z.knopfLinien.join(", ")}` + (z.knopfLinien.length > 1 ? " << stehen nicht auf einer Linie" : "")); console.log(` „Schon benutzt": Kartenhoehe ${z.wegHoehen.join(", ") || "(keine)"} px`); console.log(` ueber den Rand: ${z.zuBreit} · Seitenhoehe ${z.seitenhoehe} px`); console.log(` Text ${z.farbe} auf ${z.hinter}`); console.log(` oben: „${z.kopfSatz}"`); console.log(` unten: „${z.wegSatz}"`); if (schlimm.length) console.log(` !! ${[...new Set(schlimm)].join(" · ")}`); const pfad = `C:/Users/qciga/AppData/Local/Temp/material-${titel.toLowerCase() .replace(/[^a-z]+/g, "-").replace(/^-|-$/g, "")}${ANHANG}.png`; await seite.screenshot({ path: pfad, fullPage: true }); console.log(` -> ${pfad}`); await ctx.close(); return z; } const a1 = await hinsehen("DogFather", kDogi, "(erwartet: 2)"); const a2 = await hinsehen("Modi", kModi, "(erwartet: 0)"); const a3 = await hinsehen("Community", kGast, "(erwartet: 0)"); console.log(""); console.log("=== Was sich unterscheidet ==="); console.log(` Kasten: DogFather ${a1.kastenDa} · Modi ${a2.kastenDa} · Community ${a3.kastenDa}`); console.log(` Namen: DogFather ${a1.mitNamen} · Modi ${a2.mitNamen} · Community ${a3.mitNamen}`); console.log(` frei: ${a1.frei} / ${a2.frei} / ${a3.frei}`); await browser.close(); kind.kill(); await new Promise((r) => setTimeout(r, 400)); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }