# ТЗ: Обогащение UI расписания данными FR24
**Дата:** 2026-04-21
**Статус:** READY FOR DEV
**Файлы:** `frontend/main.py`, `frontend/static/schedule.html`, `frontend/static/schedule.js`
---
## Что сейчас есть и что не так
Таблица `/schedule` уже получает часть FR24-полей (actual_takeoff, delay, fr24_id, flight_category),
но отображение плохое:
- Трек-ссылка ✈ дублируется в столбцах 2 и 12
- `duration_min` есть в API, но не отображается в таблице
- Runway, дистанция, тип ВС, регистрация — не показываются
- Колонка "Фактически" показывает `actual_at` из Яндекса (часто null), а не FR24 actual_takeoff
- Нет признака источника данных (Яндекс vs FR24)
---
## Изменения в `main.py`
### 1. Обогатить `/api/schedule/data` данными из `flight_actual`
Добавить LEFT JOIN на `fr24_ext.flight_actual` в запрос:
```sql
SELECT
s.flight_number, s.airline_name, s.airport_iata, s.direction,
s.origin_iata, s.destination_iata,
s.scheduled_at, s.actual_at, s.status,
s.flight_date, s.duration_min, s.thread_title,
s.actual_takeoff, s.actual_landed,
s.delay_takeoff_min, s.delay_landed_min,
s.fr24_id, s.flight_category, s.source AS sched_source,
-- из flight_actual:
fa.runway_takeoff, fa.runway_landed,
fa.actual_distance,
fa.flight_time AS fa_flight_time,
fa.reg AS registration,
fa.operated_as,
fa.category AS fa_category,
-- из mart.flights (тип ВС):
mf.aircraft_type,
mf.track_source, mf.track_points
FROM fr24_ext.schedule s
LEFT JOIN fr24_ext.flight_actual fa
ON fa.fr24_id = s.fr24_id AND s.fr24_id IS NOT NULL
LEFT JOIN fr24_mart.flights mf
ON mf.flight_number = s.flight_number
AND mf.flight_date = s.flight_date
WHERE {where}
ORDER BY s.scheduled_at DESC
LIMIT %s OFFSET %s
```
### 2. Добавить новые поля в JSON-ответ
В `schedule_data()` добавить в dict полёта:
```python
"runway_takeoff": r.get("runway_takeoff"),
"runway_landed": r.get("runway_landed"),
"actual_distance": r.get("actual_distance"), # км
"flight_time_min": r.get("fa_flight_time"), # минуты из FA
"registration": r.get("registration"),
"aircraft_type": r.get("aircraft_type"),
"track_source": r.get("track_source"), # rtlsdr/fr24/fa/null
"track_points": r.get("track_points"),
"sched_source": r.get("sched_source"), # yandex/fr24
```
Эффективная длительность:
```python
"duration_eff": r.get("fa_flight_time") or r.get("duration_min"),
```
---
## Изменения в `schedule.html`
### Новые колонки таблицы (вместо текущих 12)
| # | Колонка | Данные |
|---|---------|--------|
| 1 | Дата | flight_date |
| 2 | Рейс | flight_number + cat badge + source badge |
| 3 | Авиакомпания | airline + registration (мелко) |
| 4 | ↑↓ | direction |
| 5 | Маршрут | thread_title или origin → destination |
| 6 | Аэропорт | airport_iata |
| 7 | По расп. | scheduled_at (MSK) |
| 8 | Взлёт факт | actual_takeoff (MSK) + delay badge |
| 9 | Посадка факт | actual_landed (MSK) |
| 10 | Длит. | duration_eff в формате "2ч 35м" |
| 11 | ВПП | runway_takeoff → runway_landed |
| 12 | ВС | aircraft_type |
| 13 | Трек | ✈ ссылка на FR24 + 🛰 если есть RTL-SDR трек |
**Убрать:** дублирующий столбец Трек (#12 старый), убрать Статус (всегда scheduled).
### Source badge в колонке Рейс
```html
FR
```
CSS для src-badge:
```css
.src-badge { border-radius:3px; font-size:9px; font-weight:700; padding:1px 4px; margin-left:3px; }
.src-fr24 { background:#0d2d5e; color:#58a6ff; }
```
### Track source badge в колонке Трек
```js
// ✈ = ссылка на FR24
// 🛰 = RTL-SDR трек есть в mart
const rtlBadge = f.track_source === 'rtlsdr'
? `🛰`
: '';
```
---
## Изменения в `schedule.js`
### `renderTable()` — новые колонки
```js
// Длительность
function fmtDuration(min) {
if (!min) return "—";
const h = Math.floor(min / 60);
const m = min % 60;
return h > 0 ? `${h}ч ${m}м` : `${m}м`;
}
// ВПП
const runway = (f.runway_takeoff || f.runway_landed)
? `${f.runway_takeoff || "?"}→${f.runway_landed || "?"}`
: "—";
// Actual takeoff с задержкой
const actTakeoffCell = f.actual_takeoff
? `${fmtTime(f.actual_takeoff)} ${delayCell(f.delay_takeoff_min)}`
: "—";
// Actual landed
const actLandedCell = f.actual_landed
? fmtTime(f.actual_landed)
: "—";
// Регистрация под авиакомпанией
const airlineCell = `${esc(f.airline || "—")}`
+ (f.registration ? `
${esc(f.registration)}` : "");
// Source badge
const srcBadge = f.sched_source === 'fr24'
? `FR` : "";
// Track column
const trackCol = [
f.fr24_id ? `✈` : "",
f.track_source === 'rtlsdr' ? `🛰` : "",
f.track_source === 'fa' ? `FA` : "",
].filter(Boolean).join(" ");
```
### `renderCards()` — добавить новые поля
В мобильных карточках добавить строки:
- Взлёт факт + задержка
- Посадка факт
- Длительность
- ВПП
- Тип ВС / Регистрация
- Трек (FR24 + RTL-SDR badge)
---
## Деплой
Файлы на VM лежат в контейнере `fr24-api`, монтируются из:
- `/home/fr24/projects/fr24/frontend/main.py`
- `/home/fr24/projects/fr24/frontend/static/schedule.html`
- `/home/fr24/projects/fr24/frontend/static/schedule.js`
После изменений: `docker restart fr24-api`
---
## НЕ делать
- Не трогать другие страницы (index.html, monitoring.html, data_sources)
- Не менять схему БД
- Не трогать main.py кроме функции `schedule_data()` и добавления SQL JOIN