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

672 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 localRadarOverlay = null;
let radarTimes = [];
let radarFrameIndex = 0;
let radarPlayTimer = null;
let radarPlaying = false;
let renderedFrames = [];
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 formatBytes(bytes) {
if (!bytes) return "";
const units = ["B", "KB", "MB", "GB"];
let value = bytes, unit = 0;
while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit++; }
return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
}
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`;
}
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");
}
}
async function loadSourceStatus() {
try {
const data = await api("/api/health/sources");
renderSourceStatus(data.sources || []);
} catch {
el("sourceStatusSummary").textContent = "Quellenstatus nicht verfügbar";
}
}
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>`;
}
function renderSourceStatus(sources) {
const bad = sources.filter((s) => s.status === "error").length;
el("sourceStatusSummary").textContent = bad ? `${bad} Quelle(n) mit Problemen` : "Alle Quellen erreichbar";
el("sourceStatusGrid").innerHTML = sources.map((s) => `
<div class="source-status ${s.status}">
<strong>${s.name}</strong>
<span>${s.status}${s.latency_ms ? ` · ${s.latency_ms} ms` : ""}</span>
${s.error ? `<small>${s.error}</small>` : ""}
</div>
`).join("");
}
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("");
}
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}`);
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>`;
} 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");
}
}
async function loadRadarFiles() {
const latest = await api("/api/radar/latest?limit=12");
el("radarCount").textContent = `${latest.count} online`;
el("radarMeta").textContent = `Produkt ${latest.product.toUpperCase()} · DWD OpenData`;
const local = await api("/api/radar/files");
el("radarFiles").innerHTML = local.files.length ? local.files.slice().reverse().map((file) => `
<tr><td>${file.name}</td><td>${formatBytes(file.size_bytes)}</td>
<td>${new Date(file.modified_utc).toLocaleString("de-DE")}</td>
<td><a href="${file.download_url}">Download</a></td></tr>
`).join("") : `<tr><td colspan="4" class="muted">Noch keine lokalen Radar-Dateien.</td></tr>`;
try {
const rendered = await api("/api/radar/rendered?limit=12");
renderedFrames = rendered.frames || [];
el("renderedCount").textContent = `${rendered.count} Frame(s)`;
el("renderedFrames").innerHTML = renderedFrames.length ? renderedFrames.map((f) => `
<figure class="rendered-frame">
<img src="${f.image_url}" alt="Radar ${f.filename}" loading="lazy" />
<figcaption>${f.filename}</figcaption>
</figure>
`).join("") : `<p class="muted">Noch keine gerenderten Frames. Renderer-Worker abwarten.</p>`;
} catch {
el("renderedCount").textContent = "";
el("renderedFrames").innerHTML = `<p class="muted">Renderer nicht verfügbar.</p>`;
}
}
async function syncRadar() {
const button = el("syncRadarBtn");
button.disabled = true;
button.textContent = "Sync läuft…";
try {
await api("/api/radar/sync?limit=6", { method: "POST" });
await loadRadarFiles();
} finally {
button.disabled = false;
button.textContent = "Radar jetzt syncen";
}
}
async function loadSources() {
try {
const data = await api("/api/sources");
el("sourceList").innerHTML = Object.entries(data).map(([key, value]) => `
<div class="source-item"><span>${key}</span><code>${String(value)}</code></div>
`).join("");
} catch (err) {
el("sourceList").innerHTML = `<p class="muted">${err.message}</p>`;
}
}
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 (el("useLocalRadar").checked && renderedFrames.length) {
showLocalRadarFrame(radarFrameIndex);
} else if (radarLayer) {
radarLayer.setParams({ TIME: time, _cache: Date.now() });
}
}
function showLocalRadarFrame(index) {
if (!map) return;
const frame = renderedFrames[Math.min(index, renderedFrames.length - 1)];
if (!frame?.bounds) return;
const b = frame.bounds;
const bounds = [[b.south, b.west], [b.north, b.east]];
if (localRadarOverlay) map.removeLayer(localRadarOverlay);
localRadarOverlay = L.imageOverlay(frame.image_url + `?t=${Date.now()}`, bounds, { opacity: 0.75 });
localRadarOverlay.addTo(map);
}
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();
}
async function loadAll() {
await loadHealth();
await Promise.allSettled([loadForecast(), loadObservations(), loadRadarFiles(), loadSources(), loadWarnings()]);
updateMapLocation();
if (map) loadRadarTimes();
}
async function init() {
applySettingsToUI();
await loadDefaultLocation();
renderFavorites();
await loadAll();
setInterval(loadHealth, 30000);
}
document.querySelectorAll(".tab").forEach((tab) => tab.addEventListener("click", () => activateView(tab.dataset.view)));
el("loadBtn").addEventListener("click", loadAll);
el("syncRadarBtn").addEventListener("click", syncRadar);
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("useLocalRadar")?.addEventListener("change", () => setRadarTime(radarFrameIndex));
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();
});
["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();