Files
HexaWetter/frontend/app.js
2026-06-18 23:59:08 +02:00

712 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const STORAGE_KEY = "hexawetter_settings";
const FAV_KEY = "hexawetter_favorites";
const el = (id) => document.getElementById(id);
let latestForecast = null;
let map = null;
let mapMarker = null;
let wmsLayers = [];
let layerControl = null;
let osmLayer = null;
let radarLayer = null;
let warningLayer = null;
let radarTimes = [];
let radarFrameIndex = 0;
let radarPlayTimer = null;
let radarPlaying = false;
let hourlyChart = null;
let settings = loadSettings();
let searchTimer = null;
const WEATHER_ICONS = {
0: "☀️", 1: "🌤️", 2: "⛅", 3: "☁️", 45: "🌫️", 48: "🌫️",
51: "🌦️", 53: "🌦️", 55: "🌧️", 61: "🌧️", 63: "🌧️", 65: "🌧️",
71: "🌨️", 73: "🌨️", 75: "❄️", 80: "🌦️", 81: "🌧️", 82: "⛈️",
95: "⛈️", 96: "⛈️", 99: "⛈️",
};
function loadSettings() {
try {
return { theme: "dark", unitTemp: "celsius", unitWind: "kmh", radarMinutes: 120, ...JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}") };
} catch { return { theme: "dark", unitTemp: "celsius", unitWind: "kmh", radarMinutes: 120 }; }
}
function saveSettings() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
function loadFavorites() {
try { return JSON.parse(localStorage.getItem(FAV_KEY) || "[]"); } catch { return []; }
}
function saveFavorites(items) {
localStorage.setItem(FAV_KEY, JSON.stringify(items));
}
function weatherText(code) {
const map = {
0: "Klar", 1: "Überwiegend klar", 2: "Teils bewölkt", 3: "Bewölkt",
45: "Nebel", 48: "Reifnebel", 51: "Leichter Niesel", 53: "Niesel",
55: "Starker Niesel", 61: "Leichter Regen", 63: "Regen", 65: "Starker Regen",
71: "Leichter Schnee", 73: "Schnee", 75: "Starker Schnee", 80: "Regenschauer",
81: "Starke Schauer", 82: "Heftige Schauer", 95: "Gewitter", 96: "Gewitter mit Hagel", 99: "Schweres Gewitter",
};
return `${WEATHER_ICONS[code] || ""} ${map[code] || `Code ${code}`}`.trim();
}
function convertTemp(c) {
if (settings.unitTemp === "fahrenheit") return `${Math.round(c * 9 / 5 + 32)} °F`;
return `${Math.round(c)} °C`;
}
function convertWind(kmh) {
if (settings.unitWind === "ms") return `${(kmh / 3.6).toFixed(1)} m/s`;
if (settings.unitWind === "kn") return `${(kmh * 0.539957).toFixed(1)} kn`;
return `${Math.round(kmh)} km/h`;
}
function tempValue(celsius) {
if (settings.unitTemp === "fahrenheit") return celsius * 9 / 5 + 32;
return celsius;
}
function tempUnitLabel() {
return settings.unitTemp === "fahrenheit" ? "°F" : "°C";
}
function windUnitLabel() {
if (settings.unitWind === "ms") return "m/s";
if (settings.unitWind === "kn") return "kn";
return "km/h";
}
function windValue(kmh) {
if (settings.unitWind === "ms") return kmh / 3.6;
if (settings.unitWind === "kn") return kmh * 0.539957;
return kmh;
}
function chartThemeColors() {
const isLight = document.documentElement.dataset.theme === "light";
return {
text: isLight ? "#5f5380" : "#cfc9dd",
grid: isLight ? "rgba(20, 10, 60, 0.08)" : "rgba(255, 255, 255, 0.08)",
};
}
function destroyHourlyChart() {
if (hourlyChart) {
hourlyChart.destroy();
hourlyChart = null;
}
}
function renderHourlyChart(hourly) {
if (typeof Chart === "undefined" || !hourly?.time?.length) return;
const canvas = el("hourlyChart");
if (!canvas) return;
destroyHourlyChart();
const { text: textColor, grid: gridColor } = chartThemeColors();
const tempUnit = tempUnitLabel();
const windUnit = windUnitLabel();
const labels = hourly.time.slice(0, 48).map((time) =>
new Date(time).toLocaleString("de-DE", { weekday: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" })
);
hourlyChart = new Chart(canvas, {
type: "line",
data: {
labels,
datasets: [
{
type: "bar",
label: "Regenwahrscheinlichkeit",
data: hourly.precipitation_probability.slice(0, 48).map((v) => v ?? 0),
yAxisID: "yRain",
backgroundColor: "rgba(57, 120, 255, 0.42)",
borderColor: "rgba(57, 120, 255, 0.7)",
borderWidth: 1,
borderRadius: 4,
order: 3,
},
{
label: `Temperatur (${tempUnit})`,
data: hourly.temperature_2m.slice(0, 48).map(tempValue),
yAxisID: "yMain",
borderColor: "#ff51f9",
backgroundColor: "rgba(255, 81, 249, 0.14)",
fill: true,
tension: 0.35,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2.5,
order: 1,
},
{
label: `Wind (${windUnit})`,
data: hourly.wind_speed_10m.slice(0, 48).map((v) => windValue(v ?? 0)),
yAxisID: "yMain",
borderColor: "#a348ff",
borderDash: [5, 4],
tension: 0.35,
pointRadius: 0,
borderWidth: 1.5,
fill: false,
order: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: "index", intersect: false },
plugins: {
legend: {
labels: { color: textColor, usePointStyle: true, boxWidth: 10 },
},
tooltip: {
callbacks: {
label(ctx) {
const label = ctx.dataset.label || "";
if (label.startsWith("Regen")) return `${label}: ${ctx.parsed.y}%`;
if (label.startsWith("Temperatur")) return `${label}: ${Math.round(ctx.parsed.y)} ${tempUnit}`;
return `${label}: ${ctx.parsed.y.toFixed(1)} ${windUnit}`;
},
},
},
},
scales: {
x: {
ticks: { color: textColor, maxRotation: 0, autoSkip: true, maxTicksLimit: 14 },
grid: { color: gridColor },
},
yMain: {
position: "left",
ticks: { color: textColor },
grid: { color: gridColor },
},
yRain: {
position: "right",
min: 0,
max: 100,
grid: { drawOnChartArea: false },
ticks: { color: textColor, callback: (v) => `${v}%` },
},
},
},
});
}
async function api(path, options = {}) {
const res = await fetch(path, options);
if (!res.ok) {
const text = await res.text();
throw new Error(`${res.status}: ${text}`);
}
return res.json();
}
function getLocation() {
return {
place: el("place").value || "Standort",
lat: Number(el("lat").value),
lon: Number(el("lon").value),
};
}
function setLocation(loc) {
el("place").value = loc.place;
el("lat").value = loc.lat;
el("lon").value = loc.lon;
}
function formatDateTime(value) {
return new Date(value).toLocaleString("de-DE", {
weekday: "short", day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit",
});
}
function metric(label, value, hint = "") {
return `<div class="metric"><span>${label}</span><strong>${value}</strong>${hint ? `<small>${hint}</small>` : ""}</div>`;
}
function warnLevelClass(level) {
if (level >= 50) return "warn-extreme";
if (level >= 40) return "warn-severe";
if (level >= 30) return "warn-moderate";
if (level >= 20) return "warn-minor";
return "warn-info";
}
async function loadHealth() {
try {
const data = await api("/api/health");
el("health").textContent = `API: online · v${data.version} · ${new Date(data.time_utc).toLocaleTimeString("de-DE")}`;
el("health").classList.remove("bad");
} catch {
el("health").textContent = "API: offline";
el("health").classList.add("bad");
}
}
function applyDashboard(dash) {
const loc = getLocation();
if (dash.forecast?.data) {
latestForecast = dash.forecast;
const current = dash.forecast.data.current;
el("currentTemp").textContent = convertTemp(current.temperature_2m);
el("currentMeta").textContent = `${weatherText(current.weather_code)} · Wind ${convertWind(current.wind_speed_10m)} · ${loc.place}`;
el("forecastSource").textContent = dash.forecast.cached ? `${dash.forecast.source} (Cache)` : dash.forecast.source;
el("dailySource").textContent = dash.forecast.cached ? `${dash.forecast.source} (Cache)` : dash.forecast.source;
el("currentDetails").innerHTML = [
metric("Gefühlt", convertTemp(current.apparent_temperature)),
metric("Luftfeuchte", `${current.relative_humidity_2m} %`),
metric("Luftdruck", `${Math.round(current.pressure_msl)} hPa`),
metric("Niederschlag", `${current.precipitation ?? 0} mm`),
metric("Bewölkung", `${current.cloud_cover ?? 0} %`),
metric("Böen", convertWind(current.wind_gusts_10m ?? 0)),
].join("");
renderDaily(dash.forecast.data.daily);
renderHourly(dash.forecast.data.hourly);
renderHourStrip(dash.forecast.data.hourly);
}
if (dash.observations?.data) {
const weather = dash.observations.data.weather;
const source = dash.observations.data.sources?.[0];
if (weather) {
el("obsTemp").textContent = convertTemp(weather.temperature);
el("obsMeta").textContent = `${source?.station_name || "DWD Station"} · ${weather.timestamp || ""}`;
}
}
if (dash.radar) {
el("radarCount").textContent = `${dash.radar.count} online`;
el("radarMeta").textContent = `Produkt ${dash.radar.product?.toUpperCase() || "RV"} · DWD OpenData`;
}
if (dash.warnings) {
applyWarningsData(dash.warnings);
}
}
function applyWarningsData(data) {
const localOnly = el("warningsLocalOnly")?.checked ?? true;
const warnings = data.warnings || [];
el("warnCount").textContent = String(data.count_local ?? warnings.length);
el("warnMeta").textContent = localOnly
? `${warnings.length} Warnung(en) für deinen Standort`
: `${data.count_total} Warnungen bundesweit (Top ${warnings.length})`;
const warncellEl = el("warncellInfo");
if (localOnly && data.warncells?.length) {
warncellEl.classList.remove("hidden");
warncellEl.innerHTML = `Zugeordnete Warngebiete: ${data.warncells.map((a) => `<strong>${a.name}</strong> (${a.warncell_id})`).join(" · ")}`;
} else {
warncellEl.classList.add("hidden");
warncellEl.innerHTML = "";
}
el("warningsList").innerHTML = warnings.length ? warnings.map((w) => `
<article class="warning-card ${warnLevelClass(w.level)}">
<div class="warning-head">
<strong>${w.headline || w.event}</strong>
<span class="warn-badge">Level ${w.level} · ${w.level_label}</span>
</div>
<p class="muted">${w.region_name || ""} · ${w.state || ""}</p>
<p>${(w.description || "").replace(/\n/g, "<br>")}</p>
${w.instruction ? `<p class="instruction">${w.instruction}</p>` : ""}
<small class="muted">Gültig: ${w.start ? formatDateTime(w.start) : ""} ${w.end ? formatDateTime(w.end) : ""}</small>
</article>
`).join("") : `<p class="muted">${localOnly ? "Keine aktiven Warnungen für deinen Standort." : "Keine Warnungen verfügbar."}</p>`;
}
async function loadForecast() {
const loc = getLocation();
const data = await api(`/api/forecast?lat=${loc.lat}&lon=${loc.lon}`);
latestForecast = data;
const current = data.data.current;
el("currentTemp").textContent = convertTemp(current.temperature_2m);
el("currentMeta").textContent = `${weatherText(current.weather_code)} · Wind ${convertWind(current.wind_speed_10m)} · ${loc.place}`;
el("forecastSource").textContent = data.cached ? `${data.source} (Cache)` : data.source;
el("dailySource").textContent = data.cached ? `${data.source} (Cache)` : data.source;
el("currentDetails").innerHTML = [
metric("Gefühlt", convertTemp(current.apparent_temperature)),
metric("Luftfeuchte", `${current.relative_humidity_2m} %`),
metric("Luftdruck", `${Math.round(current.pressure_msl)} hPa`),
metric("Niederschlag", `${current.precipitation ?? 0} mm`),
metric("Bewölkung", `${current.cloud_cover ?? 0} %`),
metric("Böen", convertWind(current.wind_gusts_10m ?? 0)),
].join("");
renderDaily(data.data.daily);
renderHourly(data.data.hourly);
renderHourStrip(data.data.hourly);
}
function renderDaily(daily) {
el("dailyForecast").innerHTML = daily.time.map((date, idx) => {
const d = new Date(date + "T12:00:00");
const day = d.toLocaleDateString("de-DE", { weekday: "short", day: "2-digit", month: "2-digit" });
const sunrise = daily.sunrise?.[idx] ? new Date(daily.sunrise[idx]).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }) : "";
const sunset = daily.sunset?.[idx] ? new Date(daily.sunset[idx]).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }) : "";
return `<div class="day">
<strong>${day}</strong>
<span class="muted">${weatherText(daily.weather_code[idx])}</span>
<p class="temp">${convertTemp(daily.temperature_2m_max[idx])} / ${convertTemp(daily.temperature_2m_min[idx])}</p>
<span class="muted">Regen: ${daily.precipitation_sum[idx]} mm · ${daily.precipitation_probability_max[idx] ?? 0}%</span>
<span class="muted block">Wind: ${convertWind(daily.wind_speed_10m_max[idx] ?? 0)} · Böen ${convertWind(daily.wind_gusts_10m_max[idx] ?? 0)}</span>
<span class="muted block">Sonne: ${sunrise} ${sunset}</span>
</div>`;
}).join("");
}
function renderHourly(hourly) {
el("hourlyTable").innerHTML = hourly.time.slice(0, 48).map((time, idx) => `
<tr>
<td>${formatDateTime(time)}</td>
<td>${weatherText(hourly.weather_code[idx])}</td>
<td>${convertTemp(hourly.temperature_2m[idx])}</td>
<td>${hourly.precipitation_probability[idx] ?? 0}% · ${hourly.precipitation[idx] ?? 0} mm</td>
<td>${hourly.cloud_cover[idx] ?? 0}%</td>
<td>${convertWind(hourly.wind_speed_10m[idx] ?? 0)}</td>
<td>${convertWind(hourly.wind_gusts_10m[idx] ?? 0)}</td>
</tr>
`).join("");
renderHourlyChart(hourly);
}
function renderHourStrip(hourly) {
el("hourStrip").innerHTML = hourly.time.slice(0, 12).map((time, idx) => {
const label = new Date(time).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
const rain = hourly.precipitation_probability[idx] ?? 0;
return `<div class="hour-card">
<strong>${label}</strong>
<span>${convertTemp(hourly.temperature_2m[idx])}</span>
<small>${weatherText(hourly.weather_code[idx])}</small>
<div class="bar"><i style="width:${Math.min(rain, 100)}%"></i></div>
<small>Regen ${rain}% · Wind ${convertWind(hourly.wind_speed_10m[idx] ?? 0)}</small>
</div>`;
}).join("");
}
async function loadObservations() {
const loc = getLocation();
try {
const data = await api(`/api/observations?lat=${loc.lat}&lon=${loc.lon}`);
const weather = data.data.weather;
const source = data.data.sources?.[0];
if (!weather) throw new Error("Keine Beobachtung gefunden");
el("obsTemp").textContent = convertTemp(weather.temperature);
el("obsMeta").textContent = `${source?.station_name || "DWD Station"} · ${weather.timestamp || ""}`;
} catch (err) {
el("obsTemp").textContent = "";
el("obsMeta").textContent = `Keine DWD-Beobachtung: ${err.message}`;
}
}
async function loadWarnings() {
const loc = getLocation();
const localOnly = el("warningsLocalOnly")?.checked ?? true;
try {
const data = await api(`/api/warnings?lat=${loc.lat}&lon=${loc.lon}&local_only=${localOnly}`);
applyWarningsData(data);
} catch (err) {
el("warnCount").textContent = "";
el("warnMeta").textContent = err.message;
el("warningsList").innerHTML = `<p class="muted">Warnungen nicht verfügbar: ${err.message}</p>`;
el("warncellInfo")?.classList.add("hidden");
}
}
function setRadarTime(index) {
if (!radarTimes.length) return;
radarFrameIndex = Math.max(0, Math.min(index, radarTimes.length - 1));
const time = radarTimes[radarFrameIndex];
el("radarSlider").value = radarFrameIndex;
el("radarTimeLabel").textContent = formatDateTime(time);
el("radarFrameInfo").textContent = `Frame ${radarFrameIndex + 1} / ${radarTimes.length}`;
if (radarLayer) {
radarLayer.setParams({ TIME: time, _cache: Date.now() });
}
}
async function loadRadarTimes() {
const layer = radarLayer?.wmsParams?.layers || "dwd:Niederschlagsradar";
const minutes = settings.radarMinutes || 120;
try {
const data = await api(`/api/radar/wms/times?layer=${encodeURIComponent(layer)}&minutes=${minutes}`);
radarTimes = data.times || [];
el("radarSlider").max = Math.max(0, radarTimes.length - 1);
if (radarTimes.length) setRadarTime(radarTimes.length - 1);
else el("radarTimeLabel").textContent = "Keine Zeitschritte";
} catch (err) {
el("radarTimeLabel").textContent = `Zeitleiste: ${err.message}`;
}
}
function toggleRadarPlay() {
if (radarPlaying) {
clearInterval(radarPlayTimer);
radarPlayTimer = null;
radarPlaying = false;
el("radarPlayBtn").textContent = "▶";
return;
}
if (radarTimes.length < 2) return;
radarPlaying = true;
el("radarPlayBtn").textContent = "⏸";
radarPlayTimer = setInterval(() => {
const next = radarFrameIndex + 1;
if (next >= radarTimes.length) setRadarTime(0);
else setRadarTime(next);
}, 800);
}
async function initMap() {
if (map || typeof L === "undefined") return;
const loc = getLocation();
map = L.map("map", { zoomControl: true }).setView([loc.lat, loc.lon], 8);
osmLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "&copy; OpenStreetMap-Mitwirkende",
}).addTo(map);
mapMarker = L.marker([loc.lat, loc.lon]).addTo(map).bindPopup(loc.place);
try {
const config = await api("/api/radar/wms");
const overlays = {};
wmsLayers = config.layers.map((layer) => {
const wms = L.tileLayer.wms(config.service_url, {
layers: layer.layer,
format: "image/png",
transparent: true,
version: "1.1.1",
opacity: layer.opacity ?? 0.7,
attribution: config.attribution,
});
overlays[layer.title] = wms;
if (layer.id === "niederschlagsradar") radarLayer = wms;
if (layer.id === "warnungen_gemeinden") warningLayer = wms;
if (layer.enabled) wms.addTo(map);
return { ...layer, instance: wms };
});
layerControl = L.control.layers({ "OpenStreetMap": osmLayer }, overlays, { collapsed: false }).addTo(map);
el("mapMeta").textContent = "DWD Radar + Warnungen aktiv";
await loadRadarTimes();
} catch (err) {
el("mapMeta").textContent = `Karte: ${err.message}`;
}
}
function updateMapLocation() {
if (!map || !mapMarker) return;
const loc = getLocation();
map.setView([loc.lat, loc.lon], Math.max(map.getZoom(), 8));
mapMarker.setLatLng([loc.lat, loc.lon]).bindPopup(loc.place);
}
function reloadRadarLayers() {
wmsLayers.forEach((layer) => {
if (map?.hasLayer(layer.instance)) layer.instance.setParams({ _cache: Date.now() });
});
loadRadarTimes();
}
function toggleWarningsOnMap() {
if (!warningLayer || !map) return;
if (el("showWarningsOnMap").checked) warningLayer.addTo(map);
else map.removeLayer(warningLayer);
}
async function searchPlaces(query) {
if (query.length < 2) {
el("searchResults").classList.add("hidden");
return;
}
try {
const data = await api(`/api/geocode/search?q=${encodeURIComponent(query)}`);
const results = data.results || [];
el("searchResults").innerHTML = results.length ? results.map((r, i) => `
<li role="option" data-index="${i}">
<strong>${r.place}</strong>
<span>${r.label}</span>
</li>
`).join("") : `<li class="muted">Keine Treffer</li>`;
el("searchResults").classList.remove("hidden");
el("searchResults").querySelectorAll("li[data-index]").forEach((item) => {
item.addEventListener("click", () => {
const r = results[Number(item.dataset.index)];
setLocation({ place: r.place, lat: r.lat, lon: r.lon });
el("searchResults").classList.add("hidden");
loadAll();
});
});
} catch {
el("searchResults").innerHTML = `<li class="muted">Suche fehlgeschlagen</li>`;
el("searchResults").classList.remove("hidden");
}
}
function renderFavorites() {
const favs = loadFavorites();
const panel = el("favoritesPanel");
if (!favs.length) { panel.classList.add("hidden"); return; }
panel.classList.remove("hidden");
el("favoritesList").innerHTML = favs.map((f, i) => `
<button class="chip" data-index="${i}">${f.place}</button>
`).join("");
el("favoritesList").querySelectorAll(".chip").forEach((btn) => {
btn.addEventListener("click", () => {
const f = favs[Number(btn.dataset.index)];
setLocation(f);
loadAll();
});
});
}
function addFavorite() {
const loc = getLocation();
const favs = loadFavorites().filter((f) => f.place !== loc.place || f.lat !== loc.lat);
favs.unshift(loc);
saveFavorites(favs.slice(0, 8));
renderFavorites();
}
function useGeolocation() {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(async (pos) => {
const lat = pos.coords.latitude;
const lon = pos.coords.longitude;
try {
const data = await api(`/api/geocode/reverse?lat=${lat}&lon=${lon}`);
setLocation({ place: data.result?.place || "GPS-Standort", lat, lon });
} catch {
setLocation({ place: "GPS-Standort", lat, lon });
}
loadAll();
});
}
function applyTheme() {
document.documentElement.dataset.theme = settings.theme;
el("themeToggle").textContent = settings.theme === "dark" ? "🌙" : "☀️";
}
function applySettingsToUI() {
el("unitTemp").value = settings.unitTemp;
el("unitWind").value = settings.unitWind;
el("radarMinutes").value = String(settings.radarMinutes);
applyTheme();
}
async function loadDefaultLocation() {
const stored = settings.defaultLocation;
if (stored) { setLocation(stored); return; }
try {
const data = await api("/api/location/default");
setLocation(data);
} catch { /* fallback values in HTML */ }
}
function activateView(name) {
document.querySelectorAll(".tab").forEach((tab) => tab.classList.toggle("active", tab.dataset.view === name));
document.querySelectorAll(".view").forEach((view) => view.classList.toggle("active", view.dataset.view === name));
if (name === "radar") initMap().then(() => setTimeout(() => map?.invalidateSize(), 150));
if (name === "warnings") loadWarnings();
if (name === "hourly" && hourlyChart) setTimeout(() => hourlyChart.resize(), 80);
}
async function loadDashboard() {
const loc = getLocation();
const localOnly = el("warningsLocalOnly")?.checked ?? true;
const dash = await api(`/api/dashboard?lat=${loc.lat}&lon=${loc.lon}&local_only=${localOnly}`);
applyDashboard(dash);
if (dash.bundle_cached) {
el("forecastSource").textContent += " · Bundle-Cache";
}
return dash;
}
async function loadAll() {
loadHealth();
const loc = getLocation();
try {
await loadDashboard();
} catch {
await Promise.allSettled([loadForecast(), loadObservations(), loadWarnings()]);
const radar = await api("/api/radar/latest?limit=12").catch(() => null);
if (radar) {
el("radarCount").textContent = `${radar.count} online`;
el("radarMeta").textContent = `Produkt ${radar.product.toUpperCase()} · DWD OpenData`;
}
}
updateMapLocation();
if (map) loadRadarTimes();
}
async function init() {
applySettingsToUI();
await loadDefaultLocation();
renderFavorites();
await loadAll();
setInterval(loadHealth, 60000);
}
document.querySelectorAll(".tab").forEach((tab) => tab.addEventListener("click", () => activateView(tab.dataset.view)));
el("loadBtn").addEventListener("click", loadAll);
el("centerMapBtn").addEventListener("click", updateMapLocation);
el("reloadRadarLayersBtn").addEventListener("click", reloadRadarLayers);
el("favBtn").addEventListener("click", addFavorite);
el("geoBtn").addEventListener("click", useGeolocation);
el("warningsLocalOnly")?.addEventListener("change", loadWarnings);
el("showWarningsOnMap")?.addEventListener("change", toggleWarningsOnMap);
el("radarPlayBtn").addEventListener("click", toggleRadarPlay);
el("radarPrevBtn").addEventListener("click", () => setRadarTime(radarFrameIndex - 1));
el("radarNextBtn").addEventListener("click", () => setRadarTime(radarFrameIndex + 1));
el("radarFirstBtn").addEventListener("click", () => setRadarTime(0));
el("radarLastBtn").addEventListener("click", () => setRadarTime(radarTimes.length - 1));
el("radarSlider").addEventListener("input", (e) => setRadarTime(Number(e.target.value)));
el("placeSearch").addEventListener("input", (e) => {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => searchPlaces(e.target.value.trim()), 300);
});
document.addEventListener("click", (e) => {
if (!e.target.closest(".search-wrap")) el("searchResults").classList.add("hidden");
});
el("themeToggle").addEventListener("click", () => {
settings.theme = settings.theme === "dark" ? "light" : "dark";
saveSettings();
applyTheme();
if (latestForecast?.data?.hourly) renderHourlyChart(latestForecast.data.hourly);
});
["unitTemp", "unitWind", "radarMinutes"].forEach((id) => {
el(id).addEventListener("change", (e) => {
const key = id === "radarMinutes" ? "radarMinutes" : id.replace("unit", "unit").replace("Temp", "Temp").replace("Wind", "Wind");
if (id === "unitTemp") settings.unitTemp = e.target.value;
if (id === "unitWind") settings.unitWind = e.target.value;
if (id === "radarMinutes") settings.radarMinutes = Number(e.target.value);
saveSettings();
if (latestForecast) {
renderDaily(latestForecast.data.daily);
renderHourly(latestForecast.data.hourly);
renderHourStrip(latestForecast.data.hourly);
loadForecast();
}
if (id === "radarMinutes" && map) loadRadarTimes();
});
});
el("saveDefaultLocationBtn").addEventListener("click", () => {
settings.defaultLocation = getLocation();
saveSettings();
el("saveDefaultLocationBtn").textContent = "Gespeichert ✓";
setTimeout(() => { el("saveDefaultLocationBtn").textContent = "Aktuellen Ort als Standard speichern"; }, 2000);
});
init();