Merge branch 'vm-memory-slider' of https://github.com/bobbintb/webgui into vm-memory-slider

This commit is contained in:
Armando
2025-10-03 12:56:22 -06:00
33 changed files with 1878 additions and 479 deletions

View File

@@ -53,14 +53,11 @@ cat > "$PLUGIN_NAME" << 'EOF'
icon="wrench"
support="&github;/pull/&pr;">
<!-- Put the change log within CDATA to handle circumstance if a character which needs to be escaped for xml somehow is added to the changelog -->
<CHANGES>
<![CDATA[
##&version;
- Test build for PR #&pr; (commit &commit;)
- This plugin installs modified files from the PR for testing
- Original files are backed up and restored upon removal
]]>
</CHANGES>
<!-- FILE sections run in the listed order - Check if this is an update prior to installing -->
@@ -271,7 +268,14 @@ Link='nav-user'
<script>
$(function() {
// Check for updates (non-dismissible)
caPluginUpdateCheck("webgui-pr-PR_PLACEHOLDER.plg", {noDismiss: true});
caPluginUpdateCheck("webgui-pr-PR_PLACEHOLDER.plg", {noDismiss: true},function(result){
try {
let json = JSON.parse(result);
if ( ! json.version ) {
addBannerWarning("Note: webgui-pr-PR_PLACEHOLDER has either been merged or removed");
}
} catch(e) {}
});
// Create banner with uninstall link (nondismissible)
let bannerMessage = "Modified GUI installed via <b>webgui-pr-PR_PLACEHOLDER</b> plugin. " +

View File

@@ -359,9 +359,9 @@ jobs:
### 🔄 To Remove:
Navigate to Plugins → Installed Plugins and remove `webgui-pr-${{ steps.metadata.outputs.version }}`, or run:
Navigate to Plugins → Installed Plugins and remove `webgui-pr-${{ steps.metadata.outputs.pr_number }}`, or run:
```bash
plugin remove webgui-pr-${{ steps.metadata.outputs.version }}
plugin remove webgui-pr-${{ steps.metadata.outputs.pr_number }}
```
---

View File

@@ -1548,7 +1548,6 @@ provided the following conditions are met:
* The Primary storage for a share is set to a pool.
* The Secondary storage for a share is set to **none**.
* The share exists on a single volume.
* The share is **not** exported over NFS.
The advantage of *exclusive* shares is that transfers bypass the FUSE layer which may significantly
increase I/O performance.

6
emhttp/plugins/dynamix.gui.search/gui_search.page Normal file → Executable file
View File

@@ -37,6 +37,9 @@ function guiSearchBoxSpan() {
function setupGUIsearch() {
window.addEventListener('keydown',function(e){
if (!e.shiftKey && !e.altKey && (navigator.appVersion.indexOf('Mac')==-1 ? e.ctrlKey : e.metaKey) && e.keyCode==75) {
// If a modal is visible, don't open the search box
if ($('[role="modal"]').is(':visible')) return;
e.preventDefault();
<?if ($themeHelper->isTopNavTheme()):?>
if (guiSearchBoxSpan()) closeSearchBox(e); else gui_search();
@@ -79,6 +82,9 @@ function closeSearchBox(e) {
}
function guiSearch() {
// If a modal is visible, don't navigate away from the page
if ($('[role="modal"]').is(':visible')) return;
var searchInfo = $('#guiSearchBox').val().split('**');
var separator = ('fragmentDirective' in document) ? '#:~:text=' : '#';
var scrollText = (typeof searchInfo[1] != 'undefined') ? separator+searchInfo[1].replace(' ','%20').replace('-','%2d') : '';

6
emhttp/plugins/dynamix.plugin.manager/Plugins.page Normal file → Executable file
View File

@@ -145,9 +145,9 @@ function loadlist(id,check) {
}
$(function() {
initlist();
$('.tabs').append("<span id='checkall' class='status vhshift'><input type='button' value=\"_(Check For Updates)_\" onclick='openPlugin(\"checkall\",\"_(Plugin Update Check)_\",\":return\")' disabled></span>");
$('.tabs').append("<span id='updateall' class='status vhshift' style='display:none;margin-left:12px'><input type='button' value=\"_(Update All Plugins)_\" onclick='updateList()'></span>");
$('.tabs').append("<span id='removeall' class='status vhshift' style='display:none;margin-left:12px'><input type='button' value=\"_(Remove Selected Plugins)_\" onclick='removeList()'></span>");
$('.tabs-container').append("<span id='checkall' class='status vhshift'><input type='button' value=\"_(Check For Updates)_\" onclick='openPlugin(\"checkall\",\"_(Plugin Update Check)_\",\":return\")' disabled></span>");
$('.tabs-container').append("<span id='updateall' class='status vhshift' style='display:none;margin-left:12px'><input type='button' value=\"_(Update All Plugins)_\" onclick='updateList()'></span>");
$('.tabs-container').append("<span id='removeall' class='status vhshift' style='display:none;margin-left:12px'><input type='button' value=\"_(Remove Selected Plugins)_\" onclick='removeList()'></span>");
});
</script>

0
emhttp/plugins/dynamix.vm.manager/VMMachines.page Normal file → Executable file
View File

View File

@@ -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')"

7
emhttp/plugins/dynamix.vm.manager/VMs.page Normal file → Executable file
View File

@@ -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\")"
---
<?PHP
@@ -28,9 +29,3 @@ Cond="exec(\"grep -o '^SERVICE=.enable' /boot/config/domain.cfg 2>/dev/null\")"
<? if ($noticeMessage): ?>
<p class="notice"><?= $noticeMessage ?></p>
<? endif; ?>
<?
if (count($pages) == 2) {
$tabbed = false;
}
?>

View File

@@ -1,6 +1,6 @@
#!/usr/bin/php -q
<?PHP
/* Copyright 2015-2023, Lime Technology
/* Copyright 2015-2025, Lime Technology
* Copyright 2015-2016, Guilherme Jardim, Eric Schultz, Jon Panozzo.
*
* This program is free software; you can redistribute it and/or
@@ -34,6 +34,7 @@ if (file_exists($cfgfile)) {
}
}
if (isset($cfg_new)) {
$tmp = "";
foreach ($cfg_new as $key => $value) $tmp .= "$key=\"$value\"\n";
file_put_contents($cfgfile, $tmp);
}

View File

@@ -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

View File

@@ -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
?>
<link type="text/css" rel="stylesheet" href="<?autov('/webGui/styles/jquery.switchbutton.css')?>">
<script src="<?autov('/webGui/javascript/jquery.apexcharts.js')?>"></script>
<script src="<?autov('/webGui/javascript/smoothie.js')?>"></script>
<script src="<?autov('/webGui/javascript/jquery.switchbutton.js')?>"></script>
<script src="<?autov('/plugins/dynamix.docker.manager/javascript/docker.js')?>"></script>
<script src="<?autov('/plugins/dynamix.vm.manager/javascript/vmmanager.js')?>"></script>
@@ -412,7 +417,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout
?>
<tr id='cpu_chart'>
<td>
<div id='cpuchart'></div>
<canvas id='cpuchart' style='width:100%; height:96px'></canvas>
</td>
</tr>
</tbody>
@@ -498,13 +503,19 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout
<div class='section'>
<span class='flex flex-row items-center gap-4'>
<h3 class='tile-header-main'>_(Interface)_</h3>
<span class="head_gap flex flex-row items-center gap-4">
<span class="head_gap flex flex-row items-center gap-4 network-selects">
<i class='ups fa fa-angle-double-right'></i>
<select name="port_select" onchange="portSelect(this.value)">
<select name="port_select" class="network-select network-interface" onchange="portSelect(this.value)">
<?foreach ($ports as $port):?>
<?=mk_option("",$port,_($port))?>
<?endforeach;?>
</select>
<select name="enter_view" class="network-select network-type" onchange="changeView(this.value)">
<?=mk_option("", "0", _("General info"))?>
<?=mk_option("", "1", _("Counters info"))?>
<?=mk_option("", "2", _("Errors info"))?>
<?=mk_option("", "3", _("Network traffic"))?>
</select>
</span><br>
</span>
<span class='flex flex-row flex-wrap items-center gap-4'>
@@ -531,12 +542,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout
<tr>
<td>
<span class='w26'>
<select name="enter_view" onchange="changeView(this.value)">
<?=mk_option("", "0", _("General info"))?>
<?=mk_option("", "1", _("Counters info"))?>
<?=mk_option("", "2", _("Errors info"))?>
<?=mk_option("", "3", _("Network traffic"))?>
</select>
&nbsp;
</span>
<span class='w36'>
<i class='view1'>_(Mode of operation)_</i>
@@ -584,7 +590,7 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout
?>
<tr class="view4">
<td>
<div id="netchart"></div>
<canvas id="netchart" style='width:100%; height:120px'></canvas>
</td>
</tr>
</tbody>
@@ -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 = '<thead class="stopgap"><tr><td class="stopgap"></td></tr></thead>
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:'<?=$color?>'}}, axisBorder:{show:false}, axisTicks:{show:false}},
grid:{show:true, borderColor:'<?=$grid?>'},
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:'<?=$color?>'}}, axisBorder:{show:false}, axisTicks:{show:false}},
grid:{show:true, borderColor:'<?=$grid?>'},
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: '<?=$grid?>',
fillStyle: 'transparent',
lineWidth: 1,
millisPerLine: 5000,
verticalSections: 4
},
labels: {
fillStyle: '<?=$color?>',
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: '<?=$grid?>',
fillStyle: 'transparent',
lineWidth: 1,
millisPerLine: 5000,
verticalSections: 4
},
labels: {
fillStyle: '<?=$color?>',
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 = <?=$display['critical']?>;
const warning = <?=$display['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;
}
<?if ($wireguard):?>
@@ -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();
<?if ($group):?>
@@ -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() {
});
});
</script>
</script>

View File

@@ -876,7 +876,7 @@ _(Spin down delay)_:
<?endif;?>
<?if (diskType('Data') || (isPool($tag) && !isSubpool($tag))):?>
_(File system status)_:
: <?=_(_var($disk, 'fsStatus'))?>&nbsp;
: <?=_(_var($disk, 'fsStatus'))?><?=(_var($disk,'fsStatus')==="Mounted")? (_var($disk,'fsEmpty')==="yes"? ", empty" : ", not empty"):""?>&nbsp;
<?if ($fsTypeImmutable):?>
_(File system type)_:
@@ -912,7 +912,6 @@ _(File system type)_:
echo get_inline_fs_warnings($disk);
?>
<?endif;?>
<?if (diskType('Data') || isPool($tag)):?>
<div markdown="1" id="profile">
_(Allocation profile)_:

View File

@@ -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
<br>
<label for="anonymize">
<input type="checkbox" id="anonymize" checked>
_(Anonymize diagnostics)_
</label>
<span class="inline-block">
<input id='download' type="button" value="_(Download)_" onclick="$(this).attr('disabled',true);diagnostics(zipfile())">
<input type="button" value="_(Done)_" onclick="done()">
</span>
<div class="flex flex-col gap-4 items-start">
<div class="flex flex-wrap items-center gap-4">
<label for="anonymize" class="inline-flex flex-row items-center gap-2">
<input type="checkbox" id="anonymize" checked>
<span>_(Anonymize diagnostics)_</span>
</label>
</div>
<div class="flex flex-wrap items-center gap-4">
<input id='download' type="button" value="_(Download)_" onclick="$(this).attr('disabled',true);diagnostics(zipfile())">
<input type="button" value="_(Done)_" onclick="done()">
</div>
</div>

View File

@@ -62,30 +62,38 @@ effect of making it ***impossible*** to rebuild an existing failed drive - you h
<input type="hidden" name="preserveArray" value="yes" disabled>
<input type="hidden" name="preserveCache" value="yes" disabled>
_(Preserve current assignments)_:
: <select id="s1" name="preset" multiple="multiple" style="display:none">
<option value=''>_(All)_</option>
<?=mk_option_check(0,'array',_('Array slots'))?>
<?=mk_option_check(0,'cache',_('Pool slots'))?>
</select>
<div class="flex flex-col gap-4 items-start">
<div class="flex flex-wrap items-center gap-4">
<span class="strong">_(Preserve current assignments)_:</span>
<div class="inline-block">
<select id="s1" name="preset" multiple="multiple" class="hidden">
<option value=''>_(All)_</option>
<?=mk_option_check(0,'array',_('Array slots'))?>
<?=mk_option_check(0,'cache',_('Pool slots'))?>
</select>
</div>
</div>
&nbsp;
: <span class="inline-block">
<?if ($newarray):?>
_(Array has been **Reset**)_ (_(please configure)_)
<?elseif ($disabled):?>
_(Array must be **Stopped** to change)_
<?else:?>
<label>
<div class="flex flex-wrap items-center gap-4">
<?if ($newarray) {?>
<span>
_(Array has been **Reset**)_ (_(please configure)_)
</span>
<?} elseif ($disabled) {?>
<span>
_(Array must be **Stopped** to change)_
</span>
<?} else {?>
<label class="flex flex-row items-center gap-2">
<input type="checkbox" onClick="cmdInit.disabled=!this.checked">
_(Yes, I want to do this)_
<span>_(Yes, I want to do this)_</span>
</label>
<?endif;?>
</span>
<?}?>
</div>
&nbsp;
: <span class="inline-block">
<div class="flex flex-wrap items-center gap-4">
<input type="submit" class="lock" name="cmdInit" value="_(Apply)_" disabled>
<input type="button" class="lock" value="_(Done)_" onclick="done()">
</span>
</div>
</div>
</form>

View File

@@ -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
<form method="POST" action="/update.htm" target="progressFrame">
<span class="block">
<select name="select" onchange="selection(this.value,true)">
<?=mk_option(0,"0","_(Disks)_")?>
<?=mk_option(0,"1","_(Shares)_")?>
</select>
</span>
<form markdown="1" method="POST" action="/update.htm" target="progressFrame">
<div class="flex flex-col gap-4 items-start">
<div class="flex flex-wrap items-center gap-4">
<span class="strong">_(Target)_:</span>
<div class="inline-block">
<select name="select" onchange="selection(this.value,true)">
<?=mk_option(0,"0","_(Disks)_")?>
<?=mk_option(0,"1","_(Shares)_")?>
</select>
</div>
</div>
<span id="disks" class="block">
<select id="s1" name="disks" style="display:none" multiple>
<option value=''>_(All)_</option>
<?foreach (array_filter($disks,'data_disks') as $disk):?>
<?=mk_option(1,"/mnt/{$disk['name']}",_(my_disk($disk['name'])),3)?>
<?endforeach;?>
</select>
</span>
<div class="flex flex-col gap-4 items-start">
<span class="strong">_(Items)_:</span>
<div class="flex flex-col gap-2 items-start">
<div id="disks">
<select id="s1" name="disks" class="hidden" multiple>
<option value=''>_(All)_</option>
<?foreach (array_filter($disks,'data_disks') as $disk):?>
<?=mk_option(1,"/mnt/{$disk['name']}",_(my_disk($disk['name'])),3)?>
<?endforeach;?>
</select>
</div>
<div id="shares" class="hidden">
<select id="s2" name="shares" class="hidden" multiple>
<option value=''>_(All)_</option>
<?uksort($shares,'strnatcasecmp');?>
<?foreach ($shares as $share):?>
<?=mk_option(1,"/mnt/user/{$share['name']}",$share['name'])?>
<?endforeach;?>
</select>
</div>
</div>
</div>
<span id="shares" class="block" style="display:none">
<select id="s2" name="shares" style="display:none" multiple>
<option value=''>_(All)_</option>
<?uksort($shares,'strnatcasecmp');?>
<?foreach ($shares as $share):?>
<?=mk_option(1,"/mnt/user/{$share['name']}",$share['name'])?>
<?endforeach;?>
</select>
</span>
<?if (_var($var,'fsState')=="Started"):?>
<div><input type="button" value="_(Start)_" onclick="setNewPerms(this.form)"><input type="button" value="_(Done)_" class="lock" onclick="done()"></div>
<?else:?>
<div><input type="button" value="_(Start)_" disabled><input type="button" value="_(Done)_" class="lock" onclick="done()">_(Array must be **Started** to change permissions)_.</div>
<?endif;?>
<div class="flex flex-wrap items-center gap-4">
<?if (_var($var,'fsState')=="Started"):?>
<input type="button" value="_(Start)_" onclick="setNewPerms(this.form)">
<input type="button" value="_(Done)_" class="lock" onclick="done()">
<?else:?>
<input type="button" value="_(Start)_" disabled>
<input type="button" value="_(Done)_" class="lock" onclick="done()">
<span>_(Array must be **Started** to change permissions)_.</span>
<?endif;?>
</div>
</div>
</form>

View File

@@ -31,7 +31,7 @@ _(Read settings from)_ <i class="fa fa-arrow-left fa-fw"></i>
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']));
}
?>
</select>
@@ -48,7 +48,7 @@ _(Write settings to)_ <i class="fa fa-arrow-right fa-fw"></i>
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 "<option>("._('All').")</option>";
foreach ($rows as $row) echo $row;

View File

@@ -34,7 +34,7 @@ _(Read settings from)_ <i class="fa fa-arrow-left fa-fw"></i>
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']));
}
?>
</select>
@@ -51,7 +51,7 @@ _(Write settings to)_ <i class="fa fa-arrow-right fa-fw"></i>
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 "<option>("._('All').")</option>";
foreach ($rows as $row) echo $row;
@@ -154,7 +154,7 @@ _(Read settings from)_ <i class="fa fa-arrow-left fa-fw"></i>
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']));
}
?>
</select>
@@ -171,7 +171,7 @@ _(Write settings to)_ <i class="fa fa-arrow-right fa-fw"></i>
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 "<option>("._('All').")</option>";
foreach ($rows as $row) echo $row;
@@ -217,7 +217,7 @@ _(Read settings from)_ <i class="fa fa-arrow-left fa-fw"></i>
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']));
}
?>
</select>
@@ -234,7 +234,7 @@ _(Write settings to)_ <i class="fa fa-arrow-right fa-fw"></i>
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 "<option>("._('All').")</option>";
foreach ($rows as $row) echo $row;
@@ -267,7 +267,7 @@ $(function() {
checkShareSettingsSMB(document.smb_edit);
initDropdownSMB(false);
<?if ($tabbed):?>
$('#tab'+$('input[name$="tabs"]').length).bind({click:function(){initDropdownSMB(true);}});
$('#tab'+$('.tabs-container button').length).bind({click:function(){initDropdownSMB(true);}});
<?endif;?>
<?if (count($users)==1):?>
toggleButton('readusersmb',true);

View File

@@ -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:

View File

@@ -28,6 +28,7 @@ $fileMax = (int) file_get_contents('/proc/sys/fs/file-max');
$(function() {
$('#s1').dropdownchecklist({emptyText:"_(All)_", width:<?=$width[0]?>, explicitClose:"..._(close)_"});
$('#s2').dropdownchecklist({emptyText:"_(None)_", width:<?=$width[0]?>, explicitClose:"..._(close)_"});
$('#s3').dropdownchecklist({emptyText:"_(None)_", width:<?=$width[0]?>, 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);
}
</script>
<form markdown="1" name="share_settings" method="POST" action="/update.htm" target="progressFrame" onsubmit="return prepareShare(this)">
@@ -128,6 +144,15 @@ _(Excluded disk(s))_:
:shares_excluded_disks_help:
_(Emptying disk(s))_:
: <select id="s3" name="shareUserEmptying" multiple="multiple" style="display:none">
<?foreach ($disks as $disk):?>
<?=mk_option_luks(_var($disk,'name'),_var($var,'shareUserEmptying'),strstr(_var($disk,'fsType'),':',true))?>
<?endforeach;?>
</select>
:shares_emptying_disks_help:
_(Permit exclusive shares)_:
: <select name="shareUserExclusive" <?=$disabled?>>
<?=mk_option($var['shareUserExclusive'], "no", _('No'))?>

View File

@@ -41,158 +41,203 @@ SCRIPTNAME=$(basename "$0")
LOG="/var/log/notify_${SCRIPTNAME%.*}"
# for quick test, setup environment to mimic notify script
[[ -z "${EVENT}" ]] && EVENT='Unraid Status'
[[ -z "${SUBJECT}" ]] && SUBJECT='Notification'
[[ -z "${DESCRIPTION}" ]] && DESCRIPTION='No description'
[[ -z "${IMPORTANCE}" ]] && IMPORTANCE='normal'
[[ -z "${TIMESTAMP}" ]] && TIMESTAMP=$(date +%s)
EVENT="${EVENT:-Unraid Status}"
SUBJECT="${SUBJECT:-Notification}"
DESCRIPTION="${DESCRIPTION:-No description}"
IMPORTANCE="${IMPORTANCE:-normal}"
CONTENT="${CONTENT:-}"
LINK="${LINK:-}"
HOSTNAME="${HOSTNAME:-$(hostname)}"
TIMESTAMP="${TIMESTAMP:-$(date +%s)}"
# ensure link has a host
if [[ -n "${LINK}" ]] && [[ ${LINK} != http* ]]; then
source <(grep "NGINX_DEFAULTURL" /usr/local/emhttp/state/nginx.ini 2>/dev/null)
LINK=${NGINX_DEFAULTURL}${LINK}
if [[ -r /usr/local/emhttp/state/nginx.ini ]]; then
# shellcheck disable=SC1090
source <(grep "NGINX_DEFAULTURL" /usr/local/emhttp/state/nginx.ini || true)
LINK="${NGINX_DEFAULTURL}${LINK}"
fi
fi
# Discord will not allow links with bare hostname, links must have both hostname and tld or no link at all
if [[ -n "${LINK}" ]]; then
HOST=$(echo "${LINK}" | cut -d'/' -f3)
[[ ${HOST} != *.* ]] && LINK=
HOST=$(echo "${LINK}" | cut -d'/' -f3)
[[ ${HOST} != *.* ]] && LINK=
fi
# note: there is no default for CONTENT
# send DESCRIPTION and/or CONTENT. Ignore the default DESCRIPTION.
[[ "${DESCRIPTION}" == 'No description' ]] && DESCRIPTION=""
FULL_DETAILS=""
if [[ -n "${DESCRIPTION}" ]] && [[ -n "${CONTENT}" ]]; then
FULL_DETAILS="${DESCRIPTION}\n\n${CONTENT}"
FULL_DETAILS="${DESCRIPTION}"$'\n\n'"${CONTENT}"
elif [[ -n "${DESCRIPTION}" ]]; then
FULL_DETAILS="${DESCRIPTION}"
elif [[ -n "${CONTENT}" ]]; then
FULL_DETAILS="${CONTENT}"
fi
# split into 1024 character segments
[[ -n "${FULL_DETAILS}" ]] && DESC_FIELD=$(
cat <<EOF
{
"name": "Description",
"value": "${FULL_DETAILS:0:1024}"
},
EOF
)
[[ -n "${FULL_DETAILS}" ]] && [[ ${#FULL_DETAILS} -gt 1024 ]] && DESC_FIELD=$(
cat <<EOF
${DESC_FIELD}
{
"name": "Description (cont)",
"value": "${FULL_DETAILS:1024:1024}"
},
EOF
)
[[ -n "${FULL_DETAILS}" ]] && [[ ${#FULL_DETAILS} -gt 2048 ]] && DESC_FIELD=$(
cat <<EOF
${DESC_FIELD}
{
"name": "Description (cont)",
"value": "${FULL_DETAILS:2048:1024}"
},
EOF
)
# https://birdie0.github.io/discord-webhooks-guide/structure/embed/timestamp.html
# https://www.cyberciti.biz/faq/linux-unix-formatting-dates-for-display/
FORMATTED_TIMESTAMP=$(date -u +\"%Y-%m-%dT%H:%M:%S.000Z\" -d @"${TIMESTAMP}")
# split into 1024 character segments
DESC_FIELD=""; DESC_FIELD2=""; DESC_FIELD3=""
if [[ "${FULL_DETAILS}" ]]; then
DESC_FIELD="${FULL_DETAILS:0:1024}"
if [[ ${#FULL_DETAILS} -gt 1024 ]]; then
DESC_FIELD2="${FULL_DETAILS:1024:1024}"
if [[ ${#FULL_DETAILS} -gt 2048 ]]; then
DESC_FIELD3="${FULL_DETAILS:2048:1024}"
fi
fi
fi
# Timestamp ISO 8601 (UTC) für Discord
ISO_TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z -d @"${TIMESTAMP}")
# https://birdie0.github.io/discord-webhooks-guide/structure/embed/thumbnail.html
# https://birdie0.github.io/discord-webhooks-guide/structure/embed/color.html
# vary data based on IMPORTANCE
if [[ "${IMPORTANCE}" != "normal" ]] && [[ "${IMPORTANCE}" != "warning" ]] && [[ "${IMPORTANCE}" != "alert" ]]; then
IMPORTANCE="normal"
fi
case "${IMPORTANCE}" in
normal)
THUMBNAIL="https://craftassets.unraid.net/uploads/discord/notify-normal.png"
COLOR="39208"
;;
warning)
THUMBNAIL="https://craftassets.unraid.net/uploads/discord/notify-warning.png"
COLOR="16747567"
;;
alert)
THUMBNAIL="https://craftassets.unraid.net/uploads/discord/notify-alert.png"
COLOR="14821416"
[[ -n "${DISCORD_TAG_ID}" && "${DISCORD_TAG_ID}" == "none" ]] && DISCORD_TAG_ID=""
if [[ -n "${DISCORD_TAG_ID}" ]]; then
# add leading @ if needed
[[ "${DISCORD_TAG_ID:0:1}" != "@" ]] && DISCORD_TAG_ID="@${DISCORD_TAG_ID}"
# @mentions only work in the "content" area, not the "embed" area
DISCORD_CONTENT_AREA="\"content\": \"<${DISCORD_TAG_ID}>\","
fi
normal)
THUMBNAIL="https://craftassets.unraid.net/uploads/discord/notify-normal.png"
COLOR="39208"
;;
warning)
THUMBNAIL="https://craftassets.unraid.net/uploads/discord/notify-warning.png"
COLOR="16747567"
;;
alert)
THUMBNAIL="https://craftassets.unraid.net/uploads/discord/notify-alert.png"
COLOR="14821416"
;;
*)
IMPORTANCE="normal"
THUMBNAIL="https://craftassets.unraid.net/uploads/discord/notify-normal.png"
COLOR="39208"
;;
esac
# @mentions only for alert
CONTENT_AREA=""
if [[ "${IMPORTANCE}" == "alert" ]]; then
if [[ -n "${DISCORD_TAG_ID}" && "${DISCORD_TAG_ID}" != "none" ]]; then
id="${DISCORD_TAG_ID}"
# Strip surrounding angle brackets if present
[[ "$id" == \<*\> ]] && id="${id:1:${#id}-2}"
# If it already starts with @, @! or @& keep it; else prefix @ (user)
[[ "$id" =~ ^@(|!|&)[0-9]+$ ]] || id="@${id}"
CONTENT_AREA="<${id}>"
fi
fi
# https://birdie0.github.io/discord-webhooks-guide/structure/embed/author.html
# if SERVER_ICON is defined, use it
[[ -n "${SERVER_ICON}" && "${SERVER_ICON:0:8}" == "https://" ]] && ICON_URL="\"icon_url\": \"${SERVER_ICON}\","
ICON_URL=""
if [[ -n "${SERVER_ICON}" && "${SERVER_ICON}" == "https://"* ]]; then
ICON_URL="$SERVER_ICON"
fi
# https://birdie0.github.io/discord-webhooks-guide/structure/embed/url.html
# if LINK is defined, use it
[[ -n "${LINK}" ]] && LINK_URL="\"url\": \"${LINK}\","
# shellcheck disable=SC2016
jq_filter='
# basic object
{
embeds: [
{
title: $event | tostring,
description: $subject | tostring,
timestamp: $ts | tostring,
color: ($color | tonumber),
author: {
name: $hostname
},
thumbnail: { url: $thumb },
fields: []
}
]
}
DATA=$(
cat <<EOF
{
${DISCORD_CONTENT_AREA}
"embeds": [
{
"title": "${EVENT:0:256}",
"description": "${SUBJECT:0:2043}",
${LINK_URL}
"timestamp": ${FORMATTED_TIMESTAMP},
"color": "${COLOR}",
"author": {
${ICON_URL}
"name": "${HOSTNAME}"
},
"thumbnail": {
"url": "${THUMBNAIL}"
},
"fields": [
${DESC_FIELD}
{
"name": "Priority",
"value": "${IMPORTANCE}",
"inline": true
}
]
}
]
}
EOF
# Optional: content (Mentions)
| if ($content_area | length) > 0 then . + {content: $content_area} else . end
# Optional: URL
| if ($link | length) > 0 then .embeds[0].url = $link else . end
# Optional: icon_url
| if ($icon | length) > 0 then .embeds[0].author.icon_url = $icon else . end
# Description
| .embeds[0].fields += [ { name: "Description", value: $desc_field } ]
| if ($desc_field2 | length) > 0 then .embeds[0].fields += [ { name: "Description (cont)", value: $desc_field2 } ] else . end
| if ($desc_field3 | length) > 0 then .embeds[0].fields += [ { name: "Description (cont)", value: $desc_field3 } ] else . end
# Priority
| .embeds[0].fields += [ { name: "Priority", value: $importance, inline: true } ]
'
# create valid json
args=(
-n
--arg event "${EVENT:0:256}"
--arg subject "${SUBJECT:0:2043}"
--arg ts "$ISO_TS"
--arg color "$COLOR"
--arg hostname "$HOSTNAME"
--arg thumb "$THUMBNAIL"
--arg link "$LINK"
--arg icon "$ICON_URL"
--arg importance "$IMPORTANCE"
--arg desc_field "$DESC_FIELD"
--arg desc_field2 "$DESC_FIELD2"
--arg desc_field3 "$DESC_FIELD3"
--arg content_area "${CONTENT_AREA}"
"$jq_filter"
)
# echo "${DATA}" >>"${LOG}"
json=$(jq "${args[@]}" 2>&1)
jq_status=$?
if [[ "$jq_status" -ne 0 ]]; then
echo "jq error $json" >>"$LOG"
logger -t "$SCRIPTNAME" -- "Failed sending notification ($json)"
exit 1
fi
# try several times in case we are being rate limited
# this is not foolproof, messages can still be rejected
args=(
-s
-X POST "$WEBH_URL"
-H 'Content-Type: application/json'
--data-binary "$json"
)
MAX=4
for ((i = 1; i <= "${MAX}"; i++)); do
RET=$(
curl -s -X "POST" "$WEBH_URL" -H 'Content-Type: application/json' --data-ascii @- <<EOF
${DATA}
EOF
)
for ((i=1; i<=MAX; i++)); do
ret=$(curl "${args[@]}")
# if nothing was returned, message was successfully sent. exit loop
[[ -z "${RET}" ]] && break
# log the attempt
if [[ -z "$ret" ]]; then
break
fi
{
date
echo "attempt ${i} of ${MAX} failed"
echo "${RET}"
} >>"${LOG}"
echo "attempt $i of $MAX failed"
echo "$ret"
} >>"$LOG"
# if there was an error with the submission, log details and exit loop
[[ "${RET}" != *"retry_after"* ]] && echo "${DATA}" >>"${LOG}" && logger -t "${SCRIPTNAME}" -- "Failed sending notification" && break
if [[ "$ret" != *"retry_after"* ]]; then
echo "$json" >>"$LOG"
logger -t "$SCRIPTNAME" -- "Failed sending notification"
break
fi
# if retries exhausted, log failure
[[ "${i}" -eq "${MAX}" ]] && echo "${DATA}" >>"${LOG}" && logger -t "${SCRIPTNAME}" -- "Failed sending notification - rate limited" && break
if (( i == MAX )); then
echo "$json" >>"$LOG"
logger -t "$SCRIPTNAME" -- "Failed sending notification - rate limited"
break
fi
# we were rate limited, try again after a delay
sleep 1
done
]]>
</Script>

View File

@@ -344,6 +344,7 @@ $('body').on('click','a,.ca_href', function(e) {
$.cookie('allowedDomains',JSON.stringify(domainsAllowed),{expires:3650}); // rewrite cookie to further extend expiration by 400 days
if (domainsAllowed[dom.hostname]) return;
e.preventDefault();
$('.sweet-alert').removeClass('nchan'); // Prevent GUI issues if clicking external link from a changelog
swal({
title: "<?=_('External Link')?>",
text: "<span title='"+href+"'><?=_('Clicking OK will take you to a 3rd party website not associated with Lime Technology')?><br><br><b>"+href+"<br><br><input id='Link_Always_Allow' type='checkbox'></input><?=_('Always Allow')?> "+dom.hostname+"</span>",
@@ -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 });
}
});

View File

@@ -180,7 +180,6 @@ function my_error($code) {
}
function mk_option($select, $value, $text, $extra="") {
$value = htmlspecialchars($value);
$text = htmlspecialchars($text);
return "<option value='$value'".($value == $select ? " selected" : "").(strlen($extra) ? " $extra" : "").">$text</option>";
}
@@ -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) {

View File

@@ -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':

View File

@@ -21,7 +21,8 @@ _(New folder name)_:
&nbsp;
: <span class="dfm_text"></span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This creates a folder at the current level)_</div>
&nbsp;
: <div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This creates a folder at the current level)_</div>
</div>
<div markdown="1" id="dfm_templateDeleteFolder">
@@ -31,7 +32,8 @@ _(Folder name)_:
&nbsp;
: <span class="dfm_text"></span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i><?=_("This deletes the folder and all its content")?></div>
&nbsp;
: <div class="dfm_info"><i class="fa fa-warning dfm"></i><?=_("This deletes the folder and all its content")?></div>
</div>
<div markdown="1" id="dfm_templateRenameFolder">
@@ -47,19 +49,14 @@ _(New folder name)_:
&nbsp;
: <span class="dfm_text"></span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This renames the folder to the new name)_</div>
&nbsp;
: <div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This renames the folder to the new name)_</div>
</div>
<div markdown="1" id="dfm_templateCopyFolder">
_(Source folder)_:
: <span id="dfm_source"></span>
&nbsp;
: _(copy to)_ ...
_(Target folder)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickmatch="" data-pickroot="" data-picktop="">
&nbsp;
: <span class="flex flex-col gap-4">
<label for="dfm_sparse" class="inline-flex flex-wrap items-center gap-4">
@@ -73,19 +70,22 @@ _(Target folder)_:
<span class="dfm_text"></span>
</span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i><?=_("This copies the folder and all its content to another folder")?></div>
&nbsp;
: _(copy to)_ ...
<dt class="dfm_noticeLabel">&nbsp;</dt>
<dd class="dfm_notice">
<div class="dfm_info"><i class="fa fa-warning dfm"></i><?=_("This copies the folder and all its content to another folder")?></div>
</dd>
_(Target folder)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickmatch="" data-pickroot="" data-picktop="">
</div>
<div markdown="1" id="dfm_templateMoveFolder">
_(Source folder)_:
: <span id="dfm_source"></span>
&nbsp;
: _(move to)_ ...
_(Target folder)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickmatch="" data-pickroot="" data-picktop="">
&nbsp;
: <span class="flex flex-col gap-4">
<label for="dfm_sparse" class="inline-flex flex-wrap items-center gap-4">
@@ -99,7 +99,16 @@ _(Target folder)_:
<span class="dfm_text"></span>
</span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i><?=_("This moves the folder and all its content to another folder")?></div>
&nbsp;
: _(move to)_ ...
<dt class="dfm_noticeLabel">&nbsp;</dt>
<dd class="dfm_notice">
<div class="dfm_info"><i class="fa fa-warning dfm"></i><?=_("This moves the folder and all its content to another folder")?></div>
</dd>
_(Target folder)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickmatch="" data-pickroot="" data-picktop="">
</div>
<div markdown="1" id="dfm_templateDeleteFile">
@@ -109,7 +118,8 @@ _(File name)_:
&nbsp;
: <span class="dfm_text"></span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This deletes the selected file)_</div>
&nbsp;
: <div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This deletes the selected file)_</div>
</div>
<div markdown="1" id="dfm_templateRenameFile">
@@ -125,19 +135,14 @@ _(New file name)_:
&nbsp;
: <span class="dfm_text"></span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This renames the selected file)_</div>
&nbsp;
: <div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This renames the selected file)_</div>
</div>
<div markdown="1" id="dfm_templateCopyFile">
_(Source file)_:
: <span id="dfm_source"></span>
&nbsp;
: _(copy to)_ ...
_(Target file)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="" data-pickmatch="" data-pickroot="" data-picktop="">
&nbsp;
: <span class="flex flex-col gap-4">
<label for="dfm_sparse" class="inline-flex flex-wrap items-center gap-4">
@@ -151,19 +156,22 @@ _(Target file)_:
<span class="dfm_text"></span>
</span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This copies the selected file)_</div>
&nbsp;
: _(copy to)_ ...
<dt class="dfm_noticeLabel">&nbsp;</dt>
<dd class="dfm_notice">
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This copies the selected file)_</div>
</dd>
_(Target file)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="" data-pickmatch="" data-pickroot="" data-picktop="">
</div>
<div markdown="1" id="dfm_templateMoveFile">
_(Source file)_:
: <span id="dfm_source"></span>
&nbsp;
: _(move to)_ ...
_(Target file)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="" data-pickmatch="" data-pickroot="" data-picktop="">
&nbsp;
: <span class="flex flex-col gap-4">
<label for="dfm_sparse" class="inline-flex flex-wrap items-center gap-4">
@@ -177,7 +185,16 @@ _(Target file)_:
<span class="dfm_text"></span>
</span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This moves the selected file)_</div>
&nbsp;
: _(move to)_ ...
<dt class="dfm_noticeLabel">&nbsp;</dt>
<dd class="dfm_notice">
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This moves the selected file)_</div>
</dd>
_(Target file)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="" data-pickmatch="" data-pickroot="" data-picktop="">
</div>
<div markdown="1" id="dfm_templateDeleteObject">
@@ -187,7 +204,8 @@ _(Source)_:
&nbsp;
: <span class="dfm_text"></span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This deletes all selected sources)_</div>
&nbsp;
: <div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This deletes all selected sources)_</div>
</div>
<div markdown="1" id="dfm_templateRenameObject">
@@ -200,19 +218,14 @@ _(Source)_:
_(Target)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="">
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This renames the selected source)_</div>
&nbsp;
: <div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This renames the selected source)_</div>
</div>
<div markdown="1" id="dfm_templateCopyObject">
_(Source)_:
: <select id="dfm_source"></select>
&nbsp;
: _(copy to)_ ...
_(Target)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="" data-pickmatch="" data-pickroot="" data-picktop="">
&nbsp;
: <span class="flex flex-col gap-4">
<label for="dfm_sparse" class="inline-flex flex-wrap items-center gap-4">
@@ -226,19 +239,22 @@ _(Target)_:
<span class="dfm_text"></span>
</span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This copies all the selected sources)_</div>
&nbsp;
: _(copy to)_ ...
<dt class="dfm_noticeLabel">&nbsp;</dt>
<dd class="dfm_notice">
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This copies all the selected sources)_</div>
</dd>
_(Target)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="" data-pickmatch="" data-pickroot="" data-picktop="">
</div>
<div markdown="1" id="dfm_templateMoveObject">
_(Source)_:
: <select id="dfm_source"></select>
&nbsp;
: _(move to)_ ...
_(Target)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="" data-pickmatch="" data-pickroot="" data-picktop="">
&nbsp;
: <span class="flex flex-col gap-4">
<label for="dfm_sparse" class="inline-flex flex-wrap items-center gap-4">
@@ -252,7 +268,16 @@ _(Target)_:
<span class="dfm_text"></span>
</span>
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This moves all the selected sources)_</div>
&nbsp;
: _(move to)_ ...
<dt class="dfm_noticeLabel">&nbsp;</dt>
<dd class="dfm_notice">
<div class="dfm_info"><i class="fa fa-warning dfm"></i>_(This moves all the selected sources)_</div>
</dd>
_(Target)_:
: <input type="text" id="dfm_target" autocomplete="off" spellcheck="false" value="" data-pickcloseonfile="true" data-pickfolders="true" data-pickfilter="" data-pickmatch="" data-pickroot="" data-picktop="">
</div>
<div markdown="1" id="dfm_templateChangeOwner">

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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],c<r.length;c++)"ctrl"==k||"control"==k?(n++,d.ctrl.wanted=!0):"shift"==k?(n++,d.shift.wanted=!0):"alt"==k?(n++,d.alt.wanted=!0):"meta"==k?(n++,d.meta.wanted=!0):1<k.length?o[k]==codeAZ&&n++:i.keycode?i.keycode==codeAZ&&n++:a==k?n++:s[a]&&e.shiftKey&&(a=s[a])==k&&n++;return n!=r.length||d.ctrl.pressed!=d.ctrl.wanted||d.shift.pressed!=d.shift.wanted||d.alt.pressed!=d.alt.wanted||d.meta.pressed!=d.meta.wanted||(p(e),i.propagate)?void 0:(e.cancelBubble=!0,e.returnValue=!1,e.stopPropagation&&(e.stopPropagation(),e.preventDefault()),!1)}}this.all_shortcuts[l]={callback:r,target:a,event:i.type},a.addEventListener?a.addEventListener(i.type,r,!1):a.attachEvent?a.attachEvent("on"+i.type,r):a["on"+i.type]=r},remove:function(e){e=e.toLowerCase();var t=this.all_shortcuts[e];if(delete this.all_shortcuts[e],t){var a=t.event,r=t.target,n=t.callback;r.detachEvent?r.detachEvent("on"+a,n):r.removeEventListener?r.removeEventListener(a,n,!1):r["on"+a]=!1}}};
/* readmore.js - v2.0.0, copyright Jed Foster NOT UPDATED */
(function(c){function g(b,a){this.element=b;this.options=c.extend({},h,a);c(this.element).data("max-height",this.options.maxHeight);c(this.element).data("height-margin",this.options.heightMargin);delete this.options.maxHeight;if(this.options.embedCSS&&!k){var d=".readmore-js-toggle, .readmore-js-section { "+this.options.sectionCSS+" } .readmore-js-section { overflow: hidden; }",e=document.createElement("style");e.type="text/css";e.styleSheet?e.styleSheet.cssText=d:e.appendChild(document.createTextNode(d));document.getElementsByTagName("head")[0].appendChild(e);k=!0}this._defaults=h;this._name=f;this.init()}var f="readmore",h={speed:100,maxHeight:200,heightMargin:16,moreLink:'<a href="#">Read More</a>',lessLink:'<a href="#">Close</a>',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()<a.data("expandedHeight"))&&a.css("height",a.data("expandedHeight"))})},destroy:function(){var b=this;c(this.element).each(function(){var a=c(this);a.removeClass("readmore-js-section "+b.options.collapsedClass+" "+b.options.expandedClass).css({"max-height":"",height:"auto"}).next(".readmore-js-toggle").remove();a.removeData()})}};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);
/* readmore.js - v2.3.0 PERFORMANCE OPTIMIZED for Unraid Docker page - No animations */
(function(c){function g(b,a){this.element=b;this.options=c.extend({},h,a);c(this.element).data("max-height",this.options.maxHeight);c(this.element).data("height-margin",this.options.heightMargin);delete this.options.maxHeight;if(this.options.embedCSS&&!k){var d=".readmore-js-toggle, .readmore-js-section { "+this.options.sectionCSS+" } .readmore-js-section { overflow: hidden; }",e=document.createElement("style");e.type="text/css";e.styleSheet?e.styleSheet.cssText=d:e.appendChild(document.createTextNode(d));document.getElementsByTagName("head")[0].appendChild(e);k=!0}this._defaults=h;this._name=f;this.init()}var f="readmore",h={speed:0,maxHeight:200,heightMargin:16,moreLink:'<a href="#">Read More</a>',lessLink:'<a href="#">Close</a>',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 werent enough autocomplete scripts in the world? Because Im completely insane and have NIH syndrome? Probably both. :P

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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"
}