374 lines
14 KiB
Markdown
374 lines
14 KiB
Markdown
# 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)
|