diff --git a/.env.example b/.env.example index a2089a7..ba8a48a 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,6 @@ LOG_LEVEL=INFO # Optional: Healthchecks.io / Uptime Kuma URL (wird nach jedem Sync gepingt) # HEALTHCHECK_URL=https://hc-ping.com/DEINE_UUID + +# API Server +API_PORT=8000 diff --git a/Dockerfile b/Dockerfile index 988c193..a77286b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY sync.py . +COPY api/ ./api/ ENV PYTHONUNBUFFERED=1 diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..510c817 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,133 @@ +# PLAN.md - Implementierungsplan + +Feature: REST API + Web-Frontend für Calendar Sync + +## Übersicht + +Ziel: FastAPI-basierte API und ein Jinja2 Web-Frontend hinzufügen, um die nächsten 10 Termine aus MariaDB auszulesen und anzuzeigen. Gleicher Docker Build, separater Container. + +--- + +## Phase 1: Projektstruktur + Shared Module + +### Step 1.1: API-Verzeichnisstruktur anlegen +``` +api/ +├── __init__.py +├── main.py +├── database.py +└── templates/ + └── index.html +``` + +### Step 1.2: database.py - DB Connection extrahieren +- `get_connection()` Funktion aus `sync.py:81-89` in `api/database.py` verschieben +- Connection Pooling optional (First: einfacher single connection) +- Umgebungsvariablen identisch zu sync.py +- sync.py importiert dann `from api.database import get_connection` + +**Dateien:** `api/__init__.py`, `api/database.py`, `sync.py` (Import anpassen) + +--- + +## Phase 2: FastAPI Backend + +### Step 2.1: api/main.py - FastAPI App erstellen +- FastAPI Instanz erstellen +- GET `/api/events` Endpoint + - Query Parameter: `limit` (default 10, max 50), `calendar_label` (optional), `search` (optional) + - SQL bei search: `WHERE deleted=0 AND start_at >= NOW() AND summary LIKE %s ORDER BY start_at ASC LIMIT %s` + - SQL ohne search: `WHERE deleted=0 AND start_at >= NOW() ORDER BY start_at ASC LIMIT %s` + - Response als JSON +- GET `/api/events/{id}` Endpoint + - Einzelnes Event nach ID +- GET `/api/health` Endpoint + - Response: `{"status": "ok"}` +- GET `/` Endpoint + - Jinja2 Template rendern mit Events + - Query Parameter `search` weiterleiten + +### Step 2.2: Response Model definieren +- Pydantic Model für Event Response +- DATETIME → String Konvertierung (ISO Format) + +**Dateien:** `api/main.py` + +--- + +## Phase 3: Web-Frontend + +### Step 3.1: api/templates/index.html +- Einfaches HTML5 Template +- Jinja2 Variablen: `{{ events }}`, `{{ search }}` +- CSS inline oder im ` + + +
+

Termine

+ +
+ + + {% if search %} + Zurücksetzen + {% endif %} +
+ +
+ {% if events %} + {% for event in events %} +
+
+
+
+ {% if event.all_day %} + {{ event.start_at.strftime('%d.%m.%Y') }} Ganztägig + {% elif event.end_at %} + {{ event.start_at.strftime('%d.%m.%Y %H:%M') }} - {{ event.end_at.strftime('%H:%M') }} + {% else %} + {{ event.start_at.strftime('%d.%m.%Y %H:%M') }} + {% endif %} +
+
{{ event.summary or '(Kein Titel)' }}
+ {% if event.location %} +
{{ event.location }}
+ {% endif %} +
+ + {{ event.status }} + +
+
+ {% endfor %} + {% else %} +
+ {% if search %} + Keine Termine für "{{ search }}" gefunden. + {% else %} + Keine anstehenden Termine. + {% endif %} +
+ {% endif %} +
+
+ + diff --git a/docker-compose.yml b/docker-compose.yml index bba22fa..598b5d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,7 @@ services: image: ghcr.io/skoelle/calender_sync:latest container_name: calendar-sync restart: unless-stopped + command: ["python", "sync.py"] environment: - ICS_URL=${ICS_URL} @@ -22,6 +23,26 @@ services: - 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" diff --git a/requirements.txt b/requirements.txt index 1e6d0be..8fc800c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,6 @@ 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 diff --git a/sync.py b/sync.py index 0c36511..49e6f48 100644 --- a/sync.py +++ b/sync.py @@ -18,6 +18,8 @@ import recurring_ical_events import mysql.connector from mysql.connector import Error as MySQLError +from api.database import get_connection + logging.basicConfig( level=os.environ.get("LOG_LEVEL", "INFO"), format="%(asctime)s [%(levelname)s] %(message)s", @@ -78,17 +80,6 @@ def bootstrap_database(): root_conn.close() -def get_connection(): - return mysql.connector.connect( - host=DB_HOST, - port=DB_PORT, - user=DB_USER, - password=DB_PASSWORD, - database=DB_NAME, - autocommit=False, - ) - - def ensure_schema(conn): cur = conn.cursor() cur.execute(""" @@ -228,7 +219,7 @@ def run_sync_once(): occurrences = expand_events(ics_bytes, window_start, window_end) log.info("ICS geladen, %d Instanzen im Fenster gefunden", len(occurrences)) - conn = get_connection() + conn = get_connection(autocommit=False) try: ensure_schema(conn) cur = conn.cursor()