add("network-link-"+iface.Name,"critical","Сеть",iface.Name+": нет линка","Используемый интерфейс потерял carrier или перешёл в down.")
}
}
}
ifnetworkErrors>0{
ifnetworkErrors>0{
add("network-errors","warning","Сеть","Появились новые сетевые ошибки или потери",fmt.Sprintf("За последний интервал добавилось: %d. Старые счётчики не создают активный алерт.",networkErrors))
add("network-errors","critical","Сеть","Появились новые сетевые ошибки",fmt.Sprintf("Прирост счётчиков ошибок: %d (счётчики могут пересекаться). Software RX/TX drops показаны отдельно и не создают этот алерт.",networkErrors))
health.Summary,health.LikelyCause,health.RecommendedAction="Хост не видит локальный шлюз","Проблема между Proxmox и роутером: линк, bridge, bond или VLAN","Проверьте кабель, carrier и настройки vmbr/bond/VLAN"
health.Summary,health.LikelyCause,health.RecommendedAction="Хост не видит локальный шлюз","Проблема между Proxmox и роутером: линк, bridge, bond или VLAN","Проверьте кабель, carrier и настройки vmbr/bond/VLAN"
health.LikelyCause,health.RecommendedAction="Перегрузка или ошибки линка между Proxmox и шлюзом","Проверьте errors/drops, кабель, порт коммутатора и duplex"
health.LikelyCause,health.RecommendedAction="Перегрузка или ошибки линка между Proxmox и шлюзом","Проверьте аппаратные ошибки, кабель, порт коммутатора и duplex"
case!health.DNSUp&&health.InternetUp:
case!health.DNSUp&&health.InternetUp:
health.Summary,health.LikelyCause,health.RecommendedAction="Интернет доступен, но DNS не отвечает","Проблема DNS-сервера или /etc/resolv.conf","Проверьте настроенные DNS и попробуйте разрешить домен вручную"
health.Summary,health.LikelyCause,health.RecommendedAction="Интернет доступен, но DNS не отвечает","Проблема DNS-сервера или /etc/resolv.conf","Проверьте настроенные DNS и попробуйте разрешить домен вручную"
casehealth.DNSUp&&!health.InternetUp:
casehealth.DNSUp&&!health.InternetUp:
health.Summary,health.LikelyCause,health.RecommendedAction="Локальная сеть и DNS работают, но внешняя проверка недоступна","Проблема выше роутера, блокировка HTTPS или недоступность провайдера","Проверьте маршрут наружу, firewall и доступ к внешним адресам"
health.Summary,health.LikelyCause,health.RecommendedAction="Локальная сеть и DNS работают, но внешняя проверка недоступна","Проблема выше роутера, блокировка HTTPS или недоступность провайдера","Проверьте маршрут наружу, firewall и доступ к внешним адресам"
health.Summary,health.LikelyCause,health.RecommendedAction=v.Name+": физический линк не поднят","Нет carrier либо интерфейс административно выключен","Проверьте кабель, порт коммутатора и состояние интерфейса"
health.Summary,health.LikelyCause,health.RecommendedAction=v.Name+": линк не поднят","Нет carrier либо интерфейс административно выключен","Проверьте кабель, порт коммутатора и состояние интерфейса"
return
return
}
}
ifv.NewErrors+v.NewDrops>0{
ifv.NewErrors+v.NewFaults>0{
health.Summary=fmt.Sprintf("%s: за последний интервал появилось %d сетевых ошибок/потерь",v.Name,v.NewErrors+v.NewDrops)
health.Summary=fmt.Sprintf("%s: за последний интервал появилось %d изменений счётчиков сетевых ошибок",v.Name,v.NewErrors+v.NewFaults)
health.LikelyCause,health.RecommendedAction="Счётчики интерфейса сейчас растут из-за перегрузки, кабеля, MTU или duplex","Проверьте скорость и duplex, кабель, порт коммутатора и MTU на обоих концах"
health.LikelyCause,health.RecommendedAction="Счётчики интерфейса сейчас растут из-за перегрузки, кабеля, MTU или duplex","Проверьте скорость и duplex, кабель, порт коммутатора и MTU на обоих концах"
@@ -907,7 +907,14 @@ function renderServices(svc){const items=svc.services||[];currentServiceStatuses
function renderMaintenanceTargets(){const signature=JSON.stringify([configuredServices.map(v=>[v.id,v.name]),maintenanceGuests.map(v=>[v.vmid,v.name,v.type]),maintenanceWindow.serviceIds||[],maintenanceWindow.vmids||[]]);if(signature===maintenanceTargetSignature)return;maintenanceTargetSignature=signature;const serviceIDs=new Set(maintenanceWindow.serviceIds||[]),vmids=new Set(maintenanceWindow.vmids||[]);el('maintenanceServices').innerHTML=configuredServices.length?configuredServices.map(v=>'<label class="setting-item"><input type="checkbox" data-maintenance-service="'+v.id+'" '+(serviceIDs.has(v.id)?'checked':'')+'> '+safe(v.name)+'</label>').join(''):'<span class="empty">Сервисы не добавлены.</span>';el('maintenanceGuests').innerHTML=maintenanceGuests.length?maintenanceGuests.map(v=>'<label class="setting-item"><input type="checkbox" data-maintenance-vm="'+v.vmid+'" '+(vmids.has(v.vmid)?'checked':'')+'> '+safe(v.name||('VMID '+v.vmid))+' · '+(v.type==='qemu'?'VM':'LXC')+'</label>').join(''):'<span class="empty">VM/LXC не найдены.</span>';toggleMaintenanceTargets()}
function renderMaintenanceTargets(){const signature=JSON.stringify([configuredServices.map(v=>[v.id,v.name]),maintenanceGuests.map(v=>[v.vmid,v.name,v.type]),maintenanceWindow.serviceIds||[],maintenanceWindow.vmids||[]]);if(signature===maintenanceTargetSignature)return;maintenanceTargetSignature=signature;const serviceIDs=new Set(maintenanceWindow.serviceIds||[]),vmids=new Set(maintenanceWindow.vmids||[]);el('maintenanceServices').innerHTML=configuredServices.length?configuredServices.map(v=>'<label class="setting-item"><input type="checkbox" data-maintenance-service="'+v.id+'" '+(serviceIDs.has(v.id)?'checked':'')+'> '+safe(v.name)+'</label>').join(''):'<span class="empty">Сервисы не добавлены.</span>';el('maintenanceGuests').innerHTML=maintenanceGuests.length?maintenanceGuests.map(v=>'<label class="setting-item"><input type="checkbox" data-maintenance-vm="'+v.vmid+'" '+(vmids.has(v.vmid)?'checked':'')+'> '+safe(v.name||('VMID '+v.vmid))+' · '+(v.type==='qemu'?'VM':'LXC')+'</label>').join(''):'<span class="empty">VM/LXC не найдены.</span>';toggleMaintenanceTargets()}
function toggleMaintenanceTargets(){const disabled=el('maintenanceAll').checked;document.querySelectorAll('[data-maintenance-service],[data-maintenance-vm]').forEach(v=>v.disabled=disabled)}
function toggleMaintenanceTargets(){const disabled=el('maintenanceAll').checked;document.querySelectorAll('[data-maintenance-service],[data-maintenance-vm]').forEach(v=>v.disabled=disabled)}
function applyMaintenanceState(window){maintenanceWindow=window||{};const active=!!maintenanceWindow.active,automatic=!!maintenanceWindow.automatic;el('maintenanceBanner').hidden=!active;el('maintenanceBannerReason').textContent=(automatic?'Автоматический режим · ':'')+(maintenanceWindow.reason||'');el('maintenanceBannerEnds').textContent=active?(automatic?'до завершения backup':'до '+new Date(maintenanceWindow.endsAt*1000).toLocaleString('ru-RU')):'';el('stopMaintenance').disabled=!active||automatic;el('maintenanceStatus').textContent=active?(automatic?'Автоматически включён на время backup выбранных VM/LXC и связанных сервисов.':'Активен до '+new Date(maintenanceWindow.endsAt*1000).toLocaleString('ru-RU')+' · '+maintenanceWindow.reason):'Режим выключен.';if(active&&!automatic){el('maintenanceReason').value=maintenanceWindow.reason||'';el('maintenanceDuration').value=Math.max(5,Math.ceil((maintenanceWindow.endsAt-Date.now()/1000)/60));el('maintenanceAll').checked=!!maintenanceWindow.allAlerts}renderMaintenanceTargets()}
function applyMaintenanceState(window){maintenanceWindow=window||{};const active=!!maintenanceWindow.active,automatic=!!maintenanceWindow.automatic;el('maintenanceBanner').hidden=!active;el('maintenanceBannerReason').textContent=(automatic?'Автоматический режим · ':'')+(maintenanceWindow.reason||'');el('maintenanceBannerEnds').textContent=active?(automatic?'до завершения backup':'до '+new Date(maintenanceWindow.endsAt*1000).toLocaleString('ru-RU')):'';el('stopMaintenance').disabled=!active||automatic;el('maintenanceStatus').textContent=active?(automatic?'Автоматически включён на время backup выбранных VM/LXC и связанных сервисов.':'Активен до '+new Date(maintenanceWindow.endsAt*1000).toLocaleString('ru-RU')+' · '+maintenanceWindow.reason):'Режим выключен.';if(active&&!automatic){el('maintenanceReason').value=maintenanceWindow.reason||'';el('maintenanceDuration').value=Math.max(5,Math.ceil((maintenanceWindow.endsAt-Date.now()/1000)/60));el('maintenanceAll').checked=!!maintenanceWindow.allAlerts}renderMaintenanceTargets()}
function renderNetworkGroups(interfaces){const groups=new Map();interfaces.forEach(v=>{const key=v.vmid?'guest-'+v.vmid:'host',group=groups.get(key)||{vmid:v.vmid,name:v.vmid?(v.guestName||('VMID '+v.vmid)):'Интерфейсы хоста',type:v.guestType||'Host',items:[]};group.items.push(v);groups.set(key,group)});el('networkInterfacesList').innerHTML=[...groups.values()].map(g=>{const primary=g.vmid?g.items.filter(v=>v.name.startsWith('tap')||v.name.startsWith('veth')):g.items,rx=primary.reduce((a,v)=>a+v.receivePerSec,0),tx=primary.reduce((a,v)=>a+v.transmitPerSec,0),total=g.items.reduce((a,v)=>a+v.receiveErrors+v.transmitErrors+v.receiveDrops+v.transmitDrops,0),fresh=g.items.reduce((a,v)=>a+(v.newErrors||0)+(v.newDrops||0),0),down=primary.some(v=>v.kind==='Physical'&&!v.carrier&&(v.master||v.receiveBytes+v.transmitBytes>0));return '<article class="disk-item"><div class="disk-top"><div><div class="disk-model">'+safe(g.name)+'</div><div class="disk-meta">'+(g.vmid?'VMID '+g.vmid+' · ':'')+safe(g.type)+' · '+safe(g.items.map(v=>v.name+' ['+(v.kind||'Interface')+(v.speedMbps?' · '+v.speedMbps+'Mb/s':'')+(v.duplex?' · '+v.duplex:'')+(v.master?' → '+v.master:'')+']').join(', '))+'</div></div><span class="badge '+(fresh||down?'warn':'')+'"'+(fresh||down?' tabindex="0" data-reason="'+safe(down?'Нет carrier на используемом физическом интерфейсе':'За последний цикл счётчики выросли на '+fresh+'. Общий счётчик '+total+' включает старые события и сам по себе не означает текущую проблему.')+'"':'')+'>'+(down?'Нет линка':fresh?'Новые потери: '+fresh:'Сейчас без потерь')+'</span></div><div class="disk-stats"><div class="disk-stat"><span>Сейчас ↓</span><b>'+rate(rx)+'</b></div><div class="disk-stat"><span>Сейчас ↑</span><b>'+rate(tx)+'</b></div><div class="disk-stat"><span>Получено</span><b>'+size(primary.reduce((a,v)=>a+v.receiveBytes,0))+'</b></div><div class="disk-stat"><span>Передано</span><b>'+size(primary.reduce((a,v)=>a+v.transmitBytes,0))+'</b></div><div class="disk-stat"><span>Новые / всего errors+drops</span><b>'+fresh+' / '+total+'</b></div></div></article>'}).join('')}
return '<article class="disk-item"><div class="disk-top"><div><div class="disk-model">'+safe(g.name)+'</div><div class="disk-meta">'+(g.vmid?'VMID '+g.vmid+' · ':'')+safe(g.type)+' · '+safe(g.items.map(v=>v.name+' ['+(v.kind||'Interface')+(v.speedMbps?' · '+v.speedMbps+'Mb/s':'')+(v.duplex?' · '+v.duplex:'')+(v.master?' → '+v.master:'')+']').join(', '))+'</div></div><span class="badge '+(fresh||down?'bad':'')+'">'+(down?'Нет линка':fresh?'Новые ошибки':'Нет новых ошибок')+'</span></div><div class="disk-stats"><div class="disk-stat"><span>Сейчас ↓</span><b>'+rate(rx)+'</b></div><div class="disk-stat"><span>Сейчас ↑</span><b>'+rate(tx)+'</b></div><div class="disk-stat"><span>Получено</span><b>'+size(primary.reduce((a,v)=>a+v.receiveBytes,0))+'</b></div><div class="disk-stat"><span>Передано</span><b>'+size(primary.reduce((a,v)=>a+v.transmitBytes,0))+'</b></div></div>'+counters+'<div class="disk-meta">RX/TX drops — информационные счётчики, сами по себе не означают сетевую аварию.'+(g.vmid?' Состояние гостя определяется по tap/veth.':' fwbr/fwpr/fwln относятся к инфраструктуре хоста.')+'</div></article>'
}).join('')
}
function renderIOAttribution(disks){const active=disks.filter(d=>d.ioCause||(d.topConsumers||[]).length);el('ioAttributionList').innerHTML=active.length?active.map(d=>'<article class="disk-item"><div class="disk-top"><div><div class="disk-model">'+safe(d.name)+' · '+safe(d.ioCause||'Источник не определён')+'</div><div class="disk-meta">'+safe(d.ioCauseDetail||'Собираем статистику процессов')+'</div></div><span class="badge '+(d.ioConfidence>=80?'':'warn')+'" tabindex="0" data-reason="Уверенность учитывает совпадение активной задачи Proxmox и долю I/O процессов">'+(d.ioConfidence||0)+'%</span></div><div class="topology-items">'+(d.topConsumers||[]).slice(0,5).map(v=>'<span class="topology-chip">'+safe(v.kind)+' · '+safe(v.name)+' · '+fixed(v.sharePercent)+'% · ↓'+rate(v.readBytesPerSec)+' ↑'+rate(v.writeBytesPerSec)+'</span>').join('')+'</div></article>').join(''):'<span class="empty">Для атрибуции нужно несколько realtime-циклов.</span>'}
function renderIOAttribution(disks){const active=disks.filter(d=>d.ioCause||(d.topConsumers||[]).length);el('ioAttributionList').innerHTML=active.length?active.map(d=>'<article class="disk-item"><div class="disk-top"><div><div class="disk-model">'+safe(d.name)+' · '+safe(d.ioCause||'Источник не определён')+'</div><div class="disk-meta">'+safe(d.ioCauseDetail||'Собираем статистику процессов')+'</div></div><span class="badge '+(d.ioConfidence>=80?'':'warn')+'" tabindex="0" data-reason="Уверенность учитывает совпадение активной задачи Proxmox и долю I/O процессов">'+(d.ioConfidence||0)+'%</span></div><div class="topology-items">'+(d.topConsumers||[]).slice(0,5).map(v=>'<span class="topology-chip">'+safe(v.kind)+' · '+safe(v.name)+' · '+fixed(v.sharePercent)+'% · ↓'+rate(v.readBytesPerSec)+' ↑'+rate(v.writeBytesPerSec)+'</span>').join('')+'</div></article>').join(''):'<span class="empty">Для атрибуции нужно несколько realtime-циклов.</span>'}
function explainWarningBadges(){el('snapshotList').querySelectorAll('.badge.warn').forEach(v=>{v.tabIndex=0;v.dataset.reason='Snapshot старше 30 дней. Проверьте, нужен ли он: старые снимки могут занимать место в storage.'});el('taskList').querySelectorAll('.badge.bad').forEach(v=>{v.tabIndex=0;v.dataset.reason='Задача Proxmox завершилась не со статусом OK. Откройте её журнал в интерфейсе Proxmox для подробностей.'})}
function explainWarningBadges(){el('snapshotList').querySelectorAll('.badge.warn').forEach(v=>{v.tabIndex=0;v.dataset.reason='Snapshot старше 30 дней. Проверьте, нужен ли он: старые снимки могут занимать место в storage.'});el('taskList').querySelectorAll('.badge.bad').forEach(v=>{v.tabIndex=0;v.dataset.reason='Задача Proxmox завершилась не со статусом OK. Откройте её журнал в интерфейсе Proxmox для подробностей.'})}
@@ -939,7 +946,7 @@ async function queueAgentAction(action,target){const labels={'system-update':'у
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 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'}}
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='<div class="chart-tooltip-time">'+new Date(point.timestamp*1000).toLocaleString('ru-RU')+'</div>'+state.series.map(s=>'<div class="chart-tooltip-value"><span><i style="background:'+s.color+'"></i>'+s.label+'</span><strong>'+(s.value(point)==null?'—':s.format(s.value(point)))+'</strong></div>').join('');tooltip.classList.add('visible');tooltip.style.left=Math.min(x+12,canvas.clientWidth-tooltip.offsetWidth-10)+'px';tooltip.style.top='55px'});canvas.addEventListener('mouseleave',()=>el('agentHistoryDetails').querySelector('.chart-tooltip').classList.remove('visible'))}
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='<div class="chart-tooltip-time">'+new Date(point.timestamp*1000).toLocaleString('ru-RU')+'</div>'+state.series.map(s=>'<div class="chart-tooltip-value"><span><i style="background:'+s.color+'"></i>'+s.label+'</span><strong>'+(s.value(point)==null?'—':s.format(s.value(point)))+'</strong></div>').join('');tooltip.classList.add('visible');tooltip.style.left=Math.min(x+12,canvas.clientWidth-tooltip.offsetWidth-10)+'px';tooltip.style.top='55px'});canvas.addEventListener('mouseleave',()=>el('agentHistoryDetails').querySelector('.chart-tooltip').classList.remove('visible'))}
function render(x){const c=x.cpu,m=x.memory,s=x.storage,n=x.network,z=x.zfs||{available:false,pools:[]},u=x.ups||{available:false},g=x.guests||{available:false,guests:[]},b=x.backups||{available:false,items:[]},mt=x.maintenance||{},alerts=x.alerts||[],disks=x.disks||[],avg=c.temperatures.length?c.temperatures.reduce((a,t)=>a+t.celsius,0)/c.temperatures.length:null;
function render(x){const c=x.cpu,m=x.memory,s=x.storage,n=x.network,z=x.zfs||{available:false,pools:[]},u=x.ups||{available:false},g=x.guests||{available:false,guests:[]},b=x.backups||{available:false,items:[]},mt=x.maintenance||{},alerts=x.alerts||[],disks=x.disks||[],avg=c.temperatureCelsius??null;
@@ -947,7 +954,7 @@ function render(x){const c=x.cpu,m=x.memory,s=x.storage,n=x.network,z=x.zfs||{av
const update=x.update||{},updating=['queued','installing'].includes(update.state);el('updateCurrent').textContent=update.currentVersion||'—';el('updateAvailable').textContent=update.availableVersion||'—';el('updateMode').textContent=update.mode==='auto'?'Автоматически':'Уведомлять';el('updateMessage').textContent=update.message||'Проверка ещё не выполнялась';el('updateChecked').textContent=update.checkedAt?'Последняя проверка: '+new Date(update.checkedAt).toLocaleString('ru-RU'):'Проверка запускается автоматически раз в сутки.';el('installUpdate').disabled=updating;el('installUpdate').textContent=updating?'Обновление выполняется…':'Проверить и установить обновление';
const update=x.update||{},updating=['queued','installing'].includes(update.state);el('updateCurrent').textContent=update.currentVersion||'—';el('updateAvailable').textContent=update.availableVersion||'—';el('updateMode').textContent=update.mode==='auto'?'Автоматически':'Уведомлять';el('updateMessage').textContent=update.message||'Проверка ещё не выполнялась';el('updateChecked').textContent=update.checkedAt?'Последняя проверка: '+new Date(update.checkedAt).toLocaleString('ru-RU'):'Проверка запускается автоматически раз в сутки.';el('installUpdate').disabled=updating;el('installUpdate').textContent=updating?'Обновление выполняется…':'Проверить и установить обновление';
const critical=alerts.filter(a=>a.severity==='critical').length,warnings=alerts.filter(a=>a.severity==='warning').length,infos=alerts.filter(a=>a.severity==='info').length;el('alertCount').textContent=alerts.length;el('alertCritical').textContent=critical;el('alertWarnings').textContent=warnings;el('alertHeadline').textContent=critical?'Требуется срочное внимание':warnings?'Есть предупреждения':infos?'Есть информационные события':'Система в норме';el('alertBar').style.width=(critical?100:warnings?65:infos?30:0)+'%';
const critical=alerts.filter(a=>a.severity==='critical').length,warnings=alerts.filter(a=>a.severity==='warning').length,infos=alerts.filter(a=>a.severity==='info').length;el('alertCount').textContent=alerts.length;el('alertCritical').textContent=critical;el('alertWarnings').textContent=warnings;el('alertHeadline').textContent=critical?'Требуется срочное внимание':warnings?'Есть предупреждения':infos?'Есть информационные события':'Система в норме';el('alertBar').style.width=(critical?100:warnings?65:infos?30:0)+'%';
el('alertList').innerHTML=alerts.length?alerts.map(a=>'<article class="alert-item '+safe(a.severity)+'"><i class="alert-icon"></i><div><div class="alert-source">'+safe(a.source)+' · '+(a.severity==='critical'?'Критическое':a.severity==='warning'?'Предупреждение':'Информация')+(a.firstSeen?' · С '+new Date(a.firstSeen*1000).toLocaleString('ru-RU'):'')+'</div><div class="alert-title">'+safe(a.title)+'</div><div class="alert-message">'+safe(a.message)+'</div><button class="ack-alert" data-ack="'+a.eventId+'">Принято</button></div></article>').join(''):'<div class="all-good"><strong>Всё в порядке</strong><span>Новых неподтверждённых событий нет.</span></div>';
el('alertList').innerHTML=alerts.length?alerts.map(a=>'<article class="alert-item '+safe(a.severity)+'"><i class="alert-icon"></i><div><div class="alert-source">'+safe(a.source)+' · '+(a.severity==='critical'?'Критическое':a.severity==='warning'?'Предупреждение':'Информация')+(a.firstSeen?' · С '+new Date(a.firstSeen*1000).toLocaleString('ru-RU'):'')+'</div><div class="alert-title">'+safe(a.title)+'</div><div class="alert-message">'+safe(a.message)+'</div><button class="ack-alert" data-ack="'+a.eventId+'">Принято</button></div></article>').join(''):'<div class="all-good"><strong>Всё в порядке</strong><span>Новых неподтверждённых событий нет.</span></div>';
el('cpuName').textContent=el('cpuDetailName').textContent=c.model;el('cpuUsage').textContent=fixed(c.usagePercent);el('cpuBar').style.width=Math.min(c.usagePercent,100)+'%';el('cpuTemp').title='Максимум датчиков CPU; тот же показатель записывается в историю';el('cpuTemp').textContent=avg===null?'Нет датчика':fixed(avg)+' °C';el('cpuTopology').textContent=c.physicalCores+' / '+c.logicalCpus;
el('dCpuUsage').textContent=fixed(c.usagePercent)+'%';el('dSockets').textContent=c.sockets;el('dCores').textContent=c.physicalCores;el('dThreads').textContent=c.logicalCpus;el('dFreq').textContent=(c.frequencyMhz/1000).toFixed(2)+' GHz';el('dUptime').textContent=uptime(c.uptimeSeconds);el('dLoad1').textContent=fixed(c.load1);el('dLoad5').textContent=fixed(c.load5);el('dLoad15').textContent=fixed(c.load15);el('dTemps').innerHTML=c.temperatures.length?c.temperatures.map(t=>'<span class="temp">'+safe(t.label)+'<b>'+fixed(t.celsius)+'°C</b></span>').join(''):'<span class="empty">Датчики температуры не найдены</span>';const cpuProcesses=(c.topProcesses||[]).slice().sort((a,b)=>b.cpuPercent-a.cpuPercent).slice(0,12);el('cpuProcessList').innerHTML=cpuProcesses.length?cpuProcesses.map(v=>'<div class="process-row"><div><strong>'+safe(v.name)+'</strong><small>PID '+v.pid+' · '+safe(v.command)+'</small></div><span>CPU '+processCPU(v)+'</span><span>RAM '+size(v.memoryBytes)+'</span><span>Read '+size(v.readBytes)+'</span><span>Write '+size(v.writeBytes)+'</span></div>').join(''):'<span class="empty">Процессы не получены.</span>';
el('dCpuUsage').textContent=fixed(c.usagePercent)+'%';el('dSockets').textContent=c.sockets;el('dCores').textContent=c.physicalCores;el('dThreads').textContent=c.logicalCpus;el('dFreq').textContent=(c.frequencyMhz/1000).toFixed(2)+' GHz';el('dUptime').textContent=uptime(c.uptimeSeconds);el('dLoad1').textContent=fixed(c.load1);el('dLoad5').textContent=fixed(c.load5);el('dLoad15').textContent=fixed(c.load15);el('dTemps').innerHTML=c.temperatures.length?c.temperatures.map(t=>'<span class="temp">'+safe(t.label)+'<b>'+fixed(t.celsius)+'°C</b></span>').join(''):'<span class="empty">Датчики температуры не найдены</span>';const cpuProcesses=(c.topProcesses||[]).slice().sort((a,b)=>b.cpuPercent-a.cpuPercent).slice(0,12);el('cpuProcessList').innerHTML=cpuProcesses.length?cpuProcesses.map(v=>'<div class="process-row"><div><strong>'+safe(v.name)+'</strong><small>PID '+v.pid+' · '+safe(v.command)+'</small></div><span>CPU '+processCPU(v)+'</span><span>RAM '+size(v.memoryBytes)+'</span><span>Read '+size(v.readBytes)+'</span><span>Write '+size(v.writeBytes)+'</span></div>').join(''):'<span class="empty">Процессы не получены.</span>';
const chartConfigs={cpuChart:{floorMax:100,series:[{label:'CPU',color:'#55d58a',value:p=>p.cpuUsage,format:percent},{label:'Макс. °C CPU',color:'#ff9b5c',value:p=>p.cpuTemperature,format:temperature}]},memoryChart:{series:[{label:'RAM',color:'#9d8cff',value:p=>p.memoryUsage,format:percent},{label:'Swap',color:'#ff78b8',value:p=>p.swapUsage,format:percent}]},storageChart:{series:[{label:'Раздел /',color:'#ff9b5c',value:p=>p.rootUsage,format:percent}]},networkChart:{max:0,series:[{label:'Приём',color:'#43c6d9',value:p=>p.networkReceive/1048576,format:megabytes},{label:'Передача',color:'#65ddaa',value:p=>p.networkTransmit/1048576,format:megabytes}]},zfsChart:{series:[{label:'ZFS',color:'#6577ff',value:p=>p.zfsUsage,format:percent}]},upsChart:{series:[{label:'Заряд',color:'#55d58a',value:p=>p.upsCharge,format:percent},{label:'Нагрузка',color:'#d5e85a',value:p=>p.upsLoad,format:percent}]},guestsChart:{series:[{label:'CPU',color:'#ff78b8',value:p=>p.guestsCpu,format:percent},{label:'RAM',color:'#9d8cff',value:p=>p.guestsMemory,format:percent}]}};
const historyCache=new Map();
const historyCache=new Map();
async function loadHistory(hours){const cached=historyCache.get(hours);if(cached&&Date.now()-cached.loadedAt<60000)return cached.points;const response=await fetch('/api/history?hours='+hours);if(!response.ok)throw new Error('history');const points=await response.json()||[];historyCache.set(hours,{points,loadedAt:Date.now()});return points}
async function loadHistory(hours){const cached=historyCache.get(hours);if(cached&&Date.now()-cached.loadedAt<60000)return cached.points;const response=await fetch('/api/history?hours='+hours);if(!response.ok)throw new Error('history');const points=await response.json()||[];historyCache.set(hours,{points,loadedAt:Date.now()});return points}
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.