Speiseplan-Kacheln (zukünftige Tage), Footer mit Credit (No tracking, Markus F.J. Busche)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-15 11:18:50 +01:00
parent 5cf75a79bc
commit 073ec27fc7
2 changed files with 109 additions and 6 deletions

30
app.py
View File

@@ -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)

View File

@@ -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;
}
</style>
</head>
<body>
@@ -217,6 +279,26 @@
{% endif %}
</header>
<section class="speiseplan-section">
<h2>Speiseplan (zukünftige Tage)</h2>
{% if upcoming_days %}
<div class="speiseplan-grid">
{% for date_str, dishes in upcoming_days %}
<article class="day-tile">
<h3>{{ date_str }}</h3>
<div class="dishes">
{% for dish in dishes %}
<div>{{ dish }}</div>
{% endfor %}
</div>
</article>
{% endfor %}
</div>
{% else %}
<p class="speiseplan-empty">Es liegen derzeit (noch) keine neuen Speisepläne vor.</p>
{% endif %}
</section>
<section class="card">
<h2><span class="num">1</span> Abo-URL kopieren</h2>
<p style="margin: 0 0 1rem; font-size: 0.95rem; color: var(--text-muted);">Diese URL in Ihrem Kalender als „Abonnement“ oder „Von URL hinzufügen“ eintragen:</p>
@@ -259,7 +341,8 @@
</section>
<footer>
Quelle: <a href="http://kantine-bhz.de" target="_blank" rel="noopener">kantine-bhz.de</a>. Der Speiseplan wird einmal täglich aktualisiert. Direkter Kalender-Download: <a href="{{ calendar_url }}">calendar.ics</a>.
<p>Quelle: <a href="http://kantine-bhz.de" target="_blank" rel="noopener">kantine-bhz.de</a>. Der Speiseplan wird einmal täglich aktualisiert. Direkter Kalender-Download: <a href="{{ calendar_url }}">calendar.ics</a>.</p>
<p class="footer-credit">No tracking, no cookies. Made as a hobby project in 2026 by <a href="mailto:elpatron@mailbox.org">Markus F.J. Busche</a>.</p>
</footer>
</div>