Merge branch 'master' into patch-8

This commit is contained in:
Squidly271
2025-08-13 19:28:12 -04:00
committed by GitHub
10 changed files with 198 additions and 124 deletions

View File

@@ -1177,18 +1177,19 @@ _(Use Tailscale)_:
<?if($TS_ExitNodeNeedsApproval):?>
<div markdown="1" class="TShostname noshow">
<b>Warning:</b>
<b>_(Warning)_</b>:
: Exit Node not yet approved. Navigate to the <a href="<?=$TS_DirectMachineLink?>" target='_blank'>Tailscale website</a> and approve it.
</div>
<?endif;?>
<?if(!empty($TS_expiry_diff)):?>
<div markdown="1" class="TSdivider noshow">
<b>_(Warning)_</b>:
<?if($TS_expiry_diff->invert):?>
: <b>Tailscale Key expired!</b> <a href="<?=$TS_MachinesLink?>" target='_blank'>Renew/Disable key expiry</a> for '<b><?=$TS_HostNameActual?></b>'.
<b>_(Warning)_</b>:
: <span><b>Tailscale Key expired!</b> <a href="<?=$TS_MachinesLink?>" target='_blank'>Renew/Disable key expiry</a> for '<b><?=$TS_HostNameActual?></b>'.</span>
<?else:?>
: Tailscale Key will expire in <b><?=$TS_expiry_diff->days?> days</b>! <a href="<?=$TS_MachinesLink?>" target='_blank'>Disable Key Expiry</a> for '<b><?=$TS_HostNameActual?></b>'.
<b>_(Warning)_</b>:
: <span>Tailscale Key will expire in <b><?=$TS_expiry_diff->days?> days</b>! <a href="<?=$TS_MachinesLink?>" target='_blank'>Disable Key Expiry</a> for '<b><?=$TS_HostNameActual?></b>'.</span>
<?endif;?>
<label>See <a href="https://tailscale.com/kb/1028/key-expiry" target='_blank'>key-expiry</a>.</label>
</div>
@@ -1197,7 +1198,7 @@ _(Use Tailscale)_:
<?if(!empty($TS_not_approved)):?>
<div markdown="1" class="TSdivider noshow">
<b>_(Warning)_</b>:
: The following route(s) are not approved: <b><?=trim($TS_not_approved)?></b>
: <span>The following route(s) are not approved: <b><?=trim($TS_not_approved)?></b></span>
</div>
<?endif;?>

View File

@@ -119,7 +119,7 @@ $libvirt_log = file_exists("/var/log/libvirt/libvirtd.log");
&nbsp;
: <span><input type="checkbox" class="advancedview"></span>
<?endif;?>
<form markdown="1" id="settingsForm" method="POST" action="/update.php" target="progressFrame">
<form markdown="1" id="settingsForm" method="POST" action="/update.php" target="progressFrame" onsubmit="return validateFormOnSubmit();">
<input type="hidden" name="#file" value="<?=htmlspecialchars($domain_cfgfile)?>">
<input type="hidden" name="#command" value="/plugins/dynamix/scripts/emcmd">
<input type="hidden" name="#arg[1]" value="cmdStatus=Apply">
@@ -180,7 +180,7 @@ _(Libvirt storage location)_:
<?endif;?>
_(Default VM storage path)_:
: <input type="text" id="domaindir" name="DOMAINDIR" autocomplete="off" spellcheck="false" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickroot="/mnt" value="<?=htmlspecialchars($domain_cfg['DOMAINDIR'])?>" placeholder="_(Click to Select)_" pattern="^[^\\]*/$">
: <input type="text" id="domaindir" name="DOMAINDIR" autocomplete="off" spellcheck="false" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickroot="/mnt" value="<?=htmlspecialchars($domain_cfg['DOMAINDIR'])?>" placeholder="_(Click to Select)_" pattern="^[^\\]*/$" onchange="validatePath(this)">
<?if (!$started):?>
<span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?endif;?>
@@ -188,7 +188,7 @@ _(Default VM storage path)_:
:vms_libvirt_storage_help:
_(Default ISO storage path)_:
: <input type="text" id="mediadir" name="MEDIADIR" autocomplete="off" spellcheck="false" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickroot="<?=is_dir('/mnt/user') ? '/mnt/user' : '/mnt'?>" value="<?=htmlspecialchars($domain_cfg['MEDIADIR'])?>" placeholder="_(Click to Select)_" pattern="^[^\\]*/$">
: <input type="text" id="mediadir" name="MEDIADIR" autocomplete="off" spellcheck="false" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickroot="<?=is_dir('/mnt/user') ? '/mnt/user' : '/mnt'?>" value="<?=htmlspecialchars($domain_cfg['MEDIADIR'])?>" placeholder="_(Click to Select)_" pattern="^[^'\\]*/$">
<?if (!$started):?>
<span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?endif;?>
@@ -364,6 +364,64 @@ function btrfsScrub(path) {
}
});
}
function validatePath(input) {
if (input.value.includes("'")) {
input.setCustomValidity(_("Single quote ' is not allowed in the path.")_);
} else {
input.setCustomValidity("");
}
input.reportValidity();
}
// Validate both domaindir and mediadir on submit
function validateFormOnSubmit() {
const domaindir = document.getElementById('domaindir');
const mediadir = document.getElementById('mediadir');
// Run validation
validatePath(domaindir);
validatePath(mediadir);
// Check validity in order, and focus the first invalid field
if (!domaindir.checkValidity()) {
domaindir.reportValidity();
domaindir.focus();
return false;
}
if (!mediadir.checkValidity()) {
mediadir.reportValidity();
mediadir.focus();
return false;
}
// Both valid
return true;
}
document.getElementById('settingsForm').addEventListener('submit', function(e) {
if (!validateFormOnSubmit()) {
e.preventDefault();
}
});
// Attach validation on input events
['domaindir', 'mediadir'].forEach(id => {
const input = document.getElementById(id);
input.addEventListener('input', () => validatePath(input));
});
// Hook into Unraid fileTreeAttach for both fields
$('.filepicker').each(function() {
const input = this;
$(input).fileTreeAttach(null, null, function(folder) {
$(input).val(folder);
validatePath(input);
$(document).trigger('close.fileTree');
});
});
$(function(){
$.post("/plugins/dynamix.vm.manager/include/Fedora-virtio-isos.php",{},function(isos) {
$('#winvirtio_select').html(isos).prop('disabled',false).change().each(function(){$(this).on('change',function() {

View File

@@ -271,6 +271,8 @@ function fileEdit(id) {
}
function fullWindow() {
// this class is used to determine if the dialog is sized via the default CSS in default-dynamix.css or by JS when the user clicks the "expand" button.
$('.ui-dialog').toggleClass('ui-dialog-content-full');
if ($('.ui-dfm .ui-dialog-titlebar-close').html().indexOf('expand')>=0) {
dfm.window.dialog('option','height',window.innerHeight-40);
dfm.window.dialog('option','width',window.innerWidth);
@@ -999,7 +1001,8 @@ $(function(){
} else {
url.push('<i class="fa fa-home red-text"></i>');
}
$('span.left').html(url.join('<i class="fa fa-chevron-right"></i>')).append('<span class="right"><span class="dfm_filter"><input type="text" class="dfm_filter" oninput="filter(this.value)" autocomplete="off" spellcheck="false" placeholder="_(file type)_"><i class="fa fa-filter dfm_filter"></i></span><i class="fa fa-toggle-off" onclick="toggleTime()" style="cursor:pointer;margin-left:20px" title="_(Toggle Time/Age display)_"></i></span>');
$('.title .left').html(url.join('<i class="fa fa-chevron-right"></i>'));
$('.title .right').append('<span class="dfm_filter"><input type="text" id="dfm_filter" class="dfm_filter" oninput="filter(this.value)" autocomplete="off" spellcheck="false" placeholder="_(file type)_"><i class="fa fa-filter dfm_filter"></i></span><i class="fa fa-toggle-off" onclick="toggleTime()" style="cursor:pointer;" title="_(Toggle Time/Age display)_"></i>');
table = $('table.indexer');
thead = table.find('thead');
table.bind('sortEnd',function(e,t){

View File

@@ -62,23 +62,18 @@ foreach ($allPages as $page) {
if (count($pages)) {
$running = file_exists($nchan_pid) ? file($nchan_pid, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];
$start = array_diff($nchan, $running); // returns any new scripts to be started
$stop = array_diff($running, $nchan); // returns any old scripts to be stopped
$running = array_merge($start, $running); // update list of current running nchan scripts
// start nchan scripts which are new
foreach ($start as $row) {
$script = explode(':', $row)[0];
exec("$docroot/$script &>/dev/null &");
}
// stop nchan scripts with the :stop option
foreach ($stop as $row) {
[$script, $opt] = my_explode(':', $row);
if ($opt == 'stop') {
exec('pkill --ns $$ -f '.escapeshellarg($docroot.'/'.$script).' &>/dev/null &');
array_splice($running, array_search($row, $running), 1);
}
}
// start nchan scripts which are new or have been terminated but still should be running
if (count($running)) {
file_put_contents($nchan_pid, implode("\n", $running) . "\n");
file_put_contents_atomic($nchan_pid, implode("\n", $running) . "\n");
foreach ($running as $row) {
$script = explode(':', $row, 2)[0];
$output = [];
exec('pgrep --ns $$ -f ' . escapeshellarg("$docroot/$script"),$output,$retval);
if ($retval !== 0) { // 0=found; 1=none; 2=error
exec(escapeshellarg("$docroot/$script") . ' >/dev/null 2>&1 &');
}
}
} else {
@unlink($nchan_pid);
}

View File

@@ -494,4 +494,20 @@ function fillAvailableHeight(params = { // default params
// Add the new listener
window.addEventListener('resize', window.fillAvailableHeightResizeHandler);
}
/**
* For every a.info element, we see if it has an inner span element.
* While the CSS will determine visibility, we still need to use JS to set the position of the "tooltip" span.
* Using the a.info element's offset position, we can calculate the top and left position needed for the span.
*/
$(document).on('mouseenter', 'a.info', function() {
const tooltip = $(this).find('span');
if (tooltip.length) {
const aInfoPosition = $(this).offset();
const addtionalOffset = 16;
const top = aInfoPosition.top + addtionalOffset;
const left = aInfoPosition.left + addtionalOffset;
tooltip.css({ top, left });
}
});
</script>

View File

@@ -56,6 +56,7 @@ function getArrayStatus($var) {
</span>
<span id="user-notice" class="red-text"></span>
</div>
<div class="footer-spacer">&nbsp;</div>
<div id="copyright" class="footer-right">
<unraid-theme-switcher
current="<?=$theme?>"

View File

@@ -110,6 +110,14 @@ table.tablesorter.indexer tbody tr:hover td {
background-color: var(--browse-table-tbody-tr-hover-td-bg-color);
}
.ui-dialog-content dd {
position: relative;
.fileTree {
top: 4rem;
}
}
.Theme--black {
tr.ace_optionsMenuEntry td select {
color: var(--browse-text-color);

View File

@@ -165,21 +165,19 @@ i.job {
span.dfm_filter {
position: relative;
margin-left: 12px;
top: -2px;
}
span.dfm_filter i {
position: absolute;
left: 10px;
top: 4px;
top: 6px;
font-size: 1.4rem;
}
input.dfm_filter {
border: none;
width: 100px;
background-color: var(--input-dfm-filter-bg-color);
margin: -8px 0 0 0;
padding-left: 30px;
line-height: normal;
}
input.dfm_filter:focus {
background-color: var(--input-dfm-filter-bg-color);
@@ -187,29 +185,10 @@ input.dfm_filter:focus {
input#dfm_target {
color: var(--input-dfm-target-text-color);
}
/* .fileTree {
width: 500px;
max-height: 320px;
} */
i.dfm_filter {
margin-top: -2px;
}
div.autoheight {
width: 100%;
overflow-y: auto;
}
#dfm_jobs {
padding: 2rem 0;
}
.Theme--sidebar {
span.dfm_filter {
margin-left: 0;
top: -8px;
}
span.dfm_filter i {
top: 8px;
}
i.dfm_filter {
margin-top: -4px;
}
}
}

View File

@@ -74,9 +74,11 @@ a.info span {
display: none;
white-space: nowrap;
font-variant: small-caps;
position: absolute;
top: 16px;
left: 12px;
/*
- Must be fixed to avoid CSS limitation with overflow-x: auto; on TableContainer as overflow-y: visible; with that is not supported.
- position values are determined by JS in BodyInlineJS.php w/ the a.info element's offset position.
*/
position: fixed;
line-height: 2rem;
color: var(--text-color);
padding: 5px 8px;
@@ -673,8 +675,8 @@ div.title span img {
@media (min-width: 768px) {
#footer {
flex-direction: row;
justify-content: space-between;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
position: fixed;
bottom: 0;
@@ -700,7 +702,9 @@ div.title span img {
justify-content: center;
align-items: center;
gap: 1rem;
flex: 1 1 auto; /* Take available space */
}
.footer-spacer {
display: none; /* Hidden by default on mobile */
}
.footer-right,
#copyright {
@@ -732,13 +736,14 @@ div.title span img {
@media (min-width: 768px) {
.footer-left {
justify-content: flex-start;
flex: 0 0 auto; /* Only take needed space on desktop */
}
.footer-spacer {
display: block; /* Show on desktop */
}
.footer-right,
#copyright {
text-align: right;
justify-content: flex-start; /* Start from left to enable overflow */
flex: 1 1 0; /* Take remaining space */
width: auto; /* Override mobile full width */
min-width: 0; /* Critical for overflow to work */
}

View File

@@ -1460,7 +1460,18 @@ div.icon-zip {
}
.ui-dialog {
/* Center the dialog no matter what */
box-sizing: border-box;
* {
box-sizing: border-box;
}
}
.ui-dialog:not(.ui-dialog-content-full) {
/*
- If there is no .ui-dialog-content-full class, then we need to center the dialog no matter what the library JS sets on the element.
- Otherwise, if we do have .ui-dialog-content-full on the element, then we need to use the library JS to position the dialog at "full screen" size.
*/
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
@@ -1468,10 +1479,6 @@ div.icon-zip {
width: 100% !important;
max-width: 100rem;
* {
box-sizing: border-box;
}
.ui-dialog-content {
display: flex;
flex-direction: column;
@@ -1492,75 +1499,76 @@ div.icon-zip {
margin-top: auto;
}
.ui-dialog-buttonpane {
.ui-dialog-buttonset {
button {
font-family: clear-sans;
font-size: 1.1rem;
font-weight: bold;
letter-spacing: 2px;
text-transform: uppercase;
margin: 10px 12px 10px 0;
padding: 9px 18px;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
outline: none;
border-radius: 4px;
border: 0;
color: var(--dynamix-jquery-ui-button-text-color);
background:
-webkit-gradient(
linear,
left top,
right top,
from(var(--dynamix-jquery-ui-button-background-start)),
to(var(--dynamix-jquery-ui-button-background-end))
)
0 0 no-repeat,
-webkit-gradient(
linear,
left top,
right top,
from(var(--dynamix-jquery-ui-button-background-start)),
to(var(--dynamix-jquery-ui-button-background-end))
) 0 100% no-repeat,
-webkit-gradient(
linear,
left bottom,
left top,
from(var(--dynamix-jquery-ui-button-background-start)),
to(var(--dynamix-jquery-ui-button-background-start))
) 0 100% no-repeat,
-webkit-gradient(
linear,
left bottom,
left top,
from(var(--dynamix-jquery-ui-button-background-end)),
to(var(--dynamix-jquery-ui-button-background-end))
) 100% 100% no-repeat;
background:
linear-gradient(90deg, var(--dynamix-jquery-ui-button-background-start) 0, var(--dynamix-jquery-ui-button-background-end)) 0 0 no-repeat,
linear-gradient(90deg, var(--dynamix-jquery-ui-button-background-start) 0, var(--dynamix-jquery-ui-button-background-end)) 0 100% no-repeat,
linear-gradient(0deg, var(--dynamix-jquery-ui-button-background-start) 0, var(--dynamix-jquery-ui-button-background-start)) 0 100% no-repeat,
linear-gradient(0deg, var(--dynamix-jquery-ui-button-background-end) 0, var(--dynamix-jquery-ui-button-background-end)) 100% 100% no-repeat;
background-size:
100% 2px,
100% 2px,
2px 100%,
2px 100%;
}
&:hover {
color: var(--dynamix-jquery-ui-button-hover-color);
background: -webkit-gradient(
.ui-dialog-buttonpane {
.ui-dialog-buttonset {
button {
font-family: clear-sans;
font-size: 1.1rem;
font-weight: bold;
letter-spacing: 2px;
text-transform: uppercase;
margin: 10px 12px 10px 0;
padding: 9px 18px;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
outline: none;
border-radius: 4px;
border: 0;
color: var(--dynamix-jquery-ui-button-text-color);
background:
-webkit-gradient(
linear,
left top,
right top,
from(var(--dynamix-jquery-ui-button-background-start)),
to(var(--dynamix-jquery-ui-button-background-end))
);
background: linear-gradient(90deg, var(--dynamix-jquery-ui-button-background-start) 0, var(--dynamix-jquery-ui-button-background-end));
}
)
0 0 no-repeat,
-webkit-gradient(
linear,
left top,
right top,
from(var(--dynamix-jquery-ui-button-background-start)),
to(var(--dynamix-jquery-ui-button-background-end))
) 0 100% no-repeat,
-webkit-gradient(
linear,
left bottom,
left top,
from(var(--dynamix-jquery-ui-button-background-start)),
to(var(--dynamix-jquery-ui-button-background-start))
) 0 100% no-repeat,
-webkit-gradient(
linear,
left bottom,
left top,
from(var(--dynamix-jquery-ui-button-background-end)),
to(var(--dynamix-jquery-ui-button-background-end))
) 100% 100% no-repeat;
background:
linear-gradient(90deg, var(--dynamix-jquery-ui-button-background-start) 0, var(--dynamix-jquery-ui-button-background-end)) 0 0 no-repeat,
linear-gradient(90deg, var(--dynamix-jquery-ui-button-background-start) 0, var(--dynamix-jquery-ui-button-background-end)) 0 100% no-repeat,
linear-gradient(0deg, var(--dynamix-jquery-ui-button-background-start) 0, var(--dynamix-jquery-ui-button-background-start)) 0 100% no-repeat,
linear-gradient(0deg, var(--dynamix-jquery-ui-button-background-end) 0, var(--dynamix-jquery-ui-button-background-end)) 100% 100% no-repeat;
background-size:
100% 2px,
100% 2px,
2px 100%,
2px 100%;
&:hover {
color: var(--dynamix-jquery-ui-button-hover-color);
background: -webkit-gradient(
linear,
left top,
right top,
from(var(--dynamix-jquery-ui-button-background-start)),
to(var(--dynamix-jquery-ui-button-background-end))
);
background: linear-gradient(90deg, var(--dynamix-jquery-ui-button-background-start) 0, var(--dynamix-jquery-ui-button-background-end));
}
}
}