Ask with Unraid's own dialog, not the browser's
Every confirm() and prompt() in the plugin could be switched off from inside itself — one tick of "prevent this page from creating additional dialogs" and all 32 of them returned false while drawing nothing, across every tab, until a full reload. swal is already global on every webGUI page and core uses it 370 times without a single confirm(), so this costs no new dependency. vvConfirmRun() is the one that mattered: it returned a boolean to three callers testing !it, and an unawaited promise is always truthy, so leaving those alone would have run every job without asking. The wrapper's callback is a classic function expression on purpose — SweetAlert only calls back on cancel when the callback's own source declares a parameter, and an arrow would have hung the promise forever.
This commit is contained in:
@@ -104,6 +104,82 @@ function vvSafeUrl(u) {
|
||||
if (/^\/(?!\/)/.test(s)) return s;
|
||||
return '';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// DIALOGS — never the browser's own
|
||||
//
|
||||
// confirm(), alert() and prompt() all carry a checkbox inside the dialog reading "prevent this
|
||||
// page from creating additional dialogs". The moment it is ticked, every later call from that
|
||||
// document returns false and draws nothing. Guards written as `if (!confirm(x)) return;` then
|
||||
// decline silently and the button reads as dead — and because the webGUI swaps tabs by AJAX
|
||||
// without tearing down the document, the suppression follows the operator across tabs and
|
||||
// survives until a full reload. That happened on 2026-08-09: dismissing several repair findings
|
||||
// in a row produced the checkbox, and the AI tab's buttons went dead until history was cleared.
|
||||
//
|
||||
// swal is Unraid's own dialog, defined in webGui/javascript/dynamix.js and loaded on every page
|
||||
// by DefaultPageLayout — core calls it around 370 times and uses confirm() exactly never. So
|
||||
// this is not a new dependency, and these dialogs look like the rest of the machine.
|
||||
//
|
||||
// THE TRAP, and why the callback below is not an arrow function:
|
||||
// On cancel, SweetAlert 1.x only invokes the callback if the callback's own source declares a
|
||||
// parameter — it literally does String(fn).replace(/\s/g,'') and checks that it starts with
|
||||
// "function(" and that the next character is not ")". An arrow function stringifies as "ok=>…"
|
||||
// and fails that test, so cancel would never resolve this promise and the awaiting caller
|
||||
// would hang forever with no dialog on screen. Which is the exact bug this file is replacing,
|
||||
// wearing a different hat. Classic function expression, one named parameter, deliberately.
|
||||
function vvConfirm(text, opts) {
|
||||
const o = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
// Without swal there is nothing better than the browser's own dialog. It carries the bug
|
||||
// described above, but the alternative is a guard that answers neither yes nor no.
|
||||
if (typeof swal !== 'function') { resolve(window.confirm(text)); return; }
|
||||
swal({
|
||||
title: o.title || 'Are you sure?',
|
||||
text: String(text ?? ''),
|
||||
type: o.type || 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: o.confirmText || 'Yes',
|
||||
cancelButtonText: o.cancelText || 'Cancel',
|
||||
confirmButtonColor: o.danger === false ? '#3085d6' : '#d9534f',
|
||||
closeOnConfirm: true,
|
||||
}, function (ok) { resolve(ok !== false); });
|
||||
});
|
||||
}
|
||||
|
||||
// Telling the operator something, with no question attached. Returns a promise so a caller can
|
||||
// sequence on it, but nothing has to await it.
|
||||
function vvAlert(text, opts) {
|
||||
const o = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
if (typeof swal !== 'function') { window.alert(text); resolve(true); return; }
|
||||
swal({
|
||||
title: o.title || '',
|
||||
text: String(text ?? ''),
|
||||
type: o.type || 'info',
|
||||
confirmButtonText: o.confirmText || 'OK',
|
||||
}, function (ok) { resolve(true); });
|
||||
});
|
||||
}
|
||||
|
||||
// Asking for a value. Resolves to the string, or null when cancelled — prompt()'s own contract,
|
||||
// so call sites keep reading the same way. The empty string is a real answer and is not null.
|
||||
function vvPrompt(text, def, opts) {
|
||||
const o = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
if (typeof swal !== 'function') { resolve(window.prompt(text, def || '')); return; }
|
||||
swal({
|
||||
title: o.title || '',
|
||||
text: String(text ?? ''),
|
||||
type: 'input',
|
||||
inputValue: def || '',
|
||||
inputPlaceholder: o.placeholder || '',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: o.confirmText || 'OK',
|
||||
cancelButtonText: o.cancelText || 'Cancel',
|
||||
closeOnConfirm: true,
|
||||
}, function (val) { resolve(val === false ? null : String(val)); });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
|
||||
@@ -512,7 +512,7 @@ function _showModalErr(id, msg) {
|
||||
}
|
||||
|
||||
// Proxy event delegation
|
||||
document.getElementById('vv-au-panel-proxies').addEventListener('click', e => {
|
||||
document.getElementById('vv-au-panel-proxies').addEventListener('click', async e => {
|
||||
// Add button
|
||||
if (e.target.id === 'vv-au-proxy-add') { _proxyModal(null); return; }
|
||||
// Toggle
|
||||
@@ -534,7 +534,7 @@ document.getElementById('vv-au-panel-proxies').addEventListener('click', e => {
|
||||
if (delBtn) {
|
||||
const id = parseInt(delBtn.dataset.proxyDel);
|
||||
const p = _proxies.find(x => x.id === id);
|
||||
if (!confirm('Delete proxy for ' + (p?.domain_names||['this host']).join(', ') + '?')) return;
|
||||
if (!await vvConfirm('Delete proxy for ' + (p?.domain_names||['this host']).join(', ') + '?')) return;
|
||||
_post({ action:'npm_delete', id }, r => { if (r.ok) _loadProxies(); });
|
||||
}
|
||||
});
|
||||
@@ -678,7 +678,7 @@ function _addToGroupModal(uid) {
|
||||
}
|
||||
|
||||
// User event delegation
|
||||
document.getElementById('vv-au-panel-users').addEventListener('click', e => {
|
||||
document.getElementById('vv-au-panel-users').addEventListener('click', async e => {
|
||||
if (e.target.id === 'vv-au-user-add') { _userModal(null); return; }
|
||||
|
||||
const editBtn = e.target.closest('[data-user-edit]');
|
||||
@@ -694,7 +694,7 @@ document.getElementById('vv-au-panel-users').addEventListener('click', e => {
|
||||
if (delBtn) {
|
||||
const uid = delBtn.dataset.userDel;
|
||||
const user = _users.find(u => u.id === uid);
|
||||
if (!confirm('Delete user "' + (user?.displayName||uid) + '"?')) return;
|
||||
if (!await vvConfirm('Delete user "' + (user?.displayName||uid) + '"?')) return;
|
||||
_post({ action:'lldap_delete_user', uid }, r => { if (r.ok) _loadUsers(); });
|
||||
return;
|
||||
}
|
||||
@@ -704,7 +704,7 @@ document.getElementById('vv-au-panel-users').addEventListener('click', e => {
|
||||
const uid = rmBadge.dataset.rmFromGroup;
|
||||
const gid = parseInt(rmBadge.dataset.gid);
|
||||
const grp = _groups.find(g => g.id === gid);
|
||||
if (!confirm('Remove from group "' + (grp?.displayName||gid) + '"?')) return;
|
||||
if (!await vvConfirm('Remove from group "' + (grp?.displayName||gid) + '"?')) return;
|
||||
_post({ action:'lldap_remove_from_group', uid, gid }, r => { if (r.ok) { _loadUsers(); _loadGroups(); } });
|
||||
}
|
||||
});
|
||||
@@ -745,7 +745,7 @@ function _loadGroups() {
|
||||
}
|
||||
|
||||
// Group event delegation
|
||||
document.getElementById('vv-au-panel-users').addEventListener('click', e => {
|
||||
document.getElementById('vv-au-panel-users').addEventListener('click', async e => {
|
||||
if (e.target.id === 'vv-au-group-add') {
|
||||
_modal(`<h3>Add Group</h3>
|
||||
<div class="vv-au-field">
|
||||
@@ -783,7 +783,7 @@ document.getElementById('vv-au-panel-users').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const id = parseInt(delGrp.dataset.groupDel);
|
||||
const grp = _groups.find(g => g.id === id);
|
||||
if (!confirm('Delete group "' + (grp?.displayName||id) + '"?')) return;
|
||||
if (!await vvConfirm('Delete group "' + (grp?.displayName||id) + '"?')) return;
|
||||
_post({ action:'lldap_delete_group', id }, r => { if (r.ok) _loadGroups(); });
|
||||
return;
|
||||
}
|
||||
@@ -927,7 +927,7 @@ function _ruleModal(idx) {
|
||||
}
|
||||
|
||||
// ACL event delegation
|
||||
document.getElementById('vv-au-panel-acl').addEventListener('click', e => {
|
||||
document.getElementById('vv-au-panel-acl').addEventListener('click', async e => {
|
||||
if (e.target.id === 'vv-au-rule-add') { _ruleModal(null); return; }
|
||||
|
||||
if (e.target.id === 'vv-au-ac-save') {
|
||||
@@ -950,7 +950,7 @@ document.getElementById('vv-au-panel-acl').addEventListener('click', e => {
|
||||
const delBtn = e.target.closest('[data-rule-del]');
|
||||
if (delBtn) {
|
||||
const i = parseInt(delBtn.dataset.ruleDel);
|
||||
if (!confirm('Delete this rule?')) return;
|
||||
if (!await vvConfirm('Delete this rule?')) return;
|
||||
_rules.splice(i, 1);
|
||||
_renderAcl();
|
||||
return;
|
||||
|
||||
@@ -485,18 +485,18 @@ function _bindEvents() {
|
||||
|
||||
// Delete folder
|
||||
document.querySelectorAll('[data-delete-folder]').forEach(el => {
|
||||
el.addEventListener('click', e => {
|
||||
el.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
const fid = el.dataset.deleteFolder;
|
||||
const f = (_data.folders||[]).find(x=>x.id===fid);
|
||||
if (!f || !confirm(`Delete "${f.name}"? Containers will be ungrouped.`)) return;
|
||||
if (!f || !await vvConfirm(`Delete "${f.name}"? Containers will be ungrouped.`)) return;
|
||||
_api({action:'delete_folder',folder_id:fid}, r => { if(r.ok) _reload(); else alert('Delete failed: '+(r.error||'?')); });
|
||||
});
|
||||
});
|
||||
|
||||
// New folder
|
||||
document.getElementById('vv-dk-add-folder')?.addEventListener('click', () => {
|
||||
const name = prompt('New folder name:');
|
||||
document.getElementById('vv-dk-add-folder')?.addEventListener('click', async () => {
|
||||
const name = await vvPrompt('New folder name:');
|
||||
if (name?.trim()) _api({action:'create_folder',name:name.trim()}, r => { if(r.ok) _reload(); else alert(r.error); });
|
||||
});
|
||||
}
|
||||
@@ -520,11 +520,11 @@ function _showPopover(x, y) {
|
||||
pop.style.top = Math.min(y+8, vh-180)+'px';
|
||||
|
||||
pop.querySelectorAll('[data-fid]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
el.addEventListener('click', async () => {
|
||||
const fid = el.dataset.fid;
|
||||
_hidePopover();
|
||||
if (fid==='__new__') {
|
||||
const name = prompt('New folder name:');
|
||||
const name = await vvPrompt('New folder name:');
|
||||
if (!name?.trim()) return;
|
||||
_api({action:'create_folder',name:name.trim()}, r => {
|
||||
if (!r.ok) { alert(r.error); return; }
|
||||
@@ -558,8 +558,8 @@ function _syncC2J() {
|
||||
_api({action:'sync_conf_to_json'}, r => { if(r.ok) _reload(); else alert(r.error); });
|
||||
}
|
||||
document.getElementById('vv-dk-sync-c2j').addEventListener('click', _syncC2J);
|
||||
document.getElementById('vv-dk-sync-j2c').addEventListener('click', () => {
|
||||
if (!confirm('Overwrite conf map with current JSON state?')) return;
|
||||
document.getElementById('vv-dk-sync-j2c').addEventListener('click', async () => {
|
||||
if (!await vvConfirm('Overwrite conf map with current JSON state?')) return;
|
||||
_api({action:'sync_json_to_conf'}, r => { if(r.ok) _reload(); else alert(r.error); });
|
||||
});
|
||||
|
||||
|
||||
@@ -2608,7 +2608,7 @@ function vvToggleContainer(name) {
|
||||
vvRenderDockerFolders(vvDfData);
|
||||
}
|
||||
|
||||
function vvDockerAction(action, name, webui) {
|
||||
async function vvDockerAction(action, name, webui) {
|
||||
// Filtered again at the point of use, not only where the button was built. This value originates
|
||||
// in a container's template XML, and window.open() on a javascript: URL runs it with this page's
|
||||
// origin — the one sink where an unchecked scheme is not merely a broken link.
|
||||
@@ -2625,7 +2625,7 @@ function vvDockerAction(action, name, webui) {
|
||||
// Stopping is confirmed; starting is not. The asymmetry is the point — start is recoverable by
|
||||
// clicking the other button, stop takes a service away from whoever is using it, and these
|
||||
// buttons sit inside a dense grid where the row under the cursor is easy to misjudge.
|
||||
if (action === 'stop' && !confirm('Stop ' + name + '?')) return;
|
||||
if (action === 'stop' && !await vvConfirm('Stop ' + name + '?')) return;
|
||||
|
||||
const fd = new URLSearchParams();
|
||||
fd.set('action', action);
|
||||
|
||||
@@ -225,8 +225,8 @@ function vvApiKey(btn) {
|
||||
});
|
||||
}
|
||||
|
||||
function vvPtPhase2(btn, hostId) {
|
||||
if (!confirm(`Phase 2: Deploy containers + arr stack + establish partnership on ${hostId}?\n\nRequires ${hostId} to have Varaverk installed and SSH keys set up.`)) return;
|
||||
async function vvPtPhase2(btn, hostId) {
|
||||
if (!await vvConfirm(`Phase 2: Deploy containers + arr stack + establish partnership on ${hostId}?\n\nRequires ${hostId} to have Varaverk installed and SSH keys set up.`)) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Starting…';
|
||||
_vvPtRun('Partnership/partnership_onboard.sh', '--phase2-only')
|
||||
@@ -235,8 +235,8 @@ function vvPtPhase2(btn, hostId) {
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Run Phase 2 Manually'; }, 3000));
|
||||
}
|
||||
|
||||
function vvPtOnboard(btn) {
|
||||
if (!confirm('Run full partnership_onboard.sh?\n\nRun on the MIRROR first, then on the OWNER.\n\nUse Phase 1 + Phase 2 buttons for step-by-step control.')) return;
|
||||
async function vvPtOnboard(btn) {
|
||||
if (!await vvConfirm('Run full partnership_onboard.sh?\n\nRun on the MIRROR first, then on the OWNER.\n\nUse Phase 1 + Phase 2 buttons for step-by-step control.')) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Starting…';
|
||||
_vvPtRun('Partnership/partnership_onboard.sh')
|
||||
@@ -245,8 +245,8 @@ function vvPtOnboard(btn) {
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard (Mirror)'; }, 4000));
|
||||
}
|
||||
|
||||
function vvPtPushConf(btn, hostId) {
|
||||
if (!confirm(`Push conf to ${hostId}?\n\nAssumes SSH key is already installed on ${hostId}.\nSkips key generation/install, goes straight to conf push + local setup.`)) return;
|
||||
async function vvPtPushConf(btn, hostId) {
|
||||
if (!await vvConfirm(`Push conf to ${hostId}?\n\nAssumes SSH key is already installed on ${hostId}.\nSkips key generation/install, goes straight to conf push + local setup.`)) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Pushing…';
|
||||
_vvPtRun('Partnership/partnership_onboard.sh', '--phase1-only --skip-ssh')
|
||||
@@ -255,8 +255,8 @@ function vvPtPushConf(btn, hostId) {
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Push Conf (key installed)'; }, 4000));
|
||||
}
|
||||
|
||||
function vvPtLocalSetup(btn) {
|
||||
if (!confirm('Complete HOST1 local setup?\n\nRuns FolderView3 integration and marks HOST1 as locally ready.\nDoes not require HOST2 to be online.')) return;
|
||||
async function vvPtLocalSetup(btn) {
|
||||
if (!await vvConfirm('Complete HOST1 local setup?\n\nRuns FolderView3 integration and marks HOST1 as locally ready.\nDoes not require HOST2 to be online.')) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Running…';
|
||||
_vvPtRun('Partnership/partnership_manager.sh', '--onboard --local-only')
|
||||
@@ -265,8 +265,8 @@ function vvPtLocalSetup(btn) {
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Complete HOST1 Setup'; }, 4000));
|
||||
}
|
||||
|
||||
function vvPtCancel(btn, hostId) {
|
||||
if (!confirm(`Cancel onboard for ${hostId}?\n\nThis will remove keys in both directions and reset all phase state.\n\nContinue?`)) return;
|
||||
async function vvPtCancel(btn, hostId) {
|
||||
if (!await vvConfirm(`Cancel onboard for ${hostId}?\n\nThis will remove keys in both directions and reset all phase state.\n\nContinue?`)) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Cancelling…';
|
||||
_vvPtRun('Partnership/onboard_cancel.sh', '--direction=both')
|
||||
@@ -275,8 +275,8 @@ function vvPtCancel(btn, hostId) {
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Cancel'; }, 4000));
|
||||
}
|
||||
|
||||
function vvPtDeleteH1(btn, hostId) {
|
||||
if (!confirm(`Remove HOST1's key from ${hostId}?\n\n• Deletes local SSH key pair\n• Removes it from ${hostId}'s authorized_keys\n• Clears phase state`)) return;
|
||||
async function vvPtDeleteH1(btn, hostId) {
|
||||
if (!await vvConfirm(`Remove HOST1's key from ${hostId}?\n\n• Deletes local SSH key pair\n• Removes it from ${hostId}'s authorized_keys\n• Clears phase state`)) return;
|
||||
btn.disabled = true; btn.textContent = '⟳ Removing…';
|
||||
_vvPtRun('Partnership/onboard_cancel.sh', '--direction=h1')
|
||||
.then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
|
||||
@@ -284,8 +284,8 @@ function vvPtDeleteH1(btn, hostId) {
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Remove HOST1 key'; }, 4000));
|
||||
}
|
||||
|
||||
function vvPtDeleteH2(btn, hostId) {
|
||||
if (!confirm(`Remove ${hostId}'s key from HOST1?\n\n• Removes ${hostId}'s public key from HOST1's authorized_keys\n• ${hostId} will no longer be able to SSH into HOST1`)) return;
|
||||
async function vvPtDeleteH2(btn, hostId) {
|
||||
if (!await vvConfirm(`Remove ${hostId}'s key from HOST1?\n\n• Removes ${hostId}'s public key from HOST1's authorized_keys\n• ${hostId} will no longer be able to SSH into HOST1`)) return;
|
||||
btn.disabled = true; btn.textContent = '⟳ Removing…';
|
||||
_vvPtRun('Partnership/onboard_cancel.sh', '--direction=h2')
|
||||
.then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
|
||||
@@ -293,9 +293,9 @@ function vvPtDeleteH2(btn, hostId) {
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = `✕ Remove ${hostId} key`; }, 4000));
|
||||
}
|
||||
|
||||
function vvPtOffboard(btn) {
|
||||
async function vvPtOffboard(btn) {
|
||||
if (btn.style.opacity === '0.35' || btn.style.cursor === 'default') return;
|
||||
if (!confirm('Run partnership_offboard.sh?\n\nThis will end the partnership, reconfigure WebUIs, and revoke SSH access.\n\nContinue?')) return;
|
||||
if (!await vvConfirm('Run partnership_offboard.sh?\n\nThis will end the partnership, reconfigure WebUIs, and revoke SSH access.\n\nContinue?')) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Starting…';
|
||||
_vvPtRun('Partnership/partnership_offboard.sh')
|
||||
@@ -304,10 +304,10 @@ function vvPtOffboard(btn) {
|
||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Offboard'; }, 4000));
|
||||
}
|
||||
|
||||
function vvPtTransfer(btn, token) {
|
||||
if (!confirm('Transfer ownership to the mirror?\n\nThis promotes the mirror to OWNER and demotes this server. '
|
||||
async function vvPtTransfer(btn, token) {
|
||||
if (!await vvConfirm('Transfer ownership to the mirror?\n\nThis promotes the mirror to OWNER and demotes this server. '
|
||||
+ 'partnership_transfer.sh requires sustained health checks before the switch completes.\n\nContinue?')) return;
|
||||
if (prompt('Type YES to confirm ownership transfer:') !== 'YES') return;
|
||||
if (await vvPrompt('Type YES to confirm ownership transfer:') !== 'YES') return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Transferring…';
|
||||
_vvPtRun('Partnership/partnership_transfer.sh', '--confirm=' + token)
|
||||
@@ -382,12 +382,12 @@ function vvPtHostChanged(el) {
|
||||
document.getElementById('vv-pt-hosts-save').style.display = any ? '' : 'none';
|
||||
}
|
||||
|
||||
function vvPtSaveHosts(btn) {
|
||||
async function vvPtSaveHosts(btn) {
|
||||
const changedEls = document.querySelectorAll('.vv-pt-host-inp.changed');
|
||||
if (!changedEls.length) return;
|
||||
const changes = [];
|
||||
changedEls.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));
|
||||
if (!confirm(`Save ${changes.length} host setting(s)?`)) return;
|
||||
if (!await vvConfirm(`Save ${changes.length} host setting(s)?`)) return;
|
||||
btn.disabled = true; btn.textContent = '⟳ Saving…';
|
||||
fetch('/plugins/varaverk/api/confform.php', {
|
||||
method: 'POST',
|
||||
@@ -481,12 +481,12 @@ function vvPtArrChanged(el) {
|
||||
if (saveBtn) saveBtn.style.display = any ? '' : 'none';
|
||||
}
|
||||
|
||||
function vvPtSaveArrays(btn) {
|
||||
async function vvPtSaveArrays(btn) {
|
||||
const changedEls = document.querySelectorAll('#vv-pt-arrays-wrap .vv-set-input.changed');
|
||||
if (!changedEls.length) return;
|
||||
const changes = [];
|
||||
changedEls.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));
|
||||
if (!confirm(`Save ${changes.length} changed setting(s)?`)) return;
|
||||
if (!await vvConfirm(`Save ${changes.length} changed setting(s)?`)) return;
|
||||
btn.disabled = true; btn.textContent = '⟳ Saving…';
|
||||
fetch('/plugins/varaverk/api/confform.php', {
|
||||
method: 'POST',
|
||||
@@ -591,14 +591,14 @@ function vvPtSetChanged(el) {
|
||||
document.getElementById('vv-pt-settings-save').style.display = any ? '' : 'none';
|
||||
}
|
||||
|
||||
function vvPtSaveSettings(btn) {
|
||||
async function vvPtSaveSettings(btn) {
|
||||
const changedEls = document.querySelectorAll('#vv-pt-settings-body .vv-set-input.changed');
|
||||
if (!changedEls.length) return;
|
||||
const changes = [];
|
||||
changedEls.forEach(el => changes.push({
|
||||
key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value
|
||||
}));
|
||||
if (!confirm(`Save ${changes.length} changed setting(s)?`)) return;
|
||||
if (!await vvConfirm(`Save ${changes.length} changed setting(s)?`)) return;
|
||||
btn.disabled = true; btn.textContent = '⟳ Saving…';
|
||||
const params = new URLSearchParams({
|
||||
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
||||
|
||||
@@ -1481,10 +1481,10 @@ function vvRpSave() {
|
||||
.catch(() => { btn.disabled = false; btn.textContent = 'Save Profile'; fb.style.color='#ef5350'; fb.textContent='Request failed'; });
|
||||
}
|
||||
|
||||
function vvRpDelete() {
|
||||
async function vvRpDelete() {
|
||||
const name = document.getElementById('vv-rp-name').value.trim();
|
||||
if (!name) return;
|
||||
if (!confirm(`Delete profile "${name}"?\n\nThis removes it from all PROFILE_* arrays in master.conf.`)) return;
|
||||
if (!await vvConfirm(`Delete profile "${name}"?\n\nThis removes it from all PROFILE_* arrays in master.conf.`)) return;
|
||||
|
||||
const fb = document.getElementById('vv-rp-fb');
|
||||
const del = document.getElementById('vv-rp-del-btn');
|
||||
|
||||
@@ -1573,8 +1573,8 @@ function vvSelectLog(btn) {
|
||||
//
|
||||
// Dry Run is named in the prompt on purpose. For anything destructive it is the actual answer to
|
||||
// "are you sure", and it is the button immediately beside the one that raised the question.
|
||||
function vvConfirmRun(id) {
|
||||
return confirm('Run ' + id + ' now?\n\n'
|
||||
async function vvConfirmRun(id) {
|
||||
return vvConfirm('Run ' + id + ' now?\n\n'
|
||||
+ 'It starts immediately and as root, with the same effect as a scheduled run.\n'
|
||||
+ 'Use Dry Run first if you want to see what it would change.');
|
||||
}
|
||||
@@ -1596,16 +1596,16 @@ function vvRunStart(id, data) {
|
||||
}
|
||||
|
||||
// Run a script by ID directly — no DOM card needed.
|
||||
function vvRunById(id) {
|
||||
if (!vvConfirmRun(id)) return;
|
||||
async function vvRunById(id) {
|
||||
if (!await vvConfirmRun(id)) return;
|
||||
vvRunStart(id, {id, manual: '1'});
|
||||
}
|
||||
|
||||
function vvGitPull(btn) {
|
||||
async function vvGitPull(btn) {
|
||||
const id = 'git_pull_execute.sh';
|
||||
// Asked before the button is disabled, so cancelling does not leave it stuck reading "Pulling…"
|
||||
// for the next four seconds while nothing is pulling.
|
||||
if (!vvConfirmRun(id)) return;
|
||||
if (!await vvConfirmRun(id)) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⟳ Pulling…';
|
||||
vvRunStart(id, {id, manual: '1'});
|
||||
@@ -1613,10 +1613,10 @@ function vvGitPull(btn) {
|
||||
setTimeout(() => { btn.disabled = false; btn.textContent = '↻ Git Pull'; }, 4000);
|
||||
}
|
||||
|
||||
function vvRunJob(btn) {
|
||||
async function vvRunJob(btn) {
|
||||
const job = btn.closest('[data-id]');
|
||||
const id = job.dataset.id;
|
||||
if (!vvConfirmRun(id)) return;
|
||||
if (!await vvConfirmRun(id)) return;
|
||||
const location = job.querySelector('.vv-rsync-location')?.value.trim() || '';
|
||||
const extra_args = job.querySelector('.vv-script-args')?.value.trim() || '';
|
||||
const data = {id};
|
||||
@@ -1913,14 +1913,14 @@ function vvShowEditorMode(title) {
|
||||
requestAnimationFrame(vvFitRight);
|
||||
}
|
||||
|
||||
function vvSaveScript() {
|
||||
async function vvSaveScript() {
|
||||
const name = document.getElementById('vv-editor-name').value.trim();
|
||||
const content = document.getElementById('vv-editor-body').value;
|
||||
if (!name || !/^[a-zA-Z0-9_\-]+$/.test(name)) {
|
||||
alert('Name must be letters, numbers, _ or - only (no spaces, no .sh)');
|
||||
return;
|
||||
}
|
||||
if (!confirm('Save changes to "' + name + '.sh"?')) return;
|
||||
if (!await vvConfirm('Save changes to "' + name + '.sh"?')) return;
|
||||
const btn = document.getElementById('vv-save-script-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
@@ -1933,10 +1933,10 @@ function vvSaveScript() {
|
||||
.catch(() => { btn.disabled = false; btn.textContent = 'Save Script'; });
|
||||
}
|
||||
|
||||
function vvDeleteScript() {
|
||||
async function vvDeleteScript() {
|
||||
if (!vvEditorId) return;
|
||||
const name = vvEditorId.replace(/^Custom\//, '').replace(/\.sh$/, '');
|
||||
if (!confirm('Delete "' + name + '.sh"? This cannot be undone.')) return;
|
||||
if (!await vvConfirm('Delete "' + name + '.sh"? This cannot be undone.')) return;
|
||||
const btn = document.getElementById('vv-delete-script-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting…';
|
||||
@@ -2107,10 +2107,10 @@ function _vvImpUpdateFooter() {
|
||||
btn.disabled = !_vvImpSelected;
|
||||
}
|
||||
|
||||
function _vvImpDoImport() {
|
||||
async function _vvImpDoImport() {
|
||||
if (!_vvImpSelected) return;
|
||||
const dest = (window.__vvCustomScriptsDir || 'Custom Scripts') + '/' + _vvImpSelected.replace(/^.*\//, '');
|
||||
if (!confirm('Move\n ' + _vvImpSelected + '\n→ ' + dest + '\n\nThe original will be deleted once the copy is verified. Continue?')) return;
|
||||
if (!await vvConfirm('Move\n ' + _vvImpSelected + '\n→ ' + dest + '\n\nThe original will be deleted once the copy is verified. Continue?')) return;
|
||||
|
||||
const btn = document.getElementById('vv-imp-do-btn');
|
||||
btn.disabled = true;
|
||||
@@ -2253,10 +2253,10 @@ function vvCfInitArrays(root) {
|
||||
(root || document).querySelectorAll('.vv-cf-array').forEach(vvCfLines);
|
||||
}
|
||||
|
||||
function vvSaveConf() {
|
||||
async function vvSaveConf() {
|
||||
if (!vvConfId) return;
|
||||
const _cfName = vvConfId.replace(/\.sh$/, '').split('/').pop();
|
||||
if (!confirm('Save configuration changes for "' + _cfName + '"?')) return;
|
||||
if (!await vvConfirm('Save configuration changes for "' + _cfName + '"?')) return;
|
||||
const inputs = document.querySelectorAll('#vv-confform .vv-cf-input, #vv-si-view .vv-cf-input');
|
||||
const changes = [];
|
||||
inputs.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));
|
||||
@@ -2895,8 +2895,8 @@ function vvUpdatePartner(partner) {
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function vvClearLock(file, btn) {
|
||||
if (!confirm('Clear lock "' + file + '"?\nOnly do this if the script has crashed and the lock is stale.')) return;
|
||||
async function vvClearLock(file, btn) {
|
||||
if (!await vvConfirm('Clear lock "' + file + '"?\nOnly do this if the script has crashed and the lock is stale.')) return;
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
vvPost('/plugins/varaverk/api/clearlock.php', {file})
|
||||
.then(d => { if (d.ok) vvBoardPoll(); else { btn.disabled = false; btn.textContent = 'Clear'; }})
|
||||
@@ -3491,9 +3491,9 @@ function vvShowRawConfMode(title) {
|
||||
requestAnimationFrame(vvFitRight);
|
||||
}
|
||||
|
||||
function vvSaveRawConf() {
|
||||
async function vvSaveRawConf() {
|
||||
if (!vvRawConfFile) return;
|
||||
if (!vvSetupConf && !confirm('Save changes to "' + vvRawConfFile + '"?')) return;
|
||||
if (!vvSetupConf && !await vvConfirm('Save changes to "' + vvRawConfFile + '"?')) return;
|
||||
const content = document.getElementById('vv-editor-body').value;
|
||||
const btn = document.getElementById('vv-save-rawconf-btn');
|
||||
btn.disabled = true;
|
||||
|
||||
@@ -300,7 +300,7 @@ function _vvStorRender(d) {
|
||||
}
|
||||
}
|
||||
|
||||
function vvStorMigrate() {
|
||||
async function vvStorMigrate() {
|
||||
const btn = document.getElementById('vv-stor-migrate-btn');
|
||||
const out = document.getElementById('vv-stor-out');
|
||||
const fb = document.getElementById('vv-stor-fb');
|
||||
@@ -315,7 +315,7 @@ function vvStorMigrate() {
|
||||
}
|
||||
|
||||
const label = to === 'flash' ? 'Flash (appdata)' : 'Internal (/boot)';
|
||||
if (!confirm(`Migrate Varaverk storage to ${label}?\n\nThis will:\n• Copy all scripts, conf, and git repo to the new location\n• Update varaverk.cfg and master.conf\n• Delete the old location\n\nThe page will need a reload after migration.`)) return;
|
||||
if (!await vvConfirm(`Migrate Varaverk storage to ${label}?\n\nThis will:\n• Copy all scripts, conf, and git repo to the new location\n• Update varaverk.cfg and master.conf\n• Delete the old location\n\nThe page will need a reload after migration.`)) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Migrating…';
|
||||
|
||||
Reference in New Issue
Block a user