From 42377f0b67d34cda4487268f935929b05e4cd71b Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 21 Apr 2026 12:09:44 +0200 Subject: [PATCH] UX: alarm-status, ablehnen-button, annika-footer, map polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) --- web/app.py | 47 +++++++++++++++++++++++------- web/db.py | 37 +++++++++++++++++++++++ web/static/map.js | 9 +++++- web/templates/_layout.html | 5 ++++ web/templates/_wohnungen_body.html | 17 ++++++----- 5 files changed, 97 insertions(+), 18 deletions(-) diff --git a/web/app.py b/web/app.py index 96226c6..bcc983a 100644 --- a/web/app.py +++ b/web/app.py @@ -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 # --------------------------------------------------------------------------- diff --git a/web/db.py b/web/db.py index a714b90..a0c0984 100644 --- a/web/db.py +++ b/web/db.py @@ -185,6 +185,16 @@ MIGRATIONS: list[str] = [ ALTER TABLE flats ADD COLUMN lat REAL; ALTER TABLE flats ADD COLUMN lng REAL; """, + # 0004: per-user rejections — flats the user doesn't want in the list anymore + """ + CREATE TABLE IF NOT EXISTS flat_rejections ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + flat_id TEXT NOT NULL REFERENCES flats(id), + rejected_at TEXT NOT NULL, + PRIMARY KEY (user_id, flat_id) + ); + CREATE INDEX IF NOT EXISTS idx_rejections_user ON flat_rejections(user_id); + """, ] @@ -471,6 +481,33 @@ def recent_applications(user_id: Optional[int], limit: int = 50) -> list[sqlite3 ).fetchall()) +# --------------------------------------------------------------------------- +# Rejections (flats a user doesn't want to see anymore) +# --------------------------------------------------------------------------- + +def reject_flat(user_id: int, flat_id: str) -> None: + with _lock: + _conn.execute( + "INSERT OR IGNORE INTO flat_rejections(user_id, flat_id, rejected_at) VALUES (?, ?, ?)", + (user_id, flat_id, now_iso()), + ) + + +def unreject_flat(user_id: int, flat_id: str) -> None: + with _lock: + _conn.execute( + "DELETE FROM flat_rejections WHERE user_id = ? AND flat_id = ?", + (user_id, flat_id), + ) + + +def rejected_flat_ids(user_id: int) -> set[str]: + rows = _conn.execute( + "SELECT flat_id FROM flat_rejections WHERE user_id = ?", (user_id,) + ).fetchall() + return {row["flat_id"] for row in rows} + + def last_application_for_flat(user_id: int, flat_id: str) -> Optional[sqlite3.Row]: return _conn.execute( """SELECT * FROM applications diff --git a/web/static/map.js b/web/static/map.js index 771b193..d9dc155 100644 --- a/web/static/map.js +++ b/web/static/map.js @@ -14,7 +14,14 @@ function initFlatsMap() { try { mapInstance.remove(); } catch (e) {} mapInstance = null; } - mapInstance = L.map(el).setView(BERLIN_CENTER, BERLIN_ZOOM); + mapInstance = L.map(el, { + zoomControl: false, + scrollWheelZoom: false, + doubleClickZoom: false, + boxZoom: false, + touchZoom: false, + keyboard: false, + }).setView(BERLIN_CENTER, BERLIN_ZOOM); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap", maxZoom: 18, diff --git a/web/templates/_layout.html b/web/templates/_layout.html index 6453d11..34f1f21 100644 --- a/web/templates/_layout.html +++ b/web/templates/_layout.html @@ -29,4 +29,9 @@
{% block content %}{% endblock %}
+{% if user.username == 'annika' %} +
+ Programmiert für Annika ♥ +
+{% endif %} {% endblock %} diff --git a/web/templates/_wohnungen_body.html b/web/templates/_wohnungen_body.html index 2e6a380..7f87d3d 100644 --- a/web/templates/_wohnungen_body.html +++ b/web/templates/_wohnungen_body.html @@ -100,12 +100,6 @@
- {% if not map_points %} -

- Keine Koordinaten für passende Wohnungen vorhanden — - entweder sind noch keine neuen Flats geocoded worden oder die Filter lassen noch nichts durch. -

- {% endif %}
@@ -153,11 +147,20 @@ {% endif %} +
+ + + +
{% else %}
- {% if alert_label == 'nicht eingerichtet' %} + {% if not has_filters %} Bitte zuerst Filter einstellen, damit passende Wohnungen angezeigt werden. {% else %} Aktuell keine Wohnung, die alle Filter erfüllt.