diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 104bfba..ebc0c63 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 = '455'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '456'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 7f4f6d9..9fa1103 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -74,17 +74,32 @@ window.Page_routes = (() => { // _esc und _emptyState ersetzt durch UI.escape() / UI.emptyState() - async function init(container, appState) { + async function init(container, appState, params = {}) { _container = container; _appState = appState; + + // Vorberechneter Vorschlag vom Welcome-Chip → direkt in Suggest-Tab anzeigen + if (params._suggestResult) { + _suggestResult = params._suggestResult; + _suggestKm = params._suggestKm || _suggestKm; + _suggestSeed = params._suggestSeed || _suggestSeed; + _browseMode = 'suggest'; + } + _render(); UI.loadLeaflet(); // fire & forget — bereit wenn Cards gerendert werden try { _userPos = await API.getLocation(); } catch {} await _loadData(); + // Vorschlag sofort rendern (Leaflet war noch nicht bereit bei _render) + if (params._suggestResult) { + _renderSuggestTab(); + _showSuggestResult(params._suggestResult); + } + // Deep-Link: /#routes?id=123 → direkt Route-Detail öffnen - const params = new URLSearchParams((location.hash.split('?')[1] || '')); - const deepId = params.get('id'); + const urlParams = new URLSearchParams((location.hash.split('?')[1] || '')); + const deepId = urlParams.get('id'); if (deepId) { _openDetail(parseInt(deepId, 10)); } @@ -486,19 +501,22 @@ window.Page_routes = (() => { } if (calcBtn) calcBtn.disabled = false; - // Ergebnis rendern - const distStr = result.distanz_km ? result.distanz_km.toFixed(2) + ' km' : '–'; - const durStr = result.dauer_min + _showSuggestResult(result); + } + + function _showSuggestResult(result) { + _suggestResult = result; + const res = document.getElementById('rks-result'); + if (!res) return; + + const distStr = result.distanz_km ? result.distanz_km.toFixed(2) + ' km' : '–'; + const durStr = result.dauer_min ? (result.dauer_min < 60 ? result.dauer_min + ' min' : Math.floor(result.dauer_min/60) + 'h ' + (result.dauer_min%60||'') + 'min').trim() : '–'; const diffLabel = { leicht: 'Leicht', mittel: 'Mittel', anspruchsvoll: 'Schwer' }[result.schwierigkeit] || ''; - if (!res) return; res.innerHTML = ` -
- -
${UI.icon('map-trifold')} ${UI.escape(distStr)} @@ -513,8 +531,6 @@ window.Page_routes = (() => { ${UI.escape(result.name || '')}
- -
-
- `; + `; - // Leaflet-Karte mit dem berechneten Track const _initMap = () => { const mapEl = document.getElementById('rks-map'); if (!mapEl || !window.L) return; if (_suggestMap) { _suggestMap.remove(); _suggestMap = null; } - const track = result.gps_track || []; - if (track.length < 2) { mapEl.innerHTML = '
Kein Track vorhanden
'; return; } - - const lls = track.map(p => [p.lat, p.lon]); + if (track.length < 2) return; + const lls = track.map(p => [p.lat, p.lon]); _suggestMap = L.map(mapEl, { zoomControl: false, attributionControl: false, dragging: true, touchZoom: true, scrollWheelZoom: false }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_suggestMap); const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.9 }).addTo(_suggestMap); - L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap); - L.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap); + L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap); + L.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap); _addRouteArrows(_suggestMap, track, '#3b82f6'); _suggestMap.fitBounds(poly.getBounds(), { padding: [16, 16] }); setTimeout(() => _suggestMap?.invalidateSize(), 120); }; - - if (window.L) { - _initMap(); - } else { + if (window.L) { _initMap(); } else { let tries = 0; const poll = setInterval(() => { if (window.L || ++tries > 40) { clearInterval(poll); if (window.L) _initMap(); } }, 100); } - // Navigation starten document.getElementById('rks-nav-btn')?.addEventListener('click', () => { - if (!_suggestResult) return; - const route = { - id: 'suggest-' + Date.now(), - name: _suggestResult.name, - gps_track: _suggestResult.gps_track, - distanz_km: _suggestResult.distanz_km, - }; - _openNavOverlay(route); + _openNavOverlay({ id: 'suggest-' + Date.now(), name: result.name, + gps_track: result.gps_track, distanz_km: result.distanz_km }); }); - - // Route speichern document.getElementById('rks-save-btn')?.addEventListener('click', async () => { const btn = document.getElementById('rks-save-btn'); - if (!btn || !_suggestResult) return; + if (!btn) return; await UI.asyncButton(btn, async () => { - await API.post('/routes', { - name: _suggestResult.name, - gps_track: _suggestResult.gps_track, - distanz_km: _suggestResult.distanz_km, - dauer_min: _suggestResult.dauer_min, - schwierigkeit: _suggestResult.schwierigkeit, - }); + await API.post('/routes', { name: result.name, gps_track: result.gps_track, + distanz_km: result.distanz_km, dauer_min: result.dauer_min, schwierigkeit: result.schwierigkeit }); UI.toast.success('Route gespeichert!'); await _loadData(); _setBrowseMode('mine'); diff --git a/backend/static/js/pages/welcome.js b/backend/static/js/pages/welcome.js index e8808fa..9484601 100644 --- a/backend/static/js/pages/welcome.js +++ b/backend/static/js/pages/welcome.js @@ -363,55 +363,45 @@ window.Page_welcome = (() => { `; } - // Versucht async eine Bank in 2 km Umkreis zu finden und ersetzt Chip 2 - async function _tryBenchChip(dashData) { + // Berechnet async eine Tages-Gassirunde via ORS und ersetzt Chip 2 + async function _tryRouteChip(dashData) { if (dashData?.next_appointment) return; // Termin hat Vorrang let loc; try { loc = await API.getLocation({ timeout: 5000, maximumAge: 300000 }); } catch { return; } - const d = 0.018; // ~2 km in Grad - let pois; + // Täglich stabile, aber rotierende Distanz + Variante + const dayIdx = Math.floor(Date.now() / 86400000); + const km = [2, 4, 6][dayIdx % 3]; + const seed = dayIdx % 5; + + let result; try { - pois = await API.osm.pois('bank', loc.lat - d, loc.lon - d, loc.lat + d, loc.lon + d); + result = await API.post('/routes/suggest', { lat: loc.lat, lon: loc.lon, distance_km: km, seed }); } catch { return; } - - if (!pois || pois.length === 0) return; - - // täglich stabile Auswahl, aber täglich andere Bank - const dayIdx = Math.floor(Date.now() / 86400000); - const bench = pois[dayIdx % pois.length]; - const distM = Math.round(_haversine(loc.lat, loc.lon, bench.lat, bench.lon)); - const distTxt = distM < 1000 ? `${distM} m` : `${(distM / 1000).toFixed(1)} km`; - const name = bench.name || 'Bank'; + if (!result?.gps_track?.length) return; const chipsRow = _container.querySelector('#wc-chips-row'); if (!chipsRow) return; - // Chip ggf. schon da (Termin-Chip) oder neu einfügen (nach Chip 1) let chip2 = _container.querySelector('#wc-chip-mid'); if (!chip2) { chip2 = document.createElement('div'); - chip2.className = 'wc-chip'; - chip2.id = 'wc-chip-mid'; - // nach erstem Chip einfügen + chip2.className = 'wc-chip'; + chip2.id = 'wc-chip-mid'; const first = chipsRow.querySelector('.wc-chip'); first ? first.after(chip2) : chipsRow.prepend(chip2); } - chip2.dataset.nav = 'map'; + const durStr = result.dauer_min < 60 + ? `${result.dauer_min} min` + : `${Math.floor(result.dauer_min / 60)}h ${result.dauer_min % 60}min`; chip2.innerHTML = ` Gassirunde - ${UI.escape(name)} · ${distTxt}`; - chip2.addEventListener('click', () => App.navigate('map')); - } - - function _haversine(lat1, lon1, lat2, lon2) { - const R = 6371000; - const f1 = lat1 * Math.PI / 180, f2 = lat2 * Math.PI / 180; - const df = (lat2 - lat1) * Math.PI / 180, dl = (lon2 - lon1) * Math.PI / 180; - const a = Math.sin(df/2)**2 + Math.cos(f1)*Math.cos(f2)*Math.sin(dl/2)**2; - return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + ${result.distanz_km} km · ${durStr}`; + chip2.addEventListener('click', () => { + App.navigate('routes', true, { _suggestResult: result, _suggestKm: km, _suggestSeed: seed }); + }); } // ---------------------------------------------------------- @@ -473,7 +463,7 @@ window.Page_welcome = (() => { API.dogs.welcomeDashboard(dog.id).then(dash => { _updateHeroFromDash(dash, dog); _updateChipsFromDash(dash); - _tryBenchChip(dash); // nach Chips-Update: ggf. mit naher Bank ersetzen + _tryRouteChip(dash); // nach Chips-Update: ggf. Gassirunden-Vorschlag einfügen }).catch(() => { /* Skeleton bleibt sichtbar */ }); } } diff --git a/backend/static/sw.js b/backend/static/sw.js index 4017bfe..f762bef 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-v478'; +const CACHE_VERSION = 'by-v479'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten