UX: alarm-status, ablehnen-button, annika-footer, map polish

* Alarm-Status ist jetzt nur 'aktiv' wenn ein echter Push-Channel (Telegram
  mit Token+Chat oder E-Mail mit Adresse) konfiguriert ist. UI-only zählt
  nicht mehr als eingerichteter Alarm.
* Ablehnen-Button in der Wohnungsliste: flat_rejections (migration v4)
  speichert pro-User-Ablehnungen, abgelehnte Flats fallen aus Liste und
  Karte raus. Wiederholbar pro User unabhängig.
* Footer 'Programmiert für Annika ♥' erscheint nur auf Seiten, wenn annika
  angemeldet ist.
* Map: Hinweistext unter leerer Karte entfernt; alle Zoom-Mechanismen
  deaktiviert (Scrollrad, Doppelklick, Box, Touch, Tastatur, +/- Buttons).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Moritz 2026-04-21 12:09:44 +02:00
parent 376551213a
commit 42377f0b67
5 changed files with 97 additions and 18 deletions

View file

@ -217,16 +217,25 @@ def _has_filters(f) -> bool:
return False
def _alert_status(filters_row, notifications_row) -> tuple[str, str]:
"""Return (label, chip_kind) describing the user's alert setup."""
if not _has_filters(filters_row):
def _alert_status(notifications_row) -> tuple[str, str]:
"""Return (label, chip_kind) for the user's alarm (notification) setup.
'aktiv' only if a real push channel (telegram/email) is configured with
credentials. 'ui' is not a real alarm the dashboard already shows
matches when you happen to be looking.
"""
if not notifications_row:
return "nicht eingerichtet", "warn"
ch = (notifications_row["channel"] if notifications_row else "ui") or "ui"
if ch == "telegram" and not (notifications_row["telegram_bot_token"] and notifications_row["telegram_chat_id"]):
return "Benachrichtigung fehlt", "warn"
if ch == "email" and not notifications_row["email_address"]:
return "Benachrichtigung fehlt", "warn"
return "aktiv", "ok"
ch = (notifications_row["channel"] or "ui").strip()
if ch == "telegram":
if notifications_row["telegram_bot_token"] and notifications_row["telegram_chat_id"]:
return "aktiv (Telegram)", "ok"
return "unvollständig", "warn"
if ch == "email":
if notifications_row["email_address"]:
return "aktiv (E-Mail)", "ok"
return "unvollständig", "warn"
return "nicht eingerichtet", "warn"
def _filter_summary(f) -> str:
@ -382,8 +391,11 @@ def _wohnungen_context(user) -> dict:
filters = row_to_dict(filters_row)
flats = db.recent_flats(100)
rejected = db.rejected_flat_ids(uid)
flats_view = []
for f in flats:
if f["id"] in rejected:
continue
if not flat_matches_filter({
"rooms": f["rooms"], "total_rent": f["total_rent"], "size": f["size"],
"wbs": f["wbs"], "connectivity": {"morning_time": f["connectivity_morning_time"]},
@ -393,7 +405,7 @@ def _wohnungen_context(user) -> dict:
flats_view.append({"row": f, "last": last})
allowed, reason = _manual_apply_allowed()
alert_label, alert_chip = _alert_status(filters_row, notif_row)
alert_label, alert_chip = _alert_status(notif_row)
has_running = _has_running_application(uid)
map_points = []
for item in flats_view:
@ -411,6 +423,7 @@ def _wohnungen_context(user) -> dict:
return {
"flats": flats_view,
"map_points": map_points,
"has_filters": _has_filters(filters_row),
"alert_label": alert_label,
"alert_chip": alert_chip,
"filter_summary": _filter_summary(filters_row),
@ -521,6 +534,20 @@ async def action_apply(
return _wohnungen_partial_or_redirect(request, user)
@app.post("/actions/reject")
async def action_reject(
request: Request,
flat_id: str = Form(...),
csrf: str = Form(...),
user=Depends(require_user),
):
require_csrf(user["id"], csrf)
db.reject_flat(user["id"], flat_id)
db.log_audit(user["username"], "flat.rejected", f"flat_id={flat_id}",
user_id=user["id"], ip=client_ip(request))
return _wohnungen_partial_or_redirect(request, user)
# ---------------------------------------------------------------------------
# Tab: Bewerbungen
# ---------------------------------------------------------------------------