Feature: Wetter — Gassi-Score, Schnüffel-Index, Hunde-Alter-Hinweis (SW by-v692)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rene 2026-05-04 20:18:38 +02:00
parent 759979ffce
commit 471633867c
3 changed files with 229 additions and 7 deletions

View file

@ -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';

View file

@ -325,6 +325,9 @@ window.Page_wetter = (() => {
</div>
</div>
<!-- Gassi-Score -->
${_gassiScoreBadge(d)}
<!-- Sonnenaufgang / -untergang -->
${sunriseStr && sunsetStr ? `
<div style="margin-bottom:var(--space-4)">
@ -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 = `<h3 style="font-size:var(--text-base);font-weight:700;
margin-bottom:var(--space-4)">
<svg class="ph-icon" style="width:1.1em;height:1.1em;vertical-align:-2px;color:var(--c-primary)"><use href="/icons/phosphor.svg#paw-print"></use></svg>
Hunde-Wetter
</h3>`;
const _wl = _dogWeatherLabel(d);
let html = `
<div style="border-radius:var(--radius);padding:var(--space-4);
background:${_wl.color}18;border:1px solid ${_wl.color}44;
margin-bottom:var(--space-4);text-align:center">
<div style="font-size:2rem;line-height:1;margin-bottom:4px">${_wl.emoji}</div>
<div style="font-weight:800;font-size:var(--text-lg);color:${_wl.color};line-height:1.2">
${_esc(_wl.label)}
</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px">
${_esc(_wl.sub)}
</div>
</div>
<h3 style="font-size:var(--text-base);font-weight:700;margin-bottom:var(--space-4)">
<svg class="ph-icon" style="width:1.1em;height:1.1em;vertical-align:-2px;color:var(--c-primary)"><use href="/icons/phosphor.svg#paw-print"></use></svg>
Hunde-Hinweise
</h3>`;
// 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 += `
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-3)">
${_schnueffelChip(d)}
</div>
`;
// 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 (110)
// ----------------------------------------------------------
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: 1020°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 `
<div style="display:flex;align-items:center;justify-content:center;
gap:var(--space-3);margin-bottom:var(--space-4);
padding:var(--space-3) var(--space-4);
border-radius:999px;
background:${color}1a;border:1.5px solid ${color}55">
<span style="font-size:var(--text-xs);font-weight:700;
color:var(--c-text-secondary);white-space:nowrap">🐾 Gassi-Score</span>
<span style="font-size:var(--text-2xl);font-weight:900;color:${color};line-height:1">
${score}
</span>
<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">/&nbsp;10</span>
<span style="font-size:var(--text-xs);font-weight:600;color:${color};
white-space:nowrap"> ${_esc(text)}</span>
</div>
`;
}
// ----------------------------------------------------------
// 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 `
<span style="display:inline-flex;align-items:center;gap:4px;
font-size:var(--text-xs);border-radius:999px;
padding:3px 10px;background:${s.color}22;
border:1px solid ${s.color}55;color:${s.color};font-weight:600">
<svg class="ph-icon" style="width:12px;height:12px"><use href="/icons/phosphor.svg#nose"></use></svg>
Schnüffel: ${_esc(s.label)}
</span>
`;
}
// ----------------------------------------------------------
// 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 `
<div style="display:flex;align-items:flex-start;gap:var(--space-3);
padding:var(--space-3);border-radius:var(--radius);
background:#f59e0b1a;border:1px solid #f59e0b55;
margin-bottom:var(--space-3)">
<svg class="ph-icon" style="width:1.3rem;height:1.3rem;flex-shrink:0;color:#F59E0B"><use href="/icons/phosphor.svg#baby"></use></svg>
<div style="font-size:var(--text-sm)">
<strong>Welpe</strong> kurze Spaziergänge, max. 15 Min bei Hitze.
Gelenke und Pfoten besonders schonen.
</div>
</div>
`;
}
if (ageYears >= 8) {
return `
<div style="display:flex;align-items:flex-start;gap:var(--space-3);
padding:var(--space-3);border-radius:var(--radius);
background:#6b7280 1a;border:1px solid #6b728055;
margin-bottom:var(--space-3)">
<svg class="ph-icon" style="width:1.3rem;height:1.3rem;flex-shrink:0;color:#9CA3AF"><use href="/icons/phosphor.svg#person-simple-walk"></use></svg>
<div style="font-size:var(--text-sm)">
<strong>Seniorhund</strong> Hitze und Kälte vermeiden, kurze Runden bevorzugen.
Auf Gelenkbeschwerden achten.
</div>
</div>
`;
}
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'];