From ca66fc418aa24ec7f59c604ea21a83e2468ac01c Mon Sep 17 00:00:00 2001 From: Stream Date: Tue, 5 May 2026 23:20:01 +0300 Subject: [PATCH] auto-sync: 2026-05-05 23:20:01 --- memory/.dreams/events.jsonl | 1 + memory/.dreams/short-term-recall.json | 64 ++++++++- tasks/enduro-trails/prototype/static/app.js | 142 +++++++++++++------- 3 files changed, 161 insertions(+), 46 deletions(-) diff --git a/memory/.dreams/events.jsonl b/memory/.dreams/events.jsonl index 8547efc..48f85a3 100644 --- a/memory/.dreams/events.jsonl +++ b/memory/.dreams/events.jsonl @@ -13,3 +13,4 @@ {"type":"memory.recall.recorded","timestamp":"2026-04-26T10:54:35.677Z","query":"mva154 хост сервер характеристики доступ SSH","resultCount":1,"results":[{"path":"memory/2026-04-11.md","startLine":62,"endLine":71,"score":0.3638544976711273}]} {"type":"memory.recall.recorded","timestamp":"2026-05-04T06:57:05.721Z","query":"enduro trails backlog задачи","resultCount":3,"results":[{"path":"memory/2026-05-02.md","startLine":107,"endLine":142,"score":0.375397714972496},{"path":"memory/2026-05-02.md","startLine":86,"endLine":117,"score":0.3636188447475433},{"path":"memory/2026-05-03.md","startLine":1,"endLine":36,"score":0.3552299261093139}]} {"type":"memory.recall.recorded","timestamp":"2026-05-05T05:03:32.115Z","query":"DEV_TASK_PHASE5 dev agent enduro trails фаза 5","resultCount":1,"results":[{"path":"memory/2026-05-04.md","startLine":1,"endLine":30,"score":0.40014922022819516}]} +{"type":"memory.recall.recorded","timestamp":"2026-05-05T20:11:55.846Z","query":"TTS синтез речи модель голос ElevenLabs Yandex SpeechKit","resultCount":2,"results":[{"path":"memory/2026-03-23.md","startLine":64,"endLine":80,"score":0.40174805521965024},{"path":"memory/2026-03-23.md","startLine":48,"endLine":66,"score":0.3850157171487808}]} diff --git a/memory/.dreams/short-term-recall.json b/memory/.dreams/short-term-recall.json index 7509054..0934434 100644 --- a/memory/.dreams/short-term-recall.json +++ b/memory/.dreams/short-term-recall.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-05-05T05:03:32.115Z", + "updatedAt": "2026-05-05T20:11:55.846Z", "entries": { "memory:memory/2026-04-05.md:29:55": { "key": "memory:memory/2026-04-05.md:29:55", @@ -748,6 +748,68 @@ "ui-тестирования", "playwright/puppeteer" ] + }, + "memory:memory/2026-03-23.md:64:80": { + "key": "memory:memory/2026-03-23.md:64:80", + "path": "memory/2026-03-23.md", + "startLine": 64, + "endLine": 80, + "source": "memory", + "snippet": "- **Claude Sonnet:** создаёт текст с оптимальной пунктуацией, паузами, плавными формулировками, что лучше для TTS - **DeepSeek v3.2:** генерирует более компактный текст, менее выраженная структура предложений, что может приводить к искажениям при озвучивании - **Ключевой вывод:** Качество голосовых сообщений зависит не от самого синтеза речи, а от текста, который подаётся в TTS-движок - **Рекомендация:** Для задач, где важна качественная озвучка, использовать модели с более структурированным выводом (Claude Sonnet) ### Создание специализированного агента для оптимизации текста под TTS - 23 марта 17:09 - Слава предложил создать первого специализированного агента для формулирования текста, ид", + "recallCount": 1, + "dailyCount": 0, + "groundedCount": 0, + "totalScore": 0.40174805521965024, + "maxScore": 0.40174805521965024, + "firstRecalledAt": "2026-05-05T20:11:55.846Z", + "lastRecalledAt": "2026-05-05T20:11:55.846Z", + "queryHashes": [ + "4c737bff00f4" + ], + "recallDays": [ + "2026-05-05" + ], + "conceptTags": [ + "v3.2", + "tts-движок", + "claude", + "sonnet", + "создаёт", + "текст", + "оптимальной", + "пунктуацией" + ] + }, + "memory:memory/2026-03-23.md:48:66": { + "key": "memory:memory/2026-03-23.md:48:66", + "path": "memory/2026-03-23.md", + "startLine": 48, + "endLine": 66, + "source": "memory", + "snippet": "### Расчёт экономии от использования специализированных агентов - 23 марта 16:59 - Слава попросил рассчитать экономию от использования группы специализированных агентов вместо одного универсального - Проведён расчёт на основе цен OpenRouter API: - Claude Sonnet 4.6: $3/$15 за 1M токенов (input/output) - Claude Haiku: $1/$5 за 1M токенов - Llama 4 Maverick: $0.15/$0.60 за 1M токенов - Gemini 2.0 Flash: $0.10/$0.40 за 1M токенов - Оценка ежедневного потребления: 105K input, 67K output токенов - Стоимость одной модели Sonnet: ~$1.32 в день - Стоимость команды специалистов: ~$0.69 в день - Экономия: 48% ($0.63 в день, $19 в месяц, $230 в год) - Дополнительные преимущества: повышение каче", + "recallCount": 1, + "dailyCount": 0, + "groundedCount": 0, + "totalScore": 0.3850157171487808, + "maxScore": 0.3850157171487808, + "firstRecalledAt": "2026-05-05T20:11:55.846Z", + "lastRecalledAt": "2026-05-05T20:11:55.846Z", + "queryHashes": [ + "4c737bff00f4" + ], + "recallDays": [ + "2026-05-05" + ], + "conceptTags": [ + "router", + "4.6", + "input/output", + "0.15", + "0.60", + "2.0", + "0.10", + "0.40" + ] } } } diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js index 0395439..f0b6094 100644 --- a/tasks/enduro-trails/prototype/static/app.js +++ b/tasks/enduro-trails/prototype/static/app.js @@ -1,5 +1,5 @@ // ═══════════════════════════════════════════════════════════════════ -// Enduro Trails — Phase 5 Redesign +// Enduro Trails - Phase 5 Redesign // Theme system (auto/light/dark + SunCalc), skeleton, swipe, animations // ═══════════════════════════════════════════════════════════════════ @@ -54,7 +54,7 @@ function toggleTheme() { if (themeMode === 'auto') themeMode = 'light'; else if (themeMode === 'light') themeMode = 'dark'; else themeMode = 'auto'; - + localStorage.setItem('enduro-theme-mode', themeMode); applyTheme(); } @@ -64,9 +64,9 @@ function updateThemeButtonIcon() { const moonIcon = document.getElementById('theme-icon-moon'); const label = document.getElementById('theme-label'); if (!sunIcon || !moonIcon) return; - + const dark = isDarkTheme(); - + if (themeMode === 'auto') { // Dynamic icon based on actual theme sunIcon.style.display = dark ? 'none' : 'block'; @@ -89,8 +89,8 @@ function switchMapStyle() { const dark = isDarkTheme(); const basePath = window.location.pathname.replace(/\/[^/]*$/, '') || ''; const styleUrl = dark ? basePath + '/style.json' : basePath + '/style-light.json'; - - // Check if style-light.json exists — if not, keep current + + // Check if style-light.json exists - if not, keep current fetch(styleUrl, { method: 'HEAD' }).then(r => { if (r.ok) { map.setStyle(styleUrl); @@ -170,7 +170,7 @@ function formatDuration(seconds) { } function formatDist(m) { - if (!m) return '—'; + if (!m) return '-'; if (m >= 1000) return (m / 1000).toFixed(1) + ' км'; return Math.round(m) + ' м'; } @@ -223,7 +223,7 @@ function initSheetSwipe() { document.querySelectorAll('.bottom-sheet').forEach(sheet => { let startY = 0; let isDragging = false; - + sheet.addEventListener('touchstart', (e) => { const rect = sheet.getBoundingClientRect(); const touchY = e.touches[0].clientY; @@ -234,7 +234,7 @@ function initSheetSwipe() { sheet.classList.add('swiping'); } }, { passive: true }); - + sheet.addEventListener('touchmove', (e) => { if (!isDragging) return; const dy = e.touches[0].clientY - startY; @@ -242,7 +242,7 @@ function initSheetSwipe() { sheet.style.transform = `translateY(${dy}px)`; } }, { passive: true }); - + sheet.addEventListener('touchend', (e) => { if (!isDragging) return; isDragging = false; @@ -289,7 +289,7 @@ function showSkeleton(containerId, count) { function deactivateAllModes() { // Deactivate all input modes but preserve route/scenic/link data on map - if (routeMode) { routeMode = false; document.getElementById('tb-route').classList.remove('active'); closeSheet('sheet-route'); /* NOT clearRoute — keep line on map */ } + if (routeMode) { routeMode = false; document.getElementById('tb-route').classList.remove('active'); closeSheet('sheet-route'); /* NOT clearRoute - keep line on map */ } if (rulerMode) toggleRuler(); if (markerMode) toggleMarkerMode(); if (typeof reconMode !== 'undefined' && reconMode) toggleReconMode(); @@ -380,7 +380,7 @@ function toggleLayer(group) { }); } -// ─── Роутинг — состояние ─────────────────────────────────────────── +// ─── Роутинг - состояние ─────────────────────────────────────────── const ROUTE_COLORS = ['#0066ff', '#00aa44', '#9933cc', '#ff8800', '#888888']; let routeMode = false; let routeWaypoints = []; @@ -399,25 +399,25 @@ function toggleRouteMode() { const btn = document.getElementById('tb-route'); if (routeMode) { - // If sheet is open — close sheet but stay in mode + // If sheet is open - close sheet but stay in mode const sheet = document.getElementById('sheet-route'); if (sheet && sheet.classList.contains('open')) { closeSheet('sheet-route'); return; } - // Sheet is closed — exit mode and clear route + // Sheet is closed - exit mode and clear route routeMode = false; btn.classList.remove('active'); clearRoute(); window._map.getCanvas().style.cursor = ''; } else { - // Enter route mode — do NOT open sheet + // Enter route mode - do NOT open sheet deactivateAllModes(); routeMode = true; btn.classList.add('active'); clearRoute(); window._map.getCanvas().style.cursor = 'crosshair'; - // sheet is NOT opened — user taps mini-bar to open it + // sheet is NOT opened - user taps mini-bar to open it } updateMapModeClass(); } @@ -543,8 +543,55 @@ function haversineM(a, b) { } function formatSegmentDist(m) { - if (m < 1000) return Math.round(m) + ' м'; - return (m / 1000).toFixed(1).replace('.', ',') + ' км'; + if (m < 1000) return Math.round(m) + ' м'; + return (m / 1000).toFixed(1).replace('.', ',') + ' км'; +} + +// Returns array of route-distance segments (meters) for each waypoint. +// segDists[0] = 0, segDists[i] = distance along route geometry from wp[i-1] to wp[i]. +// Falls back to haversine if route geometry is unavailable or snap fails. +function getRouteSegmentDistances() { + const route = routeResults[activeRouteIdx]; + if (!route || !route.geometry || !route.geometry.coordinates) return null; + + const coords = route.geometry.coordinates; // [[lon, lat], ...] + const n = coords.length; + if (n < 2 || routeWaypoints.length < 2) return null; + + // Convert geometry coords to {lat, lon} for haversineM + const geoPts = coords.map(([lon, lat]) => ({ lat, lon })); + + // Snap each waypoint to the nearest geometry point index + const snapIdx = routeWaypoints.map(wp => { + let bestIdx = 0; + let bestDist = Infinity; + for (let j = 0; j < n; j++) { + const d = haversineM(wp, geoPts[j]); + if (d < bestDist) { bestDist = d; bestIdx = j; } + } + return bestIdx; + }); + + // For each segment i→i+1, sum haversine along geometry from snapIdx[i] to snapIdx[i+1] + const segDists = [0]; + for (let i = 1; i < routeWaypoints.length; i++) { + const from = snapIdx[i - 1]; + const to = snapIdx[i]; + if (from === to) { + // Same snap point — fallback to straight-line + segDists.push(haversineM(routeWaypoints[i - 1], routeWaypoints[i])); + continue; + } + // Walk geometry in the correct direction + const step = to > from ? 1 : -1; + let dist = 0; + for (let j = from; j !== to; j += step) { + dist += haversineM(geoPts[j], geoPts[j + step]); + } + segDists.push(dist); + } + + return segDists; } async function renderWaypointsList() { @@ -553,13 +600,18 @@ async function renderWaypointsList() { const gripSvg = ``; + const segDists = (routeResults.length > 0 && activeRouteIdx >= 0) + ? getRouteSegmentDistances() + : null; + let html = routeWaypoints.map((wp, i) => { const isStart = i === 0; const isEnd = i === routeWaypoints.length - 1; const label = isStart ? 'S' : isEnd ? 'F' : String(i); const color = isStart ? '#2EA043' : isEnd ? '#FF3B1F' : '#0066ff'; const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`; - const distStr = i > 0 ? formatSegmentDist(haversineM(routeWaypoints[i-1], wp)) : ''; + const distStr = i > 0 && segDists ? formatSegmentDist(segDists[i]) : + i > 0 ? formatSegmentDist(haversineM(routeWaypoints[i-1], wp)) : ''; return `
${waypointPinSvg(label, color)}
@@ -769,7 +821,7 @@ async function buildRoute() { drawRouteResults(routeResults, 0); document.getElementById('route-status').textContent = `${routeResults.length} маршрут(ов)`; - // Show mini-bar with result — do NOT open main sheet + // Show mini-bar with result - do NOT open main sheet hideMiniRouteLoading(); showMiniRouteSheet(); } catch(e) { @@ -786,14 +838,14 @@ function drawRouteResults(routes, activeIdx) { activeRouteIdx = activeIdx; const wasBuilt = routeResults.length > 0; // track rebuild vs first build routeResults = routes; - + // Clear old layers for (let i = 0; i < 5; i++) { try { if (map.getLayer('route-line-' + i)) map.removeLayer('route-line-' + i); } catch(e) {} try { if (map.getLayer('route-line-' + i + '-outline')) map.removeLayer('route-line-' + i + '-outline'); } catch(e) {} try { if (map.getSource('route-' + i)) map.removeSource('route-' + i); } catch(e) {} } - + routes.forEach((route, i) => { const color = ROUTE_COLORS[i] || '#888888'; const isActive = i === activeIdx; @@ -821,7 +873,7 @@ function drawRouteResults(routes, activeIdx) { }, layout: { 'line-cap': 'round', 'line-join': 'round' } }); - + map.on('click', 'route-line-' + i, (e) => { if (e.stopPropagation) e.stopPropagation(); selectRoute(i); @@ -833,7 +885,7 @@ function drawRouteResults(routes, activeIdx) { map.getCanvas().style.cursor = routeMode ? 'crosshair' : ''; }); }); - + renderRouteCards(routes); // Update mini sheet if visible @@ -988,7 +1040,7 @@ function toggleMarkerMode() { function addMarker(lngLat) { const markers = loadMarkers(); if (markers.length >= 50) { alert('Достигнут лимит 50 меток'); return; } - + const grid = document.getElementById('marker-type-grid'); // Show marker dialog openMarkerDialog(lngLat); @@ -997,7 +1049,7 @@ function addMarker(lngLat) { function openMarkerDialog(lngLat) { const dialog = document.getElementById('marker-dialog'); const grid = document.getElementById('marker-type-grid'); - grid.innerHTML = MARKER_ICONS.map((ic, i) => + grid.innerHTML = MARKER_ICONS.map((ic, i) => `
`); - + const mlMarker = new maplibregl.Marker({ element: el, anchor: 'bottom' }) .setLngLat([markerData.lon, markerData.lat]) .setPopup(popup) .addTo(map); - + namedMarkerObjects[markerData.id] = mlMarker; } @@ -1138,7 +1190,7 @@ async function initMap() { }); function formatLength(m) { - if (!m) return '—'; + if (!m) return '-'; if (m >= 1000) return (m / 1000).toFixed(1) + ' км'; return Math.round(m) + ' м'; } @@ -1161,9 +1213,9 @@ async function initMap() { const props = e.features[0].properties; const html = ` - - - + + + ${props.mtb_scale ? `` : ''} `; @@ -1421,7 +1473,7 @@ function toggleReconMode() { btn.classList.remove('active'); closeSheet('sheet-recon'); window._map.getCanvas().style.cursor = ''; - clearRecon(); // recon data is transient — safe to clear + clearRecon(); // recon data is transient - safe to clear } else { deactivateAllModes(); reconMode = true; @@ -1446,7 +1498,7 @@ function makeCircleGeoJSON(lon, lat, radiusKm) { async function doRecon(lon, lat) { reconCenter = [lon, lat]; const map = window._map; - + const circle = makeCircleGeoJSON(lon, lat, reconRadius); if (map.getSource('recon-circle')) { map.getSource('recon-circle').setData(circle); @@ -1465,7 +1517,7 @@ async function doRecon(lon, lat) { const basePath = getBasePath(); const resultsDiv = document.getElementById('recon-results'); resultsDiv.style.display = 'block'; - + try { const resp = await fetch(`${basePath}/api/recon`, { method: 'POST', @@ -1475,12 +1527,12 @@ async function doRecon(lon, lat) { const data = await resp.json(); const t = data.trails || {}; const p = data.poi || {}; - + document.getElementById('r-total-km').textContent = t.total_km || 0; document.getElementById('r-lev12-km').textContent = t.lev12_km || 0; document.getElementById('r-lev345-km').textContent = t.lev345_km || 0; document.getElementById('r-path-km').textContent = t.path_km || 0; - + const poiList = document.getElementById('r-poi-list'); const poiTypes = [ { key: 'natural=water', icon: '💧', label: 'Озёра' }, @@ -1488,15 +1540,15 @@ async function doRecon(lon, lat) { { key: 'ford=yes', icon: '🌊', label: 'Броды' }, { key: 'historic=ruins', icon: '🏚', label: 'Руины' }, ]; - poiList.innerHTML = poiTypes.map(pt => + poiList.innerHTML = poiTypes.map(pt => `
${pt.icon} ${pt.label} ${p[pt.key] || 0}
` ).join(''); - + } catch(e) { - document.getElementById('r-total-km').textContent = '—'; + document.getElementById('r-total-km').textContent = '-'; } } @@ -1742,13 +1794,13 @@ function drawScenicRoutes(routes, activeIdx) { const map = window._map; scenicRoutes = routes; activeScenicIdx = activeIdx; - + // Clear old for (let i = 0; i < 10; i++) { try { if (map.getLayer(`scenic-line-${i}`)) map.removeLayer(`scenic-line-${i}`); } catch(e) {} try { if (map.getSource(`scenic-src-${i}`)) map.removeSource(`scenic-src-${i}`); } catch(e) {} } - + const colors = ['#0066ff', '#00aa44', '#9933cc']; routes.forEach((r, i) => { const geojson = { type: 'Feature', geometry: r.geometry, properties: {} }; @@ -1776,7 +1828,7 @@ function drawScenicRoutes(routes, activeIdx) { const pois = (r.scenic_pois || []).map(p => { const SCENIC_LABELS = {'natural=water':'💧 Озёро','tourism=viewpoint':'👁 Смотровая','historic=ruins':'🏚 Руины','natural=peak':'🔺 Вершина','natural=cave_entrance':'🕳 Пещера','ford=yes':'🌊 Брод'}; const label = SCENIC_LABELS[p.type] || '📍 ' + p.type; - const name = p.name ? ` — ${p.name}` : ''; + const name = p.name ? ` - ${p.name}` : ''; return `
${label}${name}
`; }).join(''); return `
@@ -1863,7 +1915,7 @@ function updateMiniRouteCard() { const r = routeResults[activeRouteIdx]; if (!r) return; const km = (r.distance_m / 1000).toFixed(0); - const dirt = r.stats?.dirt_total_pct ?? '—'; + const dirt = r.stats?.dirt_total_pct ?? '-'; document.getElementById('mini-dot').style.background = ROUTE_COLORS[activeRouteIdx % ROUTE_COLORS.length]; document.getElementById('mini-label').textContent = `Вариант ${activeRouteIdx + 1} из ${routeResults.length}`; document.getElementById('mini-stats').textContent = `${km} км · ${dirt}% грунт`;