From 1041777b9ac44772d3ab2ad6d345863a711da289 Mon Sep 17 00:00:00 2001 From: Christoph Gasser Date: Mon, 27 Apr 2026 15:43:41 +0200 Subject: [PATCH] Lock reboot button permanently after confirm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/templates/index.html | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/app/templates/index.html b/app/templates/index.html index 1f4f068..fdad425 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -283,14 +283,26 @@ }); // ── 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 => { btn.addEventListener('click', () => { + if (btn.disabled) return; const mac = btn.dataset.mac; const name = btn.dataset.name; const ip = btn.dataset.ip; const site_key = btn.dataset.siteKey; showModal(`Reboot AP "${name}"?\nThis will disconnect all clients connected to this AP.`, async () => { - btn.disabled = true; + lockApRow(mac); btn.textContent = 'Rebooting…'; try { const res = await fetch('/api/reboot', { @@ -299,17 +311,16 @@ body: JSON.stringify({ mac, name, ip, site_key, csrf_token: CSRF_TOKEN }), }); if (res.ok) { + btn.textContent = 'Rebooted'; showToast(`Reboot command sent to "${name}"`, 'success'); } else { const err = await res.json(); + btn.textContent = 'Failed'; showToast(`Error: ${err.detail || 'Unknown error'}`, 'error'); - btn.disabled = false; - btn.textContent = 'Reboot'; } } catch (e) { + btn.textContent = 'Failed'; 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 names = aps.map(a => a.name).join(', '); 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.querySelector('span').textContent = '0'; try { @@ -331,17 +344,22 @@ }); const data = await res.json(); 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) { showToast(`Reboot sent to ${aps.length} AP${aps.length > 1 ? 's' : ''}`, 'success'); } else { 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) { + aps.forEach(ap => { + const b = document.querySelector(`.btn-reboot[data-mac="${CSS.escape(ap.mac)}"]`); + if (b) b.textContent = 'Failed'; + }); showToast('Network error', 'error'); - updateBulkButton(); } }); });