From 073ec27fc7e27cc29612a6618b470e7ee742c3a0 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sun, 15 Feb 2026 11:18:50 +0100 Subject: [PATCH] =?UTF-8?q?Speiseplan-Kacheln=20(zuk=C3=BCnftige=20Tage),?= =?UTF-8?q?=20Footer=20mit=20Credit=20(No=20tracking,=20Markus=20F.J.=20Bu?= =?UTF-8?q?sche)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app.py | 30 +++++++++++++--- templates/index.html | 85 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index 0a5ca8a..ff4e0d9 100644 --- a/app.py +++ b/app.py @@ -8,7 +8,7 @@ import logging import os import threading import time -from datetime import datetime +from datetime import date, datetime from zoneinfo import ZoneInfo from flask import Flask, Response, render_template, request @@ -29,20 +29,23 @@ app = Flask(__name__, template_folder=_template_dir) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) _log = logging.getLogger(__name__) -# Cache: zuletzt gültige iCal-Bytes; Zeitpunkt der letzten Aktualisierung +# Cache: zuletzt gültige iCal-Bytes; strukturierte Daten (Datum → Gerichte); Zeitpunkt der letzten Aktualisierung _ical_cache: bytes = empty_ical_bytes() +_by_date_cache: dict[date, list[str]] | None = None _last_refresh_at: datetime | None = None _cache_lock = threading.Lock() _TIMEZONE = ZoneInfo("Europe/Berlin") +_WEEKDAY_NAMES = ("Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag") def _do_refresh() -> None: """Refresh ausführen und bei Erfolg Cache aktualisieren.""" - global _ical_cache, _last_refresh_at + global _ical_cache, _by_date_cache, _last_refresh_at result = refresh_speiseplan(KANTINE_BASE_URL) if result is not None: - _, ical_bytes = result + by_date, ical_bytes = result with _cache_lock: _ical_cache = ical_bytes + _by_date_cache = by_date _last_refresh_at = datetime.now(_TIMEZONE) _log.info("Speiseplan-Refresh erfolgreich, Cache aktualisiert.") else: @@ -105,17 +108,34 @@ def _format_last_refresh() -> str | None: return t.strftime("%d.%m.%Y, %H:%M") + " Uhr" +def _upcoming_days() -> list[tuple[str, list[str]]]: + """Heute und zukünftige Tage aus dem Cache, sortiert, mit formatiertem Datum (z. B. Montag, 03.02.2026).""" + with _cache_lock: + by_date = _by_date_cache + if not by_date: + return [] + today = datetime.now(_TIMEZONE).date() + upcoming = [(d, dishes) for d, dishes in by_date.items() if d >= today] + upcoming.sort(key=lambda x: x[0]) + return [ + (f"{_WEEKDAY_NAMES[d.weekday()]}, {d.strftime('%d.%m.%Y')}", dishes) + for d, dishes in upcoming + ] + + @app.route("/") def index(): - """Startseite mit Anleitung zur iCal-Einbettung (Google und andere).""" + """Startseite mit Anleitung zur iCal-Einbettung (Google und andere) und Speiseplan-Kacheln.""" base = PUBLIC_URL or request.url_root.rstrip("/") calendar_url = f"{base}/calendar.ics" last_refresh_str = _format_last_refresh() + upcoming_days = _upcoming_days() try: return render_template( "index.html", calendar_url=calendar_url, last_refresh_str=last_refresh_str, + upcoming_days=upcoming_days, ) except Exception as e: _log.exception("Template index.html: %s", e) diff --git a/templates/index.html b/templates/index.html index ffce653..1107c0f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -195,6 +195,18 @@ color: var(--text-muted); } + footer p { + margin: 0 0 0.5rem; + } + + footer p:last-child { + margin-bottom: 0; + } + + .footer-credit { + margin-top: 0.75rem; + } + footer a { color: var(--accent); text-decoration: none; @@ -203,6 +215,56 @@ footer a:hover { text-decoration: underline; } + + .speiseplan-section { + margin-bottom: 2.5rem; + } + + .speiseplan-section h2 { + font-size: 1.1rem; + font-weight: 600; + margin: 0 0 1rem; + } + + .speiseplan-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + } + + .day-tile { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem 1.5rem; + } + + .day-tile h3 { + font-size: 1rem; + font-weight: 600; + margin: 0 0 0.75rem; + color: var(--accent); + } + + .day-tile .dishes { + font-size: 0.9rem; + color: var(--text-muted); + line-height: 1.5; + } + + .day-tile .dishes div { + margin-bottom: 0.35rem; + } + + .day-tile .dishes div:last-child { + margin-bottom: 0; + } + + .speiseplan-empty { + color: var(--text-muted); + font-size: 0.95rem; + margin: 0; + } @@ -217,6 +279,26 @@ {% endif %} +
+

Speiseplan (zukünftige Tage)

+ {% if upcoming_days %} +
+ {% for date_str, dishes in upcoming_days %} +
+

{{ date_str }}

+
+ {% for dish in dishes %} +
{{ dish }}
+ {% endfor %} +
+
+ {% endfor %} +
+ {% else %} +

Es liegen derzeit (noch) keine neuen Speisepläne vor.

+ {% endif %} +
+

1 Abo-URL kopieren

Diese URL in Ihrem Kalender als „Abonnement“ oder „Von URL hinzufügen“ eintragen:

@@ -259,7 +341,8 @@