Базовый образ `python:3.12-slim` не содержит `curl`, поэтому текущий
healthcheck `["CMD", "curl", "-f", ...]` всегда падает (`exec: "curl":
executable file not found`), и контейнер `enduro-trails-app-1` висит
в статусе `unhealthy` (≥31 час, FailingStreak 3762 при RestartCount 0),
несмотря на то что приложение исправно отвечает HTTP 200 на /api/health.
Заменяем healthcheck на python one-liner через stdlib `urllib.request`
(ADR-020). Изменения:
• docker-compose.yml, сервис app:
test: ["CMD", "python", "-c",
"import urllib.request,sys; sys.exit(0 if
urllib.request.urlopen(...timeout=3).status == 200 else 1)"]
+ start_period: 20s
interval/timeout/retries сохранены (30s / 5s / 3).
Внутренний urlopen(timeout=3) строго меньше внешнего healthcheck
timeout=5s (AC-07).
• Dockerfile НЕ меняется (никаких apt-get install curl/wget — BRD §6,
AC-04). Деплой без ребилда: `docker compose up -d app` достаточно.
• src/api/main.py НЕ меняется. Контракт /api/health сохранён (AC-08).
Покрытие:
- tests/static/test_healthcheck_compose.py — 10 тестов (ST-01..ST-07
+ защита от регресса по target URL / start_period / baseline params).
- tests/unit/test_healthcheck_oneliner.py — 6 тестов (UT-01..UT-03),
исполняют ровно ту же one-liner-команду через subprocess против
локального мок-HTTPServer (200/301/404/500/503) и неиспользуемого
порта. URL подменяется через `_retarget`, чтобы тестировать живой
код из compose, а не его копию.
ADR: docs/work-items/ET-015/06-adr/ADR-020-healthcheck-via-python-urllib.md
CHANGELOG: запись в [Unreleased] / Fixed.
Refs: ET-015
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
При открытии любого .bottom-sheet через openSheet() теперь принудительно
скрывается #terrain-popup и снимается .active с #terrain-toggle. Это
устраняет z-index конфликт (popup z=500 над sheet z=400) и убирает
anti-pattern «два меню одновременно» на desktop без правки CSS-стека
(marker-dialog z=500, search-panel, ruler-info — без регрессий).
Реализация — Вариант A из ADR-019: helper closeTerrainPopup() + один
вызов первой строкой в openSheet() после null-check. Для других sheets
(sheet-route, sheet-recon, sheet-scenic, sheet-link, sheet-gpx) вызов
безопасный no-op, REQ-F-06 выполняется автоматически.
Тесты:
- tests/unit/sheet_popup.test.js — 8 behavioral JS unit-тестов
(TC-U-02, REQ-F-04, REQ-F-06 + ребра closeTerrainPopup).
- tests/unit/test_sheet_popup.py — pytest-обёртка: статические проверки
app.js (порядок вызовов в openSheet, маркеры блока), охранные тесты
что z-stack не тронут и что gps_tracks.js/index.html не правились.
Refs: ET-014
ADR: docs/work-items/ET-014/06-adr/ADR-019-terrain-popup-yields-to-sheet.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer'ом найден pre-existing P1: backend `terrain_tile` whitelist
не пропускал слой `tri`, хотя фронтенд (`onTerrainCheckbox`) шлёт
запросы на `/terrain/tri/{z}/{x}/{y}.png` для слоя «Перепады высот».
На test/prod-среде эти запросы перехватывает nginx (подтверждено
эмпирически — 404 идёт с сигнатурой `nginx/1.18.0 (Ubuntu)`, а не
с FastAPI JSON-detail), но в dev-режиме (`make dev` → FastAPI на
:5556 напрямую) endpoint обязан поддерживать `tri` нативно.
Изменения:
- `src/api/main.py:1252`: whitelist `("hypso", "hillshade")` →
`("hypso", "hillshade", "tri")`. Ответ-контракт и заголовки
идентичны существующим слоям; REQ-F-18 «API contract без изменений»
не нарушен (поведение для уже-известных layer'ов не меняется,
добавляется только поддержка нового layer'а).
- `tests/integration/test_terrain_z9_tiles.py`: новый параметризованный
тест `test_known_terrain_layer_accepted_by_whitelist[hypso|hillshade|tri]`,
фиксирующий регрессию F-1 (не требует локальных PNG-данных:
для несуществующего файла ожидает `detail: "Tile not found"`,
а не `"Unknown layer"`).
- `tests/integration/test_terrain_z9_tiles.py`: параметризация
`test_terrain_tile_available_z9_z10_z11` по `(layer × zoom)` —
6 кейсов вместо 3 (review F-2).
- `tests/integration/test_terrain_z9_tiles.py`: убран неиспользуемый
`from __future__ import annotations` (review F-4); type-аннотации
упрощены (Python 3.10+ нативно).
- `tests/integration/test_terrain_z9_tiles.py`: `test_unknown_terrain_layer_returns_404`
усилен ассертом `detail == "Unknown layer"` (парность с whitelist-тестом).
Тесты: 17/17 unit PASS, 6/6 non-data-зависимых integration PASS,
6 layer×zoom кейсов SKIPPED (нет PH-6 данных в sandbox — корректное
поведение `_maybe_skip`).
Refs: ET-013, review F-1/F-2/F-4 (`docs/work-items/ET-013/12-review.md`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Понижаем UI-минзум hillshade с 10 до 9 и переводим raster-paint
обоих terrain-слоёв в zoom-aware форму через MapLibre interpolate.
На z9-z11 — пик opacity/contrast, чтобы рельеф читался как на z8;
на z12-z14 — возврат к исходным значениям (регрессия по AC-10).
TRI на z8 остаётся 0.70 (регрессия по AC-06), пик 0.80-0.85 на z9-z11.
Изменения:
- src/web/app.js: добавлены HILLSHADE_PAINT и TRI_PAINT; applyTerrainLayer
расширена для поддержки object-paint (обратно-совместимо); порог
updateHillshadeAvailability понижен до 9; вызовы для hillshade переведены
на minzoom=9.
- src/web/index.html: hint обновлён с «Зум 10+» на «Зум 9+».
- tests/unit/test_terrain_paint.py: 17 тестов покрытия zoom-stops, контракта
applyTerrainLayer и регрессий (UT-PAINT-*, UT-REG-*).
- tests/integration/test_terrain_z9_tiles.py: smoke /terrain endpoint на
z9-z11 + кэш-заголовки (IT-TILE-*).
Backend, тайлы на диске, конфиги, стили — без изменений.
Refs: ET-013
ADR: ADR-017
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Калибровка существующих tier-таблиц `build_gps_mvt` /
`_simplify_coords` (ADR-016), чтобы при первом открытии карты
пользователь видел общее покрытие сети треков, а не пустую подложку.
Backend (src/api/gps_tracks/mvt.py):
- build_gps_mvt: добавлены тиры z<=5 (min_length=10 км, limit=1500)
и z=6 (5 км / 2000); z=7+ — без изменений (регрессия).
- _simplify_coords: tolerance для z=6 = 0.018° (~2 км),
для z<=5 = 0.04° (~4 км); z=7+ не меняется.
Frontend:
- GPS_TRACKS_MIN_ZOOM понижен с 8 до 5; vector-source.minzoom
подхватывает константу автоматически.
- line-width / halo получили stop на z=5 (0.8 / 1.8 CSS-px),
чтобы линия была читаема на любом DPR.
- Hint #public-tracks-zoom-hint: «Зум 8+» → «Зум 5+».
Тесты:
- 8 unit zoom-tier (UT-Z5/6/7/8/12) — REQ-F-09.
- 10 unit simplify (UT-SIMP-*) — REQ-F-10.
- 9 integration endpoint z5-z7 (IT-Z5/6/7, CACHE, REGRESS) — REQ-F-11/12.
- 2 perf (PERF-Z5-01/02; avg ~64 ms, p95 ~89 ms при 500 треках —
ниже бюджета 200/500 ms по M-6) — REQ-F-13.
Маркер @pytest.mark.perf, не в основном CI-gate.
Контракт API /api/gps-tracks* не меняется (REQ-F-15);
localStorage-ключи и конфиги тоже (REQ-F-16, F-18).
Refs: ET-012
ADR: docs/work-items/ET-012/06-adr/ADR-016-z5-tiling-policy.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #21 merged to main and tag v0.0.3 pushed, but docker-compose roll on
test host did not happen: /home/slin/bin/enduro-deploy-hook.sh exits with
"Permission denied" on /var/log/enduro-trails/deploy-hook.log
(root-owned, no NOPASSWD sudo for slin). Healthcheck/smoke/rollback all
skipped — new code is on main but old image still serves traffic.
Action for ops: see docs/work-items/ET-011/14-deploy-log.md
("Что нужно от ops, чтобы доехать"). After fix, re-run deploy hook —
PR/tag do not need to be redone.
CI failures на feature/ET-011 были вызваны двумя проблемами:
1. ruff `E402 Module level import not at top of file` × 10 в src/api/main.py:
- 9 ошибок от ET-008 (GPS_TRACKS_DB_PATH между импортами) +
1 новая от ET-011 (`from src.api.gps_tracks.endpoint import ...` после
определения `app`). Перенёс все импорты наверх; константы
GPS_TRACKS_DB_PATH и GPS_SOURCES_CONFIG_PATH теперь сразу после import-блока,
а создание router-а остаётся в нижней части файла (зависит от `app`).
2. pyproject.toml не объявлял runtime-deps, которые реально импортируются
в src/ (defusedxml, pyyaml) и в тестах (lxml). Dockerfile брал их из
src/api/requirements.txt, но CI jobs `lint`/`test` ставят `.[dev]` —
поэтому `pytest tests/` падал на ModuleNotFoundError при коллекции
тестов из ET-008/ET-009/ET-011. Добавил недостающие пины в pyproject
(defusedxml/pyyaml в основные deps, lxml — только в dev, нужен для
XSD-валидации в test_gps_tracks_download/_gpx_builder).
Проверено локально в чистом venv после `pip install .[dev]`:
- `ruff check src/` → All checks passed
- `pytest tests/` → 200 passed, 2 deselected
Refs: ET-011
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>