From 5aa9f0f0c3facbd1cf6f0ef5650729ac49771104 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 9 Sep 2026 14:53:29 +0000
Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20proper=20vi?=
=?UTF-8?q?sual=20and=20functional=20guards=20for=20aria-busy=20in=20conso?=
=?UTF-8?q?le.html?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.jules/palette.md | 4 ++++
scanner/dashboard/console.html | 5 +++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/.jules/palette.md b/.jules/palette.md
index ea004e2d..5f175a71 100644
--- a/.jules/palette.md
+++ b/.jules/palette.md
@@ -81,3 +81,7 @@
## 2026-08-12 - Skip to Content Accessibility
**Learning:** Screen reader and keyboard-only users experience significant friction when forced to navigate through repetitive header controls on every page load.
**Action:** Keep a visible-on-focus skip link as the first interactive element, target a programmatically focusable main container, and give the focused link a high-contrast outline.
+
+## 2024-05-19 - console.html pointer-events anti-pattern
+**Learning:** Relying solely on CSS `pointer-events: none` to disable interactions is an accessibility anti-pattern because it fails to prevent keyboard interactions (Enter/Space). Always pair visual CSS disabling with semantic state management, such as the `disabled` attribute for native buttons or `aria-disabled="true"`/`aria-busy="true"` combined with JS event guards (`if (el.getAttribute('aria-busy') === 'true') return;`) for non-native elements.
+**Action:** Implemented JS event guards in click and keydown listeners alongside CSS `pointer-events: none` and `opacity: 0.6` for loading states on `tr.scan` rows.
diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html
index 7ec262af..355e5bd8 100644
--- a/scanner/dashboard/console.html
+++ b/scanner/dashboard/console.html
@@ -46,6 +46,7 @@
.err{color:var(--crit);font-weight:600}
code{background:var(--bg);padding:1px 5px;border-radius:4px}
.hidden{display:none}
+ tr.scan[aria-busy="true"]{pointer-events:none;opacity:0.6}
@@ -135,8 +136,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-busy")==="true")return;detail(tr.dataset.id,tr);};
+ tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if(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)}`; }
From dd2e33aa254bab840cf4b6223989a9fced7e866e Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Thu, 10 Sep 2026 00:11:11 +0900
Subject: [PATCH 2/3] test(console): require semantic disabled state while
details load
---
tests/test_console_detail_loading_contract.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tests/test_console_detail_loading_contract.py b/tests/test_console_detail_loading_contract.py
index 38eebed0..e3aa24bb 100644
--- a/tests/test_console_detail_loading_contract.py
+++ b/tests/test_console_detail_loading_contract.py
@@ -24,9 +24,12 @@ def test_console_exposes_loading_busy_and_error_states():
html = _console_html()
assert 'tr.setAttribute("aria-busy","true");' in html
+ assert 'tr.setAttribute("aria-disabled","true");' in html
assert 'aria-live="polite" class="muted">Loading scan details...' in html
assert 'role="alert" class="err">Error loading details:' in html
assert 'tr.removeAttribute("aria-busy");' in html
+ assert 'tr.removeAttribute("aria-disabled");' in html
+ assert 'lastDetailFocus.removeAttribute("aria-disabled");' in html
def test_console_detail_scrolling_respects_reduced_motion():
From 5cad14bf125bbf3af0df29dded4b087c2aa90c07 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Thu, 10 Sep 2026 00:12:11 +0900
Subject: [PATCH 3/3] fix(console): expose disabled state during detail
requests
---
scanner/dashboard/console.html | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html
index 355e5bd8..e6543ac7 100644
--- a/scanner/dashboard/console.html
+++ b/scanner/dashboard/console.html
@@ -46,7 +46,7 @@
.err{color:var(--crit);font-weight:600}
code{background:var(--bg);padding:1px 5px;border-radius:4px}
.hidden{display:none}
- tr.scan[aria-busy="true"]{pointer-events:none;opacity:0.6}
+ tr.scan[aria-disabled="true"]{pointer-events:none;opacity:0.6;cursor:default}
@@ -89,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();
}
@@ -136,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=()=>{if(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-busy")==="true")return; 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)}`; }
@@ -150,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...
';
@@ -174,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;
}
}