Files
wiki/tasks/enduro-trails/DEV_TASK.md
2026-05-02 17:10:02 +03:00

14 KiB
Raw Blame History

Dev Task: Стабилизация прототипа Enduro Trails v0.1

Приоритет: 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:

min_lon REAL,
max_lon REAL,
min_lat REAL,
max_lat REAL

При вставке каждого трека вычислять bbox из координат:

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(...) эти четыре значения.

Добавить индекс:

CREATE INDEX IF NOT EXISTS idx_trails_bbox ON trails(min_lon, max_lon, min_lat, max_lat);

Для таблицы poi — добавить колонки для SQL-фильтрации:

lon REAL,
lat REAL

При вставке POI заполнять lon/lat из координат точки (coords[0], coords[1]).

Добавить индекс:

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:

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-фильтрации

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

# было:
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'а

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

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

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 из <script> блока
  • Создать static/app.css — все стили из <style> блока
  • В index.html оставить только HTML-разметку + подключение:
    <link rel="stylesheet" href="/app.css">
    ...
    <script src="/app.js" defer></script>
    

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

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:

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

#!/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):

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)