Files
dogfather-universe/server/mess-startseite.mjs
T
DogFatherGitandClaude Opus 5 25de892fdb screen5: Aus dem Treff wird das Rudel, aus der Zentrale die IrrenAnstalt
Filipe, 22.09.2026: "ersetze die zentrale durch, Die IrrenAnstalt.
genau so geschrieben bitte. und alles was treff heißt oder wo treff
steht soll durch Rudel ersetzt werden bitte."

Die Schreibweise "IrrenAnstalt" mit grossem A in der Mitte ist so
gewollt. Das steht als Hinweis daneben, damit sie beim naechsten Mal
niemand "korrigiert".

WAS UMBENANNT WURDE: alles, was jemand LIEST -- Kachelnamen,
Ueberschriften, Markenzeilen, Saetze, Meldungen, Aufgabenvorlagen.
116 Zeilen in 42 Dateien.

WAS BLEIBT: Adressen (treff-regeln.html), Bezeichner (TREFF_ROLLEN),
Datenbankwerte (bereich = 'treff'), Abfrageteile (b=treff). Eine
Adresse umzubenennen bricht jedes Lesezeichen, und ein Datenbankwert
umzuschreiben waere eine Umstellung ohne Gegenwert. Dieselbe
Entscheidung wie heute frueh bei Dogi-Media, wo material.html auch
material.html geblieben ist.

DREI GRAMMATIKFEHLER IM EIGENEN ENTWURF, alle vor dem Ausliefern
gefunden -- ein blindes Ersetzen reicht hier nicht:

1. "der Treff" ist maennlich, "das Rudel" saechlich. Ohne Tabelle
   waere ueberall "Der Rudel" gestanden. (Und "Treffer" waere zu
   "Rudeler" geworden, "Treffen" zu "Rudelen" -- deshalb greift die
   Regel nur bei Wortgrenze und nie vor einem Kleinbuchstaben.)

2. BINDESTRICH-ZUSAMMENSETZUNGEN. In "der Treff-Chat" gehoert der
   Artikel zu "Chat", nicht zu "Treff". Der erste Durchlauf machte
   daraus "das Rudel-Chat", "das Rudel-Kacheln" und "ein Rudel-Raum".
   Jetzt greift die Artikelregel nur, wenn das Wort allein steht.

3. GESCHUETZTE LEERZEICHEN. In den Markenzeilen steht
   `Der Treff`, damit die zwei Woerter nicht umbrechen. Das
   Muster hat daran vorbeigegriffen: "Der Rudel", auf fuenf
   Seiten. Jetzt wird das Trennzeichen mitgefasst und unveraendert
   wieder eingesetzt.

Gefunden wurden alle drei, weil jede einzelne der 116 Zeilen vor dem
Schreiben als ALT/NEU ausgegeben und gelesen wurde -- und danach
gezielt nach falschen Artikeln gesucht ("den Rudel", "der Rudel",
"einen Rudel"). Uebrig blieben sechs Treffer, und die sind alle
richtig: "einen Rudel-Raum", "der Rudel-Chat", "den Rudel-Regeln" --
Zusammensetzungen, bei denen der Artikel zum letzten Wort gehoert.

UND ZWEI PRUEFUNGEN, die die Umbenennung nicht mitbekommen haetten:
`/Treff/.test(...)` und `/Treff/i.test(...)`. Ein regulaerer Ausdruck
ist keine Zeichenkette -- sie haetten ab sofort nach einem Wort
gesucht, das die Seite nicht mehr sagt, und waeren still rot geworden,
ohne dass etwas kaputt ist.

Geprueft, alle gruen: pruef-treff, pruef-treffchat,
pruef-deutsche-texte, pruef-uebernahme, pruef-crew-adresse,
pruef-willkommen, pruef-wege-nach-draussen, pruef-vorlagen,
pruef-start-ansicht, pruef-struktur.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 14:25:56 +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 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 } });
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);