mirror of
https://github.com/skoelle/calender_sync.git
synced 2026-09-17 18:20:24 +00:00
Fix: korrigiere Code-Bugs und Dokumentation
- api/main.py: index() Route übernimmt Request-Objekt korrekt (statt {}),
calendar_label Query-Parameter implementiert (dynamische WHERE-Klausel)
- docker-compose.yml: DB_BOOTSTRAP/DB_ROOT_USER/DB_ROOT_PASSWORD Variablen
für calendar-sync Service hinzugefügt (waren dokumentiert, aber nie übergeben)
- .env.example: DB_BOOTSTRAP, API_PORT, TIMEZONE hinzugefügt
- AGENTS.md: Zeilennummer sync.py:91→83, gemischte Deutsch/China-Sprache
bereinigt
- SPEC.md: --entrypoint→command, API_HOST entfernt (nicht implementiert),
TIMEZONE hinzugefügt, JSON-Beispiele um timezone-Feld erweitert,
Docker-Compose-Beispiel und Projektstruktur aktualisiert,
Search als implementiert markiert
- README.md: TIMEZONE und API_PORT in Konfigtationstabelle,
timezone im JSON-Beispiel
This commit is contained in:
@@ -16,3 +16,14 @@ LOG_LEVEL=INFO
|
|||||||
|
|
||||||
# Optional: Healthchecks.io / Uptime Kuma URL (wird nach jedem Sync gepingt)
|
# Optional: Healthchecks.io / Uptime Kuma URL (wird nach jedem Sync gepingt)
|
||||||
# HEALTHCHECK_URL=https://hc-ping.com/DEINE_UUID
|
# 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
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Die API (`api/main.py`) ist eine separarte FastAPI-App die als eigenständiger C
|
|||||||
|
|
||||||
### Datenbank-Schema
|
### Datenbank-Schema
|
||||||
Schema wird in `ensure_schema()` per `CREATE TABLE IF NOT EXISTS` erstellt. Bei Schema-Änderungen:
|
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)
|
- MariaDB-kompatibles SQL verwenden (kein PostgreSQL-Specific)
|
||||||
- Indexe für Performance bedenken
|
- 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.
|
Verwendet `recurring_ical_events` Bibliothek für RRULE/EXDATE/RECURRENCE-ID Expansion. Fenster wird über `WINDOW_PAST_DAYS`/`WINDOW_FUTURE_DAYS` gesteuert.
|
||||||
|
|
||||||
### UTC-Normalisierung
|
### 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
|
### Soft-Delete
|
||||||
Events werden nicht gelöscht, sondern mit `deleted=1` markiert (`mark_missing_as_deleted()`).
|
Events werden nicht gelöscht, sondern mit `deleted=1` markiert (`mark_missing_as_deleted()`).
|
||||||
|
|||||||
@@ -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_BOOTSTRAP` | `false` | DB + User beim Start erstellen |
|
||||||
| `DB_ROOT_USER` | - | Root-User fürs Bootstrap |
|
| `DB_ROOT_USER` | - | Root-User fürs Bootstrap |
|
||||||
| `DB_ROOT_PASSWORD` | - | Root-Passwort 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
|
## Datenbank-Schema
|
||||||
|
|
||||||
@@ -132,12 +134,14 @@ curl http://localhost:8000/api/events/42
|
|||||||
"location": "Konferenzraum 1",
|
"location": "Konferenzraum 1",
|
||||||
"start_at": "2025-01-15T10:00:00",
|
"start_at": "2025-01-15T10:00:00",
|
||||||
"end_at": "2025-01-15T11:00:00",
|
"end_at": "2025-01-15T11:00:00",
|
||||||
"all_day": false,
|
"all_day": false,
|
||||||
"status": "CONFIRMED"
|
"status": "CONFIRMED",
|
||||||
}
|
"timezone": "Europe/Berlin"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"query_time": "2025-01-15T09:30:00Z"
|
"query_time": "2025-01-15T09:30:00Z",
|
||||||
|
"timezone": "Europe/Berlin"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
## 3. Bestehendes System (Sync Tool)
|
||||||
|
|
||||||
@@ -89,12 +89,14 @@ Gibt die nächsten N Termine zurück.
|
|||||||
"location": "Raum 101",
|
"location": "Raum 101",
|
||||||
"start_at": "2025-01-15T10:00:00",
|
"start_at": "2025-01-15T10:00:00",
|
||||||
"end_at": "2025-01-15T11:00:00",
|
"end_at": "2025-01-15T11:00:00",
|
||||||
"all_day": false,
|
"all_day": false,
|
||||||
"status": "CONFIRMED"
|
"status": "CONFIRMED",
|
||||||
}
|
"timezone": "Europe/Berlin"
|
||||||
],
|
}
|
||||||
|
],
|
||||||
"count": 10,
|
"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)
|
### 4.3 Technologie-Stack (API)
|
||||||
- **Framework:** FastAPI
|
- **Framework:** FastAPI
|
||||||
- **Templating:** Jinja2 (server-side rendering)
|
- **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`)
|
- **Port:** 8000 (konfigurierbar via `API_PORT`)
|
||||||
|
|
||||||
### 4.4 Additional Environment Variablen (API)
|
### 4.4 Additional Environment Variablen (API)
|
||||||
- `API_PORT` - Port für den API Server (default: 8000)
|
- `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
|
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` - Identisch zum Sync
|
||||||
|
|
||||||
## 5. Docker Setup
|
## 5. Docker Setup
|
||||||
@@ -174,6 +176,9 @@ services:
|
|||||||
- WINDOW_FUTURE_DAYS=${WINDOW_FUTURE_DAYS:-365}
|
- WINDOW_FUTURE_DAYS=${WINDOW_FUTURE_DAYS:-365}
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||||
- HEALTHCHECK_URL=${HEALTHCHECK_URL:-}
|
- HEALTHCHECK_URL=${HEALTHCHECK_URL:-}
|
||||||
|
- DB_BOOTSTRAP=${DB_BOOTSTRAP:-false}
|
||||||
|
- DB_ROOT_USER=${DB_ROOT_USER:-}
|
||||||
|
- DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-}
|
||||||
networks:
|
networks:
|
||||||
- docker-backend
|
- docker-backend
|
||||||
|
|
||||||
@@ -191,6 +196,7 @@ services:
|
|||||||
- DB_USER=${DB_USER}
|
- DB_USER=${DB_USER}
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||||
|
- TIMEZONE=${TIMEZONE:-UTC}
|
||||||
labels:
|
labels:
|
||||||
- "com.centurylinklabs.watchtower.enable=true"
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
networks:
|
networks:
|
||||||
@@ -218,6 +224,8 @@ networks:
|
|||||||
├── mariadb-setup.sql # Manuelles DB-Setup Script
|
├── mariadb-setup.sql # Manuelles DB-Setup Script
|
||||||
├── .env.example # Beispiel-Umgebungsvariablen (erweitert)
|
├── .env.example # Beispiel-Umgebungsvariablen (erweitert)
|
||||||
├── SPEC.md # Diese Spezifikation
|
├── SPEC.md # Diese Spezifikation
|
||||||
|
├── PLAN.md # Implementierungsplan
|
||||||
|
├── AGENTS.md # Richtlinien für AI-Agenten
|
||||||
└── .github/workflows/ # CI/CD (Docker Build + Push)
|
└── .github/workflows/ # CI/CD (Docker Build + Push)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -269,4 +277,4 @@ Bestehender GitHub Actions Workflow erweitern:
|
|||||||
## 11. Future Enhancements (nicht im Scope)
|
## 11. Future Enhancements (nicht im Scope)
|
||||||
|
|
||||||
- [ ] Kalender-Filter UI (nach calendar_label)
|
- [ ] Kalender-Filter UI (nach calendar_label)
|
||||||
- [ ] Suchfunktion nach Event-Titel
|
- [x] Suchfunktion nach Event-Titel (implementiert)
|
||||||
|
|||||||
+20
-17
@@ -4,7 +4,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
from fastapi import FastAPI, HTTPException, Query, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from pydantic import BaseModel
|
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()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
try:
|
try:
|
||||||
|
conditions = ["deleted = 0", "start_at >= NOW()"]
|
||||||
|
params: list = []
|
||||||
if search:
|
if search:
|
||||||
cur.execute(
|
conditions.append("summary LIKE %s")
|
||||||
f"SELECT {SELECT_COLUMNS} FROM calendar_events "
|
params.append(f"%{search}%")
|
||||||
"WHERE deleted = 0 AND start_at >= NOW() AND summary LIKE %s "
|
if calendar_label:
|
||||||
"ORDER BY start_at ASC LIMIT %s",
|
conditions.append("calendar_label = %s")
|
||||||
(f"%{search}%", limit),
|
params.append(calendar_label)
|
||||||
)
|
params.append(limit)
|
||||||
else:
|
where = " WHERE " + " AND ".join(conditions)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"SELECT {SELECT_COLUMNS} FROM calendar_events "
|
f"SELECT {SELECT_COLUMNS} FROM calendar_events "
|
||||||
"WHERE deleted = 0 AND start_at >= NOW() "
|
f"{where} ORDER BY start_at ASC LIMIT %s",
|
||||||
"ORDER BY start_at ASC LIMIT %s",
|
tuple(params),
|
||||||
(limit,),
|
)
|
||||||
)
|
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
finally:
|
finally:
|
||||||
cur.close()
|
cur.close()
|
||||||
@@ -111,9 +112,10 @@ def health():
|
|||||||
def get_events(
|
def get_events(
|
||||||
limit: int = Query(default=10, ge=1, le=50),
|
limit: int = Query(default=10, ge=1, le=50),
|
||||||
search: str | None = Query(default=None),
|
search: str | None = Query(default=None),
|
||||||
|
calendar_label: str | None = Query(default=None),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
rows = fetch_events(limit=limit, search=search)
|
rows = fetch_events(limit=limit, search=search, calendar_label=calendar_label)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("DB-Fehler bei /api/events")
|
log.exception("DB-Fehler bei /api/events")
|
||||||
raise HTTPException(status_code=500, detail="Database error")
|
raise HTTPException(status_code=500, detail="Database error")
|
||||||
@@ -156,6 +158,7 @@ def get_event(event_id: int):
|
|||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
def index(
|
def index(
|
||||||
|
request: Request,
|
||||||
search: str | None = Query(default=None),
|
search: str | None = Query(default=None),
|
||||||
limit: int = Query(default=10, ge=1, le=50),
|
limit: int = Query(default=10, ge=1, le=50),
|
||||||
):
|
):
|
||||||
@@ -181,5 +184,5 @@ def index(
|
|||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"index.html",
|
"index.html",
|
||||||
{"request": {}, "events": events, "search": search or ""},
|
{"request": request, "events": events, "search": search or ""},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ services:
|
|||||||
- WINDOW_FUTURE_DAYS=${WINDOW_FUTURE_DAYS:-365}
|
- WINDOW_FUTURE_DAYS=${WINDOW_FUTURE_DAYS:-365}
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||||
- HEALTHCHECK_URL=${HEALTHCHECK_URL:-}
|
- HEALTHCHECK_URL=${HEALTHCHECK_URL:-}
|
||||||
|
- DB_BOOTSTRAP=${DB_BOOTSTRAP:-false}
|
||||||
|
- DB_ROOT_USER=${DB_ROOT_USER:-}
|
||||||
|
- DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- docker-backend
|
- docker-backend
|
||||||
|
|||||||
Reference in New Issue
Block a user