Files
enduro-trails/docs/work-items/ET-008/03-acceptance-criteria.md
claude-bot 0840818c9a
All checks were successful
CI / lint (push) Successful in 5s
CI / test (push) Successful in 5s
CI / build (push) Successful in 1s
analyst(ET-008): BRD, TRZ, AC, TestPlan, UI tests v2
2026-06-01 11:44:40 +00:00

20 KiB
Raw Permalink Blame History

type, work_item_id, title, version, status, created_at, updated_at, changelog, authors
type work_item_id title version status created_at updated_at changelog authors
acceptance-criteria ET-008 AC: GPS-треки с публичных платформ на карте 2 draft 2026-06-01 2026-06-01
v2 (2026-06-01): полная переработка под BRD/TRZ v2 — критерии серверной агрегации, дедупликации, MVT-тайлов, фильтров активности/источника, popup, halo-на-спутнике. Предыдущая v1 описывала URL-импорт + OSM live-поиск.
agent:analyst

Acceptance Criteria — ET-008: GPS-треки с публичных платформ на карте

AC-01: Конфигурация источников и регионов

Feature: Расширяемая конфигурация

  Scenario: Включение нового источника
    Given config/gps_sources.yaml содержит источник с enabled=false
    When оператор меняет на enabled=true и перезапускает pipeline
    Then источник участвует в следующем прогоне
    And в /api/gps-tracks/health он появляется в tracks_by_source

  Scenario: Добавление нового региона
    Given оператор добавляет в config/gps_regions.yaml новую запись с bbox
    And запись не превышает 30 строк YAML
    When оператор запускает pipeline без аргументов
    Then новый регион обрабатывается всеми указанными в нём источниками
    And никаких правок Python-кода не требуется

  Scenario: Отключение источника
    Given источник был enabled=true и собрал N треков
    When оператор меняет на enabled=false
    Then следующий прогон pipeline пропускает этот источник
    And ранее собранные треки остаются в БД и отдаются API
    And в фильтре по источнику соответствующий чекбокс не выбран по умолчанию

AC-02: Pipeline сбора

Feature: Pipeline gps_collect.py

  Scenario: Полный прогон по умолчанию
    Given config содержит регион ЦФО+Чувашия и 3 source enabled
    When оператор запускает scripts/gps_collect.py
    Then pipeline проходит по всем регионам и всем enabled-источникам
    And для каждой пары (region, source) пишется запись в pipeline_runs
    And exit code == 0 если хотя бы один трек собран по каждому источнику

  Scenario: Прогон одного источника
    When оператор запускает scripts/gps_collect.py --source osm
    Then обрабатывается только OSM
    And остальные source пропускаются

  Scenario: Падение одного источника не валит остальные
    Given OSM возвращает 503 на весь прогон
    When pipeline запущен
    Then OSM-источник помечается status='error' в pipeline_runs
    And другие источники продолжают работу
    And exit code сигнализирует ошибку (1) если запрошен strict-mode, иначе 0

  Scenario: Dry-run
    When оператор запускает с --dry-run
    Then никаких INSERT в БД не делается
    And pipeline_runs тоже не пишется
    And в stdout выводится план: N треков было бы собрано

  Scenario: Уважение rate-limit
    Given у источника rate_limit_sec=5
    When pipeline делает 10 последовательных запросов к этому источнику
    Then суммарное время ≥ 9 * 5 = 45 сек (между запросами)

AC-03: Дедупликация

Feature: Дедупликация треков

  Scenario: Один трек найден в двух источниках
    Given OSM и EnduroRussia отдали один и тот же трек
       (один автор выложил на обоих)
    And bbox и длина совпадают в пределах допуска
    And даты совпадают
    When pipeline обрабатывает обе записи
    Then в БД одна запись tracks
    And sources_json содержит обоих
    And external_urls_json содержит обе ссылки

  Scenario: Похожие треки разных дат — НЕ дубли
    Given два трека с одинаковым bbox и длиной
    And даты отличаются на > 1 день
    Then записи разные, дедуп НЕ срабатывает

  Scenario: Треки без даты от разных источников
    Given оба трека без created_at
    And bbox и длина совпадают
    Then дедуп срабатывает (по умолчанию консервативный merge)
    And это поведение задокументировано в ADR-002

  Scenario: Метрика < 5% дубликатов
    Given в БД собрано ≥ 5000 треков
    When QA-инженер выбирает 100 случайных треков и руками проверяет дубли
    Then не более 5 треков (5%) являются дублями

AC-04: Endpoint /api/gps-tracks (GeoJSON)

Feature: GeoJSON endpoint

  Scenario: Запрос с малым bbox
    Given в БД 1000 треков, из них 50 в bbox=[37.5,55.6,37.7,55.8]
    When клиент шлёт GET /api/gps-tracks?bbox=37.5,55.6,37.7,55.8
    Then ответ 200, FeatureCollection с 50 features
    And total_in_bbox=50, returned=50, truncated=false
    And time ≤ 300 мс p95

  Scenario: Bbox с обрезкой по limit
    Given в bbox 1500 треков
    When клиент шлёт GET .../api/gps-tracks?bbox=...&limit=500
    Then returned=500, total_in_bbox=1500, truncated=true

  Scenario: Фильтр по активности
    Given в bbox 100 треков, 20 enduro, 30 moto, 50 hike
    When клиент шлёт ?activity=enduro,moto
    Then returned=50

  Scenario: Фильтр по источнику
    Given в bbox 100 треков: 60 OSM, 30 EnduroRussia, 10 ttrails
    When клиент шлёт ?source=osm
    Then returned=60

  Scenario: Невалидный bbox
    When клиент шлёт bbox=foo
    Then ответ 400

  Scenario: bbox вне диапазона координат
    When клиент шлёт bbox=200,100,250,150
    Then ответ 400

  Scenario: Поля feature.properties
    Then каждая feature содержит: name, activity_type, user, created_at,
         length_km, sources (array), external_urls (array)

AC-05: Endpoint /api/gps-tracks/tiles MVT

Feature: MVT tiles

  Scenario: Отдача тайла на z=10
    Given в БД есть треки в видимой области
    When клиент шлёт GET /api/gps-tracks/tiles/10/623/325.mvt
    Then ответ 200, Content-Type: application/x-protobuf
    And тело содержит layer gps_tracks с LineString features

  Scenario: Тайл из кэша
    Given тайл уже запрашивали
    When повторный запрос того же z/x/y
    Then header X-Cache: HIT
    And время ≤ 20 мс p95

  Scenario: Упрощение геометрии на низких зумах
    Given исходный трек 1000 точек на z=7
    When MVT генерируется
    Then feature имеет упрощённую геометрию (≤ 100 точек после Douglas-Peucker)

  Scenario: Properties фичи в MVT
    Then feature.properties содержит: id, activity, source, sources,
         length_km, name, ext_url

AC-06: Endpoint health

Feature: Health endpoint

  Scenario: Полный отчёт
    When клиент шлёт GET /api/gps-tracks/health
    Then ответ 200 JSON содержит:
      | db_path             |
      | db_size_mb          |
      | tracks_total        |
      | tracks_by_source    | (объект source_id → int)
      | tracks_by_activity  | (объект activity → int)
      | last_pipeline_run   | (объект с started/finished/sources_ok/sources_error)
      | tile_cache_size     |

  Scenario: Health без БД
    Given БД отсутствует на диске
    When клиент шлёт GET /api/gps-tracks/health
    Then ответ содержит tracks_total=0 и предупреждение о БД (или 503)

AC-07: Чекбокс «Публичные треки» в попапе

Feature: Включение слоя из попапа

  Scenario: Чекбокс присутствует
    Given пользователь нажимает #terrain-toggle
    Then в попапе #terrain-popup видна строка «Публичные треки» с чекбоксом

  Scenario: Включение слоя
    When пользователь ставит галку «Публичные треки»
    Then на карте появляются линии треков
    And localStorage['gps-tracks-enabled'] = 'true'
    And рядом с чекбоксом появляется ссылка «Фильтры…»

  Scenario: Выключение слоя
    When пользователь снимает галку
    Then линии исчезают с карты
    And localStorage = 'false'
    And ссылка «Фильтры…» скрывается

  Scenario: Подсказка о минимальном zoom
    Given текущий zoom < 8
    And чекбокс включён
    Then рядом с чекбоксом видна подсказка «Зум 8    And линии на карте не видны (без ошибок)

AC-08: Фильтры по активности и источнику

Feature: Sheet фильтров

  Scenario: Открытие sheet
    Given слой включён
    When пользователь нажимает «Фильтры…»
    Then открывается #sheet-gps-filters
    And видны секции «Тип активности», «Источник», «Цвет линий»
    And по умолчанию выбраны все активности и все источники

  Scenario: Фильтрация по активности
    Given в видимой области карты 743 трека, 200 enduro, 50 moto, …
    When пользователь снимает все галки кроме «Эндуро» и «Мото»
    Then на карте отображаются только enduro и moto треки
    And gps-stat-shown отражает новое число
    And фильтрация мгновенная (≤ 200 мс), без сетевого запроса

  Scenario: Фильтрация по источнику
    Given включено 3 источника
    When пользователь снимает «OSM»
    Then OSM-треки скрываются на карте

  Scenario: Переключение режима цвета
    Given color-mode = 'source'
    When пользователь выбирает «По активности»
    Then цвета линий перерисовываются по палитре активности
    And localStorage сохраняет 'gps-tracks-color-mode' = 'activity'

  Scenario: Сохранение фильтров между сессиями
    Given пользователь настроил фильтры (только enduro, только OSM)
    When пользователь перезагружает страницу
    Then sheet-фильтров восстанавливает те же чекбоксы
    And слой отображает только enduro+OSM треки

AC-09: Popup при клике на трек

Feature: Popup трека

  Scenario: Клик по линии трека
    Given на карте отображается слой публичных треков
    When пользователь кликает на линию трека
    Then открывается popup с полями: name, activity (иконка+текст),
         length_km, points_count, created_at, user, sources (со ссылками)

  Scenario: Трек из двух источников
    Given трек имеет sources=['osm', 'enduro_russia']
    Then popup показывает обе ссылки

  Scenario: Трек без user/name
    Then popup показывает «Без названия» и не показывает строку «Автор»

  Scenario: Клик по фону карты
    Given открыт popup
    When пользователь кликает на пустое место карты
    Then popup закрывается

AC-10: Z-order и совместимость с другими слоями

Feature: Z-order

  Scenario: Слой выше trails, ниже маршрута OSRM
    Given на карте: OSM tiles + trails + публичные треки + маршрут OSRM
    Then визуально маршрут OSRM перекрывает публичные треки
    And публичные треки перекрывают trails из vector tiles
    And базовая карта (OSM) — самый нижний

  Scenario: Совместимость с ET-006 (личные GPX)
    Given пользователь загрузил свой GPX-файл (ET-006)
    And слой публичных треков включён
    Then оба видны параллельно
    And личный трек визуально выше публичных

AC-11: Совместимость со спутниковой подложкой (ET-007)

Feature: Halo на спутнике

  Scenario: Включение спутника
    Given слой публичных треков включён
    When пользователь переключает подложку на «Спутник»
    Then линии треков видны на спутнике
    And появляется белая обводка (halo) для контраста

  Scenario: Возврат на схему
    When пользователь возвращается на «Схема»
    Then halo скрывается
    And линии отображаются обычными цветами

  Scenario: Halo учитывает чекбокс
    Given спутник активен
    When пользователь выключает чекбокс «Публичные треки»
    Then и линии, и halo скрываются

AC-12: Сохранение при смене стиля карты

Feature: Переживание setStyle()

  Scenario: Переключение тёмной темы
    Given слой включён, фильтры настроены
    When пользователь переключает тёмную тему (вызывает map.setStyle())
    Then слой публичных треков восстанавливается
    And линии видны с теми же цветами по тому же color-mode
    And фильтры активности/источника сохранены

  Scenario: Переключение спутник→схема
    Given слой включён, активен спутник
    When пользователь переключается на схему
    Then слой остаётся видим, halo выключается

  Scenario: Включение hillshade
    Given слой включён
    When пользователь включает hillshade
    Then публичные треки остаются видны (поверх hillshade)

AC-13: Производительность

Feature: SLA отклика

  Scenario: GeoJSON p95
    When 100 запросов GET /api/gps-tracks?bbox=… с500 треков в bbox
    Then p95300 мс

  Scenario: MVT cold
    When запрос MVT-тайла без кэша
    Then p95200 мс

  Scenario: MVT hot
    When повторный запрос того же тайла
    Then 20 мс, X-Cache: HIT

  Scenario: Pan/zoom без фризов
    Given слой включён с 500 треками в видимой области
    When пользователь делает 10 быстрых pan-операций
    Then нет видимых фризов (FPS ≥ 30 на десктопе)

AC-14: Защита от шторма запросов

Feature: Debounce и AbortController

  Scenario: Быстрый pan не плодит запросов
    Given слой включён на z ≥ 12
    When пользователь делает 5 быстрых pan-операций за 1 секунду
    Then выполняется не более 2 запросов /api/gps-tracks (debounce 500ms)
    And предыдущие запросы отменены AbortController

  Scenario: На z < 8 запросов нет
    Given пользователь на z=5
    When пользователь панит карту
    Then запросов /api/gps-tracks?bbox=… не выполняется

AC-15: Атрибуция

Feature: Атрибуция источников

  Scenario: На карте видна атрибуция
    Given слой включён, включены OSM и EnduroRussia
    Then в правом нижнем углу карты отображается строка
         «© OpenStreetMap contributors (ODbL) | EnduroRussia.ru»

  Scenario: Popup содержит ссылку на оригинал
    Given пользователь открыл popup трека
    Then в нём видна ссылка «↗» на источник (или несколько)
    When пользователь кликает на ссылку
    Then открывается новая вкладка с оригиналом

AC-16: Безопасность и юридические гарантии

Feature: Юридический минимум

  Scenario: Источник без ADR не активируется
    Given оператор пытается включить новый source в gps_sources.yaml
    But ADR licensing-review отсутствует
    Then pipeline-tests падают (CI блокирует merge)

  Scenario: Pipeline не сохраняет запрещённые поля
    Given source-ADR требует не сохранять `user`
    When pipeline получает трек с user='Vasya'
    Then в БД user=NULL для этой записи

  Scenario: Удаление по запросу автора
    Given автор оригинала пометил трек удалённым на источнике
    When следующий прогон pipeline обнаруживает это
    Then запись в нашей БД помечается как удалённая или удаляется

AC-17: Расширяемость на новые регионы

Feature: Добавление региона ≤ 30 строк YAML

  Scenario: Добавление «Северный Кавказ»
    Given существующий config/gps_regions.yaml
    When разработчик добавляет YAML-блок региона: id, name, bbox, enabled,
         sources (≤ 30 строк суммарно)
    And запускает pipeline
    Then регион обрабатывается всеми указанными источниками
    And никаких правок Python-файлов не требуется
    And в /api/gps-tracks/health новый регион виден в last_pipeline_run.regions