Verwaltung
Normalerweise landest du hier gar nicht: Wer durch die Zugangswand
diff --git a/blickfang-ueber-pc.png b/blickfang-ueber-pc.png index 8b3853c1..76de3a55 100644 Binary files a/blickfang-ueber-pc.png and b/blickfang-ueber-pc.png differ diff --git a/pruef-verwaltung-schmuck.mjs b/pruef-verwaltung-schmuck.mjs new file mode 100644 index 00000000..9a957bc9 --- /dev/null +++ b/pruef-verwaltung-schmuck.mjs @@ -0,0 +1,188 @@ +/* ===================================================================== + Prueft den Bildschmuck im Verwaltungsbereich. + + Die eigentliche Frage ist nicht "sieht man das Bild?", sondern: + Kostet es Lesbarkeit? Die Verwaltung ist ein Arbeitsplatz -- ein + Motiv, das hinter einer Zahlenspalte liegt, ist genau die Art von + Schoenheit, die ein Werkzeug unbrauchbar macht. + ===================================================================== */ +import { chromium } from "playwright"; +import http from "http"; +import fs from "fs"; +import path from "path"; + +const TYP = { + ".html": "text/html", ".css": "text/css", ".js": "text/javascript", + ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp", + ".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg", +}; + +const fehlt = []; +const srv = http.createServer((q, r) => { + const u = decodeURIComponent(q.url.split("?")[0]); + const p = path.join(process.cwd(), u); + if (fs.existsSync(p) && !p.endsWith("/")) { + r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" }); + return r.end(fs.readFileSync(p)); + } + fehlt.push(u); + r.writeHead(404); + r.end(""); +}); + +let ok = 0, fehl = 0; +const pruefe = (name, gut, info) => { + if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } + else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); } +}; + +srv.listen(4404, async () => { + const browser = await chromium.launch(); + + for (const [gr, vp] of [["Computer", { width: 1440, height: 950 }], + ["Handy", { width: 390, height: 844 }]]) { + console.log(`\n===== ${gr} =====`); + const seite = await browser.newPage({ viewport: vp }); + const skriptfehler = []; + seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 120))); + + await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html"); + await seite.waitForTimeout(1500); + + /* ---- Der Schmuckstreifen ---- */ + const streifen = await seite.evaluate(() => { + const el = document.querySelector(".vw-schmuck"); + if (!el) return null; + const s = getComputedStyle(el); + const r = el.getBoundingClientRect(); + const treffer = s.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); + return { + datei: treffer ? treffer[1] : "(keins)", + deckkraft: parseFloat(s.opacity), + z: parseInt(s.zIndex, 10), + oben: Math.round(r.top), + hoehe: Math.round(r.height), + klicks: s.pointerEvents, + versteckt: el.getAttribute("aria-hidden"), + }; + }); + if (!streifen) { pruefe(`${gr}: Schmuckstreifen vorhanden`, false); continue; } + + const kleinErwartet = vp.width <= 900; + pruefe(`${gr}: richtige Bilddatei`, + kleinErwartet === streifen.datei.includes("-klein"), streifen.datei); + /* Das ist der Kern: Der Streifen darf ANGEDEUTET sein, nicht mehr. */ + pruefe(`${gr}: bleibt sehr leise (unter 20 %)`, + streifen.deckkraft > 0 && streifen.deckkraft < 0.20, + Math.round(streifen.deckkraft * 100) + " %"); + pruefe(`${gr}: liegt hinter dem Inhalt`, streifen.z < 0, "z-index " + streifen.z); + pruefe(`${gr}: faengt keine Klicks ab`, streifen.klicks === "none", streifen.klicks); + pruefe(`${gr}: ist als Zierde ausgezeichnet`, streifen.versteckt === "true"); + /* 67px ist kein Zufallswert, sondern die Hoehe der Kopfleiste: Der + Streifen beginnt direkt darunter. Gemessen wird deshalb gegen die + tatsaechliche Kopfhoehe, nicht gegen eine geratene Zahl. */ + const kopfHoehe = await seite.evaluate(() => { + const k = document.querySelector("#wd-kopf .wd-kopf, #wd-kopf header, #wd-kopf"); + return k ? Math.round(k.getBoundingClientRect().height) : 0; + }); + pruefe(`${gr}: sitzt direkt unter der Kopfleiste`, + streifen.oben <= kopfHoehe + 8, `${streifen.oben}px, Kopf ${kopfHoehe}px`); + + /* ---- Die Anmeldekarte traegt das Wappen ---- */ + const login = await seite.evaluate(() => { + const el = document.querySelector("#vw-anmeldung"); + if (!el) return null; + el.removeAttribute("hidden"); // sonst nichts zu messen + const vor = getComputedStyle(el, "::before"); + const treffer = vor.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); + return { + klassen: el.className, + datei: treffer ? treffer[1] : "(keins)", + deckkraft: parseFloat(vor.opacity), + }; + }); + pruefe(`${gr}: die Anmeldekarte hat ein Motiv`, + login && login.datei.includes("wappen"), login && login.datei); + /* Der eigentliche Test. Deckkraft allein sagt nichts: Ein Motiv mit + hellen Kanten ist bei 20 % stoerender als ein ruhiges bei 50 %. + Was zaehlt, ist wie stark der Untergrund UNTER DER SCHRIFT + schwankt. Gemessen wird das an echten Bildpunkten aus dem + Erklaertext -- genau dort lag beim Bauen der Hundekopf, und die + Zeilen liefen quer ueber Schnauze und Schriftzug. */ + await seite.waitForTimeout(500); + + const messen = async () => { + const bild = await seite.locator("#vw-anmeldung p.wd-klein").first().screenshot(); + return seite.evaluate(async (daten) => { + const bl = await fetch("data:image/png;base64," + daten).then((r) => r.blob()); + const bm = await createImageBitmap(bl); + const c = new OffscreenCanvas(bm.width, bm.height); + const x = c.getContext("2d"); + x.drawImage(bm, 0, 0); + const d = x.getImageData(0, 0, bm.width, bm.height).data; + /* Nur die dunklen Bildpunkte: Die hellen SIND die Schrift, die + soll ja kontrastieren. Interessant ist der Grund dazwischen. */ + const grund = []; + for (let i = 0; i < d.length; i += 4) { + const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]; + if (h < 110) grund.push(h); + } + if (grund.length < 50) return null; + const m = grund.reduce((a, b) => a + b, 0) / grund.length; + return Math.round(Math.sqrt(grund.reduce((a, b) => a + (b - m) ** 2, 0) / grund.length)); + }, bild.toString("base64")); + }; + + const mitBild = await messen(); + /* Die Vergleichsmessung ohne Motiv ist der Kern. Ein fester + Grenzwert waere geraten: Schon die weichgezeichneten Kanten der + Buchstaben allein erzeugen eine Schwankung um 12 -- ein Test + gegen "unter 14" haette also fast nur diese Kanten gemessen und + waere je nach Schriftgroesse zufaellig gruen oder rot. Gefragt + ist, wie viel Unruhe DAS MOTIV hinzufuegt. */ + await seite.evaluate(() => + document.querySelector("#vw-anmeldung").classList.remove("wd-motiv", "wd-motiv--wappen")); + await seite.waitForTimeout(400); + const ohneBild = await messen(); + await seite.evaluate(() => + document.querySelector("#vw-anmeldung").classList.add("wd-motiv", "wd-motiv--wappen")); + await seite.waitForTimeout(400); + + const zusatz = (mitBild ?? 99) - (ohneBild ?? 0); + pruefe(`${gr}: das Motiv macht den Textgrund kaum unruhiger`, + mitBild !== null && ohneBild !== null && zusatz <= 5, + `mit ${mitBild}, ohne ${ohneBild} → plus ${zusatz}`); + + /* ---- Und jetzt die eigentliche Regel ---- */ + const stoerung = await seite.evaluate(() => { + const schlimm = []; + for (const el of document.querySelectorAll("table, .wd-tabelle, .vw-liste")) { + let p = el; + while (p && p !== document.body) { + if (p.classList && (p.classList.contains("wd-motiv") || + p.classList.contains("vw-schmuck"))) { + schlimm.push(el.tagName + "." + (el.className || "").slice(0, 20)); + break; + } + p = p.parentElement; + } + } + return schlimm; + }); + pruefe(`${gr}: kein Motiv hinter Tabellen oder Listen`, + stoerung.length === 0, stoerung.join(", ") || "keine"); + + pruefe(`${gr}: kein Skriptfehler`, skriptfehler.length === 0, skriptfehler[0] || ""); + + await seite.screenshot({ path: `pruef-vw-schmuck-${gr.toLowerCase()}.png` }); + await seite.close(); + } + + const verpasst = fehlt.filter((f) => /\.(webp|jpg|png|css|js|svg)$/.test(f)); + pruefe("keine fehlende Datei", verpasst.length === 0, verpasst.join(", ") || "keine"); + + console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); + await browser.close(); + srv.close(); + process.exit(fehl ? 1 : 0); +}); diff --git a/pruef-vw-schmuck-computer.png b/pruef-vw-schmuck-computer.png new file mode 100644 index 00000000..c66eb238 Binary files /dev/null and b/pruef-vw-schmuck-computer.png differ diff --git a/pruef-vw-schmuck-handy.png b/pruef-vw-schmuck-handy.png new file mode 100644 index 00000000..7ff44180 Binary files /dev/null and b/pruef-vw-schmuck-handy.png differ diff --git a/webdesign/sw.js b/webdesign/sw.js index 62711eed..1f59d14c 100644 --- a/webdesign/sw.js +++ b/webdesign/sw.js @@ -34,7 +34,7 @@ Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten Stand fest. */ -const CACHE_NAME = "dogfather-webdesign-v34"; +const CACHE_NAME = "dogfather-webdesign-v35"; /* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE HTML-Datei in dieser Liste. */ diff --git a/webdesign/verwaltung.html b/webdesign/verwaltung.html index 83f9e055..bd4c67c1 100644 --- a/webdesign/verwaltung.html +++ b/webdesign/verwaltung.html @@ -32,6 +32,75 @@ /* ---------- Anmeldung ---------- */ .vw-login { max-width: 420px; margin: clamp(2rem,8vh,5rem) auto; } + /* ---------- Schmuck ---------- + + Die Verwaltung ist ein Arbeitsplatz. Hier wird nicht geworben, + hier werden Listen gelesen und Zahlen verglichen -- deshalb ist + der Schmuck hier bewusst viel zurueckhaltender als auf den + oeffentlichen Seiten. + + Zwei Stellen, beide ausserhalb des Arbeitsflusses: + 1. Die Anmeldemaske. Dort wird nichts gelesen ausser drei Zeilen, + also darf das Motiv dort deutlich zu sehen sein. + 2. Ein Streifen ganz oben, der nach unten wegblendet. Er liegt + hinter der Kopfleiste und ist verschwunden, bevor die erste + Tabelle anfaengt. + + Was hier ABSICHTLICH nicht passiert: kein Motiv hinter Listen, + Tabellen oder Zahlen. Ein Bild hinter einer Zahlenspalte ist genau + die Art von Schoenheit, die ein Werkzeug unbrauchbar macht. */ + + .vw-schmuck { + position: absolute; + top: 0; left: 0; right: 0; + height: clamp(220px, 34vh, 380px); + z-index: -1; + pointer-events: none; + background: url("/assets/img/cryo-ring.webp") center top / cover no-repeat; + /* Sehr leise. Der Wert ist bewusst niedriger als bei den + oeffentlichen Motiven (.55): Dort traegt das Bild die Stimmung, + hier darf es die Kopfzeile nur andeuten. */ + opacity: .16; + /* Nach unten sauber ausblenden, damit es keine sichtbare Kante + gibt, an der das Bild einfach aufhoert. */ + -webkit-mask-image: linear-gradient(180deg, #000 0%, rgb(0 0 0 / .55) 45%, transparent 100%); + mask-image: linear-gradient(180deg, #000 0%, rgb(0 0 0 / .55) 45%, transparent 100%); + } + @media (max-width: 900px) { + /* Auf dem Handy die kleine Fassung -- 34 KB statt 113 KB fuer ein + Bild, das ohnehin nur angedeutet ist. */ + .vw-schmuck { background-image: url("/assets/img/cryo-ring-klein.webp"); opacity: .13; } + } + + /* Die Anmeldekarte bekommt das Wappen. Sie ist halbdurchsichtig, + das Motiv scheint also durch die Glasflaeche hindurch. + + Warum so wenig: Bei .38 lag der Hundekopf mitten im Erklaertext, + und die Zeilen liefen quer ueber Schnauze und Schriftzug. Lesbar + war das nicht mehr. Entscheidend ist hier nicht die Deckkraft an + sich, sondern wie stark der Untergrund unter der Schrift + schwankt -- ein ruhiger dunkler Grund vertraegt viel, ein Motiv mit + hellen Kanten fast nichts. */ + .vw-login.wd-motiv::before { + opacity: .16; + /* Das Wappen sitzt zentriert genau dort, wo der Text steht. Nach + unten versetzt liegt es hinter Knopf und Rand statt hinter den + Zeilen. */ + background-position: center 72%; + } + /* Die Glasflaeche zwischen Bild und Schrift wird hier dichter als auf + den oeffentlichen Seiten -- dort steht Fliesstext, hier ein + Eingabefeld, das man treffen koennen muss. */ + .vw-login.wd-motiv::after { + background: linear-gradient(180deg, + rgb(6 19 35 / .82) 0%, + rgb(6 19 35 / .88) 55%, + rgb(3 8 16 / .92) 100%); + } + @media (max-width: 900px) { + .vw-login.wd-motiv::before { background-image: url("/assets/img/cryo-wappen-klein.webp"); } + } + /* ---------- Kopfleiste ---------- */ /* Zwei Zeilen statt einer: oben WO man ist, darunter WOHIN man kann. @@ -1082,11 +1151,21 @@
Normalerweise landest du hier gar nicht: Wer durch die Zugangswand