# Фаза 2, Шаг 1: Внешние источники данных — FR24 API + онлайн табло

## Статус
✅ Завершён (20.04.2026)

### Итоги деплоя
- Контейнер `fr24-schedule` запущен и healthy
- Схема `fr24_ext` создана (таблицы `schedule`, `load_state`)
- Backfill выполнен: **28 690 рейсов** за 01.04–19.04.2026
- Cron: ежедневно в 02:00 UTC, режим T-1
- UI: `http://192.168.2.67:8080/schedule`
- API: `GET /api/schedule/data`, `GET /api/schedule/export`

### Отклонения от плана
- **OpenSky отключён** — исторические данные требуют платный аккаунт (403 Forbidden). Обогащение icao24 не работает.
- **DDL правки в процессе деплоя:** `flight_number VARCHAR(50)`, `aircraft_type VARCHAR(100)` — Яндекс возвращает полные названия самолётов ("Сухой Суперджет 100")
- **ZIA код станции:** исправлен с `s9881291` на `s9850865`
- **Файл миграции:** `004_schema_ext.sql` (вместо `003` — конфликт с существующим файлом)

## Цель
Развернуть отдельный контейнер для сбора данных из внешних источников:
1. **FR24 API** — исторические и live треки (как в прототипе noisemap)
2. **Онлайн табло** — расписание рейсов по аэропортам Москвы (SVO, DME, VKO, ZIA)

Данные сохранять в PostgreSQL в отдельной схеме `fr24_ext` — не пересекаться со схемой `fr24` (RTL-SDR ingest).

---

## Бизнес-требования

### BR-1: Сбор данных онлайн табло (приоритет)
- **Аэропорты:** SVO (Шереметьево), DME (Домодедово), VKO (Внуково), ZIA (Жуковский)
- **Режим:** T-1 (загрузка на следующий день за предыдущие сутки)
- **Данные:** номер рейса, авиакомпания, направление (прилёт/вылет), запланированное время, фактическое время (с учётом задержек/отмен), статус, ICAO24 борта (если доступен)
- **Источники:**
  - Яндекс.Расписания API (основной) — расписание, статусы, маршруты
  - OpenSky Network API (дополнительный) — фактические времена, ICAO24 бортов
- **Глубина хранения:** 3 года (параметр, изменяемый)
- **Старт загрузки:** с 01.04.2026
- **Догрузка:** возможность загружать старые периоды с учётом rate limits

### BR-2: UI для просмотра табло
- **Расположение:** в существующем фронтенде (`http://192.168.2.67:8080/schedule`)
- **Формат:** таблица с фильтрами
- **Фильтры:**
  - Дата (диапазон)
  - Тип (прилёт/вылет/все)
  - Аэропорт (SVO/DME/VKO/ZIA/все)
  - Номер рейса (поиск)
  - Часовой интервал (например, 06:00-12:00)
- **Экспорт:** CSV
- **Адаптация:** мобильная версия обязательна

### BR-3: Сбор данных FR24 API (отложен до Шага 2)
- Загрузка треков рейсов над Московской областью через FR24 API
- Покрытие: bbox ~54.5–57.0°N, 35.5–40.5°E
- Стратегия: будет определена в Шаге 2 (витрина данных)
- На Шаге 1 — только табло, FR24 треки позже

### BR-4: Хранение
- Схема `fr24_ext` в существующей PostgreSQL БД
- Не влиять на схему `fr24` (RTL-SDR данные)
- Retention: табло — 3 года (параметр)

---

## Технические требования

### Контейнер `fr24-schedule`
- Python 3.11-slim
- Отдельный сервис в docker-compose.yml
- Переменные окружения: YANDEX_RASP_API_KEY, OPENSKY_USERNAME, OPENSKY_PASSWORD (опционально)
- Два независимых воркера: yandex_worker и opensky_worker
- Логирование в stdout (Docker logs)

### Схема БД `fr24_ext`

```sql
-- Табло аэропортов (объединённые данные из источников)
fr24_ext.schedule (
  schedule_id BIGSERIAL PRIMARY KEY,
  flight_date DATE NOT NULL,
  airport_iata CHAR(3) NOT NULL,  -- SVO, DME, VKO, ZIA
  direction VARCHAR(10) NOT NULL,  -- 'arrival' | 'departure'
  flight_number VARCHAR(10) NOT NULL,
  airline_iata CHAR(2),
  airline_name VARCHAR(100),
  origin_iata CHAR(3),
  destination_iata CHAR(3),
  aircraft_type VARCHAR(10),
  scheduled_at TIMESTAMPTZ NOT NULL,
  estimated_at TIMESTAMPTZ,
  actual_at TIMESTAMPTZ,
  status VARCHAR(20),  -- 'scheduled', 'delayed', 'cancelled', 'departed', 'arrived'
  icao24 CHAR(6),  -- из OpenSky, если доступен
  source VARCHAR(20) NOT NULL,  -- 'yandex' | 'opensky' | 'merged'
  fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(flight_number, airport_iata, scheduled_at, direction)
);

CREATE INDEX idx_schedule_date ON fr24_ext.schedule(flight_date);
CREATE INDEX idx_schedule_airport ON fr24_ext.schedule(airport_iata);
CREATE INDEX idx_schedule_flight ON fr24_ext.schedule(flight_number);
CREATE INDEX idx_schedule_time ON fr24_ext.schedule(scheduled_at);

-- Состояние загрузки (курсор для догрузки)
fr24_ext.load_state (
  state_key VARCHAR(50) PRIMARY KEY,
  state_value JSONB NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Пример state_value для догрузки:
-- {"last_loaded_date": "2026-04-01", "airports": ["SVO", "DME", "VKO", "ZIA"]}
```

### Воркеры

**yandex_worker.py:**
- Загрузка расписания через Яндекс.Расписания API
- Endpoint: `https://api.rasp.yandex.net/v3.0/schedule/`
- Параметры: `station={код станции}&date={YYYY-MM-DD}`
- Коды станций: SVO=s9600213, DME=s9600366, VKO=s9600215, ZIA=s9881291
- Upsert в `fr24_ext.schedule` по `(flight_number, airport_iata, scheduled_at, direction)`
- Rate limit: ~100 запросов/мин (не документирован, консервативная оценка)

**opensky_worker.py:**
- Загрузка arrivals/departures через OpenSky Network API
- Endpoint: `https://opensky-network.org/api/flights/arrival` и `/departure`
- Параметры: `airport={ICAO}&begin={unix_ts}&end={unix_ts}`
- ICAO коды: SVO=UUEE, DME=UUDD, VKO=UUWW, ZIA=UUBW
- Обогащение существующих записей в `fr24_ext.schedule` (добавление icao24)
- Rate limit: 4000 запросов/день для зарегистрированных, 400/день для анонимов
- Батчинг: 1 день = 1 запрос на аэропорт × 2 направления = 8 запросов/день

### Режимы работы

**T-1 режим (основной):**
- Запуск: ежедневно в 02:00 UTC (05:00 MSK)
- Загружает данные за вчерашний день (T-1)
- Последовательность: сначала Яндекс (расписание), потом OpenSky (обогащение icao24)

**Догрузка исторических данных:**
- Скрипт `backfill.py` с параметрами `--start-date 2026-04-01 --end-date 2026-04-19`
- Учитывает rate limits: пауза между запросами, батчинг
- Сохраняет прогресс в `fr24_ext.load_state`
- При ошибке — возобновление с последней успешной даты

---

## ТЗ для Dev-агента

**📋 Детальная спецификация:** `docs/PHASE2_STEP1_DETAILED_SPEC.md`

Детальная спецификация содержит:
- Архитектуру контейнера и структуру процессов
- Полный код модулей с обработкой ошибок
- Конфигурацию и переменные окружения
- Retry логику и rate limiting
- Docker setup и docker-compose интеграцию
- Retention и автоочистку

### Файлы для создания
```
tasks/flightradar24/ingest/schedule/
  Dockerfile
  main.py          # точка входа, запускает оба воркера + cron для T-1
  yandex_worker.py # загрузка из Яндекс.Расписания
  opensky_worker.py # загрузка из OpenSky Network
  backfill.py      # скрипт догрузки исторических данных
  requirements.txt
tasks/flightradar24/db/init/003_schema_ext.sql  # DDL схемы fr24_ext
tasks/flightradar24/frontend/schedule.html      # UI для просмотра табло
tasks/flightradar24/frontend/schedule.js        # логика фильтров и таблицы
```

### Обновить
- `tasks/flightradar24/compose/docker-compose.yml` — добавить сервис `schedule`
- `tasks/flightradar24/frontend/main.py` — добавить endpoints:
  - `GET /schedule` — HTML страница
  - `GET /api/schedule/data` — JSON данные с фильтрами
  - `GET /api/schedule/export` — CSV экспорт

### API endpoints (новые)

```
GET /api/schedule/data
  Query params:
    - date_from: YYYY-MM-DD
    - date_to: YYYY-MM-DD
    - airport: SVO|DME|VKO|ZIA|all (default: all)
    - direction: arrival|departure|all (default: all)
    - flight_number: string (поиск, опционально)
    - time_from: HH:MM (опционально)
    - time_to: HH:MM (опционально)
    - limit: int (default: 100)
    - offset: int (default: 0)
  Response:
    {
      "total": 1234,
      "flights": [
        {
          "flight_number": "SU1234",
          "airline": "Аэрофлот",
          "airport": "SVO",
          "direction": "departure",
          "origin": "SVO",
          "destination": "LED",
          "scheduled_at": "2026-04-19T10:30:00Z",
          "actual_at": "2026-04-19T10:45:00Z",
          "delay_min": 15,
          "status": "departed",
          "icao24": "151ABC"
        },
        ...
      ]
    }

GET /api/schedule/export
  Query params: те же что у /data
  Response: CSV файл
```

### UI требования

**Макет:**
- Шапка с фильтрами (компактная, сворачиваемая на мобиле)
- Таблица с пагинацией (100 строк/страница)
- Кнопка экспорта CSV

**Колонки таблицы:**
- Дата
- Рейс (номер)
- Авиакомпания
- Аэропорт
- Направление (иконка ↑ вылет / ↓ прилёт)
- Маршрут (откуда → куда)
- Запланировано
- Фактически
- Задержка (мин)
- Статус (цветовая индикация)

**Мобильная адаптация:**
- Фильтры в выдвижной панели
- Таблица → карточки на экранах <768px
- Свайп для прокрутки таблицы на планшетах

**Цветовая индикация статусов:**
- `scheduled` — серый
- `departed` / `arrived` — зелёный
- `delayed` — жёлтый
- `cancelled` — красный

---

## Тест-кейсы

### TC-1: Яндекс.Расписания подключение
- [ ] Контейнер стартует без ошибок
- [ ] Яндекс API возвращает данные для SVO за вчерашний день
- [ ] Данные сохраняются в `fr24_ext.schedule`
- [ ] Дубликаты не создаются (upsert работает)

### TC-2: OpenSky Network подключение
- [ ] OpenSky API возвращает arrivals/departures для UUEE (SVO)
- [ ] icao24 добавляется к существующим записям в `fr24_ext.schedule`
- [ ] Rate limit соблюдается (не более 4000 запросов/день)

### TC-3: T-1 режим
- [ ] Ежедневный запуск в 02:00 UTC работает
- [ ] Загружаются данные за вчерашний день (T-1)
- [ ] Все 4 аэропорта обрабатываются

### TC-4: Догрузка исторических данных
- [ ] `backfill.py --start-date 2026-04-01 --end-date 2026-04-19` завершается успешно
- [ ] Прогресс сохраняется в `fr24_ext.load_state`
- [ ] При прерывании — возобновление с последней успешной даты
- [ ] Rate limits соблюдаются (паузы между запросами)

### TC-5: UI табло
- [ ] Страница `/schedule` открывается
- [ ] Фильтры работают (дата, аэропорт, направление, номер рейса, время)
- [ ] Пагинация работает (100 строк/страница)
- [ ] Экспорт CSV работает
- [ ] Мобильная версия адаптирована (карточки вместо таблицы на <768px)

### TC-6: Изоляция схем
- [ ] Запросы к `fr24_ext` не влияют на `fr24`
- [ ] При падении schedule контейнера остальные работают

### TC-7: Retention
- [ ] Данные старше 3 лет удаляются автоматически (cron job)

---

## Зависимости
- Существующая PostgreSQL (fr24-postgres)
- YANDEX_RASP_API_KEY в ~/.openclaw/.env
- OPENSKY_USERNAME, OPENSKY_PASSWORD в ~/.openclaw/.env (опционально, для повышенного лимита)

## Оценка объёма

### Данные
- ~800 рейсов/день (4 аэропорта × ~200 рейсов)
- 3 года = ~876K записей
- Размер записи: ~200 байт
- Объём БД: ~175 MB за 3 года

### Догрузка с 01.04.2026
- 20 дней × 4 аэропорта = 80 дней данных
- Яндекс: 80 запросов (1 запрос/аэропорт/день)
- OpenSky: 160 запросов (2 направления × 4 аэропорта × 20 дней)
- Укладывается в лимиты при батчинге

### Rate limits стратегия
- Яндекс: пауза 1 сек между запросами (консервативно)
- OpenSky: пауза 30 сек между запросами (4000/день = ~1 запрос/22 сек)
- Догрузка 20 дней: ~1.5 часа при последовательной обработке
