/* ===================================================================== WIE VIEL STEHT AUF DER STARTSEITE? (17.09.2026) Kein Pruefwerkzeug -- ein MESSWERKZEUG. Es sagt nicht "richtig" oder "falsch", es sagt, wie viel da ist. Anlass ist Filipes Wunsch, dass alles "so einfach wie moeglich" wird, und dafuer braucht es zuerst eine Zahl statt eines Eindrucks. GEMESSEN WIRD IN HANDY-BILDSCHIRMEN, nicht in Pixeln. 2955 px sagt niemandem etwas; "acht Bildschirme scrollen" sagt es sofort. UND DIE WICHTIGSTE ZAHL IST NICHT DIE LAENGE, sondern: Wie weit muss jemand scrollen, bis er das Erste sieht, das er ANFASSEN kann? Alles darueber ist Lage, nicht Handlung. Ein Werkzeug, bei dem die Arbeit erst im zweiten Bildschirm anfaengt, ist kein Werkzeug, sondern ein Bericht. MIT ECHTEN AUFGABEN, nicht im leeren Zustand -- sonst misst man den ersten Tag und nicht den Alltag. Der leere Zustand wird zusaetzlich gemessen, weil er der erste Eindruck ist. Aufrufen mit: node server/mess-startseite.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { eigenerPort } from "./helfer-port.mjs"; const PORT = await eigenerPort(import.meta); const ordner = mkdtempSync(join(tmpdir(), "ws-mess-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; 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 }); }; import { notbremse } from "./helfer-notbremse.mjs"; await import("./index.js"); notbremse(240_000, "mess-startseite"); await new Promise((r) => setTimeout(r, 700)); const BASIS = `http://127.0.0.1:${PORT}`; /* ---------- Personen anlegen ---------------------------------------- */ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const schluessel = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", schluessel, jetzt); 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, code_kennung, aktiv, erstellt)" + " VALUES (?,?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, createHmac("sha256", schluessel).update(code).digest("hex"), jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const leute = [ { rolle: "admin", kachel: "admin", name: "Filipe", code: "CODE-DOGI-0001" }, { rolle: "manager", kachel: "manager", name: "Schulle", code: "CODE-MAN-0001" }, { rolle: "scout", kachel: "scout", name: "Nane", code: "CODE-SCT-0001" }, { rolle: "creator", kachel: "creator", name: "Tili", code: "CODE-CRE-0001" }, { rolle: "hand", kachel: "creator", name: "Rieke", code: "CODE-HAND-0001" }, { rolle: "modi", kachel: "creator", name: "Marina", code: "CODE-MODI-0001" }, ]; for (const p of leute) p.id = anlegen(p.name, p.rolle, p.code); /* ---------- Aufgaben, damit der Alltag gemessen wird ---------------- */ const tag = (v) => { const x = new Date(Date.now() + v * 86400_000); const z = (n) => String(n).padStart(2, "0"); return `${x.getFullYear()}-${z(x.getMonth() + 1)}-${z(x.getDate())}`; }; const spalten = d.prepare("PRAGMA table_info(aufgaben)").all().map((s) => s.name); function aufgabe(personId, titel, status, frist) { const feld = { titel, status, verantwortlich_id: personId, erstellt: jetzt }; if (status === "erledigt" && spalten.includes("erledigt_am")) feld.erledigt_am = jetzt; if (spalten.includes("frist")) feld.frist = frist; if (spalten.includes("erstellt_von")) feld.erstellt_von = personId; const namen = Object.keys(feld).filter((n) => spalten.includes(n)); d.prepare(`INSERT INTO aufgaben (${namen.join(",")}) VALUES (${namen.map(() => "?").join(",")})`) .run(...namen.map((n) => feld[n])); } const modi = leute.find((p) => p.rolle === "modi"); for (const p of leute) { aufgabe(p.id, "Regeln des Rudels lesen", "offen", tag(-2)); aufgabe(p.id, "Chat vor der Sendung pruefen", "offen", tag(0)); aufgabe(p.id, "Drei Zeilen nach der Schicht", "offen", tag(1)); aufgabe(p.id, "Clip zuschneiden", "arbeit", tag(2)); aufgabe(p.id, "Gefilterte Kommentare durchsehen", "erledigt", tag(-1)); } d.close(); /* ---------- Anmelden ------------------------------------------------- */ async function anmelden(kachel, code) { const a = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: kachel, code }), }); return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); } /* ---------- Messen ---------------------------------------------------- */ const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await chromium.launch(); const BREITE = 390, HOEHE = 844; const bildOrdner = fileURLToPath(new URL("./", import.meta.url)); const ergebnisse = []; try { for (const p of leute) { const keks = await anmelden(p.kachel, p.code); if (!keks) { console.log(` !! ${p.rolle}: Anmeldung fehlgeschlagen`); continue; } const kontext = await browser.newContext({ viewport: { width: BREITE, height: HOEHE }, hasTouch: BREITE <= 860 }); await kontext.addCookies(keks.split("; ").map((k) => { const [name, ...rest] = k.split("="); return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; })); const seite = await kontext.newPage(); await seite.goto(`${BASIS}/workspace/start.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(900); const m = await seite.evaluate(() => { const hoehe = document.documentElement.scrollHeight; /* WAS KANN MAN ANFASSEN? Knoepfe, Verweise, Felder -- aber nur, was sichtbar ist und im Inhalt steht, nicht die Kopfleiste: Die ist auf jeder Seite gleich und zaehlt nicht zur Seite. */ const inhalt = document.querySelector("main") || document.body; const anfassbar = [...inhalt.querySelectorAll("a[href], button, input, select, textarea")] .filter((z) => z.offsetParent !== null && !z.closest(".kopfleiste") && !z.closest("header")); const erstes = anfassbar[0]; const yErstes = erstes ? Math.round(erstes.getBoundingClientRect().top + window.scrollY) : -1; /* Zahlen ohne Handlung: die grossen Zaehler-Kacheln. */ const zaehler = inhalt.querySelectorAll( "[class*='zaehler'], [class*='kennzahl'], [class*='statistik'], [class*='stapel__wert']").length; const woerter = (inhalt.innerText || "").trim().split(/\s+/).filter(Boolean).length; const ring = (document.getElementById("zring-prozent") || {}).textContent || ""; const ringUnter = (document.getElementById("zring-unter") || {}).textContent || ""; return { hoehe, anfassbar: anfassbar.length, yErstes, zaehler, woerter, ring, ringUnter, erstesWort: erstes ? (erstes.innerText || erstes.value || "").trim().slice(0, 30) : "" }; }); await seite.screenshot({ path: join(bildOrdner, `mess-start-${p.rolle}.png`), fullPage: true }); await kontext.close(); ergebnisse.push({ ...p, ...m }); } } finally { await browser.close(); } /* ---------- Ausgabe --------------------------------------------------- */ console.log(""); console.log(`Handy ${BREITE} x ${HOEHE} px, je 5 Aufgaben (1 ueberfaellig, 1 heute)`); console.log(""); console.log("Rolle".padEnd(10) + "Hoehe".padStart(8) + "Schirme".padStart(9) + "Bedien.".padStart(9) + "1. Ding".padStart(9) + "Woerter".padStart(9) + " Ring oben"); for (const e of ergebnisse) { console.log( e.rolle.padEnd(10) + `${e.hoehe}px`.padStart(8) + (e.hoehe / HOEHE).toFixed(1).padStart(9) + String(e.anfassbar).padStart(9) + (e.yErstes < 0 ? "-" : `${(e.yErstes / HOEHE).toFixed(1)}`).padStart(9) + String(e.woerter).padStart(9) + " " + (e.ring + " " + e.ringUnter).trim()); } console.log(""); console.log("Schirme = Seitenlaenge in Handy-Bildschirmen"); console.log("1. Ding = nach wie vielen Bildschirmen das erste Bedienelement kommt"); console.log("Bilder = server/mess-start-.png"); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } process.exit(0);