feat: add OWM alerts, UV index support, and provider info UI

- Parse OWM One Call 3.0 weather alerts and map to Warning structs
- Map hourly UVI from OWM response to HourlyForecast.UVIndex
- Add severity helper mapping OWM alert tags to severity levels
- Extract UVIndex through compute layer to timeline slots
- Smart warnings: use provider-supplied alerts, fall back to DWD
- Show UV index in dashboard timeline tooltips
- Add provider description below forecast dropdown with i18n
This commit is contained in:
2026-02-11 01:56:09 +01:00
parent 41649380e2
commit 201e5441cb
13 changed files with 243 additions and 30 deletions

View File

@@ -255,34 +255,53 @@ async function fetchForecastForProfile(profileId) {
sunshineMin: h.SunshineMin ?? h.sunshineMin ?? null,
apparentTempC: h.ApparentTempC ?? h.apparentTempC ?? null,
pressureHpa: h.PressureHpa ?? h.pressureHpa ?? null,
uvIndex: h.UVIndex ?? h.uvIndex ?? null,
});
}
// Fetch warnings (optional — don't fail if this errors)
// Warnings: use provider-supplied warnings if available, otherwise fall back to DWD
let warningCount = 0;
try {
const wResp = await fetch("/api/weather/warnings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lat: profile.latitude, lon: profile.longitude }),
});
if (wResp.ok) {
const wData = await wResp.json();
await deleteByIndex("warnings", "profileId", profileId);
for (const w of (wData.warnings || [])) {
await dbAdd("warnings", {
profileId,
headline: w.Headline || w.headline || "",
severity: w.Severity || w.severity || "",
description: w.Description || w.description || "",
instruction: w.Instruction || w.instruction || "",
onset: w.Onset || w.onset || "",
expires: w.Expires || w.expires || "",
});
warningCount++;
}
const providerWarnings = data.Warnings || data.warnings || [];
if (providerWarnings.length > 0) {
await deleteByIndex("warnings", "profileId", profileId);
for (const w of providerWarnings) {
await dbAdd("warnings", {
profileId,
headline: w.Headline || w.headline || "",
severity: w.Severity || w.severity || "",
description: w.Description || w.description || "",
instruction: w.Instruction || w.instruction || "",
onset: w.Onset || w.onset || "",
expires: w.Expires || w.expires || "",
});
warningCount++;
}
} catch (_) { /* warnings are optional */ }
} else {
// Fall back to DWD warnings (optional — don't fail if this errors)
try {
const wResp = await fetch("/api/weather/warnings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lat: profile.latitude, lon: profile.longitude }),
});
if (wResp.ok) {
const wData = await wResp.json();
await deleteByIndex("warnings", "profileId", profileId);
for (const w of (wData.warnings || [])) {
await dbAdd("warnings", {
profileId,
headline: w.Headline || w.headline || "",
severity: w.Severity || w.severity || "",
description: w.Description || w.description || "",
instruction: w.Instruction || w.instruction || "",
onset: w.Onset || w.onset || "",
expires: w.Expires || w.expires || "",
});
warningCount++;
}
}
} catch (_) { /* warnings are optional */ }
}
await setSetting("lastFetched", new Date().toISOString());
return { forecasts: hourly.length, warnings: warningCount };
@@ -416,6 +435,7 @@ async function getComputePayload(profileId, dateStr) {
sunshineMin: f.sunshineMin ?? null,
apparentTempC: f.apparentTempC ?? null,
pressureHpa: f.pressureHpa ?? null,
uvIndex: f.uvIndex ?? null,
})),
warnings: warnings.map(w => ({
headline: w.headline || "",