fix: treat empty IMGW warning products as no warnings

This commit is contained in:
zv
2026-06-12 21:51:44 +02:00
parent 1ff76cf847
commit 3309e03acb
5 changed files with 31 additions and 2 deletions

View File

@@ -44,6 +44,7 @@ Repozytorium nie ma obecnie skryptu testów, osobnego skryptu type-check ani for
- 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 regułowy brief dnia, 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. - 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 regułowy brief dnia, 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. - `synop.suma_opadu` jest akumulowaną sumą opadu. Nie używaj jej jako sygnału, że pada w tej chwili, ani do sterowania animacją deszczu.
- Ostrzeżenia hydro zawierają jawne województwa, a ostrzeżenia meteo kody powiatów TERYT. Normalizuj oba warianty przez `lib/provinces.ts` i `lib/warning-regions.ts`; nie filtruj ostrzeżeń wyłącznie po opisach tekstowych. - Ostrzeżenia hydro zawierają jawne województwa, a ostrzeżenia meteo kody powiatów TERYT. Normalizuj oba warianty przez `lib/provinces.ts` i `lib/warning-regions.ts`; nie filtruj ostrzeżeń wyłącznie po opisach tekstowych.
- HTTP `404` z IMGW dla ostrzeżeń z JSON `{"status":false,"message":"No products were found"}` oznacza brak produktów i ma być normalizowany do pustej listy, nie do błędu UI ani błędu workera.
- Listy ostrzeżeń zachowują priorytet lokalnego obszaru, a wewnątrz każdej grupy pokazują ostrzeżenia meteorologiczne przed hydrologicznymi. Jeśli lokalizacja ma rozpoznany powiat TERYT, ostrzeżenia meteo filtruj po tym powiecie; w przeciwnym razie stosuj fallback wojewódzki. W obrębie rodzaju zachowuj kolejność publikacji od najnowszych. - Listy ostrzeżeń zachowują priorytet lokalnego obszaru, a wewnątrz każdej grupy pokazują ostrzeżenia meteorologiczne przed hydrologicznymi. Jeśli lokalizacja ma rozpoznany powiat TERYT, ostrzeżenia meteo filtruj po tym powiecie; w przeciwnym razie stosuj fallback wojewódzki. W obrębie rodzaju zachowuj kolejność publikacji od najnowszych.
- Dashboard pokazuje kompaktowo wyłącznie aktywne i nadchodzące ostrzeżenia meteo dla wybranego obszaru. Filtruj je po `validTo` względem czasu przeglądarki i automatycznie usuwaj wygasłe komunikaty bez przeładowania strony. - Dashboard pokazuje kompaktowo wyłącznie aktywne i nadchodzące ostrzeżenia meteo dla wybranego obszaru. Filtruj je po `validTo` względem czasu przeglądarki i automatycznie usuwaj wygasłe komunikaty bez przeładowania strony.
- Brief dnia generuj deterministycznie w `lib/weather-brief.ts` z prognozy modelowej i ostrzeżeń IMGW. Nie traktuj go jako odpowiedzi modelu AI i nie wymagaj klucza OpenAI API. - Brief dnia generuj deterministycznie w `lib/weather-brief.ts` z prognozy modelowej i ostrzeżeń IMGW. Nie traktuj go jako odpowiedzi modelu AI i nie wymagaj klucza OpenAI API.

View File

@@ -12,6 +12,8 @@ Użytkownik może opcjonalnie udostępnić położenie GPS. Pozycja jest zaokrą
Widok ostrzeżeń priorytetyzuje komunikaty dla obszaru wynikającego z miejscowości lub stacji wybranej w pogodzie. Dla ostrzeżeń meteorologicznych, gdy aplikacja rozpozna powiat wybranej miejscowości, filtrowanie odbywa się po kodzie TERYT powiatu; w przeciwnym razie pozostaje fallback wojewódzki. W każdej grupie ostrzeżenia meteorologiczne, np. o burzach lub silnym wietrze, są wyświetlane przed hydrologicznymi. Ostrzeżenia meteorologiczne IMGW przypisuje do regionów na podstawie kodów TERYT, a hydrologiczne na podstawie jawnych pól województwa z API. Pozostałe aktywne komunikaty są wyświetlane niżej. Dashboard pokazuje dodatkowo kompaktowy panel aktywnych i nadchodzących ostrzeżeń meteo dla wybranego obszaru. Panel automatycznie ukrywa komunikaty po upływie ich czasu obowiązywania i nie obejmuje ostrzeżeń hydrologicznych. Widok ostrzeżeń priorytetyzuje komunikaty dla obszaru wynikającego z miejscowości lub stacji wybranej w pogodzie. Dla ostrzeżeń meteorologicznych, gdy aplikacja rozpozna powiat wybranej miejscowości, filtrowanie odbywa się po kodzie TERYT powiatu; w przeciwnym razie pozostaje fallback wojewódzki. W każdej grupie ostrzeżenia meteorologiczne, np. o burzach lub silnym wietrze, są wyświetlane przed hydrologicznymi. Ostrzeżenia meteorologiczne IMGW przypisuje do regionów na podstawie kodów TERYT, a hydrologiczne na podstawie jawnych pól województwa z API. Pozostałe aktywne komunikaty są wyświetlane niżej. Dashboard pokazuje dodatkowo kompaktowy panel aktywnych i nadchodzących ostrzeżeń meteo dla wybranego obszaru. Panel automatycznie ukrywa komunikaty po upływie ich czasu obowiązywania i nie obejmuje ostrzeżeń hydrologicznych.
Publiczne endpointy ostrzeżeń IMGW potrafią zwrócić HTTP `404` z treścią `{"status":false,"message":"No products were found"}`. Aplikacja traktuje taki wariant jako poprawną pustą listę ostrzeżeń, a nie awarię źródła danych.
## Stack ## Stack
- Next.js z App Router i TypeScript - Next.js z App Router i TypeScript

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { isImgwNoProductsResponse, readImgwResponseBody } from "@/lib/imgw-empty-response";
const ALLOWED_PATHS = new Set([ const ALLOWED_PATHS = new Set([
"synop", "synop",
@@ -28,6 +29,12 @@ export async function GET(_: Request, context: { params: Promise<{ path: string[
headers: { Accept: "application/json", "User-Agent": USER_AGENT }, headers: { Accept: "application/json", "User-Agent": USER_AGENT },
}); });
if (!response.ok) { if (!response.ok) {
const body = await readImgwResponseBody(response);
if ((resource === "warningsmeteo" || resource === "warningshydro") && response.status === 404 && isImgwNoProductsResponse(body.json)) {
return NextResponse.json([], {
headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600" },
});
}
return NextResponse.json({ error: "IMGW API jest chwilowo niedostępne." }, { status: response.status }); return NextResponse.json({ error: "IMGW API jest chwilowo niedostępne." }, { status: response.status });
} }
const data: unknown = await response.json(); const data: unknown = await response.json();

View File

@@ -0,0 +1,17 @@
export function isImgwNoProductsResponse(value: unknown) {
if (!value || typeof value !== "object") return false;
const response = value as { status?: unknown; message?: unknown };
return response.status === false
&& typeof response.message === "string"
&& response.message.toLocaleLowerCase("en-US").includes("no products were found");
}
export async function readImgwResponseBody(response: Response) {
const text = await response.text().catch(() => "");
if (!text) return { text, json: null as unknown };
try {
return { text, json: JSON.parse(text) as unknown };
} catch {
return { text, json: null as unknown };
}
}

View File

@@ -1,4 +1,5 @@
import { normalizeWarning } from "@/lib/weather-utils"; import { normalizeWarning } from "@/lib/weather-utils";
import { isImgwNoProductsResponse, readImgwResponseBody } from "@/lib/imgw-empty-response";
import type { RawWarning, WeatherWarning } from "@/types/imgw"; import type { RawWarning, WeatherWarning } from "@/types/imgw";
const IMGW_WARNINGS_METEO_URL = "https://danepubliczne.imgw.pl/api/data/warningsmeteo"; const IMGW_WARNINGS_METEO_URL = "https://danepubliczne.imgw.pl/api/data/warningsmeteo";
@@ -11,8 +12,9 @@ export async function fetchMeteoWarnings(signal?: AbortSignal): Promise<WeatherW
signal, signal,
}); });
if (!response.ok) { if (!response.ok) {
const details = await response.text().catch(() => ""); const details = await readImgwResponseBody(response);
throw new Error(`Unable to load IMGW meteorological warnings: ${response.status}${details ? ` ${details.slice(0, 240)}` : ""}`); if (response.status === 404 && isImgwNoProductsResponse(details.json)) return [];
throw new Error(`Unable to load IMGW meteorological warnings: ${response.status}${details.text ? ` ${details.text.slice(0, 240)}` : ""}`);
} }
const rows = await response.json() as RawWarning[]; const rows = await response.json() as RawWarning[];
return Array.isArray(rows) ? rows.map((warning, index) => normalizeWarning(warning, "meteo", index)) : []; return Array.isArray(rows) ? rows.map((warning, index) => normalizeWarning(warning, "meteo", index)) : [];