diff --git a/README.md b/README.md
index 5eb271d..ea2701a 100644
--- a/README.md
+++ b/README.md
@@ -708,3 +708,10 @@ HTTP/CrowdSec-импульсы также начинаются в городе,
напряжение, напряжение батареи, автономность и частота. Запись раз в минуту,
новые ряды заполняются после обновления; при потере связи сохраняются пропуски.
Пороги UPS/NUT показаны отдельно от сценария выключения Dashboard.
+
+## Компактная карточка UPS (v0.27.1)
+
+Окно UPS разделено на вкладки «Обзор», «Батарея», «Диагностика», «Графики»
+и «Исходные данные». В обзоре — компактные карточки основных показателей.
+На графиках выбирается один показатель и период; технические пояснения
+коррекции частоты и расчёта мощности находятся в исходных данных.
diff --git a/web.go b/web.go
index 71e71a3..9c7f09e 100644
--- a/web.go
+++ b/web.go
@@ -585,6 +585,9 @@ footer{color:#657581;font-size:12px;margin-top:17px}
dialog,#diagnosticsDialog{width:100%;max-width:none;height:100dvh;max-height:100dvh;margin:0;border:0;border-radius:0}.modal-head{padding:19px 17px}.modal-head>div{min-width:0}.modal-head h2{font-size:22px;overflow-wrap:anywhere}.close{flex:0 0 auto;width:42px;height:42px}.settings-tabs,.detail-tabs,.system-tabs,.task-filter{padding-left:14px;padding-right:14px;scrollbar-width:none}.settings-tabs::-webkit-scrollbar,.detail-tabs::-webkit-scrollbar,.system-tabs::-webkit-scrollbar,.task-filter::-webkit-scrollbar{display:none}.settings-tab,.detail-tab{flex:0 0 auto}.settings-body,.wide,.chart-section{padding:18px 16px}.detail-grid{grid-template-columns:1fr 1fr}.detail{min-width:0;padding:17px 16px}.detail strong{font-size:18px;overflow-wrap:anywhere}.disk-list,.agent-detail-panel{padding:12px}.disk-item{min-width:0;padding:15px}.disk-top,.endpoint-head,.topology-group-head{align-items:flex-start;gap:10px}.disk-model{overflow-wrap:anywhere}.badge[data-reason]:hover::after,.badge[data-reason]:focus::after{position:fixed;left:14px;right:14px;bottom:18px;width:auto}.disk-stats{grid-template-columns:1fr 1fr;gap:16px 10px}.setting-list,.threshold-grid,.theme-options,.maintenance-targets,.service-form{grid-template-columns:1fr}.threshold-item{align-items:flex-start;flex-direction:column}.threshold-item input,[data-settings-panel="email"] .threshold-item input{width:100%}.chart-head{align-items:stretch}.range-switch{max-width:100%;overflow-x:auto}.range-button{flex:1 0 auto}.metric-chart{height:180px}.infra-status,.agent-detail-summary{grid-template-columns:1fr 1fr}.map-board,.topology-board{min-width:0;padding:14px}.map-board{grid-template-columns:1fr}.topology-grid{grid-template-columns:1fr}.diagnostic-chain{display:grid;grid-template-columns:1fr;overflow:visible}.diagnostic-node{min-width:0}.diagnostic-arrow{transform:rotate(90deg)}.process-row{grid-template-columns:1fr 1fr}.maintenance-banner{align-items:flex-start;flex-direction:column}.service-actions>*{flex:1 1 auto;text-align:center}
}
@media(max-width:420px){main{padding-left:10px;padding-right:10px}h1{font-size:32px}.eyebrow{font-size:10px}.card-head{padding-left:15px;padding-right:15px}.percent{font-size:27px}.quick div{padding-left:15px;padding-right:15px}.quick strong{font-size:15px}.detail-grid,.infra-status,.agent-detail-summary{grid-template-columns:1fr}.detail{border-right:0!important}.disk-top{flex-direction:column}.disk-stats{grid-template-columns:1fr 1fr}.modal-head h2{font-size:20px}.process-row{grid-template-columns:1fr}.alert-list{padding:10px}.alert-item{padding:14px 12px}}
+
+#upsDialog{width:min(860px,calc(100% - 28px));height:min(660px,calc(100dvh - 32px));overflow:hidden}#upsDialog[open]{display:flex;flex-direction:column}#upsDialog .modal-head{padding:22px 24px;flex-shrink:0}#upsDialog .modal-head h2{font-size:25px}.ups-tabs{display:flex;gap:4px;padding:10px 20px;border-bottom:1px solid var(--line);flex-shrink:0;overflow-x:auto}.ups-tabs button{font:500 13px system-ui;white-space:nowrap;padding:9px 12px;border:1px solid transparent;border-radius:9px;color:var(--muted);background:transparent;cursor:pointer}.ups-tabs button[aria-selected="true"]{background:var(--panel);border-color:var(--line);color:var(--text)}.ups-tabs button:focus-visible{outline:2px solid var(--green);outline-offset:-2px}.ups-content{padding:20px 24px;overflow:auto;min-height:0;flex:1}.ups-content [hidden]{display:none!important}.ups-overview-status{display:flex;align-items:center;gap:9px;margin:0 0 18px;font-size:14px;flex-wrap:wrap}.ups-overview-status>span:last-child{margin-left:auto;color:var(--muted);font-size:12px}.ups-status-dot{width:7px;height:7px;border-radius:50%;background:var(--muted)}.ups-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}#upsDialog .detail{min-width:0;padding:15px 16px;border:1px solid var(--line);border-radius:12px;background:rgba(120,150,170,.035)}#upsDialog .detail .label{font:500 11px/1.4 system-ui;letter-spacing:.02em;text-transform:none;color:var(--muted)}#upsDialog .detail strong{font:600 19px/1.35 system-ui;margin-top:8px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}#upsPanel-overview .detail:nth-child(-n+3){background:linear-gradient(135deg,rgba(85,213,138,.09),rgba(85,213,138,.015))}#upsPanel-overview .detail:nth-child(-n+3) strong{font-size:24px}#upsDialog #dUpsDriver{font-size:14px}.ups-footnote{font-size:11px;line-height:1.6;color:var(--muted);margin:15px 0 0}.ups-technical{font-size:12px;line-height:1.6;color:var(--muted)}.ups-technical p{margin:0 0 8px}.ups-technical p:empty{display:none}.ups-raw{white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.7 ui-monospace,monospace;padding:16px;border:1px solid var(--line);border-radius:12px;background:rgba(120,150,170,.04)}#upsDialog .ups-chart-section{padding:0}.ups-chart-select{display:flex;flex-direction:column;gap:7px;font-size:11px;color:var(--muted)}.ups-chart-select select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--text);font:500 13px system-ui}#upsDialog .chart-head{align-items:flex-end;margin-bottom:24px}#upsDialog .metric-chart{height:200px}
+@media(max-width:600px){#upsDialog .modal-head{padding:17px}.ups-content{padding:14px}.ups-tabs{padding:8px 10px}.ups-tabs button{padding:8px;font-size:12px}.ups-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}#upsDialog .detail{padding:12px}#upsDialog .detail strong,#upsPanel-overview .detail:nth-child(-n+3) strong{font-size:18px}.ups-overview-status>span:last-child{margin-left:0;width:100%}#upsDialog .chart-head{align-items:stretch;gap:12px}.ups-chart-select select{width:100%}.ups-tabs{scrollbar-width:thin}}
@@ -753,7 +756,7 @@ footer{color:#657581;font-size:12px;margin-top:17px}
Заполнение каждого ZFS-пулаОтдельно по каждому пулу
-
+
@@ -969,13 +972,13 @@ function render(x){el('trafficLink').hidden=!x.trafficSettings?.enabled;const c=
const totalRate=n.receivePerSec+n.transmitPerSec,nh=n.health||{};el('netInterfaces').textContent=n.interface||'Сетевые интерфейсы';el('netTotal').textContent=(totalRate/1048576).toFixed(2);el('netBar').style.width=Math.min(totalRate/125000000*100,100)+'%';el('netDown').textContent=rate(n.receivePerSec);el('netUp').textContent=rate(n.transmitPerSec);el('dNetDown').textContent=rate(n.receivePerSec);el('dNetUp').textContent=rate(n.transmitPerSec);el('dNetTotal').textContent=rate(totalRate);el('dNetReceived').textContent=size(n.receiveBytes);el('dNetSent').textContent=size(n.transmitBytes);el('dNetInterfaces').textContent=n.interface||'—';el('dNetGateway').textContent=(nh.gateway||'Не определён')+' · '+(nh.gatewayUp?'доступен':'нет ответа')+(nh.latencyMs?' · '+fixed(nh.latencyMs)+' ms':'')+(nh.packetLoss?' · потери '+fixed(nh.packetLoss)+'%':'');el('dNetDNS').textContent=nh.dnsUp?'Работает':'Ошибка';el('dNetInternet').textContent=nh.internetUp?'Доступен':'Недоступен';el('dNetExternalIP').textContent=(nh.externalIp||'—')+(nh.externalIpChanged?' · изменился':'');el('dNetSummary').textContent=nh.summary||'Диагностика ещё не завершена';el('dNetCause').textContent=nh.likelyCause||'—';el('dNetAction').textContent=nh.recommendedAction||'—';
const pools=z.pools||[],zTotal=pools.reduce((a,p)=>a+p.sizeBytes,0),zUsed=pools.reduce((a,p)=>a+p.allocatedBytes,0),zPercent=zTotal?zUsed/zTotal*100:0,zBad=pools.filter(p=>p.health!=='ONLINE').length;el('zfsCount').textContent=pools.length;el('zfsHealth').textContent=!z.available?'Не найден':zBad?zBad+' требуют внимания':pools.length+' ONLINE';el('zfsUsed').textContent=zTotal?size(zUsed)+' / '+size(zTotal)+' · '+fixed(zPercent)+'%':'—';el('zfsBar').style.width=Math.min(zPercent,100)+'%';
const datasets=z.datasets||[],arc=z.arc||{};el('zfsArcSize').textContent=arc.available?size(arc.sizeBytes):'—';el('zfsArcTarget').textContent=arc.available?size(arc.targetBytes):'—';el('zfsArcHit').textContent=arc.available?fixed(arc.hitRatio)+'%':'—';el('zfsDatasetCount').textContent=datasets.length;el('zfsList').innerHTML=pools.length?pools.map(p=>{const bad=p.health!=='ONLINE'||p.readErrors+p.writeErrors+p.checksumErrors>0,reason=(p.attentionReasons||[]).join(' · '),tip=reason?' tabindex="0" data-reason="'+safe(reason)+'"':'',poolDatasets=datasets.filter(d=>d.pool===p.name),vdevs=p.vdevs||[];return ''+safe(p.name)+'
Scrub: '+safe(p.scrubStatus||p.scan||'нет данных')+(p.lastScrubAt?' · '+new Date(p.lastScrubAt).toLocaleString('ru-RU'):'')+(p.scrubDuration?' · '+safe(p.scrubDuration):'')+'
'+safe(p.health)+'Объём / свободно'+size(p.sizeBytes)+' / '+size(p.freeBytes)+'
Заполнение'+fixed(p.capacityPercent)+'%
Read / write / checksum'+p.readErrors+' / '+p.writeErrors+' / '+p.checksumErrors+'
Vdev · data / special / cache / log
'+(vdevs.length?vdevs.map(v=>''+safe(v.class)+' · '+safe(v.name)+' · '+safe(v.state)+' · '+v.read+'/'+v.write+'/'+v.checksum+'').join(''):'Нет данных')+'
Datasets
'+(poolDatasets.length?poolDatasets.map(d=>''+safe(d.name)+' · '+fixed(d.capacityPercent)+'% · '+size(d.usedBytes)+'').join(''):'Нет datasets')+'
'}).join(''):''+(z.available?'ZFS-пулы не найдены':'Команда zpool недоступна')+'';
- const upsDuration=v=>v==null?'—':Math.floor(v/60)+' мин '+Math.floor(v%60)+' сек';const val=(v,suffix='')=>v==null?'—':fixed(v)+suffix;el('upsName').textContent=u.available?(u.model||u.name||'Источник бесперебойного питания'):'UPS не настроен';el('upsCharge').textContent=u.chargePercent==null?'—':fixed(u.chargePercent);el('upsBar').style.width=u.chargePercent==null?'0%':Math.min(u.chargePercent,100)+'%';el('upsStatus').textContent=u.available?u.statusLabel:(u.source?'Нет связи':'Не найден');el('upsRuntime').textContent=u.runtimeSeconds==null?'—':upsDuration(u.runtimeSeconds);el('dUpsName').textContent=u.model||u.name||'Источник бесперебойного питания';el('dUpsStatus').textContent=u.available?u.statusLabel:(u.source?'Нет связи · последние данные':'Не настроен');el('dUpsCharge').textContent=val(u.chargePercent,'%');el('dUpsRuntime').textContent=u.runtimeSeconds==null?'—':upsDuration(u.runtimeSeconds);el('dUpsLoad').textContent=val(u.loadPercent,'%');el('dUpsInput').textContent=val(u.inputVoltage,' V');el('dUpsOutput').textContent=val(u.outputVoltage,' V');el('dUpsFrequency').textContent=val(u.inputFrequency,' Hz');el('dUpsBattery').textContent=val(u.batteryVoltage,' V');el('dUpsTemp').textContent=val(u.temperatureCelsius,' °C');el('dUpsCommunication').textContent=u.communicationOk?'Связь установлена':'Нет связи';el('dUpsBatteryDate').textContent=u.batteryDate||'Не сообщает';el('dUpsBatteryAge').textContent=u.batteryAgeDays?u.batteryAgeDays+' дней':'—';el('dUpsSource').textContent=u.available?(u.source+' · '+(u.name||'UPS')):'Установите и настройте NUT или apcupsd';
+ const upsDuration=v=>v==null?'—':Math.floor(v/60)+' мин '+Math.floor(v%60)+' сек';const val=(v,suffix='')=>v==null?'—':fixed(v)+suffix;el('upsName').textContent=u.available?(u.model||u.name||'Источник бесперебойного питания'):'UPS не настроен';el('upsCharge').textContent=u.chargePercent==null?'—':fixed(u.chargePercent);el('upsBar').style.width=u.chargePercent==null?'0%':Math.min(u.chargePercent,100)+'%';el('upsStatus').textContent=u.available?u.statusLabel:(u.source?'Нет связи':'Не найден');el('upsRuntime').textContent=u.runtimeSeconds==null?'—':upsDuration(u.runtimeSeconds);el('dUpsName').textContent=u.model||u.name||'Источник бесперебойного питания';el('dUpsStatus').textContent=u.available?u.statusLabel:(u.source?'Нет связи · последние данные':'Не настроен');el('dUpsCharge').textContent=val(u.chargePercent,'%');el('dUpsRuntime').textContent=u.runtimeSeconds==null?'—':upsDuration(u.runtimeSeconds);el('dUpsLoad').textContent=val(u.loadPercent,'%');el('dUpsInput').textContent=val(u.inputVoltage,' В');el('dUpsOutput').textContent=val(u.outputVoltage,' В');el('dUpsFrequency').textContent=val(u.inputFrequency,' Hz');el('dUpsBattery').textContent=val(u.batteryVoltage,' В');el('dUpsTemp').textContent=val(u.temperatureCelsius,' °C');el('dUpsCommunication').textContent=u.communicationOk?'Связь установлена':'Нет связи';el('dUpsBatteryDate').textContent=u.batteryDate||'Не сообщает';el('dUpsBatteryAge').textContent=u.batteryAgeDays?u.batteryAgeDays+' дней':'—';el('dUpsSource').textContent=u.available?(u.source+' · '+(u.name||'UPS')):'Установите и настройте NUT или apcupsd';
const raw=u.raw||{},rawVal=(key,suffix='')=>raw[key]?raw[key]+suffix:'Не сообщает';
el('upsPowerSummary').textContent=u.powerWatts==null?'Нажмите, чтобы посмотреть подробности →':(u.powerEstimated?'≈ ':'')+fixed(u.powerWatts)+' Вт · нагрузка '+val(u.loadPercent,'%')+' · подробнее →';
el('dUpsPower').textContent=(u.powerEstimated&&u.powerWatts!=null?'≈ ':'')+val(u.powerWatts,' Вт');el('dUpsNominalPower').textContent=val(u.nominalPowerWatts,' Вт');
el('dUpsPowerNote').textContent=u.powerEstimated?'Оценка: номинальная активная мощность × нагрузка / 100. Показания в ваттах UPS не передаёт.':'Мощность измерена UPS, если доступна.';
el('dUpsFrequencyNote').textContent=u.frequencyCorrected?'Коррекция масштаба CyberPower: '+u.inputFrequencyRaw+' → '+fixed(u.inputFrequency,1)+' Гц (÷10). Исходное значение сохранено ниже.':'';
- el('dUpsFrequency').textContent=u.inputFrequency==null?'—':fixed(u.inputFrequency,1)+' Гц'+(u.frequencyCorrected?' · исправлено':'');
+ el('dUpsFrequency').textContent=u.inputFrequency==null?'—':fixed(u.inputFrequency,1)+' Гц';
el('dUpsInputNominal').textContent=rawVal('input.voltage.nominal',' В');el('dUpsBatteryNominal').textContent=rawVal('battery.voltage.nominal',' В');el('dUpsBatteryType').textContent=raw['battery.type']==='PbAcid'?'Свинцово-кислотная (PbAcid)':rawVal('battery.type');
el('dUpsTransfer').textContent=raw['input.transfer.low']&&raw['input.transfer.high']?raw['input.transfer.low']+'–'+raw['input.transfer.high']+' В':'Не сообщает';
el('dUpsChargeWarning').textContent=rawVal('battery.charge.warning','%');el('dUpsChargeLow').textContent=val(u.lowBatteryCharge,'%');el('dUpsRuntimeLow').textContent=u.lowBatteryRuntime==null?'—':upsDuration(u.lowBatteryRuntime);
@@ -1006,9 +1009,15 @@ async function loadEntityChart(id,hours){const definition=entityChartKinds[id],r
function drawChart(id,points){const canvas=el(id),config=chartConfigs[id];if(!canvas||!config||!canvas.offsetWidth)return;const series=config.series,dpr=window.devicePixelRatio||1,w=canvas.offsetWidth,h=200;canvas.width=w*dpr;canvas.height=h*dpr;const ctx=canvas.getContext('2d');ctx.scale(dpr,dpr);const styles=getComputedStyle(document.documentElement),text=styles.getPropertyValue('--muted').trim(),line=styles.getPropertyValue('--line').trim();ctx.clearRect(0,0,w,h);ctx.font='11px system-ui';ctx.fillStyle=text;if(points.length<2){ctx.textAlign='center';ctx.fillText('История появится после нескольких минут работы',w/2,h/2);canvas._chartState=null;return}const values=series.flatMap(s=>points.map(p=>s.value(p)).filter(v=>v!=null&&Number.isFinite(v))),max=config.floorMax?Math.max(config.floorMax,...values)*1.05:config.max===0?Math.max(...values,1)*1.1:100,pad={l:38,r:12,t:30,b:25},pw=w-pad.l-pad.r,ph=h-pad.t-pad.b;ctx.strokeStyle=line;ctx.lineWidth=1;for(let i=0;i<=4;i++){const y=pad.t+ph*i/4;ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(w-pad.r,y);ctx.stroke();ctx.fillStyle=text;ctx.textAlign='right';ctx.fillText((max*(1-i/4)).toFixed(max<10?1:0),pad.l-7,y+4)}series.forEach((s,index)=>{ctx.strokeStyle=s.color;ctx.lineWidth=2;ctx.beginPath();let started=false;points.forEach((p,i)=>{const v=s.value(p);if(v==null||!Number.isFinite(v)){started=false;return}const x=pad.l+pw*i/(points.length-1),y=pad.t+ph*(1-v/(max||1));if(!started){ctx.moveTo(x,y);started=true}else ctx.lineTo(x,y)});ctx.stroke();ctx.fillStyle=s.color;ctx.textAlign='left';ctx.fillText(s.label,pad.l+index*130,15)});const first=new Date(points[0].timestamp*1000),last=new Date(points[points.length-1].timestamp*1000),timeOptions={hour:'2-digit',minute:'2-digit'};ctx.fillStyle=text;ctx.textAlign='left';ctx.fillText(first.toLocaleString('ru-RU',timeOptions),pad.l,h-6);ctx.textAlign='right';ctx.fillText(last.toLocaleString('ru-RU',timeOptions),w-pad.r,h-6);canvas._chartState={points,series,pad,pw,w}}
function setupChartHover(canvas){canvas.addEventListener('mousemove',event=>{const state=canvas._chartState,section=canvas.closest('.chart-section'),tooltip=section&§ion.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],date=new Date(point.timestamp*1000);tooltip.innerHTML=''+date.toLocaleString('ru-RU')+'
'+state.series.map(s=>{const value=s.value(point),shown=value==null||!Number.isFinite(value)?'—':s.format(value);return ''+s.label+''+shown+'
'}).join('');tooltip.classList.add('visible');const sectionRect=section.getBoundingClientRect(),left=Math.min(event.clientX-sectionRect.left+12,section.clientWidth-tooltip.offsetWidth-10);tooltip.style.left=Math.max(10,left)+'px';tooltip.style.top=Math.max(55,event.clientY-sectionRect.top-tooltip.offsetHeight-12)+'px'});canvas.addEventListener('mouseleave',()=>{const tooltip=canvas.closest('.chart-section').querySelector('.chart-tooltip');tooltip.classList.remove('visible')})}
async function showHistory(section){const hours=Number(section.dataset.hours||1);await Promise.all([...section.querySelectorAll('canvas')].map(async canvas=>{try{drawChart(canvas.id,entityChartKinds[canvas.id]?await loadEntityChart(canvas.id,hours):await loadHistory(hours))}catch(e){drawChart(canvas.id,[])}}))}
-function setupDetailTabs(){document.querySelectorAll('dialog').forEach(dialog=>{const chart=dialog.querySelector('.chart-section');if(!chart)return;const head=dialog.querySelector('.modal-head'),tabs=document.createElement('div'),realtime=document.createElement('div');tabs.className='detail-tabs';tabs.innerHTML='';realtime.className='detail-panel';realtime.dataset.detailPanel='realtime';while(head.nextElementSibling&&head.nextElementSibling!==chart)realtime.appendChild(head.nextElementSibling);head.after(tabs);tabs.after(realtime);chart.classList.add('detail-panel');chart.dataset.detailPanel='history';chart.dataset.hours='1';chart.hidden=true;const title=chart.querySelector('.chart-head strong'),chartHead=chart.querySelector('.chart-head');chartHead.innerHTML='';chartHead.appendChild(title);const ranges=document.createElement('div');ranges.className='range-switch';ranges.innerHTML='';chartHead.appendChild(ranges);const tooltip=document.createElement('div');tooltip.className='chart-tooltip';chart.appendChild(tooltip);chart.querySelectorAll('canvas').forEach(setupChartHover);tabs.querySelectorAll('[data-detail-tab]').forEach(tab=>tab.onclick=()=>{tabs.querySelectorAll('.detail-tab').forEach(t=>t.classList.toggle('active',t===tab));realtime.hidden=tab.dataset.detailTab!=='realtime';chart.hidden=tab.dataset.detailTab!=='history';if(!chart.hidden)setTimeout(()=>showHistory(chart),20)});ranges.querySelectorAll('[data-hours]').forEach(button=>button.onclick=()=>{chart.dataset.hours=button.dataset.hours;ranges.querySelectorAll('.range-button').forEach(b=>b.classList.toggle('active',b===button));showHistory(chart)})})}
+function setupDetailTabs(){document.querySelectorAll('dialog').forEach(dialog=>{if(dialog.id==='upsDialog')return;const chart=dialog.querySelector('.chart-section');if(!chart)return;const head=dialog.querySelector('.modal-head'),tabs=document.createElement('div'),realtime=document.createElement('div');tabs.className='detail-tabs';tabs.innerHTML='';realtime.className='detail-panel';realtime.dataset.detailPanel='realtime';while(head.nextElementSibling&&head.nextElementSibling!==chart)realtime.appendChild(head.nextElementSibling);head.after(tabs);tabs.after(realtime);chart.classList.add('detail-panel');chart.dataset.detailPanel='history';chart.dataset.hours='1';chart.hidden=true;const title=chart.querySelector('.chart-head strong'),chartHead=chart.querySelector('.chart-head');chartHead.innerHTML='';chartHead.appendChild(title);const ranges=document.createElement('div');ranges.className='range-switch';ranges.innerHTML='';chartHead.appendChild(ranges);const tooltip=document.createElement('div');tooltip.className='chart-tooltip';chart.appendChild(tooltip);chart.querySelectorAll('canvas').forEach(setupChartHover);tabs.querySelectorAll('[data-detail-tab]').forEach(tab=>tab.onclick=()=>{tabs.querySelectorAll('.detail-tab').forEach(t=>t.classList.toggle('active',t===tab));realtime.hidden=tab.dataset.detailTab!=='realtime';chart.hidden=tab.dataset.detailTab!=='history';if(!chart.hidden)setTimeout(()=>showHistory(chart),20)});ranges.querySelectorAll('[data-hours]').forEach(button=>button.onclick=()=>{chart.dataset.hours=button.dataset.hours;ranges.querySelectorAll('.range-button').forEach(b=>b.classList.toggle('active',b===button));showHistory(chart)})})}
function setupCPUTabs(){document.querySelectorAll('[data-cpu-tab]').forEach(tab=>tab.onclick=()=>{const dialog=tab.closest('dialog');dialog.querySelectorAll('[data-cpu-tab]').forEach(v=>v.classList.toggle('active',v===tab));dialog.querySelectorAll('[data-cpu-panel]').forEach(v=>v.hidden=v.dataset.cpuPanel!==tab.dataset.cpuTab)})}
setupDetailTabs();
+const upsChartSection=el('upsDialog').querySelector('.ups-chart-section');
+async function showUPSChart(){const id=el('upsChartSelect').value,hours=Number(upsChartSection.dataset.hours);upsChartSection.querySelectorAll('canvas').forEach(c=>c.hidden=c.id!==id);upsChartSection.querySelector('.chart-tooltip').classList.remove('visible');try{const points=entityChartKinds[id]?await loadEntityChart(id,hours):await loadHistory(hours);if(el('upsChartSelect').value===id&&Number(upsChartSection.dataset.hours)===hours)drawChart(id,points)}catch{drawChart(id,[])}}
+function selectUPSTab(key){document.querySelectorAll('[data-ups-tab]').forEach(b=>{const active=b.dataset.upsTab===key;b.setAttribute('aria-selected',String(active));b.tabIndex=active?0:-1});document.querySelectorAll('[data-ups-panel]').forEach(p=>p.hidden=p.dataset.upsPanel!==key);el('upsDialog').querySelector('.ups-content').scrollTop=0;if(key==='charts')requestAnimationFrame(showUPSChart)}
+document.querySelectorAll('[data-ups-tab]').forEach(b=>{b.onclick=()=>selectUPSTab(b.dataset.upsTab);b.onkeydown=e=>{if(!['ArrowLeft','ArrowRight','Home','End'].includes(e.key))return;e.preventDefault();const tabs=[...document.querySelectorAll('[data-ups-tab]')],i=tabs.indexOf(b),next=e.key==='Home'?0:e.key==='End'?tabs.length-1:(i+(e.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;tabs[next].focus();selectUPSTab(tabs[next].dataset.upsTab)}});
+el('upsChartSelect').onchange=showUPSChart;document.querySelectorAll('[data-ups-hours]').forEach(b=>b.onclick=()=>{upsChartSection.dataset.hours=b.dataset.upsHours;document.querySelectorAll('[data-ups-hours]').forEach(x=>x.classList.toggle('active',x===b));showUPSChart()});upsChartSection.querySelectorAll('canvas').forEach(setupChartHover);window.addEventListener('resize',()=>{if(el('upsDialog').open&&!el('upsPanel-charts').hidden)showUPSChart()});
+
setupCPUTabs();
document.querySelectorAll('[data-open]').forEach(b=>b.onclick=()=>el(b.dataset.open).showModal());
document.querySelectorAll('dialog').forEach(d=>{d.querySelector('.close').onclick=()=>d.close();d.onclick=e=>{if(e.target===d)d.close()}});