Files
dogfather-universe/tools/community-blick.mjs
T
DogFatherGitandClaude Opus 5 11c4a795e2 Community: Starthilfe beim Schreiben -- und vier Funde aus den Bildern
Filipe: "perfektioniere jede einzelne kategorie der community ... ich
will auch dass du fertige und beispiele machst fuer die community
damit die auch start hilfe haben ueberall."

STARTHILFE: Wer zum ersten Mal schreibt, steht vor einem leeren Feld
-- der haeufigste Grund, warum ein Brett still bleibt, obwohl viele
mitlesen. Im Formular stehen jetzt Beispiele: ein Klick fuellt Art,
Ueberschrift und Text. Auf treff, wunsch, highlight und mitmachen --
nicht auf anschlag, ansteht, regeln, denn dort schreibt die Community
nicht, und ein Angebot fuer eine verschlossene Tuer ist keins.

JEDE VORLAGE HAT LUECKEN. Eine, die man absenden KANN, wird
abgesendet; dann steht dort zehnmal derselbe Satz, und der elfte
Mensch merkt, dass hier niemand wirklich schreibt. Die Pruefung misst
das: keine Vorlage ohne "…".

NICHT ZU VERWECHSELN MIT DEM STARTKATALOG. Der ist fuers Team (fertige
Beitraege zum Uebernehmen), das hier fuer die Mitglieder.

--- Vier Funde, drei davon nur aus dem Bild -------------------------

1. ZWEI KNOPFREIHEN MIT DENSELBEN WOERTERN. Auf "Regeln & Hilfe"
   standen "Regel · Haeufige Frage · Hilfe · Was passiert, wenn"
   zweimal untereinander -- die eine filterte, die andere sprang zur
   Ueberschrift, und beide sahen gleich aus. Wo nach Art gegliedert
   wird, entfaellt jetzt der Filter; die Sprungmarken sind Links
   geworden, mit "Springe zu" davor und der Anzahl dahinter.

2. "MELDEN" UNTER JEDER REGEL. Siebzehn Melden-Knoepfe unter Texten
   des Teams -- man meldet keine Regel. Steht jetzt nur noch an
   Beitraegen von Mitgliedern. Die Gegenprobe im Messwerkzeug legt
   dafuer eigens einen Beitrag eines zweiten Mitglieds an: Ohne ihn
   saehe ein abgeschaffter Meldeweg genauso aus wie ein
   funktionierender.

3. DER HINWEIS LAG AUF DER NAECHSTEN BESCHRIFTUNG. Auf 412 px stand
   "Gehoert allen hier - wer Der Treff sieht ..." quer ueber
   "UEBERSCHRIFT". Der Hinweis haengt absolut unter dem Feld, und
   darunter sind 30 px Luft -- genug fuer EINE Zeile. Unter 560 px
   steht ohnehin nur ein Feld je Zeile; dort gibt es nichts
   auszurichten, und er darf einfach im Fluss stehen.

4. UND MEIN EIGENER FEHLER, der alles unsichtbar machte: `vorlagen`
   gab es hier schon -- als Checkliste fuer Creator, mit demselben
   Behaelter `#vorlagen`. Die spaetere Funktionsdeklaration gewinnt,
   zwei Elemente trugen dieselbe Kennung, und die Starthilfe erschien
   NIE. Keine Fehlermeldung, keine rote Pruefung, nichts in der
   Konsole. Gefunden nur durch einen Blick auf das Bild.
   Deshalb prueft pruef-vorlagen jetzt im Browser mit, dass keine
   Kennung doppelt vorkommt -- derselbe Fehler wie am 17.09. mit
   `.teilen`, und beim naechsten Mal soll ihn nicht der Zufall finden.

Nebenbei messbar: "Regeln & Hilfe" ist von 4569 auf 3623 px
geschrumpft, die Karten sind 54 px niedriger.

Geprueft: pruef-vorlagen (neu, 24), pruef-video unveraendert 67.
Gegenproben: unbekannte Art -> rot; Vorlage ohne Luecke -> rot.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-18 19:00:43 +02:00

233 lines
11 KiB
JavaScript

/* =====================================================================
DIE SIEBEN COMMUNITY-BRETTER MIT DEN AUGEN EINES MITGLIEDS
Aufruf: node tools/community-blick.mjs
BREITE=412 node tools/community-blick.mjs (Handy)
WARUM ALS GAST UND NICHT ALS ADMIN. Als DogFather sieht man auf
jedem Brett Knoepfe, Vorschlaege und Felder, die ein Mitglied nie zu
Gesicht bekommt. Wer die Bretter so beurteilt, beurteilt eine Seite,
die es fuer die Community nicht gibt -- und uebersieht genau das,
was ihr fehlt.
Der Startkatalog wird vorher vom Team uebernommen, damit die Bretter
nicht leer sind. Leere Bretter zu fotografieren beantwortet nur die
Frage, ob sie leer sind.
===================================================================== */
import { mkdtempSync, rmSync } 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 HIER = join(dirname(fileURLToPath(import.meta.url)), "..", "server");
const PORT = Number(process.env.PORT_BLICK || 4364);
const BASIS = `http://127.0.0.1:${PORT}`;
const ordner = mkdtempSync(join(tmpdir(), "ws-comm-"));
const DB = join(ordner, "workspace.db");
const BRETTER = ["treff", "anschlag", "ansteht", "wunsch", "highlight", "regeln", "mitmachen"];
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("Mara", "gast", "CODE-GAST-0001");
/* Ein ZWEITES Mitglied. Den eigenen Beitrag meldet niemand -- er
laesst sich loeschen, das ist der kuerzere Weg. Ohne dieses zweite
Konto misst die Gegenprobe unten also nicht den Meldeweg, sondern
diese Regel. */
personAnlegen("Jonas", "gast", "CODE-GAST-0002");
await start();
/** Meldet an -- mit Host-Kopfzeile, weil die Rolle davon abhaengt.
*
* „gast" gibt es nur im Haus der Community. Der Host entscheidet
* darueber, und ein `fetch` auf 127.0.0.1 schickt eben 127.0.0.1.
* Deshalb hier ueber node:http, wo sich der Kopf setzen laesst.
*
* ALTERSBESTAETIGUNG: Wer sich zum ersten Mal in der Community
* anmeldet, muss bestaetigen, dass er 18 ist -- sonst antwortet der
* Server mit 400 „alter_offen". */
function rohAnfrage(pfad, host, koerper) {
return new Promise((f, s2) => {
const a = anfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: "POST",
headers: { Host: host, "Content-Type": "application/json" },
}, (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);
a.write(JSON.stringify(koerper));
a.end();
});
}
async function anmelden(rolle, code, host = "127.0.0.1") {
let a = await rohAnfrage("/workspace/api/anmelden", host, { rolle, code });
if (a.code === 400 && a.text.includes("alter_offen")) {
a = await rohAnfrage("/workspace/api/anmelden", host,
{ rolle, code, alter_ok: true });
}
if (a.code !== 200) {
throw new Error(`Anmeldung ${rolle}@${host} -> ${a.code} ${a.text.slice(0, 120)}`);
}
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
}
/* ---- Das Team uebernimmt die Vorschlaege, damit nichts leer ist ---- */
const kAdmin = await anmelden("admin", "CODE-DOGI-0001");
console.log("=== Startkatalog uebernehmen ===");
for (const b of BRETTER) {
const a = await fetch(`${BASIS}/workspace/api/bereich/${b}/start`, {
method: "POST",
headers: { "Content-Type": "application/json", cookie: kAdmin, origin: BASIS },
body: JSON.stringify({}),
});
const r = await a.json().catch(() => ({}));
console.log(` ${b.padEnd(11)} ${a.status} ${r.angelegt ?? r.anzahl ?? ""}`);
}
/* ---- EIN BEITRAG VON EINEM MITGLIED --------------------------------
GEGENPROBE ZUM MELDEWEG. „melden" steht seit dem 18.09.2026 nicht
mehr unter Beitraegen des Teams -- unter einer Regel ergab der Knopf
keinen Sinn. Die Gefahr dabei: dass er dadurch auch dort
verschwindet, wo er gebraucht wird.
Deshalb bekommt der Treff hier einen Beitrag, der von einem
MITGLIED stammt. Unter ihm muss der Knopf stehen. Ohne diesen
Eintrag saehe eine abgeschaffte Meldefunktion genauso aus wie eine
funktionierende: Im Test schreibt sonst nur das Team.
Direkt in die Datenbank und nicht ueber die Schnittstelle: Ein
frisches Mitglied darf sieben Tage lang nicht schreiben, und diese
Regel soll gelten bleiben. */
{
const d = new DatabaseSync(DB);
const wer = d.prepare("SELECT id FROM personen WHERE rolle = 'gast' AND name = 'Jonas'").get();
d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von)
VALUES ('treff','frage',?,?,?, 'offen', ?, ?)`)
.run("Kommt Casper auch mal in den Stream?",
"Ich schaue meistens abends und habe ihn noch nie live gesehen.",
new Date().toISOString().slice(0, 10), new Date().toISOString(), wer.id);
d.close();
}
/* ---- Und jetzt als Mitglied hinsehen ------------------------------ */
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
/* DIE ECHTE ADRESSE, nicht 127.0.0.1.
Der Server entscheidet ueber den Host-Kopf, ob er im Haus der
Community steht („crew.dogfather-universe.com") oder in dem der
Agentur. Auf 127.0.0.1 ist er in der Agentur -- und dort gibt es die
Rolle „gast" nicht. Der erste Versuch hat deshalb sieben Mal die
ANMELDESEITE fotografiert, mit „0 Karten" und ohne Schreibfeld, und
das sah aus wie ein Befund ueber die Bretter.
Chromium loest den Namen auf Wunsch selbst auf. */
const browser = await pw.chromium.launch();
const BREITE = Number(process.env.BREITE || 1500);
const ctx = await browser.newContext({
viewport: { width: BREITE, height: BREITE < 700 ? 915 : 1100 },
deviceScaleFactor: 1, isMobile: BREITE < 700, hasTouch: BREITE < 700 });
const seite = await ctx.newPage();
const fehlerAufSeite = [];
seite.on("console", (m) => { if (m.type() === "error") fehlerAufSeite.push(m.text()); });
/* Welche Dateien kommen NICHT an? Ohne diese Zeile sieht man nur eine
nackte Seite und raet, woran es liegt. */
const nichtGeladen = [];
seite.on("requestfailed", (r) => nichtGeladen.push(`${r.url().slice(-60)} (${r.failure()?.errorText})`));
seite.on("response", (r) => { if (r.status() >= 400) nichtGeladen.push(`${r.url().slice(-60)} -> ${r.status()}`); });
/* DIE SITZUNG EINES GASTES IN DEN BROWSER LEGEN.
Direkt im Browser anzumelden ginge nur ueber die echte Adresse --
und dort schickt der Server `Strict-Transport-Security`, woraufhin
Chromium alle Unterdateien auf https umbiegt. Der Testserver spricht
aber nur http: Ergebnis war eine nackte Seite ohne Stil und ohne
Skripte, auf der kein Knopf mehr etwas tat. Die Kopfzeile ist
richtig so und bleibt.
Also: anmelden, wo es geht (ueber die Kopfzeile), und das
Sitzungsplaetzchen in den Browser legen. Gemessen wird danach das,
was ein Mitglied wirklich sieht. */
const kGast = await anmelden("gast", "CODE-GAST-0001", "crew.dogfather-universe.com");
await ctx.addCookies(kGast.split("; ").filter(Boolean).map((z) => {
const [name, ...rest] = z.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
console.log("");
console.log("=== Was ein Mitglied sieht ===");
for (const b of BRETTER) {
await seite.goto(`${BASIS}/workspace/bereich.html?b=${b}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(1500);
const z = await seite.evaluate(() => ({
titel: document.querySelector("h1")?.textContent?.trim() || "",
karten: document.querySelectorAll('.eintrag-karte').length,
arten: [...new Set([...document.querySelectorAll('.eintrag-art, .art')].map((e) => e.textContent.trim()))].length,
knoepfeJeKarte: document.querySelector('.eintrag-karte')
? document.querySelector('.eintrag-karte').querySelectorAll('button, a.schritt').length : 0,
/* Steht „melden" dort, wo es hingehoert -- und nur dort? */
meldenGesamt: [...document.querySelectorAll('.eintrag-karte button')]
.filter((b) => b.textContent.trim() === 'melden').length,
schreibfeld: !!document.querySelector('#neu-form, .neu-form, form#neu'),
hoeheJeKarte: document.querySelector('.eintrag-karte')
? Math.round(document.querySelector('.eintrag-karte').getBoundingClientRect().height) : 0,
startkatalog: !!document.querySelector("#startkatalog")?.children?.length,
leerText: document.querySelector(".leer, .leer-hinweis, .hinweis")?.textContent?.trim().slice(0, 80) || "",
hoehe: document.body.scrollHeight,
/* Sichtbare Sternchenpaare -- gemeint war Fettschrift. */
sternchen: (document.body.innerText.match(/\*\*/g) || []).length,
}));
console.log(`${b.padEnd(11)} „${z.titel}" · ${z.karten} Karten`
+ ` · Schreibfeld: ${z.schreibfeld ? "ja" : "NEIN"}`
+ ` · Karte ${z.hoeheJeKarte}px · melden: ${z.meldenGesamt}`
+ (z.sternchen ? ` · STERNCHEN: ${z.sternchen}` : "")
+ (z.leerText ? ` · „${z.leerText}"` : ""));
const pfad = `C:/Users/qciga/AppData/Local/Temp/comm-${b}${BREITE < 700 ? "-handy" : ""}.png`;
await seite.screenshot({ path: pfad, fullPage: true });
}
console.log("");
console.log(fehlerAufSeite.length
? `ACHTUNG: ${fehlerAufSeite.length} Konsolenfehler: ${fehlerAufSeite.slice(0, 3).join(" | ")}`
: "Keine Konsolenfehler.");
await browser.close();
kind.kill();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }