fix: stop treating rainfall total as current rain

This commit is contained in:
zv
2026-06-02 15:10:37 +02:00
parent ce2e1176fa
commit 352287bc38
8 changed files with 11 additions and 39 deletions

View File

@@ -37,6 +37,7 @@ Repozytorium nie ma obecnie skryptu testów, osobnego skryptu type-check ani for
- Dodawaj `"use client"` tylko tam, gdzie komponent lub moduł korzysta z hooków, stanu przeglądarki albo interakcji. - Dodawaj `"use client"` tylko tam, gdzie komponent lub moduł korzysta z hooków, stanu przeglądarki albo interakcji.
- Dane zewnętrzne pobieraj przez route handlery Next.js. Nie omijaj allowlisty w `app/api/imgw/[...path]/route.ts`. - 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. - 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.
- `synop.suma_opadu` jest akumulowaną sumą opadu. Nie używaj jej jako sygnału, że pada w tej chwili, ani do sterowania animacją deszczu.
- GPS wymaga świadomej zgody użytkownika i HTTPS. Zaokrąglaj współrzędne przed użyciem i utrzymuj widoczną atrybucję OpenStreetMap dla reverse geocodingu Nominatim. - GPS wymaga świadomej zgody użytkownika i HTTPS. Zaokrąglaj współrzędne przed użyciem i utrzymuj widoczną atrybucję OpenStreetMap dla reverse geocodingu Nominatim.
- Normalizuj zewnętrzne odpowiedzi i obsługuj `null`, puste pola oraz błędne wartości. Brak danych pokazuj jawnie zamiast uzupełniać estymacją. - Normalizuj zewnętrzne odpowiedzi i obsługuj `null`, puste pola oraz błędne wartości. Brak danych pokazuj jawnie zamiast uzupełniać estymacją.
- Dla pobierania danych używaj TanStack Query z sensownym `queryKey`, cache i retry. W UI zachowuj loading, error, retry oraz empty states. - Dla pobierania danych używaj TanStack Query z sensownym `queryKey`, cache i retry. W UI zachowuj loading, error, retry oraz empty states.

View File

@@ -64,6 +64,8 @@ Przeglądarka pobiera dane przez route handlery Next.js. Proxy IMGW w `app/api/i
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. `wtr.` nie generuje fikcyjnych prognoz. Widok stacji prezentuje aktualne parametry i jawnie opisany snapshot pomiarowy. Brakujące wartości są oznaczane jako `Brak danych`. 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. `wtr.` nie generuje fikcyjnych prognoz. Widok stacji prezentuje aktualne parametry i jawnie opisany snapshot pomiarowy. Brakujące wartości są oznaczane jako `Brak danych`.
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. Czas aktualizacji parametrów hydrologicznych może się różnić. Interfejs pokazuje czas pomiaru, aby starsze odczyty nie wyglądały na bieżące.
## Struktura projektu ## Struktura projektu

View File

@@ -3,15 +3,6 @@
import { motion, useReducedMotion } from "framer-motion"; import { motion, useReducedMotion } from "framer-motion";
import type { SynopStation, WeatherMood } from "@/types/imgw"; import type { SynopStation, WeatherMood } from "@/types/imgw";
const rainDrops = Array.from({ length: 58 }, (_, index) => ({
left: `${(index * 29 + 7) % 100}%`,
delay: (index % 11) * 0.13,
duration: 0.72 + (index % 6) * 0.1,
height: 15 + (index % 5) * 5,
opacity: 0.48 + (index % 4) * 0.1,
width: index % 5 === 0 ? 2 : 1,
}));
const windLines = Array.from({ length: 7 }, (_, index) => ({ const windLines = Array.from({ length: 7 }, (_, index) => ({
top: `${18 + index * 11}%`, top: `${18 + index * 11}%`,
delay: index * 0.22, delay: index * 0.22,
@@ -26,14 +17,11 @@ const stars = Array.from({ length: 16 }, (_, index) => ({
export function WeatherEffects({ station, mood }: { station: SynopStation; mood: WeatherMood }) { export function WeatherEffects({ station, mood }: { station: SynopStation; mood: WeatherMood }) {
const reduceMotion = useReducedMotion(); const reduceMotion = useReducedMotion();
const rainfall = station.rainfall ?? 0;
const isRaining = rainfall >= 0.1;
const visibleDrops = rainfall >= 5 ? rainDrops : rainfall >= 1 ? rainDrops.slice(0, 44) : rainDrops.slice(0, 30);
const isWindy = (station.windSpeed ?? 0) >= 8; const isWindy = (station.windSpeed ?? 0) >= 8;
return ( return (
<div aria-hidden="true" className="pointer-events-none absolute inset-0 z-[1] overflow-hidden"> <div aria-hidden="true" className="pointer-events-none absolute inset-0 z-[1] overflow-hidden">
{(mood === "cloudy" || mood === "rain") && ( {mood === "cloudy" && (
<> <>
<motion.div <motion.div
animate={reduceMotion ? undefined : { x: ["-4%", "5%", "-4%"], y: [0, 8, 0], scale: [1, 1.05, 1] }} animate={reduceMotion ? undefined : { x: ["-4%", "5%", "-4%"], y: [0, 8, 0], scale: [1, 1.05, 1] }}
@@ -75,16 +63,6 @@ export function WeatherEffects({ station, mood }: { station: SynopStation; mood:
style={{ top: line.top, width: line.width }} style={{ top: line.top, width: line.width }}
/> />
))} ))}
{isRaining && !reduceMotion && visibleDrops.map((drop, index) => (
<motion.span
key={index}
initial={{ y: "-8vh", opacity: 0 }}
animate={{ y: ["-8vh", "85vh"], x: [0, -16], opacity: [0, drop.opacity, 0.18] }}
transition={{ duration: drop.duration, delay: drop.delay, repeat: Infinity, ease: "linear" }}
className="absolute -top-10 rounded-full bg-slate-100/90 shadow-[0_0_6px_rgba(226,232,240,0.7)] blur-[0.25px]"
style={{ height: drop.height, left: drop.left, width: drop.width, transform: "rotate(14deg)" }}
/>
))}
{mood === "cold" && ( {mood === "cold" && (
<div className="absolute inset-x-0 bottom-0 h-28 bg-gradient-to-t from-cyan-100/20 to-transparent" /> <div className="absolute inset-x-0 bottom-0 h-28 bg-gradient-to-t from-cyan-100/20 to-transparent" />
)} )}

View File

@@ -26,7 +26,7 @@ export function WeatherHero({ station, locationName, distanceKm }: { station: Sy
const metrics = [ const metrics = [
{ icon: Droplets, label: t("weather.humidity"), value: formatHumidity(station.humidity, language) }, { icon: Droplets, label: t("weather.humidity"), value: formatHumidity(station.humidity, language) },
{ icon: Wind, label: t("weather.wind"), value: formatWind(station.windSpeed, null, language) }, { icon: Wind, label: t("weather.wind"), value: formatWind(station.windSpeed, null, language) },
{ icon: Umbrella, label: t("weather.rainfall"), value: formatRainfall(station.rainfall, language) }, { icon: Umbrella, label: t("weather.rainfallTotal"), value: formatRainfall(station.rainfall, language) },
{ icon: Gauge, label: t("weather.pressure"), value: formatPressure(station.pressure, language) }, { icon: Gauge, label: t("weather.pressure"), value: formatPressure(station.pressure, language) },
]; ];

View File

@@ -1,11 +1,10 @@
import { Cloud, CloudRain, CloudSun, MoonStar, Snowflake, ThermometerSun, Wind } from "lucide-react"; import { Cloud, CloudSun, MoonStar, Snowflake, ThermometerSun, Wind } from "lucide-react";
import type { WeatherMood } from "@/types/imgw"; import type { WeatherMood } from "@/types/imgw";
export function WeatherIcon({ mood, className = "" }: { mood: WeatherMood; className?: string }) { export function WeatherIcon({ mood, className = "" }: { mood: WeatherMood; className?: string }) {
const Icon = { const Icon = {
warm: ThermometerSun, warm: ThermometerSun,
cloudy: Cloud, cloudy: Cloud,
rain: CloudRain,
wind: Wind, wind: Wind,
cold: Snowflake, cold: Snowflake,
night: MoonStar, night: MoonStar,

View File

@@ -61,7 +61,7 @@ const translations = {
"favorites.remove": "Usuń z ulubionych", "favorites.remove": "Usuń z ulubionych",
"weather.humidity": "Wilgotność", "weather.humidity": "Wilgotność",
"weather.wind": "Wiatr", "weather.wind": "Wiatr",
"weather.rainfall": "Opad", "weather.rainfall": "Suma opadu",
"weather.pressure": "Ciśnienie", "weather.pressure": "Ciśnienie",
"weather.feelsLike": "Odczuwalna", "weather.feelsLike": "Odczuwalna",
"weather.measurement": "pomiar", "weather.measurement": "pomiar",
@@ -69,8 +69,6 @@ const translations = {
"weather.calm": "Spokojne warunki", "weather.calm": "Spokojne warunki",
"weather.humid": "Wilgotno", "weather.humid": "Wilgotno",
"weather.strongWind": "Silny wiatr", "weather.strongWind": "Silny wiatr",
"weather.rain": "Opady",
"weather.heavyRain": "Wyraźne opady",
"weather.airTemperature": "Temperatura", "weather.airTemperature": "Temperatura",
"weather.windSpeed": "Prędkość wiatru", "weather.windSpeed": "Prędkość wiatru",
"weather.rainfallTotal": "Suma opadu", "weather.rainfallTotal": "Suma opadu",
@@ -79,7 +77,7 @@ const translations = {
"weather.pressureDetail": "Ciśnienie atmosferyczne", "weather.pressureDetail": "Ciśnienie atmosferyczne",
"weather.windSpeedDetail": "Bieżący odczyt IMGW", "weather.windSpeedDetail": "Bieżący odczyt IMGW",
"weather.windDirectionDetail": "Kierunek napływu wiatru", "weather.windDirectionDetail": "Kierunek napływu wiatru",
"weather.rainfallDetail": "Suma opadu z pomiaru IMGW", "weather.rainfallDetail": "Akumulowana suma opadu z pomiaru IMGW. Nie oznacza, że pada w tej chwili.",
"weather.temperatureDetail": "Temperatura powietrza", "weather.temperatureDetail": "Temperatura powietrza",
"forecast.label": "Prognoza modelowa", "forecast.label": "Prognoza modelowa",
"forecast.title": "Najbliższe godziny i dni", "forecast.title": "Najbliższe godziny i dni",
@@ -205,7 +203,7 @@ const translations = {
"favorites.remove": "Remove from favourites", "favorites.remove": "Remove from favourites",
"weather.humidity": "Humidity", "weather.humidity": "Humidity",
"weather.wind": "Wind", "weather.wind": "Wind",
"weather.rainfall": "Rainfall", "weather.rainfall": "Rainfall total",
"weather.pressure": "Pressure", "weather.pressure": "Pressure",
"weather.feelsLike": "Feels like", "weather.feelsLike": "Feels like",
"weather.measurement": "measurement", "weather.measurement": "measurement",
@@ -213,8 +211,6 @@ const translations = {
"weather.calm": "Calm conditions", "weather.calm": "Calm conditions",
"weather.humid": "Humid", "weather.humid": "Humid",
"weather.strongWind": "Strong wind", "weather.strongWind": "Strong wind",
"weather.rain": "Rainfall",
"weather.heavyRain": "Heavy rainfall",
"weather.airTemperature": "Temperature", "weather.airTemperature": "Temperature",
"weather.windSpeed": "Wind speed", "weather.windSpeed": "Wind speed",
"weather.rainfallTotal": "Rainfall total", "weather.rainfallTotal": "Rainfall total",
@@ -223,7 +219,7 @@ const translations = {
"weather.pressureDetail": "Atmospheric pressure", "weather.pressureDetail": "Atmospheric pressure",
"weather.windSpeedDetail": "Current IMGW reading", "weather.windSpeedDetail": "Current IMGW reading",
"weather.windDirectionDetail": "Direction the wind is coming from", "weather.windDirectionDetail": "Direction the wind is coming from",
"weather.rainfallDetail": "Total rainfall from the IMGW reading", "weather.rainfallDetail": "Accumulated rainfall total from the IMGW reading. It does not mean that it is raining right now.",
"weather.temperatureDetail": "Air temperature", "weather.temperatureDetail": "Air temperature",
"forecast.label": "Model forecast", "forecast.label": "Model forecast",
"forecast.title": "Upcoming hours and days", "forecast.title": "Upcoming hours and days",

View File

@@ -160,7 +160,6 @@ export function getWindDirection(degrees: number) {
export function getWeatherMoodFromData(station: SynopStation, date = new Date()): WeatherMood { export function getWeatherMoodFromData(station: SynopStation, date = new Date()): WeatherMood {
const hour = date.getHours(); const hour = date.getHours();
if ((station.rainfall ?? 0) >= 0.1) return "rain";
if (hour < 6 || hour >= 21) return "night"; if (hour < 6 || hour >= 21) return "night";
if ((station.windSpeed ?? 0) >= 8) return "wind"; if ((station.windSpeed ?? 0) >= 8) return "wind";
if ((station.temperature ?? 15) <= 3) return "cold"; if ((station.temperature ?? 15) <= 3) return "cold";
@@ -170,8 +169,6 @@ export function getWeatherMoodFromData(station: SynopStation, date = new Date())
} }
export function getWeatherDescription(station: SynopStation, language: Language = "pl") { export function getWeatherDescription(station: SynopStation, language: Language = "pl") {
if ((station.rainfall ?? 0) >= 5) return translate(language, "weather.heavyRain");
if ((station.rainfall ?? 0) >= 0.1) return translate(language, "weather.rain");
if ((station.windSpeed ?? 0) >= 8) return translate(language, "weather.strongWind"); if ((station.windSpeed ?? 0) >= 8) return translate(language, "weather.strongWind");
if ((station.humidity ?? 0) >= 90) return translate(language, "weather.humid"); if ((station.humidity ?? 0) >= 90) return translate(language, "weather.humid");
return translate(language, "weather.calm"); return translate(language, "weather.calm");
@@ -181,7 +178,6 @@ export function moodGradient(mood: WeatherMood) {
return { return {
warm: "from-sky-400 via-blue-500 to-indigo-700", warm: "from-sky-400 via-blue-500 to-indigo-700",
cloudy: "from-slate-600 via-slate-700 to-slate-900", cloudy: "from-slate-600 via-slate-700 to-slate-900",
rain: "from-slate-600 via-blue-900 to-slate-950",
wind: "from-cyan-600 via-slate-600 to-blue-950", wind: "from-cyan-600 via-slate-600 to-blue-950",
cold: "from-cyan-300 via-blue-500 to-indigo-900", cold: "from-cyan-300 via-blue-500 to-indigo-900",
night: "from-slate-800 via-indigo-950 to-slate-950", night: "from-slate-800 via-indigo-950 to-slate-950",

View File

@@ -115,4 +115,4 @@ export interface WeatherWarning {
office: string | null; office: string | null;
} }
export type WeatherMood = "warm" | "cloudy" | "rain" | "wind" | "cold" | "night" | "mild"; export type WeatherMood = "warm" | "cloudy" | "wind" | "cold" | "night" | "mild";