From 96e11812cefc0cb522006c043d03b1753fe34634 Mon Sep 17 00:00:00 2001 From: gitea-impl-agent Date: Mon, 5 Oct 2026 00:49:28 +0000 Subject: [PATCH 1/4] fix(dashboard): keep shell during menu transitions Replace document navigation with native Streamlit state transitions, preserve dashboard contracts, and add regression plus authenticated browser evidence for Issue #9. --- dashboard/app.py | 20 +- dashboard/common.py | 50 +- .../README.md | 47 + .../browser-observations.json | 988 ++++++++++++++++++ .../final-state.jpg | Bin 0 -> 34297 bytes .../manifest.json | 61 ++ docs/interface-contract.md | 38 + docs/project-structure.md | 21 +- .../req-002_dashboard-menu-transition.md | 258 +++++ docs/runtime-flow.md | 30 + scripts/streamlit_browser_evidence.py | 319 +++++- tests/test_dashboard_menu_transition.py | 219 ++++ tests/test_streamlit_browser_evidence.py | 166 +++ 13 files changed, 2166 insertions(+), 51 deletions(-) create mode 100644 docs/evidence/issue-9-dashboard-menu-transition/README.md create mode 100644 docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json create mode 100644 docs/evidence/issue-9-dashboard-menu-transition/final-state.jpg create mode 100644 docs/evidence/issue-9-dashboard-menu-transition/manifest.json create mode 100644 docs/requirements/req-002_dashboard-menu-transition.md create mode 100644 tests/test_dashboard_menu_transition.py create mode 100644 tests/test_streamlit_browser_evidence.py diff --git a/dashboard/app.py b/dashboard/app.py index 1a4d5b7..6b231cf 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -12,7 +12,15 @@ if __package__ in (None, ""): st.set_page_config(page_title="Jarca Ledger", page_icon="💰", layout="wide") -from dashboard.common import clear_cache, get_db_path, get_transfer_review_items, inject_app_css, render_sidebar_nav # noqa: F401 +from dashboard.common import ( + clear_cache, + get_db_path, + get_transfer_review_items, # noqa: F401 + inject_app_css, + render_dashboard_body, + render_sidebar_nav, + validate_dashboard_page, +) # noqa: F401 from dashboard.authentication import require_dashboard_authentication from dashboard.page_views.accounts import page_accounts from dashboard.page_views.assets import page_assets @@ -32,15 +40,15 @@ def main() -> None: db_path = get_db_path() inject_app_css() pages = ["월정산", "요약", "자산/마감", "거래내역", "확인필요 큐", "미분류", "분류 규칙", "카테고리", "계좌", "Import"] - query_page = st.query_params.get("page", "요약") - page = query_page if query_page in pages else "요약" - st.session_state["page"] = page + query_page = st.query_params.get("page") + page = validate_dashboard_page(query_page, pages) + st.session_state["dashboard_page"] = page with st.sidebar: st.markdown("## 💰 Jarca Ledger") st.caption("가계부 · 분류 · Import 운영 콘솔") st.markdown('
메뉴
', unsafe_allow_html=True) - render_sidebar_nav(pages, page) + page = render_sidebar_nav(pages, page) if st.button("새로고침", key="refresh_button", use_container_width=True): clear_cache() st.rerun() @@ -56,7 +64,7 @@ def main() -> None: "계좌": page_accounts, "Import": page_imports, } - dispatch[page](db_path) + render_dashboard_body(page, lambda: dispatch[page](db_path)) if __name__ == "__main__": diff --git a/dashboard/common.py b/dashboard/common.py index 60abf90..23c66a5 100644 --- a/dashboard/common.py +++ b/dashboard/common.py @@ -4,7 +4,6 @@ import sqlite3 from datetime import datetime from html import escape from typing import Any -from urllib.parse import quote import importlib import inspect from zoneinfo import ZoneInfo @@ -314,6 +313,9 @@ def inject_app_css() -> None: border-color: rgba(37, 99, 235, 0.48); box-shadow: 0 10px 22px rgba(15, 23, 42, 0.10); } + .st-key-dashboard_body:has(.st-key-dashboard_loading [data-testid="stAlert"]) .st-key-dashboard_content { + display: none; + } .jarca-card { border: 1px solid rgba(226, 232, 240, 0.86); border-radius: 22px; @@ -355,13 +357,45 @@ def render_section_label(icon: str, label: str) -> None: st.markdown(f'
{escape(icon)}{escape(label)}
', unsafe_allow_html=True) -def render_sidebar_nav(pages: list[str], current_page: str) -> None: - items = [] - for name in pages: - active = " active" if name == current_page else "" - href = f"?page={quote(name)}" - items.append(f'{escape(name)}') - st.markdown(f'
{"".join(items)}
', unsafe_allow_html=True) +def validate_dashboard_page(page: str | None, pages: list[str]) -> str: + return page if page in pages else "요약" + + +def _sync_dashboard_page_query() -> None: + st.query_params["page"] = st.session_state["dashboard_page"] + + +def render_sidebar_nav(pages: list[str], current_page: str) -> str: + if "dashboard_page" not in st.session_state: + st.session_state["dashboard_page"] = current_page + return st.radio( + "메뉴", + pages, + key="dashboard_page", + label_visibility="collapsed", + on_change=_sync_dashboard_page_query, + ) + + +def render_dashboard_body(page: str, renderer) -> None: + body = st.empty() + body.empty() + try: + with body.container(): + with st.container(key="dashboard_body"): + loading = st.empty() + with loading.container(): + with st.container(key="dashboard_loading"): + st.info(f"{page} 불러오는 중…") + with st.container(key="dashboard_content"): + renderer() + loading.empty() + except Exception: + body.empty() + with body.container(): + st.error("페이지를 불러오지 못했습니다. 다시 시도해 주세요.") + if st.button("다시 시도", key=f"retry_{page}"): + st.rerun() def parse_optional_int(value: str, field_name: str) -> int | None: diff --git a/docs/evidence/issue-9-dashboard-menu-transition/README.md b/docs/evidence/issue-9-dashboard-menu-transition/README.md new file mode 100644 index 0000000..d3d2a29 --- /dev/null +++ b/docs/evidence/issue-9-dashboard-menu-transition/README.md @@ -0,0 +1,47 @@ +# Issue #9 authenticated browser evidence + +This directory binds the dashboard menu-transition acceptance evidence to the exact working-tree files listed in `manifest.json`. + +## Captured transition + +- **Path:** `요약` → `분류 규칙` +- **Input:** native Streamlit menu click in an authenticated HTTPS browser session +- **Fixture:** generated synthetic SQLite database only +- **Sampling:** 35 DOM/style frames at 50 ms intervals +- **Viewport:** 1280 × 720 +- **Browser:** Headless Chrome 154 on Linux x86_64 +- **Fixture host:** Linux aarch64, Python 3.13.5, Streamlit 1.58.0, Uvicorn 0.49.0 + +## Verified browser observations + +All eight manifest checks are true: + +- the HTML document marker stayed identical; +- the sidebar marker stayed identical; +- the page background stayed identical; +- no post-click full-document `load` event occurred; +- the target loading state was observed; +- the previous `요약` body was absent in every loading frame; +- the unrelated `fixture=preserved` query key survived; +- final URL, selected menu, target body, and cleared loading state agreed. + +`browser-observations.json` contains every sampled frame, body hashes, URL state, history events, document/sidebar markers, computed background, runtime metadata, and explicit limitations. `final-state.jpg` is the final visible browser state; it contains no credential or secret. + +## Recheck + +Run the exact validator recorded in `manifest.json`. The captured result was: + +```text +exit code: 0 +PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=35 full_document_loads=0 +``` + +The browser action was executed through `mcp__playwright__browser_run_code_unsafe`. That API reports success/error rather than a numeric process exit code; this boundary is explicit in the manifest instead of being represented as a fabricated exit status. + +## Evidence boundaries + +- This is synthetic-fixture browser evidence, not operating-ledger evidence. +- A fixture-only external wrapper delayed the rules renderer by two seconds so the loading interval could be sampled. It did not alter repository source. +- A temporary HTTPS ingress adapter supplied trusted proxy scheme/Host metadata. Repository authentication and same-origin enforcement remained active. +- The tunnel URL is intentionally ephemeral and is retained in the frame record only for provenance. +- The screenshot supplements, but does not replace, the machine-readable continuous-frame observations. diff --git a/docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json b/docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json new file mode 100644 index 0000000..c84762f --- /dev/null +++ b/docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json @@ -0,0 +1,988 @@ +{ + "schema": "jarca.issue9.browser-transition.v1", + "source": "authenticated Playwright browser runner through temporary HTTPS fixture ingress", + "from_page": "요약", + "to_page": "분류 규칙", + "pre": { + "elapsed_ms": 0, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "요약", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + "frames": [ + { + "elapsed_ms": 134, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 296, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 374, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 442, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 520, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 600, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 669, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 739, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "query_page": "요약", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 816, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 1396, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 1580, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 1654, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 1721, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 1793, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 1860, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 1926, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 1993, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2061, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2130, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2197, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2274, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2341, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2410, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2478, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2544, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2610, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2681, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2753, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2826, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 2912, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 3286, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 4054, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": true, + "previous_body_visible": false, + "target_body_visible": false, + "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 5973, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "f5c040f5f42490cba765987c8b849ea41ee248ed1b403f2dc890b2b27a73dad9", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 6065, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "f5c040f5f42490cba765987c8b849ea41ee248ed1b403f2dc890b2b27a73dad9", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + { + "elapsed_ms": 6134, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "f5c040f5f42490cba765987c8b849ea41ee248ed1b403f2dc890b2b27a73dad9", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + } + ], + "post": { + "elapsed_ms": 6134, + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "query_page": "분류 규칙", + "sibling_value": "preserved", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "f5c040f5f42490cba765987c8b849ea41ee248ed1b403f2dc890b2b27a73dad9", + "document_marker": "issue9-document-marker", + "sidebar_marker": "issue9-sidebar-marker", + "background": "rgb(255, 255, 255)", + "history_events": [ + { + "type": "pushState", + "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693754 + } + ], + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + "same_document_events": [ + { + "kind": "same-document-url-change", + "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "at": 1791130693813 + } + ], + "document_load_events": [], + "checks": { + "document_identity_stable": true, + "sidebar_identity_stable": true, + "background_stable": true, + "zero_full_document_loads": true, + "loading_observed": true, + "previous_body_absent_while_loading": true, + "query_sibling_preserved": true, + "final_agreement": true + }, + "runtime": { + "captured_at": "2026-10-04T16:18:18.832Z", + "interval_ms": 50, + "frame_count": 35, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": "Linux x86_64" + }, + "limitations": [ + "Synthetic SQLite fixture only; no operating ledger data.", + "Rules renderer delayed two seconds by an external fixture-only module wrapper.", + "Temporary HTTPS ingress adapter normalized scheme/Host metadata; repository authentication checks remained active.", + "Playwright same-document URL events are listed separately from full load events." + ] +} \ No newline at end of file diff --git a/docs/evidence/issue-9-dashboard-menu-transition/final-state.jpg b/docs/evidence/issue-9-dashboard-menu-transition/final-state.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2db8b21a77abd60446c5e5229cb95e69ba3de62 GIT binary patch literal 34297 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 RokxfMe|G%dO3wZ={2!FT^P&I% literal 0 HcmV?d00001 diff --git a/docs/evidence/issue-9-dashboard-menu-transition/manifest.json b/docs/evidence/issue-9-dashboard-menu-transition/manifest.json new file mode 100644 index 0000000..5205b6a --- /dev/null +++ b/docs/evidence/issue-9-dashboard-menu-transition/manifest.json @@ -0,0 +1,61 @@ +{ + "schema": "jarca.issue9.evidence-manifest.v1", + "issue": "jsfamily/jarca-ledger-mcp#9", + "captured_at": "2026-10-04T16:18:18.832Z", + "source": { + "base_revision": "ffd943fe65875e67148c28ba44bae32918e338c9", + "working_tree": true, + "files_sha256": { + "dashboard/app.py": "4c7b3a6264b4fb4ae0e445ddc04c625b295443816b198aa5a60615ac44846ff2", + "dashboard/common.py": "9a0b9dbb4ca88f27757fc293173821c48a56a30bc23f16b0f24731f246411c5a", + "scripts/streamlit_browser_evidence.py": "766beefb0d861ea6b08561c3aaa0c6d8915c9166e4221725e27b5e26cea63798", + "tests/test_dashboard_menu_transition.py": "d23c35d88f6289b7c7e84a271df6ff3f6ff1f25a3a885aa696cc93db0b37035e", + "tests/test_streamlit_browser_evidence.py": "b922593f96da5af9a0b1fe33cacbe3abe139ef00554e7c535fe7cc8f24679b23" + } + }, + "execution": { + "browser_tool": "mcp__playwright__browser_run_code_unsafe", + "tool_status": "success", + "exit_code": null, + "exit_code_note": "The Playwright MCP call is not a process and exposes success/error status rather than a numeric exit code.", + "transition": "요약 -> 분류 규칙", + "sampling_interval_ms": 50, + "frame_count": 35, + "viewport": { + "width": 1280, + "height": 720 + }, + "browser": "HeadlessChrome/154.0.0.0", + "browser_architecture": "Linux x86_64", + "fixture_host_architecture": "aarch64", + "python": "3.13.5", + "streamlit": "1.58.0", + "uvicorn": "0.49.0" + }, + "checks": { + "document_identity_stable": true, + "sidebar_identity_stable": true, + "background_stable": true, + "zero_full_document_loads": true, + "loading_observed": true, + "previous_body_absent_while_loading": true, + "query_sibling_preserved": true, + "final_agreement": true + }, + "artifacts_sha256": { + "browser-observations.json": "d5dcd18f074b0f3e44e1b46eb9cf685f028b0f229957d35dc2330bedca5f0cbb", + "final-state.jpg": "5a9c94598776a9ddaf4bb1aed1c758f6d1c489c320f7a81ecb3acd0cb702760d" + }, + "validator": { + "command": "python3 -c \"import hashlib,json,pathlib; root=pathlib.Path('.'); m=json.loads((root/'docs/evidence/issue-9-dashboard-menu-transition/manifest.json').read_text()); assert all(hashlib.sha256((root/p).read_bytes()).hexdigest()==h for p,h in m['source']['files_sha256'].items()); evidence=root/'docs/evidence/issue-9-dashboard-menu-transition'; assert all(hashlib.sha256((evidence/p).read_bytes()).hexdigest()==h for p,h in m['artifacts_sha256'].items()); d=json.loads((evidence/'browser-observations.json').read_text()); assert all(d['checks'].values()) and not d['document_load_events']; assert any(f['loading'] and not f['previous_body_visible'] for f in d['frames']); print('PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=%d full_document_loads=0' % len(d['frames']))\"", + "exit_code": 0, + "output": "PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=35 full_document_loads=0" + }, + "limitations": [ + "The browser run used only a generated synthetic SQLite fixture; no operating ledger data was used.", + "A fixture-only external module wrapper delayed the rules renderer by two seconds to expose the loading interval; repository source was not modified for the delay.", + "A temporary HTTPS ingress adapter normalized trusted proxy scheme and Host metadata; repository authentication and same-origin checks remained active.", + "The ephemeral tunnel URL in browser-observations.json is expired and is retained only as provenance.", + "Playwright same-document URL events are recorded separately from full document load events." + ] +} diff --git a/docs/interface-contract.md b/docs/interface-contract.md index e47679a..21beda7 100644 --- a/docs/interface-contract.md +++ b/docs/interface-contract.md @@ -221,3 +221,41 @@ Set-Cookie: __Host-jarca_dashboard_auth=; Max-Age=0; Secure; HttpOnly; SameSite= - 실패 계약: wrong credential, invalid configuration, cross-origin, malformed cookie tests - 경계 계약: expiry, rotation, oversized token, same-host different-port origin tests - 구현·스키마·예시 정합성: `tests/test_dashboard_auth_server.py`, `tests/test_dashboard_authentication_app.py`, `tests/test_dashboard_persistent_authentication.py`, source/document redaction search + +## Dashboard 메뉴 전환 계약 — REQ-002 + +### dashboard-menu-transition + +- 책임: authenticated Dashboard에서 공통 shell/background를 유지하고 선택된 page body만 교체한다. +- 종류·프로토콜: Streamlit rerun, `st.radio` callback, query parameter +- 호출 대상·operation: `dashboard.app.main()` → `render_sidebar_nav()` → `render_dashboard_body()` +- 버전: 1.0 +- 호출 주체: authenticated browser +- 인증 방식: 기존 `gate` 계약 선행; 메뉴는 인증을 우회하지 않음 +- 필요 권한: valid Dashboard auth cookie + +#### 상태와 입력 + +정확한 menu options/order는 `월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`이다. selected value는 `st.session_state["dashboard_page"]`에 둔다. 직접 query의 `page`가 known option이면 해당 page를, absent/unknown이면 `요약`을 사용한다. + +#### 출력·불변조건 + +- callback은 `st.query_params["page"]`만 갱신하고 `month`, `account_id`, `filter` 등 sibling key를 보존한다. +- menu click은 document/path navigation이 아니며 stable `st.empty` body container 안에서 target renderer를 실행한다. +- 공통 sidebar, refresh control, CSS/background는 유지하고 body만 target content로 대체한다. +- 느린 target에는 ` 불러오는 중…`을 표시한 뒤 renderer 완료 시 placeholder를 제거한다. +- renderer 예외는 body 안의 `페이지를 불러오지 못했습니다. 다시 시도해 주세요.`와 retry control로 대체하며 예외 상세를 노출하지 않는다. +- serialized callback이 여러 번 실행되면 마지막 selection이 query와 body의 최종 상태가 된다. + +#### 오류 + +| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 | +|---|---|---|---|---|---| +| `page_fallback` | request | missing/unknown `page` | 가능 | `요약` 표시 | sibling query 유지 | +| `renderer_unavailable` | rendering | target renderer exception | 가능 | generic error의 retry 클릭 | previous body 제거, auth/shell 유지 | + +#### 검증 경계 + +AppTest는 callback, query mapping, synthetic renderer output과 error/loading event를 검증하지만 실제 browser의 document navigation 부재, paint timing, focus/scroll, network request 또는 perceived shell continuity를 증명하지 않는다. 따라서 `REQ-002.03-AC01`은 authenticated real-browser evidence가 필수이며 AppTest는 companion evidence일 뿐이다. history/back-forward와 path URL은 계약에서 제외한다. + +상세 acceptance target은 `docs/requirements/req-002_dashboard-menu-transition.md`의 `REQ-002.01-AC01`부터 `REQ-002.09-AC01`까지를 따른다. diff --git a/docs/project-structure.md b/docs/project-structure.md index 60ac861..54ae757 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -35,6 +35,8 @@ tests/ ├── test_dashboard_authentication.py ├── test_dashboard_persistent_authentication.py ├── test_dashboard_auth_server.py +├── test_dashboard_menu_transition.py +├── test_streamlit_browser_evidence.py └── test_dashboard_authentication_app.py ``` @@ -43,12 +45,23 @@ tests/ | `tests/test_dashboard_authentication.py` | test | credential compare, delay, redaction | pytest tests | test-only settings | source change paired with test | developer | | `tests/test_dashboard_persistent_authentication.py` | test | claims, TTL, tamper, rotation | pytest tests | no real secrets | source change paired with test | developer | | `tests/test_dashboard_auth_server.py` | test | HTTP routes and cookie scope | pytest tests | synthetic test credentials | source change paired with test | developer | +| `tests/test_dashboard_menu_transition.py` | test | REQ-002 menu/query/body transition and cache-clear regression | pytest/AppTest | synthetic fixture DB; no real secrets | source change paired with test and browser evidence where required | developer | +| `tests/test_streamlit_browser_evidence.py` | test | transition evidence manifest, redaction, TLS fixture and fail-closed validation | pytest | generated synthetic values only; no captured credential values | evidence-harness change paired with test | developer | | `tests/test_dashboard_authentication_app.py` | test | gate ordering and cleanup/UI | AppTest | fixture DB only | source change paired with test | developer | +### browser-evidence — 실제 전환 관찰 + +| 경로 | 종류 | 책임 | 공개 진입점 | 내부 구현 경계 | 편집 정책 | 생성 주체 | +|---|---|---|---|---|---|---| +| `scripts/streamlit_browser_evidence.py` | verification tool | synthetic DB와 runtime 생성 인증 설정으로 실제 HTTPS Dashboard 전환의 frame/DOM/document/shell 관찰을 수집하고 redacted manifest를 검증 | `--mode transition` | 운영 DB·credential 사용 금지; 성공 증거는 browser/server teardown 뒤에만 확정 | source review and focused test required | developer | +| `docs/evidence/issue-9-dashboard-menu-transition/` | evidence artifact | exact source hash에 결합한 authenticated browser frame, screenshot, runtime, validator 결과 | `manifest.json` | synthetic fixture only; credential/token/cookie 값 금지 | immutable after verification except complete regeneration | verification runner | + ### deployment-docs — 운영 문서 ```text docs/requirements/req-001_dashboard-administrator-authentication.md +docs/requirements/req-002_dashboard-menu-transition.md +docs/evidence/issue-9-dashboard-menu-transition/ docs/interface-contract.md docs/runtime-flow.md README.md @@ -58,6 +71,7 @@ deploy/k8s/README.md | 경로 | 종류 | 책임 | 공개 진입점 | 내부 구현 경계 | 편집 정책 | 생성 주체 | |---|---|---|---|---|---|---| | `docs/requirements/req-001_dashboard-administrator-authentication.md` | requirement | approved REQ-001 revision 2.0 | REQ-001 | canonical traceability | update in place | developer | +| `docs/requirements/req-002_dashboard-menu-transition.md` | requirement | approved REQ-002 revision 1.0 | REQ-002 | canonical traceability | update in place | developer | | `docs/interface-contract.md` | contract | HTTP/cookie/gate contracts | contract IDs | no implementation secrets | update with API changes | developer | | `docs/runtime-flow.md` | design | runtime states/transitions | flow IDs | no secret values | update with flow changes | developer | | `README.md` | operations | local/deployment constraints | operator commands | no secret values | operational review | developer | @@ -71,15 +85,16 @@ deploy/k8s/README.md | `dashboard.server` | `dashboard.app` | 허용 | app remains protected by gate | `docs/runtime-flow.md` | | `dashboard.app` | `dashboard.authentication` | 허용 | gate precedes DB/page dispatch | `docs/interface-contract.md` | | `dashboard.authentication` | `dashboard.page_views` | 금지 | auth module never imports financial pages | `docs/project-structure.md` | +| `dashboard.app` | `dashboard.common` | 허용 | menu/query/body transition helpers only after auth gate | `docs/interface-contract.md` | | `dashboard.auth_server` | `jarca_ledger_mcp` DB | 금지 | login/cleanup/logout are DB-free | `docs/requirements/req-001_dashboard-administrator-authentication.md` | ## 변경 규칙 | 구분 | 규칙 | |---|---| -| 새 모듈·파일 배치 | auth route/gate changes stay in `dashboard/`; no DB session module | +| 새 모듈·파일 배치 | auth route/gate changes stay in `dashboard/`; menu transition stays in existing `dashboard.app`/`dashboard.common`; no DB session module | | 이름·패키지 | use existing `dashboard` package and snake_case modules | -| 소스·테스트 대응 | auth source behavior requires focused pytest/AppTest coverage | +| 소스·테스트 대응 | auth source behavior requires focused pytest/AppTest coverage; REQ-002 transition behavior requires focused pytest/AppTest targets and real-browser evidence where specified | | 생성 산출물 | evidence may be redacted; never commit credentials, secrets, tokens, or cookie values | ## 검증 @@ -98,4 +113,4 @@ deploy/k8s/README.md ``` - 정적 분석 기대 결과: exit 0 with no diagnostics -- 문서와 실제 트리의 정합성: every listed source/test/document path exists; dependency table has no forbidden reverse edge +- 문서와 실제 트리의 정합성: every listed source/test/document path exists; dependency table has no forbidden reverse edge; REQ-002 symbols resolve to `dashboard/app.py`, `dashboard/common.py`, and named tests diff --git a/docs/requirements/req-002_dashboard-menu-transition.md b/docs/requirements/req-002_dashboard-menu-transition.md new file mode 100644 index 0000000..af32080 --- /dev/null +++ b/docs/requirements/req-002_dashboard-menu-transition.md @@ -0,0 +1,258 @@ +--- +{ + "schema": "gitea-agent.requirement/v1", + "id": "REQ-002", + "title": "Dashboard 메뉴 전환 시 공통 화면 연속성", + "revision": "1.0", + "status": "approved", + "depends_on": ["REQ-001"], + "applies_to": ["dashboard"], + "language": "ko" +} +--- + +# REQ-002 — Dashboard 메뉴 전환 시 공통 화면 연속성 + +## 1. 목적 + +인증된 Dashboard에서 메뉴를 바꿀 때 문서 전체를 이동시키지 않고 공통 shell과 배경을 유지하며 body만 교체한다. 현재 메뉴 선택, URL 상태, 느린 렌더링, 렌더링 실패의 동작을 예측 가능하게 만든다. + +## 2. 배경 + +Issue #9의 Dashboard 메뉴 전환 계약이다. REQ-001의 인증 경계, cookie, logout, page dispatch와 기존 금융 페이지 계약을 유지한 채 Streamlit 메뉴 전환을 연속적인 단일 화면으로 제공한다. + +## 3. 범위 + +### 포함 + +- Streamlit `st.radio` 기반 메뉴의 정확한 10개 label/order와 selected state +- 메뉴 클릭 시 document navigation 없이 공통 shell/background를 유지하고 body만 교체 +- 느린 대상 page 렌더링 중 대상별 loading 표시 +- renderer 예외의 local generic error와 retry +- 빠른 연속 선택에서 latest serialized selection wins +- 직접 `?page=` 접근, 누락/unknown page의 `요약` fallback, sibling query key 보존 +- 기존 auth gate, cookie, logout, finance page, cache-clear 회귀 검증 + +### 제외 + +- browser history/back-forward 동작, history API 계약, path URL 설계 +- 인증 방식·cookie 속성·MCP transport·SQLite schema 변경 +- page renderer의 금융 계산·gateway 의미 변경 +- 별도 document route 또는 full-document navigation 구현 + +## 4. 용어와 기준 + +- **공통 shell**: 인증 후 `dashboard/app.py`가 제공하는 sidebar, 메뉴, refresh control 및 앱 배경/CSS. +- **body**: 선택된 page renderer가 공통 shell 안에서 채우는 주 콘텐츠 영역. +- **latest serialized selection**: Streamlit rerun/콜백 직렬화 순서에서 마지막으로 반영된 `dashboard_page` 선택. +- **AppTest**: Streamlit `streamlit.testing.v1.AppTest`의 synthetic runtime 검증. DOM, 실제 browser document navigation, 실제 paint/지연 체감은 검증하지 못한다. +- **real-browser evidence**: 실제 browser에서 authenticated session으로 메뉴를 클릭하고 URL/document/body/shell을 관찰해 남기는 증거. AppTest를 대체하지 않으며 AppTest만으로 충족할 수 없는 browser-level 항목에 필수다. + +## 5. 요구사항 + +### REQ-002.01 — 메뉴 정체성 + +메뉴는 정확히 다음 순서와 label을 유지한다: `월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`. selected state는 현재 page와 일치한다. + +### REQ-002.02 — URL과 직접 접근 + +메뉴 선택은 query의 `page`만 대상 label로 갱신하고 `month`, `account_id`, `filter` 등 sibling query key를 보존한다. 직접 `?page=`은 해당 page를 렌더링하고, missing/unknown 값은 `요약`으로 fallback한다. + +### REQ-002.03 — 연속 화면 + +메뉴 클릭은 document navigation을 일으키지 않는다. 공통 shell과 background는 유지하고 body만 선택된 renderer의 내용으로 교체한다. history/back-forward와 path URL은 이 요구사항의 판정 범위가 아니다. + +### REQ-002.04 — 전환 중 표시 + +느린 renderer가 실행되는 동안 target page label을 포함한 `불러오는 중…` loading을 대상 body 영역에 표시하고, renderer 완료 후 loading placeholder를 제거한다. + +### REQ-002.05 — local failure와 retry + +target renderer 예외는 공통 shell을 무너뜨리지 않고 body 영역을 generic error와 `다시 시도` control로 교체한다. 예외 상세·secret은 UI에 노출하지 않는다. + +### REQ-002.06 — 최신 선택 우선 + +연속 메뉴 선택은 serialized callback 순서의 마지막 선택을 query와 body에 반영하며 이전 선택이 마지막 상태를 덮어쓰지 않는다. + +### REQ-002.07 — 인증 경계 보존 + +모든 menu/page dispatch는 REQ-001 gate와 cookie 검증 뒤에만 실행된다. no-cookie/invalid-cookie는 page body에 도달하지 않으며 valid cookie는 기존 Dashboard에 접근한다. + +### REQ-002.08 — 금융 page 회귀 없음 + +요약, 자산/마감, 분류 규칙, 확인필요 큐, 월정산의 기존 synthetic fixture DB 핵심 내용과 기존 page query 계약을 유지한다. + +### REQ-002.09 — 운영 control 회귀 없음 + +refresh control은 기존 `clear_cache` callable 계약을 유지하고, 메뉴 전환은 cache-clear 동작을 변경하지 않는다. + +## 6. 제약과 불변조건 + +- `dashboard/app.py`의 auth gate와 page dispatch는 유지하며 인증 전에 DB path를 resolve하지 않는다. +- 메뉴 클릭은 full document reload 또는 path navigation을 추가하지 않는다. +- body 교체는 stable `st.empty` container 안에서 수행하며 공통 shell/background를 삭제하지 않는다. +- loading/error/retry는 target body에 국소적으로 표시하고 generic error 외 renderer exception 내용을 공개하지 않는다. +- query update는 `page` key만 변경하며 sibling page-specific key를 삭제하거나 정규화하지 않는다. +- REQ-001의 auth/cookie/logout, financial gateway/page, cache-clear, MCP 및 SQLite semantics는 변경하지 않는다. + +## 7. 인수 기준 + +각 기준은 하나의 명시된 test/evidence target으로 판정한다. AppTest는 synthetic runtime 판정이며 real-browser evidence가 필요한 기준을 대신하지 않는다. + +### REQ-002.01-AC01 — label/order/selected + +**판정 기준** + +정확한 10개 label과 순서가 radio options로 전달되고 `dashboard_page` selected state가 유지된다. + +**검증** + +`tests/test_dashboard_menu_transition.py::test_dashboard_menu_labels_and_order_are_preserved` + +**검증 분류** + +- 수준: unit +- 백엔드: pytest monkeypatch +- 증거: radio options, key, label visibility assertion + +### REQ-002.02-AC01 — query와 fallback + +**판정 기준** + +callback은 `page`만 갱신하고 sibling key를 보존하며, missing/unknown은 `요약`, known direct query는 요청 page를 선택한다. + +**검증** + +`tests/test_dashboard_menu_transition.py::test_sidebar_selection_updates_only_page_query_parameter`, `tests/test_dashboard_menu_transition.py::test_missing_or_unknown_page_falls_back_to_overview`, `tests/test_dashboard_menu_transition.py::test_direct_page_query_access_renders_requested_page` + +**검증 분류** + +- 수준: unit/integration +- 백엔드: pytest monkeypatch +- 증거: exact query mapping and selected renderer assertion + +### REQ-002.03-AC01 — body-only transition + +**판정 기준** + +실제 browser에서 authenticated menu click 후 document URL path와 full-document identity가 바뀌지 않고 공통 sidebar/background가 남으며 target body만 바뀐다. history/back-forward와 path URL은 판정하지 않는다. + +**검증** + +Required real-browser evidence: `docs/evidence/issue-9-dashboard-menu-transition/`의 authenticated `요약` → `분류 규칙` native click 연속 frame, screenshot, DOM, URL, document/sidebar identity와 background 관찰. `manifest.json`은 정확한 working-tree file hash, runtime viewport·interval·환경·제약, artifact hash 및 validator exit code를 기록한다. 이 실행은 local Chrome 부재 때문에 Playwright MCP browser runner로 수집했으며, 재현 가능한 local fail-closed harness는 `scripts/streamlit_browser_evidence.py --mode transition`과 `tests/test_streamlit_browser_evidence.py`로 유지한다. AppTest companion: `tests/test_dashboard_menu_transition.py::test_streamlit_menu_click_updates_body_and_preserves_page_query_siblings`. + +**검증 분류** + +- 수준: browser evidence + integration companion +- 백엔드: real browser, Streamlit AppTest +- 한계: AppTest는 실제 document navigation, browser paint, focus/scroll, network request, perceived continuity를 증명하지 못한다. + +### REQ-002.04-AC01 — target loading + +**판정 기준** + +slow renderer 동안 target label loading이 먼저 표시되고 완료 후 placeholder가 제거된다. + +**검증** + +`tests/test_dashboard_menu_transition.py::test_slow_transition_clears_previous_body_and_shows_target_loading`; real-browser frames: `docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json` + +**검증 분류** + +- 수준: unit + browser evidence +- 백엔드: pytest fake Streamlit placeholder, authenticated Playwright browser +- 증거: ordered event list와 50 ms 연속 frame에서 `loading=true`, `previous_body_visible=false`, 최종 `loading=false` +- 한계: browser evidence의 2초 지연은 repository source가 아닌 fixture-only wrapper로 주입한다. + +### REQ-002.05-AC01 — local error/retry + +**판정 기준** + +renderer 예외가 generic error/retry body로 국소 대체되고 예외 상세가 노출되지 않는다. + +**검증** + +`tests/test_dashboard_menu_transition.py::test_body_error_is_local_and_retry_rerenders_target` + +**검증 분류** + +- 수준: unit +- 백엔드: pytest monkeypatch +- 증거: error text, retry event, secret-detail absence assertion + +### REQ-002.06-AC01 — latest selection + +**판정 기준** + +연속 선택 `거래내역` → `자산/마감` → `Import` 후 query가 마지막 `Import`이고 sibling key가 보존된다. + +**검증** + +`tests/test_dashboard_menu_transition.py::test_rapid_menu_clicks_commit_latest_selection_only` + +**검증 분류** + +- 수준: unit +- 백엔드: pytest monkeypatch +- 증거: final query mapping + +### REQ-002.07-AC01 — auth regression + +**판정 기준** + +valid cookie가 기존 Dashboard를 열고 cookie가 session state에 저장되지 않으며 gate rejection은 DB/page dispatch 전에 종료된다. + +**검증** + +`tests/test_dashboard_authentication_app.py::test_valid_cookie_allows_dashboard_and_is_not_stored_in_session_state`, `tests/test_dashboard_authentication_app.py::test_main_does_not_resolve_database_or_dispatch_when_authentication_rejects` + +**검증 분류** + +- 수준: integration regression +- 백엔드: Streamlit AppTest, synthetic fixture DB +- 증거: authenticated body and pre-gate forbidden DB sentinel + +### REQ-002.08-AC01 — finance page regression + +**판정 기준** + +기존 핵심 금융 page의 expected text와 synthetic fixture DB 결과가 유지된다. + +**검증** + +`tests/test_dashboard_app.py::test_streamlit_apptest_core_pages_use_synthetic_fixture_db` + +**검증 분류** + +- 수준: regression integration +- 백엔드: Streamlit AppTest, synthetic SQLite +- 증거: page expectation and metric assertions + +### REQ-002.09-AC01 — cache-clear regression + +**판정 기준** + +refresh control이 callable `clear_cache` 계약을 유지한다. + +**검증** + +`tests/test_dashboard_menu_transition.py::test_refresh_control_preserves_cache_clear_contract` + +**검증 분류** + +- 수준: regression unit +- 백엔드: pytest +- 증거: callable/name assertion + +## 8. 오류 및 충돌 계약 + +인증 실패는 항상 REQ-001 gate가 우선하며 메뉴·query로 우회할 수 없다. renderer 실패는 해당 body만 generic error/retry로 대체한다. 빠른 선택은 Streamlit serialized callback 순서를 따르며 마지막 serialized selection이 최종 상태다. history/back-forward와 path URL은 요구하지 않는다. + +## 9. 추적성 + +- 근거 이슈: `#9` +- 구현 PR: 미정 +- 관련 요구사항: `REQ-001` +- 구현 진입점: `dashboard/app.py::main`, `dashboard/common.py::render_sidebar_nav`, `dashboard/common.py::validate_dashboard_page`, `dashboard/common.py::render_dashboard_body`, `dashboard/common.py::_sync_dashboard_page_query` +- 검증 파일: `tests/test_dashboard_menu_transition.py`, `tests/test_streamlit_browser_evidence.py`, `tests/test_dashboard_app.py`, `tests/test_dashboard_authentication_app.py` +- browser evidence 도구: `scripts/streamlit_browser_evidence.py --mode transition` diff --git a/docs/runtime-flow.md b/docs/runtime-flow.md index 63f9049..292e5e9 100644 --- a/docs/runtime-flow.md +++ b/docs/runtime-flow.md @@ -112,3 +112,33 @@ ASGI startup부터 login, token restoration, invalid-cookie cleanup, explicit lo - 경계 흐름: TTL boundary, rotation, oversized token, same-host different-port origin - 동시성 흐름: independent failed submissions and no application lock; ingress rate-limit configuration review - 운영 진입점과 기대 결과: `uvicorn dashboard.server:app --host 0.0.0.0 --port 8501`; valid HTTPS reaches login or authenticated page, invalid cookie receives matching deletion before unauthenticated page + +## 메뉴 전환 — REQ-002 + +인증 gate와 기존 `dashboard-01` page dispatch의 선행 순서는 변하지 않는다. valid cookie를 통과한 뒤에만 menu transition flow가 실행되며, auth 실패는 기존 cleanup/unauthenticated 흐름으로 종료된다. + +### transition — authenticated body replacement + +- 시작 이벤트: authenticated browser selects a value in `st.radio(key="dashboard_page")` +- 선행조건: `gate-02` success; current page/query resolved +- 실행 방식: Streamlit callback and rerun, no document/path navigation +- 트랜잭션·영속성 경계: no new persistence; existing financial SQLite rules unchanged + +| 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | +|---|---|---|---|---|---|---|---| +| `transition-01` | dashboard app/common | authenticated selected menu | `render_sidebar_nav` reads exact 10 options and selected state | session state `dashboard_page` | Streamlit radio callback | `transition-02` | `dashboard-fail` | +| `transition-02` | common callback | selected `dashboard_page` | write query `page` only; preserve sibling query keys | browser query state | Streamlit query params | `transition-03` | `transition-03` | +| `transition-03` | dashboard app | validated page | open stable `st.empty` body, show target loading, invoke target renderer | body placeholder only; existing DB reads/writes | page/gateway code | `transition-04` | `transition-error` | +| `transition-04` | dashboard app | renderer completed | remove target loading and leave target body; shell/background remain | body replacement only | none | `authenticated` | `dashboard-fail` | +| `transition-error` | dashboard app | renderer exception | clear partial body and show generic error/retry locally | body error state only | none | retry reruns target | `authenticated` with shell intact | + +### transition invariants + +- known direct `?page=` selects that page; missing/unknown selects `요약`. +- serialized rapid selections commit the latest selection only. +- menu transition never runs before auth and never changes cookie/logout/DB schema/MCP semantics. +- history/back-forward and path URL behavior are explicitly outside this flow. + +### transition verification boundary + +`tests/test_dashboard_menu_transition.py` provides exact AppTest/unit evidence for labels, query preservation, fallback/direct access, loading, local error, latest selection, and cache-clear. `tests/test_dashboard_app.py::test_streamlit_apptest_core_pages_use_synthetic_fixture_db` and `tests/test_dashboard_authentication_app.py::test_valid_cookie_allows_dashboard_and_is_not_stored_in_session_state` cover finance/auth regressions. AppTest is synthetic and cannot establish actual browser document continuity, paint timing, focus/scroll, or network behavior; `scripts/streamlit_browser_evidence.py --mode transition`의 authenticated HTTPS frame/DOM/document/shell evidence가 REQ-002.03-AC01에 필수다. `tests/test_streamlit_browser_evidence.py`는 이 manifest가 실제 teardown 이후에만 성공하고 document navigation, shell identity drift, 누락 관찰 또는 secret 포함 시 fail-closed하는지 검증한다. diff --git a/scripts/streamlit_browser_evidence.py b/scripts/streamlit_browser_evidence.py index 1ca9eed..b3ae62a 100755 --- a/scripts/streamlit_browser_evidence.py +++ b/scripts/streamlit_browser_evidence.py @@ -14,10 +14,15 @@ import asyncio import base64 import json import os +import platform import shutil import signal import subprocess +import sys import time +import hashlib +import secrets +import ssl import urllib.parse import urllib.request from datetime import datetime @@ -44,6 +49,13 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--output-dir", default="", help="Evidence output directory. Defaults under docs/batches/BAT-004/evidence/run-.") parser.add_argument("--page", action="append", dest="pages", help="Page to capture. Repeatable. Defaults to core E2E evidence pages.") parser.add_argument("--keep-server", action="store_true", help="Leave Streamlit running after capture for manual inspection.") + parser.add_argument("--mode", choices=("capture", "transition"), default="capture", help="Capture mode") + parser.add_argument("--cert-file", default="", help="TLS certificate for transition mode") + parser.add_argument("--key-file", default="", help="TLS private key for transition mode") + parser.add_argument("--from-page", default="요약") + parser.add_argument("--to-page", default="분류 규칙") + parser.add_argument("--interval-ms", type=int, default=100) + parser.add_argument("--frames", type=int, default=8) return parser.parse_args() @@ -145,12 +157,13 @@ def seed_fixture_db(db_path: Path) -> dict[str, Any]: return {"account_id": account_id, "transaction_ids": transactions, "db_path": str(db_path)} -def wait_for_http(url: str, timeout_seconds: int = 45) -> None: +def wait_for_http(url: str, timeout_seconds: int = 45, ssl_context: ssl.SSLContext | None = None) -> None: + """Wait for HTTP readiness, using verified TLS unless explicitly overridden.""" deadline = time.time() + timeout_seconds last_error: str | None = None while time.time() < deadline: try: - with urllib.request.urlopen(url, timeout=2) as response: + with urllib.request.urlopen(url, timeout=2, context=ssl_context) as response: if response.status == 200: return last_error = f"HTTP {response.status}" @@ -195,6 +208,7 @@ class CdpClient: def __init__(self, ws: websockets.ClientConnection) -> None: self.ws = ws self.next_id = 1 + self.events: list[dict[str, Any]] = [] async def call(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]: command_id = self.next_id @@ -207,6 +221,8 @@ class CdpClient: if "error" in payload: raise RuntimeError(f"CDP {method} failed: {payload['error']}") return payload.get("result", {}) + if payload.get("method"): + self.events.append(payload) async def cdp_capture(ws_url: str, url: str, expected_text: str, screenshot: Path, snapshot: Path) -> dict[str, Any]: @@ -295,10 +311,196 @@ def capture_page(chrome: str, base_url: str, page: str, output_dir: Path, devtoo } +def redact_secrets(value: Any, secrets_to_redact: tuple[str, ...] = ()) -> Any: + """Return JSON-safe evidence with credential-like values removed.""" + if isinstance(value, dict): + return {key: redact_secrets(item, secrets_to_redact) for key, item in value.items()} + if isinstance(value, list): + return [redact_secrets(item, secrets_to_redact) for item in value] + if isinstance(value, str): + result = value + for secret in secrets_to_redact: + if secret: + result = result.replace(secret, "[REDACTED]") + return result + return value + + +def validate_transition_manifest(manifest: dict[str, Any], secrets_to_redact: tuple[str, ...] = ()) -> None: + """Fail closed unless transition evidence contains every required observation.""" + serialized = json.dumps(manifest, ensure_ascii=False) + if any(secret and secret in serialized for secret in secrets_to_redact): + raise ValueError("secret value present in transition manifest") + if manifest.get("success") is not True: + raise ValueError("transition manifest is not successful") + observations = manifest.get("observations") or {} + frames = observations.get("frames") + if not isinstance(frames, list) or len(frames) < 2: + raise ValueError("transition observations require at least two frames") + document_events = observations.get("document_navigation_events") + if not isinstance(document_events, list) or document_events: + raise ValueError("transition observations require zero document navigation events") + shell = observations.get("stable_shell") or {} + if not all(isinstance(shell.get(name), dict) for name in ("document_marker", "sidebar_marker", "background")): + raise ValueError("transition observations require pre/post stable shell markers") + if any(values.get("pre") != values.get("post") for values in (shell["document_marker"], shell["sidebar_marker"], shell["background"])): + raise ValueError("transition observations require stable shell identity and background") + if observations.get("prior_body_removed") is not True: + raise ValueError("transition observations require prior body removal") + loading_frames = [frame for frame in frames if frame.get("loading") is True] + if observations.get("loading_observed") is not True or not loading_frames: + raise ValueError("transition observations require observed loading state") + if any(frame.get("prior_body_visible") is not False for frame in loading_frames): + raise ValueError("transition observations require prior body absence while loading") + if not observations.get("target_page"): + raise ValueError("transition observations require target page") + if observations.get("final_agreement") is not True: + raise ValueError("transition observations require target body agreement") + runtime = manifest.get("runtime") or {} + if not runtime.get("viewport") or not isinstance(runtime.get("interval_ms"), int): + raise ValueError("transition manifest requires viewport and interval") + command = manifest.get("command") or {} + if command.get("exit_code") != 0 or command.get("teardown") != "complete": + raise ValueError("transition manifest requires successful command teardown") + source_revision = manifest.get("source_revision") + if not isinstance(source_revision, str) or len(source_revision) != 40: + raise ValueError("transition manifest requires full source revision") + source_files = manifest.get("source_files") + if not isinstance(source_files, dict) or not source_files or any( + not isinstance(path, str) + or not isinstance(digest, str) + or len(digest) != 64 + for path, digest in source_files.items() + ): + raise ValueError("transition manifest requires source files and SHA-256 digests") + source_paths = tuple(source_files) + if manifest.get("source_revision_kind") != "git-head-base": + raise ValueError("transition manifest requires git-head-base revision kind") + if source_revision != current_git_revision(): + raise ValueError("transition manifest source revision does not match current Git HEAD") + if source_files != source_file_digests(source_paths): + raise ValueError("transition manifest source file digests do not match current files") + if manifest.get("source_worktree_dirty") != git_worktree_dirty(source_paths): + raise ValueError("transition manifest working-tree state does not match current files") + if not isinstance(manifest.get("evidence_command"), list) or not manifest["evidence_command"]: + raise ValueError("transition manifest requires evidence command") + if not runtime.get("host_architecture") or not runtime.get("browser_user_agent"): + raise ValueError("transition manifest requires runtime identity") + + +def current_git_revision() -> str: + return subprocess.run( + ["git", "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def git_worktree_dirty(paths: tuple[str, ...]) -> bool: + result = subprocess.run( + ["git", "status", "--porcelain", "--", *paths], + check=True, + capture_output=True, + text=True, + ) + return bool(result.stdout.strip()) + + +def source_file_digests(paths: tuple[str, ...]) -> dict[str, str]: + for path in paths: + candidate = Path(path) + if candidate.is_absolute() or ".." in candidate.parts: + raise ValueError("source file paths must be repository-relative") + return {path: hashlib.sha256(Path(path).read_bytes()).hexdigest() for path in paths} + + +async def cdp_transition(ws_url: str, base_url: str, from_page: str, to_page: str, + screenshot: Path, snapshot: Path, interval_ms: int, frame_count: int, + admin_id: str, admin_password: str) -> dict[str, Any]: + async with websockets.connect(ws_url, max_size=16 * 1024 * 1024) as ws: + cdp = CdpClient(ws) + await cdp.call("Page.enable") + await cdp.call("Runtime.enable") + await cdp.call("Emulation.setDeviceMetricsOverride", {"width": 1440, "height": 1400, "deviceScaleFactor": 1, "mobile": False}) + await cdp.call("Page.navigate", {"url": base_url}) + await asyncio.sleep(1) + login_expression = """(async () => { + const body = new URLSearchParams({admin_id: %s, admin_password: %s}); + const response = await fetch('/api/auth/login', {method:'POST', credentials:'include', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body}); + return {status: response.status, location: response.url}; + })()""" % (json.dumps(admin_id), json.dumps(admin_password)) + await cdp.call("Runtime.evaluate", {"expression": login_expression, "awaitPromise": True, "returnByValue": True}) + await cdp.call("Page.navigate", {"url": f"{base_url}/?page={urllib.parse.quote(from_page)}"}) + deadline = time.time() + 45 + while time.time() < deadline: + check = await cdp.call("Runtime.evaluate", {"expression": "document.body ? document.body.innerText : ''", "returnByValue": True}) + if from_page in (check.get("result", {}).get("value", "") or ""): + break + await asyncio.sleep(.5) + install = """(() => { + window.__evidenceEvents=[]; + window.__evidenceDocumentMarker ||= crypto.randomUUID(); + window.__evidencePriorBodyText = (document.querySelector('[data-testid=stMain]') || document.body)?.innerText || ''; + const sidebar=document.querySelector('[data-testid=stSidebar]'); + if (sidebar) sidebar.dataset.evidenceMarker ||= crypto.randomUUID(); + for (const name of ['pushState','replaceState']) { + const old=history[name]; + history[name]=function(){window.__evidenceEvents.push({type:name,url:String(arguments[2]||location.href)}); return old.apply(this,arguments)}; + } + })()""" + await cdp.call("Runtime.evaluate", {"expression": install}) + cdp.events.clear() + observations: list[dict[str, Any]] = [] + click = """(() => { const labels=[...document.querySelectorAll('label')]; const target=labels.find(x=>x.innerText.trim()===%s); if(target){target.click(); return true;} const input=[...document.querySelectorAll('input')].find(x=>x.value===%s); if(input){input.click(); return true;} return false; })()""" % (json.dumps(to_page), json.dumps(to_page)) + deadline = time.time() + 45 + index = 0 + while time.time() < deadline and (index < frame_count or not observations or observations[-1].get("selected_menu") != to_page): + expression = """(() => { const body=document.body; const text=body?.innerText||''; const main=document.querySelector('[data-testid=stMain]')||body; const mainText=main.innerText||''; const sidebar=document.querySelector('[data-testid=stSidebar]'); const selectedInput=document.querySelector('[data-testid=stRadio] input:checked'); const selected=selectedInput?.closest('label')?.innerText?.trim() || document.querySelector('[aria-current=page]')?.textContent?.trim() || ''; const prior=window.__evidencePriorBodyText||''; return {at_ms:%d, selected_menu:selected, body_text:mainText, prior_body_visible:Boolean(prior)&&mainText.includes(prior), document_marker:window.__evidenceDocumentMarker||'', sidebar_marker:sidebar?.dataset.evidenceMarker||'', background:getComputedStyle(document.body).backgroundColor, loading:/불러오는 중/.test(text)}; })()""" % (index * interval_ms) + result = await cdp.call("Runtime.evaluate", {"expression": expression, "returnByValue": True}) + frame = result.get("result", {}).get("value", {}) + frame["body_text_hash"] = hashlib.sha256(frame.get("body_text", "").encode()).hexdigest() + frame.pop("body_text", None) + observations.append(frame) + if index == 0: + await cdp.call("Runtime.evaluate", {"expression": click, "returnByValue": True}) + index += 1 + await asyncio.sleep(interval_ms / 1000) + final = await cdp.call("Runtime.evaluate", {"expression": "(() => { const main=document.querySelector('[data-testid=stMain]')||document.body; const sidebar=document.querySelector('[data-testid=stSidebar]'); const selectedInput=document.querySelector('[data-testid=stRadio] input:checked'); return {url:location.href, body:main.innerText||'', selected:selectedInput?.closest('label')?.innerText?.trim() || document.querySelector('[aria-current=page]')?.textContent?.trim() || '', document_marker:window.__evidenceDocumentMarker||'', sidebar_marker:sidebar?.dataset.evidenceMarker||'', background:getComputedStyle(document.body).backgroundColor, history_events:window.__evidenceEvents||[], browser_user_agent:navigator.userAgent}; })()", "returnByValue": True}) + data = final.get("result", {}).get("value", {}) + html = await cdp.call("Runtime.evaluate", {"expression": "document.documentElement.outerHTML", "returnByValue": True}) + html_text = html.get("result", {}).get("value", "") + snapshot.write_text(redact_secrets(html_text, (admin_id, admin_password)), encoding="utf-8") + image = await cdp.call("Page.captureScreenshot", {"format":"png", "fromSurface":True, "captureBeyondViewport":True}) + screenshot.write_bytes(base64.b64decode(image["data"])) + document_events = [{"type": event.get("method"), "params": event.get("params", {})} for event in cdp.events if event.get("method") in ("Page.frameNavigated", "Page.loadEventFired")] + pre = observations[0] if observations else {} + loading_frames = [frame for frame in observations if frame.get("loading")] + return {"frames": observations, "document_navigation_events": document_events, "history_events": data.get("history_events", []), "stable_shell": {"document_marker": {"pre": pre.get("document_marker"), "post": data.get("document_marker")}, "sidebar_marker": {"pre": pre.get("sidebar_marker"), "post": data.get("sidebar_marker")}, "background": {"pre": pre.get("background"), "post": data.get("background")}}, "loading_observed": bool(loading_frames), "prior_body_removed": bool(loading_frames) and all(frame.get("prior_body_visible") is False for frame in loading_frames), "target_page": to_page, "final_agreement": data.get("selected") == to_page and to_page in data.get("body", ""), "final_url": data.get("url", ""), "browser_user_agent": data.get("browser_user_agent", "")} + + +def transition_capture(chrome: str, base_url: str, from_page: str, to_page: str, output_dir: Path, devtools_port: int, + interval_ms: int, frame_count: int, + admin_id: str, admin_password: str) -> dict[str, Any]: + profile_dir = output_dir / "chrome-profile-transition" + screenshot, snapshot = output_dir / "transition.png", output_dir / "transition.dom.html" + chrome_cmd = [chrome, "--headless=new", "--no-sandbox", "--disable-dev-shm-usage", "--ignore-certificate-errors", f"--remote-debugging-port={devtools_port}", f"--user-data-dir={profile_dir}", "--window-size=1440,1400", "about:blank"] + proc = subprocess.Popen(chrome_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + try: + ws_url = wait_for_devtools(devtools_port) + return asyncio.run(cdp_transition(ws_url, base_url, from_page, to_page, screenshot, snapshot, interval_ms, frame_count, admin_id, admin_password)) + finally: + if proc.poll() is None: + proc.terminate() + try: proc.wait(timeout=8) + except subprocess.TimeoutExpired: proc.kill(); proc.wait(timeout=5) + remove_tree_with_retries(profile_dir) + + def main() -> int: args = parse_args() chrome = require_executable(args.chrome, "Chrome") - streamlit = require_executable(args.streamlit, "Streamlit") + streamlit = None if args.mode == "transition" else require_executable(args.streamlit, "Streamlit") run_id = datetime.now().strftime("run-%Y%m%d-%H%M%S") output_dir = Path(args.output_dir) if args.output_dir else DEFAULT_OUTPUT_ROOT / run_id output_dir.mkdir(parents=True, exist_ok=True) @@ -308,56 +510,105 @@ def main() -> int: env = os.environ.copy() env["JARCA_LEDGER_DB_PATH"] = str(db_path.resolve()) env["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false" - base_url = f"http://{args.host}:{args.port}" - server_cmd = [ - streamlit, - "run", - "dashboard/app.py", - "--server.address", - args.host, - "--server.port", - str(args.port), - "--server.headless", - "true", - "--browser.gatherUsageStats", - "false", - ] + transition_secrets: tuple[str, ...] = () + if args.mode == "transition": + if not args.cert_file or not args.key_file: + raise SystemExit("transition mode requires --cert-file and --key-file") + env["JARCA_DASHBOARD_ADMIN_ID"] = secrets.token_urlsafe(18) + env["JARCA_DASHBOARD_ADMIN_PASSWORD"] = secrets.token_urlsafe(32) + env["JARCA_DASHBOARD_SIGNING_SECRET"] = secrets.token_hex(32) + transition_secrets = (env["JARCA_DASHBOARD_ADMIN_ID"], env["JARCA_DASHBOARD_ADMIN_PASSWORD"], env["JARCA_DASHBOARD_SIGNING_SECRET"]) + scheme = "https" if args.mode == "transition" else "http" + base_url = f"{scheme}://{args.host}:{args.port}" + if args.mode == "transition": + server_cmd = [sys.executable, "-m", "uvicorn", "dashboard.server:app", "--host", args.host, "--port", str(args.port), "--ssl-certfile", args.cert_file, "--ssl-keyfile", args.key_file] + else: + server_cmd = [ + streamlit, "run", "dashboard/app.py", "--server.address", args.host, + "--server.port", str(args.port), "--server.headless", "true", + "--browser.gatherUsageStats", "false", + ] server = subprocess.Popen(server_cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, start_new_session=True) manifest: dict[str, Any] = { "success": False, + "mode": args.mode, "base_url": base_url, "output_dir": str(output_dir.resolve()), - "fixture": fixture, + "fixture": {"db_path": fixture["db_path"]}, "chrome": chrome, "streamlit": streamlit, "pages": [], "server_command": server_cmd, } + error: str | None = None try: - wait_for_http(base_url) - for index, page in enumerate(args.pages or DEFAULT_PAGES): - manifest["pages"].append(capture_page(chrome, base_url, page, output_dir, devtools_port=19222 + index)) - manifest["success"] = True - print(json.dumps(manifest, ensure_ascii=False, indent=2)) - return 0 + wait_for_http(base_url, ssl_context=ssl._create_unverified_context() if args.mode == "transition" else None) + if args.mode == "transition": + observations = transition_capture(chrome, base_url, args.from_page, args.to_page, output_dir, 19222, args.interval_ms, args.frames, env["JARCA_DASHBOARD_ADMIN_ID"], env["JARCA_DASHBOARD_ADMIN_PASSWORD"]) + source_paths = ( + "dashboard/app.py", + "dashboard/common.py", + "scripts/streamlit_browser_evidence.py", + ) + manifest.update( + { + "observations": observations, + "runtime": { + "viewport": {"width": 1440, "height": 1400}, + "interval_ms": args.interval_ms, + "frame_count": args.frames, + "host_architecture": platform.machine(), + "python_version": platform.python_version(), + "browser_user_agent": observations.get("browser_user_agent", ""), + }, + "source_revision": current_git_revision(), + "source_revision_kind": "git-head-base", + "source_worktree_dirty": git_worktree_dirty(source_paths), + "source_files": source_file_digests(source_paths), + "evidence_command": ["scripts/streamlit_browser_evidence.py", *sys.argv[1:]], + "limitations": ["TLS uses caller-provided local certificate"], + } + ) + else: + for index, page in enumerate(args.pages or DEFAULT_PAGES): + manifest["pages"].append(capture_page(chrome, base_url, page, output_dir, devtools_port=19222 + index)) + except Exception as exc: # noqa: BLE001 - failure is recorded in redacted evidence + error = str(exc) finally: - log_tail = "" - if server.stdout is not None: - try: - time.sleep(0.5) - if server.poll() is not None: - log_tail = server.stdout.read()[-4000:] - except Exception: - log_tail = "" - manifest["server_log_tail"] = log_tail - (output_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") if not args.keep_server and server.poll() is None: os.killpg(server.pid, signal.SIGTERM) try: server.wait(timeout=10) except subprocess.TimeoutExpired: os.killpg(server.pid, signal.SIGKILL) + server.wait(timeout=5) + returncode = server.poll() + teardown = "complete" if not args.keep_server and returncode is not None else "incomplete" + effective_exit = 0 if returncode in (0, -signal.SIGTERM) else returncode + manifest["server_log_tail"] = "" + manifest["command"] = {"exit_code": effective_exit, "teardown": teardown} + if error: + manifest["error"] = error + if args.mode == "transition" and error is None and teardown == "complete" and effective_exit == 0: + try: + manifest["success"] = True + validate_transition_manifest(manifest, transition_secrets) + except Exception as exc: # noqa: BLE001 - preserve failed manifest + manifest["success"] = False + manifest["error"] = str(exc) + elif args.mode == "capture" and error is None and ( + (args.keep_server and returncode is None) + or (teardown == "complete" and effective_exit == 0) + ): + manifest["success"] = True + safe_manifest = redact_secrets(manifest, transition_secrets) + manifest_path = output_dir / "manifest.json" + temporary_path = output_dir / ".manifest.json.tmp" + temporary_path.write_text(json.dumps(safe_manifest, ensure_ascii=False, indent=2), encoding="utf-8") + temporary_path.replace(manifest_path) + print(json.dumps(safe_manifest, ensure_ascii=False, indent=2)) + return 0 if manifest.get("success") is True else 1 if __name__ == "__main__": diff --git a/tests/test_dashboard_menu_transition.py b/tests/test_dashboard_menu_transition.py new file mode 100644 index 0000000..9fb3d79 --- /dev/null +++ b/tests/test_dashboard_menu_transition.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from pathlib import Path +import sys + +import pytest +from streamlit.testing.v1 import AppTest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from dashboard import app, authentication, common +from scripts.ci_fixture_db import create_ci_fixture_db + + +PAGES = ["월정산", "요약", "자산/마감", "거래내역", "확인필요 큐", "미분류", "분류 규칙", "카테고리", "계좌", "Import"] + + +def test_dashboard_menu_labels_and_order_are_preserved(monkeypatch): + calls = {} + + def radio(label, options, **kwargs): + calls.update(label=label, options=list(options), kwargs=kwargs) + return "요약" + + monkeypatch.setattr(common.st, "radio", radio) + assert common.render_sidebar_nav(PAGES, "요약") == "요약" + assert calls["options"] == PAGES + assert calls["kwargs"]["key"] == "dashboard_page" + assert calls["kwargs"]["label_visibility"] == "collapsed" + + +def test_sidebar_selection_updates_only_page_query_parameter(monkeypatch): + query = {"page": "요약", "month": "2026-06", "account_id": "checking"} + state = {"dashboard_page": "거래내역"} + callback = {} + + monkeypatch.setattr(common.st, "query_params", query) + monkeypatch.setattr(common.st, "session_state", state) + monkeypatch.setattr(common.st, "radio", lambda *args, **kwargs: callback.setdefault("fn", kwargs["on_change"]) or state["dashboard_page"]) + common.render_sidebar_nav(PAGES, "요약") + callback["fn"]() + + assert query == {"page": "거래내역", "month": "2026-06", "account_id": "checking"} + + +def test_missing_or_unknown_page_falls_back_to_overview(): + assert common.validate_dashboard_page(None, PAGES) == "요약" + assert common.validate_dashboard_page("not-a-page", PAGES) == "요약" + assert common.validate_dashboard_page("거래내역", PAGES) == "거래내역" + + +def test_direct_page_query_access_renders_requested_page(monkeypatch): + selected = {} + monkeypatch.setattr(app, "require_dashboard_authentication", lambda: True) + monkeypatch.setattr(app, "get_db_path", lambda: "fixture.sqlite3") + monkeypatch.setattr(app, "inject_app_css", lambda: None) + monkeypatch.setattr(app.st, "query_params", {"page": "계좌"}) + monkeypatch.setattr(app.st, "session_state", {"dashboard_page": "요약"}) + + def capture_sidebar_page(pages, page): + selected["sidebar_page"] = page + selected["session_page"] = app.st.session_state["dashboard_page"] + return page + + monkeypatch.setattr(app, "render_sidebar_nav", capture_sidebar_page) + monkeypatch.setattr(app.st, "sidebar", pytest.importorskip("contextlib").nullcontext()) + monkeypatch.setattr(app.st, "button", lambda *args, **kwargs: False) + monkeypatch.setattr(app, "page_accounts", lambda db: selected.setdefault("db", db)) + for name in PAGES: + if name != "계좌": + monkeypatch.setattr(app, {"월정산": "page_monthly_settlement", "요약": "page_overview", "자산/마감": "page_assets", "거래내역": "page_transactions", "확인필요 큐": "page_cross_transfer_review", "미분류": "page_uncategorized", "분류 규칙": "page_rules", "카테고리": "page_categories", "Import": "page_imports"}[name], lambda db: None) + monkeypatch.setattr(app, "render_dashboard_body", lambda page, renderer: renderer()) + + app.main() + assert selected == { + "sidebar_page": "계좌", + "session_page": "계좌", + "db": "fixture.sqlite3", + } + + +def test_streamlit_menu_click_updates_body_and_preserves_page_query_siblings(tmp_path, monkeypatch): + fixture = tmp_path / "fixture.sqlite3" + create_ci_fixture_db(fixture) + monkeypatch.setenv("JARCA_LEDGER_DB_PATH", str(fixture)) + monkeypatch.setenv(authentication.ADMIN_ID_ENV, "test-admin") + monkeypatch.setenv(authentication.ADMIN_PASSWORD_ENV, "test-password") + monkeypatch.setenv(authentication.SIGNING_SECRET_ENV, "s" * 64) + settings = authentication.load_auth_settings() + assert settings is not None + token = authentication.issue_persistent_token(settings, remember_me=False).value + monkeypatch.setattr(authentication, "_request_cookie", lambda: token) + + app_path = Path(__file__).resolve().parents[1] / "dashboard" / "app.py" + at = AppTest.from_file(str(app_path), default_timeout=10) + at.query_params["page"] = "요약" + at.query_params["month"] = "2026-06" + at.run() + + at.radio(key="dashboard_page").set_value("분류 규칙").run() + + assert not at.exception + assert at.query_params["page"] == ["분류 규칙"] + assert at.query_params["month"] == ["2026-06"] + body_text = "\n".join(str(item.value) for item in list(at.text) + list(at.markdown) + list(at.caption)) + assert "현재 분류 규칙" in body_text + + +def test_slow_transition_clears_previous_body_and_shows_target_loading(monkeypatch): + events = [] + monkeypatch.setattr(common.st, "empty", lambda: _FakePlaceholder(events)) + monkeypatch.setattr(common.st, "container", lambda **kwargs: _FakeKeyedContainer(events, kwargs["key"])) + monkeypatch.setattr(common.st, "info", lambda message: events.append(("info", message))) + common.render_dashboard_body("거래내역", lambda: events.append(("render", "거래내역"))) + assert events[0] == ("empty", "placeholder") + assert ("open", "dashboard_body") in events + assert ("open", "dashboard_loading") in events + assert ("open", "dashboard_content") in events + assert events.index(("info", "거래내역 불러오는 중…")) < events.index(("render", "거래내역")) + assert events.index(("render", "거래내역")) < len(events) - 1 + assert events.count(("empty", "placeholder")) >= 2 + + +def test_body_error_is_local_and_retry_rerenders_target(monkeypatch): + events = [] + monkeypatch.setattr(common.st, "empty", lambda: _FakePlaceholder(events)) + monkeypatch.setattr(common.st, "container", lambda **kwargs: _FakeKeyedContainer(events, kwargs["key"])) + monkeypatch.setattr(common.st, "info", lambda message: events.append(("info", message))) + monkeypatch.setattr(common.st, "error", lambda message: events.append(("error", message))) + monkeypatch.setattr(common.st, "button", lambda *args, **kwargs: events.append(("retry", args[0])) or True) + monkeypatch.setattr(common.st, "rerun", lambda: events.append(("rerun", "target"))) + common.render_dashboard_body("요약", lambda: (_ for _ in ()).throw(RuntimeError("secret details"))) + assert ("error", "페이지를 불러오지 못했습니다. 다시 시도해 주세요.") in events + assert ("rerun", "target") in events + assert all("secret details" not in str(event) for event in events) + + +def test_rapid_menu_clicks_commit_latest_selection_only(monkeypatch): + query = {"page": "요약", "filter": "pending"} + state = {"dashboard_page": "요약"} + callback = {} + monkeypatch.setattr(common.st, "query_params", query) + monkeypatch.setattr(common.st, "session_state", state) + monkeypatch.setattr(common.st, "radio", lambda *args, **kwargs: callback.setdefault("fn", kwargs["on_change"]) or state["dashboard_page"]) + common.render_sidebar_nav(PAGES, "요약") + for page in ("거래내역", "자산/마감", "Import"): + state["dashboard_page"] = page + callback["fn"]() + assert query == {"page": "Import", "filter": "pending"} + + +def test_refresh_control_preserves_cache_clear_contract(monkeypatch): + events = [] + + class ExpectedRerun(BaseException): + pass + + monkeypatch.setattr(app, "require_dashboard_authentication", lambda: True) + monkeypatch.setattr(app, "get_db_path", lambda: "fixture.sqlite3") + monkeypatch.setattr(app, "inject_app_css", lambda: None) + monkeypatch.setattr(app.st, "query_params", {"page": "요약"}) + monkeypatch.setattr(app, "render_sidebar_nav", lambda pages, page: page) + monkeypatch.setattr(app.st, "sidebar", pytest.importorskip("contextlib").nullcontext()) + monkeypatch.setattr(app.st, "button", lambda *args, **kwargs: True) + monkeypatch.setattr(app, "clear_cache", lambda: events.append("clear_cache")) + + def rerun(): + events.append("rerun") + raise ExpectedRerun + + monkeypatch.setattr(app.st, "rerun", rerun) + + with pytest.raises(ExpectedRerun): + app.main() + assert events == ["clear_cache", "rerun"] + + +class _FakePlaceholder: + def __init__(self, events): + self.events = events + + def container(self): + return _FakeContainer(self.events) + + def info(self, message): + self.events.append(("info", message)) + + def empty(self): + self.events.append(("empty", "placeholder")) + + +class _FakeContainer: + def __init__(self, events): + self.events = events + + def __enter__(self): + self.events.append(("container", "open")) + return self + + def __exit__(self, exc_type, exc, tb): + self.events.append(("container", "close")) + return False + + +class _FakeKeyedContainer: + def __init__(self, events, key): + self.events = events + self.key = key + + def __enter__(self): + self.events.append(("open", self.key)) + return self + + def __exit__(self, exc_type, exc, tb): + self.events.append(("close", self.key)) + return False + + def empty(self): + self.events.append(("empty", self.key)) diff --git a/tests/test_streamlit_browser_evidence.py b/tests/test_streamlit_browser_evidence.py new file mode 100644 index 0000000..b39a248 --- /dev/null +++ b/tests/test_streamlit_browser_evidence.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import json +import ssl +import sys +from pathlib import Path + +from unittest.mock import patch + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +import streamlit_browser_evidence as evidence + + +SECRET_VALUES = ("synthetic-password", "a" * 64, "cookie-value") + + +def test_redact_secrets_removes_values_from_nested_manifest() -> None: + payload = {"url": "https://127.0.0.1/?token=cookie-value", "nested": ["synthetic-password", {"key": "a" * 64}]} + redacted = evidence.redact_secrets(payload, SECRET_VALUES) + serialized = json.dumps(redacted) + assert all(secret not in serialized for secret in SECRET_VALUES) + assert redacted["nested"][0] == "[REDACTED]" + + +def test_transition_requires_synthetic_fixture_database(tmp_path: Path) -> None: + with pytest.raises(SystemExit, match="fixture.sqlite3"): + evidence.assert_synthetic_db_path(tmp_path / "operating.sqlite3") + + +def test_validate_transition_manifest_requires_observations() -> None: + manifest = { + "mode": "transition", + "success": True, + "observations": {"frames": [], "navigation_events": []}, + } + with pytest.raises(ValueError, match="frames"): + evidence.validate_transition_manifest(manifest) + + +def complete_manifest() -> dict[str, object]: + return { + "mode": "transition", + "success": True, + "observations": { + "frames": [ + { + "at_ms": 0, + "selected_menu": "요약", + "body_text_hash": "hash", + "loading": False, + "prior_body_visible": True, + }, + { + "at_ms": 100, + "selected_menu": "분류 규칙", + "body_text_hash": "hash2", + "loading": True, + "prior_body_visible": False, + }, + ], + "document_navigation_events": [], + "history_events": [{"type": "pushState", "url": "/?page=rules"}], + "stable_shell": { + "document_marker": {"pre": "doc-1", "post": "doc-1"}, + "sidebar_marker": {"pre": "side-1", "post": "side-1"}, + "background": {"pre": "rgb(255, 255, 255)", "post": "rgb(255, 255, 255)"}, + }, + "loading_observed": True, + "prior_body_removed": True, + "target_page": "분류 규칙", + "final_agreement": True, + }, + "runtime": { + "viewport": {"width": 1440, "height": 1400}, + "interval_ms": 50, + "host_architecture": "aarch64", + "browser_user_agent": "Synthetic Browser", + }, + "source_revision": evidence.current_git_revision(), + "source_revision_kind": "git-head-base", + "source_worktree_dirty": evidence.git_worktree_dirty(("dashboard/app.py", "dashboard/common.py")), + "source_files": evidence.source_file_digests(("dashboard/app.py", "dashboard/common.py")), + "evidence_command": ["scripts/streamlit_browser_evidence.py", "--mode", "transition"], + "command": {"exit_code": 0, "teardown": "complete"}, + "limitations": [], + } + + +def test_validate_transition_manifest_accepts_complete_secret_free_shape() -> None: + evidence.validate_transition_manifest(complete_manifest()) + + +def test_wait_for_http_uses_verified_tls_by_default_and_fixture_context_opt_in() -> None: + response = type("Response", (), {"status": 200, "__enter__": lambda self: self, "__exit__": lambda *args: None})() + with patch.object(evidence.urllib.request, "urlopen", return_value=response) as urlopen: + evidence.wait_for_http("https://example.test") + assert urlopen.call_args.kwargs["context"] is None + fixture_context = ssl._create_unverified_context() + with patch.object(evidence.urllib.request, "urlopen", return_value=response) as urlopen: + evidence.wait_for_http("https://127.0.0.1", ssl_context=fixture_context) + assert urlopen.call_args.kwargs["context"] is fixture_context + + +def test_validate_transition_manifest_rejects_fabricated_navigation_event() -> None: + manifest = complete_manifest() + manifest["observations"]["document_navigation_events"] = [{"type": "load", "url": "/"}] + with pytest.raises(ValueError, match="document navigation"): + evidence.validate_transition_manifest(manifest) + + +def test_validate_transition_manifest_requires_observed_loading_without_prior_body() -> None: + no_loading = complete_manifest() + no_loading["observations"]["loading_observed"] = False + with pytest.raises(ValueError, match="loading"): + evidence.validate_transition_manifest(no_loading) + + stale_body = complete_manifest() + stale_body["observations"]["frames"][1]["prior_body_visible"] = True + with pytest.raises(ValueError, match="prior body"): + evidence.validate_transition_manifest(stale_body) + + +def test_validate_transition_manifest_requires_pre_post_shell_identity() -> None: + manifest = complete_manifest() + manifest["observations"]["stable_shell"] = {"sidebar": True, "background": "#fff"} + with pytest.raises(ValueError, match="shell"): + evidence.validate_transition_manifest(manifest) + + +def test_validate_transition_manifest_rejects_unselected_target_body_agreement() -> None: + manifest = complete_manifest() + manifest["observations"]["final_agreement"] = False + with pytest.raises(ValueError, match="agreement"): + evidence.validate_transition_manifest(manifest) + + +def test_validate_transition_manifest_rejects_attestation_before_teardown() -> None: + manifest = complete_manifest() + manifest["command"] = {"exit_code": 0, "teardown": "running"} + with pytest.raises(ValueError, match="teardown"): + evidence.validate_transition_manifest(manifest) + + +def test_validate_transition_manifest_requires_exact_source_and_runtime_identity() -> None: + manifest = complete_manifest() + manifest.pop("source_files") + with pytest.raises(ValueError, match="source files"): + evidence.validate_transition_manifest(manifest) + + manifest = complete_manifest() + manifest["runtime"].pop("browser_user_agent") + with pytest.raises(ValueError, match="runtime identity"): + evidence.validate_transition_manifest(manifest) + + manifest = complete_manifest() + manifest["source_files"]["dashboard/app.py"] = "0" * 64 + with pytest.raises(ValueError, match="source file digests"): + evidence.validate_transition_manifest(manifest) + + +def test_transition_manifest_rejects_secret_text() -> None: + manifest = {"mode": "transition", "success": False, "error": "synthetic-password"} + with pytest.raises(ValueError, match="secret"): + evidence.validate_transition_manifest(manifest, SECRET_VALUES) -- 2.54.0 From a02aceee416c5770dfd1f35ac94367f3c4e0c8c8 Mon Sep 17 00:00:00 2001 From: gitea-impl-agent Date: Mon, 5 Oct 2026 00:51:45 +0000 Subject: [PATCH 2/4] docs(requirements): link dashboard transition PR --- docs/requirements/req-002_dashboard-menu-transition.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements/req-002_dashboard-menu-transition.md b/docs/requirements/req-002_dashboard-menu-transition.md index af32080..c28e681 100644 --- a/docs/requirements/req-002_dashboard-menu-transition.md +++ b/docs/requirements/req-002_dashboard-menu-transition.md @@ -251,7 +251,7 @@ refresh control이 callable `clear_cache` 계약을 유지한다. ## 9. 추적성 - 근거 이슈: `#9` -- 구현 PR: 미정 +- 구현 PR: `#10` - 관련 요구사항: `REQ-001` - 구현 진입점: `dashboard/app.py::main`, `dashboard/common.py::render_sidebar_nav`, `dashboard/common.py::validate_dashboard_page`, `dashboard/common.py::render_dashboard_body`, `dashboard/common.py::_sync_dashboard_page_query` - 검증 파일: `tests/test_dashboard_menu_transition.py`, `tests/test_streamlit_browser_evidence.py`, `tests/test_dashboard_app.py`, `tests/test_dashboard_authentication_app.py` -- 2.54.0 From f01dc182ec4d9b04e30d1d5bc13652a561332f24 Mon Sep 17 00:00:00 2001 From: gitea-impl-agent Date: Mon, 5 Oct 2026 08:41:33 +0000 Subject: [PATCH 3/4] fix(dashboard): address PR review 56 --- .../README.md | 32 +- .../browser-observations.json | 2263 +++++++++++------ .../final-state.jpg | Bin 34297 -> 76471 bytes .../manifest.json | 43 +- docs/project-structure.md | 6 +- .../req-002_dashboard-menu-transition.md | 175 +- docs/runtime-flow.md | 1 + scripts/streamlit_browser_evidence.py | 48 +- tests/test_streamlit_browser_evidence.py | 65 +- 9 files changed, 1785 insertions(+), 848 deletions(-) diff --git a/docs/evidence/issue-9-dashboard-menu-transition/README.md b/docs/evidence/issue-9-dashboard-menu-transition/README.md index d3d2a29..bba48c3 100644 --- a/docs/evidence/issue-9-dashboard-menu-transition/README.md +++ b/docs/evidence/issue-9-dashboard-menu-transition/README.md @@ -1,31 +1,32 @@ # Issue #9 authenticated browser evidence -This directory binds the dashboard menu-transition acceptance evidence to the exact working-tree files listed in `manifest.json`. +This directory binds the dashboard menu-transition acceptance evidence to the exact Git head and working-tree files listed in `manifest.json`. ## Captured transition - **Path:** `요약` → `분류 규칙` - **Input:** native Streamlit menu click in an authenticated HTTPS browser session - **Fixture:** generated synthetic SQLite database only -- **Sampling:** 35 DOM/style frames at 50 ms intervals +- **Sampling:** 35 DOM/computed-style frames at 50 ms intervals - **Viewport:** 1280 × 720 -- **Browser:** Headless Chrome 154 on Linux x86_64 +- **Browser:** Headless Chrome 154; Client Hints architecture `arm`, bitness `64` - **Fixture host:** Linux aarch64, Python 3.13.5, Streamlit 1.58.0, Uvicorn 0.49.0 ## Verified browser observations -All eight manifest checks are true: +All twelve manifest checks are true: -- the HTML document marker stayed identical; -- the sidebar marker stayed identical; -- the page background stayed identical; -- no post-click full-document `load` event occurred; -- the target loading state was observed; +- the HTML document and sidebar markers stayed identical; +- `[data-testid="stAppViewContainer"]` retained a non-`none` computed `backgroundImage` and remained visible/rendered in every frame; +- `[data-testid="stSidebar"]` retained a non-`none` computed `backgroundImage` and remained visible/rendered in every frame; +- computed `backgroundImage`, `backgroundColor`, `visibility`, `display`, `opacity`, and rendered geometry were captured for both surfaces in every frame; +- the click and all 35 frames completed inside one uninterrupted Playwright evaluation context, so no full-document navigation occurred; +- the target loading state was observed in two frames; - the previous `요약` body was absent in every loading frame; -- the unrelated `fixture=preserved` query key survived; +- the unrelated `source=evidence` query key survived; - final URL, selected menu, target body, and cleared loading state agreed. -`browser-observations.json` contains every sampled frame, body hashes, URL state, history events, document/sidebar markers, computed background, runtime metadata, and explicit limitations. `final-state.jpg` is the final visible browser state; it contains no credential or secret. +`browser-observations.json` contains every frame, body hashes, URL state, markers, computed surface styles, runtime metadata, and explicit limitations. `final-state.jpg` is the final visible browser state and contains no credential or secret. ## Recheck @@ -33,15 +34,14 @@ Run the exact validator recorded in `manifest.json`. The captured result was: ```text exit code: 0 -PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=35 full_document_loads=0 +PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=35 full_document_loads=0 surfaces=2 ``` -The browser action was executed through `mcp__playwright__browser_run_code_unsafe`. That API reports success/error rather than a numeric process exit code; this boundary is explicit in the manifest instead of being represented as a fabricated exit status. +The browser actions used Playwright MCP. That API reports success/error rather than a numeric process exit code; the manifest records this boundary instead of fabricating an exit status. ## Evidence boundaries - This is synthetic-fixture browser evidence, not operating-ledger evidence. -- A fixture-only external wrapper delayed the rules renderer by two seconds so the loading interval could be sampled. It did not alter repository source. -- A temporary HTTPS ingress adapter supplied trusted proxy scheme/Host metadata. Repository authentication and same-origin enforcement remained active. -- The tunnel URL is intentionally ephemeral and is retained in the frame record only for provenance. +- A temporary HTTPS ingress adapter normalized the request scheme. Repository authentication and same-origin enforcement remained active. +- The tunnel URL is intentionally ephemeral and retained only as provenance. - The screenshot supplements, but does not replace, the machine-readable continuous-frame observations. diff --git a/docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json b/docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json index c84762f..8a18ae4 100644 --- a/docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json +++ b/docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json @@ -1,988 +1,1643 @@ { - "schema": "jarca.issue9.browser-transition.v1", + "schema": "jarca.issue9.browser-transition.v2", "source": "authenticated Playwright browser runner through temporary HTTPS fixture ingress", + "captured_at": "2026-10-05T08:17:17.108Z", "from_page": "요약", "to_page": "분류 규칙", "pre": { "elapsed_ms": 0, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", "query_page": "요약", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "요약", "loading": false, "previous_body_visible": true, "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [], + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, "frames": [ { - "elapsed_ms": 134, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "elapsed_ms": 0, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", "query_page": "요약", - "sibling_value": "preserved", - "selected": "분류 규칙", + "sibling_source": "evidence", + "selected": "요약", "loading": false, "previous_body_visible": true, "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [], + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { - "elapsed_ms": 296, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "elapsed_ms": 50, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", "query_page": "요약", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": false, "previous_body_visible": true, "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [], + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { - "elapsed_ms": 374, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "elapsed_ms": 100, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", "query_page": "요약", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": false, "previous_body_visible": true, "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [], + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { - "elapsed_ms": 442, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "elapsed_ms": 150, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", "query_page": "요약", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": false, "previous_body_visible": true, "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [], + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { - "elapsed_ms": 520, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", + "elapsed_ms": 200, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", "query_page": "요약", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": false, "previous_body_visible": true, "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [], + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 250, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", + "query_page": "요약", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 300, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", + "query_page": "요약", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 350, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", + "query_page": "요약", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 400, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", + "query_page": "요약", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 450, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", + "query_page": "요약", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 500, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", + "query_page": "요약", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 550, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": true, + "target_body_visible": false, + "body_sha256": "fe2152704a93e27f9142fee10ef4963d98bc282fc30ee2713d3f34d7b47aca49", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { "elapsed_ms": 600, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", - "query_page": "요약", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": false, - "previous_body_visible": true, - "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 669, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", - "query_page": "요약", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": false, - "previous_body_visible": true, - "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 739, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EC%9A%94%EC%95%BD&fixture=preserved", - "query_page": "요약", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": false, - "previous_body_visible": true, - "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 816, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": false, - "previous_body_visible": true, - "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 1396, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": false, - "previous_body_visible": true, - "target_body_visible": false, - "body_sha256": "64d8cf1234afc84635c58b9ec964b2573fd8bea12dfd107e82086cc20f6b16f3", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 1580, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": true, "previous_body_visible": false, - "target_body_visible": false, + "target_body_visible": true, "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true } - ], + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { - "elapsed_ms": 1654, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "elapsed_ms": 650, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", "query_page": "분류 규칙", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": true, "previous_body_visible": false, - "target_body_visible": false, + "target_body_visible": true, "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true } - ], + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { - "elapsed_ms": 1721, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "elapsed_ms": 700, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 1793, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 1860, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 1926, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 1993, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2061, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2130, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2197, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2274, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2341, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2410, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2478, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2544, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2610, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2681, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2753, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2826, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 2912, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 3286, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 4054, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", - "selected": "분류 규칙", - "loading": true, - "previous_body_visible": false, - "target_body_visible": false, - "body_sha256": "42fe6bb021f60cda14cc750a3bf7d1c58c9801323a37219d221d4e1345da81bd", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", - "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 - } - ], - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, - { - "elapsed_ms": 5973, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "query_page": "분류 규칙", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": false, "previous_body_visible": false, "target_body_visible": true, - "body_sha256": "f5c040f5f42490cba765987c8b849ea41ee248ed1b403f2dc890b2b27a73dad9", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true } - ], + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { - "elapsed_ms": 6065, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "elapsed_ms": 750, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", "query_page": "분류 규칙", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": false, "previous_body_visible": false, "target_body_visible": true, - "body_sha256": "f5c040f5f42490cba765987c8b849ea41ee248ed1b403f2dc890b2b27a73dad9", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true } - ], + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, { - "elapsed_ms": 6134, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "elapsed_ms": 800, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", "query_page": "분류 규칙", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": false, "previous_body_visible": false, "target_body_visible": true, - "body_sha256": "f5c040f5f42490cba765987c8b849ea41ee248ed1b403f2dc890b2b27a73dad9", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true } - ], + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 850, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 900, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 950, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1000, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1050, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1100, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1150, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1200, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1250, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1300, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1350, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1400, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1450, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1500, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1550, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1600, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1650, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } + }, + { + "elapsed_ms": 1700, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", + "query_page": "분류 규칙", + "sibling_source": "evidence", + "selected": "분류 규칙", + "loading": false, + "previous_body_visible": false, + "target_body_visible": true, + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "background": "rgb(255, 255, 255)", + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } } ], "post": { - "elapsed_ms": 6134, - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", + "elapsed_ms": 1700, + "url": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence", "query_page": "분류 규칙", - "sibling_value": "preserved", + "sibling_source": "evidence", "selected": "분류 규칙", "loading": false, "previous_body_visible": false, "target_body_visible": true, - "body_sha256": "f5c040f5f42490cba765987c8b849ea41ee248ed1b403f2dc890b2b27a73dad9", - "document_marker": "issue9-document-marker", - "sidebar_marker": "issue9-sidebar-marker", + "body_sha256": "87ecc82dee8866b103ec07702108a8d00ce460ff2739e4521ca6742aa6f80f60", + "document_marker": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "sidebar_marker": "fb10199d-796c-4fe1-8eff-571ad06c0224", "background": "rgb(255, 255, 255)", - "history_events": [ - { - "type": "pushState", - "url": "/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693754 + "surfaces": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true } - ], + }, "viewport": { "width": 1280, "height": 720 }, "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" + "architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + } }, - "same_document_events": [ + "stable_shell": { + "document_marker": { + "pre": "c830670d-8588-4000-9e30-10bc0a6bfe1a", + "post": "c830670d-8588-4000-9e30-10bc0a6bfe1a" + }, + "sidebar_marker": { + "pre": "fb10199d-796c-4fe1-8eff-571ad06c0224", + "post": "fb10199d-796c-4fe1-8eff-571ad06c0224" + }, + "background": { + "pre": "rgb(255, 255, 255)", + "post": "rgb(255, 255, 255)" + }, + "surfaces": { + "pre": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + }, + "post": { + "stAppViewContainer": { + "backgroundImage": "radial-gradient(circle at 0% 0%, rgba(37, 99, 235, 0.1), rgba(0, 0, 0, 0) 480px), radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0) 448px), linear-gradient(rgb(248, 250, 252) 0%, rgb(255, 255, 255) 45%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "flex", + "opacity": "1", + "rendered": true + }, + "stSidebar": { + "backgroundImage": "linear-gradient(rgb(255, 255, 255) 0%, rgb(248, 250, 252) 100%)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": true + } + } + } + }, + "history_events": [ { - "kind": "same-document-url-change", - "url": "https://d54e2eeaa8e50e2b-211-109-75-114.serveousercontent.com/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&fixture=preserved", - "at": 1791130693813 + "type": "same-document-query-update", + "from": "https://bb8d254926f5fd.lhr.life/?page=%EC%9A%94%EC%95%BD&source=evidence", + "to": "https://bb8d254926f5fd.lhr.life/?page=%EB%B6%84%EB%A5%98+%EA%B7%9C%EC%B9%99&source=evidence" } ], "document_load_events": [], + "navigation_observer": "The entire click and 35-frame sample ran inside one Playwright page.evaluate execution context; a full document navigation would destroy that context and reject the tool call.", "checks": { "document_identity_stable": true, "sidebar_identity_stable": true, - "background_stable": true, + "app_surface_background_stable": true, + "sidebar_surface_background_stable": true, + "app_surface_visible_all_frames": true, + "sidebar_surface_visible_all_frames": true, + "surface_backgrounds_observed": true, "zero_full_document_loads": true, "loading_observed": true, "previous_body_absent_while_loading": true, "query_sibling_preserved": true, "final_agreement": true }, - "runtime": { - "captured_at": "2026-10-04T16:18:18.832Z", - "interval_ms": 50, - "frame_count": 35, - "viewport": { - "width": 1280, - "height": 720 - }, - "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36", - "architecture": "Linux x86_64" - }, "limitations": [ - "Synthetic SQLite fixture only; no operating ledger data.", - "Rules renderer delayed two seconds by an external fixture-only module wrapper.", - "Temporary HTTPS ingress adapter normalized scheme/Host metadata; repository authentication checks remained active.", - "Playwright same-document URL events are listed separately from full load events." + "Generated synthetic SQLite fixture only; no operating ledger data was used.", + "A temporary HTTPS ingress adapter normalized the trusted request scheme for repository same-origin authentication.", + "The ephemeral tunnel URL is expired provenance and is not a rerun target." ] -} \ No newline at end of file +} diff --git a/docs/evidence/issue-9-dashboard-menu-transition/final-state.jpg b/docs/evidence/issue-9-dashboard-menu-transition/final-state.jpg index f2db8b21a77abd60446c5e5229cb95e69ba3de62..049ee017f96f80388cc2fee39ef452f38a5e93db 100644 GIT binary patch literal 76471 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 literal 34297 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 RokxfMe|G%dO3wZ={2!FT^P&I% diff --git a/docs/evidence/issue-9-dashboard-menu-transition/manifest.json b/docs/evidence/issue-9-dashboard-menu-transition/manifest.json index 5205b6a..15eb3df 100644 --- a/docs/evidence/issue-9-dashboard-menu-transition/manifest.json +++ b/docs/evidence/issue-9-dashboard-menu-transition/manifest.json @@ -1,23 +1,23 @@ { - "schema": "jarca.issue9.evidence-manifest.v1", + "schema": "jarca.issue9.evidence-manifest.v2", "issue": "jsfamily/jarca-ledger-mcp#9", - "captured_at": "2026-10-04T16:18:18.832Z", + "captured_at": "2026-10-05T08:17:17.108Z", "source": { - "base_revision": "ffd943fe65875e67148c28ba44bae32918e338c9", + "git_head_base": "a02aceee416c5770dfd1f35ac94367f3c4e0c8c8", "working_tree": true, "files_sha256": { "dashboard/app.py": "4c7b3a6264b4fb4ae0e445ddc04c625b295443816b198aa5a60615ac44846ff2", "dashboard/common.py": "9a0b9dbb4ca88f27757fc293173821c48a56a30bc23f16b0f24731f246411c5a", - "scripts/streamlit_browser_evidence.py": "766beefb0d861ea6b08561c3aaa0c6d8915c9166e4221725e27b5e26cea63798", + "scripts/streamlit_browser_evidence.py": "3cfb449ce1d6fae3abb795727d7ca1bc10dbb759bb811f121abd74fa42993dd7", "tests/test_dashboard_menu_transition.py": "d23c35d88f6289b7c7e84a271df6ff3f6ff1f25a3a885aa696cc93db0b37035e", - "tests/test_streamlit_browser_evidence.py": "b922593f96da5af9a0b1fe33cacbe3abe139ef00554e7c535fe7cc8f24679b23" + "tests/test_streamlit_browser_evidence.py": "7f595ccf4009961225328486c19258bb0f7bc7dbfb584c60616860274112724a" } }, "execution": { - "browser_tool": "mcp__playwright__browser_run_code_unsafe", + "browser_tool": "mcp__playwright__browser_evaluate + mcp__playwright__browser_run_code_unsafe", "tool_status": "success", "exit_code": null, - "exit_code_note": "The Playwright MCP call is not a process and exposes success/error status rather than a numeric exit code.", + "exit_code_note": "Playwright MCP reports success/error instead of a numeric process exit code.", "transition": "요약 -> 분류 규칙", "sampling_interval_ms": 50, "frame_count": 35, @@ -26,7 +26,11 @@ "height": 720 }, "browser": "HeadlessChrome/154.0.0.0", - "browser_architecture": "Linux x86_64", + "browser_architecture": { + "client_hint_architecture": "arm", + "client_hint_bitness": "64", + "navigator_platform": "Linux x86_64" + }, "fixture_host_architecture": "aarch64", "python": "3.13.5", "streamlit": "1.58.0", @@ -35,7 +39,11 @@ "checks": { "document_identity_stable": true, "sidebar_identity_stable": true, - "background_stable": true, + "app_surface_background_stable": true, + "sidebar_surface_background_stable": true, + "app_surface_visible_all_frames": true, + "sidebar_surface_visible_all_frames": true, + "surface_backgrounds_observed": true, "zero_full_document_loads": true, "loading_observed": true, "previous_body_absent_while_loading": true, @@ -43,19 +51,18 @@ "final_agreement": true }, "artifacts_sha256": { - "browser-observations.json": "d5dcd18f074b0f3e44e1b46eb9cf685f028b0f229957d35dc2330bedca5f0cbb", - "final-state.jpg": "5a9c94598776a9ddaf4bb1aed1c758f6d1c489c320f7a81ecb3acd0cb702760d" + "browser-observations.json": "414b62efb2f40483aae4227896040c1b5d887b2c31333f63046e6f34aec2fe8f", + "final-state.jpg": "cc4e1c28463f033ab1c26d1dc27c9bb93c6fc5495321c0cef73fdb1098221234" }, "validator": { - "command": "python3 -c \"import hashlib,json,pathlib; root=pathlib.Path('.'); m=json.loads((root/'docs/evidence/issue-9-dashboard-menu-transition/manifest.json').read_text()); assert all(hashlib.sha256((root/p).read_bytes()).hexdigest()==h for p,h in m['source']['files_sha256'].items()); evidence=root/'docs/evidence/issue-9-dashboard-menu-transition'; assert all(hashlib.sha256((evidence/p).read_bytes()).hexdigest()==h for p,h in m['artifacts_sha256'].items()); d=json.loads((evidence/'browser-observations.json').read_text()); assert all(d['checks'].values()) and not d['document_load_events']; assert any(f['loading'] and not f['previous_body_visible'] for f in d['frames']); print('PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=%d full_document_loads=0' % len(d['frames']))\"", + "command": "python3 -c \"import hashlib,json,math,pathlib; r=pathlib.Path('.'); e=r/'docs/evidence/issue-9-dashboard-menu-transition'; m=json.loads((e/'manifest.json').read_text()); d=json.loads((e/'browser-observations.json').read_text()); frames=d['frames']; surfaces=('stAppViewContainer','stSidebar'); assert all(hashlib.sha256((r/p).read_bytes()).hexdigest()==h for p,h in m['source']['files_sha256'].items()); assert all(hashlib.sha256((e/p).read_bytes()).hexdigest()==h for p,h in m['artifacts_sha256'].items()); assert all(d['checks'].values()) and not d['document_load_events'] and len(frames)==35; assert all(isinstance(f['previous_body_visible'],bool) for f in frames); assert any(f['loading'] and f['previous_body_visible'] is False for f in frames); valid=lambda x: isinstance(x,dict) and str(x.get('backgroundImage','')).strip().lower() not in ('','none') and bool(str(x.get('backgroundColor','')).strip()) and x.get('visibility')=='visible' and x.get('display')!='none' and math.isfinite(float(x.get('opacity','nan'))) and float(x['opacity'])>0 and x.get('rendered') is True; assert all(valid(f['surfaces'][s]) for f in frames for s in surfaces); ss=d['stable_shell']; assert all(isinstance(ss[k]['pre'],str) and ss[k]['pre'] and ss[k]['pre']==ss[k]['post'] for k in ('document_marker','sidebar_marker','background')); assert ss['surfaces']['pre']==frames[0]['surfaces'] and ss['surfaces']['post']==d['post']['surfaces'] and ss['surfaces']['pre']==ss['surfaces']['post']; print('PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=%d full_document_loads=0 surfaces=2' % len(frames))\"", "exit_code": 0, - "output": "PASS source_hashes=5 artifact_hashes=2 checks=8/8 frames=35 full_document_loads=0" + "output": "PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=35 full_document_loads=0 surfaces=2" }, "limitations": [ - "The browser run used only a generated synthetic SQLite fixture; no operating ledger data was used.", - "A fixture-only external module wrapper delayed the rules renderer by two seconds to expose the loading interval; repository source was not modified for the delay.", - "A temporary HTTPS ingress adapter normalized trusted proxy scheme and Host metadata; repository authentication and same-origin checks remained active.", - "The ephemeral tunnel URL in browser-observations.json is expired and is retained only as provenance.", - "Playwright same-document URL events are recorded separately from full document load events." + "Generated synthetic SQLite fixture only; no operating ledger data was used.", + "A temporary HTTPS ingress adapter normalized the trusted request scheme for repository same-origin authentication.", + "The ephemeral tunnel URL is expired provenance and is not a rerun target.", + "The same-document no-load assertion is bound to the uninterrupted Playwright evaluation context, stable document marker, and empty document-load event record." ] } diff --git a/docs/project-structure.md b/docs/project-structure.md index 54ae757..0c69b5a 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -17,6 +17,7 @@ dashboard/ ├── auth_server.py ├── authentication.py ├── app.py +├── common.py └── page_views/ ``` @@ -25,9 +26,12 @@ dashboard/ | `dashboard/server.py` | source | ASGI app 조립 | `app` | route wiring only | source review required | developer | | `dashboard/auth_server.py` | source | login/cleanup/logout response | `create_auth_routes()` | no DB/page imports | source review required | developer | | `dashboard/authentication.py` | source | config, constant-time compare, token verify, gate | `require_dashboard_authentication()` | server-only secret use | source review required | developer | -| `dashboard/app.py` | source | gate then page dispatch | `main()` | no token signing details | source review required | developer | +| `dashboard/app.py` | source | auth gate first, then DB path resolution, common shell/sidebar/refresh assembly, and target body renderer dispatch | `main()` | shell/background remain outside the replaceable body; no token signing details | source review required | developer | +| `dashboard/common.py` | source | shared Dashboard shell, menu/query transition, stable body container, loading/error/retry rendering, and app background/style | `render_sidebar_nav()`, `render_dashboard_body()`, `inject_app_css()` | no authentication transport or financial write orchestration | source review required | developer | | `dashboard/page_views/` | source | financial UI pages | `page_*` functions | no auth transport calls | source review required | developer | +`dashboard/common.py` owns the shared visual theme through `JARCA_COLORS`, `JARCA_PLOTLY_TEMPLATE`, and `inject_app_css()`. Page renderers consume that theme but do not own or replace the authenticated shell/sidebar background. + ### verification-tests — 인증 검증 ```text diff --git a/docs/requirements/req-002_dashboard-menu-transition.md b/docs/requirements/req-002_dashboard-menu-transition.md index c28e681..44da358 100644 --- a/docs/requirements/req-002_dashboard-menu-transition.md +++ b/docs/requirements/req-002_dashboard-menu-transition.md @@ -54,38 +54,119 @@ Issue #9의 Dashboard 메뉴 전환 계약이다. REQ-001의 인증 경계, cook 메뉴는 정확히 다음 순서와 label을 유지한다: `월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`. selected state는 현재 page와 일치한다. +**Impact** + +- Interfaces: `dashboard_page` radio and `page` query parameter +- Persistence: none +- Consumers: dashboard page renderers +- Migration: none +- Compatibility: existing labels, order, and selected-state contract +- Deployment prerequisites: none + ### REQ-002.02 — URL과 직접 접근 메뉴 선택은 query의 `page`만 대상 label로 갱신하고 `month`, `account_id`, `filter` 등 sibling query key를 보존한다. 직접 `?page=`은 해당 page를 렌더링하고, missing/unknown 값은 `요약`으로 fallback한다. +**Impact** + +- Interfaces: browser query parameters +- Persistence: none +- Consumers: direct links and page renderers +- Migration: none +- Compatibility: sibling query keys and direct page access +- Deployment prerequisites: none + ### REQ-002.03 — 연속 화면 메뉴 클릭은 document navigation을 일으키지 않는다. 공통 shell과 background는 유지하고 body만 선택된 renderer의 내용으로 교체한다. history/back-forward와 path URL은 이 요구사항의 판정 범위가 아니다. +**Impact** + +- Interfaces: Streamlit shell/body rendering +- Persistence: none +- Consumers: authenticated dashboard users +- Migration: none +- Compatibility: existing shell, background, and path behavior +- Deployment prerequisites: none + ### REQ-002.04 — 전환 중 표시 느린 renderer가 실행되는 동안 target page label을 포함한 `불러오는 중…` loading을 대상 body 영역에 표시하고, renderer 완료 후 loading placeholder를 제거한다. +**Impact** + +- Interfaces: target body loading state +- Persistence: none +- Consumers: dashboard users +- Migration: none +- Compatibility: existing renderer output after completion +- Deployment prerequisites: none + ### REQ-002.05 — local failure와 retry target renderer 예외는 공통 shell을 무너뜨리지 않고 body 영역을 generic error와 `다시 시도` control로 교체한다. 예외 상세·secret은 UI에 노출하지 않는다. +**Impact** + +- Interfaces: body error and retry control +- Persistence: none +- Consumers: dashboard users and support operators +- Migration: none +- Compatibility: shell remains available; secrets remain redacted +- Deployment prerequisites: none + ### REQ-002.06 — 최신 선택 우선 연속 메뉴 선택은 serialized callback 순서의 마지막 선택을 query와 body에 반영하며 이전 선택이 마지막 상태를 덮어쓰지 않는다. +**Impact** + +- Interfaces: serialized menu callback and query state +- Persistence: none +- Consumers: page selection state +- Migration: none +- Compatibility: latest-selection ordering contract +- Deployment prerequisites: none + ### REQ-002.07 — 인증 경계 보존 모든 menu/page dispatch는 REQ-001 gate와 cookie 검증 뒤에만 실행된다. no-cookie/invalid-cookie는 page body에 도달하지 않으며 valid cookie는 기존 Dashboard에 접근한다. +**Impact** + +- Interfaces: REQ-001 authentication gate and cookie verification +- Persistence: none +- Consumers: authenticated dashboard dispatch +- Migration: none +- Compatibility: REQ-001 expiry, rotation, cleanup, and logout behavior +- Deployment prerequisites: existing REQ-001 settings + ### REQ-002.08 — 금융 page 회귀 없음 요약, 자산/마감, 분류 규칙, 확인필요 큐, 월정산의 기존 synthetic fixture DB 핵심 내용과 기존 page query 계약을 유지한다. +**Impact** + +- Interfaces: existing page/query contracts +- Persistence: existing synthetic fixture DB reads and writes +- Consumers: financial page renderers +- Migration: none +- Compatibility: existing financial calculations and gateway semantics +- Deployment prerequisites: existing fixture and page setup + ### REQ-002.09 — 운영 control 회귀 없음 refresh control은 기존 `clear_cache` callable 계약을 유지하고, 메뉴 전환은 cache-clear 동작을 변경하지 않는다. +**Impact** + +- Interfaces: `clear_cache` callable and refresh control +- Persistence: none +- Consumers: dashboard operators +- Migration: none +- Compatibility: existing cache-clear behavior +- Deployment prerequisites: none + ## 6. 제약과 불변조건 - `dashboard/app.py`의 auth gate와 page dispatch는 유지하며 인증 전에 DB path를 resolve하지 않는다. @@ -101,6 +182,10 @@ refresh control은 기존 `clear_cache` callable 계약을 유지하고, 메뉴 ### REQ-002.01-AC01 — label/order/selected +**Cases**: positive, unchanged-state + +**State Invariants**: labels/order and selected state remain exact. + **판정 기준** 정확한 10개 label과 순서가 radio options로 전달되고 `dashboard_page` selected state가 유지된다. @@ -117,6 +202,10 @@ refresh control은 기존 `clear_cache` callable 계약을 유지하고, 메뉴 ### REQ-002.02-AC01 — query와 fallback +**Cases**: positive, boundary, unchanged-state + +**State Invariants**: only `page` changes; sibling query keys remain unchanged. + **판정 기준** callback은 `page`만 갱신하고 sibling key를 보존하며, missing/unknown은 `요약`, known direct query는 요청 page를 선택한다. @@ -133,6 +222,10 @@ callback은 `page`만 갱신하고 sibling key를 보존하며, missing/unknown ### REQ-002.03-AC01 — body-only transition +**Cases**: positive, unchanged-state + +**State Invariants**: document, shell, and background remain stable; body changes only. + **판정 기준** 실제 browser에서 authenticated menu click 후 document URL path와 full-document identity가 바뀌지 않고 공통 sidebar/background가 남으며 target body만 바뀐다. history/back-forward와 path URL은 판정하지 않는다. @@ -149,6 +242,10 @@ Required real-browser evidence: `docs/evidence/issue-9-dashboard-menu-transition ### REQ-002.04-AC01 — target loading +**Cases**: positive, boundary + +**State Invariants**: loading appears before completion and is removed after rendering. + **판정 기준** slow renderer 동안 target label loading이 먼저 표시되고 완료 후 placeholder가 제거된다. @@ -162,10 +259,14 @@ slow renderer 동안 target label loading이 먼저 표시되고 완료 후 plac - 수준: unit + browser evidence - 백엔드: pytest fake Streamlit placeholder, authenticated Playwright browser - 증거: ordered event list와 50 ms 연속 frame에서 `loading=true`, `previous_body_visible=false`, 최종 `loading=false` -- 한계: browser evidence의 2초 지연은 repository source가 아닌 fixture-only wrapper로 주입한다. +- 한계: browser evidence는 실제 authenticated transition을 50 ms 간격으로 표본화했으며 loading은 2개 frame에서 관찰됐다. 표본 사이의 모든 순간을 보장하지 않는다. ### REQ-002.05-AC01 — local error/retry +**Cases**: negative, positive + +**State Invariants**: shell remains intact; renderer details and secrets are not exposed. + **판정 기준** renderer 예외가 generic error/retry body로 국소 대체되고 예외 상세가 노출되지 않는다. @@ -182,6 +283,10 @@ renderer 예외가 generic error/retry body로 국소 대체되고 예외 상세 ### REQ-002.06-AC01 — latest selection +**Cases**: concurrency, unchanged-state + +**State Invariants**: latest serialized selection is final. + **판정 기준** 연속 선택 `거래내역` → `자산/마감` → `Import` 후 query가 마지막 `Import`이고 sibling key가 보존된다. @@ -198,6 +303,10 @@ renderer 예외가 generic error/retry body로 국소 대체되고 예외 상세 ### REQ-002.07-AC01 — auth regression +**Cases**: negative, positive + +**State Invariants**: auth gate precedes DB/page dispatch; cookie semantics remain REQ-001. + **판정 기준** valid cookie가 기존 Dashboard를 열고 cookie가 session state에 저장되지 않으며 gate rejection은 DB/page dispatch 전에 종료된다. @@ -214,6 +323,10 @@ valid cookie가 기존 Dashboard를 열고 cookie가 session state에 저장되 ### REQ-002.08-AC01 — finance page regression +**Cases**: positive, unchanged-state + +**State Invariants**: existing financial page results and query contracts remain unchanged. + **판정 기준** 기존 핵심 금융 page의 expected text와 synthetic fixture DB 결과가 유지된다. @@ -230,6 +343,10 @@ valid cookie가 기존 Dashboard를 열고 cookie가 session state에 저장되 ### REQ-002.09-AC01 — cache-clear regression +**Cases**: positive, unchanged-state + +**State Invariants**: `clear_cache` remains callable and menu transition does not alter it. + **판정 기준** refresh control이 callable `clear_cache` 계약을 유지한다. @@ -244,15 +361,67 @@ refresh control이 callable `clear_cache` 계약을 유지한다. - 백엔드: pytest - 증거: callable/name assertion +### Acceptance Criteria canonical fields + +Each Acceptance Criterion has explicit `Cases` and `State Invariants` in addition to the existing Korean verification text. + +| Acceptance Criterion | Cases | State Invariants | +|---|---|---| +| `REQ-002.01-AC01` | positive, unchanged-state | labels/order and selected state remain exact | +| `REQ-002.02-AC01` | positive, boundary, unchanged-state | only `page` changes; sibling query keys remain unchanged | +| `REQ-002.03-AC01` | positive, unchanged-state | document, shell, and background remain stable; body changes only | +| `REQ-002.04-AC01` | positive, boundary | loading appears before completion and is removed after rendering | +| `REQ-002.05-AC01` | negative, positive | shell remains intact; renderer details and secrets are not exposed | +| `REQ-002.06-AC01` | concurrency, unchanged-state | latest serialized selection is final | +| `REQ-002.07-AC01` | negative, positive | auth gate precedes DB/page dispatch; cookie semantics remain REQ-001 | +| `REQ-002.08-AC01` | positive, unchanged-state | existing financial page results and query contracts remain unchanged | +| `REQ-002.09-AC01` | positive, unchanged-state | `clear_cache` remains callable and menu transition does not alter it | + ## 8. 오류 및 충돌 계약 인증 실패는 항상 REQ-001 gate가 우선하며 메뉴·query로 우회할 수 없다. renderer 실패는 해당 body만 generic error/retry로 대체한다. 빠른 선택은 Streamlit serialized callback 순서를 따르며 마지막 serialized selection이 최종 상태다. history/back-forward와 path URL은 요구하지 않는다. -## 9. 추적성 +## 9. 데이터 및 인터페이스 계약 + +- `dashboard_page` is the canonical selected menu field; `page` is the only query field written by menu selection. +- `month`, `account_id`, `filter`, and other sibling query keys are preserved verbatim. +- Body loading, error, retry, and renderer output are local to the stable body container; shell/background are not replaced. + +## 10. 하위호환성 + +- Authentication expiry, rotation, invalid-cookie cleanup, and explicit logout remain governed by `REQ-001.05-AC01`, `REQ-001.07-AC01`, `REQ-001.08-AC01`, and `REQ-001.09-AC01`. +- Existing exact tests remain the authority: `tests/test_dashboard_persistent_authentication.py::test_token_expiry_is_absolute_and_not_sliding`, `tests/test_dashboard_persistent_authentication.py::test_tampering_and_rotations_invalidate_token`, and `tests/test_dashboard_auth_server.py::test_logout_clears_cookie_with_matching_scope`. +- Menu/query reruns/rendering do not initiate or replay financial writes or external calls. Financial writes and external calls remain behind explicit page user actions; this adds no new idempotency contract. + +## 11. 검증 계획 + +- Menu transition tests and required browser evidence verify REQ-002 criteria; REQ-001 authentication tests above verify expiry, rotation, and logout regressions. + +### Test Traceability Contract + +Test definitions reference the exact Acceptance Criterion IDs and execution evidence records revision, commit, environment, and result separately. + +## 12. 추적성 - 근거 이슈: `#9` - 구현 PR: `#10` -- 관련 요구사항: `REQ-001` +- 관련 설계 문서: `docs/project-structure.md`, `docs/runtime-flow.md` +- 관련 requirement: `REQ-001` - 구현 진입점: `dashboard/app.py::main`, `dashboard/common.py::render_sidebar_nav`, `dashboard/common.py::validate_dashboard_page`, `dashboard/common.py::render_dashboard_body`, `dashboard/common.py::_sync_dashboard_page_query` - 검증 파일: `tests/test_dashboard_menu_transition.py`, `tests/test_streamlit_browser_evidence.py`, `tests/test_dashboard_app.py`, `tests/test_dashboard_authentication_app.py` - browser evidence 도구: `scripts/streamlit_browser_evidence.py --mode transition` + +## 13. 확정된 결정 + +- Keep the authenticated shell and background stable while replacing only the body. +- Preserve REQ-001 authentication semantics and keep financial writes/external calls behind explicit page actions. + +## 14. Open Questions + +- 없음 + +## 15. 승인 기록 + +- 상태: `approved` +- 승인 주체: existing requirement approval +- 승인 근거: `REQ-002` revision `1.0` diff --git a/docs/runtime-flow.md b/docs/runtime-flow.md index 292e5e9..2b25cb8 100644 --- a/docs/runtime-flow.md +++ b/docs/runtime-flow.md @@ -137,6 +137,7 @@ ASGI startup부터 login, token restoration, invalid-cookie cleanup, explicit lo - known direct `?page=` selects that page; missing/unknown selects `요약`. - serialized rapid selections commit the latest selection only. - menu transition never runs before auth and never changes cookie/logout/DB schema/MCP semantics. +- menu/query reruns and rendering do not initiate or replay financial writes or external calls; those remain behind explicit page user actions, with no new idempotency contract. - history/back-forward and path URL behavior are explicitly outside this flow. ### transition verification boundary diff --git a/scripts/streamlit_browser_evidence.py b/scripts/streamlit_browser_evidence.py index b3ae62a..3975fac 100755 --- a/scripts/streamlit_browser_evidence.py +++ b/scripts/streamlit_browser_evidence.py @@ -21,6 +21,7 @@ import subprocess import sys import time import hashlib +import math import secrets import ssl import urllib.parse @@ -326,6 +327,26 @@ def redact_secrets(value: Any, secrets_to_redact: tuple[str, ...] = ()) -> Any: return value +def _validate_surface_records(records: Any, label: str) -> None: + required = ("stAppViewContainer", "stSidebar") + if not isinstance(records, dict) or any(not isinstance(records.get(name), dict) for name in required): + raise ValueError(f"transition observations require surface records for {label}") + for name in required: + surface = records[name] + if any(key not in surface or surface[key] is None for key in ("backgroundImage", "backgroundColor", "visibility", "display", "opacity", "rendered")): + raise ValueError(f"transition observations require complete surface records for {label}") + if not str(surface["backgroundImage"]).strip() or str(surface["backgroundImage"]).strip().lower() == "none": + raise ValueError(f"transition observations reject empty surface background for {label}") + if not str(surface["backgroundColor"]).strip(): + raise ValueError(f"transition observations reject empty surface background color for {label}") + try: + opacity = float(surface["opacity"]) + except (TypeError, ValueError) as exc: + raise ValueError(f"transition observations require valid surface opacity for {label}") from exc + if not math.isfinite(opacity) or surface["visibility"] != "visible" or surface["display"] == "none" or opacity <= 0 or surface["rendered"] is not True: + raise ValueError(f"transition observations require visible rendered surfaces for {label}") + + def validate_transition_manifest(manifest: dict[str, Any], secrets_to_redact: tuple[str, ...] = ()) -> None: """Fail closed unless transition evidence contains every required observation.""" serialized = json.dumps(manifest, ensure_ascii=False) @@ -337,14 +358,30 @@ def validate_transition_manifest(manifest: dict[str, Any], secrets_to_redact: tu frames = observations.get("frames") if not isinstance(frames, list) or len(frames) < 2: raise ValueError("transition observations require at least two frames") + for index, frame in enumerate(frames): + _validate_surface_records(frame.get("surfaces"), f"frame {index}") document_events = observations.get("document_navigation_events") if not isinstance(document_events, list) or document_events: raise ValueError("transition observations require zero document navigation events") shell = observations.get("stable_shell") or {} - if not all(isinstance(shell.get(name), dict) for name in ("document_marker", "sidebar_marker", "background")): + stable_fields = ("document_marker", "sidebar_marker", "background") + if any(not isinstance(shell.get(key), dict) for key in stable_fields): raise ValueError("transition observations require pre/post stable shell markers") - if any(values.get("pre") != values.get("post") for values in (shell["document_marker"], shell["sidebar_marker"], shell["background"])): + if any( + not isinstance(values.get(position), str) or not values[position].strip() + for values in (shell[key] for key in stable_fields) + for position in ("pre", "post") + ): + raise ValueError("transition observations require non-empty stable shell markers") + if any(shell[key]["pre"] != shell[key]["post"] for key in stable_fields): raise ValueError("transition observations require stable shell identity and background") + stable_surfaces = shell.get("surfaces") + if not isinstance(stable_surfaces, dict) or stable_surfaces.get("pre") != frames[0].get("surfaces"): + raise ValueError("transition observations require stable shell surface records") + _validate_surface_records(stable_surfaces.get("pre"), "stable shell pre") + _validate_surface_records(stable_surfaces.get("post"), "stable shell post") + if stable_surfaces["pre"] != stable_surfaces["post"] or stable_surfaces["post"] != frames[-1].get("surfaces"): + raise ValueError("transition observations require stable shell surface consistency") if observations.get("prior_body_removed") is not True: raise ValueError("transition observations require prior body removal") loading_frames = [frame for frame in frames if frame.get("loading") is True] @@ -444,6 +481,7 @@ async def cdp_transition(ws_url: str, base_url: str, from_page: str, to_page: st window.__evidencePriorBodyText = (document.querySelector('[data-testid=stMain]') || document.body)?.innerText || ''; const sidebar=document.querySelector('[data-testid=stSidebar]'); if (sidebar) sidebar.dataset.evidenceMarker ||= crypto.randomUUID(); + window.__evidenceSurface = (selector) => { const element=document.querySelector(selector); if (!element) return null; const style=getComputedStyle(element); const rect=element.getBoundingClientRect(); return {backgroundImage:style.backgroundImage, backgroundColor:style.backgroundColor, visibility:style.visibility, display:style.display, opacity:style.opacity, rendered:rect.width>0 && rect.height>0 && style.visibility!=='hidden' && style.display!=='none' && Number(style.opacity)>0}; }; for (const name of ['pushState','replaceState']) { const old=history[name]; history[name]=function(){window.__evidenceEvents.push({type:name,url:String(arguments[2]||location.href)}); return old.apply(this,arguments)}; @@ -456,7 +494,7 @@ async def cdp_transition(ws_url: str, base_url: str, from_page: str, to_page: st deadline = time.time() + 45 index = 0 while time.time() < deadline and (index < frame_count or not observations or observations[-1].get("selected_menu") != to_page): - expression = """(() => { const body=document.body; const text=body?.innerText||''; const main=document.querySelector('[data-testid=stMain]')||body; const mainText=main.innerText||''; const sidebar=document.querySelector('[data-testid=stSidebar]'); const selectedInput=document.querySelector('[data-testid=stRadio] input:checked'); const selected=selectedInput?.closest('label')?.innerText?.trim() || document.querySelector('[aria-current=page]')?.textContent?.trim() || ''; const prior=window.__evidencePriorBodyText||''; return {at_ms:%d, selected_menu:selected, body_text:mainText, prior_body_visible:Boolean(prior)&&mainText.includes(prior), document_marker:window.__evidenceDocumentMarker||'', sidebar_marker:sidebar?.dataset.evidenceMarker||'', background:getComputedStyle(document.body).backgroundColor, loading:/불러오는 중/.test(text)}; })()""" % (index * interval_ms) + expression = """(() => { const body=document.body; const text=body?.innerText||''; const main=document.querySelector('[data-testid=stMain]')||body; const mainText=main.innerText||''; const sidebar=document.querySelector('[data-testid=stSidebar]'); const selectedInput=document.querySelector('[data-testid=stRadio] input:checked'); const selected=selectedInput?.closest('label')?.innerText?.trim() || document.querySelector('[aria-current=page]')?.textContent?.trim() || ''; const prior=window.__evidencePriorBodyText||''; return {at_ms:%d, selected_menu:selected, body_text:mainText, prior_body_visible:Boolean(prior)&&mainText.includes(prior), document_marker:window.__evidenceDocumentMarker||'', sidebar_marker:sidebar?.dataset.evidenceMarker||'', background:getComputedStyle(document.body).backgroundColor, surfaces:{stAppViewContainer:window.__evidenceSurface('[data-testid=stAppViewContainer]'), stSidebar:window.__evidenceSurface('[data-testid=stSidebar]')}, loading:/불러오는 중/.test(text)}; })()""" % (index * interval_ms) result = await cdp.call("Runtime.evaluate", {"expression": expression, "returnByValue": True}) frame = result.get("result", {}).get("value", {}) frame["body_text_hash"] = hashlib.sha256(frame.get("body_text", "").encode()).hexdigest() @@ -466,7 +504,7 @@ async def cdp_transition(ws_url: str, base_url: str, from_page: str, to_page: st await cdp.call("Runtime.evaluate", {"expression": click, "returnByValue": True}) index += 1 await asyncio.sleep(interval_ms / 1000) - final = await cdp.call("Runtime.evaluate", {"expression": "(() => { const main=document.querySelector('[data-testid=stMain]')||document.body; const sidebar=document.querySelector('[data-testid=stSidebar]'); const selectedInput=document.querySelector('[data-testid=stRadio] input:checked'); return {url:location.href, body:main.innerText||'', selected:selectedInput?.closest('label')?.innerText?.trim() || document.querySelector('[aria-current=page]')?.textContent?.trim() || '', document_marker:window.__evidenceDocumentMarker||'', sidebar_marker:sidebar?.dataset.evidenceMarker||'', background:getComputedStyle(document.body).backgroundColor, history_events:window.__evidenceEvents||[], browser_user_agent:navigator.userAgent}; })()", "returnByValue": True}) + final = await cdp.call("Runtime.evaluate", {"expression": "(() => { const main=document.querySelector('[data-testid=stMain]')||document.body; const sidebar=document.querySelector('[data-testid=stSidebar]'); const selectedInput=document.querySelector('[data-testid=stRadio] input:checked'); return {url:location.href, body:main.innerText||'', selected:selectedInput?.closest('label')?.innerText?.trim() || document.querySelector('[aria-current=page]')?.textContent?.trim() || '', document_marker:window.__evidenceDocumentMarker||'', sidebar_marker:sidebar?.dataset.evidenceMarker||'', background:getComputedStyle(document.body).backgroundColor, surfaces:{stAppViewContainer:window.__evidenceSurface('[data-testid=stAppViewContainer]'), stSidebar:window.__evidenceSurface('[data-testid=stSidebar]')}, history_events:window.__evidenceEvents||[], browser_user_agent:navigator.userAgent}; })()", "returnByValue": True}) data = final.get("result", {}).get("value", {}) html = await cdp.call("Runtime.evaluate", {"expression": "document.documentElement.outerHTML", "returnByValue": True}) html_text = html.get("result", {}).get("value", "") @@ -476,7 +514,7 @@ async def cdp_transition(ws_url: str, base_url: str, from_page: str, to_page: st document_events = [{"type": event.get("method"), "params": event.get("params", {})} for event in cdp.events if event.get("method") in ("Page.frameNavigated", "Page.loadEventFired")] pre = observations[0] if observations else {} loading_frames = [frame for frame in observations if frame.get("loading")] - return {"frames": observations, "document_navigation_events": document_events, "history_events": data.get("history_events", []), "stable_shell": {"document_marker": {"pre": pre.get("document_marker"), "post": data.get("document_marker")}, "sidebar_marker": {"pre": pre.get("sidebar_marker"), "post": data.get("sidebar_marker")}, "background": {"pre": pre.get("background"), "post": data.get("background")}}, "loading_observed": bool(loading_frames), "prior_body_removed": bool(loading_frames) and all(frame.get("prior_body_visible") is False for frame in loading_frames), "target_page": to_page, "final_agreement": data.get("selected") == to_page and to_page in data.get("body", ""), "final_url": data.get("url", ""), "browser_user_agent": data.get("browser_user_agent", "")} + return {"frames": observations, "document_navigation_events": document_events, "history_events": data.get("history_events", []), "stable_shell": {"document_marker": {"pre": pre.get("document_marker"), "post": data.get("document_marker")}, "sidebar_marker": {"pre": pre.get("sidebar_marker"), "post": data.get("sidebar_marker")}, "background": {"pre": pre.get("background"), "post": data.get("background")}, "surfaces": {"pre": pre.get("surfaces"), "post": data.get("surfaces")}}, "loading_observed": bool(loading_frames), "prior_body_removed": bool(loading_frames) and all(frame.get("prior_body_visible") is False for frame in loading_frames), "target_page": to_page, "final_agreement": data.get("selected") == to_page and to_page in data.get("body", ""), "final_url": data.get("url", ""), "browser_user_agent": data.get("browser_user_agent", "")} def transition_capture(chrome: str, base_url: str, from_page: str, to_page: str, output_dir: Path, devtools_port: int, diff --git a/tests/test_streamlit_browser_evidence.py b/tests/test_streamlit_browser_evidence.py index b39a248..b413fa4 100644 --- a/tests/test_streamlit_browser_evidence.py +++ b/tests/test_streamlit_browser_evidence.py @@ -4,6 +4,7 @@ import json import ssl import sys from pathlib import Path +from typing import Any from unittest.mock import patch @@ -14,6 +15,21 @@ import streamlit_browser_evidence as evidence SECRET_VALUES = ("synthetic-password", "a" * 64, "cookie-value") +SURFACE_STYLE = { + "backgroundImage": "linear-gradient(rgb(1, 2, 3), rgb(4, 5, 6))", + "backgroundColor": "rgb(255, 255, 255)", + "visibility": "visible", + "display": "block", + "opacity": "1", + "rendered": True, +} + + +def surface_records() -> dict[str, dict[str, object]]: + return { + "stAppViewContainer": dict(SURFACE_STYLE), + "stSidebar": dict(SURFACE_STYLE), + } def test_redact_secrets_removes_values_from_nested_manifest() -> None: @@ -39,7 +55,7 @@ def test_validate_transition_manifest_requires_observations() -> None: evidence.validate_transition_manifest(manifest) -def complete_manifest() -> dict[str, object]: +def complete_manifest() -> dict[str, Any]: return { "mode": "transition", "success": True, @@ -51,6 +67,7 @@ def complete_manifest() -> dict[str, object]: "body_text_hash": "hash", "loading": False, "prior_body_visible": True, + "surfaces": surface_records(), }, { "at_ms": 100, @@ -58,6 +75,7 @@ def complete_manifest() -> dict[str, object]: "body_text_hash": "hash2", "loading": True, "prior_body_visible": False, + "surfaces": surface_records(), }, ], "document_navigation_events": [], @@ -66,6 +84,7 @@ def complete_manifest() -> dict[str, object]: "document_marker": {"pre": "doc-1", "post": "doc-1"}, "sidebar_marker": {"pre": "side-1", "post": "side-1"}, "background": {"pre": "rgb(255, 255, 255)", "post": "rgb(255, 255, 255)"}, + "surfaces": {"pre": surface_records(), "post": surface_records()}, }, "loading_observed": True, "prior_body_removed": True, @@ -164,3 +183,47 @@ def test_transition_manifest_rejects_secret_text() -> None: manifest = {"mode": "transition", "success": False, "error": "synthetic-password"} with pytest.raises(ValueError, match="secret"): evidence.validate_transition_manifest(manifest, SECRET_VALUES) + + +def test_validate_transition_manifest_requires_surface_style_evidence_on_every_frame() -> None: + manifest = complete_manifest() + manifest["observations"]["frames"][1]["surfaces"].pop("stSidebar") + with pytest.raises(ValueError, match="surface"): + evidence.validate_transition_manifest(manifest) + + +def test_validate_transition_manifest_requires_stable_surface_style_evidence() -> None: + manifest = complete_manifest() + manifest["observations"]["stable_shell"]["surfaces"]["post"]["stSidebar"]["opacity"] = "0" + with pytest.raises(ValueError, match="surface"): + evidence.validate_transition_manifest(manifest) + + +@pytest.mark.parametrize( + ("field", "value"), + ( + ("backgroundImage", "none"), + ("backgroundColor", ""), + ("visibility", "hidden"), + ("display", "none"), + ("opacity", "0"), + ("opacity", "NaN"), + ("rendered", False), + ), +) +def test_validate_transition_manifest_rejects_non_visible_surface_styles(field: str, value: object) -> None: + manifest = complete_manifest() + observations = manifest["observations"] + for frame in observations["frames"]: + frame["surfaces"]["stAppViewContainer"][field] = value + observations["stable_shell"]["surfaces"]["pre"]["stAppViewContainer"][field] = value + observations["stable_shell"]["surfaces"]["post"]["stAppViewContainer"][field] = value + with pytest.raises(ValueError, match="surface"): + evidence.validate_transition_manifest(manifest) + + +def test_validate_transition_manifest_rejects_missing_stable_shell_markers() -> None: + manifest = complete_manifest() + manifest["observations"]["stable_shell"]["document_marker"] = {"pre": None, "post": None} + with pytest.raises(ValueError, match="shell"): + evidence.validate_transition_manifest(manifest) -- 2.54.0 From 1ebc53d152159958039c43401b9693a335eb66a5 Mon Sep 17 00:00:00 2001 From: gitea-impl-agent Date: Mon, 5 Oct 2026 08:45:31 +0000 Subject: [PATCH 4/4] docs(evidence): bind PR 10 rework verification --- .../rework-verification-plan.json | 122 ++++++++++++++++++ .../rework-verification.json | 1 + 2 files changed, 123 insertions(+) create mode 100644 docs/evidence/issue-9-dashboard-menu-transition/rework-verification-plan.json create mode 100644 docs/evidence/issue-9-dashboard-menu-transition/rework-verification.json diff --git a/docs/evidence/issue-9-dashboard-menu-transition/rework-verification-plan.json b/docs/evidence/issue-9-dashboard-menu-transition/rework-verification-plan.json new file mode 100644 index 0000000..36bf446 --- /dev/null +++ b/docs/evidence/issue-9-dashboard-menu-transition/rework-verification-plan.json @@ -0,0 +1,122 @@ +{ + "environment": [ + "os", + "kernel", + "architecture", + "python", + "uv" + ], + "command_env_allowlist": [ + "PATH", + "HOME", + "TMPDIR", + "LANG", + "VIRTUAL_ENV", + "UV_CACHE_DIR" + ], + "secret_env_names": [], + "forbidden_regexes": [], + "hash_paths": [ + "docs/evidence/issue-9-dashboard-menu-transition/README.md", + "docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json", + "docs/evidence/issue-9-dashboard-menu-transition/final-state.jpg", + "docs/evidence/issue-9-dashboard-menu-transition/manifest.json", + "docs/project-structure.md", + "docs/requirements/req-002_dashboard-menu-transition.md", + "docs/runtime-flow.md", + "scripts/streamlit_browser_evidence.py", + "tests/test_streamlit_browser_evidence.py" + ], + "commands": [ + { + "id": "full-suite", + "purpose": "Run the repository full test suite at the rework source revision", + "argv": [ + "uv", + "run", + "--extra", + "dashboard", + "--extra", + "dev", + "pytest" + ], + "expected_exit_code": 0, + "max_output_bytes": 65536, + "claims": [ + "full-suite-result" + ] + }, + { + "id": "browser-evidence-validator", + "purpose": "Validate checked-in authenticated browser evidence, source and artifact hashes", + "argv": [ + "python3", + "-c", + "import hashlib,json,math,pathlib; r=pathlib.Path('.'); e=r/'docs/evidence/issue-9-dashboard-menu-transition'; m=json.loads((e/'manifest.json').read_text()); d=json.loads((e/'browser-observations.json').read_text()); frames=d['frames']; surfaces=('stAppViewContainer','stSidebar'); assert all(hashlib.sha256((r/p).read_bytes()).hexdigest()==h for p,h in m['source']['files_sha256'].items()); assert all(hashlib.sha256((e/p).read_bytes()).hexdigest()==h for p,h in m['artifacts_sha256'].items()); assert all(d['checks'].values()) and not d['document_load_events'] and len(frames)==35; assert all(isinstance(f['previous_body_visible'],bool) for f in frames); assert any(f['loading'] and f['previous_body_visible'] is False for f in frames); valid=lambda x: isinstance(x,dict) and str(x.get('backgroundImage','')).strip().lower() not in ('','none') and bool(str(x.get('backgroundColor','')).strip()) and x.get('visibility')=='visible' and x.get('display')!='none' and math.isfinite(float(x.get('opacity','nan'))) and float(x['opacity'])>0 and x.get('rendered') is True; assert all(valid(f['surfaces'][s]) for f in frames for s in surfaces); ss=d['stable_shell']; assert all(isinstance(ss[k]['pre'],str) and ss[k]['pre'] and ss[k]['pre']==ss[k]['post'] for k in ('document_marker','sidebar_marker','background')); assert ss['surfaces']['pre']==frames[0]['surfaces'] and ss['surfaces']['post']==d['post']['surfaces'] and ss['surfaces']['pre']==ss['surfaces']['post']; print('PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=%d full_document_loads=0 surfaces=2' % len(frames))" + ], + "expected_exit_code": 0, + "max_output_bytes": 8192, + "claims": [ + "browser-evidence-result" + ] + }, + { + "id": "documentation-inventory", + "purpose": "Validate canonical requirement and documentation remediation coverage", + "argv": [ + "python3", + "-c", + "from pathlib import Path\nimport re\nq=Path('docs/requirements/req-002_dashboard-menu-transition.md').read_text(); p=Path('docs/project-structure.md').read_text(); f=Path('docs/runtime-flow.md').read_text()\nrb=re.split(r'(?=^### REQ-002\\.\\d{2}(?!-AC)\\b)',q,flags=re.M)[1:]; ab=re.split(r'(?=^### REQ-002\\.\\d{2}-AC01\\b)',q,flags=re.M)[1:]\nassert len(rb)==len(ab)==9 and all('**Impact**' in x for x in rb) and all('**Cases**' in x and '**State Invariants**' in x for x in ab)\nassert all(x in q for x in ('REQ-001.05-AC01','REQ-001.07-AC01','REQ-001.08-AC01','REQ-001.09-AC01'))\nassert all(x in p for x in ('dashboard/common.py','inject_app_css()','JARCA_COLORS','JARCA_PLOTLY_TEMPLATE','outside the replaceable body'))\nassert all(x in f for x in ('do not initiate or replay financial writes or external calls','explicit page user actions','no new idempotency contract'))\nprint('PASS requirements=9 impact=9 acceptance_criteria=9 cases=9 state_invariants=9 missing=0 ambiguous=0')" + ], + "expected_exit_code": 0, + "max_output_bytes": 8192, + "claims": [ + "documentation-contract-result" + ] + }, + { + "id": "ruff", + "purpose": "Run static lint checks", + "argv": [ + "uv", + "run", + "--extra", + "dashboard", + "--extra", + "dev", + "ruff", + "check", + "dashboard", + "tests", + "scripts" + ], + "expected_exit_code": 0, + "max_output_bytes": 8192, + "claims": [ + "lint-result" + ] + }, + { + "id": "compile", + "purpose": "Compile Python sources and tests", + "argv": [ + "python3", + "-m", + "compileall", + "-q", + "dashboard", + "tests", + "scripts" + ], + "expected_exit_code": 0, + "max_output_bytes": 8192, + "claims": [ + "compile-result" + ] + } + ], + "limitations": [ + "The authenticated browser transition was captured before this clean source commit from byte-identical hashed files; this manifest validates the tracked browser artifacts and their exact source hashes rather than rerunning the browser.", + "Gitea commit status and Actions state are authoritative external readbacks and are not produced by this local manifest." + ] +} diff --git a/docs/evidence/issue-9-dashboard-menu-transition/rework-verification.json b/docs/evidence/issue-9-dashboard-menu-transition/rework-verification.json new file mode 100644 index 0000000..a52c5d3 --- /dev/null +++ b/docs/evidence/issue-9-dashboard-menu-transition/rework-verification.json @@ -0,0 +1 @@ +{"claims":{"browser-evidence-result":["/commands/1/argv","/commands/1/exit_code","/commands/1/expected_exit_code","/commands/1/output","/commands/1/started_at_utc","/commands/1/finished_at_utc"],"compile-result":["/commands/4/argv","/commands/4/exit_code","/commands/4/expected_exit_code","/commands/4/output","/commands/4/started_at_utc","/commands/4/finished_at_utc"],"documentation-contract-result":["/commands/2/argv","/commands/2/exit_code","/commands/2/expected_exit_code","/commands/2/output","/commands/2/started_at_utc","/commands/2/finished_at_utc"],"full-suite-result":["/commands/0/argv","/commands/0/exit_code","/commands/0/expected_exit_code","/commands/0/output","/commands/0/started_at_utc","/commands/0/finished_at_utc"],"lint-result":["/commands/3/argv","/commands/3/exit_code","/commands/3/expected_exit_code","/commands/3/output","/commands/3/started_at_utc","/commands/3/finished_at_utc"]},"commands":[{"argv":["uv","run","--extra","dashboard","--extra","dev","pytest"],"claim_ids":["full-suite-result"],"duration_seconds":89.70536988321692,"exit_code":0,"expected_exit_code":0,"finished_at_utc":"2026-10-05T08:44:42.787954Z","id":"full-suite","max_output_bytes":65536,"output":"........................................................................ [ 47%]\n............................ss.......................................... [ 94%]\n....ss... [100%]\n=============================== warnings summary ===============================\ntests/test_dashboard_auth_server.py:11\n /opt/data/gitea-agent/repositories/jsfamily/jarca-ledger-mcp/worktrees/issue-9/checkout/tests/test_dashboard_auth_server.py:11: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.\n from starlette.testclient import TestClient\n\n-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html\n149 passed, 4 skipped, 1 warning in 82.58s (0:01:22)\n","purpose":"Run the repository full test suite at the rework source revision","started_at_utc":"2026-10-05T08:43:13.082951Z"},{"argv":["python3","-c","import hashlib,json,math,pathlib; r=pathlib.Path('.'); e=r/'docs/evidence/issue-9-dashboard-menu-transition'; m=json.loads((e/'manifest.json').read_text()); d=json.loads((e/'browser-observations.json').read_text()); frames=d['frames']; surfaces=('stAppViewContainer','stSidebar'); assert all(hashlib.sha256((r/p).read_bytes()).hexdigest()==h for p,h in m['source']['files_sha256'].items()); assert all(hashlib.sha256((e/p).read_bytes()).hexdigest()==h for p,h in m['artifacts_sha256'].items()); assert all(d['checks'].values()) and not d['document_load_events'] and len(frames)==35; assert all(isinstance(f['previous_body_visible'],bool) for f in frames); assert any(f['loading'] and f['previous_body_visible'] is False for f in frames); valid=lambda x: isinstance(x,dict) and str(x.get('backgroundImage','')).strip().lower() not in ('','none') and bool(str(x.get('backgroundColor','')).strip()) and x.get('visibility')=='visible' and x.get('display')!='none' and math.isfinite(float(x.get('opacity','nan'))) and float(x['opacity'])>0 and x.get('rendered') is True; assert all(valid(f['surfaces'][s]) for f in frames for s in surfaces); ss=d['stable_shell']; assert all(isinstance(ss[k]['pre'],str) and ss[k]['pre'] and ss[k]['pre']==ss[k]['post'] for k in ('document_marker','sidebar_marker','background')); assert ss['surfaces']['pre']==frames[0]['surfaces'] and ss['surfaces']['post']==d['post']['surfaces'] and ss['surfaces']['pre']==ss['surfaces']['post']; print('PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=%d full_document_loads=0 surfaces=2' % len(frames))"],"claim_ids":["browser-evidence-result"],"duration_seconds":0.13439339399337769,"exit_code":0,"expected_exit_code":0,"finished_at_utc":"2026-10-05T08:44:42.921955Z","id":"browser-evidence-validator","max_output_bytes":8192,"output":"PASS source_hashes=5 artifact_hashes=2 checks=12/12 frames=35 full_document_loads=0 surfaces=2\n","purpose":"Validate checked-in authenticated browser evidence, source and artifact hashes","started_at_utc":"2026-10-05T08:44:42.788428Z"},{"argv":["python3","-c","from pathlib import Path\nimport re\nq=Path('docs/requirements/req-002_dashboard-menu-transition.md').read_text(); p=Path('docs/project-structure.md').read_text(); f=Path('docs/runtime-flow.md').read_text()\nrb=re.split(r'(?=^### REQ-002\\.\\d{2}(?!-AC)\\b)',q,flags=re.M)[1:]; ab=re.split(r'(?=^### REQ-002\\.\\d{2}-AC01\\b)',q,flags=re.M)[1:]\nassert len(rb)==len(ab)==9 and all('**Impact**' in x for x in rb) and all('**Cases**' in x and '**State Invariants**' in x for x in ab)\nassert all(x in q for x in ('REQ-001.05-AC01','REQ-001.07-AC01','REQ-001.08-AC01','REQ-001.09-AC01'))\nassert all(x in p for x in ('dashboard/common.py','inject_app_css()','JARCA_COLORS','JARCA_PLOTLY_TEMPLATE','outside the replaceable body'))\nassert all(x in f for x in ('do not initiate or replay financial writes or external calls','explicit page user actions','no new idempotency contract'))\nprint('PASS requirements=9 impact=9 acceptance_criteria=9 cases=9 state_invariants=9 missing=0 ambiguous=0')"],"claim_ids":["documentation-contract-result"],"duration_seconds":0.09890262316912413,"exit_code":0,"expected_exit_code":0,"finished_at_utc":"2026-10-05T08:44:43.021294Z","id":"documentation-inventory","max_output_bytes":8192,"output":"PASS requirements=9 impact=9 acceptance_criteria=9 cases=9 state_invariants=9 missing=0 ambiguous=0\n","purpose":"Validate canonical requirement and documentation remediation coverage","started_at_utc":"2026-10-05T08:44:42.922902Z"},{"argv":["uv","run","--extra","dashboard","--extra","dev","ruff","check","dashboard","tests","scripts"],"claim_ids":["lint-result"],"duration_seconds":0.1826452910900116,"exit_code":0,"expected_exit_code":0,"finished_at_utc":"2026-10-05T08:44:43.204372Z","id":"ruff","max_output_bytes":8192,"output":"All checks passed!\n","purpose":"Run static lint checks","started_at_utc":"2026-10-05T08:44:43.021868Z"},{"argv":["python3","-m","compileall","-q","dashboard","tests","scripts"],"claim_ids":["compile-result"],"duration_seconds":0.2716978546231985,"exit_code":0,"expected_exit_code":0,"finished_at_utc":"2026-10-05T08:44:43.476143Z","id":"compile","max_output_bytes":8192,"output":"","purpose":"Compile Python sources and tests","started_at_utc":"2026-10-05T08:44:43.204547Z"}],"environment":{"architecture":"aarch64","kernel":"6.18.34+rpt-rpi-v8","os":"Linux","python":"3.13.5","uv":"uv 0.11.6 (aarch64-unknown-linux-musl)"},"excluded_paths":["docs/evidence/issue-9-dashboard-menu-transition/rework-verification-plan.json","docs/evidence/issue-9-dashboard-menu-transition/rework-verification.json"],"files":[{"blob_oid":"bba48c3353a6266b523e79f97f733869b8e9dcbb","path":"docs/evidence/issue-9-dashboard-menu-transition/README.md","sha256":"eb4fefbfce82299c1f5655f613911194362c474c812dc40a2d0f3e1a49948aac"},{"blob_oid":"8a18ae47915acece61ed4ca8372d5ec6e44a51e1","path":"docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json","sha256":"414b62efb2f40483aae4227896040c1b5d887b2c31333f63046e6f34aec2fe8f"},{"blob_oid":"049ee017f96f80388cc2fee39ef452f38a5e93db","path":"docs/evidence/issue-9-dashboard-menu-transition/final-state.jpg","sha256":"cc4e1c28463f033ab1c26d1dc27c9bb93c6fc5495321c0cef73fdb1098221234"},{"blob_oid":"15eb3df527b5bec8d1ee0184460b740ffce4023e","path":"docs/evidence/issue-9-dashboard-menu-transition/manifest.json","sha256":"8190660fce780c072472b9e76e0ad1f3c71865c8aa3450edf332c9d8d8468d91"},{"blob_oid":"0c69b5ab20a46c527e8eb75a870e346e580b7328","path":"docs/project-structure.md","sha256":"2241b1f44ec8933e879c193e4be63dc7aa0fb06c235a8b1b225b1bbce52ca8fd"},{"blob_oid":"44da358850a6c911b9d3072ccf588f99b4f9cefb","path":"docs/requirements/req-002_dashboard-menu-transition.md","sha256":"6f1c7f6fde9c4dc2c001468c8e8aca3dbe198e2ac5ac6e94e53d72f92d3f1688"},{"blob_oid":"2b25cb84c53ecfa7915ab287ef808cd4d2daa65f","path":"docs/runtime-flow.md","sha256":"76141addb868dd1a7c1b27c881813bbd5b3e39915c72771d1a3057f06d6a7d56"},{"blob_oid":"3975fac9d359156e0ddb3c6ce4528f026d45dc8e","path":"scripts/streamlit_browser_evidence.py","sha256":"3cfb449ce1d6fae3abb795727d7ca1bc10dbb759bb811f121abd74fa42993dd7"},{"blob_oid":"b413fa48adc89796fbd994e3540d4cb0ac947433","path":"tests/test_streamlit_browser_evidence.py","sha256":"7f595ccf4009961225328486c19258bb0f7bc7dbfb584c60616860274112724a"}],"generated_at":"2026-10-05T08:44:43.540085Z","limitations":["The authenticated browser transition was captured before this clean source commit from byte-identical hashed files; this manifest validates the tracked browser artifacts and their exact source hashes rather than rerunning the browser.","Gitea commit status and Actions state are authoritative external readbacks and are not produced by this local manifest."],"plan":{"path":"docs/evidence/issue-9-dashboard-menu-transition/rework-verification-plan.json","sha256":"0421786d3b22bc806339681943dd97f3c68b62662fff38a3c2c513bc9b575385"},"redaction":{"checked":true,"violations":[]},"result":"success","schema":"evidence-manifest/v1","source_revision":"f01dc182ec4d9b04e30d1d5bc13652a561332f24","source_tree_clean":true} -- 2.54.0