diff --git a/AGENTS.md b/AGENTS.md index 55f2258..6cdf843 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +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ę Open-Meteo pokazuj oddzielnie jako prognozę modelową. 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. +- 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 godzinowe dane Open-Meteo dla pełnych 7 dni. 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. - Ostrzeżenia hydro zawierają jawne województwa, a ostrzeżenia meteo kody powiatów TERYT. Normalizuj oba warianty przez `lib/provinces.ts`; nie filtruj ostrzeżeń wyłącznie po opisach tekstowych. diff --git a/README.md b/README.md index b85eebd..c7a9bf7 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,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`. Dzięki temu bieżące warunki są aktualizowane częściej niż godzinowe dane synoptyczne i mogą pokazać rzeczywisty opad z ostatnich 10 minut. Jeśli usługa Hybrid nie odpowiada, hero zachowuje pomiar `synop` jako fallback. 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`. Dzięki temu bieżące warunki są analizowane dla współrzędnych wybranej miejscowości, aktualizowane częściej niż godzinowe dane synoptyczne i mogą pokazać rzeczywisty opad z ostatnich 10 minut. Interfejs oddzielnie opisuje lokalną analizę Hybrid oraz najbliższą stację pomiarową IMGW. Jeśli usługa Hybrid nie odpowiada, hero zachowuje pomiar `synop` jako jawnie oznaczony fallback i ostrzega, gdy stacja jest oddalona od miejscowości o co najmniej 30 km. 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 pełną prognozę godzinową lub wielodniową i nie historię odczytów. Dlatego prognoza pochodzi z Open-Meteo i jest wyraźnie oddzielona od pomiarów IMGW oraz bieżącej analizy Hybrid. `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/components/dashboard/dashboard-page.tsx b/components/dashboard/dashboard-page.tsx index 29c855d..d65642b 100644 --- a/components/dashboard/dashboard-page.tsx +++ b/components/dashboard/dashboard-page.tsx @@ -32,14 +32,15 @@ export function DashboardPage() { const forecastLatitude = hasActiveLocationCoordinates ? activeLocation?.latitude : stationPosition?.latitude; const forecastLongitude = hasActiveLocationCoordinates ? activeLocation?.longitude : stationPosition?.longitude; const forecastLocationName = hasActiveLocationCoordinates ? activeLocation?.name ?? selectedStation?.name : selectedStation?.name; - const { data: currentWeather } = useCurrentWeather(forecastLatitude, forecastLongitude); + const { data: currentWeather, isPending: isCurrentWeatherPending } = useCurrentWeather(forecastLatitude, forecastLongitude); + const isCurrentWeatherLoading = Number.isFinite(forecastLatitude) && Number.isFinite(forecastLongitude) && isCurrentWeatherPending; if (isPending) return ; if (isError || !stations?.length || !selectedStation) return refetch()} description={t("dashboard.error")} />; return (
- + diff --git a/components/weather/weather-hero.tsx b/components/weather/weather-hero.tsx index 66ec8ad..2a78867 100644 --- a/components/weather/weather-hero.tsx +++ b/components/weather/weather-hero.tsx @@ -1,7 +1,7 @@ "use client"; import { motion } from "framer-motion"; -import { Droplets, Gauge, MapPin, Navigation, Umbrella, Wind } from "lucide-react"; +import { AlertTriangle, Droplets, Gauge, MapPin, Navigation, Umbrella, Wind } from "lucide-react"; import { calculateFeelsLike, formatDateTime, @@ -20,8 +20,10 @@ import { WeatherIcon } from "@/components/weather/weather-icon"; import { WeatherEffects } from "@/components/weather/weather-effects"; import { useI18n } from "@/lib/i18n"; -export function WeatherHero({ station, currentWeather, locationName, distanceKm }: { station: SynopStation; currentWeather?: ImgwCurrentWeather | null; locationName?: string; distanceKm?: number }) { +export function WeatherHero({ station, currentWeather, currentWeatherLoading = false, locationName, distanceKm }: { station: SynopStation; currentWeather?: ImgwCurrentWeather | null; currentWeatherLoading?: boolean; locationName?: string; distanceKm?: number }) { const { language, t } = useI18n(); + const displayedLocationName = locationName ?? station.name; + const hasDistantFallback = !currentWeather && !currentWeatherLoading && distanceKm !== undefined && distanceKm >= 30; const displayedStation = currentWeather ? { ...station, measuredAt: currentWeather.measuredAt, @@ -52,9 +54,19 @@ export function WeatherHero({ station, currentWeather, locationName, distanceKm
-
- {locationName ?? station.name} - {locationName && {t("location.heroSource", { station: station.name, distance: distanceKm ?? 0 })}} +
+ {displayedLocationName} +
+

{currentWeatherLoading + ? t("location.heroHybridLoading", { station: station.name }) + : currentWeather + ? t("location.heroHybridSource", { location: displayedLocationName }) + : locationName + ? t("location.heroStationFallbackWithDistance", { station: station.name, distance: distanceKm ?? 0 }) + : t("location.heroStationFallback", { station: station.name })}

+ {currentWeather && locationName &&

{t("location.heroNearestStation", { station: station.name, distance: distanceKm ?? 0 })}

} + {hasDistantFallback &&

{t("location.heroDistantFallback")}

} +
@@ -62,7 +74,7 @@ export function WeatherHero({ station, currentWeather, locationName, distanceKm {formatTemperature(displayedStation.temperature, language)}

{getWeatherDescription(displayedStation, language, currentWeather?.condition)}

-

{t("weather.feelsLike")} {formatTemperature(feelsLike, language)} · {t("weather.measurement")} {formatDateTime(displayedStation.measuredAt, language)}{currentWeather ? ` · ${t("weather.hybridAnalysis")}` : ""}

+

{t("weather.feelsLike")} {formatTemperature(feelsLike, language)} · {t("weather.measurement")} {formatDateTime(displayedStation.measuredAt, language)}

diff --git a/lib/i18n.tsx b/lib/i18n.tsx index f797cee..294ff5e 100644 --- a/lib/i18n.tsx +++ b/lib/i18n.tsx @@ -33,8 +33,13 @@ const translations = { "location.preparing": "Przygotowuję listę najbliższych stacji IMGW…", "location.empty": "Nie znaleziono pasującej miejscowości w Polsce.", "location.nearest": "Najbliższa stacja IMGW", - "location.currentSource": "{location}: odczyt ze stacji IMGW {station}, około {distance} km.", - "location.heroSource": "stacja IMGW: {station} · około {distance} km", + "location.currentSource": "{location}: bieżąca pogoda jest analizowana lokalnie dla współrzędnych miejscowości. Najbliższa stacja pomiarowa IMGW: {station} · około {distance} km.", + "location.heroHybridSource": "Analiza IMGW Hybrid dla lokalizacji: {location}", + "location.heroHybridLoading": "Pobieram lokalną analizę IMGW Hybrid. Tymczasowo pokazuję odczyt stacji: {station}.", + "location.heroNearestStation": "Najbliższa stacja pomiarowa IMGW: {station} · około {distance} km", + "location.heroStationFallback": "Dane zastępcze ze stacji IMGW: {station}", + "location.heroStationFallbackWithDistance": "Dane zastępcze ze stacji IMGW: {station} · około {distance} km", + "location.heroDistantFallback": "Stacja jest oddalona od lokalizacji. Lokalne warunki mogą się różnić.", "location.attribution": "Wyszukiwanie miejscowości:", "location.gpsUse": "Użyj mojej lokalizacji", "location.gpsLocating": "Ustalam lokalizację…", @@ -72,7 +77,6 @@ const translations = { "weather.currentRain": "Opady deszczu", "weather.currentSnow": "Opady śniegu", "weather.thunderstorm": "Burza", - "weather.hybridAnalysis": "analiza IMGW Hybrid", "weather.airTemperature": "Temperatura", "weather.windSpeed": "Prędkość wiatru", "weather.rainfallTotal": "Suma opadu", @@ -85,7 +89,7 @@ const translations = { "weather.temperatureDetail": "Temperatura powietrza", "forecast.label": "Prognoza modelowa", "forecast.title": "Najbliższe godziny i dni", - "forecast.description": "Prognoza dla {location}. Bieżący pomiar powyżej pochodzi ze stacji IMGW, a poniższe wartości są prognozą modelową.", + "forecast.description": "Prognoza dla {location}. Bieżące warunki powyżej pochodzą z IMGW, a poniższe wartości są prognozą modelową.", "forecast.hourly": "Najbliższe 24 godziny", "forecast.daily": "Prognoza 7-dniowa", "forecast.today": "Dzisiaj", @@ -205,8 +209,13 @@ const translations = { "location.preparing": "Preparing the nearest IMGW stations…", "location.empty": "No matching place was found in Poland.", "location.nearest": "Nearest IMGW station", - "location.currentSource": "{location}: reading from IMGW station {station}, approximately {distance} km away.", - "location.heroSource": "IMGW station: {station} · approximately {distance} km", + "location.currentSource": "{location}: current weather is analysed locally for the place coordinates. Nearest IMGW measurement station: {station} · approximately {distance} km away.", + "location.heroHybridSource": "IMGW Hybrid analysis for: {location}", + "location.heroHybridLoading": "Loading local IMGW Hybrid analysis. Temporarily showing the station reading: {station}.", + "location.heroNearestStation": "Nearest IMGW measurement station: {station} · approximately {distance} km away", + "location.heroStationFallback": "Fallback data from IMGW station: {station}", + "location.heroStationFallbackWithDistance": "Fallback data from IMGW station: {station} · approximately {distance} km away", + "location.heroDistantFallback": "The station is far from this place. Local conditions may differ.", "location.attribution": "Place search:", "location.gpsUse": "Use my location", "location.gpsLocating": "Finding your location…", @@ -244,7 +253,6 @@ const translations = { "weather.currentRain": "Rain", "weather.currentSnow": "Snow", "weather.thunderstorm": "Thunderstorm", - "weather.hybridAnalysis": "IMGW Hybrid analysis", "weather.airTemperature": "Temperature", "weather.windSpeed": "Wind speed", "weather.rainfallTotal": "Rainfall total", @@ -257,7 +265,7 @@ const translations = { "weather.temperatureDetail": "Air temperature", "forecast.label": "Model forecast", "forecast.title": "Upcoming hours and days", - "forecast.description": "Forecast for {location}. The current reading above comes from an IMGW station. The values below are a model forecast.", + "forecast.description": "Forecast for {location}. The current conditions above come from IMGW. The values below are a model forecast.", "forecast.hourly": "Next 24 hours", "forecast.daily": "7-day forecast", "forecast.today": "Today",