/* modals.js — управление модальными окнами Help и Invoke */ // makeDraggable — делает .panel внутри modalId перетаскиваемым за заголовок (h3). function makeDraggable(modalId) { var modal = document.getElementById(modalId); if (!modal) return; var panel = modal.querySelector('.panel'); if (!panel) return; var handle = panel.querySelector('h3'); if (!handle) return; handle.style.cursor = 'move'; handle.style.userSelect = 'none'; var startX, startY, startLeft, startTop; handle.addEventListener('mousedown', function(e) { e.preventDefault(); var rect = panel.getBoundingClientRect(); // Переводим в position:fixed если ещё не if (!panel.style.left) { panel.style.position = 'fixed'; panel.style.left = rect.left + 'px'; panel.style.top = rect.top + 'px'; panel.style.margin = '0'; } startX = e.clientX; startY = e.clientY; startLeft = parseInt(panel.style.left, 10) || rect.left; startTop = parseInt(panel.style.top, 10) || rect.top; function onMove(e) { var dx = e.clientX - startX; var dy = e.clientY - startY; panel.style.left = (startLeft + dx) + 'px'; panel.style.top = (startTop + dy) + 'px'; } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); } // Инициализация после загрузки DOM document.addEventListener('DOMContentLoaded', function() { ['create-code-modal', 'create-archive-modal', 'edit-modal', 'help-modal', 'invoke-modal', 'logs-modal', 'info-modal'].forEach(makeDraggable); // ESC закрывает активное модальное окно document.addEventListener('keydown', function(e) { if (e.key !== 'Escape') return; var modals = ['create-code-modal', 'create-archive-modal', 'edit-modal', 'help-modal', 'invoke-modal', 'logs-modal', 'info-modal']; for (var i = 0; i < modals.length; i++) { var el = document.getElementById(modals[i]); if (el && el.classList.contains('open')) { el.classList.remove('open'); break; } } }); }); // showModalError — показывает ошибку внутри модалки. // errorDivId — id элемента с классом modal-error. function showModalError(errorDivId, message) { var el = document.getElementById(errorDivId); if (!el) return; el.textContent = message; el.style.display = 'block'; } function hideModalError(errorDivId) { var el = document.getElementById(errorDivId); if (el) { el.style.display = 'none'; el.textContent = ''; } } function openHelp() { document.getElementById('help-modal').classList.add('open'); } function closeHelp() { document.getElementById('help-modal').classList.remove('open'); } function openInvoke(name) { S.currentInvoke = name; document.getElementById('i-title').textContent = 'Вызов: ' + name; document.getElementById('i-resp').value = ''; document.getElementById('i-status').textContent = ''; document.getElementById('i-meta').style.display = 'none'; document.getElementById('i-meta').textContent = ''; document.getElementById('invoke-modal').classList.add('open'); } function closeInvoke() { document.getElementById('invoke-modal').classList.remove('open'); S.currentInvoke = null; } async function submitInvoke() { if (!S.currentInvoke) return; const btn = document.getElementById('i-submit'); const statusEl = document.getElementById('i-status'); const metaEl = document.getElementById('i-meta'); const respEl = document.getElementById('i-resp'); btn.disabled = true; respEl.value = ''; metaEl.style.display = 'none'; metaEl.textContent = ''; let elapsed = 0; statusEl.textContent = 'Вызов...'; const timer = setInterval(() => { elapsed++; if (elapsed < 5) { statusEl.textContent = 'Вызов... ' + elapsed + 'с'; } else if (elapsed < 10) { statusEl.textContent = '⏳ Возможен cold start — прогрев пула... ' + elapsed + 'с'; } else { statusEl.textContent = '⏳ Возможны cold start или specialization... ' + elapsed + 'с'; } }, 1000); try { const raw = document.getElementById('i-body').value.trim(); let parsed = {}; if (raw) parsed = JSON.parse(raw); const result = await requestJSON(API_BASE + '/functions/' + encodeURIComponent(S.currentInvoke) + '/invoke', 'POST', parsed); var latencyMs = Number(result.latency_ms || 0); var measuredSeconds = latencyMs > 0 ? Math.max(1, Math.round(latencyMs / 1000)) : elapsed; statusEl.textContent = '✓ Выполнено за ' + formatSeconds(measuredSeconds); metaEl.style.display = 'block'; metaEl.textContent = [ 'HTTP status: ' + (result.status || 'n/a'), 'Latency: ' + (latencyMs || 'n/a') + ' ms', 'Причина задержки: ' + explainDelay(measuredSeconds) ].join('\n'); respEl.value = JSON.stringify(result, null, 2); } catch (e) { statusEl.textContent = '✗ Ошибка после ' + formatSeconds(elapsed); metaEl.style.display = 'block'; metaEl.textContent = [ 'Последняя ошибка: ' + e.message, 'Вероятная причина задержки: ' + explainDelay(elapsed) ].join('\n'); respEl.value = 'Ошибка вызова: ' + e.message; } finally { clearInterval(timer); btn.disabled = false; } } // openLogs — открывает модалку с логами функции. async function openLogs(name) { S.currentLogs = name; document.getElementById('logs-title').textContent = 'Логи: ' + name; document.getElementById('logs-output').value = 'Загрузка...'; document.getElementById('logs-modal').classList.add('open'); await _fetchLogs(name); } function closeLogs() { document.getElementById('logs-modal').classList.remove('open'); S.currentLogs = null; } async function refreshLogs() { if (S.currentLogs) await _fetchLogs(S.currentLogs); } async function _fetchLogs(name) { var out = document.getElementById('logs-output'); try { var data = await getJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/logs'); out.value = data.logs || '(нет логов)'; // прокручиваем вниз out.scrollTop = out.scrollHeight; } catch (e) { out.value = 'Ошибка загрузки логов: ' + e.message; } } // openInfo — открывает модалку с подробной информацией о функции. // Данные берутся из data-атрибута кнопки (без дополнительного запроса к API). function openInfo(name, btn) { var raw = btn ? btn.getAttribute('data-info') : null; var info = null; try { info = raw ? JSON.parse(raw) : null; } catch(e) {} if (!info) { document.getElementById('info-rows').innerHTML = 'Нет данных'; document.getElementById('info-title').textContent = 'Информация: ' + name; document.getElementById('info-modal').classList.add('open'); return; } // copyBtn — кнопка копирования конкретного значения через data-атрибут (безопасно для любых символов). function copyBtn(val) { if (!val || val === '-') return ''; var encoded = val.replace(/&/g, '&').replace(/"/g, '"'); return ' '; } // Строки без кнопки копирования (только отображение). function plain(val) { return '' + h(val || '-') + ''; } // Строки с кнопкой копирования (Имя, Пакет, Маршрут — генерируемые или идентификаторы). function withCopy(val) { return '' + h(val || '-') + copyBtn(val) + ''; } function labelTd(label) { return '' + h(label) + ''; } // CURL-строка для внешнего вызова (полная, с токеном из localStorage). var token = localStorage.getItem('auth_token') || ''; var method = (Array.isArray(info.methods) && info.methods.length) ? info.methods[0] : 'GET'; var externalUrl = window.location.origin + '/fn' + (info.route !== '-' ? info.route : ''); var curlFull = 'curl -H "X-Auth-Token: ' + token + '" -X ' + method + ' "' + externalUrl + '"'; // В отображении скрываем токен — показываем только метод и URL. var curlDisplay = 'curl ... -X ' + method + ' "' + externalUrl + '"'; // Храним полную команду в data-атрибуте (HTML-encode), читаем через dataset — безопасно для любых символов. var curlAttr = curlFull.replace(/&/g, '&').replace(/"/g, '"'); var html = [ '' + labelTd('Имя') + withCopy(info.name) + '', '' + labelTd('Окружение') + plain(info.env) + '', '' + labelTd('Пакет') + withCopy(info.pkg) + '', '' + labelTd('Entrypoint') + plain(info.entrypoint) + '', '' + labelTd('Таймаут') + plain(info.timeout ? info.timeout + ' сек' : '-') + '', '' + labelTd('Тип источника') + plain(info.sourceType === 'archive' ? '📦 archive' : '📝 code') + '', '' + labelTd('Маршрут') + withCopy(info.route) + '', '' + labelTd('Методы') + plain(Array.isArray(info.methods) && info.methods.length ? info.methods.join(', ') : '-') + '', '' + labelTd('Cron') + plain(info.cron || '-') + '', '' + labelTd('Создана') + plain(info.createdAt) + '', '' + labelTd('Изменена') + plain(info.updatedAt) + '', '' + labelTd('curl') + '' + h(curlDisplay) + ' ' + '', ].join(''); document.getElementById('info-title').textContent = 'Информация: ' + name; document.getElementById('info-rows').innerHTML = html; document.getElementById('info-modal').classList.add('open'); } // closeInfo — закрывает Info-модалку. function closeInfo() { document.getElementById('info-modal').classList.remove('open'); }