Files
dogfather-universe/server/mess-chat-optik.mjs
T
DogFatherGitandClaude Opus 5 bfae4447cd Der Chat bekommt Tiefe -- Lichtkante, Glas und Schatten statt flacher Flächen
Filipe: „ich will dass du die komplette seite viel geiler und moderner
machst. die komplette kachel. die chat liste und die chats selber. …
es soll komplett aus der rolle fahren und was was wir noch nie hatten,
ich will es wirklich übertrieben krass."

EIN SYSTEM, NICHT ZWANZIG EINFÄLLE. Alles Folgende geht auf dieselben
drei Regeln zurück, und deshalb passt es zusammen:

  1. LICHTKANTE  — jede Fläche hat oben eine haardünne helle Linie und
     unten eine dunkle. Damit wird aus einer Fläche ein Körper: Licht
     fällt von oben. Was VERTIEFT ist (Suchfeld, Knopfgruppe), bekommt
     es genau andersherum.
  2. TIEFENSCHATTEN — lang und weich, weit unterhalb. Er trägt, er
     umrandet nicht.
  3. GLAS — was oben liegt, ist leicht durchscheinend und verwischt,
     was dahinter ist. Dadurch sieht man die Ebene, ohne eine Linie.

WAS DAS KONKRET HEISST
  * Der Rahmen hat eine Kante statt eines Strichs; zwischen den beiden
    Spalten stossen zwei Platten aneinander.
  * Die Gesprächszeile HEBT sich beim Überfahren, statt sich zu färben
    — der Unterschied zwischen einer Tabelle und einer Bedienung.
  * Das Zeichen (Kreis mit Buchstabe) ist ein Körper mit Licht, Saum
    und eigenem Schein in der Rollenfarbe.
  * Die fünf Handgriffe unter jeder Blase waren unterstrichene Wörter
    — im Netz heisst das seit dreissig Jahren „führt woandershin", und
    genau das tun sie nicht. Jetzt leise Marken. Sie bleiben SICHTBAR:
    Die Entscheidung vom 23.09. gilt weiter (auf dem Handy gibt es kein
    Überfahren).
  * Der Datumstrenner ist ein Schild auf der Linie statt nackter
    Grossbuchstaben.
  * Die Eingabe ist eine Konsole: Glas, Lichtkante, Schatten nach oben.
    Die vier Buchstaben (F K U S) standen frei im Raum — jetzt Schalter
    in einem Streifen über dem Schreibfeld.
  * Der Verlauf hat einen weichen Saum: Nachrichten laufen UNTER Kopf
    und Konsole, statt an einer harten Kante abzubrechen.
  * Titel, Unterzeile und die vier Kopfknöpfe (jetzt eine Gruppe in
    einer Mulde) bekommen eine Rangfolge.

WAS ABSICHTLICH UNANGETASTET BLEIBT: die FARBE der Blase. Sie ist die
persönliche Kachel und wird von pruef-chatkachel gemessen — die Prüfung
rechnet mit dem Farbwert selbst. Ein Verlauf oder Glas darauf hätte den
gemessenen und den gesehenen Wert auseinandergebracht, und zwar still.
Die Blase bekommt Tiefe über Kante und Schatten, nicht über den Grund.

DREI EIGENE FEHLER, VON DEN PRÜFUNGEN GEFUNDEN
  * Die Formatknöpfe hatte ich auf 32 px verkleinert — hübscher, und
    damit unter der Grenze von 44 px, unter der ein Daumen danebentrifft.
  * Vier statt zwei Pixel Abstand dazwischen = sechs Pixel mehr an der
    schmalsten Stelle. Die Zeile ist dort seit dem 23.09. auf den Pixel
    voll.
  * Meine erste Fassung der Gestaltungsleiste zerlegte die Konsole in
    drei Zeilen.

UND EIN FEHLALARM, DER SEIT LANGEM ROT WAR: pruef-chat-optik verglich
die OBERKANTEN von Schreibfeld und Senden-Knopf. Die Zeile ist aber
unten bündig, das Feld zwei Zeilen hoch — die Oberkanten liegen
zwangsläufig 24 px auseinander, obwohl beide nebeneinander stehen.
Gemerkt habe ich es erst, als zwei Reparaturen die Zahl nicht bewegt
haben: Eine Zahl, die sich durch die Reparatur nicht ändert, misst
etwas anderes, als man denkt. Sie fragt jetzt nach der GEMEINSAMEN
Höhe (44 von 44) und ist damit strenger als vorher.

GEPRÜFT: chat-optik, chatkachel, chat-ausbau, chat, chat-neu,
chat-kanaele, css-klassen, tippziele, lesbarkeit — alle 0 Fehler.
Dazu mess-chat-optik.mjs: vier Bilder (Liste und Verlauf, 1440 und
412 px) auf eigener Wegwerf-Datenbank.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-25 01:20:16 +02:00

204 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
WIE DER CHAT AUSSIEHT (25.09.2026)
Filipe: „ich will dass du die komplette seite viel geiler und
moderner machst. die komplette kachel. die chat liste und die chats
selber. … es soll komplett aus der rolle fahren und was was wir noch
nie hatten."
SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder,
auf denen man vergleichen kann. Deshalb `mess-` und nicht `pruef-`.
VIER BILDER, weil die Seite zwei Zustaende und zwei Breiten hat:
Liste ohne Gespraech / Liste mit offenem Gespraech, jeweils am
Rechner und am Handy. Wer nur eines ansieht, baut fuer eines.
EIGENE WEGWERF-DATENBANK: Es entstehen echte Gespraeche und
Nachrichten. In einem laufenden System waeren das Testdaten, und die
sind verboten. Hier liegt alles in einem `mktemp`-Ordner.
Aufrufen mit: node server/mess-chat-optik.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFileSync } from "node:child_process";
import { request as httpAnfrage } from "node:http";
import { createServer as httpsServer } from "node:https";
const PORT = 5403, HP = 5404;
const CREW = "crew.dogfather-universe.com";
const ordner = mkdtempSync(join(tmpdir(), "ws-chatoptik-"));
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(260_000, "mess-chat-optik");
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); });
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 vorStunden = (h) => new Date(Date.now() - h * 3600_000).toISOString();
const sch = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", sch, jetzt);
const anlegen = (name, rolle, code) => {
const s = randomBytes(16).toString("hex");
const h = scryptSync(code, s, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
return d.prepare("INSERT INTO personen"
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?) RETURNING id")
.get(name, rolle, h, s, 32768,
createHmac("sha256", sch).update(code).digest("hex"), jetzt).id;
};
const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001");
const idVan = anlegen("VanVan", "hand", "CODE-HAND-0001");
const idFunny = anlegen("Funny", "linke", "CODE-LINK-0001");
const idDiene = anlegen("Diene", "modi", "CODE-MODI-0001");
const idMarina = anlegen("Marina", "modi", "CODE-MODI-0002");
const idGhost = anlegen("Ghost", "modi", "CODE-MODI-0003");
const idMiss = anlegen("Miss", "modi", "CODE-MODI-0004");
/* ---- Gespraeche und Nachrichten direkt in die Datenbank -------------
Ueber die Schnittstelle waeren es fuenfzehn Aufrufe fuer einen
Zustand, den es nur fuer das Bild braucht. */
const raum = (art, name, kategorie, haus = "crew") => d.prepare(
"INSERT INTO chat_raeume (art, name, kategorie, erstellt, erstellt_von, haus)"
+ " VALUES (?,?,?,?,?,?) RETURNING id").get(art, name, kategorie, jetzt, idDogi, haus).id;
const rein = (r, p) => d.prepare(
"INSERT INTO chat_teilnehmer (raum_id, person_id, seit, gelesen_bis) VALUES (?,?,?,0)")
.run(r, p, jetzt);
const sagen = (r, p, text, wann) => d.prepare(
"INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)")
.run(r, p, text, wann);
const rudel = raum("kanal", "Das Rudel", "treff");
for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost, idMiss]) rein(rudel, p);
sagen(rudel, idFunny, "@rudel das mache ich heute Abend fertig, versprochen 🙌", vorStunden(20));
const modkanal = raum("kanal", "Chat-Moderation", "chat");
for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost]) rein(modkanal, p);
sagen(modkanal, idMarina, "Ach man 😢😢", vorStunden(30));
const gruppe = raum("gruppe", "Abmeldungen 🏥", null);
for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost, idMiss]) rein(gruppe, p);
sagen(gruppe, idDiene, "Guten Morgen ihr Lieben – ich bin heute leider raus, Hals tut weh.", vorStunden(31));
const mitVan = raum("direkt", null, null);
rein(mitVan, idDogi); rein(mitVan, idVan);
sagen(mitVan, idVan, "Da hat Miss was falsch eingegeben glaube ich", vorStunden(29));
sagen(mitVan, idDogi, "Hab's gesehen. Ich schreib ihr gleich, kein Ding.", vorStunden(28));
sagen(mitVan, idVan, "Top. Und die Abrechnung vom Wochenende liegt bei dir im Ordner.", vorStunden(27));
sagen(mitVan, idDogi, "Perfekt, danke dir ❤️", vorStunden(2));
const mitGhost = raum("direkt", null, null);
rein(mitGhost, idDogi); rein(mitGhost, idGhost);
sagen(mitGhost, idGhost, "Sorry hab's gefunden ist gelöscht 👍", vorStunden(29));
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(20), rudel);
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(2), mitVan);
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(29), mitGhost);
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(30), modkanal);
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(31), gruppe);
d.close();
const anfrage = (pfad, art, koerper) => new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: art,
headers: { Host: CREW, "Content-Type": "application/json" },
}, (r) => {
let t = ""; r.on("data", (x) => { t += x; });
r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
const an = await anfrage("/workspace/api/anmelden", "POST",
{ rolle: "admin", code: "CODE-DOGI-0001" });
if (an.code !== 200) { console.log(`ABBRUCH: Anmeldung ${an.code}`); process.exit(2); }
const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl,
"-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
{ stdio: "ignore" });
const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) },
(von, nach) => {
const w = httpAnfrage({
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
headers: { ...von.headers, host: `${CREW}:${HP}` },
}, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
von.pipe(w);
});
await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
const pw = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch({
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
});
const MARKE = process.env.MARKE || "jetzt";
try {
for (const [wie, breite, hoehe] of [["computer", 1440, 980], ["handy", 412, 915]]) {
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
await kontext.addCookies(keks.split("; ").map((k) => {
const [n, ...r] = k.split("=");
return { name: n, value: r.join("="), domain: CREW, path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (e) => {
if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR|410/i.test(e.text())) {
konsole.push(e.text().slice(0, 140));
}
});
await seite.goto(`https://${CREW}:${HP}/workspace/chat.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(1600);
const liste = await seite.evaluate(() => ({
raeume: document.querySelectorAll("#raeume li").length,
namen: [...document.querySelectorAll(".chat-raum__name")].map((x) => x.textContent),
}));
console.log(`\n=== Chat, ${breite} x ${hoehe} (${wie}) ===`);
console.log(" Gespraeche:", liste.raeume, "|", liste.namen.join(" · "));
await seite.screenshot({ path: `server/mess-chat-${MARKE}-${wie}-liste.png` });
/* Und jetzt mit offenem Gespraech -- das ist die Haelfte der
Seite, die man am laengsten ansieht. */
await seite.evaluate(() => {
const zeilen = [...document.querySelectorAll("#raeume .chat-raum__knopf")];
(zeilen.find((z) => /VanVan/.test(z.textContent)) || zeilen[0])?.click();
});
await seite.waitForTimeout(1500);
const verlauf = await seite.evaluate(() => ({
nachrichten: document.querySelectorAll(".chat-nachricht").length,
titel: document.getElementById("verlauf-titel")?.textContent,
}));
console.log(" Verlauf:", verlauf.nachrichten, "Nachrichten in", verlauf.titel);
await seite.screenshot({ path: `server/mess-chat-${MARKE}-${wie}-verlauf.png` });
if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole);
console.log(` Bilder: server/mess-chat-${MARKE}-${wie}-{liste,verlauf}.png`);
await kontext.close();
}
} finally {
await browser.close();
vorbau.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(0);
}