Files
wtr/README.md

17 KiB

wtr.

Pogoda z danych IMGW. Prosto. Pięknie. Aktualnie.

wtr. to nowoczesna pogodowa PWA dla Polski oparta o publiczne dane IMGW i jawnie oznaczoną prognozę modelową łączącą IMGW ALARO z Open-Meteo. Aplikacja prezentuje bieżącą analizę pogody IMGW Hybrid, odczyty synoptyczne, prognozę godzinową i 7-dniową, regułowy brief dnia, stacje hydrologiczne oraz ostrzeżenia w spokojnym, mobilnym interfejsie z czytelną typografią, opaque surfaces i subtelnymi animacjami. Dashboard pokazuje rozszerzony desktopowy podgląd godzin z podsumowaniem najbliższej doby, a także wykresy temperatury i opadu dla bieżącego dnia. Każdy dzień prognozy można także otworzyć w animowanym widoku szczegółowym z przebiegiem godzinowym oraz wykresami.

Interfejs jest dostępny po polsku i angielsku. Wybrany język jest zapisywany lokalnie w przeglądarce. Oryginalne treści ostrzeżeń oraz nazwy stacji pochodzą bezpośrednio z API IMGW i nie są automatycznie tłumaczone.

Wyszukiwarka na stronie głównej obsługuje miejscowości w całej Polsce. Nazwa miejscowości jest rozpoznawana przez Open-Meteo Geocoding API oparte o GeoNames. Bieżące warunki są analizowane lokalnie przez IMGW Hybrid, a najbliższa rzeczywista stacja IMGW jest pokazywana jako kontekst i fallback. Interfejs jawnie pokazuje nazwę tej stacji oraz przybliżoną odległość.

Użytkownik może opcjonalnie udostępnić położenie GPS. Pozycja jest zaokrąglana do trzech miejsc po przecinku, czyli około 100 metrów, a nazwa miejscowości jest ustalana przez Nominatim / OpenStreetMap. Po zgodzie aplikacja wybiera lokalizację, najbliższą stację IMGW i prognozę. Geolocation API wymaga bezpiecznego kontekstu HTTPS. Wyjątkiem jest localhost; wejście z iPhone przez lokalny adres typu http://192.168.x.x:3000 nie uruchomi systemowego pytania Safari.

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

  • Next.js z App Router i TypeScript
  • Tailwind CSS oraz komponenty w stylu shadcn/ui
  • Framer Motion
  • Recharts
  • Lucide React
  • TanStack Query
  • Zustand z trwałym stanem localStorage
  • web-push do wysyłki Web Push
  • własny service worker, manifest i offline fallback

Uruchomienie

Wymagany jest Node.js 20.9 lub nowszy.

npm install
npm run dev

Aplikacja będzie dostępna pod adresem http://localhost:3000.

Sprawdzenie jakości i build produkcyjny:

npm run lint
npm run build
npm run start

Worker powiadomień uruchamiany obok aplikacji:

npm run start
npm run notifications:worker

Worker musi mieć dostęp do działającej aplikacji Next.js. Domyślnie odpytuje http://127.0.0.1:3000; jeśli aplikacja działa pod innym adresem albo portem, ustaw WTR_APP_URL. Skrypt workera sam wczytuje .env i .env.local, a zmienne podane bezpośrednio w shellu mają pierwszeństwo.

Źródła danych

Bieżące pomiary i komunikaty pochodzą z rzeczywistych publicznych danych IMGW:

  • bieżąca analiza IMGW Hybrid używana przez oficjalny portal: https://meteo.imgw.pl/api/v1/forecast/fcapi
  • prognoza godzinowa IMGW ALARO używana przez oficjalny portal: https://meteo.imgw.pl/api/v1/forecast/fcapi?m=alaro
  • dane synoptyczne: https://danepubliczne.imgw.pl/api/data/synop
  • pojedyncza stacja synoptyczna: https://danepubliczne.imgw.pl/api/data/synop/id/{id}
  • dane hydrologiczne: https://danepubliczne.imgw.pl/api/data/hydro/
  • ostrzeżenia meteorologiczne: https://danepubliczne.imgw.pl/api/data/warningsmeteo
  • ostrzeżenia hydrologiczne: https://danepubliczne.imgw.pl/api/data/warningshydro
  • dane meteorologiczne: https://danepubliczne.imgw.pl/api/data/meteo/
  • lista produktów: https://danepubliczne.imgw.pl/api/data/product

Prognoza modelowa łączy dwa źródła. IMGW ALARO dostarcza dostępne godziny prognozy, zwykle około 72 godzin od cyklu modelu. Open-Meteo Forecast API (https://api.open-meteo.com/v1/forecast) dostarcza prawdopodobieństwo opadu dla całego zakresu, uzupełnia dalszy horyzont do pełnych 7 dni i pozostaje fallbackiem, jeśli ALARO chwilowo nie odpowiada. Interfejs pokazuje oba źródła i ich role.

Do wyszukiwania nazw miejscowości używany jest endpoint https://geocoding-api.open-meteo.com/v1/search. Przed wdrożeniem komercyjnym należy sprawdzić aktualne warunki korzystania z Open-Meteo lub zastąpić usługę własnym dostawcą.

Opcjonalny reverse geocoding dla GPS korzysta z publicznego endpointu Nominatim: https://nominatim.openstreetmap.org/reverse. Wywołanie następuje wyłącznie po zgodzie użytkownika. Interfejs pokazuje atrybucję OpenStreetMap. Przed wdrożeniem o większym ruchu należy sprawdzić aktualną politykę użycia publicznej instancji Nominatim lub zastąpić ją własną usługą.

Przeglądarka pobiera dane przez route handlery Next.js. Proxy IMGW w app/api/imgw/[...path]/route.ts pozwala ujednolicić cache, błędy API i bezpiecznie obsłużyć hydro bez mixed content. Bieżącą analizę pogody obsługuje app/api/imgw-current/route.ts, prognozę app/api/forecast/route.ts, a reverse geocoding GPS app/api/locations/reverse/route.ts.

Wewnętrzne endpointy API

Metoda Endpoint Przeznaczenie
GET /api/forecast?latitude={lat}&longitude={lon} Zwraca 7-dniową prognozę modelową dla współrzędnych, łącząc IMGW ALARO z fallbackiem Open-Meteo. Odpowiedź ma cache s-maxage=900.
GET /api/imgw-current?latitude={lat}&longitude={lon} Pobiera lokalną analizę IMGW Hybrid z oficjalnego portalu IMGW dla współrzędnych. Cache jest krótki, s-maxage=120.
GET /api/imgw/{path} Proxy allowlistowanych publicznych endpointów IMGW danepubliczne.imgw.pl, m.in. synop, synop/id/{id}, hydro, meteo, warningsmeteo, warningshydro i product. Empty warningsmeteo/warningshydro z komunikatem No products were found jest normalizowane do pustej listy.
GET /api/locations/search?name={query} Wyszukuje miejscowości w Polsce przez Open-Meteo Geocoding i zwraca znormalizowane lokalizacje dla wyszukiwarki. Cache s-maxage=86400.
GET /api/locations/reverse?latitude={lat}&longitude={lon} Ustala nazwę miejsca dla pozycji GPS przez Nominatim / OpenStreetMap po zgodzie użytkownika. Cache s-maxage=86400.
GET /api/notifications/vapid-key Zwraca publiczny klucz VAPID i informację, czy Web Push jest skonfigurowany. Używane przez /settings.
POST /api/notifications/subscriptions Zapisuje lub aktualizuje subskrypcję Web Push dla urządzenia, wraz z preferencjami ostrzeżeń, briefu o 7:00, briefu o 18:00, lokalizacją i powiatem TERYT. Wymaga skonfigurowanych kluczy VAPID.
DELETE /api/notifications/subscriptions Usuwa subskrypcję Web Push na podstawie endpointu subskrypcji.
GET /api/notifications/check Endpoint harmonogramu sprawdzający nowe ostrzeżenia meteorologiczne IMGW i wysyłający Web Push do pasujących subskrypcji. W produkcji wymaga Authorization: Bearer <NOTIFICATIONS_CRON_SECRET> albo x-cron-secret.
GET /api/notifications/daily-brief Endpoint harmonogramu wysyłający raz dziennie poranny brief o 7:00 dla subskrypcji z włączoną opcją i zapisaną lokalizacją. W produkcji wymaga sekretu crona.
GET /api/notifications/tomorrow-brief Endpoint harmonogramu wysyłający wieczorny brief o 18:00 z prognozą na jutro, w tym sygnałami burz z modelu. W produkcji wymaga sekretu crona.
POST /api/notifications/test Wysyła testowe powiadomienie Web Push na konkretny endpoint subskrypcji. Używane wyłącznie z /settings.

Ograniczenia API

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. Jeśli pełny rekord 10-minutowy jest wyraźnie starszy niż pełny rekord godzinowy z tej samej odpowiedzi, aplikacja wybiera świeższy rekord godzinowy zamiast nadpisywać aktualny fallback starymi danymi. 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.

Brief dnia jest deterministycznym podsumowaniem danych, a nie wywołaniem modelu AI. Moduł lib/weather-brief.ts analizuje najbliższe 24 godziny prognozy modelowej oraz aktywne i nadchodzące ostrzeżenia meteo IMGW dla obszaru użytkownika. Ten sam moduł przygotowuje osobny brief na jutro z pełnego jutrzejszego dnia prognozy, wykrywając m.in. burze po kodach pogodowych modelu także wtedy, gdy IMGW nie opublikowało jeszcze oficjalnego ostrzeżenia. Na tej podstawie tworzy dłuższy opis na dashboard oraz krótsze wersje do powiadomień. Dzięki temu funkcja nie wymaga klucza OpenAI API i działa przewidywalnie na małym serwerze, np. Raspberry Pi.

Pole suma_opadu z endpointu synoptycznego jest prezentowane jako akumulowana suma opadu. Nie służy do wnioskowania, że w danej chwili pada, ani do uruchamiania animacji deszczu.

Czas aktualizacji parametrów hydrologicznych może się różnić. Interfejs pokazuje czas pomiaru, aby starsze odczyty nie wyglądały na bieżące.

Stany pogody i efekty wizualne

Prognoza godzinowa i dzienna rozpoznaje następujące stany warunków pogodowych:

Stan Opis w interfejsie
clear Bezchmurnie
partlyCloudy Częściowe zachmurzenie
cloudy Pochmurno
fog Mgła
drizzle Mżawka
rain Opady deszczu
snow Opady śniegu
thunderstorm Burza
unknown Brak opisu

Bieżąca analiza IMGW Hybrid rozpoznaje bezpośrednio opad deszczu, śnieg i burzę. Gdy żadne z tych zjawisk nie występuje, hero może pokazać pomocniczy opis: Silny wiatr, Wilgotne warunki albo Spokojne warunki.

Hero aktualnej pogody korzysta z uproszczonego nastroju wizualnego do wyboru ikony, tekstu i małego akcentu stanu. Nie steruje już pełnoekranowym gradientem.

Mood Obecna reguła
night godzina przed 06:00 lub od 21:00
wind wiatr od 8 m/s
cold temperatura do 3°C
cloudy wilgotność od 80%
warm temperatura od 20°C
mild pozostałe przypadki

Warstwa efektów wizualnych jest ograniczona do subtelnych efektów informacyjnych: kropli przy lokalnym opadzie oraz błysku przy burzy. Mood hero jest obecnie heurystyką opartą o porę dnia, temperaturę, wilgotność i wiatr. Nie jest jeszcze pełną klasyfikacją sterowaną kodem warunków IMGW Hybrid.

Struktura projektu

app/                  routing, layout, proxy danych, offline fallback
components/forecast/ prognoza godzinowa i dzienna IMGW ALARO + Open-Meteo
components/charts/   wykresy odczytów i szczegółów prognozy
components/dashboard dashboard aplikacji
components/weather/  hero, stacje, metryki i szczegóły
components/warnings/ alerty meteo i hydro
components/hydro/     lista stacji hydrologicznych
components/settings/ ustawienia języka, motywu i preferencji alertów
components/ui/        bazowe komponenty interfejsu
components/states/    loading, empty i error states
hooks/                zapytania TanStack Query
lib/                  API, normalizacja, helpery i stan
types/                typy danych IMGW
public/               manifest, ikony i service worker

PWA i offline

Manifest znajduje się w public/manifest.json, a service worker w public/sw.js. Rejestracja service workera działa w buildzie produkcyjnym. Powłoka aplikacji ma podstawowy offline fallback. Odpowiedzi API mogą być dostępne z pamięci urządzenia przy braku sieci, ale UI nadal prezentuje czas pomiaru i status świeżości.

Widok /settings zawiera konfigurację powiadomień o ostrzeżeniach meteorologicznych IMGW: wybór województwa, zgodę Web Push, zapis subskrypcji urządzenia, poranny brief, wieczorny brief na jutro i wysłanie powiadomienia testowego. Na iOS/iPadOS powiadomienia webowe wymagają dodania PWA do ekranu początkowego i uruchomienia aplikacji z ikony. Na Androidzie i desktopie wystarczy HTTPS oraz przeglądarka obsługująca Web Push; instalacja PWA nie jest wymagana. Endpoint /api/notifications/check sprawdza warningsmeteo i wysyła tylko nowe ostrzeżenia meteo do pasujących subskrypcji, /api/notifications/daily-brief wysyła raz dziennie krótkie podsumowanie dla subskrypcji z zapisaną lokalizacją, /api/notifications/tomorrow-brief wysyła wieczorem prognozę na kolejny dzień, a /api/notifications/test wysyła test na wskazany endpoint subskrypcji. Endpointy harmonogramu nie uruchamiają się same: na self-hostingu uruchom npm run notifications:worker obok npm run start, albo użyj zewnętrznego crona. Obecny magazyn subskrypcji działa w pamięci procesu, więc produkcja wymaga podmiany lib/push-store.ts na trwałą bazę lub KV.

Do wysyłki Web Push wymagane są zmienne środowiskowe:

WEB_PUSH_VAPID_PUBLIC_KEY=
WEB_PUSH_VAPID_PRIVATE_KEY=
WEB_PUSH_VAPID_SUBJECT=mailto:admin@example.com
NOTIFICATIONS_CRON_SECRET=
WTR_APP_URL=http://127.0.0.1:3000
NOTIFICATIONS_WARNING_INTERVAL_MINUTES=5
NOTIFICATIONS_MORNING_BRIEF_TIME=07:00
NOTIFICATIONS_TOMORROW_BRIEF_TIME=18:00

Klucze VAPID można wygenerować poleceniem npx web-push generate-vapid-keys. Worker npm run notifications:worker wczytuje .env i .env.local, domyślnie odpytuje http://127.0.0.1:3000, więc npm run start musi działać równolegle na tym samym hoście albo WTR_APP_URL musi wskazywać właściwy adres aplikacji. Worker sprawdza ostrzeżenia co 5 minut, wysyła poranny brief po 07:00 i wieczorny brief na jutro po 18:00 czasu polskiego. Jeśli używasz zewnętrznego crona zamiast workera, cron ostrzeżeń powinien wywoływać GET /api/notifications/check z nagłówkiem Authorization: Bearer <NOTIFICATIONS_CRON_SECRET> albo x-cron-secret. Drugi cron, ustawiony na około 07:00 czasu polskiego, powinien wywoływać GET /api/notifications/daily-brief, a trzeci cron około 18:00 GET /api/notifications/tomorrow-brief z tym samym nagłówkiem.

Wdrożenie na Vercel

  1. Umieść repozytorium w serwisie Git.
  2. Importuj projekt do Vercel jako aplikację Next.js.
  3. Nie dodawaj kluczy API: publiczne endpointy IMGW ich nie wymagają.
  4. Wdróż standardowym poleceniem builda npm run build.

Proxy IMGW działa jako route handler Next.js i jest zgodne z hostingiem Vercel.

Bezpieczeństwo zależności

Projekt używa stabilnego Next.js 16.2.6. npm audit --omit=dev raportuje obecnie umiarkowane zgłoszenie GHSA-qx2v-qp2m-jg93 dla PostCSS 8.4.31 bundlowanego bezpośrednio przez najnowszy Next.js. Główna konfiguracja projektu korzysta z poprawionego PostCSS 8.5.x, ale wewnętrznej kopii Next.js nie należy ręcznie podmieniać. Po publikacji poprawki upstream należy zaktualizować Next.js i ponownie uruchomić audyt.