architect(ET): auto-commit from architect run_id=88
This commit is contained in:
@@ -21,3 +21,4 @@
|
||||
| ADR-015 | Политика реэкспорта публичных треков: per-source `download_allowed` в `gps_sources.yaml`, default-deny (whitelist `osm` для MVP) | accepted | 2026-06-03 | [ET-011](../../work-items/ET-011/06-adr/ADR-015-source-redistribution-policy.md) |
|
||||
| ADR-016 | Снижение minzoom публичных GPS-треков до z5: калибровка существующих tier-таблиц `build_gps_mvt`/`_simplify_coords`, on-demand MVT остаётся, без heat-map/clustering | accepted | 2026-06-04 | [ET-012](../../work-items/ET-012/06-adr/ADR-016-z5-tiling-policy.md) |
|
||||
| ADR-017 | Zoom-aware paint для hillshade/TRI на z9-z11: `interpolate`-выражения по `raster-opacity` и `raster-contrast`, `raster-resampling: 'nearest'`, понижение UI-минзума hillshade с 10 до 9; без перегенерации растровых тайлов | accepted | 2026-06-04 | [ET-013](../../work-items/ET-013/06-adr/ADR-017-zoom-aware-terrain-paint.md) |
|
||||
| ADR-019 | Z-index фикс terrain-popup vs bottom-sheet: при `openSheet(id)` принудительно скрывать `#terrain-popup` через helper `closeTerrainPopup()`; без правок CSS-стека (marker-dialog z=500, search-panel z=600, ruler-info z=600 остаются нетронутыми) | accepted | 2026-06-04 | [ET-014](../../work-items/ET-014/06-adr/ADR-019-terrain-popup-yields-to-sheet.md) |
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
---
|
||||
type: adr
|
||||
work_item_id: ET-014
|
||||
adr_id: ADR-019
|
||||
title: "ADR-019: При открытии любого bottom-sheet принудительно закрывать terrain-popup — без правки z-index стека"
|
||||
status: accepted
|
||||
created_at: 2026-06-04
|
||||
updated_at: 2026-06-04
|
||||
authors:
|
||||
- "agent:architect"
|
||||
supersedes: []
|
||||
superseded_by: []
|
||||
labels:
|
||||
- "ET-014:ui-z-index"
|
||||
- "minor-change"
|
||||
---
|
||||
|
||||
# ADR-019 — Terrain-popup уступает место bottom-sheet'у
|
||||
|
||||
## Статус
|
||||
|
||||
**Accepted.** Архитектурное решение для ET-014.
|
||||
|
||||
Это **UI / DOM-stacking фикс**. По BRD §5 (BR-04, BR-05) — не arch:major-change.
|
||||
ADR оформляется для фиксации **отказа от двух альтернативных вариантов**
|
||||
(подъём z-index всей категории `.bottom-sheet` и точечный подъём
|
||||
`#sheet-gps-filters`), чтобы они не вернулись в обсуждение в следующем
|
||||
work-item, который столкнётся с похожим конфликтом.
|
||||
|
||||
## Контекст
|
||||
|
||||
### Текущее состояние (как есть)
|
||||
|
||||
Стек z-index клиентского UI (`src/web/app.css`):
|
||||
|
||||
| Элемент | z-index | Файл/строка |
|
||||
|-------------------|---------|-------------------------|
|
||||
| `#map` | 0 | app.css:68 |
|
||||
| `#no-data-warning`| 200 | app.css:410 |
|
||||
| `#sheet-backdrop` | 390 | app.css:225 |
|
||||
| `.bottom-sheet` | 400 | app.css:188 |
|
||||
| `#map-controls-r` | 400 | app.css:129 |
|
||||
| `.terrain-popup` | **500** | app.css:787 |
|
||||
| `#marker-dialog` | 500 | app.css:399 |
|
||||
| `#search-panel` | 600 | app.css:1101 |
|
||||
| `#ruler-info` | 600 | app.css:1122 |
|
||||
|
||||
Поток открытия фильтров (`src/web/gps_tracks.js:737`):
|
||||
|
||||
1. `#terrain-toggle` (кнопка-гора) → `toggleTerrainPopup()` показывает
|
||||
`#terrain-popup` (z=500), вешает `closeTerrainOnOutside` на `document`.
|
||||
2. Пользователь жмёт `#public-tracks-filters-btn` («Фильтры…») внутри popup'а.
|
||||
3. `togglePublicTracksFiltersSheet()` вызывает `openSheet('sheet-gps-filters')`.
|
||||
4. `openSheet()` (`app.js:206`) добавляет класс `.open` на sheet и `.visible`
|
||||
на `#sheet-backdrop`.
|
||||
5. **`#terrain-popup` остаётся открытым** (display: block, z=500).
|
||||
6. Sheet (z=400) и backdrop (z=390) визуально оказываются **под** popup'ом.
|
||||
7. `closeTerrainOnOutside` не срабатывает: клик произошёл по
|
||||
`#public-tracks-filters-btn`, который `.contains()` целью popup'а.
|
||||
|
||||
### Проблема
|
||||
|
||||
- На mobile (viewport 360-414): popup занимает ~60% ширины справа, sheet
|
||||
выезжает снизу, его правые ~60% перекрыты popup'ом → пользователь видит
|
||||
узкую левую полоску, фильтрами пользоваться нельзя (BR-01).
|
||||
- На desktop (≥1024): popup справа, sheet выезжает как боковая панель
|
||||
слева → они геометрически не пересекаются, но **семантически открыты
|
||||
два меню одновременно** — это нарушение BR-02 («панель слоёв не должна
|
||||
перекрывать панель фильтров») и BR-03 («без артефактов наложения»),
|
||||
плюс выход за пределы BRD §3 «бизнес-цель: сделать фильтры реально
|
||||
доступными» в части UX-чистоты.
|
||||
- Backdrop sheet'а (z=390) не визуализирован: попадает под popup, на
|
||||
mobile отсутствует «фон не-фильтра затемнён» эффект; на desktop backdrop
|
||||
всё равно скрыт media-query (`app.css:543`).
|
||||
|
||||
### Архитектурный вопрос
|
||||
|
||||
**Как заставить sheet быть полноценно «верхним» виджетом, не вводя
|
||||
точечных z-index хаков и не рискуя стеком marker-dialog (z=500),
|
||||
search-panel (z=600), ruler-info (z=600).**
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
### Вариант A — закрывать `#terrain-popup` при открытии sheet (выбран)
|
||||
|
||||
При открытии любого `.bottom-sheet` принудительно скрывать
|
||||
`#terrain-popup` (display:none), снимать `.active` с `#terrain-toggle`,
|
||||
отвязывать висящий `closeTerrainOnOutside`.
|
||||
|
||||
Точка вставки — общий `openSheet()` в `src/web/app.js`. Не
|
||||
точечно в `togglePublicTracksFiltersSheet()`, потому что:
|
||||
- Сейчас «Фильтры…» — единственная точка входа в sheet из popup'а
|
||||
(BRD §8 допущение). Будущее: если фильтры POI или фильтры маршрута
|
||||
тоже окажутся «ссылками внутри popup'а», правило срабатывает само,
|
||||
без новой задачи.
|
||||
- Для существующих 5 sheet'ов (`sheet-route`, `sheet-recon`,
|
||||
`sheet-scenic`, `sheet-link`, `sheet-gpx`) вызов — no-op (popup
|
||||
при их открытии не открыт). REQ-F-06 («регрессий нет») выполняется
|
||||
автоматически.
|
||||
|
||||
Pros:
|
||||
- 0 правок CSS → 0 риска регрессии стека (marker-dialog z=500,
|
||||
search-panel z=600, ruler-info z=600 — REQ-NF-03).
|
||||
- Лечит **обе** среды одной правкой (mobile: фильтры доступны; desktop:
|
||||
«два меню одновременно» — устранено).
|
||||
- Backdrop sheet'а (z=390) теперь корректно затемняет фон на mobile
|
||||
(popup больше не закрывает его).
|
||||
- Логика «открыл sheet → скрыли pointer-меню» — стандартный mobile UX
|
||||
(так ведут себя dropdown'ы в Material / iOS Sheets).
|
||||
- BRD R2 это разрешает: «после закрытия фильтров пользователь
|
||||
возвращается к карте, а не к панели слоёв».
|
||||
- Локализация: 1 helper + 1 строка в `openSheet`. ~7 строк кода.
|
||||
|
||||
Cons / Принимаем:
|
||||
- Пользователь, привыкший «жму Фильтры… → panel слоёв остаётся открытой
|
||||
на фоне» — больше так не увидит. Это не регрессия, это устранение
|
||||
бага: BRD §1 признаёт текущее поведение блокером.
|
||||
- Если случай «нужны два открытых меню одновременно» появится в будущем
|
||||
— придётся переосмыслить. Сейчас такого сценария нет.
|
||||
|
||||
### Вариант B — поднять z-index всех `.bottom-sheet` выше terrain-popup
|
||||
|
||||
`.bottom-sheet { z-index: 510; }`, `#sheet-backdrop { z-index: 505; }`.
|
||||
|
||||
Pros:
|
||||
- Системное решение: вся категория `.bottom-sheet` гарантированно
|
||||
сверху.
|
||||
|
||||
Cons (отклонён):
|
||||
- **Столкновение с `#marker-dialog` (z=500).** Marker-dialog —
|
||||
отдельный виджет (не `.bottom-sheet`), но визуально это тоже
|
||||
«sheet-like». Если пользователь активирует «Метку» поверх открытого
|
||||
sheet'а (через swipe-down и тулбар), marker-dialog окажется под
|
||||
sheet'ом → AC-10 / REQ-NF-03 нарушится. Сейчас совместное открытие
|
||||
редко, но не запрещено.
|
||||
- **На desktop не лечит «два меню».** Popup справа (z=500), sheet слева
|
||||
(z=510) — геометрически не пересекаются, sheet «сверху» в стеке, но
|
||||
визуально на экране всё ещё видны оба меню. BR-03 «без артефактов
|
||||
наложения» формально нарушено.
|
||||
- Backdrop поднимать до z=505 — нормально, но это всё ещё ниже popup'а
|
||||
по логике стека («backdrop sheet'а» оказывается **над** terrain-popup,
|
||||
что может затемнить popup — формально не баг, но визуально странно).
|
||||
- Расширяет blast radius CSS-правки на всех 6 sheet'ов сразу.
|
||||
|
||||
### Вариант C — точечный z-index только `#sheet-gps-filters`
|
||||
|
||||
`#sheet-gps-filters { z-index: 510; }`, без правки backdrop.
|
||||
|
||||
Pros:
|
||||
- Самое маленькое изменение CSS (2 строки).
|
||||
|
||||
Cons (отклонён):
|
||||
- **Узкий хак.** Если завтра «Фильтры…» появятся ещё где-то (например,
|
||||
фильтр POI прямо из popup'а POI или фильтр маршрута из мини-sheet'а
|
||||
маршрута), у нас будет та же проблема и новая «специальная» правка.
|
||||
- **На desktop не лечит «два меню».** Та же проблема, что у варианта B.
|
||||
- Backdrop (`#sheet-backdrop` z=390) на mobile всё равно остаётся под
|
||||
popup'ом → визуально popup остаётся «поверх затемнения» → нарушает
|
||||
ожидание пользователя «sheet полноценно перекрыл всё, кроме самого
|
||||
себя».
|
||||
- Создаёт прецедент «один sheet — особенный». Каждая следующая итерация
|
||||
будет соблазн добавить ещё один специальный z-index.
|
||||
|
||||
### Вариант D — отказаться от popup'а, перенести «Фильтры…» на тулбар
|
||||
|
||||
Полностью убрать `#public-tracks-filters-btn` из `#terrain-popup`,
|
||||
добавить отдельную кнопку на правом тулбаре.
|
||||
|
||||
Cons (отклонён):
|
||||
- **Out of scope BRD §5**: «Добавление новых способов открытия фильтров
|
||||
(например, отдельной кнопки на toolbar) — не входит в scope.»
|
||||
- Меняет UX, нарушает архитектуру «slots в panel слоёв».
|
||||
|
||||
### Вариант E — открывать sheet модально внутри popup'а
|
||||
|
||||
Превратить sheet в child popup'а с собственным позиционированием.
|
||||
|
||||
Cons (отклонён):
|
||||
- Радикальная перестройка DOM-структуры sheet'а: он должен оставаться
|
||||
bottom-sheet'ом по другим сценариям (другие work-items предполагают
|
||||
единый компонент).
|
||||
- Сложнее testabilitу (Playwright-кейсы рассчитаны на текущую
|
||||
семантику `.open` класса на корневом `.bottom-sheet`).
|
||||
- Большой scope creep для bug-fix задачи.
|
||||
|
||||
## Решение
|
||||
|
||||
1. **В `src/web/app.js`** добавить helper:
|
||||
|
||||
```js
|
||||
function closeTerrainPopup() {
|
||||
const popup = document.getElementById('terrain-popup');
|
||||
const btn = document.getElementById('terrain-toggle');
|
||||
if (!popup || popup.style.display === 'none') return;
|
||||
popup.style.display = 'none';
|
||||
if (btn) btn.classList.remove('active');
|
||||
document.removeEventListener('click', closeTerrainOnOutside);
|
||||
}
|
||||
```
|
||||
|
||||
2. **В `openSheet(id)`** (`src/web/app.js:206`) **первой строкой
|
||||
после null-check** вызвать `closeTerrainPopup()`:
|
||||
|
||||
```js
|
||||
function openSheet(id) {
|
||||
const sheet = document.getElementById(id);
|
||||
if (!sheet) return;
|
||||
// ET-014: terrain-popup yields to any opening sheet (see ADR-019).
|
||||
// Prevents z-index collision (popup z=500 over sheet z=400) and
|
||||
// resolves the "two menus open at once" anti-pattern on desktop.
|
||||
closeTerrainPopup();
|
||||
// Close all other sheets first
|
||||
document.querySelectorAll('.bottom-sheet.open').forEach(s => {
|
||||
if (s.id !== id) closeSheet(s.id);
|
||||
});
|
||||
sheet.classList.add('open');
|
||||
const backdrop = document.getElementById('sheet-backdrop');
|
||||
backdrop.classList.add('visible');
|
||||
}
|
||||
```
|
||||
|
||||
3. **`closeTerrainOnOutside(e)` не меняется** — продолжает работать как
|
||||
раньше для сценария «клик вне popup'а и вне `#terrain-toggle`»
|
||||
(REQ-F-05 / AC-08). Если хочется DRY — реализатор может вызвать
|
||||
`closeTerrainPopup()` из тела `closeTerrainOnOutside`, но это
|
||||
опциональный cleanup; обязательного требования нет (две функции с
|
||||
одинаковым эффектом окей в vanilla JS без зависимостей).
|
||||
|
||||
4. **`togglePublicTracksFiltersSheet()` в `gps_tracks.js` не меняется.**
|
||||
Логика закрытия popup'а теперь живёт в `openSheet()` — общий путь
|
||||
для всех будущих и текущих sheet'ов.
|
||||
|
||||
### Что НЕ меняется
|
||||
|
||||
- `src/web/app.css` — **никаких z-index правок**. Стек marker-dialog (500),
|
||||
search-panel (600), ruler-info (600), `.bottom-sheet` (400),
|
||||
`#sheet-backdrop` (390), `.terrain-popup` (500), `#map-controls-r`
|
||||
(400), `#no-data-warning` (200), `#map` (0) — без изменений.
|
||||
- `src/web/index.html` — без изменений.
|
||||
- `src/web/gps_tracks.js` — без изменений.
|
||||
- `src/web/style.json` / `style-dark.json` — без изменений.
|
||||
- `src/api/*` — без изменений.
|
||||
- `Dockerfile`, `docker-compose.yml`, nginx, БД, миграции — без изменений.
|
||||
|
||||
## Классификация изменения
|
||||
|
||||
**minor-change.**
|
||||
|
||||
Меняется 1 файл:
|
||||
- `src/web/app.js` (+1 helper-функция ~7 строк, +1 вызов в `openSheet`).
|
||||
|
||||
Эскалация: **не arch:major-change.** Не требует расширенного approve.
|
||||
Не относится к категориям из CLAUDE.md «всё в Docker / on-premise / new
|
||||
service / new DB» — чистый клиентский UI fix.
|
||||
|
||||
## Последствия
|
||||
|
||||
### Положительные
|
||||
|
||||
- BR-01..BR-03 (фильтры реально доступны, без артефактов) — закрываются
|
||||
атомарной правкой одной функции.
|
||||
- BR-04 (другие sheets без регрессии) — автоматически: `closeTerrainPopup()`
|
||||
для них — no-op.
|
||||
- BR-05 (terrain-popup сам по себе без регрессии) — `toggleTerrainPopup`,
|
||||
`closeTerrainOnOutside`, чекбоксы рельефа, переключатели подложки/единиц
|
||||
не трогаются.
|
||||
- BR-06 (свет/тёмная тема) — нет theme-specific кода → одинаково работает.
|
||||
- REQ-NF-03 (marker-dialog, search-panel, ruler-info не регрессируют) —
|
||||
z-index не трогается → нулевой риск.
|
||||
- REQ-NF-04 (PWA / safe-area) — не задействован.
|
||||
- На mobile backdrop sheet'а (z=390) теперь корректно затемняет фон
|
||||
(раньше popup z=500 его перекрывал) → пользователь визуально
|
||||
понимает, что sheet — модальный.
|
||||
- Семантика «sheet — главный модальный виджет» становится единым правилом
|
||||
для всей `openSheet()` функции.
|
||||
|
||||
### Отрицательные / Принимаем
|
||||
|
||||
- Пользователь, открывший фильтры из panel слоёв, после закрытия
|
||||
фильтров **не возвращается** к panel слоёв — он видит карту.
|
||||
Чтобы снова попасть в panel слоёв, нужно повторно нажать `#terrain-toggle`.
|
||||
Принимаем по BRD R2: «панель слоёв — точка входа в фильтры, после
|
||||
закрытия фильтров пользователь возвращается к карте». Это решение
|
||||
оператора.
|
||||
- Если когда-нибудь появится сценарий «sheet и terrain-popup должны
|
||||
сосуществовать» — нужно будет вводить параметр в `openSheet({ keepPopup })`
|
||||
или вообще другую функцию. Сейчас такого сценария нет.
|
||||
|
||||
### Технический долг
|
||||
|
||||
- **TD-1: Унификация `closeTerrainOnOutside` через `closeTerrainPopup`.**
|
||||
Опциональный cleanup: рефакторинг тела `closeTerrainOnOutside` на
|
||||
вызов нового helper'а. Не блокирует ET-014, можно сделать отдельным
|
||||
fix-up коммитом. Если не сделать — две функции с почти одинаковым
|
||||
телом будут жить рядом.
|
||||
- **TD-2: Параметризация `openSheet(id, opts)`.** Если в будущем
|
||||
потребуется открыть sheet, **не** закрывая popup (новый редкий
|
||||
сценарий — пока не предвидится), `openSheet` нужно будет расширить
|
||||
объектом опций. Сейчас YAGNI.
|
||||
- **TD-3: Общий «модальный менеджер» для popup + sheet + dialog.**
|
||||
Сейчас три виджета (`.terrain-popup`, `.bottom-sheet`, `#marker-dialog`)
|
||||
имеют пересекающиеся z-index'ы (500, 400, 500). Если когда-нибудь
|
||||
появятся новые модальные виджеты или сложные комбинации, можно
|
||||
выделить общий «modal stack manager» с явным API
|
||||
`pushModal/popModal`. Сейчас overkill — три виджета и одно правило
|
||||
«sheet выгоняет popup» решают всё.
|
||||
|
||||
## Альтернативы для будущего
|
||||
|
||||
| # | Идея | Когда возвращаться |
|
||||
|---|------|---------------------|
|
||||
| F-1 | Подъём z-index `.bottom-sheet` до 510 (Вариант B) | Если появится сценарий «два меню одновременно нужны» и Вариант A не сработает |
|
||||
| F-2 | Точечный z-index `#sheet-gps-filters` (Вариант C) | Никогда — порождает специальные случаи |
|
||||
| F-3 | Перенос «Фильтры…» на тулбар (Вариант D) | По бизнес-запросу, отдельный work-item (изменит scope BRD ET-014) |
|
||||
| F-4 | Modal stack manager (TD-3) | Когда модальных виджетов станет ≥5 или появятся вложенные модалки |
|
||||
| F-5 | Параметризация `openSheet(id, opts)` (TD-2) | По мере появления исключений из правила «sheet выгоняет popup» |
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- BRD: `docs/work-items/ET-014/01-brd.md` §4 (BR-01..BR-06), §9 (R1..R3)
|
||||
- TRZ: `docs/work-items/ET-014/02-trz.md` §1.3 (корень проблемы),
|
||||
§2.1 (REQ-F-01..REQ-F-07), §2.2 (REQ-NF-01..REQ-NF-05), §3 (варианты)
|
||||
- AC: `docs/work-items/ET-014/03-acceptance-criteria.md` (AC-01..AC-14)
|
||||
- UI test cases: `docs/work-items/ET-014/04b-ui-test-cases.md`
|
||||
(TC-UI-01..TC-UI-08)
|
||||
- Инфра: `docs/work-items/ET-014/07-infra-requirements.md`
|
||||
- Данные: `docs/work-items/ET-014/08-data-requirements.md`
|
||||
- Риски: `docs/work-items/ET-014/10-tech-risks.md`
|
||||
- Глобальный ADR-индекс: `docs/architecture/adr/README.md`
|
||||
- Прецедент ADR-017 (ET-013) — формат «UI-калибровочного» ADR
|
||||
250
docs/work-items/ET-014/07-infra-requirements.md
Normal file
250
docs/work-items/ET-014/07-infra-requirements.md
Normal file
@@ -0,0 +1,250 @@
|
||||
---
|
||||
type: infra-requirements
|
||||
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
|
||||
|
||||
## 1. Резюме
|
||||
|
||||
ET-014 — **frontend UI/DOM-stacking fix**. Меняется один файл исходного
|
||||
кода (`src/web/app.js`) на ~8 строк (+1 helper-функция, +1 вызов в
|
||||
`openSheet`). Инфраструктура **не меняется**:
|
||||
|
||||
- 0 новых docker-сервисов;
|
||||
- 0 изменений в `Dockerfile`;
|
||||
- 0 изменений в `docker-compose.yml`;
|
||||
- 0 новых файлов БД, миграций, индексов;
|
||||
- 0 новых cron-записей;
|
||||
- 0 новых env / секретов / API-ключей;
|
||||
- 0 новых исходящих HTTPS-соединений;
|
||||
- 0 новых портов;
|
||||
- 0 изменений в nginx;
|
||||
- 0 изменений в backend (`src/api/*` без правок);
|
||||
- 0 изменений в `src/web/app.css` (z-index стек не трогается — см. ADR-019);
|
||||
- 0 изменений в `src/web/index.html`;
|
||||
- 0 изменений в `src/web/gps_tracks.js`;
|
||||
- 0 изменений в `style.json` / `style-dark.json`.
|
||||
|
||||
Эскалация: **minor change** (см. ADR-019 §«Классификация изменения»).
|
||||
|
||||
## 2. Контейнеры и сервисы
|
||||
|
||||
| Аспект | Требование |
|
||||
|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Новый сервис | **Нет** |
|
||||
| Изменения `Dockerfile` | **Нет** |
|
||||
| Изменения `docker-compose.yml` | **Нет** |
|
||||
| Перезапуск `app` после деплоя | Нужен — `docker compose up -d --no-deps app` (≈ 5 сек простоя). Подхватывает обновлённый `src/web/app.js` (отдаётся как статика из контейнера) |
|
||||
| Перезапуск `gps-collector` | Не нужен (не затронут) |
|
||||
| Очистка серверных кэшей | Не требуется (backend не меняется) |
|
||||
| Очистка клиентских кэшей | Не требуется. При первом обращении после деплоя браузер сделает conditional GET (`If-Modified-Since`) → 200 (свежий `app.js`) или 304 |
|
||||
|
||||
### 2.1 Зависимости между сервисами
|
||||
|
||||
Без изменений vs PH-6 / ET-013:
|
||||
|
||||
- `app` → отдаёт `/enduro/app.js` как статику.
|
||||
- `nginx (host)` → `app:8000` через docker-network bridge.
|
||||
|
||||
Никаких новых межсервисных вызовов.
|
||||
|
||||
## 3. Сеть
|
||||
|
||||
| Аспект | Требование |
|
||||
|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Новые входящие порты | **Нет** |
|
||||
| Изменения nginx | **Нет** |
|
||||
| Новые исходящие соединения | **Нет** |
|
||||
| CORS | Без изменений |
|
||||
| HTTPS / TLS | Без изменений — nginx с Let's Encrypt сертификатом DuckDNS |
|
||||
|
||||
### 3.1 Ingress / Egress — оценка дельты
|
||||
|
||||
ET-014 меняет порядок вызовов JS-функций; **сетевой паттерн не меняется**.
|
||||
|
||||
- `/enduro/app.js`: при первом GET после деплоя — `app.js` отдаётся
|
||||
целиком (∆ размера +~300 байт за счёт helper'а и комментариев).
|
||||
- Запросы к `/api/gps-tracks/*`, `/terrain/*`, `/api/route/*`,
|
||||
`/api/health` — без изменений.
|
||||
|
||||
Дельта на пользователя: ~300 байт единоразово при первой загрузке
|
||||
после деплоя. Пренебрежимо.
|
||||
|
||||
## 4. Серверные ресурсы
|
||||
|
||||
| Аспект | Требование |
|
||||
|-------------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| CPU `app` | Без изменений |
|
||||
| RAM `app` | Без изменений |
|
||||
| Disk `app` | Без изменений (`app.js` ~300 байт больше — пренебрежимо) |
|
||||
| CPU `gps-collector` | Без изменений (не затронут) |
|
||||
| RAM `gps-collector` | Без изменений |
|
||||
| Disk `gps-collector` | Без изменений |
|
||||
|
||||
## 5. Конфигурация и секреты
|
||||
|
||||
| Аспект | Требование |
|
||||
|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Новые env-переменные | **Нет** |
|
||||
| Новые секреты | **Нет** |
|
||||
| Новые API-ключи | **Нет** |
|
||||
| Изменения `config/*.yaml` | **Нет** |
|
||||
| Изменения runtime config | **Нет** |
|
||||
| Изменения `style.json`/`style-dark.json` | **Нет** |
|
||||
|
||||
## 6. Деплой
|
||||
|
||||
### 6.1 Среды
|
||||
|
||||
- **dev (локально)**: `make dev` (docker compose up `app`). Достаточно
|
||||
`git pull && make dev` для смены поведения.
|
||||
- **test (mva154)**: `https://openclaw.mva154.duckdns.org/enduro/`.
|
||||
CI/CD — Gitea Actions; деплой через `make deploy-test` или ручной
|
||||
SSH + `docker compose up -d --no-deps --build app` (см. §6.2).
|
||||
- **prod** — пока не задействован; ET-014 деплоится только в test.
|
||||
|
||||
### 6.2 Процедура деплоя в test
|
||||
|
||||
1. **Pre-deploy smoke**: проверить, что test-среда доступна:
|
||||
```bash
|
||||
curl -sI 'https://openclaw.mva154.duckdns.org/enduro/' | head -1
|
||||
curl -sI 'https://openclaw.mva154.duckdns.org/enduro/app.js' | head -1
|
||||
```
|
||||
Ожидается `HTTP/1.1 200 OK` на оба.
|
||||
|
||||
2. **Сборка образа**: `docker compose build app` на mva154 (после `git pull`).
|
||||
|
||||
3. **Перезапуск `app`**: `docker compose up -d --no-deps app`.
|
||||
|
||||
4. **Post-deploy smoke** — два grep'а по свежей статике:
|
||||
```bash
|
||||
# Helper-функция доехала
|
||||
curl -s 'https://openclaw.mva154.duckdns.org/enduro/app.js' | grep -c 'function closeTerrainPopup'
|
||||
# Ожидается = 1
|
||||
|
||||
# Вызов в openSheet доехал
|
||||
curl -s 'https://openclaw.mva154.duckdns.org/enduro/app.js' \
|
||||
| grep -A 4 'function openSheet' | grep -c 'closeTerrainPopup'
|
||||
# Ожидается ≥ 1
|
||||
```
|
||||
|
||||
5. **Ручная валидация AC-01..AC-14** через мобильный и desktop браузер:
|
||||
- Mobile (DevTools 390×844, тёмная тема): Рельеф → ✓ Публичные треки →
|
||||
Фильтры… → ожидается **полностью видимая** панель «Фильтры публичных
|
||||
треков» поверх затемнённого backdrop'а (AC-01, AC-14).
|
||||
- Mobile: Фильтры открыты → клик по чекбоксу активности →
|
||||
ожидается изменение состояния (AC-03).
|
||||
- Mobile: Фильтры открыты → клик `✕` → ожидается возврат к карте без
|
||||
артефактов (AC-04).
|
||||
- Mobile: Фильтры открыты → клик по `#sheet-backdrop` → закрытие (AC-05).
|
||||
- Mobile: повторное открытие 3 раза подряд (AC-06).
|
||||
- Mobile: Рельеф → переключение чекбоксов рельефа/подложки/единиц →
|
||||
popup без изменений (AC-07).
|
||||
- Mobile: Рельеф → клик по карте → popup закрывается (AC-08).
|
||||
- Mobile: открыть `sheet-route`, `sheet-recon`, `sheet-scenic`,
|
||||
`sheet-link`, `sheet-gpx` через тулбар → без артефактов (AC-09).
|
||||
- Mobile: «Метка» → marker-dialog (z=500) поверх (AC-10).
|
||||
- Mobile: «Поиск» → search-panel (z=600) поверх (AC-11).
|
||||
- Mobile: «Линейка» → ruler-info (z=600) поверх (AC-12).
|
||||
- Mobile, светлая тема (`#btn-theme`): повторить AC-01 (AC-13).
|
||||
- Desktop 1440×900: Рельеф → ✓ Публичные треки → Фильтры… →
|
||||
sheet слева, popup исчез (AC-02).
|
||||
|
||||
6. **Запись результатов в `13-test-report.md` и `14-deploy-log.md`**.
|
||||
|
||||
### 6.3 Rollback
|
||||
|
||||
В случае проблем (например, регрессия закрытия одного из 5 «здоровых»
|
||||
sheet'ов — крайне маловероятно, см. R-T-3 в `10-tech-risks.md`):
|
||||
|
||||
1. **Frontend rollback**: `git revert <commit>` + `docker compose up -d --no-deps --build app`.
|
||||
2. **Cache invalidation**: не требуется (browser cache на `app.js`
|
||||
инвалидируется по `If-Modified-Since` автоматически).
|
||||
|
||||
RTO: ≤ 5 минут.
|
||||
RPO: 0 — никаких изменений в БД, никаких данных не теряется.
|
||||
|
||||
### 6.4 CI/CD-гейты
|
||||
|
||||
- `make lint` (ruff + eslint) — должен быть зелёным.
|
||||
- `make test` (pytest unit + integration) — зелёный (никаких новых
|
||||
python-тестов в ET-014, существующие не задеты).
|
||||
- Playwright UI test cases TC-UI-01..TC-UI-08
|
||||
(`04b-ui-test-cases.md`) — зелёные на CI или в локальном Playwright
|
||||
прогоне. Если Playwright не интегрирован в CI — ручная валидация
|
||||
по §6.2 шаг 5.
|
||||
|
||||
## 7. Observability / Логирование
|
||||
|
||||
| Аспект | Требование |
|
||||
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Новые лог-сообщения | **Нет** |
|
||||
| Существующие лог-сообщения | `uvicorn.access` без изменений (трафик паттерн тот же) |
|
||||
| Метрики / Prometheus | Не вводим |
|
||||
| Health-endpoint | `GET /api/gps-tracks/health` — без изменений |
|
||||
|
||||
### 7.1 Что мониторить после деплоя
|
||||
|
||||
В `nginx access.log` на mva154 (вручную, без алёртов) — первые сутки:
|
||||
|
||||
- **Запросы к `/enduro/app.js`** — должны вернуть 200 (свежая версия) или
|
||||
304 (для пользователей, у которых cache не протух).
|
||||
- **Status codes для `/api/gps-tracks/*`** — без 5xx (мы не трогаем API).
|
||||
|
||||
Дополнительно, при ручной валидации (§6.2 шаг 5) — DevTools Console:
|
||||
- Не должно быть новых warning'ов или error'ов JS.
|
||||
- При открытии фильтров не должно быть `Uncaught ReferenceError:
|
||||
closeTerrainPopup is not defined` (sanity на правильность сборки).
|
||||
|
||||
## 8. Резервное копирование / Disaster recovery
|
||||
|
||||
| Аспект | Требование |
|
||||
|------------------------------|-----------------------------------------------------------------------------------------------------|
|
||||
| Backup БД | Без изменений vs ET-013/ET-008 (ET-014 не трогает БД) |
|
||||
| Backup статики `src/web/` | Без изменений; git — источник истины |
|
||||
| Время восстановления (RTO) | ≤ 5 минут (rollback контейнера, см. §6.3) |
|
||||
| Точка восстановления (RPO) | 0 — никаких данных не теряется |
|
||||
|
||||
## 9. Безопасность
|
||||
|
||||
| Аспект | Требование |
|
||||
|-------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
|
||||
| Auth / Authorization | Без изменений |
|
||||
| Валидация входных данных | Не применимо — клиентский UI-fix, никаких новых входов |
|
||||
| CSP | Без изменений |
|
||||
| Rate-limit | Без изменений |
|
||||
| TLS | Без изменений |
|
||||
|
||||
## 10. Совместимость
|
||||
|
||||
| Аспект | Требование |
|
||||
|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| API контракт | Без изменений (никакие endpoint'ы не трогаются) |
|
||||
| Совместимость с PH-5/PH-6/PH-8 UI | Полностью совместимо: terrain-popup, bottom-sheets, gps_tracks слой работают как раньше; меняется только порядок UI-вызовов |
|
||||
| Совместимость с ET-007 (Спутник) | Не задействован |
|
||||
| Совместимость с ET-008 (Публичные треки) | Логика `togglePublicTracksFiltersSheet` не меняется; вызов `openSheet('sheet-gps-filters')` теперь корректно закрывает popup |
|
||||
| Совместимость с ET-013 (terrain paint) | Не задействован — paint terrain-слоёв в `applyTerrainLayer` без связи |
|
||||
| Совместимость с MapLibre 4.7.0 | Не задействован — ET-014 не трогает MapLibre API |
|
||||
| localStorage migration | Не нужно. Никаких ключей `localStorage` ET-014 не добавляет и не меняет |
|
||||
| Совместимость со старыми вкладками | Старый `app.js` в кэше браузера продолжает работать со старой багой; при reload браузер дёрнет свежий → fix применится. Никакого hard-reload не нужно |
|
||||
|
||||
## 11. Связанные документы
|
||||
|
||||
- `01-brd.md` §4 (BR-01..BR-06), §9 (R1..R3)
|
||||
- `02-trz.md` §1.3 (корень), §2.1 REQ-F-01..REQ-F-07, §2.2 REQ-NF-01..REQ-NF-05
|
||||
- `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`
|
||||
- `08-data-requirements.md` (этот пакет)
|
||||
- `10-tech-risks.md` (этот пакет)
|
||||
- `docs/work-items/ET-013/07-infra-requirements.md` — образец «zero-infra»
|
||||
work-item (наследие)
|
||||
- `docs/work-items/ET-012/07-infra-requirements.md` — образец «zero-infra»
|
||||
work-item (наследие)
|
||||
264
docs/work-items/ET-014/08-data-requirements.md
Normal file
264
docs/work-items/ET-014/08-data-requirements.md
Normal file
@@ -0,0 +1,264 @@
|
||||
---
|
||||
type: data-requirements
|
||||
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
|
||||
|
||||
## 1. Резюме
|
||||
|
||||
ET-014 — **pure client UI ordering change**. Никаких изменений в данных:
|
||||
ни в БД, ни в файлах на диске, ни в localStorage, ни в API-контрактах,
|
||||
ни в конфигурациях.
|
||||
|
||||
Меняется **порядок вызова двух уже существующих UI-функций** в
|
||||
`src/web/app.js`: при открытии любого `.bottom-sheet` теперь
|
||||
принудительно вызывается helper `closeTerrainPopup()`, который скрывает
|
||||
`#terrain-popup` (если он открыт) и снимает класс `.active` с
|
||||
`#terrain-toggle`.
|
||||
|
||||
**Меняется:**
|
||||
|
||||
- Порядок DOM-операций при `openSheet(id)` (1 дополнительный вызов).
|
||||
- Видимое состояние `#terrain-popup` в момент открытия любого
|
||||
bottom-sheet (теперь скрывается; раньше оставался открытым → визуальный
|
||||
баг ET-014).
|
||||
|
||||
**Не меняется:**
|
||||
|
||||
- Содержимое и схема БД `centralfederal.sqlite`, `gps_tracks.sqlite`.
|
||||
- Содержимое и формат PNG-тайлов в `data/terrain/*`.
|
||||
- Контракты API (`/api/gps-tracks/*`, `/terrain/*`, `/api/route/*`,
|
||||
`/api/health`, прочие).
|
||||
- Ключи `localStorage` (`terrain-hillshade`, `terrain-tri`,
|
||||
`gps-tracks-enabled`, gps-фильтры, theme, units и т. д.).
|
||||
- `style.json`, `style-dark.json`.
|
||||
- `config/*.yaml`.
|
||||
- `src/web/index.html`, `src/web/gps_tracks.js`, `src/web/app.css`.
|
||||
|
||||
## 2. Архитектурные границы данных
|
||||
|
||||
| Слой данных | Тип | Расположение | Изменения в ET-014 |
|
||||
|-----------------------------------|----------------|----------------------------------------------|-------------------------------------------------|
|
||||
| OSM-vector (`trails`) | существующий | `/app/data/centralfederal.sqlite` | **нет** |
|
||||
| Личные GPX треки (ET-006) | существующий | браузер (memory) | **нет** |
|
||||
| Публичные GPS треки (ET-008) | существующий | `/app/data/gps_tracks.sqlite` | **нет** |
|
||||
| OSRM-граф | существующий | `/app/data/enduro.osrm.*` | **нет** |
|
||||
| Terrain hillshade/TRI/hypso PNG | существующий | `data/terrain/*` | **нет** |
|
||||
| User UI state | существующий | `localStorage` | **нет** новых ключей, нет миграции |
|
||||
| MapLibre client tile cache | существующий | браузер (LRU MapLibre) | **нет** |
|
||||
| Серверный кэш | не предусмотрен | n/a | **нет** |
|
||||
| DOM-state `#terrain-popup` | runtime UI | браузер (DOM) | **меняется**: `display:none` при `openSheet()` |
|
||||
| DOM-state `#terrain-toggle` | runtime UI | браузер (DOM) | **меняется**: класс `.active` снимается |
|
||||
| DOM-state `.bottom-sheet` | runtime UI | браузер (DOM) | **не меняется** (та же логика `.open`) |
|
||||
| DOM-state `#sheet-backdrop` | runtime UI | браузер (DOM) | **не меняется** (та же логика `.visible`) |
|
||||
| `closeTerrainOnOutside` listener | runtime UI | браузер (event listener на `document`) | **снимается** через `removeEventListener` |
|
||||
|
||||
## 3. Серверные данные
|
||||
|
||||
### 3.1 БД
|
||||
|
||||
**Без изменений vs ET-013/ET-008.**
|
||||
|
||||
- `centralfederal.sqlite` — read-only для ET-014.
|
||||
- `gps_tracks.sqlite` — read-only для ET-014.
|
||||
- Никаких ALTER/CREATE/INSERT/UPDATE/DELETE.
|
||||
- Никаких миграций.
|
||||
|
||||
### 3.2 Тайлы на диске
|
||||
|
||||
**Без изменений.** `data/terrain/*`, `data/osm/*`, `data/osrm/*` — не
|
||||
трогаются.
|
||||
|
||||
### 3.3 Статика `src/web/`
|
||||
|
||||
| Файл | Изменение |
|
||||
|-----------------------|-----------------------------------------------------------------|
|
||||
| `src/web/app.js` | +1 helper-функция `closeTerrainPopup()` (~7 строк), +1 вызов в `openSheet()` |
|
||||
| `src/web/app.css` | **нет** |
|
||||
| `src/web/index.html` | **нет** |
|
||||
| `src/web/gps_tracks.js` | **нет** |
|
||||
| `src/web/gpx.js` | **нет** |
|
||||
| `src/web/units.js` | **нет** |
|
||||
| `src/web/style.json` | **нет** |
|
||||
| `src/web/style-dark.json` | **нет** |
|
||||
|
||||
Дельта размера `app.js`: ~+300 байт (helper-функция + комментарий +
|
||||
вызов). Пренебрежимо.
|
||||
|
||||
## 4. Клиентские данные
|
||||
|
||||
### 4.1 localStorage
|
||||
|
||||
**Без изменений.** Используются существующие ключи (read-only для
|
||||
ET-014):
|
||||
|
||||
| Ключ | Назначение | Изменения в ET-014 |
|
||||
|----------------------------|---------------------------------------------|--------------------|
|
||||
| `terrain-hillshade` | `'1' | '0'` — чекбокс «Тени рельефа» | **нет** |
|
||||
| `terrain-tri` | `'1' | '0'` — чекбокс «Перепады» | **нет** |
|
||||
| `gps-tracks-enabled` | публичные треки on/off | **нет** |
|
||||
| `gps-filter-*` | состояние фильтров публичных треков | **нет** |
|
||||
| `theme` | `'dark' | 'light'` | **нет** |
|
||||
| `units` | `'km' | 'mi'` | **нет** |
|
||||
| `base-layer` | подложка | **нет** |
|
||||
|
||||
Никакой миграции. Существующие сессии при следующей загрузке
|
||||
автоматически получают исправленное UI-поведение.
|
||||
|
||||
### 4.2 MapLibre LRU (browser-side)
|
||||
|
||||
Без изменений. Тайловый кэш не задействован — мы не меняем тайлы,
|
||||
zoom-уровни, source.minzoom, или paint properties.
|
||||
|
||||
### 4.3 DOM runtime state
|
||||
|
||||
Ниже — единственное место, где ET-014 «меняет данные» (в runtime
|
||||
браузера, не на диске):
|
||||
|
||||
#### `#terrain-popup`
|
||||
|
||||
- **До ET-014**: при клике на `#public-tracks-filters-btn` popup
|
||||
остаётся `display: block`, z=500.
|
||||
- **После ET-014**: при любом `openSheet(id)`, если
|
||||
`popup.style.display !== 'none'`, popup переключается в
|
||||
`display: none`.
|
||||
|
||||
#### `#terrain-toggle`
|
||||
|
||||
- **До ET-014**: при открытии sheet'а сохраняет класс `.active`.
|
||||
- **После ET-014**: при `openSheet(id)` класс `.active` снимается
|
||||
(синхронно с popup'ом).
|
||||
|
||||
#### Event listener `closeTerrainOnOutside` на `document`
|
||||
|
||||
- **До ET-014**: добавлен в `toggleTerrainPopup()` через
|
||||
`addEventListener('click', closeTerrainOnOutside)`. Удаляется в двух
|
||||
местах: повторный клик по `#terrain-toggle` и срабатывание самого
|
||||
`closeTerrainOnOutside`.
|
||||
- **После ET-014**: дополнительно удаляется внутри
|
||||
`closeTerrainPopup()`, который вызывается из `openSheet()`. Двойной
|
||||
`removeEventListener` безвреден (DOM-спека: removeEventListener на
|
||||
отсутствующий listener — no-op).
|
||||
|
||||
### 4.4 In-memory constants
|
||||
|
||||
**Нет.** Никаких новых JS-констант (в отличие от ET-013 с
|
||||
`HILLSHADE_PAINT` / `TRI_PAINT`). Только новая функция и вызов.
|
||||
|
||||
## 5. Контракты API
|
||||
|
||||
### 5.1 Backend endpoints
|
||||
|
||||
**Без изменений.** ET-014 — чистый клиент. Никаких новых вызовов,
|
||||
никакого изменения параметров запросов, никакого изменения частоты
|
||||
запросов.
|
||||
|
||||
| Endpoint | До ET-014 | После ET-014 |
|
||||
|-----------------------------------------|-------------|--------------|
|
||||
| `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt` | без изменений | без изменений |
|
||||
| `GET /api/gps-tracks?bbox=…` | без изменений | без изменений |
|
||||
| `GET /api/gps-tracks/{id}/download` | без изменений | без изменений |
|
||||
| `GET /api/gps-tracks/health` | без изменений | без изменений |
|
||||
| `GET /terrain/{layer}/{z}/{x}/{y}.png` | без изменений | без изменений |
|
||||
| `GET /api/route/*` | без изменений | без изменений |
|
||||
| `GET /api/trails/*` | без изменений | без изменений |
|
||||
|
||||
### 5.2 Frontend internal API (`src/web/app.js`)
|
||||
|
||||
| Функция | До ET-014 | После ET-014 |
|
||||
|-------------------------------|-------------------------------------------------|------------------------------------------------------------------------------|
|
||||
| `openSheet(id)` | публичный (вызывается из всех `toggle*Sheet`) | публичный, контракт сохранён; добавлен внутренний вызов `closeTerrainPopup()` |
|
||||
| `closeSheet(id)` | публичный | без изменений |
|
||||
| `closeAllSheets()` | публичный | без изменений |
|
||||
| `toggleTerrainPopup()` | публичный | без изменений |
|
||||
| `closeTerrainOnOutside(e)` | публичный (выставляется как event handler) | без изменений (опциональный TD-1 рефакторинг описан в ADR-019) |
|
||||
| `closeTerrainPopup()` | **отсутствует** | **новая** publish-функция (для возможного reuse) |
|
||||
|
||||
Контракт `openSheet(id)` совместим со всеми существующими вызовами:
|
||||
|
||||
```bash
|
||||
$ grep -n 'openSheet(' src/web/*.js
|
||||
```
|
||||
|
||||
- `app.js:openSheet(...)` — собственная реализация.
|
||||
- `app.js:openSheet('sheet-route')`, `openSheet('sheet-recon')`,
|
||||
`openSheet('sheet-scenic')`, `openSheet('sheet-link')`,
|
||||
`openSheet('sheet-gpx')` — все продолжают работать как раньше.
|
||||
- `gps_tracks.js:openSheet('sheet-gps-filters')` — продолжает работать;
|
||||
дополнительно теперь корректно закрывает popup.
|
||||
|
||||
## 6. Миграции
|
||||
|
||||
**Нет.** Никаких миграций БД, миграций localStorage, миграций конфигов.
|
||||
|
||||
При деплое в test:
|
||||
- `data/*` — без изменений.
|
||||
- БД — без изменений.
|
||||
- localStorage — старые ключи интерпретируются как раньше.
|
||||
- MapLibre LRU — самоочищается при reload браузера; явной инвал. не нужно.
|
||||
|
||||
## 7. Тестовые данные
|
||||
|
||||
### 7.1 Для unit-тестов
|
||||
|
||||
В ET-014 **новых python unit-тестов не добавляется** — поведение
|
||||
исключительно UI и тестируется через Playwright.
|
||||
|
||||
Опционально (cleanup, не обязательно): тест на статический grep по
|
||||
`src/web/app.js`, что:
|
||||
- Есть функция `closeTerrainPopup`.
|
||||
- В теле `openSheet` есть вызов `closeTerrainPopup()`.
|
||||
|
||||
Если такой тест добавляется, формат — как `test_terrain_paint.py` в
|
||||
ET-013 (`tests/unit/test_ui_z_index_fix.py`, regex по исходнику без
|
||||
JS-runtime). Это **не блокирующий гейт** ET-014.
|
||||
|
||||
### 7.2 Для integration-тестов
|
||||
|
||||
Не применимо. ET-014 не трогает API endpoints, integration-тесты не нужны.
|
||||
|
||||
### 7.3 Для UI-тестов (Playwright)
|
||||
|
||||
`04b-ui-test-cases.md` — TC-UI-01..TC-UI-08:
|
||||
|
||||
- Запускается на test-среде `https://openclaw.mva154.duckdns.org/enduro/`.
|
||||
- Данные — реальные (БД, тайлы) на mva154.
|
||||
- Скриншоты в `tests/e2e/__screenshots__/ET-014/`.
|
||||
- Не пиксельный diff; визуальная приёмка оператором + DOM-assertion'ы
|
||||
(`classList.contains('open')`, `elementFromPoint`,
|
||||
`getBoundingClientRect`).
|
||||
|
||||
## 8. Резервные копии и DR
|
||||
|
||||
**Без изменений.** ET-014 не пишет данных. RPO = 0.
|
||||
|
||||
## 9. Privacy / Compliance
|
||||
|
||||
| Аспект | Требование |
|
||||
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| PII | **Нет.** ET-014 не собирает, не обрабатывает, не передаёт никаких данных |
|
||||
| Licensing | Не применимо |
|
||||
| Attribution | MapLibre attribution control — без изменений |
|
||||
| GDPR / 152-ФЗ | Не применимо |
|
||||
|
||||
## 10. Связанные документы
|
||||
|
||||
- `01-brd.md` §1 (бизнес-контекст), §3 (бизнес-цель), §4 (BR-01..BR-06)
|
||||
- `02-trz.md` §1.1 (DOM-структура), §1.2 (стек z-index), §1.3 (корень),
|
||||
§2 (REQ-F, REQ-NF), §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`
|
||||
- `10-tech-risks.md`
|
||||
- `docs/work-items/ET-013/08-data-requirements.md` — образец «read-only
|
||||
data» документа (наследие)
|
||||
- `docs/work-items/ET-012/08-data-requirements.md` — образец «read-pattern
|
||||
change» документа (наследие)
|
||||
295
docs/work-items/ET-014/10-tech-risks.md
Normal file
295
docs/work-items/ET-014/10-tech-risks.md
Normal file
@@ -0,0 +1,295 @@
|
||||
---
|
||||
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» документа (наследие)
|
||||
Reference in New Issue
Block a user