fix: consolidate controller statistics widgets

This commit is contained in:
2026-09-20 16:53:38 +03:00
parent e0404617d2
commit 7f49335847
3 changed files with 23 additions and 9 deletions
@@ -1269,7 +1269,7 @@ progress::-moz-progress-bar {
sourceAuto:'Автоматически',sourceNative:'Нативный источник',sourceInflux:'Influx/Grafana',sourceAll:'Все источники',
presetCo2Ventilation:'CO₂ → вентиляция',presetTemperatureHeating:'Температура → отопление',presetHumidityVentilation:'Влажность → вентиляция',presetCustom:'Своя пара',
native:'Нативный источник',influx:'Influx/Grafana',available:'доступен',unavailable:'недоступен',
configured:'настроен',points:'точек',source:'Источник',sourceDetails:'Источник и качество',
configured:'настроен',points:'точек',source:'Источник',actualSource:'Фактический источник',sourceDetails:'Источник и качество',statisticsSummary:'Сводка статистики',summaryValue:'Значения',notAvailable:'нет данных',
chartTitle:'Общий график',last:'последнее',seriesOne:'ряд',seriesFew:'ряда',seriesMany:'рядов',
time:'Время',value:'Значение',name:'Название',controller:'Контроллер',device:'Устройство',
room:'Комната',category:'Категория',type:'Тип',status:'Статус',error:'Ошибка',readonly:'Только чтение',
@@ -1303,7 +1303,7 @@ progress::-moz-progress-bar {
sourceAuto:'Automatic',sourceNative:'Native source',sourceInflux:'Influx/Grafana',sourceAll:'All sources',
presetCo2Ventilation:'CO₂ → ventilation',presetTemperatureHeating:'Temperature → heating',presetHumidityVentilation:'Humidity → ventilation',presetCustom:'Custom pair',
native:'Native source',influx:'Influx/Grafana',available:'available',unavailable:'unavailable',
configured:'configured',points:'points',source:'Source',sourceDetails:'Source and quality',
configured:'configured',points:'points',source:'Source',actualSource:'Actual source',sourceDetails:'Source and quality',statisticsSummary:'Statistics summary',summaryValue:'Values',notAvailable:'no data',
chartTitle:'Shared chart',last:'last',seriesOne:'series',seriesFew:'series',seriesMany:'series',
time:'Time',value:'Value',name:'Name',controller:'Controller',device:'Device',
room:'Room',category:'Category',type:'Type',status:'Status',error:'Error',readonly:'Read-only',
@@ -1340,7 +1340,7 @@ progress::-moz-progress-bar {
function metaFrom(p){var m=obj(p.meta)?p.meta:{},w=obj(m.widget)?m.widget:{};state.mode=String(w.mode||m.mode||state.mode||'history');state.config=merge(state.config,w);if(obj(w.sourceArgs)&&Object.keys(w.sourceArgs).length)state.config.sourceArgs=w.sourceArgs;else if(obj(m.sourceArgs)&&Object.keys(m.sourceArgs).length)state.config.sourceArgs=m.sourceArgs;return m}
function dataFrom(p){var source=unwrap(p);metaFrom(source);var data=source.meta&&source.meta.widget&&source.data!==undefined?source.data:source,timezone=data&&data.timezone,timezoneSource=data&&data.timezone_source;if(!timezone&&data&&Array.isArray(data.series)){var candidate=data.series.find(function(series){return series&&series.timezone});timezone=candidate&&candidate.timezone;timezoneSource=candidate&&candidate.timezone_source}if(typeof timezone==='string'&&timezone&&timezoneSource!=='normalized_utc'&&timezoneSource!=='influx_timestamp')state.timezone=timezone;return data}
function translateOptions(select){select.querySelectorAll('[data-i18n]').forEach(function(option){option.textContent=t(option.getAttribute('data-i18n'))})}
function labels(){var heading=T[state.lang][state.mode]||T[state.lang].history;document.documentElement.lang=state.lang;el.title.textContent=heading[0];el.lede.textContent=heading[1];el.refresh.textContent=t('refresh');translateOptions(el.source);translateOptions(el.preset);el.source.hidden=!['history','compare','health','statistics'].includes(state.mode);el.preset.hidden=state.mode!=='compare';if(state.mode==='compare'&&state.config.preset)el.preset.value=state.config.preset;if(state.config.sourceArgs&&state.config.sourceArgs.source)el.source.value=state.config.sourceArgs.source}
function labels(){var heading=T[state.lang][state.mode]||T[state.lang].history,isCollection=state.mode==='statistics'&&state.data&&Array.isArray(state.data.series);document.documentElement.lang=state.lang;el.title.textContent=heading[0];el.lede.textContent=heading[1];el.refresh.textContent=t('refresh');translateOptions(el.source);translateOptions(el.preset);el.refresh.hidden=!!isCollection;el.source.hidden=isCollection||!['history','compare','health','statistics'].includes(state.mode);el.preset.hidden=state.mode!=='compare';if(state.mode==='compare'&&state.config.preset)el.preset.value=state.config.preset;if(state.config.sourceArgs&&state.config.sourceArgs.source)el.source.value=state.config.sourceArgs.source}
function summary(items){el.summary.replaceChildren();(items||[]).forEach(function(x){var s=document.createElement('span');s.className='pill '+(x.kind||'');s.textContent=text(x.value);el.summary.append(s)})}
function formatNumber(value){var number=Number(value);if(value===null||value===undefined||value===''||!Number.isFinite(number))return text(value);return number.toLocaleString(state.lang==='ru'?'ru-RU':'en-US',{maximumFractionDigits:2})}
function dateOnlyText(raw){var parts=raw.split('-').map(Number);if(parts.length!==3||parts.some(function(part){return!Number.isFinite(part)}))return null;var date=new Date(Date.UTC(parts[0],parts[1]-1,parts[2],12));try{return new Intl.DateTimeFormat(state.lang,{dateStyle:'medium',timeZone:'UTC'}).format(date)}catch(err){return null}}
@@ -1350,6 +1350,12 @@ progress::-moz-progress-bar {
function formatDuration(value){var seconds=Number(value);if(!Number.isFinite(seconds))return text(value);seconds=Math.max(0,Math.round(seconds));var parts=[];if(seconds>=86400){parts.push(Math.floor(seconds/86400)+' '+t('day'));seconds%=86400}if(seconds>=3600){parts.push(Math.floor(seconds/3600)+' '+t('hour'));seconds%=3600}if(seconds>=60){parts.push(Math.floor(seconds/60)+' '+t('minute'));seconds%=60}if(seconds||parts.length===0)parts.push(seconds+' '+t('second'));return parts.join(' ')}
function formatFreshness(value){var ms=Number(value);return Number.isFinite(ms)?formatDuration(ms/1000):text(value)}
function sourceLabel(value){return value==='native'?t('native'):value==='influx'?t('influx'):text(value)}
function sourceArgs(){return obj(state.config.sourceArgs)?state.config.sourceArgs:{}}
function statisticsSeriesScope(d,useArgs){var args=useArgs?sourceArgs():{},room=String(d.room||d.room_name||d.room_label||args.room||'').trim(),item=String(d.item_name||d.label||d.name||d.item_id||args.item_id||'').trim(),controller=String(d.controller_name||d.controller||d.source_controller_id||args.controller||'').trim(),project=String(d.project||args.project||'').trim();return{project:project,controller:controller,room:room,item:item}}
function scopeLabel(d){var scope=statisticsSeriesScope(d,true),parts=[];[scope.project,scope.controller,scope.room,scope.item].forEach(function(value){if(value&&!parts.includes(value))parts.push(value)});return parts.join(' / ')}
function statisticsSourcePill(d){var args=sourceArgs(),requested=String(d.history_source_requested||args.source||'auto').trim().toLowerCase()||'auto',selected=String(d.selected_history_source||'').trim().toLowerCase();if(requested==='auto'||requested==='all'||(selected!==''&&selected!==requested))return{value:t('actualSource')+': '+sourceLabel(selected||requested),kind:selected?'good':'bad'};return null}
function statisticsValueText(d){var s=obj(d.summary)?d.summary:{},kind=d.statistics_kind,parts=[];if(kind==='numeric'){parts.push(t('samples')+': '+formatNumber(s.sample_count),t('minimum')+': '+formatNumber(s.min),t('maximum')+': '+formatNumber(s.max),t('average')+': '+formatNumber(s.average),t('median')+': '+formatNumber(s.median))}else if(kind==='binary'){parts.push(t('events')+': '+formatNumber(s.event_count),t('transitions')+': '+formatNumber(s.transition_count),t('periods')+': '+formatNumber(s.period_count),t('activeShare')+': '+formatNumber(s.active_percent)+'%')}else{parts.push(t('notAvailable'))}return parts.join('; ')}
function renderStatisticsCollection(d){var list=Array.isArray(d.series)?d.series:[],first=list.find(function(item){return obj(item)})||{},from=d.from||first.from,to=d.to||first.to,available=list.filter(function(item){return item&&item.statistics_available===true}).length;summary([{value:seriesLabel(list.length)},{value:available+' '+t('available')},{value:dateText(from)+' → '+dateText(to)}]);if(!list.length){el.content.append(empty());return}var rows=list.map(function(item){var scope=statisticsSeriesScope(item,false),selected=item.selected_history_source||item.history_source_requested||item.source,hasStats=item.statistics_available===true|| (item.statistics_available===undefined&&item.available!==false),status=hasStats?t('available'):text(item.statistics_error_code||item.error_code||t('notAvailable'));return[text(scope.room),text(scope.item),text(scope.controller),sourceLabel(selected),kindLabel(item.statistics_kind),text(item.quality),status,statisticsValueText(item)]});el.content.append(card(t('statisticsSummary'),table([t('room'),t('device'),t('controller'),t('source'),t('kind'),t('quality'),t('status'),t('summaryValue')],rows)))}
function kindLabel(value){return value==='binary'?t('binary'):value==='numeric'?t('numeric'):text(value)}
function boolLabel(value){return value?t('yes'):t('no')}
function numberRange(points,binary){
@@ -1477,7 +1483,7 @@ progress::-moz-progress-bar {
function renderDevices(d){var items=Array.isArray(d.items)?d.items:[];var total=Number.isFinite(Number(d.total))?d.total:items.length;summary([{value:total+' '+t('deviceCount')},{value:t('source')+': '+sourceLabel(d.source_type||d.source),kind:d.inventory_available?'good':'bad'},{value:t('revision')+': '+text(d.inventory_revision||d.revision)}]);if(!items.length){el.content.append(empty());return}var rows=items.map(function(item){var detail=document.createElement('button');detail.type='button';detail.className='inline-action';detail.textContent=t('details');detail.onclick=function(){showProfile(item,d)};return[item.id,item.name||item.title||item.device_name,item.room,item.category,item.type||item.value_type,item.access,itemValue(item),detail]});el.content.append(table([t('id'),t('name'),t('room'),t('category'),t('type'),t('access'),t('value'),t('details')],rows))}
function liveRows(d){var rows=[];var controllers=Array.isArray(d.controllers)?d.controllers:[];var names={};controllers.forEach(function(c){names[String(c.controller||c.source_controller_id||'')]=c.controller_name||c.controller||c.source_controller_id});(Array.isArray(d.states)?d.states:[]).forEach(function(s){rows.push([names[String(s.source_controller_id||'')]||s.source_controller_id,s.item_id,s.name,s.value_present?s.value:null,s.unit,s.quality,formatFreshness(s.freshness_ms),boolLabel(s.readback),s.unavailable_reason])});return rows}
function renderLiveState(d){var rows=liveRows(d);var controllers=Array.isArray(d.controllers)?d.controllers:[];var controllerCount=d.controller_count||controllers.length||new Set(rows.map(function(r){return r[0]})).size;summary([{value:controllerCount+' '+t('controllerCount')},{value:(d.state_count||rows.length)+' '+t('stateCount')},{value:t('readback')+': '+boolLabel(d.readback)}]);el.content.append(rows.length?table([t('controller'),t('id'),t('name'),t('value'),t('unit'),t('quality'),t('freshness'),t('readback'),t('unavailableReason')],rows):empty())}
function renderStatistics(d){var s=obj(d.summary)?d.summary:{};var available=d.statistics_available===true;summary([{value:kindLabel(d.statistics_kind)},{value:sampleLabel(s.sample_count||0)},{value:t('source')+': '+sourceLabel(d.selected_history_source||d.history_source_requested),kind:d.selected_history_source?'good':'bad'},{value:dateText(d.from)+' → '+dateText(d.to)}]);if(!available){var unavailable=card(t('status'));unavailable.append(document.createTextNode(t('noStats')+' '+text(d.statistics_error_code||'')));el.content.append(unavailable)}else if(d.statistics_kind==='binary'){el.content.append(metricGrid([[t('events'),formatNumber(s.event_count)], [t('transitions'),formatNumber(s.transition_count)], [t('periods'),formatNumber(s.period_count)], [t('activeTime'),formatDuration(s.active_seconds)], [t('activeShare'),formatNumber(s.active_percent)+'%'], [t('coverage'),formatDuration(s.coverage_seconds)], [t('averageDuration'),formatDuration(s.average_duration_seconds)], [t('medianDuration'),formatDuration(s.median_duration_seconds)], [t('shortest'),formatDuration(s.min_duration_seconds)], [t('longest'),formatDuration(s.max_duration_seconds)]]))}else if(d.statistics_kind==='numeric'){el.content.append(metricGrid([[t('samples'),formatNumber(s.sample_count)], [t('minimum'),formatNumber(s.min)], [t('maximum'),formatNumber(s.max)], [t('average'),formatNumber(s.average)], [t('median'),formatNumber(s.median)]]))}var meta=card(t('sourceDetails'));meta.append(table([t('source'),t('kind'),t('quality'),t('unit'),t('start'),t('end')],[[sourceLabel(d.selected_history_source||d.history_source_requested),kindLabel(d.statistics_kind),d.quality,d.value_unit||'—',dateText(d.from),dateText(d.to)]]));el.content.append(meta);var intervals=Array.isArray(d.intervals)?d.intervals:[];if(intervals.length){el.content.append(card(t('intervals'),table([t('start'),t('end'),t('duration'),t('status')],intervals.map(function(i){return[dateText(i.start),dateText(i.end),formatDuration(i.duration_seconds),i.open?t('open'):t('checked')]}))))}var buckets=Array.isArray(d.buckets)?d.buckets:[];if(buckets.length){var headers=d.statistics_kind==='binary'?[t('bucket'),t('events'),t('transitions'),t('periods'),t('activeTime')]:[t('bucket'),t('samples'),t('minimum'),t('maximum'),t('average')];el.content.append(card(t('hourly'),table(headers,buckets.map(function(b){return d.statistics_kind==='binary'?[dateText(b.bucket_start),formatNumber(b.event_count),formatNumber(b.transition_count),formatNumber(b.period_count),formatDuration(b.active_seconds)]:[dateText(b.bucket_start),formatNumber(b.sample_count),formatNumber(b.min),formatNumber(b.max),formatNumber(b.average)]}))));}var points=Array.isArray(d.points)?d.points:[];if(points.length)el.content.append(card(t('points'),table([t('time'),t('value'),t('quality')],points.slice(-80).reverse().map(function(p){return[dateText(p.time!==undefined?p.time:p.source_timestamp),p.value,p.quality]}))))}
function renderStatistics(d){if(Array.isArray(d.series)){renderStatisticsCollection(d);return}var s=obj(d.summary)?d.summary:{};var available=d.statistics_available===true;var scope=scopeLabel(d),sourcePill=statisticsSourcePill(d),summaryItems=[];if(scope)summaryItems.push({value:scope});summaryItems.push({value:kindLabel(d.statistics_kind)},{value:sampleLabel(s.sample_count||0)});if(sourcePill)summaryItems.push(sourcePill);summaryItems.push({value:dateText(d.from)+' → '+dateText(d.to)});summary(summaryItems);if(!available){var unavailable=card(t('status'));unavailable.append(document.createTextNode(t('noStats')+' '+text(d.statistics_error_code||'')));el.content.append(unavailable)}else if(d.statistics_kind==='binary'){el.content.append(metricGrid([[t('events'),formatNumber(s.event_count)], [t('transitions'),formatNumber(s.transition_count)], [t('periods'),formatNumber(s.period_count)], [t('activeTime'),formatDuration(s.active_seconds)], [t('activeShare'),formatNumber(s.active_percent)+'%'], [t('coverage'),formatDuration(s.coverage_seconds)], [t('averageDuration'),formatDuration(s.average_duration_seconds)], [t('medianDuration'),formatDuration(s.median_duration_seconds)], [t('shortest'),formatDuration(s.min_duration_seconds)], [t('longest'),formatDuration(s.max_duration_seconds)]]))}else if(d.statistics_kind==='numeric'){el.content.append(metricGrid([[t('samples'),formatNumber(s.sample_count)], [t('minimum'),formatNumber(s.min)], [t('maximum'),formatNumber(s.max)], [t('average'),formatNumber(s.average)], [t('median'),formatNumber(s.median)]]))}var meta=card(t('sourceDetails'));meta.append(table([t('source'),t('kind'),t('quality'),t('unit'),t('start'),t('end')],[[sourceLabel(d.selected_history_source||d.history_source_requested),kindLabel(d.statistics_kind),d.quality,d.value_unit||'—',dateText(d.from),dateText(d.to)]]));el.content.append(meta);var intervals=Array.isArray(d.intervals)?d.intervals:[];if(intervals.length){el.content.append(card(t('intervals'),table([t('start'),t('end'),t('duration'),t('status')],intervals.map(function(i){return[dateText(i.start),dateText(i.end),formatDuration(i.duration_seconds),i.open?t('open'):t('checked')]}))))}var buckets=Array.isArray(d.buckets)?d.buckets:[];if(buckets.length){var headers=d.statistics_kind==='binary'?[t('bucket'),t('events'),t('transitions'),t('periods'),t('activeTime')]:[t('bucket'),t('samples'),t('minimum'),t('maximum'),t('average')];el.content.append(card(t('hourly'),table(headers,buckets.map(function(b){return d.statistics_kind==='binary'?[dateText(b.bucket_start),formatNumber(b.event_count),formatNumber(b.transition_count),formatNumber(b.period_count),formatDuration(b.active_seconds)]:[dateText(b.bucket_start),formatNumber(b.sample_count),formatNumber(b.min),formatNumber(b.max),formatNumber(b.average)]}))));}var points=Array.isArray(d.points)?d.points:[];if(points.length)el.content.append(card(t('points'),table([t('time'),t('value'),t('quality')],points.slice(-80).reverse().map(function(p){return[dateText(p.time!==undefined?p.time:p.source_timestamp),p.value,p.quality]}))))}
function renderTopology(d){var controllers=Array.isArray(d.controllers)?d.controllers:[];summary([{value:controllers.length+' '+t('controllerCount')},{value:t('inventorySeparate')}]);el.content.append(controllers.length?table([t('controller'),t('type'),t('revision')],controllers.map(function(c){var role=c.controller_role==='source'?t('source'):t('controller');return[c.controller_name||c.controller,role+' · '+(c.controller_type||'—'),c.source_revision]})):empty())}
function renderHealth(d){var list=Array.isArray(d.controllers)?d.controllers:[d];summary([{value:list.length+' '+t('controllerCount')}]);var grid=document.createElement('div');grid.className='grid';list.forEach(function(c){var sources=Array.isArray(c.sources)?c.sources:[];grid.append(card(c.controller||c.source_controller_id||t('controller'),'<p>'+pill(c.status||'—',c.status==='available'?'good':'')+'</p><div class="source-list">'+sources.map(function(s){return '<div class="source-row"><span>'+esc(sourceLabel(s.name))+'</span><span>'+pill(s.available?t('available'):(s.configured?t('configured'):t('unavailable')),s.available?'good':(s.configured?'':'bad'))+'</span></div>'}).join('')+'</div>'))});el.content.append(grid)}
function renderAudit(d){var rows=Array.isArray(d.audit)?d.audit:(Array.isArray(d.commands)?d.commands:[]);summary([{value:rows.length+' '+t('records')}]);el.content.append(rows.length?table([t('time'),t('device'),t('action'),t('status'),t('requested'),t('actual')],rows.map(function(x){return[dateText(x.created_at),x.item_id,x.action,x.status,text(x.requested_value),text(x.actual_readback)]})):empty())}