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