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 os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from flask import Flask, Response, render_template, request
|
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)
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
|
||||||
_log = logging.getLogger(__name__)
|
_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()
|
_ical_cache: bytes = empty_ical_bytes()
|
||||||
|
_by_date_cache: dict[date, list[str]] | None = None
|
||||||
_last_refresh_at: datetime | None = None
|
_last_refresh_at: datetime | None = None
|
||||||
_cache_lock = threading.Lock()
|
_cache_lock = threading.Lock()
|
||||||
_TIMEZONE = ZoneInfo("Europe/Berlin")
|
_TIMEZONE = ZoneInfo("Europe/Berlin")
|
||||||
|
_WEEKDAY_NAMES = ("Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag")
|
||||||
|
|
||||||
def _do_refresh() -> None:
|
def _do_refresh() -> None:
|
||||||
"""Refresh ausführen und bei Erfolg Cache aktualisieren."""
|
"""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)
|
result = refresh_speiseplan(KANTINE_BASE_URL)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
_, ical_bytes = result
|
by_date, ical_bytes = result
|
||||||
with _cache_lock:
|
with _cache_lock:
|
||||||
_ical_cache = ical_bytes
|
_ical_cache = ical_bytes
|
||||||
|
_by_date_cache = by_date
|
||||||
_last_refresh_at = datetime.now(_TIMEZONE)
|
_last_refresh_at = datetime.now(_TIMEZONE)
|
||||||
_log.info("Speiseplan-Refresh erfolgreich, Cache aktualisiert.")
|
_log.info("Speiseplan-Refresh erfolgreich, Cache aktualisiert.")
|
||||||
else:
|
else:
|
||||||
@@ -105,17 +108,34 @@ def _format_last_refresh() -> str | None:
|
|||||||
return t.strftime("%d.%m.%Y, %H:%M") + " Uhr"
|
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("/")
|
@app.route("/")
|
||||||
def index():
|
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("/")
|
base = PUBLIC_URL or request.url_root.rstrip("/")
|
||||||
calendar_url = f"{base}/calendar.ics"
|
calendar_url = f"{base}/calendar.ics"
|
||||||
last_refresh_str = _format_last_refresh()
|
last_refresh_str = _format_last_refresh()
|
||||||
|
upcoming_days = _upcoming_days()
|
||||||
try:
|
try:
|
||||||
return render_template(
|
return render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
calendar_url=calendar_url,
|
calendar_url=calendar_url,
|
||||||
last_refresh_str=last_refresh_str,
|
last_refresh_str=last_refresh_str,
|
||||||
|
upcoming_days=upcoming_days,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_log.exception("Template index.html: %s", e)
|
_log.exception("Template index.html: %s", e)
|
||||||
|
|||||||
@@ -195,6 +195,18 @@
|
|||||||
color: var(--text-muted);
|
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 {
|
footer a {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -203,6 +215,56 @@
|
|||||||
footer a:hover {
|
footer a:hover {
|
||||||
text-decoration: underline;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -217,6 +279,26 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</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">
|
<section class="card">
|
||||||
<h2><span class="num">1</span> Abo-URL kopieren</h2>
|
<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>
|
<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>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user