From 7ac421fcf909bba1008a3d1d2c0950a13f367847 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 24 Apr 2026 09:46:15 +0200 Subject: [PATCH] =?UTF-8?q?Routen-Validierung:=20>15=20km/h=20=C3=98=20z?= =?UTF-8?q?=C3=A4hlt=20nicht=20f=C3=BCr=20Stats/Troph=C3=A4en,=20SW=20by-v?= =?UTF-8?q?331?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/database.py | 7 +++++++ backend/routes/achievements.py | 15 ++++++++------- backend/routes/routen.py | 22 +++++++++++++++++----- backend/routes/stats.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/pages/map.js | 6 +++++- backend/static/sw.js | 2 +- 7 files changed, 40 insertions(+), 16 deletions(-) diff --git a/backend/database.py b/backend/database.py index dd55484..8e4ad40 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1017,3 +1017,10 @@ def _migrate(conn_factory): except Exception: pass logger.info("Migration: push_subscriptions last_lat/lon bereit.") + + # Routen: Geschwindigkeits-Validierung + try: + conn.execute("ALTER TABLE routes ADD COLUMN is_valid INTEGER NOT NULL DEFAULT 1") + except Exception: + pass + logger.info("Migration: routes.is_valid bereit.") diff --git a/backend/routes/achievements.py b/backend/routes/achievements.py index 12c13fa..dae91ff 100644 --- a/backend/routes/achievements.py +++ b/backend/routes/achievements.py @@ -125,10 +125,10 @@ def update_streak(user_id: int, conn): def check_and_award(user_id: int, conn): stats = conn.execute(""" SELECT ROUND( - COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? ), 0) + + COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? AND r.is_valid=1), 0) + COALESCE((SELECT SUM(w.walked_km) FROM route_walks w WHERE w.user_id=?), 0), 1) AS total_km, - (SELECT COUNT(*) FROM routes r WHERE r.user_id=? ) AS routen, + (SELECT COUNT(*) FROM routes r WHERE r.user_id=? AND r.is_valid=1) AS routen, (SELECT COUNT(*) FROM user_map_pois p WHERE p.user_id=?) AS pois FROM (SELECT 1) """, (user_id, user_id, user_id, user_id)).fetchone() @@ -178,17 +178,17 @@ async def my_achievements(user=Depends(get_current_user)): stats = conn.execute(""" SELECT ROUND( - COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? ), 0) + + COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? AND r.is_valid=1), 0) + COALESCE((SELECT SUM(w.walked_km) FROM route_walks w WHERE w.user_id=?), 0), 1) AS total_km, - (SELECT COUNT(*) FROM routes r WHERE r.user_id=? ) AS routen, + (SELECT COUNT(*) FROM routes r WHERE r.user_id=? AND r.is_valid=1) AS routen, (SELECT COUNT(*) FROM user_map_pois p WHERE p.user_id=?) AS pois, ROUND( - COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? ), 0) + + COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? AND r.is_valid=1), 0) + COALESCE((SELECT SUM(w.walked_km) FROM route_walks w WHERE w.user_id=?), 0), 1)*1 + (SELECT COUNT(*) FROM user_map_pois p WHERE p.user_id=?)*5 - + (SELECT COUNT(*) FROM routes r WHERE r.user_id=? )*10 AS punkte + + (SELECT COUNT(*) FROM routes r WHERE r.user_id=? AND r.is_valid=1)*10 AS punkte FROM (SELECT 1) """, (uid, uid, uid, uid, uid, uid, uid, uid)).fetchone() @@ -208,7 +208,8 @@ async def my_achievements(user=Depends(get_current_user)): +COUNT(DISTINCT p.id)*5 +COUNT(DISTINCT r.id)*10 AS punkte FROM users u - LEFT JOIN routes r ON r.user_id=u.id LEFT JOIN user_map_pois p ON p.user_id=u.id + LEFT JOIN routes r ON r.user_id=u.id AND r.is_valid=1 + LEFT JOIN user_map_pois p ON p.user_id=u.id GROUP BY u.id ) WHERE punkte > ? """, (stats["punkte"] if stats else 0,)).fetchone() diff --git a/backend/routes/routen.py b/backend/routes/routen.py index 8233e0e..f2a2efc 100644 --- a/backend/routes/routen.py +++ b/backend/routes/routen.py @@ -13,6 +13,14 @@ from routes.push import send_push_to_user router = APIRouter() +_MAX_AVG_KMH = 15.0 # Über diesem Wert wird die Route nicht für Stats/Trophäen gewertet + +def _check_speed(distanz_km, dauer_min) -> bool: + """True = gültig, False = zu schnell (wahrscheinlich motorisiert).""" + if not distanz_km or not dauer_min or dauer_min <= 0: + return True + return (distanz_km / (dauer_min / 60)) <= _MAX_AVG_KMH + def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float: R = 6_371_000 @@ -135,26 +143,30 @@ async def create_route(data: RouteCreate, user=Depends(get_current_user)): if len(data.gps_track) < 2: raise HTTPException(400, "GPS-Track braucht mindestens 2 Punkte.") - gps_json = json.dumps([p.model_dump() for p in data.gps_track]) + gps_json = json.dumps([p.model_dump() for p in data.gps_track]) + is_valid = int(_check_speed(data.distanz_km, data.dauer_min)) with db() as conn: cur = conn.execute(""" INSERT INTO routes (user_id, name, beschreibung, gps_track, distanz_km, dauer_min, - schwierigkeit, untergrund, schatten, leine_empfohlen, is_public, hunde_tauglichkeit) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + schwierigkeit, untergrund, schatten, leine_empfohlen, is_public, + hunde_tauglichkeit, is_valid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user['id'], data.name, data.beschreibung, gps_json, data.distanz_km, data.dauer_min, data.schwierigkeit, data.untergrund, int(data.schatten) if data.schatten is not None else None, int(data.leine_empfohlen) if data.leine_empfohlen is not None else None, int(data.is_public) if data.is_public is not None else 1, - data.hunde_tauglichkeit, + data.hunde_tauglichkeit, is_valid, )) row = conn.execute("SELECT * FROM routes WHERE id = ?", (cur.lastrowid,)).fetchone() update_streak(user['id'], conn) check_and_award(user['id'], conn) - return _parse(row) + result = _parse(row) + result['is_valid'] = bool(is_valid) + return result # ------------------------------------------------------------------ diff --git a/backend/routes/stats.py b/backend/routes/stats.py index e1cc906..e8cd68c 100644 --- a/backend/routes/stats.py +++ b/backend/routes/stats.py @@ -13,7 +13,7 @@ _STATS_SQL = """ + COUNT(DISTINCT p.id) * 5 + COUNT(DISTINCT r.id) * 10 AS punkte FROM users u - LEFT JOIN routes r ON r.user_id = u.id + LEFT JOIN routes r ON r.user_id = u.id AND r.is_valid = 1 LEFT JOIN user_map_pois p ON p.user_id = u.id GROUP BY u.id """ diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 65bb5a3..ffa6e82 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '318'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '319'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index bc281b2..4f723c7 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -1510,7 +1510,11 @@ window.Page_map = (() => { UI.modal.close(); if (_recPolyline) { _recPolyline.remove(); _recPolyline = null; } if (_recMarker) { _recMarker.remove(); _recMarker = null; } - UI.toast.success(`Route „${saved.name}" gespeichert!`); + if (saved.is_valid === false) { + UI.toast.warning(`Route „${saved.name}" gespeichert — wird nicht für Statistiken gewertet (Geschwindigkeit zu hoch).`); + } else { + UI.toast.success(`Route „${saved.name}" gespeichert!`); + } }); }); } diff --git a/backend/static/sw.js b/backend/static/sw.js index 5d370e3..6e9bf0b 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v330'; +const CACHE_VERSION = 'by-v331'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten