fix: clarify local weather data sources
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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 <PageLoadingSkeleton />;
|
||||
if (isError || !stations?.length || !selectedStation) return <ErrorState onRetry={() => refetch()} description={t("dashboard.error")} />;
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<LocationSearch stations={stations} positions={positions} />
|
||||
<WeatherHero station={selectedStation} currentWeather={currentWeather} locationName={activeLocation?.name} distanceKm={activeLocation?.distanceKm} />
|
||||
<WeatherHero station={selectedStation} currentWeather={currentWeather} currentWeatherLoading={isCurrentWeatherLoading} locationName={activeLocation?.name} distanceKm={activeLocation?.distanceKm} />
|
||||
<ForecastPanel latitude={forecastLatitude} longitude={forecastLongitude} locationName={forecastLocationName ?? selectedStation.name} />
|
||||
<FavoritesSection stations={stations} />
|
||||
<FeaturedStationsSection stations={stations} />
|
||||
|
||||
@@ -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
|
||||
<div className="absolute -right-20 -top-20 size-72 rounded-full bg-white/15 blur-3xl" />
|
||||
<div className="absolute -bottom-24 -left-16 size-72 rounded-full bg-cyan-200/15 blur-3xl" />
|
||||
<div className="relative z-10">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-white/85"><MapPin className="size-4" />{locationName ?? station.name}</span>
|
||||
{locationName && <span className="text-xs text-white/65">{t("location.heroSource", { station: station.name, distance: distanceKm ?? 0 })}</span>}
|
||||
<div>
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-white/85"><MapPin className="size-4" />{displayedLocationName}</span>
|
||||
<div className="mt-1.5 space-y-1 text-xs text-white/65">
|
||||
<p>{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 })}</p>
|
||||
{currentWeather && locationName && <p>{t("location.heroNearestStation", { station: station.name, distance: distanceKm ?? 0 })}</p>}
|
||||
{hasDistantFallback && <p className="flex items-start gap-1.5 text-amber-100"><AlertTriangle className="mt-0.5 size-3.5 shrink-0" />{t("location.heroDistantFallback")}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex items-end justify-between gap-3 sm:mt-10">
|
||||
<div>
|
||||
@@ -62,7 +74,7 @@ export function WeatherHero({ station, currentWeather, locationName, distanceKm
|
||||
{formatTemperature(displayedStation.temperature, language)}
|
||||
</div>
|
||||
<p className="mt-5 text-xl font-medium tracking-tight sm:text-2xl">{getWeatherDescription(displayedStation, language, currentWeather?.condition)}</p>
|
||||
<p className="mt-1 text-sm text-white/75">{t("weather.feelsLike")} {formatTemperature(feelsLike, language)} · {t("weather.measurement")} {formatDateTime(displayedStation.measuredAt, language)}{currentWeather ? ` · ${t("weather.hybridAnalysis")}` : ""}</p>
|
||||
<p className="mt-1 text-sm text-white/75">{t("weather.feelsLike")} {formatTemperature(feelsLike, language)} · {t("weather.measurement")} {formatDateTime(displayedStation.measuredAt, language)}</p>
|
||||
</div>
|
||||
<WeatherIcon mood={mood} condition={currentWeather?.condition} className="mb-4 size-20 text-white/80 sm:size-28" />
|
||||
</div>
|
||||
|
||||
24
lib/i18n.tsx
24
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",
|
||||
|
||||
Reference in New Issue
Block a user