diff --git a/backend/main.py b/backend/main.py index 7ccfaa1..9ed099e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -93,6 +93,7 @@ from routes.import_data import router as import_router from routes.sharing import dog_router as sharing_dog_router, share_router as sharing_share_router from routes.widget import router as widget_router from routes.notifications import router as notifications_router +from routes.alerts import router as alerts_router from routes.services import router as services_router from routes.ratings import router as ratings_router from routes.sitting_access import router as sitting_access_router @@ -129,6 +130,7 @@ app.include_router(sharing_dog_router, prefix="/api/dogs", tags=["Teilen" app.include_router(sharing_share_router, prefix="/api/share", tags=["Teilen"]) app.include_router(widget_router, prefix="/api/widget", tags=["Widget"]) app.include_router(notifications_router, prefix="/api/notifications", tags=["Notifications"]) +app.include_router(alerts_router, prefix="/api/alerts", tags=["Alerts"]) app.include_router(services_router, prefix="/api/services", tags=["Services"]) app.include_router(ratings_router, prefix="/api/ratings", tags=["Ratings"]) app.include_router(sitting_access_router, prefix="/api/sitting-access", tags=["SittingAccess"]) diff --git a/backend/routes/alerts.py b/backend/routes/alerts.py new file mode 100644 index 0000000..e5afb2a --- /dev/null +++ b/backend/routes/alerts.py @@ -0,0 +1,36 @@ +"""BAN YARO — Nearby Alerts (Giftköder + Vermisste Hunde)""" +import math +from datetime import datetime +from fastapi import APIRouter + +from database import db + +router = APIRouter() + +_RADIUS_M = 20_000 # 20 km + + +def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + R = 6_371_000 + p1, p2 = math.radians(lat1), math.radians(lat2) + dp = math.radians(lat2 - lat1) + dl = math.radians(lon2 - lon1) + a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 + return 2 * R * math.asin(math.sqrt(a)) + + +@router.get("") +async def nearby_alerts(lat: float, lon: float): + now = datetime.utcnow().isoformat() + with db() as conn: + poisons = conn.execute( + "SELECT lat, lon FROM poison WHERE geloest=0 AND expires_at > ?", (now,) + ).fetchall() + lost = conn.execute( + "SELECT lat, lon FROM lost_dogs WHERE is_active=1" + ).fetchall() + + has_poison = any(_haversine(lat, lon, r["lat"], r["lon"]) <= _RADIUS_M for r in poisons) + has_lost = any(_haversine(lat, lon, r["lat"], r["lon"]) <= _RADIUS_M for r in lost) + + return {"poison": has_poison, "lost": has_lost} diff --git a/backend/static/css/layout.css b/backend/static/css/layout.css index 8d6e8ec..ea0770d 100644 --- a/backend/static/css/layout.css +++ b/backend/static/css/layout.css @@ -176,6 +176,22 @@ display: flex; align-items: stretch; box-shadow: 0 -2px 12px rgba(42, 31, 20, 0.08); + transition: border-top-color 0.4s ease; +} + +@keyframes nav-alert-pulse { + 0%, 100% { box-shadow: 0 -2px 12px rgba(42,31,20,0.08); } + 50% { box-shadow: 0 -4px 20px var(--nav-alert-color, rgba(220,38,38,0.4)); } +} +#bottom-nav.alert-poison { + border-top: 3px solid var(--c-danger); + --nav-alert-color: rgba(220, 38, 38, 0.4); + animation: nav-alert-pulse 2s ease-in-out infinite; +} +#bottom-nav.alert-lost { + border-top: 3px solid #f59e0b; + --nav-alert-color: rgba(245, 158, 11, 0.4); + animation: nav-alert-pulse 2s ease-in-out infinite; } .nav-item { diff --git a/backend/static/index.html b/backend/static/index.html index dadbe51..fdab76d 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -168,6 +168,11 @@ style="width:18px;height:18px;color:var(--c-text-muted)"> + @@ -290,9 +295,9 @@ @@ -376,6 +378,11 @@ navigator.serviceWorker.addEventListener('controllerchange', () => { window.location.reload(); }); + navigator.serviceWorker.addEventListener('message', e => { + if (e.data?.type === 'CHECK_NEARBY_ALERTS') { + window.App?._checkNearbyAlerts?.(); + } + }); } diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 9a1de2b..2291bd0 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 = '261'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '262'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { @@ -361,17 +361,14 @@ const App = (() => { `; UI.modal.open({ - title: 'Was möchtest du hinzufügen?', + title: 'Schnellmeldung', body: `
- ${authBtn('diary', 'btn-secondary', 'book-open', 'Tagebuch-Eintrag')} - ${authBtn('health', 'btn-secondary', 'syringe', 'Gesundheits-Eintrag')} - ${authBtn('chat', 'btn-secondary', 'chat-circle-dots','Neue Nachricht')} - ${authBtn('forum', 'btn-secondary', 'push-pin', 'Forenbeitrag erstellen')} - ${authBtn('walk', 'btn-nature', 'paw-print', 'Gassi-Treffen erstellen')} + ${authBtn('walk', 'btn-secondary', 'paw-print', 'Gassi-Treffen erstellen')} + ${authBtn('lost', 'btn-secondary', 'magnifying-glass','Verlorener Hund melden')}
${!loggedIn ? `

@@ -391,12 +388,9 @@ const App = (() => { // Ohne Delay trifft es das neu geöffnete Modal und schließt es sofort. setTimeout(() => { if (action.startsWith('auth-')) { navigate('settings'); return; } - if (action === 'diary') { navigate('diary'); pages['diary'].module?.openNew?.(); } - if (action === 'health') { navigate('health'); pages['health'].module?.openNew?.(); } if (action === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); } if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); } - if (action === 'chat') { navigate('chat'); setTimeout(() => pages['chat'].module?._showNewMessagePicker?.(), 400); } - if (action === 'forum') { navigate('forum'); setTimeout(() => pages['forum'].module?.openNew?.(), 400); } + if (action === 'lost') { navigate('lost'); setTimeout(() => pages['lost'].module?.openNew?.(), 400); } }, 350); }, { once: true }); } @@ -434,11 +428,14 @@ const App = (() => { _updateNotifBadge(); _updateChatBadge(); + _checkNearbyAlerts(); setInterval(() => { _updateNotifBadge(); _updateChatBadge(); }, 30_000); + setInterval(_checkNearbyAlerts, 5 * 60_000); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { _updateNotifBadge(); _updateChatBadge(); + _checkNearbyAlerts(); if (state.page === 'chat') { pages['chat']?.module?.refresh?.(); } @@ -452,6 +449,22 @@ const App = (() => { } } + async function _checkNearbyAlerts() { + const nav = document.getElementById('bottom-nav'); + if (!nav) return; + try { + const pos = await new Promise((resolve, reject) => + navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 5000, maximumAge: 120_000 }) + ); + const { latitude: lat, longitude: lon } = pos.coords; + const data = await API.get(`/alerts?lat=${lat}&lon=${lon}`); + nav.classList.toggle('alert-poison', !!data.poison); + nav.classList.toggle('alert-lost', !data.poison && !!data.lost); + } catch { + // Kein Standort verfügbar — kein Alert anzeigen + } + } + async function _updateNotifBadge() { if (!state.user) return; try { @@ -820,7 +833,8 @@ const App = (() => { renderDogSwitcher: _renderDogSwitcher, getInstallPrompt: () => _installPrompt, requireAuth, showOnboarding: _showOnboardingModal, - updateNotifBadge: _updateNotifBadge }; + updateNotifBadge: _updateNotifBadge, + _checkNearbyAlerts }; })(); diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 43d8c8c..4dd8d4d 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -191,8 +191,6 @@ window.Page_map = (() => {

- -
@@ -270,8 +268,6 @@ window.Page_map = (() => { }); document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode); - document.getElementById('map-offline-btn').addEventListener('click', _cacheTiles); - document.getElementById('map-rec-btn').addEventListener('click', _toggleRecording); } // ---------------------------------------------------------- diff --git a/backend/static/sw.js b/backend/static/sw.js index 21a78c0..e52e3e4 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-v271'; +const CACHE_VERSION = 'by-v272'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten @@ -223,6 +223,13 @@ self.addEventListener('push', event => { event.waitUntil( self.registration.showNotification(data.title || 'Ban Yaro', options) ); + + // App informieren damit sie Nearby-Alerts neu prüft + if (data.type === 'poison_alert' || data.type === 'lost_dog_alert') { + self.clients.matchAll({ type: 'window' }).then(clients => { + clients.forEach(c => c.postMessage({ type: 'CHECK_NEARBY_ALERTS' })); + }); + } }); // ----------------------------------------------------------