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',