diff --git a/config.yaml b/config.yaml index 2765f62..b7ee17c 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ google_sheet: sheet_id: "15_SuNjWruwHcX4_R5OZPYb8Zwa__DrQ56ZcAodPj7QQ" - gid_times: "0" # Meistens 0 für das erste Blatt - gid_remarks: "92621051" # Beispiel-ID deines Bemerkungen-Blatts + gid_times: "635683780" + gid_remarks: "1437056802" date_column: "Datum" processing: diff --git a/core/data_processor.py b/core/data_processor.py index 2eab065..02039ed 100644 --- a/core/data_processor.py +++ b/core/data_processor.py @@ -1,66 +1,83 @@ import pandas as pd import requests +import markdown from io import StringIO from datetime import datetime, timedelta from .config_loader import config -_cache = {"events": None, "remarks": None} +_cache = {"events": None, "remarks": None, "timestamp": None} HEADERS = {"User-Agent": "Mozilla/5.0"} def invalidate_cache(): global _cache - _cache = {"events": None, "remarks": None} - print("DEBUG: Cache wurde manuell geleert.") + _cache = {"events": None, "remarks": None, "timestamp": None} return "Cache gelöscht" -def get_upcoming_events(): - if _cache["events"] is not None: - return _cache["events"] - - url = config['links']['times_csv'] - print(f"DEBUG: Lade Zeiten von URL: {url}") - - response = requests.get(url, headers=HEADERS) - df = pd.read_csv(StringIO(response.text)) - - # Debug: Spaltennamen prüfen - print(f"DEBUG: Spalten in Zeiten-Sheet gefunden: {df.columns.tolist()}") +def _is_cache_valid(): + if _cache["timestamp"] is None: + return False + return (datetime.now() - _cache["timestamp"]) < timedelta(hours=1) + +def get_upcoming_events(days_to_show=None, limit=None): + # Sofort Standardwert aus Config setzen, falls None oder 0 + if not days_to_show: + days_to_show = config['processing']['days_to_show'] + + # 1. Daten laden (entweder aus Cache oder von Google) + if not _is_cache_valid() or _cache["events"] is None: + url = config['links']['times_csv'] + response = requests.get(url, headers=HEADERS) + response.raise_for_status() + response.encoding = 'utf-8' + + df = pd.read_csv(StringIO(response.text)) + + # Typografie: Bindestrich durch En-Dash (–) ersetzen + for col in ['Morgen', 'Nachmittag']: + df[col] = df[col].fillna('').astype(str).str.replace('-', '–', regex=False) + + date_col = config['google_sheet']['date_column'] + df = df.dropna(subset=[date_col]) + df[date_col] = pd.to_datetime(df[date_col], dayfirst=True, errors='coerce') + + wt_map = {0: 'Mo', 1: 'Di', 2: 'Mi', 3: 'Do', 4: 'Fr', 5: 'Sa', 6: 'So'} + df['Wochentag'] = df[date_col].dt.weekday.map(wt_map) + + _cache["events"] = df.sort_values(by=date_col).to_dict(orient='records') + _cache["timestamp"] = datetime.now() - date_col = config['google_sheet']['date_column'] - df = df.dropna(subset=[date_col]) - df[date_col] = pd.to_datetime(df[date_col], dayfirst=True, errors='coerce') - df = df.dropna(subset=[date_col]).fillna('') - heute = pd.Timestamp(datetime.now().date()) - ende = heute + timedelta(days=config['processing']['days_to_show']) + date_col = config['google_sheet']['date_column'] + + # PRIORITÄT 1: Zeilen-Limit (gewinnt immer) + if limit and limit > 0: + return [e for e in _cache["events"] if e[date_col] >= heute][:limit] - mask = (df[date_col] >= heute) & (df[date_col] <= ende) - gefilterte = df.loc[mask].sort_values(by=date_col).copy() - - wt_map = {0: 'Mo', 1: 'Di', 2: 'Mi', 3: 'Do', 4: 'Fr', 5: 'Sa', 6: 'So'} - gefilterte['Wochentag'] = gefilterte[date_col].dt.weekday.map(wt_map) - - _cache["events"] = gefilterte.to_dict(orient='records') - return _cache["events"] + # PRIORITÄT 2: Tage-Logik + ende = heute + timedelta(days=int(days_to_show)) + return [e for e in _cache["events"] if heute <= e[date_col] <= ende] def get_remarks(): - if _cache["remarks"] is not None: + if _is_cache_valid() and _cache["remarks"] is not None: return _cache["remarks"] url = config['links']['remarks_csv'] - print(f"DEBUG: Lade Bemerkungen von URL: {url}") - response = requests.get(url, headers=HEADERS) - - # Debug: Die ersten 100 Zeichen der Antwort sehen - print(f"DEBUG: Raw Data Bemerkungen (Anfang): {response.text[:100]}...") + response.raise_for_status() + response.encoding = 'utf-8' df = pd.read_csv(StringIO(response.text), skiprows=2, header=None) if not df.empty: - print(f"DEBUG: Bemerkungen-DF Head:\n{df.head(3)}") - remarks = df[0].dropna().astype(str).tolist() - _cache["remarks"] = [r.strip() for r in remarks if r.strip()] + raw_remarks = df[0].dropna().astype(str).tolist() + processed = [] + for r in raw_remarks: + html = markdown.markdown(r.strip()) + if html.startswith("
") and html.endswith("
"): + html = html[3:-4] + processed.append(html) + _cache["remarks"] = processed + _cache["timestamp"] = datetime.now() else: _cache["remarks"] = [] diff --git a/main.py b/main.py index 2c24f1c..cc9ec27 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from routers.web_routes import router as web_router import uvicorn from core.config_loader import config @@ -7,6 +8,7 @@ app = FastAPI() # Router einbinden app.include_router(web_router) +app.mount("/static", StaticFiles(directory="static"), name="static") if __name__ == "__main__": uvicorn.run(app, host=config['server']['host'], port=config['server']['port']) diff --git a/requirements.txt b/requirements.txt index 39862c1..93f5e1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ pyyaml fastapi uvicorn jinja2 -python-multipart \ No newline at end of file +python-multipart +markdown \ No newline at end of file diff --git a/routers/web_routes.py b/routers/web_routes.py index a889ce0..d72763f 100644 --- a/routers/web_routes.py +++ b/routers/web_routes.py @@ -2,31 +2,39 @@ from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from core.data_processor import get_upcoming_events, get_remarks, invalidate_cache +from core.config_loader import config from pathlib import Path router = APIRouter() BASE_DIR = Path(__file__).parent.parent templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) -# Admin auf Root -@router.get("/", response_class=HTMLResponse) -async def admin_page(request: Request): - from core.config_loader import config - return templates.TemplateResponse("admin.html", { - "request": request, "links": config['links'] +@router.get("/zeiten", response_class=HTMLResponse) +async def public_table(request: Request, days: int = None, lines: int = None, test: bool = False): + # Wenn nichts angegeben wurde, greift der Standard aus der Config + if not days and not lines: + days = config['processing']['days_to_show'] + + events = get_upcoming_events(days_to_show=days, limit=lines) + remarks = get_remarks() + + # Template 'zeiten.html' laden und 'test' Parameter durchreichen + return templates.TemplateResponse("zeiten.html", { + "request": request, + "events": events, + "remarks": remarks, + "test": test }) -# Tabelle unter /zeiten -@router.get("/zeiten", response_class=HTMLResponse) -async def times_table(request: Request): - events = get_upcoming_events() - remarks = get_remarks() - return templates.TemplateResponse("index.html", { - "request": request, "events": events, "remarks": remarks +@router.get("/", response_class=HTMLResponse) +async def admin_page(request: Request): + return templates.TemplateResponse("admin.html", { + "request": request, + "links": config['links'], + "config_days": config['processing']['days_to_show'] }) @router.get("/cache-clear") async def clear_cache(): invalidate_cache() - # Relativer Redirect zurück zum Admin-Interface - return RedirectResponse(url="./") \ No newline at end of file + return RedirectResponse(url="/") \ No newline at end of file diff --git a/static/BrittanySignature.ttf b/static/BrittanySignature.ttf new file mode 100644 index 0000000..36a273d Binary files /dev/null and b/static/BrittanySignature.ttf differ diff --git a/static/BrittanySignature.woff2 b/static/BrittanySignature.woff2 new file mode 100644 index 0000000..21fcb64 Binary files /dev/null and b/static/BrittanySignature.woff2 differ diff --git a/templates/admin.html b/templates/admin.html index 396e1af..45d372f 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -1,11 +1,39 @@ -➔ Tabelle: Öffnungszeiten bearbeiten
➔ Tabelle: Bemerkungen bearbeiten
+ ++ Zeilen hat Vorrang vor Tagen. Wenn beide Werte 0 sind, werden die Standard-Tage aus der Config genutzt (aktuell: {{ config_days }}). +
+ +Nach Änderungen in Google Sheets muss der Cache gelöscht werden, damit die Website sofort aktualisiert wird.
- Invalidate Cache (Cache löschen) +Der Cache wird automatisch alle 60 Minuten aktualisiert. Nach manuellen Änderungen in Google Sheets können Sie ihn hier sofort leeren.
+ Cache jetzt löschen