296 lines
22 KiB
Markdown
296 lines
22 KiB
Markdown
---
|
||
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» документа (наследие)
|