---
type: test-report
work_item_id: ET-013
title: "Test Report: Перепады высот на z9-z11 — zoom-aware paint"
version: 1
status: blocked
verdict: BLOCKED
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
  - "agent:tester"
related:
  - "ET-007"
  - "PH-6.terrain"
adr_refs:
  - "ADR-017"
---

# Test Report — ET-013

## TL;DR

- `make lint` ✅, прицельный прогон unit/integration ET-013 ✅
  (23 passed, 7 skipped — skip ожидаемы: нет PNG-fixtures в sandbox).
- Полный `make test` падает на этапе collection из-за **внешней**
  проблемы (`ModuleNotFoundError: No module named 'lxml'` в тестах
  `tests/api/test_gps_tracks_download.py` / `_gpx_builder.py`) — это
  наследие ET-011, не имеет отношения к ET-013. После исключения
  этих двух файлов: **191 passed, 46 skipped, 0 failed**, регрессий
  ET-007/008/009/011/012 нет.
- Код в ветке `feature/ET-013-z9-z11-z8` 1:1 соответствует TRZ
  (REQ-F-01..F-21) и ADR-017 (подтверждено Review v2, **APPROVED**).
- **❌ Pre-deploy gate AC-19 — FAIL (P1):** на test-среде отсутствуют
  тайлы `hillshade/9/*` (а также `hillshade/8/*`). Проверка по
  координатам `[37.6, 54.5]` (юг МО / Кашира — основная зона UI-тестов):
  `hillshade/z9/309/348.png → 404`. Тайлы `hillshade/z10`,
  `hillshade/z11`, `tri/z8..z11` присутствуют (200 OK). Это блокирует
  основную пользовательскую ценность ET-013: после деплоя на z=9
  чекбокс «Тени рельефа» станет активным, но карта 404'нется на каждом
  hillshade-запросе, и пользователь увидит включённый слой **без теней**
  (хуже, чем до ET-013, где чекбокс был disabled с честным hint'ом
  «Зум 10+»).
- **UI Playwright (TC-UI-01..12) — NOT EXECUTED:** раннер
  `/home/slin/tools/ui-test/run_tests.js` и `playwright`/`npx`
  недоступны в этом контейнере. Дополнительно: test-среда сейчас
  держит **до-ET-013** код (`if (zoom < 10)`, `HILLSHADE_PAINT` нет),
  поэтому даже при наличии раннера большинство TC дали бы PASS «по
  старому контракту» — нерелевантный сигнал. Визуальные TC должны
  выполниться **после** деплоя.

**Вердикт: BLOCKED.** Реализация ET-013 в коде корректна и готова,
но деплой остановлен по TRZ REQ-F-20 §1: «При 404 — задача
останавливается, тайлы z9 нужно догенерировать в рамках PH-6
follow-up». Следующий шаг — открыть PH-6 follow-up
(«generate hillshade tiles z8-z9 для CFO») и после генерации тайлов
повторно прогнать pre-deploy probe + Playwright UI suite.

---

## 1. Окружение прогона

| Параметр                | Значение                                                                |
|-------------------------|-------------------------------------------------------------------------|
| Ветка                   | `feature/ET-013-z9-z11-z8`                                              |
| HEAD                    | `397dc60 reviewer(ET): auto-commit from reviewer run_id=84`             |
| Содержательные коммиты  | `5be81f9 feat(terrain): zoom-aware paint для hillshade/TRI на z9-z11 (ET-013)`<br>`099669d fix(terrain): расширить whitelist endpoint'а на `tri` (ET-013 review F-1)` |
| Python                  | 3.12.13                                                                 |
| pytest                  | 8.3.3                                                                   |
| Ruff                    | через `python -m ruff check src/api/`                                   |
| Test-среда (HTTP)       | https://openclaw.mva154.duckdns.org/enduro/                             |
| Состояние test-среды    | **до-ET-013** (фронт ещё с `if (zoom < 10)`, без `HILLSHADE_PAINT`/`TRI_PAINT`). Это ожидаемо: деплой ET-013 — следующий этап пайплайна. |
| `curl` в sandbox        | отсутствует; HTTP-проверки выполнены через `urllib.request` (Python).   |

Сетевая проверка `/health`:
```
GET /enduro/api/health → 200
{"status":"ok","db_path":"/app/data/centralfederal.sqlite","db_exists":true}
```

---

## 2. Шаг 1 — `make lint`

```
python -m ruff check src/api/
All checks passed!
```
**Результат:** ✅ PASS (часть AC-18).

---

## 3. Шаг 2 — `make test` (целевой gate)

### 3.1 Прицельный прогон ET-013

```
python -m pytest tests/unit/test_terrain_paint.py \
                 tests/integration/test_terrain_z9_tiles.py -v

collected 30 items
…
=================== 23 passed, 7 skipped, 1 warning in 0.46s ===================
```

| Suite                                         | Кейсов | PASS | SKIP | Покрытие AC               |
|-----------------------------------------------|--------|------|------|----------------------------|
| `tests/unit/test_terrain_paint.py`            | 17     | 17   | 0    | AC-01, AC-04, AC-05, AC-15, AC-22 |
| `tests/integration/test_terrain_z9_tiles.py`  | 13     | 6    | 7    | AC-16                      |

Что покрывают unit-тесты (выборка):
- `test_hillshade_paint_defined`, `test_hillshade_opacity_is_interpolate_by_zoom`,
  `test_hillshade_opacity_stops`, `test_hillshade_contrast_peak_z9`,
  `test_hillshade_resampling_nearest` — структура `HILLSHADE_PAINT`,
  stops 9/10/11/12/14 → 0.65/0.60/0.55/0.50/0.40, contrast пик z9 ≥0.30 / z14 ≤0.10.
- `test_tri_paint_defined`, `test_tri_opacity_z8_regression` («8, 0.70»
  ровно, защита AC-06), `test_tri_opacity_peak_z9_z11` (z10/z11 ≥ 0.80),
  `test_tri_resampling_nearest`.
- `test_apply_terrain_layer_signature_uses_opacity_or_paint`,
  `test_apply_terrain_layer_normalizes_number_to_legacy_paint`,
  `test_apply_terrain_layer_uses_paint_variable` — обратная
  совместимость `applyTerrainLayer` (AC-22).
- `test_minzoom_threshold_lowered_to_9` (`if (zoom < 9)` найден,
  `< 10` отсутствует), `test_hint_text_updated_to_z9` («Зум 9+»),
  `test_apply_terrain_layer_caller_count` (ровно 2 вызова),
  `test_hillshade_call_uses_paint_constant_and_minzoom_9`,
  `test_tri_call_uses_paint_constant_and_minzoom_5`.

Что покрывают integration-тесты:
- **PASS:** `test_known_terrain_layer_accepted_by_whitelist[hypso|hillshade|tri]`
  (доказывает фикс F-1 review v1), `test_unknown_terrain_layer_returns_404`,
  `test_missing_terrain_tile_returns_404`, `test_invalid_zoom_returns_404`.
- **SKIP:** `test_terrain_tile_available_z9_z10_z11[*]` ×6,
  `test_terrain_tile_cache_control_immutable` — требуют PNG-fixtures
  в `data/terrain/`, которых нет в sandbox-репо. Skip — корректный
  механизм через `_maybe_skip`; AC-16 говорит «при отсутствии тайлов
  в CI — тесты skipped с reason», что в точности и наблюдается.

### 3.2 Полный регресс (`pytest tests/`)

Полный прогон падает на collection из-за **внешней** проблемы:

```
ERROR tests/api/test_gps_tracks_download.py
ERROR tests/api/test_gps_tracks_gpx_builder.py
    from lxml import etree as lxml_et
E   ModuleNotFoundError: No module named 'lxml'
!!! Interrupted: 2 errors during collection !!!
```

`lxml` не установлен в этом контейнере. Это **наследие ET-011 / GPX
download**, не связано с ET-013 (ветка не трогает `gps_tracks/`).
В CI-окружении проекта `lxml` устанавливается через
`src/api/requirements.txt`, и эти тесты зелёные.

Прогон без этих двух файлов:
```
python -m pytest tests/ \
  --ignore=tests/api/test_gps_tracks_download.py \
  --ignore=tests/api/test_gps_tracks_gpx_builder.py
…
========== 191 passed, 46 skipped, 4 deselected, 79 warnings in 3.47s ==========
```

- `4 deselected` — perf/network маркеры (стандартный exclude).
- `46 skipped` — async-тесты `gps_tracks` (нет pytest-asyncio в
  sandbox) + integration без fixtures. Не относится к ET-013.
- **Регрессий ET-007 / ET-008 / ET-009 / ET-011 / ET-012 — НЕТ.**

**Результат:** ✅ PASS (AC-15, AC-16 в части автоматики, AC-17, AC-18).

---

## 4. Шаг 3 — E2E (контракт API на test-среде)

### 4.1 IT-TILE-* «вживую» против test-среды

Поскольку sandbox без data fixtures даёт SKIP, я выполнил эквивалент
IT-TILE-* напрямую HTTP-запросом к test-среде. Координата
`[37.6, 54.5]` (юг МО / Кашира) — основная для UI-тестов (см.
04b-ui-test-cases.md §«Координаты»). Тайлы под TMS-схемой (как
объявлено в `addSource(... scheme: 'tms' ...)`):

| z  | hillshade (x, y_tms)      | hillshade status | tri (x, y_tms)            | tri status |
|----|---------------------------|------------------|---------------------------|------------|
| 8  | `8/154/174`               | **❌ 404**       | `8/154/174`               | ✅ 200     |
| 9  | `9/309/348`               | **❌ 404**       | `9/309/348`               | ✅ 200     |
| 10 | `10/618/697`              | ✅ 200           | `10/618/697`              | ✅ 200     |
| 11 | `11/1237/1395`            | ✅ 200           | `11/1237/1395`            | ✅ 200     |
| 14 | `14/9903/11162`           | ✅ 200           | `14/9903/11162`           | ❌ 404 ¹   |

¹ TRI z=14 404 — за пределами TRI-стека (TRI генерится до z11 в
PH-6, регрессия известная, в скоупе ET-013 не трогается). Чекбокс TRI
на z=14 включит источник с minzoom=5/maxzoom=15, но реально тайлы
отдадутся только до z=11; визуально на z>11 — пусто. Это **не**
новая регрессия ET-013, такое же поведение было до ET-013. Фиксирую
как P3 для PH-6 follow-up.

Дополнительная проверка покрытия hillshade z=9 — wide grid 5×5 вокруг
центра `(309, 348)`:
```
hillshade z=9 found: 0 tiles around (309,348)
hillshade z=10 found: 9 tiles around (618,697)
```
То есть на z=9 нет ни одного hillshade-тайла, не только «целевого»;
данных просто нет в pipeline.

### 4.2 Заголовок Cache-Control

```
hillshade z=10 → Cache-Control: max-age=31536000
hillshade z=11 → Cache-Control: max-age=31536000
tri      z=8  → Cache-Control: max-age=31536000
…
```

Только `max-age=31536000`; `immutable`-флаг **отсутствует** в ответах
nginx-перед-fastapi на test-среде. Это **предсуществующая** ситуация
(не введена ET-013): backend FastAPI отдаёт `Cache-Control: max-age=…,
immutable`, но nginx-конфиг на test-среде стрипает `immutable`. На
бизнес-логику это не влияет (`max-age=1y` достаточен), но формальная
формулировка REQ-F-18 / IT-TILE-CACHE-HEADER «immutable сохраняется»
выполняется только на backend-уровне (см. integration-тест
`test_terrain_tile_cache_control_immutable`, корректно SKIPPED здесь).
**Не блокер ET-013.** Фиксирую как P3 (известная инфра-косметика,
не в скоупе).

### 4.3 `/health` стабилен
См. раздел 1. ✅

---

## 5. Шаг 4 — UI / Visual тесты

### 5.1 Состояние раннера

```
ls /home/slin/tools/ui-test/      → No such file or directory
which playwright / npx            → not found
find / -name run_tests.js -type f → (нет результатов)
```

UI-test раннер, Playwright и `npx` в этом контейнере отсутствуют.
Запустить TC-UI-01..12 невозможно.

### 5.2 Состояние test-среды (до-ET-013)

```
GET https://openclaw.mva154.duckdns.org/enduro/app.js
HILLSHADE_PAINT in body: False
TRI_PAINT       in body: False
'if (zoom < 9)' in body: False
'if (zoom < 10)' in body: True
```

На test-среде сейчас выкатан **до-ET-013** код. Это **ожидаемо**:
деплой ET-013 — следующий этап пайплайна (deployer → `14-deploy-log.md`).
Визуальную регрессию TC-UI-01..12 имеет смысл прогонять только
ПОСЛЕ деплоя.

### 5.3 План постдеплойного прогона (DEFERRED)

| TC                      | Тип                | viewport | Зум | Что проверяем                                        | Severity | Статус       |
|-------------------------|--------------------|----------|-----|-------------------------------------------------------|----------|--------------|
| TC-UI-01-Z9             | functional+visual  | desktop  | 9   | Чекбокс активен, hint скрыт, hillshade виден         | **P1**   | DEFERRED ¹   |
| TC-UI-02-Z8-REGRESS     | regression+visual  | desktop  | 8   | TRI выглядит как до ET-013                            | P2       | DEFERRED     |
| TC-UI-03-Z9-Q           | visual (qual.)     | desktop  | 9   | Перепады читаются ≥ z=8                               | **P1**   | DEFERRED ¹   |
| TC-UI-04-Z10-Q          | visual (qual.)     | desktop  | 10  | Перепады читаются                                     | P2       | DEFERRED     |
| TC-UI-05-Z11-Q          | visual (qual.)     | desktop  | 11  | Перепады читаются                                     | P2       | DEFERRED     |
| TC-UI-06-Z14-REGRESS    | regression+visual  | desktop  | 14  | Hillshade не «перегрет» (opacity 0.40, contrast 0)    | P2       | DEFERRED     |
| TC-UI-07-Z9-MOBILE      | visual             | mobile   | 9   | Чекбокс/hint работают, нет H-scroll                   | **P1**   | DEFERRED ¹   |
| TC-UI-08-Z10-SAT-Q      | visual (qual.)     | desktop  | 10  | Hillshade поверх спутника не «глушит»                 | P2       | DEFERRED     |
| TC-UI-09-Z10-DARK-Q     | visual (qual.)     | desktop  | 10  | Hillshade на тёмной теме читается                     | P2       | DEFERRED     |
| TC-UI-10-PERSIST        | functional+visual  | desktop  | 10  | F5 не теряет состояние, оба слоя восстановлены        | P2       | DEFERRED     |
| TC-UI-11-NETWORK-Q      | perf (network)     | desktop  | 8-11 | Σ traffic ≤ 135% baseline                            | P2       | DEFERRED     |
| TC-UI-12-Z9-PAN         | perf+visual        | desktop  | 9   | Pan без «белых дыр» в hillshade/TRI                   | P3       | DEFERRED     |

¹ **TC-UI-01, TC-UI-03, TC-UI-07 — заблокированы pre-deploy gate
(см. §4.1):** даже после деплоя ET-013 эти три кейса дадут FAIL,
потому что `/terrain/hillshade/9/*` отдаёт 404 → MapLibre нарисует
hillshade-слой пустым (или с «белыми дырами»), что не соответствует
AC-03 «На карте видны тени рельефа».

**DEFERRED** = тест не запущен в текущем окружении и должен быть
выполнен оператором/Playwright против test-среды **после**:
(a) генерации hillshade z8-z9 тайлов (PH-6 follow-up);
(b) деплоя ET-013.

Результаты приколоть к `14-deploy-log.md`.

---

## 6. Матрица Acceptance Criteria → Test

| AC      | Покрытие                                                                                  | Результат              |
|---------|-------------------------------------------------------------------------------------------|------------------------|
| AC-01   | `test_minzoom_threshold_lowered_to_9`, `test_hint_text_updated_to_z9`                     | ✅ PASS                 |
| AC-02   | DevTools на test-среде                                                                    | ⏳ DEFER → deploy log   |
| AC-03   | TC-UI-01-Z9 + видимость hillshade-слоя                                                    | **❌ BLOCKED** (нет тайлов z9) |
| AC-04   | `test_hillshade_opacity_is_interpolate_by_zoom`, `…contrast_peak_z9`, `…resampling_nearest` | ✅ PASS                 |
| AC-05   | `test_tri_opacity_z8_regression`, `test_tri_opacity_peak_z9_z11`, `…resampling_nearest`   | ✅ PASS                 |
| AC-06   | `test_tri_opacity_z8_regression` (z8 = 0.70 ровно) + TC-UI-02-Z8-REGRESS                  | ✅ PASS (код) / ⏳ DEFER (visual) |
| AC-07   | TC-UI-03-Z9-Q                                                                              | **❌ BLOCKED** (нет тайлов z9) |
| AC-08   | TC-UI-04-Z10-Q                                                                             | ⏳ DEFER → deploy log   |
| AC-09   | TC-UI-05-Z11-Q                                                                             | ⏳ DEFER → deploy log   |
| AC-10   | TC-UI-06-Z14-REGRESS                                                                       | ⏳ DEFER → deploy log   |
| AC-11   | TC-UI-09-Z10-DARK-Q                                                                        | ⏳ DEFER → deploy log   |
| AC-12   | TC-UI-08-Z10-SAT-Q                                                                         | ⏳ DEFER → deploy log   |
| AC-13   | TC-UI-07-Z9-MOBILE                                                                         | **❌ BLOCKED** (нет тайлов z9) |
| AC-14   | TC-UI-10-PERSIST                                                                           | ⏳ DEFER → deploy log   |
| AC-15   | `pytest tests/unit/test_terrain_paint.py` — 17/17                                          | ✅ PASS                 |
| AC-16   | `pytest tests/integration/test_terrain_z9_tiles.py` — 6 pass / 7 skip (по плану)           | ✅ PASS                 |
| AC-17   | Полный `pytest tests/` (исключая lxml-зависимые) — 191 passed, 46 skipped                  | ✅ PASS                 |
| AC-18   | `make lint` (✅) + `make test` (✅ модуль ET-013; полный — внешняя lxml-проблема)            | ✅ PASS                 |
| AC-19   | Pre-deploy `curl -sI .../hillshade/{9,10,11}/X/Y.png` — `hillshade/9` отдаёт **404**       | **❌ FAIL (P1)**       |
| AC-20   | Документация work item (см. §8)                                                            | ✅ PASS (12+ файлов)   |
| AC-21   | TC-UI-11-NETWORK-Q (требует baseline + Playwright)                                         | ⏳ DEFER → deploy log   |
| AC-22   | `test_apply_terrain_layer_normalizes_number_to_legacy_paint` + `…uses_paint_variable`      | ✅ PASS                 |

**Итого:** 10/22 AC закрыты автоматически зелёные · 1 AC **FAIL
(блокер P1)** · 3 AC **BLOCKED** (зависят от AC-19) · 8 AC
делегированы Deployer-агенту.

---

## 7. Findings

### P0
Нет.

### P1

#### P1-01 — Pre-deploy gate AC-19: hillshade z=9 тайлы отсутствуют

**Где.** Test-среда `https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/*.png`.

**Симптом.** Все запросы вида `GET /terrain/hillshade/9/X/Y.png` (и
`hillshade/8/…`) возвращают 404. Покрытие отсутствует на всю
изученную область юга МО / ЦФО (проверено grid'ом 5×5 вокруг
ожидаемой целевой плитки `(309, 348)` под TMS).

**Почему блокер.** После деплоя ET-013 фронт:
- понизит UI-минзум hillshade до 9 → чекбокс «Тени рельефа» станет
  активным на z=9;
- понизит `source.minzoom` до 9 → MapLibre начнёт запрашивать
  `/terrain/hillshade/9/X/Y.png`;
- получит 404 → слой нарисуется пустым.

Пользователь увидит **включённый** слой **без теней**. Это хуже, чем
до ET-013, где чекбокс был disabled с честным hint'ом «Зум 10+».
**Регрессия UX**, явно противоречащая AC-03 / AC-07 / AC-13 / BRD-цели
ET-013 («перепады читаются на z9-z11»).

**Что делать.** TRZ REQ-F-20 §1 и AC-19 однозначно говорят:
> Если 404 — задача останавливается, тайлы z9 нужно догенерировать в
> рамках PH-6 follow-up.

Действия:
1. Открыть PH-6 follow-up: «Generate hillshade tiles z8-z9 for CFO
   coverage» (как минимум область, покрываемая текущим
   `data/terrain/hillshade/10..14/`).
2. После генерации повторно прогнать probe из §4.1.
3. После 200 OK на z=9 — повторный запуск Tester'а + переход на
   Deployer.

**Severity = P1, не P0** только потому, что: (a) код ET-013 корректен
и proven unit/integration-тестами; (b) рег-серверная UI-страница
сейчас работает (тестовая среда держит до-ET-013, чекбокс правомерно
disabled); (c) рабочий процесс PH-6 follow-up — стандартная процедура
для такого класса проблем.

### P2
Нет.

### P3

#### P3-01 — TRI z=14 отдаёт 404 (предсуществующая регрессия PH-6, не в скоупе ET-013)
`GET .../tri/14/X/Y.png → 404`. ET-013 не трогает TRI pipeline,
но при включённом TRI и z>11 пользователь видит пустой слой. Покрыть
follow-up'ом «extend TRI tiles to z14».

#### P3-02 — Cache-Control `immutable` стрипается nginx-проксей на test
Backend FastAPI отдаёт `max-age=31536000, immutable`, на проде через
nginx остаётся только `max-age=31536000`. Формально REQ-F-18 нарушен
на edge-слое, но `max-age=1y` функционально достаточен. Не в скоупе
ET-013.

#### P3-03 — `from __future__ import annotations` в unit-тесте не используется
`tests/unit/test_terrain_paint.py:15` — косметика (унаследовано из
review v2 F-5).

#### P3-04 — Комментарий в `HILLSHADE_PAINT` не учитывает MapLibre clamping ниже z9
`src/web/app.js:2728-2733` — унаследовано из review v2 F-3. Не блокер;
актуально только если UI-минзум hillshade когда-нибудь понизят до z<9.

---

## 8. Документация work item (AC-20)

```
docs/work-items/ET-013/
  00-business-request.md              ✅
  01-brd.md                           ✅
  02-trz.md                           ✅
  03-acceptance-criteria.md           ✅
  04-test-plan.yaml                   ✅
  04b-ui-test-cases.md                ✅
  06-adr/ADR-017-zoom-aware-terrain-paint.md  ✅
  07-infra-requirements.md            ✅
  08-data-requirements.md             ✅
  10-tech-risks.md                    ✅
  12-review.md                        ✅
  13-test-report.md                   ← этот файл
  14-deploy-log.md                    ⏳ ожидается после устранения P1-01
```

---

## 9. Вердикт

**BLOCKED.** Реализация ET-013 в коде корректна и готова к деплою:
- `make lint` и прицельный `make test` (ET-013 модуль) — зелёные.
- 23/23 PASS unit/integration ET-013 (7 SKIP — ожидаемые без data
  fixtures), 0 регрессий на 191 кейсе остальных тестов.
- Соответствие TRZ / ADR-017 — 1:1 (подтверждено Review v2).
- Контракт API на test-среде — стабилен.

Однако **pre-deploy gate AC-19 не пройден** (P1-01): на test-среде
отсутствуют `hillshade/z9/*` (и `z8`) тайлы. Деплой остановлен
согласно TRZ REQ-F-20 §1 и BRD-приоритету «UX-regression > frontend-fix
ready».

### Что должно произойти дальше

1. **Открыть PH-6 follow-up:** «Generate hillshade tiles z8..z9 for
   CFO coverage area» (≈ область, покрытая `data/terrain/hillshade/10/`,
   расширенная вверх по zoom-иерархии).
2. **После генерации тайлов:**
   - повторный пробинг по §4.1 — все 6 ячеек (hillshade/tri × z=9..11)
     должны вернуть 200;
   - повторный запуск Tester'а (изменения отчёта — в виде патча версии
     v2 этого файла, без `back-to:dev` для самого ET-013);
   - переход на Deployer.
3. **Deployer:**
   - накатить ветку `feature/ET-013-z9-z11-z8` в test;
   - выполнить ручные шаги REQ-F-20 §2: открыть карту, `setZoom(9)`,
     включить hillshade, скриншот → визуальная приёмка AC-03..AC-05;
   - прогнать Playwright TC-UI-01..12 (или хотя бы P1: TC-UI-01,
     TC-UI-03, TC-UI-07);
   - замерить network-объём (TC-UI-11/AC-21) против baseline;
   - зафиксировать всё в `14-deploy-log.md`.
4. **Если визуальная приёмка AC-07..AC-09 «перепады недостаточно
   выразительны»** — корректировка stops в HILLSHADE_PAINT/TRI_PAINT
   (это калибровка, не баг — см. BRD §6 «известная итеративность
   калибровки»).

### Что НЕ нужно делать

- **Не back-to:dev для ET-013-frontend.** Код ETM-013 правильный, тесты
  зелёные, ревью пройдено. Изменения в `src/web/app.js` / `src/web/index.html`
  не требуются.
- **Не закрывать ET-013 без устранения P1-01.** Деплой без z9-тайлов
  даст регрессию UX (включённый, но пустой hillshade на z=9).
