From 704849962482dee44e0787f0ba231e8ca2bd07c9 Mon Sep 17 00:00:00 2001
From: rene
Date: Wed, 29 Apr 2026 08:23:55 +0200
Subject: [PATCH] =?UTF-8?q?Feature:=20ORS-Wochenlimit=20(20/Woche),=20Tage?=
=?UTF-8?q?s-Cache,=20Privilegien-Bypass,=20Datenschutz-Update=20=E2=80=94?=
=?UTF-8?q?=20SW=20by-v480,=20APP=5FVER=20457?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/auth.py | 2 +-
backend/database.py | 11 +++++++
backend/routes/routen.py | 45 +++++++++++++++++++++++---
backend/static/js/app.js | 2 +-
backend/static/js/pages/datenschutz.js | 25 +++++++++++++-
backend/static/js/pages/routes.js | 26 ++++++++++++---
backend/static/js/pages/welcome.js | 22 +++++++++++++
backend/static/sw.js | 2 +-
8 files changed, 121 insertions(+), 14 deletions(-)
diff --git a/backend/auth.py b/backend/auth.py
index beedb65..b3365aa 100644
--- a/backend/auth.py
+++ b/backend/auth.py
@@ -87,7 +87,7 @@ def get_current_user(
user_id = int(payload["sub"])
with db() as conn:
row = conn.execute(
- "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled FROM users WHERE id=?",
+ "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status FROM users WHERE id=?",
(user_id,)
).fetchone()
diff --git a/backend/database.py b/backend/database.py
index 3bea359..4db82be 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -1411,3 +1411,14 @@ def _migrate(conn_factory):
if col not in existing_h:
conn.execute(f"ALTER TABLE health ADD COLUMN {col} {typedef}")
logger.info(f"Migration: health.{col} hinzugefügt.")
+
+ # Route-Suggest Rate-Limiting
+ conn.executescript("""
+ CREATE TABLE IF NOT EXISTS route_suggest_usage (
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ week TEXT NOT NULL,
+ count INTEGER NOT NULL DEFAULT 0,
+ PRIMARY KEY (user_id, week)
+ );
+ """)
+ logger.info("Migration: route_suggest_usage Tabelle bereit.")
diff --git a/backend/routes/routen.py b/backend/routes/routen.py
index 5d27e04..f812284 100644
--- a/backend/routes/routen.py
+++ b/backend/routes/routen.py
@@ -1,5 +1,6 @@
"""BAN YARO — Gassi-Routen"""
+import datetime as _dt
import json, math, os, uuid
import httpx
import polyline as _polyline
@@ -184,6 +185,8 @@ class SuggestRequest(BaseModel):
distance_km: float # Zieldistanz in km (z.B. 2.0, 4.0, 6.0)
seed: int = 0 # 0-4: verschiedene Routenvarianten
+WEEKLY_LIMIT = 20
+
@router.post("/suggest")
async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)):
if not (0.5 <= data.distance_km <= 15):
@@ -192,6 +195,27 @@ async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)):
if not ORS_API_KEY:
raise HTTPException(503, "ORS nicht konfiguriert")
+ is_privileged = (
+ user.get("rolle") in ("admin", "moderator") or
+ user.get("is_moderator") or
+ user.get("is_social_media") or
+ user.get("breeder_status") == "approved"
+ )
+
+ today = _dt.date.today()
+ week_start = (today - _dt.timedelta(days=today.weekday())).isoformat()
+
+ if not is_privileged:
+ with db() as conn:
+ row = conn.execute(
+ "SELECT count FROM route_suggest_usage WHERE user_id=? AND week=?",
+ (user["id"], week_start)
+ ).fetchone()
+ current_count = row["count"] if row else 0
+
+ if current_count >= WEEKLY_LIMIT:
+ raise HTTPException(429, f"Wochenlimit von {WEEKLY_LIMIT} Routenvorschlägen erreicht.")
+
payload = {
"coordinates": [[data.lon, data.lat]],
"options": {
@@ -251,12 +275,23 @@ async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)):
else:
schwierigkeit = "anspruchsvoll"
+ if not is_privileged:
+ with db() as conn:
+ conn.execute("""
+ INSERT INTO route_suggest_usage (user_id, week, count) VALUES (?, ?, 1)
+ ON CONFLICT(user_id, week) DO UPDATE SET count = count + 1
+ """, (user["id"], week_start))
+ current_count += 1
+
+ weekly_remaining = None if is_privileged else max(0, WEEKLY_LIMIT - current_count)
+
return {
- "name": f"Rundweg {distanz_km:.0f} km",
- "gps_track": gps_track,
- "distanz_km": distanz_km,
- "dauer_min": dauer_min,
- "schwierigkeit": schwierigkeit,
+ "name": f"Rundweg {distanz_km:.0f} km",
+ "gps_track": gps_track,
+ "distanz_km": distanz_km,
+ "dauer_min": dauer_min,
+ "schwierigkeit": schwierigkeit,
+ "weekly_remaining": weekly_remaining,
}
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index ebc0c63..8c3b3a2 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 = '456'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '457'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {
diff --git a/backend/static/js/pages/datenschutz.js b/backend/static/js/pages/datenschutz.js
index 676523a..838aaf4 100644
--- a/backend/static/js/pages/datenschutz.js
+++ b/backend/static/js/pages/datenschutz.js
@@ -105,6 +105,29 @@ window.Page_datenschutz = (() => {
findet nicht statt.
`)}
+ ${sec('Routenvorschläge (OpenRouteService)', `
+
+ Die Funktion „Routenvorschläge" berechnet auf Wunsch einen Rundweg
+ ausgehend von deinem aktuellen Standort. Dazu werden deine GPS-Koordinaten einmalig
+ an den Dienst OpenRouteService übermittelt, der von
+ HeiGIT am Karlsruher Institut für Technologie (KIT), Deutschland,
+ betrieben wird. Es werden ausschließlich die Koordinaten übertragen —
+ keine Account- oder Profildaten. OpenRouteService speichert keine
+ personenbezogenen Daten dauerhaft.
+
+
+ Die Funktion wird nur aktiv, wenn du deinen Standort im Browser freigibst und
+ bewusst einen Routenvorschlag anforderst (Einwilligung gem. Art. 6 Abs. 1 lit. a DSGVO).
+ Der Tagesvorschlag auf der Startseite wird nur berechnet, wenn du eingeloggt bist und
+ Standortzugriff erteilt hast — das Ergebnis wird lokal zwischengespeichert und
+ maximal einmal täglich neu abgerufen.
+
+
+ Datenschutzerklärung von OpenRouteService:
+ openrouteservice.org/privacy-policy
+
`)}
+
${sec('Push-Benachrichtigungen', `
Wenn du Push-Benachrichtigungen aktivierst, wird ein Abonnement-Token an den
@@ -164,7 +187,7 @@ window.Page_datenschutz = (() => {
`)}
- Stand: April 2026
+ Stand: Mai 2026
diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js
index 9fa1103..bdc48c1 100644
--- a/backend/static/js/pages/routes.js
+++ b/backend/static/js/pages/routes.js
@@ -490,11 +490,21 @@ window.Page_routes = (() => {
});
_suggestResult = result;
} catch (err) {
+ const is429 = err.status === 429 || String(err.message).includes('Wochenlimit');
if (res) res.innerHTML = `
-
- ${UI.icon('warning')} ${UI.escape(err.message || 'Fehler beim Berechnen des Rundwegs.')}
+
+
+
+
+
+ ${is429 ? 'Wochenlimit erreicht' : 'Fehler beim Berechnen'}
+
+
+ ${is429 ? 'Du hast diese Woche alle 20 Routenvorschläge genutzt. Montag gibt es neue.' : UI.escape(err.message || 'Unbekannter Fehler')}
+
`;
if (calcBtn) calcBtn.disabled = false;
return;
@@ -515,8 +525,14 @@ window.Page_routes = (() => {
: '–';
const diffLabel = { leicht: 'Leicht', mittel: 'Mittel', anspruchsvoll: 'Schwer' }[result.schwierigkeit] || '';
+ const limitHint = (result.weekly_remaining != null)
+ ? `
+ Noch ${result.weekly_remaining} von 20 Anfragen diese Woche
+
`
+ : '';
+
res.innerHTML = `
-
+ ${limitHint}
${UI.icon('map-trifold')} ${UI.escape(distStr)}
diff --git a/backend/static/js/pages/welcome.js b/backend/static/js/pages/welcome.js
index 9484601..1300d55 100644
--- a/backend/static/js/pages/welcome.js
+++ b/backend/static/js/pages/welcome.js
@@ -375,12 +375,34 @@ window.Page_welcome = (() => {
const km = [2, 4, 6][dayIdx % 3];
const seed = dayIdx % 5;
+ // Tages-Cache prüfen — ORS nur einmal pro Tag anfragen
+ const today = new Date().toISOString().slice(0, 10); // 'YYYY-MM-DD'
+ const cacheKey = 'by_daily_route_' + today;
+ const cached = localStorage.getItem(cacheKey);
+ if (cached) {
+ try {
+ const result = JSON.parse(cached);
+ _applyRouteChip(result, km, seed);
+ return;
+ } catch {}
+ }
+
let result;
try {
result = await API.post('/routes/suggest', { lat: loc.lat, lon: loc.lon, distance_km: km, seed });
} catch { return; }
if (!result?.gps_track?.length) return;
+ // Ergebnis cachen und alte Einträge aufräumen
+ localStorage.setItem(cacheKey, JSON.stringify(result));
+ Object.keys(localStorage)
+ .filter(k => k.startsWith('by_daily_route_') && k !== cacheKey)
+ .forEach(k => localStorage.removeItem(k));
+
+ _applyRouteChip(result, km, seed);
+ }
+
+ function _applyRouteChip(result, km, seed) {
const chipsRow = _container.querySelector('#wc-chips-row');
if (!chipsRow) return;
diff --git a/backend/static/sw.js b/backend/static/sw.js
index f762bef..d2486a9 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-v479';
+const CACHE_VERSION = 'by-v480';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten