${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,"\\'").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=`