// scenario-list.js — список сценариев, запуск, поллинг // // Отвечает за: // - Загрузку списка сценариев из GET /api/scenarios // - Раскрытие шагов сценария (GET /api/scenario/definitions/{id}) // - Запуск сценария (POST /api/scenario/run) // - Поллинг статуса запуска (GET /api/scenario/run/{run_id}) // // Цветовая маркировка: RUNNING → жёлтый фон (#fef3c7) let scenarioOpen=false, scenarioPollTimer=null; /** * Переключить панель сценариев (открыть/закрыть). * При открытии — загружает список + последний запуск. */ async function toggleScenario(){ const body=document.getElementById('scenario-body'); scenarioOpen=!scenarioOpen; body.style.display=scenarioOpen?'block':'none'; if(!scenarioOpen){ stopScenarioPoll(); return; } await loadScenarios(); } /** * Загрузить список сценариев + последний запуск. * * Два параллельных запроса: * GET /api/scenarios — список определений [{id, name, steps}, ...] * GET /api/scenario/status — последние 10 запусков [{scenario_name, status, ...}, ...] */ async function loadScenarios(){ const body=document.getElementById('scenario-body'); try{ const [sr, srHistory]=await Promise.all([ fetch('/api/scenarios').then(r=>r.json()), fetch('/api/scenario/status').then(r=>r.json()), ]); let html=''; // Кнопка создания нового сценария html+=``; if(sr.length){ // Список сценариев — каждый кликабелен html+='
'; sr.forEach(s=>{ const seed = s.is_seed; // seed-сценарий (из YAML) — нельзя удалить html+=`
`; // Заголовок сценария — клик раскрывает шаги html+=`
`; html+=`${_esc(s.name)}`; html+=`${s.steps} шаг.`; if(seed) html+=`seed`; html+=`
`; // Контейнер для раскрытых шагов (изначально скрыт) html+=``; html+=`
`; }); html+='
'; }else{ html+='Нет сценариев в БД'; } // Последний запуск — фильтруем по текущей версии приложения const curVer=window.APP&&window.APP.version||''; const verHistory=srHistory&&srHistory.filter(r=>r.app_version===curVer); if(verHistory && verHistory.length){ const last=verHistory[0]; const icon=last.status==='OK'?'✅':last.status==='FAIL'?'❌':last.status==='RUNNING'?'⏳':'⏱'; const time=last.created_at?last.created_at.replace('T',' ').substring(0,19):'?'; // RUNNING → жёлтый фон const runningStyle = last.status==='RUNNING' ? 'background:#fef3c7;padding:2px 4px;border-radius:3px;' : ''; html+=`
Последний: ${icon} ${_esc(last.scenario_name)} — ${last.status} (шаг ${last.current_step}/${last.total_steps}) ${last.duration_sec!=null?last.duration_sec.toFixed(1)+'s':''} — ${time}
`; if(last.error_log) html+=`
${_esc(last.error_log)}
`; } body.innerHTML=html; }catch(e){ body.innerHTML='Ошибка загрузки'; } } /** * Раскрыть шаги сценария — загрузить полное определение. * * Показывает: * - Список шагов: операция → сервис + параметры * - Кнопки: ▶ Запустить, ✏ Редактировать, 📋 Копировать, 🗑 Удалить * * Закрывает другие раскрытые сценарии (только один открыт одновременно). * * @param {number} defId — ID сценария */ async function toggleScenarioSteps(defId) { const el = document.getElementById('scenario-steps-' + defId); if (!el) return; // Если уже открыт — закрыть (toggle) if (el.style.display === 'block') { el.style.display = 'none'; return; } try { const r = await fetch('/api/scenario/definitions/' + defId); const def = await r.json(); if (def.error) return; const steps = def.steps || []; let html = '
'; // Перечисляем шаги steps.forEach((s, i) => { const svcName = s.service_id ? 'сервис ' + s.service_id : '?'; html += `
${i+1}. ${_esc(s.operation)} → ${svcName}`; if (s.output) html += ` (${_esc(s.output)})`; if (s.instance_ref) html += ` → ${_esc(s.instance_ref)}`; if (s.instance_uid) html += ` → ${_esc(s.instance_uid.substring(0,8))}...`; const params = Object.entries(s.params || {}); if (params.length) html += ' (' + params.map(([k,v]) => _esc(k)+'='+_esc(v)).join(', ') + ')'; html += '
'; }); // Кнопки действий // JS-escape для имени сценария в inline onclick: // \ → \\ (backslash) // ' → \' (terminate JS string literal) // HTML-escape уже не нужен — внутри JS-строки в атрибуте HTML-теги не парсятся. const escName = def.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'"); html += ``; html += ``; html += ``; // Удалить — только для НЕ-seed сценариев if (!def.is_seed) html += ``; html += '
'; el.innerHTML = html; el.style.display = 'block'; // Закрыть другие раскрытые сценарии document.querySelectorAll('[id^="scenario-steps-"]').forEach(e => { if (e !== el) e.style.display = 'none'; }); } catch (e) {} } /** * Запустить сценарий. * * Алгоритм: * 1. confirm — подтверждение * 2. POST /api/scenario/run → {status: "RUNNING", run_id} * 3. Поллинг каждые 3 секунды: * GET /api/scenario/run/{run_id} → {status, current_step, total_steps, ...} * 4. При OK/FAIL → остановить поллинг, обновить инстансы и историю * * @param {number} defId — ID определения сценария */ async function runScenario(defId){ if(busy){ return; } // другая операция уже идёт if(!confirm('Запустить сценарий?')) return; busy=true; stopScenarioPoll(); const body=document.getElementById('scenario-body'); body.innerHTML=`
⏳ Запуск...
`; try{ const r=await fetch('/api/scenario/run',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({definition_id:defId}) }); const d=await r.json(); if(d.error){ body.innerHTML=`Ошибка: ${_esc(d.error)}`; busy=false; return; } const runId=d.run_id; if(!runId){ body.innerHTML=`Ошибка: нет run_id в ответе`; busy=false; return; } // Поллинг статуса // Поллинг статуса с защитой от бесконечного зависания let _scenarioPollErrors = 0; scenarioPollTimer=setInterval(async()=>{ try{ const sr=await fetch('/api/scenario/run/'+runId); if(!sr.ok){ _scenarioPollErrors++; if(_scenarioPollErrors>=5){ stopScenarioPoll(); busy=false; body.innerHTML='Ошибка: сервер недоступен'; } return; } _scenarioPollErrors = 0; // сброс при успехе const last=await sr.json(); if(!last||last.error) return; const icon=last.status==='OK'?'✅':last.status==='FAIL'?'❌':last.status==='RUNNING'?'⏳':'⏱'; const time=last.created_at?last.created_at.replace('T',' ').substring(0,19):'?'; const runningBg = last.status==='RUNNING' ? 'background:#fef3c7;padding:2px 4px;border-radius:3px;' : ''; let html=`
${icon} ${_esc(last.scenario_name)} — ${last.status} (шаг ${last.current_step}/${last.total_steps}) ${last.duration_sec!=null?last.duration_sec.toFixed(1)+'s':''} — ${time}
`; if(last.error_log) html+=`
${_esc(last.error_log)}
`; body.innerHTML=html; if(last.status!=='RUNNING'){ stopScenarioPoll(); busy=false; await refreshInstances(); // обновить список инстансов if(historyOpen) await loadHistory(); // обновить историю } }catch(e){ _scenarioPollErrors++; if(_scenarioPollErrors>=5){ stopScenarioPoll(); busy=false; body.innerHTML=`Ошибка поллинга: ${_esc(e.message||'')}`; } } },3000); }catch(e){ body.innerHTML=`Ошибка: ${_esc(e.message)}`; busy=false; } } /** * Остановить поллинг сценария. */ function stopScenarioPoll(){ if(scenarioPollTimer){ clearInterval(scenarioPollTimer); scenarioPollTimer=null; } }