From f5bd719a0fdbcaa86e170a98c26830089304afd0 Mon Sep 17 00:00:00 2001 From: zv Date: Thu, 4 Jun 2026 19:23:58 +0200 Subject: [PATCH] fix: select full IMGW Hybrid current record --- AGENTS.md | 2 +- README.md | 2 +- lib/imgw-current-api.ts | 31 +++++++++++++++++++++++-------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 929a3ed..a7150f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ Repozytorium nie ma obecnie skryptu testów, osobnego skryptu type-check ani for - Dane zewnętrzne pobieraj przez route handlery Next.js. Nie omijaj allowlisty w `app/api/imgw/[...path]/route.ts`. - Traktuj IMGW jako źródło bieżących pomiarów, hydro i ostrzeżeń. Prognozę pokazuj oddzielnie jako prognozę modelową preferującą IMGW ALARO i jawnie uzupełnioną przez Open-Meteo. Nie generuj fikcyjnych danych ani nie przedstawiaj prognozy jako pomiaru IMGW. - Dashboard hero korzysta z publicznego endpointu Hybrid oficjalnego portalu IMGW przez `app/api/imgw-current/route.ts`, z fallbackiem do godzinowego `synop`. Hybrid ma krótki cache i dostarcza m.in. opad 10-minutowy; nie przedstawiaj go jako akumulowanej sumy opadu stacji. -- Hybrid wybieraj z lokalnego rekordu aktualnej godziny UTC, zgodnie z portalem `meteo.imgw.pl`; rekord może być 10-minutowy albo godzinowy. Jeśli IMGW zwraca wyłącznie lokalny opad MERGE bez pełnych parametrów, zachowuj go jako częściową analizę lokalną, a pozostałe parametry uzupełniaj jawnym fallbackiem `synop`. +- Hybrid wybieraj z pierwszego pełnego rekordu analizy zwracanego przez endpoint dla lokalizacji, preferując `Type_Ten_Minutes`, a potem `Type_Hour`. Wymagaj realnych wartości liczbowych; nie traktuj `null` jako pełnego pola i nie opieraj wyboru na zegarze przeglądarki. Jeśli IMGW zwraca wyłącznie lokalny opad MERGE bez pełnych parametrów, zachowuj go jako częściową analizę lokalną, a pozostałe parametry uzupełniaj jawnym fallbackiem `synop`. - W UI rozdzielaj lokalną analizę Hybrid dla współrzędnych miejscowości od kontekstowej informacji o najbliższej stacji pomiarowej. Fallback `synop` oznaczaj jawnie; dla stacji oddalonej o co najmniej 30 km zachowuj ostrzeżenie o możliwej różnicy warunków lokalnych. - Route handler prognozy pobiera pełne 7 dni Open-Meteo oraz godzinowe IMGW ALARO. W godzinach pokrytych przez ALARO parametry IMGW mają pierwszeństwo, Open-Meteo dostarcza prawdopodobieństwo opadu i dalszy horyzont, a awaria ALARO pozostawia działający fallback Open-Meteo. Dashboard pokazuje najbliższe 24 przyszłe godziny oraz wykresy pełnego bieżącego dnia, a widok szczegółowy dnia korzysta z pełnego zestawu godzin dla wybranej daty. - `synop.suma_opadu` jest akumulowaną sumą opadu. Nie używaj jej jako sygnału, że pada w tej chwili, ani do sterowania animacją deszczu. diff --git a/README.md b/README.md index 9b7483a..6b35113 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Przeglądarka pobiera dane przez route handlery Next.js. Proxy IMGW w `app/api/i ## Ograniczenia API -Dashboard korzysta z publicznego endpointu IMGW Hybrid używanego przez oficjalny portal `meteo.imgw.pl`. Bieżące warunki są wybierane z lokalnego rekordu aktualnej godziny dla współrzędnych miejscowości, zgodnie z zachowaniem portalu IMGW, i mogą dodatkowo pokazać rzeczywisty opad z ostatnich 10 minut. Interfejs oddzielnie opisuje lokalną analizę Hybrid oraz najbliższą stację pomiarową IMGW. Pokrycie Hybrid może być częściowe: jeśli IMGW publikuje lokalny opad MERGE bez pełnego rekordu parametrów, hero zachowuje lokalny opad, a temperaturę, wiatr, wilgotność i ciśnienie jawnie uzupełnia fallbackiem ze stacji. Jeśli usługa Hybrid nie odpowiada, hero zachowuje cały pomiar `synop` jako oznaczony fallback. Gdy fallback pochodzi ze stacji oddalonej od miejscowości o co najmniej 30 km, interfejs ostrzega o możliwej różnicy warunków lokalnych. Endpoint Hybrid jest częścią publicznego frontendu IMGW, ale nie jest opisany w stabilnej dokumentacji `danepubliczne.imgw.pl`, więc integrację należy monitorować przy zmianach portalu. +Dashboard korzysta z publicznego endpointu IMGW Hybrid używanego przez oficjalny portal `meteo.imgw.pl`. Bieżące warunki są wybierane z pierwszego pełnego rekordu analizy Hybrid dla współrzędnych miejscowości, preferując rekord 10-minutowy i wymagając realnych wartości liczbowych. Dzięki temu `null` z rekordów MERGE nie jest traktowany jako pełny pomiar. Interfejs może dodatkowo pokazać rzeczywisty opad z ostatnich 10 minut, oddzielnie opisuje lokalną analizę Hybrid oraz najbliższą stację pomiarową IMGW. Pokrycie Hybrid może być częściowe: jeśli IMGW publikuje lokalny opad MERGE bez pełnego rekordu parametrów, hero zachowuje lokalny opad, a temperaturę, wiatr, wilgotność i ciśnienie jawnie uzupełnia fallbackiem ze stacji. Jeśli usługa Hybrid nie odpowiada, hero zachowuje cały pomiar `synop` jako oznaczony fallback. Gdy fallback pochodzi ze stacji oddalonej od miejscowości o co najmniej 30 km, interfejs ostrzega o możliwej różnicy warunków lokalnych. Endpoint Hybrid jest częścią publicznego frontendu IMGW, ale nie jest opisany w stabilnej dokumentacji `danepubliczne.imgw.pl`, więc integrację należy monitorować przy zmianach portalu. Publiczny endpoint synoptyczny IMGW udostępnia najnowszy pomiar, a nie historię odczytów. Prognoza modelowa jest wyraźnie oddzielona od pomiarów IMGW oraz bieżącej analizy Hybrid. Parametry ALARO mają pierwszeństwo w godzinach objętych tym modelem, natomiast prawdopodobieństwo opadu i dalszy horyzont pochodzą z Open-Meteo, ponieważ ALARO nie publikuje prawdopodobieństwa opadu i nie obejmuje pełnych 7 dni. `wtr.` nie generuje fikcyjnych prognoz. Widok stacji prezentuje aktualne parametry i jawnie opisany snapshot pomiarowy. Brakujące wartości są oznaczane jako `Brak danych`. diff --git a/lib/imgw-current-api.ts b/lib/imgw-current-api.ts index 544ad02..4d945bf 100644 --- a/lib/imgw-current-api.ts +++ b/lib/imgw-current-api.ts @@ -32,8 +32,20 @@ function getCondition(weatherCode: number | null, rainfall10m: number | null, sn return null; } -function getCurrentUtcHour() { - return new Date().toISOString().slice(0, 13); +function hasNumericValue(value: unknown) { + return toNumber(value) !== null; +} + +function isFullWeatherRow(candidate: RawImgwHybridWeatherRow) { + return hasNumericValue(candidate.Temperature) + && hasNumericValue(candidate.Chill) + && hasNumericValue(candidate.Humidity) + && hasNumericValue(candidate.Wind_Speed) + && hasNumericValue(candidate.PressureMSL); +} + +function hasPrecipitationValue(candidate: RawImgwHybridWeatherRow) { + return hasNumericValue(candidate.Precipitation10m) || hasNumericValue(candidate.Rain10m) || hasNumericValue(candidate.Snow10m); } export function normalizeImgwCurrentWeather(payload: RawImgwHybridWeatherResponse): ImgwCurrentWeather | null { @@ -45,16 +57,19 @@ export function normalizeImgwCurrentWeather(payload: RawImgwHybridWeatherRespons return (candidate.Type === "Type_Ten_Minutes" || candidate.Type === "Type_Hour") && normalizeDate(candidate.Date) !== null; }) .sort((left, right) => String(left.Date).localeCompare(String(right.Date))); - const currentUtcHour = getCurrentUtcHour(); - const fullRow = rows.find((candidate) => String(candidate.Date).startsWith(currentUtcHour) && candidate.Temperature !== undefined); - const precipitationRow = rows.find((candidate) => String(candidate.Date).startsWith(currentUtcHour) && candidate.Precipitation10m !== undefined); + const fullRow = rows.find((candidate) => candidate.Type === "Type_Ten_Minutes" && isFullWeatherRow(candidate)) + ?? rows.find((candidate) => candidate.Type === "Type_Hour" && isFullWeatherRow(candidate)); + const precipitationRow = fullRow && hasPrecipitationValue(fullRow) + ? fullRow + : rows.find((candidate) => candidate.Type === "Type_Ten_Minutes" && hasPrecipitationValue(candidate)); const row = fullRow ?? precipitationRow; if (!row) return null; const measuredAt = normalizeDate(row.Date); if (!measuredAt) return null; - const rainfall10m = toNumber(row.Rain10m); - const snowfall10m = toNumber(row.Snow10m); + const precipitationSource = precipitationRow ?? row; + const rainfall10m = toNumber(precipitationSource.Rain10m); + const snowfall10m = toNumber(precipitationSource.Snow10m); const weatherCode = getWeatherCode(row.Icon10); return { @@ -66,7 +81,7 @@ export function normalizeImgwCurrentWeather(payload: RawImgwHybridWeatherRespons windDirection: toNumber(row.Wind_Dir), humidity: toNumber(row.Humidity), pressure: toHectopascals(row.PressureMSL), - precipitation10m: toNumber(row.Precipitation10m), + precipitation10m: toNumber(precipitationSource.Precipitation10m), rainfall10m, snowfall10m, cloudCover: toNumber(row.Cloud),