Kalender: was auf den Brettern steht und einen Termin hat

Filipe, zu einer Clip-Karte: "ich will dass die auch in einer kalender
drin sind, aber ich will dass es richtig geil ist und perfektionier das
bitte."

Gemessen vor dem Bau: /workspace/api/termine lieferte `termine` und
`fristen`. Die Zettel von den achtzehn Brettern kamen nirgends vor --
obwohl `eintraege` seit langem VIER Zeitfelder hat: datum, uhrzeit,
geplant, event_ende.

WELCHE HINEINGEHOEREN, IST DIE GANZE FRAGE -- und sie laesst sich
messen statt raten. `datum` ist ein Pflichtfeld und faellt auf HEUTE
zurueck, wenn niemand eins waehlt. Alle Eintraege hineinzukippen hiesse
also: jeder je getippte Zettel steht an dem Tag, an dem ihn jemand
getippt hat. Nach einem Monat waere der Kalender ein Protokoll und kein
Plan -- und ein Kalender, in dem alles steht, sagt nichts mehr.

GEZEIGT WIRD NUR, WOFUER JEMAND EINE ZEIT BESTIMMT HAT:
  geplant        eine Zusage ("Wird gemacht -- am 24.09.")
  event_ende     ein Zeitraum
  uhrzeit        niemand tippt aus Versehen 20:00
  datum > heute  der Rueckfall ist IMMER heute oder frueher
Erledigtes bleibt draussen. Der vierte Fall ist der wichtigste und der
unauffaelligste: kein neues Feld, keine Umgewoehnung.

RECHTE: dieselbe Funktion wie die Bretter selbst (`sichtbarEintrag`),
nicht eine zweite Meinung. Im Kalender steht nur eine kleine Pille mit
einem Titel -- dass darin ein vertrauliches Vorhaben steckt, faellt
niemandem auf, der nicht danach sucht. Genau so kam am 03.09. eine
Managerin an fremde Aufgabenfristen.

DREI FEHLER AM BILDSCHIRMFOTO GEFUNDEN, nicht an einer Zahl:
  1. Der Brettname stand VOR dem Titel -- aus "Clip am Wochenende"
     wurde "Cli...". Das Etikett verdraengte, was es einordnen sollte.
     Jetzt zweite Zeile: Hoehe ist im Monatsraster der billigere Platz.
  2. Am Handy (45px Zellbreite) wurde daraus ein "Co..." -- Hoehe ohne
     Information. Jetzt eine CONTAINER-Abfrage: entschieden wird nach
     der Breite der ZELLE, nicht des Fensters. Eine Fensterschwelle
     waere wieder die Rechnung von gestern (September, Kopfleiste,
     zweimal).
  3. .64rem = 10,24px -- unter der Hausgrenze 11,5px, gefunden von
     pruef-css-klassen. Gedaempft wird jetzt ueber Farbe und Gewicht,
     nicht ueber Groesse.

EIN ECHTER FEHLER VERHINDERT: In der Listenansicht waeren die Zettel im
Termin-Zweig gelandet -- mit Wecker, "erledigt" und "loeschen". Der
Knopf haette PATCH /api/termine/b12 geschickt: eine Nummer, die es dort
nicht gibt, an eine Schnittstelle, die davon nichts weiss. Still, ohne
Wirkung, ohne Meldung.

DER WEG FUEHRT AUF DEN ZETTEL, nicht nur auf das Brett -- ueber
`?zeigen=` (kopf.js), den Weg, den das Haus dafuer schon hat. Zwischen
zwanzig Eintraegen waere der gesuchte sonst von Hand zu suchen.

pruef-kalender: +25 Pruefungen. Sieben Gegenproben, jede zielgenau:
  Schnitt weg            -> 6 rot   Erledigtes rein      -> 6 rot
  Rechteregel ausgehebelt-> 1 rot   COALESCE weg         -> 1 rot
  Termin-Knoepfe an      -> 2 rot   Container-Regel weg  -> 1 rot
  Sprung-Zweig weg       -> 3 rot

UND SIEBEN FESTE ZAHLEN ABGELEITET. Die alten Pruefungen zaehlten
"alle fünf Einträge", "vier Arten", "vier Filter" -- mit den neuen
Testdaten wurden daraus zehn, und sieben Pruefungen je Bildschirmgroesse
wurden rot, ohne dass am Kalender etwas kaputt war. Mit `git stash`
nachgemessen (ohne meine Aenderung: 0 Fehler), also nicht angepasst,
sondern gerechnet: jede Zahl kommt jetzt aus den Testdaten darueber.

Zwei eigene Pruefungsfehler dabei behoben: ein falscher Umschalter-
Selektor liess `every()` auf einem leeren Feld laufen (gruen ohne
Daten), und die Sichtbarkeit der zweiten Zeile wurde ueber
`textContent` gemessen -- das liefert auch, was `display: none`
ausblendet.

Gruen: kalender, sprung, namen, css-klassen, struktur.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 22:48:38 +02:00
co-authored by Claude Opus 5
parent e3e015bca4
commit fabb1f94a8
38 changed files with 997 additions and 415 deletions
+83
View File
@@ -1546,3 +1546,86 @@
.k-schritt { border: 1px solid ButtonText; background: ButtonFace; color: ButtonText; }
.k-ansicht { border: 1px solid ButtonText; }
}
/* =====================================================================
WAS VOM BRETT KOMMT (17.09.2026)
Filipe: „ich will dass die auch in einer kalender drin sind, aber ich
will dass es richtig geil ist."
ZWEI ZEILEN, NICHT ZWEI SPALTEN. Der erste Anlauf stellte den
Brettnamen neben den Titel -- und das Bildschirmfoto zeigte sofort,
dass daraus „CONTENT-IDEEN Cli…" wird. Das Etikett verdraengte genau
die Information, die es einordnen sollte.
Jetzt steht der Titel in seiner Zeile und das Brett darunter, klein
und gedaempft. Es beantwortet „was ist das?", ohne dem Titel Platz
wegzunehmen.
===================================================================== */
.k-pille__brett {
display: block;
margin-top: 1px;
/* 11,52 px -- gerade eben ueber der Hausgrenze von 11,5 px.
Der erste Anlauf stand auf .64rem (10,24 px) und wurde von
pruef-css-klassen gefunden: eine Stelle mehr als die Grundlinie.
GEDAEMPFT WIRD UEBER FARBE UND GEWICHT, NICHT UEBER GROESSE. Eine
Nebenzeile darf leiser sein als der Titel -- sie darf nicht
schlechter lesbar sein. Das ist der Unterschied zwischen
„zurueckhaltend" und „zusammengekniffen", und die Hausregel
„augenschonend" meint genau den. */
font-size: .72rem; font-weight: 600;
letter-spacing: .01em;
/* Gedaempft, aber nicht per `opacity`: Das macht die ganze Zeile
durchsichtig, und der Hintergrund des Tages scheint hindurch. Eine
leisere FARBE tut dasselbe, ohne die Flaeche anzufassen. */
color: color-mix(in srgb, var(--pfarbe, #7f9cc0) 55%, var(--text-still));
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* DER HAKEN VOR DEM TITEL -- fuer das, was jemandem zugesagt wurde.
Er ist ein Zeichen UND eine Farbe, nicht nur eine Farbe: Eine
Rueckmeldung, die allein aus einem Farbwechsel besteht, kommt bei
niemandem an, der Farben schlecht unterscheidet. */
.k-pille__zusage {
margin-inline-end: 4px;
font-weight: 800;
color: color-mix(in srgb, var(--pfarbe, #7f9cc0) 30%, #ffffff);
}
/* In der Listenansicht steht das Brett ohnehin neben dem Titel
(k-zeile__art). Dort waere die zweite Zeile eine Dopplung. */
.k-zeile .k-pille__brett { display: none; }
/* DIE ZWEITE ZEILE NUR, WO SIE LESBAR IST (17.09.2026).
Am Handy ist eine Monatszelle rund 45 px breit. Dort wurde aus
„Content-Ideen" ein „Co…" -- eine Zeile, die Hoehe kostet und nichts
sagt. Am Bildschirmfoto in 390 px sofort zu sehen, an keiner Zahl.
GEMESSEN STATT GESCHALTET: Entschieden wird nach der Breite der
ZELLE, nicht nach der des Fensters. Eine Fensterschwelle waere
wieder die Rechnung von gestern -- sie muesste bei jeder Aenderung
am Raster nachgezogen werden (und genau das ist im September an der
Kopfleiste zweimal schiefgegangen). Eine Zellenabfrage stimmt auch
dann noch, wenn das Raster morgen eine Spalte mehr oder weniger hat.
110 px ist die Breite, ab der ein Brettname mehr als drei Buchstaben
zeigt. Darunter steht nur der Titel -- und das Brett weiterhin im
Tooltip, in der Listenansicht und im Tagesdialog. */
.k-tag { container-type: inline-size; }
.k-pille__brett { display: none; }
@container (min-width: 110px) {
.k-pille__brett { display: block; }
}
/* Ausserhalb einer Zelle -- Wochenspalte, Zeitstrahl -- gibt es keinen
Container, und `@container` greift nicht. Dort ist Platz, also steht
der Name da. `:not()` ueber den Vorfahren geht in CSS nicht; deshalb
die umgekehrte Richtung: erst ueberall aus (oben), dann dort wieder
an, wo es passt. */
.k-wochentag .k-pille__brett,
.k-strahl .k-pille__brett { display: block; }
.k-zeile .k-pille__brett { display: none; }
+16
View File
@@ -900,6 +900,22 @@
} else if (was === 'offen') {
window.zielMarkieren?.([...karten.values()], 'Einträge, die noch offen sind',
async () => { filter = ''; await laden(); });
} else if (was.startsWith('eintrag-')) {
/* EIN EINZELNER ZETTEL (17.09.2026).
Der Kalender zeigt seit heute, was auf den Brettern steht und
einen Termin hat. Ein Klick darauf fuehrte bisher auf das
BRETT -- und dort steht der gesuchte Zettel irgendwo zwischen
zwanzig anderen. Wer ihn dann von Hand sucht, hat vom Link
nichts gehabt.
BENUTZT WIRD DER VORHANDENE WEG (`?zeigen=`, kopf.js) und
keine zweite Mechanik: Sonst springt die eine Seite hart, die
andere weich, und niemand traut dem Weg mehr -- das steht
woertlich in der Begruendung dieses Helfers. */
const k = karten.get(Number(was.slice(8)));
window.zielMarkieren?.(k ? [k] : [], 'diesen einen Eintrag',
async () => { filter = ''; await laden(); });
}
}
+129 -4
View File
@@ -60,6 +60,19 @@
call: 'Call', termin: 'Termin', review: 'Review', frist: 'Frist',
bigmatch: 'BigMatch', turnier: 'Turnier', special: 'Special-Live',
collab: 'Collab', raid: 'Raid-Train', charity: 'Charity',
/* WAS AUF DEN BRETTERN STEHT UND EINEN TERMIN HAT (17.09.2026).
Eine EINZIGE Art, nicht eine je Brett. Der Filter haette sonst
achtzehn Schalter, und eine Leiste mit achtzehn Schaltern liest
niemand mehr -- derselbe Grund, aus dem die sechs Auftritts-Arten
nur bei Bedarf erscheinen.
UNTERSCHIEDEN WIRD UEBER DAS WORT, NICHT UEBER DIE FARBE: In der
Pille steht der Name des Bretts. Eine Farbe je Brett waere eine
von Hand gepflegte Liste -- und die waere beim naechsten neuen
Brett vergessen, genau wie die Filterliste, die hier schon
einmal stehengeblieben ist (siehe unten bei `zeigen`). */
brett: 'Vom Brett',
};
/* Jede Art trägt ihre eigene Farbe -- dieselbe Idee wie bei den Kacheln
auf der Startseite: Die Farbe gehört zur Sache, nicht zum Rang. */
@@ -92,7 +105,25 @@
collab: '#2fd6c4',
raid: '#ff9040',
charity: '#a7d64a',
/* BEWUSST RUHIGER ALS DIE AUFTRITTE. Ein Zettel vom Brett ist
keine Sendung -- er soll im Monatsraster sichtbar sein, aber
einem BigMatch nicht die Aufmerksamkeit wegnehmen. Stahlblau ist
ausserdem der letzte Ton, der sich von allen neun anderen klar
unterscheidet: nicht das helle Blau der Calls, nicht das Violett
der Termine, nicht das Tuerkis der Collabs. */
brett: '#7f9cc0',
};
/* Warum ein Zettel vom Brett im Kalender steht -- in Worten, die man
ohne Erklaerung versteht. Die Schluessel kommen vom Server; steht
ein unbekannter dabei, bleibt die Zeile einfach ohne dieses Stueck
statt einen Rohwert zu zeigen. */
const GRUNDWORT = {
zugesagt: 'zugesagt',
zeitraum: 'laeuft ueber mehrere Tage',
uhrzeit: null,
termin: 'geplant',
};
const FEIERTAG_FARBE = '#ff9aa2';
const ANLASS_FARBE = '#d8a13a';
@@ -335,6 +366,10 @@
const fmt = (o) => new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', ...o });
const tagLang = (t) =>
fmt({ weekday: 'long', day: '2-digit', month: 'long' }).format(ausText(t));
/* Kurzfassung fuer eine Pille: „24.09." -- ohne Jahr und ohne
Wochentag. In einer Zelle von 150 px ist jedes weitere Zeichen
eines, das den Titel abschneidet. */
const kurz = (t) => fmt({ day: '2-digit', month: '2-digit' }).format(ausText(t));
/* ---------- Anlässe ------------------------------------------------------ */
@@ -515,6 +550,46 @@
ziel: 'aufgaben.html',
});
}
/* ---- Was auf den Brettern steht (17.09.2026) --------------------
Filipe, zu einer Clip-Karte: „ich will dass die auch in einer
kalender drin sind."
WELCHE hier ankommen, entscheidet der Server (workspace-
kalender.js) -- und zwar nach der Regel „nur, wofuer jemand eine
Zeit bestimmt hat". Der Browser rechnet das NICHT nach: Eine
zweite Fassung derselben Regel ist die, die beim naechsten
Nachschaerfen auseinanderlaeuft. Er zeigt, was kommt. */
for (const b of roh.brettsachen || []) {
raus.push({
art: 'brett', tag: b.wann, uhr: b.uhrzeit || null,
titel: b.titel, erledigt: false, id: 'b' + b.id,
farbe: ARTFARBE.brett,
brett_name: b.brett_name,
/* DER GRUND STEHT DABEI. „Zugesagt am 24." ist etwas anderes
als „liegt auf dem 24." -- das erste ist ein Versprechen an
jemanden, das zweite eine Notiz an sich selbst. Wer das nicht
unterscheiden kann, behandelt beides gleich, und dann zaehlt
die Zusage so wenig wie die Notiz. */
grund: b.grund,
bis: b.event_ende || null,
zusatz: [b.brett_name, GRUNDWORT[b.grund] || null,
b.event_ende && b.event_ende !== b.wann
? 'bis ' + kurz(b.event_ende) : null,
b.von_name ? 'von ' + b.von_name : null].filter(Boolean).join(' · '),
beschreibung: b.text || null,
/* Der Weg zurueck zum Brett. Ein Kalendereintrag, den man nur
ansehen und nicht oeffnen kann, ist eine Sackgasse -- und der
ganze Vorgang (kommentieren, erledigen, verschieben) steht
dort, nicht hier. */
/* Auf den ZETTEL, nicht nur auf das Brett. Zwischen zwanzig
anderen ist der gesuchte sonst wieder von Hand zu suchen --
und dann hat der Link nichts gebracht. `?zeigen=` ist der
Weg, den das Haus dafuer schon hat (kopf.js). */
ziel: 'bereich.html?b=' + encodeURIComponent(b.bereich)
+ '&zeigen=eintrag-' + b.id,
});
}
/* Erst nach Tag, dann nach Uhrzeit. Fristen haben keine Uhrzeit und
stehen deshalb ans Ende des Tages -- sie blockieren keinen Slot. */
return raus.sort((a, b) =>
@@ -563,7 +638,41 @@
(siehe zeile()) ist Platz, dort steht es hinter dem Titel und
liest sich natürlicher. */
if (e.serie_id) k.append(wiederholZeichen());
/* WOHER ER KOMMT, STEHT VOR DEM TITEL (17.09.2026).
„Wer ist euer Komet?!" allein sagt im Kalender nichts -- man
weiss nicht, ob das ein Stream, eine Aufgabe oder ein Zettel
ist. „Content · Wer ist euer Komet?!" beantwortet das in einem
Wort, und zwar VOR dem Titel: Eine Zelle im Monatsraster ist gut
150 px breit und schneidet hinten ab. Dieselbe Ueberlegung wie
beim Wiederhol-Zeichen eine Zeile darueber.
UND ES IST EINE ZUSAGE ERKENNBAR. Was jemandem versprochen
wurde, traegt ein eigenes Zeichen -- ein Versprechen, das
aussieht wie eine Notiz, wird auch wie eine behandelt. */
/* EINE ZUSAGE TRAEGT EIN HAKEN, und der steht VOR dem Titel.
„Wird gemacht -- am 24.09." ist ein Versprechen an jemanden;
ein selbstgesetztes Datum ist eine Notiz an sich selbst. Wer das
nicht unterscheiden kann, behandelt beides gleich -- und dann
zaehlt die Zusage so wenig wie die Notiz.
Zwei Zeichen statt eines Wortes: Im Monatsraster ist jede
Buchstabenbreite eine, die dem Titel fehlt. */
if (e.grund === 'zugesagt') k.append(el('span', 'k-pille__zusage', '\u2713'));
k.append(document.createTextNode(e.titel));
/* WOHER ER KOMMT, STEHT DARUNTER -- nicht davor (17.09.2026).
Der erste Anlauf setzte den Brettnamen VOR den Titel. Am
Bildschirmfoto war sofort zu sehen, warum das nicht geht: In
einer Monatszelle von rund 150 px blieb von „Clip am Wochenende"
genau „Cli…" uebrig. Das Etikett hat die Information verdraengt,
die es einordnen sollte.
In der zweiten Zeile kostet es KEINE Breite. Hoehe ist im
Monatsraster der billigere Platz: Eine Zelle ist gut 110 px hoch
und traegt meist ein bis zwei Eintraege. */
if (e.brett_name) k.append(el('span', 'k-pille__brett', e.brett_name));
return k;
}
@@ -1023,13 +1132,29 @@
ANGABE ueber den Termin ("das ist ein Termin, kein Call"), genau
wie Uhrzeit und Titel. Jetzt steht sie neben dem Titel, und die
Knopfreihe enthaelt nur noch Dinge, die etwas tun. */
titel.append(el('span', 'k-zeile__art', ARTNAME[e.art] || e.art));
/* Bei einem Zettel vom Brett steht dort das BRETT und nicht „Vom
Brett" -- in der Liste ist Platz fuer das Wort, das wirklich
etwas sagt. In der Filterleiste bleibt es eine Art; dort waeren
achtzehn Schalter unbrauchbar. */
titel.append(el('span', 'k-zeile__art',
e.brett_name || ARTNAME[e.art] || e.art));
const rechts = el('div', 'k-zeile__knoepfe');
if (e.art === 'frist') {
const a = el('a', 'schritt', 'zur Aufgabe');
a.href = 'aufgaben.html';
/* WAS NICHT AUS DEM KALENDER STAMMT, BEKOMMT KEINE KALENDER-KNOEPFE.
Ohne diese Zeile waere der Zettel vom Brett im `else`-Zweig
gelandet -- mit Wecker, „erledigt" und „loeschen". Und „erledigt"
haette `PATCH /workspace/api/termine/b12` geschickt: eine Nummer,
die es dort nicht gibt, an eine Schnittstelle, die davon nichts
weiss. Der Fehler waere still gewesen (eine 404 im Hintergrund),
und der Knopf haette einfach nichts getan.
Dasselbe gilt seit jeher fuer die Frist -- deshalb steht beides
in EINER Bedingung und nicht in zwei: Es ist dieselbe Regel. */
if (e.art === 'frist' || e.art === 'brett') {
const a = el('a', 'schritt', e.art === 'brett' ? 'zum Brett' : 'zur Aufgabe');
a.href = e.ziel || 'aufgaben.html';
rechts.append(a);
} else {
/* DER WECKER (07.09.2026). Er steht bei JEDEM Termin, den man