docs(ET-007): architecture - ADR, infra-requirements, data-requirements, tech-risks
All checks were successful
CI / lint (push) Successful in 3s
CI / test (push) Successful in 6s
CI / build (push) Successful in 1s

This commit is contained in:
2026-05-31 20:01:06 +00:00
parent d7d06bb046
commit 231c99c045
6 changed files with 818 additions and 1 deletions

View File

@@ -11,12 +11,25 @@
- **Database** — SQLite + Spatialite (точки интереса, маршруты)
## Слои карты
- Base map (OpenStreetMap)
- Base map: **Схема** (OpenStreetMap raster) либо **Спутник** (Esri World Imagery raster) — переключается в UI (ET-007 / ADR-004)
- Hillshade (рельеф с тенями)
- TRI (Terrain Ruggedness Index — сложность рельефа)
- Hypsometric (высотная раскраска)
- Trails (маршруты из OSM)
## Внешние тайл-провайдеры
Клиент (браузер) обращается напрямую к двум внешним raster-tile сервисам.
Сервер mva154 эти тайлы не проксирует и не кэширует.
| Провайдер | Назначение | URL | Активация | API-ключ |
|-----------|-----------|-----|-----------|----------|
| OpenStreetMap | Базовый слой «Схема» | `https://tile.openstreetmap.org/{z}/{x}/{y}.png` | всегда (default подложка) | нет |
| Esri World Imagery | Базовый слой «Спутник» | `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}` | лениво — только при включении «Спутник» пользователем (ET-007) | нет |
Атрибуция обоих провайдеров выводится MapLibre автоматически при
активном source.
## Деплой
Один Docker Compose на mva154. Nginx проксирует /enduro/ на контейнер.

View File

@@ -7,3 +7,4 @@
| ADR-001 | Блокировка шлагбаумов через `mode.inaccessible` | accepted | 2026-05-15 | [ET-001](../../work-items/ET-001/06-adr/ADR-001-barrier-blocking.md) |
| ADR-002 | GPX-фича как отдельный модуль `gpx.js` | accepted | 2026-05-22 | [ET-006](../../work-items/ET-006/06-adr/ADR-002-gpx-module-structure.md) |
| ADR-003 | Парсинг GPX — `DOMParser` в основном потоке с чанковой конвертацией | accepted | 2026-05-22 | [ET-006](../../work-items/ET-006/06-adr/ADR-003-gpx-parsing-strategy.md) |
| ADR-004 | Спутниковая подложка: Esri World Imagery, ленивый raster-source, гибридное halo | accepted | 2026-05-31 | [ET-007](../../work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md) |

View File

@@ -0,0 +1,305 @@
---
type: adr
work_item_id: ET-007
adr_id: ADR-004
title: "ADR-004: Спутниковая подложка — провайдер Esri World Imagery, лениво-добавляемый raster-source, гибридная стратегия halo"
status: accepted
created_at: 2026-05-31
authors:
- "agent:architect"
supersedes: []
superseded_by: []
labels: []
---
# ADR-004 — Спутниковая подложка: провайдер, размещение кода, схема восстановления
## Статус
Accepted
## Контекст
ET-007 вводит вторую базовую подложку карты — спутниковые растровые
снимки — с переключателем «Схема / Спутник» в попапе слоёв
(см. `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`).
Существующее состояние, проверенное в коде:
- В обоих стилях карты (`src/web/style.json` стр. 1641,
`src/web/style-dark.json`) уже определён единственный raster-source
`osm-raster` и слой `osm-base`, лежащий поверх слоя `background`.
Тайлы OSM раздаются `https://tile.openstreetmap.org/{z}/{x}/{y}.png`
— то есть прецедент **внешней (кросс-оригинальной) тайл-зависимости с
атрибуцией без API-ключа уже существует**.
- `src/web/app.js` (3 132 строки) содержит функцию `rebuildMapOverlays()`
(стр. 127), которая последовательно вызывает `restoreTerrainState()`,
`restoreTrailsState()`, `restorePoiState()`, перерисовку маршрутов /
GPX / линейки. Эта функция — единственная точка восстановления
визуальных слоёв после `map.setStyle()` (переключение тёмной/светлой
темы, `switchMapStyle()` стр. 100117).
- Фронтенд плоский, без сборщика: `index.html`, `app.js`, `units.js`
(190 строк, ADR-0001), `gpx.js` (1 242 строки, ADR-002). Сложившийся
паттерн — «одна крупная фича = один классический скрипт + глобали»
(ADR-002). Все JS-функции глобальные, обработчики навешаны через
инлайновые `onclick`.
- Динамические мутации слоёв через `setPaintProperty` /
`setLayoutProperty` / `addSource` / `addLayer` в `app.js` уже широко
используются (~30 вхождений).
- В `app.js` уже есть зрелые «restore*State()»-функции для каждой
группы слоёв; ET-007 встраивается в этот же паттерн ещё одной такой
функцией `restoreBaseLayerState()`.
Решения, которые предстоит зафиксировать архитектурно:
1. Какого провайдера спутниковых тайлов выбрать.
2. Где разместить код переключателя — в `app.js` или в новом модуле.
3. Как именно добавлять спутниковый source/layer (заранее в `style.json`
или лениво из JS), и как переживать `map.setStyle()`.
4. Каким способом обеспечивать читаемость линий грунтовок/троп и POI
на тёмной спутниковой подложке (halo).
5. Классификацию изменения и нужна ли эскалация `arch:major-change`.
## Рассмотренные варианты
### Вариант P (провайдер) — выбор провайдера спутниковых тайлов
| Провайдер | API-ключ | Лицензия / условия | Покрытие | Решение |
|---|---|---|---|---|
| **Esri World Imagery** (`server.arcgisonline.com/.../World_Imagery/MapServer/tile/{z}/{y}/{x}`) | нет | Условия Esri ArcGIS Online: бесплатное использование с атрибуцией для некоммерческой и demo-разработки; широко применяется open-source-проектами (Leaflet, OpenLayers, QGIS) | глобальное, до z19 | **выбран** |
| Mapbox Satellite | требуется | бесплатный квот-лимит, далее платно | глобальное | отклонён — BRD F-02 явно требует «без API-ключа» |
| Bing Maps | требуется | сложная лицензия, обязательная регистрация | глобальное | отклонён — то же |
| Google Maps Tiles | требуется | прямо запрещён ToS для нативного встраивания не через Google Maps JS API | глобальное | отклонён |
| OpenAerialMap | нет | open-source, CC-BY | **фрагментарное**, нет глобального бесшовного слоя | отклонён — не покрывает РФ-эндуро-сценарии |
| MapTiler Satellite | требуется | бесплатный квот-лимит | глобальное | отклонён — API-ключ |
Esri World Imagery — единственный вариант, удовлетворяющий
**одновременно** трём ограничениям BRD: без API-ключа, с глобальным
покрытием, с лицензионно допустимой формой использования через
атрибуцию.
### Вариант M (модуль) — где разместить код
- **M-A — добавить в `app.js`** (выбран). +~150 строк
(`onBaseLayerToggle`, `applyBaseLayer`, `restoreBaseLayerState`,
`syncBaseLayerUI`, плюс хук в `rebuildMapOverlays()` и handler
`onclick` в `index.html`). Минимальный blast radius, никаких новых
файлов, никаких изменений в подключении скриптов.
- **M-B — выделить `src/web/basemap.js`** (по аналогии с ADR-002 для
GPX). Отклонён: ADR-002 разделил фичу, потому что её объём был
600900 строк и она имела собственную модель данных (`gpxTracks`),
собственный bottom sheet и собственный canvas. Здесь фича плоская и
объём в 57 раз меньше; разделение даёт чистоту, но не покрывает
стоимости новой связки `app.js ↔ basemap.js` ради ~150 строк.
Контракт интеграции с `rebuildMapOverlays()` и так глобальный —
никакой инкапсуляции отдельный файл не добавит.
### Вариант S (source) — как добавить спутниковый source/layer
- **S-A — задекларировать source `satellite-raster` и слой
`satellite-base` (`visibility: none`) в обоих `style.json` /
`style-dark.json`**. Source активен всегда, тайлы не запрашиваются
до показа слоя. Плюс: восстановление после `setStyle()`
тривиально (`setLayoutProperty('satellite-base', 'visibility', ...)`).
Минус: `style.json` обоих тем нужно править симметрично; дрейф
значений между двумя стилями.
- **S-B — лениво создавать source и layer из JS при первом включении
«Спутник»** (выбран, совпадает с TRZ §1 REQ-F-02). Плюс: `style.json`
не трогаем; ноль внешних запросов у пользователей, которые не
включают спутник; единая точка определения source — в `app.js`. После
`map.setStyle()` source и layer исчезают и переcоздаются вызовом
`restoreBaseLayerState()` из `rebuildMapOverlays()` — это та же
логика, что уже используется для terrain/trails/POI/GPX. Минус:
холодное переключение «Схема → Спутник» включает в себя `addSource`
+ `addLayer` + сетевой запрос — но укладывается в НФТ 500 мс.
### Вариант O (order) — порядок восстановления в `rebuildMapOverlays()`
- **O-A — `restoreBaseLayerState()` вызывается ПЕРВЫМ**, до
`restoreTerrainState()` (выбран, совпадает с TRZ §5.5). Гарантирует
z-order: `background``satellite-base``osm-base` → terrain →
trails → POI → routes → GPX. terrain/trails/POI оказываются выше
спутника, маршрут/GPX — выше terrain.
- **O-B — добавлять `satellite-base` с явным `beforeId` первого
trails-слоя**. Идемпотентно к порядку, но в `rebuildMapOverlays()`
моменты создания слоёв не атомарны (terrain/trails добавляются
асинхронно); использовать `beforeId` слоёв, которых ещё нет, нельзя.
Поэтому простой «вызвать первым» надёжнее.
### Вариант H (halo) — обеспечение читаемости поверх спутника
- **H-A — динамический `setPaintProperty` по всем затрагиваемым слоям**.
Все правки делаем из `applyBaseLayer()`; на «Схема» возвращаем
исходные значения. Минус: нужно где-то хранить «исходные» paint-
значения; при `map.setStyle()` они сбрасываются, что повышает риск
drift между двумя темами.
- **H-B — отдельные «underlay»-слои с halo, `visibility: none` по
умолчанию, включаются на спутнике** + **`setPaintProperty` только
для POI text-halo** (выбран, совпадает с TRZ §1 REQ-F-04). Halo-линии
декларативны в `style.json` обеих тем — никакого «запомнить
исходное» не нужно, восстановление по `visibility`. Для POI label
правок одна (`text-halo-color`/`text-halo-width`) — её проще менять
динамически, чем заводить параллельные label-слои.
- **H-C — толстая полупрозрачная белая обводка прямо в существующих
trails-слоях через `line-gap-width`**. Отклонён: ломает «Схему»
(там halo не нужен и портит вид светлой подложки).
## Решение
Принимается комбинация: **P-Esri + M-A + S-B + O-A + H-B**.
1. **Провайдер — Esri World Imagery.** URL-шаблон, атрибуция и параметры
source — как в TRZ §4.1. HTTPS обязателен. Атрибуция строки —
`"Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community"`.
Альтернативные провайдеры не закладываются в код фичи; точка
расширения — единственный объект source-spec в `applyBaseLayer()`,
при будущей смене провайдера правка локализуется одним местом
(см. R-2 в `10-tech-risks.md`).
2. **Код фичи живёт в `app.js`.** Никакой новый JS-файл не вводится.
Новые глобальные функции — `onBaseLayerToggle()`, `applyBaseLayer()`,
`restoreBaseLayerState()`, `syncBaseLayerUI()` — добавляются по
соседству с уже существующими `restoreTerrainState()` /
`restoreTrailsState()`. Если в будущей фазе появится потребность
(например, второй провайдер, гибридный режим, оффлайн-кэш) — фича
мигрирует в `src/web/basemap.js` без изменения публичного контракта
(имена функций глобальные и стабильные).
3. **Source и layer добавляются лениво** при первом включении
«Спутник» через `addSource('satellite-raster', {...})` +
`addLayer({ id: 'satellite-base', ... })`. До этого момента
запросов к `server.arcgisonline.com` не происходит. Это важно с
точки зрения приватности: пользователи, которые никогда не
используют спутник, не светят свой IP на серверы Esri (см.
`10-tech-risks.md`, R-3).
4. **Восстановление после `map.setStyle()` — через `rebuildMapOverlays()`.**
В функцию добавляется **первым** вызов
`if (typeof restoreBaseLayerState === 'function') restoreBaseLayerState();`
до `restoreTerrainState()`. Это гарантирует, что terrain и trails
окажутся выше спутника, без необходимости вычислять `beforeId`.
`restoreBaseLayerState()` идемпотентен: читает `localStorage` ключа
`map-base-layer` и применяет `applyBaseLayer()`.
5. **Halo — гибридный подход:**
- Для **линий grade1..5 и paths/bridleway** в обоих `style.json` /
`style-dark.json` заводятся парные «underlay»-слои
(`*-halo-satellite`) с более широкой полупрозрачной белой
обводкой и `layout.visibility = "none"`. При входе в «Спутник»
эти слои становятся видимыми; при возврате на «Схему» —
скрываются. Никаких runtime-правок paint не требуется.
- Для **POI labels** меняются динамически только два свойства —
`text-halo-color` (`#000000` на спутнике / исходное на схеме) и
`text-halo-width` (`2` на спутнике / исходное на схеме) — через
`setPaintProperty`. Эти исходные значения известны и
зафиксированы в `style.json`; читать «текущее» через
`getPaintProperty` не нужно — всегда выставляем явные значения
для обоих режимов.
- **POI circles** — обводка `circle-stroke-color: #ffffff` /
`circle-stroke-width: 2` динамически на спутнике, возврат к
исходным значениям из `style.json` на схеме.
6. **Цвет `background`** в режиме «Спутник» меняется через
`setPaintProperty('background', 'background-color', '#2a2a2a')`
(тёмно-серый), чтобы не «бликовало» под медленно подгружающимися
спутниковыми плитками. При возврате на «Схему» восстанавливаются
исходные значения из `style.json` (`#f0ede6` для светлой темы,
тёмное значение из `style-dark.json` для тёмной). Эти константы —
единственные «дублирующие» значения; они зафиксированы в
`applyBaseLayer()` и в `08-data-requirements.md` §5.
7. **localStorage — ключ `map-base-layer`** (см. TRZ §4.3), значения
`"schematic"` / `"satellite"`, default `"schematic"`. Ключ
полностью обособлен от существующих UI-настроек
(`enduro-theme-mode`, `distance_unit`, `terrain-*`, `trails-*`,
`poi-visible`) — никаких миграций старых значений не требуется.
8. **C4 / архитектурная диаграмма.** В репозитории нет файлов
`c4-*.mmd`; описание архитектуры — текстовое в
`docs/architecture/README.md`. Туда добавляется отдельный раздел
«Внешние тайл-провайдеры» с двумя строками: OSM (существующий)
и Esri World Imagery (новый, для подложки «Спутник»). Дополнительно
`docs/architecture/adr/README.md` пополняется записью ADR-004.
## Последствия
### Положительные
- Изменения — **только в коде фронтенда** (`src/web/index.html`,
`src/web/app.js`, `src/web/app.css`, оба `style*.json`). Backend,
БД, OSRM, nginx, Docker-конфигурация — без изменений (см.
`07-infra-requirements.md`).
- Лазерная локальность точки расширения: для смены провайдера
достаточно отредактировать один объект source-spec в `app.js`.
- НФТ 500 мс выполнима: при холодном переключении расходы — это
единичные вызовы `addSource` + `addLayer` + первая сетевая загрузка
плитки z=текущий; последующие переключения мгновенные (только
`visibility`).
- Пользователи, никогда не использующие «Спутник», не отправляют ни
одного запроса на серверы Esri — минимизация утечки данных по
умолчанию (см. R-3).
- Существующая инфраструктура восстановления после `map.setStyle()`
переиспользуется без изменения её формы — единый паттерн для
terrain/trails/POI/GPX/base-layer.
### Отрицательные / ограничения
- **Зависимость от третьей стороны.** Сервис Esri может ввести
лимит / потребовать API-ключ / изменить URL. Митигация: точка
расширения в `applyBaseLayer()`; риск зафиксирован
(`10-tech-risks.md`, R-2).
- **Утечка IP при использовании спутника.** При активном «Спутник»
IP пользователя становится виден Esri (так же, как сейчас он виден
tile.openstreetmap.org). Это **не регрессия приватности относительно
OSM**, но — расширение перечня третьих сторон, к которым клиент
обращается. Зафиксировано в `08-data-requirements.md` §5 и
`10-tech-risks.md` R-3.
- **Корпоративные / анти-трекинг блокировки.** Часть пользователей
(корпсети, NextDNS-фильтры) могут блокировать `arcgisonline.com`.
Поведение в этом случае — MapLibre показывает прозрачные плитки
поверх `#2a2a2a` фона; пользователь сам переключится на «Схему».
Это нормальное поведение (TRZ §1 REQ-F-08); fallback на схему
автоматически — **не закладываем**.
- **Halo-слои в `style.json` обоих тем.** Любые будущие правки
trails-слоёв требуют согласованной правки соответствующих
`*-halo-satellite` слоёв. Зафиксировано в `10-tech-risks.md` R-1.
- **Background цвет.** В коде `applyBaseLayer()` появляется маленький
дубль констант фона по темам. При смене палитры тем — править здесь
тоже. Зафиксировано в `10-tech-risks.md` R-5.
### Технический долг
- Если позже появится потребность во **втором** провайдере (например,
для альтернативной геополитической юрисдикции) или в гибридном
режиме «Спутник + подписи дорог OSM поверх», логичный путь —
вынести фичу в `src/web/basemap.js` (ADR-002-стиль) и расширить
локальное состояние до `{ provider, hybrid }`. Имена глобальных
функций (`onBaseLayerToggle`, `restoreBaseLayerState`) остаются
стабильным контрактом — `index.html` и `app.js` не меняются.
- Если в проект введут CSP-заголовок (сейчас его нет, см. ET-006
`07-infra-requirements.md` §4), для спутника потребуется
`img-src 'self' https://*.openstreetmap.org https://server.arcgisonline.com data:;`.
## Классификация изменения
**Minor change.** Новых контейнеров, сервисов, БД, серверных API
ET-007 не вводит. Внешний тайл-провайдер — расширение уже
существующего класса зависимостей (OSM-tile), а не новый
архитектурный класс. Лейбл `arch:major-change` **не требуется**.
Обязательного дополнительного архитектурного approve не требуется.
## Связанные документы
- `docs/work-items/ET-007/01-brd.md`
- `docs/work-items/ET-007/02-trz.md`
- `docs/work-items/ET-007/03-acceptance-criteria.md`
- `docs/work-items/ET-007/04-test-plan.yaml`
- `docs/work-items/ET-007/04b-ui-test-cases.md`
- `docs/work-items/ET-007/07-infra-requirements.md`
- `docs/work-items/ET-007/08-data-requirements.md`
- `docs/work-items/ET-007/10-tech-risks.md`
- `docs/architecture/README.md`
- `docs/architecture/adr/README.md`
- ADR-0001 (ET-005) — паттерн классических скриптов
- ADR-002 (ET-006) — «одна фича = один скрипт + глобали»

View File

@@ -0,0 +1,163 @@
---
type: infra-requirements
work_item_id: ET-007
title: "Инфраструктурные требования — ET-007: Спутниковая карта (Схема / Спутник)"
version: 1
status: approved
created_at: 2026-05-31
authors:
- "agent:architect"
---
# Инфраструктурные требования — ET-007
## 1. Резюме
ET-007 — изменение **исключительно фронтенда**: `src/web/index.html`,
`src/web/app.js`, `src/web/app.css`, `src/web/style.json`,
`src/web/style-dark.json`. Новой инфраструктуры, новых контейнеров,
новых портов и серверной конфигурации **не требуется**. Документ
зафиксирован для полноты work-item и явно подтверждает отсутствие
инфра-воздействия (см. `06-adr/ADR-004-satellite-base-layer.md`).
## 2. Контейнеры и сервисы
| Аспект | Требование |
|--------|------------|
| Новые контейнеры | Нет |
| Изменения существующих сервисов (api, osrm, nginx) | Нет |
| Изменения `docker-compose.yml` | Нет |
| Изменения `Dockerfile` | Нет — все правки попадают в образ через уже существующий `COPY src/web/ ./src/web/` |
| Изменения подключения скриптов в `index.html` | Нет новых `<script>`; добавляется только разметка попапа и обработчики |
| Перезапуск backend / OSRM | Не требуется |
| Простой (downtime) | Отсутствует — изменение только в статике фронтенда |
## 3. Сеть
| Аспект | Требование |
|--------|------------|
| Новые серверные порты | Нет |
| Изменения reverse proxy (nginx, `/enduro/`) | Нет |
| Новые внутренние DNS-записи | Нет |
| **Новые исходящие сетевые вызовы из браузера клиента** | **Да**`GET https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}` (HTTPS, без авторизации, raster PNG/JPEG ≈ 3080 КБ на плитку). Запросы инициируются **только** при активном режиме «Спутник» (лениво — см. ADR-004 §5) |
| Серверный трафик | Не меняется — спутниковые тайлы идут напрямую браузер ↔ Esri, не через mva154 |
### 3.1 Корпоративные/DNS-фильтры
Часть пользователей может работать в сетях, блокирующих
`arcgisonline.com` (анти-трекинг-DNS, корпсети). Поведение в этом
случае задокументировано в TRZ §1 REQ-F-08: MapLibre показывает
прозрачные плитки поверх фона `#2a2a2a`; пользователь возвращается на
«Схему» вручную. Никаких серверных обходов или прокси через
`/enduro/` не закладывается.
### 3.2 CSP-заголовок
В проекте сейчас CSP не задаётся (подтверждено в ET-006
`07-infra-requirements.md` §4). Если CSP будет введён в будущем,
директива `img-src` должна включать `https://server.arcgisonline.com`
(а также уже используемые `https://tile.openstreetmap.org` и
`data:`). На данном этапе никаких заголовков ET-007 не вводит.
## 4. Хранилища данных
| Аспект | Требование |
|--------|------------|
| Изменения схемы SQLite / Spatialite | Нет |
| Миграции БД (`migrations/`) | Нет |
| Серверное хранилище состояния | Нет |
| Клиентское хранилище | `localStorage`, единственный ключ `map-base-layer`, значения `"schematic"` \| `"satellite"`, ≤ 16 байт |
| Кэширование спутниковых тайлов | Только штатный HTTP-кэш браузера. Самостоятельный offline-кэш (Service Worker, IndexedDB) — out of scope, относится к PH-9 (см. BRD §3) |
Подробности по данным — `08-data-requirements.md`.
## 5. Конфигурация и секреты
| Аспект | Требование |
|--------|------------|
| Новые переменные окружения | Нет |
| Новые секреты / API-ключи | **Нет** — выбран провайдер без API-ключа (см. ADR-004 §1, BRD F-02) |
| Изменения конфигурации FastAPI / uvicorn | Нет |
| Изменения конфигурации OSRM | Нет |
## 6. Зависимости
| Аспект | Требование |
|--------|------------|
| Новые npm / Python-пакеты | Нет |
| Версия MapLibre GL JS | Без изменений (4.7.0) |
| Новые self-hosted сервисы | Нет |
| **Новые третьи стороны во время выполнения** | **Да**`server.arcgisonline.com` (Esri ArcGIS Online, World Imagery). Юридическое основание: бесплатное использование с атрибуцией для некоммерческой / demo-разработки; атрибуция выводится автоматически MapLibre при активном source. Зафиксировано в `docs/architecture/README.md` §«Внешние тайл-провайдеры» |
| Альтернативный провайдер (fail-over) | Не закладывается; точка расширения — один объект source-spec в `applyBaseLayer()` |
## 7. Сборка и деплой
- **Pipeline:** существующий Gitea Actions без изменений (`make lint`
+ `make test` + `make build`). ESLint автоматически покрывает
правки в `app.js`. Бэкенд-тесты (`pytest`) ET-007 не затрагивает.
- **Артефакт:** статические ассеты фронтенда (`src/web/`). Никаких
новых файлов — модифицируются существующие.
- **Деплой:** стандартный — `make deploy-test`
`docker compose up -d` на mva154. Время простоя: 0 (только
перевыкладка статики).
- **Smoke-проверка после деплоя** на
`https://openclaw.mva154.duckdns.org/enduro/`:
1. Открыть карту, открыть попап «Рельеф».
2. Убедиться, что виден переключатель «Подложка [Схема][Спутник]».
3. Переключить на «Спутник» — увидеть растровые снимки и атрибуцию
Esri в правом нижнем углу.
4. Перезагрузить страницу — режим «Спутник» сохранён.
5. Переключить тёмную/светлую тему — режим «Спутник» сохранён,
слои не исчезли.
## 8. Rollback
- **План отката:** обратный коммит (revert) и повторный
`docker compose up -d`. Времени отката ≈ 12 минуты (пересборка
Docker-образа со статикой).
- **Серверного состояния / миграций / графов**, которые требуется
отдельно откатывать, нет.
- **Сохранившиеся `localStorage`-значения у пользователей.** После
отката ключ `map-base-layer` остаётся в `localStorage`, но
игнорируется старым кодом — безвреден. Принудительная очистка не
требуется.
## 9. Ресурсы (CPU / RAM / диск)
- **Сервер mva154:** воздействие отсутствует. Спутниковые тайлы идут
напрямую от Esri к браузеру; mva154 не проксирует, не кэширует, не
логирует их.
- **Клиент-браузер:** при активном «Спутник» — дополнительные
растровые загрузки 3080 КБ × число видимых плиток (типично
1030 плиток на viewport). Это сопоставимо со стоимостью текущего
OSM-слоя и не создаёт регрессий по памяти/CPU.
## 10. Наблюдаемость
- Новые серверные метрики, логи и алерты **не требуются**.
- Поведение проверяется автотестами (UI/e2e) по плану
`04-test-plan.yaml` и `04b-ui-test-cases.md`.
- Серверные логи /enduro/ дополнений не получают — все обращения к
Esri идут с браузера, минуя mva154.
## 11. Влияние на C4 / архитектурную документацию
Состав внутренних компонентов системы (Frontend, Backend, Tile
Server, OSRM, БД) **не меняется**. Меняется только перечень внешних
зависимостей в выполнении: добавляется Esri World Imagery как второй
внешний raster-tile провайдер наряду с уже используемым
tile.openstreetmap.org.
В репозитории нет файлов `c4-*.mmd` — описание архитектуры текстовое
в `docs/architecture/README.md`. ET-007 обновляет этот документ:
добавляется раздел/строка «Внешние тайл-провайдеры» со списком из двух
провайдеров и условием активации каждого.
## 12. Вывод
Инфраструктурных, сетевых, конфигурационных, серверных и
БД-изменений на стороне mva154 **нет**. Единственное архитектурное
расширение — новая клиентская зависимость от внешнего raster-tile
провайдера (Esri World Imagery), активируемая лениво и только при
явном пользовательском выборе режима «Спутник». Деплой штатный,
эскалация `arch:major-change` не требуется.

View File

@@ -0,0 +1,135 @@
---
type: data-requirements
work_item_id: ET-007
title: "Требования к данным — ET-007: Спутниковая карта (Схема / Спутник)"
version: 1
status: approved
created_at: 2026-05-31
authors:
- "agent:architect"
---
# Требования к данным — ET-007
## 1. Резюме
ET-007 не вводит и не изменяет ни одной серверной структуры данных.
Единственные «данные» фичи на стороне приложения — пользовательский
UI-выбор подложки в `localStorage`. На стороне внешнего источника —
бесконтекстные растровые плитки PNG/JPEG, потребляемые браузером.
## 2. Серверные данные
| Аспект | Требование |
|--------|------------|
| Изменения схемы SQLite / Spatialite | Нет |
| Новые таблицы / колонки / индексы | Нет |
| Миграции (`migrations/`) | Нет |
| Изменения контракта API `/api/*` | Нет |
| Серверное логирование выбора подложки | Нет — выбор остаётся в браузере |
## 3. Внешние входные данные (спутниковые тайлы)
| Параметр | Значение |
|----------|----------|
| Источник | Esri World Imagery (см. ADR-004 §1) |
| URL-шаблон | `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}` |
| Протокол | HTTPS, без авторизации |
| Формат | растровый PNG / JPEG, 256 × 256 px |
| Размер плитки | ≈ 3080 КБ |
| Диапазон z | 0 … 19 |
| Привязка | Web Mercator (EPSG:3857) — совместима с MapLibre по умолчанию |
| Атрибуция (обязательна) | `"Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community"` |
| Содержимое запроса | `{z}`, `{y}`, `{x}` — обезличенные координаты тайла; больше ничего не передаётся |
| Cookies / заголовки авторизации | Не отправляются |
Изменение MapLibre source при будущей смене провайдера локализовано
одним объектом source-spec в `applyBaseLayer()` — это единственная
точка системы, знающая URL Esri (см. ADR-004 §1 «точка расширения»).
## 4. Клиентское хранилище
| Параметр | Значение |
|----------|----------|
| Механизм | `localStorage` |
| Ключ | `map-base-layer` |
| Допустимые значения | `"schematic"` \| `"satellite"` |
| Значение по умолчанию | `"schematic"` (при отсутствии ключа или некорректном значении) |
| Объём полезной нагрузки | ≤ 16 байт на браузер |
| Запись | в `onBaseLayerToggle(base)` при изменении выбора |
| Чтение | в `restoreBaseLayerState()` — при старте приложения и в каждом вызове `rebuildMapOverlays()` (после `map.setStyle()`) |
| Миграция со старых значений | Не требуется — ключ новый, конфликта нет |
Имя ключа `map-base-layer` соответствует сложившейся в проекте
конвенции UI-настроек в `localStorage` (`enduro-theme-mode`,
`distance_unit`, `terrain-*`, `trails-*`, `poi-visible`,
`MARKERS_KEY`). Префиксации проектом не предусмотрено.
## 5. Внутреннее состояние модуля
Дополнительные неперсистентные данные, удерживаемые в памяти браузера
в течение сессии:
| Поле | Тип | Назначение |
|------|-----|------------|
| текущий базовый слой | `'schematic' \| 'satellite'` | проекция `localStorage['map-base-layer']` |
| baseline-значения paint POI (text-halo, circle-stroke) | объекты per-layer | референсы для возврата с «Спутник» на «Схему» |
| baseline-значения `background-color` для тёмной/светлой темы | две строковые константы | `#f0ede6` (light), `#1a1a1a` (dark) — задублированы из `style*.json`, см. ADR-004 §6 |
| флаг «satellite source уже добавлен в стиль» | bool | оптимизация: при повторном входе в «Спутник» в той же сессии стиля не добавляем повторно |
baseline POI-значения и `background-color` — единственные
**задублированные** значения между `style*.json` и `app.js`. Их
рассинхрон ловится UI-тестами AC-04 и AC-06.
## 6. Halo-слои в `style.json`
В обоих `src/web/style.json` и `src/web/style-dark.json` добавляются
парные «underlay»-слои halo для линий грунтовок/троп, например:
| Базовый слой | Halo-слой | Назначение |
|--------------|-----------|------------|
| `trails-grade1` | `trails-grade1-halo-satellite` | широкая полупрозрачная белая обводка под основной линией |
| `trails-grade2` | `trails-grade2-halo-satellite` | то же |
| ... | ... | для каждой grade и для paths/bridleway |
| `paths-bridleway` | `paths-bridleway-halo-satellite` | то же |
Параметры halo-слоёв (ширина, цвет, opacity) — на этапе разработки;
дизайн уточняется визуальной проверкой на тёмных снимках. У всех
halo-слоёв `layout.visibility = "none"` по умолчанию; включаются в
`applyBaseLayer('satellite')` через `setLayoutProperty`. Точные
численные значения halo — данные дизайна, не данные домена; их
изменение не требует миграции пользовательского состояния.
## 7. Персональные данные
| Канал | PII |
|-------|-----|
| `localStorage['map-base-layer']` | нет (обезличенный UI-флаг) |
| Запросы к `tile.openstreetmap.org` (уже существуют) | IP пользователя становится виден OSM при использовании «Схемы» |
| Запросы к `server.arcgisonline.com` (новые) | IP пользователя становится виден Esri **только** при активном режиме «Спутник» (лениво — см. ADR-004 §3) |
| Передача координат поездок / маршрутов на сторонние сервисы | Нет — координаты в URL не передаются, передаётся только `{z}/{y}/{x}` тайл-сетки |
Это **не регрессия** относительно текущего состояния (OSM-tile
уже работает на тех же условиях), но — расширение списка третьих
сторон, к которым обращается клиент. Пользователи, никогда не
включающие «Спутник», ни одного запроса в Esri не отправляют — это
прямое следствие ленивого создания source (ADR-004 §3). См. также
`10-tech-risks.md`, R-3.
Серверных обязательств по хранению / удержанию / удалению PII
ET-007 **не порождает** — на mva154 никаких новых данных не оседает.
## 8. Резервное копирование и ретенция
Не применимо — серверных данных у ET-007 нет. Клиентский
`localStorage['map-base-layer']` не подлежит резервному копированию
(пользовательская UI-настройка, утрата которой безболезненна).
## 9. Вывод
Серверная модель данных, схемы и контракты API ET-007 **не
затрагивает**. Единственное персистентное данное — обезличенный
клиентский флаг `localStorage['map-base-layer']` (≤ 16 байт).
Внешний источник предоставляет публичные растровые тайлы; никакие
данные пользователя в запросах к нему не передаются помимо штатной
для HTTP-клиента информации (IP, User-Agent).

View File

@@ -0,0 +1,200 @@
---
type: tech-risks
work_item_id: ET-007
title: "Технические риски — ET-007: Спутниковая карта (Схема / Спутник)"
version: 1
status: approved
created_at: 2026-05-31
authors:
- "agent:architect"
---
# Технические риски — ET-007
Технические риски этапа разработки. Бизнес-риски — в BRD §5
(пересечение есть, здесь акцент на техническую митигацию).
Шкала: вероятность (Н/С/В) × влияние (Н/С/В).
## R-1 — Дрейф halo-слоёв в `style.json` / `style-dark.json`
- **Описание:** ADR-004 §5 решает читаемость линий грунтовок и троп
на спутнике через парные «underlay»-слои `*-halo-satellite` с
`visibility: none` в обоих файлах стилей. Любая будущая правка
основных trails-слоёв (цвет, ширина, фильтр) требует **согласованной
правки halo-слоёв** в обоих файлах. Без явной проверки легко
забыть один из четырёх случаев (2 темы × 2 рода слоёв).
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
- При разработке завести единый список затрагиваемых пар в
`applyBaseLayer()`: массив `['trails-grade1', 'trails-grade2', ...]`
с производным правилом `<base>-halo-satellite`. Это исключит
«забытый» halo-слой со стороны JS.
- Code review-чеклист: при правке trails-* в `style*.json`
обязательная сверка `*-halo-satellite` в том же файле.
- UI-тест AC-04 проверяет видимость линий поверх спутника в обеих
темах.
## R-2 — Провайдер Esri меняет условия / URL / вводит API-ключ
- **Описание:** Esri World Imagery — единственная внешняя зависимость
фичи, выбранная без формального соглашения; в перспективе Esri
может ограничить бесплатный доступ, изменить URL-схему или ввести
обязательный API-ключ (BRD §5 риск №1). Тогда «Спутник» перестаёт
работать у всех пользователей одновременно.
- **Вероятность / Влияние:** С / В.
- **Митигация:**
- Точка расширения локализована: единственный объект source-spec
в `applyBaseLayer()` (ADR-004 §1).
- При деградации провайдера выполняется одна правка JS-фронтенда
(новый URL-шаблон + новая атрибуция), без миграций и серверных
изменений; откат прежнего поведения — обратный коммит.
- Альтернативные провайдеры предварительно рассмотрены в ADR-004
§«Вариант P»; быстрый switch на следующего по приоритету —
Mapbox или MapTiler — потребует только введения переменной
окружения для API-ключа (это уже инфра-изменение, выходящее за
scope ET-007).
- Регулярная smoke-проверка дос­тупности через UI-тест AC-02.
## R-3 — Утечка IP клиента на серверы Esri
- **Описание:** при активном «Спутник» браузер обращается напрямую
к `server.arcgisonline.com`; IP пользователя и User-Agent видны
Esri. Это **не регрессия** (OSM tile уже работает аналогично), но
расширение списка третьих сторон, к которым обращается клиент.
- **Вероятность / Влияние:** В (т.е. произойдёт всегда при включении
спутника, дизайн именно такой) / Н.
- **Митигация:**
- **Ленивое создание source** (ADR-004 §3): пользователь,
никогда не включающий «Спутник», ни одного запроса в Esri не
отправляет. Это обеспечивает «приватный по умолчанию» режим.
- Документировано в `08-data-requirements.md` §7.
- В отдельный политический документ выноситься не требуется —
приватность фичи на уровне рейзанса вынесена в ADR-004 §«Последствия».
## R-4 — Корпсеть / DNS-блокировка `arcgisonline.com`
- **Описание:** часть пользователей работает в сетях, блокирующих
arcgisonline.com (анти-трекинг-DNS типа NextDNS/Pi-hole,
корпоративные firewall). MapLibre будет показывать прозрачные
плитки поверх фона `#2a2a2a`; пользователь увидит «дыры».
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
- TRZ §1 REQ-F-08 явно фиксирует: автоматический fallback на
«Схему» не закладывается — пользователь возвращается на схему
вручную; это сознательное проектное решение.
- В виду фона `#2a2a2a` пустота визуально опознаётся как ошибка
подложки, а не «лёг" сайт.
- Эскалация / альтернативный провайдер при единичных жалобах не
требуется; при системных — переход к R-2.
## R-5 — Дублирование `background-color` между `style*.json` и `app.js`
- **Описание:** ADR-004 §6 требует менять `background-color` на
тёмно-серый при включении «Спутник» и возвращать к исходному при
возврате на «Схему». «Исходные» значения (`#f0ede6` для светлой,
тёмное для тёмной) дублируются в `applyBaseLayer()` и в
`style*.json` — при смене палитры тем легко забыть один из двух.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- Альтернатива — при возврате на «Схему» **читать** актуальное
значение через `getPaintProperty('background', 'background-color')`
непосредственно перед мутацией в «Спутник», и кэшировать его в
замыкании. Однако `setStyle()` сбрасывает кэш, что усложняет
логику. Принято: задублировать в коде с явным комментарием
в `app.js` и code review-чеклистом.
- Покрытие AC-06 (смена темы при активном «Спутник») косвенно
проверяет согласованность.
## R-6 — Накопление обработчиков и source/layer после `map.setStyle()`
- **Описание:** при `map.setStyle()` (переключение тёмной/светлой
темы) спутниковый source/layer удаляются вместе со стилем.
`restoreBaseLayerState()` пересоздаёт их в `rebuildMapOverlays()`.
Аналогичный риск зафиксирован для GPX (ET-006, R-4: «дублирование
обработчиков»). Спутник, в отличие от GPX, **не вешает свои
`map.on('click', ...)`** обработчиков на свои слои (он —
невзаимодействующий растр), поэтому дублирования обработчиков
здесь не возникает.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:** проверка перед `addSource` — `if
(!map.getSource('satellite-raster')) map.addSource(...)`; то же для
layer. Это идемпотентный паттерн, уже используемый в проекте для
terrain/trails.
## R-7 — Несовместимость z-order при `restoreBaseLayerState()` после terrain
- **Описание:** если разработчик случайно вызовет
`restoreBaseLayerState()` **после** `restoreTerrainState()` в
`rebuildMapOverlays()`, спутник окажется поверх hillshade и
перекроет его. Это нарушит AC-04 («Hillshade поверх спутника»).
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
- ADR-004 §4 явно фиксирует: `restoreBaseLayerState()` вызывается
**ПЕРВЫМ** в `rebuildMapOverlays()`.
- Комментарий в коде `app.js` непосредственно у вызова —
`// ET-007/ADR-004: ПЕРВЫМ, чтобы trails/terrain легли поверх`.
- UI-тест AC-04 «Hillshade поверх спутника» отлавливает регрессию.
## R-8 — Производительность переключения «Схема → Спутник» > 500 мс
- **Описание:** НФТ ТЗ — ≤ 500 мс до первой видимой плитки. При
холодном переключении в одном кадре происходит: чтение
`localStorage`, `addSource`, `addLayer`, `setLayoutProperty`,
`setPaintProperty` ×N для POI, `setLayoutProperty` ×K для halo-
underlays. Главная неопределённость — сетевая задержка до Esri.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
- Все операции стиля MapLibre — синхронные O(1) на source/layer;
суммарно < 50 мс.
- Сетевая задержка для PNG 3080 КБ из Esri CDN на канале
≥ 5 Мбит/с укладывается в 200300 мс на тайл (по практике
Leaflet/OpenLayers с этим же провайдером).
- Тест НФТ TP-Performance в `04-test-plan.yaml` проверяет
верхнюю границу.
## R-9 — Конфликт mobile-вёрстки попапа
- **Описание:** новая строка `terrain-base-row` добавляется в
`#terrain-popup` сверху. На узких экранах (375 px, ET-005 TP-05)
возможен выход за пределы попапа или перекрытие смежных строк.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- Переиспользуется готовый компонент `.seg-control` (адаптивен по
ширине), без введения нового CSS-компонента.
- UI-тест AC-09 (mobile viewport 375 × 812) — обязательный.
## R-10 — Включение спутника после рестарта при отсутствии сети у Esri
- **Описание:** пользователь сохранил `map-base-layer = "satellite"`,
затем при следующем визите Esri недоступен. `restoreBaseLayerState()`
вызовет `applyBaseLayer('satellite')`, source создастся, плиток не
будет — пользователь увидит пустой тёмный фон вместо привычной
карты.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- Поведение явно соответствует TRZ §1 REQ-F-08; на mobile/desktop
пользователь нажмёт «Схема» и продолжит работу.
- Авто-fallback на «Схему» при сетевой ошибке провайдера —
**не закладывается** (см. ADR-004 §«Последствия»). Введение
fallback возможно в будущей итерации без изменения внешнего
контракта `applyBaseLayer()`.
## Сводная таблица
| ID | Риск | Вер. | Влияние | Класс | Статус |
|-----|------|------|---------|-------|--------|
| R-1 | Дрейф halo-слоёв в обоих style.json | С | Н | Средний | внимание разработки + review |
| R-2 | Esri меняет условия / URL / вводит ключ | С | В | Высокий | митигация — точка расширения |
| R-3 | Утечка IP на Esri при активном спутнике | В | Н | Средний | приватный-по-умолчанию (lazy) |
| R-4 | DNS-блокировка `arcgisonline.com` | Н | С | Низкий | принят (TRZ REQ-F-08) |
| R-5 | Дубль background-color в style/app.js | Н | Н | Низкий | принят + комментарий в коде |
| R-6 | Source/layer после setStyle | Н | Н | Низкий | идемпотентные `if (!getSource)` |
| R-7 | Неверный порядок restoreBaseLayerState | Н | С | Низкий | ADR явно + комментарий + AC-04 |
| R-8 | Переключение > 500 мс | Н | С | Низкий | покрыто НФТ-тестом |
| R-9 | Mobile-вёрстка попапа | Н | Н | Низкий | AC-09 |
| R-10 | Restore satellite при недоступности Esri | Н | Н | Низкий | принят, fallback не закладываем |
Блокирующих рисков нет. R-2 — единственный «высокий» класс, но
вероятность средняя и митигация (локализация точки расширения)
делает реакцию операционно простой. Эскалация `arch:major-change`
или возврат в Анализ не требуются.