Files
wiki/tasks/enduro-trails/DEV_TASK.md
2026-05-04 09:40:01 +03:00

374 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 из `<script>` блока
- Создать `static/app.css` — все стили из `<style>` блока
- В `index.html` оставить только 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`
```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)