# Dev Task: Стабилизация прототипа Enduro Trails v0.1 > ⚠️ **АРХИВ (выполнено 02.05.2026)** — все задачи из Definition of Done выполнены. > Документ описывает состояние "до фиксов" — сохранён для исторического контекста. > Актуальное состояние проекта — в `PROJECT.md`. **Приоритет:** HIGH **Тип:** стабилизация, без новых фич **Проект:** enduro-trails **Сервер:** `slin@82.22.50.71`, пароль: `motoZ@yaz2010` **Контейнер:** `enduro-trails`, порт `5558` **Workspace:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/` --- ## Контекст Прототип работает, но после серии горячих фиксов накопились несоответствия между кодом, схемой БД и фронтом. Нужно привести всё в консистентное состояние. **Никаких новых фич.** --- ## Текущее состояние файлов ### `prototype/app.py` — рабочий, но есть проблемы: 1. `build_mvt()` не передаёт `length_m` и `mtb_scale` в props trails 2. POI выбирается без bbox-фильтра в SQL (`SELECT ... FROM poi LIMIT 2000`), фильтрация в Python 3. SQL для trails использует f-string для LIMIT: `LIMIT {limit}` — нужно параметризовать 4. Нет валидации z/x/y параметров ### `prototype/static/index.html` — есть баги: 1. `toggleLayer()` использует глобальную переменную `map`, но она объявлена как `const map` внутри `initMap()` — вне функции недоступна. Нужно использовать `window._map` (оно уже присваивается: `window._map = map`) 2. `layerGroups` ссылается на несуществующие слои: - `'trails-grade12'` — нет в style.json - `'trails-grade345'` — нет в style.json - Реальные слои в style.json: `'trails-track'`, `'trails-asphalt'`, `'trails-path-bridleway'` 3. Весь JS и CSS в одном файле — нужно вынести в `app.js` и `app.css` ### `prototype/static/style.json` — чистый, слои: - `background`, `osm-base`, `trails-asphalt`, `trails-track`, `trails-path-bridleway`, `poi-circles`, `poi-labels` ### `prototype/requirements.txt` — версия неверная: - Указано `mapbox-vector-tile==2.0.1` - В контейнере реально установлено `2.2.0` (нужен параметр `default_options={'y_coord_down': True}`, появился в 2.2.0) - Нужно исправить на `mapbox-vector-tile==2.2.0` - Убрать parse-зависимости из runtime requirements: `python-osmium`, `pysqlite3-binary`, `pyproj` ### `scripts/parse.py` — не соответствует реальной схеме БД: - Создаёт таблицу `trails` БЕЗ колонок `min_lon`, `max_lon`, `min_lat`, `max_lat` - Реальная БД имеет эти колонки + индекс `idx_trails_bbox` - `app.py` использует эти колонки в SQL — без них упадёт при пересоздании БД - Нужно добавить вычисление bbox и запись этих колонок при парсинге - Таблица `poi` не имеет колонок `lon`/`lat` для SQL-фильтрации --- ## Задачи ### 1. Исправить `scripts/parse.py` Добавить в схему таблицы `trails`: ```sql min_lon REAL, max_lon REAL, min_lat REAL, max_lat REAL ``` При вставке каждого трека вычислять bbox из координат: ```python lons = [c[0] for c in coords] lats = [c[1] for c in coords] min_lon, max_lon = min(lons), max(lons) min_lat, max_lat = min(lats), max(lats) ``` Добавить в `batch_trails.append(...)` эти четыре значения. Добавить индекс: ```sql CREATE INDEX IF NOT EXISTS idx_trails_bbox ON trails(min_lon, max_lon, min_lat, max_lat); ``` Для таблицы `poi` — добавить колонки для SQL-фильтрации: ```sql lon REAL, lat REAL ``` При вставке POI заполнять `lon`/`lat` из координат точки (`coords[0]`, `coords[1]`). Добавить индекс: ```sql CREATE INDEX IF NOT EXISTS idx_poi_coords ON poi(lon, lat); ``` **Expected:** после `python scripts/parse.py` новая БД совместима с `app.py` без ручных доправок. --- ### 2. Исправить `prototype/app.py` #### 2.1 Добавить `length_m` и `mtb_scale` в trail props В `build_mvt()`, в блоке формирования `props` для trails: ```python props = { "highway": row["highway_type"] or "", "tracktype": row["track_type"] or "", "surface": row["surface"] or "", "name": row["name"] or "", "length_m": row["length_m"] or 0, "mtb_scale": row["mtb_scale"] or "", } ``` #### 2.2 Исправить POI запрос — SQL bbox вместо Python-фильтрации ```python cur.execute(""" SELECT osm_id, poi_type, name, geom FROM poi WHERE lon >= ? AND lon <= ? AND lat >= ? AND lat <= ? LIMIT 500 """, (q_west, q_east, q_south, q_north)) poi_rows = cur.fetchall() ``` Убрать Python-фильтрацию `poi_rows` после запроса. #### 2.3 Параметризовать LIMIT ```python # было: cur.execute(f"... LIMIT {limit}", (q_east, q_west, q_north, q_south)) # стало: cur.execute("... LIMIT ?", (q_east, q_west, q_north, q_south, limit)) ``` #### 2.4 Добавить валидацию z/x/y в начало endpoint'а ```python if z < 0 or z > 22: raise HTTPException(400, "Invalid z") max_coord = 2 ** z if x < 0 or x >= max_coord or y < 0 or y >= max_coord: raise HTTPException(400, "Invalid x/y for zoom level") ``` --- ### 3. Исправить `prototype/static/index.html` #### 3.1 Починить toggleLayer — доступ к map ```js function toggleLayer(group) { layerState[group] = !layerState[group]; const btn = document.getElementById('btn-' + group); btn.classList.toggle('active', layerState[group]); const visibility = layerState[group] ? 'visible' : 'none'; layerGroups[group].forEach(id => { if (window._map && window._map.getLayer(id)) { window._map.setLayoutProperty(id, 'visibility', visibility); } }); } ``` #### 3.2 Исправить layerGroups — привести к реальным ids из style.json ```js const layerGroups = { tracks: ['trails-track', 'trails-asphalt'], paths: ['trails-path-bridleway'], poi: ['poi-circles', 'poi-labels'], basemap: ['osm-base'], }; ``` #### 3.3 Вынести JS и CSS в отдельные файлы - Создать `static/app.js` — весь JS из ` ``` --- ### 4. Исправить `prototype/requirements.txt` ``` fastapi==0.111.0 uvicorn==0.29.0 shapely==2.0.4 mapbox-vector-tile==2.2.0 ``` Создать `scripts/requirements-parse.txt`: ``` python-osmium==3.7.0 pyproj==3.6.1 shapely==2.0.4 ``` --- ### 5. Создать `prototype/Dockerfile` ```dockerfile FROM python:3.11-slim RUN apt-get update -qq && \ apt-get install -y -qq libsqlite3-mod-spatialite && \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . ENV PORT=5558 EXPOSE 5558 CMD ["python", "app.py"] ``` Обновить `docker-compose.yml` сервис `enduro-trails`: ```yaml enduro-trails: build: context: ./prototype dockerfile: Dockerfile volumes: - ./data:/data ports: - "5558:5558" environment: - DATA_PATH=/data/centralfederal.sqlite restart: unless-stopped ``` --- ### 6. Добавить `scripts/smoke_check.py` ```python #!/usr/bin/env python3 """Smoke checks для Enduro Trails. Запуск: python scripts/smoke_check.py""" import sys, sqlite3, os, json, urllib.request, urllib.error DB_PATH = os.environ.get("DATA_PATH", "../data/centralfederal.sqlite") API_BASE = os.environ.get("API_BASE", "http://localhost:5558") errors = [] print("==> Схема БД...") try: conn = sqlite3.connect(DB_PATH) cur = conn.cursor() cur.execute("SELECT name FROM sqlite_master WHERE type='table'") tables = {r[0] for r in cur.fetchall()} for t in ("trails", "poi"): if t not in tables: errors.append(f"Таблица {t} не найдена") cur.execute("PRAGMA table_info(trails)") trail_cols = {r[1] for r in cur.fetchall()} for col in ("min_lon","max_lon","min_lat","max_lat","length_m","mtb_scale"): if col not in trail_cols: errors.append(f"trails.{col} не найдена") cur.execute("PRAGMA table_info(poi)") poi_cols = {r[1] for r in cur.fetchall()} for col in ("lon","lat"): if col not in poi_cols: errors.append(f"poi.{col} не найдена") cur.execute("SELECT name FROM sqlite_master WHERE type='index'") indexes = {r[0] for r in cur.fetchall()} for idx in ("idx_trails_bbox","idx_poi_coords"): if idx not in indexes: errors.append(f"Индекс {idx} не найден") cur.execute("SELECT COUNT(*) FROM trails"); n = cur.fetchone()[0] print(f" trails: {n:,}") if n == 0: errors.append("trails пуста") cur.execute("SELECT COUNT(*) FROM poi"); n = cur.fetchone()[0] print(f" poi: {n:,}") cur.execute("SELECT COUNT(*) FROM trails WHERE min_lon IS NULL") null_bbox = cur.fetchone()[0] if null_bbox > 0: errors.append(f"{null_bbox} trails с NULL bbox") conn.close() except Exception as e: errors.append(f"Ошибка БД: {e}") print("==> API...") try: with urllib.request.urlopen(f"{API_BASE}/api/health", timeout=5) as r: data = json.loads(r.read()) if not data.get("db_exists"): errors.append("db_exists=false") else: print(" health: OK") tile_url = f"{API_BASE}/api/tiles/10/619/320.mvt" with urllib.request.urlopen(tile_url, timeout=10) as r: td = r.read() if len(td) == 0: errors.append("Тайл z10/619/320 пустой") else: print(f" tile z10/619/320: {len(td)} bytes OK") try: urllib.request.urlopen(f"{API_BASE}/api/tiles/99/0/0.mvt", timeout=5) errors.append("z=99 должен вернуть 400") except urllib.error.HTTPError as e: if e.code == 400: print(" validation z=99: 400 OK") else: errors.append(f"z=99 вернул {e.code}") except Exception as e: errors.append(f"Ошибка API: {e}") print() if errors: print(f"FAILED ({len(errors)} ошибок):") for e in errors: print(f" - {e}") sys.exit(1) else: print("OK — все проверки прошли") ``` --- ### 7. Деплой на сервер После внесения изменений в workspace — задеплоить на `slin@82.22.50.71` (пароль: `motoZ@yaz2010`): ```bash docker cp /home/node/.openclaw/workspace/tasks/enduro-trails/prototype/app.py enduro-trails:/app/app.py docker cp /home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/index.html enduro-trails:/app/static/index.html docker cp /home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/app.js enduro-trails:/app/static/app.js docker cp /home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/app.css enduro-trails:/app/static/app.css docker restart enduro-trails sleep 3 && curl -s http://localhost:5558/api/health ``` **Важно:** `parse.py` не запускать — БД уже есть и рабочая (1.1М треков). Изменения в `parse.py` нужны только для будущего пересоздания БД. --- ### 8. Обновить документацию - Создать `prototype/README.md` — как запустить, как проверить - Обновить `PROJECT.md` — текущий статус - Обновить `TASKS/active/prototype-setup/TASK.md` — отметить выполненные пункты --- ## Definition of Done - [ ] `parse.py` создаёт схему с bbox-колонками и индексом `idx_trails_bbox` - [ ] `parse.py` создаёт `poi` с `lon`/`lat` и индексом `idx_poi_coords` - [ ] `app.py` передаёт `length_m` и `mtb_scale` в tile props - [ ] `app.py` использует SQL bbox-фильтр для POI - [ ] `app.py` валидирует z/x/y, возвращает 400 при невалидных значениях - [ ] `app.py` использует параметризованный LIMIT - [ ] `toggleLayer()` работает через `window._map` - [ ] `layerGroups` содержит реальные ids: `trails-track`, `trails-asphalt`, `trails-path-bridleway` - [ ] JS вынесен в `static/app.js`, CSS в `static/app.css` - [ ] `requirements.txt` содержит `mapbox-vector-tile==2.2.0`, без parse-зависимостей - [ ] Создан `prototype/Dockerfile` - [ ] Создан `scripts/smoke_check.py` - [ ] Изменения задеплоены на сервер `82.22.50.71` - [ ] `smoke_check.py` проходит без ошибок на живом сервере - [ ] Документация обновлена ## Что НЕ делать - Не добавлять роутинг, новые слои, новые фичи - Не менять визуальный стиль карты (цвета, толщины линий) - Не трогать tile math (`tile_to_bbox`, `quantize_bounds`, `y_coord_down`) — это работает - Не пересоздавать БД - Не менять порт (5558)