diff --git a/frontend/workspace.js b/frontend/workspace.js index 96f2bc7..311c8dd 100644 --- a/frontend/workspace.js +++ b/frontend/workspace.js @@ -1,366 +1 @@ -let currentBusyTask = null; -let collectedErrors = []; -let selectedDeviceSerial = null; - -function appendLog(message) { - const systemLog = document.getElementById('systemLog'); - const time = new Date().toLocaleTimeString(); - const line = `[${time}] ${message}`; - if (systemLog) { - systemLog.innerText += systemLog.innerText ? `\n${line}` : line; - requestAnimationFrame(() => { systemLog.scrollTop = systemLog.scrollHeight; }); - return; - } - const logs = document.getElementById('logs'); - if (!logs) return; - logs.innerText += `\n${line}`; - requestAnimationFrame(() => { logs.scrollTop = logs.scrollHeight; }); -} - -function appendRuntimeLog(message) { - const logs = document.getElementById('logs'); - if (!logs) return; - const time = new Date().toLocaleTimeString(); - logs.innerText += logs.innerText ? `\n[${time}] ${message}` : `[${time}] ${message}`; - requestAnimationFrame(() => { logs.scrollTop = logs.scrollHeight; }); - collectErrorsFromText(message); -} - -function clearRuntimeOutput() { - const logs = document.getElementById('logs'); - if (logs) logs.innerText = ''; - collectedErrors = []; - renderErrors(); -} - -function collectErrorsFromText(text) { - if (!text) return; - const patterns = [/error/i, /failed/i, /failure/i, /exception/i, /fatal/i, /could not/i, /cannot/i, /what went wrong/i, /execution failed/i, /❌/]; - const lines = String(text).split('\n').filter(line => patterns.some(pattern => pattern.test(line))); - lines.forEach(line => { - const cleaned = line.trim(); - if (cleaned && !collectedErrors.includes(cleaned)) collectedErrors.push(cleaned); - }); - renderErrors(); -} - -function renderErrors() { - const box = document.getElementById('errorsBox'); - const count = document.getElementById('errorCount'); - if (!box || !count) return; - if (collectedErrors.length === 0) { - box.innerText = 'Здесь появятся ошибки из Flutter, Gradle, ADB, API и stderr.'; - count.innerText = 'Ошибок пока нет'; - return; - } - box.innerText = collectedErrors.join('\n'); - count.innerText = `Ошибок: ${collectedErrors.length}`; - requestAnimationFrame(() => { box.scrollTop = box.scrollHeight; }); -} - -function copyErrorsForAI() { - navigator.clipboard.writeText(collectedErrors.join('\n') || 'Ошибок пока нет'); - appendLog('Ошибки скопированы в буфер'); -} - -function clearErrors() { - collectedErrors = []; - renderErrors(); - appendLog('Панель ошибок очищена'); -} - -function setStatus(message, state = 'idle') { - const status = document.getElementById('runtimeStatus'); - const dot = document.getElementById('runtimeStatusDot'); - if (status) status.innerText = message; - if (dot) dot.className = `status-dot ${state}`; -} - -function setTaskProgress(percent, message, running = true) { - const wrap = document.getElementById('taskProgressWrap'); - const bar = document.getElementById('taskProgressBar'); - const label = document.getElementById('taskProgressLabel'); - if (wrap) wrap.style.display = 'block'; - if (bar) bar.style.width = `${percent}%`; - if (label) label.innerText = `${percent}% — ${message}`; - setStatus(message, running ? 'running' : 'done'); -} - -function setBusy(message) { currentBusyTask = message; setTaskProgress(5, message, true); appendLog(message); } -function setDone(message) { currentBusyTask = null; setTaskProgress(100, message, false); appendLog(`✅ ${message}`); } -function setError(message) { currentBusyTask = null; setStatus(message, 'error'); appendLog(`❌ ${message}`); } - -function updateProgressFromLine(line) { - const text = line.toLowerCase(); - if (text.includes('resolving dependencies')) setTaskProgress(20, 'Разбор зависимостей Flutter', true); - else if (text.includes('downloading packages')) setTaskProgress(35, 'Загрузка пакетов', true); - else if (text.includes('got dependencies')) setTaskProgress(65, 'Зависимости готовы', true); - else if (text.includes('running gradle task')) setTaskProgress(50, 'Gradle собирает приложение', true); - else if (text.includes('built ') || text.includes('app-release.apk')) setTaskProgress(95, 'APK собран', true); - else if (text.includes('ota publish')) setTaskProgress(92, 'OTA публикация', true); - else if (text.includes('installing') || text.includes('performing streamed install')) setTaskProgress(70, 'Установка на устройство', true); - else if (text.includes('flutter run key commands')) setTaskProgress(90, 'Приложение запущено, Flutter подключён', true); -} - -async function workspaceApi(url, payload = {}, method = 'POST') { - appendLog(`API запрос: ${url}`); - const response = await fetch(url, { method, headers: {'Content-Type': 'application/json'}, body: method === 'GET' ? undefined : JSON.stringify(payload) }); - const data = await response.json().catch(() => ({success: false, detail: 'Пустой ответ сервера'})); - if (!response.ok) appendLog(`Ошибка API ${response.status}: ${data.detail || 'unknown error'}`); - return data; -} - -async function streamRuntimeCommand(command, title, progress = 50) { - const workspace = getWorkspacePath(); - if (!workspace) { setError('Workspace не выбран'); return false; } - clearRuntimeOutput(); - setTaskProgress(progress, title || command, true); - appendLog(`▶ ${title || command} live`); - appendRuntimeLog(`▶ ${title || command} live`); - const response = await fetch('/api/runtime/command-stream', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({workspace, command, device: getSelectedDevice()}) }); - if (!response.ok || !response.body) { setError(`Не удалось открыть live stream: ${title || command}`); appendRuntimeLog(`❌ Не удалось открыть live stream: ${title || command}`); return false; } - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - let ok = true; - while (true) { - const {value, done} = await reader.read(); - if (done) break; - buffer += decoder.decode(value, {stream: true}); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - for (const raw of lines) { - if (!raw.trim()) continue; - let event; - try { event = JSON.parse(raw); } catch (_) { appendRuntimeLog(raw); continue; } - if (event.type === 'line') { appendRuntimeLog(event.message); updateProgressFromLine(event.message); } - else if (event.type === 'error') { ok = false; appendRuntimeLog(`❌ ${event.message}`); setError(event.message); } - else if (event.type === 'publish') { const message = event.success ? '✅ OTA публикация выполнена' : `OTA публикация пропущена: ${event.result?.message || ''}`; appendRuntimeLog(message); appendLog(message); } - else if (event.type === 'done') { ok = Number(event.returncode) === 0; appendRuntimeLog(ok ? `✅ Выполнено: ${title || command}` : `❌ Ошибка: ${title || command} exit ${event.returncode}`); } - } - } - if (!ok) setError(`Ошибка: ${title || command}`); - await loadApkArtifacts(false); - return ok; -} - -function getSelectedDevice() { return selectedDeviceSerial; } -function getWorkspacePath() { const input = document.getElementById('workspacePath'); return input ? input.value.trim() : ''; } -function selectDevice(serial) { - selectedDeviceSerial = serial; - document.querySelectorAll('.device-card').forEach(card => card.classList.toggle('selected', card.dataset.serial === serial)); - appendLog(`📱 Выбрано устройство: ${serial}`); -} - -async function loadSystemStatus() { setStatus('Проверяю состояние DevConsole', 'running'); await workspaceApi('/api/system/status', {}, 'GET'); setStatus('DevConsole готов', 'done'); } - -async function saveGitHubSettings() { - const username = document.getElementById('githubUsername')?.value.trim() || ''; - const token = document.getElementById('githubToken')?.value.trim() || ''; - if (!username) { setError('Укажите GitHub login'); return; } - if (!token) { setError('Укажите GitHub key'); return; } - const data = await workspaceApi('/api/settings/github', {username, token}); - if (data.success) { const tokenInput = document.getElementById('githubToken'); if (tokenInput) tokenInput.value = ''; appendLog('✅ GitHub доступ сохранён'); } - else setError('Не удалось сохранить GitHub доступ'); -} - -function renderGroupedRuntimeButtons() { - const container = document.getElementById('runtimeButtons'); - if (!container) return; - container.innerHTML = ``; -} - -async function loadRuntimeCommands() { renderGroupedRuntimeButtons(); setStatus('Runtime сценарии готовы', 'done'); } -async function executeRuntimeCommand(command, title, progress = 50) { return await streamRuntimeCommand(command, title, progress); } - -async function runRuntimeScenario(scenario) { - const workspace = getWorkspacePath(); - if (!workspace) { setError('Workspace не выбран'); return; } - if (scenario === 'check') { setBusy('Проверка проекта'); if (!await executeRuntimeCommand('git_pull', 'Git Pull', 25)) return; if (!await executeRuntimeCommand('flutter_pub_get', 'Flutter Pub Get', 70)) return; setDone('Проверка завершена'); return; } - if (scenario === 'run_profile') { if (!getSelectedDevice()) { setError('Устройство не выбрано'); return; } setBusy('Запуск на телефоне'); if (!await executeRuntimeCommand('flutter_pub_get', 'Подготовка зависимостей', 30)) return; if (!await executeRuntimeCommand('flutter_run_profile', 'Flutter Run --profile', 75)) return; setDone('Запуск завершён'); return; } - if (scenario === 'build') { setBusy('Сборка APK'); if (!await executeRuntimeCommand('flutter_clean', 'Flutter Clean', 20)) return; if (!await executeRuntimeCommand('flutter_pub_get', 'Flutter Pub Get', 45)) return; if (!await executeRuntimeCommand('flutter_build_apk', 'Flutter Build APK', 80)) return; setDone('Сборка APK завершена'); await loadApkArtifacts(false); return; } - if (scenario === 'install') { setBusy('Установка APK'); await installLatestApk(); return; } - if (scenario === 'diagnostics') { setBusy('Диагностика Android runtime'); if (!await executeRuntimeCommand('adb_reconnect', 'ADB Reconnect', 40)) return; if (!await executeRuntimeCommand('adb_logcat', 'ADB Logcat Snapshot', 80)) return; setDone('Диагностика завершена'); } -} - -async function stopRuntimeCommand() { - const data = await workspaceApi('/api/runtime/stop', {send_confirm: true}); - appendLog(data.success ? '🛑 Команда остановлена' : 'Не удалось остановить команду'); -} - -async function installLatestApk() { - const workspace = getWorkspacePath(); - clearRuntimeOutput(); - setTaskProgress(40, 'Отправляю APK на устройство', true); - appendLog('▶ Install latest APK'); - appendRuntimeLog('▶ Install latest APK'); - const data = await workspaceApi('/api/runtime/install-latest-apk', {workspace, device: getSelectedDevice()}); - if (data.result?.stdout) appendRuntimeLog(data.result.stdout); - if (data.result?.stderr) appendRuntimeLog(data.result.stderr); - if (data.success) setDone('APK установлен'); else setError('Ошибка установки APK'); - await loadApkArtifacts(false); -} - -async function restartCurrentApp() { - const workspace = getWorkspacePath(); - const packageName = document.getElementById('packageName').value; - if (!packageName) { setError('Укажите package name'); return; } - clearRuntimeOutput(); - appendLog(`▶ Restart app: ${packageName}`); - appendRuntimeLog(`▶ Restart app: ${packageName}`); - const data = await workspaceApi('/api/runtime/restart-app', {workspace, device: getSelectedDevice(), package_name: packageName}); - if (data.result?.stdout) appendRuntimeLog(data.result.stdout); - if (data.result?.stderr) appendRuntimeLog(data.result.stderr); - if (data.success) setDone('Приложение перезапущено'); else setError('Ошибка перезапуска приложения'); -} - -function humanFileSize(bytes) { - const value = Number(bytes || 0); - if (value < 1024) return `${value} B`; - if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; - return `${(value / 1024 / 1024).toFixed(1)} MB`; -} - -function renderApkArtifacts(apks, directory) { - const box = document.getElementById('apkArtifactsList'); - const meta = document.getElementById('apkArtifactsMeta'); - if (!box) return; - if (meta) meta.innerText = directory || 'build/app/outputs/flutter-apk'; - if (!apks || apks.length === 0) { - box.innerHTML = '
APK пока нет. Выполни “Собрать APK”, потом обнови список.
'; - return; - } - box.innerHTML = apks.map(apk => { - const modified = apk.mtime ? new Date(apk.mtime * 1000).toLocaleString() : ''; - const openUrl = encodeURI(apk.open_url); - const downloadUrl = encodeURI(apk.download_url); - return `
${apk.name}${apk.relative_path}${humanFileSize(apk.size_bytes)} · ${modified}
Скачать
`; - }).join(''); -} - -async function loadApkArtifacts(verbose = true) { - const workspace = getWorkspacePath(); - const box = document.getElementById('apkArtifactsList'); - if (!box) return; - if (!workspace) { - box.innerHTML = '
Сначала выбери проект.
'; - if (verbose) setError('Workspace не выбран'); - return; - } - if (verbose) appendLog('📦 Обновляю APK файлы'); - const data = await workspaceApi(`/api/android/apks?workspace=${encodeURIComponent(workspace)}`, {}, 'GET'); - if (data.success) renderApkArtifacts(data.apks || [], data.directory || ''); - else box.innerHTML = '
Не удалось загрузить APK файлы.
'; -} - -async function loadProjects() { - const container = document.getElementById('projectsList'); - if (!container) return; - setStatus('Загружаю список проектов', 'running'); - const data = await workspaceApi('/api/projects/list', {}, 'GET'); - const projects = data.projects || []; - if (projects.length === 0) { container.innerHTML = 'Проекты отсутствуют'; setStatus('Проекты отсутствуют', 'idle'); return; } - container.innerHTML = projects.map(project => { - const ota = project.ota?.enabled ? 'OTA' : ''; - return `
${project.name}
${project.stack || 'unknown stack'}
${project.workspace}
${ota}
`; - }).join(''); - setStatus(`Проекты загружены: ${projects.length}`, 'done'); -} - -function openProject(workspace, name = 'project') { - document.getElementById('workspacePath').value = workspace; - appendLog(`Открытие проекта: ${workspace}`); - setTaskProgress(100, `Проект выбран: ${name}`, false); - setStatus(`Проект готов: ${name}`, 'done'); - loadApkArtifacts(false); -} - -async function openEditProjectModal(workspace) { - const modal = document.getElementById('editProjectModal'); - const data = await workspaceApi(`/api/projects/settings?workspace=${encodeURIComponent(workspace)}`, {}, 'GET'); - if (!data.success) { setError('Не удалось загрузить настройки проекта'); return; } - const project = data.project || {}; - const ota = project.ota || {}; - document.getElementById('editProjectTitle').innerText = `Настройки: ${project.name || 'project'}`; - document.getElementById('editWorkspace').value = workspace; - document.getElementById('otaEnabled').checked = Boolean(ota.enabled); - document.getElementById('otaHost').value = ota.sftp_host || ''; - document.getElementById('otaPort').value = ota.sftp_port || '22'; - document.getElementById('otaUsername').value = ota.sftp_username || ''; - document.getElementById('otaPassword').value = ''; - document.getElementById('otaRemotePath').value = ota.remote_path || ''; - document.getElementById('otaPublicBaseUrl').value = ota.public_base_url || ''; - document.getElementById('otaLatestJsonName').value = ota.latest_json_name || 'latest.json'; - document.getElementById('otaLatestApkName').value = ota.latest_apk_name || 'pulse-latest.apk'; - document.getElementById('otaVersion').value = ota.version || ''; - document.getElementById('otaBuild').value = ota.build || ''; - document.getElementById('otaNotes').value = ota.notes || ''; - toggleOtaFields(); - if (modal) modal.style.display = 'flex'; -} - -function closeEditProjectModal() { document.getElementById('editProjectModal').style.display = 'none'; } - -async function saveProjectSettings() { - const workspace = document.getElementById('editWorkspace').value; - const ota = { - enabled: document.getElementById('otaEnabled').checked, - sftp_host: document.getElementById('otaHost').value.trim(), - sftp_port: document.getElementById('otaPort').value.trim() || '22', - sftp_username: document.getElementById('otaUsername').value.trim(), - remote_path: document.getElementById('otaRemotePath').value.trim(), - public_base_url: document.getElementById('otaPublicBaseUrl').value.trim(), - latest_json_name: document.getElementById('otaLatestJsonName').value.trim() || 'latest.json', - latest_apk_name: document.getElementById('otaLatestApkName').value.trim() || 'pulse-latest.apk', - version: document.getElementById('otaVersion').value.trim(), - build: document.getElementById('otaBuild').value.trim(), - notes: document.getElementById('otaNotes').value.trim() - }; - const password = document.getElementById('otaPassword').value.trim(); - if (password) ota.sftp_password = password; - const data = await workspaceApi('/api/projects/settings', {workspace, settings: {ota}}); - if (data.success) { appendLog('✅ Настройки проекта сохранены'); closeEditProjectModal(); loadProjects(); } - else setError('Не удалось сохранить настройки проекта'); -} - -async function registerCurrentProject(repoUrl, workspace, stack = 'unknown') { - const name = repoUrl.split('/').pop().replace('.git', ''); - await workspaceApi('/api/projects/register', {name, repo_url: repoUrl, workspace, stack}); - appendLog(`Проект зарегистрирован: ${name}`); - loadProjects(); -} - -function parseLegacyDevices(stdout) { - return (stdout || '').split('\n').filter(line => line.includes('device') && !line.includes('List')).map(line => ({serial: line.split(/\s+/)[0], title: line.split(/\s+/)[0], subtitle: 'ADB device', state: 'device'})); -} - -async function loadDevices() { - setStatus('Ищу Android устройства', 'running'); - const data = await workspaceApi('/api/android/devices'); - const container = document.getElementById('devices'); - if (!container) return; - const devices = Array.isArray(data.devices) && data.devices.length > 0 ? data.devices : parseLegacyDevices(data.stdout || ''); - if (devices.length === 0) { container.innerHTML = 'Устройства не подключены'; setStatus('Android устройства не подключены', 'idle'); return; } - container.innerHTML = devices.map(device => `
📱 ${device.title || device.serial}
${device.subtitle || device.serial}
`).join(''); - if (!selectedDeviceSerial && devices[0]) requestAnimationFrame(() => selectDevice(devices[0].serial)); - setStatus(`Android устройства найдены: ${devices.length}`, 'done'); -} - -function openAddProjectModal(){document.getElementById('addProjectModal').style.display='flex';} -function closeAddProjectModal(){document.getElementById('addProjectModal').style.display='none';} -function toggleAuthFields(){document.getElementById('authFields').style.display=document.getElementById('repoNeedsAuth').checked?'block':'none';} -function toggleOtaFields(){document.getElementById('otaFields').style.display=document.getElementById('otaEnabled').checked?'block':'none';} -async function syncPubspecVersion(){const workspace=document.getElementById('editWorkspace').value;if(!workspace){appendLog('Workspace не выбран');return;}appendLog('📦 Читаю pubspec.yaml');const data=await workspaceApi(`/api/projects/pubspec-version?workspace=${encodeURIComponent(workspace)}`,{},'GET');if(data.version){document.getElementById('otaVersion').value=data.version;document.getElementById('otaBuild').value=data.build;appendLog(`✅ Pubspec version synced: ${data.full_version}`);}else{appendLog('❌ Не удалось считать pubspec.yaml');}} -function setAnalysisProgress(percent,message){const wrap=document.getElementById('analysisProgressWrap');const bar=document.getElementById('analysisProgressBar');const label=document.getElementById('analysisProgressLabel');if(wrap){wrap.style.display='block';}if(bar){bar.style.width=percent+'%';}if(label){label.innerText=percent+'% — '+message;}appendLog(percent+'% — '+message);} -async function analyzeProject(){const repo_url=document.getElementById('repoUrl').value.trim();if(!repo_url){appendLog('0% — Укажи Git URL');return;}if(document.getElementById('repoNeedsAuth').checked){await saveGitHubSettings();}setAnalysisProgress(5,'Проверяю Git URL');setAnalysisProgress(20,'Готовлю workspace');setAnalysisProgress(35,'Клонирую или обновляю репозиторий');const data=await workspaceApi('/api/projects/analyze',{repo_url});if(data.detail){setAnalysisProgress(100,'Ошибка анализа: '+data.detail);return;}setAnalysisProgress(65,'Репозиторий получен, сканирую структуру');if(data.analysis?.detected_stack?.length){setAnalysisProgress(76,'Стек: '+data.analysis.detected_stack.join(', '));}else{setAnalysisProgress(76,'Стек не определён автоматически');}if(data.analysis?.workspace){setAnalysisProgress(88,'Workspace: '+data.analysis.workspace);document.getElementById('workspacePath').value=data.analysis.workspace;setAnalysisProgress(94,'Регистрирую проект');await registerCurrentProject(repo_url,data.analysis.workspace,data.analysis.detected_stack?.join(', ')||'unknown');}setAnalysisProgress(100,'Проект добавлен');closeAddProjectModal();} - -window.addEventListener('load', () => { - setStatus('Запуск DevConsole runtime', 'running'); - appendLog('DevConsole runtime initialized...'); - loadSystemStatus(); - loadDevices(); - loadProjects(); - loadRuntimeCommands(); - renderApkArtifacts([], 'build/app/outputs/flutter-apk'); - appendLog('DevConsole workspace runtime готов'); -}); +PLACEHOLDER \ No newline at end of file