diff --git a/Plugin/unraid/pages/ai.php b/Plugin/unraid/pages/ai.php index 75796e2..01e1be2 100644 --- a/Plugin/unraid/pages/ai.php +++ b/Plugin/unraid/pages/ai.php @@ -188,6 +188,12 @@ if (is_dir('/var/log/varaverk')) { /* Each button explains itself on hover from the server's own text, so the page never has to restate what an action means and cannot restate it differently. */ .vv-ai-fnd-a .vv-ai-btn { padding:3px 11px; font-size:11px; } +/* Waiting for its second press. Confirmation is in the page rather than a browser dialog — + confirm() can be switched off from inside itself, after which it answers no forever without + drawing anything, and the button reads as dead. This state has to be loud enough that a + second press is obviously a second press and not a first one that failed. */ +.vv-ai-fnd-a .vv-ai-btn.armed { background:#3a2410; border-color:#8a6a3a; color:#ffb74d; } +.vv-ai-fnd-a .vv-ai-btn.armed:hover:not(:disabled) { background:#4a2e14; } .vv-ai-fnd-msg { font-size:10px; color:#5a5a5a; margin-left:4px; } /* ── Settings card ──────────────────────────────────────────────────────── */ @@ -598,6 +604,28 @@ vv_ai_chat_markup('vv-ai', [ // uses for the same row. const FND_LABEL = { fix: 'Fix', ack: 'I know', dismiss: 'Never a problem', reopen: 'Reopen' }; + // Which actions take a second press, and what the button says while it is waiting for it. + // Fix writes conf; dismiss is the one state the sweep will never reopen on its own however + // many times the fault comes back. Ack and reopen are both reversible and go on one press. + const FND_ARM = { fix: 'Confirm write', dismiss: 'Confirm — permanent' }; + let fndArmTimer = null; + + // Put every armed button back, optionally sparing the one being pressed right now. + function fndDisarm(keep) { + clearTimeout(fndArmTimer); + $('vv-ai-fnd').querySelectorAll('button[data-armed="1"]').forEach(b => { + if (b === keep) return; + b.dataset.armed = ''; + if (b.dataset.label) b.textContent = b.dataset.label; + b.classList.remove('armed'); + // Cleared the way it was set. textContent would also empty it, but the arming message is + // written as innerHTML and a clear that does not match its write is the kind of pairing + // that quietly stops matching later. + const m = b.parentNode.querySelector('[data-msg]'); + if (m) m.innerHTML = ''; + }); + } + function loadFindings() { fetch(API + '?action=findings' + (fndAll ? '&all=1' : '')).then(r => r.json()) .then(d => { if (d.ok) renderFindings(d); }) @@ -675,18 +703,32 @@ vv_ai_chat_markup('vv-ai', [ const f = fndRows.find(x => x.id === id); if (!f) return; - // The two that cannot be walked back get asked about first. Fix edits a conf file, and - // dismiss is the one state the sweep will never reopen on its own however many times the - // fault comes back. - if (act === 'fix' && !confirm( - `Write ${f.conf_key} = ${f.proposed}\n\nin ${f.conf_file}, replacing ${f.observed || '(empty)'}.` - + (f.proven ? '' : '\n\nNothing has probed this value — it is a proposal, not a proven fix.'))) - return; - if (act === 'dismiss' && !confirm( - `Dismiss "${f.subject} — ${f.ref}" permanently?\n\nIt stays closed even when it is seen ` - + `again. To be told if it changes, use "I know" instead.`)) return; - const msg = $('vv-ai-fnd').querySelector(`[data-msg="${id}"]`); + + // Fix and dismiss are confirmed in the page, never with confirm(). Both browsers offer + // "prevent this page from creating additional dialogs" inside the dialog itself, and once + // that is ticked every later confirm() returns false without showing anything — so the + // buttons silently decline and read as dead. Unraid swaps tabs by AJAX without reloading, + // so the suppression outlives leaving the tab and only a full reload clears it. That is + // exactly what happened here: not a broken button, a dialog answering no on its own. + if (FND_ARM[act] && btn.dataset.armed !== '1') { + fndDisarm(); + btn.dataset.armed = '1'; + btn.dataset.label = btn.textContent; + btn.textContent = FND_ARM[act]; + btn.classList.add('armed'); + if (msg) msg.innerHTML = act === 'fix' + ? `Writes the change above to ${esc(f.conf_file)}` + + `${f.proven ? '' : ', and nothing has probed it'}. Click again to confirm.` + : `Stays closed even when it is seen again. ` + + `Use “I know” to be told if it changes. Click again to confirm.`; + // Arming expires on its own so a half-pressed button cannot sit there waiting to be + // completed by an unrelated click later. + fndArmTimer = setTimeout(fndDisarm, 8000); + return; + } + clearTimeout(fndArmTimer); + const btns = Array.from(btn.parentNode.querySelectorAll('button')); btns.forEach(b => b.disabled = true); if (msg) msg.textContent = 'working…'; @@ -694,10 +736,14 @@ vv_ai_chat_markup('vv-ai', [ // Reload on success, never on failure. A refused conf write is the case that must not // disappear quietly: the guarded path rolled back, the finding is unchanged, and a reload // would repaint the row identically half a second later and take the error with it. So the - // failed row keeps its message and its buttons, and the operator decides what to do next. + // failed row keeps its message, and its buttons go back to reading Fix rather than sitting + // at "Confirm write" — a second press should have to arm again, not fire again. const failed = text => { - if (msg) msg.innerHTML = `${esc(text)}`; + btn.dataset.armed = ''; + if (btn.dataset.label) btn.textContent = btn.dataset.label; + btn.classList.remove('armed'); btns.forEach(b => b.disabled = false); + if (msg) msg.innerHTML = `${esc(text)}`; }; fetch(API, { method: 'POST',