From 7d865b0a09b0f3c77adc3bddc2bd329a308e117b Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sun, 9 Aug 2026 21:44:43 -0400 Subject: [PATCH] Ask with Unraid's own dialog, not the browser's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Plugin/unraid/Varaverk.page | 76 +++++++++++++++++++++++++++++ Plugin/unraid/pages/auth.php | 18 +++---- Plugin/unraid/pages/docker.php | 16 +++--- Plugin/unraid/pages/monitor.php | 4 +- Plugin/unraid/pages/partnership.php | 50 +++++++++---------- Plugin/unraid/pages/rsync.php | 4 +- Plugin/unraid/pages/scheduler.php | 40 +++++++-------- Plugin/unraid/pages/settings.php | 4 +- 8 files changed, 144 insertions(+), 68 deletions(-) diff --git a/Plugin/unraid/Varaverk.page b/Plugin/unraid/Varaverk.page index f568e0a..6400179 100644 --- a/Plugin/unraid/Varaverk.page +++ b/Plugin/unraid/Varaverk.page @@ -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)); }); + }); +} { +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(`

Add Group

@@ -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; diff --git a/Plugin/unraid/pages/docker.php b/Plugin/unraid/pages/docker.php index 9acc220..ec700ce 100644 --- a/Plugin/unraid/pages/docker.php +++ b/Plugin/unraid/pages/docker.php @@ -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); }); }); diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index 7bd6c2c..0d68002 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -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); diff --git a/Plugin/unraid/pages/partnership.php b/Plugin/unraid/pages/partnership.php index 221a8e8..6b401d4 100644 --- a/Plugin/unraid/pages/partnership.php +++ b/Plugin/unraid/pages/partnership.php @@ -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 : '', diff --git a/Plugin/unraid/pages/rsync.php b/Plugin/unraid/pages/rsync.php index 86a3a9d..46ea576 100644 --- a/Plugin/unraid/pages/rsync.php +++ b/Plugin/unraid/pages/rsync.php @@ -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'); diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index b48a446..c4859d7 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -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) { + '
'; } -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; diff --git a/Plugin/unraid/pages/settings.php b/Plugin/unraid/pages/settings.php index 244db49..1480a9c 100644 --- a/Plugin/unraid/pages/settings.php +++ b/Plugin/unraid/pages/settings.php @@ -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…';