analyst(ET-008): BRD, TRZ, AC, TestPlan, UI tests v2
All checks were successful
CI / lint (push) Successful in 5s
CI / test (push) Successful in 5s
CI / build (push) Successful in 1s

This commit is contained in:
claude-bot
2026-06-01 11:44:40 +00:00
parent dc557ab884
commit 0840818c9a
5 changed files with 2222 additions and 1257 deletions

View File

@@ -2,10 +2,12 @@
type: brd
work_item_id: ET-008
title: "BRD: GPS-треки с публичных платформ на карте"
version: 1
version: 2
status: draft
created_at: 2026-06-01
updated_at: 2026-06-01
changelog:
- "v2 (2026-06-01): полная переработка под реальный business request — серверная агрегация из ≥3 источников по региону, дедупликация, фильтры по активности и источнику, расширяемость на регионы. Предыдущая v1 трактовала задачу как URL-импорт + OSM live-поиск, что не соответствовало бизнес-цели."
authors:
- "agent:analyst"
---
@@ -14,85 +16,213 @@ authors:
## 1. Цель
Дать пользователю возможность увидеть на карте Enduro Trails GPS-треки
с публичных источников без скачивания файлов вручную: либо вставив
прямую ссылку на GPX, либо найдя чужие публичные треки в видимой
области карты (через OSM Public GPS Traces).
Показать пользователю Enduro Trails реальные GPS-треки, **заранее
собранные с публичных платформ** (Wikiloc, Offmaps.ru, Тропинки.ру,
EnduroRussia.ru, OSM Public GPS Traces, Nakarte.me, Komoot и т.п.) и
сохранённые на сервере. Цель — три практические задачи мотоэндуриста:
1. **Видеть реальные дороги/тропы, которых нет в OSM.** Vector-тайлы
`trails` показывают только OSM-данные; реальные грунтовки/тропы из
GPS-логов дают информацию, которой в OSM никогда не было.
2. **Понимать, где реально ездят.** Плотность публичных треков на
участке — прямая прокси-метрика популярности и проходимости.
3. **Выявлять «мёртвые» дороги.** OSM-грунтовка, не покрытая ни одним
публичным треком за последние N лет — кандидат на «давно никто не
ездит, может быть заросла».
ET-008 даёт **новый отдельный слой** (поверх `trails`, ниже маршрута
OSRM) с отдельными линиями (не heatmap), цветом по источнику или типу
активности, с UI-фильтрами.
## 2. Контекст
- ET-006 реализовал клиентский GPX-стек: парсер, модель
`window.gpxTracks`, sheet `#sheet-gpx`, статистика, профиль высот,
переживание `map.setStyle()` через `rebuildGpxOverlays()`. Источник
данных — только локальный файл пользователя.
- Roadmap-фаза PH-3 «Smart Route» включает работу с GPX (импорт/экспорт).
- В стеке нет пользовательских аккаунтов и БД пользователей. Платформы с
обязательным OAuth (Strava, Komoot) поэтому вне scope текущей итерации.
- Браузер не может тянуть GPX напрямую с большинства публичных платформ
из-за CORS. OSM API не разрешает кросс-доменные запросы → прокси
через FastAPI обязателен.
- OSM Public GPS Traces — открытый бесплатный источник публичных
GPS-треков, формат GPX, есть bbox-поиск, нет авторизации для чтения.
- Vector-тайлы из OSM (`/api/tiles/{z}/{x}/{y}.mvt`) уже отдают
грунтовки/тропы/POI. ET-008 их **не заменяет** — добавляет
параллельный слой публичных GPS-треков.
- ET-006 реализовал клиентский импорт GPX-файлов пользователем
(`window.gpxTracks`, `#sheet-gpx`). Это **другой сценарий**: ET-006 —
«мой трек в памяти браузера», ET-008 — «треки сообщества с сервера».
Модели данных не пересекаются.
- Стек БД: SQLite + Spatialite. Для ET-008 заводится **отдельная** БД
`data/gps_tracks.sqlite` — чтобы не смешивать данные с основной
`centralfederal.sqlite` и иметь независимый цикл обновления / бэкапа.
- Pipeline сбора**офлайн-скрипт на cron**, не runtime. На запрос
пользователя сервер отдаёт уже собранные данные.
- Регион MVP: **ЦФО + Чувашия** (18 субъектов ЦФО + Чувашская
Республика, площадь ≈ 670 тыс. км²). Расширение на другие регионы —
через конфиг-файл.
## 3. Scope
### In scope
| # | Функция |
|------|---------|
| F-01 | Поле ввода URL прямой ссылки на GPX в `#sheet-gpx` |
| F-02 | Импорт GPX по URL через прокси-эндпоинт `/api/gpx/fetch` |
| F-03 | Кнопка «Найти публичные треки» в `#sheet-gpx` — поиск в bbox видимой области карты |
| F-04 | Прокси-эндпоинт `/api/gpx/osm/traces` для OSM Public GPS Traces |
| F-05 | Список найденных OSM-треков с метаданными (длина, точек, описание, автор) |
| F-06 | Импорт выбранного OSM-трека одним тапом |
| F-07 | Серверный LRU-кэш ответов внешних API (TTL 24 ч, in-memory) |
| F-08 | Источник трека (URL / OSM trace id + ссылка) виден в карточке трека |
| F-09 | Лимит размера загруженного по URL файла: 50 МБ (как ET-006) |
| F-10 | Внятные сообщения об ошибках (CORS-фейл, 404, лимит API, битый GPX) |
| F-11 | Импортированные треки попадают в общий список `window.gpxTracks` и неотличимы от локальных по поведению |
| # | Функция |
| ----- | ----------------------------------------------------------------------------- |
| F-01 | Pipeline сбора GPX-треков с ≥ 3 публичных источников |
| F-02 | Хранение треков в SQLite + Spatialite: геометрия + метаданные |
| F-03 | Дедупликация: один реальный трек = одна запись, даже если найден в N источниках |
| F-04 | Метаданные трека: источник, URL, тип активности, дата, длина, кол-во точек, автор (если публичен) |
| F-05 | API endpoint `GET /api/gps-tracks?bbox=…&activity=…&source=…` для отдачи треков клиенту |
| F-06 | Векторные тайлы публичных треков `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt` для эффективной отдачи на низких зумах |
| F-07 | Визуализация **отдельными линиями** (не heatmap) на карте |
| F-08 | Цветовая дифференциация: палитра по источнику (default) с возможностью переключения на палитру по типу активности |
| F-09 | UI-чекбокс «Публичные треки» в `#terrain-popup`: включить/выключить весь слой |
| F-10 | UI-фильтр по типу активности (enduro / moto / offroad / bicycle / hike / other), multi-select |
| F-11 | UI-фильтр по источнику, multi-select |
| F-12 | Конфиг-файл регионов: bbox + название + список активных источников |
| F-13 | MVP-датасет: ЦФО + Чувашия, ≥ 5000 треков |
| F-14 | Совместимость со сменой стиля карты (через `rebuildMapOverlays()` по аналогии с ET-006 REQ-F-13 и ET-007 REQ-F-06) |
| F-15 | Совместимость со спутниковой подложкой (ET-007): треки видны на спутнике с halo для контраста |
| F-16 | Клик по треку → popup с метаданными: имя/тип активности/источник/дата/длина и ссылка на оригинал |
| F-17 | Health-эндпоинт `/api/gps-tracks/health`: дата последнего сбора, кол-во треков по источникам, ошибки последнего прогона |
### Out of scope
- OAuth-интеграции (Strava, Komoot)
- Платный API Wikiloc
- Поиск треков глобально (без bbox)
- Сохранение треков в БД между сессиями
- Подписки на пользователей других платформ
- Загрузка собственных треков на публичные платформы
- **Real-time сбор**: только периодический офлайн (cron, 12 раза в неделю).
- **Wikiloc Premium / Komoot Premium / любые платные API**: используем
только бесплатные публичные endpoints и публичные HTML-страницы там,
где это разрешено ToS источника.
- **Strava Metro как источник линий**: это heatmap, не отдельные треки —
не соответствует бизнес-требованию «отдельные линии». Опционально в
будущем — как метрика популярности для валидации, не для MVP.
- **OAuth-интеграции** (вход пользователя в Strava/Komoot со своим
аккаунтом): отдельный work item.
- **Загрузка пользователем своих треков в общую базу**: отдельный work item.
- **Редактирование/обрезка треков на стороне сервера**.
- **Конвертация из KML/FIT/TCX**: pipeline принимает только GPX.
- **Snap-to-road** для треков (выравнивание под дороги OSM).
- **Учёт сложности (drag-level) внутри трека**: фильтр только по типу
активности; сложность — отдельная задача (требует анализа геометрии и
скорости).
## 4. Метрики успеха
## 4. Источники (с оценкой реализуемости в MVP)
| Метрика | Критерий |
|---------|----------|
| URL-импорт | Прямая ссылка на GPX до 50 МБ загружается за ≤ 5 сек на средней сети |
| OSM-поиск bbox | Запрос видимой области возвращает результат за ≤ 3 сек (с кэшем — мгновенно) |
| Точность | OSM-трек после импорта визуально совпадает с тем же треком из osm.org |
| Кэш | Повторный запрос той же области/URL в течение 24 ч — без обращения к внешнему API |
| UX | Все ошибки (CORS, 404, лимит, формат) — внятные toast-уведомления, не падение |
| Совместимость с ET-006 | Локальные и удалённые треки в одном списке, поведение идентично |
| Сохранение при смене стиля | Импортированные треки переживают переключение тёмной темы и слоёв рельефа |
Анализ каждого источника из business request с честной оценкой
доступности и юридических условий:
## 5. Риски
| # | Источник | Тип доступа | MVP | Комментарий |
| - | ------------------------- | ------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------- |
| 1 | OSM Public GPS Traces | Документированный API | **да** | `/api/0.6/trackpoints?bbox=…&page=…`. Лицензия ODbL, атрибуция OSM. Объём для ЦФО оценочно ≈ 50100K точек, тыс. треков. |
| 2 | EnduroRussia.ru | Web (HTML + GPX-ссылки) | **да** | По регионам, есть прямые GPX-ссылки. Лицензия и условия скрейпинга — фиксируются в ADR `06-adr/source-licensing.md` до начала разработки. |
| 3 | Тропинки.ру / ttrails.ru | Web (GPX/KML) | **да** | Эндуро-категория, GPX доступен без авторизации. Условия скрейпинга — то же ADR. |
| 4 | Offmaps.ru | Web | пилот | Требует ревью формата выдачи и лицензии. Подключаем в пилот-режим если ADR разрешает. |
| 5 | Nakarte.me | Public layers + JSON | пилот | Агрегатор: содержит ссылки на треки внешних источников. Может быть «бесплатным» путём к Wikiloc/Strava-treki косвенно. Требует ревью лицензии. |
| 6 | Wikiloc | API (премиум) | нет | Бесплатный публичный API не отдаёт GPX. Без премиума — невозможно. **Откладываем.** |
| 7 | Komoot | API (партнёрский) | нет | Публичный API ограничен, нет публичной выдачи GPX по bbox. **Откладываем.** |
| 8 | Strava Metro | API (исследовательский) | нет | Heatmap, не отдельные треки → не соответствует бизнес-требованию. **Out of scope.** |
| Риск | Вероятность | Влияние | Митигация |
|------|-------------|---------|-----------|
| OSM API rate limit (1 запрос / IP / сек) | Высокая | Среднее | Серверный кэш по bbox + дебаунс на клиенте |
| URL-прокси превращается в open redirect / SSRF | Средняя | Высокое | Whitelist схем (http/https), блок приватных IP, лимит размера, таймаут |
| Большие OSM-страницы (1000+ треков) → длинный список | Средняя | Низкое | Пагинация: показывать первые N, кнопка «ещё» |
| GPX по URL не существует / 404 | Высокая | Низкое | Toast с понятной ошибкой |
| Content-Type не `application/gpx+xml` | Высокая | Низкое | Проверять по содержимому (DOMParser), не по заголовкам |
| Чужой публичный трек содержит вредоносный XML / XXE | Низкая | Высокое | DOMParser в браузере (XXE отключён), на бэкенде — `defusedxml` |
| Внешний API внезапно недоступен | Средняя | Низкое | Graceful degradation: показать сообщение, не блокировать другие функции |
**MVP-минимум: 3 источника живут в продакшне** — обязательно OSM
(гарантированно доступен), плюс минимум 2 из (2)(5) по результатам
ADR-ревью лицензий.
## 6. Зависимости
### Юридический минимум
- **ET-006** — модель `window.gpxTracks`, рендеринг, sheet `#sheet-gpx`,
парсер `parseGpx()`. Без ET-006 эта задача не имеет смысла.
- **Backend (FastAPI)** — новые эндпоинты `/api/gpx/fetch`,
`/api/gpx/osm/traces`, добавление `httpx` (уже есть) и `defusedxml`
(новая зависимость, опционально — для server-side валидации).
- Внешние сервисы:
- `https://api.openstreetmap.org/api/0.6/trackpoints` — публичный API
OSM, ограничения: 1 req/sec/IP, 5000 точек/страница, до 5 страниц.
- Произвольные HTTPS-хосты (для URL-импорта) — без SLA, fail-soft.
Перед началом разработки каждого источника (2)(5) — **обязательный
ADR** `docs/work-items/ET-008/06-adr/<source>-licensing.md`:
1. Что говорит ToS источника о скрейпинге / массовой загрузке GPX.
2. Что говорит robots.txt.
3. На каких условиях разрешена публикация чужих треков
(имя/анонимизация/атрибуция).
4. Rate-limit, который мы будем соблюдать (default: 1 req / 5 sec, с
корректным `User-Agent: enduro-trails/<v> (+contact)`).
5. Список метаданных, которые **нельзя** сохранять/публиковать (личные
адреса, имена при отсутствии явного согласия).
Источник без явного зелёного света в ADR — **не включается** в pipeline.
## 5. Метрики успеха
| Метрика | Критерий MVP |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| Покрытие региона | ≥ 5000 уникальных треков для ЦФО + Чувашии после первого полного прогона pipeline |
| Источники в продакшне | ≥ 3 источника, отдающих данные в БД |
| Дедупликация | < 5% дублей (один реальный трек — одна запись). Метрика: руками отсэмплировать 100 треков, посчитать дубли. |
| Производительность отдачи bbox | `GET /api/gps-tracks?bbox=…` ≤ 300 мс p95 на z ≥ 10 (≤ 500 треков в видимой области) |
| Производительность отдачи тайлов | `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt` ≤ 200 мс p95 на z = 811 |
| Производительность отрисовки | При включённом слое pan/zoom без видимых фризов на десктопе и мобильных с 4 ГБ RAM |
| Расширяемость на регион | Добавить новый регион (bbox + название + список источников) — ≤ 30 строк YAML-конфига, без правки кода |
| Скорость UI-фильтров | Переключение фильтра по активности/источнику меняет видимую выборку за ≤ 200 мс (фильтрация на клиенте) |
| Сохранение слоя при `setStyle()` | Слой не теряется при переключении тёмной темы / спутника / hillshade — восстанавливается через `rebuildMapOverlays()` |
| Pipeline стабильность | Падение парсера одного источника не валит остальных; лог + алерт в `/api/gps-tracks/health` |
| Атрибуция | На карте видна атрибуция каждого активного источника; в popup трека — ссылка на оригинал |
## 6. Риски
| Риск | Вероятность | Влияние | Митигация |
| ----------------------------------------------------------------------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Источник меняет HTML → парсер ломается | Высокая | Среднее | Каждый источник в отдельном модуле, изолированная ошибка. Pipeline пишет статус по источнику в health-эндпоинт. Алерт при 2 неудачных прогонах подряд. |
| ToS источника запрещает скрейпинг | Средняя | Высокое | Обязательный ADR с фиксацией лицензии до подключения источника. Источник без явного разрешения — не включается. |
| Дубли треков из разных источников (один и тот же трек выкладывают на 2 платформах) | Высокая | Среднее | Spatial+temporal hash: bbox округлённый до 0.01° + длина ± 5% + дата ± 1 день → одна запись. Алгоритм в TRZ §6. |
| Перегрузка карты на низких зумах (10K+ треков в видимой области) | Высокая | Высокое | На клиенте: на z < 10 — отдача через MVT-тайлы с упрощением геометрии (как `simplify_coords` для `trails`). На z ≥ 10 — JSON с лимитом 500 треков. |
| Размер БД растёт неконтролируемо (миллионы треков при расширении на РФ) | Низкая | Среднее | Отдельная `gps_tracks.sqlite`. Ротация: треки старше N лет (по конфигу, default 5) удаляются. Метрика размера БД в health. |
| Скрейпер банится по IP | Средняя | Среднее | Rate-limit + backoff + `User-Agent` с контактом. Сбор по cron 12 раза в неделю, не чаще. Per-source конфигурируемый delay. |
| Персональные данные в треках (точки «дом», имена) | Низкая | Высокое | Не сохраняем waypoint без явного публичного флага. Не сохраняем `author` если ToS требует анонимизации. Список запрещённых полей — в `08-data-requirements.md`. |
| Лицензия источника обязывает менять/удалять данные по требованию автора | Средняя | Среднее | Сохраняем `external_id` и `external_url` — можем удалить точечно по запросу. Pipeline уважает «удалённое на источнике» → удалять и у нас. |
| Pipeline ест слишком много трафика mva154 | Средняя | Низкое | Per-source лимит на прогон (например, max 1000 новых треков за прогон). Метрики в health. |
| Отдача больших MVT тайлов медленная | Средняя | Среднее | Серверный кэш тайлов (LRU 1024 записи, как уже сделано для `trails`). Упрощение геометрии по зуму. |
## 7. Зависимости
### Backend
- Новый пакет `src/api/gps_tracks/` с подмодулями:
- `models.py` — Pydantic + SQL schema
- `sources/<source>.py` — модули per-source (OSM, EnduroRussia, ttrails, …)
- `dedup.py` — алгоритм дедупликации
- `db.py` — обвязка SQLite + Spatialite
- `endpoint.py` — FastAPI routes
- `mvt.py` — генерация MVT-тайлов
- Зависимости Python: `httpx` (есть), `lxml` или `defusedxml` (новая —
для безопасного парсинга XML на сервере), `shapely` (есть).
### Pipeline
- Скрипт `scripts/gps_collect.py` — точка входа.
- Конфиг `config/gps_sources.yaml` — список источников и параметры.
- Конфиг `config/gps_regions.yaml` — список регионов (bbox + список
активных источников per-region).
- Cron на mva154: `0 3 * * 1,4 /usr/local/bin/python
/opt/enduro-trails/scripts/gps_collect.py` (Mon + Thu, 03:00 UTC).
- Логи: `/var/log/enduro-trails/gps-collect.log`.
### Frontend
- Новый модуль `src/web/gps_tracks.js` — слой, фильтры, popup, по
аналогии с `gpx.js`.
- Расширение `index.html`:
- Чекбокс «Публичные треки» и кнопка «Фильтры» в `#terrain-popup`.
- Bottom sheet `#sheet-gps-filters` с фильтрами по активности и
источнику.
- Расширение `style.json` / `style-dark.json`: layer + halo-layer для
спутника (по аналогии с `trails-track-halo-satellite` из ET-007).
- Интеграция с `rebuildMapOverlays()` в `app.js`.
### Инфра
- Файловая: `data/gps_tracks.sqlite` на mva154, права чтения для FastAPI,
права записи только для pipeline. Бэкап в общий backup-стек проекта.
- Сетевая: исходящие HTTPS к источникам с mva154 (уже разрешено).
### Документация
- `06-adr/source-licensing.md` — лицензии всех источников.
- `06-adr/dedup-algorithm.md` — обоснование выбора алгоритма
дедупликации.
- `06-adr/storage-schema.md` — обоснование отдельной БД vs единой.
- `07-infra-requirements.md` — cron, бэкапы, ротация, мониторинг.
- `08-data-requirements.md` — схема БД, поля, ограничения, политика
персональных данных.
- `10-tech-risks.md` — расширенный риск-реестр (расширяет §6 BRD).
### Связи с другими work items
- **ET-006** — модель `window.gpxTracks` живёт параллельно. ET-008 не
трогает её, использует свою модель `window.gpsTracksLayer`.
- **ET-007** — спутниковая подложка. ET-008 добавляет halo-слой для
публичных треков в режиме «Спутник» по тому же паттерну.
- **PH-3 Smart Route** — публичные треки в будущем могут стать входом
для построения «реально-езженого» маршрута. Не в scope ET-008.
- **PH-9 PWA** — слой публичных треков должен корректно работать в
офлайне (через cached MVT). Учитывается в TRZ, но реализация офлайна
— задача PH-9.

File diff suppressed because it is too large Load Diff

View File

@@ -2,274 +2,442 @@
type: acceptance-criteria
work_item_id: ET-008
title: "AC: GPS-треки с публичных платформ на карте"
version: 1
version: 2
status: draft
created_at: 2026-06-01
updated_at: 2026-06-01
changelog:
- "v2 (2026-06-01): полная переработка под BRD/TRZ v2 — критерии серверной агрегации, дедупликации, MVT-тайлов, фильтров активности/источника, popup, halo-на-спутнике. Предыдущая v1 описывала URL-импорт + OSM live-поиск."
authors:
- "agent:analyst"
---
# Acceptance Criteria — ET-008: GPS-треки с публичных платформ на карте
## AC-01: Секция «Источники» в `#sheet-gpx`
## AC-01: Конфигурация источников и регионов
```gherkin
Feature: Переключатель источников треков
Feature: Расширяемая конфигурация
Scenario: Открытие GPX-панели
Given пользователь нажимает кнопку GPX в нижнем тулбаре
Then открывается панель #sheet-gpx
And в верхней части видна секция «Источники» с тремя кнопками: «Из файла», «По ссылке», «Найти рядом»
And по умолчанию активна кнопка «Из файла»
Scenario: Включение нового источника
Given config/gps_sources.yaml содержит источник с enabled=false
When оператор меняет на enabled=true и перезапускает pipeline
Then источник участвует в следующем прогоне
And в /api/gps-tracks/health он появляется в tracks_by_source
Scenario: Переключение на «По ссылке»
Given панель #sheet-gpx открыта
When пользователь нажимает кнопку «По ссылке»
Then кнопка «По ссылке» становится активной
And отображается поле ввода URL и кнопка «Загрузить»
And контент других вкладок скрыт
Scenario: Добавление нового региона
Given оператор добавляет в config/gps_regions.yaml новую запись с bbox
And запись не превышает 30 строк YAML
When оператор запускает pipeline без аргументов
Then новый регион обрабатывается всеми указанными в нём источниками
And никаких правок Python-кода не требуется
Scenario: Переключение на «Найти рядом»
Given панель #sheet-gpx открыта
When пользователь нажимает кнопку «Найти рядом»
Then отображается кнопка «Найти треки в этой области карты»
Scenario: Отключение источника
Given источник был enabled=true и собрал N треков
When оператор меняет на enabled=false
Then следующий прогон pipeline пропускает этот источник
And ранее собранные треки остаются в БД и отдаются API
And в фильтре по источнику соответствующий чекбокс не выбран по умолчанию
```
## AC-02: Импорт по URL — успешный сценарий
## AC-02: Pipeline сбора
```gherkin
Feature: Загрузка GPX по прямой ссылке
Feature: Pipeline gps_collect.py
Scenario: Валидная публичная ссылка
Given активна вкладка «По ссылке»
When пользователь вставляет https://example.com/test-track.gpx (валидный, 1 МБ)
And нажимает «Загрузить»
Then показывается индикатор загрузки
And через 5 сек трек появляется на карте
And карта выполняет fit bounds
And трек добавляется в список #gpx-list
And в карточке трека отображается «🔗 example.com»
Scenario: Полный прогон по умолчанию
Given config содержит регион ЦФО+Чувашия и 3 source enabled
When оператор запускает scripts/gps_collect.py
Then pipeline проходит по всем регионам и всем enabled-источникам
And для каждой пары (region, source) пишется запись в pipeline_runs
And exit code == 0 если хотя бы один трек собран по каждому источнику
Scenario: Загрузка по Enter
Given активна вкладка «По ссылке»
When пользователь вставляет URL и нажимает Enter
Then загрузка начинается без клика по кнопке
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: Импорт по URL — ошибки
## AC-03: Дедупликация
```gherkin
Feature: Обработка ошибок URL-импорта
Feature: Дедупликация треков
Scenario: Невалидный URL (схема)
Given активна вкладка «По ссылке»
When пользователь вставляет ftp://example.com/file.gpx
Then показывается toast «Невалидная ссылка»
And запрос на бэкенд не отправляется
Scenario: Один трек найден в двух источниках
Given OSM и EnduroRussia отдали один и тот же трек
(один автор выложил на обоих)
And bbox и длина совпадают в пределах допуска
And даты совпадают
When pipeline обрабатывает обе записи
Then в БД одна запись tracks
And sources_json содержит обоих
And external_urls_json содержит обе ссылки
Scenario: Приватный IP
Given пользователь вставляет http://192.168.1.1/file.gpx
Then бэкенд возвращает 400
And показывается toast «Эта ссылка недоступна»
Scenario: Похожие треки разных дат — НЕ дубли
Given два трека с одинаковым bbox и длиной
And даты отличаются на > 1 день
Then записи разные, дедуп НЕ срабатывает
Scenario: Несуществующий файл
Given URL ведёт на 404
Then показывается toast «Файл не найден по этой ссылке»
Scenario: Треки без даты от разных источников
Given оба трека без created_at
And bbox и длина совпадают
Then дедуп срабатывает (по умолчанию консервативный merge)
And это поведение задокументировано в ADR-002
Scenario: Файл больше 50 МБ
Given URL ведёт на GPX > 50 МБ
Then показывается toast «Файл слишком большой (макс. 50 МБ)»
Scenario: Не GPX (HTML по ссылке)
Given URL отдаёт HTML-страницу
Then показывается toast «По этой ссылке не GPX-файл»
Scenario: Внешний сервер не отвечает
Given внешний сервер таймаутит
Then показывается toast «Сервер не отвечает, попробуйте позже»
Scenario: Метрика < 5% дубликатов
Given в БД собрано 5000 треков
When QA-инженер выбирает 100 случайных треков и руками проверяет дубли
Then не более 5 треков (5%) являются дублями
```
## AC-04: Поиск OSM-треков
## AC-04: Endpoint /api/gps-tracks (GeoJSON)
```gherkin
Feature: Поиск публичных треков OSM в видимой области
Feature: GeoJSON endpoint
Scenario: Успешный поиск с результатами
Given активна вкладка «Найти рядом»
And карта показывает область с публичными треками
When пользователь нажимает «Найти треки в этой области карты»
Then показывается индикатор загрузки
And через 3 сек появляется список найденных треков
And каждая карточка содержит: иконку OSM, описание (page N), длину в км, кнопку «Показать», ссылку «»
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: Пустая область
Given карта показывает область без публичных треков
When пользователь нажимает «Найти треки»
Then отображается inline-сообщение «В этой области нет публичных GPS-треков»
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 > 0.25 deg²
When пользователь нажимает «Найти треки»
Then показывается toast «Слишком большая область, увеличьте zoom»
And запрос на бэкенд не отправляется (или возвращается 400)
Scenario: Фильтр по активности
Given в bbox 100 треков, 20 enduro, 30 moto, 50 hike
When клиент шлёт ?activity=enduro,moto
Then returned=50
Scenario: Пагинация
Given поиск вернул has_more=true
Then в конце списка отображается кнопка «Показать ещё»
When пользователь нажимает «Показать ещё»
Then дозагружаются результаты следующей страницы
And они добавляются в конец списка
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: Импорт OSM-трека
## AC-05: Endpoint /api/gps-tracks/tiles MVT
```gherkin
Feature: Импорт выбранного OSM-трека на карту
Feature: MVT tiles
Scenario: Импорт по кнопке «Показать»
Given найдено 3 OSM-трека в списке
When пользователь нажимает «Показать» у первого трека
Then показывается индикатор загрузки
And через 5 сек трек появляется на карте
And карта выполняет fit bounds
And трек добавляется в #gpx-list
And в карточке трека отображается «🌍 OSM #...» (кликабельная ссылка)
And карточка в #gpx-nearby-results получает индикатор «✓ Загружен»
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 OSM-трек уже импортирован
When пользователь нажимает «Показать» у этой же карточки в найденных
Then показывается toast «Уже загружен»
And новый трек НЕ добавляется
Scenario: Тайл из кэша
Given тайл уже запрашивали
When повторный запрос того же z/x/y
Then header X-Cache: HIT
And время 20 мс p95
Scenario: Внешняя ссылка на osm.org
Given в карточке найденного трека есть кнопка «»
When пользователь нажимает «»
Then открывается новая вкладка с страницей трека на openstreetmap.org
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: Отображение источника в карточке трека
## AC-06: Endpoint health
```gherkin
Feature: Источник трека виден пользователю
Feature: Health endpoint
Scenario: Локальный файл (ET-006 совместимость)
Given загружен GPX из локального файла
Then в карточке трека под именем файла отображается «📁 локальный файл»
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: Загружен по URL
Given загружен GPX по ссылке https://github.com/user/repo/track.gpx
Then в карточке трека отображается «🔗 github.com»
Scenario: Загружен из OSM
Given загружен OSM-трек page 0
Then в карточке трека отображается ссылка «🌍 OSM #..» которая ведёт на osm.org
Scenario: Health без БД
Given БД отсутствует на диске
When клиент шлёт GET /api/gps-tracks/health
Then ответ содержит tracks_total=0 и предупреждение о БД (или 503)
```
## AC-07: Кэширование на бэкенде
## AC-07: Чекбокс «Публичные треки» в попапе
```gherkin
Feature: Серверный кэш ответов внешних API
Feature: Включение слоя из попапа
Scenario: Повторный запрос URL из кэша
Given URL запрашивался менее 24 часов назад
When клиент делает повторный GET /api/gpx/fetch?url=...
Then ответ возвращается с заголовком X-Cache: HIT
And время ответа 50 мс
And внешний запрос НЕ выполняется
Scenario: Чекбокс присутствует
Given пользователь нажимает #terrain-toggle
Then в попапе #terrain-popup видна строка «Публичные треки» с чекбоксом
Scenario: Cache miss
Given URL запрашивается впервые
Then выполняется внешний запрос
And ответ возвращается с X-Cache: MISS
And следующий запрос того же URL HIT
Scenario: Включение слоя
When пользователь ставит галку «Публичные треки»
Then на карте появляются линии треков
And localStorage['gps-tracks-enabled'] = 'true'
And рядом с чекбоксом появляется ссылка «Фильтры»
Scenario: Повторный bbox-поиск из кэша
Given bbox запрашивался менее 24 часов назад
When клиент делает повторный GET /api/gpx/osm/traces?bbox=...
Then ответ из кэша
And внешний запрос к OSM API НЕ выполняется
Scenario: Выключение слоя
When пользователь снимает галку
Then линии исчезают с карты
And localStorage = 'false'
And ссылка «Фильтры» скрывается
Scenario: Подсказка о минимальном zoom
Given текущий zoom < 8
And чекбокс включён
Then рядом с чекбоксом видна подсказка «Зум 8+»
And линии на карте не видны (без ошибок)
```
## AC-08: Безопасность
## AC-08: Фильтры по активности и источнику
```gherkin
Feature: SSRF protection
Feature: Sheet фильтров
Scenario: Прямой запрос к loopback
When клиент шлёт GET /api/gpx/fetch?url=http://127.0.0.1/data
Then бэкенд возвращает 400
And никакого запроса к 127.0.0.1 не делается
Scenario: Открытие sheet
Given слой включён
When пользователь нажимает «Фильтры»
Then открывается #sheet-gps-filters
And видны секции «Тип активности», «Источник», «Цвет линий»
And по умолчанию выбраны все активности и все источники
Scenario: Запрос к приватной подсети
When клиент шлёт URL ведущий на 10.0.0.1, 192.168.x.x, 172.16.x.x
Then бэкенд возвращает 400
Scenario: Фильтрация по активности
Given в видимой области карты 743 трека, 200 enduro, 50 moto,
When пользователь снимает все галки кроме «Эндуро» и «Мото»
Then на карте отображаются только enduro и moto треки
And gps-stat-shown отражает новое число
And фильтрация мгновенная ( 200 мс), без сетевого запроса
Scenario: Редирект на приватный IP
Given внешний URL отдаёт 302 redirect на http://127.0.0.1/...
When клиент шлёт GET /api/gpx/fetch?url=<external>
Then редирект проверяется повторно и блокируется
And бэкенд возвращает 400
Scenario: Фильтрация по источнику
Given включено 3 источника
When пользователь снимает «OSM»
Then OSM-треки скрываются на карте
Scenario: Запрещённая схема
When клиент шлёт URL с file:// или gopher://
Then бэкенд возвращает 400
Scenario: Переключение режима цвета
Given color-mode = 'source'
When пользователь выбирает «По активности»
Then цвета линий перерисовываются по палитре активности
And localStorage сохраняет 'gps-tracks-color-mode' = 'activity'
Scenario: Размер ответа превышает лимит
Given внешний сервер начинает стримить файл > 50 МБ
Then бэкенд прерывает соединение
And возвращает 413
Scenario: Сохранение фильтров между сессиями
Given пользователь настроил фильтры (только enduro, только OSM)
When пользователь перезагружает страницу
Then sheet-фильтров восстанавливает те же чекбоксы
And слой отображает только enduro+OSM треки
```
## AC-09: Совместимость с ET-006
## AC-09: Popup при клике на трек
```gherkin
Feature: Локальные и удалённые треки в одной модели
Feature: Popup трека
Scenario: Смешанный список
Given загружен 1 локальный файл, 1 по URL, 1 из OSM
Then в #gpx-list отображаются 3 карточки
And каждая имеет уникальный цвет из палитры
And каждая имеет свой индикатор источника
And любую можно активировать, удалить, увидеть профиль высот
Scenario: Клик по линии трека
Given на карте отображается слой публичных треков
When пользователь кликает на линию трека
Then открывается popup с полями: name, activity (иконка+текст),
length_km, points_count, created_at, user, sources (со ссылками)
Scenario: Сохранение при смене темы
Given на карте 3 трека разных источников
When пользователь переключает тёмную тему
Then все 3 трека остаются на карте
And источники в карточках сохраняются
And статистика и профиль активного трека сохраняются
Scenario: Трек из двух источников
Given трек имеет sources=['osm', 'enduro_russia']
Then popup показывает обе ссылки
Scenario: Сохранение при переключении слоёв рельефа
Given на карте 3 трека разных источников
Scenario: Трек без user/name
Then popup показывает «Без названия» и не показывает строку «Автор»
Scenario: Клик по фону карты
Given открыт popup
When пользователь кликает на пустое место карты
Then popup закрывается
```
## AC-10: Z-order и совместимость с другими слоями
```gherkin
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)
```gherkin
Feature: Halo на спутнике
Scenario: Включение спутника
Given слой публичных треков включён
When пользователь переключает подложку на «Спутник»
Then линии треков видны на спутнике
And появляется белая обводка (halo) для контраста
Scenario: Возврат на схему
When пользователь возвращается на «Схема»
Then halo скрывается
And линии отображаются обычными цветами
Scenario: Halo учитывает чекбокс
Given спутник активен
When пользователь выключает чекбокс «Публичные треки»
Then и линии, и halo скрываются
```
## AC-12: Сохранение при смене стиля карты
```gherkin
Feature: Переживание setStyle()
Scenario: Переключение тёмной темы
Given слой включён, фильтры настроены
When пользователь переключает тёмную тему (вызывает map.setStyle())
Then слой публичных треков восстанавливается
And линии видны с теми же цветами по тому же color-mode
And фильтры активности/источника сохранены
Scenario: Переключение спутник→схема
Given слой включён, активен спутник
When пользователь переключается на схему
Then слой остаётся видим, halo выключается
Scenario: Включение hillshade
Given слой включён
When пользователь включает hillshade
Then все 3 трека видны поверх hillshade
Then публичные треки остаются видны (поверх hillshade)
```
## AC-10: Метрики кэша в `/api/health`
## AC-13: Производительность
```gherkin
Feature: Наблюдаемость кэшей
Feature: SLA отклика
Scenario: Размер кэшей в health-эндпоинте
When клиент шлёт GET /api/health
Then ответ содержит поля gpx_fetch_cache_size и gpx_osm_cache_size
And значения целые числа 0
Scenario: GeoJSON p95
When 100 запросов GET /api/gps-tracks?bbox= с 500 треков в bbox
Then p95 300 мс
Scenario: MVT cold
When запрос MVT-тайла без кэша
Then p95 200 мс
Scenario: MVT hot
When повторный запрос того же тайла
Then 20 мс, X-Cache: HIT
Scenario: Pan/zoom без фризов
Given слой включён с 500 треками в видимой области
When пользователь делает 10 быстрых pan-операций
Then нет видимых фризов (FPS 30 на десктопе)
```
## AC-11: Производительность
## AC-14: Защита от шторма запросов
```gherkin
Feature: Лимиты времени отклика
Feature: Debounce и AbortController
Scenario: OSM bbox запрос с кэш-хитом
Given bbox в кэше
Then GET /api/gpx/osm/traces возвращается за 50 мс (p95)
Scenario: Быстрый pan не плодит запросов
Given слой включён на z 12
When пользователь делает 5 быстрых pan-операций за 1 секунду
Then выполняется не более 2 запросов /api/gps-tracks (debounce 500ms)
And предыдущие запросы отменены AbortController
Scenario: URL-импорт малого файла (1 МБ)
Then GET /api/gpx/fetch для 1 МБ файла завершается за 2 сек
Scenario: OSM bbox запрос без кэша
Then GET /api/gpx/osm/traces без кэша возвращается за 3 сек (p95)
Scenario: На z < 8 запросов нет
Given пользователь на z=5
When пользователь панит карту
Then запросов /api/gps-tracks?bbox= не выполняется
```
## AC-15: Атрибуция
```gherkin
Feature: Атрибуция источников
Scenario: На карте видна атрибуция
Given слой включён, включены OSM и EnduroRussia
Then в правом нижнем углу карты отображается строка
«© OpenStreetMap contributors (ODbL) | EnduroRussia.ru»
Scenario: Popup содержит ссылку на оригинал
Given пользователь открыл popup трека
Then в нём видна ссылка «» на источник (или несколько)
When пользователь кликает на ссылку
Then открывается новая вкладка с оригиналом
```
## AC-16: Безопасность и юридические гарантии
```gherkin
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: Расширяемость на новые регионы
```gherkin
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
```

View File

@@ -2,423 +2,564 @@
type: test-plan
work_item_id: ET-008
title: "Test Plan: GPS-треки с публичных платформ на карте"
version: 1
version: 2
status: draft
created_at: 2026-06-01
updated_at: 2026-06-01
changelog:
- "v2 (2026-06-01): полная переработка под BRD/TRZ/AC v2 — серверная агрегация, дедупликация, MVT, фильтры активности/источника. Предыдущая v1 описывала URL-импорт + OSM live-поиск."
authors:
- "agent:analyst"
test_suites:
- name: unit-gpx-proxy-validation
- name: unit-config-loader
type: unit
description: "SSRF-валидация URL в gpx_proxy.is_safe_url()"
description: "Загрузка и валидация YAML-конфигов sources/regions"
cases:
- id: U-01
name: "Принимает валидный публичный HTTPS URL"
input: "https://example.com/track.gpx (резолвится в публичный IP)"
expected: "is_safe_url() возвращает True"
name: "Валидный gps_sources.yaml парсится"
input: "Корректный YAML с 3 источниками"
expected: "Возвращает список объектов Source с обязательными полями"
- id: U-02
name: "Отклоняет схему ftp://"
input: "ftp://example.com/track.gpx"
expected: "is_safe_url() возвращает False"
name: "Источник без license_adr — ошибка"
input: "YAML с enabled=true, но без license_adr"
expected: "ConfigError: 'enabled source requires license_adr'"
- id: U-03
name: "Отклоняет схему file://"
input: "file:///etc/passwd"
expected: "is_safe_url() возвращает False"
name: "Регион с unknown source — ошибка"
input: "regions.sources содержит ID, которого нет в sources.yaml"
expected: "ConfigError: 'unknown source id'"
- id: U-04
name: "Отклоняет loopback IP"
input: "http://127.0.0.1/x.gpx"
expected: "is_safe_url() возвращает False"
name: "Bbox региона валидируется"
input: "bbox=[200, 100, 250, 150]"
expected: "ConfigError: 'bbox out of valid range'"
- id: U-05
name: "Отклоняет приватный IP (10.0.0.0/8)"
input: "http://10.1.2.3/x.gpx"
expected: "is_safe_url() возвращает False"
- id: U-06
name: "Отклоняет приватный IP (192.168.0.0/16)"
input: "http://192.168.1.1/x.gpx"
expected: "is_safe_url() возвращает False"
- id: U-07
name: "Отклоняет приватный IP (172.16.0.0/12)"
input: "http://172.16.0.1/x.gpx"
expected: "is_safe_url() возвращает False"
- id: U-08
name: "Отклоняет link-local IP (169.254.x.x)"
input: "http://169.254.169.254/metadata"
expected: "is_safe_url() возвращает False"
- id: U-09
name: "Отклоняет невалидный URL"
input: "not a url"
expected: "is_safe_url() возвращает False (без exception)"
name: "Disabled source игнорируется в pipeline"
input: "Регион ссылается на disabled source"
expected: "Pipeline пропускает этот source, warning в логе"
- name: unit-dedup
type: unit
description: "compute_dedup_key и merge-логика"
cases:
- id: U-10
name: "Отклоняет хост, который не резолвится"
input: "http://nonexistent-host-xyz-12345.invalid/x.gpx"
expected: "is_safe_url() возвращает False"
name: "Два трека с одинаковым bbox+length+date → один ключ"
input: "geom1, geom2 с близкими bounds, length_m differ < 5%, dates same day"
expected: "compute_dedup_key(g1) == compute_dedup_key(g2)"
- id: U-11
name: "Разные даты → разные ключи"
input: "Те же bbox+length, daty отличаются на 2 дня"
expected: "compute_dedup_key различаются"
- id: U-12
name: "Bbox-округление до 0.01°"
input: "geom1.bounds=(37.6173, 55.7558, …), geom2.bounds=(37.6171, 55.7559, …)"
expected: "Один ключ (округление до 2 знаков)"
- id: U-13
name: "Merge: union sources"
input: "track в БД с sources=['osm'], новый с source='enduro_russia', тот же dedup_key"
expected: "Запись в БД обновлена: sources=['osm','enduro_russia']"
- id: U-14
name: "Merge: union external_urls"
input: "track в БД с external_urls=[...A], новый с [...B], тот же dedup_key"
expected: "В БД external_urls=[...A,...B] без дубликатов"
- id: U-15
name: "Merge: приоритет metadata по порядку sources.yaml"
input: "OSM (priority 1) собрал name='X', EnduroRussia (priority 2) собрал name='Y' с тем же dedup_key"
expected: "В БД name='X' (приоритет первого source)"
- name: unit-activity-mapping
type: unit
description: "Маппинг категорий источников в ACTIVITY_TYPES"
cases:
- id: U-20
name: "OSM tag 'enduro' → 'enduro'"
input: "['enduro', 'motorcycle']"
expected: "'enduro'"
- id: U-21
name: "OSM tag 'mtb' → 'bicycle'"
input: "['mtb']"
expected: "'bicycle'"
- id: U-22
name: "Unknown tag → 'other'"
input: "['xyz']"
expected: "'other'"
- id: U-23
name: "Пустой список тэгов → 'other'"
input: "[]"
expected: "'other'"
- name: unit-bbox-validation
type: unit
description: "Валидация bbox в osm_traces"
cases:
- id: U-20
name: "Принимает малый bbox"
input: "bbox=[37.6, 55.7, 37.7, 55.8] (0.01 deg²)"
expected: "validate_bbox() возвращает True"
- id: U-21
name: "Отклоняет bbox > 0.25 deg²"
input: "bbox=[37.0, 55.0, 38.0, 56.0] (1.0 deg²)"
expected: "validate_bbox() возвращает False"
- id: U-22
name: "Отклоняет невалидные координаты"
input: "bbox=[200, 100, 250, 150]"
expected: "validate_bbox() возвращает False"
- id: U-23
name: "Отклоняет перевёрнутый bbox (west > east)"
input: "bbox=[38.0, 55.0, 37.0, 56.0]"
expected: "validate_bbox() возвращает False"
- name: unit-cache
type: unit
description: "LRU кэш с TTL"
description: "Валидация bbox в /api/gps-tracks"
cases:
- id: U-30
name: "TTL истёк → cache miss"
input: "Положить запись с TTL 1 сек, ждать 2 сек, запросить"
expected: "Возвращает None (или вызывает loader)"
name: "Валидный bbox"
input: "bbox=37.0,55.0,38.0,56.0"
expected: "validate_bbox() = True"
- id: U-31
name: "LRU вытеснение при переполнении"
input: "Заполнить кэш max=4 записями, добавить 5-ю"
expected: "Первая (LRU) запись вытеснена"
name: "bbox out-of-range"
input: "bbox=200,100,250,150"
expected: "validate_bbox() = False"
- id: U-32
name: "Округление bbox-ключа до 4 знаков"
input: "bbox=[37.6172999, 55.7558001, ...] и bbox=[37.6173, 55.7558, ...]"
expected: "Один и тот же кэш-ключ → cache hit"
name: "Перевёрнутый bbox"
input: "bbox=38,55,37,56 (west > east)"
expected: "validate_bbox() = False"
- id: U-33
name: "URL > 5 МБ не кэшируется"
input: "Положить запись размером 6 МБ"
expected: "Запись не попадает в кэш (cache.get → None)"
name: "Невалидный формат"
input: "bbox=foo"
expected: "validate_bbox() = False"
- name: unit-osm-parser
type: unit
description: "Парсинг OSM trackpoints GPX → JSON"
description: "Парсер OSM trackpoints"
cases:
- id: U-40
name: "Извлечение точек из GPX 1.0"
input: "GPX с 1 <trk>, 1 <trkseg>, 50 <trkpt>"
expected: "JSON: {tracks: [{points_count: 50, distance_km: ~X}]}"
name: "Группировка trkpt по gpx_id"
input: "GPX 1.0 с trkpt разных gpx_id"
expected: "Возвращает по треку на каждый gpx_id"
- id: U-41
name: "Расчёт длины через Haversine"
input: "GPX с 3 точками: [37.6,55.7], [37.7,55.8], [37.8,55.9]"
expected: "distance_km ≈ 28.3 (±0.5)"
name: "Анонимные точки (без gpx_id) — пропуск"
input: "GPX с точками без gpx_id"
expected: "Эти точки не попадают в результат"
- id: U-42
name: "Пустой GPX (нет trkpt)"
input: "GPX без точек"
expected: "JSON: {tracks: [], total_points: 0}"
name: "Bbox-разбиение региона"
input: "region.bbox=(37, 55, 39, 57), cell_size=0.25"
expected: "len(cells) = 8 * 8 = 64"
- id: U-43
name: "Защита от XXE (defusedxml)"
input: "GPX с DOCTYPE и внешней entity"
expected: "Парсер не выполняет загрузку внешней entity (или бросает ошибку)"
name: "Расчёт length_m через Haversine"
input: "trkpt: [37.6,55.7], [37.7,55.8], [37.8,55.9]"
expected: "length_m ≈ 28300 (±500)"
- name: unit-web-gpx-source
- id: U-44
name: "Защита от XXE"
input: "GPX с DOCTYPE и внешней entity"
expected: "defusedxml блокирует, парсер не выполняет загрузку"
- id: U-45
name: "Тэги из GPX → activity_type"
input: "<tag>enduro</tag><tag>motorcycle</tag>"
expected: "activity_type='enduro'"
- name: unit-mvt-generation
type: unit
description: "Расширение модели window.gpxTracks полем source"
description: "Генерация MVT-тайлов для gps_tracks"
cases:
- id: U-50
name: "Импорт по URL: source.kind='url'"
input: "importGpxFromUrl('https://github.com/x/y.gpx', mockedFetch)"
expected: "Трек добавлен с source={kind:'url', url:'https://github.com/x/y.gpx'}"
name: "Тайл z=10 с 50 треками"
input: "tile_to_bbox(10, x, y), 50 треков в bbox"
expected: "Валидный MVT с layer gps_tracks, 50 features"
- id: U-51
name: "Импорт OSM: source.kind='osm'"
input: "importOsmTrace({osm_page:0, osm_bbox:[...], gpx_url:'...'}, mockedFetch)"
expected: "Трек добавлен с source={kind:'osm', osm_page:0, osm_bbox:[...], url:'...'}"
name: "Упрощение геометрии на z=7"
input: "Трек 1000 точек, z=7"
expected: "После simplify_coords ≤ 100 точек"
- id: U-52
name: "Обратная совместимость: трек без source читается как 'file'"
input: "window.gpxTracks[0] без поля source"
expected: "renderSourceRow() возвращает '📁 локальный файл'"
name: "Min-length фильтр на z ≤ 7"
input: "Треки с length_m=500 и 5000 на z=7"
expected: "Только трек ≥ 2000м попадает в тайл (min_length для z≤7)"
- id: U-53
name: "Hostname extraction для URL-источника"
input: "source.url='https://raw.githubusercontent.com/user/repo/main/track.gpx'"
expected: "renderSourceRow() возвращает '🔗 raw.githubusercontent.com'"
name: "Properties в feature"
input: "Track в БД"
expected: "feature.properties содержит id, activity, source, sources,
length_km, name, ext_url"
- name: integration-gpx-fetch
- id: U-54
name: "Пустой тайл"
input: "Bbox без треков"
expected: "build_mvt() возвращает b'' (или валидный пустой MVT)"
- name: unit-color-palette
type: unit
description: "Цветовая палитра по источнику и активности"
cases:
- id: U-60
name: "Color by source: OSM = #3cb44b"
input: "feature.source='osm'"
expected: "Match-expression возвращает '#3cb44b'"
- id: U-61
name: "Color by activity: enduro = #e6194b"
input: "feature.activity='enduro'"
expected: "'#e6194b'"
- id: U-62
name: "Unknown source → fallback"
input: "feature.source='unknown'"
expected: "'#808080' (или fallback из палитры)"
- name: integration-pipeline
type: integration
description: "GET /api/gpx/fetch — прокси с реальным HTTP"
description: "Pipeline gps_collect.py end-to-end с mock-источниками"
cases:
- id: I-01
name: "Успешная загрузка GPX по URL (mock-сервер)"
input: "GET /api/gpx/fetch?url=http://test-server/track.gpx"
expected: "200, Content-Type: application/gpx+xml, тело = GPX, X-Cache: MISS"
name: "Полный прогон с 1 mock-источником"
input: "Mock OSM API → 100 треков; пустая БД"
expected: "После прогона в БД 100 tracks, pipeline_runs.status='ok',
tracks_new=100, tracks_updated=0"
- id: I-02
name: "Повторный запрос — cache hit"
input: "GET тот же URL"
expected: "200, X-Cache: HIT, время ≤ 50 мс"
name: "Повторный прогон того же источника — все треки updated"
input: "Тот же mock + та же БД с предыдущей записью"
expected: "tracks_new=0, tracks_updated=100"
- id: I-03
name: "Отклонение приватного IP"
input: "GET /api/gpx/fetch?url=http://127.0.0.1/x.gpx"
expected: "400, JSON {error: ...}"
name: "Прогон двух источников с пересечением"
input: "OSM mock = 100 треков, EnduroRussia mock = 50, из них 20 — те же по dedup_key"
expected: "В БД 130 уникальных записей (100 + 50 - 20). 20 пересекающихся имеют sources=['osm','enduro_russia']"
- id: I-04
name: "Отклонение редиректа на приватный IP"
input: "Внешний URL → 302 на http://127.0.0.1/x.gpx"
expected: "400, JSON {error: ...}"
name: "Падение одного источника"
input: "OSM mock OK, EnduroRussia mock возвращает 503"
expected: "OSM треки в БД, EnduroRussia status='error' в pipeline_runs,
но pipeline exit=0 (не strict-mode)"
- id: I-05
name: "Внешний 404"
input: "URL ведёт на несуществующий путь"
expected: "404, JSON {error: ...}"
name: "Dry-run"
input: "Любой источник + флаг --dry-run"
expected: "БД не меняется, pipeline_runs не пишется,
stdout содержит план"
- id: I-06
name: "Лимит размера 50 МБ"
input: "Mock-сервер стримит 60 МБ"
expected: "413, соединение прервано до конца"
name: "Rate-limit соблюдается"
input: "Mock source с rate_limit_sec=2, 5 запросов"
expected: "Суммарное время ≥ 8 сек (4 интервала × 2 сек)"
- id: I-07
name: "Таймаут"
input: "Mock-сервер ничего не отвечает"
expected: "504 после 15 сек"
name: "Backoff на 429"
input: "Mock source первый раз 429, второй раз 200"
expected: "Pipeline делает retry после exponential backoff,
трек собран"
- id: I-08
name: "URL > 5 МБ не попадает в кэш"
input: "Запросить URL с ответом 6 МБ дважды"
expected: "Второй запрос: X-Cache: MISS, внешний запрос повторно выполнен"
- name: integration-osm-traces
- name: integration-endpoint-geojson
type: integration
description: "GET /api/gpx/osm/traces — OSM API клиент"
description: "/api/gps-tracks GeoJSON"
cases:
- id: I-20
name: "Bbox-запрос с результатами"
input: "GET /api/gpx/osm/traces?bbox=37.6,55.7,37.65,55.75 (mock OSM API)"
expected: "200, JSON с tracks[], каждый имеет points_count, distance_km, gpx_url"
name: "Малый bbox с фильтрами"
input: "GET /api/gps-tracks?bbox=...&activity=enduro&source=osm"
expected: "200, FeatureCollection только enduro+OSM треков"
- id: I-21
name: "Bbox > 0.25 deg² → 400"
input: "bbox=37,55,38,56"
expected: "400, error 'bbox too large'"
name: "Truncation"
input: "В bbox 1500 треков, limit=500"
expected: "returned=500, total_in_bbox=1500, truncated=true"
- id: I-22
name: "OSM API недоступен → 502"
input: "OSM mock возвращает 500"
expected: "502, JSON error"
name: "Невалидный bbox → 400"
input: "bbox=foo"
expected: "400, JSON error"
- id: I-23
name: "Cache hit на повторный bbox"
input: "Тот же bbox дважды"
expected: "Второй запрос: внешний запрос НЕ выполнен, ответ из кэша"
name: "Bbox в океане → пустой результат"
input: "bbox=0,0,1,1"
expected: "200, features=[], total=0"
- id: I-24
name: "Пустой bbox → пустой список"
input: "bbox в океане"
expected: "200, tracks=[], has_more=false"
name: "CORS headers"
input: "Origin: https://example.com"
expected: "Response содержит Access-Control-Allow-Origin: *"
- id: I-25
name: агинация"
input: "page=0 возвращает has_more=true, page=1 возвращает следующие"
expected: "Корректное смещение, оба запроса валидны"
name: роизводительность"
input: "100 запросов на bbox с 500 треков"
expected: "p95 ≤ 300 мс"
- name: integration-health-metrics
- name: integration-endpoint-mvt
type: integration
description: "Метрики кэшей в /api/health"
description: "/api/gps-tracks/tiles/{z}/{x}/{y}.mvt"
cases:
- id: I-30
name: "Health возвращает размеры кэшей"
input: "GET /api/health"
expected: "JSON содержит gpx_fetch_cache_size, gpx_osm_cache_size (числа ≥ 0)"
name: "Тайл MVT отдаётся"
input: "GET /api/gps-tracks/tiles/10/623/325.mvt"
expected: "200, Content-Type: application/x-protobuf,
X-Cache: MISS"
- id: I-31
name: "Счётчики растут после запросов"
input: "После N успешных fetch и M osm_traces запросов"
expected: "Размеры кэшей отражают добавленные записи"
name: "Cache hit"
input: "Повторный запрос того же тайла"
expected: "X-Cache: HIT, ≤ 20 мс"
- name: e2e-url-import
- id: I-32
name: "Невалидные z/x/y"
input: "z=25 / x вне диапазона"
expected: "400"
- id: I-33
name: "Очистка кэша"
input: "POST /api/gps-tracks/cache/clear, повторный запрос тайла"
expected: "X-Cache: MISS"
- name: integration-endpoint-health
type: integration
description: "/api/gps-tracks/health"
cases:
- id: I-40
name: "Полный отчёт"
input: "GET /api/gps-tracks/health"
expected: "200, JSON со всеми полями (см. REQ-F-12)"
- id: I-41
name: "БД отсутствует"
input: "Удалить data/gps_tracks.sqlite, GET /api/gps-tracks/health"
expected: "503 или 200 с tracks_total=0 и warning"
- id: I-42
name: "Счётчики корректны"
input: "БД с 100 OSM + 50 EnduroRussia"
expected: "tracks_by_source: {osm: 100, enduro_russia: 50}"
- name: integration-web-layer
type: integration
description: "Клиентский слой публичных треков"
cases:
- id: I-50
name: "Включение/выключение слоя"
input: "Симуляция click на #public-tracks-cb"
expected: "map.getSource('gps-tracks-tiles') существует,
layer 'gps-tracks-layer' visibility=visible"
- id: I-51
name: "Фильтр по активности через setFilter"
input: "filters.activities = ['enduro']"
expected: "map.getFilter('gps-tracks-layer') содержит ['in', ['get','activity'], ['literal',['enduro']]]"
- id: I-52
name: "Переключение color-mode"
input: "Переключить с source на activity"
expected: "Layer paint['line-color'] переустановлен на activity-палитру"
- id: I-53
name: "GeoJSON-загрузка при z ≥ 12"
input: "map.zoom=14, moveend"
expected: "Через 500мс debounce — fetch /api/gps-tracks?bbox=…"
- id: I-54
name: "AbortController при быстром pan"
input: "Два moveend подряд за 100мс"
expected: "Первый fetch отменён, выполняется только второй"
- id: I-55
name: "Halo на спутнике"
input: "applyBaseLayer('satellite'), public-tracks включен"
expected: "layer 'gps-tracks-halo-satellite' visibility=visible"
- id: I-56
name: "Halo выключен на схеме"
input: "applyBaseLayer('schematic')"
expected: "halo visibility=none"
- id: I-57
name: "Сохранение слоя при setStyle"
input: "Переключение тёмной темы (switchMapStyle)"
expected: "rebuildMapOverlays() → restorePublicTracksState() →
слой пересоздан, фильтры применены"
- name: e2e-pipeline
type: e2e
description: "Импорт GPX по ссылке — полный сценарий"
description: "Полный pipeline на тестовых mock-источниках"
cases:
- id: E-01
name: "URL-импорт валидного трека"
name: "Сбор → API → визуализация"
steps:
- "Открыть приложение"
- "Нажать кнопку GPX в нижнем тулбаре"
- ереключиться на вкладку «По ссылке»"
- "Вставить URL валидного GPX (тестовый mock)"
- "Нажать «Загрузить»"
- "Убедиться: индикатор показан, через ≤ 5 сек трек на карте"
- "Убедиться: в #gpx-list появилась карточка с источником «🔗 host»"
- "Кликнуть на трек → отображается статистика и профиль высот"
- "Очистить test-БД"
- "Запустить pipeline с mock OSM + mock EnduroRussia"
- роверить: tracks_total > 0 в /api/gps-tracks/health"
- "Открыть веб-интерфейс"
- "Включить чекбокс «Публичные треки»"
- "Убедиться: на карте видны линии треков"
- "Кликнуть по треку → popup с метаданными"
- id: E-02
name: "URL-импорт по Enter"
name: "Дедупликация — два прогона"
steps:
- "Активировать «По ссылке»"
- "Вставить URL, нажать Enter"
- "Убедиться: трек загружен (как при клике)"
- "Запустить pipeline (mock-источники отдают 100 треков)"
- "Запомнить tracks_total"
- "Запустить pipeline повторно (mock отдаёт те же 100)"
- "Убедиться: tracks_total не изменился"
- "Убедиться: pipeline_runs.tracks_updated=100"
- id: E-03
name: "Невалидный URL → toast"
steps:
- "Вставить ftp://x.com/y"
- "Нажать «Загрузить»"
- "Убедиться: toast «Невалидная ссылка»"
- "Убедиться: на карте ничего нового"
- id: E-04
name: "Приватный IP блокируется"
steps:
- "Вставить http://192.168.1.1/x.gpx"
- "Нажать «Загрузить»"
- "Убедиться: toast «Эта ссылка недоступна»"
- id: E-05
name: "Не GPX по ссылке"
steps:
- "Вставить URL HTML-страницы"
- "Нажать «Загрузить»"
- "Убедиться: toast «По этой ссылке не GPX-файл»"
- name: e2e-osm-search
- name: e2e-ui-filters
type: e2e
description: "Поиск и импорт OSM треков"
description: "UI-фильтры по активности и источнику"
cases:
- id: E-10
name: "Поиск треков в области и импорт"
name: "Открытие фильтров и переключение"
steps:
- "Открыть приложение, отзумиться к области Москвы (zoom 12)"
- "Открыть #sheet-gpx, активировать «Найти рядом»"
- "Нажать «Найти треки в этой области карты»"
- "Убедиться: индикатор, потом список карточек"
- "Нажать «Показать» у первой карточки"
- "Убедиться: трек появился на карте, fit bounds"
- "Убедиться: карточка в найденных получила «✓ Загружен»"
- "Убедиться: в #gpx-list появилась карточка с «🌍 OSM #...»"
- "Включить чекбокс «Публичные треки»"
- "Нажать «Фильтры…» → открывается #sheet-gps-filters"
- "Снять все галки активности кроме «Эндуро»"
- "Убедиться: на карте видны только enduro-треки"
- "Снять «OSM» в источниках"
- "Убедиться: OSM enduro-треки скрылись"
- id: E-11
name: "Слишком большая область"
name: "Переключение color-mode"
steps:
- "Отзумиться на всю Россию"
- "Активировать «Найти рядом»"
- "Нажать «Найти»"
- "Убедиться: toast «Слишком большая область, увеличьте zoom»"
- "Включить слой"
- "Открыть фильтры"
- "Выбрать «По активности»"
- "Убедиться: цвета линий перерисованы (например, enduro = красный)"
- "Перезагрузить страницу"
- "Убедиться: color-mode='activity' сохранён"
- id: E-12
name: "Пустая область"
name: "Persistence фильтров"
steps:
- "Перейти к области без треков (океан)"
- "Активировать «Найти рядом»"
- "Нажать «Найти»"
- "Убедиться: сообщение «В этой области нет публичных GPS-треков»"
- "Настроить фильтры (только moto, только EnduroRussia)"
- "Перезагрузить страницу"
- "Открыть фильтры"
- "Убедиться: чекбоксы соответствуют настройкам"
- id: E-13
name: "Пагинация"
steps:
- "Найти треки в области с большим количеством"
- "Убедиться: кнопка «Показать ещё» внизу"
- "Нажать «Показать ещё»"
- "Убедиться: список расширился"
- id: E-14
name: "Повторный импорт → toast"
steps:
- "Импортировать трек по «Показать»"
- "Нажать «Показать» у той же карточки ещё раз"
- "Убедиться: toast «Уже загружен»"
- id: E-15
name: "Внешняя ссылка на osm.org"
steps:
- "Найти треки, нажать «↗» у карточки"
- "Убедиться: новая вкладка открыта на openstreetmap.org"
- name: e2e-mixed-sources
- name: e2e-popup
type: e2e
description: "Совместимость трёх источников в одной сессии"
description: "Popup трека"
cases:
- id: E-20
name: "3 трека разных источников"
name: "Popup полный набор полей"
steps:
- "Загрузить 1 локальный файл"
- "Загрузить 1 по URL"
- "Загрузить 1 из OSM"
- "Убедиться: 3 карточки в #gpx-list, разные цвета, разные источники"
- "Удалить URL-трек"
- "Убедиться: 2 трека на карте, корректные источники"
- "Включить слой"
- "Кликнуть на трек на карте"
- "Убедиться: popup содержит name, activity-иконку, км, дату, user, sources"
- "Кликнуть по ссылке источника"
- "Убедиться: открыта новая вкладка"
- id: E-21
name: "Сохранение при смене темы"
name: "Popup для трека без user"
steps:
- "Загрузить 3 трека разных источников"
- "Переключить тёмную тему"
- "Убедиться: все 3 трека на карте"
- "Убедиться: источники в карточках сохранены"
- "Найти трек без user"
- "Кликнуть → popup без строки «Автор»"
- id: E-22
name: "Сохранение при включении hillshade"
steps:
- "Загрузить 3 трека"
- "Включить hillshade"
- "Убедиться: все 3 трека видны поверх hillshade"
- name: e2e-cache
- name: e2e-compat
type: e2e
description: "Поведение кэша через API"
description: "Совместимость с другими функциями"
cases:
- id: E-30
name: "Кэш URL-fetch снижает время"
name: "Слой + спутник + halo"
steps:
- "GET /api/gpx/fetch?url=<test-url> — измерить t1"
- "GET /api/gpx/fetch?url=<тот же url> — измерить t2"
- "Убедиться: t2 < 100 мс, заголовок X-Cache: HIT"
- "Включить «Публичные треки»"
- "Переключить подложку на «Спутник»"
- "Убедиться: треки видны на спутнике с белой обводкой"
- id: E-31
name: "Размеры кэша в health"
name: "Слой + тёмная тема"
steps:
- "Сделать N запросов /api/gpx/fetch"
- "GET /api/health"
- "Убедиться: gpx_fetch_cache_size == N (или min(N, лимит))"
- "Включить слой"
- "Переключить тёмную тему"
- "Убедиться: треки остаются на карте"
- "Убедиться: фильтры сохранены"
- id: E-32
name: "Слой + личный GPX (ET-006)"
steps:
- "Включить слой"
- "Загрузить личный GPX"
- "Убедиться: оба видны"
- "Убедиться: личный трек выше публичных по z-order"
- id: E-33
name: "Слой + маршрут OSRM"
steps:
- "Включить слой"
- "Построить маршрут OSRM"
- "Убедиться: маршрут OSRM визуально выше публичных треков"
- id: E-34
name: "Слой + hillshade"
steps:
- "Включить слой"
- "Включить hillshade"
- "Убедиться: оба видны"
- name: e2e-low-zoom-protection
type: e2e
description: "Защита от шторма запросов на low-zoom"
cases:
- id: E-40
name: "Слой скрыт на z<8"
steps:
- "Включить слой"
- "Отзумиться до z=5"
- "Убедиться: линии не отображаются"
- "Убедиться: появилась подсказка «Зум 8+» у чекбокса"
- id: E-41
name: "Pan на z 14 не штормит запросы"
steps:
- "Включить слой, z=14"
- "Быстро панить карту (5 раз за 1 сек)"
- "Проверить network log: не более 2 запросов /api/gps-tracks"
- name: load-pipeline
type: load
description: "Нагрузочные сценарии pipeline и API"
cases:
- id: L-01
name: "Полный прогон pipeline на ЦФО+Чувашию (mock)"
input: "Mock OSM с реальным объёмом ≈ 50K треков"
expected: "Прогон завершается за ≤ 6 часов (cron-окно)"
- id: L-02
name: "API под нагрузкой"
input: "10 параллельных клиентов делают по 100 запросов /api/gps-tracks"
expected: "p95 ≤ 500 мс, нет ошибок"
- id: L-03
name: "MVT-тайлы под нагрузкой"
input: "100 параллельных запросов разных тайлов"
expected: "p95 cold ≤ 300 мс, hit-rate кэша > 80% на повторах"
test_data:
- name: "test-track-public.gpx"
description: "Валидный GPX 1.1, 1 МБ, для URL-импорта (mock-сервер)"
- name: "test-track-large.gpx"
description: "GPX 60 МБ — для проверки лимита размера"
- name: "test-osm-trackpoints.gpx"
description: "Реальный ответ OSM trackpoints API (зафиксирован для mock)"
- name: "test-html-page.html"
description: "HTML вместо GPX — для проверки валидации формата"
- name: "test-xxe-payload.gpx"
description: "GPX с DOCTYPE и внешней entity — для проверки defusedxml"
- name: "bbox-moscow-small"
description: "[37.6, 55.7, 37.65, 55.75] — реальная область с публичными треками OSM"
- name: "bbox-too-large"
description: "[37.0, 55.0, 38.0, 56.0] — > 0.25 deg² для проверки 400"
fixtures_dir: "tests/fixtures/gps-tracks/"
fixtures:
- name: "osm-trackpoints-bbox-moscow.gpx"
description: "Реальный ответ OSM API на bbox центра Москвы"
- name: "osm-trackpoints-multipage.json"
description: "Серия ответов OSM с has_more=true на нескольких страницах"
- name: "enduro-russia-mock-listing.html"
description: "Главная страница региона на EnduroRussia (mock)"
- name: "enduro-russia-mock-track.gpx"
description: "GPX-файл, отдаваемый EnduroRussia mock"
- name: "ttrails-mock-track.gpx"
description: "GPX от ttrails mock"
- name: "xxe-payload.gpx"
description: "GPX с DOCTYPE и внешней entity (для проверки defusedxml)"
- name: "dedup-pair-osm-enduro.json"
description: "Пара треков (одна и та же поездка из двух источников) для проверки dedup"
- name: "gps_tracks_seed.sql"
description: "SQL-сид: 1000 синтетических треков для интеграционных тестов"
test_environment:
mock_servers:
- "Mock HTTP-сервер для /api/gpx/fetch тестов (отдаёт фиксированные ответы)"
- "Mock OSM API для /api/gpx/osm/traces тестов"
fixtures_dir: "tests/fixtures/gpx-public/"
- "Mock OSM API (отвечает на /api/0.6/trackpoints и /api/0.6/gpx/<id>)"
- "Mock EnduroRussia.ru (HTML-страницы + GPX-файлы)"
- "Mock ttrails.ru"
cron_simulation:
- "В тестах cron заменяется на pytest fixture, вызывающий run() напрямую"
db_isolation:
- "Каждый тест использует in-memory или временный sqlite-файл в pytest tmp_path"
network:
- "Все исходящие HTTP в unit/integration — через httpx_mock или respx (без реальной сети)"
notes:
- "OSM API в e2e тестах должен мокироваться, чтобы не зависеть от внешней доступности"
- "Для нагрузочных тестов кэша использовать pytest-benchmark"
- "L-01 (полный прогон pipeline) запускается отдельно, не в обычном CI"
- "E2E UI-тесты — Playwright; URL test-среды https://openclaw.mva154.duckdns.org/enduro/ (см. 04b-ui-test-cases.md)"
- "Для load-тестов использовать pytest-benchmark + locust"

View File

@@ -2,10 +2,12 @@
type: ui-test-cases
work_item_id: ET-008
title: "UI Test Cases: GPS-треки с публичных платформ"
version: 1
version: 2
status: draft
created_at: 2026-06-01
updated_at: 2026-06-01
changelog:
- "v2 (2026-06-01): полная переработка под BRD/TRZ v2 — чекбокс «Публичные треки» в попапе, sheet фильтров, halo на спутнике, popup трека. Предыдущая v1 описывала вкладки источников в #sheet-gpx (URL/OSM)."
authors:
- "agent:analyst"
---
@@ -14,299 +16,33 @@ authors:
Базовый URL: `https://openclaw.mva154.duckdns.org/enduro/`
Все тесты проверяют появление и поведение секции «Источники» в
`#sheet-gpx`, импорта по URL и поиска OSM-треков. Внешние сетевые
запросы в test-окружении мокаются (см. test-plan).
Все тесты проверяют появление и поведение нового слоя «Публичные
треки»: чекбокса в `#terrain-popup`, sheet фильтров, отрисовки линий,
popup и совместимости со спутниковой подложкой / тёмной темой.
Селекторы (новые, добавляются ET-008):
- `#source-seg` — segmented control «Источники»
- `#source-btn-file`, `#source-btn-url`, `#source-btn-nearby` — кнопки вкладок
- `#gpx-source-pane-file`, `#gpx-source-pane-url`, `#gpx-source-pane-nearby` — контент-блоки
- `#gpx-url-input` — поле ввода URL
- `#btn-gpx-fetch-url` — кнопка «Загрузить» URL
- `#btn-gpx-find-nearby` — кнопка «Найти треки в этой области»
- `#gpx-nearby-results` — контейнер списка найденных
- `.gpx-nearby-card` — карточка найденного OSM-трека
- `.gnc-import` — кнопка «Показать»
- `.gnc-external` — ссылка «↗»
- `.gpx-source-row` — индикатор источника в карточке трека
Существующие селекторы (ET-006): `#tb-gpx`, `#sheet-gpx`, `#gpx-list`.
- `#public-tracks-cb` — чекбокс «Публичные треки» в `#terrain-popup`
- `#public-tracks-zoom-hint` — подсказка «Зум 8+»
- `#public-tracks-filters-btn` — ссылка «Фильтры…»
- `#sheet-gps-filters` — bottom sheet фильтров
- `#gps-activity-grid` — секция чекбоксов активности
- `#gps-source-grid` — секция чекбоксов источников
- `#gps-color-by-source`, `#gps-color-by-activity` — переключатель color-mode
- `#gps-stat-total`, `#gps-stat-shown` — счётчики в sheet
- `.gps-track-popup` — MapLibre Popup с метаданными трека (имя класса
можно задать через `setHTML` и контейнер)
Существующие селекторы: `#terrain-toggle`, `#terrain-popup`,
`#btn-theme`, `#base-btn-satellite`, `#base-btn-schematic`,
`#terrain-hillshade-cb`, `#tb-gpx`, `#map`.
Предусловие: тестовая среда содержит pre-collected dataset публичных
треков (или mock-backend подменяет `/api/gps-tracks*` фикстурами).
---
### TC-UI-01 — Секция «Источники» видна в #sheet-gpx
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. screenshot: "01-sheet-gpx-sources-section"
6. check-visual: "В верхней части #sheet-gpx (под заголовком, над списком треков) видна секция «ИСТОЧНИКИ» с тремя кнопками segmented control: «Из файла», «По ссылке», «Найти рядом». По умолчанию активна (подсвечена оранжевым) кнопка «Из файла»."
---
### TC-UI-02 — Переключение на вкладку «По ссылке»
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-url"
6. wait: 500
7. screenshot: "02-source-url-pane"
8. check-visual: "Кнопка «По ссылке» подсвечена оранжевым, «Из файла» и «Найти рядом» — нет. Под кнопками видно поле ввода с placeholder «https://example.com/track.gpx» и кнопка «Загрузить» справа."
---
### TC-UI-03 — Переключение на вкладку «Найти рядом»
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. screenshot: "03-source-nearby-pane"
8. check-visual: "Кнопка «Найти рядом» подсвечена оранжевым. Под кнопками видна крупная кнопка «Найти треки в этой области карты». Список найденных треков пуст или отсутствует."
---
### TC-UI-04 — Поле URL принимает ввод
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-url"
6. wait: 500
7. click: "#gpx-url-input"
8. wait: 200
9. screenshot: "04-url-input-focused"
10. check-visual: "Поле #gpx-url-input получило фокус (видна рамка/каретка), placeholder виден если поле пустое. Кнопка «Загрузить» рядом, активна (не дизейблена)."
---
### TC-UI-05 — Невалидный URL: toast об ошибке
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-url"
6. wait: 500
7. click: "#gpx-url-input"
8. wait: 200
9. type: "not-a-url"
10. click: "#btn-gpx-fetch-url"
11. wait: 1000
12. screenshot: "05-invalid-url-toast"
13. check-visual: "Сверху по центру экрана отображается toast-уведомление с текстом «Невалидная ссылка» (или похожим). Никаких изменений на карте."
---
### TC-UI-06 — Кнопка «Найти треки» дизейблится во время запроса
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. click: "#btn-gpx-find-nearby"
8. wait: 300
9. screenshot: "06-finding-tracks-loading"
10. check-visual: "Кнопка «Найти треки в этой области карты» визуально дизейблена (серая / opacity снижен). Виден индикатор загрузки (spinner или moto-wheel)."
---
### TC-UI-07 — Список найденных треков
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. click: "#btn-gpx-find-nearby"
8. wait: 4000
9. screenshot: "07-nearby-tracks-list"
10. check-visual: "Под кнопкой поиска появился список карточек .gpx-nearby-card. Каждая карточка содержит: иконку 🌍 (или OSM-логотип) слева, имя/описание трека и метаданные (км, аноним/автор), кнопку «Показать» справа, маленькую ссылку «↗». Карточки разделены тонкими линиями."
---
### TC-UI-08 — Импорт OSM-трека: трек на карте, индикатор в карточке
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. click: "#btn-gpx-find-nearby"
8. wait: 4000
9. click: ".gnc-import"
10. wait: 4000
11. screenshot: "08-osm-track-imported"
12. check-visual: "На карте видна цветная линия импортированного трека. В списке найденных карточка первого трека показывает индикатор «✓ Загружен» вместо кнопки «Показать». В нижней части #sheet-gpx (в #gpx-list) появилась новая карточка трека с источником «🌍 OSM #...»."
---
### TC-UI-09 — Источник «OSM» — кликабельная ссылка в #gpx-list
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. click: "#btn-gpx-find-nearby"
8. wait: 4000
9. click: ".gnc-import"
10. wait: 4000
11. screenshot: "09-gpx-list-source-osm"
12. check-visual: "В нижнем списке #gpx-list карточка импортированного трека под именем содержит строку «.gpx-source-row» с текстом «🌍 OSM #<число>». Текст оформлен как ссылка (подчёркнут или другой цвет)."
---
### TC-UI-10 — Смешанные источники в #gpx-list
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-url"
6. wait: 500
7. click: "#gpx-url-input"
8. wait: 200
9. type: "https://example.test/mock-track.gpx"
10. click: "#btn-gpx-fetch-url"
11. wait: 4000
12. click: "#source-btn-nearby"
13. wait: 500
14. click: "#btn-gpx-find-nearby"
15. wait: 4000
16. click: ".gnc-import"
17. wait: 4000
18. screenshot: "10-mixed-sources-list"
19. check-visual: "В #gpx-list 2 карточки: одна с источником «🔗 example.test», вторая с «🌍 OSM #...». Карточки имеют разные цветовые индикаторы слева. Обе видны на карте как линии разных цветов."
---
### TC-UI-11 — Импорт по URL: трек появляется на карте
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-url"
6. wait: 500
7. click: "#gpx-url-input"
8. wait: 200
9. type: "https://example.test/mock-track.gpx"
10. click: "#btn-gpx-fetch-url"
11. wait: 5000
12. screenshot: "11-url-track-loaded"
13. check-visual: "На карте видна цветная линия загруженного трека. В #gpx-list появилась карточка с именем «mock-track» и источником «🔗 example.test». Карта выполнила fit bounds — трек по центру экрана."
---
### TC-UI-12 — Секция «Источники» на мобильном
- тип: ui
- viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. screenshot: "12-sources-mobile-default"
6. check-visual: "На мобильном viewport секция «Источники» помещается по ширине экрана. Три кнопки segmented control видны и нажимаемы, не выходят за экран. Активна «Из файла»."
---
### TC-UI-13 — Поле URL на мобильном
- тип: ui
- viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-url"
6. wait: 500
7. screenshot: "13-url-pane-mobile"
8. check-visual: "На мобильном поле #gpx-url-input занимает большую часть ширины, кнопка «Загрузить» справа. Оба элемента не перекрываются, нажимаемы, помещаются в экран."
---
### TC-UI-14 — Список найденных OSM треков на мобильном
- тип: ui
- viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. click: "#btn-gpx-find-nearby"
8. wait: 4000
9. screenshot: "14-nearby-list-mobile"
10. check-visual: "На мобильном карточки .gpx-nearby-card отображаются вертикально, занимают всю ширину. Кнопка «Показать» и ссылка «↗» в каждой карточке нажимаемы, не перекрываются. Список скроллится."
---
### TC-UI-15 — Совместимость со спутниковой подложкой
### TC-UI-01 — Чекбокс «Публичные треки» виден в попапе
- тип: ui
- viewport: desktop
@@ -316,22 +52,381 @@ authors:
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#base-btn-satellite"
6. wait: 5000
5. screenshot: "01-popup-with-public-tracks-checkbox"
6. check-visual: "В открытом попапе #terrain-popup между секциями «Тропы» и «POI» (после соответствующего разделителя `<hr>`) видна строка «Публичные треки» с чекбоксом #public-tracks-cb. По умолчанию чекбокс снят, ссылка «Фильтры…» не видна."
---
### TC-UI-02 — Включение слоя «Публичные треки»
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. screenshot: "02-public-tracks-enabled"
8. check-visual: "Чекбокс установлен. На карте поверх существующих trail-линий и POI видны цветные линии публичных треков (отдельные линии, не heatmap). Рядом с чекбоксом появилась ссылка «Фильтры…»."
---
### TC-UI-03 — Подсказка «Зум 8+» на низком зуме
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?z=5
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 1500
7. screenshot: "03-public-tracks-zoom-hint"
8. check-visual: "Чекбокс включён, но на карте линии публичных треков не видны. Рядом с чекбоксом (или под ним) отображается подсказка «Зум 8+» (стилем как существующая подсказка «Зум 10+» у hillshade)."
---
### TC-UI-04 — Открытие sheet фильтров
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 2000
7. click: "#public-tracks-filters-btn"
8. wait: 800
9. screenshot: "04-gps-filters-sheet-open"
10. check-visual: "Открылся bottom sheet #sheet-gps-filters с заголовком «Фильтры публичных треков». Видны секции: «ТИП АКТИВНОСТИ» (7 чекбоксов: эндуро, мото, off-road, велосипед, пешком, лыжи, другое), «ИСТОЧНИК» (≥ 3 чекбокса), «ЦВЕТ ЛИНИЙ» (segmented control «По источнику» / «По активности»). По умолчанию все чекбоксы установлены, color-mode='По источнику' активен."
---
### TC-UI-05 — Фильтрация по активности (клиентская, мгновенная)
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#public-tracks-filters-btn"
8. wait: 800
9. screenshot: "05a-filters-all-on"
10. check-visual: "В sheet видны все 7 чекбоксов активности — установлены. На карте видно много линий разных типов."
11. click: "#gps-activity-grid input[value='bicycle']"
12. wait: 300
13. click: "#gps-activity-grid input[value='hike']"
14. wait: 300
15. click: "#gps-activity-grid input[value='ski']"
16. wait: 300
17. click: "#gps-activity-grid input[value='other']"
18. wait: 500
19. screenshot: "05b-filters-only-moto-types"
20. check-visual: "Выключены чекбоксы «Велосипед», «Пешком», «Лыжи», «Другое». На карте линий стало заметно меньше (только enduro/moto/offroad). Счётчик «Видны (фильтр)» в нижней части sheet уменьшился."
---
### TC-UI-06 — Фильтрация по источнику
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#public-tracks-filters-btn"
8. wait: 800
9. click: "#gps-source-grid input[value='osm']"
10. wait: 500
11. screenshot: "06-source-osm-disabled"
12. check-visual: "Чекбокс «OSM» снят. На карте все линии цвета OSM (зелёного — при color-by-source) скрыты. Счётчик «Видны» уменьшился."
---
### TC-UI-07 — Переключение color-mode
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#public-tracks-filters-btn"
8. wait: 800
9. screenshot: "07a-color-by-source"
10. check-visual: "Активна кнопка «По источнику». Линии на карте окрашены по источникам (например, зелёный = OSM, красный = EnduroRussia)."
11. click: "#gps-color-by-activity"
12. wait: 600
13. screenshot: "07b-color-by-activity"
14. check-visual: "Активна кнопка «По активности». Линии перекрашены: например, красные = enduro, оранжевые = moto. Кнопка «По источнику» больше не подсвечена."
---
### TC-UI-08 — Popup при клике на трек
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#map"
8. wait: 1500
9. screenshot: "08-track-popup"
10. check-visual: "При клике на линию трека (предполагается, что под центром карты есть трек) открылся MapLibre Popup. В нём видны: иконка активности (🏍 / 🚴 / …) + текстовая метка, длина в км, дата (если есть), автор (если есть), список источников со ссылками '↗'. Popup имеет крестик закрытия."
---
### TC-UI-09 — Halo на спутниковой подложке
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#base-btn-satellite"
8. wait: 5000
9. screenshot: "09-public-tracks-on-satellite"
10. check-visual: "Карта показывает спутниковые снимки. Линии публичных треков видны поверх спутника, у каждой линии есть белая (или светлая) обводка-halo для контраста на тёмном фоне. Цвета линий по-прежнему отличаются по источнику/активности."
---
### TC-UI-10 — Возврат на схему — halo пропадает
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#base-btn-satellite"
8. wait: 5000
9. click: "#base-btn-schematic"
10. wait: 3000
11. screenshot: "10-back-to-schematic-no-halo"
12. check-visual: "Карта вернулась на схему OSM. Линии публичных треков видны без halo (обычная толщина и цвет). На фоне светлой схемы — без обводки."
---
### TC-UI-11 — Сохранение слоя при переключении тёмной темы
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#btn-theme"
8. wait: 3000
9. screenshot: "11-public-tracks-after-theme-switch"
10. check-visual: "После переключения темы (например, на тёмную) линии публичных треков остались на карте. Цвета сохранены. На тёмной теме линии хорошо различимы."
---
### TC-UI-12 — Сохранение слоя при включении hillshade
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#terrain-hillshade-cb"
8. wait: 3000
9. screenshot: "12-public-tracks-over-hillshade"
10. check-visual: "Включён hillshade (тени рельефа). Линии публичных треков остаются видны поверх теней рельефа. Контраст сохраняется."
---
### TC-UI-13 — Совместимость с маршрутом OSRM (z-order)
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#tb-route"
8. wait: 1000
9. click: "#map"
10. wait: 1500
11. scroll: 100
12. click: "#map"
13. wait: 5000
14. screenshot: "13-public-tracks-and-osrm-route"
15. check-visual: "Видны и линии публичных треков, и линия маршрута OSRM (синяя/оранжевая). Маршрут OSRM визуально лежит поверх публичных треков (выше по z-order). Обе системы линий читаемы."
---
### TC-UI-14 — Sheet фильтров на мобильном
- тип: ui
- viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#public-tracks-filters-btn"
8. wait: 800
9. screenshot: "14-gps-filters-mobile"
10. check-visual: "На мобильном viewport sheet #sheet-gps-filters занимает всю ширину. Все 7 чекбоксов активности видны (например, 2-3 колонки grid). Чекбоксы источников видны. Segmented control color-mode помещается. Все элементы нажимаемы, не перекрываются."
---
### TC-UI-15 — Включение слоя на мобильном
- тип: ui
- viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. screenshot: "15-public-tracks-mobile"
8. check-visual: "На мобильном устройстве после включения чекбокса линии публичных треков видны на карте. Попап слоёв и тулбар не перекрывают карту целиком — слой просматривается."
---
### TC-UI-16 — Persistence: слой включён после перезагрузки
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. navigate: https://openclaw.mva154.duckdns.org/enduro/
8. wait: 6000
9. screenshot: "16-public-tracks-after-reload"
10. check-visual: "После перезагрузки страницы карта сразу показывает линии публичных треков (слой автоматически восстановлен из localStorage). Открытие попапа слоёв должно показать чекбокс установленным."
---
### TC-UI-17 — Persistence: фильтры сохраняются после перезагрузки
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#public-tracks-filters-btn"
8. wait: 800
9. click: "#gps-activity-grid input[value='bicycle']"
10. wait: 300
11. click: "#gps-activity-grid input[value='hike']"
12. wait: 300
13. click: "#gps-color-by-activity"
14. wait: 500
15. navigate: https://openclaw.mva154.duckdns.org/enduro/
16. wait: 6000
17. click: "#terrain-toggle"
18. wait: 500
19. click: "#public-tracks-filters-btn"
20. wait: 800
21. screenshot: "17-filters-after-reload"
22. check-visual: "Чекбоксы «Велосипед» и «Пешком» по-прежнему сняты. Color-mode = «По активности» (соответствующая кнопка подсвечена). Линии на карте окрашены по активности."
---
### TC-UI-18 — Атрибуция источников
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. screenshot: "18-attribution-public-tracks"
8. check-visual: "В правом нижнем углу карты (в стандартной MapLibre-панели атрибуции) видны строки с атрибуцией источников публичных треков: например, «© OpenStreetMap contributors (ODbL)» и «EnduroRussia.ru» (либо иконка info, при клике на которую разворачивается полный текст)."
---
### TC-UI-19 — Совместимость с личным GPX (ET-006)
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#tb-gpx"
8. wait: 1000
9. click: "#source-btn-nearby"
10. wait: 500
11. click: "#btn-gpx-find-nearby"
12. wait: 4000
13. click: ".gnc-import"
14. wait: 4000
15. screenshot: "15-osm-track-on-satellite"
16. check-visual: "На спутниковой подложке видна цветная линия импортированного OSM-трека. Линия имеет hover-видимость (контрастная для спутника). Панель #sheet-gpx не конфликтует со спутником визуально."
9. screenshot: "19-public-tracks-with-gpx-sheet"
10. check-visual: "Открыт sheet #sheet-gpx (для личных треков из ET-006). Слой публичных треков на карте остаётся видимым. Sheet и слой не конфликтуют визуально. Список личных треков в sheet — пустой (если ничего не загружено)."
---
### TC-UI-16Сохранение треков при переключении тёмной темы
### TC-UI-20Выключение слоя — линии исчезают
- тип: ui
- viewport: desktop
@@ -339,57 +434,11 @@ authors:
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. click: "#btn-gpx-find-nearby"
8. wait: 4000
9. click: ".gnc-import"
10. wait: 4000
11. click: "#btn-theme"
12. wait: 3000
13. screenshot: "16-osm-track-after-theme-switch"
14. check-visual: "После переключения тёмной темы цветная линия импортированного OSM-трека остаётся на карте. В #gpx-list карточка трека с источником «🌍 OSM #...» сохранилась."
---
### TC-UI-17 — Сохранение треков при переключении источника «Из файла»
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. click: "#btn-gpx-find-nearby"
8. wait: 4000
9. click: ".gnc-import"
10. wait: 4000
11. click: "#source-btn-file"
12. wait: 500
13. screenshot: "17-back-to-file-tab"
14. check-visual: "Активна вкладка «Из файла», секция «Найти рядом» свернута. Импортированный OSM-трек остаётся в нижнем списке #gpx-list (карточка с «🌍 OSM #...»). Сам трек по-прежнему видим на карте."
---
### TC-UI-18 — Внешняя ссылка ↗ на osm.org
- тип: ui
- viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 5000
3. click: "#tb-gpx"
4. wait: 1000
5. click: "#source-btn-nearby"
6. wait: 500
7. click: "#btn-gpx-find-nearby"
8. wait: 4000
9. screenshot: "18-external-link-button"
10. check-visual: "В каждой карточке .gpx-nearby-card в правом углу видна кнопка «↗» (.gnc-external). Кнопка имеет hover-состояние (cursor:pointer), визуально отличима от основной кнопки «Показать»."
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#public-tracks-cb"
6. wait: 3000
7. click: "#public-tracks-cb"
8. wait: 1500
9. screenshot: "20-public-tracks-disabled"
10. check-visual: "Чекбокс снят. Все линии публичных треков исчезли с карты. Ссылка «Фильтры…» рядом с чекбоксом скрылась. Базовые слои (схема, trails, POI) остались видимыми и без изменений."