22 Commits

Author SHA1 Message Date
543099b740 fix(infra): use python urllib for container healthcheck (ET-015)
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 12s
CI / build (push) Successful in 2s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 12s
CI / build (pull_request) Successful in 2s
Базовый образ `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>
2026-06-05 15:32:34 +00:00
39348f6781 fix(ui): terrain-popup закрывается при открытии bottom-sheet (ET-014)
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 10s
CI / build (push) Successful in 3s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 9s
CI / build (pull_request) Successful in 2s
При открытии любого .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>
2026-06-04 11:20:49 +00:00
099669deeb fix(terrain): расширить whitelist endpoint'а на tri (ET-013 review F-1)
All checks were successful
CI / lint (push) Successful in 5s
CI / lint (pull_request) Successful in 5s
CI / test (push) Successful in 11s
CI / build (push) Successful in 3s
CI / test (pull_request) Successful in 10s
CI / build (pull_request) Successful in 2s
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>
2026-06-04 09:59:51 +00:00
5be81f97a5 feat(terrain): zoom-aware paint для hillshade/TRI на z9-z11 (ET-013)
All checks were successful
CI / lint (push) Successful in 5s
CI / test (push) Successful in 9s
CI / build (push) Successful in 2s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 9s
CI / build (pull_request) Successful in 2s
Понижаем 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>
2026-06-04 09:45:43 +00:00
bbed0e1082 feat(gps-tracks): lower public-tracks minzoom to z5 (ET-012)
All checks were successful
CI / lint (push) Successful in 5s
CI / test (push) Successful in 9s
CI / build (push) Successful in 2s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 10s
CI / build (pull_request) Successful in 3s
Калибровка существующих 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>
2026-06-04 06:29:41 +00:00
721b33a2f6 fix(gps-tracks): address ET-011 review — JS UI tests + flat 403 contract
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 6s
CI / lint (pull_request) Successful in 4s
CI / build (push) Successful in 2s
CI / test (pull_request) Successful in 6s
CI / build (pull_request) Successful in 4s
Закрывает findings из docs/work-items/ET-011/12-review.md (REQUEST_CHANGES,
попытка 3/3):

P1-01 — добавлены поведенческие JS unit-тесты UI download-flow
  - tests/web/track_download.test.js — 28 кейсов (node --test):
      • _parseFilenameFromCD — RFC 5987 приоритет, plain fallback,
        битый percent-encoding, null/empty (REQ-F-05.2, AC-2 UI)
      • _handleDownloadError — 400/403/404/413/5xx тосты, defensive
        при отсутствии showToast, поддержка flat (ADR-015 §G) и legacy
        wrapped 403-форм (REQ-F-05.4, AC-7 UI)
      • _renderTrackPopupHtml — наличие кнопки, aria-label «Скачать GPX»,
        data-track-id, отсутствие при невалидном id, регрессия прочих
        полей (REQ-F-01, AC-1)
  - tests/web/test_track_download.py — pytest-обёртка (статические
    проверки + запуск Node-раннера), исполняется в обычном pytest tests/
  - 04b-ui-test-cases.md: AC-13 (mobile-bbox) явно маркирован как
    MANUAL release-smoke (Playwright-раннер в проекте не настроен;
    альтернатива согласована reviewer'ом в P1-01).

P2-01 — устранено расхождение «doc vs runtime» по контракту 403
  - endpoint.py: HTTPException(detail={...}) → JSONResponse(content={...}),
    чтобы FastAPI не оборачивал dict во второй слой «detail». Контракт
    теперь совпадает с ADR-015 §G и ADR-014 §6:
        {"detail":"source_forbidden","external_urls":[...]}
  - test_gps_tracks_download.py IT-05: упрощено — body уже плоский,
    без двухуровневого `body.get("detail", body)` workaround.
  - gps_tracks.js::_handleDownloadError: flat-форма стала приоритетной,
    wrapped-форма оставлена как defensive fallback (с комментарием).

Регрессия: 89/89 API-тестов + 24/24 предыдущих JS-тестов + 28 новых
JS-тестов download-flow проходят. ruff check — clean.

Refs: ET-011

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 23:01:19 +00:00
eea6c846c2 feat(gps-tracks): GPX download from public track popup
Some checks failed
CI / lint (push) Failing after 5s
CI / test (push) Failing after 6s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 4s
CI / test (pull_request) Failing after 5s
CI / build (pull_request) Has been skipped
Реализация ET-011: кнопка «Скачать GPX» в popup публичного GPS-трека и
новый эндпоинт GET /api/gps-tracks/{track_id}/download (GPX 1.1 +
Content-Disposition с UTF-8 именем по RFC 5987). Реэкспорт защищён
per-source флагом `download_allowed` в `config/gps_sources.yaml`
(default-deny, MVP whitelist = `osm`).

Backend:
- `src/api/gps_tracks/export.py` — чистый stdlib-builder GPX 1.1
  (`build_gpx`) + санитизация имени файла (`safe_filename`, RFC 5987).
- `src/api/gps_tracks/endpoint.py` — новый route с проверками
  400 / 403 / 404 / 413; cap 200 000 точек (REQ-NF-02).
- `src/api/gps_tracks/config.py` — `load_download_allowed_sources()`
  читает YAML, default-deny при отсутствии поля; fallback на `{"osm"}`
  при отсутствии конфига.
- `src/api/main.py` — пробрасывает `GPS_SOURCES_CONFIG_PATH` в router.

Frontend:
- `src/web/gps_tracks.js` — кнопка в `_renderTrackPopupHtml`,
  обработчик `_downloadPublicTrack` (fetch + Blob + a.download — тот же
  паттерн, что в `app.js::downloadGPX`, R-1 митигирован), парсер
  `_parseFilenameFromCD` для RFC 5987, маппинг ошибок
  `_handleDownloadError` (403/404/413/5xx → showToast).
- `src/web/app.css` — стиль кнопки, 32×32 CSS px (REQ-NF-04).

Тесты:
- 13 unit для GPX-builder (UT-01/02/03/05; XSD-валидация против
  `tests/fixtures/gpx-1.1/gpx.xsd`).
- 10 unit для `safe_filename` (UT-04).
- 11 integration для download-эндпоинта (IT-01..08 +
  ANY-rule license check + default-deny без конфига).

ADR-014 (gpx-download-endpoint), ADR-015 (source-redistribution-policy).
Refs: ET-011

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 20:59:53 +00:00
3577ff32ac feat(ET-009): activate EnduroRussia + Wikiloc GPS sources
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 5s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 4s
CI / test (pull_request) Failing after 5s
CI / build (pull_request) Has been skipped
Конфиг-only активация двух новых источников GPS-треков поверх
pipeline ET-008. Не вводит новых компонентов, БД-таблиц, endpoint'ов.

Config:
- config/gps_sources.yaml: enduro_russia enabled=true, base_url исправлен
  на endurorussia.ru (без дефиса); добавлена запись wikiloc с
  max_tracks_per_run=50, activity_filter=[motorcycle, enduro].
- config/gps_regions.yaml: wikiloc добавлен в tsfo_plus_chuvashia.sources.

Parser:
- wikiloc.py: добавлен soft-cap max_tracks_per_run в collect(),
  извлечение created_at из GPX metadata/первого trkpt — для корректной
  межисточниковой дедупликации с EnduroRussia.

UI (src/web/gps_tracks.js):
- GPS_SOURCE_COLORS: добавлен цвет wikiloc (#4363d8).
- Дефолтный фильтр sources включает wikiloc.
- GPS_SOURCE_ATTRIBUTIONS: маппинг source_id → строка атрибуции;
  _updateGpsAttribution() подтягивает /api/gps-tracks/health и
  выставляет attribution с теми источниками, у которых tracks > 0.
- _buildGpsFiltersUI: чекбокс «Wikiloc» в #gps-source-grid.

Tests:
- Fixtures: 7 файлов в tests/fixtures/gps-tracks/.
- Unit: 10 UT-ER + 10 UT-WL — парсеры, MAPPING, bbox-фильтр,
  pagination, 429/403 graceful-stop, rate-limit, max_tracks_per_run.
- Integration: IT-ER-01, IT-WL-01, IT-WL-02, IT-DEDUP-01, IT-LIC-01
  через scripts.gps_collect.main + httpx.MockTransport.
- Contract: 2 CT-ER с маркером @pytest.mark.network (nightly only).
- JS: 2 новых теста на наличие wikiloc в SOURCE_COLORS и в фильтрах.

Linters/Tests: ruff clean (новые файлы), 166 pytest passed,
24 JS-tests passed.

Refs: ET-009
Acceptance: AC-01..AC-08, AC-14..AC-17 (для AC-09..AC-13 — продакшн-прогон)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:38:55 +00:00
1ffa178b38 fix(gps-tracks): aggregate last_pipeline_run in health endpoint (REQ-F-12)
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 5s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 3s
CI / test (pull_request) Failing after 5s
CI / build (pull_request) Has been skipped
Replace raw single-row fetch with aggregation over all pipeline_runs
rows sharing the latest started_at. Returns structured object with
regions[], sources_ok[], sources_error[], tracks_added instead of
a raw DB row with region_id/source_id strings.

Returns null when no runs exist (empty DB).

Update test_i40_health_endpoint: add db_with_pipeline_runs fixture
(two rows, same started_at, two regions) and assert the full
aggregated shape including concrete values.

Refs: ET-008
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:27:52 +00:00
ba356ae317 fix(gps-tracks): rename health fields and fix layer insert priority (F-04, F-05)
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 5s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 4s
CI / test (pull_request) Failing after 5s
CI / build (pull_request) Has been skipped
F-04: rename gps_health() response fields per tester feedback:
  - total_tracks → tracks_total
  - by_activity  → tracks_by_activity
  - recent_pipeline_runs (list) → last_pipeline_run (object | null)
  Change LIMIT from 10 to 1; fetch single row instead of a list.

F-05: rewrite _findGpsInsertPosition with explicit priority order:
  1. gpx-layer-* (ET-006 GPX file layers) — highest priority
  2. route-* (ET-002 routing layers)
  Remove old combined find() that lacked clear priority semantics.

Add tests/web/gps_tracks.test.js (22 JS unit tests via node:test):
  - _findGpsInsertPosition priority logic (9 cases)
  - Filter state management — default state assertions (5 cases)
  - Color palette mapping and _buildColorExpression (8 cases)

Add tests/web/test_gps_tracks.py — Python pytest runner (8 static
  checks + node --test invocation).

Refs: ET-008

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:18:06 +00:00
edbe9a3044 fix(gps-tracks): normalise GeoJSON props, add health fields, OSM meta fetch, z-order fix
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 4s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 3s
CI / test (pull_request) Failing after 4s
CI / build (pull_request) Has been skipped
Refs: ET-008

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:42:36 +00:00
0060003f28 feat(gps-tracks): ET-008 публичные GPS-треки с публичных платформ
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 4s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 4s
CI / test (pull_request) Failing after 4s
CI / build (pull_request) Has been skipped
Backend:
- Миграция gps_tracks_001_init.sql: таблицы tracks + pipeline_runs
- Пакет src/api/gps_tracks/: models, db (WAL+upsert с dedup), dedup
  (bbox+length+date bucket-hash), mvt (LRU-кэш 1024 тайла), endpoint
  (GET /api/gps-tracks, GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt,
   GET /api/gps-tracks/health, POST /api/gps-tracks/cache/clear), config
- Парсеры: osm (split_bbox, haversine, defusedxml XXE-защита),
  enduro_russia + ttrails — заглушки (ADR-010/011 proposed, блокированы)
- Licensing guard: pipeline проверяет status ADR-файла до запуска источника
- scripts/gps_collect.py: CLI с --region/--source/--dry-run/--gc

Frontend:
- src/web/gps_tracks.js: двухрежимный слой (MVT z≤11, GeoJSON z≥12),
  debounced fetch + AbortController, фильтры активности/источника,
  цветовая палитра by-source/by-activity, halo на спутнике, popup трека,
  restorePublicTracksState(), localStorage persistence
- index.html: чекбокс «Публичные треки» в terrain-popup, #sheet-gps-filters
- app.css: .terrain-link-btn, .gps-filter-grid, .track-popup
- app.js: вызов restorePublicTracksState() в rebuildMapOverlays(),
  applyGpsHaloVisibility() в applyBaseLayer()

Конфиги:
- config/gps_sources.yaml: osm (enabled), enduro_russia/ttrails (disabled)
- config/gps_regions.yaml: ЦФО+Чувашия (enabled), Кавказ (disabled)

Docker:
- gps-collector service с profiles: [batch]

Тесты: 48 новых тестов (unit + integration), 125/125 pass

Refs: ET-008

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:28:54 +00:00
1984b0bde6 fix(ET-007): address 6 P1 findings from review (docs + code)
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 6s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 5s
CI / build (push) Successful in 4s
CI / build (pull_request) Successful in 2s
12-review.md (REQUEST_CHANGES, attempt 2/3) flagged 6 must-fix items
in the analysis/architecture artefacts plus matching bugs that had
already leaked into the committed implementation. This patch lands
both: documents corrected, code aligned with corrected specs, tests
updated.

P1-1: TRZ/ADR/Data/Risks referenced fictional layer ids
(`trails-grade1..5-halo-satellite`, `paths-bridleway-halo-satellite`).
Actual style*.json has only `trails-track-halo-satellite` and
`trails-path-bridleway-halo-satellite`; grade differentiation lives
inside one `match` expression on `tracktype` within `trails-track`.
Docs rewritten to operate on real ids.

P1-2: POI labels contrast was broken — spec changed only halo-color
to black, leaving `text-color: #333333` (light theme baseline)
unreadable over the new black halo. Code+docs now switch BOTH
`text-color` (-> `#ffffff` on satellite) AND halo together, with
per-theme baselines (`#333333` light / `#e0e0e0` dark) restored on
return to Schematic.

P1-3: BRD §5 hillshade risk said «hillshade auto-disabled on
satellite», contradicting TRZ/ADR/AC. BRD wording aligned: hillshade
keeps working over satellite; visual check is AC-04.

P1-4: background-color had four divergent sources (`#1a1a1a`,
`#2a2a2a`, `#1a1a2e`, `#f0ede6`), incl. an inverted-theme typo and a
baseline `#1a1a1a` that didn't match the actual `style-dark.json:28`
value `#1a1a2e`. Settled on ADR-004's single-constant model: `#2a2a2a`
on satellite for both themes; on Schematic restore per-theme baselines
`#f0ede6` (light) / `#1a1a2e` (dark). `_applyBackgroundForSatellite`
fixed accordingly.

P1-5: app.js already had `layerState.basemap` and `toggleLayer
('basemap')` (legacy «Базовая карта» switch). Neither TRZ nor ADR
specified the interaction. Added save&restore contract: on entering
Satellite save `layerState.basemap` to `_savedBasemapState` and
force-hide `osm-base`; on returning to Schematic restore osm-base
visibility from the saved value. CSS hook `body.satellite-active
#btn-basemap { display:none }` keeps the user from trying to enable
a hybrid mode (out of scope, BRD §3). TRZ §5.6, ADR-004 §8.

P1-6: `restoreTrailsState()` and `onTrailsCheckbox()` only managed
visibility of `trails-track` / `trails-path-bridleway`, leaving
their halo-underlay siblings as «phantom» halos when the user
unchecked grunты/тропы under Satellite. Introduced
`_applyTrailHaloVisibility(map, base)` reading checkbox state from
DOM; called from `onTrailsCheckbox`, `restoreTrailsState`, and both
branches of `applyBaseLayer`. Rule: halo visible ⇔ (base ===
satellite) AND (checkbox ON). TRZ §5.7, ADR-004 §9.

Docs bumped: BRD v2, TRZ v2, AC v2, Data v2, Risks v2; ADR-004
получает «Ревизии»-секцию (status remains accepted — only editorial
fixes, no decision change).

Tests:
- tests/unit/base_layer.test.js: rewritten 2 background-color
  assertions (#1a1a1a expectation removed), added 6 new tests for
  P1-2 / P1-4 (POI text-color per-theme baselines, single satellite
  bg #2a2a2a, baseline restore on Schematic).
- All 33 JS unit tests + 22 pytest static checks green.
- Full pytest suite: 76 passed (excluding pre-existing
  shapely-import skipped collection in tests/unit/test_health.py).

Refs: ET-007
Review: docs/work-items/ET-007/12-review.md (P1-1..P1-6)
ADR: docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md (rev. 2026-05-31)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 21:05:49 +00:00
475d42187d feat(web): спутниковая подложка с переключателем Схема/Спутник
All checks were successful
CI / lint (push) Successful in 3s
CI / test (push) Successful in 5s
CI / build (push) Successful in 1s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 5s
CI / build (pull_request) Successful in 1s
ET-007: добавлен сегментированный переключатель «Подложка» в попап
слоёв; ленивое создание Esri World Imagery raster-source при первом
включении «Спутник»; восстановление выбора из localStorage и переживание
смены темы через rebuildMapOverlays().

- src/web/index.html: блок .terrain-base-row в #terrain-popup
- src/web/app.css: стили .terrain-base-row / .terrain-base-label / .base-seg
- src/web/app.js: блок ET-007 с onBaseLayerToggle, applyBaseLayer,
  restoreBaseLayerState, syncBaseLayerUI; хук в rebuildMapOverlays()
  первым, чтобы terrain/trails/POI лежали поверх спутника
- src/web/style.json, style-dark.json: halo-underlay-слои
  trails-track-halo-satellite и trails-path-bridleway-halo-satellite
  (visibility:none по умолчанию, включаются на спутнике для контраста)
- tests/unit/base_layer.test.js: 28 behavioural JS-тестов (U-01..U-05,
  U-10..U-11, I-01..I-07, halo, z-order, private mode, тёмная тема)
- tests/unit/test_base_layer.py: 22 pytest-проверки (HTML/CSS/app.js/
  style.json структурные + node --test runner)

Refs: ET-007
ADR: docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 20:09:19 +00:00
b09907e683 feat: add UI/visual testing to pipeline
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 5s
CI / build (push) Successful in 2s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 4s
CI / build (pull_request) Successful in 1s
2026-05-23 01:58:14 +03:00
25e4476cf7 fix(gpx): устранить падение статистики на больших треках, учесть все треки файла
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 5s
CI / lint (pull_request) Successful in 3s
CI / test (pull_request) Successful in 5s
CI / build (push) Successful in 3s
CI / build (pull_request) Successful in 2s
Правки по код-ревью ET-006 (docs/work-items/ET-006/12-review.md):

- P1-1: trackStats считал min/max высот через Math.min/max.apply — на
  треках в сотни тысяч точек это бросало RangeError и валило загрузку
  файла (нарушение REQ-NF-01). Расчёт переписан на однопроходный
  аккумулятор (makeStatsAccumulator/accumulatePoint/finalizeStats)
  без apply.
- P2-1: статистика и профиль высот учитывали только tracks[0].
  Добавлены aggregateStats() и buildFileProfileSamples() — сводка и
  профиль теперь охватывают все треки файла (REQ-F-09, AC-02).
- P2-2: расчёт статистики на async-пути парсинга вынесен в чанковый
  trackStatsChunked() — соответствие букве ADR-003 §2.
- P3-1: ось и tooltip профиля высот форматируют расстояние через
  formatKm() — согласование с выбором км/мили из ET-005.
- P3-2: childText() переименована в firstTagText() — имя соответствует
  фактическому поведению (поиск по всем потомкам).
- P3-4: убран дублирующийся 'use strict'.

Добавлены регрессионные unit-тесты: большой трек без падения,
эквивалентность trackStatsChunked синхронному trackStats (в т.ч. на
треке длиннее размера чанка), агрегация статистики и профиля по
многотрековому файлу.

Refs: ET-006

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 06:01:51 +00:00
55c9c389cd feat(gpx): загрузка и визуализация GPX-треков (ET-006)
Клиентская загрузка GPX 1.1: парсинг через DOMParser с чанковой
конвертацией (ADR-003), отрисовка треков и waypoints на карте,
панель #sheet-gpx со списком треков, статистикой и canvas-профилем
высот. GPX-слои встают ниже маршрута OSRM и восстанавливаются после
смены стиля карты (REQ-F-13).

- src/web/gpx.js — новый модуль фичи (ADR-002): парсинг, модель
  window.gpxTracks, слои/маркеры карты, sheet-gpx, профиль высот
- index.html / app.css — кнопка загрузки, кнопка тулбара, панель
  #sheet-gpx, toast-уведомления, индикатор парсинга
- app.js — один хук rebuildGpxOverlays() в rebuildMapOverlays()
- тесты: gpx.test.js (node --test, U-01..U-21) +
  test_gpx_upload.py (pytest: статические проверки + JS-раннер)

Refs: ET-006

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:00:27 +00:00
2fe5cfe453 feat(web): переключатель единиц измерения расстояний (км/мили)
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 5s
CI / build (push) Successful in 2s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 4s
CI / build (pull_request) Successful in 1s
Добавляет сегментированный toggle км/мили в попап рельефа. Новый модуль
src/web/units.js — единственный источник истины по выбору единицы, её
персистентности (localStorage: distance_unit, дефолт km) и форматированию
отображаемых расстояний (Units.formatDistance).

Все места форматирования в app.js переведены на централизованный
форматтер; пересчёт всех видимых расстояний выполняет единый оркестратор
onUnitChange по событию unitchange (карточки маршрутов, лист точек,
линейка, масштабная линейка, связка, «красивый» маршрут).

Экспорт GPX и параметры построения маршрута остаются метрическими
(риск R6). units.js подключается строго перед app.js (риск R7).

Refs: ET-005
2026-05-21 19:36:13 +00:00
8c17a4f508 feat(web): add POI visibility checkbox to terrain popup
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 5s
CI / build (push) Successful in 15s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 4s
CI / build (pull_request) Successful in 3s
Adds a «POI» checkbox to the terrain popup that toggles the
poi-circles and poi-labels layers via map.setLayoutProperty. The
choice is persisted in localStorage (key `poi-visible`) and restored
on page load and after style changes, kept consistent with the
runtime layerState.poi per ADR-0001.

Tests: behavioral JS unit tests (TP-01..TP-04) run via `node --test`,
wrapped by tests/unit/test_poi_toggle.py with static structure checks
so they execute under the existing `pytest tests/` CI step.

Refs: ET-002

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:50:54 +00:00
e263f8425c feat(ET-001): implement barrier blocking and footway exclusion in OSRM profile
All checks were successful
CI / lint (push) Successful in 7s
CI / test (push) Successful in 6s
CI / build (push) Successful in 2s
- enduro.lua: блокировка нод barrier=gate|bollard|lift_gate|chain|cycle_barrier|
  motorcycle_barrier|border_control|block через mode.inaccessible (ADR-001).
  cattle_grid и ford остаются проезжими.
- enduro.lua: highway=footway|pedestrian|steps|corridor полностью исключены
  из графа (early return в process_way). Эти типы удалены из highway_rate
  и highway_speeds, чтобы профиль был самодостаточным.
- scripts/rebuild-osrm.sh: пересборка графа (extract → partition → customize)
  и рестарт контейнера osrm-routed.
- tests/integration/test_routing_barriers.py: 7 тестов (TC-001..TC-005 +
  статический анализ blocked_barriers/excluded_highways). Интеграционные тесты
  скипаются если OSRM не доступен.

Refs: ET-001
2026-05-15 22:11:32 +03:00
76ff0c6587 feat: add pyproject.toml, dev dependencies, first unit test
Some checks failed
CI / lint (push) Failing after 16s
CI / test (push) Failing after 2s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 3s
CI / test (pull_request) Failing after 2s
CI / build (pull_request) Has been skipped
2026-05-15 17:05:46 +03:00
5d7fda44bb feat: initial project structure
- CLAUDE.md project passport
- Agent system prompts (architect, developer, reviewer, tester, deployer)
- Gitea Actions CI pipeline (lint, test, build)
- Docker configuration (Dockerfile, docker-compose.yml)
- Canonical directory structure per BRD
- Makefile with dev/test/lint/build/deploy targets
- Architecture docs skeleton

Refs: multi-agent F0-3, F0-5, F0-6, F0-8
2026-05-15 13:30:14 +03:00