# Runtime Flow — Dashboard 관리자 인증 ## 목적과 범위 ASGI startup부터 login, token restoration, invalid-cookie cleanup, explicit logout까지의 실행 순서와 상태 계약을 정의한다. - 포함 범위: config, HTTPS routes, cookie verification, Streamlit gate, cleanup/deletion - 제외 범위: MCP request authentication, DB session/token registry, per-token revocation - 시작 조건: trusted ASGI process receives request - 성공 종료 상태: authenticated Dashboard page executes - 실패 종료 상태: matching cookie deletion and unauthenticated login page ## 흐름 ### startup — ASGI startup - 시작 이벤트: process startup - 선행조건: deployment injects configuration without recording values - 실행 방식: synchronous startup/request handling - 트랜잭션·영속성 경계: no auth persistence; existing financial SQLite is untouched | 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | |---|---|---|---|---|---|---|---| | `startup-01` | Uvicorn | process command | load `dashboard.server:app` | process memory only | none | `startup-02` | `startup-fail` | | `startup-02` | ASGI app | app loaded | mount auth routes and Streamlit runtime | route table only | none | `gate-01` | `startup-fail` | ### login — Credential login - 시작 이벤트: same-origin HTTPS form POST - 선행조건: valid trusted scheme, origin and bounded body - 실행 방식: synchronous request - 트랜잭션·영속성 경계: no DB transaction; cookie is browser state | 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | |---|---|---|---|---|---|---|---| | `login-01` | auth server | POST `/api/auth/login` | validate scheme/origin/body | request only | none | `login-02` | `login-reject` | | `login-02` | authentication | validated form | load three settings, fail closed | server memory | secret provider/environment | `login-03` | `config-fail` | | `login-03` | authentication | configured settings | constant-time compare ID/password | no persistent write | none | `login-04` | `login-fail` | | `login-04` | authentication | valid credential | issue exactly `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti`; fix `iat`/`exp` at login success for default 24 hours or `remember_me=on` 30 days | server memory | none | `login-05` | `login-fail` | | `login-05` | auth server | signed token | set hardened cookie with selected absolute expiry and redirect; later activity never reissues or extends it | browser cookie write | response | `gate-01` | `login-fail` | ### restore — Existing cookie restoration - 시작 이벤트: new request/session with browser cookie - 선행조건: cookie present or absent; current configuration available - 실행 방식: synchronous gate - 트랜잭션·영속성 경계: no server-side session; financial DB begins only after gate success | 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | |---|---|---|---|---|---|---|---| | `gate-01` | dashboard app | page startup | call authentication before DB path/page dispatch | no DB access | none | `gate-02` | `unauthenticated` | | `gate-02` | authentication | current config and cookie | verify signature, exact `v`/`sub=dashboard-admin`/`iat`/`exp`/`jti` claims, current admin-ID/password/signing-secret binding and absolute `exp`; never refresh or extend on restoration, rerun or activity | no session or token write | none | `dashboard-01` | `cleanup-01` | | `dashboard-01` | dashboard app | accepted token | resolve DB path and dispatch requested page | existing SQLite reads/writes | gateway/page code | `authenticated` | `dashboard-fail` | ### cleanup — Invalid cookie cleanup - 시작 이벤트: gate rejects malformed, expired, tampered, claim-invalid, or config-failure cookie - 선행조건: same-origin HTTPS cleanup request - 실행 방식: synchronous request - 트랜잭션·영속성 경계: no DB or token registry write | 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | |---|---|---|---|---|---|---|---| | `cleanup-01` | browser/auth gate | invalid cookie | navigate to `GET /api/auth/cleanup` | request only | auth route | `cleanup-02` | `cleanup-reject` | | `cleanup-02` | auth server | same-origin HTTPS `Referer` | emit matching deletion (`Max-Age=0`) and root redirect | browser cookie deletion | response | `unauthenticated` | `cleanup-reject` | ### logout — Explicit logout - 시작 이벤트: user submits logout form - 선행조건: same-origin HTTPS POST - 실행 방식: synchronous request - 트랜잭션·영속성 경계: no financial DB change and no per-token revocation | 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | |---|---|---|---|---|---|---|---| | `logout-01` | browser | POST `/api/auth/logout` | validate same-origin HTTPS | request only | none | `logout-02` | `logout-reject` | | `logout-02` | auth server | validated request | matching deletion and root redirect | browser cookie deletion | response | `unauthenticated` | `logout-reject` | ## 실패와 복구 | 대상 단계 | 실패 조건 | 추가 쓰기 중단 | 재시도 계약 | 중복 방지 | 보상·수동 복구 | 최종 상태 | |---|---|---|---|---|---|---| | `login-02` | missing/empty/read-error/short secret | yes | fix deployment config | no cookie | operator injects secret | `config-fail` | | `login-03` | wrong ID/password | yes | retry after generic error and 1s delay | no lock | external rate limit | `login-fail` | | `gate-02` | expired, malformed/tampered claims, or admin-ID/password/signing-secret rotation failure | yes | cleanup then login | matching deletion | user logs in again | `unauthenticated` | | `cleanup-02` | non-HTTPS/cross-origin | yes | retry same origin | deletion is idempotent | browser clears manually if needed | `cleanup-reject` | | `logout-02` | non-HTTPS/cross-origin | yes | retry same origin | deletion is idempotent | browser clears manually if needed | `logout-reject` | ### 동시성과 순서 - 동시 실행 허용 여부: independent requests allowed; no application-wide auth lock - 잠금·직렬화 단위: none for authentication; existing SQLite rules remain for financial writes - 순서 보장: config/cookie gate precedes DB path, gateway and page dispatch - 충돌 검출·처리: token is stateless; password/signing-secret/admin-ID rotation invalidates old claims - 만료 불변조건: login 성공 시 고정한 `iat`와 `exp`는 복원·rerun·활동 동안 바뀌지 않으며 token을 재발급하거나 만료를 연장하지 않음 - rotation 불변조건: admin ID, password 또는 signing secret 변경은 이전 설정으로 발급한 모든 token을 무효화하고 `cleanup-01`로 전이 ## 관찰 가능성 | 신호 종류 | 이름 | 발생 단계·조건 | 상관 식별자 | 진단 속성 | 민감정보 처리 | |---|---|---|---|---|---| | event log | `authentication_required` | `gate-01` rejects | non-sensitive correlation ID | event class only | no cookie/token/reason | | event log | `login_succeeded` | `login-05` | non-sensitive correlation ID | TTL class only | no credential/token | | event log | `login_failed` | `login-03` | non-sensitive correlation ID | generic failure class | no ID/password | | event log | `cleanup` | `cleanup-02` | non-sensitive correlation ID | generic cleanup class | no cookie header | | event log | `logout` | `logout-02` | non-sensitive correlation ID | operation result | no token/cookie value | ## 검증 - 정상 흐름: auth server tests plus loopback HTTPS login and fresh session restoration - 실패 흐름: configuration, wrong credential, malformed/tampered/expired cookie and origin rejection tests - 경계 흐름: 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. - 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 `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하는지 검증한다. ## REQ-003 런타임 흐름 ### valuation-register — register/select/correct - 시작 이벤트: account form, MCP call, account/import write with dated evidence - 선행조건: DB initialized; Dashboard path passed auth gate; account exists - 실행 방식: synchronous SQLite transaction - 트랜잭션·영속성 경계: one valuation insert or no write on validation/conflict | 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | |---|---|---|---|---|---|---|---| | `valuation-01` | adapter | typed request | delegate shared service | read account | none | `valuation-02` | `valuation-fail` | | `valuation-02` | account valuation service | basis/amount/scope | validate exact integer, date, purpose, loan definition | no write | none | `valuation-03` | `valuation-fail` | | `valuation-03` | SQLite | valid payload | reuse idempotent row or insert append-only row | account_valuations write | SQLite | `valuation-04` | `valuation-fail` | | `valuation-04` | selector/report | requested basis | select verified leaf exact/as_of; warn candidates | read only | none | `valuation-success` | `valuation-fail` | ### snapshot-preview-close — preview/conditional close - 시작 이벤트: assets page or MCP preview/close - 선행조건: basis date; auth before Dashboard DB; explicit approval for conditional state - 실행 방식: preview synchronous read; close synchronous atomic write - 트랜잭션·영속성 경계: preview no write; close writes run and rows together | 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | |---|---|---|---|---|---|---|---| | `close-01` | assets service | basis/policy | gather verified account/manual items and warnings | read accounts/valuations/manual_assets | none | `close-02` | `close-fail` | | `close-02` | Dashboard/MCP | preview request | return live authority and status | no write | none | `close-end` | `close-fail` | | `close-03` | operator/service | explicit close + approval if needed | enforce conditions and correction reason | transaction begins | none | `close-04` | `close-fail` | | `close-04` | SQLite | approved payload | insert run and immutable snapshot rows | runs/snapshots write | SQLite | `close-end` | `close-fail` | ### import-candidate-reconciliation-monthly-ui — propagation - import updates transactions/account balance then records only dated unverified candidate; reconciliation uses valuation-managed investment without fake transaction; monthly/UI reads shared reports and explicit form actions only. | 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 | |---|---|---|---|---|---|---|---| | `prop-01` | import/account write | real dated balance | `_record_*_valuation_candidate` | account_valuations unverified insert | parser | `prop-02` | `prop-fail` | | `prop-02` | reconciliation service | account baseline | calculate signed ledger with provenance; investment verified => VALUATION_MANAGED | read only | none | `prop-03` | `prop-fail` | | `prop-03` | monthly/UI page | month/basis query | read snapshot/report and render | read only unless explicit button | Streamlit | `prop-end` | `prop-fail` | ## 실패와 복구 추가 | 대상 단계 | 실패 조건 | 추가 쓰기 중단 | 재시도 계약 | 중복 방지 | 보상·수동 복구 | 최종 상태 | |---|---|---|---|---|---|---| | `valuation-03` | idempotency/correction conflict | yes | same payload inspect/reuse; changed payload no blind retry | unique indexes | operator chooses new correction | `valuation-fail` | | `close-03` | missing/future/unverified/unknown or missing approval | yes | preview then explicit approval | close idempotency key | manual evidence review | `close-fail` | | `close-04` | SQLite exception/concurrency | transaction rollback | retry same key after lock resolution | unique run key | restore from backup if needed | `close-fail` | | `prop-01` | no real dated evidence | yes | no candidate write | source hash/date key | wait for evidence | `prop-end` without candidate | ### 동시성과 순서 추가 - 동시 실행 허용 여부: independent reads; SQLite writes serialized by transaction - 잠금·직렬화 단위: account valuation/idempotency and snapshot close transaction - 순서 보장: auth → DB → read/preview → explicit approval → close; latest run selected after correction - 충돌 검출·처리: unique idempotency/single correction indexes and explicit error codes ## 관찰 가능성 추가 | 신호 종류 | 이름 | 발생 단계·조건 | 상관 식별자 | 진단 속성 | 민감정보 처리 | |---|---|---|---|---|---| | result/event | `valuation_registered` | `valuation-03` success | request/idempotency key surrogate | basis/kind/status only | no source secret | | result/event | `snapshot_previewed` | `close-02` | request correlation | basis/status/warning codes | no account number | | result/event | `snapshot_closed` | `close-04` | snapshot_run_id | status/count/formula | evidence redacted | | result/event | `valuation_conflict` | validation/idempotency failure | correlation ID | error_code only | no payload secrets | ## REQ-003 검증 - 정상 흐름: `tests/test_account_valuations.py`, `tests/test_asset_snapshots.py`, `tests/test_contract_regression.py` - 실패 흐름: conflict, conditional approval, validation tests - 경계 흐름: exact/as_of, stale/carry-forward, correction/re-close, empty fallback - 동시성 흐름: SQLite unique/idempotency and transaction rollback review - 운영 진입점과 기대 결과: Dashboard authenticated `자산/마감`/`계좌`, MCP tools; preview leaves row count unchanged, explicit close returns run id ### canonical transition and failure semantics `valuation-02` validates principal/interest/fee, source_ref account-scoped identity, parent scope, cyclic/self parent, one child, reason, and key/source mismatch before `valuation-03`; each failure is no-write with `validation_error`, `idempotency_conflict`, or `correction_conflict`. `close-01` computes actual valuation_date, age, carry, verification, reconciliation completeness, investment unavailable, and loan purpose/definition/ambiguity. `close-02` returns live authority, `close_status`, warnings, and exclusions without writing; `close-03` records explicit run/approval/correction; `close-04` atomically persists immutable rows. Drift, missing approval, unsafe evidence, and SQLite failure transition to `close-fail` with no partial write. The close response exposes top-level run, approval, correction, warnings, `excluded_items`, and `carried_items`; its nested provenance exposes actual valuation date, age, carry, verification, approval/correction, warnings, and redacted exclusions. Live/closed/timeseries/monthly read models propagate the same meanings in their documented top-level and nested provenance fields. `monthly_source_snapshot_id` is a read-model alias of `snapshot_run_id`, not a persistence field. `last_balance` is external current comparison rather than opening evidence; missing balance_after/opening evidence transitions to `INCOMPLETE_CALCULATION`. No fake writes are made for unavailable investment or valuation-managed reconciliation. ### C13 database lifecycle transitions `auth-before-DB` is the first Dashboard boundary. `write-first-use-01` explicitly calls `init_db()` and may migrate. `db-read-01` calls `read_db()` in SQLite `mode=ro`; a missing database becomes `db_not_initialized`. `db-read-02` succeeds without mkdir, migrate, or commit. An initialized database's schema or SQLite read failure becomes generic `db_error`, never `db_not_initialized`, and raw engine details are not exposed. ## D06 runtime decision guards - **Current-cache no rollback:** a late historical valuation may update historical authority but cannot roll back the newer current cache (`last_balance`); current display and historical basis selection are separate decisions. - **Reconciliation decision:** return `reconciliation complete` only when required ledger/evidence inputs are complete. If required evidence is absent or unreadable, return `reconciliation unavailable` with provenance and do not infer a balance or create a transaction. - **Preview drift revalidation:** preview is read-only and records the authority/evidence fingerprint observed. Before close writes, revalidate current authority, approval policy, exclusions, carried items, and the preview fingerprint. Drift routes back to preview/review with a stale-preview or conflict decision and no partial write; only a revalidated preview reaches `close-04`. The fingerprint input is exactly basis date, selection policy, included and excluded evidence state hashes, warnings (excluding the informational existing-run warning), and candidate state hashes. The preview returns `preview_fingerprint`; close recomputes it before any insert. A changed authority returns `authority_drift`, and the transaction produces no partial write. An idempotent close is replayed only for equal request fingerprints. Replay reconstructs the response from the persisted run/evidence (no replay-only reused payload), while a different payload returns `idempotency_conflict`. Recursive close metadata is allowlisted and redacted before it reaches Dashboard/MCP. Dashboard missing-DB reads return stable unavailable `db_not_initialized` without create/migrate/commit. The missing DB stable unavailable contract is no-create. Initialized read failures return `db_error`; successful initialized reads are read-only. The initialized DB read failure is db_error and initialized read has no write. Reconciliation requires opening evidence and coverage through the requested basis; otherwise it reports `INCOMPLETE_CALCULATION`/`reconciliation unavailable` with provenance, no inferred balance, and no fake transaction.