diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html
index 7ec262af..2509de1d 100644
--- a/scanner/dashboard/console.html
+++ b/scanner/dashboard/console.html
@@ -26,6 +26,7 @@
input{font:inherit;padding:8px 10px;border:1px solid var(--border);border-radius:8px;min-width:280px}
button{font:inherit;font-weight:600;padding:8px 14px;border:0;border-radius:8px;background:var(--primary);color:var(--on-primary);cursor:pointer}
button.ghost{background:var(--surface);color:var(--primary);border:1px solid var(--border)}
+ button:disabled, button[aria-busy="true"], tr.scan[aria-busy="true"], tr.scan[aria-disabled="true"] { opacity: 0.7; pointer-events: none; }
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin-bottom:16px}
.stats{display:flex;gap:12px;flex-wrap:wrap}
.stat{flex:1;min-width:130px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px}
@@ -88,6 +89,7 @@
AppGuardrail Console
detail.innerHTML="";
if(lastDetailFocus instanceof HTMLElement && lastDetailFocus.isConnected){
lastDetailFocus.removeAttribute("aria-busy");
+ lastDetailFocus.removeAttribute("aria-disabled");
delete lastDetailFocus.dataset.detailRequest;
lastDetailFocus.focus();
}
@@ -135,8 +137,8 @@ AppGuardrail Console
${esc(s.created_at)} | ${esc(s.repo||"—")} | ${esc((s.commit||"—").slice(0,10))} |
${s.total} | ${pill(s.deploy_blocking,"var(--crit)")} | ${pill(s.new_blocking,"var(--high)")} | `).join("")||'| No scans. POST to /api/v1/scans from CI. |
';
document.querySelectorAll("tr.scan").forEach(tr=>{
- tr.onclick=()=>detail(tr.dataset.id,tr);
- tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); detail(tr.dataset.id,tr); } });
+ tr.onclick=()=>{ if(tr.getAttribute("aria-disabled")==="true" || tr.getAttribute("aria-busy")==="true") return; detail(tr.dataset.id,tr); };
+ tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if(tr.getAttribute("aria-disabled")==="true" || tr.getAttribute("aria-busy")==="true") return; detail(tr.dataset.id,tr); } });
});
}catch(e){ $("#msg").classList.remove("hidden");$("#app").classList.add("hidden");
$("#msg").innerHTML=`${esc(e.message)}`; }
@@ -149,6 +151,7 @@ AppGuardrail Console
if(tr){
tr.dataset.detailRequest=String(requestId);
tr.setAttribute("aria-busy","true");
+ tr.setAttribute("aria-disabled","true");
}
d.classList.remove("hidden");
d.innerHTML='Loading scan details...
';
@@ -173,6 +176,7 @@ AppGuardrail Console
}finally{
if(tr&&tr.dataset.detailRequest===String(requestId)){
tr.removeAttribute("aria-busy");
+ tr.removeAttribute("aria-disabled");
delete tr.dataset.detailRequest;
}
}
diff --git a/tests/test_console_detail_loading_contract.py b/tests/test_console_detail_loading_contract.py
index 38eebed0..d6d8c3da 100644
--- a/tests/test_console_detail_loading_contract.py
+++ b/tests/test_console_detail_loading_contract.py
@@ -29,6 +29,16 @@ def test_console_exposes_loading_busy_and_error_states():
assert 'tr.removeAttribute("aria-busy");' in html
+def test_console_busy_scan_row_exposes_temporary_action_unavailability():
+ """Busy focusable scan rows must expose and enforce transient unavailability."""
+ html = _console_html()
+
+ assert 'tr.setAttribute("aria-disabled","true");' in html
+ assert 'tr.getAttribute("aria-disabled")==="true"' in html
+ assert 'lastDetailFocus.removeAttribute("aria-disabled");' in html
+ assert 'tr.removeAttribute("aria-disabled");' in html
+
+
def test_console_detail_scrolling_respects_reduced_motion():
"""Successful and failed detail requests must honor reduced-motion preferences."""
html = _console_html()