diff --git a/server/helfer-zeit.mjs b/server/helfer-zeit.mjs new file mode 100644 index 00000000..e663d1e7 --- /dev/null +++ b/server/helfer-zeit.mjs @@ -0,0 +1,52 @@ +/* WELCHER TAG IST HEUTE? — für Prüfungen. + + DER FEHLER, DEN DAS VERHINDERT, gefunden am 06.09.2026 um 00:10. + + Der Gesamtlauf meldete plötzlich 13 Fehlschläge in + pruef-teilnehmer.mjs -- an einer Datei, die seit Stunden niemand + angefasst hatte. "der alte Termin ist da: FEHL", "sein Teilnehmer + wurde uebernommen: LEER". Es sah aus, als sei die Teilnehmerfunktion + kaputt. + + Sie war es nicht. Es war zehn nach Mitternacht. + + Ortszeit (MESZ): 06.09.2026, 00:10 + UTC: 05.09.2026, 22:10 + + Die Prüfung bildete ihr Datum mit + `new Date().toISOString().slice(0, 10)` -- das ist UTC und ergab den + 5. September. Die Anwendung rechnet den Tag dagegen LOKAL + (heuteLokal() in workspace.js) und suchte am 6. Die Prüfung legte + ihre Termine also auf gestern und wunderte sich, dass sie heute + keine fand. + + ZWEI STUNDEN AM TAG war diese Prüfung damit rot -- zwischen + Mitternacht und zwei Uhr, im Winter eine Stunde. Wer nur tagsüber + läuft, sieht das nie; wer nachts arbeitet, sucht stundenlang an der + falschen Stelle. Dieselbe Sorte Fehler wie die Layout-Messung, die + je nach Serverlaune anschlug: Ein Ergebnis, das von der Uhrzeit + abhängt, ist kein Ergebnis. + + Deshalb gibt es das hier EINMAL statt viermal in Kopie. Die + Rechenweise ist bewusst dieselbe wie in workspace.js -- eine + Prüfung, die den Tag anders bestimmt als die Anwendung, prüft nicht + die Anwendung, sondern sich selbst. + ===================================================================== */ + +/** Der heutige Tag in ORTSZEIT, als "JJJJ-MM-TT". */ +export function heuteLokal() { + const d = new Date(); + const p = (n) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; +} + +/** Derselbe Tag, um n Tage verschoben (negativ = in der Vergangenheit). + * + * Gerechnet wird über die Millisekunden und danach lokal ausgelesen. + * `setDate(getDate() + n)` täte es auch, hat aber an den zwei + * Umstellungstagen im Jahr seine Tücken. */ +export function tagLokal(versatz) { + const d = new Date(Date.now() + versatz * 86400_000); + const p = (n) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; +} diff --git a/server/pruef-alle-wege.mjs b/server/pruef-alle-wege.mjs index f55547f1..294dd425 100644 --- a/server/pruef-alle-wege.mjs +++ b/server/pruef-alle-wege.mjs @@ -37,6 +37,7 @@ import { mkdtempSync, rmSync, readFileSync, readdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { heuteLokal } from "./helfer-zeit.mjs"; const HIER = dirname(fileURLToPath(import.meta.url)); const ordner = mkdtempSync(join(tmpdir(), "ws-wege-")); @@ -101,7 +102,10 @@ for (let i = 0; i < 60; i++) { } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const heute = jetzt.slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. Mit + `jetzt.slice(0, 10)` lag dieser Wert zwischen Mitternacht und + zwei Uhr einen Tag daneben. */ +const heute = heuteLokal(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); diff --git a/server/pruef-barrierefrei-workspace.mjs b/server/pruef-barrierefrei-workspace.mjs index 63c6ccec..6d0ea39b 100644 --- a/server/pruef-barrierefrei-workspace.mjs +++ b/server/pruef-barrierefrei-workspace.mjs @@ -28,6 +28,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-a11y-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -64,7 +65,10 @@ for (let i = 0; i < 60; i++) { } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const heute = jetzt.slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. Mit + `jetzt.slice(0, 10)` lag dieser Wert zwischen Mitternacht und + zwei Uhr einen Tag daneben. */ +const heute = heuteLokal(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); diff --git a/server/pruef-breiten.mjs b/server/pruef-breiten.mjs index 84b8c815..6e9c6fa8 100644 --- a/server/pruef-breiten.mjs +++ b/server/pruef-breiten.mjs @@ -31,6 +31,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-breit-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -67,7 +68,10 @@ for (let i = 0; i < 60; i++) { } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const heute = jetzt.slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. Mit + `jetzt.slice(0, 10)` lag dieser Wert zwischen Mitternacht und + zwei Uhr einen Tag daneben. */ +const heute = heuteLokal(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); diff --git a/server/pruef-browser.mjs b/server/pruef-browser.mjs index 9f41ef0c..7b338958 100644 --- a/server/pruef-browser.mjs +++ b/server/pruef-browser.mjs @@ -33,6 +33,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-browser-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -88,7 +89,10 @@ for (let i = 0; i < 60; i++) { } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const heute = jetzt.slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. Mit + `jetzt.slice(0, 10)` lag dieser Wert zwischen Mitternacht und + zwei Uhr einen Tag daneben. */ +const heute = heuteLokal(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); diff --git a/server/pruef-css-klassen.mjs b/server/pruef-css-klassen.mjs index 927c453f..511a8c1d 100644 --- a/server/pruef-css-klassen.mjs +++ b/server/pruef-css-klassen.mjs @@ -251,6 +251,109 @@ console.log(` -- ${nurAnker.size} Klassen sind nirgends gestaltet ` + `(Struktur-Anker, kein Fehler): ${[...nurAnker].slice(0, 8).join(", ")}` + (nurAnker.size > 8 ? " …" : "")); +/* ======================================================================= + Schriftgrößen: nichts unter 11,5 Pixeln + ======================================================================= + + ZUM ZWEITEN MAL AM SELBEN TAG (06.09.2026). Erst hatten die + Art-Plaketten des Tagesblicks 10,56 px, dann -- nach der Korrektur -- + die neue "JETZT"-Marke 10,88. Beide Male gemeldet von pruef-handy + und pruef-grosscheck, beide Male erst nach einem Browserlauf von + mehreren Minuten. + + Die Regel steht seit dem 01.09.2026 im Haus: Unter 11,5 px liest auf + einem Handy niemand mehr, und was dort niemand liest, ist auf dem + Rechner nur Zierde. Sie wurde bisher nur AM BILD geprüft. Hier wird + sie an der Quelle geprüft -- in Sekunden statt in Minuten, und + bevor überhaupt jemand einen Browser startet. + + WARUM BEIDE PRÜFUNGEN BLEIBEN: Diese hier findet den Wert im + Stylesheet. Sie kann nicht wissen, ob eine Regel am Ende überhaupt + greift oder von einer späteren überschrieben wird -- das sieht nur + der Browser. Die eine ist schnell und ungenau, die andere langsam + und genau. Zusammen fangen sie beides ab. + + GERECHNET WIRD MIT 16 px Grundgröße: 0,72 rem = 11,52 px ist gerade + noch erlaubt, 0,71 rem = 11,36 px nicht mehr. */ +console.log("\n=== Ist irgendeine Schrift zu klein? ==="); +{ + const GRENZE_PX = 11.5; + const GRUND_PX = 16; + + let gemessen = 0; + const zuKlein = []; + + for (const datei of readdirSync(join(WS, "assets/css")).filter((n) => n.endsWith(".css"))) { + const text = readFileSync(join(WS, "assets/css", datei), "utf8") + .replace(/\/\*[\s\S]*?\*\//g, " "); // Kommentare weg + + for (const treffer of text.matchAll(/font-size:\s*([\d.]+)(rem|em|px)/g)) { + const wert = Number(treffer[1]); + const einheit = treffer[2]; + /* `em` hängt von der Umgebung ab und lässt sich hier nicht + ausrechnen -- solche Stellen werden nicht gezählt, statt sie zu + raten. Sie sind selten und die Browserprüfung sieht sie. */ + if (einheit === "em") continue; + const px = einheit === "px" ? wert : wert * GRUND_PX; + gemessen++; + if (px < GRENZE_PX) { + const zeile = text.slice(0, treffer.index).split("\n").length; + zuKlein.push(`${datei}:${zeile} → ${treffer[1]}${einheit} = ${px.toFixed(2)} px`); + } + } + } + + /* DIE ANZAHL GEHÖRT IN DIE BEDINGUNG: Fände das Muster gar nichts, + stünde sonst "0 zu kleine" da -- grün, ohne je eine Größe + angesehen zu haben. */ + ok(gemessen > 150, `${gemessen} Schriftgrößen im Stylesheet geprüft`); + + /* EINE GRUNDLINIE, KEIN WUNSCHWERT -- dasselbe Vorgehen wie beim + Layout-Sprung in pruef-tempo-workspace. + + Im Bestand stehen 43 solche Stellen. Sie alle zu ändern wäre ein + Eingriff ins Aussehen von dreizehn Stylesheets, kein Fehlerfix -- + und die Browserprüfung (pruef-handy) beanstandet davon nur die, + die auf einem Handy WIRKLICH sichtbar sind. Viele greifen dort + gar nicht oder werden überschrieben. + + Eine Prüfung, die vom ersten Tag an rot ist, wird weggeklickt -- + und mit ihr die echte Warnung. Sie hält deshalb den heutigen Stand + fest und schlägt an, sobald es MEHR werden. Genau das hätte heute + zweimal gegriffen: erst bei den Art-Plaketten (10,56 px), dann bei + der Jetzt-Marke (10,88) -- beide Male hätte es Minuten statt eines + Browserlaufs gekostet. + + Wird der Bestand irgendwann aufgeräumt, gehört die Zahl gesenkt. + Sie steht hier als Zahl und nicht als Liste, damit niemand eine + neue Stelle in eine Ausnahmeliste einträgt und sie damit + stillschweigend erlaubt. */ + const GRUNDLINIE = 43; + ok(zuKlein.length <= GRUNDLINIE && gemessen > 150, + zuKlein.length > GRUNDLINIE + ? `${zuKlein.length} Stellen unter ${GRENZE_PX} px — das sind ` + + `${zuKlein.length - GRUNDLINIE} MEHR als die Grundlinie ${GRUNDLINIE}. ` + + `Neu hinzugekommen ist vermutlich eine davon:\n ` + + zuKlein.slice(-6).join("\n ") + : `${zuKlein.length} Stellen unter ${GRENZE_PX} px ` + + `(Grundlinie ${GRUNDLINIE}, nicht mehr geworden)`); + + if (zuKlein.length && zuKlein.length <= GRUNDLINIE) { + console.log(` -- davon im Bestand: ${zuKlein.slice(0, 3).join(" · ")} …` + + ` — sichtbar sind sie laut pruef-handy nicht, sonst stünden sie dort`); + } + + /* GEGENPROBE: Erkennt die Rechnung beide Einheiten -- und lässt sie + einen erlaubten Wert in Ruhe? Ohne das wäre "0 Treffer" wertlos. */ + const probe = ".a { font-size: .68rem; } .b { font-size: 10px; } .c { font-size: .95rem; }"; + const gefunden = [...probe.matchAll(/font-size:\s*([\d.]+)(rem|em|px)/g)] + .map(([, w, e]) => (e === "px" ? Number(w) : Number(w) * GRUND_PX)) + .filter((px) => px < GRENZE_PX); + ok(gefunden.length === 2, + `Gegenprobe: von drei Größen werden die zwei zu kleinen erkannt ` + + `(${gefunden.map((p) => p.toFixed(2)).join(", ")} px), die dritte nicht`); +} + /* ======================================================================= GEGENPROBE — die Messung muss beides können ======================================================================= */ diff --git a/server/pruef-freie-namen.mjs b/server/pruef-freie-namen.mjs index 17db8257..1f04a87e 100644 --- a/server/pruef-freie-namen.mjs +++ b/server/pruef-freie-namen.mjs @@ -22,6 +22,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal, tagLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-frei-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -154,7 +155,8 @@ ok((uebersicht.daten.offen ?? 0) >= 1, "die Übersicht zählt sie mit"); console.log("\n— Termin mit frei eingetragenem Namen —"); -const morgen = new Date(Date.now() + 86400_000).toISOString().slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. */ +const morgen = tagLokal(1); const termin = await api(seiteDogi, "/workspace/api/termine", { method: "POST", koerper: { diff --git a/server/pruef-manager-sicht.mjs b/server/pruef-manager-sicht.mjs index 8c45ab02..95bfa528 100644 --- a/server/pruef-manager-sicht.mjs +++ b/server/pruef-manager-sicht.mjs @@ -34,6 +34,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal, tagLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-msicht-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -59,7 +60,8 @@ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const tag = (v) => new Date(Date.now() + v * 86400_000).toISOString().slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. */ +const tag = (v) => tagLokal(v); function person(name, rolle, code) { const salt = randomBytes(16).toString("hex"); diff --git a/server/pruef-rollen.mjs b/server/pruef-rollen.mjs index 96ffc274..e2a997d5 100644 --- a/server/pruef-rollen.mjs +++ b/server/pruef-rollen.mjs @@ -29,6 +29,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal, tagLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-rollen-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -58,7 +59,8 @@ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const tag = (v) => new Date(Date.now() + v * 86400_000).toISOString().slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. */ +const tag = (v) => tagLokal(v); function person(name, rolle, code) { const salt = randomBytes(16).toString("hex"); diff --git a/server/pruef-struktur.mjs b/server/pruef-struktur.mjs index 694738ea..8bc5527e 100644 --- a/server/pruef-struktur.mjs +++ b/server/pruef-struktur.mjs @@ -490,6 +490,84 @@ console.log(`\n=== Laedt jedes Server-Modul? ===`); for (const f of [boese, brav]) { try { unlinkSync(f); } catch { /* egal */ } } } +/* ---------- Rechnet jede Pruefung den TAG so wie die Anwendung? -------- + + GEFUNDEN AM 06.09.2026 UM 00:10, und zwar auf die harte Tour: Der + Gesamtlauf meldete 13 Fehlschlaege in pruef-teilnehmer.mjs -- an + einer Datei, die seit Stunden niemand angefasst hatte. + + Ortszeit (MESZ): 06.09.2026, 00:10 + UTC: 05.09.2026, 22:10 + + Die Pruefung bildete ihr Datum mit `toISOString().slice(0, 10)` -- + das ist UTC. Sie legte ihre Termine also auf gestern und fand heute + nichts. ZWEI STUNDEN AM TAG war sie damit rot (im Winter eine). + + Wer nur tagsueber laeuft, sieht das nie. Wer nachts arbeitet, sucht + stundenlang am falschen Ende -- die Meldungen sagen "der Termin ist + weg", nicht "dein Datum ist falsch". + + Deshalb wird es hier gesucht, statt darauf zu warten, dass es wieder + jemanden trifft. Gemeint ist nur das TAGESDATUM; ein Zeitstempel + (`erstellt`) darf und soll UTC sein. */ +console.log(`\n=== Rechnen die Pruefungen mit Ortszeit? ===`); +{ + const dateien = readdirSync(SERVER).filter((n) => n.startsWith("pruef-") && n.endsWith(".mjs")); + ok(dateien.length >= 30, `${dateien.length} Pruefdateien durchgesehen`); + + const verdaechtig = []; + for (const datei of dateien) { + /* Die eigene Datei bleibt aussen vor: Die Gegenprobe unten MUSS + das gesuchte Muster als Text enthalten, sonst könnte sie nicht + beweisen, dass sie es erkennt. Sie würde sich sonst selbst + anzeigen -- eine Warnung, die immer kommt, und die liest bald + niemand mehr. */ + if (datei === "pruef-struktur.mjs") continue; + const text = readFileSync(join(SERVER, datei), "utf8").replace(/\/\*[\s\S]*?\*\//g, " "); + /* Das Muster ist eng gefasst: NUR toISOString() gefolgt von einem + Schnitt auf zehn Zeichen -- also genau die Bildung eines + Tagesdatums. `new Date().toISOString()` allein (ein voller + Zeitstempel) ist voellig in Ordnung und faellt nicht darunter. */ + for (const treffer of text.matchAll(/toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) { + const zeile = text.slice(0, treffer.index).split("\n").length; + verdaechtig.push(`${datei}:${zeile}`); + } + /* Und die zweite Schreibweise: erst in eine Variable, dann geschnitten. */ + for (const treffer of text.matchAll(/=\s*(\w+)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/g)) { + const name = treffer[1]; + /* Nur melden, wenn diese Variable aus toISOString() stammt. */ + if (new RegExp(`(const|let)\\s+${name}\\s*=\\s*new Date\\(\\)\\.toISOString\\(\\)`).test(text)) { + const zeile = text.slice(0, treffer.index).split("\n").length; + verdaechtig.push(`${datei}:${zeile} (über ${name})`); + } + } + } + + ok(verdaechtig.length === 0, + verdaechtig.length + ? `bilden ihr Tagesdatum aus UTC statt Ortszeit — nachts zwischen ` + + `00:00 und 02:00 schlagen sie fälschlich an:\n ` + + verdaechtig.join("\n ") + : `keine Prüfung bildet ihr Tagesdatum aus UTC (alle nutzen helfer-zeit.mjs)`); + + /* GEGENPROBE: Findet das Muster überhaupt etwas? Ohne diesen + Nachweis wäre "0 Treffer" auch dann grün, wenn der reguläre + Ausdruck gar nichts erkennt. */ + const probe = "const jetzt = new Date().toISOString();\nconst heute = jetzt.slice(0, 10);"; + const erkanntA = /toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/ + .test("new Date().toISOString().slice(0, 10)"); + const erkanntB = /=\s*(\w+)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/.test(probe) + && /(const|let)\s+jetzt\s*=\s*new Date\(\)\.toISOString\(\)/.test(probe); + ok(erkanntA && erkanntB, + "Gegenprobe: beide Schreibweisen werden erkannt (direkt und über eine Variable)"); + + /* Und ein harmloser Zeitstempel darf NICHT anschlagen -- sonst wäre + die Prüfung eine Warnung, die immer kommt. */ + const harmlos = /toISOString\(\)\s*\.\s*slice\(\s*0\s*,\s*10\s*\)/ + .test("const jetzt = new Date().toISOString();"); + ok(!harmlos, "Gegenprobe: ein voller Zeitstempel (ohne Schnitt) schlägt nicht an"); +} + /* ---------- Gegenprobe --------------------------------------------------- */ console.log(`\n=== Gegenprobe (muss anschlagen) ===`); diff --git a/server/pruef-tagesblick.mjs b/server/pruef-tagesblick.mjs index cc066430..1049ff02 100644 --- a/server/pruef-tagesblick.mjs +++ b/server/pruef-tagesblick.mjs @@ -220,15 +220,46 @@ melde("\n=== Die Termine des Tages ==="); Eine Prüfung, deren Ergebnis von der Uhrzeit des Laufs abhängt, ist keine Prüfung -- sie ist morgens grün und abends rot, und irgendwann glaubt ihr niemand mehr. Deshalb wird hier --ton - gelesen: die Zuordnung Art → Farbe, die immer gilt. Der Rand - folgt darunter, aber nur an einem Termin, der nachweislich noch - nicht vorbei ist. */ - const toene = await seite.$$eval(".tagesliste__punkt", (punkte) => punkte.map((p) => ({ - art: p.dataset.art, - ton: getComputedStyle(p).getPropertyValue("--ton").trim().toLowerCase(), - vorbei: p.dataset.vorbei === "ja", - rand: getComputedStyle(p.querySelector(".tagesliste__marke")).borderTopColor, - }))); + gelesen: die Zuordnung Art → Farbe, die immer gilt. Die sichtbare + Kante folgt darunter, aber nur an einem Termin, der nachweislich + noch nicht vorbei ist. */ + const toene = await seite.$$eval(".tagesliste__punkt", (punkte) => punkte.map((p) => { + const vorbei = p.dataset.vorbei === "ja"; + /* DIE DÄMPFUNG WIRD FÜR DIE MESSUNG KURZ ABGESCHALTET. + + Ein vorbeigezogener Termin setzt --ton bewusst auf Grau. Wer + die Zuordnung Art → Farbe an ihm misst, misst die Dämpfung und + nennt sie einen Fehler -- genau das ist im ersten Entwurf + passiert ("falsche Farbe: call=#75859a"). + + Statt nur die ausstehenden zu messen (dann hinge das Ergebnis + wieder an der Uhrzeit des Laufs, und abends bliebe fast nichts + übrig) wird das Merkmal hier kurz entfernt, gemessen und sofort + zurückgesetzt. Damit ist die Zuordnung für JEDEN Termin geprüft, + unabhängig davon, wann jemand den Lauf startet. */ + if (vorbei) delete p.dataset.vorbei; + const ton = getComputedStyle(p).getPropertyValue("--ton").trim().toLowerCase(); + if (vorbei) p.dataset.vorbei = "ja"; + + return { + art: p.dataset.art, + ton, + vorbei, + /* Die farbige Kante links ist seit dem Umbau auf Karten + (05.09.2026) ein ::before -- vorher war es der Ring einer + Marke. Gemessen wird ihr Hintergrund, dort steckt die Farbe. */ + kante: getComputedStyle(p, "::before").backgroundImage, + /* Und der Dauerbalken: Seine Füllung sagt, wie lang der Termin + ist. Ein Balken, der bei jeder Dauer gleich aussieht, wäre + Deko -- und Deko, die aussieht wie eine Angabe, führt in die + Irre. */ + laenge: getComputedStyle(p.querySelector(".tagesliste__balken") || p) + .getPropertyValue("--laenge").trim(), + /* Der gedämpfte Ton, separat: So lässt sich beides beweisen -- + die richtige Zuordnung UND dass die Dämpfung greift. */ + tonGedaempft: getComputedStyle(p).getPropertyValue("--ton").trim().toLowerCase(), + }; + })); const SOLL = { call: "#3fbdf5", termin: "#8a76ff", review: "#50a752", frist: "#d17717" }; const falsch = toene.filter((t) => SOLL[t.art] && t.ton !== SOLL[t.art]); @@ -238,17 +269,17 @@ melde("\n=== Die Termine des Tages ==="); : `alle ${toene.length} Termine tragen die Kalenderfarbe ihrer Art ` + `(${toene.map((t) => `${t.art} ${t.ton}`).join(", ")})`); - /* Und der Rand nimmt sie auch wirklich an -- gemessen an einem + /* Und die Kante nimmt sie auch wirklich an -- gemessen an einem Termin, der noch aussteht. Gibt es keinen (ein Lauf kurz vor Mitternacht), wird das gesagt statt behauptet: der dritte Ausgang. */ const offen = toene.find((t) => !t.vorbei); if (offen) { const soll = SOLL[offen.art]; const r = parseInt(soll.slice(1, 3), 16), g = parseInt(soll.slice(3, 5), 16), b = parseInt(soll.slice(5, 7), 16); - ok(offen.rand.replace(/\s/g, "") === `rgb(${r},${g},${b})`, - `der Ring des nächsten Termins (${offen.art}) trägt sie sichtbar: ${offen.rand}`); + ok(offen.kante.replace(/\s/g, "").includes(`rgb(${r},${g},${b})`), + `die Kante des nächsten Termins (${offen.art}) trägt die Farbe sichtbar`); } else { - melde(` -- kein ausstehender Termin zur Zeit des Laufs — der Ring ` + melde(` -- kein ausstehender Termin zur Zeit des Laufs — die Kante ` + `wurde nicht am Bild geprüft (die Zuordnung darüber schon)`); } @@ -257,11 +288,30 @@ melde("\n=== Die Termine des Tages ==="); eine Behauptung im Stylesheet. */ const vorbei = toene.find((t) => t.vorbei); if (vorbei) { - ok(vorbei.rand.replace(/\s/g, "") !== `rgb(80,167,82)`.replace(/\s/g, "") - || vorbei.art !== "review", - `ein vorbeigezogener Termin ist gedämpft (Ring ${vorbei.rand} statt der Artfarbe)`); + /* Beides zusammen ist der Beweis: Die Zuordnung stimmt (ton), UND + die Dämpfung überschreibt sie (tonGedaempft). Nur eines von + beidem zu messen liesse offen, ob die Dämpfung überhaupt wirkt + oder ob die Farbe von vornherein fehlte. */ + ok(vorbei.tonGedaempft === "#75859a" && vorbei.ton !== "#75859a", + `vorbei wird gedämpft: ${vorbei.art} trägt ${vorbei.ton}, ` + + `angezeigt wird ${vorbei.tonGedaempft}`); } + /* DER DAUERBALKEN muss die Dauer WIRKLICH abbilden. Ein Balken, der + bei 30 und bei 60 Minuten gleich aussieht, ist Deko -- und Deko, + die aussieht wie eine Angabe, ist schlimmer als keine. */ + const laengen = toene.map((t) => t.laenge).filter(Boolean); + ok(laengen.length === 3 && new Set(laengen).size >= 2, + laengen.length !== 3 + ? `nur ${laengen.length} von 3 Terminen haben einen Dauerbalken` + : `die Balken unterscheiden sich nach Dauer: ${laengen.join(", ")}`); + + /* Und die JETZT-Marke steht genau am nächsten Termin -- nicht an + allen, nicht an keinem. */ + const jetztMarken = await seite.$$eval(".tagesliste__jetzt", + (m) => m.filter((x) => getComputedStyle(x).display !== "none").length); + ok(jetztMarken === 1, `die Marke "Jetzt" steht ${jetztMarken}× (erwartet genau 1×)`); + /* GENAU EINER ist "als Nächstes dran" -- nicht null, nicht zwei. */ const naechste = zeilen.filter((z) => z.naechster).length; ok(naechste === 1, `genau ${naechste} Termin ist als Nächstes hervorgehoben`); diff --git a/server/pruef-teilnehmer.mjs b/server/pruef-teilnehmer.mjs index a42e1388..7d6f78c5 100644 --- a/server/pruef-teilnehmer.mjs +++ b/server/pruef-teilnehmer.mjs @@ -27,6 +27,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-teiln-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -63,7 +64,10 @@ for (let i = 0; i < 60; i++) { } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const heute = jetzt.slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. Mit + `jetzt.slice(0, 10)` war diese Pruefung jede Nacht zwischen 00:00 + und 02:00 rot, weil sie ihre Termine auf gestern legte. */ +const heute = heuteLokal(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); diff --git a/server/pruef-tempo-workspace.mjs b/server/pruef-tempo-workspace.mjs index 6846764d..9f12ae96 100644 --- a/server/pruef-tempo-workspace.mjs +++ b/server/pruef-tempo-workspace.mjs @@ -31,6 +31,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { heuteLokal } from "./helfer-zeit.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-tempo-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -67,7 +68,8 @@ for (let i = 0; i < 60; i++) { } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); -const heute = jetzt.slice(0, 10); +/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. */ +const heute = heuteLokal(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); @@ -122,6 +124,25 @@ await seite.fill("#code", "CODE-DOGI-0001"); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 20000 }); +/* ERST ZUR RUHE KOMMEN LASSEN, dann messen (06.09.2026). + + Die Anmeldung landet auf start.html -- und diese Seite holt ihre + Daten nach. Startet die Messschleife sofort, laufen diese Anfragen + noch, und der Zaehler der ERSTEN Seite bekommt sie mit. Genau das + ist passiert, als der Tagesblick dazukam: Die Pruefung meldete + "NEUE Doppelabfrage: start.html 2x /workspace/api/termine". + + Es waren aber zwei SEITENAUFRUFE mit je einer Anfrage -- einmal + durch die Weiterleitung nach dem Anmelden, einmal durch die + Messschleife. Nachgemessen an einem einzelnen Aufruf: genau eine + Anfrage. Die Pruefung hat also nicht den Code beanstandet, sondern + ihre eigene Vorbereitung. + + Wer hier nicht wartet, gibt jeder Seite die Schuld an dem, was die + vorherige noch offen hatte. */ +await seite.waitForLoadState("networkidle"); +await seite.waitForTimeout(600); + /* Grenzen. Grosszuegig -- sie sollen den Ausreisser finden, nicht das Kilobyte. */ const GRENZE_ANFRAGEN = 40; diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index 05c00ae9..e68b6c88 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -3222,6 +3222,18 @@ body[data-fremde-sicht="ja"]::after { .tagesblick__seite--dran { grid-column: 1; } .tagesblick__seite--heute { grid-column: 3; } +/* SOLANGE GELADEN WIRD, hält die Kachel ihre Höhe. Ohne das sprang + alles darunter nach unten, sobald die Daten ankamen -- gemessen als + Layout-Sprung 0,96 auf der Startseite. Der Wert entspricht ungefähr + einer Kachel mit zwei bis drei Einträgen je Seite. Gemessen: ein + Termin ergibt 169 px, drei ergeben 327 px -- 280 liegt dazwischen, + der Rest-Sprung ist damit in beide Richtungen klein. */ +.tagesblick[data-laedt="ja"] { min-height: 280px; } +/* Die Überschriften stehen schon da, der Rest bleibt ruhig leer. Ein + zappelndes Skelett wäre hier Selbstzweck -- die Wartezeit ist der + eine Moment, in dem die Daten aus der Datenbank kommen. */ +.tagesblick[data-laedt="ja"] .tagesblick__leer { visibility: hidden; } + .tagesblick__seite { display: flex; flex-direction: column; padding: 17px 19px 19px; @@ -3263,131 +3275,247 @@ body[data-fremde-sicht="ja"]::after { font-size: .84rem; color: var(--text-still); } -/* ---------- Die Termine als Zeitstrahl ---------------------------------- */ +/* ---------- Die Termine des Tages --------------------------------------- + + ZWEITER ANLAUF (05.09.2026). Der erste war eine Liste mit einem + dünnen Strich dahinter. Filipes Urteil an einem Tag mit EINEM + Termin: *"das soll viel geiler und krasser aussehen."* + + Er hatte recht, und der Grund ist der Einzelfall: Ein Zeitstrahl + lebt davon, dass er etwas VERBINDET. Bei einem einzigen Eintrag + verbindet er nichts -- übrig bleiben eine magere Zeile, ein Strich + ins Leere und viel Weißraum bis zur Plakette am rechten Rand. + + WAS SICH ÄNDERT: Jeder Termin ist jetzt eine eigene Fläche mit + farbiger Kante statt einer nackten Zeile. Das trägt auch allein. + Dazu drei Dinge, die vorher fehlten: + + * Die Uhrzeit ist groß und trägt die Farbe der Terminart. Sie ist + die Auskunft, die man sucht -- vorher war sie kleiner als der + Titel. + * Ein Balken zeigt die DAUER. Aus "30 min" als Text wird etwas, + das man sieht, ohne zu lesen: Ein Zweistundenblock ist ein + anderer Tag als vier Viertelstunden. + * Der nächste Termin bekommt einen Ring und die Marke "JETZT" -- + nicht bloß einen helleren Hintergrund. + + WAS SICH NICHT ÄNDERT: kein Neon, keine harten Weißflächen, keine + Bewegung. "Krass" entsteht hier aus Kontrast und Hierarchie, nicht + aus Leuchtkraft -- augenschonend bleibt Hausregel. Die Farbe steckt + weiterhin in wenigen kleinen Flächen; sie wirkt nur stärker, weil + drumherum mehr Ruhe ist. */ .tagesliste { - display: grid; gap: 2px; + display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; - position: relative; -} - -/* Die senkrechte Linie hinter den Punkten. Sie beginnt am ersten und - endet am letzten Punkt -- eine Linie, die über den letzten Termin - hinausläuft, verspricht etwas, das nicht kommt. */ -.tagesliste::before { - content: ""; - position: absolute; left: 61px; top: 18px; bottom: 18px; width: 1px; - background: linear-gradient(180deg, - transparent, var(--rand-hell) 12%, var(--rand-hell) 88%, transparent); } .tagesliste__punkt { --ton: #8a76ff; position: relative; display: grid; - grid-template-columns: 46px 16px 1fr auto; + grid-template-columns: auto 1fr auto; align-items: center; - gap: 10px; - padding: 9px 10px 9px 0; + gap: 14px; + padding: 13px 15px 13px 17px; + border: 1px solid var(--rand); border-radius: var(--radius-klein); - transition: background .18s ease; + /* Ein Hauch der Terminfarbe von links, dann nichts mehr. Die Fläche + bleibt fast schwarz -- die Farbe soll die Kante betonen, nicht die + Karte einfärben. */ + background: + linear-gradient(90deg, + color-mix(in srgb, var(--ton) 8%, transparent), + transparent 42%), + rgba(255, 255, 255, .022); + transition: border-color .18s ease, background .18s ease, transform .18s ease; + overflow: hidden; } -/* Dieselben vier Farben wie im Kalender (ARTFARBE in kalender.js). - Eine Farbe, die hier etwas anderes bedeutete als dort, müsste man - zweimal lernen. */ + +/* DIE KANTE LINKS ist der eigentliche Träger der Farbe. Oben kräftig, + unten auslaufend -- ein durchgehender Balken wirkt wie ein Etikett, + ein auslaufender wie Licht. */ +.tagesliste__punkt::before { + content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; + background: linear-gradient(180deg, + var(--ton), color-mix(in srgb, var(--ton) 30%, transparent)); +} + .tagesliste__punkt[data-art="call"] { --ton: #3fbdf5; } .tagesliste__punkt[data-art="termin"] { --ton: #8a76ff; } .tagesliste__punkt[data-art="review"] { --ton: #50a752; } .tagesliste__punkt[data-art="frist"] { --ton: #d17717; } +.tagesliste__punkt:hover { + border-color: color-mix(in srgb, var(--ton) 40%, var(--rand)); + background: + linear-gradient(90deg, + color-mix(in srgb, var(--ton) 13%, transparent), + transparent 48%), + rgba(255, 255, 255, .045); +} + +/* ---- Die Zeit: groß, farbig, mit Dauerbalken ---- */ + .tagesliste__zeit { - display: flex; flex-direction: column; align-items: flex-end; - gap: 1px; - /* Tabellenziffern: Sonst tanzen die Uhrzeiten untereinander, weil - die 1 schmaler ist als die 8. */ - font-variant-numeric: tabular-nums; + display: flex; flex-direction: column; align-items: flex-start; + gap: 7px; + min-width: 92px; +} +/* Balken und Minutenzahl stehen NEBENEINANDER, nicht übereinander. + Untereinander lag der Balken direkt unter der Uhrzeit und las sich + als deren Unterstreichung -- er sah aus wie Deko und nicht wie eine + Angabe. Nebeneinander ist klar, dass die Länge etwas bedeutet, und + die linke Spalte wird eine Zeile flacher. */ +.tagesliste__laenge { + display: flex; align-items: center; gap: 8px; + width: 100%; } .tagesliste__stunde { - font-size: .88rem; font-weight: 650; color: var(--text-leise); - letter-spacing: .01em; + font-size: 1.12rem; font-weight: 700; + letter-spacing: -.01em; line-height: 1; + color: color-mix(in srgb, var(--ton) 62%, #ffffff); + font-variant-numeric: tabular-nums; } -/* 0,74 rem = 11,84 px. NICHT kleiner: Unter 11,5 px meldet die - Geräteprüfung zu Recht "zu kleine Schrift" -- und was auf dem Handy - niemand liest, ist auf dem Rechner nur Zierde. Der erste Entwurf - hatte 0,68 rem (10,88 px). */ -.tagesliste__dauer { font-size: .74rem; color: var(--text-still); } -/* Der Punkt auf der Linie. Ein Ring, kein Klecks -- er soll die Linie - markieren, nicht überdecken. */ -.tagesliste__marke { - justify-self: center; - width: 9px; height: 9px; border-radius: 50%; - background: #0b0f16; - border: 2px solid var(--ton); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--ton) 12%, transparent); +/* DER DAUERBALKEN. Seine Breite kommt aus --laenge (in start.js + gesetzt): 30 Minuten sind kurz, zwei Stunden füllen ihn. Damit sieht + man die Länge, statt sie zu lesen. + + Gedeckelt bei 100 %, weil ein Ganztagestermin den Balken sonst aus + der Karte schöbe. */ +.tagesliste__balken { + position: relative; flex: none; + /* Feste Breite statt 100 %: Die Spur ist der Maßstab, an dem man die + Füllung abliest. Wäre sie mal breiter, mal schmaler, sagte die + Füllung nichts mehr. */ + width: 40px; height: 4px; + border-radius: 999px; + background: rgba(255, 255, 255, .08); + overflow: hidden; } +.tagesliste__balken::after { + content: ""; position: absolute; inset: 0; + width: var(--laenge, 30%); + border-radius: 999px; + background: linear-gradient(90deg, + var(--ton), color-mix(in srgb, var(--ton) 45%, transparent)); +} +.tagesliste__dauer { + font-size: .74rem; color: var(--text-still); + font-variant-numeric: tabular-nums; +} + +/* ---- Text ---- */ .tagesliste__text { min-width: 0; } .tagesliste__titel { margin: 0; - font-size: .89rem; font-weight: 600; color: var(--text-leise); - /* Ein langer Titel darf die Kachel nicht breiter machen. */ + font-size: .96rem; font-weight: 620; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tagesliste__unter { - margin: 2px 0 0; - font-size: .75rem; color: var(--text-still); + margin: 3px 0 0; + font-size: .78rem; color: var(--text-still); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* ---- Rechte Seite: Art und Zustand ---- */ + +.tagesliste__rechts { + display: flex; flex-direction: column; align-items: flex-end; gap: 6px; +} + .tagesliste__art { - padding: 2px 9px; border-radius: 999px; - /* 11,84 px statt der ursprünglichen 10,56. Gemeldet von pruef-handy - auf allen drei Geräteklassen: "zu kleine Schrift: Call 10.56px". - Großbuchstaben mit Sperrung wirken kleiner als sie sind -- gerade - deshalb ist die Grenze hier nicht verhandelbar. */ + padding: 3px 10px; border-radius: 999px; + /* 11,84 px. Unter 11,5 meldet die Geräteprüfung zu Recht "zu kleine + Schrift" -- und Großbuchstaben mit Sperrung wirken kleiner, als + sie sind. */ font-size: .74rem; font-weight: 700; - letter-spacing: .05em; text-transform: uppercase; - color: color-mix(in srgb, var(--ton) 72%, #ffffff); - background: color-mix(in srgb, var(--ton) 11%, transparent); - border: 1px solid color-mix(in srgb, var(--ton) 26%, transparent); + letter-spacing: .06em; text-transform: uppercase; + color: color-mix(in srgb, var(--ton) 78%, #ffffff); + background: color-mix(in srgb, var(--ton) 13%, transparent); + border: 1px solid color-mix(in srgb, var(--ton) 30%, transparent); white-space: nowrap; } -/* Der ganze Eintrag ist anklickbar, ohne dass ein um alles liegt: - Ein Link, der Überschrift, Zeit und Marke umschließt, liest sich für - ein Vorleseprogramm als ein einziger langer Name. So bleibt die - Struktur erhalten, und das aria-label sagt den ganzen Satz. */ +/* DIE JETZT-MARKE. Sie sagt in einem Wort, was der hellere Hintergrund + vorher nur andeutete. */ +.tagesliste__jetzt { + display: none; + align-items: center; gap: 5px; + /* 11,84 px. Zum ZWEITEN Mal an dieser Kachel korrigiert -- erst die + Art-Plaketten, jetzt diese Marke. Unter 11,5 px meldet die + Geraetepruefung zu Recht "zu kleine Schrift", und Grossbuchstaben + mit weiter Sperrung wirken noch kleiner, als sie sind. */ + font-size: .74rem; font-weight: 800; + letter-spacing: .1em; text-transform: uppercase; + color: var(--ton); +} +.tagesliste__jetzt::before { + content: ""; width: 6px; height: 6px; border-radius: 50%; + background: var(--ton); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--ton) 22%, transparent); +} +.tagesliste__punkt[data-naechster="ja"] .tagesliste__jetzt { display: inline-flex; } + +/* ---- Als Nächstes dran ---- */ + +.tagesliste__punkt[data-naechster="ja"] { + border-color: color-mix(in srgb, var(--ton) 45%, transparent); + background: + linear-gradient(90deg, + color-mix(in srgb, var(--ton) 15%, transparent), + transparent 55%), + rgba(255, 255, 255, .04); +} +.tagesliste__punkt[data-naechster="ja"]::before { width: 4px; } +.tagesliste__punkt[data-naechster="ja"] .tagesliste__stunde { + color: color-mix(in srgb, var(--ton) 40%, #ffffff); +} + +/* ---- Vorbei ---- + Zurücktreten, nicht verschwinden: Man will sehen, was man heute + schon hinter sich hat. Statt alles blass zu machen (dann leidet die + Lesbarkeit), verliert nur die FARBE an Kraft -- der Text bleibt. */ +.tagesliste__punkt[data-vorbei="ja"] { + --ton: #75859a; + background: rgba(255, 255, 255, .015); +} +.tagesliste__punkt[data-vorbei="ja"] .tagesliste__titel { color: var(--text-leise); } +.tagesliste__punkt[data-vorbei="ja"]::before { opacity: .5; } +.tagesliste__punkt[data-vorbei="ja"]:hover { background: rgba(255, 255, 255, .04); } + +/* Der Haken sagt "erledigt" schneller als jede Farbe. */ +.tagesliste__punkt[data-vorbei="ja"] .tagesliste__art::before { + content: "✓ "; + font-weight: 700; +} + +/* ---- Der ganze Eintrag ist anklickbar ---- + Ein um alles herum läse sich für ein Vorleseprogramm als ein + einziger langer Name. So bleibt die Struktur erhalten, und das + aria-label sagt den ganzen Satz. */ .tagesliste__link { position: absolute; inset: 0; border-radius: var(--radius-klein); } +/* "3 weitere Termine heute" -- der Hinweis auf das, was nicht mehr + in die Kachel passte. Still gehalten: Er ist ein Wegweiser, keine + sechste Karte. */ +.tagesliste__mehr { + padding: 4px 0 0 2px; +} +.tagesliste__mehr-link { + font-size: .8rem; color: var(--text-still); + text-decoration: none; + transition: color .16s ease; +} +.tagesliste__mehr-link:hover { color: var(--text-leise); } + .tagesliste__link:focus-visible { - outline: 2px solid var(--ton); outline-offset: -2px; + outline: 2px solid var(--ton); outline-offset: -3px; } -.tagesliste__punkt:hover { background: rgba(255, 255, 255, .035); } - -/* WAS ALS NÄCHSTES DRAN IST. Beim Überfliegen ist das die eigentliche - Auskunft -- nicht "der erste Termin des Tages". */ -.tagesliste__punkt[data-naechster="ja"] { - background: color-mix(in srgb, var(--ton) 7%, transparent); -} -.tagesliste__punkt[data-naechster="ja"] .tagesliste__titel { color: var(--text); } -.tagesliste__punkt[data-naechster="ja"] .tagesliste__stunde { color: var(--text); } -.tagesliste__punkt[data-naechster="ja"] .tagesliste__marke { - background: var(--ton); - box-shadow: 0 0 0 4px color-mix(in srgb, var(--ton) 16%, transparent); -} - -/* VORBEI heißt nicht weg. Man will sehen, was man heute schon hinter - sich hat -- es tritt nur zurück. */ -.tagesliste__punkt[data-vorbei="ja"] { opacity: .45; } -.tagesliste__punkt[data-vorbei="ja"]:hover { opacity: .75; } -.tagesliste__punkt[data-vorbei="ja"] .tagesliste__marke { - border-color: var(--text-still); - box-shadow: none; -} - /* ---------- Schmale Schirme -------------------------------------------- Untereinander statt nebeneinander -- und der Trenner wird waagerecht. @@ -3411,42 +3539,44 @@ body[data-fremde-sicht="ja"]::after { } @media (max-width: 460px) { - /* Die Dauer entfällt zuerst -- sie ist die Angabe, die man am - seltensten braucht, und schafft Platz für den Titel. */ - .tagesliste__dauer { display: none; } .tagesliste__punkt { - grid-template-columns: 40px 14px 1fr; - gap: 8px; - /* OBEN AUSRICHTEN, nicht mittig. Sobald die Plakette unter den - Titel rutscht, wird die Zeile hoch -- eine mittig gesetzte - Uhrzeit steht dann auf Höhe der zweiten Zeile und fluchtet mit - gar nichts. Am Rechner bleibt es mittig, dort ist die Zeile - einzeilig. */ + /* Die Uhrzeitspalte wird schmaler, der Rest bleibt. Die Plakette + nach unten zu schieben wäre der naheliegende Griff -- sie steht + dann aber unter dem Titel und macht jede Karte fünfzig Pixel + höher, ohne etwas zu gewinnen. */ + gap: 11px; + padding: 12px 13px 12px 15px; + /* OBEN AUSRICHTEN: Bricht der Titel auf zwei Zeilen, stünde eine + mittig gesetzte Uhrzeit auf Höhe der zweiten und fluchtete mit + nichts. Am Rechner bleibt es mittig, dort ist alles einzeilig. */ align-items: start; } - /* Die Zeit und der Punkt auf der Linie rücken auf die Höhe des - Titels -- die drei gehören optisch zusammen. */ - .tagesliste__zeit { padding-top: 1px; } - .tagesliste__marke { margin-top: 6px; } - /* Die Art steht dann unter dem Titel statt daneben. */ - .tagesliste__art { - grid-column: 3; justify-self: start; - margin-top: 6px; - } - .tagesliste::before { left: 54px; } + .tagesliste__zeit { min-width: 78px; } + .tagesliste__stunde { font-size: 1.02rem; } + /* Der Balken entfaellt am Handy -- neben ihm bliebe fuer den Titel + zu wenig. Die Zahl sagt es weiterhin. */ + .tagesliste__balken { display: none; } + .tagesliste__titel { font-size: .92rem; white-space: normal; } + .tagesliste__rechts { padding-top: 2px; } } @media (prefers-reduced-motion: reduce) { .tagesliste__punkt, .tagesblick__mehr-pfeil { transition: none; } } -/* Ohne Farbverläufe tragen Kante und Ring allein. Der Trenner wird zur - echten Linie -- ein Verlauf, den es nicht gibt, wäre sonst gar nichts. */ +/* Ohne Farbverläufe trägt die Kante allein -- und die ist hier ein + Verlauf, also muss sie zur echten Fläche werden. Sonst verschwindet + ausgerechnet das Merkmal, das die Terminart anzeigt. */ @media (forced-colors: active) { .tagesblick { border: 1px solid CanvasText; } .tagesblick__seite--heute::before { background: CanvasText; } - .tagesliste::before { background: CanvasText; } - .tagesliste__marke { border-color: CanvasText; } + .tagesliste__punkt { border: 1px solid CanvasText; } + .tagesliste__punkt::before { background: CanvasText; } + .tagesliste__punkt[data-naechster="ja"] { border-width: 2px; } + .tagesliste__balken::after { background: CanvasText; } .tagesliste__art { border: 1px solid CanvasText; } + .tagesliste__jetzt::before { background: CanvasText; box-shadow: none; } + /* Im Kontrastmodus gibt es keine gedämpften Töne -- der Haken vor + der Art bleibt das Merkmal für "vorbei". */ .tagesliste__punkt[data-vorbei="ja"] { opacity: 1; } } diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index d6919a90..6a0fc903 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -535,11 +535,13 @@ const etwasRechts = $('tagestermine').childElementCount > 0; /* Solange die Termine noch unterwegs sind, wird nicht versteckt -- sonst blitzt die Kachel auf und verschwindet wieder. */ - if (!tagesterminGeladen) { - if (etwasLinks) $('dran-block').hidden = false; - return; - } - $('dran-block').hidden = !etwasLinks && !etwasRechts; + const kachel = $('dran-block'); + if (!tagesterminGeladen) return; /* der Platzhalter haelt die Hoehe */ + + /* Jetzt steht beides fest: Der Platzhalter wird abgeraeumt, und die + Kachel bleibt nur, wenn sie etwas zu zeigen hat. */ + delete kachel.dataset.laedt; + kachel.hidden = !etwasLinks && !etwasRechts; } /** hh:mm aus einem Zeitstempel, ohne Zeitzonen-Rechnerei. @@ -587,7 +589,34 @@ return !Number.isNaN(ende.getTime()) ? ende >= jetzt : uhrzeit(t.beginn) >= jetztText; }); - for (const t of termine) { + /* HÖCHSTENS FÜNF. Zwei Gründe, und beide zählen: + + Fachlich: Eine Kachel auf der Startseite beantwortet "was kommt + als Nächstes", nicht "was war alles". Wer zwanzig Termine hat, + will die nächsten sehen und dann in den Kalender -- eine Liste, + die den halben Bildschirm füllt, nimmt den Blick auf alles + darunter. + + Technisch: Ohne Grenze hängt die Höhe der Kachel an der Zahl der + Termine, und der reservierte Platz kann nie passen. Der + Layout-Sprung lag deshalb bei 0,93 -- alles unterhalb rutschte + beim Laden nach unten. Mit einer Obergrenze ist die Höhe + planbar. + + Warum DREI und nicht fuenf: Gemessen wurde die Kachelhoehe -- + ein Termin 169 px, drei 327 px, fuenf 486 px. Je weiter die + Spanne, desto weniger kann der reservierte Platz passen, und + desto staerker springt die Seite. Bei drei liegt die Spanne bei + gut 150 Pixeln statt bei 320. + + Die weggelassenen werden GENANNT, nicht verschwiegen: Eine + Liste, die stillschweigend abschneidet, liest sich wie der + ganze Tag -- und man verpasst den Termin um sechs. */ + const ZEIGE_HOECHSTENS = 3; + const gezeigt = termine.slice(0, ZEIGE_HOECHSTENS); + const weitere = termine.length - gezeigt.length; + + for (const t of gezeigt) { const li = el('li', 'tagesliste__punkt'); li.dataset.art = t.art || 'termin'; @@ -597,19 +626,30 @@ hinter sich hat. Es tritt nur zurück. */ else if (beginn < jetztText) li.dataset.vorbei = 'ja'; + /* ---- Links: Uhrzeit, Dauerbalken, Dauer ---- */ const zeit = el('span', 'tagesliste__zeit'); zeit.append(el('span', 'tagesliste__stunde', beginn)); + if (t.dauer_min) { - zeit.append(el('span', 'tagesliste__dauer', `${t.dauer_min} min`)); + /* DER BALKEN ZEIGT DIE LÄNGE. Bezugsgröße sind zwei Stunden -- + darüber ist er voll. Das ist keine Skala, sondern ein + Verhältnis: Man soll auf einen Blick sehen, ob ein Termin + kurz oder lang ist, ohne "30 min" zu lesen. + + Zwei Stunden als Vollausschlag, weil darüber im Alltag + kaum etwas liegt; ein Ganztagestermin würde jeden anderen + Balken sonst auf einen Strich zusammendrücken. */ + const anteil = Math.max(12, Math.min(100, (Number(t.dauer_min) / 120) * 100)); + const balken = el('span', 'tagesliste__balken'); + balken.style.setProperty('--laenge', `${Math.round(anteil)}%`); + balken.setAttribute('aria-hidden', 'true'); // die Zahl daneben sagt es + const laenge = el('span', 'tagesliste__laenge'); + laenge.append(balken, el('span', 'tagesliste__dauer', `${t.dauer_min} min`)); + zeit.append(laenge); } li.append(zeit); - /* Der Punkt auf der Linie. Er trägt die Farbe der Terminart -- - dieselbe wie im Kalender, damit man sie nicht zweimal lernt. */ - const marke = el('span', 'tagesliste__marke'); - marke.setAttribute('aria-hidden', 'true'); - li.append(marke); - + /* ---- Mitte: Titel und Beteiligte ---- */ const text = el('div', 'tagesliste__text'); text.append(el('p', 'tagesliste__titel', t.titel)); @@ -620,8 +660,14 @@ if (unten.length) text.append(el('p', 'tagesliste__unter', unten.join(' · '))); li.append(text); - const art = el('span', 'tagesliste__art', ART_NAME[t.art] || t.art || ''); - li.append(art); + /* ---- Rechts: Art und, beim nächsten, die Jetzt-Marke ---- */ + const rechts = el('div', 'tagesliste__rechts'); + rechts.append(el('span', 'tagesliste__art', ART_NAME[t.art] || t.art || '')); + /* Steht im Markup immer, wird aber nur beim nächsten Termin + eingeblendet (CSS). Ein Wort sagt hier mehr als ein hellerer + Hintergrund -- und ein Vorleseprogramm bekommt es ebenfalls. */ + rechts.append(el('span', 'tagesliste__jetzt', 'Jetzt')); + li.append(rechts); /* Der ganze Eintrag führt in den Kalender, auf den richtigen Tag. Ein Termin, den man nur anschauen kann, ist eine Sackgasse. */ @@ -633,6 +679,19 @@ liste.append(li); } + + /* Was nicht mehr hineinpasste, wird gesagt -- mit dem Weg dorthin. + Eine Liste, die stillschweigend abschneidet, liest sich wie der + ganze Tag, und man verpasst den Termin um sechs. */ + if (weitere > 0) { + const mehr = el('li', 'tagesliste__mehr'); + const a = el('a', 'tagesliste__mehr-link', + `${weitere} weitere${weitere === 1 ? 'r Termin' : ' Termine'} heute →`); + a.href = 'kalender.html'; + mehr.append(a); + liste.append(mehr); + } + tagesblickPruefen(); } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index bdc44e3d..0a209387 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -9,9 +9,9 @@ - - - + + + @@ -211,10 +211,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 6350c69f..82f2a4d2 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -134,10 +134,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index e1de9456..619c3450 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -117,14 +117,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 0f4d7c21..f5c8a9ec 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -76,11 +76,11 @@ - - - - - - + + + + + + diff --git a/workspace/content.html b/workspace/content.html index e0d4879a..b6115a13 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -166,12 +166,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 2d40efeb..afaaa540 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -95,10 +95,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index 63c409b2..47ae15b8 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -27,7 +27,7 @@ - + @@ -145,6 +145,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index 82ef5520..ba106adc 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -206,10 +206,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 05001abc..253d3167 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -123,10 +123,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 9bc71a33..fccb419a 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -201,11 +201,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index 24645e7a..91dbf2da 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -93,11 +93,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 85bc0c22..8e2d6362 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -126,10 +126,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 163be923..82f43259 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -9,8 +9,8 @@ - - + + @@ -73,7 +73,23 @@
mit Überschrift je Hälfte, damit ein Vorleseprogramm die beiden Bereiche ansagen und anspringen kann -- ein Raster allein ist für ihn nur eine Reihenfolge. --> -