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()