From bfae4447cdcbbf741d4620bd15c050f9d37a08b2 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 25 Sep 2026 01:20:16 +0200 Subject: [PATCH] =?UTF-8?q?Der=20Chat=20bekommt=20Tiefe=20--=20Lichtkante,?= =?UTF-8?q?=20Glas=20und=20Schatten=20statt=20flacher=20Fl=C3=A4chen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/mess-chat-optik.mjs | 203 ++++++++++++ server/pruef-chat-optik.mjs | 32 +- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/chat.css | 536 ++++++++++++++++++++++++++++---- workspace/aufgaben.html | 38 +-- workspace/automation.html | 32 +- workspace/befinden.html | 34 +- workspace/bereich.html | 44 +-- workspace/bewerben.html | 38 +-- workspace/bewerbungen.html | 36 +-- workspace/calls.html | 34 +- workspace/chat.html | 38 +-- workspace/content.html | 34 +- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +- workspace/entwicklung.html | 36 +-- workspace/hilfe.html | 30 +- workspace/index.html | 10 +- workspace/kalender.html | 34 +- workspace/leistung.html | 32 +- workspace/material.html | 32 +- workspace/personen.html | 32 +- workspace/profil.html | 36 +-- workspace/rechte.html | 34 +- workspace/report.html | 36 +-- workspace/scouting.html | 32 +- workspace/start.html | 32 +- workspace/startcheck.html | 34 +- workspace/steckbrief.html | 36 +-- workspace/support.html | 28 +- workspace/talente.html | 36 +-- workspace/team.html | 32 +- workspace/teamlage.html | 34 +- workspace/teilen.html | 30 +- workspace/treff-moderation.html | 32 +- workspace/treff-regeln.html | 32 +- workspace/uebersicht.html | 32 +- workspace/unsere-seiten.html | 30 +- workspace/unterstuetzen.html | 32 +- workspace/werdegang.html | 32 +- workspace/willkommen.html | 30 +- workspace/wissen.html | 34 +- 44 files changed, 1343 insertions(+), 688 deletions(-) create mode 100644 server/mess-chat-optik.mjs diff --git a/server/mess-chat-optik.mjs b/server/mess-chat-optik.mjs new file mode 100644 index 00000000..7a655a71 --- /dev/null +++ b/server/mess-chat-optik.mjs @@ -0,0 +1,203 @@ +/* ===================================================================== + 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); +} diff --git a/server/pruef-chat-optik.mjs b/server/pruef-chat-optik.mjs index 95e4249f..717be9e7 100644 --- a/server/pruef-chat-optik.mjs +++ b/server/pruef-chat-optik.mjs @@ -769,8 +769,36 @@ melde("\n=== Die Schreibzeile auf dem Handy ==="); `das Schreibfeld ist ${zeile.feld?.b} px breit (mindestens 220)`); /* SENDEN STEHT BEIM FELD. Vorher lag es eine Zeile tiefer, unten links -- weiter vom Text entfernt als die Bueroklammer. */ - ok(Math.abs((zeile.senden?.y || 0) - (zeile.feld?.y || 0)) <= 6, - `und "Senden" steht daneben, nicht darunter (y ${zeile.senden?.y} zu ${zeile.feld?.y})`); + /* ==== SIE VERGLICH DIE OBERKANTEN (behoben 25.09.2026) ========= + + Hier stand `Math.abs(senden.y - feld.y) <= 6`. Die Absicht ist + richtig, die Messung war es nicht: Die Zeile ist `align-items: + flex-end` -- beide stehen UNTEN buendig. Das Schreibfeld ist + zwei Zeilen hoch (66 px), der Knopf 42. Die Oberkanten liegen + damit zwangslaeufig 24 px auseinander, OBWOHL beide in + derselben Zeile stehen. + + Die Pruefung war dadurch dauerhaft rot, ohne dass etwas kaputt + war -- und eine Warnung, die immer kommt, wird ueberlesen. + Gefunden habe ich es erst, als ich zweimal an der falschen + Stelle repariert hatte (Knopfbreite, Abstand): Beide Male + aenderte sich nichts an der Zahl. Eine Zahl, die sich durch die + Reparatur nicht bewegt, misst etwas anderes, als man denkt. + + GEFRAGT WIRD JETZT, WAS GEMEINT IST: Teilen sich die beiden + eine Zeile? Das heisst, ihre senkrechten Bereiche UEBERLAPPEN + sich -- und zwar deutlich, nicht nur um einen Pixel. Rutscht + "Senden" wirklich eine Zeile tiefer, ist die Ueberlappung null + und die Zeile wird rot. Sie prueft damit mehr als vorher, nicht + weniger: Die alte Fassung haette zwei gleich hohe Elemente auch + dann durchgelassen, wenn sie in verschiedenen Spalten stehen. */ + const fO = zeile.feld?.y || 0, fU = fO + (zeile.feld?.h || 0); + const sO = zeile.senden?.y || 0, sU = sO + (zeile.senden?.h || 0); + const gemeinsam = Math.min(fU, sU) - Math.max(fO, sO); + const kleinste = Math.min(zeile.feld?.h || 0, zeile.senden?.h || 0); + ok(kleinste > 0 && gemeinsam >= kleinste * 0.8, + `und "Senden" steht daneben, nicht darunter ` + + `(${gemeinsam} von ${kleinste} px gemeinsame Hoehe)`); ok(zeile.drueber.length === 0, `nichts liegt uebereinander (${zeile.drueber.join(", ") || "nichts"})`); ok(zeile.klein.length === 0, diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index f76d2f83..75b645a3 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +