23 KiB
23 KiB
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 |
|
Технические риски — 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).
- Архитектурное решение (ADR-014 §A): используем
- Наследник от: существующий
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-8filename*=UTF-8''. Современные браузеры читаютfilename*, древние — ASCII-fallback (=track-<id>.gpx). - Тест IT-06 проверяет наличие обоих параметров.
- UT-04 проверяет санитизацию (запрещённые символы →
_, длина ≤ 80 байт UTF-8).
- Архитектурное решение (ADR-014 §F): всегда отдаём ОБА
параметра: ASCII-fallback
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" ∈ sourcesnameиdescriptionуже от OSM. - Эскалация: если в Build review-стадии review-агент найдёт
конкретный случай утечки (например, фикстура с
wikiloc.description = "<длинный proprietary текст>") — возврат в Analysis для расширения схемы.
- Архитектурное решение (ADR-014 §G):
R-4 — Запрос на трек 200000+ точек срывает worker по timeout
- Описание: Сборка
xml.etree.ElementTreeдля 200000 trkpt в строку занимает 400–500 мс CPU. Несколько параллельных таких запросов могут превысить uvicorn--timeout-keep-aliveили nginxproxy_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в отдельной итерации.
- Сознательный компромисс (ADR-014 §«Отрицательные»): прятать
кнопку требует знать
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.
- Архитектурное решение (ADR-014 §B):
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 проще.
- Архитектурное решение (ADR-014 §B, §5): добавить
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 фиксирует поведение для запрещённого источника (страж-тест).
- Default-deny в Pydantic-модели (ADR-015 §«Решение C»): отсутствие
поля =
- Наследник от: 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если парсинг не удался — файл всё равно сохраняется.
- Backend контролирует header — мы сами знаем, что отдаём
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).
- Архитектурное решение (ADR-014 §3.c):
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.
- Архитектурное решение (ADR-014 §3.a): явно прописываем
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-сервере — нормально без видимого индикатора в большинстве случаев.
- Архитектурное решение (ADR-014 §3.b): CSS-класс
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.
- Архитектурное решение (ADR-015 §«Решение F»): если конфиг
недоступен — 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.
- CSS
- Backend идемпотентен (read-only), повторный запрос не вредит state.
- Архитектурное решение (ADR-014 §3.b):
Сводная таблица
| 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 требуется только если:
- Owner закрывает Q-1 как разрешающий — обновление ADR-015 (но не back-to:analysis).
- Review-агент находит конкретный случай утечки proprietary
metadata (R-3) —
back-to:analysisдля расширения схемы БД. - iOS Safari возвращает регресс по R-1 —
back-to:build(неback-to:analysis) для добавления fallback'а наwindow.location.href.