From f701843f536a09f257087e734bea17d3b318eee8 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:17:27 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EB=B9=84?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=20=EB=A1=9C=EB=94=A9=20=EB=B0=8F=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EC=83=81=ED=83=9C=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `console.html`에 `:disabled` 및 `[aria-busy="true"]` 상태에 대한 CSS 추가 (투명도 감소, 커서 변경, 포인터 이벤트 차단) - `test_console_detail_loading_contract.py`에 새 CSS 규칙 검증 로직 추가 - `.jules/palette.md`에 ARIA 상태와 시각적 UI 간의 일치성에 대한 학습 기록 추가 --- .jules/palette.md | 4 ++++ scanner/dashboard/console.html | 1 + tests/test_console_detail_loading_contract.py | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/.jules/palette.md b/.jules/palette.md index ea004e2d..d07a2378 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-16 - Visual Parity for ARIA States +**Learning:** Adding ARIA states like `aria-busy="true"` or semantic states like `:disabled` without corresponding visual styles leaves sighted users unaware of the background network request or disabled state, creating a disconnect between the visual UI and the accessibility tree. +**Action:** Always map ARIA states (like `aria-busy="true"`) and semantic states (like `:disabled`) to CSS visual styles (e.g., `opacity`, `cursor: not-allowed`, or `pointer-events: none`) to ensure visual parity with the accessibility tree. diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 7ec262af..9f1b5155 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -37,6 +37,7 @@ tr.scan{cursor:pointer} tr.scan:hover{background:var(--bg)} input:focus-visible, button:focus-visible, tr.scan:focus-visible, .bar:focus-visible, #detail:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } + :disabled, [aria-busy="true"]{opacity:.6;cursor:not-allowed;pointer-events:none} .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)} .pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;color:#fff} diff --git a/tests/test_console_detail_loading_contract.py b/tests/test_console_detail_loading_contract.py index 38eebed0..c3723535 100644 --- a/tests/test_console_detail_loading_contract.py +++ b/tests/test_console_detail_loading_contract.py @@ -23,6 +23,10 @@ def test_console_exposes_loading_busy_and_error_states(): """Loading and failure states must remain perceivable to assistive technology.""" html = _console_html() + assert ( + ':disabled, [aria-busy="true"]{opacity:.6;cursor:not-allowed;pointer-events:none}' + in html + ) assert 'tr.setAttribute("aria-busy","true");' in html assert 'aria-live="polite" class="muted">Loading scan details...' in html assert 'role="alert" class="err">Error loading details:' in html From 4ebc1fc61fa2d3bfe64f3199235f0541ab2d943e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:02:57 +0900 Subject: [PATCH 2/8] test(a11y): separate busy and disabled semantics --- tests/test_console_detail_loading_contract.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_console_detail_loading_contract.py b/tests/test_console_detail_loading_contract.py index c3723535..559d20dc 100644 --- a/tests/test_console_detail_loading_contract.py +++ b/tests/test_console_detail_loading_contract.py @@ -20,17 +20,18 @@ def test_console_ignores_out_of_order_detail_results_and_errors(): def test_console_exposes_loading_busy_and_error_states(): - """Loading and failure states must remain perceivable to assistive technology.""" + """Busy and unavailable semantics must stay distinct and keyboard-consistent.""" html = _console_html() - assert ( - ':disabled, [aria-busy="true"]{opacity:.6;cursor:not-allowed;pointer-events:none}' - in html - ) + assert ':disabled, [aria-disabled="true"]{opacity:.6;cursor:not-allowed;pointer-events:none}' in html + assert '[aria-busy="true"]{opacity:.6;cursor:progress}' in html assert 'tr.setAttribute("aria-busy","true");' in html + assert 'tr.setAttribute("aria-disabled","true");' in html + assert 'if(tr&&tr.getAttribute("aria-disabled")==="true")return;' 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 def test_console_detail_scrolling_respects_reduced_motion(): From 348b17a2e206cad3cde3f25cf3ec908cb48108cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:04:09 +0900 Subject: [PATCH 3/8] fix(a11y): align busy and unavailable interaction states --- scanner/dashboard/console.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 9f1b5155..74288be0 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -37,7 +37,8 @@ tr.scan{cursor:pointer} tr.scan:hover{background:var(--bg)} input:focus-visible, button:focus-visible, tr.scan:focus-visible, .bar:focus-visible, #detail:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } - :disabled, [aria-busy="true"]{opacity:.6;cursor:not-allowed;pointer-events:none} + :disabled, [aria-disabled="true"]{opacity:.6;cursor:not-allowed;pointer-events:none} + [aria-busy="true"]{opacity:.6;cursor:progress} .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)} .pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;color:#fff} @@ -89,6 +90,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(); } @@ -143,6 +145,7 @@

AppGuardrail Console

$("#msg").innerHTML=`${esc(e.message)}`; } } async function detail(id,tr){ + if(tr&&tr.getAttribute("aria-disabled")==="true")return; const requestId=++currentDetailRequest; const d=$("#detail"); lastDetailFocus=tr||document.activeElement; @@ -150,6 +153,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 +178,7 @@

AppGuardrail Console

}finally{ if(tr&&tr.dataset.detailRequest===String(requestId)){ tr.removeAttribute("aria-busy"); + tr.removeAttribute("aria-disabled"); delete tr.dataset.detailRequest; } } @@ -184,4 +189,4 @@

AppGuardrail Console

if(KEY)load(); - + \ No newline at end of file From 0bf5099460139b72efd09ab0cef28f1097020b17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:05:01 +0900 Subject: [PATCH 4/8] docs(a11y): distinguish busy from disabled semantics --- .jules/palette.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index d07a2378..506c5142 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -82,6 +82,6 @@ **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-16 - Visual Parity for ARIA States -**Learning:** Adding ARIA states like `aria-busy="true"` or semantic states like `:disabled` without corresponding visual styles leaves sighted users unaware of the background network request or disabled state, creating a disconnect between the visual UI and the accessibility tree. -**Action:** Always map ARIA states (like `aria-busy="true"`) and semantic states (like `:disabled`) to CSS visual styles (e.g., `opacity`, `cursor: not-allowed`, or `pointer-events: none`) to ensure visual parity with the accessibility tree. +## 2026-08-16 - Busy and unavailable state parity +**Learning:** `aria-busy="true"` communicates that an element is being updated; it does not itself mean the control is unavailable. Styling every busy element as disabled can create pointer/keyboard inconsistencies and misleading accessibility semantics. +**Action:** Give busy elements a distinct progress visual. When an interaction must be temporarily unavailable, expose `aria-disabled="true"` (or native `disabled` where applicable), guard both pointer and keyboard activation, and clear busy and disabled states together when the request finishes. From dd2c431536dc00931c02a4cbf5e4d033a13c4dbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:05:59 +0900 Subject: [PATCH 5/8] test(a11y): require scoped busy-state token --- tests/test_console_detail_loading_contract.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_console_detail_loading_contract.py b/tests/test_console_detail_loading_contract.py index 559d20dc..5e9d9b04 100644 --- a/tests/test_console_detail_loading_contract.py +++ b/tests/test_console_detail_loading_contract.py @@ -23,8 +23,11 @@ def test_console_exposes_loading_busy_and_error_states(): """Busy and unavailable semantics must stay distinct and keyboard-consistent.""" html = _console_html() - assert ':disabled, [aria-disabled="true"]{opacity:.6;cursor:not-allowed;pointer-events:none}' in html - assert '[aria-busy="true"]{opacity:.6;cursor:progress}' in html + assert "--busy-opacity:.6;" in html + assert 'button:disabled, tr.scan[aria-disabled="true"]' in html + assert '#connect[aria-busy="true"], tr.scan[aria-busy="true"]' in html + assert "opacity:var(--busy-opacity)" in html + assert "pointer-events:none" in html assert 'tr.setAttribute("aria-busy","true");' in html assert 'tr.setAttribute("aria-disabled","true");' in html assert 'if(tr&&tr.getAttribute("aria-disabled")==="true")return;' in html From 57a281ab349d65d46f5cca6172f10367e9559538 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:07:20 +0900 Subject: [PATCH 6/8] fix(a11y): scope busy-state design token --- scanner/dashboard/console.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 74288be0..d4206d14 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -15,7 +15,7 @@ --bg:#F4F5F7; --surface:#FFFFFF; --text:#1A1D24; --muted:#5B6472; --border:#D6DAE0; --divider:#E7EAEF; --primary:#256EF4; --on-primary:#fff; --crit:#D93B3B; --high:#E06C00; --warn:#B7791F; --info:#5B6472; --ok:#1E874B; - --radius:12px; + --radius:12px; --busy-opacity:.6; } *{box-sizing:border-box} body{margin:0;font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text)} @@ -37,8 +37,8 @@ tr.scan{cursor:pointer} tr.scan:hover{background:var(--bg)} input:focus-visible, button:focus-visible, tr.scan:focus-visible, .bar:focus-visible, #detail:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } - :disabled, [aria-disabled="true"]{opacity:.6;cursor:not-allowed;pointer-events:none} - [aria-busy="true"]{opacity:.6;cursor:progress} + button:disabled, tr.scan[aria-disabled="true"]{opacity:var(--busy-opacity);cursor:not-allowed;pointer-events:none} + #connect[aria-busy="true"], tr.scan[aria-busy="true"]{opacity:var(--busy-opacity);cursor:progress} .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)} .pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;color:#fff} From aba5129d42659bfde1134450487e8e361ea68a31 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:16:07 +0000 Subject: [PATCH 7/8] test(a11y): pin close-path and busy-rule split Lock the WAI-ARIA busy/unavailable contract so a later busy detail panel cannot inherit pointer-events:none, and so closeDetail clears aria-disabled on the trigger. Co-authored-by: Seongho Bae --- scanner/dashboard/console.html | 2 +- tests/test_console_dashboard_security.py | 2 + tests/test_console_detail_loading_contract.py | 44 +++++++++++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index d4206d14..928f186d 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -189,4 +189,4 @@

AppGuardrail Console

if(KEY)load(); - \ No newline at end of file + diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index 6e514c84..1406682f 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -26,6 +26,8 @@ def test_detail_panel_close_invalidates_async_work_and_restores_focus() -> None: assert "function closeDetail()" in html assert "currentDetailRequest+=1;" in html assert "lastDetailFocus instanceof HTMLElement && lastDetailFocus.isConnected" in html + assert 'lastDetailFocus.removeAttribute("aria-busy");' in html + assert 'lastDetailFocus.removeAttribute("aria-disabled");' in html assert 'e.key==="Escape"' in html assert html.count('class="close-btn" aria-label="Close details"') == 2 assert html.count('d.querySelector(".close-btn").addEventListener("click",closeDetail);') == 2 diff --git a/tests/test_console_detail_loading_contract.py b/tests/test_console_detail_loading_contract.py index 5e9d9b04..8f8aace2 100644 --- a/tests/test_console_detail_loading_contract.py +++ b/tests/test_console_detail_loading_contract.py @@ -1,13 +1,24 @@ """Regression tests for asynchronous console detail rendering.""" +from pathlib import Path + from scanner.cli.appguardrail import dashboard_index_path +ROOT = Path(__file__).resolve().parents[1] + def _console_html() -> str: """Return the standalone console document as UTF-8 text.""" return dashboard_index_path().with_name("console.html").read_text(encoding="utf-8") +def _css_declaration(html: str, selector: str) -> str: + """Return the CSS declaration block for an exact selector string.""" + token = selector + "{" + start = html.index(token) + len(token) + return html[start : html.index("}", start)] + + def test_console_ignores_out_of_order_detail_results_and_errors(): """Only the most recently selected scan may update the shared detail panel.""" html = _console_html() @@ -24,10 +35,17 @@ def test_console_exposes_loading_busy_and_error_states(): html = _console_html() assert "--busy-opacity:.6;" in html - assert 'button:disabled, tr.scan[aria-disabled="true"]' in html - assert '#connect[aria-busy="true"], tr.scan[aria-busy="true"]' in html - assert "opacity:var(--busy-opacity)" in html - assert "pointer-events:none" in html + assert ':disabled, [aria-busy="true"]' not in html + unavailable = _css_declaration( + html, 'button:disabled, tr.scan[aria-disabled="true"]' + ) + busy = _css_declaration( + html, '#connect[aria-busy="true"], tr.scan[aria-busy="true"]' + ) + assert "opacity:var(--busy-opacity)" in unavailable + assert "pointer-events:none" in unavailable + assert "opacity:var(--busy-opacity)" in busy + assert "pointer-events" not in busy assert 'tr.setAttribute("aria-busy","true");' in html assert 'tr.setAttribute("aria-disabled","true");' in html assert 'if(tr&&tr.getAttribute("aria-disabled")==="true")return;' in html @@ -37,6 +55,24 @@ def test_console_exposes_loading_busy_and_error_states(): assert 'tr.removeAttribute("aria-disabled");' in html +def test_console_busy_state_docs_name_the_next_reusable_surface(): + """Buyers and later Storybook work must find the token contract without reading CSS.""" + inventory = (ROOT / "docs" / "storybook-inventory.md").read_text(encoding="utf-8") + changelog = ( + ROOT / "CHANGELOG.d" / "974-console-busy-unavailable.md" + ).read_text(encoding="utf-8") + adr = ( + ROOT / "docs" / "adr" / "0007-console-busy-unavailable-tokens.md" + ).read_text(encoding="utf-8") + + assert "--busy-opacity" in inventory + assert "ScanRow/BusyUnavailable" in inventory + assert "pointer-events: none" in inventory + assert "aria-disabled" in changelog + assert "WAI-ARIA" in adr + assert "https://www.w3.org/TR/wai-aria-1.2/" in adr + + def test_console_detail_scrolling_respects_reduced_motion(): """Successful and failed detail requests must honor reduced-motion preferences.""" html = _console_html() From 8759efb721020c99dd171fe43ce3f6bb398df95e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:16:09 +0000 Subject: [PATCH 8/8] docs(a11y): record console busy-state tokens and Storybook inventory Add ADR-0007, APA 7th WAI-ARIA/WCAG citations, and a no-build Storybook inventory so later dashboard surfaces reuse --busy-opacity instead of a global aria-busy rule. Co-authored-by: Seongho Bae --- .jules/palette.md | 8 +++- ARCHITECTURE.md | 2 + CHANGELOG.d/974-console-busy-unavailable.md | 3 ++ docs/PRD.md | 4 ++ docs/TEST_STRATEGY.md | 3 +- docs/TRACEABILITY.md | 1 + .../0007-console-busy-unavailable-tokens.md | 36 +++++++++++++++ docs/adr/README.md | 1 + docs/storybook-inventory.md | 46 +++++++++++++++++++ 9 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.d/974-console-busy-unavailable.md create mode 100644 docs/adr/0007-console-busy-unavailable-tokens.md create mode 100644 docs/storybook-inventory.md diff --git a/.jules/palette.md b/.jules/palette.md index 506c5142..36655f00 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -84,4 +84,10 @@ ## 2026-08-16 - Busy and unavailable state parity **Learning:** `aria-busy="true"` communicates that an element is being updated; it does not itself mean the control is unavailable. Styling every busy element as disabled can create pointer/keyboard inconsistencies and misleading accessibility semantics. -**Action:** Give busy elements a distinct progress visual. When an interaction must be temporarily unavailable, expose `aria-disabled="true"` (or native `disabled` where applicable), guard both pointer and keyboard activation, and clear busy and disabled states together when the request finishes. +**Action:** Give busy elements a distinct progress visual. When an interaction must be temporarily unavailable, expose `aria-disabled="true"` (or native `disabled` where applicable), guard both pointer and keyboard activation, and clear busy and disabled states together when the request finishes. Keep `--busy-opacity` as the shared token and never apply `pointer-events: none` to a busy-only selector. Next: reuse the same token list from `docs/storybook-inventory.md` when adding a Storybook story; do not introduce a build step into `console.html`. + +### References (APA 7th) + +World Wide Web Consortium. (2023). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria-1.2/ + +World Wide Web Consortium. (2023). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bb3a2105..c81972f4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -87,6 +87,8 @@ Stored SSRF prevention and scanner detection are separate controls. The control- Current standalone control plane is stdlib HTTP + SQLite, with tenant API-key roles and scan/history/drift/webhook configuration. Persistent organization identity is resolved from authenticated key context, not untrusted payload strings. Enterprise replacement of SQLite is behind stable repository service functions and requires migrations/authz/recovery evidence. +The organization console is a no-build HTML surface. In-flight (`aria-busy`) and unavailable (`aria-disabled` / native `disabled`) states stay scoped to live controls and share `--busy-opacity`. Storybook may later consume `docs/storybook-inventory.md`; it is not a runtime dependency of `appguardrail serve`. See ADR-0007. + ## Remediation authority Autofix can perform only narrowly proven semantics-preserving transformations. Other fixes are guidance for a user/agent and become accepted only after rescanning/reverification. Model-generated remediation is never a substitute for scanner evidence. diff --git a/CHANGELOG.d/974-console-busy-unavailable.md b/CHANGELOG.d/974-console-busy-unavailable.md new file mode 100644 index 00000000..d11f578c --- /dev/null +++ b/CHANGELOG.d/974-console-busy-unavailable.md @@ -0,0 +1,3 @@ +### Changed + +- The organization console now keeps in-flight (`aria-busy`) and temporarily unavailable (`aria-disabled` / native `disabled`) states distinct, shares `--busy-opacity` as the busy-state token, and blocks repeat scan-detail activation from both pointer and keyboard until the request finishes. Close, success, failure, and `finally` clear both attributes so a late response cannot leave a row looking disabled. Next: reuse the token list in `docs/storybook-inventory.md` for any new dashboard or Storybook surface; do not add a global `[aria-busy="true"]` rule. diff --git a/docs/PRD.md b/docs/PRD.md index 77c43ade..fc68a642 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -94,6 +94,10 @@ Buyer/agency/founder/fix-pack/org evidence SHALL be derived from normalized find AppGuardrail SHALL produce deterministic component inventory with lockfile provenance where available and preserve tool/source/version evidence required for review or acquisition diligence. +### PRD-FR-010 Console interaction states + +The organization console SHALL distinguish in-flight updates (`aria-busy`) from temporarily unavailable controls (`aria-disabled` or native `disabled`), keep pointer and keyboard activation on one gate, and clear both states on success, failure, and close. Shared visual values SHALL live in named design tokens documented in the Storybook inventory. A Storybook package MAY consume that inventory later; it SHALL NOT become a runtime dependency of the standalone console. + ## 7. Security/privacy requirements - scan targets and their source may contain PII/secrets; minimize retention/disclosure rather than blindly copying findings/context; diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 2dcf63f5..0215b3e8 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -65,7 +65,8 @@ Adapters distinguish tool unavailable, tool failed, clean, and findings. Normali - webhook config URL validation and execution safety; - API-key bootstrap/revocation/logging; - idempotency/concurrency where endpoints can be retried; -- SQLite migration/upgrade/backup/recovery for changed persistent state. +- SQLite migration/upgrade/backup/recovery for changed persistent state; +- console busy versus unavailable token/selector split, shared activation gate, and close-path attribute clear. ## Remediation tests diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index b9757a95..b20c17bb 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -20,6 +20,7 @@ | authenticated workflow-result detector evidence | issue-detection audit workflow evidence | PR #911 active-PR | | automatic scanner detection of unsafe stored-webhook SSRF pattern | built-in `python-stored-ssrf-webhook-url` rule | implemented-main through PR #910 for tested Python `set_webhook` direct and one-hop persistence flows; bounded scope | | structural Semgrep-style `pattern:` execution by lightweight engine | built-in scanner | not implemented unless a real structural matcher is added; fixtures are not execution | +| org console busy vs unavailable interaction | `scanner/dashboard/console.html` plus contract tests | active-PR through #974; Storybook inventory is documentation-only until an optional package exists | ## Promotion rules diff --git a/docs/adr/0007-console-busy-unavailable-tokens.md b/docs/adr/0007-console-busy-unavailable-tokens.md new file mode 100644 index 00000000..0affe135 --- /dev/null +++ b/docs/adr/0007-console-busy-unavailable-tokens.md @@ -0,0 +1,36 @@ +# ADR-0007: Console busy and unavailable tokens stay scoped + +**Status:** Accepted +**Date:** 2026-08-16 + +## Context + +The organization console is a standalone, no-build HTML page. Sighted and assistive-technology users need distinct feedback while a scan-detail request is in flight. A previous Palette change styled every `[aria-busy="true"]` element as disabled and applied `pointer-events: none`, which blocked pointer input while keyboard activation remained live and would also disable a future busy detail panel, including its close control. + +WAI-ARIA 1.2 defines `aria-busy` as an in-flight update state and `aria-disabled` as perceivable-but-not-operable (World Wide Web Consortium, 2023a). Those states must not be conflated. + +Repeated console surfaces (Connect, scan rows, detail close) also need a reusable token list so dashboard and a later Storybook package can share one busy/unavailable contract without forcing a JavaScript build into the standalone page. + +## Decision + +1. Native `disabled` buttons and `tr.scan[aria-disabled="true"]` share unavailable styling and pointer suppression. +2. `#connect[aria-busy="true"]` and `tr.scan[aria-busy="true"]` share a progress visual only. Busy-only rules MUST NOT set `pointer-events: none`. +3. `--busy-opacity` is the shared design token for both states. +4. `detail()` refuses repeat activation while the row is `aria-disabled`. Success, failure, close, and `finally` clear both attributes. Close also clears `lastDetailFocus` so a late `finally` cannot revive the row. +5. The standalone console remains ponytail: no framework and no build step. Storybook consumption is documented in `docs/storybook-inventory.md` and may later live in an optional package. It must not become a runtime dependency of `appguardrail serve`. + +## Consequences + +- Contract tests pin the scoped selectors, the busy-rule exclusion of `pointer-events`, and the close-path `aria-disabled` clear. +- A later busy `#detail` region can keep its close button operable. +- Adding `@storybook/html` requires a separate optional package and an inventory update; it is not a merge requirement for this console contract. + +## Next action + +Reuse `--busy-opacity` and the inventory stories when the next dashboard or Storybook surface needs an in-flight or unavailable control. Do not add a global `[aria-busy="true"]` rule. + +## References (APA 7th) + +World Wide Web Consortium. (2023a). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria-1.2/ + +World Wide Web Consortium. (2023b). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ diff --git a/docs/adr/README.md b/docs/adr/README.md index 984420a9..f3538639 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,6 +10,7 @@ | [0004](0004-tenant-network-boundaries.md) | Tenant authority and outbound destinations are explicit security boundaries | Accepted | | [0005](0005-remediation-authority.md) | Deterministic autofix is limited to proven semantics-preserving transforms | Accepted | | [0006](0006-automation-authority.md) | Autonomous development remains separate from independent merge/release authority | Accepted | +| [0007](0007-console-busy-unavailable-tokens.md) | Console busy and unavailable tokens stay scoped; Storybook stays optional | Accepted | ## ADR triggers diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md new file mode 100644 index 00000000..f05da845 --- /dev/null +++ b/docs/storybook-inventory.md @@ -0,0 +1,46 @@ +# Storybook inventory — organization console + +**Status:** Accepted inventory for reusable console objects +**Last reviewed:** 2026-08-16 + +The live console is `scanner/dashboard/console.html`. It must keep working as a standalone no-build page. Use this inventory when adding a Storybook story or another dashboard surface so the same tokens and states are reused. + +## Design tokens + +| Token | Current value | Use this when | +|---|---|---| +| `--busy-opacity` | `.6` | Any in-flight or temporarily unavailable control | +| `--primary` | `#256EF4` | Focus rings and primary actions | +| `--muted` | `#5B6472` | Secondary text and close-button rest state | +| `--crit` | `#D93B3B` | Blocking counts and error text | +| `--radius` | `12px` | Cards and stat tiles | + +Do not embed a one-off opacity or cursor in a new selector. Add a token here first. + +## Stories to implement + +| Story | Object | Required attributes | Buyer-visible next action | +|---|---|---|---| +| `Connect/Default` | `#connect` | none | Paste an org API key and connect | +| `Connect/Busy` | `#connect` | `aria-busy="true"` plus native `disabled` | Wait; the control is connecting | +| `ScanRow/Default` | `tr.scan` | `tabindex="0" role="button"` | Open scan details | +| `ScanRow/BusyUnavailable` | `tr.scan` | `aria-busy="true"` and `aria-disabled="true"` | Wait; do not activate again | +| `Detail/Loading` | `#detail` | polite live region only | Wait for findings; close stays available | +| `Detail/Error` | `#detail` | `role="alert"` | Read the error, then close or retry another row | + +## Interaction contract + +- Busy is progress. Unavailable is `aria-disabled` or native `disabled`. +- Pointer and keyboard (Enter/Space) must share one activation gate. +- Close and Escape must clear both attributes on the trigger and restore focus. +- A future busy parent must not apply `pointer-events: none` to `.close-btn`. + +## Optional Storybook package + +If a later change adds Storybook, ship it as an optional `@appguardrail/console-stories` (or equivalent) package that imports these tokens. Do not add a bundler to `appguardrail serve`. After adding a story, update this table and keep the contract tests in `tests/test_console_detail_loading_contract.py` as the executable source of truth. + +## References (APA 7th) + +World Wide Web Consortium. (2023a). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria-1.2/ + +World Wide Web Consortium. (2023b). *Web Content Accessibility Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/