Navigation, Karten-FABs, Nearby-Alerts
- Bottom-Nav neu: Karte | Routen | + | Tagebuch | Forum - + reduziert auf: Giftköder, Gassi-Treffen, Verlorener Hund - Notification-Badge auf User-Avatar (Header) - Nearby-Alerts: Nav-Leiste pulsiert rot/orange bei Giftköder/vermisstm Hund in 20km - SW postMessage bei poison/lost_alert → sofortiger Alert-Check - Karten-FABs: nur Marker setzen + Standort (Route aufzeichnen + Offline entfernt) - SW by-v272, APP_VER 262
This commit is contained in:
parent
5141ba9969
commit
65d1cf6c7f
7 changed files with 105 additions and 27 deletions
|
|
@ -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.sharing import dog_router as sharing_dog_router, share_router as sharing_share_router
|
||||||
from routes.widget import router as widget_router
|
from routes.widget import router as widget_router
|
||||||
from routes.notifications import router as notifications_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.services import router as services_router
|
||||||
from routes.ratings import router as ratings_router
|
from routes.ratings import router as ratings_router
|
||||||
from routes.sitting_access import router as sitting_access_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(sharing_share_router, prefix="/api/share", tags=["Teilen"])
|
||||||
app.include_router(widget_router, prefix="/api/widget", tags=["Widget"])
|
app.include_router(widget_router, prefix="/api/widget", tags=["Widget"])
|
||||||
app.include_router(notifications_router, prefix="/api/notifications", tags=["Notifications"])
|
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(services_router, prefix="/api/services", tags=["Services"])
|
||||||
app.include_router(ratings_router, prefix="/api/ratings", tags=["Ratings"])
|
app.include_router(ratings_router, prefix="/api/ratings", tags=["Ratings"])
|
||||||
app.include_router(sitting_access_router, prefix="/api/sitting-access", tags=["SittingAccess"])
|
app.include_router(sitting_access_router, prefix="/api/sitting-access", tags=["SittingAccess"])
|
||||||
|
|
|
||||||
36
backend/routes/alerts.py
Normal file
36
backend/routes/alerts.py
Normal file
|
|
@ -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}
|
||||||
|
|
@ -176,6 +176,22 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
box-shadow: 0 -2px 12px rgba(42, 31, 20, 0.08);
|
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 {
|
.nav-item {
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,11 @@
|
||||||
style="width:18px;height:18px;color:var(--c-text-muted)">
|
style="width:18px;height:18px;color:var(--c-text-muted)">
|
||||||
<use href="/icons/phosphor.svg#user"></use>
|
<use href="/icons/phosphor.svg#user"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
<span id="notif-nav-badge" class="hidden"
|
||||||
|
style="position:absolute;top:-3px;right:-3px;min-width:16px;height:16px;
|
||||||
|
border-radius:8px;background:var(--c-danger);color:#fff;font-size:10px;
|
||||||
|
font-weight:700;line-height:16px;text-align:center;padding:0 3px;
|
||||||
|
pointer-events:none;z-index:1"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="header-menu-btn" id="header-menu-btn" aria-label="Menü"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#list"></use></svg></button>
|
<button class="header-menu-btn" id="header-menu-btn" aria-label="Menü"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#list"></use></svg></button>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -290,9 +295,9 @@
|
||||||
|
|
||||||
<!-- MOBILE BOTTOM NAVIGATION -->
|
<!-- MOBILE BOTTOM NAVIGATION -->
|
||||||
<nav id="bottom-nav" role="navigation" aria-label="Hauptnavigation">
|
<nav id="bottom-nav" role="navigation" aria-label="Hauptnavigation">
|
||||||
<div class="nav-item active" data-page="diary">
|
<div class="nav-item" data-page="map">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-trifold"></use></svg>
|
||||||
<span class="nav-item-label">Tagebuch</span>
|
<span class="nav-item-label">Karte</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="routes">
|
<div class="nav-item" data-page="routes">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg>
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg>
|
||||||
|
|
@ -302,17 +307,14 @@
|
||||||
<div class="nav-item nav-item-center" id="nav-add">
|
<div class="nav-item nav-item-center" id="nav-add">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#plus"></use></svg>
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#plus"></use></svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item active" data-page="diary">
|
||||||
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>
|
||||||
|
<span class="nav-item-label">Tagebuch</span>
|
||||||
|
</div>
|
||||||
<div class="nav-item" data-page="forum">
|
<div class="nav-item" data-page="forum">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg>
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg>
|
||||||
<span class="nav-item-label">Forum</span>
|
<span class="nav-item-label">Forum</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="notifications">
|
|
||||||
<span style="position:relative;display:inline-flex">
|
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bell"></use></svg>
|
|
||||||
<span class="nav-badge hidden" id="notif-nav-badge">0</span>
|
|
||||||
</span>
|
|
||||||
<span class="nav-item-label">Aktuelles</span>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
</div><!-- #app -->
|
</div><!-- #app -->
|
||||||
|
|
@ -376,6 +378,11 @@
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
navigator.serviceWorker.addEventListener('message', e => {
|
||||||
|
if (e.data?.type === 'CHECK_NEARBY_ALERTS') {
|
||||||
|
window.App?._checkNearbyAlerts?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
@ -361,17 +361,14 @@ const App = (() => {
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|
||||||
UI.modal.open({
|
UI.modal.open({
|
||||||
title: 'Was möchtest du hinzufügen?',
|
title: 'Schnellmeldung',
|
||||||
body: `
|
body: `
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
${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')}
|
|
||||||
<button class="btn btn-danger w-full" data-quick="poison">
|
<button class="btn btn-danger w-full" data-quick="poison">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning-octagon"></use></svg> Giftköder melden
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning-octagon"></use></svg> Giftköder melden
|
||||||
</button>
|
</button>
|
||||||
${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')}
|
||||||
</div>
|
</div>
|
||||||
${!loggedIn ? `<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin-top:var(--space-3)">
|
${!loggedIn ? `<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin-top:var(--space-3)">
|
||||||
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#info"></use></svg>
|
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#info"></use></svg>
|
||||||
|
|
@ -391,12 +388,9 @@ const App = (() => {
|
||||||
// Ohne Delay trifft es das neu geöffnete Modal und schließt es sofort.
|
// Ohne Delay trifft es das neu geöffnete Modal und schließt es sofort.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (action.startsWith('auth-')) { navigate('settings'); return; }
|
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 === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); }
|
||||||
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
||||||
if (action === 'chat') { navigate('chat'); setTimeout(() => pages['chat'].module?._showNewMessagePicker?.(), 400); }
|
if (action === 'lost') { navigate('lost'); setTimeout(() => pages['lost'].module?.openNew?.(), 400); }
|
||||||
if (action === 'forum') { navigate('forum'); setTimeout(() => pages['forum'].module?.openNew?.(), 400); }
|
|
||||||
}, 350);
|
}, 350);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}
|
}
|
||||||
|
|
@ -434,11 +428,14 @@ const App = (() => {
|
||||||
|
|
||||||
_updateNotifBadge();
|
_updateNotifBadge();
|
||||||
_updateChatBadge();
|
_updateChatBadge();
|
||||||
|
_checkNearbyAlerts();
|
||||||
setInterval(() => { _updateNotifBadge(); _updateChatBadge(); }, 30_000);
|
setInterval(() => { _updateNotifBadge(); _updateChatBadge(); }, 30_000);
|
||||||
|
setInterval(_checkNearbyAlerts, 5 * 60_000);
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
_updateNotifBadge();
|
_updateNotifBadge();
|
||||||
_updateChatBadge();
|
_updateChatBadge();
|
||||||
|
_checkNearbyAlerts();
|
||||||
if (state.page === 'chat') {
|
if (state.page === 'chat') {
|
||||||
pages['chat']?.module?.refresh?.();
|
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() {
|
async function _updateNotifBadge() {
|
||||||
if (!state.user) return;
|
if (!state.user) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -820,7 +833,8 @@ const App = (() => {
|
||||||
renderDogSwitcher: _renderDogSwitcher,
|
renderDogSwitcher: _renderDogSwitcher,
|
||||||
getInstallPrompt: () => _installPrompt, requireAuth,
|
getInstallPrompt: () => _installPrompt, requireAuth,
|
||||||
showOnboarding: _showOnboardingModal,
|
showOnboarding: _showOnboardingModal,
|
||||||
updateNotifBadge: _updateNotifBadge };
|
updateNotifBadge: _updateNotifBadge,
|
||||||
|
_checkNearbyAlerts };
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,8 +191,6 @@ window.Page_map = (() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="map-fabs">
|
<div class="map-fabs">
|
||||||
<button class="map-fab map-fab--rec" id="map-rec-btn" title="Route aufzeichnen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg></button>
|
|
||||||
<button class="map-fab map-fab--offline" id="map-offline-btn" title="Karte offline speichern"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#floppy-disk"></use></svg></button>
|
|
||||||
<button class="map-fab map-fab--pin" id="map-pin-btn" title="Marker setzen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg></button>
|
<button class="map-fab map-fab--pin" id="map-pin-btn" title="Marker setzen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg></button>
|
||||||
<button class="map-fab" id="map-locate-btn" title="Meinen Standort"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg></button>
|
<button class="map-fab" id="map-locate-btn" title="Meinen Standort"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -270,8 +268,6 @@ window.Page_map = (() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode);
|
document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode);
|
||||||
document.getElementById('map-offline-btn').addEventListener('click', _cacheTiles);
|
|
||||||
document.getElementById('map-rec-btn').addEventListener('click', _toggleRecording);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v271';
|
const CACHE_VERSION = 'by-v272';
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -223,6 +223,13 @@ self.addEventListener('push', event => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
self.registration.showNotification(data.title || 'Ban Yaro', options)
|
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' }));
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue