- FastAPI Backend mit /api/events, /api/events/{id}, /api/health Endpoints
- Optionale Suche nach Event-Titel (Query Parameter ?search=...)
- Jinja2 Web-Frontend auf / mit Suchfeld
- Shared DB Connection Module (api/database.py)
- Docker Compose: calendar-api Service hinzugefügt
- Sync.py refactored: nutzt shared database.py
10 KiB
SPEC.md - Calendar Sync Projekt
1. Projektübersicht
Python-basiertes System zur Synchronisation eines Google Calendar ICS-Feeds nach MariaDB, mit zusätzlichem REST API + Web-Frontend für die Anzeige der nächsten Termine. Läuft als Docker Container im Homelab.
2. Architektur
┌─────────────────────┐ ┌─────────────────────┐
│ calendar-sync │ │ calendar-api │
│ (Sync Tool) │ │ (FastAPI + HTML) │
│ │ │ │
│ - ICS Polling │ │ - REST API │
│ - RRULE Expansion │ │ - Web-Frontend │
│ - MariaDB Write │ │ - MariaDB Read │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
└────────────┬───────────────┘
│
┌────────▼────────┐
│ MariaDB │
│ calendar_sync │
└─────────────────┘
Entscheidung: Gleicher Docker Build (ein Dockerfile), zwei verschiedene Container/Services via docker-compose.yml. Das Image wird mit einem --entrypoint Parameter gesteuert.
3. Bestehendes System (Sync Tool)
3.1 Funktionen
- Lädt periodisch einen ICS-Feed von Google Calendar
- Expandiert wiederkehrende Events (RRULE/EXDATE/RECURRENCE-ID)
- Schreibt Einzel-Instanzen in MariaDB (calendar_events Tabelle)
- Soft-Delete: Events werden mit
deleted=1markiert statt gelöscht - Optionaler Healthcheck-Ping nach jedem Sync-Durchlauf
- Optionale Datenbank-Bootstrap (DB + User anlegen)
3.2 Datenbank-Schema (calendar_events)
| Feld | Typ | Beschreibung |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | Primärschlüssel |
| calendar_label | VARCHAR(64) | Kalender-Bezeichnung |
| instance_key | VARCHAR(255) | SHA1 Hash (UID + RECURRENCE-ID) |
| uid | VARCHAR(255) | Originale Event UID |
| recurrence_id | VARCHAR(64) | RECURRENCE-ID (nullable) |
| summary | VARCHAR(512) | Titel des Events |
| description | TEXT | Beschreibung |
| location | VARCHAR(512) | Ort |
| start_at | DATETIME | Startzeit (naive UTC) |
| end_at | DATETIME | Endzeit (naive UTC, nullable) |
| all_day | TINYINT(1) | Ganzägiges Event |
| status | VARCHAR(32) | CONFIRMED/CANCELLED/TENTATIVE |
| deleted | TINYINT(1) | Soft-Delete Flag |
| last_seen_at | DATETIME | Letzter Sync-Zeitpunkt |
| created_at | DATETIME | Erstellungszeitpunkt |
| updated_at | DATETIME | Letzter Update-Zeitpunkt |
3.3 Environment Variablen
ICS_URL(required) - Google Calendar ICS Feed URLCALENDAR_LABEL- Bezeichnung für den KalenderDB_HOST,DB_PORT,DB_NAME,DB_USER,DB_PASSWORD- MariaDB ZugangsdatenSYNC_INTERVAL_MINUTES- Sync Intervall (default: 15)WINDOW_PAST_DAYS,WINDOW_FUTURE_DAYS- Zeitfenster für EventsLOG_LEVEL- Logging LevelHEALTHCHECK_URL- Optionaler Healthcheck EndpointDB_BOOTSTRAP,DB_ROOT_USER,DB_ROOT_PASSWORD- Optionales DB Bootstrap
4. Neues System (API + Web-Frontend)
4.1 REST API Endpoints
GET /api/events
Gibt die nächsten N Termine zurück.
Query Parameter:
limit(optional, default: 10, max: 50) - Anzahl der Eventscalendar_label(optional) - Filter nach Kalendersearch(optional) - Suchbegriff für Event-Titel (LIKE %search%)
Response (JSON):
{
"events": [
{
"id": 123,
"summary": "Meeting mit Team",
"description": "...",
"location": "Raum 101",
"start_at": "2025-01-15T10:00:00",
"end_at": "2025-01-15T11:00:00",
"all_day": false,
"status": "CONFIRMED"
}
],
"count": 10,
"query_time": "2025-01-14T14:30:00Z"
}
GET /api/events/{id}
Gibt ein einzelnes Event zurück.
GET /api/health
Healthcheck Endpoint für den API Container.
4.2 Web-Frontend
URL: http://localhost:8000/ (oder konfigurierbarer Port)
Funktionen:
- Zeigt die nächsten 10 Termine in einer übersichtlichen Liste
- Suchfeld oben (optional, filtert nach Event-Titel)
- Responsive Design (funktioniert auf Desktop und Handy)
- Einfaches, cleanes Design ohne Framework (nur HTML + CSS + vanilla JS)
Darstellung pro Event:
- Datum + Uhrzeit (oder "Ganztägig")
- Titel (summary)
- Ort (location) - falls vorhanden
- Status-Anzeige (Farbcode: grün=CONFIRMED, gelb=TENTATIVE, rot=CANCELLED)
4.3 Technologie-Stack (API)
- Framework: FastAPI
- Templating: Jinja2 (server-side rendering)
- DB-Zugriff: mysql-connector-python (gleicher Connection-Pool wie Sync)
- Port: 8000 (konfigurierbar via
API_PORT)
4.4 Additional Environment Variablen (API)
API_PORT- Port für den API Server (default: 8000)API_HOST- Bind Address (default: 0.0.0.0)DB_HOST,DB_PORT,DB_NAME,DB_USER,DB_PASSWORD- Identisch zum Sync
5. Docker Setup
5.1 Dockerfile (erweitert)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY sync.py .
COPY api/ ./api/
ENV PYTHONUNBUFFERED=1
# Default: Sync Tool
CMD ["python", "sync.py"]
Das Image enthält sowohl das Sync-Tool als auch die API. Der jeweilige Service wird via docker-compose.yml gesteuert.
5.2 Docker Compose (erweitert)
services:
calendar-sync:
image: ghcr.io/skoelle/calender_sync:latest
container_name: calendar-sync
restart: unless-stopped
command: ["python", "sync.py"]
environment:
- ICS_URL=${ICS_URL}
- CALENDAR_LABEL=${CALENDAR_LABEL:-privat}
- DB_HOST=${DB_HOST:-mariadb.fritz.box}
- DB_PORT=${DB_PORT:-3306}
- DB_NAME=${DB_NAME:-calendar_sync}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- SYNC_INTERVAL_MINUTES=${SYNC_INTERVAL_MINUTES:-15}
- WINDOW_PAST_DAYS=${WINDOW_PAST_DAYS:-90}
- WINDOW_FUTURE_DAYS=${WINDOW_FUTURE_DAYS:-365}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- HEALTHCHECK_URL=${HEALTHCHECK_URL:-}
networks:
- docker-backend
calendar-api:
image: ghcr.io/skoelle/calender_sync:latest
container_name: calendar-api
restart: unless-stopped
command: ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
ports:
- "${API_PORT:-8000}:8000"
environment:
- DB_HOST=${DB_HOST:-mariadb.fritz.box}
- DB_PORT=${DB_PORT:-3306}
- DB_NAME=${DB_NAME:-calendar_sync}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- docker-backend
networks:
docker-backend:
external: true
6. Projektstruktur (Zielstruktur)
.
├── sync.py # Hauptskript Sync Tool
├── api/
│ ├── __init__.py
│ ├── main.py # FastAPI App + Routes
│ ├── database.py # DB Connection Pool (shared)
│ └── templates/
│ └── index.html # Jinja2 Template für Web-Frontend
├── requirements.txt # Python Dependencies (erweitert)
├── Dockerfile # Docker Image Definition (erweitert)
├── docker-compose.yml # Docker Compose Konfiguration (erweitert)
├── mariadb-setup.sql # Manuelles DB-Setup Script
├── .env.example # Beispiel-Umgebungsvariablen (erweitert)
├── SPEC.md # Diese Spezifikation
└── .github/workflows/ # CI/CD (Docker Build + Push)
7. Dependencies (requirements.txt)
requests==2.32.3
icalendar==6.1.0
recurring-ical-events==3.4.1
mysql-connector-python==9.1.0
fastapi==0.115.0
uvicorn[standard]==0.30.0
jinja2==3.1.4
8. Design-Entscheidungen
| Thema | Entscheidung |
|---|---|
| API Authentifizierung | Keine (nur Homelab intern) |
| Caching | Kein (SQL Query bei jedem Request) |
| Auto-Refresh Frontend | Kein (manueller Reload) |
| CORS | Kein (Same-Origin via Jinja2 Templates) |
| Multi-Sync | Nicht benötigt |
| iCal Export | Nicht benötigt |
| Benachrichtigungen | Nicht benötigt |
| Dark Mode | Nicht benötigt |
| Search | Optionaler Suchbegriff auf Event-Titel |
| DB Bootstrap | Bleibt in sync.py |
| Template Styling | Einfaches CSS, kein Framework |
| Logging | Nur Errors + Request Log auf INFO Level |
9. Testing
9.1 Unit Tests (optional, später)
test_to_naive_utc()- Zeitkonvertierungtest_instance_key_for()- Key Generation- API Endpoint Tests mit
httpx+pytest
9.2 Integration Tests (optional, später)
- Sync Tool → DB → API → Response validieren
10. CI/CD
Bestehender GitHub Actions Workflow erweitern:
- Build einmal für beide Services
- Optional: Separater Tag für API-only Image
11. Future Enhancements (nicht im Scope)
- Kalender-Filter UI (nach calendar_label)
- Suchfunktion nach Event-Titel