/* ============================================================
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…
`;
}
const LS_LAST_POS = 'by_last_position';
async function _tryAutoLocate() {
// Letzte bekannte Position als Fallback einlesen (für offline / GPS-verweigert)
let cached = null;
try {
const raw = localStorage.getItem(LS_LAST_POS);
if (raw) cached = JSON.parse(raw);
} catch {}
try {
const pos = await API.getLocation({ timeout: 8000, maximumAge: 300_000 });
try {
localStorage.setItem(LS_LAST_POS, JSON.stringify({
lat: pos.lat, lon: pos.lon, ts: Date.now(),
}));
} catch {}
await _loadData(pos.lat, pos.lon);
} catch (err) {
// GPS fehlgeschlagen → letzte bekannte Position nutzen (statt Error-Banner)
if (cached?.lat != null && cached?.lon != null) {
await _loadData(cached.lat, cached.lon);
} else {
_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.
- Öffne Einstellungen → Datenschutz & Sicherheit → Ortungsdienste
- Scrolle ganz nach unten zu Ban Yaro (nicht Safari!)
- Wähle „Beim Verwenden der App"
- 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 → Standort → Erlauben, 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]) => `
`).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;
}
const locName = _data.location_name ? `${_esc(_data.location_name)}
` : '';
el.innerHTML = `
${locName}
${_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.wind_kmh ?? 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'}) : '')}
${!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 += `
`;
}
}
// 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.wind_kmh ?? 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.wind_kmh ?? 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 };
})();