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 @@
- Keine Koordinaten für passende Wohnungen vorhanden —
- entweder sind noch keine neuen Flats geocoded worden oder die Filter lassen noch nichts durch.
-