diff --git a/.jules/palette.md b/.jules/palette.md
index ea004e2d..fe85b31d 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-09-03 - Added Proxy Button for File Input in Header
+**Learning:** In zero-build dashboard (scanner/dashboard/index.html), the native does not support firing the change event when a user attempts to select the same file path sequentially. Additionally, hiding the file input and exposing a proxy allows users to have a much larger and better-styled click target.
+**Action:** Visually hide the native with the hidden attribute and create a proxy that triggers the file input. Remove aria-label from the native input when using a proxy button to adhere to 'label-in-name' principles and update UI contract tests appropriately.
diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html
index 132bc31b..975caeb9 100644
--- a/scanner/dashboard/index.html
+++ b/scanner/dashboard/index.html
@@ -93,7 +93,8 @@
AppGuardrail
no findings loaded
-
+
+
@@ -325,6 +326,7 @@ Dashboard
}
const fileInput = document.getElementById('file');
+document.getElementById('header-browse').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
const selectedFile = fileInput.files?.[0];
fileInput.value = '';
diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py
index 75a5809f..be738615 100644
--- a/tests/test_dashboard_core.py
+++ b/tests/test_dashboard_core.py
@@ -77,7 +77,7 @@ def test_dashboard_rows_are_keyboard_accessible():
assert 'tabindex="0" role="button"' in html
assert 'title="View details for finding"' in html
assert "tbody tr:focus-visible" in html
- assert "aria-label=\"Upload findings file\"" in html
+ assert ">Upload findings file " in html
assert "aria-label=\"Search findings\"" in html
assert "aria-label=\"Filter by severity\"" in html
assert "tr.addEventListener('keydown'" in html
diff --git a/tests/test_dashboard_upload_proxy_contract.py b/tests/test_dashboard_upload_proxy_contract.py
new file mode 100644
index 00000000..66e8405e
--- /dev/null
+++ b/tests/test_dashboard_upload_proxy_contract.py
@@ -0,0 +1,38 @@
+"""Contracts for the dashboard file-upload proxy control."""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+
+DASHBOARD = Path("scanner/dashboard/index.html")
+
+
+def _header_upload_button(html: str) -> str:
+ """Return the shipped header upload button tag."""
+ match = re.search(r']*\bid="header-browse"[^>]*>', html)
+ assert match is not None, "header upload proxy button must exist"
+ return match.group(0)
+
+
+def test_header_upload_proxy_preserves_primary_action_touch_target() -> None:
+ """The header proxy must not override the shared 44px primary-action target."""
+ html = DASHBOARD.read_text(encoding="utf-8")
+ button = _header_upload_button(html)
+
+ assert 'class="primary-action"' in button
+ assert "min-height:auto" not in button.replace(" ", "")
+ assert "padding:6px 12px" not in button
+
+
+def test_header_upload_proxy_keeps_visible_name_and_native_file_boundary() -> None:
+ """The visible button owns the name while the hidden native input owns selection."""
+ html = DASHBOARD.read_text(encoding="utf-8")
+
+ assert ">Upload findings file " in html
+ assert ' ' in html
+ assert "document.getElementById('header-browse').addEventListener('click', () => fileInput.click())" in html
+ # Same-file re-selection is enabled by clearing the native input after handling it;
+ # it is not a property of the proxy button itself.
+ assert "fileInput.value = '';" in html