From c6dc37e7a897f4ceb3a569be53cb860ef591b61f Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:06:16 +0000
Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?=
=?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=A0=91=EA=B7=BC=EC=84=B1=20?=
=?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20URL=20=EC=A4=84=EB=B0=94?=
=?UTF-8?q?=EA=BF=88=20=EC=B2=98=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 외부 링크가 새 탭에서 열림을 시각적(아이콘), 청각적(스크린 리더용 텍스트)으로 명확히 안내
- 긴 URL이 레이아웃을 깨지 않도록 `.ext-link` 클래스를 통해 `word-break: break-all` 적용
- 대시보드 코어 테스트에 외부 링크 접근성 검증 코드 추가
---
.jules/palette.md | 4 ++++
scanner/dashboard/index.html | 3 ++-
tests/test_dashboard_core.py | 14 ++++++++++++++
3 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/.jules/palette.md b/.jules/palette.md
index ea004e2d2..3d2a102c3 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.
+
+## 2026-08-14 - External link accessibility
+**Learning:** `target="_blank"` links pose accessibility issues for screen readers (who may not know a new window opened) and usability issues for sighted users, while long URLs often overflow their containers when rendered as regular text.
+**Action:** When using `target="_blank"`, explicitly provide both an assistive-technology warning (e.g., visually hidden `.sr-only` text) and a visible warning (e.g., an `aria-hidden="true"` icon). For long URL text, apply `word-break: break-all` via a scoped utility class to prevent layout breakage without affecting regular text links.
diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html
index 132bc31b2..faecd158b 100644
--- a/scanner/dashboard/index.html
+++ b/scanner/dashboard/index.html
@@ -82,6 +82,7 @@
body.drag-active::after{content:"Drop findings.json here";position:fixed;inset:0;z-index:99;background:rgba(37,110,244,.08);border:4px dashed var(--primary);display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:var(--primary);pointer-events:none;backdrop-filter:blur(2px)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
a{color:var(--primary)}
+ .ext-link{word-break:break-all}
.skip-link{position:absolute;top:-48px;left:0;background:var(--primary);color:#fff;padding:8px 16px;z-index:100;font-weight:700;border-radius:0 0 8px 0;transition:top .2s}\n .skip-link:focus{top:0;outline:3px solid #fff;outline-offset:2px;box-shadow:0 0 0 5px var(--primary)}\n
@@ -281,7 +282,7 @@
Dashboard
function openDetail(f){
lastFocus = document.activeElement;
const s = String(f.severity||'INFO').toUpperCase();
- const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
');
+ const refs = (f.references||[]).map(r=>`${esc(r)}(새 탭에서 열림)↗`).join('
');
const owasp = (f.owasp||[]).join(', ');
const cwe = (f.cwe||[]).join(', ');
const d = document.getElementById('detail');
diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py
index 75a5809f8..9edc0fbeb 100644
--- a/tests/test_dashboard_core.py
+++ b/tests/test_dashboard_core.py
@@ -302,3 +302,17 @@ def test_dashboard_search_escape_clears_input():
assert "e.key === 'Escape'" in html
assert "query = '';" in html
assert "render();" in html
+
+
+def test_dashboard_external_link_accessibility():
+ """External links must have a screen reader warning and visual indicator."""
+ html = dashboard_index_path().read_text(encoding="utf-8")
+
+ match = re.search(r'const refs =.*?\`]*>.*?\`', html)
+ assert match is not None, "Could not find references template literal"
+
+ anchor_html = match.group(0)
+
+ assert 'class="ext-link"' in anchor_html
+ assert '(새 탭에서 열림)' in anchor_html
+ assert 'aria-hidden="true"' in anchor_html
From f2ad976bb481bb4f421297a1025f3e271eddf334 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 30 Sep 2026 05:38:43 +0000
Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=ED=85=8C?=
=?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EA=B7=9C?=
=?UTF-8?q?=EC=8B=9D=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95=20=EB=B0=8F?=
=?UTF-8?q?=20=EC=99=B8=EB=B6=80=20=EB=A7=81=ED=81=AC=20=EC=A0=91=EA=B7=BC?=
=?UTF-8?q?=EC=84=B1=20=EA=B0=9C=EC=84=A0=20=EC=99=84=EB=A3=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 테스트 코드 내 잘못된 이스케이프 구문(`\``) 제거하여 CI 에러 해결
- `target="_blank"` 접근성 속성 유지
From 2138a8246e0eb1198549da33a5f3cb1dfe43d61f Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 3 Oct 2026 18:52:29 +0900
Subject: [PATCH 3/5] chore: retire duplicate .jules/palette.md delta in favor
of #1361
---
.jules/palette.md | 4 ----
1 file changed, 4 deletions(-)
diff --git a/.jules/palette.md b/.jules/palette.md
index 3d2a102c3..ea004e2d2 100644
--- a/.jules/palette.md
+++ b/.jules/palette.md
@@ -81,7 +81,3 @@
## 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.
-
-## 2026-08-14 - External link accessibility
-**Learning:** `target="_blank"` links pose accessibility issues for screen readers (who may not know a new window opened) and usability issues for sighted users, while long URLs often overflow their containers when rendered as regular text.
-**Action:** When using `target="_blank"`, explicitly provide both an assistive-technology warning (e.g., visually hidden `.sr-only` text) and a visible warning (e.g., an `aria-hidden="true"` icon). For long URL text, apply `word-break: break-all` via a scoped utility class to prevent layout breakage without affecting regular text links.
From 00d69d940b2bd95d09468ecd62fd70ceea983b28 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 3 Oct 2026 18:52:30 +0900
Subject: [PATCH 4/5] chore: retire duplicate scanner/dashboard/index.html
delta in favor of #1361
---
scanner/dashboard/index.html | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html
index faecd158b..132bc31b2 100644
--- a/scanner/dashboard/index.html
+++ b/scanner/dashboard/index.html
@@ -82,7 +82,6 @@
body.drag-active::after{content:"Drop findings.json here";position:fixed;inset:0;z-index:99;background:rgba(37,110,244,.08);border:4px dashed var(--primary);display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:var(--primary);pointer-events:none;backdrop-filter:blur(2px)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
a{color:var(--primary)}
- .ext-link{word-break:break-all}
.skip-link{position:absolute;top:-48px;left:0;background:var(--primary);color:#fff;padding:8px 16px;z-index:100;font-weight:700;border-radius:0 0 8px 0;transition:top .2s}\n .skip-link:focus{top:0;outline:3px solid #fff;outline-offset:2px;box-shadow:0 0 0 5px var(--primary)}\n
@@ -282,7 +281,7 @@ Dashboard
function openDetail(f){
lastFocus = document.activeElement;
const s = String(f.severity||'INFO').toUpperCase();
- const refs = (f.references||[]).map(r=>`${esc(r)}(새 탭에서 열림)↗`).join('
');
+ const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
');
const owasp = (f.owasp||[]).join(', ');
const cwe = (f.cwe||[]).join(', ');
const d = document.getElementById('detail');
From 5c725eed7b43321c3ed4c6db9fec7186b424ecf4 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 3 Oct 2026 18:52:32 +0900
Subject: [PATCH 5/5] chore: retire duplicate tests/test_dashboard_core.py
delta in favor of #1361
---
tests/test_dashboard_core.py | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py
index 9edc0fbeb..75a5809f8 100644
--- a/tests/test_dashboard_core.py
+++ b/tests/test_dashboard_core.py
@@ -302,17 +302,3 @@ def test_dashboard_search_escape_clears_input():
assert "e.key === 'Escape'" in html
assert "query = '';" in html
assert "render();" in html
-
-
-def test_dashboard_external_link_accessibility():
- """External links must have a screen reader warning and visual indicator."""
- html = dashboard_index_path().read_text(encoding="utf-8")
-
- match = re.search(r'const refs =.*?\`]*>.*?\`', html)
- assert match is not None, "Could not find references template literal"
-
- anchor_html = match.group(0)
-
- assert 'class="ext-link"' in anchor_html
- assert '(새 탭에서 열림)' in anchor_html
- assert 'aria-hidden="true"' in anchor_html