feat(gps-tracks): GPX download from public track popup (ET-011) #21
Reference in New Issue
Block a user
Delete Branch "feature/ET-011-popup-enduro-trails"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
GET /api/gps-tracks/{track_id}/downloadотдаёт GPX 1.1 сContent-Disposition: attachment; filename*=UTF-8''…(RFC 5987).download_allowedper source вconfig/gps_sources.yaml(default-deny, MVP whitelist =osm).Реализовано
src/api/gps_tracks/export.py—build_gpx(xml.etree.ElementTree, без runtime-deps) +safe_filename.src/api/gps_tracks/endpoint.py— route с проверками 400/403/404/413 (cap 200 000 точек) и логированием.src/api/gps_tracks/config.py—load_download_allowed_sources()(default-deny при отсутствии конфига →{"osm"}).src/api/main.py— пробрасываетGPS_SOURCES_CONFIG_PATHвcreate_gps_router.src/web/gps_tracks.js— popup-кнопка,_downloadPublicTrack,_parseFilenameFromCD,_handleDownloadError.src/web/app.css— стиль кнопки.config/gps_sources.yaml— полеdownload_allowedна всех 4 источниках (толькоosm: true).Тесты
test_gps_tracks_gpx_builder.py(UT-01/02/03/05, валидация по GPX 1.1 XSD).test_gps_tracks_filename.py(UT-04: UTF-8, запрещённые символы, fallback).test_gps_tracks_download.py(IT-01..08 + ANY-rule license + default-deny).tests/fixtures/gpx-1.1/gpx.xsd— официальная схема topografix (для XSD-валидации).Покрытие AC
Test plan
main.pyне трогал)pytest tests/— 200 passedADR-014 (gpx-download-endpoint), ADR-015 (source-redistribution-policy). Refs: ET-011.
🤖 Generated with Claude Code
Реализация ET-011: кнопка «Скачать GPX» в popup публичного GPS-трека и новый эндпоинт GET /api/gps-tracks/{track_id}/download (GPX 1.1 + Content-Disposition с UTF-8 именем по RFC 5987). Реэкспорт защищён per-source флагом `download_allowed` в `config/gps_sources.yaml` (default-deny, MVP whitelist = `osm`). Backend: - `src/api/gps_tracks/export.py` — чистый stdlib-builder GPX 1.1 (`build_gpx`) + санитизация имени файла (`safe_filename`, RFC 5987). - `src/api/gps_tracks/endpoint.py` — новый route с проверками 400 / 403 / 404 / 413; cap 200 000 точек (REQ-NF-02). - `src/api/gps_tracks/config.py` — `load_download_allowed_sources()` читает YAML, default-deny при отсутствии поля; fallback на `{"osm"}` при отсутствии конфига. - `src/api/main.py` — пробрасывает `GPS_SOURCES_CONFIG_PATH` в router. Frontend: - `src/web/gps_tracks.js` — кнопка в `_renderTrackPopupHtml`, обработчик `_downloadPublicTrack` (fetch + Blob + a.download — тот же паттерн, что в `app.js::downloadGPX`, R-1 митигирован), парсер `_parseFilenameFromCD` для RFC 5987, маппинг ошибок `_handleDownloadError` (403/404/413/5xx → showToast). - `src/web/app.css` — стиль кнопки, 32×32 CSS px (REQ-NF-04). Тесты: - 13 unit для GPX-builder (UT-01/02/03/05; XSD-валидация против `tests/fixtures/gpx-1.1/gpx.xsd`). - 10 unit для `safe_filename` (UT-04). - 11 integration для download-эндпоинта (IT-01..08 + ANY-rule license check + default-deny без конфига). ADR-014 (gpx-download-endpoint), ADR-015 (source-redistribution-policy). Refs: ET-011 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>CI failures на feature/ET-011 были вызваны двумя проблемами: 1. ruff `E402 Module level import not at top of file` × 10 в src/api/main.py: - 9 ошибок от ET-008 (GPS_TRACKS_DB_PATH между импортами) + 1 новая от ET-011 (`from src.api.gps_tracks.endpoint import ...` после определения `app`). Перенёс все импорты наверх; константы GPS_TRACKS_DB_PATH и GPS_SOURCES_CONFIG_PATH теперь сразу после import-блока, а создание router-а остаётся в нижней части файла (зависит от `app`). 2. pyproject.toml не объявлял runtime-deps, которые реально импортируются в src/ (defusedxml, pyyaml) и в тестах (lxml). Dockerfile брал их из src/api/requirements.txt, но CI jobs `lint`/`test` ставят `.[dev]` — поэтому `pytest tests/` падал на ModuleNotFoundError при коллекции тестов из ET-008/ET-009/ET-011. Добавил недостающие пины в pyproject (defusedxml/pyyaml в основные deps, lxml — только в dev, нужен для XSD-валидации в test_gps_tracks_download/_gpx_builder). Проверено локально в чистом venv после `pip install .[dev]`: - `ruff check src/` → All checks passed - `pytest tests/` → 200 passed, 2 deselected Refs: ET-011 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Закрывает findings из docs/work-items/ET-011/12-review.md (REQUEST_CHANGES, попытка 3/3): P1-01 — добавлены поведенческие JS unit-тесты UI download-flow - tests/web/track_download.test.js — 28 кейсов (node --test): • _parseFilenameFromCD — RFC 5987 приоритет, plain fallback, битый percent-encoding, null/empty (REQ-F-05.2, AC-2 UI) • _handleDownloadError — 400/403/404/413/5xx тосты, defensive при отсутствии showToast, поддержка flat (ADR-015 §G) и legacy wrapped 403-форм (REQ-F-05.4, AC-7 UI) • _renderTrackPopupHtml — наличие кнопки, aria-label «Скачать GPX», data-track-id, отсутствие при невалидном id, регрессия прочих полей (REQ-F-01, AC-1) - tests/web/test_track_download.py — pytest-обёртка (статические проверки + запуск Node-раннера), исполняется в обычном pytest tests/ - 04b-ui-test-cases.md: AC-13 (mobile-bbox) явно маркирован как MANUAL release-smoke (Playwright-раннер в проекте не настроен; альтернатива согласована reviewer'ом в P1-01). P2-01 — устранено расхождение «doc vs runtime» по контракту 403 - endpoint.py: HTTPException(detail={...}) → JSONResponse(content={...}), чтобы FastAPI не оборачивал dict во второй слой «detail». Контракт теперь совпадает с ADR-015 §G и ADR-014 §6: {"detail":"source_forbidden","external_urls":[...]} - test_gps_tracks_download.py IT-05: упрощено — body уже плоский, без двухуровневого `body.get("detail", body)` workaround. - gps_tracks.js::_handleDownloadError: flat-форма стала приоритетной, wrapped-форма оставлена как defensive fallback (с комментарием). Регрессия: 89/89 API-тестов + 24/24 предыдущих JS-тестов + 28 новых JS-тестов download-flow проходят. ruff check — clean. Refs: ET-011 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>