diff --git a/.env.example b/.env.example index a2089a7..0e02f8c 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,14 @@ LOG_LEVEL=INFO # Optional: Healthchecks.io / Uptime Kuma URL (wird nach jedem Sync gepingt) # HEALTHCHECK_URL=https://hc-ping.com/DEINE_UUID + +# Optional: Database Bootstrap (erstellt DB und User beim Start) +# DB_BOOTSTRAP=true +# DB_ROOT_USER=root +# DB_ROOT_PASSWORD=dein_root_passwort + +# API-Server Port (für Web-UI + REST API) +API_PORT=8000 + +# Zeitzone für die API/Web-UI (Default: UTC) +# TIMEZONE=Europe/Berlin diff --git a/AGENTS.md b/AGENTS.md index 2f89565..80c708d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,7 +48,7 @@ Die API (`api/main.py`) ist eine separarte FastAPI-App die als eigenständiger C ### Datenbank-Schema Schema wird in `ensure_schema()` per `CREATE TABLE IF NOT EXISTS` erstellt. Bei Schema-Änderungen: -- `ensure_schema()` in `sync.py:91` anpassen +- `ensure_schema()` in `sync.py:83` anpassen - MariaDB-kompatibles SQL verwenden (kein PostgreSQL-Specific) - Indexe für Performance bedenken @@ -56,7 +56,7 @@ Schema wird in `ensure_schema()` per `CREATE TABLE IF NOT EXISTS` erstellt. Bei Verwendet `recurring_ical_events` Bibliothek für RRULE/EXDATE/RECURRENCE-ID Expansion. Fenster wird über `WINDOW_PAST_DAYS`/`WINDOW_FUTURE_DAYS` gesteuert. ### UTC-Normalisierung -Alle Zeiten werden in naive UTC datetime konvertiert (`to_naive_utc()`). Bei Datumsänderungen sicherstellen, dass Zeitzone korrekt处理 wird. +Alle Zeiten werden in naive UTC datetime konvertiert (`to_naive_utc()`). Bei Datumsänderungen sicherstellen, dass die Zeitzone korrekt verarbeitet wird. ### Soft-Delete Events werden nicht gelöscht, sondern mit `deleted=1` markiert (`mark_missing_as_deleted()`). diff --git a/README.md b/README.md index 190e0bb..127a1f9 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Läuft als Docker Container, pollt periodisch einen privaten Google Calendar ICS | `DB_BOOTSTRAP` | `false` | DB + User beim Start erstellen | | `DB_ROOT_USER` | - | Root-User fürs Bootstrap | | `DB_ROOT_PASSWORD` | - | Root-Passwort fürs Bootstrap | +| `API_PORT` | `8000` | Port für den API/Web-UI Container | +| `TIMEZONE` | `UTC` | Zeitzone für API/Web-UI Anzeige (z.B. `Europe/Berlin`) | ## Datenbank-Schema @@ -132,12 +134,14 @@ curl http://localhost:8000/api/events/42 "location": "Konferenzraum 1", "start_at": "2025-01-15T10:00:00", "end_at": "2025-01-15T11:00:00", - "all_day": false, - "status": "CONFIRMED" - } + "all_day": false, + "status": "CONFIRMED", + "timezone": "Europe/Berlin" + } ], "count": 1, - "query_time": "2025-01-15T09:30:00Z" + "query_time": "2025-01-15T09:30:00Z", + "timezone": "Europe/Berlin" } ``` diff --git a/SPEC.md b/SPEC.md index 6bbe1c4..7089fcc 100644 --- a/SPEC.md +++ b/SPEC.md @@ -24,7 +24,7 @@ Python-basiertes System zur Synchronisation eines Google Calendar ICS-Feeds nach └─────────────────┘ ``` -**Entscheidung:** Gleicher Docker Build (ein Dockerfile), zwei verschiedene Container/Services via `docker-compose.yml`. Das Image wird mit einem `--entrypoint` Parameter gesteuert. +**Entscheidung:** Gleicher Docker Build (ein Dockerfile), zwei verschiedene Container/Services via `docker-compose.yml`. Der jeweilige Service wird via `command` Parameter gesteuert (`python sync.py` vs. `uvicorn api.main:app`). ## 3. Bestehendes System (Sync Tool) @@ -89,12 +89,14 @@ Gibt die nächsten N Termine zurück. "location": "Raum 101", "start_at": "2025-01-15T10:00:00", "end_at": "2025-01-15T11:00:00", - "all_day": false, - "status": "CONFIRMED" - } - ], + "all_day": false, + "status": "CONFIRMED", + "timezone": "Europe/Berlin" + } + ], "count": 10, - "query_time": "2025-01-14T14:30:00Z" + "query_time": "2025-01-14T14:30:00Z", + "timezone": "Europe/Berlin" } ``` @@ -123,12 +125,12 @@ Healthcheck Endpoint für den API Container. ### 4.3 Technologie-Stack (API) - **Framework:** FastAPI - **Templating:** Jinja2 (server-side rendering) -- **DB-Zugriff:** mysql-connector-python (gleicher Connection-Pool wie Sync) +- **DB-Zugriff:** mysql-connector-python (shared `get_connection()` aus `api/database.py`) - **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) +- `TIMEZONE` - Zeitzone für die Anzeige von Zeiten (default: UTC) - `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` - Identisch zum Sync ## 5. Docker Setup @@ -174,6 +176,9 @@ services: - WINDOW_FUTURE_DAYS=${WINDOW_FUTURE_DAYS:-365} - LOG_LEVEL=${LOG_LEVEL:-INFO} - HEALTHCHECK_URL=${HEALTHCHECK_URL:-} + - DB_BOOTSTRAP=${DB_BOOTSTRAP:-false} + - DB_ROOT_USER=${DB_ROOT_USER:-} + - DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-} networks: - docker-backend @@ -191,6 +196,7 @@ services: - DB_USER=${DB_USER} - DB_PASSWORD=${DB_PASSWORD} - LOG_LEVEL=${LOG_LEVEL:-INFO} + - TIMEZONE=${TIMEZONE:-UTC} labels: - "com.centurylinklabs.watchtower.enable=true" networks: @@ -218,6 +224,8 @@ networks: ├── mariadb-setup.sql # Manuelles DB-Setup Script ├── .env.example # Beispiel-Umgebungsvariablen (erweitert) ├── SPEC.md # Diese Spezifikation +├── PLAN.md # Implementierungsplan +├── AGENTS.md # Richtlinien für AI-Agenten └── .github/workflows/ # CI/CD (Docker Build + Push) ``` @@ -269,4 +277,4 @@ Bestehender GitHub Actions Workflow erweitern: ## 11. Future Enhancements (nicht im Scope) - [ ] Kalender-Filter UI (nach calendar_label) -- [ ] Suchfunktion nach Event-Titel +- [x] Suchfunktion nach Event-Titel (implementiert) diff --git a/api/main.py b/api/main.py index a62071f..5a1e811 100644 --- a/api/main.py +++ b/api/main.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from pathlib import Path from zoneinfo import ZoneInfo -from fastapi import FastAPI, HTTPException, Query +from fastapi import FastAPI, HTTPException, Query, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from pydantic import BaseModel @@ -76,25 +76,26 @@ def row_to_event(row) -> EventResponse: ) -def fetch_events(limit: int = 10, search: str | None = None) -> list[dict]: +def fetch_events(limit: int = 10, search: str | None = None, calendar_label: str | None = None) -> list[dict]: conn = get_connection() try: cur = conn.cursor() try: + conditions = ["deleted = 0", "start_at >= NOW()"] + params: list = [] if search: - cur.execute( - f"SELECT {SELECT_COLUMNS} FROM calendar_events " - "WHERE deleted = 0 AND start_at >= NOW() AND summary LIKE %s " - "ORDER BY start_at ASC LIMIT %s", - (f"%{search}%", limit), - ) - else: - cur.execute( - f"SELECT {SELECT_COLUMNS} FROM calendar_events " - "WHERE deleted = 0 AND start_at >= NOW() " - "ORDER BY start_at ASC LIMIT %s", - (limit,), - ) + conditions.append("summary LIKE %s") + params.append(f"%{search}%") + if calendar_label: + conditions.append("calendar_label = %s") + params.append(calendar_label) + params.append(limit) + where = " WHERE " + " AND ".join(conditions) + cur.execute( + f"SELECT {SELECT_COLUMNS} FROM calendar_events " + f"{where} ORDER BY start_at ASC LIMIT %s", + tuple(params), + ) return cur.fetchall() finally: cur.close() @@ -111,9 +112,10 @@ def health(): def get_events( limit: int = Query(default=10, ge=1, le=50), search: str | None = Query(default=None), + calendar_label: str | None = Query(default=None), ): try: - rows = fetch_events(limit=limit, search=search) + rows = fetch_events(limit=limit, search=search, calendar_label=calendar_label) except Exception: log.exception("DB-Fehler bei /api/events") raise HTTPException(status_code=500, detail="Database error") @@ -156,6 +158,7 @@ def get_event(event_id: int): @app.get("/", response_class=HTMLResponse) def index( + request: Request, search: str | None = Query(default=None), limit: int = Query(default=10, ge=1, le=50), ): @@ -181,5 +184,5 @@ def index( return templates.TemplateResponse( "index.html", - {"request": {}, "events": events, "search": search or ""}, + {"request": request, "events": events, "search": search or ""}, ) diff --git a/docker-compose.yml b/docker-compose.yml index 4245c52..6277222 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,9 @@ services: - WINDOW_FUTURE_DAYS=${WINDOW_FUTURE_DAYS:-365} - LOG_LEVEL=${LOG_LEVEL:-INFO} - HEALTHCHECK_URL=${HEALTHCHECK_URL:-} + - DB_BOOTSTRAP=${DB_BOOTSTRAP:-false} + - DB_ROOT_USER=${DB_ROOT_USER:-} + - DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-} networks: - docker-backend