/* ============================================================ BAN YARO — Wetter (7-Tage-Wettervorhersage) Seiten-Modul: Hunde-optimierte Wettervorhersage mit GPS. ============================================================ */ window.Page_wetter = (() => { // ---------------------------------------------------------- // KONSTANTEN // ---------------------------------------------------------- // WMO-Code → Phosphor-Icon-Name (aus Sprite) const WMO_ICON = { 0:'sun', 1:'sun-dim', 2:'cloud-sun', 3:'cloud', 45:'cloud-fog', 48:'cloud-fog', 51:'cloud-rain', 53:'cloud-rain', 55:'cloud-rain', 61:'cloud-rain', 63:'cloud-rain', 65:'cloud-rain', 71:'cloud-snow', 73:'cloud-snow', 75:'cloud-snow', 77:'snowflake', 80:'rainbow-cloud', 81:'cloud-rain', 82:'cloud-rain', 85:'cloud-snow', 86:'cloud-snow', 95:'cloud-lightning', 96:'cloud-lightning', 99:'cloud-lightning', }; // Farben passend zum Wetter (für Icon-Tinting) const WMO_COLOR = { 0:'#F59E0B', 1:'#F59E0B', 2:'#94A3B8', 3:'#64748B', 45:'#94A3B8', 48:'#94A3B8', 51:'#60A5FA', 53:'#3B82F6', 55:'#2563EB', 61:'#3B82F6', 63:'#2563EB', 65:'#1D4ED8', 71:'#BAE6FD', 73:'#7DD3FC', 75:'#38BDF8', 77:'#BAE6FD', 80:'#60A5FA', 81:'#3B82F6', 82:'#2563EB', 85:'#7DD3FC', 86:'#38BDF8', 95:'#7C3AED', 96:'#6D28D9', 99:'#5B21B6', }; function _wmoIcon(code, size = '2rem', extraStyle = '') { const name = WMO_ICON[code] || 'cloud'; const color = WMO_COLOR[code] || 'var(--c-text-secondary)'; return ``; } const WMO_DESC = { 0:'Klarer Himmel', 1:'Überwiegend klar', 2:'Teilweise bewölkt', 3:'Bedeckt', 45:'Nebel', 48:'Gefrierender Nebel', 51:'Leichter Sprühregen', 53:'Mäßiger Sprühregen', 55:'Starker Sprühregen', 61:'Leichter Regen', 63:'Mäßiger Regen', 65:'Starker Regen', 71:'Leichter Schneefall', 73:'Mäßiger Schneefall', 75:'Starker Schneefall', 77:'Schneekörner', 80:'Leichte Regenschauer', 81:'Mäßige Regenschauer', 82:'Starke Regenschauer', 85:'Leichte Schneeschauer', 86:'Starke Schneeschauer', 95:'Gewitter', 96:'Gewitter mit leichtem Hagel', 99:'Gewitter mit starkem Hagel' }; const DAY_NAMES = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- let _container = null; let _appState = null; let _data = null; let _selDay = 0; let _loading = false; let _recordsLoaded = false; // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; _selDay = 0; _renderShell(); _tryAutoLocate(); } async function refresh() { _selDay = 0; _recordsLoaded = false; _renderShell(); _tryAutoLocate(); } function _renderShell() { _container.innerHTML = `
${_wmoIcon(2, '2.5rem')}

Standort wird ermittelt…

`; } async function _tryAutoLocate() { try { const pos = await API.getLocation({ timeout: 8000, maximumAge: 300_000 }); await _loadData(pos.lat, pos.lon); } catch (err) { _showLocationError(err?.code); } } function _showLocationError(errCode) { const body = _container?.querySelector('#wttr-body'); if (!body) return; const isLoggedIn = !!_appState?.user; const isDenied = errCode === 1; // GeolocationPositionError.PERMISSION_DENIED const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent); const deniedHint = isDenied ? `
Standort-Zugriff blockiert
${isIos ? `
Wichtig: Die App läuft getrennt von Safari — Safari-Einstellungen gelten hier nicht.
  1. Öffne Einstellungen → Datenschutz & Sicherheit → Ortungsdienste
  2. Scrolle ganz nach unten zu Ban Yaro (nicht Safari!)
  3. Wähle „Beim Verwenden der App"
  4. Komm zurück und tippe nochmal auf den Button
Letzter Ausweg: Einstellungen → Apps → Safari → Erweitert → Website-Daten → banyaro.app → löschen. Danach nochmal öffnen und Button tippen.
` : `
Klicke auf das Schloss-Symbol in der Adressleiste → StandortErlauben, dann nochmal tippen.
`}
` : ''; body.innerHTML = `
${deniedHint}
🌤️🐾

Das Gassi-Wetter wartet auf dich

Erfahre sekundengenau, ob gerade der perfekte Moment für eine Runde ist — zugeschnitten auf dich und deinen Hund.

${[ ['sun', '#F59E0B', 'Gassi-Score 1–10', 'Wetter bewertet nach Temperatur, Regen und Wind'], ['thermometer', '#3B82F6', '7-Tage-Vorschau', 'Plane deine Runden für die ganze Woche voraus'], ['drop', '#06B6D4', 'Regenradar stündlich', '24h-Niederschlagstimeline auf einen Blick'], ['trophy', '#10B981', 'Wetter-Rekorde', 'Wärmster, nassester und stürmischster Gassi-Tag'], ].map(([icon, color, title, sub]) => `
${title}
${sub}
`).join('')}
${!isLoggedIn ? `

Mit Account werden Rekorde & Gassi-Score für deinen Hund gespeichert.

` : ''}
`; body.querySelector('#wttr-btn-retry')?.addEventListener('click', () => { _renderShell(); _tryAutoLocate(); }); body.querySelector('#wttr-btn-login')?.addEventListener('click', () => { if (window.App) App.navigate('settings'); }); } // ---------------------------------------------------------- // DATEN LADEN // ---------------------------------------------------------- async function _loadData(lat, lon) { if (_loading) return; _loading = true; try { _data = await API.weather.forecast(lat, lon); _selDay = 0; _renderWeather(); } catch { const body = _container.querySelector('#wttr-body'); if (body) body.innerHTML = `
⚠️

Wetter nicht verfügbar

Die Wetterdaten konnten nicht geladen werden.

`; body?.querySelector('#wttr-btn-reload')?.addEventListener('click', () => { refresh(); }); } finally { _loading = false; } } // ---------------------------------------------------------- // HAUPT-RENDER // ---------------------------------------------------------- function _renderWeather() { const body = _container.querySelector('#wttr-body'); if (!body || !_data) return; const days = _data.days || []; if (!days.length) return; body.innerHTML = `
${days.map((d, i) => _dayCard(d, i)).join('')}
`; // Strip-Klick-Events body.querySelectorAll('[data-wttr-day]').forEach(card => { card.addEventListener('click', () => { _selDay = parseInt(card.dataset.wttrDay); _updateStrip(); _renderDetail(); _renderRainTimeline(); _renderDog(); }); }); _renderDetail(); _renderRainTimeline(); _renderDog(); _loadRecords(); } // ---------------------------------------------------------- // STRIP AKTUALISIEREN (aktiver Tag) // ---------------------------------------------------------- function _updateStrip() { const body = _container.querySelector('#wttr-body'); if (!body) return; const days = _data?.days || []; body.querySelectorAll('[data-wttr-day]').forEach((card, i) => { const active = i === _selDay; card.style.background = active ? 'var(--c-primary)' : 'var(--c-bg-card)'; card.style.color = active ? '#fff' : 'var(--c-text)'; card.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)'; card.style.transform = active ? 'translateY(-2px)' : ''; card.style.boxShadow = active ? '0 4px 12px rgba(196,132,58,0.3)' : '0 1px 3px rgba(0,0,0,0.07)'; // Temperatur-Farbe im aktiven Zustand const tempEl = card.querySelector('.wttr-temp'); if (tempEl) tempEl.style.color = active ? 'rgba(255,255,255,0.85)' : 'var(--c-text-secondary)'; const precipEl = card.querySelector('.wttr-precip'); if (precipEl) precipEl.style.color = active ? 'rgba(255,255,255,0.75)' : 'var(--c-text-secondary)'; }); } // ---------------------------------------------------------- // TAG-KARTE (Strip) // ---------------------------------------------------------- function _dayCard(d, i) { const active = i === _selDay; const dateObj = new Date(d.date); const dayName = i === 0 ? 'Heute' : DAY_NAMES[dateObj.getDay()]; const bg = active ? 'var(--c-primary)' : 'var(--c-bg-card)'; const col = active ? '#fff' : 'var(--c-text)'; const shadow = active ? '0 4px 12px rgba(196,132,58,0.3)' : '0 1px 3px rgba(0,0,0,0.07)'; const border = active ? 'var(--c-primary)' : 'var(--c-border)'; const transform = active ? 'translateY(-2px)' : ''; const textSec = active ? 'rgba(255,255,255,0.85)' : 'var(--c-text-secondary)'; const textMut = active ? 'rgba(255,255,255,0.75)' : 'var(--c-text-secondary)'; return `
${_esc(dayName)}
${_wmoIcon(d.weathercode, '1.5rem', active ? 'filter:brightness(0) invert(1)' : '')}
${Math.round(d.temp_max)}°/${Math.round(d.temp_min)}° ${d.precip_prob ?? 0}%
`; } // ---------------------------------------------------------- // DETAIL-CARD // ---------------------------------------------------------- function _renderDetail() { const el = _container.querySelector('#wttr-detail'); if (!el || !_data) return; const d = (_data.days || [])[_selDay]; if (!d) return; const desc = WMO_DESC[d.weathercode] || ''; const [uvLabel, uvColor] = _uvLabel(d.uv_index ?? 0); const uvPct = Math.min(100, ((d.uv_index ?? 0) / 11) * 100); const bft = _beaufort(d.wind_kmh ?? 0); const windDir = d.wind_dir_deg ?? 0; const compass = d.wind_dir ?? _compass(windDir); // Sunrise/Sunset Balken const now = new Date(); const sunriseStr = d.sunrise || ''; const sunsetStr = d.sunset || ''; let sunPct = 0; if (sunriseStr && sunsetStr) { const [rH, rM] = sunriseStr.split(':').map(Number); const [sH, sM] = sunsetStr.split(':').map(Number); const riseMin = rH * 60 + rM; const setMin = sH * 60 + sM; const curMin = now.getHours() * 60 + now.getMinutes(); sunPct = _selDay === 0 ? Math.min(100, Math.max(0, ((curMin - riseMin) / (setMin - riseMin)) * 100)) : 0; } el.innerHTML = `
${_wmoIcon(d.weathercode, '3.5rem')}
${_esc(desc)}
${Math.round(d.temp_max)}° / ${Math.round(d.temp_min)}°
${d.feels_max != null ? `
Gefühlt ${Math.round(d.feels_max)}° / ${Math.round(d.feels_min ?? d.feels_max)}°
` : ''}
${_gassiScoreBadge(d)} ${sunriseStr && sunsetStr ? `
${_esc(sunriseStr)} ${_esc(sunsetStr)}
` : ''}
${UI.icon('arrow-up')}
${_esc(compass)} · ${Math.round(d.windspeed_max ?? 0)} km/h
${_esc(bft)}
${d.precip_sum != null ? `
${d.precip_sum} mm
Niederschlag
` : ''}
UV-Index ${d.uv_index ?? 0} — ${_esc(uvLabel)}
`; } // ---------------------------------------------------------- // NIEDERSCHLAGS-ZEITSKALA (stündlich) // ---------------------------------------------------------- function _renderRainTimeline() { const el = _container.querySelector('#wttr-rain'); if (!el || !_data) return; const d = (_data.days || [])[_selDay]; if (!d) return; const hourly = d.hourly || []; // Filtere auf Stunden mit Daten, die eine Niederschlagswahrscheinlichkeit haben const entries = hourly.filter(h => h.precip_prob != null); if (!entries.length) { el.style.display = 'none'; return; } el.style.display = ''; // Für "Heute" (Tag 0): ab jetzt, sonst alle 24h const now = new Date(); const nowMin = now.getHours() * 60 + now.getMinutes(); let slots = entries; if (_selDay === 0) { // Zeige ab der aktuellen Stunde (und die letzten 2h als Kontext) const pastCutoff = now.getHours() - 2; slots = entries.filter(h => { const hHour = parseInt(h.hour.split(':')[0]); return hHour >= pastCutoff; }); // Falls nichts übrig bleibt, zeige alles if (!slots.length) slots = entries; } // Max probability für Skalierung (mindestens 30 damit die Balken sichtbar sind) const maxProb = Math.max(30, ...slots.map(h => h.precip_prob ?? 0)); // Farb-Funktion: blau basierend auf Wahrscheinlichkeit function _rainColor(prob) { if (prob < 10) return 'rgba(148,163,184,0.4)'; // grau, kaum Regen if (prob < 25) return 'rgba(147,197,253,0.65)'; // hellblau if (prob < 50) return 'rgba(96,165,250,0.8)'; // blau if (prob < 75) return 'rgba(59,130,246,0.9)'; // kräftig blau return 'rgba(29,78,216,1)'; // dunkelblau } // Aktuell aktiver Slot (nur bei Heute) const currentHour = now.getHours(); const bars = slots.map(h => { const prob = h.precip_prob ?? 0; const hHour = parseInt(h.hour.split(':')[0]); const isNow = _selDay === 0 && hHour === currentHour; const barH = Math.max(2, Math.round((prob / 100) * 56)); // max 56px Balkenhöhe const color = _rainColor(prob); const labelHour = h.hour.substring(0, 2); // 'HH' return `
${prob >= 20 ? prob + '%' : ''}
${isNow ? 'jetzt' : labelHour + 'h'}
`; }).join(''); // Gibt es überhaupt nennenswerten Niederschlag? const hasRain = slots.some(h => (h.precip_prob ?? 0) >= 10); const titleColor = hasRain ? '#60A5FA' : 'var(--c-text-secondary)'; const titleIcon = hasRain ? 'cloud-rain' : 'cloud'; el.innerHTML = `
Niederschlagswahrscheinlichkeit ${_selDay === 0 ? 'heute' : _esc(d.date ? new Date(d.date + 'T12:00').toLocaleDateString('de', {weekday:'short', day:'numeric', month:'short'}) : '')}
${bars}
${!hasRain ? `
Kein Regen erwartet
` : ''} `; // Scroll zum aktuellen Slot wenn Heute if (_selDay === 0) { requestAnimationFrame(() => { const wrap = el.querySelector('div[style*="overflow-x"]'); if (!wrap) return; const nowIdx = slots.findIndex(h => parseInt(h.hour.split(':')[0]) === currentHour); if (nowIdx > 2) { wrap.scrollLeft = (nowIdx - 2) * 41; // ca. 38px + 3px gap } }); } } // ---------------------------------------------------------- // HUNDE-WETTER // ---------------------------------------------------------- function _renderDog() { const el = _container.querySelector('#wttr-dog'); if (!el || !_data) return; const d = (_data.days || [])[_selDay]; if (!d) return; const _POLLEN_NAMES = { erle:'Erle', birke:'Birke', graeser:'Gräser', beifuss:'Beifuß', ambrosia:'Ambrosia' }; const felltyp = (_appState?.activeDog ?? _appState?.dogs?.[0])?.fell_typ || null; const _wl = _dogWeatherLabel(d, felltyp); let html = `
${_wl.emoji}
${_esc(_wl.label)}
${_esc(_wl.sub)}

Hunde-Hinweise

`; // Asphalt-Temperatur if (d.asphalt_temp != null) { const [aspText, aspColor, aspAdvice] = _asphaltLevel(d.asphalt_temp); html += `
Asphalt ~${Math.round(d.asphalt_temp)}°C — ${_esc(aspText)}
${aspAdvice ? `
${_esc(aspAdvice)}
` : ''}
`; } // Pfoten-Kälteschutz if (d.paw_cold) { html += `
Kälteschutz für Pfoten: Eis und Streusalz können die Pfoten reizen. Pfotenpflege empfohlen.
`; } // Gewitter if (d.thunderstorm) { html += `
Gewitter erwartet: Hunde können auf Gewitter sensibel reagieren. Sichere Umgebung schaffen.
`; } // Pollenflug const pollen = d.pollen; if (pollen && typeof pollen === 'object' && Object.keys(pollen).length) { const pollenEntries = Object.entries(pollen) .filter(([, v]) => v != null && v.level > 0); if (pollenEntries.length) { html += `
Pollenflug
${pollenEntries.map(([key, lvlObj]) => { const col = _pollenColor(lvlObj?.level ?? 0); const name = _POLLEN_NAMES[key] || key; const lbl = lvlObj?.label || ''; return ` ${_esc(name)}: ${_esc(lbl)} `; }).join('')}
`; } } // Zecken if (d.zecken != null) { const [tickLabel, tickColor] = _tickLevel(d.zecken); html += `
Zecken-Risiko: ${_esc(tickLabel)}
`; } // Fell-spezifische Hinweise if (felltyp) { const tempNow = d.temp_max ?? 20; let fellHint = null; if (felltyp === 'doppel' && tempNow > 20) { fellHint = { icon: 'thermometer-hot', color: '#F97316', text: 'Doppeltes Fell — heute besonders auf Überhitzung achten.' }; } else if (felltyp === 'nackt' && tempNow < 15) { fellHint = { icon: 'coat-hanger', color: '#60A5FA', text: 'Nackthund braucht heute eine Hundejacke oder einen -pullover.' }; } else if (felltyp === 'kurz' && tempNow < 5) { fellHint = { icon: 'snowflake', color: '#38BDF8', text: 'Kurzhaar friert schnell — Hundemantel empfohlen.' }; } if (fellHint) { html += `
${_esc(fellHint.text)}
`; } } // 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)) { html += `

Keine besonderen Hinweise für heute.

`; } 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 // ---------------------------------------------------------- function _beaufort(kmh) { if (kmh < 2) return 'Windstille'; if (kmh < 12) return 'leicht'; if (kmh < 29) return 'mäßig'; if (kmh < 50) return 'frisch'; if (kmh < 62) return 'stark'; if (kmh < 75) return 'stürmisch'; return 'Sturm'; } function _uvLabel(uv) { if (uv <= 2) return ['niedrig', '#4CAF50']; if (uv <= 5) return ['mittel', '#FFC107']; if (uv <= 7) return ['hoch', '#FF9800']; if (uv <= 10) return ['sehr hoch', '#F44336']; return ['extrem', '#9C27B0']; } function _compass(deg) { const dirs = ['N','NO','O','SO','S','SW','W','NW']; return dirs[Math.round(deg / 45) % 8]; } function _asphaltLevel(temp) { if (temp < 40) return ['Pfoten sicher', '#4CAF50', '']; if (temp < 50) return ['leicht erwärmt', '#FFC107', 'Kurze Kontaktzeiten sind unbedenklich.']; if (temp < 60) return ['Vorsicht — Pfoten schützen!', '#FF9800', 'Heiße Oberfläche! Auf Gras ausweichen oder Hundeschuhe verwenden.']; return ['GEFAHR — Verbrennungsgefahr!', '#F44336', 'Asphalt kann Pfoten in Sekunden verbrennen. Spaziergang vermeiden!']; } function _pollenColor(level) { if (level === 0) return '#9E9E9E'; if (level === 1) return '#4CAF50'; if (level === 2) return '#FFC107'; if (level === 3) return '#FF9800'; return '#F44336'; // level 4+ } function _dogWeatherLabel(d, felltyp) { 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; // Fell-spezifische Temperaturschwellen const heatLimit = { kurz: 25, mittel: 27, lang: 22, drahtaar: 26, doppel: 30, nackt: 20 }[felltyp] ?? 28; const coldLimit = { kurz: 8, mittel: 5, lang: 3, drahtaar: 5, doppel: -5, nackt: 15 }[felltyp] ?? 5; 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 < coldLimit && precip > 50) return { label:'Kuschelwetter', sub:'Kurze Runde, dann ab auf das Sofa', emoji:'🏠', color:'#6B7280' }; if (temp < coldLimit) return { label:'Fellkuschelwetter', sub:'Frisch und klar — ideal für aktive Rassen', emoji:'🧣', color:'#93C5FD' }; if (temp > heatLimit && asphalt > 50) return { label:'Pfoten-Alarm!', sub:'Asphalt zu heiß — früh morgens oder abends raus', emoji:'🔥', color:'#EF4444' }; if (temp > heatLimit) 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']; if (r === 'mittel') return ['mittel', '#FF9800']; return ['hoch', '#F44336']; } function _esc(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ---------------------------------------------------------- // MEINE WETTERREKORDE // ---------------------------------------------------------- async function _loadRecords() { // Nur wenn User eingeloggt if (!_appState?.user) return; // Nur einmal pro Seitenaufruf laden if (_recordsLoaded) return; _recordsLoaded = true; try { const res = await API.get('/weather/records'); _renderRecords(res?.records || null); } catch { // Stumm scheitern — Rekorde sind ein Nice-to-have } } function _fmtDate(datum) { if (!datum) return ''; try { return new Date(datum + 'T12:00').toLocaleDateString('de', { day: 'numeric', month: 'short', year: 'numeric' }); } catch { return datum; } } function _recordCard(emoji, title, value, subtitle, color) { return `
${emoji} ${_esc(title)}
${_esc(value)}
${_esc(subtitle)}
`; } function _renderRecords(records) { const el = _container.querySelector('#wttr-records'); if (!el) return; // Mindestens 3 Einträge nötig if (!records || (records.gesamt_eintraege || 0) < 3) { el.innerHTML = ''; return; } const cards = []; if (records.kaeltester) { const e = records.kaeltester; const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum); cards.push(_recordCard('🥶', 'Kältester Gassi', `${Math.round(e.temp_c)}°C`, sub, '#60A5FA')); } if (records.heissester) { const e = records.heissester; const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum); cards.push(_recordCard('🔥', 'Heißester Gassi', `${Math.round(e.temp_c)}°C`, sub, '#EF4444')); } if (records.stuermischster) { const e = records.stuermischster; const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum); cards.push(_recordCard('🌬️', 'Stürmischster Tag', `${Math.round(e.wind_kmh)} km/h`, sub, '#A78BFA')); } const regenCount = records.regen_eintraege || 0; const gesamt = records.gesamt_eintraege || 0; cards.push(_recordCard('💧', 'Regentage', `${regenCount} Einträge`, `von ${gesamt} Tagebucheinträgen`, '#3B82F6')); el.innerHTML = `

Meine Wetterrekorde

${cards.join('')}
`; } // ---------------------------------------------------------- // PUBLIC API // ---------------------------------------------------------- return { init, refresh }; })();