Files
dogfather-universe/server/mess-anleitung.mjs
T
DogFatherGitandClaude Opus 5 df67574f90 Anleitung, Etappe 2: Erste Schritte zum Abhaken -- und eine Farbe, die vier Tage falsch war
Ein Creator hakt seine fuenf Ersten Schritte ab, sieht seinen
Fortschritt im Kopf, bekommt unter „Was ist dran" den Punkt „Erste
Schritte noch offen" und ganz oben auf der Startseite eine breite
Kachel „Fang hier an". Alle drei verschwinden von selbst, sobald er
durch ist -- niemand muss etwas wegklicken oder abschalten.

EINE QUELLE, DREI ANZEIGEN.

Der Bauplan wollte einen Punkt bei „Was ist dran" UND einen eigenen
Zaehler an der Kachel. Zwei getrennt gebaute Zaehler zeigen irgendwann
zwei verschiedene Zahlen uebereinander auf einem Bildschirm. Es gibt
deshalb nur `offeneHinweise()`: Daraus kommen die Zeile, die Zahl an
der kleinen Kachel und die Zahl an der breiten. Die breite Kachel
entsteht ausserdem aus der BEREITS GEFILTERTEN Kachelliste und erbt
damit beide Schranken (Rollenliste und Rechtetafel), statt sie ein
drittes Mal zu beantworten.

Gespeichert wird nur Erledigtes: eine Zeile heisst „abgehakt", keine
Zeile heisst „offen". Kein Feld mit 0 und 1 -- sonst gaebe es zwei
Arten, „offen" zu sagen.

DIE WICHTIGSTE ENTDECKUNG WAR NICHT TEIL DIESER ETAPPE.

`pruef-kachel-universum` wurde rot. Nachgemessen gegen den Stand VOR
Etappe 1: Sie war schon rot -- seit dem 02.10., und zwar wegen Ton 47
(Manager-Ziele, #7368ff), den ich selbst eingetragen habe. Im
Kommentar daneben steht sogar meine eigene Messung: „Abstand zum
naechsten Nachbarn 0,0899 ... knapp unter der 0,090". Ich habe die
Zahl hingeschrieben und die Pruefung nie laufen lassen. Vier Tage.

#7368ff -> #7269ff. Eine Hexziffer, Abstand zum alten Wert 0,0024 --
das sieht kein Mensch --, aber der Abstand zur naechsten Kachel im
eigenen Haus steigt von 0,0899 auf 0,0918. Nicht die am weitesten
entfernte Farbe genommen (das waere ein grelles Gruen gewesen):
Manager-Ziele ist in Benutzung und violett, gesucht war die kleinste
Aenderung, die die Regel haelt.

UND DIE REGEL SELBST WAR FUER EIN HAUS GESCHRIEBEN.

Auch mit sauberem Ton 47 blieb die Pruefung rot: Sie vergleicht ALLE
48 Toene miteinander, und rechnerisch ist 0,09 fuer 48 Farben nicht zu
halten (das Werkzeug findet als besten freien Platz 0,0793). Seit dem
24.09.2026 gibt es aber zwei Haeuser, und eine Kachel des einen steht
nie neben einer des anderen -- sie liegen auf verschiedenen Adressen.

Die Messung ist jetzt haus-bewusst: 0,09 innerhalb eines Hauses
(streng, und mit rund 24 Farden je Haus zu halten -- gemessen 0,0905),
ein Boden von 0,06 ueber die Haeuser hinweg. Das ist SCHAERFER, nicht
weicher: Vorher verteilte sich die Grenze auf 48 Farben und war
unerreichbar, weshalb die Pruefung dauerhaft rot stand -- und eine
Warnung, die immer kommt, ist keine mehr. Welcher Ton zu welchem Haus
gehoert, wird aus bereiche.js abgeleitet, nicht aufgelistet. Dazu eine
Gegenprobe, dass die Einteilung ueberhaupt trennt (553 Paare im Haus,
575 darueber hinweg).

NOCH EINE FESTE ZAHL ERSETZT: `pruef-start-ansicht` verlangte „genau
drei Gruppen" fuer einen Creator. Mit „Fang hier an" sind es
zeitweilig vier, und beides ist richtig. Gezaehlt wird nicht mehr,
sondern benannt -- auch das die schaerfere Pruefung: Die alte Zeile
waere gruen geblieben, wenn eine der drei Gruppen verschwindet und
eine fremde dazukommt.

Gemessen
  pruef-anleitung        70 Pruefungen, 0 Fehler (vorher 47)
  pruef-rollen          402 Pruefungen, 0 Fehler (vorher 401)
  pruef-kachel-universum 16 Pruefungen, 0 Fehler (vorher 13, davon 1 rot)
  pruef-start-ansicht     alles in Ordnung (vorher 1 rot)
  pruef-kachelraster      24 Pruefungen, 0 Fehler
  pruef-css-klassen       alles in Ordnung, 42 Stellen unter 11,5 px
  mess-anleitung          mit offenen Schritten: Gruppe „Fang hier an",
                          breite Kachel mit Zaehler 5, Hinweiszeile.
                          Nach dem Abhaken: alle drei weg, ein Weg
                          statt drei. Kein Querschieben bei 412/1440 px.

Sicherung: ~/sicherungen/workspace-vor-anleitung-e2-20261006-1421.db

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-06 14:22:07 +02:00

272 lines
12 KiB
JavaScript

/* =====================================================================
WIE DIE ANLEITUNG AUSSIEHT (06.10.2026, Etappe 1)
SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder,
auf denen man nachsehen kann, und misst vier Dinge, die man auf
einem Bild schlecht sieht: waagerechtes Schieben, Beruehrziele unter
40 px, Fehler in der Browserkonsole und ob der Filter wirklich etwas
tut.
Deshalb `mess-` und nicht `pruef-`.
ZWEI BREITEN, und die kleinere ist die wichtigere: 412 px ist die
haeufigste Android-Breite. Am 06.09.2026 hat genau diese Breite im
Creator Workspace eine Ueberdeckung aufgedeckt, bei der ein Knopf
ueber einem anderen lag -- auf dem Computer war nichts zu sehen.
EIGENE WEGWERF-DATENBANK in einem mktemp-Ordner. In einem laufenden
System waeren das Testdaten, und die sind verboten.
Aufrufen mit: node server/mess-anleitung.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta, "mess-anleitung");
const ordner = mkdtempSync(join(tmpdir(), "ws-anl-bild-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
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-anleitung");
await new Promise((r) => setTimeout(r, 900));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
/* ---------- Leute ----------------------------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetztIso = new Date().toISOString();
const kenn = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", kenn, jetztIso);
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", kenn).update(code).digest("hex"), jetztIso);
}
anlegen("Luna", "creator", "CODE-LUNA-0001");
anlegen("Filipe", "admin", "CODE-DOGI-0001");
d.close();
function anfrage(pfad, methode, koerper, keks) {
return new Promise((fertig, schief) => {
const daten = koerper ? JSON.stringify(koerper) : null;
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: {
Origin: `http://127.0.0.1:${PORT}`,
...(keks ? { Cookie: keks } : {}),
...(daten ? { "Content-Type": "application/json",
"Content-Length": Buffer.byteLength(daten) } : {}),
},
}, (antwort) => {
let text = "";
antwort.on("data", (t) => { text += t; });
antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers, text }));
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
}
async function keksFuer(rolle, code) {
const an = await anfrage("/workspace/api/anmelden", "POST", { rolle, code });
if (an.code !== 200) { console.log(`ABBRUCH: Anmeldung ${rolle} ${an.code}`); process.exit(2); }
return (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
}
const keksCreator = await keksFuer("creator", "CODE-LUNA-0001");
const keksDogi = await keksFuer("admin", "CODE-DOGI-0001");
/* ---------- Bilder ---------------------------------------------------- */
const pw = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch();
const nachmessen = () => ({
schiebtQuer: document.documentElement.scrollWidth > window.innerWidth + 2,
zuKlein: [...document.querySelectorAll("button, a[href], summary")]
.filter((e) => {
const k = e.getBoundingClientRect();
return k.width > 0 && k.height > 0 && k.height < 40;
})
.map((e) => `${e.tagName.toLowerCase()}.${(e.className || "-")}: ${Math.round(e.getBoundingClientRect().height)}px`),
karten: document.querySelectorAll(".an-karte").length,
/* Je Stufe: wie viele Karten, und ist der Kasten offen? */
stufen: [...document.querySelectorAll(".an-stufe")].map((s) => ({
stufe: s.dataset.stufe,
zahl: s.querySelector(".an-stufe__zahl")?.textContent,
offen: !!s.querySelector(".an-stufe__falte")?.open,
sichtbar: !s.hidden,
})),
/* Tragen alle Karten den Ton ihrer Kachel? Eine ohne waere grau. */
ohneTon: [...document.querySelectorAll(".an-karte")]
.filter((k) => !k.dataset.ton)
.map((k) => k.querySelector(".an-karte__name")?.textContent),
schritte: document.querySelectorAll(".an-schritt").length,
saetze: document.querySelectorAll(".an-saetze__zeile").length,
nicht: document.querySelectorAll(".an-nicht__zeile").length,
takt: document.querySelectorAll(".an-takt__wann").length,
fragen: document.querySelectorAll(".an-frage").length,
ohne: document.getElementById("ohne-fassung")?.hidden === false
? document.getElementById("ohne-fassung").textContent.slice(0, 70) : null,
/* Etappe 2 */
haken: document.querySelectorAll(".an-haken").length,
abgehakt: document.querySelectorAll(".an-haken__feld:checked").length,
stand: document.getElementById("stand")?.hidden === false
? document.getElementById("stand-satz").textContent : "(kein Fortschritt)",
});
async function bild(name, keks, breite, hoehe, vorher) {
const kontext = await browser.newContext({
viewport: { width: breite, height: hoehe }, hasTouch: breite <= 860,
});
await kontext.addCookies(keks.split("; ").map((k) => {
const [n, ...r] = k.split("=");
return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (e) => {
if (e.type() === "error" && !/favicon|net::ERR/i.test(e.text())) konsole.push(e.text().slice(0, 160));
});
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
await seite.goto(`http://127.0.0.1:${PORT}/workspace/anleitung.html`,
{ waitUntil: "networkidle" });
await seite.waitForTimeout(1000);
if (vorher) await vorher(seite);
await seite.waitForTimeout(400);
const blick = await seite.evaluate(nachmessen);
await seite.screenshot({ path: `bild-anleitung-${name}.png`, fullPage: true });
console.log(`\n--- ${name} (${breite}px) ---`);
if (blick.ohne) console.log(` Ohne Fassung: ${blick.ohne}…`);
console.log(` Karten: ${blick.karten} Schritte: ${blick.schritte} `
+ `Saetze: ${blick.saetze} Nicht deins: ${blick.nicht} `
+ `Takt: ${blick.takt} Fragen: ${blick.fragen}`);
for (const s of blick.stufen) {
console.log(` ${String(s.stufe).padEnd(13)} ${String(s.zahl).padEnd(3)} `
+ `${s.offen ? "offen" : "zu "} ${s.sichtbar ? "" : "(ausgeblendet)"}`);
}
console.log(` Fortschritt: ${blick.stand} Haken: ${blick.haken} (${blick.abgehakt} gesetzt)`);
console.log(` Karten ohne Farbe: ${blick.ohneTon.length ? blick.ohneTon.join(", ") : "keine"}`);
console.log(` Quer schieben: ${blick.schiebtQuer ? "JA — Befund!" : "nein"}`);
console.log(` Zu kleine Ziele: ${blick.zuKlein.length ? blick.zuKlein.join(", ") : "keine"}`);
if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`);
await kontext.close();
return blick;
}
await bild("creator-handy", keksCreator, 412, 950);
await bild("creator-rechner", keksCreator, 1440, 1000);
/* Der Filter muss etwas TUN. Auf einem Bild sieht man nur den
gedrueckten Knopf -- ob die anderen Kaesten verschwinden, nicht. */
await bild("creator-nur-muss", keksCreator, 412, 950, async (seite) => {
await seite.click('.an-filter__knopf[data-wert="muss"]');
});
/* DogFather ohne gewaehlte Sicht: Er bekommt keine Fassung, sondern
eine Auskunft. Das Bild zeigt, ob die wie eine Erklaerung aussieht
und nicht wie ein Fehler. */
await bild("dogfather-ohne-sicht", keksDogi, 412, 950);
/* =====================================================================
DIE BREITE KACHEL AUF DER STARTSEITE (Etappe 2)
Sie entsteht nur, solange Erste Schritte offen sind. Beide Zustaende
werden gemessen -- ein Bild von "sie ist da" allein beweist nicht,
dass sie auch wieder verschwindet, und genau das ist ihr Versprechen.
===================================================================== */
async function startBild(name, keks, breite) {
const kontext = await browser.newContext({
viewport: { width: breite, height: 1100 }, hasTouch: breite <= 860,
});
await kontext.addCookies(keks.split("; ").map((k) => {
const [n, ...r] = k.split("=");
return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (e) => {
if (e.type() === "error" && !/favicon|net::ERR/i.test(e.text())) konsole.push(e.text().slice(0, 160));
});
await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(1400);
const blick = await seite.evaluate(() => {
const gruppen = [...document.querySelectorAll(".gruppe")].map((g) =>
g.querySelector(".gruppe__name")?.textContent);
const breiteKachel = [...document.querySelectorAll('.kachel[data-gross="ja"]')]
.map((k) => k.querySelector(".kachel__name")?.textContent
+ " [" + (k.querySelector(".kachel__zahl")?.textContent || "ohne Zahl") + "]");
const dran = [...document.querySelectorAll(".dran__text")].map((e) => e.textContent);
return { gruppen, breiteKachel, dran,
anleitungKacheln: [...document.querySelectorAll('a[href="anleitung.html"]')].length };
});
/* Ganze Seite: Die breite Kachel sitzt unter der Zentrale und waere
im Ausschnitt nicht zu sehen -- also waere das Bild kein Beleg. */
await seite.screenshot({ path: `bild-anleitung-${name}.png`, fullPage: true });
console.log(`
--- ${name} (${breite}px, Startseite) ---`);
console.log(` Gruppen: ${blick.gruppen.join(" | ")}`);
console.log(` Breite Kacheln: ${blick.breiteKachel.length ? blick.breiteKachel.join(", ") : "keine"}`);
console.log(` Wege zur Anleitung: ${blick.anleitungKacheln}`);
console.log(` Was ist dran: ${blick.dran.join(" | ") || "nichts"}`);
if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`);
await kontext.close();
return blick;
}
await startBild("start-mit-schritten", keksCreator, 412);
/* Jetzt alle fuenf abhaken -- ueber die Schnittstelle, wie es die
Seite auch tut -- und noch einmal hinsehen. */
const standVorher = await anfrage("/workspace/api/anleitung", "GET", null, keksCreator);
if (standVorher.code !== 200) {
console.log(`ABBRUCH: Anleitung nicht abrufbar (${standVorher.code})`);
process.exit(2);
}
const schritte = JSON.parse(standVorher.text).schritte || [];
for (const sch of schritte) {
await new Promise((fertig, schief) => {
const daten = JSON.stringify({ erledigt: true });
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: `/workspace/api/anleitung/schritt/${sch.id}`,
method: "PUT",
headers: { Cookie: keksCreator, "Content-Type": "application/json",
"Content-Length": Buffer.byteLength(daten), Origin: `http://127.0.0.1:${PORT}` },
}, (antwort) => { antwort.on("data", () => {}); antwort.on("end", fertig); });
a.on("error", schief);
a.write(daten); a.end();
});
}
console.log(`
(${schritte.length} Schritte abgehakt)`);
await startBild("start-ohne-schritte", keksCreator, 412);
await bild("creator-fertig", keksCreator, 412, 950);
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log("\nBilder geschrieben: bild-anleitung-*.png");
process.exit(0);