Confirm a finding in the page instead of with a browser dialog

Both browsers offer "prevent this page from creating additional dialogs" inside the dialog
itself, and once it is ticked every later confirm() returns false without drawing anything — so
Fix and Never a problem silently declined and read as dead buttons. Unraid swaps tabs by AJAX
without reloading, so the suppression outlived leaving the tab and only clearing history got it
back. Both now arm on the first press and act on the second, and say what the press will do
while they wait for it.
This commit is contained in:
Gmer4Lfe
2026-08-09 21:33:06 -04:00
parent a5be719261
commit 5b48561f36
+59 -13
View File
@@ -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 /* 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. */ restate what an action means and cannot restate it differently. */
.vv-ai-fnd-a .vv-ai-btn { padding:3px 11px; font-size:11px; } .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; } .vv-ai-fnd-msg { font-size:10px; color:#5a5a5a; margin-left:4px; }
/* ── Settings card ──────────────────────────────────────────────────────── */ /* ── Settings card ──────────────────────────────────────────────────────── */
@@ -598,6 +604,28 @@ vv_ai_chat_markup('vv-ai', [
// uses for the same row. // uses for the same row.
const FND_LABEL = { fix: 'Fix', ack: 'I know', dismiss: 'Never a problem', reopen: 'Reopen' }; 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() { function loadFindings() {
fetch(API + '?action=findings' + (fndAll ? '&all=1' : '')).then(r => r.json()) fetch(API + '?action=findings' + (fndAll ? '&all=1' : '')).then(r => r.json())
.then(d => { if (d.ok) renderFindings(d); }) .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); const f = fndRows.find(x => x.id === id);
if (!f) return; 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}"]`); 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'
? `<span class="vv-ai-warn">Writes the change above to ${esc(f.conf_file)}`
+ `${f.proven ? '' : ', and nothing has probed it'}. Click again to confirm.</span>`
: `<span class="vv-ai-warn">Stays closed even when it is seen again. `
+ `Use “I know” to be told if it changes. Click again to confirm.</span>`;
// 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')); const btns = Array.from(btn.parentNode.querySelectorAll('button'));
btns.forEach(b => b.disabled = true); btns.forEach(b => b.disabled = true);
if (msg) msg.textContent = 'working…'; 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 // 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 // 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 // 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 => { const failed = text => {
if (msg) msg.innerHTML = `<span class="vv-ai-bad">${esc(text)}</span>`; btn.dataset.armed = '';
if (btn.dataset.label) btn.textContent = btn.dataset.label;
btn.classList.remove('armed');
btns.forEach(b => b.disabled = false); btns.forEach(b => b.disabled = false);
if (msg) msg.innerHTML = `<span class="vv-ai-bad">${esc(text)}</span>`;
}; };
fetch(API, { method: 'POST', fetch(API, { method: 'POST',