diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 7ec262af..87e7221f 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -36,6 +36,7 @@ th{font-size:11px;color:var(--muted);text-transform:uppercase} tr.scan{cursor:pointer} tr.scan:hover{background:var(--bg)} + tr.scan[aria-disabled="true"]{pointer-events:none;opacity:0.6} input:focus-visible, button:focus-visible, tr.scan:focus-visible, .bar:focus-visible, #detail:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } .close-btn{float:right;border:0;background:transparent;font-size:16px;cursor:pointer;color:var(--muted);padding:0 4px;margin-top:-2px} .close-btn:hover{color:var(--text)} @@ -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")return; detail(tr.dataset.id,tr); }; + tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if(tr.getAttribute("aria-disabled")==="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..fd9beadf 100644 --- a/tests/test_console_detail_loading_contract.py +++ b/tests/test_console_detail_loading_contract.py @@ -37,3 +37,23 @@ def test_console_detail_scrolling_respects_reduced_motion(): assert "element.scrollIntoView();" in html assert 'element.scrollIntoView({behavior:"smooth"});' in html assert html.count("scrollDetailIntoView(d);") == 2 + + +def test_console_table_rows_block_interaction_while_busy(): + """Busy row buttons must expose disabled semantics and reject activation.""" + html = _console_html() + + assert 'tr.scan[aria-disabled="true"]{pointer-events:none' in html + assert 'opacity:0.6' in html + assert 'tr.setAttribute("aria-disabled","true");' in html + assert ( + 'tr.onclick=()=>{ if(tr.getAttribute("aria-disabled")==="true")return; ' + 'detail(tr.dataset.id,tr); };' + ) in html + assert ( + "tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') " + '{ e.preventDefault(); if(tr.getAttribute("aria-disabled")==="true")return; ' + 'detail(tr.dataset.id,tr); } });' + ) in html + assert 'lastDetailFocus.removeAttribute("aria-disabled");' in html + assert 'tr.removeAttribute("aria-disabled");' in html