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:
30
app.py
30
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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user