diff --git a/server/pruef-formulare.mjs b/server/pruef-formulare.mjs new file mode 100644 index 00000000..2a81dee9 --- /dev/null +++ b/server/pruef-formulare.mjs @@ -0,0 +1,147 @@ +/* Prueft die Formularzeilen auf allen Seiten. + + Beanstandet am 01.09.2026: "das sieht scheisse aus, so verzogen". + Gemeint war die Zeile im Kalender -- "Art" schmal, "Beginn" breit, + "Dauer (Minuten)" zweizeilig umgebrochen, alle Felder auf + verschiedenen Hoehen. + + Ursache war eine Regel, die nur fuer Kinder mit der Klasse .feld galt. + Der Kalender benutzt schlichte
ohne Klasse; die fielen hindurch, + bekamen von zwoelf Rasterspalten je EINE und wurden nur so breit, wie + ihr Inhalt sie zwang. + + Geprueft wird deshalb nicht "sieht gut aus", sondern messbar: + * alle Eingabefelder einer Zeile gleich hoch + * ihre Unterkanten auf einer Linie (sonst haengt eines tiefer) + * kein Feld absurd schmal + * keine Beschriftung mehrzeilig */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-form-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4203"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +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 }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4203"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_000).unref?.(); + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +for (let i = 0; i < 60; i++) { + try { + const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const salt = randomBytes(16).toString("hex"); +const hash = scryptSync("CODE-DOGI-0001", 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run("Filipe", "admin", hash, salt, 32768, new Date().toISOString()); +d.close(); + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +/* Seite, Knopf zum Aufklappen des Formulars. */ +const FORMULARE = [ + ["kalender.html", "#neu-oeffnen"], + ["aufgaben.html", "#neu-oeffnen"], + ["personen.html", "#neu-oeffnen"], + ["bereich.html?b=live", "#neu-oeffnen"], + ["scouting.html", "#neu-oeffnen"], +]; + +for (const [breite, wie] of [[1280, "Computer"], [390, "Handy"]]) { + console.log(`\n=== ${wie} (${breite}px)`); + const kontext = await browser.newContext({ viewport: { width: breite, height: 1000 } }); + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + + for (const [s, knopf] of FORMULARE) { + await seite.goto(BASIS + "/workspace/" + s, { waitUntil: "networkidle" }); + await seite.waitForTimeout(700); + const da = await seite.locator(knopf).count(); + if (!da) { console.log(` -- ${s}: kein Formularknopf`); continue; } + await seite.click(knopf).catch(() => {}); + await seite.waitForTimeout(450); + + const r = await seite.evaluate(() => { + const raster = document.querySelector(".neu__raster"); + if (!raster) return null; + const felder = [...raster.children].filter((k) => k.offsetParent !== null); + const eingaben = felder + .map((f) => f.querySelector("input, select, textarea")) + .filter(Boolean) + .map((e) => e.getBoundingClientRect()); + const schilder = felder + .map((f) => f.querySelector(".feld-schild")) + .filter(Boolean); + /* Mehrzeilige Beschriftung: tatsaechliche Hoehe gegen eine Zeile. */ + const mehrzeilig = schilder.filter((s2) => { + const zeile = parseFloat(getComputedStyle(s2).lineHeight) || 16; + return s2.getBoundingClientRect().height > zeile * 1.6; + }).map((s2) => s2.textContent.trim()); + return { + anzahl: eingaben.length, + hoehen: eingaben.map((e) => Math.round(e.height)), + /* Auf 3 px genau zusammengefasst: Unterkanten liegen durch + Teilpixel-Rundung selten exakt gleich, obwohl sie auf einer + Linie sitzen. Ein erster Anlauf meldete deshalb "3 Unterkanten" + bei drei Feldern derselben Reihe -- ein Messfehler, kein + Gestaltungsfehler. */ + unterkanten: eingaben.map((e) => Math.round(e.bottom / 3) * 3), + breiten: eingaben.map((e) => Math.round(e.width)), + einzeln: felder.map((f) => { const e=f.querySelector("input, select, textarea"); if(!e) return null; const r2=e.getBoundingClientRect(); const fr=f.getBoundingClientRect(); return { id:e.id, tag:e.tagName, top:Math.round(r2.top), bottom:Math.round(r2.bottom), h:Math.round(r2.height), feldTop:Math.round(fr.top), feldH:Math.round(fr.height) }; }).filter(Boolean), + mehrzeilig, + }; + }); + + if (!r || r.anzahl < 2) { console.log(` -- ${s}: kein Raster mit mehreren Feldern`); continue; } + + const hoehenGleich = new Set(r.hoehen).size === 1; + /* Unterkanten auf einer Linie -- auf dem Handy stehen die Felder + untereinander, dort gilt das natuerlich nicht. */ + const reihen = new Set(r.unterkanten).size; + const buendig = breite < 700 ? true : reihen <= Math.ceil(r.anzahl / 3); + const nichtWinzig = Math.min(...r.breiten) >= 90; + + const gut = hoehenGleich && buendig && nichtWinzig && !r.mehrzeilig.length; + if (!gut) fehler++; + console.log(` ${gut ? "ok " : "FEHL"} ${String(s).padEnd(20)}` + + `${r.anzahl} Felder Höhen=${[...new Set(r.hoehen)].join("/")} ` + + `Breiten=${Math.min(...r.breiten)}–${Math.max(...r.breiten)}` + + (hoehenGleich ? "" : " UNGLEICH HOCH") + + (buendig ? "" : ` NICHT BÜNDIG (Unterkanten ${r.unterkanten.join(",")})`) + + (nichtWinzig ? "" : " ZU SCHMAL") + + (r.mehrzeilig.length ? ` MEHRZEILIG: ${r.mehrzeilig.join(", ")}` : "") + + (gut ? "" : " | " + r.einzeln.map((e) => `${e.id}(${e.tag}) Feld:${e.feldTop}+${e.feldH} Eingabe:${e.top}..${e.bottom}`).join(" "))); + } + await kontext.close(); +} + +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(fehler ? `\n${fehler} Formular(e) fehlerhaft.` : "\nAlles in Ordnung."); +process.exit(fehler ? 1 : 0); diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css index 0b833ebf..15d98c3c 100644 --- a/workspace/assets/css/aufgaben.css +++ b/workspace/assets/css/aufgaben.css @@ -24,7 +24,46 @@ grid-template-columns: repeat(12, 1fr); } /* Vorgabe: ein Drittel. Reicht fuer Auswahlfelder und Datumsangaben. */ -.neu__raster > .feld { grid-column: span 4; min-width: 0; } +/* JEDES direkte Kind, nicht nur die mit der Klasse .feld. + + Genau daran lag die "verzogene" Formularzeile im Kalender: Dort + stehen schlichte
ohne Klasse. Sie fielen durch diese Regel + hindurch, bekamen von den zwoelf Spalten je EINE und wurden nur so + breit, wie ihr Inhalt sie zwang. Ergebnis: "Art" schmal, "Beginn" + breit, "Dauer (Minuten)" zweizeilig umgebrochen, alles auf + verschiedenen Hoehen. Es sah nach einem Fehler aus und war einer. + + :not() haelt die Sonderbreiten am Leben. */ +.neu__raster > *:not(.feld--breit):not(.feld--halb) { grid-column: span 4; min-width: 0; } + +/* Alle Felder gleich hoch und unten buendig -- unabhaengig davon, ob + die Beschriftung darueber ein- oder zweizeilig ist. Ohne das sitzt + ein Feld mit langer Beschriftung tiefer als seine Nachbarn. */ +.neu__raster { align-items: stretch; } +/* Zwei Zeilen je Feld: Die Beschriftung oben DEHNT sich (1fr), das + Eingabefeld unten hat feste Hoehe. Dadurch sitzen alle Eingabefelder + auf exakt derselben Linie -- unabhaengig davon, ob die Beschriftung + darueber ein- oder zweizeilig ist. + + Der Umweg ueber flex mit justify-content: flex-end reichte nicht: Ein + derselben Hoehe standen damit trotzdem 3 px + versetzt, weil ihre Kaesten unterschiedlich sitzen. Drei Pixel klingen + nach nichts und sind genau das, was man als "verzogen" sieht. */ +/* Die zweite Rasterzeile hat eine FESTE Hoehe, und das Feld fuellt sie + ganz aus. Mit "auto" bestimmte jedes Element die Zeilenhoehe selbst: + Ein und sass dadurch + hoeher. Gemessen: Container beider Felder 652+71 identisch, Eingabe + aber 676..720 gegen 679..723. Drei Pixel -- genau das, was man als + "verzogen" sieht. */ +.neu__raster > * { display: grid; grid-template-rows: 1fr 44px; } +.neu__raster > * > .feld-schild { align-self: end; margin-bottom: 6px; } +.neu input, .neu select { height: 44px; box-sizing: border-box; } +/* Die Zeile ganz ausfuellen -- sonst zentriert der Browser das Element + darin und der Versatz waere wieder da. */ +.neu__raster > * > input, +.neu__raster > * > select, +.neu__raster > * > textarea { align-self: stretch; height: 100%; } +.neu__raster textarea { min-height: 44px; } /* Diese Klasse stand in drei HTML-Dateien, war aber NIE definiert -- deshalb quetschte das Raster jedes Feld in dieselbe Breite und lange @@ -33,7 +72,12 @@ .neu__raster > .feld--halb { grid-column: span 6; } @media (max-width: 860px) { - .neu__raster > .feld { grid-column: span 6; } + .neu__raster > *:not(.feld--breit) { grid-column: span 6; } +} +@media (max-width: 560px) { + /* Auf dem Handy untereinander -- zwei Felder auf 300 px sind zwei + Streifen, in die nichts hineinpasst. */ + .neu__raster > * { grid-column: 1 / -1 !important; } } @media (max-width: 560px) { .neu__raster > .feld, .neu__raster > .feld--halb { grid-column: 1 / -1; } @@ -267,3 +311,41 @@ } @media (prefers-reduced-motion: reduce) { .karte__rueck { transition: none; } } + +/* Die letzten drei Pixel. + + Trotz gleicher Hoehe und gleicher Rasterzeile sassen Auswahl- und + Eingabefelder 3 px versetzt. Grund: Ein ist von Haus aus + inline und wird an der SCHRIFTGRUNDLINIE ausgerichtet -- die liegt je + nach Element unterschiedlich. Drei Pixel klingen nach nichts und sind + genau das, was man als "verzogen" sieht. + + display: block nimmt die Grundlinie aus dem Spiel; margin: 0 die + Vorgaben des Browsers. */ +.neu__raster > * > input, +.neu__raster > * > select, +.neu__raster > * > textarea { + display: block; + width: 100%; + margin: 0; + vertical-align: top; +} + +/* Die Beschriftung bekommt eine FESTE Hoehe. + + Damit beginnt das Eingabefeld in jeder Spalte an derselben Stelle -- + ganz gleich, wie das Feld darunter beschaffen ist. Vorher hing es an + Grundlinien und Zeilenhoehen, und drei Pixel Versatz blieben trotz + gleicher Feldhoehe und gleicher Rasterzeile uebrig. + + Feste Werte sind hier ausnahmsweise das richtige Mittel: Eine + Beschriftung ist immer einzeilig (mehrzeilige werden gekuerzt, siehe + die Handypruefung), und "alle Felder auf einer Linie" ist eine + Zusicherung, keine Faustregel. */ +.neu__raster > * > .feld-schild { + height: 17px; + margin-bottom: 7px; + line-height: 17px; + overflow: hidden; + white-space: nowrap; text-overflow: ellipsis; +} diff --git a/workspace/assets/css/scouting.css b/workspace/assets/css/scouting.css index f2bfa9cc..3a12a0b4 100644 --- a/workspace/assets/css/scouting.css +++ b/workspace/assets/css/scouting.css @@ -176,3 +176,64 @@ @media (prefers-reduced-motion: reduce) { .kk { transition: none; } } + +/* ---------- Die Erklaerung im leeren Zustand --------------------------- + Der leere Zustand ist der einzige Moment, in dem jemand garantiert + liest, was dort steht -- spaeter ist die Flaeche von Daten belegt. + Deshalb steht hier keine Meldung, sondern die Erklaerung, wofuer die + Pipeline ueberhaupt gut ist. Sie ist laenger als ein Hinweis und + bekommt deshalb linksbuendigen Text statt zentrierten. */ +.leer-hinweis--erklaerung { + text-align: left; + max-width: 62ch; + margin: 0 auto; + padding: 30px 28px 26px; +} +.erklaerung__satz { + margin: 0 0 14px; + font-size: 1.02rem; color: var(--text); line-height: 1.55; +} +.erklaerung__warum { + margin: 0 0 20px; + font-size: .9rem; color: var(--text-leise); line-height: 1.65; +} +.erklaerung__stufen { + display: grid; gap: 10px; + margin: 0 0 20px; padding: 0; list-style: none; + counter-reset: none; +} +.erklaerung__stufe { + display: flex; align-items: flex-start; gap: 12px; + font-size: .88rem; +} +/* Die Nummer als eigener Koerper: Die fuenf Stufen sind eine REIHENFOLGE, + und die soll man sehen, ohne die Ueberschriften zu lesen. */ +.erklaerung__nr { + flex: none; + display: grid; place-items: center; + width: 24px; height: 24px; border-radius: 50%; + font-size: .74rem; font-weight: 700; + color: var(--ton, var(--akzent)); + border: 1px solid color-mix(in srgb, var(--ton, var(--akzent)) 40%, transparent); + background: color-mix(in srgb, var(--ton, var(--akzent)) 12%, transparent); +} +.erklaerung__stufe strong { display: block; color: var(--text); } +.erklaerung__was { + display: block; margin-top: 1px; + font-size: .83rem; color: var(--text-still); line-height: 1.5; +} +.erklaerung__fuss { + margin: 0 0 16px; padding-top: 16px; + border-top: 1px solid var(--rand); + font-size: .86rem; color: var(--text-leise); line-height: 1.6; +} +.erklaerung__los { + margin: 0; + font-size: .88rem; color: var(--ton, var(--akzent)); +} +/* Der Ring des leeren Zustands passt hier nicht -- es ist ja Text da. */ +.leer-hinweis--erklaerung::before { display: none; } + +@media (max-width: 560px) { + .leer-hinweis--erklaerung { padding: 22px 16px 20px; } +} diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index d96211d9..2c203689 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -1810,3 +1810,11 @@ body.start #protokoll { .kopf-zeile > .steuerung > .knopf, .kopf-zeile > .kopf-knoepfe > .knopf { width: 100%; justify-content: center; } } + +/* Der leise Zusatz in einer Feldbeschriftung ("Dauer Minuten"). Vorher + stand dort "DAUER (MINUTEN)" -- in einer schmalen Spalte brach das + um, und das Feld darunter sass tiefer als seine Nachbarn. */ +.feld-schild__still { + text-transform: none; letter-spacing: normal; + font-weight: 400; color: var(--text-still); +} diff --git a/workspace/assets/js/scouting.js b/workspace/assets/js/scouting.js index e0c67364..49bed198 100644 --- a/workspace/assets/js/scouting.js +++ b/workspace/assets/js/scouting.js @@ -247,8 +247,59 @@ } if (!liste.length) { - ziel.append(el('p', 'leer-hinweis', - 'Noch niemand in der Pipeline. „Neuer Kontakt" legt den ersten an.')); + /* WOFUER IST DAS GUT? (Wunsch vom 01.09.2026) + + Vorher stand hier ein Satz, der nur wiederholte, was man + ohnehin sieht: dass nichts da ist. Wer die Seite zum ersten + Mal oeffnet, weiss danach immer noch nicht, warum es sie gibt. + + Der leere Zustand ist der EINZIGE Moment, in dem jemand + garantiert liest, was dort steht -- spaeter ist die Flaeche + von Daten belegt. Deshalb steht die Erklaerung genau hier und + nicht in einer Hilfe, die niemand aufmacht. + + Erklaert wird der NUTZEN, nicht die Bedienung: nicht "hier + klicken", sondern warum ein Scout ohne diese Liste Leute + verliert. */ + const kasten = el('div', 'leer-hinweis leer-hinweis--erklaerung'); + + kasten.append(el('p', 'erklaerung__satz', + 'Hier steht jeder, den ihr entdeckt habt – von der ersten Nachricht ' + + 'bis zur Übergabe an einen Manager.')); + + kasten.append(el('p', 'erklaerung__warum', + 'Wozu das gut ist: Ein Scout spricht viele an. Ohne Liste erinnert ' + + 'sich niemand, wer schon gefragt wurde, wer zugesagt hat und bei ' + + 'wem seit drei Wochen nichts passiert ist. Genau die gehen ' + + 'verloren – nicht die, die Nein sagen.')); + + const stufenListe = el('ol', 'erklaerung__stufen'); + for (const [nr, name, was] of [ + ['1', 'Neu', 'Entdeckt. Noch nicht angeschrieben.'], + ['2', 'Angesprochen', 'Erste Nachricht ist raus. Jetzt läuft die Uhr.'], + ['3', 'Gespräch', 'Ihr seid im Austausch – Fragen, Bedingungen, Termine.'], + ['4', 'Interessiert', 'Grundsätzlich dabei. Jetzt zählt, dass es nicht liegen bleibt.'], + ['5', 'Übergeben', 'Ein Manager übernimmt. Ab hier ist es Betreuung, kein Scouting mehr.'], + ]) { + const z = el('li', 'erklaerung__stufe'); + z.append(el('span', 'erklaerung__nr', nr)); + const t = el('div', null); + t.append(el('strong', null, name)); + t.append(el('span', 'erklaerung__was', was)); + z.append(t); + stufenListe.append(z); + } + kasten.append(stufenListe); + + kasten.append(el('p', 'erklaerung__fuss', + 'Zu jedem Kontakt gehört ein Datum für den nächsten Schritt. Ist es ' + + 'überfällig, erscheint der Kontakt auf deiner Startseite – du musst ' + + 'also nicht daran denken, die Seite denkt für dich.')); + + kasten.append(el('p', 'erklaerung__los', + '„Neuer Kontakt" oben rechts legt den ersten an.')); + + ziel.append(kasten); return; } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 8d0c6ff2..997cd03e 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -139,9 +139,9 @@ - - - - + + + + diff --git a/workspace/automation.html b/workspace/automation.html index edf91c2b..2385766a 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -132,9 +132,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 017c2a0d..d104ec75 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -107,9 +107,9 @@ - - - - + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 5b4a9437..d111ceda 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -54,10 +54,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 3118be2a..65786f83 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -158,9 +158,9 @@ - - - - + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 454d0dea..aff17c9f 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -93,9 +93,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 08626a4b..f928124b 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index 1ab5f54a..e7891b8d 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -86,7 +86,7 @@
- +
- - - - + + + + diff --git a/workspace/start.html b/workspace/start.html index 85c00b41..061d3d23 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -7,8 +7,8 @@ - - + + @@ -99,9 +99,9 @@

- - - - + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 86fde41f..4be24a4e 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -70,10 +70,10 @@ - - - - - + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index cf4241dc..1dc431b1 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -85,9 +85,9 @@ - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 2c387b1f..528854fc 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -7,9 +7,9 @@ - - - + + + @@ -57,9 +57,9 @@ - + - - + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 30737ec8..8ff592c7 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -175,9 +175,9 @@ - - - - + + + +