auto-sync: 2026-05-05 23:20:01

This commit is contained in:
Stream
2026-05-05 23:20:01 +03:00
parent 60fd17f88f
commit ca66fc418a
3 changed files with 161 additions and 46 deletions

View File

@@ -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}]}

View File

@@ -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"
]
}
}
}

View File

@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>`;
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 `<div class="wl-item" id="wl-item-${i}" data-idx="${i}">
<div class="wl-pin">${waypointPinSvg(label, color)}</div>
<div class="wl-info">
@@ -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) =>
`<button class="marker-type-btn" onclick="selectMarkerType(${i}, ${lngLat.lat}, ${lngLat.lng})">
<span class="mt-icon">${ic}</span>
<span class="mt-label">${['Флаг','Лагерь','Ремонт','Заправка','Вода','Точка'][i]}</span>
@@ -1030,7 +1082,7 @@ function drawNamedMarker(markerData) {
el.className = 'named-marker-el marker-anim';
el.textContent = markerData.icon;
el.title = markerData.name;
const popup = new maplibregl.Popup({ offset: 25, closeButton: true })
.setHTML(`
<div class="popup-title">${escapeXml(markerData.name)}</div>
@@ -1041,12 +1093,12 @@ function drawNamedMarker(markerData) {
<button onclick="removeMarker(${markerData.id})" style="flex:1;padding:3px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:11px;">🗑 Удалить</button>
</div>
`);
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 = `
<div class="popup-title">${props.name || 'Без названия'}</div>
<div class="popup-row"><span class="popup-key">Тип</span><span class="popup-val">${props.highway || ''}</span></div>
<div class="popup-row"><span class="popup-key">Покрытие</span><span class="popup-val">${props.surface || ''}</span></div>
<div class="popup-row"><span class="popup-key">Категория</span><span class="popup-val">${props.tracktype || ''}</span></div>
<div class="popup-row"><span class="popup-key">Тип</span><span class="popup-val">${props.highway || '-'}</span></div>
<div class="popup-row"><span class="popup-key">Покрытие</span><span class="popup-val">${props.surface || '-'}</span></div>
<div class="popup-row"><span class="popup-key">Категория</span><span class="popup-val">${props.tracktype || '-'}</span></div>
<div class="popup-row"><span class="popup-key">Длина</span><span class="popup-val">${formatLength(props.length_m)}</span></div>
${props.mtb_scale ? `<div class="popup-row"><span class="popup-key">MTB scale</span><span class="popup-val">${props.mtb_scale}</span></div>` : ''}
`;
@@ -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 =>
`<div class="poi-row">
<span class="poi-row-label"><span class="poi-icon">${pt.icon}</span> ${pt.label}</span>
<span class="poi-row-count">${p[pt.key] || 0}</span>
</div>`
).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 `<div class="scenic-poi-item">${label}${name}</div>`;
}).join('');
return `<div class="route-card ${i===activeIdx?'active':''}" onclick="selectScenicRoute(${i})">
@@ -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}% грунт`;