Status: Umgesetzt (2026-06-09) — siehe Eintrag in 04_projektfortschritt.md · erstellt 2026-06-09
Die Events kommen künftig aus einem Baikal (CalDAV)-Server statt aus statischem data/events.ts. Baikal wird Teil eines docker-compose-Stacks; Credentials sind per Config setzbar und zeigen im Default auf die lokale Instanz + den dort eingerichteten Account. Die bisherigen Demo-Events werden nach Baikal geseedet (ein Kalender je Kategorie). Die Gesamtheit aller Termine wird wie bisher gemeinsam angezeigt.
Das Schwesterprojekt webcraftmedia/jahrweiser (IT4Change, gleicher Stack) implementiert genau dieses Muster und dient als Blueprint. Wir übernehmen die Transport-Schicht weitgehend, weichen aber beim Datenmodell ab.
- jahrweiser-Pattern übernehmen (Compose + Baikal-Bootstrap + Nitro-Proxy +
tsdav/ical.js+ Seed-Skript). - Kein MariaDB-Sidecar — jahrweiser nutzt ihn nur für User-Login/Sync; wir starten read-only ohne User-Accounts.
- Read-only: Posting/Moderation bleibt vorerst Mockup (eigenes späteres Paket).
- Locations:
data/locations.tsbleibt statische Frontend-Referenz; im VEVENTLOCATION-Text +X-SB-LOCATION-ID.regionbleibt ungenutzt. - Kalender pro Kategorie (5 Kalender als Buckets; Kategorie-Optik bleibt im Frontend).
Browser ──$fetch('/api/events')──▶ Nitro (Nuxt server/) ──tsdav/CalDAV──▶ Baikal
│ ical.js: parse, RRULE-Expand, TZID
▼
Event[] (unser bestehendes Interface)
- Kein direkter Browser→DAV-Zugriff (CORS/Secrets). Credentials nur server-seitig.
useEvents()tauscht nur die Datenquelle (stattimport events→useFetch('/api/events')); Filter/Wochengruppierung/Pagination/Komponenten bleiben unverändert (entspricht der in CLAUDE.md dokumentierten Erwartung).
tsdav(CalDAV-Client),ical.js(Parser/RRULE/TZID) — Runtime.tsx— Dev, für das Seed-CLI.
X-SB-* = projektspezifische X-Property (round-trippt verlustfrei in sabre/dav, fremde Clients ignorieren sie).
Event-Feld |
iCal | Typ |
|---|---|---|
uuid |
UID |
nativ |
id |
(abgeleitet = uuid) |
— |
title |
SUMMARY |
nativ |
category |
Kalender-Bucket + CATEGORIES:<key> |
nativ |
start/end |
DTSTART/DTEND (TZID=Europe/Berlin) |
nativ |
detailedDescription |
DESCRIPTION |
nativ |
url |
URL |
nativ |
image |
IMAGE;VALUE=URI |
nativ (RFC 7986) |
organizer+email |
ORGANIZER;CN=<name>:mailto:<email> |
nativ* |
description (Teaser) |
X-SB-TEASER |
X-prop |
subcategory |
X-SB-SUBCATEGORY |
X-prop |
registration |
X-SB-REGISTRATION |
X-prop |
price |
X-SB-PRICE |
X-prop |
source |
X-SB-SOURCE |
X-prop |
aggregatorNote |
X-SB-NOTE |
X-prop |
locationId |
X-SB-LOCATION-ID (+ LOCATION-Text denormalisiert) |
X-prop |
mapsUrl |
X-SB-MAPS-URL (das eine URL ist vergeben) |
X-prop |
phone |
X-SB-PHONE |
X-prop |
* Edge-Case Organizer/Email: ORGANIZER braucht eine cal-address. Regel: beide vorhanden → ORGANIZER;CN=name:mailto:email; nur Email → ORGANIZER:mailto:email; nur Name (kein Email) → X-SB-ORGANIZER (Name) statt ORGANIZER. Mapping liest beide Quellen.
- Kalenderfarbe: beim Anlegen
calendar-color=accentder Kategorie (kosmetisch). - Recurrence:
RRULE/EXDATEwerden im Nitro-Layer viaICAL.RecurExpansionüber das Anzeigefenster expandiert. Unser bisheriges Modell kann das nicht — Bonus, optional ein wiederkehrendes Demo-Event seeden (z. B. „Ekstatischer Tanz am Donnerstag" wöchentlich).
- Neu:
docker-compose.yml(+ optionaldocker-compose.override.ymlfür Dev) mit:- baikal (
ckulka/baikal:0.10.1-nginx), Volumes fürconfig+Specific, Healthcheck, Port8088:80. infra/baikal/init-bootstrap.sh(aus jahrweiser adaptiert): überspringt den Web-Installer, schreibtbaikal.yaml(SQLite,Europe/Berlin, Basic-Auth, Admin-Hash ausBAIKAL_ADMIN_PASSWORD), exit 0.- app (Nuxt): eigenes
Dockerfile(Nitro),DAV_URL=http://baikal,depends_on: baikal. (Für reines lokales Dev ohne App-Container:DAV_URL=http://localhost:8088.)
- baikal (
- DAV-User/Principal provisionieren:
infra/baikal/provision-dav-user.php+ CLIcli/baikal-bootstrap.ts(aus jahrweiser), legt denevents-Principal an (oder Admin-Principal nutzen).
nuxt.config.ts→runtimeConfig(server-only):dav: { url, username, password }ausDAV_URL/DAV_USERNAME/DAV_PASSWORD.- Defaults:
http://localhost:8088,admin/admin(= lokale Baikal-Instanz). .env.examplemit den Variablen; echte Secrets nie committen. Öffentlicher Betrieb nur via TLS (Hinweis in der Doku).
server/helpers/dav.ts(aus jahrweiser adaptiert):createCalDAVAccount,findCalendars,findEvents(calendar-query time-range),findEvent(multiget).server/utils/mapVeventToEvent.ts:ical.js-Parsing → unserEvent(inkl. X-SB-Props, ORGANIZER-Split, TZID, RRULE-Expand). Kategorie = Quell-Kalender.server/api/events.get.ts: liest alle 5 Kalender über ein Vorwärtsfenster (heute … +N Monate, konfigurierbar), mappt + merged →Event[]. Optional?from=&to=.server/api/event.get.ts?uuid=: Einzel-Event (für die Detailseite) — sucht über die Kalender bzw. multiget.- Caching: einfacher Nitro-Cache/
cachedEventHandlerüber das Fenster; später optionalsync-collection-Token. (Skalierung ist anfangs unkritisch bei ~65 Events.)
composables/useEvents.ts:allEventsaususeFetch('/api/events')statt statischem Import;filtered/visibleByDay/loadMoreetc. bleiben.getEventByUuid/Event-Detailseite: aufuseFetch('/api/event?uuid=')umstellen (statt sync über das Array).data/events.tswird nicht gelöscht, sondern zur Seed-Fixture (Quelle des Seed-CLIs).data/locations.ts(statische Referenz) unddata/categories.ts(Struktur) bleiben.
cli/seed-baikal.ts(aus jahrweisersseed-demo.tsadaptiert):ensureCalendar()(MKCALENDAR) für die 5 Kategorie-Kalender (displayname=Kategorie-Key, calendar-color=accent).- Für jedes Event aus
data/events.ts:buildIcs()mit obigem Mapping (inkl. X-SB-Props, VTIMEZONE Europe/Berlin),PUTals<uuid>.icsin den Kategorie-Kalender.
cli/seed-reset.ts(optional): Kalender leeren/neu.- npm-Scripts:
cli:baikal:bootstrap,cli:seed.
04_projektfortschritt.md-Eintrag; CLAUDE.md (Datenkonventionen → Backend statt statisch, Compose/Env, X-SB-Konvention, neueserver/-Struktur).
- Bild-Hosting: Demo-Bilder liegen in
public/img/→IMAGEreferenziert die App-URL (kein externer Store nötig). Echte Uploads später = eigenes Thema. - Ort/Region-/Pagination-Filter: nicht server-seitig in CalDAV, sondern wie bisher im Frontend (
useEvents) über das geladene Fenster. OK bei aktueller Größe. - Zeitzonen: konsequent
TZID=Europe/Berlin+VTIMEZONE;ical.js TimezoneServiceregistrieren. Mehrtages-Events sauber (DATE vs DATETIME). - Detailseite über 5 Kalender:
event.get.tsmuss kategorieübergreifend finden (multiget je Kalender oder Kategorie aus Route mitführen). - Installer-Friktion: durch
init-bootstrap.shumgangen (kein manueller Web-Wizard). - Read-only-Annahme: kein Schreibpfad; der Service-Account liest nur. Posting/Moderation = späteres Paket.
docker compose up→ Baikal healthy;cli:baikal:bootstrap+cli:seed→ 5 Kalender mit den 65 Events./api/eventsliefert gemapptesEvent[](Stichproben: X-SB-Felder, ORGANIZER-Split, Kategorie aus Kalender, korrekte Zeiten).- Frontend DE +
/en: Wochenansicht, Filter (Kategorie/Ort/Datum), Pagination, Detailseite, Event-404 — unverändertes Verhalten. - Konsole/Server-Log sauber; Mobile 375/390/430 unverändert (kein Layout-Eingriff).
- Posting/Moderations-Workflow (Schreibpfad,
STATUS/Inbox-Kalender, Admin-UI). - Übersetzte Event-Inhalte (Demo-Events bleiben DE; i18n betrifft UI, nicht Event-Daten).
webcal://-Abo-Feed für Endnutzer (möglich, aber separat).- Echtes Bild-Upload-Hosting.
regionals Filtermerkmal.