# ТЗ: Фикс пагинации fr24_worker — commit per page + дедупликация

**Дата:** 2026-04-22  
**Статус:** READY FOR DEV  
**Приоритет:** Критический (из-за бага слиты 44K кредитов за ночь)

---

## Суть проблемы

`fetch_flight_summaries()` накапливает **все** страницы в памяти, потом `run()` вставляет всё разом.  
При ошибке на любой странице — ничего не коммитится, все данные теряются.

Дополнительно: `both:SVO,both:DME,both:VKO,both:ZIA` возвращает дубли —  
рейс SVO→DME попадает в выборку и как рейс SVO, и как рейс DME.  
Итого: ~3 500 уникальных рейсов → 14 660 записей (×4 дублирование).

---

## Изменения в `fetch_flight_summaries()` → убрать, заменить на генератор

Файл: `ingest/tracks_fr24/fr24_worker.py`

### Было
```python
def fetch_flight_summaries(target_date: date) -> List[Dict]:
    all_items = []
    offset = 0
    while True:
        data = _get(...)
        items = data.get("data", ...) 
        all_items.extend(items)
        if len(items) < PAGE: break
        offset += PAGE
    return all_items
```

### Стало — генератор страниц
```python
def iter_flight_summary_pages(target_date: date):
    """Yield one page (list of flights) at a time. Stops on 402/empty."""
    PAGE = 20
    airports_param = _build_airports_param()
    offset = 0
    seen_fr24_ids = set()   # дедупликация между страницами

    while True:
        try:
            data = _get("/api/flight-summary/full", params={
                "flight_datetime_from": f"{target_date}T00:00:00",
                "flight_datetime_to":   f"{target_date}T23:59:59",
                "airports":             airports_param,
                "limit":                PAGE,
                "offset":               offset,
            })
        except Exception as e:
            log.error("fetch page offset=%d failed: %s", offset, e)
            break

        items = data.get("data", data) if isinstance(data, dict) else data
        if not items or not isinstance(items, list):
            break

        # Дедупликация по fr24_id
        unique = [x for x in items if x.get("fr24_id") not in seen_fr24_ids]
        seen_fr24_ids.update(x["fr24_id"] for x in items if x.get("fr24_id"))

        yield unique

        if len(items) < PAGE:
            break
        offset += PAGE
```

---

## Изменения в `run()` — commit после каждой страницы

### Было
```python
summaries = fetch_flight_summaries(target_date)
stats["flights_found"] = len(summaries)
for item in summaries:
    ...upsert...
    conn.commit()
```

### Стало
```python
for page in iter_flight_summary_pages(target_date):
    stats["flights_found"] += len(page)
    for item in page:
        fr24_id = item.get("fr24_id")
        if not fr24_id:
            continue
        try:
            actual_id = upsert_flight_actual(conn, item, target_date)
            if actual_id:
                stats["flights_upserted"] += 1
            ...FETCH_TRACKS логика...
        except Exception as e:
            conn.rollback()
            stats["errors"] += 1
            log.error("FR24: error processing %s: %s", fr24_id, e)
    
    # Коммит после каждой страницы
    try:
        conn.commit()
        log.debug("Committed page, total so far: %d", stats["flights_upserted"])
    except Exception as e:
        conn.rollback()
        log.error("Commit failed: %s", e)
        stats["errors"] += 1
```

---

## Дополнительно — лимит страниц

Добавить защитный лимит:
```python
MAX_PAGES = int(os.getenv("FR24_MAX_PAGES", "200"))  # 200 × 20 = 4000 рейсов max
```

В генераторе добавить счётчик:
```python
page_num = 0
while True:
    ...
    page_num += 1
    if page_num >= MAX_PAGES:
        log.warning("Reached MAX_PAGES=%d, stopping pagination", MAX_PAGES)
        break
```

В `config.py` добавить:
```python
MAX_PAGES: int = int(os.getenv("FR24_MAX_PAGES", "200"))
```

---

## Проверка

После деплоя запустить:
```
POST http://fr24-vm:8001/run?date=2026-04-19
```
(дата когда кредиты были) — проверить что `flight_actual` заполняется постепенно по страницам.

В БД сразу должны появляться строки, не ждать конца всей загрузки.

---

## Файлы для изменения

- `ingest/tracks_fr24/fr24_worker.py` — главные изменения
- `ingest/tracks_fr24/config.py` — добавить `MAX_PAGES`

**Деплой:** только `docker cp` (не rebuild), потом проверить синтаксис.
