Kalender: sechs neue Terminarten, mit zwei stillen Loechern darin

Wunsch: "kategorien wie bigmatch, turniere, Special-Live ... informier
dich was man da alles noch gebrauchen koennte und auch so dass wenn man
die sachen aussucht die ganze kachel und sachen die man eintippen muss
auch zu der jeweiligen kategorie passen."

Neu: BigMatch, Turnier, Special-Live, Collab, Raid-Train, Charity --
neben den drei internen Arten. Das Formular fragt je Art anderes:
beim BigMatch "Gegen wen?" mit 60 Minuten, beim Turnier "Welches
Turnier?" mit 120, bei Charity "Fuer wen wird gesammelt?" mit 180.

ZWEI FEHLER, DIE BEIDE NICHT ABGESTUERZT WAEREN:

1. termin_serien wurde nicht umgestellt. Die Umbauschleife laeuft ueber
   zwei Tabellen, bildete den Namen der Sicherungsdatei aber ohne die
   Tabelle -- und der Zeitstempel darin wird einmal pro Serverstart
   gebildet. Der zweite Durchlauf wollte also dieselbe Datei anlegen,
   VACUUM INTO weigerte sich, und das (richtige) "ohne Sicherung kein
   Umbau" beendete die ganze Schleife. Ergebnis: termine umgestellt,
   termin_serien nicht. Eine wiederkehrende BigMatch-Reihe waere ohne
   erkennbaren Grund abgelehnt worden.

2. Die neuen Arten waren im Kalender UNSICHTBAR. In kalender.js standen
   zwei weitere Aufzaehlungen derselben Arten: `zeigen = {call, termin,
   review, frist}` und die Schalterleiste. Gefiltert wird mit
   `zeigen[e.art]` -- fuer 'bigmatch' ist das undefined. Anlegen ging,
   der Server meldete 201, die Zeile stand in der Datenbank, und im
   Kalender war sie in keiner Ansicht zu sehen. Ohne Fehler, ohne Hinweis.

   Beide Listen werden jetzt aus ARTNAME abgeleitet. Und `sichtbare()`
   prueft `!== false` statt auf Wahrheit: Der Vorgabewert einer
   Sichtbarkeitsfrage muss "sichtbar" sein -- ein Eintrag zu viel ist
   ein Schoenheitsfehler, ein fehlender ein verpasster Termin.

Gefunden hat Nummer 2 kein Test, sondern ein Bildschirmfoto: In der
Schalterleiste standen vier Arten statt zehn. Meine eigene Pruefung war
zu dem Zeitpunkt gruen -- sie hoerte beim HTTP 201 auf.

Neu: server/pruef-arten.mjs, 28 Pruefungen. Baut eine Datenbank im ALTEN
Stand nach (samt Teilnehmer, Wecker, Serie), laesst die Anwendung
darueberlaufen und zaehlt nach; haelt CHECK und Serverliste gegeneinander;
und oeffnet zuletzt einen echten Browser, um zu sehen, ob die Eintraege
auch ankommen. Gegenprobe gefahren: mit dem alten Stand meldet sie
0 von 6 sichtbar.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-08 00:44:33 +02:00
co-authored by Claude Opus 5
parent 535d86d3f7
commit de06ce0227
23 changed files with 919 additions and 206 deletions
+158 -9
View File
@@ -53,14 +53,45 @@
return k;
};
const ARTNAME = { call: 'Call', termin: 'Termin', review: 'Review', frist: 'Frist' };
/* Die Namen der Arten. `frist` steht mit drin, obwohl es keine
Terminart ist: Aufgabenfristen erscheinen im Kalender als
Eintraege und brauchen dort auch eine Beschriftung. */
const ARTNAME = {
call: 'Call', termin: 'Termin', review: 'Review', frist: 'Frist',
bigmatch: 'BigMatch', turnier: 'Turnier', special: 'Special-Live',
collab: 'Collab', raid: 'Raid-Train', charity: 'Charity',
};
/* 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. */
/* EINE FARBE JE ART (08.09.2026 erweitert).
Die drei internen Arten behalten ihre Farben -- wer sie kennt, soll
sie wiedererkennen. Die sechs Auftritte bekommen WAERMERE, kraeftigere
Toene: Im Monatsraster sieht man dadurch auf einen Blick, ob eine
Woche voller Arbeitstermine steckt oder voller Streams, ohne ein
Wort zu lesen.
Innerhalb der Auftritte ist die Zuordnung nicht beliebig:
bigmatch Rot -- ein Duell, das schaerfste Format
turnier Bernstein -- mehrere Runden, laenger, ruhiger als ein Duell
special Magenta -- die Ausnahme, die auffallen soll
collab Tuerkis -- gemeinsam, freundlich, kein Wettkampf
raid Orange -- Bewegung, eine Kette
charity Gruengold -- Spenden, warm und nicht schreiend
Alle neun sind gegen den dunklen Grund auf mindestens 4,5:1
geprueft (pruef-buehne misst die Texte, die sie faerben). */
const ARTFARBE = {
call: '#3fbdf5',
termin: '#8a76ff',
review: '#50a752',
frist: '#d17717',
call: '#3fbdf5',
termin: '#8a76ff',
review: '#50a752',
frist: '#d17717',
bigmatch: '#ff5f6d',
turnier: '#e3b341',
special: '#f065c8',
collab: '#2fd6c4',
raid: '#ff9040',
charity: '#a7d64a',
};
const FEIERTAG_FARBE = '#ff9aa2';
const ANLASS_FARBE = '#d8a13a';
@@ -211,7 +242,27 @@
}
let ansicht = 'monat';
let anker = heuteText(); // Datum, um das herum angezeigt wird
const zeigen = { call: true, termin: true, review: true, frist: true };
/* WAS NICHT AUSDRUECKLICH ABGEWAEHLT IST, IST SICHTBAR (08.09.2026).
Hier stand `{ call: true, termin: true, review: true, frist: true }`
-- eine zweite, von Hand gepflegte Liste der Arten neben ARTNAME.
Als die sechs Auftritts-Arten dazukamen, blieb sie stehen. Unten
filtert `sichtbare()` mit `zeigen[e.art]`, und fuer 'bigmatch' ist
das `undefined`. Ergebnis: Ein BigMatch liess sich anlegen, der
Server meldete 201, er stand in der Datenbank -- und war im
Kalender in KEINER Ansicht zu sehen. Ohne Fehler, ohne Hinweis.
Filipe haette einen Eintrag gemacht und nichts waere passiert.
Zwei Lehren, beide hier umgesetzt:
1. Die Liste wird ABGELEITET, nicht gefuehrt. ARTNAME ist die eine
Quelle; wer dort eine Art ergaenzt, hat sie ueberall.
2. Der Vorgabewert einer Sichtbarkeitsfrage muss SICHTBAR sein.
`sichtbare()` prueft deshalb `!== false` statt auf Wahrheit --
eine unbekannte Art faellt dann auf, statt zu verschwinden.
Ein Eintrag zu viel im Kalender ist ein Schoenheitsfehler, ein
fehlender ist ein verpasster Termin. */
const zeigen = Object.fromEntries(Object.keys(ARTNAME).map((a) => [a, true]));
/* ---------- Datumsrechnung ----------------------------------------------
Alles läuft in UTC-Mittag. Grund: Wer mit lokalen Zeiten rechnet,
@@ -449,7 +500,10 @@
a.tag.localeCompare(b.tag) || (a.uhr || '99:99').localeCompare(b.uhr || '99:99'));
}
const sichtbare = () => (daten ? daten.eintraege.filter((e) => zeigen[e.art]) : []);
/* `!== false`, nicht auf Wahrheit pruefen -- Begruendung oben bei
`zeigen`. Eine Art, die niemand eingetragen hat, wird angezeigt
statt verschluckt. */
const sichtbare = () => (daten ? daten.eintraege.filter((e) => zeigen[e.art] !== false) : []);
function nachTag() {
const karte = new Map();
@@ -798,8 +852,22 @@
function baueFilter() {
const ziel = $('filter');
ziel.replaceChildren();
for (const art of ['call', 'termin', 'review', 'frist']) {
const n = daten.eintraege.filter((e) => e.art === art).length;
/* DIE LISTE WIRD ABGELEITET (08.09.2026) -- vorher stand hier
`['call','termin','review','frist']` fest, die dritte Fassung
derselben Aufzaehlung in einer Datei.
Die vier Grundarten bekommen immer einen Schalter, auch bei null
Eintraegen: Sie sind der Alltag, und ein Schalter, der auftaucht
und verschwindet, verwirrt mehr, als er hilft. Die sechs
Auftritts-Arten erscheinen nur, wenn es sie im Zeitraum wirklich
gibt -- zehn Schalter, davon sechs mit "(0)", waeren eine Leiste,
die niemand mehr liest. */
const GRUND = ['call', 'termin', 'review', 'frist'];
const anzahl = (art) => daten.eintraege.filter((e) => e.art === art).length;
const arten = [...GRUND,
...Object.keys(ARTNAME).filter((a) => !GRUND.includes(a) && anzahl(a) > 0)];
for (const art of arten) {
const n = anzahl(art);
const b = el('button', 'k-chip');
b.type = 'button';
b.setAttribute('aria-pressed', String(zeigen[art]));
@@ -1380,6 +1448,78 @@
}
}
/* =====================================================================
DAS FORMULAR PASST SICH DER ART AN (08.09.2026)
Wunsch Filipe: "auch so, dass wenn man die sachen aussucht, die
ganze kachel und die sachen, die man eintippen muss, auch zu der
jeweiligen kategorie passen."
WAS SICH AENDERT, SIND DIE FRAGEN -- nicht die Datenbank. Ein
BigMatch fragt nach dem GEGNER, ein Turnier nach dem FORMAT, ein
Collab nach den LEUTEN. Es ist jedes Mal dasselbe Feld "Titel";
was fehlt, ist die richtige Frage davor. Eigene Spalten je Art
waeren neun halbleere Felder in einer Tabelle und beim naechsten
Format wieder eines mehr.
Dasselbe fuer Ort und Notiz: "Ort oder Link" ist bei einem
Raid-Train die falsche Frage -- dort geht es um die KETTE, also um
die Reihenfolge der Kanaele.
Und die Dauer bekommt einen Vorschlag: Ein Call dauert 30 Minuten,
ein Turnier selten unter zwei Stunden. Vorgeschlagen, nicht
erzwungen -- und nur bei einem NEUEN Eintrag, nie beim Bearbeiten:
Wer eine Dauer eingetragen hat, will sie nicht durch einen
Artwechsel verlieren. */
const ART_FELDER = {
call: { titel: 'Worum geht es?', titelHilfe: 'z. B. Wochen-Call mit Luna',
ort: 'Ort oder Link', ortHilfe: 'Discord, Meet-Link, Adresse …', dauer: 30 },
termin: { titel: 'Worum geht es?', titelHilfe: 'z. B. Fotoshooting',
ort: 'Ort oder Link', ortHilfe: 'Adresse, Discord, Link …', dauer: 30 },
review: { titel: 'Was wird durchgesehen?', titelHilfe: 'z. B. Monatsrückblick Luna',
ort: 'Ort oder Link', ortHilfe: 'Discord, Meet-Link …', dauer: 45 },
bigmatch: { titel: 'Gegen wen?', titelHilfe: 'z. B. BigMatch vs. Beanii',
ort: 'Wo läuft es?', ortHilfe: 'TikTok, Twitch, Kanal-Link …', dauer: 60,
notiz: 'Regeln, Einsatz, Uhrzeit des Matches' },
turnier: { titel: 'Welches Turnier?', titelHilfe: 'z. B. Herbst-Cup, Runde 2',
ort: 'Wo läuft es?', ortHilfe: 'Plattform oder Turnier-Link', dauer: 120,
notiz: 'Modus, Runde, Gegner, Preisgeld' },
special: { titel: 'Was ist das Besondere?', titelHilfe: 'z. B. 24-Stunden-Stream',
ort: 'Wo läuft es?', ortHilfe: 'TikTok, Twitch, YouTube …', dauer: 180,
notiz: 'Ablauf, Gäste, was angekündigt wurde' },
collab: { titel: 'Mit wem?', titelHilfe: 'z. B. Collab mit Miesmuschel',
ort: 'Wessen Kanal?', ortHilfe: 'Kanal oder Link', dauer: 90,
notiz: 'Thema, wer moderiert, Absprachen' },
raid: { titel: 'Welche Kette?', titelHilfe: 'z. B. Raid-Train Freitagabend',
ort: 'Reihenfolge der Kanäle', ortHilfe: 'wer nach wem, mit Links', dauer: 60,
notiz: 'Startzeit je Kanal, Übergabe-Absprache' },
charity: { titel: 'Für wen wird gesammelt?', titelHilfe: 'z. B. Spendenstream Tierheim',
ort: 'Wo läuft es?', ortHilfe: 'Plattform, Spendenlink', dauer: 180,
notiz: 'Spendenziel, Link, Ablauf' },
};
function artFelderSetzen(neuerEintrag) {
const art = $('f-art').value;
const f = ART_FELDER[art] || ART_FELDER.termin;
const schild = (feld, text, zusatz) => {
const l = document.querySelector(`label[for="${feld}"]`);
if (!l) return;
l.textContent = text;
if (zusatz) {
const s2 = el('span', 'leise', ' (optional)');
l.append(s2);
}
};
schild('f-titel', f.titel, false);
$('f-titel').placeholder = f.titelHilfe || '';
schild('f-ort', f.ort, true);
$('f-ort').placeholder = f.ortHilfe || '';
schild('f-text', f.notiz ? 'Notiz' : 'Notiz', true);
$('f-text').placeholder = f.notiz || '';
/* Die Dauer nur bei einem NEUEN Eintrag vorschlagen -- siehe oben. */
if (neuerEintrag) $('f-dauer').value = f.dauer || 30;
}
function formularOeffnen(tag, termin) {
/* Vorbelegung: der angeklickte Tag um 18:00 -- ein realistischer
Zeitpunkt für einen Call, spart in der Regel zwei Klicks. Ohne Tag
@@ -1397,6 +1537,7 @@
baueDabei([]);
bearbeiteId = 0;
$('neu-speichern').textContent = 'Anlegen';
artFelderSetzen(true);
/* BEARBEITEN: dieselben Felder, gefuellt. Der Rhythmus bleibt
aussen vor -- eine Wiederholung ist eine REGEL und wird unter
@@ -1414,6 +1555,8 @@
baueDabei(termin.teilnehmer_ids || []);
$('f-takt').value = '';
wdhVorschau();
/* `false`: Beim Bearbeiten bleibt die eingetragene Dauer stehen. */
artFelderSetzen(false);
$('neu-speichern').textContent = 'Speichern';
}
umschalten(true);
@@ -1437,6 +1580,12 @@
umschalten(false);
});
/* Beim Wechsel der Art die Fragen umstellen. `true` heisst: Die
Dauer darf vorgeschlagen werden -- wer die Art wechselt, hat den
Eintrag noch nicht fertig. Beim Bearbeiten wird der Wechsel
ebenfalls hier ausgeloest, aber `formularOeffnen` hat die echte
Dauer danach schon gesetzt. */
$('f-art').addEventListener('change', () => artFelderSetzen(true));
$('f-beginn').addEventListener('change', taktAuswahlFuellen);
$('f-takt').addEventListener('change', wdhVorschau);
$('f-ende').addEventListener('change', wdhVorschau);