# ТЗ: Обогащение 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
<!-- если sched_source == 'fr24' -->
<span class="src-badge src-fr24" title="Данные из FR24 API">FR</span>
<!-- если sched_source == 'yandex' -->
<!-- не показываем ничего, это норма -->
```

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' 
  ? `<span title="${f.track_points} точек RTL-SDR" style="color:#3fb950;cursor:default">🛰</span>` 
  : '';
```

---

## Изменения в `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 ? `<br><small style="color:#6e7681">${esc(f.registration)}</small>` : "");

// Source badge
const srcBadge = f.sched_source === 'fr24'
  ? `<span class="src-badge src-fr24" title="Источник: FR24">FR</span>` : "";

// Track column
const trackCol = [
  f.fr24_id ? `<a href="https://www.flightradar24.com/data/flights/${f.fr24_id}" target="_blank" class="track-link" title="Трек FR24">✈</a>` : "",
  f.track_source === 'rtlsdr' ? `<span title="${f.track_points||0} точек RTL-SDR" style="color:#3fb950">🛰</span>` : "",
  f.track_source === 'fa'     ? `<span title="${f.track_points||0} точек FA" style="color:#d29922">FA</span>` : "",
].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
