6.3 KiB
JARVIS Nextcloud-Kalender-Integration (Phase 4c) - Design
Datum: 2026-09-13 Status: Approved, bereit fuer Implementierungsplan
Kontext
Naechste Erweiterung nach dem Wetter-Widget: Zugriff auf einen bestehenden Nextcloud-Kalender ("FFW-Onza-Alle", ein geteilter Organisations-Kalender der Freiwilligen Feuerwehr Onza) via CalDAV. JARVIS soll Termine anzeigen, im Chat abfragbar machen und - nach Bestaetigung im Chat - neue Termine anlegen koennen.
Verbindung verifiziert (13.09.2026):
- CalDAV-URL:
https://cloud.ffw-onza.de/remote.php/dav/calendars/jonny/ffw-onza-alle/ - Auth: HTTP Basic mit Nextcloud-App-Passwort (User
jonny) - Python-Library
caldav(+icalendarals Abhaengigkeit) liest und schreibt erfolgreich gegen diesen Kalender (Testtermin angelegt und wieder geloescht) - Kalender-Timezone: Europe/Berlin
- Enthaelt sowohl Termine mit Uhrzeit (
dtstartalsdatetime) als auch ganztaegige Termine (dtstartalsdateohne Uhrzeit, z.B. "Volksfestwache")
Sicherheit: Schreibzugriff nur nach Bestaetigung
Der Kalender ist ein geteilter Organisations-Kalender, kein privater. Direkte, unbestaetigte Termin-Erstellung durch ein LLM (Halluzinationsrisiko) waere in einem geteilten Kalender riskanter als in einem privaten. Deshalb:
- Der Chat-System-Prompt weist Claude explizit an, vor jedem Aufruf von
create_calendar_eventim Klartext nachzufragen ("Soll ich den Termin XY am [Datum] anlegen?") und das Tool erst aufzurufen, nachdem der Nutzer im naechsten Chat-Turn zugestimmt hat. - Das ist Prompt-Steuerung, kein hartes technisches Gate (kein separates Freigabe-UI) - Standardmuster fuer LLM-Tool-Use mit Human-in-the-loop bei diesem Umfang. Ein hartes Gate (z.B. ein Bestaetigungs-Endpoint) waere fuer diese Phase Overengineering (YAGNI) und ist ein moegliches Follow-up, falls sich in der Praxis zeigt, dass die Prompt-Steuerung nicht ausreicht.
Architektur
Neues Modul-internes CalDAV-Setup in main.py (kein eigenes File noetig bei
diesem Umfang - passt zur bestehenden Ein-Datei-Struktur des Projekts):
NEXTCLOUD_CALDAV_URL = os.getenv("NEXTCLOUD_CALDAV_URL")
NEXTCLOUD_USER = os.getenv("NEXTCLOUD_USER")
NEXTCLOUD_APP_PASSWORD = os.getenv("NEXTCLOUD_APP_PASSWORD")
Zwei synchrone Helper-Funktionen (die caldav-Library ist nicht async-nativ,
daher ueber asyncio.to_thread aufgerufen - gleiches Muster wie die
bestehenden Postgres-Helper via db_query):
list_upcoming_events(days_ahead: int) -> list[dict]- liest Termine der naechsten N Tage, sortiert nach Start, gibt{summary, start, end, description, all_day}pro Termin zurueck (Datumswerte als ISO-Strings).create_event(summary: str, start: str, end: str, description: str = "") -> dict- legt einen neuen Termin an (ISO-8601-Datums-/Zeitstrings als Input, vom Aufrufer bzw. von Claude im Tool-Call geliefert), gibt die gespeicherten Werte zurueck.
Backend-Endpoint (fuer das Dashboard-Widget)
GET /api/v1/calendar/events?days=14 (geschuetzt wie alle anderen Routen)
ruft list_upcoming_events auf und gibt {"events": [...]} zurueck. Bei
Verbindungs-/Auth-Fehler zu Nextcloud: 503 mit Fehlermeldung statt
stillem leerem Array - ein leeres Array waere nicht von "wirklich keine
Termine" unterscheidbar.
Chat-Integration: Claude Tool Use
/api/v1/chat bekommt einen tools-Parameter mit zwei Tool-Definitionen:
list_calendar_events- Parameterdays_ahead(Ganzzahl). Ruftlist_upcoming_eventsauf.create_calendar_event- Parametersummary,start(ISO-8601),end(ISO-8601), optionaldescription. Ruftcreate_eventauf.
Ablauf pro Chat-Request: Claude wird mit den Tools aufgerufen. Antwortet
Claude mit einem tool_use-Block, fuehrt das Backend die entsprechende
Python-Funktion aus, haengt das Ergebnis als tool_result an die
Konversation an und ruft Claude ein zweites Mal auf, um die finale,
natuerlichsprachige Antwort zu bekommen (ein Tool-Call-Zyklus reicht fuer die
hier vorgesehenen Anwendungsfaelle - keine mehrstufige Tool-Verkettung noetig,
YAGNI). Der System-Prompt wird um die Bestaetigungs-Regel (siehe oben)
ergaenzt.
Die vom Tool-Use erzeugten Zwischennachrichten (der tool_use- und
tool_result-Turn) werden nicht in der messages-Tabelle gespeichert -
nur die sichtbare finale Nutzer-/Assistant-Nachricht, wie bisher. Das haelt
die Persistenz-Logik unveraendert und die Chat-Historie fuer das Frontend
weiterhin einfach (reine user/assistant-Paare).
Frontend: "Nächste Termine"-Widget im Dashboard
Neue Sektion in Dashboard.tsx (oder eigene Komponente CalendarWidget,
analog zu WeatherWidget): laedt GET /api/v1/calendar/events?days=14 und
zeigt eine einfache Liste (Datum + Uhrzeit falls vorhanden + Titel). Kein
eigenes Erstellen/Bearbeiten im Dashboard (das laeuft ueber den Chat) - YAGNI
fuer diese Phase.
Fehlerbehandlung
- Nextcloud nicht erreichbar/Auth ungueltig:
list_upcoming_events/create_eventwerfen eine Exception mit klarer Meldung. Im Dashboard-Endpoint wird daraus ein503. Im Chat-Tool-Use-Pfad wird die Fehlermeldung alstool_resultmitis_error: truean Claude zurueckgegeben, das sie dann verstaendlich im Chat formuliert (kein roher Stacktrace fuer den Nutzer). - Ganztaegige Termine (kein
dtstart-Zeitanteil): werden mitall_day: truemarkiert, Frontend zeigt dafuer nur das Datum ohne Uhrzeit.
Testing
- Backend: Unit-Tests fuer
list_upcoming_eventsundcreate_eventmit gemocktemcaldav.DAVClient(keine echten Netzwerkaufrufe im Test). - Manueller End-to-End-Test nach Deployment: Dashboard-Widget zeigt echte Termine (bereits verifiziert: 11 vorhandene Termine im Kalender), im Chat nach "was steht diese Woche an?" fragen, dann testweise einen Termin anlegen lassen inkl. Bestaetigungs-Nachfrage - danach den Test-Termin wieder aus dem echten Kalender loeschen (manuell oder per Skript), damit der Organisations-Kalender sauber bleibt.
Out of Scope (bewusst nicht Teil dieser Phase)
- Bearbeiten/Loeschen bestehender Termine ueber Chat oder Dashboard.
- Mehrere Kalender/Kalenderauswahl (nur der eine angegebene Kalender).
- Wiederkehrende Termine anlegen (nur einmalige Termine ueber
create_calendar_event). - Hartes technisches Freigabe-Gate fuer Termin-Erstellung (siehe "Sicherheit" oben) - Prompt-Steuerung reicht fuer den Start.