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.
This commit is contained in:
2026-10-05 00:49:28 +00:00
parent ffd943fe65
commit 96e11812ce
13 changed files with 2166 additions and 51 deletions
@@ -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.
@@ -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."
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@@ -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."
]
}
+38
View File
@@ -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에는 `<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`까지를 따른다.
+18 -3
View File
@@ -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
@@ -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=<known>`은 해당 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`
+30
View File
@@ -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하는지 검증한다.