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):**
- `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://`.
---
@@ -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
- **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:**
```bash

20
app.py
View File

@@ -18,7 +18,7 @@ from kantine2ical import BASE_URL, empty_ical_bytes, refresh_speiseplan
# Konfiguration (Umgebungsvariablen mit Fallback)
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
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)
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 ...")
_do_refresh()
_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)
@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")
def calendar_ics() -> Response:
"""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 io
import logging
import re
from datetime import date, datetime, time
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)
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]:
@@ -177,7 +179,9 @@ def refresh_speiseplan(base_url: str = BASE_URL) -> tuple[dict[date, list[str]],
try:
urls = fetch_speiseplan_pdf_urls(base_url)
if not urls:
_log.warning("refresh_speiseplan: Keine PDF-URLs gefunden auf %s", base_url)
return None
_log.info("refresh_speiseplan: %d PDF(s) gefunden", len(urls))
all_parsed: list[list[tuple[date, list[str]]]] = []
for url in urls:
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)
days = parse_speiseplan_text(text)
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
if not all_parsed:
_log.warning("refresh_speiseplan: Kein PDF erfolgreich gelesen")
return None
by_date = merge_day_events(all_parsed)
ical_bytes = build_ical_bytes(by_date)
_log.info("refresh_speiseplan: %d Termine im Kalender", len(by_date))
return (by_date, ical_bytes)
except Exception:
except Exception as e:
_log.exception("refresh_speiseplan: Ablauf fehlgeschlagen: %s", e)
return None