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 @@
-
-
-
-
-
+
+
+
+
+