14 KiB
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 — рабочий, но есть проблемы:
build_mvt()не передаётlength_mиmtb_scaleв props trails- POI выбирается без bbox-фильтра в SQL (
SELECT ... FROM poi LIMIT 2000), фильтрация в Python - SQL для trails использует f-string для LIMIT:
LIMIT {limit}— нужно параметризовать - Нет валидации z/x/y параметров
prototype/static/index.html — есть баги:
toggleLayer()использует глобальную переменнуюmap, но она объявлена какconst mapвнутриinitMap()— вне функции недоступна. Нужно использоватьwindow._map(оно уже присваивается:window._map = map)layerGroupsссылается на несуществующие слои:'trails-grade12'— нет в style.json'trails-grade345'— нет в style.json- Реальные слои в style.json:
'trails-track','trails-asphalt','trails-path-bridleway'
- Весь 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_bboxparse.pyсоздаётpoiсlon/latи индексомidx_poi_coordsapp.pyпередаётlength_mиmtb_scaleв tile propsapp.pyиспользует SQL bbox-фильтр для POIapp.pyвалидирует z/x/y, возвращает 400 при невалидных значенияхapp.pyиспользует параметризованный LIMITtoggleLayer()работает черезwindow._maplayerGroupsсодержит реальные 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)