Files
enduro-trails/docs/work-items/ET-011/10-tech-risks.md
claude-bot 6fe2ecf12b
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Successful in 6s
CI / build (push) Has been skipped
architect(ET): auto-commit from architect run_id=64
2026-06-03 20:44:55 +00:00

23 KiB
Raw Blame History

type, work_item_id, title, version, status, created_at, authors
type work_item_id title version status created_at authors
tech-risks ET-011 Технические риски — ET-011: Скачивание трека из popup 1 approved 2026-06-03
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 + <a download> паттерн — тот же, что 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-<id>.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): <copyright> ставим только для OSM (license = openstreetmap.org/copyright); для не- OSM <copyright> опускаем. Это защищает от ложной атрибуции.
    • Архитектурное ограничение (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 в строку занимает 400500 мс 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 & <special>" или подобные кейсы.
    • Тест 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="<ascii_no_quote>.gpx" без escaped quotes (санитизация в safe_filename заменяет " на _).
    • Fallback track-<id>.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" на <button>.
    • Code-review checklist: проверить наличие aria-label для всех icon-only buttons.

R-16 — Зависание popup при медленном API (типичное скачивание > 1 сек)

  • Описание: При построении GPX на 50000 точек + плохой downlink у пользователя — visual stall на кнопке. Если индикатор не показан, кажется «не работает».
  • Вероятность / Влияние: С / Н.
  • Митигация:
    • Архитектурное решение (ADR-014 §3.b): CSS-класс .is-loading с visual spinner через ::after псевдоэлемент. Применяется на время fetch().
    • Снимается в finally блоке (даже при ошибке).
    • REQ-NF-01 = 300 ms p95 на 50k точек на test-сервере — нормально без видимого индикатора в большинстве случаев.

R-17 — gps_sources.yaml не существует на runtime → download падает

  • Описание: Если SOURCES_CONFIG_PATH указывает на несуществующий файл (например, после refactor'а директорий), create_gps_router при старте упадёт.
  • Вероятность / Влияние: Н / В.
  • Митигация:
    • Архитектурное решение (ADR-015 §«Решение F»): если конфиг недоступен — fallback allowed_sources = {"osm"}. Это совпадает с production-дефолтом, поэтому функциональность сохраняется.
    • Логируется WARNING в stdout: gps_sources.yaml not found, falling back to safe-deny whitelist.
    • Test-fixtures без конфига работают через тот же fallback.

R-18 — gzip middleware не сжимает GPX → большой объём egress

  • Описание: Если starlette GZipMiddleware не настроен или настроен на minimum size > 1 МБ, GPX-ответ для маленького трека (5k точек ≈ 650 КБ) уходит несжатым.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Не блокирует функциональность. Egress test-сервера ≥ 100 Mbps, нагрузка от download'ов минимальна.
    • Опционально (out of scope): добавить GZipMiddleware в src/api/main.py, если ещё не добавлен. Это affects все эндпоинты, не только download — отдельная задача.
    • GPX-XML сжимается gzip'ом обычно ×3..5.

R-19 — Параллельные клики на «Скачать» создают N запросов

  • Описание: Если пользователь нервно тапает кнопку 5 раз подряд — N параллельных fetch к одному треку. Тратятся ресурсы.
  • Вероятность / Влияние: С / Н.
  • Митигация:
    • Архитектурное решение (ADR-014 §3.b): btnEl.classList.add('is-loading')
      • CSS pointer-events: none блокирует повторные клики до finally.
    • Backend идемпотентен (read-only), повторный запрос не вредит state.

Сводная таблица

ID Риск Вер. Влияние Класс Статус
R-1 iOS Safari игнорирует Content-Disposition С С Средний переиспользование рабочего паттерна downloadGPX()
R-2 Кириллица в filename С Н Низкий RFC 5987 filename* + ASCII-fallback
R-3 Утечка proprietary metadata через merged GPX С С Средний <copyright> только OSM; per-field tracking — отдельный work item
R-4 Patho-трек срывает timeout Н С Низкий cap REQ-NF-02 = 200k → 413
R-5 RAM от параллельных скачиваний Н С Низкий 200 МБ при 10 параллельных, < free RAM × 5
R-6 Кнопка всегда видна → 403 после клика В Н Низкий сознательный UX-compromise + toast c CTA
R-7 XML-escape tracks.name В (без ET) / Н (с ET) В Средний xml.etree.ElementTree авто-escape
R-8 lxml в test-deps Случилось Н Низкий optional add в requirements-dev.txt
R-9 Юридическая ошибка в download_allowed whitelist С В Высокий default-deny + ADR-015 §D + IT-05 + review
R-10 Регрессия существующих эндпоинтов Н С Низкий IT-08 smoke + opt arg sources_config_path
R-11 Frontend парсинг Content-Disposition Н Н Низкий fallback track-<id>.gpx
R-12 XSD-фикстура устаревает Н Н Низкий GPX 1.1 frozen
R-13 Race delete Н Н Низкий 404 = корректный UX
R-14 Кнопка не тапается на маленьких viewport Н С Низкий CSS 32px × 32px + E2E-02 + TC-UI-02
R-15 Screen-reader не получает label Н С Низкий aria-label + title + review
R-16 Visual stall при медленном API С Н Низкий .is-loading spinner
R-17 Конфиг не существует на runtime Н В Высокий fallback {"osm"} + WARNING log
R-18 gzip не сжимает Н Н Низкий optional middleware add
R-19 Параллельные клики С Н Низкий pointer-events: none + idempotent backend

Высокие классы:

  • R-9 — legal/license risk. Митигация многослойная: default-deny в Pydantic + явный whitelist в ADR-015 + integration-тест + code-review чеклист.
  • R-17 — runtime safety. Митигация: silent-fallback на consistent с production default (= {"osm"}), не падаем при стартe.

Средние классы:

  • R-1 — переиспользуем de facto проверенный паттерн.
  • R-3 — известный compromise, задокументирован в ADR-015 §B; полное решение — отдельный work item.

Блокирующих рисков нет. Высокие классы требуют внимания на этапе разработки и code review.

Эскалация

  • arch:major-changeне выставляется (см. ADR-014 §«Классификация», ADR-015 §«Классификация»). ET-011 не вводит новых архитектурных компонентов.
  • back-to:analysis — не требуется. ТЗ полное, BRD-вопросы Q-1/Q-2/Q-3 закрыты дефолтными значениями (см. BRD §9).
  • Эскалация в Architecture требуется только если:
    1. Owner закрывает Q-1 как разрешающий — обновление ADR-015 (но не back-to:analysis).
    2. Review-агент находит конкретный случай утечки proprietary metadata (R-3) — back-to:analysis для расширения схемы БД.
    3. iOS Safari возвращает регресс по R-1 — back-to:build (не back-to:analysis) для добавления fallback'а на window.location.href.