From 0bc0517ddf937f0b81034ead16fc9b2f39104caf Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Thu, 10 Sep 2026 14:25:59 +0000
Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20explicit=20inte?=
=?UTF-8?q?raction=20blocking=20for=20async=20loading=20states?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.jules/palette.md | 4 +
patch.diff | 20 +++
scanner/dashboard/console.html | 5 +-
scanner/dashboard/console.html.orig | 186 ++++++++++++++++++++++++++++
test_ui.py | 12 ++
5 files changed, 225 insertions(+), 2 deletions(-)
create mode 100644 patch.diff
create mode 100644 scanner/dashboard/console.html.orig
create mode 100644 test_ui.py
diff --git a/.jules/palette.md b/.jules/palette.md
index ea004e2d..6deffaa5 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-11-20 - Enforcing Interaction Blocking on Non-Button Loading States
+**Learning:** Adding visual cues (like opacity) and `pointer-events: none` to elements with `aria-busy="true"` is insufficient for full accessibility, as keyboard interactions (`Enter`/`Space`) are not blocked. This allows users to trigger duplicate async requests if they use a keyboard.
+**Action:** Always combine CSS visual blocking (`pointer-events: none`) with explicit JavaScript event guards (`if(el.getAttribute("aria-busy")==="true")return;`) in both `onclick` and `keydown` listeners for non-native interactive elements (like `tr[role="button"]`) during loading states.
diff --git a/patch.diff b/patch.diff
new file mode 100644
index 00000000..beffa678
--- /dev/null
+++ b/patch.diff
@@ -0,0 +1,20 @@
+--- scanner/dashboard/console.html
++++ 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}
++ [aria-busy="true"]{pointer-events:none;opacity:0.6}
+
+
+
+@@ -135,8 +136,8 @@
+ ${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)}`; }
diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html
index 7ec262af..02339016 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}
+ [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)}`; }
diff --git a/scanner/dashboard/console.html.orig b/scanner/dashboard/console.html.orig
new file mode 100644
index 00000000..7ec262af
--- /dev/null
+++ b/scanner/dashboard/console.html.orig
@@ -0,0 +1,186 @@
+
+
+
+
+
+AppGuardrail Console
+
+
+
+
+
+
+ Paste your org API key and connect to view scan history.
+
+
+
+
Deploy-blocking trend (oldest → newest)
+
+
+
+
Scan history
+
| When | Repo | Commit | Total | Blocking | New |
|---|
+
+
+
+
+
+
+
diff --git a/test_ui.py b/test_ui.py
new file mode 100644
index 00000000..fe91a90a
--- /dev/null
+++ b/test_ui.py
@@ -0,0 +1,12 @@
+from playwright.sync_api import sync_playwright
+
+def run():
+ with sync_playwright() as p:
+ browser = p.chromium.launch()
+ page = browser.new_page()
+ page.goto("file:///app/scanner/dashboard/console.html")
+ page.wait_for_timeout(1000)
+ print("Page title:", page.title())
+ browser.close()
+
+run()