diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index a40150a..70740ab 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -553,7 +553,31 @@ _Started: 2026-06-07_
2. `import * as api from '../api/client.js'` in any existing consumer file resolves all previously-exported names without modification to that consumer file
3. No Pydantic model definition appears more than once across `api/admin/`, `api/documents/`, and `api/auth/` sub-packages — shared types live in a dedicated module
4. `PERF-01` packages are installed and `npm list` confirms `vue@^3.5.x`, `vite@^6.x`, `@vueuse/core@^14.x`, `sortablejs`, `@tailwindcss/forms`, `rollup-plugin-visualizer` are present
-**Plans**: TBD
+**Plans**: 8 plans (3 waves)
+
+**Wave 0** — Foundation (parallel)
+
+- [ ] 08-01-PLAN.md — CR-01/02/03 test stubs (3 xfail stubs in test_auth.py) + Wave 0 scaffolds for regression detection
+- [ ] 08-02-PLAN.md — `api/schemas.py` creation + `CloudConnectionOut` migration from admin.py (MUST precede admin split)
+
+**Wave 1** *(blocked on Wave 0)* — Phase 7.1 completion (frontend only — backend already implemented)
+
+- [ ] 08-03-PLAN.md — `useToastStore` stub (`frontend/src/stores/toast.js`) + CR test promotion + SettingsAccountTab.vue + TotpEnrollment.vue inline toast replacement
+
+**Wave 2** *(blocked on Wave 1)* — Backend decomposition + frontend (parallel)
+
+- [ ] 08-04-PLAN.md — Split `api/admin.py` → `api/admin/` package: users.py, quotas.py, ai.py, shared.py, __init__.py (CODE-01)
+- [ ] 08-05-PLAN.md — Split `api/documents.py` → `api/documents/` package: upload.py, crud.py, content.py, shared.py, __init__.py (CODE-02)
+- [ ] 08-06-PLAN.md — Split `api/auth.py` → `api/auth/` package: tokens.py, totp.py, password.py, sessions.py, shared.py, __init__.py (CODE-03)
+- [ ] 08-07-PLAN.md — Frontend `client.js` decomposition: utils.js + 7 domain modules + barrel rewrite (CODE-04)
+- [ ] 08-08-PLAN.md — PERF-01 dependency bump + tailwind/vite config wiring + requirements.txt exact pinning (PERF-01, D-17)
+
+**Cross-cutting constraints:**
+
+- Sub-routers MUST have NO prefix on `APIRouter()` — only package `__init__.py` carries the prefix (Plans 04, 05, 06)
+- `api/schemas.py` must exist before admin split — `api/cloud.py` imports `CloudConnectionOut` from admin (Plan 02 before Plan 04)
+- `request()` moves to `utils.js` for circular-import avoidance; `client.js` re-exports it — zero consumer changes (Plan 07)
+- Every admin sub-module handler retains `Depends(get_current_admin)` explicitly — never omit (Plans 04)
---
diff --git a/.planning/STATE.md b/.planning/STATE.md
index dbfa283..f0748bd 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -2,14 +2,14 @@
gsd_state_version: 1.0
milestone: v0.2
milestone_name: Phases
-current_phase: Phase 8 (context gathered)
-status: planning
-last_updated: "2026-06-07"
-last_activity: 2026-06-07 — Phase 8 context gathered
+current_phase: "Phase 8: Stack Upgrade & Backend Decomposition"
+status: planned
+last_updated: "2026-06-08T00:00:00.000Z"
+last_activity: 2026-06-08 — Phase 8 planned (8 plans, 3 waves)
progress:
total_phases: 17
completed_phases: 13
- total_plans: 65
+ total_plans: 73
completed_plans: 65
percent: 76
---
@@ -17,22 +17,22 @@ progress:
# Project State
**Project:** DocuVault
-**Status:** Phase 8 context gathered — ready to plan
+**Status:** Phase 8 planned — ready to execute
**Current Phase:** Phase 8: Stack Upgrade & Backend Decomposition
-**Last Updated:** 2026-06-07
+**Last Updated:** 2026-06-08
## Current Position
-Phase: Phase 8 (context gathered, not started)
+Phase: Phase 8 (planned, 8 plans ready)
Plan: —
-Status: Context captured — 8 areas discussed, 17 decisions locked
-Last activity: 2026-06-07 — Phase 8 context gathered
+Status: Ready to execute — 8 plans across 3 waves
+Last activity: 2026-06-08 — Phase 8 planned
## Phase Status
| Phase | Requirements | Status |
|-------|-------------|--------|
-| 8. Stack Upgrade & Backend Decomposition | PERF-01, CODE-01, CODE-02, CODE-03, CODE-04, CODE-08 | Not started |
+| 8. Stack Upgrade & Backend Decomposition | PERF-01, CODE-01, CODE-02, CODE-03, CODE-04, CODE-08 | Planned (8 plans) |
| 9. Admin Panel Rearchitecture | ADMIN-08..12, CODE-06, CODE-09 | Not started |
| 10. UX & Interaction | UX-01..14, CODE-05 | Not started |
| 11. Visual Design, Responsive Layout & Cleanup | VISUAL-01..04, RESP-01..05, CODE-07, PERF-02, PERF-03 | Not started |
@@ -43,7 +43,7 @@ Last activity: 2026-06-07 — Phase 8 context gathered
|---|---|
| Phases complete | 0 / 4 |
| Requirements mapped | 40 / 40 |
-| Plans written | 0 |
+| Plans written | 8 |
| Plans complete | 0 |
## Accumulated Context
@@ -82,7 +82,7 @@ _Updated at each phase transition._
| Field | Value |
|---|---|
-| Last session | 2026-06-07 — v0.2 roadmap created; 4 phases defined |
-| Next action | /gsd:plan-phase 8 |
+| Last session | 2026-06-08 — Phase 8 planned; 8 plans created |
+| Next action | /gsd:execute-phase 8 |
| Pending decisions | None |
-| Resume file | .planning/ROADMAP.md — Phase 8 detail section |
+| Resume file | .planning/phases/08-stack-upgrade-backend-decomposition/ |
diff --git a/.planning/phases/08-stack-upgrade-backend-decomposition/08-01-PLAN.md b/.planning/phases/08-stack-upgrade-backend-decomposition/08-01-PLAN.md
new file mode 100644
index 0000000..7add89f
--- /dev/null
+++ b/.planning/phases/08-stack-upgrade-backend-decomposition/08-01-PLAN.md
@@ -0,0 +1,129 @@
+---
+phase: 08-stack-upgrade-backend-decomposition
+plan: 01
+type: execute
+wave: 0
+depends_on: []
+files_modified:
+ - backend/tests/test_auth.py
+autonomous: true
+requirements: [CR-01, CR-02, CR-03]
+tags: [tests, session-revocation, wave-0]
+must_haves:
+ truths:
+ - "Three new pytest tests exist for CR-01, CR-02, CR-03 as xfail stubs"
+ - "Stubs name the exact behavior they cover so executor of plan 08-03 can promote them"
+ - "Running pytest -v shows three new tests with status xfail (not error, not pass)"
+ artifacts:
+ - path: "backend/tests/test_auth.py"
+ provides: "Three new xfail test stubs for session revocation on privilege change"
+ contains: "test_change_password_revokes_other_sessions"
+ key_links:
+ - from: "backend/tests/test_auth.py"
+ to: "backend/api/auth.py change_password / enable_totp / disable_totp"
+ via: "test exercises real handler via httpx.AsyncClient"
+ pattern: "client.post\\(\"/api/auth/(change-password|totp/enable|totp)\""
+---
+
+
+Create Wave 0 test stubs (xfail) for CR-01, CR-02, CR-03. These tests will be promoted to passing in plan 08-03 after the `useToastStore` stub and frontend wiring are complete. The backend implementation for all three is ALREADY in place (verified in RESEARCH.md §"Wave 1: Session Revocation"); these stubs lock the expected behavior contract before any refactoring touches `api/auth.py`.
+
+Purpose: Anti-regression Nyquist scaffold — when plan 08-06 splits `api/auth.py` into `api/auth/` package, these promoted tests guarantee the session revocation logic still works through the new module structure.
+
+Output: Three xfail-marked tests added to `backend/tests/test_auth.py`.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-VALIDATION.md
+@backend/api/auth.py
+@backend/services/auth.py
+@backend/tests/test_auth.py
+@backend/tests/conftest.py
+
+
+
+
+
+ Task 1: Add three xfail stubs for CR-01/CR-02/CR-03 to test_auth.py
+ backend/tests/test_auth.py
+
+ - backend/tests/test_auth.py (read the full file to discover existing fixtures, login helper pattern, and how authenticated requests are issued)
+ - backend/tests/conftest.py (locate `auth_user` fixture, `client` fixture, and `auth_limiter` reset hook)
+ - backend/api/auth.py lines 478-540 (change_password — confirm response shape includes `sessions_revoked`)
+ - backend/api/auth.py lines 579-636 (enable_totp — confirm response shape)
+ - backend/api/auth.py lines 641-682 (disable_totp — confirm response shape)
+ - backend/services/auth.py lines 250-273 (revoke_all_refresh_tokens signature with skip_token_hash)
+
+
+ - Test `test_change_password_revokes_other_sessions`: register user, log in twice to obtain TWO refresh-token rows in DB (call them session A and session B). Using session A's access token, POST `/api/auth/change-password` with the correct current password and a new valid password. Assert: response 200, body contains `"sessions_revoked": 1` (session B revoked, session A preserved via `skip_token_hash`); session A's refresh token is still usable on POST `/api/auth/refresh`; session B's refresh token fails on POST `/api/auth/refresh` with 401.
+ - Test `test_enable_totp_revokes_other_sessions`: register user, log in twice (sessions A and B). Using session A, POST `/api/auth/totp/setup` to obtain a `provisioning_uri`, derive a valid TOTP code via `pyotp.TOTP(secret).now()`, POST `/api/auth/totp/enable` with that code. Assert: response 200, body contains `"sessions_revoked": 1`, session B refresh fails 401, session A refresh succeeds.
+ - Test `test_disable_totp_revokes_other_sessions`: register user, enable TOTP, then log in twice with TOTP (sessions A and B). Using session A, DELETE `/api/auth/totp` with the current TOTP code. Assert: response 200, body contains `"sessions_revoked": 1`, session B refresh fails 401, session A refresh succeeds.
+
+
+Append three test functions to `backend/tests/test_auth.py`, each decorated with `@pytest.mark.xfail(reason="Wave 0 stub — promoted to passing in 08-03", strict=False)`. Use the existing test patterns in the file (httpx.AsyncClient async fixtures, `auth_user` fixture from conftest, `await client.post(...)`). The three function names MUST be exactly:
+ - `async def test_change_password_revokes_other_sessions(client, db_session)`
+ - `async def test_enable_totp_revokes_other_sessions(client, db_session)`
+ - `async def test_disable_totp_revokes_other_sessions(client, db_session)`
+Inside each test body, write the full assertion logic per the `` block — do NOT leave them as bare `pass` stubs. The tests SHOULD pass right now (backend already implemented per RESEARCH.md), but `strict=False` xfail allows either xpassed or xfailed without erroring the suite. Plan 08-03 will remove the `@pytest.mark.xfail` decorator and confirm they pass cleanly. Use `pyotp.TOTP(secret).now()` for TOTP code generation; import pyotp at the top of the test file if not already imported. For the login-twice pattern, use distinct `User-Agent` headers on each login to ensure separate refresh-token rows; capture `response.cookies['refresh_token']` for each session.
+
+
+ cd backend && pytest tests/test_auth.py::test_change_password_revokes_other_sessions tests/test_auth.py::test_enable_totp_revokes_other_sessions tests/test_auth.py::test_disable_totp_revokes_other_sessions --tb=no -q
+
+
+ - `grep -c "def test_change_password_revokes_other_sessions" backend/tests/test_auth.py` returns 1
+ - `grep -c "def test_enable_totp_revokes_other_sessions" backend/tests/test_auth.py` returns 1
+ - `grep -c "def test_disable_totp_revokes_other_sessions" backend/tests/test_auth.py` returns 1
+ - `grep -c "pytest.mark.xfail" backend/tests/test_auth.py` increased by at least 3 vs. pre-change baseline
+ - Pytest output for these three test IDs shows status `XPASS` or `XFAIL` — never `ERROR` and never `FAILED`
+ - Body of each test asserts `data["sessions_revoked"] == 1` (not `>= 1`, not `is not None`)
+ - Body of each test verifies the OTHER session's refresh token returns 401 on `/api/auth/refresh`
+ - Body of each test verifies the CURRENT session's refresh token returns 200 on `/api/auth/refresh`
+
+ Three xfail-marked tests appended to test_auth.py with full assertion logic exercising the documented CR-01/02/03 contracts; pytest collects and runs them without error.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| client → POST /api/auth/change-password | Authenticated user requests password change; backend must revoke all OTHER refresh tokens |
+| client → POST /api/auth/totp/enable | Authenticated user enables TOTP; backend must revoke all OTHER refresh tokens |
+| client → DELETE /api/auth/totp | Authenticated user disables TOTP; backend must revoke all OTHER refresh tokens |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-08-01-01 | Tampering | Test fixture isolation | mitigate | Each test creates its own user via `auth_user` fixture; tests do not share session state |
+| T-08-01-02 | Repudiation | xfail strict mode | mitigate | `strict=False` permits XPASS without erroring; plan 08-03 removes the marker and runs strict |
+| T-08-01-SC | Supply Chain | pytest, pyotp | accept | Already pinned in requirements.txt; this plan adds no new packages |
+
+
+
+- `cd backend && pytest tests/test_auth.py --tb=no -q` shows three new tests with xpassed/xfailed status (no errors)
+- Full backend suite still green: `cd backend && pytest -v` — zero new failures vs. baseline
+- File diff shows only additions to `backend/tests/test_auth.py` (no other files touched)
+
+
+
+- Three new xfail tests exist with the exact names listed
+- Each test body contains real assertion logic (not `pass` or `pytest.skip`)
+- Pytest collects all three without error
+
+
+
diff --git a/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-PLAN.md b/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-PLAN.md
new file mode 100644
index 0000000..e273581
--- /dev/null
+++ b/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-PLAN.md
@@ -0,0 +1,172 @@
+---
+phase: 08-stack-upgrade-backend-decomposition
+plan: 02
+type: execute
+wave: 0
+depends_on: []
+files_modified:
+ - backend/api/schemas.py
+ - backend/api/cloud.py
+autonomous: true
+requirements: [CODE-08]
+tags: [shared-schemas, cross-package, prerequisite, wave-0]
+must_haves:
+ truths:
+ - "backend/api/schemas.py exists and defines CloudConnectionOut with field_validator coerce_id_to_str"
+ - "backend/api/cloud.py imports CloudConnectionOut from api.schemas (not api.admin)"
+ - "All cloud endpoints continue to return the same JSON shape as before"
+ - "CloudConnectionOut definition appears exactly once across the entire backend tree"
+ artifacts:
+ - path: "backend/api/schemas.py"
+ provides: "Cross-package Pydantic response models (D-10 destination for shared schemas)"
+ contains: "class CloudConnectionOut"
+ - path: "backend/api/cloud.py"
+ provides: "Cloud endpoint router updated to import from api.schemas"
+ contains: "from api.schemas import CloudConnectionOut"
+ key_links:
+ - from: "backend/api/cloud.py"
+ to: "backend/api/schemas.py"
+ via: "import statement at module top"
+ pattern: "from api.schemas import CloudConnectionOut"
+ - from: "backend/api/admin.py"
+ to: "backend/api/schemas.py"
+ via: "import statement to be added in plan 08-04 admin split"
+ pattern: "from api.schemas import CloudConnectionOut"
+---
+
+
+Create the new `backend/api/schemas.py` module and move `CloudConnectionOut` into it. Update `backend/api/cloud.py` to import from the new location. This MUST happen BEFORE plan 08-04 splits `api/admin.py` — otherwise the admin-split plan would break `cloud.py`'s `from api.admin import CloudConnectionOut` import.
+
+Purpose: Eliminate the cross-package coupling between `api/cloud.py` and `api/admin.py` (RESEARCH.md §"Pitfall 3"). Establishes the `api/schemas.py` module that plan 08-04 will continue populating with any models discovered to be shared.
+
+Output: New `backend/api/schemas.py` with `CloudConnectionOut`; one updated import in `backend/api/cloud.py`.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md
+@backend/api/admin.py
+@backend/api/cloud.py
+
+
+
+
+
+class CloudConnectionOut(BaseModel):
+ id: str
+ provider: str
+ display_name: str
+ status: str
+ connected_at: datetime
+ server_url: Optional[str] = None
+ connection_username: Optional[str] = None
+ model_config = {"from_attributes": True}
+
+ @field_validator("id", mode="before")
+ @classmethod
+ def coerce_id_to_str(cls, v) -> str:
+ return str(v)
+
+
+from api.admin import CloudConnectionOut
+
+
+from api.schemas import CloudConnectionOut
+
+
+
+
+
+
+ Task 1: Create backend/api/schemas.py with CloudConnectionOut
+ backend/api/schemas.py
+
+ - backend/api/admin.py lines 198-223 (current CloudConnectionOut definition — must be copied verbatim including the field_validator)
+ - backend/api/__init__.py (confirm it exists as a package marker; do not modify)
+ - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md section "backend/api/schemas.py" (exact pattern template)
+
+
+Create `backend/api/schemas.py` as a new file. Add a `from __future__ import annotations` header. Add a module docstring: `"""Cross-package Pydantic response models. Models here are used by 2+ API packages and cannot live in a single package without creating circular imports (D-10, RESEARCH.md Pitfall 3)."""`. Add imports: `from datetime import datetime`, `from typing import Optional`, `from pydantic import BaseModel, field_validator`. Then define `class CloudConnectionOut(BaseModel)` with the exact field set from `backend/api/admin.py` lines ~198-223 (per D-10): `id: str`, `provider: str`, `display_name: str`, `status: str`, `connected_at: datetime`, `server_url: Optional[str] = None`, `connection_username: Optional[str] = None`, `model_config = {"from_attributes": True}`, and the `@field_validator("id", mode="before") coerce_id_to_str` classmethod that returns `str(v)`. Add a class-level docstring noting SEC-08: `credentials_enc` deliberately excluded, moved from `api/admin.py`, used by `api/cloud.py` and `api/admin/`. Do NOT remove the class from `api/admin.py` in this task — task 2 handles the import switch in cloud.py, and plan 08-04 will delete the original definition during the admin split.
+
+
+ cd backend && python -c "from api.schemas import CloudConnectionOut; obj = CloudConnectionOut.model_validate({'id': 'abc-123', 'provider': 'google_drive', 'display_name': 'Test', 'status': 'ACTIVE', 'connected_at': '2026-06-07T00:00:00'}); assert obj.id == 'abc-123' and obj.provider == 'google_drive'"
+
+
+ - File `backend/api/schemas.py` exists
+ - `grep -c "^class CloudConnectionOut" backend/api/schemas.py` returns 1
+ - `grep -c "coerce_id_to_str" backend/api/schemas.py` returns 1
+ - `grep -c "from_attributes" backend/api/schemas.py` returns 1
+ - `python -c "from api.schemas import CloudConnectionOut"` from inside `backend/` exits 0 with no output
+ - All seven fields (id, provider, display_name, status, connected_at, server_url, connection_username) are present per `grep -c " [a-z_]*: " backend/api/schemas.py` ≥ 7
+
+ schemas.py exists, importable, CloudConnectionOut validates a sample dict, original admin.py class is still in place (deletion deferred to plan 08-04).
+
+
+
+ Task 2: Switch backend/api/cloud.py import to api.schemas
+ backend/api/cloud.py
+
+ - backend/api/cloud.py line 35 (current `from api.admin import CloudConnectionOut`)
+ - backend/api/cloud.py top imports block (confirm no other re-exports from `api.admin` exist)
+
+
+In `backend/api/cloud.py`, replace the single line `from api.admin import CloudConnectionOut` with `from api.schemas import CloudConnectionOut`. Do not change any other imports or any code below. After this edit, run the full test suite to confirm cloud endpoints still respond with the identical JSON shape (existing `tests/test_cloud.py` covers admin response surface; if not, the broader `pytest -v` smoke covers the admin list-cloud-connections endpoint too).
+
+
+ cd backend && grep -c "from api.admin import CloudConnectionOut" api/cloud.py; cd backend && grep -c "from api.schemas import CloudConnectionOut" api/cloud.py; cd backend && pytest tests/test_cloud.py -x -v 2>&1 | tail -20
+
+
+ - `grep -c "from api.admin import CloudConnectionOut" backend/api/cloud.py` returns 0
+ - `grep -c "from api.schemas import CloudConnectionOut" backend/api/cloud.py` returns 1
+ - `cd backend && pytest tests/test_cloud.py -x` exits 0
+ - `cd backend && pytest tests/test_admin.py -x -k "cloud"` exits 0 (admin endpoints that serialize CloudConnectionOut continue to work because the original definition in admin.py is still untouched at this point)
+
+ cloud.py imports CloudConnectionOut from api.schemas; tests for cloud endpoints and any admin endpoints that surface cloud connections continue to pass.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| api/cloud.py → api/schemas.py | Module-load-time import; no runtime data crosses this boundary other than a class reference |
+| Admin list-cloud-connections endpoint → CloudConnectionOut serialization | SEC-08 requires `credentials_enc` to be deliberately excluded from this model |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-08-02-01 | Information Disclosure | CloudConnectionOut schema | mitigate | Field list must NOT include `credentials_enc`; field set is restricted to the 7 fields documented in PATTERNS.md |
+| T-08-02-02 | Tampering | id coercion | mitigate | Preserve `@field_validator("id", mode="before") coerce_id_to_str` so UUID→str conversion behavior is byte-identical to the old definition |
+| T-08-02-03 | Denial of Service | Duplicate class definitions | mitigate | Plan 08-04 removes the old definition from api/admin.py during the admin split; until then both definitions coexist but only the schemas.py version is imported by cloud.py |
+| T-08-02-SC | Supply Chain | No new packages | accept | This plan installs zero new packages |
+
+
+
+- `cd backend && pytest tests/test_cloud.py tests/test_admin.py -x` — zero failures
+- `grep -rn "from api.admin import CloudConnectionOut" backend/` returns no matches (cloud.py was the only consumer)
+- `grep -rn "from api.schemas import CloudConnectionOut" backend/` returns exactly one match (cloud.py)
+- `backend/api/admin.py` still contains the original `class CloudConnectionOut` definition (deletion deferred to plan 08-04)
+
+
+
+- `backend/api/schemas.py` exists with `CloudConnectionOut` defined correctly
+- `backend/api/cloud.py` imports from `api.schemas`
+- Cloud and admin tests continue to pass
+- No other file is modified
+
+
+
diff --git a/.planning/phases/08-stack-upgrade-backend-decomposition/08-03-PLAN.md b/.planning/phases/08-stack-upgrade-backend-decomposition/08-03-PLAN.md
new file mode 100644
index 0000000..697d4ff
--- /dev/null
+++ b/.planning/phases/08-stack-upgrade-backend-decomposition/08-03-PLAN.md
@@ -0,0 +1,228 @@
+---
+phase: 08-stack-upgrade-backend-decomposition
+plan: 03
+type: execute
+wave: 1
+depends_on: [08-01]
+files_modified:
+ - frontend/src/stores/toast.js
+ - frontend/src/components/settings/SettingsAccountTab.vue
+ - frontend/src/components/auth/TotpEnrollment.vue
+ - backend/tests/test_auth.py
+autonomous: true
+requirements: [CR-01, CR-02, CR-03]
+tags: [phase-7.1, toast-stub, session-revocation, frontend]
+must_haves:
+ truths:
+ - "frontend/src/stores/toast.js exists, exports useToastStore, exposes show(message, type, duration)"
+ - "show() is a silent no-op in this phase (Phase 10 will render); calling it never throws"
+ - "SettingsAccountTab.vue calls toastStore.show('Other sessions have been terminated.', 'success') when data.sessions_revoked > 0 in both changePassword and disableTotp handlers"
+ - "TotpEnrollment.vue calls toastStore.show('Other sessions have been terminated.', 'success') when data.sessions_revoked > 0 in confirmEnrollment handler"
+ - "Inline sessionRevokedToast ref + setTimeout + inline toast template blocks are removed from both components"
+ - "CR-01, CR-02, CR-03 tests run as plain passing tests (xfail decorator removed)"
+ artifacts:
+ - path: "frontend/src/stores/toast.js"
+ provides: "Pinia toast store stub with the show() contract Phase 10 must honor"
+ contains: "export const useToastStore"
+ - path: "frontend/src/components/settings/SettingsAccountTab.vue"
+ provides: "Refactored to use toastStore.show() in changePassword + disableTotp handlers"
+ contains: "toastStore.show"
+ - path: "frontend/src/components/auth/TotpEnrollment.vue"
+ provides: "Refactored to use toastStore.show() in confirmEnrollment handler"
+ contains: "toastStore.show"
+ - path: "backend/tests/test_auth.py"
+ provides: "Three session-revocation tests promoted from xfail to passing"
+ contains: "test_change_password_revokes_other_sessions"
+ key_links:
+ - from: "SettingsAccountTab.vue changePassword handler"
+ to: "useToastStore.show"
+ via: "function call when data.sessions_revoked > 0"
+ pattern: "toastStore\\.show\\(['\"]Other sessions have been terminated"
+ - from: "TotpEnrollment.vue confirmEnrollment handler"
+ to: "useToastStore.show"
+ via: "function call when data.sessions_revoked > 0"
+ pattern: "toastStore\\.show\\(['\"]Other sessions have been terminated"
+---
+
+
+Complete the absorbed Phase 7.1 work. The backend code for CR-01/CR-02/CR-03 is already implemented in `backend/api/auth.py` (RESEARCH.md confirmed lines 518, 615, 662). This plan: (1) creates the `useToastStore` Pinia stub per the UI-SPEC.md contract, (2) replaces the inline `sessionRevokedToast` ref + `setTimeout` pattern in `SettingsAccountTab.vue` and `TotpEnrollment.vue` with `toastStore.show(...)` calls, (3) removes the inline toast HTML blocks from both components, and (4) promotes the three CR xfail stubs from plan 08-01 to passing tests.
+
+Purpose: Ship CR-01/CR-02/CR-03 to the user-visible behavior contract that the UI-SPEC.md locks (toast on `sessions_revoked > 0`), with the call signature Phase 10 must honor without modifying any Phase 8 call site.
+
+Output: New toast store, refactored two Vue components, promoted three backend tests.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md
+@.planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md
+@frontend/src/stores/topics.js
+@frontend/src/components/settings/SettingsAccountTab.vue
+@frontend/src/components/auth/TotpEnrollment.vue
+@backend/tests/test_auth.py
+
+
+
+toastStore.show(message: string, type: 'success'|'error'|'info' = 'success', duration: number = 4000): void
+
+
+SettingsAccountTab.vue / changePassword() → on data.sessions_revoked > 0 → toastStore.show('Other sessions have been terminated.', 'success')
+SettingsAccountTab.vue / disableTotp() → on data.sessions_revoked > 0 → toastStore.show('Other sessions have been terminated.', 'success')
+TotpEnrollment.vue / confirmEnrollment() → on data.sessions_revoked > 0 → toastStore.show('Other sessions have been terminated.', 'success')
+
+
+SettingsAccountTab.vue: lines 6, 17, 211, 226-227, 263-264 (sessionRevokedToast ref + setTimeout + inline HTML block)
+TotpEnrollment.vue: lines 6, 15, 149, 175-176 (sessionRevokedToast ref + setTimeout + inline HTML block)
+
+
+
+
+
+
+ Task 1: Create useToastStore Pinia stub at frontend/src/stores/toast.js
+ frontend/src/stores/toast.js
+
+ - frontend/src/stores/topics.js (simplest existing Pinia store — copy the setup-store style with defineStore + named function exports)
+ - .planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md §"useToastStore API Contract" (exact signature, default values, "MUST NOT" rules)
+ - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/stores/toast.js" (full stub template)
+
+
+ - Importing `useToastStore` from `'../stores/toast.js'` resolves successfully
+ - Calling `useToastStore().show('hello')` returns `undefined` and does not throw
+ - Calling `useToastStore().show('hello', 'error')` returns `undefined` and does not throw
+ - Calling `useToastStore().show('hello', 'success', 8000)` returns `undefined` and does not throw
+ - The store does not render any DOM element (verified visually — Phase 10 implements rendering)
+
+
+Create `frontend/src/stores/toast.js`. Use the setup-store style (`defineStore('toast', () => { ... })`). Define a single function `show(message, type = 'success', duration = 4000)` with positional parameters only (the UI-SPEC explicitly forbids object-argument shape `show({message, type})`). The function body is empty (no-op stub). Return `{ show }` so the store exposes the method. Add a top-of-file docstring (JS comment block) stating: Phase 7.1 STUB — Phase 10 (UX-10) fills in the full implementation; the signature `show(message, type, duration)` is locked and Phase 10 must honor it without modifying Phase 8 call sites. Default values match UI-SPEC.md: `type = 'success'` (NOT `'info'`), `duration = 4000`. Export as named export: `export const useToastStore = defineStore(...)`. Do NOT import or depend on any component.
+
+
+ cd frontend && node -e "import('./src/stores/toast.js').then(m => { const s = m.useToastStore; if (typeof s !== 'function') throw new Error('useToastStore not a function'); console.log('ok'); })" 2>&1 | tail -5
+
+
+ - File `frontend/src/stores/toast.js` exists
+ - `grep -c "export const useToastStore" frontend/src/stores/toast.js` returns 1
+ - `grep -c "defineStore('toast'" frontend/src/stores/toast.js` returns 1
+ - `grep -c "function show(message, type = 'success', duration = 4000)" frontend/src/stores/toast.js` returns 1
+ - `cd frontend && npm test -- --run stores/toast 2>/dev/null || true` does not throw an import error
+ - The file contains NO `import` of any Vue component or DOM API
+
+ toast.js exists with the exact UI-SPEC signature; the module imports cleanly; show() is a silent no-op.
+
+
+
+ Task 2: Refactor SettingsAccountTab.vue and TotpEnrollment.vue to use toastStore
+ frontend/src/components/settings/SettingsAccountTab.vue, frontend/src/components/auth/TotpEnrollment.vue
+
+ - frontend/src/components/settings/SettingsAccountTab.vue (full file — current inline toast HTML at lines 5-25, `sessionRevokedToast` ref at line 211, setTimeouts in changePassword at 225-228 and disableTotp at 261-264)
+ - frontend/src/components/auth/TotpEnrollment.vue (full file — current inline toast HTML at lines 5-23, `sessionRevokedToast` ref at line 149, setTimeout in confirmEnrollment at 174-177)
+ - frontend/src/stores/toast.js (the stub created in task 1)
+ - .planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md §"Sessions-Revoked Notification — Interaction Contract" (message copy locked exactly: 'Other sessions have been terminated.')
+
+
+For `frontend/src/components/settings/SettingsAccountTab.vue`:
+ 1. Delete the inline `...
` template block (lines ~5-25 — the fixed-position toast).
+ 2. Delete the line `const sessionRevokedToast = ref(false)` (~line 211).
+ 3. In the `changePassword` handler (~lines 220-230), replace `sessionRevokedToast.value = true; setTimeout(() => { sessionRevokedToast.value = false }, 5000)` with `toastStore.show('Other sessions have been terminated.', 'success')`. Keep the surrounding `if (data.sessions_revoked > 0) { ... }` guard.
+ 4. In the `disableTotp` handler (~lines 258-267), apply the same replacement.
+ 5. Add `import { useToastStore } from '../../stores/toast.js'` at the top with the other imports.
+ 6. Add `const toastStore = useToastStore()` near the other store/ref declarations in `