--- type: tech-risks work_item_id: ET-011 title: "Технические риски — ET-011: Скачивание трека из popup" version: 1 status: approved created_at: 2026-06-03 authors: - "agent:architect" --- # Технические риски — ET-011 Технические риски этапа добавления GPX-download эндпоинта и UI-кнопки в popup публичного трека. Бизнес-риски — в BRD §8 ET-011. Шкала: вероятность (Н/С/В) × влияние (Н/С/В). ## R-1 — iOS Safari игнорирует `Content-Disposition: attachment` - **Описание:** Исторически iOS Safari склонен открывать XML inline вместо скачивания. Если эндпоинт отдаёт правильный header, но Safari показывает GPX как текст в новой вкладке — UX сломан. - **Вероятность / Влияние:** С (был — В, де факто митигирован) / С. - **Митигация:** - **Архитектурное решение (ADR-014 §A)**: используем `fetch + Blob + URL.createObjectURL + ` паттерн — тот же, что `app.js::downloadGPX()` для построенного маршрута. Этот паттерн в проде работает на iOS Safari (проверено в ET-006 / PH-3). - При downloads с `a.download` от blob-URL iOS Safari 13+ корректно сохраняет файл с указанным именем в downloads. - E2E-01/02 (Playwright) проверяет на desktop + mobile viewport; iOS-specific quirk проверяется ручным smoke на физическом iPhone (BRD §8 R-1). - **Наследник от:** существующий `downloadGPX()` (PH-3 / ET-006 patterns). ## R-2 — Кириллица в имени файла ломается в downloaders некоторых браузеров - **Описание:** Headers `Content-Disposition: filename="<кириллица>.gpx"` без RFC 5987 ASCII-fallback ломаются в старых Edge, не-Unicode Windows-устройствах. - **Вероятность / Влияние:** С / Н. - **Митигация:** - **Архитектурное решение (ADR-014 §F)**: всегда отдаём ОБА параметра: ASCII-fallback `filename=` + UTF-8 `filename*=UTF-8''`. Современные браузеры читают `filename*`, древние — ASCII-fallback (= `track-.gpx`). - Тест IT-06 проверяет наличие обоих параметров. - UT-04 проверяет санитизацию (запрещённые символы → `_`, длина ≤ 80 байт UTF-8). ## R-3 — Утечка proprietary metadata через merged GPX (ADR-015 §B trade-off) - **Описание:** Трек с `sources=["osm", "wikiloc"]` (после dedup-merge) проходит license-guard по правилу ANY (есть OSM ⇒ download разрешён). Но `tracks.name` / `tracks.description` могут быть взяты из Wikiloc (если у Wikiloc был выше source_priority). В скачанный GPX попадает proprietary текст. - **Вероятность / Влияние:** С / С. - **Митигация:** - **Архитектурное решение (ADR-014 §G)**: `` ставим только для OSM (`license = openstreetmap.org/copyright`); для не- OSM `` опускаем. Это защищает от ложной атрибуции. - **Архитектурное ограничение (ADR-015 §B)**: per-field source tracking не вводим (требует ALTER TABLE — out of ET-011 scope). - **Compensation**: `source_priority` в ET-009 фиксирует osm=100 > enduro_russia=80 > wikiloc=70. При merge OSM-метаданные перекрывают остальные. На практике для треков с `"osm" ∈ sources` `name` и `description` уже от OSM. - **Эскалация**: если в Build review-стадии review-агент найдёт конкретный случай утечки (например, фикстура с `wikiloc.description = "<длинный proprietary текст>"`) — возврат в Analysis для расширения схемы. ## R-4 — Запрос на трек 200000+ точек срывает worker по timeout - **Описание:** Сборка `xml.etree.ElementTree` для 200000 trkpt в строку занимает 400–500 мс CPU. Несколько параллельных таких запросов могут превысить uvicorn `--timeout-keep-alive` или nginx `proxy_read_timeout`. - **Вероятность / Влияние:** Н / С. - **Митигация:** - **Архитектурное решение (REQ-NF-02, ADR-014 §H)**: cap 200000 → 413 ДО сборки XML. - Проверка делается через `tracks.points_count` (read-only field в схеме ET-008, indexed PK lookup — < 1 ms). - Тест IT-04 проверяет 413 для фиктивной записи `points_count=300000`. - В случае массового тяжёлого трафика — отдельный rate-limit middleware (out of scope, см. `07-infra-requirements.md` §3.2). ## R-5 — Массовые скачивания одного трека забивают RAM сервера - **Описание:** Cap 200k → ~20 МБ XML per request. 10 параллельных скачиваний = 200 МБ heap. test-сервер имеет ~1 ГБ свободно у контейнера app. - **Вероятность / Влияние:** Н / С. - **Митигация:** - 200 МБ < free RAM × запас 5×. Не блокирующий. - Если в проде проявится — переключение на `StreamingResponse` (ADR-014 §C опция C2). Это не меняет API-контракт и тесты, можно делать без нового ADR. - Garbage collection после `Response(...)` корректно освобождает heap (Python ссылается только на raw bytes для отправки в TCP). ## R-6 — Кнопка «Скачать» появляется для треков с `download_allowed: false` → 403 после клика - **Описание:** Frontend (ADR-014 §3.b) показывает кнопку **всегда**. При клике на трек EnduroRussia/Wikiloc/ttrails backend возвращает 403. Пользователь думает «функция сломана». - **Вероятность / Влияние:** В / Н. - **Митигация:** - **Сознательный компромисс** (ADR-014 §«Отрицательные»): прятать кнопку требует знать `download_allowed` на клиенте — расширение MVT/GeoJSON-контракта на новое поле. Не делаем в ET-011. - **Toast с CTA**: при 403 → `showToast('Источник запрещает скачивание. Откройте трек на сайте источника.')` + кликабельная ссылка на `external_urls[0]` (см. ADR-015 §5). - **Release-notes** (если ведутся): «Качаем пока только OSM-треки». - При негативном UX-фидбэке в проде — расширение GeoJSON-properties флагом `downloadable: bool` в отдельной итерации. ## R-7 — Сборка GPX-XML без экранирования спецсимволов в `tracks.name` - **Описание:** Имя трека может содержать `&`, `<`, `>`, `"` — обязательные для XML escape-symbols. Если builder использует f-string templates без escape — broken XML, провал AC-5 (XSD validation). - **Вероятность / Влияние:** В (если бы выбрали f-string) / В. - **Митигация:** - **Архитектурное решение (ADR-014 §B)**: `xml.etree.ElementTree` автоматически экранирует текст и атрибуты при сериализации. - Тест UT-01 (см. test-plan) использует `name = "Trail & "` или подобные кейсы. - Тест UT-03 / IT-07 валидирует против XSD. ## R-8 — Валидация по XSD требует `lxml` в test-deps - **Описание:** `xml.etree.ElementTree` (stdlib) **не** умеет валидацию по XSD. Для UT-03 / IT-07 нужен `lxml.etree.XMLSchema`. - **Вероятность / Влияние:** Случилось / Н. - **Митигация:** - **Архитектурное решение (ADR-014 §B, §5)**: добавить `lxml` в `requirements-dev.txt` (только для тестов). - Если `lxml` уже присутствует через `defusedxml` транзитивно — нет действия. - Альтернатива: `xmllint --schema` через subprocess — добавляет C-зависимость в CI image, более хрупкая. `lxml` через pip проще. ## R-9 — Юридическая ошибка в whitelist `download_allowed` - **Описание:** Архитектор закрыл BRD Q-1 как «только OSM» (default). Если Owner после merge'a определит, что EnduroRussia/Wikiloc разрешено отдавать — нужен update ADR-015 + правка `gps_sources.yaml`. В обратную сторону: если кто-то ошибочно выставит `download_allowed: true` для proprietary источника — нарушение ToS. - **Вероятность / Влияние:** С / В. - **Митигация:** - **Default-deny** в Pydantic-модели (ADR-015 §«Решение C»): отсутствие поля = `false`. - **Документация в ADR-015 §«Решение D»** — явный whitelist с юридическим обоснованием для каждого источника. - **Code review check** при изменении `gps_sources.yaml`: любая смена `download_allowed: false → true` требует ссылки на обновлённый licensing-ADR. - **Integration test IT-05** фиксирует поведение для запрещённого источника (страж-тест). - **Наследник от:** ET-008 R-9 (regression of accepted ADR to proposed). ## R-10 — Регрессия существующих эндпоинтов `/api/gps-tracks/*` - **Описание:** Расширение `endpoint.py::create_gps_router` новым route и аргументом `sources_config_path` может случайно сломать существующий контракт (`""`, `/tiles`, `/health`, `/cache/clear`). - **Вероятность / Влияние:** Н / С. - **Митигация:** - **Архитектурное решение**: новый аргумент `sources_config_path` опциональный, default — `None` (= `{"osm"}` whitelist). Старые тесты, вызывающие `create_gps_router(db_path)`, продолжают работать. - **Тест IT-08** — smoke-проверка, что GET `""`, `/tiles/...`, `/health` отвечают так же, как до ET-011. - **AC-15** — регрессионный пункт acceptance для UI: sheet-gpx, sheet-route, фильтры публичных треков работают как раньше. ## R-11 — Frontend парсинг `Content-Disposition` некорректен на каком-то браузере - **Описание:** Если `_parseFilenameFromCD()` (см. ADR-014 §3.b) не справляется с экзотическими header-форматами (например, кавычки в `filename="track \"name\".gpx"`), файл сохраняется с дефолтным именем. - **Вероятность / Влияние:** Н / Н. - **Митигация:** - Backend контролирует header — мы сами знаем, что отдаём `filename=".gpx"` без escaped quotes (санитизация в `safe_filename` заменяет `"` на `_`). - Fallback `track-.gpx` если парсинг не удался — файл всё равно сохраняется. ## R-12 — XSD-фикстура `gpx.xsd` устаревает - **Описание:** `gpx.xsd` от topografix может обновиться (хотя спецификация GPX 1.1 заморожена с 2004 года). Снимок 2026-06 будет валиден неопределённое время. - **Вероятность / Влияние:** Н / Н. - **Митигация:** - GPX 1.1 — frozen spec; topografix не выпускают новые версии 1.1. - Снимок коммитится один раз; если что-то изменится — refresh. ## R-13 — Race-condition: трек удалён из БД между HEAD и GET - **Описание:** Если в момент tap'а на popup трек удалили из БД (например, через ad-hoc `DELETE`), эндпоинт вернёт 404. Popup уже показал кнопку, пользователь увидит «Трек не найден» в toast. - **Вероятность / Влияние:** Н / Н. - **Митигация:** - Принято as-is. Toast «Трек не найден» — корректный UX. - В проекте нет ручного `DELETE FROM tracks` в нормальном потоке; GC pipeline (ET-008) удаляет orphan-записи раз в месяц. ## R-14 — Кнопка «Скачать» некорректно тапается на ультра-маленьких viewport - **Описание:** REQ-NF-04 требует ≥ 32×32 CSS px тапабельной зоны. При CSS-typo или ошибке в стилях кнопка может вписаться в padding'и popup'а, сжимаясь. - **Вероятность / Влияние:** Н / С. - **Митигация:** - **Архитектурное решение (ADR-014 §3.c)**: `width: 32px; height: 32px` в `.track-popup-download-btn`. - **E2E-02 (mobile)** проверяет bounding box ≥ 32×32 px. - **TC-UI-02 (Playwright UI test cases)** — визуальная проверка на iPhone SE (375×667). ## R-15 — Tooltip не объявляется screen-reader'у - **Описание:** REQ-F-01 / AC-14: tooltip «Скачать GPX». Если builder забудет `aria-label` — screen-reader пользователь не услышит название действия. - **Вероятность / Влияние:** Н / С. - **Митигация:** - **Архитектурное решение (ADR-014 §3.a)**: явно прописываем `aria-label="Скачать GPX"` И `title="Скачать GPX"` на `