diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a1c8a5..7825a8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,3 +11,5 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
- Agent system prompts (architect, developer, reviewer, tester, deployer)
- CI pipeline (Gitea Actions)
- Docker configuration
+- ET-002: чекбокс «POI» в попапе рельефа — показ/скрытие маркеров POI
+ с сохранением состояния в localStorage (ключ `poi-visible`)
diff --git a/CLAUDE.md b/CLAUDE.md
index 51242ca..eb1d8c6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -48,6 +48,22 @@
6. Коммиты от имени claude-bot (git config user.name/email уже настроен).
7. Не использовать `--no-verify` без явного одобрения Owner.
+## Фазы
+
+| # | Название | Описание |
+|---|----------|----------|
+| PH-1 | MVP | Карта грунтовок + MVT тайлы |
+| PH-2 | Routing | OSRM роутинг + базовый UI |
+| PH-3 | Smart Route | Альтернативы, статистика, GPX |
+| PH-4 | Advanced Routing | Красивый маршрут, связка, разведка |
+| PH-5 | Redesign | Тёмная тема, mobile UI, UX |
+| PH-6 | Terrain | Hillshade + гипсометрия + TRI |
+| PH-7 | Barriers | Шлагбаумы, тротуары, слой препятствий |
+| PH-8 | Elevation Profile | Профиль высот, режим «Горка» |
+| PH-9 | PWA | Офлайн режим |
+
+Детали каждой фазы: [docs/phases/](docs/phases/)
+
## Данные
- Terrain tiles: /home/slin/enduro-trails/data/terrain/ (hillshade, TRI, hypso)
- OSM данные: /home/slin/enduro-trails/data/osm/
diff --git a/README.md b/README.md
index 0e96dde..33616ce 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,22 @@ migrations/ — миграции БД
.openclaw/ — system prompts агентов
```
+## Фазы
+
+| # | Название | Описание |
+|---|----------|----------|
+| PH-1 | MVP | Карта грунтовок + MVT тайлы |
+| PH-2 | Routing | OSRM роутинг + базовый UI |
+| PH-3 | Smart Route | Альтернативы, статистика, GPX |
+| PH-4 | Advanced Routing | Красивый маршрут, связка, разведка |
+| PH-5 | Redesign | Тёмная тема, mobile UI, UX |
+| PH-6 | Terrain | Hillshade + гипсометрия + TRI |
+| PH-7 | Barriers | Шлагбаумы, тротуары, слой препятствий |
+| PH-8 | Elevation Profile | Профиль высот, режим «Горка» |
+| PH-9 | PWA | Офлайн режим |
+
+Детали каждой фазы: [docs/phases/](docs/phases/)
+
## Лицензия
Данные: © OpenStreetMap contributors (ODbL)
diff --git a/docs/README.md b/docs/README.md
index 049669d..803bf59 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -6,3 +6,15 @@
- [design/](./design/) — дизайн-токены, компоненты
- [operations/](./operations/) — runbook, мониторинг
- [api/](./api/) — OpenAPI спецификация
+
+## Фазы
+
+- [PH-1.mvp](./phases/PH-1.mvp/) — MVP: карта грунтовок + MVT тайлы
+- [PH-2.routing](./phases/PH-2.routing/) — OSRM роутинг + базовый UI
+- [PH-3.smart-route](./phases/PH-3.smart-route/) — Альтернативы, статистика, GPX
+- [PH-4.advanced-routing](./phases/PH-4.advanced-routing/) — Красивый маршрут, связка, разведка
+- [PH-5.redesign](./phases/PH-5.redesign/) — Тёмная тема, mobile UI, UX
+- [PH-6.terrain](./phases/PH-6.terrain/) — Hillshade + гипсометрия + TRI
+- [PH-7.barriers](./phases/PH-7.barriers/) — Шлагбаумы, тротуары, слой препятствий
+- [PH-8.elevation-profile](./phases/PH-8.elevation-profile/) — Профиль высот, режим «Горка»
+- [PH-9.pwa](./phases/PH-9.pwa/) — Офлайн режим
diff --git a/docs/phases/PH-1.mvp/00-phase-brd.md b/docs/phases/PH-1.mvp/00-phase-brd.md
new file mode 100644
index 0000000..5f3ca59
--- /dev/null
+++ b/docs/phases/PH-1.mvp/00-phase-brd.md
@@ -0,0 +1,46 @@
+---
+type: phase-brd
+phase_id: PH-1.mvp
+title: "MVP: карта грунтовок ЦФО + MVT тайлы"
+version: 1
+status: done
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-1 — MVP: карта грунтовок ЦФО
+
+## Цель
+
+Создать веб-карту с фокусом на грунтовые дороги: грунтовки и тропы — главный слой, асфальт — тусклый фон. Данные из OSM, отображение через MapLibre GL JS с кастомным стилем.
+
+## Scope
+
+- Загрузка OSM PBF (Geofabrik, ЦФО + Чувашия)
+- Конвертация в SQLite/Spatialite (1.1M треков, 14K POI)
+- Self-hosted MVT (vector tiles) через FastAPI
+- MapLibre GL JS с кастомным стилем (грунтовки яркие, асфальт тусклый)
+- Деплой через Docker Compose на mva154
+
+## Что реализовано
+
+- FastAPI backend с эндпоинтами для MVT тайлов
+- SQLite/Spatialite БД с данными ЦФО + Чувашия
+- Кастомный стиль MapLibre: цветовая дифференциация по типу покрытия (track grade 1-5)
+- Docker Compose конфигурация
+- nginx reverse proxy (`/enduro/` → контейнер)
+
+## Ключевые решения
+
+| Решение | Причина |
+|---------|---------|
+| MapLibre GL JS (не Leaflet) | WebGL, производительность, vector tiles |
+| Vanilla JS (не React) | Простота, нет build step, быстрый старт |
+| FastAPI (не Django) | Лёгкий, async, минимум зависимостей |
+| SQLite/Spatialite (не PostGIS) | Портативность, zero-config, достаточно для 1 региона |
+| Self-hosted MVT (не TileServer GL) | Меньше зависимостей, контроль над фильтрацией |
+
+## Дата завершения
+
+02.05.2026
diff --git a/docs/phases/PH-2.routing/00-phase-brd.md b/docs/phases/PH-2.routing/00-phase-brd.md
new file mode 100644
index 0000000..b582962
--- /dev/null
+++ b/docs/phases/PH-2.routing/00-phase-brd.md
@@ -0,0 +1,43 @@
+---
+type: phase-brd
+phase_id: PH-2.routing
+title: "Роутинг: OSRM с кастомным эндуро-профилем"
+version: 1
+status: done
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-2 — Роутинг: «Дикий путь»
+
+## Цель
+
+Добавить построение маршрутов с приоритетом грунтовых дорог. Использовать OSRM с кастомным Lua-профилем, оптимизированным под эндуро.
+
+## Scope
+
+- Кастомный профиль `enduro.lua` для OSRM (приоритет грунтовок, штраф за асфальт)
+- Пересборка OSRM графа из OSM PBF (~5.2 GB)
+- Базовый UI для построения маршрута (точка А → Б)
+- Отображение маршрута на карте
+
+## Что реализовано
+
+- OSRM с профилем enduro.lua (веса: track > path > unclassified > tertiary > secondary)
+- API эндпоинт `/api/route` (FastAPI → OSRM)
+- UI: клик по карте для установки точек старта/финиша
+- Отображение маршрута (GeoJSON LineString на карте)
+- Docker-сервис OSRM в compose
+
+## Ключевые решения
+
+| Решение | Причина |
+|---------|---------|
+| OSRM (не GraphHopper) | Быстрый, проверенный, кастомный lua-профиль |
+| Кастомный enduro.lua | Стандартные профили не учитывают грунтовки как приоритет |
+| Swap 6 GB | OSRM граф требует ~5.2 GB RAM |
+
+## Дата завершения
+
+03.05.2026
diff --git a/docs/phases/PH-3.smart-route/00-phase-brd.md b/docs/phases/PH-3.smart-route/00-phase-brd.md
new file mode 100644
index 0000000..a6d3794
--- /dev/null
+++ b/docs/phases/PH-3.smart-route/00-phase-brd.md
@@ -0,0 +1,43 @@
+---
+type: phase-brd
+phase_id: PH-3.smart-route
+title: "Умный маршрут: альтернативы, статистика, GPX"
+version: 1
+status: done
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-3 — Умный маршрут
+
+## Цель
+
+Предоставить пользователю выбор из нескольких вариантов маршрута с разным балансом грунт/асфальт. Показать статистику покрытия и дать возможность экспорта в GPX.
+
+## Scope
+
+- До 5 альтернативных маршрутов с разным балансом грунт/асфальт
+- Промежуточные точки (до 8)
+- Статистика покрытия (% по типам: track, path, unclassified, tertiary, secondary, primary)
+- GPX экспорт (трек + waypoints)
+
+## Что реализовано
+
+- Мульти-запрос к OSRM с варьированием весов профиля
+- UI выбора альтернатив (карточки с превью и статистикой)
+- Drag-and-drop промежуточных точек на карте
+- Панель статистики: дистанция, время, % грунта/асфальта по типам
+- GPX экспорт с метаданными (имя маршрута, waypoints, timestamps)
+
+## Ключевые решения
+
+| Решение | Причина |
+|---------|---------|
+| Варьирование весов (не OSRM alternatives) | OSRM alternatives даёт похожие маршруты; варьирование весов — реально разные |
+| До 8 промежуточных точек | Баланс между гибкостью и UX |
+| GPX 1.1 формат | Совместимость с большинством навигаторов |
+
+## Дата завершения
+
+04.05.2026
diff --git a/docs/phases/PH-4.advanced-routing/00-phase-brd.md b/docs/phases/PH-4.advanced-routing/00-phase-brd.md
new file mode 100644
index 0000000..19cde56
--- /dev/null
+++ b/docs/phases/PH-4.advanced-routing/00-phase-brd.md
@@ -0,0 +1,42 @@
+---
+type: phase-brd
+phase_id: PH-4.advanced-routing
+title: "Продвинутый роутинг: красивый маршрут, связка, разведка"
+version: 1
+status: done
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-4 — Продвинутый роутинг
+
+## Цель
+
+Добавить три специализированных режима маршрутизации: «Красивый маршрут» (замкнутый круг через POI), «Связка» (соединить два трека грунтовками), «Разведка» (статистика грунтовок в радиусе).
+
+## Scope
+
+- **Красивый маршрут** — замкнутый круг заданной длины через живописные POI (озёра, виды, руины, броды)
+- **Связка** — соединить два загруженных трека оптимальным грунтовым участком
+- **Разведка** — показать статистику грунтовок в радиусе 20/50/100 км от точки
+
+## Что реализовано
+
+- Алгоритм «Красивый маршрут»: выбор POI в радиусе → TSP-оптимизация порядка → OSRM route
+- UI загрузки GPX для режима «Связка»
+- Алгоритм соединения: найти ближайшие точки двух треков → построить грунтовый мост
+- «Разведка»: spatial query по SQLite → агрегация по типам → визуализация на карте (heatmap)
+- Переключение режимов в toolbar
+
+## Ключевые решения
+
+| Решение | Причина |
+|---------|---------|
+| TSP через nearest-neighbor heuristic | Достаточно для 5-10 POI, O(n²) приемлемо |
+| Spatialite для spatial queries | Уже есть в стеке, не нужен отдельный сервис |
+| Радиусы 20/50/100 км | Покрывают типичные дневные маршруты |
+
+## Дата завершения
+
+04.05.2026
diff --git a/docs/phases/PH-5.redesign/00-phase-brd.md b/docs/phases/PH-5.redesign/00-phase-brd.md
new file mode 100644
index 0000000..99134e4
--- /dev/null
+++ b/docs/phases/PH-5.redesign/00-phase-brd.md
@@ -0,0 +1,50 @@
+---
+type: phase-brd
+phase_id: PH-5.redesign
+title: "Редизайн: тёмная тема, mobile UI, UX"
+version: 1
+status: done
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-5 — Редизайн
+
+## Цель
+
+Переработать UI/UX приложения: добавить тёмную/светлую тему с автопереключением, адаптировать под мобильные устройства, улучшить юзабилити (линейка, метки, поиск).
+
+## Scope
+
+- Тёмная/светлая тема (авто по SunCalc — переключение по закату/рассвету)
+- Mobile UI: bottom sheets, toolbar, touch-оптимизация
+- Линейка (измерение расстояний на карте)
+- Метки (сохранение точек интереса, localStorage)
+- Поиск (Nominatim geocoding)
+- CSS custom properties для theming
+- Синхронизация темы карты и UI
+
+## Что реализовано
+
+- Система тем: CSS custom properties, авто (SunCalc), ручное переключение
+- Стиль карты MapLibre синхронизирован с UI-темой
+- Bottom sheet для мобильных (маршрут, статистика, настройки)
+- Toolbar с иконками (режимы, инструменты)
+- Touch-события: long press для метки, swipe для bottom sheet
+- Линейка: клик-клик для измерения, отображение дистанции
+- Метки: создание, редактирование, удаление, persist в localStorage
+- Поиск: Nominatim API, debounce, fly-to результату
+
+## Ключевые решения
+
+| Решение | Причина |
+|---------|---------|
+| SunCalc для авто-темы | Точное время заката для координат пользователя |
+| CSS custom properties | Нативный theming без препроцессоров |
+| Bottom sheets (не sidebar) | Мобильный паттерн, thumb-friendly |
+| localStorage для меток | Простота, нет backend-зависимости |
+
+## Дата завершения
+
+05-06.05.2026
diff --git a/docs/phases/PH-6.terrain/00-phase-brd.md b/docs/phases/PH-6.terrain/00-phase-brd.md
new file mode 100644
index 0000000..0ea904b
--- /dev/null
+++ b/docs/phases/PH-6.terrain/00-phase-brd.md
@@ -0,0 +1,48 @@
+---
+type: phase-brd
+phase_id: PH-6.terrain
+title: "Рельеф: гипсометрия + hillshade + TRI"
+version: 1
+status: done
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-6 — Рельеф
+
+## Цель
+
+Добавить визуализацию рельефа: гипсометрическая раскраска, hillshade (теневой рельеф), TRI (Terrain Ruggedness Index) для оценки сложности местности.
+
+## Scope
+
+- Загрузка и обработка SRTM 30м (NASA, public domain)
+- Генерация raster tiles: гипсометрия, hillshade, TRI
+- Раздача через nginx (статические файлы)
+- UI: переключение слоёв рельефа, прозрачность
+- Легенда для гипсометрии и TRI
+
+## Что реализовано
+
+- Pipeline обработки SRTM: скачивание → merge → reproject → tile generation
+- Гипсометрия: цветовая шкала высот (зелёный → коричневый → белый)
+- Hillshade: azimuth 315°, altitude 45°, z-factor 1.5
+- TRI: классификация (flat, nearly flat, slightly rugged, rugged, very rugged)
+- Raster tiles zoom 8-14, формат PNG
+- nginx location для раздачи tiles
+- UI: layer switcher с opacity slider
+- Легенда с цветовой шкалой
+
+## Ключевые решения
+
+| Решение | Причина |
+|---------|---------|
+| Raster tiles (не Mapbox Terrain RGB) | Простота генерации, nginx отдаёт статику |
+| SRTM 30м | Бесплатно, достаточно для ЦФО (равнина) |
+| Zoom 8-14 | Баланс между детализацией и объёмом данных |
+| TRI как отдельный слой | Помогает оценить сложность без профиля высот |
+
+## Дата завершения
+
+12-14.05.2026
diff --git a/docs/phases/PH-7.barriers/00-phase-brd.md b/docs/phases/PH-7.barriers/00-phase-brd.md
new file mode 100644
index 0000000..751c09f
--- /dev/null
+++ b/docs/phases/PH-7.barriers/00-phase-brd.md
@@ -0,0 +1,51 @@
+---
+type: phase-brd
+phase_id: PH-7.barriers
+title: "Барьеры: исключить шлагбаумы и тротуары, слой препятствий"
+version: 1
+status: active
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-7 — Барьеры
+
+## Цель
+
+Сделать роутинг безопасным: маршрут не проходит через физические препятствия (шлагбаумы) и запрещённые для мотоциклов дороги (тротуары, пешеходные зоны). Добавить визуальный слой препятствий на карту.
+
+## Scope
+
+### F-07: Исключить шлагбаумы из OSRM
+- Ноды с `barrier=gate|bollard|lift_gate|chain|cycle_barrier|motorcycle_barrier|border_control|block` → `mode.inaccessible` в OSRM
+- `cattle_grid` и `ford` — оставить (проезжие)
+
+### F-08: Исключить тротуары из OSRM
+- Ways с `highway=footway|pedestrian|steps|corridor` → исключить из графа (return в process_way)
+
+### F-10: Слой препятствий на карте
+- Визуализация шлагбаумов, ворот, блоков на карте
+- Иконки по типу барьера
+- Popup с информацией (тип, OSM ID)
+
+## Метрики успеха
+
+- Маршрут через точку с шлагбаумом → OSRM обходит или возвращает "не найден"
+- Маршрут в городе → не проходит по тротуарам
+- Время пересборки графа ≤ 60 мин
+- Существующие маршруты без шлагбаумов/тротуаров — не ломаются
+
+## Зависимости
+
+- OSRM граф (пересборка с обновлённым enduro.lua)
+- OSM PBF данные (уже есть)
+- Work item: [ET-001](../../work-items/ET-001/)
+
+## Риски
+
+| Риск | Митигация |
+|------|-----------|
+| Пересборка графа ~40 мин (сервис недоступен) | Пересобирать ночью или в low-traffic |
+| Слишком много заблокированных нод → маршруты не строятся | cattle_grid и ford оставлены; тестировать на реальных маршрутах |
+| OSRM RAM при пересборке | Swap 6 GB уже настроен |
diff --git a/docs/phases/PH-8.elevation-profile/00-phase-brd.md b/docs/phases/PH-8.elevation-profile/00-phase-brd.md
new file mode 100644
index 0000000..ce51885
--- /dev/null
+++ b/docs/phases/PH-8.elevation-profile/00-phase-brd.md
@@ -0,0 +1,35 @@
+---
+type: phase-brd
+phase_id: PH-8.elevation-profile
+title: "Профиль высот на маршруте, режим «Горка»"
+version: 1
+status: planned
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-8 — Профиль высот
+
+## Цель
+
+Показать профиль высот построенного маршрута. Добавить режим «Горка» — поиск участков с максимальным перепадом высот (подъёмы/спуски).
+
+## Scope
+
+- Профиль высот: график elevation по дистанции для построенного маршрута
+- Данные высот из SRTM 30м (уже есть в PH-6)
+- Интерактивный график: hover → маркер на карте
+- Статистика: набор высоты, сброс высоты, макс/мин высота
+- Режим «Горка»: поиск участков с перепадом > N метров в радиусе
+
+## Зависимости
+
+- PH-6 (SRTM данные, terrain pipeline)
+- PH-2/PH-3 (роутинг, маршруты)
+
+## Открытые вопросы
+
+- Формат графика: Canvas или SVG?
+- Сэмплирование точек: каждые N метров или адаптивное?
+- «Горка»: порог перепада настраиваемый или фиксированный?
diff --git a/docs/phases/PH-9.pwa/00-phase-brd.md b/docs/phases/PH-9.pwa/00-phase-brd.md
new file mode 100644
index 0000000..64d121a
--- /dev/null
+++ b/docs/phases/PH-9.pwa/00-phase-brd.md
@@ -0,0 +1,37 @@
+---
+type: phase-brd
+phase_id: PH-9.pwa
+title: "PWA: офлайн режим, кэширование тайлов"
+version: 1
+status: planned
+created_at: 2026-05-18
+authors:
+ - "agent:stream"
+---
+
+# PH-9 — PWA офлайн
+
+## Цель
+
+Сделать приложение доступным офлайн: Service Worker для кэширования, предзагрузка тайлов для выбранного региона, сохранение маршрутов локально.
+
+## Scope
+
+- Service Worker: кэширование статики (JS, CSS, шрифты, иконки)
+- Предзагрузка тайлов: выбрать область на карте → скачать тайлы для офлайн
+- Кэширование vector tiles (MVT) и raster tiles (terrain)
+- Офлайн-роутинг: сохранение построенных маршрутов для просмотра без сети
+- Install prompt (Add to Home Screen)
+- Manifest.json
+
+## Зависимости
+
+- PH-1 (MVT тайлы)
+- PH-6 (raster terrain tiles)
+- Все предыдущие фазы (UI, роутинг)
+
+## Открытые вопросы
+
+- Лимит кэша (сколько тайлов можно скачать?)
+- Стратегия инвалидации кэша
+- Офлайн-роутинг: только просмотр сохранённых или локальный OSRM?
diff --git a/docs/phases/pilot/00-phase-brd.md b/docs/phases/pilot/00-phase-brd.md
deleted file mode 100644
index 0034cb1..0000000
--- a/docs/phases/pilot/00-phase-brd.md
+++ /dev/null
@@ -1,122 +0,0 @@
----
-type: phase-brd
-phase_id: pilot
-title: "Enduro Trails — пилотный проект мультиагентной разработки"
-version: 1
-status: approved
-created_at: 2026-05-15
-authors:
- - "agent:stream"
- - "human:slava"
----
-
-# BRD — Enduro Trails (пилотный проект)
-
-## 1. Цель и метрика успеха
-
-**Цель:** Создать веб-приложение для планирования эндуро-маршрутов с визуализацией грунтовых дорог, рельефа и навигацией — как пилотный проект мультиагентной системы разработки.
-
-**Метрики успеха:**
-- Рабочее приложение доступно по URL (https://openclaw.mva154.duckdns.org/enduro/)
-- Покрытие региона: ЦФО + Чувашия (1.1M треков, 14K POI)
-- Построение маршрута < 5 секунд
-- Мобильный UI (PWA-ready)
-- Агентный конвейер: фича от постановки до деплоя ≤ 4 часа
-
-## 2. Стейкхолдеры
-
-| Роль | Кто | Интерес |
-|------|-----|---------|
-| Заказчик / Owner | Слава | Использует для планирования поездок |
-| Analyst | Стрим (OpenClaw) | BRD, ТЗ, координация |
-| Разработка | Claude Code CLI агенты | Architect, Developer, Reviewer, Tester, Deployer |
-
-## 3. Scope
-
-### В скоупе
-- Карта грунтовых дорог (MapLibre GL JS, кастомный стиль)
-- Роутинг «Дикий путь» (OSRM, кастомный профиль enduro.lua)
-- Альтернативные маршруты (до 5 вариантов)
-- Промежуточные точки (до 8)
-- Статистика покрытия (% грунт/асфальт по типам)
-- «Красивый маршрут» (замкнутый круг через POI)
-- «Связка» (соединить два трека)
-- «Разведка» (грунтовки в радиусе)
-- Рельеф (гипсометрия + hillshade, SRTM 30м)
-- TRI (Terrain Ruggedness Index)
-- Линейка, метки, GPX экспорт
-- Тёмная/светлая тема (авто по SunCalc)
-- Мобильный UI (bottom sheets, toolbar, touch)
-- Поиск (Nominatim geocoding)
-
-### Вне скоупа (v1)
-- PWA офлайн режим
-- GPS-трекинг в реальном времени
-- Народные треки (Wikiloc, Komoot)
-- Профиль высот на маршруте
-- Мультирегион (вся Россия)
-- Нативное мобильное приложение
-
-## 4. Архитектура
-
-### Компоненты
-- **Frontend** — MapLibre GL JS, vanilla JS (ES modules), CSS custom properties
-- **Backend API** — FastAPI (Python 3.12), uvicorn (4 workers)
-- **Tile Server** — статические raster tiles через nginx
-- **Vector Tiles** — MVT из SQLite (self-hosted, FastAPI)
-- **Routing Engine** — OSRM с кастомным профилем `enduro.lua`
-- **Database** — SQLite + Spatialite (431 MB)
-- **Reverse Proxy** — nginx (`/enduro/` → контейнер)
-
-### Инфраструктура
-- Один сервер: mva154 (82.22.50.71)
-- Docker Compose
-- Gitea (git + CI)
-- Plane (управление задачами)
-
-### Данные
-- OSM PBF (Geofabrik, ЦФО + Чувашия)
-- SRTM 30м (NASA, public domain)
-- OSRM граф (~5.2 GB)
-
-## 5. Реализованные фазы
-
-| Фаза | Описание | Статус | Дата |
-|------|----------|--------|------|
-| 1 | MVP: карта + MVT тайлы | ✅ | 02.05.2026 |
-| 2 | Роутинг + базовый UI | ✅ | 03.05.2026 |
-| 3 | Умный маршрут (альтернативы, статистика, GPX) | ✅ | 04.05.2026 |
-| 4 | Продвинутый роутинг (красивый, связка, разведка) | ✅ | 04.05.2026 |
-| 5 | Редизайн (тёмная тема, mobile UI, UX) | ✅ | 05-06.05.2026 |
-| 5.4 | Рельеф (hillshade + гипсометрия + TRI) | ✅ | 12-14.05.2026 |
-
-## 6. Бэклог
-
-| Фаза | Описание | Приоритет |
-|------|----------|-----------|
-| 3.1 | Улучшение роутинга (шлагбаумы, тротуары, слой препятствий) | Высокий |
-| 6 | SRTM продвинутый (профиль высот, «Горка») | Средний |
-| 7 | PWA + офлайн | Средний |
-| 8 | Народные треки | Низкий |
-
-## 7. Ключевые решения
-
-| Решение | Причина |
-|---------|---------|
-| MapLibre GL JS (не Leaflet) | WebGL, производительность, vector tiles |
-| Vanilla JS (не React) | Простота, нет build step, быстрый старт |
-| FastAPI (не Django) | Лёгкий, async, минимум зависимостей |
-| SQLite/Spatialite (не PostGIS) | Портативность, zero-config, достаточно для 1 региона |
-| OSRM (не GraphHopper) | Быстрый, проверенный, кастомный lua-профиль |
-| Self-hosted MVT (не TileServer GL) | Меньше зависимостей, контроль над фильтрацией |
-| Raster tiles для terrain (не Mapbox Terrain RGB) | Простота генерации, nginx отдаёт статику |
-| Docker Compose | Один файл — весь стек |
-
-## 8. Риски
-
-| Риск | Митигация |
-|------|-----------|
-| OSRM граф большой (5.2 GB RAM) | Swap 6 GB настроен |
-| SQLite не масштабируется | Миграция на PostGIS при необходимости |
-| Один сервер — single point of failure | Бэкапы, Docker restart policy |
-| SRTM 30м недостаточно для крутых склонов | Достаточно для ЦФО (равнина) |
diff --git a/docs/phases/pilot/01-phase-plan.md b/docs/phases/pilot/01-phase-plan.md
deleted file mode 100644
index 2a9e4d6..0000000
--- a/docs/phases/pilot/01-phase-plan.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-type: phase-plan
-phase_id: pilot
-title: "План пилотной фазы"
-version: 1
-status: active
----
-
-# План пилотной фазы — Enduro Trails
-
-## Текущий статус
-
-Проект в стадии перехода от прототипа к управляемой мультиагентной разработке.
-
-### Выполнено (прототип, 02-14.05.2026)
-- Фазы 1-5.4 реализованы вручную (Стрим + Dev-агент)
-- Все основные фичи работают
-- Приложение доступно по URL
-
-### В процессе (мультиагентная инфраструктура, 15.05.2026)
-- ✅ Репо в Gitea с канонической структурой
-- ✅ Claude Code CLI авторизован
-- ✅ Service account claude-bot
-- ✅ Branch protection
-- ✅ CI pipeline
-- ✅ System prompts агентов
-- 🔄 Миграция прототипа в репо
-
-### Следующие шаги
-1. Merge миграции в main
-2. Первая задача через полный агентный конвейер (Фаза 1 мультиагентного BRD)
-3. Orchestrator MVP (Фаза 2 мультиагентного BRD)
-
-## Приоритет фич (бэклог)
-
-1. **F-07 + F-08** — Исключить шлагбаумы и тротуары из OSRM (пересборка графа)
-2. **F-10** — Слой препятствий на карте
-3. **F-09** — Больше альтернатив (penalized re-query)
-4. Профиль высот на маршруте
-5. PWA офлайн
diff --git a/docs/work-items/ET-002/00-business-request.md b/docs/work-items/ET-002/00-business-request.md
new file mode 100644
index 0000000..afeb911
--- /dev/null
+++ b/docs/work-items/ET-002/00-business-request.md
@@ -0,0 +1,20 @@
+---
+type: business-request
+work_item_id: ET-002
+title: "Чекбокс показа/скрытия POI на карте"
+created_at: 2026-05-20
+source: telegram
+requester: Слава
+---
+
+# Исходный запрос — ET-002
+
+## Формулировка заказчика
+
+> На карте сейчас всегда отражаются маркеры POI. Нужен в кнопке рельефа добавить чекбокс показывать/не показывать POI.
+
+## Контекст
+
+- Канал: Telegram
+- Дата: 2026-05-19
+- Приоритет: не указан (обычный)
diff --git a/docs/work-items/ET-002/01-brd.md b/docs/work-items/ET-002/01-brd.md
new file mode 100644
index 0000000..c8b83f4
--- /dev/null
+++ b/docs/work-items/ET-002/01-brd.md
@@ -0,0 +1,61 @@
+---
+type: brd
+work_item_id: ET-002
+title: "BRD: Чекбокс показа/скрытия POI в попапе рельефа"
+version: 1
+status: approved
+created_at: 2026-05-20
+authors:
+ - "agent:analyst"
+---
+
+# BRD — ET-002: Чекбокс показа/скрытия POI в попапе рельефа
+
+## 1. Цель
+
+Дать пользователю возможность скрывать маркеры POI на карте, чтобы они не загромождали обзор при планировании маршрута. Управление — через чекбокс в существующем попапе кнопки «Рельеф».
+
+## 2. Контекст
+
+- Сейчас POI (слои `poi-circles`, `poi-labels`) отображаются всегда при загрузке карты.
+- Попап кнопки «Рельеф» (`terrain-popup`) уже содержит чекбоксы: Тени рельефа, Перепады, Грунтовки, Тропы.
+- Механизм `toggleLayer('poi')` уже реализован в коде, но не привязан к UI в попапе.
+
+## 3. Scope
+
+### In scope
+
+- Добавить чекбокс «POI» в попап кнопки «Рельеф» (после секции «Тропы»)
+- POI включены по умолчанию (checked)
+- Состояние чекбокса сохраняется в localStorage (ключ `poi-visible`)
+- При загрузке страницы — восстанавливать состояние из localStorage
+- Скрытие/показ слоёв `poi-circles` и `poi-labels` через MapLibre `setLayoutProperty`
+
+### Out of scope
+
+- Фильтрация POI по типу (кафе, заправки, и т.д.)
+- Отдельная кнопка для POI вне попапа рельефа
+- Изменение иконок или стилей POI
+- Backend-изменения
+
+## 4. Метрики успеха
+
+| Метрика | Критерий |
+|---------|----------|
+| Чекбокс отображается в попапе | Визуально присутствует после «Тропы» |
+| Снятие чекбокса скрывает POI | Слои `poi-circles` и `poi-labels` получают visibility: none |
+| Установка чекбокса показывает POI | Слои получают visibility: visible |
+| Состояние сохраняется | После перезагрузки страницы чекбокс и видимость POI соответствуют последнему выбору |
+| Не ломает существующий функционал | Грунтовки, тропы, рельеф, роутинг работают как прежде |
+
+## 5. Риски
+
+| Риск | Вероятность | Митигация |
+|------|-------------|-----------|
+| Конфликт с существующим toggleLayer('poi') | Низкая | Использовать тот же механизм layerState, не дублировать логику |
+| Попап становится слишком длинным на мобильных | Низкая | Один чекбокс — минимальное увеличение высоты |
+
+## 6. Зависимости
+
+- Нет внешних зависимостей
+- Только фронтенд (vanilla JS + HTML)
diff --git a/docs/work-items/ET-002/02-trz.md b/docs/work-items/ET-002/02-trz.md
new file mode 100644
index 0000000..be7bbe6
--- /dev/null
+++ b/docs/work-items/ET-002/02-trz.md
@@ -0,0 +1,98 @@
+---
+type: trz
+work_item_id: ET-002
+title: "ТЗ: Чекбокс показа/скрытия POI в попапе рельефа"
+version: 1
+status: approved
+created_at: 2026-05-20
+authors:
+ - "agent:analyst"
+---
+
+# ТЗ — ET-002: Чекбокс показа/скрытия POI в попапе рельефа
+
+## 1. Функциональные требования
+
+### REQ-F-01: Чекбокс POI в попапе рельефа
+
+Система должна отображать чекбокс «POI» в попапе кнопки «Рельеф» (`terrain-popup`), расположенный после чекбокса «Тропы» (`trails-path-cb`), отделённый горизонтальной линией (`
`).
+
+### REQ-F-02: Состояние по умолчанию
+
+При первом посещении (отсутствие ключа в localStorage) чекбокс POI должен быть включён (checked), маркеры POI видимы.
+
+### REQ-F-03: Скрытие POI
+
+При снятии чекбокса система должна установить `visibility: 'none'` для слоёв `poi-circles` и `poi-labels` через `map.setLayoutProperty()`.
+
+### REQ-F-04: Показ POI
+
+При установке чекбокса система должна установить `visibility: 'visible'` для слоёв `poi-circles` и `poi-labels`.
+
+### REQ-F-05: Сохранение состояния
+
+Система должна сохранять состояние чекбокса в `localStorage` под ключом `poi-visible` (значения: `'1'` — показывать, `'0'` — скрывать).
+
+### REQ-F-06: Восстановление состояния при загрузке
+
+При загрузке страницы система должна:
+1. Прочитать значение `localStorage.getItem('poi-visible')`
+2. Если значение `'0'` — снять чекбокс и скрыть слои POI
+3. Если значение `'1'` или отсутствует (null) — установить чекбокс и показать слои POI
+
+### REQ-F-07: Синхронизация с layerState
+
+Изменение чекбокса должно обновлять `layerState.poi` (true/false), чтобы состояние было консистентно с внутренней моделью приложения.
+
+## 2. Нефункциональные требования
+
+### REQ-NF-01: Производительность
+
+Переключение видимости POI должно происходить мгновенно (< 50 мс), без перезагрузки тайлов.
+
+### REQ-NF-02: Совместимость
+
+Решение должно работать во всех браузерах, поддерживающих MapLibre GL JS (Chrome 80+, Firefox 78+, Safari 14+, Edge 80+).
+
+### REQ-NF-03: Мобильная адаптация
+
+Чекбокс должен быть доступен и нажимаем на мобильных устройствах (touch target ≥ 44px).
+
+### REQ-NF-04: Отсутствие регрессий
+
+Существующие чекбоксы (Тени рельефа, Перепады, Грунтовки, Тропы) должны продолжать работать без изменений.
+
+## 3. UI-спецификация
+
+### Расположение в HTML
+
+```html
+
+
+
+```
+
+### Стилизация
+
+Использовать существующий класс `terrain-checkbox` — единообразие с остальными чекбоксами в попапе.
+
+## 4. Данные
+
+- localStorage ключ: `poi-visible`
+- Значения: `'1'` (показывать) | `'0'` (скрывать)
+- MapLibre слои: `poi-circles`, `poi-labels`
+- Состояние в JS: `layerState.poi`
+
+## 5. API
+
+Нет изменений в backend API. Вся логика — клиентская.
+
+## 6. Затрагиваемые файлы
+
+| Файл | Изменение |
+|------|-----------|
+| `src/web/index.html` | Добавить чекбокс в `terrain-popup` |
+| `src/web/app.js` | Добавить функцию `onPoiCheckbox()`, логику восстановления состояния при загрузке |
diff --git a/docs/work-items/ET-002/03-acceptance-criteria.md b/docs/work-items/ET-002/03-acceptance-criteria.md
new file mode 100644
index 0000000..ce706a1
--- /dev/null
+++ b/docs/work-items/ET-002/03-acceptance-criteria.md
@@ -0,0 +1,85 @@
+---
+type: acceptance-criteria
+work_item_id: ET-002
+title: "Acceptance Criteria: Чекбокс POI"
+version: 1
+status: approved
+created_at: 2026-05-20
+authors:
+ - "agent:analyst"
+---
+
+# Acceptance Criteria — ET-002
+
+## AC-01: Чекбокс отображается в попапе
+
+```gherkin
+Given пользователь открыл карту
+When пользователь нажимает кнопку «Рельеф»
+Then в попапе отображается чекбокс «POI» после секции «Тропы»
+And чекбокс отделён горизонтальной линией от секции выше
+```
+
+## AC-02: POI включены по умолчанию
+
+```gherkin
+Given пользователь открыл карту впервые (нет ключа poi-visible в localStorage)
+When попап рельефа открыт
+Then чекбокс «POI» установлен (checked)
+And маркеры POI видны на карте
+```
+
+## AC-03: Скрытие POI
+
+```gherkin
+Given чекбокс «POI» установлен
+And маркеры POI видны на карте
+When пользователь снимает чекбокс «POI»
+Then маркеры POI исчезают с карты
+And слои poi-circles и poi-labels имеют visibility: none
+```
+
+## AC-04: Показ POI
+
+```gherkin
+Given чекбокс «POI» снят
+And маркеры POI скрыты
+When пользователь устанавливает чекбокс «POI»
+Then маркеры POI появляются на карте
+And слои poi-circles и poi-labels имеют visibility: visible
+```
+
+## AC-05: Состояние сохраняется после перезагрузки
+
+```gherkin
+Given пользователь снял чекбокс «POI»
+When пользователь перезагружает страницу
+Then чекбокс «POI» снят
+And маркеры POI скрыты
+```
+
+## AC-06: Восстановление включённого состояния
+
+```gherkin
+Given пользователь установил чекбокс «POI»
+When пользователь перезагружает страницу
+Then чекбокс «POI» установлен
+And маркеры POI видны
+```
+
+## AC-07: Не ломает существующие чекбоксы
+
+```gherkin
+Given попап рельефа открыт
+When пользователь переключает чекбоксы «Тени рельефа», «Перепады», «Грунтовки», «Тропы»
+Then каждый чекбокс работает как прежде
+And чекбокс «POI» не влияет на их поведение
+```
+
+## AC-08: Синхронизация с layerState
+
+```gherkin
+Given чекбокс «POI» снят
+When внешний код читает layerState.poi
+Then значение равно false
+```
diff --git a/docs/work-items/ET-002/04-test-plan.yaml b/docs/work-items/ET-002/04-test-plan.yaml
new file mode 100644
index 0000000..85d842d
--- /dev/null
+++ b/docs/work-items/ET-002/04-test-plan.yaml
@@ -0,0 +1,116 @@
+---
+type: test-plan
+work_item_id: ET-002
+title: "Test Plan: Чекбокс POI"
+version: 1
+status: approved
+created_at: 2026-05-20
+authors:
+ - "agent:analyst"
+---
+
+tests:
+ - id: TP-01
+ type: unit
+ description: "onPoiCheckbox() устанавливает visibility слоёв"
+ steps:
+ - "Mock map.getLayer() → true"
+ - "Mock map.setLayoutProperty()"
+ - "Установить checkbox.checked = false"
+ - "Вызвать onPoiCheckbox()"
+ expected:
+ - "setLayoutProperty('poi-circles', 'visibility', 'none') вызван"
+ - "setLayoutProperty('poi-labels', 'visibility', 'none') вызван"
+ - "localStorage.setItem('poi-visible', '0') вызван"
+ - "layerState.poi === false"
+
+ - id: TP-02
+ type: unit
+ description: "onPoiCheckbox() показывает слои при checked=true"
+ steps:
+ - "Mock map.getLayer() → true"
+ - "Установить checkbox.checked = true"
+ - "Вызвать onPoiCheckbox()"
+ expected:
+ - "setLayoutProperty('poi-circles', 'visibility', 'visible') вызван"
+ - "setLayoutProperty('poi-labels', 'visibility', 'visible') вызван"
+ - "localStorage.setItem('poi-visible', '1') вызван"
+ - "layerState.poi === true"
+
+ - id: TP-03
+ type: unit
+ description: "Восстановление состояния при загрузке — POI скрыты"
+ steps:
+ - "localStorage.setItem('poi-visible', '0')"
+ - "Вызвать функцию инициализации POI"
+ expected:
+ - "checkbox.checked === false"
+ - "layerState.poi === false"
+ - "Слои скрыты"
+
+ - id: TP-04
+ type: unit
+ description: "Восстановление состояния при загрузке — POI видны (default)"
+ steps:
+ - "localStorage не содержит ключ poi-visible"
+ - "Вызвать функцию инициализации POI"
+ expected:
+ - "checkbox.checked === true"
+ - "layerState.poi === true"
+
+ - id: TP-05
+ type: e2e
+ description: "Чекбокс POI виден в попапе рельефа"
+ steps:
+ - "Открыть карту в браузере"
+ - "Нажать кнопку «Рельеф»"
+ expected:
+ - "В попапе виден чекбокс с текстом «POI»"
+ - "Чекбокс расположен после «Тропы», отделён линией"
+
+ - id: TP-06
+ type: e2e
+ description: "Переключение POI скрывает/показывает маркеры"
+ steps:
+ - "Открыть карту, дождаться загрузки POI"
+ - "Открыть попап рельефа"
+ - "Снять чекбокс POI"
+ - "Убедиться что маркеры исчезли"
+ - "Установить чекбокс POI"
+ - "Убедиться что маркеры появились"
+ expected:
+ - "Маркеры POI скрываются и появляются в соответствии с чекбоксом"
+
+ - id: TP-07
+ type: e2e
+ description: "Состояние POI сохраняется после перезагрузки"
+ steps:
+ - "Открыть карту"
+ - "Снять чекбокс POI"
+ - "Перезагрузить страницу"
+ - "Открыть попап рельефа"
+ expected:
+ - "Чекбокс POI снят"
+ - "Маркеры POI не отображаются"
+
+ - id: TP-08
+ type: integration
+ description: "Чекбокс POI не влияет на другие слои"
+ steps:
+ - "Включить Тени рельефа и Грунтовки"
+ - "Снять чекбокс POI"
+ expected:
+ - "Тени рельефа остаются видимыми"
+ - "Грунтовки остаются видимыми"
+ - "Только POI скрыты"
+
+ - id: TP-09
+ type: e2e
+ description: "Мобильная доступность чекбокса"
+ steps:
+ - "Открыть карту на мобильном viewport (375px)"
+ - "Открыть попап рельефа"
+ - "Тапнуть чекбокс POI"
+ expected:
+ - "Чекбокс нажимается без проблем"
+ - "Touch target достаточный (≥ 44px)"
diff --git a/docs/work-items/ET-002/06-adr/adr-0001-poi-visibility-client-side.md b/docs/work-items/ET-002/06-adr/adr-0001-poi-visibility-client-side.md
new file mode 100644
index 0000000..fffc5a1
--- /dev/null
+++ b/docs/work-items/ET-002/06-adr/adr-0001-poi-visibility-client-side.md
@@ -0,0 +1,139 @@
+---
+type: adr
+work_item_id: ET-002
+adr_id: adr-0001
+title: "ADR-0001: Управление видимостью POI — клиентское решение на localStorage"
+status: accepted
+created_at: 2026-05-21
+authors:
+ - "agent:architect"
+supersedes: []
+superseded_by: []
+labels: []
+---
+
+# ADR-0001 — Управление видимостью POI: клиентское решение на localStorage
+
+## Статус
+
+Accepted
+
+## Контекст
+
+ET-002 добавляет в попап кнопки «Рельеф» (`terrain-popup`) чекбокс «POI»,
+позволяющий пользователю скрывать маркеры POI (слои `poi-circles`,
+`poi-labels`), чтобы не загромождать карту при планировании маршрута.
+
+Существующее состояние кодовой базы (`src/web/app.js`):
+
+- объект `layerState = { tracks, paths, poi, basemap }` — внутренняя модель
+ видимости слоёв (строка 377), `poi: true` по умолчанию;
+- карта групп слоёв `poi: ['poi-circles', 'poi-labels']` (строка 381);
+- функция `toggleLayer(group)` уже переключает видимость через
+ `map.setLayoutProperty(..., 'visibility', ...)` (строки 385–389), но
+ завязана на кнопку-тулбар (`btn.classList.toggle('active', ...)`), а не на
+ чекбокс попапа;
+- сложился устойчивый паттерн персистентности в `localStorage` со
+ значениями `'1'`/`'0'`: ключи `terrain-hillshade`, `terrain-tri`,
+ `trails-track`, `trails-path`, `enduro-theme-mode`, `MARKERS_KEY`.
+
+Backend (FastAPI) и БД (SQLite/Spatialite) к видимости POI отношения не
+имеют — POI отдаются как часть тайлов/источника, видимость регулируется
+исключительно на уровне рендера MapLibre.
+
+## Рассматриваемые варианты
+
+### Вариант A — Клиентское решение: `setLayoutProperty` + `localStorage` (выбран)
+
+Видимость переключается через `map.setLayoutProperty()` для слоёв
+`poi-circles` и `poi-labels`; состояние хранится в `localStorage` под
+ключом `poi-visible`; `layerState.poi` остаётся единственным источником
+истины в рантайме.
+
+- **Плюсы:** нулевые изменения backend/БД/инфраструктуры; переключение
+ мгновенное (тайлы не перезагружаются — слой остаётся в источнике,
+ меняется только `layout.visibility`); полностью консистентно с уже
+ существующими чекбоксами попапа и принципом «минимум зависимостей».
+- **Минусы:** настройка не синхронизируется между устройствами/браузерами
+ (для данной фичи это приемлемо и зафиксировано как Out of scope в BRD).
+
+### Вариант B — Хранение настройки на backend (профиль пользователя)
+
+Видимость POI сохраняется через новый API-эндпоинт в SQLite.
+
+- **Плюсы:** синхронизация между устройствами.
+- **Минусы:** требует модели пользователя/сессий (в проекте отсутствует),
+ новый эндпоинт, миграцию БД, сетевой round-trip — несопоставимо
+ избыточно для одного UI-флага. Противоречит принципам BRD (минимум
+ зависимостей, нет необходимости в server-state).
+
+### Вариант C — Удаление/повторное добавление слоёв POI
+
+Скрытие через `map.removeLayer()`, показ через `map.addLayer()`.
+
+- **Плюсы:** нет «невидимых» слоёв в стиле.
+- **Минусы:** дороже по производительности, риск рассинхрона порядка
+ слоёв и обработчиков событий (`map.on('click', 'poi-circles', ...)`,
+ строка 1515), усложняет код. `setLayoutProperty` — штатный механизм
+ MapLibre именно для этого сценария.
+
+## Решение
+
+Принимается **Вариант A**.
+
+1. **Видимость** слоёв `poi-circles` и `poi-labels` переключается через
+ `map.setLayoutProperty(layerId, 'visibility', 'visible' | 'none')`.
+2. **Персистентность** — `localStorage`, ключ `poi-visible`, значения
+ `'1'` / `'0'`, в соответствии с уже сложившейся конвенцией проекта.
+3. **Источник истины в рантайме** — `layerState.poi`. Обработчик
+ `onPoiCheckbox()` обязан синхронно обновлять `layerState.poi`,
+ `localStorage` и `layout.visibility` обоих слоёв.
+4. **Без дублирования логики.** Логика установки видимости группы POI
+ должна переиспользовать существующую карту групп слоёв
+ (`poi: ['poi-circles','poi-labels']`). Допускается выделение общего
+ приватного хелпера (например `applyPoiVisibility(visible)`),
+ вызываемого как из `onPoiCheckbox()`, так и из восстановления
+ состояния при загрузке. `toggleLayer('poi')` (тулбар-кнопка) и
+ `onPoiCheckbox()` (чекбокс попапа) должны оставаться консистентны
+ через общий `layerState.poi` — недопустимо, чтобы кнопка и чекбокс
+ показывали разное состояние.
+5. **Backend, БД, API, инфраструктура — без изменений.** Состав
+ компонентов C4 не меняется, обновление C4-диаграмм не требуется.
+
+## Последствия
+
+### Положительные
+
+- Изменение полностью клиентское: deploy без миграций и без изменения
+ состава контейнеров.
+- Переключение < 50 мс (REQ-NF-01) — тайлы не перезагружаются.
+- Конвенция `localStorage` остаётся единообразной — ниже когнитивная
+ нагрузка при поддержке.
+
+### Отрицательные / ограничения
+
+- Настройка локальна для браузера: при очистке `localStorage` или смене
+ устройства сбрасывается на дефолт (POI включены). Принято осознанно.
+- В стиле карты остаются слои с `visibility: none` — это штатно для
+ MapLibre и не влияет на производительность.
+
+### Технический долг
+
+- `toggleLayer()` и `onPoiCheckbox()` частично дублируют намерение
+ «переключить группу слоёв». Если в будущих фазах появятся ещё
+ popup-чекбоксы для слоёв, стоит унифицировать тулбар-кнопки и
+ popup-чекбоксы в единый контроллер слоёв. На ET-002 — вне scope.
+
+## Классификация изменения
+
+Minor change. Новые сервисы/БД/эндпоинты не вводятся, состав компонентов
+не меняется. Лейбл `arch:major-change` **не требуется**. Обязательного
+архитектурного approve не требуется.
+
+## Связанные документы
+
+- `docs/work-items/ET-002/01-brd.md`
+- `docs/work-items/ET-002/02-trz.md`
+- `docs/work-items/ET-002/03-acceptance-criteria.md`
+- `docs/work-items/ET-002/07-infra-requirements.md`
+- `docs/architecture/README.md`
diff --git a/docs/work-items/ET-002/07-infra-requirements.md b/docs/work-items/ET-002/07-infra-requirements.md
new file mode 100644
index 0000000..0dced72
--- /dev/null
+++ b/docs/work-items/ET-002/07-infra-requirements.md
@@ -0,0 +1,94 @@
+---
+type: infra-requirements
+work_item_id: ET-002
+title: "Инфраструктурные требования — ET-002: Чекбокс показа/скрытия POI"
+version: 1
+status: approved
+created_at: 2026-05-21
+authors:
+ - "agent:architect"
+---
+
+# Инфраструктурные требования — ET-002
+
+## 1. Резюме
+
+ET-002 — изменение **исключительно фронтенда** (`src/web/index.html`,
+`src/web/app.js`). Новой инфраструктуры не требуется. Документ
+зафиксирован для полноты work-item и явно подтверждает отсутствие
+инфра-воздействия (см. ADR-0001).
+
+## 2. Контейнеры и сервисы
+
+| Аспект | Требование |
+|--------|------------|
+| Новые контейнеры | Нет |
+| Изменения существующих сервисов (api, osrm, nginx) | Нет |
+| Изменения `docker-compose.yml` | Нет |
+| Изменения `Dockerfile` | Нет |
+
+## 3. Сеть
+
+| Аспект | Требование |
+|--------|------------|
+| Новые порты | Нет |
+| Изменения reverse proxy (nginx, `/enduro/`) | Нет |
+| Новые внешние домены / DNS | Нет |
+| Исходящие сетевые вызовы из фронтенда | Нет (вся логика локальная) |
+
+## 4. Хранилища данных
+
+| Аспект | Требование |
+|--------|------------|
+| Изменения схемы SQLite/Spatialite | Нет |
+| Миграции БД (`migrations/`) | Нет |
+| Серверное хранилище состояния | Нет |
+| Клиентское хранилище | `localStorage`, ключ `poi-visible` (`'1'`/`'0'`), ≈ 1 байт полезной нагрузки на браузер |
+
+## 5. Конфигурация и секреты
+
+| Аспект | Требование |
+|--------|------------|
+| Новые переменные окружения | Нет |
+| Новые секреты | Нет |
+| Изменения конфигурации FastAPI / uvicorn | Нет |
+
+## 6. Зависимости
+
+| Аспект | Требование |
+|--------|------------|
+| Новые npm/Python пакеты | Нет |
+| Новые внешние сервисы | Нет |
+| Версия MapLibre GL JS | Без изменений (`setLayoutProperty` — штатный API) |
+
+## 7. Сборка и деплой
+
+- **Pipeline:** существующий Gitea Actions без изменений (`lint`, `test`,
+ `build`).
+- **Артефакт:** статические ассеты фронтенда (`src/web/`). Деплой —
+ штатная пересборка/перевыкладка и `docker compose up -d` на mva154.
+- **Простой (downtime):** нет — изменение только в статике фронтенда.
+- **План отката:** обратный коммит (revert) и повторный деплой;
+ миграций/состояния, требующих отдельного отката, нет.
+
+## 8. Ресурсы (CPU / RAM / диск)
+
+Воздействие отсутствует. Переключение `layout.visibility` слоёв
+выполняется в браузере клиента; тайлы не перезапрашиваются (REQ-NF-01).
+
+## 9. Наблюдаемость
+
+Новые метрики, логи и алерты не требуются. Поведение проверяется
+e2e-тестами фронтенда согласно `04-test-plan.yaml`.
+
+## 10. Влияние на C4
+
+Состав компонентов системы не меняется. Обновление
+`docs/architecture/c4-*.mmd` не требуется (диаграммы C4 в репозитории
+на данный момент отсутствуют — только `docs/architecture/README.md`).
+
+## 11. Вывод
+
+Инфраструктурных, сетевых, конфигурационных изменений и изменений БД
+**нет**. ET-002 безопасен для деплоя в рамках обычного релизного цикла
+фронтенда. Эскалация `arch:major-change` не требуется.
diff --git a/docs/work-items/ET-002/09-review.md b/docs/work-items/ET-002/09-review.md
new file mode 100644
index 0000000..9f4806b
--- /dev/null
+++ b/docs/work-items/ET-002/09-review.md
@@ -0,0 +1,144 @@
+---
+type: review
+work_item_id: ET-002
+title: "Code Review: Чекбокс показа/скрытия POI в попапе рельефа"
+version: 2
+status: approved
+verdict: APPROVED
+created_at: 2026-05-21
+authors:
+ - "agent:reviewer"
+---
+
+# Code Review — ET-002
+
+## Вердикт
+
+**APPROVED** — найдены только замечания уровня P3 (nice-to-have).
+Блокеров (P0) и must-fix (P1) нет.
+
+## Объём ревью
+
+Проверены изменения в `src/web/app.js` и `src/web/index.html` против:
+
+- `docs/work-items/ET-002/02-trz.md` (ТЗ, v1, approved)
+- `docs/work-items/ET-002/03-acceptance-criteria.md` (AC, v1, approved)
+- `docs/work-items/ET-002/06-adr/adr-0001-poi-visibility-client-side.md`
+- `docs/work-items/ET-002/04-test-plan.yaml`
+- `CLAUDE.md`
+
+Дополнительно учтён юнит-тест `tests/unit/poi_toggle.test.js`.
+
+---
+
+## 1. Соответствие ТЗ
+
+| Требование | Статус | Где реализовано |
+|------------|--------|-----------------|
+| REQ-F-01 — чекбокс «POI» в `terrain-popup` после `trails-path-cb`, отделён `
` | ✅ | `index.html:56-60` |
+| REQ-F-02 — состояние по умолчанию: checked, POI видимы | ✅ | `index.html:58` (атрибут `checked`); `app.js:2848-2852` (`stored === null → poiOn = true`) |
+| REQ-F-03 — снятие чекбокса → `visibility: 'none'` для `poi-circles`, `poi-labels` | ✅ | `app.js:2833-2837` → `applyPoiVisibility(false)` → `app.js:2815-2825` |
+| REQ-F-04 — установка чекбокса → `visibility: 'visible'` | ✅ | те же функции, ветка `visible` |
+| REQ-F-05 — сохранение в `localStorage['poi-visible']` (`'1'`/`'0'`) | ✅ | `app.js:2835` |
+| REQ-F-06 — восстановление при загрузке (`'0'` скрыть / `'1'`\|null показать) | ✅ | `app.js:2847-2853` (`restorePoiState`) |
+| REQ-F-07 — синхронизация `layerState.poi` | ✅ | `app.js:2816` (`layerState.poi = visible` в общем хелпере) |
+| REQ-NF-01 — переключение < 50 мс, без перезагрузки тайлов | ✅ | используется `setLayoutProperty`, источник слоёв не трогается |
+| REQ-NF-02 — совместимость с MapLibre GL JS | ✅ | штатные API MapLibre, без экзотики |
+| REQ-NF-03 — мобильный touch target ≥ 44px | ⚠️ см. P3-2 | переиспользован класс `terrain-checkbox` (как предписано ТЗ §3) |
+| REQ-NF-04 — отсутствие регрессий | ✅ | существующие чекбоксы и их обработчики не затронуты |
+
+UI-спецификация ТЗ §3 (разметка, `id="poi-visible-cb"`, `onchange="onPoiCheckbox()"`,
+класс `terrain-checkbox`) воспроизведена в `index.html` дословно.
+
+**Вывод:** все функциональные требования выполнены.
+
+## 2. Соответствие ADR (adr-0001)
+
+| Пункт решения | Статус | Комментарий |
+|---------------|--------|-------------|
+| 1. Видимость через `map.setLayoutProperty(... 'visibility' ...)` | ✅ | `app.js:2822` |
+| 2. Персистентность — `localStorage['poi-visible']`, `'1'`/`'0'` | ✅ | соответствует конвенции проекта (`trails-track`, `terrain-*`) |
+| 3. Источник истины — `layerState.poi`; `onPoiCheckbox()` синхронно обновляет `layerState`, `localStorage`, `layout.visibility` | ✅ | `onPoiCheckbox` → `setItem` + `applyPoiVisibility` (правит `layerState` и оба слоя) |
+| 4. Без дублирования: общий приватный хелпер, переиспользование `layerGroups.poi` | ✅ | выделен `applyPoiVisibility(visible)`, итерирует `layerGroups.poi`; используется и `onPoiCheckbox`, и `restorePoiState` — ровно как предлагает ADR |
+| 5. Backend/БД/API/инфраструктура без изменений | ✅ | изменения чисто клиентские |
+
+Выбран и реализован Вариант A. Решение полностью соответствует ADR.
+Замечание по консистентности с `toggleLayer('poi')` — см. P3-1.
+
+## 3. Качество кода
+
+Сильные стороны:
+
+- Изменения локализованы в явно размеченном блоке
+ `>>> ET-002 POI visibility block ... <<<` (`app.js:2800-2854`) — удобно
+ для ревью и для тест-харнеса.
+- JSDoc на всех трёх функциях, есть ссылка на ADR в шапке блока.
+- `applyPoiVisibility` имеет guard `if (!map) return` и проверку
+ `map.getLayer(id)` перед `setLayoutProperty` — устойчиво к раннему вызову
+ и к смене стиля.
+- `restorePoiState` корректно интегрирован в существующую цепочку
+ восстановления: `style.load → onMapStyleLoad → rebuildMapOverlays`
+ (`app.js:131`) — POI восстанавливается и при первой загрузке, и при
+ переключении темы. Паттерн идентичен `restoreTrailsState`.
+- `restorePoiState` не пишет в `localStorage` (восстановление не должно
+ иметь побочных эффектов) — поведение задокументировано в JSDoc и
+ покрыто тестом TP-03.
+- Разделение ответственности: персистентность — только в `onPoiCheckbox`,
+ применение видимости — в общем хелпере.
+
+Замечаний P0/P1/P2 нет.
+
+## 4. Качество тестов
+
+`tests/unit/poi_toggle.test.js` исполняет **реальный** код из `app.js`
+(блок извлекается по маркерам ET-002 и оборачивается через `new Function`
+с мок-зависимостями) — это покрывает риск рассинхрона тестов и продакшн-кода.
+
+- TP-01 — снятие чекбокса: скрытие слоёв + `setItem('poi-visible','0')` + `layerState.poi=false` ✅
+- TP-02 — установка чекбокса: показ слоёв + `setItem('1')` + `layerState.poi=true` ✅
+- TP-03 — `restorePoiState()` при `'0'`: скрытие + чекбокс снят + без записи в `localStorage` ✅
+- TP-04 — `restorePoiState()` без ключа: дефолт «видимы» ✅
+- Доп.: значение `'1'`, изоляция чужих слоёв (дух TP-08), синхронизация
+ `layerState` без слоёв на карте.
+
+TP-05..TP-07, TP-09 (e2e) и TP-08 (integration) по своей природе не
+покрываются юнит-тестом — это ожидаемо и не является замечанием к данному PR.
+
+## Findings
+
+### P3-1 (nice-to-have) — рассинхрон `toggleLayer('poi')` с чекбоксом
+
+`toggleLayer(group)` (`app.js:386-396`) меняет `layerState.poi`, но не
+обновляет ни чекбокс `poi-visible-cb`, ни `localStorage`. ADR-0001 п.4
+указывает, что состояние кнопки и чекбокса не должно расходиться.
+
+Смягчающие факты: `toggleLayer` не имеет ни одного вызова в кодовой базе,
+элемента `btn-poi` в `index.html` нет (вызов `toggleLayer('poi')` упал бы
+на `btn.classList`). Фактически это недостижимый код, расхождение
+пользователю не наблюдаемо. ADR сам относит унификацию тулбар-кнопок и
+popup-чекбоксов к будущему техдолгу (раздел «Технический долг»).
+
+Рекомендация (вне scope ET-002): удалить мёртвый `toggleLayer` либо
+оформить unified-контроллер слоёв отдельной задачей.
+
+### P3-2 (nice-to-have) — REQ-NF-03 не проверяется по диффу
+
+Touch target ≥ 44px зависит от CSS-класса `terrain-checkbox` в `app.css`,
+который в рамках ET-002 не менялся (ТЗ §3 предписывает переиспользовать
+существующий класс). Замечания к коду нет — отметка для приёмки:
+подтвердить REQ-NF-03 прогоном e2e-теста TP-09.
+
+### P3-3 (nice-to-have) — неровный отступ в IIFE `initTerrain`
+
+В `app.js:2949-2950` и `2962-2963` вызовы `restoreTrailsState()` /
+`restorePoiState()` имеют отступ, не совпадающий с окружающим блоком.
+Это предсуществующая стилевая мелочь, которую ET-002 лишь продолжил.
+Косметика; рекомендуется выровнять при ближайшем касании файла.
+
+## Итог
+
+Реализация полностью соответствует ТЗ и ADR-0001, код аккуратен, юнит-тесты
+исполняют реальный код и покрывают TP-01..TP-04. Найдены только три
+замечания уровня P3, ни одно из которых не блокирует мерж.
+
+**Вердикт: APPROVED.**
diff --git a/docs/work-items/ET-002/12-review.md b/docs/work-items/ET-002/12-review.md
new file mode 100644
index 0000000..dbe9064
--- /dev/null
+++ b/docs/work-items/ET-002/12-review.md
@@ -0,0 +1,168 @@
+---
+type: review
+work_item_id: ET-002
+title: "Code Review: Чекбокс показа/скрытия POI в попапе рельефа"
+version: 1
+status: approved
+verdict: APPROVED
+created_at: 2026-05-21
+authors:
+ - "agent:reviewer"
+---
+
+# Code Review — ET-002
+
+## Вердикт
+
+**APPROVED** (с комментариями).
+
+P0/P1-findings нет. Зафиксировано 1×P2 и 3×P3 — все некритичные, не
+блокируют merge. P2 относится к рассогласованию апстрим-артефактов
+(test-plan vs infra-requirements), а не к дефекту реализации.
+
+## Объект ревью
+
+- Ветка: `feature/ET-002-poi-toggle`
+- Базовая точка: `main` (832099c3) — ветка на 4 коммита впереди
+- Код-коммит: `8c17a4f` `feat(web): add POI visibility checkbox to terrain popup`
+- Изменённые файлы кода:
+ - `src/web/index.html` (+5 строк)
+ - `src/web/app.js` (+58 строк)
+ - `tests/unit/poi_toggle.test.js` (новый, 167 строк)
+ - `tests/unit/test_poi_toggle.py` (новый, 162 строки)
+- Прочитано: `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`,
+ `06-adr/adr-0001-poi-visibility-client-side.md`, `07-infra-requirements.md`,
+ `CLAUDE.md`.
+
+## 1. Соответствие ТЗ
+
+| Требование | Статус | Комментарий |
+|------------|--------|-------------|
+| REQ-F-01 Чекбокс в попапе после «Тропы», отделён `
` | ✅ | `index.html:56-60` — `
` + `label.terrain-checkbox`, идёт сразу после `trails-path-cb` |
+| REQ-F-02 Состояние по умолчанию — checked | ✅ | Атрибут `checked` в HTML; `restorePoiState()` при отсутствии ключа даёт `poiOn = true` |
+| REQ-F-03 Скрытие → `visibility: 'none'` обоих слоёв | ✅ | `applyPoiVisibility(false)` → `setLayoutProperty` для `layerGroups.poi` |
+| REQ-F-04 Показ → `visibility: 'visible'` | ✅ | `applyPoiVisibility(true)` |
+| REQ-F-05 Сохранение в `localStorage` `poi-visible` `'1'`/`'0'` | ✅ | `onPoiCheckbox()` → `localStorage.setItem('poi-visible', …)` |
+| REQ-F-06 Восстановление при загрузке | ⚠️ | Реализовано; нештатные значения ключа трактуются как «скрыть» — см. R-02 (P3) |
+| REQ-F-07 Синхронизация `layerState.poi` | ✅ | `applyPoiVisibility()` пишет `layerState.poi` первой строкой |
+| REQ-NF-01 Производительность < 50 мс | ✅ | Только `setLayoutProperty`, тайлы не перезапрашиваются |
+| REQ-NF-02 Совместимость браузеров | ✅ | Стандартные API (`localStorage`, `setLayoutProperty`) |
+| REQ-NF-03 Мобильная доступность ≥ 44px | ✅ | Переиспользован общий класс `terrain-checkbox` (как у соседних чекбоксов) |
+| REQ-NF-04 Отсутствие регрессий | ✅ | Изменения аддитивные; точки восстановления зеркалят `restoreTrailsState()` |
+
+Все функциональные и нефункциональные требования ТЗ выполнены.
+
+## 2. Соответствие ADR-0001
+
+| Пункт решения ADR | Статус | Комментарий |
+|-------------------|--------|-------------|
+| п.1 Видимость через `setLayoutProperty` | ✅ | Подтверждено; `removeLayer`/`addLayer` не используются (Вариант C отвергнут корректно) |
+| п.2 Персистентность `localStorage` `poi-visible` `'1'`/`'0'` | ✅ | Соответствует конвенции проекта |
+| п.3 Источник истины — `layerState.poi`; `onPoiCheckbox()` синхронно обновляет state + storage + visibility | ✅ | Выполнено |
+| п.4 Без дублирования; переиспользование `layerGroups.poi`; общий хелпер | ✅ | Создан `applyPoiVisibility(visible)` — буквально имя, предложенное в ADR; используется и `onPoiCheckbox()`, и `restorePoiState()` |
+| п.4 Консистентность `toggleLayer('poi')` ↔ `onPoiCheckbox()` через `layerState.poi` | ✅ | Обе функции читают/пишут `layerState.poi`. POI-кнопки в тулбаре нет (`btn-poi` отсутствует, `toggleLayer` нигде не вызывается) — визуальный рассинхрон невозможен |
+| п.5 Backend/БД/API/инфраструктура без изменений | ✅ | Затронут только `src/web/` |
+
+Реализация полностью соответствует принятому ADR. Отдельно отмечается
+точное следование п.4 — выделен ровно тот приватный хелпер, который ADR
+описывал как рекомендуемый.
+
+## 3. Acceptance Criteria
+
+| AC | Покрытие |
+|----|----------|
+| AC-01 Чекбокс в попапе после «Тропы», отделён линией | ✅ `test_poi_checkbox_placed_after_trails_separated_by_hr` |
+| AC-02 POI включены по умолчанию | ✅ `test_poi_checkbox_checked_by_default`, JS `TP-04` |
+| AC-03 Скрытие POI | ✅ JS `TP-01` |
+| AC-04 Показ POI | ✅ JS `TP-02` |
+| AC-05 Состояние сохраняется после перезагрузки | ✅ JS `TP-03` (restore при `poi-visible=0`) |
+| AC-06 Восстановление включённого состояния | ✅ JS `restorePoiState() при poi-visible=1` |
+| AC-07 Не ломает существующие чекбоксы | ✅ JS `onPoiCheckbox() меняет только poi-circles/poi-labels`; изменения HTML аддитивны |
+| AC-08 Синхронизация с `layerState` | ✅ JS `TP-01`/`TP-02` (`layerState.poi`), `applyPoiVisibility()` без слоёв |
+
+Все 8 критериев имеют поведенческое покрытие.
+
+## 4. Качество кода
+
+Сильные стороны:
+
+- **Нет дублирования.** Логика видимости группы вынесена в единый
+ `applyPoiVisibility()`; карта слоёв `layerGroups.poi` не продублирована.
+- **Консистентность с кодовой базой.** `restorePoiState()` подключён в
+ тех же трёх точках, что и `restoreTrailsState()` (`rebuildMapOverlays`
+ + обе ветки `initTerrain`), используется `window._map`, паттерн
+ `localStorage` `'1'`/`'0'` — всё единообразно с существующим кодом.
+- **Защитное программирование.** Проверки `if (!map)`, `if (map.getLayer(id))`,
+ `if (cb)`; `layerState.poi` обновляется даже когда карта/слои ещё не
+ готовы (порядок инициализации безопасен).
+- **Документированность.** JSDoc на всех трёх функциях, блок-маркеры с
+ ссылкой на ADR, запись в `CHANGELOG.md`.
+- **Коммиты.** Conventional Commits соблюдён (`feat(web):`, `docs(ET-002):`).
+
+Замечания — см. findings R-02, R-03 (P3).
+
+## 5. Качество тестов
+
+- **Unit (`poi_toggle.test.js`)** — высокое качество: тесты исполняют
+ **реальный** код из `app.js` (блок извлекается по маркерам и
+ оборачивается через `new Function` с инъекцией моков), а не его копию.
+ Покрыты TP-01..TP-04 + 3 дополнительных кейса (значение `'1'`,
+ изоляция чужих слоёв, синхронизация state без слоёв на карте).
+- **Python (`test_poi_toggle.py`)** — статические проверки структуры
+ HTML/JS (REQ-F-01, REQ-F-02, ADR-0001) + запуск JS-раннера через
+ `node --test`, со `skip` при отсутствии `node` (по аналогии с
+ существующим `test_lua_syntax`/`luac`).
+- Браузерные e2e TP-05..TP-09 не реализованы — см. R-01 (P2).
+
+## Findings
+
+### R-01 — Отклонение от test-plan: e2e TP-05..TP-09 не реализованы (P2)
+
+`04-test-plan.yaml` определяет TP-05..TP-09 как `type: e2e`, однако
+`07-infra-requirements.md` §6 явно запрещает новые npm-пакеты, а
+Playwright-инфраструктуры в репозитории нет. Это конфликт между двумя
+**approved**-артефактами, а не дефект разработчика: реализовать e2e «как
+написано» означало бы нарушить инфра-требования. Поведение всех
+сценариев покрыто статическими + unit-тестами, отклонение подробно
+задокументировано в шапке `test_poi_toggle.py`.
+**Рекомендация:** Analyst — согласовать `04-test-plan.yaml` с
+`07-infra-requirements.md` (пометить TP-05..09 как покрытые альтернативно
+либо завести отдельную инфра-задачу на Playwright). На merge ET-002 не
+влияет.
+
+### R-02 — `restorePoiState()`: нештатное значение ключа скрывает POI (P3)
+
+`const poiOn = stored === null || stored === '1';` — любое значение,
+отличное от `'1'`/`null` (например мусор в `localStorage`), трактуется
+как «скрыть». REQ-F-06 описывает только `'0'` и `'1'`/`null`, а дефолт
+фичи (REQ-F-02) — POI включены. Надёжнее: `const poiOn = stored !== '0';`
+— тогда повреждённое значение деградирует к дефолту «показать».
+Крайний кейс (ключ пишет только это приложение), отсюда P3.
+
+### R-03 — Непоследовательные отступы в `initTerrain` (P3)
+
+`app.js:2948-2950` и `2961-2963`: `restoreTrailsState()`/`restorePoiState()`
+смещены относительно `restoreTerrainState()`. Дефект унаследован из
+существующего кода — новые строки повторяют локальный (сломанный) отступ.
+Косметика; при желании выровнять блок целиком.
+
+### R-04 — Хрупкая связанность тестов с исходником (P3)
+
+Тесты зависят от комментариев-маркеров `>>> ET-002 POI visibility block`
+в `app.js` и от точного совпадения пробелов в
+`test_poi_logic_reuses_layer_state_and_groups`
+(`"poi: ['poi-circles', 'poi-labels']"`). Для монолитного non-module
+`app.js` это прагматичное и задокументированное решение, но хрупкое при
+рефакторинге/прогоне линтера. Технический долг — учесть при будущей
+модуляризации фронтенда.
+
+## Заключение
+
+Реализация ET-002 корректна, полна и точно следует ТЗ и ADR-0001.
+Дублирования нет, регрессионные риски закрыты повторением проверенного
+паттерна `restoreTrailsState()`. Тесты исполняют реальный код и
+покрывают все acceptance-критерии. Блокирующих замечаний нет.
+
+**Вердикт: APPROVED.** R-02/R-03/R-04 — на усмотрение разработчика
+(можно поправить в этом же PR или вынести в техдолг). R-01 — действие на
+стороне Analyst, merge не блокирует.
diff --git a/docs/work-items/ET-002/13-test-report.md b/docs/work-items/ET-002/13-test-report.md
new file mode 100644
index 0000000..53b82a2
--- /dev/null
+++ b/docs/work-items/ET-002/13-test-report.md
@@ -0,0 +1,227 @@
+---
+type: test-report
+work_item_id: ET-002
+version: 1
+status: pass
+tester: "agent:tester"
+date: 2026-05-21
+commit_tested: 8c17a4f
+verdict: PASS
+---
+
+# Test Report — ET-002
+
+## Verdict: **PASS** → `stage:ready-to-deploy`
+
+Полный регресс зелёный: **14 passed, 4 skipped, 0 failed**, lint чистый,
+JS-юнит-тесты POI 7/7 pass, тест-окружение отвечает 200. Блокирующих
+багов (P0/P1) не найдено.
+
+Тест-кейсы TP-01..TP-04 и TP-08 из `04-test-plan.yaml` исполнены и
+прошли. e2e-кейсы TP-05..TP-07, TP-09 **не исполнялись** — в репозитории
+нет Playwright-инфраструктуры, а `07-infra-requirements.md §6` запрещает
+новые npm-пакеты (конфликт двух approved-артефактов, зафиксирован
+reviewer'ом как R-01/P2). Поведенческая суть этих сценариев покрыта
+unit- и статическими тестами. На merge/деплой ET-002 не влияет.
+
+## Окружение
+
+- **Дата прогона:** 2026-05-21
+- **Ветка:** `feature/ET-002-poi-toggle`
+- **Коммит:** `8c17a4f` (`feat(web): add POI visibility checkbox to terrain popup`;
+ взят из `12-review.md` — `git` в окружении тестера недоступен)
+- **Python:** 3.12.13
+- **pytest:** 8.3.3 (plugins: asyncio-1.3.0, anyio-4.13.0)
+- **Node:** v22.22.2 (`node --test`)
+- **ruff:** 0.15.14
+- **test-env:** https://openclaw.mva154.duckdns.org/enduro/ → HTTP 200
+
+## Healthcheck
+
+| Среда | URL | Код |
+|---|---|---|
+| local dev | http://localhost:5556/health | connection refused (dev не поднят — ОК, прогон оффлайн) |
+| test | https://openclaw.mva154.duckdns.org/enduro/ | 200 |
+| test API | https://openclaw.mva154.duckdns.org/enduro/api/health | 200 `{"status":"ok","db_exists":true}` |
+
+ET-002 — фронтенд-изменение; в test задеплоен `main`, поэтому healthcheck
+подтверждает только живость окружения, а не наличие фичи (фича попадёт
+в test штатной перевыкладкой после merge).
+
+## Команды запуска
+
+```bash
+# Unit + integration (эквивалент make test)
+python -m pytest tests/ -v
+
+# JS behavioral unit-тесты POI (TP-01..TP-04)
+node --test tests/unit/poi_toggle.test.js
+
+# Lint
+ruff check src/
+ruff check tests/
+```
+
+## Результаты pytest
+
+`python -m pytest tests/ -v` → **14 passed, 4 skipped, 1 warning in 0.51s**
+
+| # | Тест | Тип | Результат |
+|---|---|---|---|
+| 1 | `test_routing_barriers.py::test_lua_syntax` | unit (структура lua) | **PASS** |
+| 2 | `test_routing_barriers.py::test_blocked_barriers_match_trz` | static AC | **PASS** |
+| 3 | `test_routing_barriers.py::test_excluded_highways_match_trz` | static AC | **PASS** |
+| 4 | `test_routing_barriers.py::test_route_avoids_barrier` | integration | **SKIP** (OSRM недоступен) |
+| 5 | `test_routing_barriers.py::test_route_no_footway` | integration | **SKIP** (OSRM недоступен) |
+| 6 | `test_routing_barriers.py::test_route_allows_cattle_grid` | integration | **SKIP** (OSRM недоступен) |
+| 7 | `test_routing_barriers.py::test_existing_route_works` | regression | **SKIP** (OSRM недоступен) |
+| 8 | `test_health.py::test_health_endpoint` | unit (async) | **PASS** |
+| 9 | `test_poi_toggle.py::test_poi_checkbox_present_in_html` | static (REQ-F-01) | **PASS** |
+| 10 | `test_poi_toggle.py::test_poi_checkbox_checked_by_default` | static (REQ-F-02) | **PASS** |
+| 11 | `test_poi_toggle.py::test_poi_checkbox_placed_after_trails_separated_by_hr` | static (REQ-F-01) | **PASS** |
+| 12 | `test_poi_toggle.py::test_poi_checkbox_uses_shared_style_class` | static (UI-спец) | **PASS** |
+| 13 | `test_poi_toggle.py::test_poi_functions_defined` | static (ADR-0001) | **PASS** |
+| 14 | `test_poi_toggle.py::test_poi_logic_uses_localstorage_key` | static (REQ-F-05) | **PASS** |
+| 15 | `test_poi_toggle.py::test_poi_logic_reuses_layer_state_and_groups` | static (ADR п.3-4) | **PASS** |
+| 16 | `test_poi_toggle.py::test_restore_poi_state_wired_into_init` | static (REQ-F-06) | **PASS** |
+| 17 | `test_poi_toggle.py::test_poi_visibility_toggled_via_set_layout_property` | static (ADR п.1) | **PASS** |
+| 18 | `test_poi_toggle.py::test_js_unit_tests_pass` | behavioral (Node-раннер) | **PASS** |
+
+**4 SKIP** — интеграционные тесты роутинга ET-001; требуют поднятого OSRM
+(`OSRM_URL`, по умолчанию `http://172.22.0.1:5559`), который в окружении
+тестера недоступен. Это штатное поведение (`test_routing_barriers.py`
+помечает их `skip`, чтобы CI без инфраструктуры не падал). ET-002 —
+фронтенд-изменение, на роутинг влиять не может; к регрессу не относится.
+
+Предупреждение `PendingDeprecationWarning` из `starlette/formparsers.py` —
+внешняя зависимость, к ET-002 отношения не имеет, не блокирует.
+
+## Результаты JS unit-тестов (TP-01..TP-04)
+
+`node --test tests/unit/poi_toggle.test.js` → **# pass 7, # fail 0**
+
+Тесты исполняют **реальный** код POI-блока из `src/web/app.js` (блок
+извлекается по маркерам `ET-002 POI visibility block` и оборачивается
+через `new Function` с инъекцией моков).
+
+| Тест | TC | Результат |
+|---|---|---|
+| снятый чекбокс скрывает слои POI и сохраняет `0` | TP-01 | **PASS** |
+| установленный чекбокс показывает слои POI и сохраняет `1` | TP-02 | **PASS** |
+| `restorePoiState()` при `poi-visible=0` скрывает POI | TP-03 | **PASS** |
+| `restorePoiState()` без ключа включает POI по умолчанию | TP-04 | **PASS** |
+| `restorePoiState()` при `poi-visible=1` показывает POI | доп. | **PASS** |
+| `onPoiCheckbox()` меняет только `poi-circles`/`poi-labels` | доп. (дух TP-08) | **PASS** |
+| `applyPoiVisibility()` синхронизирует `layerState` без слоёв | доп. | **PASS** |
+
+## Результаты lint
+
+| Команда | Результат |
+|---|---|
+| `ruff check src/` | **All checks passed!** |
+| `ruff check tests/` | **All checks passed!** |
+
+## Покрытие тест-плана (04-test-plan.yaml)
+
+| TC | Тип | Исполнение | Статус |
+|---|---|---|---|
+| **TP-01** | unit | JS-тест «TP-01» (через `test_js_unit_tests_pass`) | **PASS** |
+| **TP-02** | unit | JS-тест «TP-02» | **PASS** |
+| **TP-03** | unit | JS-тест «TP-03» | **PASS** |
+| **TP-04** | unit | JS-тест «TP-04» | **PASS** |
+| **TP-05** | e2e | **BLOCKED** (нет Playwright). Покрыто статически: `test_poi_checkbox_present_in_html`, `test_poi_checkbox_placed_after_trails_separated_by_hr` | COVERED (alt) |
+| **TP-06** | e2e | **BLOCKED** (нет Playwright). Покрыто поведенчески: JS «TP-01»/«TP-02», «меняет только poi-circles/poi-labels» | COVERED (alt) |
+| **TP-07** | e2e | **BLOCKED** (нет Playwright). Покрыто поведенчески: JS «TP-03» (`restorePoiState` при `poi-visible=0`) | COVERED (alt) |
+| **TP-08** | integration | JS «onPoiCheckbox() меняет только слои poi-circles и poi-labels» — изоляция чужих слоёв подтверждена | **PASS** (alt) |
+| **TP-09** | e2e | **BLOCKED** (нет Playwright). Класс `terrain-checkbox` подтверждён статически; по touch-target см. P3-2 | PARTIAL |
+
+**Исполнено и пройдено: TP-01..TP-04, TP-08 (5/9).**
+**e2e TP-05..TP-07, TP-09 (4/9) — заблокированы инфраструктурой**, их
+поведение покрыто unit/статикой. Прямой инструментальной проверки в
+реальном браузере не было.
+
+## Соответствие Acceptance Criteria
+
+| AC | Описание | Источник проверки | Статус |
+|---|---|---|---|
+| AC-01 | Чекбокс в попапе после «Тропы», отделён `
` | `test_poi_checkbox_placed_after_trails_separated_by_hr` | **PASS** |
+| AC-02 | POI включены по умолчанию | `test_poi_checkbox_checked_by_default` + JS «TP-04» | **PASS** |
+| AC-03 | Скрытие POI → `visibility: none` | JS «TP-01» | **PASS** |
+| AC-04 | Показ POI → `visibility: visible` | JS «TP-02» | **PASS** |
+| AC-05 | Состояние сохраняется после перезагрузки | JS «TP-03» (`restorePoiState` при `poi-visible=0`) | **PASS** |
+| AC-06 | Восстановление включённого состояния | JS «restorePoiState при poi-visible=1» | **PASS** |
+| AC-07 | Не ломает существующие чекбоксы | JS «меняет только poi-circles/poi-labels»; HTML-изменения аддитивны | **PASS** |
+| AC-08 | Синхронизация с `layerState.poi` | JS «TP-01»/«TP-02» + «applyPoiVisibility без слоёв» | **PASS** |
+
+Все 8 критериев имеют поведенческое покрытие; ни один не нарушен.
+Браузерная (e2e) проверка AC-01/03/04/05 ограничена отсутствием
+Playwright — поведение подтверждено на уровне реального кода POI-блока.
+
+## Найденные баги
+
+### P0 (блокирующие)
+Нет.
+
+### P1 (критические)
+Нет.
+
+### P2 (важные)
+
+**T-01 (= R-01 из `12-review.md`) — e2e TP-05..TP-09 не исполнимы.**
+`04-test-plan.yaml` определяет TP-05..TP-09 как `type: e2e`, но
+`07-infra-requirements.md §6` запрещает новые npm-пакеты, а
+Playwright-инфраструктуры в репозитории нет (проверено: нет
+`package.json`, `node_modules`, бинаря `playwright`, `npx`). Это конфликт
+двух **approved**-артефактов, а не дефект разработки. Поведение покрыто
+unit/статикой.
+**Действие:** Analyst — согласовать `04-test-plan.yaml` с
+`07-infra-requirements.md` (пометить TP-05..09 как покрытые альтернативно
+либо завести инфра-задачу на Playwright). **Merge/деплой ET-002 не
+блокирует.**
+
+### P3 (косметика / наблюдения)
+
+1. **(= R-02 из `12-review.md`)** `app.js:2849` —
+ `const poiOn = stored === null || stored === '1';`: нештатное значение
+ ключа `poi-visible` трактуется как «скрыть». Надёжнее
+ `stored !== '0'` (деградация к дефолту «показать», REQ-F-02).
+ Крайний кейс — ключ пишет только это приложение. Не блокирует.
+2. **REQ-NF-03 (touch target ≥ 44px)** — `.terrain-checkbox`
+ (`app.css:806`) при `padding:8px` и `font-size:15px` даёт высоту
+ строки ≈ 35px, что ниже ориентира 44px. Однако чекбокс POI
+ использует **тот же** класс, что и все соседние чекбоксы попапа
+ (Тени/Перепады/Грунтовки/Тропы) — **регрессии нет**, кликабельна вся
+ строка-`