Compare commits
4 Commits
feature/ET
...
fix/analys
| Author | SHA1 | Date | |
|---|---|---|---|
| 019d944557 | |||
| bd7903e191 | |||
| c18b4280f4 | |||
| d4f1591be3 |
@@ -12,22 +12,34 @@ tools:
|
||||
Ты — бизнес-аналитик проекта enduro-trails. По бизнес-запросу создаёшь
|
||||
полный пакет документов для разработки.
|
||||
|
||||
## КРИТИЧЕСКИ ВАЖНО: Используй Write tool!
|
||||
|
||||
**Ты ОБЯЗАН создавать файлы через Write tool.** Не описывай содержимое в тексте ответа —
|
||||
ЗАПИСЫВАЙ каждый артефакт в файл. Orchestrator проверяет наличие файлов на диске.
|
||||
|
||||
Порядок работы:
|
||||
1. Прочитай входные данные (Read tool)
|
||||
2. Создай КАЖДЫЙ deliverable через Write tool (полное содержимое файла)
|
||||
3. В конце выведи краткий summary что создано
|
||||
|
||||
Если ты просто напишешь текст без вызова Write — артефакты будут потеряны!
|
||||
|
||||
## Что прочесть
|
||||
1. CLAUDE.md — паспорт проекта
|
||||
2. docs/work-items/<plane-id>/00-business-request.md — входные данные
|
||||
3. docs/phases/ — текущий roadmap
|
||||
4. src/web/index.html, src/api/main.py — текущий стейт приложения
|
||||
|
||||
## Deliverables (создать в docs/work-items/<plane-id>/)
|
||||
## Deliverables (создать через Write tool в docs/work-items/<plane-id>/)
|
||||
|
||||
### Обязательные
|
||||
- `01-brd.md` — Business Requirements Document
|
||||
- `02-trz.md` — Техническое задание
|
||||
- `03-acceptance-criteria.md` — Критерии приёмки
|
||||
- `04-test-plan.yaml` — план функциональных тестов (unit, integration, e2e)
|
||||
- 01-brd.md — Business Requirements Document
|
||||
- 02-trz.md — Техническое задание
|
||||
- 03-acceptance-criteria.md — Критерии приёмки
|
||||
- 04-test-plan.yaml — план функциональных тестов (unit, integration, e2e)
|
||||
|
||||
### UI тест-кейсы (обязательно если задача затрагивает UI)
|
||||
- `04b-ui-test-cases.md` — Playwright UI тест-кейсы для визуального тестирования
|
||||
- 04b-ui-test-cases.md — Playwright UI тест-кейсы для визуального тестирования
|
||||
|
||||
**Когда создавать 04b-ui-test-cases.md:**
|
||||
- Задача добавляет новый UI элемент (кнопка, панель, слой на карте)
|
||||
@@ -40,12 +52,12 @@ tools:
|
||||
Каждый тест-кейс — заголовок ### TC-UI-XX — Название, тип ui, viewport desktop|mobile|both.
|
||||
|
||||
Шаги — нумерованный список:
|
||||
- navigate: <url>
|
||||
- wait: <ms> (3000-5000 для карты)
|
||||
- click: "<css-selector>"
|
||||
- scroll: <pixels>
|
||||
- screenshot: "<name>"
|
||||
- check-visual: "<что проверяем>"
|
||||
- navigate: url
|
||||
- wait: ms (3000-5000 для карты)
|
||||
- click: css-selector
|
||||
- scroll: pixels
|
||||
- screenshot: name
|
||||
- check-visual: что проверяем
|
||||
|
||||
URL: всегда https://openclaw.mva154.duckdns.org/enduro/
|
||||
CSS-селекторы: проверяй по src/web/index.html. Типичные ID: #sheet-gpx, #unit-toggle, #terrain-toggle, #poi-checkbox, #map.
|
||||
@@ -54,3 +66,4 @@ CSS-селекторы: проверяй по src/web/index.html. Типичны
|
||||
- Предлагать архитектурные решения
|
||||
- Писать код
|
||||
- Изменять артефакты других work item
|
||||
- Выводить содержимое файлов в stdout вместо записи через Write tool
|
||||
|
||||
@@ -1,32 +1,128 @@
|
||||
---
|
||||
name: deployer
|
||||
description: DevOps-агент. Merge → deploy → smoke → rollback при необходимости.
|
||||
description: DevOps-агент. Merge PR → tag → deploy → smoke → rollback при необходимости.
|
||||
model: claude-sonnet-4-6
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/14-deploy-log.md, CHANGELOG.md)
|
||||
- Git (merge, tag)
|
||||
- Bash (docker compose, curl)
|
||||
- Read (везде)
|
||||
- Write (только docs/work-items/*/14-deploy-log.md, CHANGELOG.md)
|
||||
- Bash (git, curl, docker)
|
||||
---
|
||||
|
||||
# System prompt: Deployer
|
||||
|
||||
Ты — DevOps-агент проекта enduro-trails. Безопасно проводишь изменение через test-окружение.
|
||||
Ты — DevOps-агент проекта enduro-trails. Твоя задача — безопасно довести код до production.
|
||||
|
||||
## Среды
|
||||
- test: https://openclaw.mva154.duckdns.org/enduro/
|
||||
- Деплой: docker compose up -d на mva154
|
||||
- Deploy: docker compose на хосте (через docker exec или SSH)
|
||||
- Gitea API: http://localhost:3000/api/v1
|
||||
- Gitea token: из переменной ORCH_GITEA_TOKEN
|
||||
- Repo owner: admin
|
||||
- Repo name: enduro-trails
|
||||
|
||||
## Алгоритм
|
||||
1. Проверь предусловия: QG-6 green, лейбл stage:ready-to-deploy
|
||||
2. Merge PR (squash)
|
||||
3. Создай tag vX.Y.Z (semver по типам коммитов)
|
||||
4. docker compose pull && docker compose up -d
|
||||
5. Healthcheck 5 минут
|
||||
6. Smoke-тесты
|
||||
7. Если fail — rollback к предыдущему тегу
|
||||
8. Запиши 14-deploy-log.md
|
||||
## Алгоритм (выполняй строго по порядку)
|
||||
|
||||
### 1. Merge PR
|
||||
```bash
|
||||
# Найти PR для ветки
|
||||
BRANCH=$(grep "^Branch:" .task-deploy.md | awk '{print $2}')
|
||||
GITEA_TOKEN=$ORCH_GITEA_TOKEN
|
||||
PR_NUMBER=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
"http://localhost:3000/api/v1/repos/admin/enduro-trails/pulls?state=open&head=$BRANCH" \
|
||||
| python3 -c "import sys,json; prs=json.load(sys.stdin); print(prs[0]['number'] if prs else '')")
|
||||
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "ERROR: No open PR for $BRANCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Merge
|
||||
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
"http://localhost:3000/api/v1/repos/admin/enduro-trails/pulls/$PR_NUMBER/merge" \
|
||||
-H "Content-Type: application/json" -d '{"Do":"merge"}'
|
||||
```
|
||||
|
||||
### 2. Создать tag
|
||||
```bash
|
||||
# Определить версию
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||
# Инкремент patch (упрощённо)
|
||||
MAJOR=$(echo $LAST_TAG | cut -d. -f1 | tr -d v)
|
||||
MINOR=$(echo $LAST_TAG | cut -d. -f2)
|
||||
PATCH=$(echo $LAST_TAG | cut -d. -f3)
|
||||
NEW_TAG="v${MAJOR}.${MINOR}.$((PATCH+1))"
|
||||
|
||||
git fetch origin main
|
||||
git tag $NEW_TAG origin/main
|
||||
git push origin $NEW_TAG
|
||||
```
|
||||
|
||||
### 3. Deploy
|
||||
```bash
|
||||
cd /repos/enduro-trails
|
||||
git fetch origin && git checkout main && git pull origin main
|
||||
# Deploy зависит от проекта. Для enduro-trails:
|
||||
# Файлы уже на месте после merge в main, nginx обслуживает static
|
||||
```
|
||||
|
||||
### 4. Healthcheck (до 60 сек)
|
||||
```bash
|
||||
for i in $(seq 1 12); do
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://openclaw.mva154.duckdns.org/enduro/ 2>/dev/null)
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "Healthcheck OK"
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "ERROR: Healthcheck failed (HTTP $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### 5. Smoke test
|
||||
```bash
|
||||
# Проверить ключевые ресурсы
|
||||
curl -sf https://openclaw.mva154.duckdns.org/enduro/ > /dev/null || exit 1
|
||||
curl -sf https://openclaw.mva154.duckdns.org/enduro/static/style.json > /dev/null || exit 1
|
||||
curl -sf https://openclaw.mva154.duckdns.org/enduro/static/app.js > /dev/null || exit 1
|
||||
echo "Smoke tests PASS"
|
||||
```
|
||||
|
||||
### 6. Rollback (если smoke fail)
|
||||
```bash
|
||||
# Откатить к предыдущему тегу
|
||||
git checkout $LAST_TAG
|
||||
echo "ROLLED BACK to $LAST_TAG"
|
||||
# Уведомить
|
||||
exit 1
|
||||
```
|
||||
|
||||
### 7. Финализация
|
||||
- Записать `docs/work-items/<WORK_ITEM_ID>/14-deploy-log.md`:
|
||||
- Версия (tag)
|
||||
- Время deploy
|
||||
- Результат smoke
|
||||
- PR number
|
||||
- Обновить CHANGELOG.md (новая запись сверху)
|
||||
- Commit + push в main
|
||||
|
||||
## Формат 14-deploy-log.md
|
||||
```markdown
|
||||
# Deploy Log — <WORK_ITEM_ID>
|
||||
|
||||
- **Version:** vX.Y.Z
|
||||
- **Date:** YYYY-MM-DD HH:MM UTC
|
||||
- **PR:** #N
|
||||
- **Environment:** test
|
||||
- **Healthcheck:** PASS
|
||||
- **Smoke:** PASS
|
||||
- **Status:** SUCCESS
|
||||
```
|
||||
|
||||
## Запрещено
|
||||
- Менять код
|
||||
- Деплоить без зелёного QG-6
|
||||
- --force-push
|
||||
- Менять исходный код (src/, tests/)
|
||||
- Деплоить без merge
|
||||
- Force push
|
||||
- Игнорировать failed healthcheck/smoke
|
||||
|
||||
51
docs/work-items/ET-008/00-business-request.md
Normal file
51
docs/work-items/ET-008/00-business-request.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
type: business-request
|
||||
work_item_id: ET-008
|
||||
title: "GPS-треки с публичных платформ на карте"
|
||||
created_at: 2026-06-01
|
||||
source: plane
|
||||
requester: Слава
|
||||
---
|
||||
|
||||
# Бизнес-запрос — ET-008
|
||||
|
||||
## Исходная формулировка
|
||||
|
||||
> Хочу видеть на карте GPS-треки с публичных платформ (OSM, чужие ссылки
|
||||
> на GPX), а не только локальные файлы. Минимум: вставить ссылку на
|
||||
> GPX-файл — увидеть трек. Дальше — поиск чужих публичных треков в
|
||||
> видимой области карты, чтобы перед поездкой посмотреть, кто и где ездил.
|
||||
|
||||
## Контекст и ограничения
|
||||
|
||||
1. ET-006 уже даёт инфраструктуру отображения GPX-треков (модель,
|
||||
рендеринг, sheet, профиль высот). Эту инфраструктуру переиспользуем.
|
||||
2. В стеке нет авторизации пользователей и БД с user accounts —
|
||||
платформы с обязательным OAuth (Strava, Komoot) **вне scope MVP**.
|
||||
3. Платный API Wikiloc — **вне scope MVP**.
|
||||
4. CORS не позволяет браузеру тянуть GPX напрямую с большинства
|
||||
платформ — нужен прокси через FastAPI.
|
||||
5. Rate limits публичных API (OSM, Overpass) — нужен server-side кэш.
|
||||
|
||||
## Решения аналитика (по умолчанию, при отсутствии явных уточнений)
|
||||
|
||||
| Вопрос | Решение | Обоснование |
|
||||
|--------|---------|-------------|
|
||||
| Платформы MVP | OSM Public GPS Traces + универсальный GPX-по-URL | Открытые API без авторизации, бесплатные, покрывают сценарии «свой трек по ссылке» и «чужие треки рядом» |
|
||||
| Сценарии | (1) импорт по URL; (2) bbox-поиск треков в видимой области | Минимальный полезный набор, не требующий новых разделов UI |
|
||||
| Хранение | Сессия (как ET-006) + server-side LRU-кэш на бэкенде | Не вводим БД и аккаунты; кэш защищает от rate limits |
|
||||
| Auth | Нет | Все запросы — публичные данные |
|
||||
| Платформы post-MVP | Wikiloc API, Strava OAuth, Komoot OAuth | Будут отдельными work item, когда появится система аккаунтов |
|
||||
|
||||
## Уточнения
|
||||
|
||||
1. URL-импорт должен работать с любой прямой ссылкой на `.gpx`-файл
|
||||
(GitHub raw, gist, личный сайт, веб-сервер пользователя).
|
||||
2. Поиск по OSM-трекам ограничен видимой областью карты (bbox).
|
||||
Глобальный поиск не требуется.
|
||||
3. Загруженные с публичных платформ треки попадают в тот же sheet
|
||||
`#sheet-gpx`, что и локальные GPX, и ведут себя идентично (статистика,
|
||||
профиль высот, удаление, fit bounds, переживание смены стиля).
|
||||
4. Источник трека (URL / OSM trace id) сохраняется в модели и
|
||||
отображается в карточке трека для пользователя.
|
||||
5. Кэш на сервере — TTL 24 часа, не персистентный (in-memory).
|
||||
98
docs/work-items/ET-008/01-brd.md
Normal file
98
docs/work-items/ET-008/01-brd.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
type: brd
|
||||
work_item_id: ET-008
|
||||
title: "BRD: GPS-треки с публичных платформ на карте"
|
||||
version: 1
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
|
||||
# BRD — ET-008: GPS-треки с публичных платформ на карте
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Дать пользователю возможность увидеть на карте Enduro Trails GPS-треки
|
||||
с публичных источников без скачивания файлов вручную: либо вставив
|
||||
прямую ссылку на GPX, либо найдя чужие публичные треки в видимой
|
||||
области карты (через OSM Public GPS Traces).
|
||||
|
||||
## 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-поиск, нет авторизации для чтения.
|
||||
|
||||
## 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` и неотличимы от локальных по поведению |
|
||||
|
||||
### Out of scope
|
||||
|
||||
- OAuth-интеграции (Strava, Komoot)
|
||||
- Платный API Wikiloc
|
||||
- Поиск треков глобально (без bbox)
|
||||
- Сохранение треков в БД между сессиями
|
||||
- Подписки на пользователей других платформ
|
||||
- Загрузка собственных треков на публичные платформы
|
||||
|
||||
## 4. Метрики успеха
|
||||
|
||||
| Метрика | Критерий |
|
||||
|---------|----------|
|
||||
| URL-импорт | Прямая ссылка на GPX до 50 МБ загружается за ≤ 5 сек на средней сети |
|
||||
| OSM-поиск bbox | Запрос видимой области возвращает результат за ≤ 3 сек (с кэшем — мгновенно) |
|
||||
| Точность | OSM-трек после импорта визуально совпадает с тем же треком из osm.org |
|
||||
| Кэш | Повторный запрос той же области/URL в течение 24 ч — без обращения к внешнему API |
|
||||
| UX | Все ошибки (CORS, 404, лимит, формат) — внятные toast-уведомления, не падение |
|
||||
| Совместимость с ET-006 | Локальные и удалённые треки в одном списке, поведение идентично |
|
||||
| Сохранение при смене стиля | Импортированные треки переживают переключение тёмной темы и слоёв рельефа |
|
||||
|
||||
## 5. Риски
|
||||
|
||||
| Риск | Вероятность | Влияние | Митигация |
|
||||
|------|-------------|---------|-----------|
|
||||
| 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: показать сообщение, не блокировать другие функции |
|
||||
|
||||
## 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.
|
||||
473
docs/work-items/ET-008/02-trz.md
Normal file
473
docs/work-items/ET-008/02-trz.md
Normal file
@@ -0,0 +1,473 @@
|
||||
---
|
||||
type: trz
|
||||
work_item_id: ET-008
|
||||
title: "ТЗ: GPS-треки с публичных платформ на карте"
|
||||
version: 1
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
|
||||
# ТЗ — ET-008: GPS-треки с публичных платформ на карте
|
||||
|
||||
## 1. Функциональные требования
|
||||
|
||||
### REQ-F-01: Расширение sheet `#sheet-gpx`
|
||||
|
||||
В верхней части `#sheet-gpx` (под header, над списком треков) добавить
|
||||
секцию «Источники» с двумя вкладками-кнопками (segmented control):
|
||||
|
||||
- **Из файла** — текущее поведение ET-006 (`#btn-gpx-upload`).
|
||||
- **По ссылке** — поле ввода URL + кнопка «Загрузить».
|
||||
- **Найти рядом** — кнопка «Найти публичные треки в этой области карты».
|
||||
|
||||
При первом открытии активна вкладка **Из файла** (обратная совместимость).
|
||||
|
||||
### REQ-F-02: Импорт по URL
|
||||
|
||||
- Поле `<input id="gpx-url-input" type="url">` с placeholder
|
||||
«https://example.com/track.gpx».
|
||||
- Кнопка `#btn-gpx-fetch-url` рядом — «Загрузить».
|
||||
- При нажатии:
|
||||
1. Клиентская валидация URL (`new URL()`, схема `https?:`).
|
||||
2. Запрос `GET /api/gpx/fetch?url=<encoded>`.
|
||||
3. Полученный текст GPX парсится тем же `parseGpx()` из `gpx.js`.
|
||||
4. Результат добавляется в `window.gpxTracks` как обычно. Поле
|
||||
`source` = `{kind: 'url', url: '<original>'}`.
|
||||
5. `filename` для отображения: последний segment URL без `.gpx` или
|
||||
`<gpx><metadata><name>` если есть.
|
||||
- Поддерживается также Enter в поле ввода.
|
||||
|
||||
### REQ-F-03: Прокси-эндпоинт `/api/gpx/fetch`
|
||||
|
||||
```
|
||||
GET /api/gpx/fetch?url=<percent-encoded-url>
|
||||
```
|
||||
|
||||
- Валидация:
|
||||
- Схема URL ∈ {`http`, `https`}.
|
||||
- Хост резолвится в публичный IP (не RFC1918, не loopback, не link-local).
|
||||
Проверка через `socket.getaddrinfo()` + `ipaddress.ip_address().is_global`.
|
||||
- Запрет редиректов на приватные IP (`httpx.AsyncClient(follow_redirects=False)`,
|
||||
ручная обработка max 3 редиректов с повторной валидацией хоста).
|
||||
- Загрузка:
|
||||
- Таймаут 15 секунд.
|
||||
- Лимит размера ответа: 50 МБ (стримом, прервать при превышении).
|
||||
- Заголовок `User-Agent: enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)`.
|
||||
- Кэш:
|
||||
- Ключ = SHA-256(url).
|
||||
- In-memory LRU, max 64 записи, TTL 24 ч.
|
||||
- При cache hit — отдаётся из кэша.
|
||||
- Ответ:
|
||||
- `200 OK`, `Content-Type: application/gpx+xml`, тело GPX.
|
||||
- Заголовок `X-Cache: HIT|MISS`.
|
||||
- Ошибки → JSON `{error: "..."}`:
|
||||
- `400` — невалидный URL / приватный IP / запрещённая схема.
|
||||
- `404` — внешний сервер вернул 404.
|
||||
- `413` — превышен лимит размера.
|
||||
- `502` — внешний сервер недоступен / таймаут.
|
||||
- `504` — таймаут на нашей стороне.
|
||||
|
||||
### REQ-F-04: Кнопка «Найти публичные треки»
|
||||
|
||||
- Кнопка `#btn-gpx-find-nearby` в секции «Источники».
|
||||
- Текст: «Найти треки в этой области».
|
||||
- При нажатии:
|
||||
1. Получить bbox видимой области карты: `map.getBounds()`.
|
||||
2. Валидация: площадь bbox ≤ 0.25 deg² (OSM API limit — иначе ошибка).
|
||||
Если больше — toast «Слишком большая область, увеличьте zoom».
|
||||
3. Запрос `GET /api/gpx/osm/traces?bbox=west,south,east,north`.
|
||||
4. Открыть подсекцию «Найденные треки» (REQ-F-05).
|
||||
|
||||
### REQ-F-05: Прокси-эндпоинт `/api/gpx/osm/traces`
|
||||
|
||||
```
|
||||
GET /api/gpx/osm/traces?bbox=<west>,<south>,<east>,<north>&page=<n>
|
||||
```
|
||||
|
||||
- Параметры:
|
||||
- `bbox` — обязательный, 4 числа через запятую.
|
||||
- `page` — опциональный, целое ≥ 0, default 0.
|
||||
- Валидация:
|
||||
- Каждая координата — валидный float, в допустимом диапазоне.
|
||||
- Площадь bbox ≤ 0.25 deg² — иначе `400`.
|
||||
- Запрос к OSM:
|
||||
```
|
||||
GET https://api.openstreetmap.org/api/0.6/trackpoints
|
||||
?bbox=<bbox>&page=<page>
|
||||
```
|
||||
- Таймаут 10 секунд.
|
||||
- User-Agent как в REQ-F-03.
|
||||
- Парсинг ответа:
|
||||
- OSM возвращает GPX 1.0 с `<trkseg>` и атрибутом `gpx_id` у некоторых
|
||||
точек (см. формат OSM API). Группируем точки по `gpx_id` →
|
||||
массив треков-метаданных.
|
||||
- Анонимные треки (без `gpx_id`) объединяются в один общий «Анонимные треки этой области».
|
||||
- Кэш:
|
||||
- Ключ = `(bbox_rounded_to_4_digits, page)`.
|
||||
- In-memory LRU, max 256 записей, TTL 24 ч.
|
||||
- Ответ (JSON):
|
||||
```json
|
||||
{
|
||||
"bbox": [w, s, e, n],
|
||||
"page": 0,
|
||||
"has_more": false,
|
||||
"tracks": [
|
||||
{
|
||||
"osm_id": 12345,
|
||||
"name": "Trail in the woods",
|
||||
"description": "...",
|
||||
"user": "username",
|
||||
"points_count": 320,
|
||||
"distance_km": 12.4,
|
||||
"url": "https://www.openstreetmap.org/user/.../traces/12345",
|
||||
"gpx_url": "https://api.openstreetmap.org/api/0.6/gpx/12345/data"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- Поле `distance_km` — посчитано на сервере (Haversine).
|
||||
- Ошибки → JSON `{error: "..."}`:
|
||||
- `400` — невалидный bbox / слишком большая область.
|
||||
- `502` — OSM API недоступен.
|
||||
- `504` — таймаут.
|
||||
|
||||
### REQ-F-06: UI списка найденных треков
|
||||
|
||||
В подсекции `#gpx-nearby-results` под кнопкой «Найти треки»:
|
||||
|
||||
- Заголовок: «Найдено N треков в этой области».
|
||||
- Список карточек, каждая:
|
||||
- Иконка-индикатор источника (OSM-логотип маленький).
|
||||
- Имя трека (или «Без названия»).
|
||||
- Метаданные: длина (км, через `units.js`), автор (если есть).
|
||||
- Кнопка «Показать» — импортирует трек на карту.
|
||||
- Кнопка «↗» — открывает страницу трека на osm.org в новой вкладке.
|
||||
- Если `has_more` — кнопка «Показать ещё» внизу списка (увеличивает page).
|
||||
- Если треков нет — текст «В этой области нет публичных GPS-треков».
|
||||
|
||||
### REQ-F-07: Импорт выбранного OSM-трека
|
||||
|
||||
При клике на «Показать»:
|
||||
1. Запрос `GET /api/gpx/fetch?url=<gpx_url>` — тот же эндпоинт, что для
|
||||
произвольного URL (переиспользование кэша и валидации).
|
||||
2. После загрузки — те же шаги, что REQ-F-02 (парсинг → модель → рендеринг).
|
||||
3. Поле `source` = `{kind: 'osm', osm_id: <id>, url: <osm_page_url>}`.
|
||||
4. Карточка в списке найденных треков получает индикатор «✓ Загружен».
|
||||
5. Повторный клик «Показать» — no-op (toast «Уже загружен»).
|
||||
|
||||
### REQ-F-08: Отображение источника в карточке трека
|
||||
|
||||
В существующей карточке трека в списке `#gpx-list` (ET-006):
|
||||
|
||||
- Под именем файла мелким шрифтом добавить строку «источник»:
|
||||
- Локальный файл: «📁 локальный файл» (без изменения для ET-006).
|
||||
- URL: «🔗 <hostname>» (например, «🔗 github.com»).
|
||||
- OSM: «🌍 OSM #<id>» — кликабельная ссылка на страницу osm.org.
|
||||
|
||||
### REQ-F-09: Расширение модели `window.gpxTracks`
|
||||
|
||||
Каждый элемент `window.gpxTracks` дополнительно содержит:
|
||||
|
||||
```javascript
|
||||
{
|
||||
// ... существующие поля ET-006 (id, filename, color, tracks, waypoints, ...)
|
||||
source: {
|
||||
kind: 'file' | 'url' | 'osm',
|
||||
url: string | null, // для kind='url' и 'osm'
|
||||
osm_id: number | null, // для kind='osm'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Для треков ET-006 (загруженных из файла) `source.kind = 'file'`
|
||||
(обратная совместимость через миграцию на лету: если `source` отсутствует,
|
||||
читать как `{kind: 'file'}`).
|
||||
|
||||
### REQ-F-10: Обработка ошибок и toast-уведомления
|
||||
|
||||
| Ситуация | Toast |
|
||||
|----------|-------|
|
||||
| Невалидный URL | «Невалидная ссылка» |
|
||||
| URL → приватный IP | «Эта ссылка недоступна» |
|
||||
| Внешний 404 | «Файл не найден по этой ссылке» |
|
||||
| Внешний таймаут / 502 | «Сервер не отвечает, попробуйте позже» |
|
||||
| Файл > 50 МБ | «Файл слишком большой (макс. 50 МБ)» |
|
||||
| Не GPX (DOMParser fail) | «По этой ссылке не GPX-файл» |
|
||||
| OSM: bbox > 0.25 deg² | «Слишком большая область, увеличьте zoom» |
|
||||
| OSM: 0 треков | «В этой области нет публичных GPS-треков» (не toast, а inline-сообщение) |
|
||||
| OSM: rate limit (429) | «Слишком много запросов к OSM, попробуйте через минуту» |
|
||||
|
||||
### REQ-F-11: Сохранение при смене стиля карты
|
||||
|
||||
Импортированные треки переживают `map.setStyle()` через тот же механизм
|
||||
`rebuildGpxOverlays()`, что и локальные ET-006. Никаких изменений в
|
||||
этой функции не требуется — модель данных совместима.
|
||||
|
||||
## 2. Нефункциональные требования
|
||||
|
||||
### REQ-NF-01: Безопасность
|
||||
|
||||
- Прокси `/api/gpx/fetch` защищён от SSRF (REQ-F-03):
|
||||
- Whitelist схем.
|
||||
- Резолв и проверка хоста на публичность.
|
||||
- Ручная обработка редиректов с повторной валидацией.
|
||||
- Лимит размера ответа стримом.
|
||||
- Парсинг XML на бэкенде (если потребуется — для OSM-ответа) через
|
||||
`defusedxml.ElementTree` — защита от XXE / billion laughs.
|
||||
- Парсинг GPX на клиенте — нативный `DOMParser`, XXE отключён по умолчанию.
|
||||
- CORS на новых эндпоинтах — наследуется от существующей конфигурации
|
||||
(`allow_origins=["*"]`), отдельных правил не требуется.
|
||||
|
||||
### REQ-NF-02: Производительность
|
||||
|
||||
- Запрос OSM с кэш-хитом: ≤ 50 мс.
|
||||
- Запрос OSM без кэша: ≤ 3 сек (зависит от OSM API).
|
||||
- URL-импорт GPX 1 МБ: ≤ 2 сек.
|
||||
- URL-импорт GPX 50 МБ: ≤ 10 сек (с учётом сети).
|
||||
- Bbox-валидация и серилизация на бэкенде: ≤ 5 мс.
|
||||
|
||||
### REQ-NF-03: Кэширование
|
||||
|
||||
- LRU-кэш `/api/gpx/fetch`: 64 записи × до 50 МБ = до 3.2 ГБ памяти —
|
||||
**слишком много**. Решение: хранить только treki ≤ 5 МБ, остальные не
|
||||
кэшировать. Корректировка: кэш до 64 записей размером ≤ 5 МБ каждая.
|
||||
- LRU-кэш `/api/gpx/osm/traces`: 256 записей × ≤ 200 КБ JSON ≈ 50 МБ.
|
||||
- Оба кэша — in-memory, не персистентные, теряются при рестарте контейнера.
|
||||
- TTL: 24 часа.
|
||||
- Метрики кэша (`/api/health`): `gpx_fetch_cache_size`, `gpx_osm_cache_size`.
|
||||
|
||||
### REQ-NF-04: Совместимость
|
||||
|
||||
- Браузеры: те же, что ET-006 (Chrome 90+, Firefox 90+, Safari 15+).
|
||||
- Мобильные: input type=url с режимом клавиатуры url.
|
||||
- Backend: Python 3.12, FastAPI, httpx (уже есть), `defusedxml` (новая).
|
||||
|
||||
### REQ-NF-05: UX
|
||||
|
||||
- Во время сетевого запроса показывать индикатор (повторно используем
|
||||
`#gpx-loading` из ET-006).
|
||||
- Кнопка «Найти треки» дизейблится во время запроса.
|
||||
- Все toast-уведомления — через существующий механизм `showToast()` из `gpx.js`.
|
||||
|
||||
## 3. UI-спецификация
|
||||
|
||||
### 3.1 Расширение `#sheet-gpx` — секция «Источники»
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ═══ (handle) │
|
||||
│ 📄 GPX-треки [свернуть]│
|
||||
├─────────────────────────────────────┤
|
||||
│ ИСТОЧНИКИ │
|
||||
│ [📁 Из файла] [🔗 По ссылке] [🌍 Найти рядом] │
|
||||
│ │
|
||||
│ ─ если активна «По ссылке»: ─ │
|
||||
│ ┌──────────────────────────┐ ┌────┐ │
|
||||
│ │https://example.com/...gpx│ │Загр│ │
|
||||
│ └──────────────────────────┘ └────┘ │
|
||||
│ │
|
||||
│ ─ если активна «Найти рядом»: ─ │
|
||||
│ [ Найти треки в этой области карты ]│
|
||||
│ Найдено 5 треков: │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │🌍 Trail in the woods [Показ.] │ │
|
||||
│ │ 12.4 км · автор: user42 [↗] │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │🌍 Без названия [✓ Загр.]│ │
|
||||
│ │ 3.1 км · аноним [↗]│ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ [ Показать ещё ] │
|
||||
├─────────────────────────────────────┤
|
||||
│ ЗАГРУЖЕННЫЕ ТРЕКИ (как в ET-006) │
|
||||
│ 🔴 morning.gpx [✕] │
|
||||
│ 📁 локальный файл │
|
||||
│ 🔵 trail_woods [✕] │
|
||||
│ 🌍 OSM #12345 │
|
||||
│ 🟢 strava-export [✕] │
|
||||
│ 🔗 github.com │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Segmented control «Источники»
|
||||
|
||||
- Контейнер: `<div class="seg-control source-seg" id="source-seg">`.
|
||||
- Кнопки: `<button class="seg-btn" id="source-btn-file|url|nearby">`.
|
||||
- Стилизация — переиспользовать существующий `.seg-control` (как в
|
||||
выборе единиц `unit-seg`).
|
||||
- Поведение: одна активна, остальные неактивны; контент под секцией
|
||||
переключается по data-атрибуту.
|
||||
|
||||
### 3.3 Карточка найденного OSM-трека
|
||||
|
||||
- Контейнер: `<div class="gpx-nearby-card" data-osm-id="...">`.
|
||||
- Структура:
|
||||
```html
|
||||
<div class="gpx-nearby-card">
|
||||
<div class="gnc-icon">🌍</div>
|
||||
<div class="gnc-body">
|
||||
<div class="gnc-title">Trail in the woods</div>
|
||||
<div class="gnc-meta">12.4 км · автор: user42</div>
|
||||
</div>
|
||||
<button class="gnc-import">Показать</button>
|
||||
<a class="gnc-external" href="..." target="_blank" rel="noopener">↗</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3.4 Расширение карточки трека в `#gpx-list`
|
||||
|
||||
Добавить под именем файла строку:
|
||||
|
||||
```html
|
||||
<div class="gpx-source-row">
|
||||
<!-- для kind='file' -->
|
||||
<span>📁 локальный файл</span>
|
||||
<!-- для kind='url' -->
|
||||
<span>🔗 <span class="gpx-host">github.com</span></span>
|
||||
<!-- для kind='osm' -->
|
||||
<a href="https://www.openstreetmap.org/.../traces/12345"
|
||||
target="_blank" rel="noopener">🌍 OSM #12345</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 4. Данные
|
||||
|
||||
### 4.1 Формат OSM Public GPS Traces API
|
||||
|
||||
OSM возвращает GPX 1.0:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.0" creator="OpenStreetMap.org" xmlns="http://www.topografix.com/GPX/1/0">
|
||||
<trk>
|
||||
<name>Anonymous tracks</name>
|
||||
<trkseg>
|
||||
<trkpt lat="55.7558" lon="37.6173">
|
||||
<time>2024-05-01T08:00:00Z</time>
|
||||
</trkpt>
|
||||
...
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>
|
||||
```
|
||||
|
||||
`gpx_id` атрибут точек официально устарел; вместо группировки треков по
|
||||
gpx_id отдаём весь bbox-ответ как «Публичные треки этой области (N точек)»
|
||||
— **единая карточка**, импорт всей выборки как одного трека.
|
||||
Метаданные индивидуальных треков (user, name) недоступны через
|
||||
`trackpoints` endpoint без дополнительного запроса.
|
||||
|
||||
**Уточнение требования REQ-F-05/F-06** (исходя из реального API):
|
||||
|
||||
- Список найденных «треков» — это страницы trackpoints (page 0, 1, 2…).
|
||||
- Карточка отображает: page N, количество точек, длину, bbox-центр.
|
||||
- Импорт = загрузить эту страницу как один GPX-трек.
|
||||
- Кнопка «Показать ещё» → следующая страница.
|
||||
|
||||
Это упрощает реализацию и соответствует ограничениям OSM API.
|
||||
|
||||
### 4.2 Внутренняя модель — расширение
|
||||
|
||||
```javascript
|
||||
window.gpxTracks = [
|
||||
{
|
||||
// существующие поля ET-006
|
||||
id: 'gpx-1716336000000',
|
||||
filename: 'trail_woods',
|
||||
color: '#3cb44b',
|
||||
tracks: [...],
|
||||
waypoints: [...],
|
||||
sourceId: 'gpx-source-...',
|
||||
layerId: 'gpx-layer-...',
|
||||
waypointLayerId: 'gpx-wpt-...',
|
||||
// новое поле ET-008
|
||||
source: {
|
||||
kind: 'file' | 'url' | 'osm',
|
||||
url: 'https://...', // null для kind='file'
|
||||
osm_page: 0, // только для kind='osm'
|
||||
osm_bbox: [w, s, e, n] // только для kind='osm'
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
## 5. Файловая структура изменений
|
||||
|
||||
```
|
||||
src/api/
|
||||
├── main.py # + 2 эндпоинта, импорт нового модуля
|
||||
├── gpx_proxy.py # НОВЫЙ: SSRF-валидация, fetch, кэш
|
||||
├── osm_traces.py # НОВЫЙ: OSM trackpoints клиент, парсинг
|
||||
├── requirements.txt # + defusedxml
|
||||
|
||||
src/web/
|
||||
├── index.html # + секция «Источники» в #sheet-gpx
|
||||
├── gpx.js # + URL-импорт, OSM-поиск, расширение модели
|
||||
├── app.css # + стили .source-seg, .gpx-nearby-card, .gpx-source-row
|
||||
|
||||
tests/
|
||||
├── api/test_gpx_proxy.py # НОВЫЙ
|
||||
├── api/test_osm_traces.py # НОВЫЙ
|
||||
├── web/gpx.test.js # + тесты на URL/OSM источники
|
||||
|
||||
docs/work-items/ET-008/
|
||||
├── 06-adr/
|
||||
│ ├── ADR-001-ssrf-protection.md
|
||||
│ └── ADR-002-osm-trackpoints-aggregation.md
|
||||
```
|
||||
|
||||
## 6. Алгоритмы
|
||||
|
||||
### 6.1 SSRF-защита `/api/gpx/fetch`
|
||||
|
||||
```python
|
||||
def is_safe_url(url: str) -> bool:
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme not in ('http', 'https'):
|
||||
return False
|
||||
try:
|
||||
infos = socket.getaddrinfo(parsed.hostname, None)
|
||||
except socket.gaierror:
|
||||
return False
|
||||
for info in infos:
|
||||
ip = ipaddress.ip_address(info[4][0])
|
||||
if not ip.is_global or ip.is_loopback or ip.is_private:
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
При следовании редиректам — повторная валидация хоста на каждом шаге.
|
||||
|
||||
### 6.2 Bbox area check
|
||||
|
||||
```python
|
||||
def bbox_area_deg2(w, s, e, n):
|
||||
return abs(e - w) * abs(n - s)
|
||||
|
||||
if bbox_area_deg2(*bbox) > 0.25:
|
||||
raise HTTPException(400, "bbox too large")
|
||||
```
|
||||
|
||||
### 6.3 Кэш-ключ для bbox
|
||||
|
||||
Округление до 4 знаков (≈ 11 метров на экваторе):
|
||||
```python
|
||||
key = (round(w, 4), round(s, 4), round(e, 4), round(n, 4), page)
|
||||
```
|
||||
|
||||
Это обеспечивает попадание в кэш при незначительном движении карты.
|
||||
|
||||
## 7. Взаимодействие с существующими модулями
|
||||
|
||||
- **ET-006 `gpx.js`** — расширяем, не переписываем. Существующие функции
|
||||
(`parseGpx`, `addGpxTrack`, `rebuildGpxOverlays`) остаются. Добавляются:
|
||||
`importGpxFromUrl(url)`, `findOsmTracesInView()`,
|
||||
`importOsmTrace(osm_url)`.
|
||||
- **`units.js`** — используется для форматирования длины треков в списке.
|
||||
- **`#sheet-gpx`** — единственный sheet для всех источников. Никаких
|
||||
новых sheet не создаётся.
|
||||
- **`#toolbar`** — кнопка `#tb-gpx` уже открывает `#sheet-gpx`. Не меняется.
|
||||
- **`/api/health`** — расширить выдачей размеров кэшей (REQ-NF-03).
|
||||
275
docs/work-items/ET-008/03-acceptance-criteria.md
Normal file
275
docs/work-items/ET-008/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,275 @@
|
||||
---
|
||||
type: acceptance-criteria
|
||||
work_item_id: ET-008
|
||||
title: "AC: GPS-треки с публичных платформ на карте"
|
||||
version: 1
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
|
||||
# Acceptance Criteria — ET-008: GPS-треки с публичных платформ на карте
|
||||
|
||||
## AC-01: Секция «Источники» в `#sheet-gpx`
|
||||
|
||||
```gherkin
|
||||
Feature: Переключатель источников треков
|
||||
|
||||
Scenario: Открытие GPX-панели
|
||||
Given пользователь нажимает кнопку GPX в нижнем тулбаре
|
||||
Then открывается панель #sheet-gpx
|
||||
And в верхней части видна секция «Источники» с тремя кнопками: «Из файла», «По ссылке», «Найти рядом»
|
||||
And по умолчанию активна кнопка «Из файла»
|
||||
|
||||
Scenario: Переключение на «По ссылке»
|
||||
Given панель #sheet-gpx открыта
|
||||
When пользователь нажимает кнопку «По ссылке»
|
||||
Then кнопка «По ссылке» становится активной
|
||||
And отображается поле ввода URL и кнопка «Загрузить»
|
||||
And контент других вкладок скрыт
|
||||
|
||||
Scenario: Переключение на «Найти рядом»
|
||||
Given панель #sheet-gpx открыта
|
||||
When пользователь нажимает кнопку «Найти рядом»
|
||||
Then отображается кнопка «Найти треки в этой области карты»
|
||||
```
|
||||
|
||||
## AC-02: Импорт по URL — успешный сценарий
|
||||
|
||||
```gherkin
|
||||
Feature: Загрузка GPX по прямой ссылке
|
||||
|
||||
Scenario: Валидная публичная ссылка
|
||||
Given активна вкладка «По ссылке»
|
||||
When пользователь вставляет https://example.com/test-track.gpx (валидный, 1 МБ)
|
||||
And нажимает «Загрузить»
|
||||
Then показывается индикатор загрузки
|
||||
And через ≤ 5 сек трек появляется на карте
|
||||
And карта выполняет fit bounds
|
||||
And трек добавляется в список #gpx-list
|
||||
And в карточке трека отображается «🔗 example.com»
|
||||
|
||||
Scenario: Загрузка по Enter
|
||||
Given активна вкладка «По ссылке»
|
||||
When пользователь вставляет URL и нажимает Enter
|
||||
Then загрузка начинается без клика по кнопке
|
||||
```
|
||||
|
||||
## AC-03: Импорт по URL — ошибки
|
||||
|
||||
```gherkin
|
||||
Feature: Обработка ошибок URL-импорта
|
||||
|
||||
Scenario: Невалидный URL (схема)
|
||||
Given активна вкладка «По ссылке»
|
||||
When пользователь вставляет ftp://example.com/file.gpx
|
||||
Then показывается toast «Невалидная ссылка»
|
||||
And запрос на бэкенд не отправляется
|
||||
|
||||
Scenario: Приватный IP
|
||||
Given пользователь вставляет http://192.168.1.1/file.gpx
|
||||
Then бэкенд возвращает 400
|
||||
And показывается toast «Эта ссылка недоступна»
|
||||
|
||||
Scenario: Несуществующий файл
|
||||
Given URL ведёт на 404
|
||||
Then показывается toast «Файл не найден по этой ссылке»
|
||||
|
||||
Scenario: Файл больше 50 МБ
|
||||
Given URL ведёт на GPX > 50 МБ
|
||||
Then показывается toast «Файл слишком большой (макс. 50 МБ)»
|
||||
|
||||
Scenario: Не GPX (HTML по ссылке)
|
||||
Given URL отдаёт HTML-страницу
|
||||
Then показывается toast «По этой ссылке не GPX-файл»
|
||||
|
||||
Scenario: Внешний сервер не отвечает
|
||||
Given внешний сервер таймаутит
|
||||
Then показывается toast «Сервер не отвечает, попробуйте позже»
|
||||
```
|
||||
|
||||
## AC-04: Поиск OSM-треков
|
||||
|
||||
```gherkin
|
||||
Feature: Поиск публичных треков OSM в видимой области
|
||||
|
||||
Scenario: Успешный поиск с результатами
|
||||
Given активна вкладка «Найти рядом»
|
||||
And карта показывает область с публичными треками
|
||||
When пользователь нажимает «Найти треки в этой области карты»
|
||||
Then показывается индикатор загрузки
|
||||
And через ≤ 3 сек появляется список найденных треков
|
||||
And каждая карточка содержит: иконку OSM, описание (page N), длину в км, кнопку «Показать», ссылку «↗»
|
||||
|
||||
Scenario: Пустая область
|
||||
Given карта показывает область без публичных треков
|
||||
When пользователь нажимает «Найти треки»
|
||||
Then отображается inline-сообщение «В этой области нет публичных GPS-треков»
|
||||
|
||||
Scenario: Слишком большая область
|
||||
Given карта показывает область с bbox > 0.25 deg²
|
||||
When пользователь нажимает «Найти треки»
|
||||
Then показывается toast «Слишком большая область, увеличьте zoom»
|
||||
And запрос на бэкенд не отправляется (или возвращается 400)
|
||||
|
||||
Scenario: Пагинация
|
||||
Given поиск вернул has_more=true
|
||||
Then в конце списка отображается кнопка «Показать ещё»
|
||||
When пользователь нажимает «Показать ещё»
|
||||
Then дозагружаются результаты следующей страницы
|
||||
And они добавляются в конец списка
|
||||
```
|
||||
|
||||
## AC-05: Импорт OSM-трека
|
||||
|
||||
```gherkin
|
||||
Feature: Импорт выбранного OSM-трека на карту
|
||||
|
||||
Scenario: Импорт по кнопке «Показать»
|
||||
Given найдено 3 OSM-трека в списке
|
||||
When пользователь нажимает «Показать» у первого трека
|
||||
Then показывается индикатор загрузки
|
||||
And через ≤ 5 сек трек появляется на карте
|
||||
And карта выполняет fit bounds
|
||||
And трек добавляется в #gpx-list
|
||||
And в карточке трека отображается «🌍 OSM #...» (кликабельная ссылка)
|
||||
And карточка в #gpx-nearby-results получает индикатор «✓ Загружен»
|
||||
|
||||
Scenario: Повторный импорт того же трека
|
||||
Given OSM-трек уже импортирован
|
||||
When пользователь нажимает «Показать» у этой же карточки в найденных
|
||||
Then показывается toast «Уже загружен»
|
||||
And новый трек НЕ добавляется
|
||||
|
||||
Scenario: Внешняя ссылка на osm.org
|
||||
Given в карточке найденного трека есть кнопка «↗»
|
||||
When пользователь нажимает «↗»
|
||||
Then открывается новая вкладка с страницей трека на openstreetmap.org
|
||||
```
|
||||
|
||||
## AC-06: Отображение источника в карточке трека
|
||||
|
||||
```gherkin
|
||||
Feature: Источник трека виден пользователю
|
||||
|
||||
Scenario: Локальный файл (ET-006 совместимость)
|
||||
Given загружен GPX из локального файла
|
||||
Then в карточке трека под именем файла отображается «📁 локальный файл»
|
||||
|
||||
Scenario: Загружен по URL
|
||||
Given загружен GPX по ссылке https://github.com/user/repo/track.gpx
|
||||
Then в карточке трека отображается «🔗 github.com»
|
||||
|
||||
Scenario: Загружен из OSM
|
||||
Given загружен OSM-трек page 0
|
||||
Then в карточке трека отображается ссылка «🌍 OSM #..» которая ведёт на osm.org
|
||||
```
|
||||
|
||||
## AC-07: Кэширование на бэкенде
|
||||
|
||||
```gherkin
|
||||
Feature: Серверный кэш ответов внешних API
|
||||
|
||||
Scenario: Повторный запрос URL из кэша
|
||||
Given URL запрашивался менее 24 часов назад
|
||||
When клиент делает повторный GET /api/gpx/fetch?url=...
|
||||
Then ответ возвращается с заголовком X-Cache: HIT
|
||||
And время ответа ≤ 50 мс
|
||||
And внешний запрос НЕ выполняется
|
||||
|
||||
Scenario: Cache miss
|
||||
Given URL запрашивается впервые
|
||||
Then выполняется внешний запрос
|
||||
And ответ возвращается с X-Cache: MISS
|
||||
And следующий запрос того же URL — HIT
|
||||
|
||||
Scenario: Повторный bbox-поиск из кэша
|
||||
Given bbox запрашивался менее 24 часов назад
|
||||
When клиент делает повторный GET /api/gpx/osm/traces?bbox=...
|
||||
Then ответ из кэша
|
||||
And внешний запрос к OSM API НЕ выполняется
|
||||
```
|
||||
|
||||
## AC-08: Безопасность
|
||||
|
||||
```gherkin
|
||||
Feature: SSRF protection
|
||||
|
||||
Scenario: Прямой запрос к loopback
|
||||
When клиент шлёт GET /api/gpx/fetch?url=http://127.0.0.1/data
|
||||
Then бэкенд возвращает 400
|
||||
And никакого запроса к 127.0.0.1 не делается
|
||||
|
||||
Scenario: Запрос к приватной подсети
|
||||
When клиент шлёт URL ведущий на 10.0.0.1, 192.168.x.x, 172.16.x.x
|
||||
Then бэкенд возвращает 400
|
||||
|
||||
Scenario: Редирект на приватный IP
|
||||
Given внешний URL отдаёт 302 redirect на http://127.0.0.1/...
|
||||
When клиент шлёт GET /api/gpx/fetch?url=<external>
|
||||
Then редирект проверяется повторно и блокируется
|
||||
And бэкенд возвращает 400
|
||||
|
||||
Scenario: Запрещённая схема
|
||||
When клиент шлёт URL с file:// или gopher://
|
||||
Then бэкенд возвращает 400
|
||||
|
||||
Scenario: Размер ответа превышает лимит
|
||||
Given внешний сервер начинает стримить файл > 50 МБ
|
||||
Then бэкенд прерывает соединение
|
||||
And возвращает 413
|
||||
```
|
||||
|
||||
## AC-09: Совместимость с ET-006
|
||||
|
||||
```gherkin
|
||||
Feature: Локальные и удалённые треки в одной модели
|
||||
|
||||
Scenario: Смешанный список
|
||||
Given загружен 1 локальный файл, 1 по URL, 1 из OSM
|
||||
Then в #gpx-list отображаются 3 карточки
|
||||
And каждая имеет уникальный цвет из палитры
|
||||
And каждая имеет свой индикатор источника
|
||||
And любую можно активировать, удалить, увидеть профиль высот
|
||||
|
||||
Scenario: Сохранение при смене темы
|
||||
Given на карте 3 трека разных источников
|
||||
When пользователь переключает тёмную тему
|
||||
Then все 3 трека остаются на карте
|
||||
And источники в карточках сохраняются
|
||||
And статистика и профиль активного трека сохраняются
|
||||
|
||||
Scenario: Сохранение при переключении слоёв рельефа
|
||||
Given на карте 3 трека разных источников
|
||||
When пользователь включает hillshade
|
||||
Then все 3 трека видны поверх hillshade
|
||||
```
|
||||
|
||||
## AC-10: Метрики кэша в `/api/health`
|
||||
|
||||
```gherkin
|
||||
Feature: Наблюдаемость кэшей
|
||||
|
||||
Scenario: Размер кэшей в health-эндпоинте
|
||||
When клиент шлёт GET /api/health
|
||||
Then ответ содержит поля gpx_fetch_cache_size и gpx_osm_cache_size
|
||||
And значения — целые числа ≥ 0
|
||||
```
|
||||
|
||||
## AC-11: Производительность
|
||||
|
||||
```gherkin
|
||||
Feature: Лимиты времени отклика
|
||||
|
||||
Scenario: OSM bbox запрос с кэш-хитом
|
||||
Given bbox в кэше
|
||||
Then GET /api/gpx/osm/traces возвращается за ≤ 50 мс (p95)
|
||||
|
||||
Scenario: URL-импорт малого файла (1 МБ)
|
||||
Then GET /api/gpx/fetch для 1 МБ файла завершается за ≤ 2 сек
|
||||
|
||||
Scenario: OSM bbox запрос без кэша
|
||||
Then GET /api/gpx/osm/traces без кэша возвращается за ≤ 3 сек (p95)
|
||||
```
|
||||
424
docs/work-items/ET-008/04-test-plan.yaml
Normal file
424
docs/work-items/ET-008/04-test-plan.yaml
Normal file
@@ -0,0 +1,424 @@
|
||||
---
|
||||
type: test-plan
|
||||
work_item_id: ET-008
|
||||
title: "Test Plan: GPS-треки с публичных платформ на карте"
|
||||
version: 1
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
|
||||
test_suites:
|
||||
|
||||
- name: unit-gpx-proxy-validation
|
||||
type: unit
|
||||
description: "SSRF-валидация URL в gpx_proxy.is_safe_url()"
|
||||
cases:
|
||||
- id: U-01
|
||||
name: "Принимает валидный публичный HTTPS URL"
|
||||
input: "https://example.com/track.gpx (резолвится в публичный IP)"
|
||||
expected: "is_safe_url() возвращает True"
|
||||
|
||||
- id: U-02
|
||||
name: "Отклоняет схему ftp://"
|
||||
input: "ftp://example.com/track.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
|
||||
- id: U-03
|
||||
name: "Отклоняет схему file://"
|
||||
input: "file:///etc/passwd"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
|
||||
- id: U-04
|
||||
name: "Отклоняет loopback IP"
|
||||
input: "http://127.0.0.1/x.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
|
||||
- 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)"
|
||||
|
||||
- id: U-10
|
||||
name: "Отклоняет хост, который не резолвится"
|
||||
input: "http://nonexistent-host-xyz-12345.invalid/x.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
|
||||
- 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"
|
||||
cases:
|
||||
- id: U-30
|
||||
name: "TTL истёк → cache miss"
|
||||
input: "Положить запись с TTL 1 сек, ждать 2 сек, запросить"
|
||||
expected: "Возвращает None (или вызывает loader)"
|
||||
|
||||
- id: U-31
|
||||
name: "LRU вытеснение при переполнении"
|
||||
input: "Заполнить кэш max=4 записями, добавить 5-ю"
|
||||
expected: "Первая (LRU) запись вытеснена"
|
||||
|
||||
- id: U-32
|
||||
name: "Округление bbox-ключа до 4 знаков"
|
||||
input: "bbox=[37.6172999, 55.7558001, ...] и bbox=[37.6173, 55.7558, ...]"
|
||||
expected: "Один и тот же кэш-ключ → cache hit"
|
||||
|
||||
- id: U-33
|
||||
name: "URL > 5 МБ не кэшируется"
|
||||
input: "Положить запись размером 6 МБ"
|
||||
expected: "Запись не попадает в кэш (cache.get → None)"
|
||||
|
||||
- name: unit-osm-parser
|
||||
type: unit
|
||||
description: "Парсинг OSM trackpoints GPX → JSON"
|
||||
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}]}"
|
||||
|
||||
- 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)"
|
||||
|
||||
- id: U-42
|
||||
name: "Пустой GPX (нет trkpt)"
|
||||
input: "GPX без точек"
|
||||
expected: "JSON: {tracks: [], total_points: 0}"
|
||||
|
||||
- id: U-43
|
||||
name: "Защита от XXE (defusedxml)"
|
||||
input: "GPX с DOCTYPE и внешней entity"
|
||||
expected: "Парсер не выполняет загрузку внешней entity (или бросает ошибку)"
|
||||
|
||||
- name: unit-web-gpx-source
|
||||
type: unit
|
||||
description: "Расширение модели window.gpxTracks полем source"
|
||||
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'}"
|
||||
|
||||
- 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:'...'}"
|
||||
|
||||
- id: U-52
|
||||
name: "Обратная совместимость: трек без source читается как 'file'"
|
||||
input: "window.gpxTracks[0] без поля source"
|
||||
expected: "renderSourceRow() возвращает '📁 локальный файл'"
|
||||
|
||||
- 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: integration-gpx-fetch
|
||||
type: integration
|
||||
description: "GET /api/gpx/fetch — прокси с реальным HTTP"
|
||||
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"
|
||||
|
||||
- id: I-02
|
||||
name: "Повторный запрос — cache hit"
|
||||
input: "GET тот же URL"
|
||||
expected: "200, X-Cache: HIT, время ≤ 50 мс"
|
||||
|
||||
- id: I-03
|
||||
name: "Отклонение приватного IP"
|
||||
input: "GET /api/gpx/fetch?url=http://127.0.0.1/x.gpx"
|
||||
expected: "400, JSON {error: ...}"
|
||||
|
||||
- id: I-04
|
||||
name: "Отклонение редиректа на приватный IP"
|
||||
input: "Внешний URL → 302 на http://127.0.0.1/x.gpx"
|
||||
expected: "400, JSON {error: ...}"
|
||||
|
||||
- id: I-05
|
||||
name: "Внешний 404"
|
||||
input: "URL ведёт на несуществующий путь"
|
||||
expected: "404, JSON {error: ...}"
|
||||
|
||||
- id: I-06
|
||||
name: "Лимит размера 50 МБ"
|
||||
input: "Mock-сервер стримит 60 МБ"
|
||||
expected: "413, соединение прервано до конца"
|
||||
|
||||
- id: I-07
|
||||
name: "Таймаут"
|
||||
input: "Mock-сервер ничего не отвечает"
|
||||
expected: "504 после 15 сек"
|
||||
|
||||
- id: I-08
|
||||
name: "URL > 5 МБ не попадает в кэш"
|
||||
input: "Запросить URL с ответом 6 МБ дважды"
|
||||
expected: "Второй запрос: X-Cache: MISS, внешний запрос повторно выполнен"
|
||||
|
||||
- name: integration-osm-traces
|
||||
type: integration
|
||||
description: "GET /api/gpx/osm/traces — OSM API клиент"
|
||||
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"
|
||||
|
||||
- id: I-21
|
||||
name: "Bbox > 0.25 deg² → 400"
|
||||
input: "bbox=37,55,38,56"
|
||||
expected: "400, error 'bbox too large'"
|
||||
|
||||
- id: I-22
|
||||
name: "OSM API недоступен → 502"
|
||||
input: "OSM mock возвращает 500"
|
||||
expected: "502, JSON error"
|
||||
|
||||
- id: I-23
|
||||
name: "Cache hit на повторный bbox"
|
||||
input: "Тот же bbox дважды"
|
||||
expected: "Второй запрос: внешний запрос НЕ выполнен, ответ из кэша"
|
||||
|
||||
- id: I-24
|
||||
name: "Пустой bbox → пустой список"
|
||||
input: "bbox в океане"
|
||||
expected: "200, tracks=[], has_more=false"
|
||||
|
||||
- id: I-25
|
||||
name: "Пагинация"
|
||||
input: "page=0 возвращает has_more=true, page=1 возвращает следующие"
|
||||
expected: "Корректное смещение, оба запроса валидны"
|
||||
|
||||
- name: integration-health-metrics
|
||||
type: integration
|
||||
description: "Метрики кэшей в /api/health"
|
||||
cases:
|
||||
- id: I-30
|
||||
name: "Health возвращает размеры кэшей"
|
||||
input: "GET /api/health"
|
||||
expected: "JSON содержит gpx_fetch_cache_size, gpx_osm_cache_size (числа ≥ 0)"
|
||||
|
||||
- id: I-31
|
||||
name: "Счётчики растут после запросов"
|
||||
input: "После N успешных fetch и M osm_traces запросов"
|
||||
expected: "Размеры кэшей отражают добавленные записи"
|
||||
|
||||
- name: e2e-url-import
|
||||
type: e2e
|
||||
description: "Импорт GPX по ссылке — полный сценарий"
|
||||
cases:
|
||||
- id: E-01
|
||||
name: "URL-импорт валидного трека"
|
||||
steps:
|
||||
- "Открыть приложение"
|
||||
- "Нажать кнопку GPX в нижнем тулбаре"
|
||||
- "Переключиться на вкладку «По ссылке»"
|
||||
- "Вставить URL валидного GPX (тестовый mock)"
|
||||
- "Нажать «Загрузить»"
|
||||
- "Убедиться: индикатор показан, через ≤ 5 сек трек на карте"
|
||||
- "Убедиться: в #gpx-list появилась карточка с источником «🔗 host»"
|
||||
- "Кликнуть на трек → отображается статистика и профиль высот"
|
||||
|
||||
- id: E-02
|
||||
name: "URL-импорт по Enter"
|
||||
steps:
|
||||
- "Активировать «По ссылке»"
|
||||
- "Вставить URL, нажать Enter"
|
||||
- "Убедиться: трек загружен (как при клике)"
|
||||
|
||||
- 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
|
||||
type: e2e
|
||||
description: "Поиск и импорт OSM треков"
|
||||
cases:
|
||||
- id: E-10
|
||||
name: "Поиск треков в области и импорт"
|
||||
steps:
|
||||
- "Открыть приложение, отзумиться к области Москвы (zoom 12)"
|
||||
- "Открыть #sheet-gpx, активировать «Найти рядом»"
|
||||
- "Нажать «Найти треки в этой области карты»"
|
||||
- "Убедиться: индикатор, потом список карточек"
|
||||
- "Нажать «Показать» у первой карточки"
|
||||
- "Убедиться: трек появился на карте, fit bounds"
|
||||
- "Убедиться: карточка в найденных получила «✓ Загружен»"
|
||||
- "Убедиться: в #gpx-list появилась карточка с «🌍 OSM #...»"
|
||||
|
||||
- id: E-11
|
||||
name: "Слишком большая область"
|
||||
steps:
|
||||
- "Отзумиться на всю Россию"
|
||||
- "Активировать «Найти рядом»"
|
||||
- "Нажать «Найти»"
|
||||
- "Убедиться: toast «Слишком большая область, увеличьте zoom»"
|
||||
|
||||
- id: E-12
|
||||
name: "Пустая область"
|
||||
steps:
|
||||
- "Перейти к области без треков (океан)"
|
||||
- "Активировать «Найти рядом»"
|
||||
- "Нажать «Найти»"
|
||||
- "Убедиться: сообщение «В этой области нет публичных GPS-треков»"
|
||||
|
||||
- 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
|
||||
type: e2e
|
||||
description: "Совместимость трёх источников в одной сессии"
|
||||
cases:
|
||||
- id: E-20
|
||||
name: "3 трека разных источников"
|
||||
steps:
|
||||
- "Загрузить 1 локальный файл"
|
||||
- "Загрузить 1 по URL"
|
||||
- "Загрузить 1 из OSM"
|
||||
- "Убедиться: 3 карточки в #gpx-list, разные цвета, разные источники"
|
||||
- "Удалить URL-трек"
|
||||
- "Убедиться: 2 трека на карте, корректные источники"
|
||||
|
||||
- id: E-21
|
||||
name: "Сохранение при смене темы"
|
||||
steps:
|
||||
- "Загрузить 3 трека разных источников"
|
||||
- "Переключить тёмную тему"
|
||||
- "Убедиться: все 3 трека на карте"
|
||||
- "Убедиться: источники в карточках сохранены"
|
||||
|
||||
- id: E-22
|
||||
name: "Сохранение при включении hillshade"
|
||||
steps:
|
||||
- "Загрузить 3 трека"
|
||||
- "Включить hillshade"
|
||||
- "Убедиться: все 3 трека видны поверх hillshade"
|
||||
|
||||
- name: e2e-cache
|
||||
type: e2e
|
||||
description: "Поведение кэша через API"
|
||||
cases:
|
||||
- id: E-30
|
||||
name: "Кэш URL-fetch снижает время"
|
||||
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"
|
||||
steps:
|
||||
- "Сделать N запросов /api/gpx/fetch"
|
||||
- "GET /api/health"
|
||||
- "Убедиться: gpx_fetch_cache_size == N (или min(N, лимит))"
|
||||
|
||||
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"
|
||||
|
||||
test_environment:
|
||||
mock_servers:
|
||||
- "Mock HTTP-сервер для /api/gpx/fetch тестов (отдаёт фиксированные ответы)"
|
||||
- "Mock OSM API для /api/gpx/osm/traces тестов"
|
||||
fixtures_dir: "tests/fixtures/gpx-public/"
|
||||
notes:
|
||||
- "OSM API в e2e тестах должен мокироваться, чтобы не зависеть от внешней доступности"
|
||||
- "Для нагрузочных тестов кэша использовать pytest-benchmark"
|
||||
395
docs/work-items/ET-008/04b-ui-test-cases.md
Normal file
395
docs/work-items/ET-008/04b-ui-test-cases.md
Normal file
@@ -0,0 +1,395 @@
|
||||
---
|
||||
type: ui-test-cases
|
||||
work_item_id: ET-008
|
||||
title: "UI Test Cases: GPS-треки с публичных платформ"
|
||||
version: 1
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
|
||||
# UI Test Cases — ET-008: GPS-треки с публичных платформ
|
||||
|
||||
Базовый URL: `https://openclaw.mva154.duckdns.org/enduro/`
|
||||
|
||||
Все тесты проверяют появление и поведение секции «Источники» в
|
||||
`#sheet-gpx`, импорта по URL и поиска OSM-треков. Внешние сетевые
|
||||
запросы в test-окружении мокаются (см. test-plan).
|
||||
|
||||
Селекторы (новые, добавляются 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`.
|
||||
|
||||
---
|
||||
|
||||
### 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 — Совместимость со спутниковой подложкой
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#base-btn-satellite"
|
||||
6. wait: 5000
|
||||
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 не конфликтует со спутником визуально."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-16 — Сохранение треков при переключении тёмной темы
|
||||
|
||||
- тип: 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: "#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), визуально отличима от основной кнопки «Показать»."
|
||||
Reference in New Issue
Block a user