Files
enduro-trails/docs/work-items/ET-014/10-tech-risks.md
claude-bot bc63122221
All checks were successful
CI / lint (push) Successful in 5s
CI / test (push) Successful in 10s
CI / build (push) Successful in 2s
architect(ET): auto-commit from architect run_id=88
2026-06-04 11:15:52 +00:00

22 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-014 Технические риски — ET-014: Z-index фикс — terrain-popup уступает sheet'у 1 approved 2026-06-04
agent:architect

Технические риски — ET-014

Технические риски фикса z-index конфликта #terrain-popup#sheet-gps-filters. Бизнес-риски — в BRD §9 (R1..R3). Шкала: вероятность (Н/С/В) × влияние (Н/С/В).

R-T-1 — closeTerrainPopup() падает на ранней загрузке, когда DOM не готов

  • Описание: Если по какому-то race condition openSheet() вызывается до того, как #terrain-popup / #terrain-toggle появятся в DOM, getElementById вернёт null. Helper защищён ранним возвратом (if (!popup || popup.style.display === 'none') return;), но если btn null, а popup есть — btn.classList.remove упадёт.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Архитектурное решение (ADR-019 §«Решение»): в helper'е проверка if (btn) btn.classList.remove('active');.
    • DOM-инвариант: #terrain-popup и #terrain-toggleоба статически прописаны в index.html (строки ~43 и в #map-controls-r). Они существуют сразу после парсинга HTML, ещё до выполнения app.js (который грузится с defer). Реалистичная вероятность null — околонулевая.
    • Acceptance гейт: AC-09 (TC-UI-05) — все 5 sheet'ов открываются последовательно, helper срабатывает 5 раз без ошибок.

R-T-2 — Двойной removeEventListener на closeTerrainOnOutside

  • Описание: При сценарии «открыт popup → клик по ссылке Фильтры… → openSheet(...) вызвал closeTerrainPopup()removeEventListener сработал» — а затем пользователь закрывает sheet и снова открывает popup, addEventListener повесит listener заново. Но если closeTerrainOnOutside был вызван иначе (например, через клик по карте в момент закрытия sheet'а — гипотетически), то оба removeEventListener'а отработают над одним и тем же handler'ом.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • DOM-спека: removeEventListener на отсутствующий handler — no-op (silent). Никаких exception'ов.
    • Архитектурное решение: helper идемпотентен по построению: if (popup.style.display === 'none') return; — повторный вызов при уже закрытом popup'е выходит мгновенно, без вызовов remove*.

R-T-3 — Регрессия открытия других sheet'ов (sheet-route и пр.)

  • Описание: Изменение openSheet затрагивает 6 sheet'ов: route, recon, scenic, link, gpx, gps-filters. Если новый вызов closeTerrainPopup() имеет побочный эффект для случая «popup закрыт», это сломает все 5 «здоровых» sheet'ов.
  • Вероятность / Влияние: Н / С.
  • Митигация:
    • Архитектурное решение (ADR-019 §«Решение»): helper строго no-op'ит при popup.style.display === 'none' (ранний выход первой строкой после null-check). При открытии sheet-route/recon/scenic/ link/gpx popup гарантированно закрыт (нет UI-пути открыть его до клика на #terrain-toggle, который не задействован в этих сценариях).
    • Acceptance гейт: AC-09 (TC-UI-05) — открытие всех 5 «здоровых» sheet'ов через тулбар. Обязательный гейт перед merge.
    • Sanity unit-тест (опциональный): статический grep, что в openSheet ровно один вызов closeTerrainPopup (не два, не забытый).

R-T-4 — display:none ломает положение popup'а после повторного открытия

  • Описание: toggleTerrainPopup() использует popup.style.display !== 'none' для определения текущего состояния (app.js:2775). Если мы скрыли popup через closeTerrainPopup(), при следующем клике на #terrain-toggle функция правильно определит «закрыт» и откроется снова. Но если осталась inline top/right, popup появится в старой позиции — может быть некорректно при resize окна между открытиями.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Архитектурное решение: toggleTerrainPopup() (app.js:2779- 2786) каждый раз пересчитывает top и right из btn.getBoundingClientRect() при открытии. Никакой stale-позиции не остаётся.
    • Acceptance гейт: AC-07, AC-08 — повторное открытие popup'а после закрытия sheet'а проверяется.

R-T-5 — Marker-dialog/search-panel/ruler-info регрессии при правке openSheet

  • Описание: #marker-dialog (z=500), #search-panel (z=600), #ruler-info (z=600) не относятся к .bottom-sheet. Они открываются не через openSheet, а через свои обработчики (tb-marker/tb-search/tb-ruler). Если наша правка случайно затронула общий код пути этих виджетов — регрессия.
  • Вероятность / Влияние: Н / С.
  • Митигация:
    • Архитектурное решение (ADR-019 §«Что НЕ меняется»): правка локализована только в openSheet (вызывается только для .bottom-sheet). z-index стек не трогается → marker-dialog, search-panel, ruler-info остаются на своих местах в стеке.
    • Acceptance гейт: AC-10, AC-11, AC-12 (TC-UI-08 + ручные проверки search-panel и ruler-info).
    • REQ-NF-03: прямое отражение этого риска в TRZ.

R-T-6 — Старый клиент в кэше браузера получает старый багованный app.js

  • Описание: Пользователь с открытой вкладкой неделю назад имеет закэшированный старый app.js без closeTerrainPopup. Service worker — не настроен в MVP. До reload браузер не дёрнет свежий код.
  • Вероятность / Влияние: С / Н.
  • Митигация:
    • Архитектурное решение: src/web/index.html грузит app.js напрямую. nginx + стандартный Cache-Control на *.js (не immutable). При reload браузер делает conditional GET → 200 (свежий) или 304.
    • Backwards compat: старый кэшированный клиент с багом продолжает работать в багованном режиме, никаких 4xx/5xx нет. Никакого hard-reload не требуется — обычный F5 / pull-to-refresh подхватит fix.
    • Долгосрочная митигация: PWA / SW (PH-9) введёт правильную инвалидацию.

R-T-7 — Пользователь ожидает «возврат к panel слоёв» после закрытия sheet'а

  • Описание: BRD R2 явно описан: «пользователь может удивиться, что панель слоёв сама закрылась». После закрытия фильтров пользователь оказывается на карте, а не в panel слоёв. Кому-то это может показаться «прыжок UX».
  • Вероятность / Влияние: С / Н.
  • Митигация:
    • Архитектурное решение (ADR-019 §A): BRD R2 разрешает такое поведение: «панель слоёв — точка входа в фильтры, после закрытия фильтров пользователь возвращается к карте». Это решение оператора, зафиксировано в BRD.
    • UX-нота для test-report: оператор фиксирует свои наблюдения в 13-test-report.md.
    • Fallback (если оператор передумает): в closeAllSheets / closeSheet('sheet-gps-filters') дополнительно перезапускать toggleTerrainPopup — но это существенное расширение scope и требует отдельной задачи (ET-014.1 или новый work-item).

R-T-8 — Свайп фильтров вниз — popup не возвращается

  • Описание: Та же концептуальная проблема, что R-T-7, но через жест свайпа. Пользователь свайпом закрывает sheet, видит карту, а не panel слоёв.
  • Вероятность / Влияние: С / Н.
  • Митигация:
    • Та же что R-T-7: BRD R2 это разрешает.
    • Acceptance гейт: AC-03 включает чекбоксы внутри sheet'а; свайп не тестируется отдельно (он = клик поведенчески).

R-T-9 — В будущем кто-то откроет sheet с явным намерением «не закрывать popup»

  • Описание: Пока такого сценария нет (BRD §8 допускает, что единственная точка входа в sheet-gps-filters из popup'а — это «Фильтры…»). Но если завтра появится «открыть мини-фильтр из popup'а, оставив popup открытым», текущее общее правило openSheet → closeTerrainPopup заблокирует такой сценарий.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Архитектурное решение (ADR-019 §«Технический долг» TD-2): при появлении такого сценария — расширение openSheet(id, opts) объектом опций с флагом keepPopup: true. Сейчас — YAGNI.

R-T-10 — eslint падает на новой функции из-за code style

  • Описание: Если в проекте настроен eslint с правилами на prefer-const, func-style, no-implicit-globals — новая function closeTerrainPopup() может не пройти конкретные правила стиля.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Архитектурное решение: другие helper'ы в app.js (openSheet, closeSheet, closeAllSheets, closeTerrainOnOutside) объявлены через function name() без проблем — значит, eslint их пропускает.
    • Acceptance гейт: make lint зелёный (часть DoD).

R-T-11 — Playwright TC-UI-* нестабильны на test-среде из-за тайминга

  • Описание: TC-UI-01..TC-UI-08 используют фиксированные wait (300-600 мс) после кликов. На загруженной test-среде анимация открытия sheet'а (transition: transform 0.3s) может не успеть завершиться, скриншот будет «полу-открытым».
  • Вероятность / Влияние: С / Н.
  • Митигация:
    • Архитектурное решение: TC-UI-* — операторские, не CI-blocking (см. 04b-ui-test-cases.md). Оператор делает финальную приёмку.
    • Tuning: если CI-прогон нестабилен — поднимать wait'ы до 800 мс (мажорная анимация = 300 мс + слабая retry).
    • Это вне scope ADR-019.

R-T-12 — В будущем z-index у #sheet-backdrop или .bottom-sheet поднимут до >500 без знания о ADR-019

  • Описание: Кто-то решит «давайте сделаем sheets z=510» (Вариант B), не зная, что мы выбрали Вариант A. Тогда правка не сломает ничего (она лишь подкрепит fix), но логика становится двойной: и popup закрывается, и z-index хитрый. Сложнее понимать систему.
  • Вероятность / Влияние: С / Н.
  • Митигация:
    • Архитектурное решение (ADR-019 §«Альтернативы»): Вариант B зафиксирован как отклонённый. Если кто-то будет менять z-index, он прочитает ADR-индекс и увидит запись.
    • Прецедент: комментарий в коде app.js: // ET-014: terrain-popup yields to any opening sheet (see ADR-019).

R-T-13 — Десктоп: после закрытия фильтров пользователь не видит ни popup'а, ни фильтров, ни panel слоёв

  • Описание: На desktop backdrop скрыт media-query (app.css:543: #sheet-backdrop { display: none; }). Sheet занимает слева ~380 px. После закрытия sheet'а пользователь видит чистую карту. Никаких «фантомных» элементов — но и контекста, где он только что был, нет.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Архитектурное решение: это специально так — BRD §3 «после закрытия пользователь возвращается к карте». На desktop нет визуальной потери (карта всегда видна, sheet был сбоку).
    • Acceptance гейт: AC-02, AC-04.

R-T-14 — Регрессия повторного открытия popup'а с уже выставленной inline-позицией

  • Описание: При закрытии через closeTerrainPopup() мы выставляем popup.style.display = 'none', но не сбрасываем popup.style.top и popup.style.right. При следующем открытии через toggleTerrainPopup значения top/right пересчитываются, поэтому стейл не страшен. Но если кто-то в будущем добавит ветку «открыть popup без пересчёта позиции» — может сработать на остатках.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Архитектурное решение: toggleTerrainPopup (app.js:2779- 2786) безусловно пересчитывает top/right при каждом открытии.
    • Тест: AC-08 (TC-UI-07) — popup закрывается кликом вне, потом открывается заново; проверка визуальной корректности.

R-T-15 — Сценарий «открыть фильтры, прокрутить sheet вниз и обратно к popup»

  • Описание: Пользователь открыл фильтры, popup закрылся. Если бы popup остался в DOM-tree «фоном» (например, при z-index решении), можно было бы свайпом или ESC вернуться к нему. После ET-014 этого пути нет.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Архитектурное решение: этот сценарий не был доступен и до ET-014 (popup display:block не позволял прокрутить «к popup'у» — он и так был видим). UX не теряет ничего.

R-T-16 — Service worker в будущем (PH-9) перехватит app.js

  • Описание: Когда PH-9 (PWA) введёт SW, он начнёт кэшировать app.js в Cache Storage. Деплой ET-014 потребует cache-busting стратегии (?v=, hash в имени файла или clients.claim()+ skipWaiting() в SW).
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • PH-9 — отдельный work-item. К моменту его реализации ET-014 уже давно в test/prod, новый SW при первой установке возьмёт свежий app.js. Никаких специальных действий для ET-014 не нужно.

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

# Риск Вер Влиян Митигация (тип)
R-T-1 closeTerrainPopup падает на ранней DOM-загрузке Н Н null-check в helper; DOM-инвариант; AC-09
R-T-2 Двойной removeEventListener Н Н DOM-спека = no-op; идемпотентность helper'а
R-T-3 Регрессия открытия 5 «здоровых» sheet'ов Н С Ранний выход no-op; AC-09 = обязательный гейт
R-T-4 Stale top/right у popup'а после reopen Н Н toggleTerrainPopup пересчитывает каждый раз; AC-07
R-T-5 Marker-dialog/search-panel/ruler-info регрессия Н С Локализация правки; AC-10/AC-11/AC-12 = REQ-NF-03
R-T-6 Закэшированный старый app.js у пользователей С Н Conditional GET (If-Modified-Since); backwards compat
R-T-7 UX-удивление «panel слоёв сама закрылась» С Н BRD R2 разрешает; test-report фиксирует
R-T-8 Свайп вниз — popup не возвращается С Н То же что R-T-7
R-T-9 Будущий сценарий «открыть sheet, не закрывая popup» Н Н YAGNI; TD-2 в ADR-019
R-T-10 eslint падает на новой функции Н Н Существующий стиль function name() принят
R-T-11 Playwright TC-UI нестабильны по таймингу С Н Операторская приёмка; tuning wait'ов
R-T-12 Будущий developer не знает про ADR-019, поднимет z-index С Н ADR в индексе; комментарий в коде
R-T-13 Desktop: пустая карта после закрытия — нет контекста Н Н Specified by BRD §3
R-T-14 Stale inline-позиция popup'а Н Н Пересчёт в toggleTerrainPopup каждый раз
R-T-15 «Возврат к popup'у» через свайп невозможен Н Н Сценарий не существовал и раньше
R-T-16 PH-9 (SW) перехватит app.js Н Н Не задача ET-014; SW при первой установке свежий

Связанные документы

  • 01-brd.md §4 BR-01..BR-06, §9 R1..R3 (бизнес-риски пересекаются)
  • 02-trz.md §2.1 REQ-F-01..REQ-F-07, §2.2 REQ-NF-01..REQ-NF-05, §3 (варианты)
  • 03-acceptance-criteria.md AC-01..AC-14 (все гейты)
  • 04b-ui-test-cases.md TC-UI-01..TC-UI-08
  • 06-adr/ADR-019-terrain-popup-yields-to-sheet.md §«Решение», §«Последствия», §«Технический долг»
  • 07-infra-requirements.md §6 (deploy procedure), §7 (мониторинг)
  • 08-data-requirements.md
  • docs/work-items/ET-013/10-tech-risks.md — образец «calibration risks» документа (наследие)