Refresh-Endpoint, Logging mit Timestamp, 12h-Intervall, PDF-Fehler-Logs

This commit is contained in:
2026-01-31 15:47:01 +01:00
parent c13f07d98c
commit 5cf75a79bc
3 changed files with 33 additions and 5 deletions

View File

@@ -81,7 +81,7 @@ Die eigentliche iCal-Datei für Abos und direkten Download ist unter `/calendar.
**Konfiguration (optional, Umgebungsvariablen):** **Konfiguration (optional, Umgebungsvariablen):**
- `KANTINE_BASE_URL` Basis-URL der Kantine (Standard: `http://kantine-bhz.de`) - `KANTINE_BASE_URL` Basis-URL der Kantine (Standard: `http://kantine-bhz.de`)
- `REFRESH_INTERVAL_SECONDS` Sekunden zwischen Aktualisierungen (Standard: 86400 = 24 h) - `REFRESH_INTERVAL_SECONDS` Sekunden zwischen Aktualisierungen (Standard: 43200 = 12 h)
- `PUBLIC_URL` Öffentliche Basis-URL (z. B. `https://kantine.elpatron.me`). Wenn gesetzt, wird diese URL für die Kalender-Abo-URL auf der Startseite verwendet. **Empfohlen hinter HTTPS-Proxy**, falls der Proxy keine `X-Forwarded-Proto`/`X-Forwarded-Host`-Header sendet sonst erscheint dort weiterhin `http://`. - `PUBLIC_URL` Öffentliche Basis-URL (z. B. `https://kantine.elpatron.me`). Wenn gesetzt, wird diese URL für die Kalender-Abo-URL auf der Startseite verwendet. **Empfohlen hinter HTTPS-Proxy**, falls der Proxy keine `X-Forwarded-Proto`/`X-Forwarded-Host`-Header sendet sonst erscheint dort weiterhin `http://`.
--- ---
@@ -99,6 +99,9 @@ docker run -p 8000:8000 -e PUBLIC_URL=https://kantine.elpatron.me kantine2ical
- **Startseite** (Anleitung + Abo-URL): `http://<host>:8000/` bzw. Ihre HTTPS-URL - **Startseite** (Anleitung + Abo-URL): `http://<host>:8000/` bzw. Ihre HTTPS-URL
- **iCal-Abo:** `http://<host>:8000/calendar.ics` bzw. `https://<host>/calendar.ics` - **iCal-Abo:** `http://<host>:8000/calendar.ics` bzw. `https://<host>/calendar.ics`
- **Manueller Refresh:** `GET /refresh` (z. B. `https://<host>/refresh`) löst sofort einen Abruf der Speiseplan-PDFs aus. Nützlich, wenn auf [kantine-bhz.de](http://kantine-bhz.de) ein neuer Plan liegt und der 24h-Refresh noch nicht gelaufen ist. Nach dem Aufruf enthält der Kalender die aktuellen Daten.
**Kalender wird nicht aktualisiert?** Container-Logs prüfen (`docker logs <container>`): Dort erscheinen gefundene PDFs und Fehler beim Laden/Parsen. Bei fehlgeschlagenem PDF wird die URL und die Exception geloggt.
**Mit Docker Compose:** **Mit Docker Compose:**
```bash ```bash

20
app.py
View File

@@ -18,7 +18,7 @@ from kantine2ical import BASE_URL, empty_ical_bytes, refresh_speiseplan
# Konfiguration (Umgebungsvariablen mit Fallback) # Konfiguration (Umgebungsvariablen mit Fallback)
KANTINE_BASE_URL = os.environ.get("KANTINE_BASE_URL", BASE_URL) KANTINE_BASE_URL = os.environ.get("KANTINE_BASE_URL", BASE_URL)
REFRESH_INTERVAL_SECONDS = int(os.environ.get("REFRESH_INTERVAL_SECONDS", "86400")) # 24h REFRESH_INTERVAL_SECONDS = int(os.environ.get("REFRESH_INTERVAL_SECONDS", "43200")) # 12h
# Öffentliche Basis-URL (z. B. https://kantine.elpatron.me), wenn Proxy keine X-Forwarded-* sendet # Öffentliche Basis-URL (z. B. https://kantine.elpatron.me), wenn Proxy keine X-Forwarded-* sendet
PUBLIC_URL = os.environ.get("PUBLIC_URL", "").strip().rstrip("/") PUBLIC_URL = os.environ.get("PUBLIC_URL", "").strip().rstrip("/")
@@ -60,7 +60,11 @@ def _refresh_loop() -> None:
# Beim Import: einmal Refresh, Hintergrund-Thread starten (gilt auch für Gunicorn) # Beim Import: einmal Refresh, Hintergrund-Thread starten (gilt auch für Gunicorn)
logging.basicConfig(level=logging.INFO) logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
_log.info("Starte Speiseplan-Refresh beim Start ...") _log.info("Starte Speiseplan-Refresh beim Start ...")
_do_refresh() _do_refresh()
_refresh_thread = threading.Thread(target=_refresh_loop, daemon=True) _refresh_thread = threading.Thread(target=_refresh_loop, daemon=True)
@@ -68,6 +72,18 @@ _refresh_thread.start()
_log.info("Hintergrund-Refresh alle %s Sekunden.", REFRESH_INTERVAL_SECONDS) _log.info("Hintergrund-Refresh alle %s Sekunden.", REFRESH_INTERVAL_SECONDS)
@app.route("/refresh")
def refresh() -> Response:
"""Manuellen Refresh auslösen (z. B. nach neuem Speiseplan auf kantine-bhz.de)."""
_log.info("Manueller Refresh ausgelöst")
_do_refresh()
last = _format_last_refresh()
return Response(
f"Refresh ausgeführt. Zuletzt aktualisiert: {last or ''}.\n",
mimetype="text/plain; charset=utf-8",
)
@app.route("/calendar.ics") @app.route("/calendar.ics")
def calendar_ics() -> Response: def calendar_ics() -> Response:
"""iCal-Kalender ausliefern (für Abo-URL z. B. in Google Kalender).""" """iCal-Kalender ausliefern (für Abo-URL z. B. in Google Kalender)."""

View File

@@ -6,6 +6,7 @@ Termine täglich 12:00 mit allen Tagesgerichten (I. bis V.).
import argparse import argparse
import io import io
import logging
import re import re
from datetime import date, datetime, time from datetime import date, datetime, time
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -33,6 +34,7 @@ DATE_LINE_RE = re.compile(
DISH_LINE_RE = re.compile(r"^\s*(IV\.?|V\.?|I{1,3}\.?)\s*(.*)$", re.IGNORECASE) DISH_LINE_RE = re.compile(r"^\s*(IV\.?|V\.?|I{1,3}\.?)\s*(.*)$", re.IGNORECASE)
ROMAN_ORDER = {"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5} ROMAN_ORDER = {"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5}
_log = logging.getLogger(__name__)
def fetch_speiseplan_pdf_urls(base_url: str = BASE_URL) -> list[str]: def fetch_speiseplan_pdf_urls(base_url: str = BASE_URL) -> list[str]:
@@ -177,7 +179,9 @@ def refresh_speiseplan(base_url: str = BASE_URL) -> tuple[dict[date, list[str]],
try: try:
urls = fetch_speiseplan_pdf_urls(base_url) urls = fetch_speiseplan_pdf_urls(base_url)
if not urls: if not urls:
_log.warning("refresh_speiseplan: Keine PDF-URLs gefunden auf %s", base_url)
return None return None
_log.info("refresh_speiseplan: %d PDF(s) gefunden", len(urls))
all_parsed: list[list[tuple[date, list[str]]]] = [] all_parsed: list[list[tuple[date, list[str]]]] = []
for url in urls: for url in urls:
try: try:
@@ -185,14 +189,19 @@ def refresh_speiseplan(base_url: str = BASE_URL) -> tuple[dict[date, list[str]],
text = extract_text_from_pdf(pdf_bytes) text = extract_text_from_pdf(pdf_bytes)
days = parse_speiseplan_text(text) days = parse_speiseplan_text(text)
all_parsed.append(days) all_parsed.append(days)
except Exception: _log.info("refresh_speiseplan: %s -> %d Tage", url.split("/")[-1], len(days))
except Exception as e:
_log.exception("refresh_speiseplan: PDF fehlgeschlagen %s: %s", url, e)
continue continue
if not all_parsed: if not all_parsed:
_log.warning("refresh_speiseplan: Kein PDF erfolgreich gelesen")
return None return None
by_date = merge_day_events(all_parsed) by_date = merge_day_events(all_parsed)
ical_bytes = build_ical_bytes(by_date) ical_bytes = build_ical_bytes(by_date)
_log.info("refresh_speiseplan: %d Termine im Kalender", len(by_date))
return (by_date, ical_bytes) return (by_date, ical_bytes)
except Exception: except Exception as e:
_log.exception("refresh_speiseplan: Ablauf fehlgeschlagen: %s", e)
return None return None