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;
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
+9 -9
View File
@@ -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;
+8 -8
View File
@@ -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); });
});
+2 -2
View File
@@ -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);
+25 -25
View File
@@ -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 : '',
+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'; });
}
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');
+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
// "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;
+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 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…';