auto-sync: 2026-04-20 23:00:01

This commit is contained in:
Stream
2026-04-20 23:00:01 +03:00
parent 7bfccd1ee0
commit d2ac1ed3e5

View File

@@ -1,137 +1,349 @@
# Фаза 2, Шаг 2: Витрина данных — объединение FR24 API + табло + RTL-SDR
# Фаза 2, Шаг 2: Загрузка треков + Витрина данных
## Статус
🔲 Не начат (зависит от Шага 1)
🔲 БТ готово, реализация не начата
## Цель
Спроектировать и реализовать витрину данных (data mart) которая объединяет три источника:
1. **FR24 API** — треки и рейсы из внешнего API (схема `fr24_ext`)
2. **Онлайн табло** — расписание и статусы рейсов (схема `fr24_ext`)
3. **RTL-SDR dongle** — локально принятые ADS-B треки (схема `fr24`)
Витрина — это набор материализованных представлений и таблиц в схеме `fr24_mart`, которые склеивают данные из всех источников в единую модель для визуализации и анализа.
Загрузить треки рейсов из двух внешних источников (FR24 API и FlightAware AeroAPI) в отдельные таблицы, затем объединить всё в витрину `fr24_mart` для шумовой карты.
---
## Бизнес-требования
## Архитектура источников
### BR-1: Единая модель рейса
- Рейс идентифицируется по callsign + дата
- Один рейс может иметь данные из нескольких источников одновременно
- Приоритет данных: RTL-SDR (наиболее точный) > FR24 API > табло
- Витрина должна показывать из какого источника взяты данные
### BR-2: Обогащение данных
- К треку из RTL-SDR добавлять данные из табло (авиакомпания, маршрут, тип ВС)
- К треку из FR24 API добавлять данные из табло (статус, фактическое время)
- Если рейс есть в табло но нет трека — показывать как "нет данных о треке"
### BR-3: Покрытие источников
- Метрика: % рейсов из табло для которых есть трек (RTL-SDR или FR24)
- Метрика: % рейсов с треком из RTL-SDR vs только FR24
- Метрика: среднее качество трека (кол-во точек, покрытие маршрута)
### BR-4: Шумовая модель
- Витрина должна содержать поле `noise_score` для каждой точки трека
- Расчёт: на основе высоты, скорости, типа ВС (упрощённая модель из прототипа)
- Агрегация: суммарный шум по сетке 0.01° × 0.01° за период
```
fr24_ext.schedule ← Яндекс.Расписания (готово ✅)
fr24_ext.flight_tracks_fr24 ← FR24 API (Flight tracks)
fr24_ext.flight_tracks_fa ← FlightAware AeroAPI (track endpoint)
fr24.flights + fr24.positions ← RTL-SDR (готово ✅)
fr24_mart.flights_unified
fr24_mart.track_points
fr24_mart.noise_grid
```
---
## Технические требования
## Источник 1: FR24 API — Flight Tracks
### Ключевые решения
- **Endpoint:** `GET /api/flight-tracks?flight_id={fr24_id}` — полный трек рейса одним запросом
- **Стоимость:** 40 кредитов/рейс ($0.012)
- **Ключ:** `MVM0hi4S7RRh7Dm4EOl1ShpDPc8CrmITXT2LY5y4dd84a62a` (KEY2, подтверждён рабочим)
- **Тариф:** Explorer (30K кредитов/мес, история 30 дней)
- **Загрузка:** ТОЛЬКО по явной команде Славы (не автоматически)
- **Период:** T-1 (вчерашний день)
- **Scope:** рейсы аэропортов SVO/DME/VKO/ZIA (через `flight-summary/light` за день)
- **Rate limit:** 10 запросов/мин (Explorer)
### Стратегия загрузки
1. `flight-summary/light` → список fr24_id за вчерашний день по 4 аэропортам (~1500 рейсов)
2. Для каждого fr24_id → `flight-tracks` → треки с точками
3. Сохранить в `fr24_ext.flight_tracks_fr24` + `fr24_ext.track_points_fr24`
4. Отметить загрузку в `fr24_ext.load_state`
### Что возвращает `flight-summary/light`
```json
{
"fr24_id": "3f509576",
"flight": "SU208",
"callsign": "AFL208",
"type": "B77W",
"reg": "RA-73140",
"orig_icao": "UUEE",
"dest_icao": "ZSPD",
"datetime_takeoff": "2026-04-20T17:16:13Z",
"datetime_landed": null,
"flight_ended": true
}
```
### Что возвращает `flight-tracks`
```json
[{
"fr24_id": "3f4f0101",
"tracks": [{
"timestamp": "2026-04-20T10:07:14Z",
"lat": 49.72, "lon": 43.57,
"alt": 25975, "gspeed": 432,
"vspeed": 1280, "track": 336,
"squawk": "0054", "source": "MLAT"
}, ...]
}]
```
~782 точки на рейс (интервал ~5-6 сек)
### Расчёт расхода кредитов
- Summary: 1500 × 3 кредита = 4500 кредитов
- Tracks: 1500 × 40 кредитов = 60 000 кредитов
- **Итого за 1 день: ~64 500 кредитов**
- Explorer (30K/мес): хватит на ~0.5 дня → нужно пополнять кредиты перед каждым запуском
- При пополнении до Essential ($90/мес, 333K): ~5 дней истории/мес
### Схема БД `fr24_ext`
```sql
CREATE TABLE fr24_ext.flight_tracks_fr24 (
id BIGSERIAL PRIMARY KEY,
fr24_id VARCHAR(20) NOT NULL UNIQUE,
flight_number VARCHAR(20),
callsign VARCHAR(20),
aircraft_type VARCHAR(10),
registration VARCHAR(15),
origin_icao VARCHAR(5),
destination_icao VARCHAR(5),
actual_takeoff TIMESTAMPTZ,
actual_landed TIMESTAMPTZ,
flight_date DATE NOT NULL,
fetched_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE fr24_ext.track_points_fr24 (
id BIGSERIAL PRIMARY KEY,
track_id BIGINT REFERENCES fr24_ext.flight_tracks_fr24(id),
observed_at TIMESTAMPTZ NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lon DOUBLE PRECISION NOT NULL,
altitude_ft INTEGER,
gspeed_kt INTEGER,
vspeed_fpm INTEGER,
heading SMALLINT,
squawk VARCHAR(5),
source VARCHAR(10)
);
CREATE INDEX ON fr24_ext.track_points_fr24 (track_id, observed_at);
```
---
## Источник 2: FlightAware AeroAPI — Треки
### Ключевые решения
- **Endpoint:** `GET /aeroapi/flights/{fa_flight_id}/track`
- **Ключ:** `7qMijd3b3gVudezng3eVhKtup8iKFr75` (подтверждён рабочим)
- **Тариф:** Personal (500 запросов/мес бесплатно)
- **Использование:** треки рейсов, для которых нет трека в FR24 или нужна верификация
- **История:** до января 2011
- **Загрузка:** только по команде (как и FR24)
### Стратегия загрузки
1. `GET /aeroapi/flights/{ident}` → получить fa_flight_id для рейса
2. `GET /aeroapi/flights/{fa_flight_id}/track` → полный трек
3. Сохранить в `fr24_ext.flight_tracks_fa` + `fr24_ext.track_points_fa`
### Что возвращает track endpoint
```json
{
"actual_distance": 948,
"positions": [{
"altitude": 222, // сотни футов
"groundspeed": 382,
"heading": 315,
"latitude": 54.75,
"longitude": 37.55,
"timestamp": "2026-04-20T11:00:14Z",
"update_type": "M" // M=ADS-B, D=dead reckoning
}, ...]
}
```
~400 точек на рейс (интервал ~30-60 сек)
### Бонус — фактические времена из `/flights/{ident}`
FlightAware возвращает полный набор фактических времён:
- `actual_off` — фактический взлёт (wheels off)
- `actual_on` — фактическая посадка (wheels on)
- `departure_delay` — задержка вылета (сек)
- `arrival_delay` — задержка прилёта (сек)
Эти данные можно сохранять в `fr24_ext.schedule` (UPDATE) для обогащения табло.
### Расчёт лимитов
- Personal: 500 запросов/мес
- На каждый рейс нужно 2 запроса (flights + track) = 250 рейсов/мес бесплатно
- При T-1 загрузке: 250 рейсов/мес ÷ 22 рабочих дня ≈ **11 рейсов/день** на free tier
- Для полного покрытия нужен Standard тариф (уточнить цену в portal)
### Схема БД `fr24_ext`
```sql
CREATE TABLE fr24_ext.flight_tracks_fa (
id BIGSERIAL PRIMARY KEY,
fa_flight_id VARCHAR(50) NOT NULL UNIQUE,
ident_iata VARCHAR(10),
ident_icao VARCHAR(10),
registration VARCHAR(15),
aircraft_type VARCHAR(10),
origin_icao VARCHAR(5),
destination_icao VARCHAR(5),
actual_off TIMESTAMPTZ,
actual_on TIMESTAMPTZ,
departure_delay INTEGER, -- секунды
arrival_delay INTEGER, -- секунды
actual_distance INTEGER, -- nautical miles
flight_date DATE NOT NULL,
fetched_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE fr24_ext.track_points_fa (
id BIGSERIAL PRIMARY KEY,
track_id BIGINT REFERENCES fr24_ext.flight_tracks_fa(id),
observed_at TIMESTAMPTZ NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lon DOUBLE PRECISION NOT NULL,
altitude_ft INTEGER, -- сотни футов × 100
gspeed_kt INTEGER,
heading SMALLINT,
update_type VARCHAR(5) -- M=ADS-B, D=dead reckoning
);
CREATE INDEX ON fr24_ext.track_points_fa (track_id, observed_at);
```
---
## Витрина `fr24_mart`
### Цель
Единая модель для шумовой карты — объединяет все источники, рассчитывает шум по сетке.
### Приоритет источников для трека
`RTL-SDR` > `FR24 API` > `FlightAware` > нет данных
RTL-SDR точнее (5-сек интервал, локальный приём), FR24 детальнее FlightAware (5 vs 30 сек).
### Схема `fr24_mart`
```sql
-- Единая таблица рейсов (объединение источников)
fr24_mart.flights_unified (
unified_id, callsign, flight_date,
origin_iata, destination_iata, airline_iata, aircraft_type,
scheduled_dep, actual_dep, status,
has_rtlsdr_track BOOL, has_fr24_track BOOL,
rtlsdr_points INT, fr24_points INT,
source_priority TEXT, -- 'rtlsdr' | 'fr24' | 'schedule_only'
updated_at
)
-- Единая таблица рейсов
CREATE TABLE fr24_mart.flights (
id BIGSERIAL PRIMARY KEY,
flight_number VARCHAR(20),
callsign VARCHAR(20),
icao24 CHAR(6),
airline_iata VARCHAR(5),
origin_iata VARCHAR(5),
destination_iata VARCHAR(5),
aircraft_type VARCHAR(50),
flight_date DATE NOT NULL,
scheduled_dep TIMESTAMPTZ,
actual_dep TIMESTAMPTZ,
actual_arr TIMESTAMPTZ,
duration_min INTEGER,
-- источники
has_schedule BOOLEAN DEFAULT false,
has_rtlsdr BOOLEAN DEFAULT false,
has_fr24 BOOLEAN DEFAULT false,
has_fa BOOLEAN DEFAULT false,
track_source VARCHAR(10), -- 'rtlsdr'|'fr24'|'fa'|null
track_points INTEGER,
-- ключи источников
schedule_id BIGINT,
fr24_track_id BIGINT,
fa_track_id BIGINT,
rtlsdr_flight_id BIGINT,
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE UNIQUE INDEX ON fr24_mart.flights (flight_date, callsign);
-- Единые точки трека (лучший доступный источник)
fr24_mart.track_points_unified (
point_id, unified_id, observed_at,
lat, lon, altitude_m, speed_kt, heading_deg,
source TEXT, -- 'rtlsdr' | 'fr24'
noise_score FLOAT
)
-- Единые точки трека (лучший источник)
CREATE TABLE fr24_mart.track_points (
id BIGSERIAL PRIMARY KEY,
flight_id BIGINT REFERENCES fr24_mart.flights(id),
observed_at TIMESTAMPTZ NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lon DOUBLE PRECISION NOT NULL,
altitude_m INTEGER,
speed_kt INTEGER,
heading SMALLINT,
source VARCHAR(10)
);
CREATE INDEX ON fr24_mart.track_points (flight_id, observed_at);
CREATE INDEX ON fr24_mart.track_points (lat, lon);
-- Шумовая сетка (агрегат)
fr24_mart.noise_grid (
grid_id, grid_lat FLOAT, grid_lon FLOAT,
period_date DATE,
flight_count INT, total_noise_score FLOAT,
avg_altitude_m FLOAT,
updated_at
)
-- Шумовая сетка (агрегат по 0.01°)
CREATE TABLE fr24_mart.noise_grid (
id BIGSERIAL PRIMARY KEY,
grid_lat NUMERIC(7,4) NOT NULL,
grid_lon NUMERIC(7,4) NOT NULL,
period_date DATE NOT NULL,
flight_count INTEGER DEFAULT 0,
noise_score FLOAT DEFAULT 0,
avg_altitude_m FLOAT,
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (grid_lat, grid_lon, period_date)
);
CREATE INDEX ON fr24_mart.noise_grid (period_date);
-- Покрытие источников (метрики)
fr24_mart.source_coverage (
coverage_date DATE,
total_schedule INT,
with_rtlsdr INT, with_fr24 INT, schedule_only INT,
rtlsdr_pct FLOAT, fr24_pct FLOAT
)
-- Метрики покрытия источников
CREATE TABLE fr24_mart.source_coverage (
coverage_date DATE PRIMARY KEY,
total_schedule INTEGER DEFAULT 0,
with_rtlsdr INTEGER DEFAULT 0,
with_fr24 INTEGER DEFAULT 0,
with_fa INTEGER DEFAULT 0,
schedule_only INTEGER DEFAULT 0,
rtlsdr_pct FLOAT,
fr24_pct FLOAT,
fa_pct FLOAT,
updated_at TIMESTAMPTZ DEFAULT now()
);
```
### Обновление витрины
- Отдельный контейнер `fr24-mart` или cron внутри существующего
- Обновление каждые 10 минут
- Инкрементальное (только новые данные)
- Контейнер `fr24-mart` (отдельный)
- Cron: ежечасно (инкрементальное обновление)
- Полный rebuild по флагу
---
## ТЗ для Dev-агента
## UI статистики — страница `/data-sources`
### Файлы для создания
```
tasks/flightradar24/ingest/mart/
Dockerfile
main.py # планировщик обновления витрины
build_unified.py # логика объединения источников
noise_model.py # расчёт noise_score (перенос из прототипа)
requirements.txt
tasks/flightradar24/db/init/004_schema_mart.sql # DDL схемы fr24_mart
```
### Блоки страницы
### Обновить
- `tasks/flightradar24/compose/docker-compose.yml` — добавить сервис `mart`
**1. Покрытие источников** (за выбранный период, по дням)
- Яндекс.Расписания: N рейсов
- FR24 треки: N рейсов, % от табло
- FlightAware: N рейсов, % от табло
- RTL-SDR: N рейсов, % от табло
- График: stacked bar по дням
**2. Качество данных**
- % рейсов с маршрутом (origin/destination)
- % рейсов с треком
- % рейсов с фактическими временами
- % рейсов с типом самолёта
- Медиана точек в треке по источнику
**3. Расход кредитов FR24**
- Потрачено кредитов на загрузку (из load_state)
- Среднее кредитов/день
**4. Топ авиакомпаний** (по количеству рейсов)
**5. Топ маршрутов** (откуда/куда)
**6. Загрузка аэропортов** (SVO/DME/VKO/ZIA по часам)
---
## Тест-кейсы
## Контейнеры
### TC-1: Объединение источников
- [ ] Рейс с треком из RTL-SDR и записью в табло — объединяется корректно
- [ ] Рейс только в табло — создаётся запись с `source_priority = 'schedule_only'`
- [ ] Рейс только в RTL-SDR (нет в табло) — создаётся с `source_priority = 'rtlsdr'`
### TC-2: Приоритет источников
- [ ] При наличии RTL-SDR трека — используется он, не FR24
- [ ] При отсутствии RTL-SDR — используется FR24
### TC-3: Шумовая модель
- [ ] noise_score рассчитывается для каждой точки трека
- [ ] noise_grid обновляется после добавления новых точек
- [ ] Агрегация по сетке 0.01° корректна
### TC-4: Метрики покрытия
- [ ] source_coverage обновляется ежедневно
- [ ] % покрытия считается корректно
### TC-5: Производительность
- [ ] Обновление витрины за 10 минут занимает < 60 сек
- [ ] Запрос noise_grid за 1 день < 500 мс
| Контейнер | Назначение | Порт |
|---|---|---|
| `fr24-schedule` | Яндекс.Расписания (готов ✅) | 8000 |
| `fr24-tracks-fr24` | Загрузчик FR24 треков | 8001 |
| `fr24-tracks-fa` | Загрузчик FlightAware треков | 8002 |
| `fr24-mart` | Обновление витрины | — |
---
## Зависимости
- Шаг 1 (схема fr24_ext должна быть заполнена)
- Схема fr24 (RTL-SDR данные)
- Модель шума из прототипа (tasks/flightradar24/prototype/docs/NOISE_MODEL.md)
## Зависимости и порядок реализации
1. **DDL** — схемы `fr24_ext` (доп. таблицы) + `fr24_mart`
2. **fr24-tracks-fr24** — воркер FR24 API (ручной запуск)
3. **fr24-tracks-fa** — воркер FlightAware API (ручной запуск)
4. **fr24-mart** — воркер витрины (автоматический, каждый час)
5. **UI `/data-sources`** — страница статистики
---
## Открытые вопросы
- [ ] Тариф FlightAware Standard — уточнить цену за track запрос
- [ ] Глубина исторической загрузки FR24 — сколько дней заливать при первом запуске
- [ ] Шумовая модель — использовать прототип из `tasks/flightradar24/prototype/` или переписать