').join(''):'Файловые системы ещё не получены.')+'
';el('agentProcessDetails').innerHTML='
Слушающие порты
'+(ports.length?ports.map(v=>''+safe(v.protocol)+' · '+safe(v.address)+':'+v.port+' · '+safe(v.process||'PID '+(v.pid||'—'))+'').join(''):'Не найдены или ss недоступен.')+'
'+(container?.updateAvailable?'Доступен новый образ':'Запущен текущий проверенный образ')+'
'+(container?.compose?'':'Обновление из панели доступно для Docker Compose')+(container?.state!=='running'?'':'')+'
')})}
-async function queueAgentAction(action,target){const labels={'system-update':'установить обновления операционной системы','updates-refresh':'проверить обновления операционной системы','docker-check':'проверить и загрузить свежий Docker-образ','docker-update':'пересоздать Compose-контейнер на свежем образе','docker-restart':'перезапустить Docker-контейнер','docker-remove':'удалить остановленный Docker-контейнер без удаления volumes','systemd-restart':'перезапустить службу','systemd-reset-failed':'сбросить состояние failed'};if(!currentDetailAgent||!confirm('Выполнить действие «'+(labels[action]||action)+'» для '+(target||currentDetailAgent.name)+'?'))return;const status=el('agentActionStatus');status.hidden=false;status.className='settings-note';status.textContent='Отправляем команду агенту…';document.querySelectorAll('#agentDetailDialog [data-agent-action]').forEach(v=>v.disabled=true);const response=await fetch('/api/agent-actions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agentId:currentDetailAgent.id,action,target})});if(!response.ok){status.className='settings-note status-down';status.textContent=await response.text();document.querySelectorAll('#agentDetailDialog [data-agent-action]').forEach(v=>v.disabled=false);return}const queued=await response.json();status.textContent='Команда в очереди · агент проверяет команды каждые 3 секунды';await watchAgentAction(queued.id,status)}
+async function queueAgentAction(action,target){const labels={'system-update':'установить обновления операционной системы','system-reboot':'перезагрузить операционную систему VM/LXC','updates-refresh':'проверить обновления операционной системы','docker-check':'проверить и загрузить свежий Docker-образ','docker-update':'пересоздать Compose-контейнер на свежем образе','docker-restart':'перезапустить Docker-контейнер','docker-remove':'удалить остановленный Docker-контейнер без удаления volumes','systemd-restart':'перезапустить службу','systemd-reset-failed':'сбросить состояние failed'},warning=action==='system-reboot'?'\n\nVM/LXC станет недоступна на время загрузки. Несохранённые данные приложений могут быть потеряны.':'';if(!currentDetailAgent||!confirm('Выполнить действие «'+(labels[action]||action)+'» для '+(target||currentDetailAgent.name)+'?'+warning))return;const status=el('agentActionStatus');status.hidden=false;status.className='settings-note';status.textContent='Отправляем команду агенту…';document.querySelectorAll('#agentDetailDialog [data-agent-action]').forEach(v=>v.disabled=true);const response=await fetch('/api/agent-actions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agentId:currentDetailAgent.id,action,target})});if(!response.ok){status.className='settings-note status-down';status.textContent=await response.text();document.querySelectorAll('#agentDetailDialog [data-agent-action]').forEach(v=>v.disabled=false);return}const queued=await response.json();status.textContent='Команда в очереди · агент проверяет команды каждые 3 секунды';await watchAgentAction(queued.id,status)}
async function watchAgentAction(id,status){for(let attempt=0;attempt<180;attempt++){await new Promise(resolve=>setTimeout(resolve,1000));const response=await fetch('/api/agent-actions');if(!response.ok)continue;const items=await response.json()||[],item=items.find(v=>v.id===id);if(!item)continue;status.textContent=item.status==='queued'?'Ожидаем агента…':item.status==='running'?'Выполняется на агенте…':item.status==='completed'?'Готово: '+(item.result||'успешно'):'Ошибка: '+(item.result||'команда не выполнена');if(item.status==='completed'||item.status==='failed'){status.className='settings-note '+(item.status==='failed'?'status-down':'status-up');document.querySelectorAll('#agentDetailDialog [data-agent-action]').forEach(v=>v.disabled=false);await loadAgentActions();return}}status.className='settings-note status-down';status.textContent='Агент не вернул результат за 3 минуты. Проверьте его состояние в истории действий.';document.querySelectorAll('#agentDetailDialog [data-agent-action]').forEach(v=>v.disabled=false)}
async function loadAgentHistory(agent,hours=24){const kinds=['agent-resource','agent-swap'],responses=await Promise.all(kinds.map(kind=>fetch('/api/entity-history?kind='+kind+'&hours='+hours).then(r=>r.json()))),key=agent.id+' · ',resource=(responses[0]||[]).find(v=>v.name.startsWith(key)),swap=(responses[1]||[]).find(v=>v.name.startsWith(key)),points=new Map();(resource?.points||[]).forEach(p=>points.set(p.timestamp,{timestamp:p.timestamp,cpu:p.value1,ram:p.value2}));(swap?.points||[]).forEach(p=>{const item=points.get(p.timestamp)||{timestamp:p.timestamp};item.swap=p.value1;points.set(p.timestamp,item)});chartConfigs.agentHistoryChart={series:[{label:'CPU',color:'#55d58a',value:p=>p.cpu,format:percent},{label:'RAM',color:'#9d8cff',value:p=>p.ram,format:percent},{label:'Swap',color:'#ff78b8',value:p=>p.swap,format:percent}]};const canvas=el('agentHistoryChart');drawChart('agentHistoryChart',[...points.values()].sort((a,b)=>a.timestamp-b.timestamp));if(!canvas.dataset.agentHover){setupAgentHistoryHover(canvas);canvas.dataset.agentHover='1'}}
function setupAgentHistoryHover(canvas){canvas.addEventListener('mousemove',event=>{const state=canvas._chartState,tooltip=el('agentHistoryDetails').querySelector('.chart-tooltip');if(!state||!tooltip)return;const rect=canvas.getBoundingClientRect(),x=event.clientX-rect.left,index=Math.max(0,Math.min(state.points.length-1,Math.round((x-state.pad.l)/state.pw*(state.points.length-1)))),point=state.points[index];tooltip.innerHTML='