diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index d2655e8..bf0f697 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 = '690'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '692'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
diff --git a/backend/static/js/pages/wetter.js b/backend/static/js/pages/wetter.js
index d9fbab2..538b64f 100644
--- a/backend/static/js/pages/wetter.js
+++ b/backend/static/js/pages/wetter.js
@@ -325,6 +325,9 @@ window.Page_wetter = (() => {
+
+ ${_gassiScoreBadge(d)}
+
${sunriseStr && sunsetStr ? `
@@ -528,11 +531,23 @@ window.Page_wetter = (() => {
if (!d) return;
const _POLLEN_NAMES = { erle:'Erle', birke:'Birke', graeser:'Gräser', beifuss:'Beifuß', ambrosia:'Ambrosia' };
- let html = `
-
- Hunde-Wetter
-
`;
+ const _wl = _dogWeatherLabel(d);
+ let html = `
+
+
${_wl.emoji}
+
+ ${_esc(_wl.label)}
+
+
+ ${_esc(_wl.sub)}
+
+
+
+
+ Hunde-Hinweise
+
`;
// Asphalt-Temperatur
if (d.asphalt_temp != null) {
@@ -638,6 +653,16 @@ window.Page_wetter = (() => {
`;
}
+ // Schnüffel-Index + Hunde-Alter Chips
+ const ageYears = _dogAgeYears();
+ html += _dogAgeChip(ageYears);
+
+ html += `
+
+ ${_schnueffelChip(d)}
+
+ `;
+
// Wenn keine Hunde-Daten vorhanden
if (!d.asphalt_temp && !d.paw_cold && !d.thunderstorm
&& !d.zecken && !(pollen && Object.keys(pollen).length)) {
@@ -651,6 +676,160 @@ window.Page_wetter = (() => {
el.innerHTML = html;
}
+ // ----------------------------------------------------------
+ // GASSI-SCORE (1–10)
+ // ----------------------------------------------------------
+ function _gassiScore(d) {
+ let score = 10;
+ const temp = d.temp_max ?? 20;
+ const precip = d.precip_prob ?? 0;
+ const wind = d.windspeed_max ?? 0;
+ const asphalt = d.asphalt_temp ?? 0;
+
+ // Temperatur (ideal: 10–20°C)
+ if (temp > 30) score -= 3;
+ else if (temp > 25) score -= 1;
+ else if (temp < 0) score -= 3;
+ else if (temp < 5) score -= 1;
+
+ // Regen
+ if (precip > 70) score -= 3;
+ else if (precip > 40) score -= 2;
+ else if (precip > 20) score -= 1;
+
+ // Wind
+ if (wind > 60) score -= 2;
+ else if (wind > 40) score -= 1;
+
+ // Asphalt
+ if (asphalt > 55) score -= 2;
+ else if (asphalt > 45) score -= 1;
+
+ // Gewitter
+ if (d.thunderstorm) score -= 3;
+
+ return Math.max(1, Math.min(10, score));
+ }
+
+ function _gassiScoreBadge(d) {
+ const score = _gassiScore(d);
+ let color, text;
+ if (score >= 8) {
+ color = '#10B981';
+ text = 'Toller Gassi-Tag!';
+ } else if (score >= 5) {
+ color = '#F59E0B';
+ text = 'Geht so';
+ } else {
+ color = '#EF4444';
+ text = 'Lieber drinbleiben';
+ }
+ return `
+
+ 🐾 Gassi-Score
+
+ ${score}
+
+ / 10
+ — ${_esc(text)}
+
+ `;
+ }
+
+ // ----------------------------------------------------------
+ // SCHNÜFFEL-INDEX
+ // ----------------------------------------------------------
+ function _schnueffelIndex(d) {
+ const temp = d.temp_max ?? 20;
+ const precip = d.precip_prob ?? 0;
+
+ // Feuchtigkeit aus precip_prob ableiten
+ const feucht = precip > 60 ? 'feucht' : precip > 30 ? 'leicht-feucht' : 'trocken';
+
+ if (feucht === 'feucht' && temp >= 10 && temp <= 18)
+ return { label:'Exzellent 👃', color:'#10B981' };
+ if (feucht === 'feucht' && temp > 10 && temp <= 22)
+ return { label:'Sehr gut 👃', color:'#34D399' };
+ if (temp < 5)
+ return { label:'Gut (kalte Luft trägt Gerüche)', color:'#60A5FA' };
+ if (feucht === 'leicht-feucht' && temp >= 10 && temp <= 22)
+ return { label:'Gut 👃', color:'#4CAF50' };
+ if (feucht === 'trocken' && temp > 25)
+ return { label:'Schwach', color:'#94A3B8' };
+ return { label:'Mittel', color:'#F59E0B' };
+ }
+
+ function _schnueffelChip(d) {
+ const s = _schnueffelIndex(d);
+ return `
+
+
+ Schnüffel: ${_esc(s.label)}
+
+ `;
+ }
+
+ // ----------------------------------------------------------
+ // HUNDE-ALTER aus appState
+ // ----------------------------------------------------------
+ function _dogAgeYears() {
+ try {
+ const dog = _appState?.activeDog || _appState?.dog || _appState?.active_dog;
+ if (!dog) return null;
+ const geb = dog.geburtsdatum || dog.birthdate;
+ if (!geb) return null;
+ const birth = new Date(geb);
+ if (isNaN(birth)) return null;
+ const now = new Date();
+ let age = now.getFullYear() - birth.getFullYear();
+ const m = now.getMonth() - birth.getMonth();
+ if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) age--;
+ return age < 0 ? 0 : age;
+ } catch { return null; }
+ }
+
+ function _dogAgeChip(ageYears) {
+ if (ageYears === null) return '';
+ if (ageYears < 1) {
+ return `
+
+
+
+ Welpe — kurze Spaziergänge, max. 15 Min bei Hitze.
+ Gelenke und Pfoten besonders schonen.
+
+
+ `;
+ }
+ if (ageYears >= 8) {
+ return `
+
+
+
+ Seniorhund — Hitze und Kälte vermeiden, kurze Runden bevorzugen.
+ Auf Gelenkbeschwerden achten.
+
+
+ `;
+ }
+ return '';
+ }
+
// ----------------------------------------------------------
// HILFSFUNKTIONEN — Wetter
// ----------------------------------------------------------
@@ -695,6 +874,49 @@ window.Page_wetter = (() => {
return '#F44336'; // level 4+
}
+ function _dogWeatherLabel(d) {
+ const temp = d.temp_max ?? 20;
+ const tempMin = d.temp_min ?? temp;
+ const precip = d.precip_prob ?? 0;
+ const wind = d.windspeed_max ?? 0;
+ const asphalt = d.asphalt_temp ?? 0;
+ const wcode = d.weathercode ?? 0;
+ const isSnow = wcode >= 71 && wcode <= 77;
+ if (d.thunderstorm)
+ return { label:'Gewitterangst-Wetter', sub:'Angsthasen lieber zu Hause lassen', emoji:'⛈️', color:'#7C3AED' };
+ if (isSnow && temp < 3)
+ return { label:'Schnee-Toben-Wetter', sub:'Pudel im Schnee — der Klassiker', emoji:'❄️', color:'#38BDF8' };
+ if (isSnow)
+ return { label:'Matschpfoten-Wetter', sub:'Pfoten nach der Runde gut abtrocknen', emoji:'🌨️', color:'#60A5FA' };
+ if (tempMin < 0 && precip < 30)
+ return { label:'Kristallklare Nasenluft', sub:'Kalt aber herrlich — Schnüffeln auf Maximum', emoji:'🌡️', color:'#60A5FA' };
+ if (temp < 5 && precip > 50)
+ return { label:'Kuschelwetter', sub:'Kurze Runde, dann ab auf das Sofa', emoji:'🏠', color:'#6B7280' };
+ if (temp < 5)
+ return { label:'Fellkuschelwetter', sub:'Frisch und klar — ideal für aktive Rassen', emoji:'🧣', color:'#93C5FD' };
+ if (temp > 28 && asphalt > 50)
+ return { label:'Pfoten-Alarm!', sub:'Asphalt zu heiß — früh morgens oder abends raus', emoji:'🔥', color:'#EF4444' };
+ if (temp > 28)
+ return { label:'Schwimm-Wetter', sub:'Bach oder See suchen — Hunde überhitzen schnell', emoji:'🏊', color:'#F97316' };
+ if (precip > 70 && temp < 15)
+ return { label:'Nass-Hund-Wetter', sub:'Handtuch bereit? Der Geruch kommt garantiert', emoji:'💧', color:'#3B82F6' };
+ if (precip > 70)
+ return { label:'Warm-Dusch-Wetter', sub:'Wer braucht noch ein Bad — der Regen übernimmt', emoji:'🌧️', color:'#60A5FA' };
+ if (precip > 30 && temp >= 10 && temp <= 20)
+ return { label:'Schnüffel-Wetter', sub:'Feuchte Luft = Nasenarbeit pur — Gerüche lieben das', emoji:'👃', color:'#34D399' };
+ if (wind > 50)
+ return { label:'Sturmfrisur-Wetter', sub:'Fell in alle Richtungen — Leine gut festhalten', emoji:'🌬️', color:'#A78BFA' };
+ if (wind > 30 && temp >= 15)
+ return { label:'Ohren-im-Wind-Wetter', sub:'Optimal für Hunde mit Schlappohren', emoji:'💨', color:'#A78BFA' };
+ if (precip > 30 && precip <= 70)
+ return { label:'Gassiregen-Wetter', sub:'Leichte Jacke, kurze Runde — Hund findet es gut', emoji:'🌦️', color:'#60A5FA' };
+ if (temp >= 18 && temp <= 26 && precip < 20)
+ return { label:'Perfektes Gassi-Wetter',sub:'Heute müssen alle Routen genossen werden', emoji:'🐾', color:'#10B981' };
+ if (temp >= 10 && temp < 18 && precip < 30)
+ return { label:'Klassisches Hunde-Wetter', sub:'Nicht zu warm, nicht zu kalt — Vierbeiner-Paradies', emoji:'🐕', color:'#4CAF50' };
+ return { label:'Gutes Hunde-Wetter', sub:'Raus mit dem Hund!', emoji:'🐶', color:'#10B981' };
+ }
+
function _tickLevel(risk) {
const r = (risk || '').toLowerCase();
if (r === 'niedrig') return ['niedrig', '#4CAF50'];
diff --git a/backend/static/sw.js b/backend/static/sw.js
index ca60ee8..894a342 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-v690';
+const CACHE_VERSION = 'by-v692';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache