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:
2026-04-27 15:43:41 +02:00
parent 47f553f410
commit 1041777b9a

View File

@@ -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();
} }
}); });
}); });