diff --git a/.github/scripts/generate-pr-plugin.sh b/.github/scripts/generate-pr-plugin.sh index 71d14a680..477d31d25 100755 --- a/.github/scripts/generate-pr-plugin.sh +++ b/.github/scripts/generate-pr-plugin.sh @@ -53,14 +53,11 @@ cat > "$PLUGIN_NAME" << 'EOF' icon="wrench" support="&github;/pull/≺"> - - @@ -271,7 +268,14 @@ Link='nav-user' diff --git a/emhttp/plugins/dynamix.vm.manager/VMMachines.page b/emhttp/plugins/dynamix.vm.manager/VMMachines.page old mode 100644 new mode 100755 diff --git a/emhttp/plugins/dynamix.vm.manager/VMUsageStats.page b/emhttp/plugins/dynamix.vm.manager/VMUsageStats.page index 14ad65693..e957f8ff1 100755 --- a/emhttp/plugins/dynamix.vm.manager/VMUsageStats.page +++ b/emhttp/plugins/dynamix.vm.manager/VMUsageStats.page @@ -1,4 +1,4 @@ -Menu="VMs:0" +Menu="VMs:2" Title="VM Usage Statistics" Nchan="vm_usage" Cond="exec(\"grep -o '^USAGE=.Y' /boot/config/domain.cfg 2>/dev/null\") && is_file('/var/run/libvirt/libvirtd.pid')" diff --git a/emhttp/plugins/dynamix.vm.manager/VMs.page b/emhttp/plugins/dynamix.vm.manager/VMs.page old mode 100644 new mode 100755 index 51528d0d0..1c73df341 --- a/emhttp/plugins/dynamix.vm.manager/VMs.page +++ b/emhttp/plugins/dynamix.vm.manager/VMs.page @@ -2,6 +2,7 @@ Menu="Tasks:70" Type="xmenu" Code="e918" Lock="true" +Tabs="true" Cond="exec(\"grep -o '^SERVICE=.enable' /boot/config/domain.cfg 2>/dev/null\")" --- /dev/null\")"

- - \ No newline at end of file diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig index d09296f9a..12ae12634 100755 --- a/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig +++ b/emhttp/plugins/dynamix.vm.manager/scripts/libvirtconfig @@ -1,6 +1,6 @@ #!/usr/bin/php -q $value) $tmp .= "$key=\"$value\"\n"; file_put_contents($cfgfile, $tmp); } diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index d67495468..e67d655b2 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -459,6 +459,10 @@ function doAction(action, title, id) { clearTimeout(timers.calc); $('div.spinner.fixed').hide('slow'); swal({title:"_(Calculate Occupied Space)_",text:text,html:true,confirmButtonText:"_(Ok)_"}); + }).fail(function(xhr){ + clearTimeout(timers.calc); + $('div.spinner.fixed').hide('slow'); + swal({title:"Error", text:"Calculate failed: "+xhr.status+" "+xhr.statusText, type:"error"}); }); return; case 15: // search @@ -735,6 +739,10 @@ function doActions(action, title) { clearTimeout(timers.calc); $('div.spinner.fixed').hide('slow'); swal({title:"_(Calculate Occupied Space)_",text:text,html:true,confirmButtonText:"_(Ok)_"}); + }).fail(function(xhr){ + clearTimeout(timers.calc); + $('div.spinner.fixed').hide('slow'); + swal({title:"Error", text:"Calculate failed: "+xhr.status+" "+xhr.statusText, type:"error"}); }); return; case 15: // search diff --git a/emhttp/plugins/dynamix/DashStats.page b/emhttp/plugins/dynamix/DashStats.page index c1d4203ed..5522666ae 100755 --- a/emhttp/plugins/dynamix/DashStats.page +++ b/emhttp/plugins/dynamix/DashStats.page @@ -169,11 +169,16 @@ foreach ($memory_array as $device) { if ($base>=1) $memory_maximum += $size*pow(1024,$base); if (!$ecc && isset($device['Error Correction Type']) && $device['Error Correction Type']!='None') $ecc = "{$device['Error Correction Type']} "; } +if ($memory_installed >= 1048576) { + $memory_installed = round($memory_installed/1048576); + $memory_maximum = round($memory_maximum/1048576); + $unit = 'TiB'; +} else { if ($memory_installed >= 1024) { $memory_installed = round($memory_installed/1024); $memory_maximum = round($memory_maximum/1024); - $unit = 'GiB'; -} else $unit = 'MiB'; + $unit = 'GiB';} +else $unit = 'MiB'; } // get system resources size exec("df --output=size /boot /var/log /var/lib/docker 2>/dev/null|awk '(NR>1){print $1*1024}'",$df); @@ -196,7 +201,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout ?> - + @@ -412,7 +417,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout ?> -
+ @@ -498,13 +503,19 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout

_(Interface)_

- + - +
@@ -531,12 +542,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout - +   _(Mode of operation)_ @@ -584,7 +590,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout ?> -
+ @@ -1427,8 +1433,8 @@ var rxd = []; var txd = []; var cputime = 0; var nettime = 0; -var cpuline = cookie.cpuline||30; -var netline = cookie.netline||30; +var cpuline = parseInt(cookie.cpuline)||30; +var netline = parseInt(cookie.netline)||30; var update2 = true; var box = null; var startup = true; @@ -1436,56 +1442,92 @@ var stopgap = ' var recall = null; var recover = null; -var options_cpu = { - series:[{name:'load', data:cpu.slice()}], - chart:{height:120, type:'line', fontFamily:'clear-sans', animations:{enabled:true, easing:'linear', dynamicAnimation:{speed:980}}, toolbar:{show:false}, zoom:{enabled:false},events:{updated:function(){if (cpuchart.customData.updateCount == 0) {cpuchart.customData.animationPending = false}cpuchart.customData.updateCount++;},animationEnd:function(){cpuchart.customData.animationPending = false;updateCPUChart();}}}, - dataLabels:{enabled:false}, - tooltip:{enabled:false}, - stroke:{curve:'smooth', width:1}, - colors:['#ff8c2f'], - markers:{size:0}, - xaxis:{type:'datetime', range:cpuline-1, labels:{show:false}, axisTicks:{show:false}, axisBorder:{show:false}}, - yaxis:{max:100, min:0, tickAmount:4, labels:{formatter:function(v,i){return v.toFixed(0)+' %';}, style:{colors:''}}, axisBorder:{show:false}, axisTicks:{show:false}}, - grid:{show:true, borderColor:''}, - legend:{show:false} -}; -var options_net = { - series:[{name:'receive', data:rxd.slice()},{name:'transmit', data:txd.slice()}], - chart:{height:120, type:'line', fontFamily:'clear-sans', animations:{enabled:true, easing:'linear', dynamicAnimation:{speed:980}}, toolbar:{show:false}, zoom:{enabled:false},events:{updated:function(){if (netchart.customData.updateCount == 0) {netchart.customData.animationPending = false}netchart.customData.updateCount++;},animationEnd:function(){netchart.customData.animationPending = false;updateNetChart();}}}, - dataLabels:{enabled:false}, - tooltip:{enabled:false}, - stroke:{curve:'smooth', width:1}, - colors:['#e22828','#ff8c2f'], - markers:{size:0}, - xaxis:{type:'datetime', range:netline-1, labels:{show:false}, axisTicks:{show:false}, axisBorder:{show:false}}, - yaxis:{tickAmount:4, labels:{formatter:function(v,i){return autoscale(v,'bps',1);}, style:{colors:''}}, axisBorder:{show:false}, axisTicks:{show:false}}, - grid:{show:true, borderColor:''}, - legend:{show:false} -}; +// Helper function to calculate millisPerPixel based on container width +function getMillisPerPixel(timeInSeconds, containerId) { + var container = document.getElementById(containerId); + var width = (container && container.offsetWidth > 0) ? container.offsetWidth : 600; // fallback to 600 if not found or zero width + return (timeInSeconds * 1000) / width; +} -var cpuchart = new ApexCharts(document.querySelector('#cpuchart'), options_cpu); -var netchart = new ApexCharts(document.querySelector('#netchart'), options_net); +// SmoothieCharts initialization +var cpuchart = new SmoothieChart({ + millisPerPixel: (cpuline * 1000) / 600, // Safe initial value + minValue: -1, + maxValue: 100, + responsive: true, + grid: { + strokeStyle: '', + fillStyle: 'transparent', + lineWidth: 1, + millisPerLine: 5000, + verticalSections: 4 + }, + labels: { + fillStyle: '', + fontSize: 11, + precision: 0 + }, + timestampFormatter: function(date) { return ''; }, + minValueScale: 1.02, + maxValueScale: 1.02, + yMinFormatter: function(value) { + return Math.max(0, Math.floor(value)) + ' %'; + }, + yMaxFormatter: function(value) { + return Math.max(0, Math.min(100, Math.ceil(value))) + ' %'; + } +}); -// Add custom global variable to ncharts (won't affect ApexCharts functionality) +var netchart = new SmoothieChart({ + millisPerPixel: (netline * 1000) / 600, // Safe initial value + minValue: 0, + responsive: true, + grid: { + strokeStyle: '', + fillStyle: 'transparent', + lineWidth: 1, + millisPerLine: 5000, + verticalSections: 4 + }, + labels: { + fillStyle: '', + fontSize: 11, + placement: 'left' + }, + timestampFormatter: function(date) { return ''; }, + minValueScale: 1.02, + maxValueScale: 1.02, + yMaxFormatter: function(value) { + if (value >= 1000) { + return Math.floor(value / 1000) + ' Mbps'; + } + return Math.floor(value) + ' kbps'; + }, + yMinFormatter: function(value) { + return Math.floor(value) + ' kbps'; + } +}); + +// Create TimeSeries for CPU and Network data +var cpuTimeSeries = new TimeSeries(); +var rxTimeSeries = new TimeSeries(); +var txTimeSeries = new TimeSeries(); + +// Add TimeSeries to charts with colors +cpuchart.addTimeSeries(cpuTimeSeries, { strokeStyle: '#ff8c2f', lineWidth: 1 }); +netchart.addTimeSeries(rxTimeSeries, { strokeStyle: '#e22828', lineWidth: 1 }); +netchart.addTimeSeries(txTimeSeries, { strokeStyle: '#ff8c2f', lineWidth: 1 }); + +// Custom data for chart state management netchart.customData = { - isVisible: false, - BrowserVisibility: true, - animationPending: false, - netAnimationInterval: null, - newData: false, - updateCount: 0, - initialized: false + isVisible: false }; cpuchart.customData = { isVisible: false, - BrowserVisibility: true, - animationPending: false, - cpuAnimationInterval: null, coresVisible: false, - newData: false, - updateCount: 0, - initialized: false + cpuData: null, + cpuLoad: 0 }; $(function() { @@ -1495,9 +1537,9 @@ $(function() { entries.forEach(entry => { if (entry.target === netchartElement) { netchart.customData.isVisible = entry.isIntersecting; - // Reset the update count as the chart doesn't always fire animationEnd in this case + // Chart visibility changed if (netchart.customData.isVisible) { - resetNetUpdateCount(); + // SmoothieCharts handles visibility automatically } } }); @@ -1520,9 +1562,9 @@ $(function() { entries.forEach(entry => { if (entry.target === cpuchartElement) { cpuchart.customData.isVisible = entry.isIntersecting; - // Reset the update count as the chart doesn't always fire animationEnd in this case + // Chart visibility changed if (cpuchart.customData.isVisible) { - resetCPUUpdateCount(); + // SmoothieCharts handles visibility automatically } } }); @@ -1588,8 +1630,9 @@ let viewportResizeTimeout; window.addEventListener('resize', function(){ clearTimeout(viewportResizeTimeout); viewportResizeTimeout = setTimeout(function(){ - resetNetUpdateCount(); - resetCPUUpdateCount(); + // Update millisPerPixel based on new container sizes + cpuchart.options.millisPerPixel = getMillisPerPixel(cpuline, 'cpuchart'); + netchart.options.millisPerPixel = getMillisPerPixel(netline, 'netchart'); }, 250); // Wait 250ms after resize stops before executing }); @@ -1627,80 +1670,45 @@ function sanitizeMultiCookie(cookieName, delimiter, removeDuplicates=false) { } function initCharts(clear) { - // initialize graphs entries - var data = []; - data.cpu = data.rxd = data.txd = ""; - - var now = new Date().getTime(); - if (!clear) { - var c = data.cpu.split(';'); - var r = data.rxd.split(';'); - var t = data.txd.split(';'); - for (var i=0; i < cpuline; i++) { - var x = now + i; - var y = c[i]||0; cpu.push({x,y}); - } - cputime = x + 1; - } else { - // clear network graph - var r = ''; var t = ''; - rxd = []; txd = []; + // SmoothieCharts manages its own data through TimeSeries + if (clear) { + // Clear the TimeSeries data if needed + rxTimeSeries.clear(); + txTimeSeries.clear(); } - for (var i=0; i < netline; i++) { - var x = now + i; - var y = r[i]||0; rxd.push({x,y}); - var y = t[i]||0; txd.push({x,y}); - } - nettime = x + 1; } function resetCharts() { - // prevent unlimited graph growing - limit to 300 (5 minutes) of data - cpu = cpu.slice(-300); - rxd = rxd.slice(-300); - txd = txd.slice(-300); + // SmoothieCharts automatically manages data retention + // No manual cleanup needed } function addChartCpu(load) { - cputime++; - var i = cpu.length - cpuline; - if (i > 0) { // clear value outside graph - i = i - 1; - cpu[i].x = cputime - cpuline; - cpu[i].y = 0; - } - cpu.push({x:cputime, y:load}); + // Add data point to SmoothieCharts TimeSeries + cpuTimeSeries.append(new Date().getTime(), load); } function addChartNet(rx, tx) { - nettime++; - var i = rxd.length - netline; - if (i > 0) { // clear value outside graph - i = i - 1; - rxd[i].x = nettime - netline; - rxd[i].y = 0; - txd[i].x = nettime - netline; - txd[i].y = 0; - } - rxd.push({x:nettime, y:rx}); - txd.push({x:nettime, y:tx}); + // Add data points to SmoothieCharts TimeSeries (convert bps to kbps) + var now = new Date().getTime(); + rxTimeSeries.append(now, Math.floor(rx / 1000)); + txTimeSeries.append(now, Math.floor(tx / 1000)); } -function resetCPUUpdateCount() { - cpuchart.customData.updateCount = 0; - cpuchart.customData.animationPending = false; -} +// Cache for last values to avoid unnecessary DOM updates +var lastCpuValues = { + load: -1, + color: '', + fontColor: '', + coreValues: {} +}; -function resetNetUpdateCount() { - netchart.customData.updateCount = 0; - netchart.customData.animationPending = false; -} function updateCPUBarCharts() { if (!isPageVisible()) { return; } // prevent an initial JS error if the first datapoint isn't available yet - // (cpuchart.customData.newData is reset by the updateCPUChart function so can't be used) + // Update CPU bar charts based on current data const customData = cpuchart.customData; if (!customData.cpuData?.cpus || typeof customData.cpuLoad === 'undefined') { return; @@ -1709,90 +1717,91 @@ function updateCPUBarCharts() { const cpuLoad = customData.cpuLoad; const critical = ; const warning = ; - - // Cache DOM elements and calculations for overall CPU load - const cpuLoadText = cpuLoad + '%'; - const cpuLoadColor = setColor(cpuLoad, critical, warning); - const cpuLoadFontColor = fontColor(cpuLoad, critical, warning); - - // Batch DOM updates for overall CPU load - const $cpuElements = $('.cpu_, .cpu'); - const $cpuAliveElements = $('#cpu_, #cpu'); - - $cpuElements.text(cpuLoadText).css({'color': cpuLoadFontColor}); - $cpuAliveElements.alive(cpuLoadText, cpuLoadColor); + + // Only update DOM if values have changed + if (cpuLoad !== lastCpuValues.load) { + const cpuLoadText = cpuLoad + '%'; + const cpuLoadColor = setColor(cpuLoad, critical, warning); + const cpuLoadFontColor = fontColor(cpuLoad, critical, warning); + + // Batch DOM updates for overall CPU load + const $cpuElements = $('.cpu_, .cpu'); + const $cpuAliveElements = $('#cpu_, #cpu'); + + $cpuElements.text(cpuLoadText).css({'color': cpuLoadFontColor}); + + // Only call alive() if color actually changed + //if (cpuLoadColor !== lastCpuValues.color) { + $cpuAliveElements.alive(cpuLoadText, cpuLoadColor); + lastCpuValues.color = cpuLoadColor; + // } + + lastCpuValues.load = cpuLoad; + lastCpuValues.fontColor = cpuLoadFontColor; + } // Update individual CPU cores if they are visible if (customData.coresVisible) { const cpus = customData.cpuData.cpus; - + // Batch DOM updates for CPU cores const cpuCoreUpdates = []; const cpuAliveUpdates = []; - + cpus.forEach((cpuCore, index) => { - const coreLoad = Math.round(cpuCore.percentTotal); - const coreLoadText = coreLoad + '%'; - const coreColor = setColor(coreLoad, critical, warning); - const coreFontColor = fontColor(coreLoad, critical, warning); - - cpuCoreUpdates.push({ - selector: '.cpu' + index, - text: coreLoadText, - color: coreFontColor + const coreLoad = Math.floor(cpuCore.percentTotal); + + // Only process if value changed + if (!lastCpuValues.coreValues[index] || lastCpuValues.coreValues[index].load !== coreLoad) { + const coreLoadText = coreLoad + '%'; + const coreColor = setColor(coreLoad, critical, warning); + const coreFontColor = fontColor(coreLoad, critical, warning); + + cpuCoreUpdates.push({ + selector: '.cpu' + index, + text: coreLoadText, + color: coreFontColor + }); + + // Only update alive() if color changed + const lastCore = lastCpuValues.coreValues[index] || {}; + //if (coreColor !== lastCore.color) { + cpuAliveUpdates.push({ + selector: '#cpu' + index, + text: coreLoadText, + color: coreColor + }); + lastCore.color = coreColor; + // } + + // Update cache + lastCpuValues.coreValues[index] = { + load: coreLoad, + color: coreColor, + fontColor: coreFontColor + }; + } + }); + + // Apply batch updates only for changed values + if (cpuCoreUpdates.length > 0) { + cpuCoreUpdates.forEach(update => { + $(update.selector).text(update.text).css({'color': update.color}); }); - - cpuAliveUpdates.push({ - selector: '#cpu' + index, - text: coreLoadText, - color: coreColor + } + + if (cpuAliveUpdates.length > 0) { + cpuAliveUpdates.forEach(update => { + $(update.selector).alive(update.text, update.color); }); - }); - - // Apply all CPU core updates in batches - cpuCoreUpdates.forEach(update => { - $(update.selector).text(update.text).css({'color': update.color}); - }); - - cpuAliveUpdates.forEach(update => { - $(update.selector).alive(update.text, update.color); - }); + } } } -function updateCPUChart() { - if (!cpuchart.customData.newData || !cpuchart.customData.isVisible || !isPageVisible()) { - return; - } - // Check if the animation is still running and a timeout hasn't been set - if (!cpuchart.customData.animationPending) { - // No animation running. Clear out the timeout and update the chart - cpuchart.customData.animationPending = true; - cpuchart.customData.newData = false; - cpuchart.updateSeries([{data:cpu}]); - } -} - -function updateNetChart() { - if (!netchart.customData.newData || !netchart.customData.isVisible || !isPageVisible()) { - return; - } - // Check if the animation is still running and a timeout hasn't been set - if (!netchart.customData.animationPending) { - // No animation running. Clear out the timeout and update the chart - netchart.customData.animationPending = true; - netchart.customData.newData = false; - netchart.updateSeries([{data:rxd},{data:txd}]); - } -} function isPageVisible() { - // Check if charts are good to go - if (netchart.customData.initialized && cpuchart.customData.initialized) { - return !document.hidden; - } else { - return false; - } + // SmoothieCharts handles visibility automatically + return !document.hidden; } @@ -1905,21 +1914,23 @@ function changeView(item) { } function changeCPUline(val) { - cpuline = val; + cpuline = parseInt(val); if (val==30) delete cookie.cpuline; else cookie.cpuline = val; saveCookie(); - // Reset the update count as the chart doesn't always fire animationEnd - resetCPUUpdateCount(); - cpuchart.updateOptions({xaxis:{range:cpuline-1}}); + // Update chart range + // Update SmoothieChart time window based on actual chart width + cpuchart.options.millisPerPixel = getMillisPerPixel(cpuline, 'cpuchart'); + // No need to stop and restart, just update the option } function changeNetline(val) { - netline = val; + netline = parseInt(val); if (val==30) delete cookie.netline; else cookie.netline = val; saveCookie(); - // Reset the update count as the chart doesn't always fire animationEnd - resetNetUpdateCount(); - netchart.updateOptions({xaxis:{range:netline-1}}); + // Update chart range + // Update SmoothieChart time window based on actual chart width + netchart.options.millisPerPixel = getMillisPerPixel(netline, 'netchart'); + // No need to stop and restart, just update the option } function smartMenu(table) { @@ -2585,20 +2596,41 @@ dashboard.on('message',function(msg,meta) { // rx & tx speeds for (let i=0,port; port=get.port[i]; i++) { if (port[0] == port_select) { - $('#inbound').text(port[1]); - $('#outbound').text(port[2]); + // Cache for network values to avoid unnecessary DOM updates + if (!window.lastNetValues) window.lastNetValues = { inbound: '', outbound: '' }; + + // Only update DOM if values changed + if (port[1] !== window.lastNetValues.inbound) { + $('#inbound').text(port[1]); + window.lastNetValues.inbound = port[1]; + } + if (port[2] !== window.lastNetValues.outbound) { + $('#outbound').text(port[2]); + window.lastNetValues.outbound = port[2]; + } + // update the netchart but only send to ApexCharts if the chart is visible - addChartNet(port[3], port[4]); - netchart.customData.newData = true; - updateNetChart(); - + addChartNet(port[3], port[4]); + break; } } - // port counters - for (let k=0; k < get.mode.length; k++) $('#main'+k).html(get.mode[k]); - for (let k=0; k < get.rxtx.length; k++) $('#port'+k).html(get.rxtx[k]); - for (let k=0; k < get.stat.length; k++) $('#link'+k).html(get.stat[k]); + // port counters - batch check for changes + for (let k=0; k < get.mode.length; k++) { + const $elem = $('#main'+k); + const newVal = get.mode[k]; + if ($elem.html() !== newVal) $elem.html(newVal); + } + for (let k=0; k < get.rxtx.length; k++) { + const $elem = $('#port'+k); + const newVal = get.rxtx[k]; + if ($elem.html() !== newVal) $elem.html(newVal); + } + for (let k=0; k < get.stat.length; k++) { + const $elem = $('#link'+k); + const newVal = get.stat[k]; + if ($elem.html() !== newVal) $elem.html(newVal); + } // current date and time $('#current_time').html(get.time[0]); $('#current_time_').html(get.time[0]); @@ -2653,8 +2685,15 @@ $(function() { initCharts(); - cpuchart.render(); - netchart.render(); + // Start SmoothieCharts streaming + cpuchart.streamTo(document.getElementById('cpuchart'), 1000); + netchart.streamTo(document.getElementById('netchart'), 1000); + + // Set millisPerPixel after DOM is ready and charts are attached + setTimeout(function() { + cpuchart.options.millisPerPixel = getMillisPerPixel(cpuline, 'cpuchart'); + netchart.options.millisPerPixel = getMillisPerPixel(netline, 'netchart'); + }, 100); addProperties(); @@ -2686,13 +2725,11 @@ $(function() { next: (result) => { if (result.data?.systemMetricsCpu) { cpuchart.customData.cpuData = result.data.systemMetricsCpu; - cpuchart.customData.cpuLoad = Math.round(cpuchart.customData.cpuData.percentTotal); + cpuchart.customData.cpuLoad = Math.floor(cpuchart.customData.cpuData.percentTotal); - //update cpu chart data - addChartCpu(cpuchart.customData.cpuLoad); - cpuchart.customData.newData = true; + //update cpu chart data + addChartCpu(cpuchart.customData.cpuLoad); updateCPUBarCharts(); - updateCPUChart(); } }, @@ -2731,8 +2768,7 @@ $(function() { // Inhibit chart updates until DOM quiets down setTimeout(function() { - netchart.customData.initialized = true; - cpuchart.customData.initialized = true; + // Charts initialized },500); // Cleanup GraphQL subscription on page unload @@ -2743,4 +2779,4 @@ $(function() { }); }); - \ No newline at end of file + diff --git a/emhttp/plugins/dynamix/DeviceInfo.page b/emhttp/plugins/dynamix/DeviceInfo.page index d4ff614e8..88df03ea2 100755 --- a/emhttp/plugins/dynamix/DeviceInfo.page +++ b/emhttp/plugins/dynamix/DeviceInfo.page @@ -876,7 +876,7 @@ _(Spin down delay)_: _(File system status)_: -:   +:   _(File system type)_: @@ -912,7 +912,6 @@ _(File system type)_: echo get_inline_fs_warnings($disk); ?> -
_(Allocation profile)_: diff --git a/emhttp/plugins/dynamix/Diagnostics.page b/emhttp/plugins/dynamix/Diagnostics.page index d710fc25d..7cbba6656 100644 --- a/emhttp/plugins/dynamix/Diagnostics.page +++ b/emhttp/plugins/dynamix/Diagnostics.page @@ -110,15 +110,16 @@ to the system log.* *Use* **Anonymize diagnostics** *when publishing the diagnostics file in the public forum. In private communication with Limetech it is recommended to uncheck this setting and capture all information unaltered.* :end -
- - - - - - - +
+
+ +
+
+ + +
+
diff --git a/emhttp/plugins/dynamix/NewConfig.page b/emhttp/plugins/dynamix/NewConfig.page index 0f3205335..53e50942e 100644 --- a/emhttp/plugins/dynamix/NewConfig.page +++ b/emhttp/plugins/dynamix/NewConfig.page @@ -62,30 +62,38 @@ effect of making it ***impossible*** to rebuild an existing failed drive - you h -_(Preserve current assignments)_: -: +
+
+ _(Preserve current assignments)_: +
+ +
+
-  -: - - _(Array has been **Reset**)_ (_(please configure)_) - - _(Array must be **Stopped** to change)_ - -
diff --git a/emhttp/plugins/dynamix/NewPerms.page b/emhttp/plugins/dynamix/NewPerms.page index 3dc93c648..70f5726f2 100644 --- a/emhttp/plugins/dynamix/NewPerms.page +++ b/emhttp/plugins/dynamix/NewPerms.page @@ -91,36 +91,50 @@ Closing the window before completion will terminate the background process - so Note that this tool may negatively affect any docker containers if you allow your **appdata** share to be included. :end -
- - - + +
+
+ _(Target)_: +
+ +
+
- - - +
+ _(Items)_: +
+
+ +
+ +
+
- - - -
- -
_(Array must be **Started** to change permissions)_.
- +
+ + + + + + + _(Array must be **Started** to change permissions)_. + +
+
diff --git a/emhttp/plugins/dynamix/SecurityNFS.page b/emhttp/plugins/dynamix/SecurityNFS.page index e74674f2f..da4a9cf20 100755 --- a/emhttp/plugins/dynamix/SecurityNFS.page +++ b/emhttp/plugins/dynamix/SecurityNFS.page @@ -31,7 +31,7 @@ _(Read settings from)_ if (isset($disks[$name])) { foreach (array_filter($disks,'clone_list') as $list) if ($list['name']!=$name) echo mk_option("", $list['name'], _(my_disk($list['name']),3)); } else { - foreach ($shares as $list) if ($list['name']!=$name) echo mk_option("", $list['name'], compress($list['name'])); + foreach ($shares as $list) if (strpos($list['name'],"'") === false && $list['name']!=$name) echo mk_option("", $list['name'], compress($list['name'])); } ?> @@ -48,7 +48,7 @@ _(Write settings to)_ if (isset($disks[$name])) { foreach (array_filter($disks,'clone_list') as $list) if ($list['name']!=$name) $rows[] = mk_option("", $list['name'], _(my_disk($list['name']),3)); } else { - foreach ($shares as $list) if ($list['name']!=$name) $rows[] = mk_option("", $list['name'], compress($list['name'])); + foreach ($shares as $list) if (strpos($list['name'],"'") === false && $list['name']!=$name) $rows[] = mk_option("", $list['name'], compress($list['name'])); } if ($rows) echo ""; foreach ($rows as $row) echo $row; diff --git a/emhttp/plugins/dynamix/SecuritySMB.page b/emhttp/plugins/dynamix/SecuritySMB.page index 7991090dd..de4af57b8 100755 --- a/emhttp/plugins/dynamix/SecuritySMB.page +++ b/emhttp/plugins/dynamix/SecuritySMB.page @@ -34,7 +34,7 @@ _(Read settings from)_ if (isset($disks[$name])) { foreach (array_filter($disks,'clone_list') as $list) if ($list['name']!=$name) echo mk_option("", $list['name'], _(my_disk($list['name']),3)); } else { - foreach ($shares as $list) if ($list['name']!=$name) echo mk_option("", $list['name'], compress($list['name'])); + foreach ($shares as $list) if (strpos($list['name'],"'") === false && $list['name']!=$name) echo mk_option("", $list['name'], compress($list['name'])); } ?> @@ -51,7 +51,7 @@ _(Write settings to)_ if (isset($disks[$name])) { foreach (array_filter($disks,'clone_list') as $list) if ($list['name']!=$name) $rows[] = mk_option("", $list['name'], _(my_disk($list['name']),3)); } else { - foreach ($shares as $list) if ($list['name']!=$name) $rows[] = mk_option("", $list['name'], compress($list['name'])); + foreach ($shares as $list) if (strpos($list['name'],"'") === false && $list['name']!=$name) $rows[] = mk_option("", $list['name'], compress($list['name'])); } if ($rows) echo ""; foreach ($rows as $row) echo $row; @@ -154,7 +154,7 @@ _(Read settings from)_ if (isset($disks[$name])) { foreach (array_filter($disks,'clone_list') as $list) if ($list['name']!=$name && $sec[$list['name']]['security']=='secure') echo mk_option("", $list['name'], _(my_disk($list['name']),3)); } else { - foreach ($shares as $list) if ($list['name']!=$name && $sec[$list['name']]['security']=='secure') echo mk_option("", $list['name'], compress($list['name'])); + foreach ($shares as $list) if (strpos($list['name'],"'") === false && $list['name']!=$name && $sec[$list['name']]['security']=='secure') echo mk_option("", $list['name'], compress($list['name'])); } ?> @@ -171,7 +171,7 @@ _(Write settings to)_ if (isset($disks[$name])) { foreach (array_filter($disks,'clone_list') as $list) if ($list['name']!=$name && $sec[$list['name']]['security']=='secure') $rows[] = mk_option("", $list['name'], _(my_disk($list['name']),3)); } else { - foreach ($shares as $list) if ($list['name']!=$name && $sec[$list['name']]['security']=='secure') $rows[] = mk_option("", $list['name'], compress($list['name'])); + foreach ($shares as $list) if (strpos($list['name'],"'") === false && $list['name']!=$name && $sec[$list['name']]['security']=='secure') $rows[] = mk_option("", $list['name'], compress($list['name'])); } if ($rows) echo ""; foreach ($rows as $row) echo $row; @@ -217,7 +217,7 @@ _(Read settings from)_ if (isset($disks[$name])) { foreach (array_filter($disks,'clone_list') as $list) if ($list['name']!=$name && $sec[$list['name']]['security']=='private') echo mk_option("", $list['name'], _(my_disk($list['name']),3)); } else { - foreach ($shares as $list) if ($list['name']!=$name && $sec[$list['name']]['security']=='private') echo mk_option("", $list['name'], compress($list['name'])); + foreach ($shares as $list) if (strpos($list['name'],"'") === false && $list['name']!=$name && $sec[$list['name']]['security']=='private') echo mk_option("", $list['name'], compress($list['name'])); } ?> @@ -234,7 +234,7 @@ _(Write settings to)_ if (isset($disks[$name])) { foreach (array_filter($disks,'clone_list') as $list) if ($list['name']!=$name && $sec[$list['name']]['security']=='private') $rows[] = mk_option("", $list['name'], _(my_disk($list['name']),3)); } else { - foreach ($shares as $list) if ($list['name']!=$name && $sec[$list['name']]['security']=='private') $rows[] = mk_option("", $list['name'], compress($list['name'])); + foreach ($shares as $list) if (strpos($list['name'],"'") === false && $list['name']!=$name && $sec[$list['name']]['security']=='private') $rows[] = mk_option("", $list['name'], compress($list['name'])); } if ($rows) echo ""; foreach ($rows as $row) echo $row; @@ -267,7 +267,7 @@ $(function() { checkShareSettingsSMB(document.smb_edit); initDropdownSMB(false); - $('#tab'+$('input[name$="tabs"]').length).bind({click:function(){initDropdownSMB(true);}}); + $('#tab'+$('.tabs-container button').length).bind({click:function(){initDropdownSMB(true);}}); toggleButton('readusersmb',true); diff --git a/emhttp/plugins/dynamix/ShareEdit.page b/emhttp/plugins/dynamix/ShareEdit.page index 73f9ec998..53cc9cde0 100755 --- a/emhttp/plugins/dynamix/ShareEdit.page +++ b/emhttp/plugins/dynamix/ShareEdit.page @@ -327,7 +327,7 @@ function direction() { $myDisks = array_filter(array_diff(array_keys(array_filter($disks,'my_disks')), explode(',',$var['shareUserExclude'])), 'globalInclude'); $filteredShares = array_filter($shares, function($list) use ($name) { - return $list['name'] != $name || !$name; + return (strpos($list['name'],"'") === false) && ($list['name'] != $name || !$name) ; }); ?> :share_edit_global1_help: diff --git a/emhttp/plugins/dynamix/ShareSettings.page b/emhttp/plugins/dynamix/ShareSettings.page index 346a63544..a61a05f00 100644 --- a/emhttp/plugins/dynamix/ShareSettings.page +++ b/emhttp/plugins/dynamix/ShareSettings.page @@ -28,6 +28,7 @@ $fileMax = (int) file_get_contents('/proc/sys/fs/file-max'); $(function() { $('#s1').dropdownchecklist({emptyText:"_(All)_", width:, explicitClose:"..._(close)_"}); $('#s2').dropdownchecklist({emptyText:"_(None)_", width:, explicitClose:"..._(close)_"}); + $('#s3').dropdownchecklist({emptyText:"_(None)_", width:, explicitClose:"..._(close)_"}); presetShare(document.share_settings); }); @@ -47,6 +48,7 @@ async function prepareShare(form) { item = form.shareUserInclude.options[0]; item.value = include; item.selected = true; + var exclude = ''; for (var i=0,item; item=form.shareUserExclude.options[i]; i++) { if (item.selected) { @@ -59,6 +61,18 @@ async function prepareShare(form) { item.value = exclude; item.selected = true; + var emptying = ''; + for (var i=0,item; item=form.shareUserEmptying.options[i]; i++) { + if (item.selected) { + if (emptying.length) emptying += ','; + emptying += item.value; + item.selected = false; + } + } + item = form.shareUserEmptying.options[0]; + item.value = emptying; + item.selected = true; + /* Validate file count input against fileMax */ try { const fileCountInput = form.querySelector('#file_count'); @@ -87,8 +101,10 @@ function presetShare(form,shares) { var onOff = disabled ? 'disable':'enable'; form.shareUserInclude.disabled = disabled; form.shareUserExclude.disabled = disabled; + form.shareUserEmptying.disabled = disabled; $('#s1').dropdownchecklist(onOff); $('#s2').dropdownchecklist(onOff); + $('#s3').dropdownchecklist(onOff); }
@@ -128,6 +144,15 @@ _(Excluded disk(s))_: :shares_excluded_disks_help: +_(Emptying disk(s))_: +: + +:shares_emptying_disks_help: + _(Permit exclusive shares)_: : "+dom.hostname+"", @@ -504,9 +505,11 @@ $(document).on('mouseenter', 'a.info', function() { const tooltip = $(this).find('span'); if (tooltip.length) { const aInfoPosition = $(this).offset(); + const scrollTop = $(window).scrollTop(); + const scrollLeft = $(window).scrollLeft(); const addtionalOffset = 16; - const top = aInfoPosition.top + addtionalOffset; - const left = aInfoPosition.left + addtionalOffset; + const top = aInfoPosition.top - scrollTop + addtionalOffset; + const left = aInfoPosition.left - scrollLeft + addtionalOffset; tooltip.css({ top, left }); } }); diff --git a/emhttp/plugins/dynamix/include/Helpers.php b/emhttp/plugins/dynamix/include/Helpers.php index 1e0594956..5c3dfacc5 100644 --- a/emhttp/plugins/dynamix/include/Helpers.php +++ b/emhttp/plugins/dynamix/include/Helpers.php @@ -180,7 +180,6 @@ function my_error($code) { } function mk_option($select, $value, $text, $extra="") { - $value = htmlspecialchars($value); $text = htmlspecialchars($text); return ""; } @@ -387,7 +386,7 @@ function cpu_list() { } function my_explode($split, $text, $count=2) { - return array_pad(explode($split, $text, $count), $count, ''); + return array_pad(explode($split, $text??"", $count), $count, ''); } function my_preg_split($split, $text, $count=2) { diff --git a/emhttp/plugins/dynamix/include/Report.php b/emhttp/plugins/dynamix/include/Report.php index 867a547ea..8247cc130 100644 --- a/emhttp/plugins/dynamix/include/Report.php +++ b/emhttp/plugins/dynamix/include/Report.php @@ -24,6 +24,10 @@ case 'config': $filename = $plugin ? "$config/plugins/$name/$name.cfg" : "$config/$name.cfg"; for ( $i=0;$i<2;$i++) { if (($need && !file_exists($filename)) || (file_exists($filename) && !@parse_ini_file($filename))) { + if ( ! $need && $plugin && @filesize($filename) === 0) { + $flag = 0; + break; + } $flag = 1; sleep(1); } else { @@ -33,6 +37,9 @@ case 'config': } if ($flag) break; } + if ($flag) { + my_logger("$filename corrupted or missing"); + } echo $flag; break; case 'notice': diff --git a/emhttp/plugins/dynamix/include/Templates.php b/emhttp/plugins/dynamix/include/Templates.php index 8532684f5..b06c5d803 100644 --- a/emhttp/plugins/dynamix/include/Templates.php +++ b/emhttp/plugins/dynamix/include/Templates.php @@ -21,7 +21,8 @@ _(New folder name)_:   : -
_(This creates a folder at the current level)_
+  +:
_(This creates a folder at the current level)_
@@ -31,7 +32,8 @@ _(Folder name)_:   : -
+  +:
@@ -47,19 +49,14 @@ _(New folder name)_:   : -
_(This renames the folder to the new name)_
+  +:
_(This renames the folder to the new name)_
_(Source folder)_: : -  -: _(copy to)_ ... - -_(Target folder)_: -: -   : -
+  +: _(copy to)_ ... + +
 
+
+
+
+ +_(Target folder)_: +:
_(Source folder)_: : -  -: _(move to)_ ... - -_(Target folder)_: -: -   : -
+  +: _(move to)_ ... + +
 
+
+
+
+ +_(Target folder)_: +:
@@ -109,7 +118,8 @@ _(File name)_:   : -
_(This deletes the selected file)_
+  +:
_(This deletes the selected file)_
@@ -125,19 +135,14 @@ _(New file name)_:   : -
_(This renames the selected file)_
+  +:
_(This renames the selected file)_
_(Source file)_: : -  -: _(copy to)_ ... - -_(Target file)_: -: -   : -
_(This copies the selected file)_
+  +: _(copy to)_ ... + +
 
+
+
_(This copies the selected file)_
+
+ +_(Target file)_: +:
_(Source file)_: : -  -: _(move to)_ ... - -_(Target file)_: -: -   : -
_(This moves the selected file)_
+  +: _(move to)_ ... + +
 
+
+
_(This moves the selected file)_
+
+ +_(Target file)_: +:
@@ -187,7 +204,8 @@ _(Source)_:   : -
_(This deletes all selected sources)_
+  +:
_(This deletes all selected sources)_
@@ -200,19 +218,14 @@ _(Source)_: _(Target)_: : -
_(This renames the selected source)_
+  +:
_(This renames the selected source)_
_(Source)_: : -  -: _(copy to)_ ... - -_(Target)_: -: -   : -
_(This copies all the selected sources)_
+  +: _(copy to)_ ... + +
 
+
+
_(This copies all the selected sources)_
+
+ +_(Target)_: +:
_(Source)_: : -  -: _(move to)_ ... - -_(Target)_: -: -   : -
_(This moves all the selected sources)_
+  +: _(move to)_ ... + +
 
+
+
_(This moves all the selected sources)_
+
+ +_(Target)_: +:
diff --git a/emhttp/plugins/dynamix/include/publish.php b/emhttp/plugins/dynamix/include/publish.php index 880f941f6..b50ff0286 100755 --- a/emhttp/plugins/dynamix/include/publish.php +++ b/emhttp/plugins/dynamix/include/publish.php @@ -83,7 +83,6 @@ function publish($endpoint, $message, $len=1, $abort=false, $abortTime=30) { $abortStart[$endpoint] = time(); if ( (time() - $abortStart[$endpoint]) > $abortTime) { $script = removeNChanScript(); - my_logger("$script timed out after $abortTime seconds. Exiting.", 'publish'); exit(); } $reply = false; // if no subscribers, force return value to false diff --git a/emhttp/plugins/dynamix/include/timezones.key b/emhttp/plugins/dynamix/include/timezones.key index a68c34e42..45db382ed 100644 --- a/emhttp/plugins/dynamix/include/timezones.key +++ b/emhttp/plugins/dynamix/include/timezones.key @@ -42,7 +42,7 @@ Europe/Athens|(UTC+02:00) Athens, Bucharest Asia/Beirut|(UTC+02:00) Beirut Africa/Cairo|(UTC+02:00) Cairo Africa/Johannesburg|(UTC+02:00) Harare, Pretoria -Europe/Kiev|(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius +Europe/Kyiv|(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius Europe/Istanbul|(UTC+02:00) Istanbul Asia/Jerusalem|(UTC+02:00) Jerusalem Asia/Baghdad|(UTC+03:00) Baghdad diff --git a/emhttp/plugins/dynamix/include/update.wireguard.php b/emhttp/plugins/dynamix/include/update.wireguard.php index 5454f393e..10edd7afd 100644 --- a/emhttp/plugins/dynamix/include/update.wireguard.php +++ b/emhttp/plugins/dynamix/include/update.wireguard.php @@ -541,8 +541,9 @@ case 'import': $vpn = (in_array($default4,$vpn) || in_array($default6,$vpn)) ? 8 : 0; if ($vpn==8) $import["Address:$n"] = ''; $import["TYPE:$n"] = $vpn; - ipfilter(_var($import,"AllowedIPs:$n")); - if (_var($import,"TYPE:$n") == 0) $var['subnets1'] = "AllowedIPs="._var($import,"AllowedIPs:$n"); + $allowedIPs = _var($import, "AllowedIPs:$n"); + ipfilter($allowedIPs); + if (_var($import,"TYPE:$n") == 0) $var['subnets1'] = "AllowedIPs=$allowedIPs"; } foreach ($import as $key => $val) $sort[] = explode(':',$key)[1]; array_multisort($sort, $import); diff --git a/emhttp/plugins/dynamix/javascript/dynamix.js b/emhttp/plugins/dynamix/javascript/dynamix.js index 0293f6147..31059cd46 100644 --- a/emhttp/plugins/dynamix/javascript/dynamix.js +++ b/emhttp/plugins/dynamix/javascript/dynamix.js @@ -54,8 +54,8 @@ Q.find=(function(){var aP=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][ /* Modified by Andrew Zawadzki - Changed variable code to be codeAZ due to conflict with const code in vue */ shortcut={all_shortcuts:{},add:function(l,p,i){var e={type:"keydown",propagate:!1,disable_in_input:!1,target:document,keycode:!1};if(i)for(var t in e)void 0===i[t]&&(i[t]=e[t]);else i=e;var a=i.target;"string"==typeof i.target&&(a=document.getElementById(i.target));l=l.toLowerCase();function r(e){var t;if(e=e||window.event,!i.disable_in_input||(e.target?t=e.target:e.srcElement&&(t=e.srcElement),3==t.nodeType&&(t=t.parentNode),"INPUT"!=t.tagName&&"TEXTAREA"!=t.tagName)){e.keyCode?codeAZ=e.keyCode:e.which&&(codeAZ=e.which);var a=String.fromCharCode(codeAZ).toLowerCase();188==codeAZ&&(a=","),190==codeAZ&&(a=".");var r=l.split("+"),n=0,s={"`":"~",1:"!",2:"@",3:"#",4:"$",5:"%",6:"^",7:"&",8:"*",9:"(",0:")","-":"_","=":"+",";":":","'":'"',",":"<",".":">","/":"?","\\":"|"},o={esc:27,escape:27,tab:9,space:32,return:13,enter:13,backspace:8,scrolllock:145,scroll_lock:145,scroll:145,capslock:20,caps_lock:20,caps:20,numlock:144,num_lock:144,num:144,pause:19,break:19,insert:45,home:36,delete:46,end:35,pageup:33,page_up:33,pu:33,pagedown:34,page_down:34,pd:34,left:37,up:38,right:39,down:40,f1:112,f2:113,f3:114,f4:115,f5:116,f6:117,f7:118,f8:119,f9:120,f10:121,f11:122,f12:123},d={shift:{wanted:!1,pressed:!1},ctrl:{wanted:!1,pressed:!1},alt:{wanted:!1,pressed:!1},meta:{wanted:!1,pressed:!1}};e.ctrlKey&&(d.ctrl.pressed=!0),e.shiftKey&&(d.shift.pressed=!0),e.altKey&&(d.alt.pressed=!0),e.metaKey&&(d.meta.pressed=!0);for(var c=0;k=r[c],cRead More',lessLink:'Close',embedCSS:!0,sectionCSS:"display: block; width: 100%;",startOpen:!1,expandedClass:"readmore-js-expanded",collapsedClass:"readmore-js-collapsed",beforeToggle:function(){},afterToggle:function(){}},k=!1;g.prototype={init:function(){var b=this;c(this.element).each(function(){var a=c(this),d=a.css("max-height").replace(/[^-\d\.]/g,"")>a.data("max-height")?a.css("max-height").replace(/[^-\d\.]/g,""):a.data("max-height"),e=a.data("height-margin");"none"!=a.css("max-height")&&a.css("max-height","none");b.setBoxHeight(a);if(a.outerHeight(!0)<=d+e)return!0;a.addClass("readmore-js-section "+b.options.collapsedClass).data("collapsedHeight",d);a.after(c(b.options.startOpen?b.options.lessLink:b.options.moreLink).on("click",function(c){b.toggleSlider(this,a,c)}).addClass("readmore-js-toggle"));b.options.startOpen||a.css({height:d})});c(window).on("resize",function(a){b.resizeBoxes()})},toggleSlider:function(b,a,d){d.preventDefault();var e=this;d=newLink=sectionClass="";var f=!1;d=c(a).data("collapsedHeight");c(a).height()<=d?(d=c(a).data("expandedHeight")+"px",newLink="lessLink",f=!0,sectionClass=e.options.expandedClass):(newLink="moreLink",sectionClass=e.options.collapsedClass);e.options.beforeToggle(b,a,f);c(a).animate({height:d},{duration:e.options.speed,complete:function(){e.options.afterToggle(b,a,f);c(b).replaceWith(c(e.options[newLink]).on("click",function(b){e.toggleSlider(this,a,b)}).addClass("readmore-js-toggle"));c(this).removeClass(e.options.collapsedClass+" "+e.options.expandedClass).addClass(sectionClass)}})},setBoxHeight:function(b){var a=b.clone().css({height:"auto",width:b.width(),overflow:"hidden"}).insertAfter(b),c=a.outerHeight(!0);a.remove();b.data("expandedHeight",c)},resizeBoxes:function(){var b=this;c(".readmore-js-section").each(function(){var a=c(this);b.setBoxHeight(a);(a.height()>a.data("expandedHeight")||a.hasClass(b.options.expandedClass)&&a.height()Read More',lessLink:'Close',embedCSS:!0,sectionCSS:"display: block; width: 100%;",startOpen:!1,expandedClass:"readmore-js-expanded",collapsedClass:"readmore-js-collapsed",beforeToggle:function(){},afterToggle:function(){}},k=!1,resizeTimer=null,measuredHeights=new WeakMap();g.prototype={init:function(){var b=this;c(this.element).each(function(){var a=c(this);if(a.hasClass("readmore-js-section")){return}var d=a.css("max-height").replace(/[^-\d\.]/g,"")>a.data("max-height")?a.css("max-height").replace(/[^-\d\.]/g,""):a.data("max-height"),e=a.data("height-margin");"none"!=a.css("max-height")&&a.css("max-height","none");a.css({height:"auto",overflow:"visible"});var expandedHeight=a.outerHeight(!0);if(expandedHeight<=d+e){a.css({height:"",overflow:""});return!0}a.addClass("readmore-js-section "+b.options.collapsedClass).data("collapsedHeight",d).data("expandedHeight",expandedHeight);if(!b.options.startOpen){a.css({height:d+"px",overflow:"hidden"})}else{a.css({height:expandedHeight+"px"})}var toggle=c(b.options.startOpen?b.options.lessLink:b.options.moreLink).on("click",function(c){b.toggleSlider(this,a,c)}).addClass("readmore-js-toggle");a.after(toggle)});c(window).off("resize.readmore").on("resize.readmore",function(a){clearTimeout(resizeTimer);resizeTimer=setTimeout(function(){b.resizeBoxes()},250)})},toggleSlider:function(b,a,d){d.preventDefault();var e=this;var collapsed=a.hasClass(e.options.collapsedClass);var newHeight=collapsed?a.data("expandedHeight"):a.data("collapsedHeight");if(!newHeight||newHeight==0){a.css({height:"auto",overflow:"visible"});newHeight=collapsed?a.outerHeight(!0):a.data("collapsedHeight");if(collapsed)a.data("expandedHeight",newHeight)}var newLink=collapsed?e.options.lessLink:e.options.moreLink;var sectionClass=collapsed?e.options.expandedClass:e.options.collapsedClass;e.options.beforeToggle(b,a,collapsed);a.css({height:newHeight+"px",overflow:collapsed?"visible":"hidden"});a.removeClass(e.options.collapsedClass+" "+e.options.expandedClass).addClass(sectionClass);var newToggle=c(newLink).on("click",function(c){e.toggleSlider(this,a,c)}).addClass("readmore-js-toggle");c(b).replaceWith(newToggle);e.options.afterToggle(b,a,collapsed)},resizeBoxes:function(){var b=this;c(".readmore-js-section").each(function(){var a=c(this);a.css({height:"auto",overflow:"visible"});var expandedHeight=a.outerHeight(!0);a.data("expandedHeight",expandedHeight);if(a.hasClass(b.options.expandedClass)){a.css({height:expandedHeight+"px"})}else if(a.hasClass(b.options.collapsedClass)){a.css({height:a.data("collapsedHeight")+"px",overflow:"hidden"})}})},destroy:function(){var b=this;c(window).off("resize.readmore");c(this.element).each(function(){var a=c(this);a.removeClass("readmore-js-section "+b.options.collapsedClass+" "+b.options.expandedClass).css({"max-height":"",height:"",overflow:""}).next(".readmore-js-toggle").remove();a.removeData();measuredHeights.delete(a[0])})}};c.fn[f]=function(b){var a=arguments;if(void 0===b||"object"===typeof b)return this.each(function(){if(c.data(this,"plugin_"+f)){var a=c.data(this,"plugin_"+f);a.destroy.apply(a)}c.data(this,"plugin_"+f,new g(this,b))});if("string"===typeof b&&"_"!==b[0]&&"init"!==b)return this.each(function(){var d=c.data(this,"plugin_"+f);d instanceof g&&"function"===typeof d[b]&&d[b].apply(d,Array.prototype.slice.call(a,1))})}})(jQuery); /** * Simple, lightweight, usable local autocomplete library for modern browsers * Because there weren’t enough autocomplete scripts in the world? Because I’m completely insane and have NIH syndrome? Probably both. :P diff --git a/emhttp/plugins/dynamix/javascript/smoothie.js b/emhttp/plugins/dynamix/javascript/smoothie.js new file mode 100644 index 000000000..2af8f1580 --- /dev/null +++ b/emhttp/plugins/dynamix/javascript/smoothie.js @@ -0,0 +1,1176 @@ +;(function(exports) { + +/** + * @license + * MIT License: + * + * Copyright (c) 2010-2013, Joe Walnes + * 2013-2018, Drew Noakes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Smoothie Charts - http://smoothiecharts.org/ + * (c) 2010-2013, Joe Walnes + * 2013-2018, Drew Noakes + * + * v1.0: Main charting library, by Joe Walnes + * v1.1: Auto scaling of axis, by Neil Dunn + * v1.2: fps (frames per second) option, by Mathias Petterson + * v1.3: Fix for divide by zero, by Paul Nikitochkin + * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds + * v1.5: Set default frames per second to 50... smoother. + * .start(), .stop() methods for conserving CPU, by Dmitry Vyal + * options.interpolation = 'bezier' or 'line', by Dmitry Vyal + * options.maxValue to fix scale, by Dmitry Vyal + * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla + * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin + * Smooth rescaling, by Kostas Michalopoulos + * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni + * v1.9: Display timestamps along the bottom, by Nick and Stev-io + * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D) + * Refactored by Krishna Narni, to support timestamp formatting function + * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh + * v1.11: options.grid.sharpLines option added, by @drewnoakes + * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes + * v1.12: Support for horizontalLines added, by @drewnoakes + * Support for yRangeFunction callback added, by @drewnoakes + * v1.13: Fixed typo (#32), by @alnikitich + * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano + * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes + * v1.15: Support for npm package (#18), by @dominictarr + * Fixed broken removeTimeSeries function (#24) by @davidgaleano + * Minor performance and tidying, by @drewnoakes + * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes + * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12) + * Documentation and some local variable renaming for clarity, by @drewnoakes + * v1.17: Allow control over font size (#10), by @drewnoakes + * Timestamp text won't overlap, by @drewnoakes + * v1.18: Allow control of max/min label precision, by @drewnoakes + * Added 'borderVisible' chart option, by @drewnoakes + * Allow drawing series with fill but no stroke (line), by @drewnoakes + * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai + * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes + * v1.21: Add 'step' interpolation mode, by @drewnoakes + * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic + * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes + * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf + * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92 + * Draw time labels on top of series, by @comolosabia + * Add TimeSeries.clear function, by @drewnoakes + * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic + * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush + * v1.28: Add 'minValueScale' option, by @megawac + * Fix 'labelPos' for different size of 'minValueString' 'maxValueString', by @henryn + * v1.29: Support responsive sizing, by @drewnoakes + * v1.29.1: Include types in package, and make property optional, by @TrentHouliston + * v1.30: Fix inverted logic in devicePixelRatio support, by @scanlime + * v1.31: Support tooltips, by @Sly1024 and @drewnoakes + * v1.32: Support frame rate limit, by @dpuyosa + * v1.33: Use Date static method instead of instance, by @nnnoel + * Fix bug with tooltips when multiple charts on a page, by @jpmbiz70 + * v1.34: Add disabled option to TimeSeries, by @TechGuard (#91) + * Add nonRealtimeData option, by @annazhelt (#92, #93) + * Add showIntermediateLabels option, by @annazhelt (#94) + * Add displayDataFromPercentile option, by @annazhelt (#95) + * Fix bug when hiding tooltip element, by @ralphwetzel (#96) + * Support intermediate y-axis labels, by @beikeland (#99) + * v1.35: Fix issue with responsive mode at high DPI, by @drewnoakes (#101) + * v1.36: Add tooltipLabel to ITimeSeriesPresentationOptions. + * If tooltipLabel is present, tooltipLabel displays inside tooltip + * next to value, by @jackdesert (#102) + * Fix bug rendering issue in series fill when using scroll backwards, by @olssonfredrik + * Add title option, by @mesca + * Fix data drop stoppage by rejecting NaNs in append(), by @timdrysdale + * Allow setting interpolation per time series, by @WofWca (#123) + * Fix chart constantly jumping in 1-2 pixel steps, by @WofWca (#131) + * Fix a memory leak appearing when some `timeSeries.disabled === true`, by @WofWca (#132) + * Fix: make all lines sharp, remove the `grid.sharpLines` option by @WofWca (#134) + * Improve performance, by @WofWca (#135) + * Fix `this.delay` not being respected with `nonRealtimeData: true`, by @WofWca (#137) + * Fix series fill & stroke being inconsistent for last data time < render time, by @WofWca (#138) + * v1.36.1: Fix a potential XSS when `tooltipLabel` or `strokeStyle` are controlled by users, by @WofWca + * v1.36.2: fix: 1px lines jumping 1px left and right at rational `millisPerPixel`, by @WofWca + * perf: improve `render()` performane a bit, by @WofWca + * v1.37: Add `fillToBottom` option to fill timeSeries to 0 instead of to the bottom of the canvas, by @socketpair & @WofWca (#140) + */ + + // Date.now polyfill + Date.now = Date.now || function() { return new Date().getTime(); }; + + var Util = { + extend: function() { + arguments[0] = arguments[0] || {}; + for (var i = 1; i < arguments.length; i++) + { + for (var key in arguments[i]) + { + if (arguments[i].hasOwnProperty(key)) + { + if (typeof(arguments[i][key]) === 'object') { + if (arguments[i][key] instanceof Array) { + arguments[0][key] = arguments[i][key]; + } else { + arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]); + } + } else { + arguments[0][key] = arguments[i][key]; + } + } + } + } + return arguments[0]; + }, + binarySearch: function(data, value) { + var low = 0, + high = data.length; + while (low < high) { + var mid = (low + high) >> 1; + if (value < data[mid][0]) + high = mid; + else + low = mid + 1; + } + return low; + }, + // So lines (especially vertical and horizontal) look a) consistent along their length and b) sharp. + pixelSnap: function(position, lineWidth) { + if (lineWidth % 2 === 0) { + // Closest pixel edge. + return Math.round(position); + } else { + // Closest pixel center. + return Math.floor(position) + 0.5; + } + }, + }; + + /** + * Initialises a new TimeSeries with optional data options. + * + * Options are of the form (defaults shown): + * + *
+   * {
+   *   resetBounds: true,        // enables/disables automatic scaling of the y-axis
+   *   resetBoundsInterval: 3000 // the period between scaling calculations, in millis
+   * }
+   * 
+ * + * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries. + * + * @constructor + */ + function TimeSeries(options) { + this.options = Util.extend({}, TimeSeries.defaultOptions, options); + this.disabled = false; + this.clear(); + } + + TimeSeries.defaultOptions = { + resetBoundsInterval: 3000, + resetBounds: true + }; + + /** + * Clears all data and state from this TimeSeries object. + */ + TimeSeries.prototype.clear = function() { + this.data = []; + this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries. + this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries. + }; + + /** + * Recalculate the min/max values for this TimeSeries object. + * + * This causes the graph to scale itself in the y-axis. + */ + TimeSeries.prototype.resetBounds = function() { + if (this.data.length) { + // Walk through all data points, finding the min/max value + this.maxValue = this.data[0][1]; + this.minValue = this.data[0][1]; + for (var i = 1; i < this.data.length; i++) { + var value = this.data[i][1]; + if (value > this.maxValue) { + this.maxValue = value; + } + if (value < this.minValue) { + this.minValue = value; + } + } + } else { + // No data exists, so set min/max to NaN + this.maxValue = Number.NaN; + this.minValue = Number.NaN; + } + }; + + /** + * Adds a new data point to the TimeSeries, preserving chronological order. + * + * @param timestamp the position, in time, of this data point + * @param value the value of this data point + * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls + * whether it is replaced, or the values summed (defaults to false.) + */ + TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) { + // Reject NaN + if (isNaN(timestamp) || isNaN(value)){ + return + } + + var lastI = this.data.length - 1; + if (lastI >= 0) { + // Rewind until we find the place for the new data + var i = lastI; + while (true) { + var iThData = this.data[i]; + if (timestamp >= iThData[0]) { + if (timestamp === iThData[0]) { + // Update existing values in the array + if (sumRepeatedTimeStampValues) { + // Sum this value into the existing 'bucket' + iThData[1] += value; + value = iThData[1]; + } else { + // Replace the previous value + iThData[1] = value; + } + } else { + // Splice into the correct position to keep timestamps in order + this.data.splice(i + 1, 0, [timestamp, value]); + } + + break; + } + + i--; + if (i < 0) { + // This new item is the oldest data + this.data.splice(0, 0, [timestamp, value]); + + break; + } + } + } else { + // It's the first element + this.data.push([timestamp, value]); + } + + this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); + this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); + }; + + TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) { + // We must always keep one expired data point as we need this to draw the + // line that comes into the chart from the left, but any points prior to that can be removed. + var removeCount = 0; + while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { + removeCount++; + } + if (removeCount !== 0) { + this.data.splice(0, removeCount); + } + }; + + /** + * Initialises a new SmoothieChart. + * + * Options are optional, and should be of the form below. Just specify the values you + * need and the rest will be given sensible defaults as shown: + * + *
+   * {
+   *   minValue: undefined,                      // specify to clamp the lower y-axis to a given value
+   *   maxValue: undefined,                      // specify to clamp the upper y-axis to a given value
+   *   maxValueScale: 1,                         // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
+   *   minValueScale: 1,                         // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
+   *   yRangeFunction: undefined,                // function({min: , max: }) { return {min: , max: }; }
+   *   scaleSmoothing: 0.125,                    // controls the rate at which y-value zoom animation occurs
+   *   millisPerPixel: 20,                       // sets the speed at which the chart pans by
+   *   enableDpiScaling: true,                   // support rendering at different DPI depending on the device
+   *   yMinFormatter: function(min, precision) { // callback function that formats the min y value label
+   *     return parseFloat(min).toFixed(precision);
+   *   },
+   *   yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
+   *     return parseFloat(max).toFixed(precision);
+   *   },
+   *   yIntermediateFormatter: function(intermediate, precision) { // callback function that formats the intermediate y value labels
+   *     return parseFloat(intermediate).toFixed(precision);
+   *   },
+   *   maxDataSetLength: 2,
+   *   interpolation: 'bezier'                   // one of 'bezier', 'linear', or 'step'
+   *   timestampFormatter: null,                 // optional function to format time stamps for bottom of chart
+   *                                             // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
+   *   scrollBackwards: false,                   // reverse the scroll direction of the chart
+   *   horizontalLines: [],                      // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
+   *   grid:
+   *   {
+   *     fillStyle: '#000000',                   // the background colour of the chart
+   *     lineWidth: 1,                           // the pixel width of grid lines
+   *     strokeStyle: '#777777',                 // colour of grid lines
+   *     millisPerLine: 1000,                    // distance between vertical grid lines
+   *     verticalSections: 2,                    // number of vertical sections marked out by horizontal grid lines
+   *     borderVisible: true                     // whether the grid lines trace the border of the chart or not
+   *   },
+   *   labels
+   *   {
+   *     disabled: false,                        // enables/disables labels showing the min/max values
+   *     fillStyle: '#ffffff',                   // colour for text of labels,
+   *     fontSize: 15,
+   *     fontFamily: 'sans-serif',
+   *     precision: 2,
+   *     showIntermediateLabels: false,          // shows intermediate labels between min and max values along y axis
+   *     intermediateLabelSameAxis: true,
+   *   },
+   *   title
+   *   {
+   *     text: '',                               // the text to display on the left side of the chart
+   *     fillStyle: '#ffffff',                   // colour for text
+   *     fontSize: 15,
+   *     fontFamily: 'sans-serif',
+   *     verticalAlign: 'middle'                 // one of 'top', 'middle', or 'bottom'
+   *   },
+   *   tooltip: false                            // show tooltip when mouse is over the chart
+   *   tooltipLine: {                            // properties for a vertical line at the cursor position
+   *     lineWidth: 1,
+   *     strokeStyle: '#BBBBBB'
+   *   },
+   *   tooltipFormatter: SmoothieChart.tooltipFormatter, // formatter function for tooltip text
+   *   nonRealtimeData: false,                   // use time of latest data as current time
+   *   displayDataFromPercentile: 1,             // display not latest data, but data from the given percentile
+   *                                             // useful when trying to see old data saved by setting a high value for maxDataSetLength
+   *                                             // should be a value between 0 and 1
+   *   responsive: false,                        // whether the chart should adapt to the size of the canvas
+   *   limitFPS: 0                               // maximum frame rate the chart will render at, in FPS (zero means no limit)
+   * }
+   * 
+ * + * @constructor + */ + function SmoothieChart(options) { + this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); + this.seriesSet = []; + this.currentValueRange = 1; + this.currentVisMinValue = 0; + this.lastRenderTimeMillis = 0; + this.lastChartTimestamp = 0; + + this.mousemove = this.mousemove.bind(this); + this.mouseout = this.mouseout.bind(this); + } + + /** Formats the HTML string content of the tooltip. */ + SmoothieChart.tooltipFormatter = function (timestamp, data) { + var timestampFormatter = this.options.timestampFormatter || SmoothieChart.timeFormatter, + // A dummy element to hold children. Maybe there's a better way. + elements = document.createElement('div'), + label; + elements.appendChild(document.createTextNode( + timestampFormatter(new Date(timestamp)) + )); + + for (var i = 0; i < data.length; ++i) { + label = data[i].series.options.tooltipLabel || '' + if (label !== ''){ + label = label + ' '; + } + var dataEl = document.createElement('span'); + dataEl.style.color = data[i].series.options.strokeStyle; + dataEl.appendChild(document.createTextNode( + label + this.options.yMaxFormatter(data[i].value, this.options.labels.precision) + )); + elements.appendChild(document.createElement('br')); + elements.appendChild(dataEl); + } + + return elements.innerHTML; + }; + + SmoothieChart.defaultChartOptions = { + millisPerPixel: 20, + enableDpiScaling: true, + yMinFormatter: function(min, precision) { + return parseFloat(min).toFixed(precision); + }, + yMaxFormatter: function(max, precision) { + return parseFloat(max).toFixed(precision); + }, + yIntermediateFormatter: function(intermediate, precision) { + return parseFloat(intermediate).toFixed(precision); + }, + maxValueScale: 1, + minValueScale: 1, + interpolation: 'bezier', + scaleSmoothing: 0.125, + maxDataSetLength: 2, + scrollBackwards: false, + displayDataFromPercentile: 1, + grid: { + fillStyle: '#000000', + strokeStyle: '#777777', + lineWidth: 2, + millisPerLine: 1000, + verticalSections: 2, + borderVisible: true + }, + labels: { + fillStyle: '#ffffff', + disabled: false, + fontSize: 10, + fontFamily: 'monospace', + precision: 2, + showIntermediateLabels: false, + intermediateLabelSameAxis: true, + }, + title: { + text: '', + fillStyle: '#ffffff', + fontSize: 15, + fontFamily: 'monospace', + verticalAlign: 'middle' + }, + horizontalLines: [], + tooltip: false, + tooltipLine: { + lineWidth: 1, + strokeStyle: '#BBBBBB' + }, + tooltipFormatter: SmoothieChart.tooltipFormatter, + nonRealtimeData: false, + responsive: false, + limitFPS: 0 + }; + + // Based on http://inspirit.github.com/jsfeat/js/compatibility.js + SmoothieChart.AnimateCompatibility = (function() { + var requestAnimationFrame = function(callback, element) { + var requestAnimationFrame = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(function() { + callback(Date.now()); + }, 16); + }; + return requestAnimationFrame.call(window, callback, element); + }, + cancelAnimationFrame = function(id) { + var cancelAnimationFrame = + window.cancelAnimationFrame || + function(id) { + clearTimeout(id); + }; + return cancelAnimationFrame.call(window, id); + }; + + return { + requestAnimationFrame: requestAnimationFrame, + cancelAnimationFrame: cancelAnimationFrame + }; + })(); + + SmoothieChart.defaultSeriesPresentationOptions = { + lineWidth: 1, + strokeStyle: '#ffffff', + // Maybe default to false in the next breaking version. + fillToBottom: true, + }; + + /** + * Adds a TimeSeries to this chart, with optional presentation options. + * + * Presentation options should be of the form (defaults shown): + * + *
+   * {
+   *   lineWidth: 1,
+   *   strokeStyle: '#ffffff',
+   *   fillStyle: undefined,
+   *   interpolation: undefined;
+   *   tooltipLabel: undefined,
+   *   fillToBottom: true,
+   * }
+   * 
+ */ + SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { + this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)}); + if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { + timeSeries.resetBoundsTimerId = setInterval( + function() { + timeSeries.resetBounds(); + }, + timeSeries.options.resetBoundsInterval + ); + } + }; + + /** + * Removes the specified TimeSeries from the chart. + */ + SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + this.seriesSet.splice(i, 1); + break; + } + } + // If a timer was operating for that timeseries, remove it + if (timeSeries.resetBoundsTimerId) { + // Stop resetting the bounds, if we were + clearInterval(timeSeries.resetBoundsTimerId); + } + }; + + /** + * Gets render options for the specified TimeSeries. + * + * As you may use a single TimeSeries in multiple charts with different formatting in each usage, + * these settings are stored in the chart. + */ + SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + return this.seriesSet[i].options; + } + } + }; + + /** + * Brings the specified TimeSeries to the top of the chart. It will be rendered last. + */ + SmoothieChart.prototype.bringToFront = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + var set = this.seriesSet.splice(i, 1); + this.seriesSet.push(set[0]); + break; + } + } + }; + + /** + * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay. + * + * @param canvas the target canvas element + * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series + * from appearing on screen, with new values flashing into view, at the expense of some latency. + */ + SmoothieChart.prototype.streamTo = function(canvas, delayMillis) { + this.canvas = canvas; + + this.clientWidth = parseInt(this.canvas.getAttribute('width')); + this.clientHeight = parseInt(this.canvas.getAttribute('height')); + + this.delay = delayMillis; + this.start(); + }; + + SmoothieChart.prototype.getTooltipEl = function () { + // Create the tool tip element lazily + if (!this.tooltipEl) { + this.tooltipEl = document.createElement('div'); + this.tooltipEl.className = 'smoothie-chart-tooltip'; + this.tooltipEl.style.pointerEvents = 'none'; + this.tooltipEl.style.position = 'absolute'; + this.tooltipEl.style.display = 'none'; + document.body.appendChild(this.tooltipEl); + } + return this.tooltipEl; + }; + + SmoothieChart.prototype.updateTooltip = function () { + if(!this.options.tooltip){ + return; + } + var el = this.getTooltipEl(); + + if (!this.mouseover || !this.options.tooltip) { + el.style.display = 'none'; + return; + } + + var time = this.lastChartTimestamp; + + // x pixel to time + var t = this.options.scrollBackwards + ? time - this.mouseX * this.options.millisPerPixel + : time - (this.clientWidth - this.mouseX) * this.options.millisPerPixel; + + var data = []; + + // For each data set... + for (var d = 0; d < this.seriesSet.length; d++) { + var timeSeries = this.seriesSet[d].timeSeries; + if (timeSeries.disabled) { + continue; + } + + // find datapoint closest to time 't' + var closeIdx = Util.binarySearch(timeSeries.data, t); + if (closeIdx > 0 && closeIdx < timeSeries.data.length) { + data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] }); + } + } + + if (data.length) { + // TODO make `tooltipFormatter` return element(s) instead of an HTML string so it's harder for users + // to introduce an XSS. This would be a breaking change. + el.innerHTML = this.options.tooltipFormatter.call(this, t, data); + el.style.display = 'block'; + } else { + el.style.display = 'none'; + } + }; + + SmoothieChart.prototype.mousemove = function (evt) { + this.mouseover = true; + this.mouseX = evt.offsetX; + this.mouseY = evt.offsetY; + this.mousePageX = evt.pageX; + this.mousePageY = evt.pageY; + if(!this.options.tooltip){ + return; + } + var el = this.getTooltipEl(); + el.style.top = Math.round(this.mousePageY) + 'px'; + el.style.left = Math.round(this.mousePageX) + 'px'; + this.updateTooltip(); + }; + + SmoothieChart.prototype.mouseout = function () { + this.mouseover = false; + this.mouseX = this.mouseY = -1; + if (this.tooltipEl) + this.tooltipEl.style.display = 'none'; + }; + + /** + * Make sure the canvas has the optimal resolution for the device's pixel ratio. + */ + SmoothieChart.prototype.resize = function () { + var dpr = !this.options.enableDpiScaling || !window ? 1 : window.devicePixelRatio, + width, height; + if (this.options.responsive) { + // Newer behaviour: Use the canvas's size in the layout, and set the internal + // resolution according to that size and the device pixel ratio (eg: high DPI) + width = this.canvas.offsetWidth; + height = this.canvas.offsetHeight; + + if (width !== this.lastWidth) { + this.lastWidth = width; + this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); + this.canvas.getContext('2d').scale(dpr, dpr); + } + if (height !== this.lastHeight) { + this.lastHeight = height; + this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); + this.canvas.getContext('2d').scale(dpr, dpr); + } + + this.clientWidth = width; + this.clientHeight = height; + } else { + width = parseInt(this.canvas.getAttribute('width')); + height = parseInt(this.canvas.getAttribute('height')); + + if (dpr !== 1) { + // Older behaviour: use the canvas's inner dimensions and scale the element's size + // according to that size and the device pixel ratio (eg: high DPI) + + if (Math.floor(this.clientWidth * dpr) !== width) { + this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); + this.canvas.style.width = width + 'px'; + this.clientWidth = width; + this.canvas.getContext('2d').scale(dpr, dpr); + } + + if (Math.floor(this.clientHeight * dpr) !== height) { + this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); + this.canvas.style.height = height + 'px'; + this.clientHeight = height; + this.canvas.getContext('2d').scale(dpr, dpr); + } + } else { + this.clientWidth = width; + this.clientHeight = height; + } + } + }; + + /** + * Starts the animation of this chart. + */ + SmoothieChart.prototype.start = function() { + if (this.frame) { + // We're already running, so just return + return; + } + + this.canvas.addEventListener('mousemove', this.mousemove); + this.canvas.addEventListener('mouseout', this.mouseout); + + // Renders a frame, and queues the next frame for later rendering + var animate = function() { + this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() { + if(this.options.nonRealtimeData){ + var dateZero = new Date(0); + // find the data point with the latest timestamp + var maxTimeStamp = this.seriesSet.reduce(function(max, series){ + var dataSet = series.timeSeries.data; + var indexToCheck = Math.round(this.options.displayDataFromPercentile * dataSet.length) - 1; + indexToCheck = indexToCheck >= 0 ? indexToCheck : 0; + indexToCheck = indexToCheck <= dataSet.length -1 ? indexToCheck : dataSet.length -1; + if(dataSet && dataSet.length > 0) + { + // timestamp corresponds to element 0 of the data point + var lastDataTimeStamp = dataSet[indexToCheck][0]; + max = max > lastDataTimeStamp ? max : lastDataTimeStamp; + } + return max; + }.bind(this), dateZero); + // use the max timestamp as current time + this.render(this.canvas, maxTimeStamp > dateZero ? maxTimeStamp : null); + } else { + this.render(); + } + animate(); + }.bind(this)); + }.bind(this); + + animate(); + }; + + /** + * Stops the animation of this chart. + */ + SmoothieChart.prototype.stop = function() { + if (this.frame) { + SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); + delete this.frame; + this.canvas.removeEventListener('mousemove', this.mousemove); + this.canvas.removeEventListener('mouseout', this.mouseout); + } + }; + + SmoothieChart.prototype.updateValueRange = function() { + // Calculate the current scale of the chart, from all time series. + var chartOptions = this.options, + chartMaxValue = Number.NaN, + chartMinValue = Number.NaN; + + for (var d = 0; d < this.seriesSet.length; d++) { + // TODO(ndunn): We could calculate / track these values as they stream in. + var timeSeries = this.seriesSet[d].timeSeries; + if (timeSeries.disabled) { + continue; + } + + if (!isNaN(timeSeries.maxValue)) { + chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; + } + + if (!isNaN(timeSeries.minValue)) { + chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; + } + } + + // Scale the chartMaxValue to add padding at the top if required + if (chartOptions.maxValue != null) { + chartMaxValue = chartOptions.maxValue; + } else { + chartMaxValue *= chartOptions.maxValueScale; + } + + // Set the minimum if we've specified one + if (chartOptions.minValue != null) { + chartMinValue = chartOptions.minValue; + } else { + chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue); + } + + // If a custom range function is set, call it + if (this.options.yRangeFunction) { + var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue}); + chartMinValue = range.min; + chartMaxValue = range.max; + } + + if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { + var targetValueRange = chartMaxValue - chartMinValue; + var valueRangeDiff = (targetValueRange - this.currentValueRange); + var minValueDiff = (chartMinValue - this.currentVisMinValue); + this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1; + this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff; + this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff; + } + + this.valueRange = { min: chartMinValue, max: chartMaxValue }; + }; + + SmoothieChart.prototype.render = function(canvas, time) { + var chartOptions = this.options, + nowMillis = Date.now(); + + // Respect any frame rate limit. + if (chartOptions.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < (1000/chartOptions.limitFPS)) + return; + + time = (time || nowMillis) - (this.delay || 0); + + // Round time down to pixel granularity, so that pixel sample values remain the same, + // just shifted 1px to the left, so motion appears smoother. + time -= time % chartOptions.millisPerPixel; + + if (!this.isAnimatingScale) { + // We're not animating. We can use the last render time and the scroll speed to work out whether + // we actually need to paint anything yet. If not, we can return immediately. + var sameTime = this.lastChartTimestamp === time; + if (sameTime) { + // Render at least every 1/6th of a second. The canvas may be resized, which there is + // no reliable way to detect. + var needToRenderInCaseCanvasResized = nowMillis - this.lastRenderTimeMillis > 1000/6; + if (!needToRenderInCaseCanvasResized) { + return; + } + } + } + + this.lastRenderTimeMillis = nowMillis; + this.lastChartTimestamp = time; + + this.resize(); + + canvas = canvas || this.canvas; + var context = canvas.getContext('2d'), + // Using `this.clientWidth` instead of `canvas.clientWidth` because the latter is slow. + dimensions = { top: 0, left: 0, width: this.clientWidth, height: this.clientHeight }, + // Calculate the threshold time for the oldest data points. + oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel), + valueToYPosition = function(value, lineWidth) { + var offset = value - this.currentVisMinValue, + unsnapped = this.currentValueRange === 0 + ? dimensions.height + : dimensions.height * (1 - offset / this.currentValueRange); + return Util.pixelSnap(unsnapped, lineWidth); + }.bind(this), + timeToXPosition = function(t, lineWidth) { + // Why not write it as `(time - t) / chartOptions.millisPerPixel`: + // If a datapoint's `t` is very close or is at the center of a pixel, that expression, + // due to floating point error, may take value whose `% 1` sometimes is very close to + // 0 and sometimes is close to 1, depending on the value of render time (`time`), + // which would make `pixelSnap` snap it sometimes to the right and sometimes to the left, + // which would look like it's jumping. + // You can try the default examples, with `millisPerPixel = 100 / 3` and + // `grid.lineWidth = 1`. The grid would jump. + // Writing it this way seems to avoid such inconsistency because in the above example + // `offset` is (almost?) always a whole number. + // TODO Maybe there's a more elegant (and reliable?) way. + var offset = time / chartOptions.millisPerPixel - t / chartOptions.millisPerPixel; + var unsnapped = chartOptions.scrollBackwards + ? offset + : dimensions.width - offset; + return Util.pixelSnap(unsnapped, lineWidth); + }; + + this.updateValueRange(); + + context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; + + // Save the state of the canvas context, any transformations applied in this method + // will get removed from the stack at the end of this method when .restore() is called. + context.save(); + + // Move the origin. + context.translate(dimensions.left, dimensions.top); + + // Create a clipped rectangle - anything we draw will be constrained to this rectangle. + // This prevents the occasional pixels from curves near the edges overrunning and creating + // screen cheese (that phrase should need no explanation). + context.beginPath(); + context.rect(0, 0, dimensions.width, dimensions.height); + context.clip(); + + // Clear the working area. + context.save(); + context.fillStyle = chartOptions.grid.fillStyle; + context.clearRect(0, 0, dimensions.width, dimensions.height); + context.fillRect(0, 0, dimensions.width, dimensions.height); + context.restore(); + + // Grid lines... + context.save(); + context.lineWidth = chartOptions.grid.lineWidth; + context.strokeStyle = chartOptions.grid.strokeStyle; + // Vertical (time) dividers. + if (chartOptions.grid.millisPerLine > 0) { + context.beginPath(); + for (var t = time - (time % chartOptions.grid.millisPerLine); + t >= oldestValidTime; + t -= chartOptions.grid.millisPerLine) { + var gx = timeToXPosition(t, chartOptions.grid.lineWidth); + context.moveTo(gx, 0); + context.lineTo(gx, dimensions.height); + } + context.stroke(); + } + + // Horizontal (value) dividers. + for (var v = 1; v < chartOptions.grid.verticalSections; v++) { + var gy = Util.pixelSnap(v * dimensions.height / chartOptions.grid.verticalSections, chartOptions.grid.lineWidth); + context.beginPath(); + context.moveTo(0, gy); + context.lineTo(dimensions.width, gy); + context.stroke(); + } + // Bounding rectangle. + if (chartOptions.grid.borderVisible) { + context.strokeRect(0, 0, dimensions.width, dimensions.height); + } + context.restore(); + + // Draw any horizontal lines... + if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { + for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { + var line = chartOptions.horizontalLines[hl], + lineWidth = line.lineWidth || 1, + hly = valueToYPosition(line.value, lineWidth); + context.strokeStyle = line.color || '#ffffff'; + context.lineWidth = lineWidth; + context.beginPath(); + context.moveTo(0, hly); + context.lineTo(dimensions.width, hly); + context.stroke(); + } + } + + // For each data set... + for (var d = 0; d < this.seriesSet.length; d++) { + var timeSeries = this.seriesSet[d].timeSeries, + dataSet = timeSeries.data; + + // Delete old data that's moved off the left of the chart. + timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); + if (dataSet.length <= 1 || timeSeries.disabled) { + continue; + } + context.save(); + + var seriesOptions = this.seriesSet[d].options, + // Keep in mind that `context.lineWidth = 0` doesn't actually set it to `0`. + drawStroke = seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none', + lineWidthMaybeZero = drawStroke ? seriesOptions.lineWidth : 0; + + // Draw the line... + context.beginPath(); + // Retain lastX, lastY for calculating the control points of bezier curves. + var firstX = timeToXPosition(dataSet[0][0], lineWidthMaybeZero), + firstY = valueToYPosition(dataSet[0][1], lineWidthMaybeZero), + lastX = firstX, + lastY = firstY, + draw; + context.moveTo(firstX, firstY); + switch (seriesOptions.interpolation || chartOptions.interpolation) { + case "linear": + case "line": { + draw = function(x, y, lastX, lastY) { + context.lineTo(x,y); + } + break; + } + case "bezier": + default: { + // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves + // + // Assuming A was the last point in the line plotted and B is the new point, + // we draw a curve with control points P and Q as below. + // + // A---P + // | + // | + // | + // Q---B + // + // Importantly, A and P are at the same y coordinate, as are B and Q. This is + // so adjacent curves appear to flow as one. + // + draw = function(x, y, lastX, lastY) { + context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop + Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) + Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) + x, y); // endPoint (B) + } + break; + } + case "step": { + draw = function(x, y, lastX, lastY) { + context.lineTo(x,lastY); + context.lineTo(x,y); + } + break; + } + } + + for (var i = 1; i < dataSet.length; i++) { + var iThData = dataSet[i], + x = timeToXPosition(iThData[0], lineWidthMaybeZero), + y = valueToYPosition(iThData[1], lineWidthMaybeZero); + draw(x, y, lastX, lastY); + lastX = x; lastY = y; + } + + if (drawStroke) { + context.lineWidth = seriesOptions.lineWidth; + context.strokeStyle = seriesOptions.strokeStyle; + context.stroke(); + } + + if (seriesOptions.fillStyle) { + // Close up the fill region. + var fillEndY = seriesOptions.fillToBottom + ? dimensions.height + lineWidthMaybeZero + 1 + : valueToYPosition(0, 0); + context.lineTo(lastX, fillEndY); + context.lineTo(firstX, fillEndY); + + context.fillStyle = seriesOptions.fillStyle; + context.fill(); + } + + context.restore(); + } + + if (chartOptions.tooltip && this.mouseX >= 0) { + // Draw vertical bar to show tooltip position + context.lineWidth = chartOptions.tooltipLine.lineWidth; + context.strokeStyle = chartOptions.tooltipLine.strokeStyle; + context.beginPath(); + context.moveTo(this.mouseX, 0); + context.lineTo(this.mouseX, dimensions.height); + context.stroke(); + } + this.updateTooltip(); + + var labelsOptions = chartOptions.labels; + // Draw the axis values on the chart. + if (!labelsOptions.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { + var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, labelsOptions.precision), + minValueString = chartOptions.yMinFormatter(this.valueRange.min, labelsOptions.precision), + maxLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2, + minLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(minValueString).width - 2; + context.fillStyle = labelsOptions.fillStyle; + context.fillText(maxValueString, maxLabelPos, labelsOptions.fontSize); + context.fillText(minValueString, minLabelPos, dimensions.height - 2); + } + + // Display intermediate y axis labels along y-axis to the left of the chart + if ( labelsOptions.showIntermediateLabels + && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max) + && chartOptions.grid.verticalSections > 0) { + // show a label above every vertical section divider + var step = (this.valueRange.max - this.valueRange.min) / chartOptions.grid.verticalSections; + var stepPixels = dimensions.height / chartOptions.grid.verticalSections; + for (var v = 1; v < chartOptions.grid.verticalSections; v++) { + var gy = dimensions.height - Math.round(v * stepPixels), + yValue = chartOptions.yIntermediateFormatter(this.valueRange.min + (v * step), labelsOptions.precision), + //left of right axis? + intermediateLabelPos = + labelsOptions.intermediateLabelSameAxis + ? (chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(yValue).width - 2) + : (chartOptions.scrollBackwards ? dimensions.width - context.measureText(yValue).width - 2 : 0); + + context.fillText(yValue, intermediateLabelPos, gy - chartOptions.grid.lineWidth); + } + } + + // Display timestamps along x-axis at the bottom of the chart. + if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) { + var textUntilX = chartOptions.scrollBackwards + ? context.measureText(minValueString).width + : dimensions.width - context.measureText(minValueString).width + 4; + for (var t = time - (time % chartOptions.grid.millisPerLine); + t >= oldestValidTime; + t -= chartOptions.grid.millisPerLine) { + var gx = timeToXPosition(t, 0); + // Only draw the timestamp if it won't overlap with the previously drawn one. + if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) { + // Formats the timestamp based on user specified formatting function + // SmoothieChart.timeFormatter function above is one such formatting option + var tx = new Date(t), + ts = chartOptions.timestampFormatter(tx), + tsWidth = context.measureText(ts).width; + + textUntilX = chartOptions.scrollBackwards + ? gx + tsWidth + 2 + : gx - tsWidth - 2; + + context.fillStyle = chartOptions.labels.fillStyle; + if(chartOptions.scrollBackwards) { + context.fillText(ts, gx, dimensions.height - 2); + } else { + context.fillText(ts, gx - tsWidth, dimensions.height - 2); + } + } + } + } + + // Display title. + if (chartOptions.title.text !== '') { + context.font = chartOptions.title.fontSize + 'px ' + chartOptions.title.fontFamily; + var titleXPos = chartOptions.scrollBackwards ? dimensions.width - context.measureText(chartOptions.title.text).width - 2 : 2; + if (chartOptions.title.verticalAlign == 'bottom') { + context.textBaseline = 'bottom'; + var titleYPos = dimensions.height; + } else if (chartOptions.title.verticalAlign == 'middle') { + context.textBaseline = 'middle'; + var titleYPos = dimensions.height / 2; + } else { + context.textBaseline = 'top'; + var titleYPos = 0; + } + context.fillStyle = chartOptions.title.fillStyle; + context.fillText(chartOptions.title.text, titleXPos, titleYPos); + } + + context.restore(); // See .save() above. + }; + + // Sample timestamp formatting function + SmoothieChart.timeFormatter = function(date) { + function pad2(number) { return (number < 10 ? '0' : '') + number } + return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); + }; + + exports.TimeSeries = TimeSeries; + exports.SmoothieChart = SmoothieChart; + +})(typeof exports === 'undefined' ? this : exports); + diff --git a/emhttp/plugins/dynamix/sheets/DashStats.css b/emhttp/plugins/dynamix/sheets/DashStats.css index 71cdbbf6d..371ffb4e7 100644 --- a/emhttp/plugins/dynamix/sheets/DashStats.css +++ b/emhttp/plugins/dynamix/sheets/DashStats.css @@ -55,6 +55,31 @@ span.head_bar { span.head_gap { /* padding-left: 14px; */ } +span.head_gap.network-selects { + width: 100%; + display: flex; + align-items: center; + gap: 1rem; + flex: 1 1 auto; +} +span.head_gap.network-selects > * { + min-width: 0; +} +select.network-select { + min-width: 0; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +select.network-interface { + flex: 0 0 auto; + width: auto; +} +select.network-type { + flex: 1 1 auto; + width: 100%; +} span.head_time { padding-left: 40px; font-size: inherit !important; diff --git a/emhttp/plugins/dynamix/styles/default-base.css b/emhttp/plugins/dynamix/styles/default-base.css index 7df257b1b..ebfa6ec31 100755 --- a/emhttp/plugins/dynamix/styles/default-base.css +++ b/emhttp/plugins/dynamix/styles/default-base.css @@ -631,7 +631,7 @@ div.title span img { background-color: var(--orange-800); } .nav-item.active:after { - background-color: var(--background-color); + background-color: var(--header-text-color); } .nav-user a { color: var(--inverse-text-color); diff --git a/etc/rc.d/rc.samba b/etc/rc.d/rc.samba index 2b72ca7a2..8d30144f8 100755 --- a/etc/rc.d/rc.samba +++ b/etc/rc.d/rc.samba @@ -39,7 +39,7 @@ samba_running(){ samba_waitfor_shutdown(){ local i - for i in {1..5}; do + for i in {1..10}; do if ! samba_running; then break; fi sleep 1 done diff --git a/sbin/mover b/sbin/mover index bab99064b..c21480551 100755 --- a/sbin/mover +++ b/sbin/mover @@ -83,6 +83,29 @@ start() { fi done + # maybe empty an array disk + EMPTYING=$(grep '^shareUserEmptying=' /boot/config/share.cfg | cut -d'"' -f2) + IFS=',' read -ra arr <<< "$EMPTYING" + for DISK in "${arr[@]}"; do + # we can only empty share directories + for SHAREPATH in /mnt/$DISK/* ; do + SHARE=$(basename "$SHAREPATH") + if [[ -d "$SHAREPATH" && -f "$CFGPATH/$SHARE.cfg" ]]; then + move "$SHAREPATH" "-e" + fi + done + + # output list of files which could not be moved + # use 'find' in case huge number of files left in /mnt/$DISK + count=$(find /mnt/$DISK -mindepth 1 | wc -l) + if [ "$count" -gt 0 ]; then + find /mnt/$DISK -mindepth 1 -depth -printf 'move: %p Not moved\n' | head -n 100 + if [ "$count" -gt 100 ]; then + echo "[output truncated to first 100 entries]" + fi + fi + done + rm -f $PIDFILE echo "mover: finished" }