// 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]) => k+'='+v).join(', ') + ')';
html += '
';
});
// Кнопки действий
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;
}
// Поллинг статуса
scenarioPollTimer=setInterval(async()=>{
try{
const sr=await fetch('/api/scenario/run/'+runId);
if(!sr.ok){ return; }
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){}
},3000);
}catch(e){
body.innerHTML=`Ошибка: ${_esc(e.message)}`;
busy=false;
}
}
/**
* Остановить поллинг сценария.
*/
function stopScenarioPoll(){
if(scenarioPollTimer){ clearInterval(scenarioPollTimer); scenarioPollTimer=null; }
}