WEATHER_API_FRONTEND.md · версия 1.1.0

Каталка - API погоды для фронта

Версия документа: 1.1.0 Дата: 02.09.2026 Backend API: https://api.katalka.ski

1. Что нужно фронту

Для основной карточки погоды на курорте фронту в первую очередь нужны:

Основное поле для выбора погодной иконки — condition.

Backend объединяет два контура данных:

  1. MET.no — основной погодный прогноз, температура, состояние погоды, ветер, влажность, UV, солнце и базовые осадки.
  2. Open-Meteo — дополнительный альпийский/снежный контур: снег за последние 24 часа, прогноз снега, глубина снежного покрова и уровень замерзания.

Open-Meteo не вызывается из пользовательского HTTP-запроса для курорта. Его данные заранее обновляются backend-ом и читаются из кэша.

2. Основные endpoint-ы

Погода конкретного курорта — компактная

GET /api/v1/resorts/{resortId}/weather/nearby

Использовать для карточки курорта, Live и компактного погодного блока.

Погода конкретного курорта — подробная

GET /api/v1/resorts/{resortId}/weather/details

Использовать для отдельного экрана погоды, почасового прогноза и прогноза по дням.

Погода по координатам — компактная

GET /api/v1/weather/nearby?lat={latitude}&lng={longitude}

Опционально можно передать высоту:

GET /api/v1/weather/nearby?lat={latitude}&lng={longitude}&altitudeMeters={altitude}

Погода по координатам — подробная

GET /api/v1/weather/details?lat={latitude}&lng={longitude}

Опционально:

GET /api/v1/weather/details?lat={latitude}&lng={longitude}&altitudeMeters={altitude}

В coordinate-based endpoint-ах resortId и resortName будут null — это нормально.

Важно: для координатных endpoint-ов расширенный Open-Meteo cache не используется. Это сделано специально, чтобы не создавать неограниченный кэш для произвольных координат.

3. Компактный ответ

type WeatherNearbyResponse = {
  resortId: UUID | null;
  resortName: string | null;
  latitude: number;
  longitude: number;
  updatedAt: string;
  current: WeatherCurrent | null;
  wind: WeatherWind | null;
  snow: WeatherSnow | null;
  visibility: WeatherVisibility | null;
  alpineDataStatus: WeatherDataStatus;
};

Для основной карточки фронту достаточно:

response.current?.temperature
response.current?.feelsLike
response.current?.condition
response.current?.minTemperature
response.current?.maxTemperature
response.wind?.speedMetersPerSecond
response.wind?.gustMetersPerSecond
response.wind?.direction
response.snow
response.updatedAt

4. Текущая температура и состояние

type WeatherCurrent = {
  temperature: number | null;
  feelsLike: number | null;
  minTemperature: number | null;
  maxTemperature: number | null;
  condition: WeatherCondition;
};

Пример:

{
  "temperature": 1.9,
  "feelsLike": -1.5,
  "minTemperature": 1.1,
  "maxTemperature": 1.9,
  "condition": "CLOUDY"
}

Для текущего интерфейса:

5. WeatherCondition

Backend отдаёт собственные стабильные значения:

type WeatherCondition =
  | 'CLEAR'
  | 'PARTLY_CLOUDY'
  | 'CLOUDY'
  | 'FOG'
  | 'LIGHT_RAIN'
  | 'RAIN'
  | 'HEAVY_RAIN'
  | 'LIGHT_SNOW'
  | 'SNOW'
  | 'HEAVY_SNOW'
  | 'SLEET'
  | 'THUNDERSTORM'
  | 'UNKNOWN';

Рекомендуемое соответствие:

conditionСмыслИконка
CLEARясносолнце / ясно
PARTLY_CLOUDYчастично облачно / fairсолнце + облако
CLOUDYоблачнооблако
FOGтумантуман
LIGHT_RAINслабый дождьлёгкий дождь
RAINдождьдождь
HEAVY_RAINсильный дождьсильный дождь
LIGHT_SNOWслабый снеглёгкий снег
SNOWснегснег
HEAVY_SNOWсильный снегсильный снег
SLEETмокрый снег / смешанные осадкидождь + снег
THUNDERSTORMгрозагроза
UNKNOWNпровайдер не дал достаточно данных для уверенной классификацииfallback

Фронту не нужно использовать оригинальные коды MET.no.

Редкий UNKNOWN на дальнем горизонте допустим: это означает, что источник не дал достаточного описания состояния, а не ошибку frontend.

6. Ветер

type WeatherWind = {
  speedMetersPerSecond: number | null;
  gustMetersPerSecond: number | null;
  directionDegrees: number | null;
  direction: string | null;
};

Пример:

{
  "speedMetersPerSecond": 3.4,
  "gustMetersPerSecond": null,
  "directionDegrees": 241.7,
  "direction": "SW"
}

Семантика gustMetersPerSecond:

null  = провайдер не дал значение порывов
0     = значение получено и порывов нет
> 0   = значение порывов получено

Нельзя трактовать null как 0.

7. Снег и осадки

7.1. Сводка по снегу

type WeatherSnow = {
  last24HoursCm: number | null;
  forecast24HoursCm: number | null;
  condition: SnowCondition;
  dataStatus: WeatherDataStatus;
};
type SnowCondition =
  | 'NO_SNOW'
  | 'POWDER'
  | 'FRESH'
  | 'WET'
  | 'SLUSH'
  | 'PACKED'
  | 'ICE'
  | 'UNKNOWN';

Текущий backend уже уверенно различает отсутствие снежного покрова (NO_SNOW) и отсутствие данных (UNKNOWN).

Пример курортной выдачи:

{
  "last24HoursCm": 0.00,
  "forecast24HoursCm": 0.00,
  "condition": "NO_SNOW",
  "dataStatus": "AVAILABLE"
}

7.2. Дождь и снег по часам

В подробном прогнозе:

hourly[].rainMm
hourly[].snowCm

Семантика числовых значений:

0     = данные получены, осадков нет
> 0   = ожидаемое/рассчитанное количество осадков
null  = соответствующие данные для этого временного периода не получены

Для rainMm backend использует MET.no. На ближнем часовом горизонте отсутствие дождя нормализуется в 0.

Для snowCm на курортных endpoint-ах backend использует закэшированный Open-Meteo прогноз. На покрытом кэшем горизонте отсутствие снега возвращается как 0.00.

Для картинки осадков всё равно использовать condition, а не только проверку rainMm > 0 или snowCm > 0.

8. Статусы доступности данных

type WeatherDataStatus =
  | 'AVAILABLE'
  | 'NOT_RECEIVED'
  | 'NOT_REQUESTED';

AVAILABLE

Backend получил данные.

Важно:

0 + AVAILABLE

означает достоверное нулевое значение.

Например:

{
  "last24HoursCm": 0.00,
  "dataStatus": "AVAILABLE"
}

означает: данные получены, за последние 24 часа снега не было.

NOT_RECEIVED

Backend пытался получить/обновить данные, но актуального значения нет.

Например:

{
  "last24HoursCm": null,
  "forecast24HoursCm": null,
  "condition": "UNKNOWN",
  "dataStatus": "NOT_RECEIVED"
}

NOT_REQUESTED

Данный внешний контур для такого типа запроса сознательно не вызывается.

Типичный пример — запрос по произвольным координатам:

{
  "snow": {
    "last24HoursCm": null,
    "forecast24HoursCm": null,
    "condition": "UNKNOWN",
    "dataStatus": "NOT_REQUESTED"
  },
  "alpineDataStatus": "NOT_REQUESTED"
}

alpineDataStatus

Поле верхнего уровня:

alpineDataStatus: WeatherDataStatus;

Описывает доступность дополнительного Open-Meteo alpine/hourly enrichment:

Для курортного weather при заполненном кэше:

alpineDataStatus = AVAILABLE

Для coordinate-based weather:

alpineDataStatus = NOT_REQUESTED

9. Как работает кэширование и задержки

Это важная часть интеграционного контракта.

9.1. Что происходит в пользовательском запросе

Для курортного endpoint-а схема следующая:

Frontend
   |
   v
Katalka Backend
   |
   +--> MET.no — основной прогноз
   |
   +--> PostgreSQL cache — снежные/alpine данные Open-Meteo
   |
   v
Response

Open-Meteo не вызывается в request path frontend-а.

Поэтому медленный ответ Open-Meteo, его временная недоступность или timeout не должны заставлять мобильного клиента ждать этот внешний сервис.

9.2. Scheduler Open-Meteo

Backend самостоятельно обновляет Open-Meteo cache:

раз в час, на 10-й минуте часа

Текущая конфигурация:

HH:10

Например:

18:10
19:10
20:10
21:10
...

Scheduler проходит по активным курортам с заданными координатами и обновляет данные Open-Meteo.

Один внешний запрос на курорт получает сразу несколько типов данных:

9.3. Горизонт Open-Meteo cache

Сейчас запрашивается:

past_hours = 24
forecast_hours = 72

То есть backend хранит:

Поэтому freezingLevelMeters и snowCm наиболее полно заполнены примерно на ближайшие 3 суток.

На более дальнем MET.no прогнозе эти поля могут снова быть null. Это нормальная граница текущего Open-Meteo cache, а не ошибка.

9.4. Возраст данных

При успешной работе scheduler значение Open-Meteo cache обычно не старше примерно одного часа.

Фактическая задержка зависит от момента запроса:

сразу после HH:10      -> данные только что обновлены
перед следующим HH:10 -> данным около часа

Это сознательный компромисс: снежные и alpine-показатели не требуют обновления на каждый пользовательский запрос.

9.5. Что происходит при ошибке Open-Meteo

Если очередное обновление Open-Meteo не удалось:

То есть система предпочитает немного устаревшие данные полной недоступности снежного блока.

9.6. Рестарт backend

Open-Meteo cache хранится в PostgreSQL, а не только в памяти Java.

Обычный restart backend не уничтожает кэш:

restart приложения
    -> PostgreSQL cache остаётся
    -> пользовательские запросы продолжают читать последние данные

Если база/таблица кэша была очищена полностью, до первого успешного scheduler refresh возможны:

snow.dataStatus = NOT_RECEIVED
alpineDataStatus = NOT_RECEIVED

После следующего успешного обновления они снова становятся AVAILABLE.

9.7. MET.no и задержка frontend

MET.no остаётся основным live-provider и вызывается при формировании погодного ответа.

Поэтому:

Frontend не должен самостоятельно повторно опрашивать endpoint только ради ожидания Open-Meteo cache.

10. Подробный прогноз

type WeatherDetailsResponse = {
  resortId: UUID | null;
  resortName: string | null;
  latitude: number;
  longitude: number;
  updatedAt: string;
  current: WeatherCurrent | null;
  wind: WeatherWind | null;
  snow: WeatherSnow | null;
  visibility: WeatherVisibility | null;
  alpineDataStatus: WeatherDataStatus;
  sun: WeatherSun | null;
  hourly: WeatherHourly[];
  daily: WeatherDaily[];
};

11. Почасовой прогноз

type WeatherHourly = {
  datetime: string;
  temperature: number | null;
  feelsLike: number | null;
  humidity: number | null;
  uv: number | null;
  freezingLevelMeters: number | null;
  windSpeedMetersPerSecond: number | null;
  windGustMetersPerSecond: number | null;
  windDirectionDegrees: number | null;
  windDirection: string | null;
  condition: WeatherCondition;
  snowCm: number | null;
  rainMm: number | null;
};

Пример курортной точки ближнего прогноза:

{
  "datetime": "2026-09-02T18:00:00Z",
  "temperature": 1.9,
  "feelsLike": -1.5,
  "humidity": 93.8,
  "uv": 0.0,
  "freezingLevelMeters": 1590.00,
  "windSpeedMetersPerSecond": 3.4,
  "windGustMetersPerSecond": null,
  "windDirectionDegrees": 241.7,
  "windDirection": "SW",
  "condition": "CLOUDY",
  "snowCm": 0.00,
  "rainMm": 0
}

Особенности:

12. Прогноз по дням

type WeatherDaily = {
  date: string;
  minTemperature: number | null;
  maxTemperature: number | null;
  condition: WeatherCondition;
  snowCm: number | null;
};

На датах, покрытых Open-Meteo hourly cache:

{
  "date": "2026-09-03",
  "minTemperature": 0.6,
  "maxTemperature": 7.9,
  "condition": "CLEAR",
  "snowCm": 0.00
}

После окончания Open-Meteo cache horizon допустимо:

{
  "date": "2026-09-07",
  "minTemperature": 4.8,
  "maxTemperature": 14.8,
  "condition": "CLEAR",
  "snowCm": null
}

Это означает не «ожидается 0 снега», а «для этой даты snow enrichment не покрыт текущим кэшем».

13. Видимость

type WeatherVisibility = {
  meters: number | null;
};

Backend рассчитывает видимость из погодного состояния и доступной информации о тумане.

Пример:

{
  "meters": 20000
}

Frontend может переводить в километры:

const visibilityKm =
  weather.visibility?.meters != null
    ? weather.visibility.meters / 1000
    : null;

14. Солнце

В подробном ответе:

type WeatherSun = {
  sunrise: string | null;
  sunset: string | null;
};

Пример:

{
  "sunrise": "2026-09-01T23:19:00Z",
  "sunset": "2026-09-02T12:55:00Z"
}

Backend отдаёт timestamp. Локальное представление времени — задача frontend-а.

15. Какие null считаются нормальными

Не каждый null означает ошибку.

Нормальные optional null:

wind.gustMetersPerSecond
дальний hourly[].uv
дальний hourly[].freezingLevelMeters
дальний hourly[].snowCm
дальний hourly[].rainMm
дальний daily[].snowCm
редкий дальний WeatherCondition.UNKNOWN

Для snow/alpine данных причину отсутствия нужно определять по:

snow.dataStatus
alpineDataStatus

Не заменять null на 0 на frontend без проверки семантики поля.

16. Рекомендуемый минимальный компонент фронта

const current = weather.current;
const wind = weather.wind;

const viewModel = {
  temperature: current?.temperature ?? null,
  feelsLike: current?.feelsLike ?? null,
  minTemperature: current?.minTemperature ?? null,
  maxTemperature: current?.maxTemperature ?? null,
  condition: current?.condition ?? 'UNKNOWN',
  windSpeed: wind?.speedMetersPerSecond ?? null,
  windDirection: wind?.direction ?? null,
  snow: weather.snow,
  snowDataStatus: weather.snow?.dataStatus ?? 'NOT_RECEIVED',
  alpineDataStatus: weather.alpineDataStatus,
  updatedAt: weather.updatedAt,
};

Иконку выбирать отдельно:

const icon = weatherIconByCondition[viewModel.condition];

17. Важные правила для frontend

Не определять основное состояние погоды самостоятельно по температуре или количеству осадков.

Основное состояние уже определяет backend через condition.

Правильно:

switch (condition) {
  case 'RAIN':
  case 'HEAVY_RAIN':
  case 'LIGHT_RAIN':
    // дождь
    break;

  case 'SNOW':
  case 'HEAVY_SNOW':
  case 'LIGHT_SNOW':
    // снег
    break;
}

Также:

  1. null и 0 — разные состояния.
  2. gustMetersPerSecond = null не означает отсутствие порывов.
  3. snow.dataStatus = NOT_REQUESTED для координатной погоды — штатное поведение.
  4. alpineDataStatus = AVAILABLE не означает, что alpine-поля заполнены на все 10 дней: cache horizon сейчас около 72 часов.
  5. Frontend не должен самостоятельно обращаться к Open-Meteo или MET.no — внешний погодный контур агрегирует backend.

18. Кратко для интеграции

Для карточки курорта:

GET /api/v1/resorts/{resortId}/weather/nearby

Использовать:

current.temperature
current.feelsLike
current.condition
current.minTemperature
current.maxTemperature
wind.speedMetersPerSecond
wind.direction
snow
visibility
updatedAt

Для отдельного экрана погоды:

GET /api/v1/resorts/{resortId}/weather/details

Дополнительно использовать:

sun
hourly[]
daily[]
alpineDataStatus

Для произвольных координат:

GET /api/v1/weather/nearby?lat=...&lng=...
GET /api/v1/weather/details?lat=...&lng=...

При известной высоте рекомендуется передавать:

altitudeMeters

Это влияет на температурный прогноз для горной местности.

condition — главный стабильный код погоды для выбора изображения на фронте.

Ключевая семантика snow/alpine данных:

AVAILABLE      = данные есть
NOT_RECEIVED   = backend ожидал данные, но не получил актуальное значение
NOT_REQUESTED  = этот контур для данного запроса сознательно не запрашивается