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

296 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: tech-risks
work_item_id: ET-014
title: "Технические риски — ET-014: Z-index фикс — terrain-popup уступает sheet'у"
version: 1
status: approved
created_at: 2026-06-04
authors:
- "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» документа (наследие)