diff --git a/tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md b/tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md
new file mode 100644
index 0000000..1214b7c
--- /dev/null
+++ b/tasks/enduro-trails/DEV_TASK_PHASE5_UX1.md
@@ -0,0 +1,166 @@
+# Dev Task: Enduro Trails — UX фиксы #1, #3, #5
+
+**Файлы:** `/home/node/.openclaw/workspace/tasks/enduro-trails/prototype/static/`
+**Деплой:** `node /tmp/deploy_static.js`
+**Бэкенд не трогать.**
+
+---
+
+## Фикс 1: Кнопка «+» на мини-баре
+
+Сейчас чтобы добавить промежуточную точку нужно открыть полную панель.
+Нужно: добавить кнопку «+» прямо на мини-баре.
+
+### HTML — в `#sheet-route-mini` добавить кнопку рядом со стрелками:
+```html
+
+```
+
+Вставить в `.mini-route-info` после `.mini-route-arrows`.
+
+### CSS:
+```css
+.mini-add-btn {
+ width: 32px; height: 32px;
+ display: flex; align-items: center; justify-content: center;
+ background: var(--accent); border: none;
+ border-radius: 10px; color: #fff;
+ cursor: pointer; flex-shrink: 0;
+ -webkit-tap-highlight-color: transparent;
+}
+.mini-add-btn:active { opacity: 0.8; transform: scale(0.94); }
+```
+
+### JS — новая функция:
+```js
+function miniAddWaypoint() {
+ // Enter waypoint-adding mode without opening full sheet
+ if (!routeMode) {
+ // Re-enter route mode silently (no sheet open)
+ routeMode = true;
+ document.getElementById('tb-route').classList.add('active');
+ updateMapModeClass();
+ }
+ addWaypointMode();
+ // Show hint on mini-bar
+ document.getElementById('mini-stats').textContent = 'Тапни на карте для добавления точки';
+}
+```
+
+После добавления точки (в обработчике клика карты где `addingWaypoint = false`) — восстановить нормальный текст мини-бара:
+```js
+// После rebuildWaypointMarkers(); renderWaypointsList(); при addingWaypoint
+updateMiniRouteCard(); // обновить мини-бар
+```
+
+---
+
+## Фикс 3: После добавления точки — свернуть панель в мини-бар
+
+Сейчас после добавления точки полная панель остаётся открытой.
+Нужно: после успешного построения маршрута (когда routeResults обновились) — если панель открыта, свернуть её в мини-бар.
+
+### В функции `drawRouteResults()` — в конце добавить:
+```js
+// Auto-minimize sheet after route is built
+const sheet = document.getElementById('sheet-route');
+if (sheet && sheet.classList.contains('open')) {
+ minimizeSheet('sheet-route');
+}
+```
+
+**Важно:** это должно срабатывать только когда маршрут уже был (перестройка), а не при первом построении. Проверить: если `routeResults.length > 0` до вызова — значит это перестройка, сворачивать. Если первый раз — не сворачивать (пусть пользователь видит варианты).
+
+Логика:
+```js
+const wasBuilt = routeResults.length > 0; // до обновления routeResults
+// ... существующий код drawRouteResults ...
+// В конце:
+if (wasBuilt) {
+ const sheet = document.getElementById('sheet-route');
+ if (sheet && sheet.classList.contains('open')) {
+ minimizeSheet('sheet-route');
+ }
+}
+```
+
+---
+
+## Фикс 5: Reverse geocoding — названия мест вместо координат
+
+Сейчас в списке точек показываются координаты: `55.7234, 37.6123`.
+Нужно: показывать название места через Nominatim reverse geocoding.
+
+### Добавить функцию reverse geocoding:
+```js
+const geocodeCache = {};
+
+async function reverseGeocode(lat, lon) {
+ const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
+ if (geocodeCache[key]) return geocodeCache[key];
+ try {
+ const resp = await fetch(
+ `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=ru`,
+ { headers: { 'Accept-Language': 'ru' } }
+ );
+ const data = await resp.json();
+ // Приоритет: village/town/city > road > county
+ const a = data.address || {};
+ const name = a.village || a.town || a.city || a.suburb || a.road || a.county || a.state || `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
+ geocodeCache[key] = name;
+ return name;
+ } catch(e) {
+ return `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
+ }
+}
+```
+
+### Изменить `renderWaypointsList()`:
+
+Сделать функцию async и использовать geocoding:
+```js
+async function renderWaypointsList() {
+ const list = document.getElementById('waypoints-list');
+ if (!routeWaypoints.length) { list.innerHTML = ''; return; }
+
+ // Render immediately with coords, then update with names
+ list.innerHTML = routeWaypoints.map((wp, i) => {
+ const labelClass = i === 0 ? 'start' : i === routeWaypoints.length - 1 ? 'end' : 'mid';
+ const color = labelClass === 'start' ? 'var(--success)' : labelClass === 'end' ? 'var(--red)' : '#0066ff';
+ const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`;
+ return `
`;
+ }).join('');
+
+ // Async update with place names
+ routeWaypoints.forEach(async (wp, i) => {
+ const name = await reverseGeocode(wp.lat, wp.lon);
+ const el = document.getElementById(`wl-label-${i}`);
+ if (el) el.textContent = name;
+ });
+}
+```
+
+### Также обновить мини-бар stats при первом построении маршрута:
+В `updateMiniRouteCard()` — stats показывает км и % грунт, это ок. Не трогать.
+
+---
+
+## Порядок реализации
+1. Фикс 5 (geocoding) — добавить функцию, изменить renderWaypointsList
+2. Фикс 3 (auto-minimize) — изменить drawRouteResults
+3. Фикс 1 (кнопка + на мини-баре) — HTML + CSS + JS
+4. Деплой + проверка
+
+## Проверка
+1. Построить маршрут A→B → панель показывает варианты (не сворачивается при первом построении)
+2. Добавить промежуточную точку через кнопку «+» в мини-баре → маршрут перестраивается → панель сворачивается
+3. Открыть полную панель → в списке точек видны названия мест (не координаты)
+4. Добавить точку через кнопку «+ Точка» в полной панели → маршрут перестраивается → панель сворачивается
diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js
index af2f6a2..fe3236b 100644
--- a/tasks/enduro-trails/prototype/static/app.js
+++ b/tasks/enduro-trails/prototype/static/app.js
@@ -497,22 +497,51 @@ function rebuildWaypointMarkers() {
});
}
-function renderWaypointsList() {
+// ─── Reverse Geocoding ───────────────────────────────────────────
+const geocodeCache = {};
+
+async function reverseGeocode(lat, lon) {
+ const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
+ if (geocodeCache[key]) return geocodeCache[key];
+ try {
+ const resp = await fetch(
+ `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=ru`,
+ { headers: { 'Accept-Language': 'ru' } }
+ );
+ const data = await resp.json();
+ const a = data.address || {};
+ const name = a.village || a.town || a.city || a.suburb || a.road || a.county || a.state || `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
+ geocodeCache[key] = name;
+ return name;
+ } catch(e) {
+ return `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
+ }
+}
+
+async function renderWaypointsList() {
const list = document.getElementById('waypoints-list');
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
+
+ // Render immediately with coords, then update with place names
list.innerHTML = routeWaypoints.map((wp, i) => {
- let labelClass, labelText;
- if (i === 0) { labelClass = 'start'; labelText = 'A'; }
- else if (i === routeWaypoints.length - 1) { labelClass = 'end'; labelText = 'B'; }
- else { labelClass = 'mid'; labelText = String(i); }
- return `
-
-
${wp.lat.toFixed(4)}, ${wp.lon.toFixed(4)}
+ const labelClass = i === 0 ? 'start' : i === routeWaypoints.length - 1 ? 'end' : 'mid';
+ const color = labelClass === 'start' ? 'var(--success)' : labelClass === 'end' ? 'var(--red)' : '#0066ff';
+ const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`;
+ return `
`;
}).join('');
+
+ // Async update labels with place names
+ routeWaypoints.forEach(async (wp, i) => {
+ const name = await reverseGeocode(wp.lat, wp.lon);
+ const el = document.getElementById(`wl-label-${i}`);
+ if (el) el.textContent = name;
+ });
}
function removeWaypoint(idx) {
@@ -576,6 +605,7 @@ async function buildRoute() {
function drawRouteResults(routes, activeIdx) {
const map = window._map;
activeRouteIdx = activeIdx;
+ const wasBuilt = routeResults.length > 0; // track rebuild vs first build
routeResults = routes;
// Clear old layers
@@ -630,6 +660,14 @@ function drawRouteResults(routes, activeIdx) {
// Update mini sheet if visible
const miniEl = document.getElementById('sheet-route-mini');
if (miniEl && miniEl.classList.contains('visible')) showMiniRouteSheet();
+
+ // Auto-minimize sheet on rebuild (not on first build)
+ if (wasBuilt) {
+ const sheet = document.getElementById('sheet-route');
+ if (sheet && sheet.classList.contains('open')) {
+ minimizeSheet('sheet-route');
+ }
+ }
}
function selectRoute(idx) {
@@ -1034,6 +1072,7 @@ function initRouteClicks(map) {
}
rebuildWaypointMarkers(); renderWaypointsList();
if (routeWaypoints.length >= 2) debounceBuildRoute();
+ updateMiniRouteCard();
return;
}
@@ -1618,6 +1657,19 @@ document.addEventListener('DOMContentLoaded', () => {
// ─── Mini Route Bar ──────────────────────────────────────────────────
+function miniAddWaypoint() {
+ // Enter waypoint-adding mode without opening full sheet
+ if (!routeMode) {
+ routeMode = true;
+ document.getElementById('tb-route').classList.add('active');
+ updateMapModeClass();
+ }
+ addWaypointMode();
+ // Show hint on mini-bar
+ const statsEl = document.getElementById('mini-stats');
+ if (statsEl) statsEl.textContent = 'Тапни на карте для добавления точки';
+}
+
function showMiniRouteSheet() {
if (!routeResults || routeResults.length === 0) return;
updateMiniRouteCard();
diff --git a/tasks/enduro-trails/prototype/static/index.html b/tasks/enduro-trails/prototype/static/index.html
index f8c6502..4424d93 100644
--- a/tasks/enduro-trails/prototype/static/index.html
+++ b/tasks/enduro-trails/prototype/static/index.html
@@ -242,6 +242,9 @@
‹
›
+