Files
dogfather-universe/server/mess-startseite.mjs
T
DogFatherGitandClaude Opus 5 b57b9cebb5 Der Ring oben zeigt Arbeit statt der Uhrzeit
Auf der Startseite von Creator, rechter Hand und Modis ist der Ring das
groesste Element, ganz oben. Er rechnete:

    prozent = (aktuelle Stunde - 6) / 18

...und nannte das "Tag geschafft". Um 9 Uhr also 17 %, um 23 Uhr 94 %
-- unabhaengig davon, ob jemand etwas getan hatte.

Aufgefallen auf einem Bildschirmfoto: Bei einer Modi stand "0 % TAG
GESCHAFFT", waehrend sie ueberhaupt nichts offen hatte. Beide Angaben
waren fuer sich richtig und zusammen Unsinn -- und es war das Erste,
was sie sah.

JETZT ZAEHLT ER ARBEIT. Nenner ist, was heute auf dem Tisch liegt:
heute faellig ODER laenger offen. Die naheliegende Rechnung ueber "nur
heute faellig" waere derselbe Fehler noch einmal gewesen -- bei drei
ueberfaelligen Sachen haette der Ring "nichts faellig" gemeldet.
Zaehler ist, was HEUTE fertig wurde (erledigt_am), nicht was irgendwann
abgehakt wurde. Liegt nichts an: voller Ring, und der Titel sagt es in
Worten statt nur eine 100 zu zeigen.

Gemessen danach, dieselbe Seite: "33 % Heute geschafft", darunter
"2 Sachen sind ueberfaellig -- das zuerst". Die Seite erzaehlt jetzt
eine Geschichte statt zweier.

NEU: pruef-zentrale-ring (10 Pruefungen)
Drei Lagen, drei Menschen: etwas anliegend und teils fertig / nichts
anliegend / drei ueberfaellige und nichts fertig. Dazu zwei
Abgrenzungen (morgen faellig und vorgestern erledigt duerfen nicht
mitzaehlen) und die Gegenprobe: zwischen zwei Abfragen wird eine
Aufgabe fertig gemacht, der Ring MUSS springen -- er tut es, 0 % -> 33 %.
Der letzte Abschnitt rechnet die alte Uhrzeit-Formel mit und meldet,
falls der Ring je wieder auf ihren Wert faellt.

NEU: mess-startseite -- ein Messwerkzeug, kein Pruefwerkzeug
Es sagt nicht richtig/falsch, sondern wie viel da ist: Seitenlaenge in
Handy-Bildschirmen, Bedienelemente, Woerter, Ring, je Rolle, mit
echten Aufgaben statt im leeren Zustand. Gemessener Stand:

    admin 7,0 Schirme / 58 Bedienelemente   hand    5,7 / 43
    modi  5,0 / 37                          manager 4,9 / 38
    scout 4,5 / 34                          creator 4,3 / 32

pruef-sicht, pruef-verborgen, pruef-modi-verborgen und
pruef-haus-trennung laufen unveraendert durch -- die neue Abfrage
folgt der fremden Sicht ueber dieselbe Variable wie der Rest.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-17 02:21:12 +02:00

189 lines
8.9 KiB
JavaScript

/* =====================================================================
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 { portMussFreiSein } from "./helfer-port.mjs";
const PORT = await portMussFreiSein(4471, "mess-startseite");
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 Treffs 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 } });
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-<rolle>.png");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(0);