Lock reboot button permanently after confirm
After confirming a reboot (single or bulk), the row button and checkbox are immediately disabled and can't be re-triggered in the current session. Button text changes to "Rebooting…" while the request is in flight, then "Rebooted" on success or "Failed" on error — but stays disabled either way. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -283,14 +283,26 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Single reboot ──────────────────────────────────────────────────────────
|
// ── Single reboot ──────────────────────────────────────────────────────────
|
||||||
|
function lockApRow(mac) {
|
||||||
|
const rowBtn = document.querySelector(`.btn-reboot[data-mac="${CSS.escape(mac)}"]`);
|
||||||
|
if (rowBtn) {
|
||||||
|
rowBtn.disabled = true;
|
||||||
|
rowBtn.className = 'btn-reboot px-3 py-1.5 text-xs rounded-lg font-medium bg-gray-100 border border-gray-200 text-gray-400 cursor-not-allowed opacity-50';
|
||||||
|
}
|
||||||
|
const cb = document.querySelector(`.ap-checkbox[data-mac="${CSS.escape(mac)}"]`);
|
||||||
|
if (cb) { cb.disabled = true; cb.checked = false; }
|
||||||
|
updateBulkButton();
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.btn-reboot').forEach(btn => {
|
document.querySelectorAll('.btn-reboot').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
if (btn.disabled) return;
|
||||||
const mac = btn.dataset.mac;
|
const mac = btn.dataset.mac;
|
||||||
const name = btn.dataset.name;
|
const name = btn.dataset.name;
|
||||||
const ip = btn.dataset.ip;
|
const ip = btn.dataset.ip;
|
||||||
const site_key = btn.dataset.siteKey;
|
const site_key = btn.dataset.siteKey;
|
||||||
showModal(`Reboot AP "${name}"?\nThis will disconnect all clients connected to this AP.`, async () => {
|
showModal(`Reboot AP "${name}"?\nThis will disconnect all clients connected to this AP.`, async () => {
|
||||||
btn.disabled = true;
|
lockApRow(mac);
|
||||||
btn.textContent = 'Rebooting…';
|
btn.textContent = 'Rebooting…';
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/reboot', {
|
const res = await fetch('/api/reboot', {
|
||||||
@@ -299,17 +311,16 @@
|
|||||||
body: JSON.stringify({ mac, name, ip, site_key, csrf_token: CSRF_TOKEN }),
|
body: JSON.stringify({ mac, name, ip, site_key, csrf_token: CSRF_TOKEN }),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
btn.textContent = 'Rebooted';
|
||||||
showToast(`Reboot command sent to "${name}"`, 'success');
|
showToast(`Reboot command sent to "${name}"`, 'success');
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
|
btn.textContent = 'Failed';
|
||||||
showToast(`Error: ${err.detail || 'Unknown error'}`, 'error');
|
showToast(`Error: ${err.detail || 'Unknown error'}`, 'error');
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Reboot';
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
btn.textContent = 'Failed';
|
||||||
showToast('Network error', 'error');
|
showToast('Network error', 'error');
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Reboot';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -321,6 +332,8 @@
|
|||||||
const aps = checked.map(cb => ({ mac: cb.dataset.mac, name: cb.dataset.name, ip: cb.dataset.ip, site_key: cb.dataset.siteKey }));
|
const aps = checked.map(cb => ({ mac: cb.dataset.mac, name: cb.dataset.name, ip: cb.dataset.ip, site_key: cb.dataset.siteKey }));
|
||||||
const names = aps.map(a => a.name).join(', ');
|
const names = aps.map(a => a.name).join(', ');
|
||||||
showModal(`Reboot ${aps.length} AP${aps.length > 1 ? 's' : ''}?\n${names}`, async () => {
|
showModal(`Reboot ${aps.length} AP${aps.length > 1 ? 's' : ''}?\n${names}`, async () => {
|
||||||
|
// Lock all selected AP rows immediately on confirm
|
||||||
|
aps.forEach(ap => lockApRow(ap.mac));
|
||||||
btnRebootSel.disabled = true;
|
btnRebootSel.disabled = true;
|
||||||
btnRebootSel.querySelector('span').textContent = '0';
|
btnRebootSel.querySelector('span').textContent = '0';
|
||||||
try {
|
try {
|
||||||
@@ -331,17 +344,22 @@
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const errors = (data.results || []).filter(r => r.result === 'error');
|
const errors = (data.results || []).filter(r => r.result === 'error');
|
||||||
|
// Update button text per AP based on result
|
||||||
|
(data.results || []).forEach(r => {
|
||||||
|
const b = document.querySelector(`.btn-reboot[data-mac="${CSS.escape(r.mac)}"]`);
|
||||||
|
if (b) b.textContent = r.result === 'success' ? 'Rebooted' : 'Failed';
|
||||||
|
});
|
||||||
if (errors.length === 0) {
|
if (errors.length === 0) {
|
||||||
showToast(`Reboot sent to ${aps.length} AP${aps.length > 1 ? 's' : ''}`, 'success');
|
showToast(`Reboot sent to ${aps.length} AP${aps.length > 1 ? 's' : ''}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showToast(`${errors.length} AP(s) failed to reboot`, 'error');
|
showToast(`${errors.length} AP(s) failed to reboot`, 'error');
|
||||||
}
|
}
|
||||||
document.querySelectorAll('.ap-checkbox').forEach(cb => { cb.checked = false; });
|
|
||||||
if (chkAll) chkAll.checked = false;
|
|
||||||
updateBulkButton();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
aps.forEach(ap => {
|
||||||
|
const b = document.querySelector(`.btn-reboot[data-mac="${CSS.escape(ap.mac)}"]`);
|
||||||
|
if (b) b.textContent = 'Failed';
|
||||||
|
});
|
||||||
showToast('Network error', 'error');
|
showToast('Network error', 'error');
|
||||||
updateBulkButton();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user