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:
Gmer4Lfe
2026-08-09 21:44:43 -04:00
parent 5b48561f36
commit 7d865b0a09
8 changed files with 144 additions and 68 deletions
+76
View File
@@ -104,6 +104,82 @@ function vvSafeUrl(u) {
if (/^\/(?!\/)/.test(s)) return s; if (/^\/(?!\/)/.test(s)) return s;
return ''; 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> </script>
<?php <?php
+9 -9
View File
@@ -512,7 +512,7 @@ function _showModalErr(id, msg) {
} }
// Proxy event delegation // Proxy event delegation
document.getElementById('vv-au-panel-proxies').addEventListener('click', e => { document.getElementById('vv-au-panel-proxies').addEventListener('click', async e => {
// Add button // Add button
if (e.target.id === 'vv-au-proxy-add') { _proxyModal(null); return; } if (e.target.id === 'vv-au-proxy-add') { _proxyModal(null); return; }
// Toggle // Toggle
@@ -534,7 +534,7 @@ document.getElementById('vv-au-panel-proxies').addEventListener('click', e => {
if (delBtn) { if (delBtn) {
const id = parseInt(delBtn.dataset.proxyDel); const id = parseInt(delBtn.dataset.proxyDel);
const p = _proxies.find(x => x.id === id); 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(); }); _post({ action:'npm_delete', id }, r => { if (r.ok) _loadProxies(); });
} }
}); });
@@ -678,7 +678,7 @@ function _addToGroupModal(uid) {
} }
// User event delegation // 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; } if (e.target.id === 'vv-au-user-add') { _userModal(null); return; }
const editBtn = e.target.closest('[data-user-edit]'); const editBtn = e.target.closest('[data-user-edit]');
@@ -694,7 +694,7 @@ document.getElementById('vv-au-panel-users').addEventListener('click', e => {
if (delBtn) { if (delBtn) {
const uid = delBtn.dataset.userDel; const uid = delBtn.dataset.userDel;
const user = _users.find(u => u.id === uid); 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(); }); _post({ action:'lldap_delete_user', uid }, r => { if (r.ok) _loadUsers(); });
return; return;
} }
@@ -704,7 +704,7 @@ document.getElementById('vv-au-panel-users').addEventListener('click', e => {
const uid = rmBadge.dataset.rmFromGroup; const uid = rmBadge.dataset.rmFromGroup;
const gid = parseInt(rmBadge.dataset.gid); const gid = parseInt(rmBadge.dataset.gid);
const grp = _groups.find(g => g.id === 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(); } }); _post({ action:'lldap_remove_from_group', uid, gid }, r => { if (r.ok) { _loadUsers(); _loadGroups(); } });
} }
}); });
@@ -745,7 +745,7 @@ function _loadGroups() {
} }
// Group event delegation // 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') { if (e.target.id === 'vv-au-group-add') {
_modal(`<h3>Add Group</h3> _modal(`<h3>Add Group</h3>
<div class="vv-au-field"> <div class="vv-au-field">
@@ -783,7 +783,7 @@ document.getElementById('vv-au-panel-users').addEventListener('click', e => {
e.stopPropagation(); e.stopPropagation();
const id = parseInt(delGrp.dataset.groupDel); const id = parseInt(delGrp.dataset.groupDel);
const grp = _groups.find(g => g.id === id); 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(); }); _post({ action:'lldap_delete_group', id }, r => { if (r.ok) _loadGroups(); });
return; return;
} }
@@ -927,7 +927,7 @@ function _ruleModal(idx) {
} }
// ACL event delegation // 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-rule-add') { _ruleModal(null); return; }
if (e.target.id === 'vv-au-ac-save') { 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]'); const delBtn = e.target.closest('[data-rule-del]');
if (delBtn) { if (delBtn) {
const i = parseInt(delBtn.dataset.ruleDel); const i = parseInt(delBtn.dataset.ruleDel);
if (!confirm('Delete this rule?')) return; if (!await vvConfirm('Delete this rule?')) return;
_rules.splice(i, 1); _rules.splice(i, 1);
_renderAcl(); _renderAcl();
return; return;
+8 -8
View File
@@ -485,18 +485,18 @@ function _bindEvents() {
// Delete folder // Delete folder
document.querySelectorAll('[data-delete-folder]').forEach(el => { document.querySelectorAll('[data-delete-folder]').forEach(el => {
el.addEventListener('click', e => { el.addEventListener('click', async e => {
e.stopPropagation(); e.stopPropagation();
const fid = el.dataset.deleteFolder; const fid = el.dataset.deleteFolder;
const f = (_data.folders||[]).find(x=>x.id===fid); 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||'?')); }); _api({action:'delete_folder',folder_id:fid}, r => { if(r.ok) _reload(); else alert('Delete failed: '+(r.error||'?')); });
}); });
}); });
// New folder // New folder
document.getElementById('vv-dk-add-folder')?.addEventListener('click', () => { document.getElementById('vv-dk-add-folder')?.addEventListener('click', async () => {
const name = prompt('New folder name:'); 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); }); 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.style.top = Math.min(y+8, vh-180)+'px';
pop.querySelectorAll('[data-fid]').forEach(el => { pop.querySelectorAll('[data-fid]').forEach(el => {
el.addEventListener('click', () => { el.addEventListener('click', async () => {
const fid = el.dataset.fid; const fid = el.dataset.fid;
_hidePopover(); _hidePopover();
if (fid==='__new__') { if (fid==='__new__') {
const name = prompt('New folder name:'); const name = await vvPrompt('New folder name:');
if (!name?.trim()) return; if (!name?.trim()) return;
_api({action:'create_folder',name:name.trim()}, r => { _api({action:'create_folder',name:name.trim()}, r => {
if (!r.ok) { alert(r.error); return; } 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); }); _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-c2j').addEventListener('click', _syncC2J);
document.getElementById('vv-dk-sync-j2c').addEventListener('click', () => { document.getElementById('vv-dk-sync-j2c').addEventListener('click', async () => {
if (!confirm('Overwrite conf map with current JSON state?')) return; 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); }); _api({action:'sync_json_to_conf'}, r => { if(r.ok) _reload(); else alert(r.error); });
}); });
+2 -2
View File
@@ -2608,7 +2608,7 @@ function vvToggleContainer(name) {
vvRenderDockerFolders(vvDfData); 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 // 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 // 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. // 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 // 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 // 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. // 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(); const fd = new URLSearchParams();
fd.set('action', action); fd.set('action', action);
+25 -25
View File
@@ -225,8 +225,8 @@ function vvApiKey(btn) {
}); });
} }
function vvPtPhase2(btn, hostId) { async 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; 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.disabled = true;
btn.textContent = '⟳ Starting…'; btn.textContent = '⟳ Starting…';
_vvPtRun('Partnership/partnership_onboard.sh', '--phase2-only') _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)); .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Run Phase 2 Manually'; }, 3000));
} }
function vvPtOnboard(btn) { async 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; 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.disabled = true;
btn.textContent = '⟳ Starting…'; btn.textContent = '⟳ Starting…';
_vvPtRun('Partnership/partnership_onboard.sh') _vvPtRun('Partnership/partnership_onboard.sh')
@@ -245,8 +245,8 @@ function vvPtOnboard(btn) {
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard (Mirror)'; }, 4000)); .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard (Mirror)'; }, 4000));
} }
function vvPtPushConf(btn, hostId) { async 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; 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.disabled = true;
btn.textContent = '⟳ Pushing…'; btn.textContent = '⟳ Pushing…';
_vvPtRun('Partnership/partnership_onboard.sh', '--phase1-only --skip-ssh') _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)); .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Push Conf (key installed)'; }, 4000));
} }
function vvPtLocalSetup(btn) { async 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; 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.disabled = true;
btn.textContent = '⟳ Running…'; btn.textContent = '⟳ Running…';
_vvPtRun('Partnership/partnership_manager.sh', '--onboard --local-only') _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)); .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Complete HOST1 Setup'; }, 4000));
} }
function vvPtCancel(btn, hostId) { async 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; 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.disabled = true;
btn.textContent = '⟳ Cancelling…'; btn.textContent = '⟳ Cancelling…';
_vvPtRun('Partnership/onboard_cancel.sh', '--direction=both') _vvPtRun('Partnership/onboard_cancel.sh', '--direction=both')
@@ -275,8 +275,8 @@ function vvPtCancel(btn, hostId) {
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Cancel'; }, 4000)); .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Cancel'; }, 4000));
} }
function vvPtDeleteH1(btn, hostId) { async 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; 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…'; btn.disabled = true; btn.textContent = '⟳ Removing…';
_vvPtRun('Partnership/onboard_cancel.sh', '--direction=h1') _vvPtRun('Partnership/onboard_cancel.sh', '--direction=h1')
.then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); }) .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)); .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Remove HOST1 key'; }, 4000));
} }
function vvPtDeleteH2(btn, hostId) { async 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; 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…'; btn.disabled = true; btn.textContent = '⟳ Removing…';
_vvPtRun('Partnership/onboard_cancel.sh', '--direction=h2') _vvPtRun('Partnership/onboard_cancel.sh', '--direction=h2')
.then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); }) .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)); .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 (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.disabled = true;
btn.textContent = '⟳ Starting…'; btn.textContent = '⟳ Starting…';
_vvPtRun('Partnership/partnership_offboard.sh') _vvPtRun('Partnership/partnership_offboard.sh')
@@ -304,10 +304,10 @@ function vvPtOffboard(btn) {
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Offboard'; }, 4000)); .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Offboard'; }, 4000));
} }
function vvPtTransfer(btn, token) { async function vvPtTransfer(btn, token) {
if (!confirm('Transfer ownership to the mirror?\n\nThis promotes the mirror to OWNER and demotes this server. ' 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; + '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.disabled = true;
btn.textContent = '⟳ Transferring…'; btn.textContent = '⟳ Transferring…';
_vvPtRun('Partnership/partnership_transfer.sh', '--confirm=' + token) _vvPtRun('Partnership/partnership_transfer.sh', '--confirm=' + token)
@@ -382,12 +382,12 @@ function vvPtHostChanged(el) {
document.getElementById('vv-pt-hosts-save').style.display = any ? '' : 'none'; 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'); const changedEls = document.querySelectorAll('.vv-pt-host-inp.changed');
if (!changedEls.length) return; if (!changedEls.length) return;
const changes = []; const changes = [];
changedEls.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value})); 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…'; btn.disabled = true; btn.textContent = '⟳ Saving…';
fetch('/plugins/varaverk/api/confform.php', { fetch('/plugins/varaverk/api/confform.php', {
method: 'POST', method: 'POST',
@@ -481,12 +481,12 @@ function vvPtArrChanged(el) {
if (saveBtn) saveBtn.style.display = any ? '' : 'none'; 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'); const changedEls = document.querySelectorAll('#vv-pt-arrays-wrap .vv-set-input.changed');
if (!changedEls.length) return; if (!changedEls.length) return;
const changes = []; const changes = [];
changedEls.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value})); 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…'; btn.disabled = true; btn.textContent = '⟳ Saving…';
fetch('/plugins/varaverk/api/confform.php', { fetch('/plugins/varaverk/api/confform.php', {
method: 'POST', method: 'POST',
@@ -591,14 +591,14 @@ function vvPtSetChanged(el) {
document.getElementById('vv-pt-settings-save').style.display = any ? '' : 'none'; 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'); const changedEls = document.querySelectorAll('#vv-pt-settings-body .vv-set-input.changed');
if (!changedEls.length) return; if (!changedEls.length) return;
const changes = []; const changes = [];
changedEls.forEach(el => changes.push({ changedEls.forEach(el => changes.push({
key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value 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…'; btn.disabled = true; btn.textContent = '⟳ Saving…';
const params = new URLSearchParams({ const params = new URLSearchParams({
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '', csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
+2 -2
View File
@@ -1481,10 +1481,10 @@ function vvRpSave() {
.catch(() => { btn.disabled = false; btn.textContent = 'Save Profile'; fb.style.color='#ef5350'; fb.textContent='Request failed'; }); .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(); const name = document.getElementById('vv-rp-name').value.trim();
if (!name) return; 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 fb = document.getElementById('vv-rp-fb');
const del = document.getElementById('vv-rp-del-btn'); const del = document.getElementById('vv-rp-del-btn');
+20 -20
View File
@@ -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 // 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. // "are you sure", and it is the button immediately beside the one that raised the question.
function vvConfirmRun(id) { async function vvConfirmRun(id) {
return confirm('Run ' + id + ' now?\n\n' return vvConfirm('Run ' + id + ' now?\n\n'
+ 'It starts immediately and as root, with the same effect as a scheduled run.\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.'); + '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. // Run a script by ID directly — no DOM card needed.
function vvRunById(id) { async function vvRunById(id) {
if (!vvConfirmRun(id)) return; if (!await vvConfirmRun(id)) return;
vvRunStart(id, {id, manual: '1'}); vvRunStart(id, {id, manual: '1'});
} }
function vvGitPull(btn) { async function vvGitPull(btn) {
const id = 'git_pull_execute.sh'; const id = 'git_pull_execute.sh';
// Asked before the button is disabled, so cancelling does not leave it stuck reading "Pulling…" // Asked before the button is disabled, so cancelling does not leave it stuck reading "Pulling…"
// for the next four seconds while nothing is pulling. // for the next four seconds while nothing is pulling.
if (!vvConfirmRun(id)) return; if (!await vvConfirmRun(id)) return;
btn.disabled = true; btn.disabled = true;
btn.textContent = '⟳ Pulling…'; btn.textContent = '⟳ Pulling…';
vvRunStart(id, {id, manual: '1'}); vvRunStart(id, {id, manual: '1'});
@@ -1613,10 +1613,10 @@ function vvGitPull(btn) {
setTimeout(() => { btn.disabled = false; btn.textContent = '↻ Git Pull'; }, 4000); setTimeout(() => { btn.disabled = false; btn.textContent = '↻ Git Pull'; }, 4000);
} }
function vvRunJob(btn) { async function vvRunJob(btn) {
const job = btn.closest('[data-id]'); const job = btn.closest('[data-id]');
const id = job.dataset.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 location = job.querySelector('.vv-rsync-location')?.value.trim() || '';
const extra_args = job.querySelector('.vv-script-args')?.value.trim() || ''; const extra_args = job.querySelector('.vv-script-args')?.value.trim() || '';
const data = {id}; const data = {id};
@@ -1913,14 +1913,14 @@ function vvShowEditorMode(title) {
requestAnimationFrame(vvFitRight); requestAnimationFrame(vvFitRight);
} }
function vvSaveScript() { async function vvSaveScript() {
const name = document.getElementById('vv-editor-name').value.trim(); const name = document.getElementById('vv-editor-name').value.trim();
const content = document.getElementById('vv-editor-body').value; const content = document.getElementById('vv-editor-body').value;
if (!name || !/^[a-zA-Z0-9_\-]+$/.test(name)) { if (!name || !/^[a-zA-Z0-9_\-]+$/.test(name)) {
alert('Name must be letters, numbers, _ or - only (no spaces, no .sh)'); alert('Name must be letters, numbers, _ or - only (no spaces, no .sh)');
return; 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'); const btn = document.getElementById('vv-save-script-btn');
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Saving…'; btn.textContent = 'Saving…';
@@ -1933,10 +1933,10 @@ function vvSaveScript() {
.catch(() => { btn.disabled = false; btn.textContent = 'Save Script'; }); .catch(() => { btn.disabled = false; btn.textContent = 'Save Script'; });
} }
function vvDeleteScript() { async function vvDeleteScript() {
if (!vvEditorId) return; if (!vvEditorId) return;
const name = vvEditorId.replace(/^Custom\//, '').replace(/\.sh$/, ''); 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'); const btn = document.getElementById('vv-delete-script-btn');
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Deleting…'; btn.textContent = 'Deleting…';
@@ -2107,10 +2107,10 @@ function _vvImpUpdateFooter() {
btn.disabled = !_vvImpSelected; btn.disabled = !_vvImpSelected;
} }
function _vvImpDoImport() { async function _vvImpDoImport() {
if (!_vvImpSelected) return; if (!_vvImpSelected) return;
const dest = (window.__vvCustomScriptsDir || 'Custom Scripts') + '/' + _vvImpSelected.replace(/^.*\//, ''); 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'); const btn = document.getElementById('vv-imp-do-btn');
btn.disabled = true; btn.disabled = true;
@@ -2253,10 +2253,10 @@ function vvCfInitArrays(root) {
(root || document).querySelectorAll('.vv-cf-array').forEach(vvCfLines); (root || document).querySelectorAll('.vv-cf-array').forEach(vvCfLines);
} }
function vvSaveConf() { async function vvSaveConf() {
if (!vvConfId) return; if (!vvConfId) return;
const _cfName = vvConfId.replace(/\.sh$/, '').split('/').pop(); 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 inputs = document.querySelectorAll('#vv-confform .vv-cf-input, #vv-si-view .vv-cf-input');
const changes = []; const changes = [];
inputs.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value})); 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>'; + '</div>';
} }
function vvClearLock(file, btn) { async function vvClearLock(file, btn) {
if (!confirm('Clear lock "' + file + '"?\nOnly do this if the script has crashed and the lock is stale.')) return; 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 = '…'; btn.disabled = true; btn.textContent = '…';
vvPost('/plugins/varaverk/api/clearlock.php', {file}) vvPost('/plugins/varaverk/api/clearlock.php', {file})
.then(d => { if (d.ok) vvBoardPoll(); else { btn.disabled = false; btn.textContent = 'Clear'; }}) .then(d => { if (d.ok) vvBoardPoll(); else { btn.disabled = false; btn.textContent = 'Clear'; }})
@@ -3491,9 +3491,9 @@ function vvShowRawConfMode(title) {
requestAnimationFrame(vvFitRight); requestAnimationFrame(vvFitRight);
} }
function vvSaveRawConf() { async function vvSaveRawConf() {
if (!vvRawConfFile) return; 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 content = document.getElementById('vv-editor-body').value;
const btn = document.getElementById('vv-save-rawconf-btn'); const btn = document.getElementById('vv-save-rawconf-btn');
btn.disabled = true; btn.disabled = true;
+2 -2
View File
@@ -300,7 +300,7 @@ function _vvStorRender(d) {
} }
} }
function vvStorMigrate() { async function vvStorMigrate() {
const btn = document.getElementById('vv-stor-migrate-btn'); const btn = document.getElementById('vv-stor-migrate-btn');
const out = document.getElementById('vv-stor-out'); const out = document.getElementById('vv-stor-out');
const fb = document.getElementById('vv-stor-fb'); const fb = document.getElementById('vv-stor-fb');
@@ -315,7 +315,7 @@ function vvStorMigrate() {
} }
const label = to === 'flash' ? 'Flash (appdata)' : 'Internal (/boot)'; 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.disabled = true;
btn.textContent = 'Migrating…'; btn.textContent = 'Migrating…';