Feature: ORS-Wochenlimit (20/Woche), Tages-Cache, Privilegien-Bypass, Datenschutz-Update — SW by-v480, APP_VER 457
This commit is contained in:
parent
ca8bb495b0
commit
7048499624
8 changed files with 121 additions and 14 deletions
|
|
@ -87,7 +87,7 @@ def get_current_user(
|
||||||
user_id = int(payload["sub"])
|
user_id = int(payload["sub"])
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
row = conn.execute(
|
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,)
|
(user_id,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1411,3 +1411,14 @@ def _migrate(conn_factory):
|
||||||
if col not in existing_h:
|
if col not in existing_h:
|
||||||
conn.execute(f"ALTER TABLE health ADD COLUMN {col} {typedef}")
|
conn.execute(f"ALTER TABLE health ADD COLUMN {col} {typedef}")
|
||||||
logger.info(f"Migration: health.{col} hinzugefügt.")
|
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.")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""BAN YARO — Gassi-Routen"""
|
"""BAN YARO — Gassi-Routen"""
|
||||||
|
|
||||||
|
import datetime as _dt
|
||||||
import json, math, os, uuid
|
import json, math, os, uuid
|
||||||
import httpx
|
import httpx
|
||||||
import polyline as _polyline
|
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)
|
distance_km: float # Zieldistanz in km (z.B. 2.0, 4.0, 6.0)
|
||||||
seed: int = 0 # 0-4: verschiedene Routenvarianten
|
seed: int = 0 # 0-4: verschiedene Routenvarianten
|
||||||
|
|
||||||
|
WEEKLY_LIMIT = 20
|
||||||
|
|
||||||
@router.post("/suggest")
|
@router.post("/suggest")
|
||||||
async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)):
|
async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)):
|
||||||
if not (0.5 <= data.distance_km <= 15):
|
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:
|
if not ORS_API_KEY:
|
||||||
raise HTTPException(503, "ORS nicht konfiguriert")
|
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 = {
|
payload = {
|
||||||
"coordinates": [[data.lon, data.lat]],
|
"coordinates": [[data.lon, data.lat]],
|
||||||
"options": {
|
"options": {
|
||||||
|
|
@ -251,12 +275,23 @@ async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)):
|
||||||
else:
|
else:
|
||||||
schwierigkeit = "anspruchsvoll"
|
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 {
|
return {
|
||||||
"name": f"Rundweg {distanz_km:.0f} km",
|
"name": f"Rundweg {distanz_km:.0f} km",
|
||||||
"gps_track": gps_track,
|
"gps_track": gps_track,
|
||||||
"distanz_km": distanz_km,
|
"distanz_km": distanz_km,
|
||||||
"dauer_min": dauer_min,
|
"dauer_min": dauer_min,
|
||||||
"schwierigkeit": schwierigkeit,
|
"schwierigkeit": schwierigkeit,
|
||||||
|
"weekly_remaining": weekly_remaining,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,29 @@ window.Page_datenschutz = (() => {
|
||||||
findet nicht statt.
|
findet nicht statt.
|
||||||
</p>`)}
|
</p>`)}
|
||||||
|
|
||||||
|
${sec('Routenvorschläge (OpenRouteService)', `
|
||||||
|
<p style="${S.p}">
|
||||||
|
Die Funktion <strong>„Routenvorschläge"</strong> berechnet auf Wunsch einen Rundweg
|
||||||
|
ausgehend von deinem aktuellen Standort. Dazu werden deine GPS-Koordinaten einmalig
|
||||||
|
an den Dienst <strong>OpenRouteService</strong> übermittelt, der von
|
||||||
|
<strong>HeiGIT</strong> 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.
|
||||||
|
</p>
|
||||||
|
<p style="${S.p};margin-top:var(--space-3)">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p style="${S.p};margin-top:var(--space-3)">
|
||||||
|
Datenschutzerklärung von OpenRouteService:
|
||||||
|
<a href="https://openrouteservice.org/privacy-policy/" target="_blank" rel="noopener"
|
||||||
|
style="${S.a}">openrouteservice.org/privacy-policy</a>
|
||||||
|
</p>`)}
|
||||||
|
|
||||||
${sec('Push-Benachrichtigungen', `
|
${sec('Push-Benachrichtigungen', `
|
||||||
<p style="${S.p}">
|
<p style="${S.p}">
|
||||||
Wenn du Push-Benachrichtigungen aktivierst, wird ein Abonnement-Token an den
|
Wenn du Push-Benachrichtigungen aktivierst, wird ein Abonnement-Token an den
|
||||||
|
|
@ -164,7 +187,7 @@ window.Page_datenschutz = (() => {
|
||||||
</p>`)}
|
</p>`)}
|
||||||
|
|
||||||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:0">
|
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:0">
|
||||||
Stand: April 2026
|
Stand: Mai 2026
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -490,11 +490,21 @@ window.Page_routes = (() => {
|
||||||
});
|
});
|
||||||
_suggestResult = result;
|
_suggestResult = result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const is429 = err.status === 429 || String(err.message).includes('Wochenlimit');
|
||||||
if (res) res.innerHTML = `
|
if (res) res.innerHTML = `
|
||||||
<div style="padding:var(--space-4);border-radius:var(--radius-lg);
|
<div style="padding:var(--space-5);border-radius:var(--radius-lg);
|
||||||
background:rgba(220,38,38,0.08);border:1px solid rgba(220,38,38,0.25);
|
background:${is429 ? 'rgba(234,179,8,0.08)' : 'rgba(220,38,38,0.08)'};
|
||||||
color:#f87171;font-size:0.9rem">
|
border:1px solid ${is429 ? 'rgba(234,179,8,0.3)' : 'rgba(220,38,38,0.25)'};
|
||||||
${UI.icon('warning')} ${UI.escape(err.message || 'Fehler beim Berechnen des Rundwegs.')}
|
color:${is429 ? '#facc15' : '#f87171'};text-align:center">
|
||||||
|
<svg class="ph-icon" style="width:28px;height:28px;margin-bottom:var(--space-3)" aria-hidden="true">
|
||||||
|
<use href="/icons/phosphor.svg#${is429 ? 'calendar-x' : 'warning'}"></use>
|
||||||
|
</svg>
|
||||||
|
<p style="margin:0;font-size:var(--text-sm);font-weight:var(--weight-semibold)">
|
||||||
|
${is429 ? 'Wochenlimit erreicht' : 'Fehler beim Berechnen'}
|
||||||
|
</p>
|
||||||
|
<p style="margin:var(--space-2) 0 0;font-size:var(--text-xs);opacity:0.85">
|
||||||
|
${is429 ? 'Du hast diese Woche alle 20 Routenvorschläge genutzt. Montag gibt es neue.' : UI.escape(err.message || 'Unbekannter Fehler')}
|
||||||
|
</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
if (calcBtn) calcBtn.disabled = false;
|
if (calcBtn) calcBtn.disabled = false;
|
||||||
return;
|
return;
|
||||||
|
|
@ -515,8 +525,14 @@ window.Page_routes = (() => {
|
||||||
: '–';
|
: '–';
|
||||||
const diffLabel = { leicht: 'Leicht', mittel: 'Mittel', anspruchsvoll: 'Schwer' }[result.schwierigkeit] || '';
|
const diffLabel = { leicht: 'Leicht', mittel: 'Mittel', anspruchsvoll: 'Schwer' }[result.schwierigkeit] || '';
|
||||||
|
|
||||||
|
const limitHint = (result.weekly_remaining != null)
|
||||||
|
? `<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:right;margin:0 0 var(--space-2)">
|
||||||
|
Noch ${result.weekly_remaining} von 20 Anfragen diese Woche
|
||||||
|
</p>`
|
||||||
|
: '';
|
||||||
|
|
||||||
res.innerHTML = `
|
res.innerHTML = `
|
||||||
<div id="rks-map" style="height:250px;background:var(--c-surface);margin-bottom:var(--space-3)"></div>
|
${limitHint}<div id="rks-map" style="height:250px;background:var(--c-surface);margin-bottom:var(--space-3)"></div>
|
||||||
<div style="display:flex;gap:var(--space-3);align-items:center;flex-wrap:wrap;margin-bottom:var(--space-4)">
|
<div style="display:flex;gap:var(--space-3);align-items:center;flex-wrap:wrap;margin-bottom:var(--space-4)">
|
||||||
<span style="${_pillStyle('rgba(107,114,128,0.10)','#9ca3af','rgba(107,114,128,0.30)')}">
|
<span style="${_pillStyle('rgba(107,114,128,0.10)','#9ca3af','rgba(107,114,128,0.30)')}">
|
||||||
${UI.icon('map-trifold')} ${UI.escape(distStr)}
|
${UI.icon('map-trifold')} ${UI.escape(distStr)}
|
||||||
|
|
|
||||||
|
|
@ -375,12 +375,34 @@ window.Page_welcome = (() => {
|
||||||
const km = [2, 4, 6][dayIdx % 3];
|
const km = [2, 4, 6][dayIdx % 3];
|
||||||
const seed = dayIdx % 5;
|
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;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = await API.post('/routes/suggest', { lat: loc.lat, lon: loc.lon, distance_km: km, seed });
|
result = await API.post('/routes/suggest', { lat: loc.lat, lon: loc.lon, distance_km: km, seed });
|
||||||
} catch { return; }
|
} catch { return; }
|
||||||
if (!result?.gps_track?.length) 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');
|
const chipsRow = _container.querySelector('#wc-chips-row');
|
||||||
if (!chipsRow) return;
|
if (!chipsRow) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v479';
|
const CACHE_VERSION = 'by-v480';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue