Ein Termin von-bis belegt jeden Tag dazwischen

Zwei Fehler, nicht einer.

Der sichtbare: Die Kachel stand nur am Anfangstag. Wer im
Monatsraster auf den Mittwoch sah, sah nichts -- obwohl die Aktion
von Montag bis Freitag lief.

Der unsichtbare, und der ist der schlimmere: Der SERVER suchte
Eintraege, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion vom
28.09. bis zum 05.10. kam im Oktober deshalb ueberhaupt nicht an --
nicht "nur am ersten Tag markiert", sondern gar nicht da. Wer im
Oktober plante, sah eine freie Woche, die belegt war. Jetzt
entscheidet die UEBERSCHNEIDUNG, nicht der Anfang.

Gebaut wurde es in nachTag() -- der einzigen Stelle, an der
Eintraege auf Tage verteilt werden. Monat, Woche, Liste und
Zeitstrahl holen sich alle dort; vier Ansichten einzeln
nachzuruesten waeren vier Stellen, an denen die fuenfte vergessen
wird.

Das Ende wird abgeleitet, nicht gepflegt: aus event_ende ODER aus
Uhrzeit plus Dauer. Ein Live von 22:00 ueber vier Stunden endet um
02:00 am naechsten Tag -- das stand bisher nirgends, obwohl die
Zahlen da waren.

Der Tagesdialog filterte selbst auf den Anfangstag. Im Raster war
der Mittwoch markiert, tippte man ihn an, stand da "An diesem Tag
steht nichts." Jetzt fragt er dieselbe Stelle wie das Raster.

pruef-zeitraum.mjs: 16 Pruefungen, beide Fehler einzeln, mit drei
Gegenproben (vor dem Anfang, nach dem Ende, Punkttermin).
pruef-terminregel.mjs weiterhin 35/0.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-20 19:20:18 +02:00
co-authored by Claude Opus 5
parent 947ea7ff49
commit d77dd216c5
5 changed files with 443 additions and 8 deletions
+218
View File
@@ -0,0 +1,218 @@
/* =====================================================================
pruef-zeitraum.mjs — Ein Termin von–bis belegt jeden Tag dazwischen
Filipe, 20.09.2026: „wenn ich termine eintrage mit von bis, will ich
dass die ganze zeit markiert ist und nicht nur der anfang."
---------------------------------------------------------------------
ZWEI FEHLER, NICHT EINER
Der sichtbare: Die Kachel stand nur am Anfangstag. Wer im
Monatsraster auf den Mittwoch sah, sah nichts — obwohl die Aktion
von Montag bis Freitag lief.
Der unsichtbare, und der ist der schlimmere: Der SERVER suchte
Einträge, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion vom
28.09. bis zum 05.10. kam im Oktober deshalb überhaupt nicht an —
nicht „nur am ersten Tag markiert", sondern gar nicht da. Wer im
Oktober plante, sah eine freie Woche, die belegt war.
Deshalb wird hier BEIDES gemessen, und der zweite Fall zuerst.
AUFRUF: node server/pruef-zeitraum.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { portMussFreiSein } from "./helfer-port.mjs";
import { notbremse } from "./helfer-notbremse.mjs";
const PORT = await portMussFreiSein(4437, "pruef-zeitraum");
const ordner = mkdtempSync(join(tmpdir(), "ws-zeitraum-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
await import("./index.js");
notbremse(230_000, "pruef-zeitraum");
await new Promise((r) => setTimeout(r, 900));
const BASIS = `http://127.0.0.1:${PORT}`;
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
console.log("\n=== Termine über mehrere Tage ===\n");
/* ---- Daten anlegen ------------------------------------------------
RELATIV ZU HEUTE, nie mit einem festen Datum: Ein fest eingetragener
Termin wird vom Kalender eingeholt, und dann schlägt die Prüfung
fehl, ohne dass etwas kaputt ist (Hausregel, 06.09.2026). */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-DOGI-0001", salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,1,?)").run("Filipe", "admin", hash, salt, 32768, new Date().toISOString());
const tagPlus = (n) => {
const x = new Date();
x.setHours(12, 0, 0, 0);
x.setDate(x.getDate() + n);
return x.toISOString().slice(0, 10);
};
/* Eine Aktion über fünf Tage, die GESTERN angefangen hat — genau der
Fall, den der Server vorher verschluckt hat. */
const LAUF_START = tagPlus(-1), LAUF_ENDE = tagPlus(3);
/* Und ein Punkttermin als Gegenprobe. */
const EINZEL = tagPlus(5);
const spalten = new Set(d.prepare("PRAGMA table_info(eintraege)").all().map((z) => z.name));
ok(spalten.has("event_ende") && spalten.has("geplant"),
"die Tabelle kennt Anfang und Ende (event_ende, geplant)");
const einf = (titel, von, bis) => d.prepare(
"INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit,"
+ " datum, geplant, event_ende, erstellt_von, erstellt)"
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?)")
.run("live", "idee", titel, "", "offen", "mittel", von, von, bis, 1,
new Date().toISOString());
einf("Wochenaktion quer", LAUF_START, LAUF_ENDE);
einf("Nur ein Tag", EINZEL, null);
d.close();
const an = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json", Origin: BASIS },
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
});
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
ok(an.ok && !!keks, "angemeldet");
/* ---- 1. DER SERVER: Überschneidung statt Anfang ------------------- */
const hole = async (von, tage) => {
const a = await fetch(`${BASIS}/workspace/api/termine?von=${von}&tage=${tage}`,
{ headers: { Cookie: keks } });
return a.ok ? a.json() : null;
};
const titel = (j) => (j?.brettsachen || []).map((b) => b.titel);
/* Ein Fenster, das MITTEN im Zeitraum liegt — der Anfang ist davor. */
const mitten = await hole(tagPlus(1), 2);
ok(titel(mitten).includes("Wochenaktion quer"),
"ein laufender Zeitraum kommt mit, auch wenn sein Anfang vor dem Fenster liegt");
console.log(` Fenster ${tagPlus(1)} + 2 Tage, Aktion läuft ${LAUF_START}–${LAUF_ENDE}`);
/* GEGENPROBE: Ein Fenster NACH dem Ende darf ihn nicht bringen. Ohne
diese Probe würde auch ein Server bestehen, der einfach alles
schickt — und dann stünde jeder je getippte Zettel im Kalender. */
const danach = await hole(tagPlus(10), 5);
ok(!titel(danach).includes("Wochenaktion quer"),
"Gegenprobe: nach dem Ende kommt er nicht mehr — es wird nicht einfach alles geschickt");
const davor = await hole(tagPlus(-30), 20);
ok(!titel(davor).includes("Wochenaktion quer"),
"Gegenprobe: und vor dem Anfang auch nicht");
/* Der Punkttermin muss sich genauso verhalten wie vorher. */
const beimEinzel = await hole(EINZEL, 1);
ok(titel(beimEinzel).includes("Nur ein Tag"),
"der Punkttermin kommt weiterhin an seinem Tag");
/* ---- 2. DER BILDSCHIRM: jeder Tag ist markiert -------------------- */
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch();
try {
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
const s = await ctx.newPage();
await s.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
await s.click('.rolle[data-rolle="admin"]');
await s.fill("#code", "CODE-DOGI-0001");
await s.click("button[type=submit]");
await s.waitForURL("**/start.html", { timeout: 25000 });
await s.goto(BASIS + "/workspace/kalender.html", { waitUntil: "networkidle" });
await s.waitForTimeout(1800);
/* Wie viele Tageszellen tragen die Aktion? */
const lage = await s.evaluate(() => {
const zellen = [...document.querySelectorAll(".k-tag")];
const treffer = [];
for (const z of zellen) {
for (const p of z.querySelectorAll(".k-pille")) {
if (/Wochenaktion quer/.test(p.textContent)) {
treffer.push({
zahl: z.querySelector(".k-tag__zahl")?.textContent || "?",
spanne: p.dataset.spanne || null,
text: p.textContent.replace(/\s+/g, " ").trim(),
breit: Math.round(p.getBoundingClientRect().width),
});
}
}
}
const einzel = [...document.querySelectorAll(".k-pille")]
.filter((p) => /Nur ein Tag/.test(p.textContent));
return { zellen: zellen.length, treffer,
einzel: einzel.length, einzelSpanne: einzel[0]?.dataset.spanne ?? null };
});
ok(lage.zellen > 0, `das Monatsraster steht (${lage.zellen} Tageszellen)`);
ok(lage.treffer.length === 5,
`die Aktion ist an allen fünf Tagen markiert (${lage.treffer.length})`);
console.log(" " + lage.treffer.map((t) => `${t.zahl}.[${t.spanne}]`).join(" "));
const pos = lage.treffer.map((t) => t.spanne);
ok(pos[0] === "start" && pos.at(-1) === "ende",
`Anfang und Ende sind als solche erkennbar (${pos[0]} … ${pos.at(-1)})`);
ok(pos.slice(1, -1).every((p) => p === "mitte"),
"dazwischen steht überall 'mitte' — die Form sagt, dass es weitergeht");
/* Auf dem ersten Tag steht die Uhrzeit-Spalte, danach die Lage. Eine
Uhrzeit am Mittwoch zu wiederholen behauptet, es ginge am Mittwoch
wieder los. */
ok(/Tag 2\/5/.test(lage.treffer[1]?.text || ""),
`der zweite Tag sagt, der wievielte er ist ("${lage.treffer[1]?.text.slice(0, 34)}")`);
ok(!/Tag \d+\/\d+/.test(lage.treffer[0]?.text || ""),
"am ersten Tag steht das NICHT — dort gehört die Startzeit hin");
/* GEGENPROBE AM BILDSCHIRM: Der Punkttermin darf sich nicht
angesteckt haben. Ohne sie bestünde auch eine Fassung, die jede
Kachel über den halben Monat zieht. */
ok(lage.einzel === 1,
`Gegenprobe: der Punkttermin steht an genau einem Tag (${lage.einzel})`);
ok(lage.einzelSpanne === null,
"Gegenprobe: und trägt keine Spannen-Kennzeichnung");
/* ---- 3. Der Tagesdialog widerspricht dem Raster nicht ----------- */
/* Vorher filterte er selbst auf den Anfangstag: Im Raster war der
Mittwoch markiert, tippte man ihn an, stand da „An diesem Tag
steht nichts." */
const mitteTag = lage.treffer[2]?.zahl;
await s.evaluate((zahl) => {
for (const z of document.querySelectorAll(".k-tag")) {
if (z.querySelector(".k-tag__zahl")?.textContent === zahl) { z.click(); return; }
}
}, mitteTag);
await s.waitForTimeout(700);
const dialog = await s.evaluate(() => {
const dl = document.querySelector("#tagdialog[open]");
return dl ? dl.textContent.replace(/\s+/g, " ").trim() : null;
});
ok(!!dialog && /Wochenaktion quer/.test(dialog),
`der Tagesdialog am ${mitteTag}. zeigt die Aktion auch`);
ok(!/An diesem Tag steht nichts/.test(dialog || ""),
" und sagt nicht mehr 'An diesem Tag steht nichts'");
await ctx.close();
} finally {
await browser.close();
}
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);
+10 -3
View File
@@ -36,7 +36,7 @@ import { sichtbarEintrag, BEREICHE } from "./workspace-bereiche.js";
laufen auseinander, und dann zeigt die Karte etwas anderes als der
Kalender. */
import {
TERMIN_TAG_SQL, TERMIN_BEDINGUNG_SQL, TERMIN_GRUND_SQL, OHNE_ZUSTAENDE,
TERMIN_TAG_SQL, TERMIN_ENDE_SQL, TERMIN_BEDINGUNG_SQL, TERMIN_GRUND_SQL, OHNE_ZUSTAENDE,
} from "./workspace-termin-regel.js";
export const kalenderRouter = express.Router();
@@ -463,10 +463,17 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => {
WHERE ${eintragRegel.wo}
AND e.status NOT IN (${OHNE_ZUSTAENDE.map(() => "?").join(", ")})
AND ${TERMIN_BEDINGUNG_SQL}
AND ${TERMIN_TAG_SQL} >= ?
/* UEBERSCHNEIDUNG, NICHT ANFANG (20.09.2026).
Vorher stand hier zweimal TERMIN_TAG_SQL -- also: „faengt
im sichtbaren Fenster an". Eine Aktion vom 28.09. bis zum
05.10. fiel damit im Oktober komplett heraus. Richtig ist
die Frage, ob sich Zeitraum und Fenster BERUEHREN:
faengt vor dem Fensterende an UND hoert nach dem
Fensteranfang auf. */
AND ${TERMIN_TAG_SQL} <= ?
AND ${TERMIN_ENDE_SQL} >= ?
ORDER BY wann, e.uhrzeit, e.id`)
.all(...eintragRegel.werte, ...OHNE_ZUSTAENDE, heuteLokal(), von, bis) : [];
.all(...eintragRegel.werte, ...OHNE_ZUSTAENDE, heuteLokal(), bis, von) : [];
/* DER NAME DES BRETTS KOMMT MIT -- und zwar aus dem Katalog, nicht
als abgeschriebene Liste im Browser. Ein Brett, das morgen
+20
View File
@@ -52,6 +52,26 @@
* den 24., auch wenn der Zettel am 3. entstanden ist. */
export const TERMIN_TAG_SQL = "COALESCE(e.geplant, e.datum)";
/** Der LETZTE Tag, an dem ein Eintrag noch laeuft -- als SQL-Ausdruck.
*
* Gebraucht seit dem 20.09.2026. Filipe: „wenn ich termine eintrage
* mit von bis, will ich dass die ganze zeit markiert ist und nicht
* nur der anfang."
*
* DER FEHLER SASS TIEFER ALS IN DER ANZEIGE. Der Kalender suchte
* Eintraege, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion
* vom 28.09. bis zum 05.10. kam damit im Oktober ueberhaupt nicht an
* -- nicht „nur am ersten Tag markiert", sondern gar nicht da. Wer im
* Oktober plante, sah eine freie Woche, die belegt war.
*
* `MAX` und nicht `COALESCE(e.event_ende, ...)`: Ein Ende, das VOR
* dem Anfang liegt (vertippt, Anfang spaeter verschoben), wuerde
* sonst den Eintrag aus seinem eigenen Zeitraum werfen. Der leere
* Text sortiert vor jedem Datum -- ohne Ende gewinnt damit immer der
* Anfang, und das ist genau richtig. */
export const TERMIN_ENDE_SQL =
"MAX(COALESCE(e.event_ende, ''), COALESCE(e.geplant, e.datum))";
/** Die Bedingung als SQL. Erwartet GENAU EINEN Platzhalter: den
* heutigen Tag in Ortszeit (`heuteLokal()`).
*
+69
View File
@@ -355,6 +355,75 @@
Kontrastmodus nicht zu erkennen. */
.k-pille.erledigt { opacity: .5; text-decoration: line-through; }
/* =====================================================================
EIN TERMIN, DER MEHRERE TAGE LAEUFT (20.09.2026)
Filipe: „wenn ich termine eintrage mit von bis, will ich dass die
ganze zeit markiert ist und nicht nur der anfang."
Die Kachel steht jetzt in jeder Tageszelle dazwischen. Damit man
sieht, dass es EIN Termin ist und nicht vier gleichnamige, wird sie
an den Seiten angeschnitten, an denen es weitergeht:
Mo [ 20:00 Aktion ━
Di ━ Tag 2/4 ━
Mi ━ Tag 3/4 ━
Do ━ Tag 4/4 ]
DIE FORM MACHT DIE AUSSAGE, nicht die Farbe -- an einer eckigen
Kante sieht man „geht weiter" auch dann, wenn man Farben schlecht
unterscheidet oder der Kontrastmodus alles einfaerbt.
WARUM KEIN ECHTER BALKEN UEBER DIE ZELLEN: Ein Element, das sich
ueber mehrere Rasterzellen legt, muesste wissen, wo die Woche
umbricht, wie hoch die Zeile gerade ist und was darunter noch steht.
Das ist bei jeder Fensterbreite eine andere Rechnung -- und eine
Rechnung, die irgendwann danebenliegt, schiebt Kacheln uebereinander.
Die angeschnittene Kante kostet nichts und kann nicht verrutschen. */
/* Laeuft gestern schon: links glatt abgeschnitten, kein Randstrich. */
.k-pille[data-spanne="mitte"],
.k-pille[data-spanne="ende"] {
border-left: 0;
border-top-left-radius: 0; border-bottom-left-radius: 0;
/* Der Strich am linken Rand wird zum FORTSETZUNGSZEICHEN: ein
schmaler, waagerechter Balken in der Farbe, auf halber Hoehe. */
padding-left: 12px;
background-image: linear-gradient(90deg,
var(--pfarbe) 0 4px, transparent 4px);
background-repeat: no-repeat;
background-position: 0 50%;
background-size: 100% 2px;
}
/* Geht morgen weiter: rechts glatt abgeschnitten. */
.k-pille[data-spanne="start"],
.k-pille[data-spanne="mitte"] {
border-top-right-radius: 0; border-bottom-right-radius: 0;
/* Eine Kante, die zeigt, dass hier nichts endet. */
box-shadow: inset -3px 0 0 -1px color-mix(in srgb, var(--pfarbe) 70%, transparent);
}
/* Der ganze Zeitraum liegt etwas satter als ein Punkttermin -- er
BELEGT die Tage, und das soll man beim Ueberfliegen sehen. */
.k-pille[data-spanne] {
background-color: color-mix(in srgb, var(--pfarbe) 22%, transparent);
}
.k-pille[data-spanne]:hover {
background-color: color-mix(in srgb, var(--pfarbe) 32%, transparent);
}
/* „Tag 2/4" ist eine Angabe zur Lage, keine Uhrzeit -- deshalb ruhiger
als die fette Startzeit daneben. */
.k-pille__lauf { font-weight: 550; opacity: .82; }
@media (forced-colors: active) {
/* Im Kontrastmodus verschwinden Farbflaechen und Schatten. Dann muss
die Kante als echter Rahmen dastehen, sonst sieht die Kachel am
Mittwoch genauso aus wie ein eigener Termin. */
.k-pille[data-spanne="start"],
.k-pille[data-spanne="mitte"] { border-right: 2px dashed CanvasText; }
.k-pille[data-spanne="mitte"],
.k-pille[data-spanne="ende"] { border-left: 2px dashed CanvasText; }
}
/* ---------- Wochenansicht ----------------------------------------------- */
.k-woche {
display: grid; gap: 1px;
+126 -5
View File
@@ -601,11 +601,93 @@
statt verschluckt. */
const sichtbare = () => (daten ? daten.eintraege.filter((e) => zeigen[e.art] !== false) : []);
/* =====================================================================
EIN TERMIN VON--BIS STEHT AN JEDEM TAG DAZWISCHEN (20.09.2026)
Filipe: „wenn ich termine eintrage mit von bis, will ich dass die
ganze zeit markiert ist und nicht nur der anfang."
BISHER stand er nur am Anfangstag. Wer im Monatsraster auf den
Mittwoch sah, sah nichts -- obwohl die Aktion von Montag bis
Freitag lief. Genau in der Ansicht, in der man plant, war der
belegte Zeitraum unsichtbar, und man legte etwas mitten hinein.
WARUM GENAU HIER: `nachTag()` ist die EINZIGE Stelle, an der
Eintraege auf Tage verteilt werden -- Monat, Woche, Liste und
Zeitstrahl holen sich alle `karte.get(tag)`. Vier Ansichten
einzeln nachzuruesten waere vier Stellen, an denen die naechste
Ansicht vergessen wird.
DAS ENDE WIRD ABGELEITET, NICHT GEPFLEGT:
* `bis` -- Brettkarten mit event_ende
* Uhr + Dauer -- ein Live von 22:00 ueber 240 Minuten endet um
02:00 am naechsten Tag. Das stand bisher
NIRGENDS, obwohl die Zahlen da waren.
Eine Liste „diese Arten haben ein Ende" waere die Liste, die beim
naechsten Termintyp veraltet.
DIE KOPIEN AENDERN `tag` NICHT. Wer auf eine Kachel am Mittwoch
tippt, will den Termin bearbeiten -- und der faengt am Montag an.
Wuerde die Kopie `tag` auf den Mittwoch setzen, verschoebe ein
Speichern den Termin auf den Tag, an dem man zufaellig
hingetippt hat. Der Anzeigetag steht deshalb getrennt daneben. */
/** Der letzte Tag, an dem dieser Eintrag noch laeuft. */
function endTagVon(e) {
if (e.bis) return String(e.bis).slice(0, 10);
if (e.uhr && Number(e.dauer_min) > 0) {
const [st, mi] = e.uhr.split(':').map(Number);
const start = ausText(e.tag);
if (Number.isNaN(st) || Number.isNaN(start.getTime())) return e.tag;
/* AB MITTERNACHT RECHNEN, NICHT AB `ausText`. `ausText` legt den
Tag bewusst auf 12:00 UTC (damit Zeitzonen ihn nicht um einen
Tag verschieben) -- wer darauf die Uhrzeit ADDIERT, rechnet
zwoelf Stunden zu viel. Erster Anlauf ergab damit fuer
„13:00, 60 Minuten" den FOLGETAG. */
const mitternacht = Date.UTC(
start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate());
/* Minus eine Millisekunde: Ein Stream von 22:00 ueber genau zwei
Stunden endet um 00:00 -- er laeuft am naechsten Tag NICHT
mehr, auch wenn die Zahl dort steht. */
const endeMs = mitternacht
+ ((st * 60 + mi + Number(e.dauer_min)) * 60000) - 1;
return alsText(new Date(Math.max(endeMs, mitternacht)));
}
return e.tag;
}
/* Eine Obergrenze, die nicht schoen ist, aber ehrlich: Ein Eintrag
mit kaputtem Enddatum (Jahr 9999) wuerde sonst Millionen Kopien
bauen und den Browser anhalten. 400 Tage sind mehr als jede
Ansicht je zeigt -- die Grenze ist also nie im Weg und faengt nur
den Unfall ab. */
const SPANNE_MAX = 400;
function nachTag() {
const karte = new Map();
const lege = (tag, e) => {
if (!karte.has(tag)) karte.set(tag, []);
karte.get(tag).push(e);
};
for (const e of sichtbare()) {
if (!karte.has(e.tag)) karte.set(e.tag, []);
karte.get(e.tag).push(e);
const ende = endTagVon(e);
if (!ende || ende <= e.tag) { lege(e.tag, e); continue; }
const tage = Math.min(SPANNE_MAX,
Math.round((ausText(ende) - ausText(e.tag)) / 86400000) + 1);
let d = ausText(e.tag);
for (let i = 0; i < tage; i++) {
lege(alsText(d), {
...e,
/* Der Tag, an dem die Kachel steht -- NICHT `tag`. */
spann_tag: alsText(d),
spann_nr: i + 1,
spann_tage: tage,
spann_bis: ende,
spann_pos: i === 0 ? 'start' : (i === tage - 1 ? 'ende' : 'mitte'),
});
d = plus(d, 1);
}
}
return karte;
}
@@ -629,8 +711,38 @@
if (e.ziel) k.href = e.ziel;
k.style.setProperty('--pfarbe', e.farbe);
k.title = `${e.titel}${e.zusatz ? ' — ' + e.zusatz : ''}`
+ (e.serie_id ? ' — wiederholt sich' : '');
if (e.uhr) k.append(el('span', 'k-pille__zeit', e.uhr));
+ (e.serie_id ? ' — wiederholt sich' : '')
/* Der ganze Zeitraum, nicht nur „Tag 2/4": Wer am Mittwoch
draufzeigt, will wissen, WANN es anfing und aufhoert -- sonst
muss er selbst zurueckblaettern und zaehlen. */
+ (e.spann_tage > 1
? ` — läuft vom ${tagLang(e.tag)} bis ${tagLang(e.spann_bis)}`
+ ` (Tag ${e.spann_nr} von ${e.spann_tage})`
: '');
/* ---- LAEUFT UEBER MEHRERE TAGE (20.09.2026) --------------------
Drei Dinge unterscheiden die Kachel eines Zeitraums von der eines
Punkttermins, und alle drei sind noetig:
1. `data-spanne` -- daraus macht das Stilblatt einen
durchgehenden Balken: links angeschnitten, wo er gestern
schon lief, rechts angeschnitten, wo er morgen weiterlaeuft.
Man sieht an der FORM, dass es weitergeht, ohne ein Wort.
2. „Tag 2/4" statt der Uhrzeit auf den Folgetagen. Die Uhrzeit
gehoert zum Anfang; sie am Mittwoch zu wiederholen behauptet,
es ginge am Mittwoch um 20:00 wieder los.
3. Der ganze Zeitraum im Titel -- fuer Maus und Vorleseprogramm.
AUF DEM ANFANGSTAG BLEIBT ALLES WIE VORHER. Wer den Termin
eintraegt, sieht dort die Uhrzeit, die er eingetragen hat. */
if (e.spann_tage > 1) {
k.dataset.spanne = e.spann_pos;
k.style.setProperty('--spann-tage', String(e.spann_tage));
}
if (e.spann_tage > 1 && e.spann_pos !== 'start') {
k.append(el('span', 'k-pille__zeit k-pille__lauf',
`Tag ${e.spann_nr}/${e.spann_tage}`));
} else if (e.uhr) k.append(el('span', 'k-pille__zeit', e.uhr));
/* Das Zeichen steht im Raster VOR dem Titel, nicht dahinter: Eine
Zelle im Monatsraster ist gut 150 px breit, der Titel wird dort
abgeschnitten -- ein Zeichen am Ende wäre genau in der Ansicht
@@ -1355,7 +1467,16 @@
const liste = $('tagdialog-liste');
liste.replaceChildren();
const eintraege = sichtbare().filter((e) => e.tag === tag);
/* UEBER `nachTag()`, NICHT UEBER `e.tag` (20.09.2026).
Vorher stand hier ein eigener Filter auf den Anfangstag. Folge:
Im Monatsraster war die Aktion am Mittwoch markiert -- tippte man
die Zelle an, sagte der Tagesdialog „An diesem Tag steht
nichts." Zwei Antworten auf dieselbe Frage, und die zweite
widerspricht dem, was man gerade gesehen hat.
`nachTag()` ist die eine Stelle, die das entscheidet. */
const eintraege = nachTag().get(tag) || [];
if (!eintraege.length) {
liste.append(el('p', 'leise', 'An diesem Tag steht nichts.'));
} else {