tui2: add managed account session commands and parity matrix

This commit is contained in:
thePR0M3TH3AN committed 2026-03-02 08:30:59 -05:00
1 parent 3769153033
commit 924f32fefc
9 files changed
+264 -2

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ This directory contains SeedPass project documentation.
- `tui_v2_plan.md`
- `tui_v2_parity_checklist.md`
- `tui_v2_parity_backlog.md`
- `tui_v2_legacy_parity_matrix.md`
- `tui_v2_cutover_decision.md`
- `tui_v2_large_vault_validation.md`
- `security.md`
+11 -1
View File
@@ -28,6 +28,7 @@ Use these docs as inputs, but treat this file as the decision layer:
- TUI v2 plan: `docs/tui_v2_plan.md`
- TUI parity backlog: `docs/tui_v2_parity_backlog.md`
- TUI parity checklist: `docs/tui_v2_parity_checklist.md`
- TUI legacy parity matrix: `docs/tui_v2_legacy_parity_matrix.md`
- Cutover memo: `docs/tui_v2_cutover_decision.md`
- Security readiness: `docs/security_readiness_checklist.md`
- Supply-chain integrity: `docs/supply_chain_release_integrity.md`
@@ -71,7 +72,7 @@ Use these docs as inputs, but treat this file as the decision layer:
### Slice A: “Legacy Parity Closure Matrix”
Deliverables:
- Add `docs/tui_v2_legacy_parity_matrix.md` mapping legacy interactive actions to TUI v2 equivalents (or explicit gaps).
- Add `docs/tui_v2_legacy_parity_matrix.md` mapping legacy interactive actions to TUI v2 equivalents (or explicit gaps). ✅ (initial matrix added)
- Add tests for any currently uncovered high-value legacy actions surfaced by that matrix.
- Update `docs/tui_v2_parity_checklist.md` to remove or narrow “Full legacy workflow coverage: Pending”.
@@ -80,6 +81,15 @@ Exit criteria:
- New tests pass in CI-equivalent run.
- Remaining parity gaps are explicit and severity-ranked.
Progress update (2026-03-02):
- Matrix added: `docs/tui_v2_legacy_parity_matrix.md`
- Managed-account session parity landed in TUI v2:
- `managed-load (optional: entry_id)`
- `managed-exit`
- service wrappers and tests added
- Current next implementation target:
- explicit Nostr sync-state reset/fresh-namespace parity commands
## 6) Working Rule
When priorities conflict:
+69
View File
@@ -0,0 +1,69 @@
# TUI v2 Legacy Parity Matrix
Status: In progress (2026-03-02)
Branch: `beta`
Purpose: map legacy interactive workflows to Textual TUI v2 equivalents and isolate remaining gaps.
## Summary
- Implemented: core daily workflows are covered in TUI v2 (add/retrieve/search/modify/archive, reveal/QR, 2FA board, settings/profile/relay/sync, export/import/checksum paths).
- Partial: a few legacy menu affordances are represented differently in v2 (palette-first vs nested menus).
- Gaps: remaining high-value gaps are mostly around managed-account session operations and a few settings/Nostr maintenance affordances.
## Matrix
| Legacy area/action | Legacy path | TUI v2 path | Status | Notes |
|---|---|---|---|---|
| Add password | Main > Add Entry | `add-password` (palette) | Implemented | Covered by interaction tests. |
| Add TOTP (make/import) | Main > Add Entry > 2FA | `add-totp` (palette) | Implemented | Supports secret/period/digits args. |
| Add SSH | Main > Add Entry | `add-ssh` (palette) | Implemented | |
| Add seed | Main > Add Entry | `add-seed` (palette) | Implemented | |
| Add nostr key | Main > Add Entry | `add-nostr` (palette) | Implemented | |
| Add PGP | Main > Add Entry | `add-pgp` (palette) | Implemented | |
| Add key/value | Main > Add Entry | `add-key-value` (palette) | Implemented | |
| Add managed account entry | Main > Add Entry | `add-managed-account` (palette) | Implemented | |
| Add document | Main > Add Entry | `add-document` (palette) | Implemented | |
| Retrieve entry details | Main > Retrieve Entry | `open <id>`, list select | Implemented | |
| Search entries | Main > Search Entries | `/` search input or `search <q>` | Implemented | |
| List entries | Main > List Entries | center list + pagination | Implemented | |
| Modify entry | Main > Modify Entry | `set-field`, `clear-field`, `notes-*`, `tag-*`, `field-*`, doc editor | Implemented | |
| Archive/unarchive | Retrieve/List actions | `a` key or `archive`/`restore` | Implemented | |
| Entry notes | Entry Actions | `notes-set`, `notes-clear` | Implemented | |
| Entry tags | Entry Actions | `tag-add`, `tag-rm`, `tags-set`, `tags-clear` | Implemented | |
| Custom fields | Entry Actions | `field-add`, `field-rm` | Implemented | |
| Show sensitive reveal | Entry Actions | `v` or `reveal [confirm]` | Implemented | Confirm gates present for high-risk kinds. |
| Show QR | Entry Actions | `g` or `qr [public/private] [confirm]` | Implemented | Includes managed-account + nostr private confirm flow. |
| 2FA codes display | Main > 2FA Codes | `6` toggle board / `2fa-*` commands | Implemented | |
| Link add/remove | N/A or limited | `link-add`, `link-rm` | Implemented | |
| Link traversal | N/A | `l`, `[`, `]`, `o` and `link-*` | Implemented | |
| Profile list/switch/add/remove | Settings > Profiles | `profiles-list`, `profile-switch`, `profile-add`, `profile-remove` | Implemented | |
| Profile rename | Settings > Profiles | `profile-rename` | Implemented | |
| Secret mode toggle | Settings | `setting-secret` | Implemented | |
| Quick unlock toggle | Settings | `setting-quick-unlock` | Implemented | |
| Offline mode toggle | Settings | `setting-offline` | Implemented | |
| Inactivity timeout | Settings | `setting-timeout` | Implemented | |
| KDF mode/iterations | Settings | `setting-kdf-mode`, `setting-kdf-iterations` | Implemented | |
| Relay list/add/remove/reset | Settings > Nostr | `relay-list`, `relay-add`, `relay-rm`, `relay-reset` | Implemented | |
| Sync now/background | Settings > Nostr | `sync-now`, `sync-bg` | Implemented | |
| Verify/generate checksum | Settings | `checksum-verify`, `checksum-update` | Implemented | |
| Export/import database | Settings | `db-export`, `db-import` | Implemented | |
| Export TOTP codes | Settings | `totp-export` | Implemented | |
| Backup/reveal parent seed | Settings | `parent-seed-backup [path] [password]` | Implemented | |
| Display npub | Main utility flow | No direct dedicated command | Partial | Possible via entry reveal for nostr entry; direct utility-style command absent. |
| View archived-only list | Settings/List flow | No dedicated archived-only pane/command | Gap | Archived state visible in list; archive-only view affordance missing. |
| Reset Nostr sync state | Settings > Nostr | No explicit command in v2 palette | Gap | Legacy has explicit reset/fresh namespace operations. |
| Start fresh Nostr namespace | Settings > Nostr | No explicit command in v2 palette | Gap | |
| Load managed-account session | Retrieve managed account action | `managed-load (optional: entry_id)` | Implemented | Added in TUI v2 palette + tests. |
| Exit managed-account session | Main loop when in child profile | `managed-exit` | Implemented | Added in TUI v2 palette + tests. |
## Prioritized Gap Queue
1. Medium: explicit Nostr maintenance actions (`reset sync state`, `fresh namespace`) in TUI v2 palette.
2. Medium: archived-only list/view affordance parity.
3. Low: dedicated utility-style npub display command in v2.
## Suggested implementation order
1. Nostr sync-state reset and fresh-namespace palette commands + tests.
2. Archived-only view filter/command + tests.
+17
View File
@@ -581,3 +581,20 @@
- Pre-slice health verification executed first:
- remote sync check on `origin/beta` (no new remote commits)
- `scripts/run_ci_tests.sh` pass with full suite: `979 passed, 16 skipped`, coverage `85.65%`.
## 2026-03-02 Slice A implementation progress: managed-account session parity
- Added parity matrix doc: `docs/tui_v2_legacy_parity_matrix.md` and linked it from control-center/readme docs.
- Implemented managed-account session controls in TUI v2 palette:
- `managed-load (optional: entry_id)`
- `managed-exit`
- Added core service wrappers in `src/seedpass/core/api.py`:
- `EntryService.load_managed_account(entry_id)`
- `EntryService.exit_managed_account()`
- Added tests:
- `src/tests/test_tui_v2_textual_interactions.py` (session command behavior + validation)
- `src/tests/test_tui_v2_action_matrix.py` (command matrix coverage)
- `src/tests/test_core_api_services.py` (service wrapper calls)
- Validation:
- `pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py src/tests/test_core_api_services.py`
- Result: `86 passed`.
- Updated matrix priorities: next parity target is explicit Nostr maintenance commands (`reset sync state`, `fresh namespace`).
+8
View File
@@ -715,6 +715,14 @@ class EntryService:
with self._lock:
self._manager.handle_display_totp_codes()
def load_managed_account(self, entry_id: int) -> None:
with self._lock:
self._manager.load_managed_account(int(entry_id))
def exit_managed_account(self) -> None:
with self._lock:
self._manager.exit_managed_account()
class ConfigService:
"""Thread-safe wrapper around configuration access."""
+82 -1
View File
@@ -301,6 +301,7 @@ def launch_tui2(
"relay-reset | sync-now | sync-bg | "
"checksum-verify | checksum-update | db-export <path> | db-import <path> | "
"totp-export <path> | parent-seed-backup [path] [password] | "
"managed-load [entry_id] | managed-exit | "
"doc-export [output_path] | "
"link-add <target> [relation] [note] | "
"link-rm <target> [relation] | "
@@ -617,6 +618,7 @@ def launch_tui2(
"- 6 toggle dedicated 2FA board",
"- profile/relay/sync/settings",
"- checksum/db/totp/seed backup",
"- managed-load/managed-exit session",
"- l cycle link relation",
"- [ / ] select link",
"- o open link target",
@@ -1384,7 +1386,7 @@ def launch_tui2(
"setting-kdf-iterations, setting-kdf-mode, "
"relay-list, relay-add, relay-rm, relay-reset, sync-now, sync-bg, "
"checksum-verify, checksum-update, db-export, db-import, "
"totp-export, parent-seed-backup, "
"totp-export, parent-seed-backup, managed-load, managed-exit, "
"reveal [confirm], qr [public|private] [confirm], link-filter, "
"link-next, link-prev, link-open, page-next, page-prev, page <n>, "
"retry, jump <id>"
@@ -2805,6 +2807,85 @@ def launch_tui2(
retry=lambda: self._run_palette_command(cmd),
hint="Press 'x' to retry.",
)
return
if cmd == "managed-load":
if self._service is None:
self._set_status("Entry service unavailable")
return
if len(args) > 1:
self._set_status("Usage: managed-load (optional: entry_id)")
return
if len(args) == 1:
try:
entry_id = int(args[0])
except ValueError:
self._set_status("managed-load entry_id must be an integer")
return
else:
if self._selected_entry_id is None:
self._set_status(
"managed-load requires a selected managed account entry"
)
return
entry_id = int(self._selected_entry_id)
loader = getattr(self._service, "load_managed_account", None)
if not callable(loader):
self._set_status(
"Entry service does not support managed account session loading"
)
return
entry = self._service.retrieve_entry(entry_id)
kind = (
str(entry.get("kind") or entry.get("type") or "").strip().lower()
if isinstance(entry, dict)
else ""
)
if kind != "managed_account":
self._set_status("Selected entry is not a managed account")
return
try:
loader(entry_id)
self._load_entries(query="", reset_page=True)
self._clear_failure()
self._set_status(
f"Loaded managed account session from entry #{entry_id}"
)
except Exception as exc:
self._record_failure(
"managed-load failed",
exc,
retry=lambda: self._run_palette_command(raw),
hint="Press 'x' to retry.",
)
return
if cmd == "managed-exit":
if self._service is None:
self._set_status("Entry service unavailable")
return
if args:
self._set_status("Usage: managed-exit")
return
exiter = getattr(self._service, "exit_managed_account", None)
if not callable(exiter):
self._set_status(
"Entry service does not support managed account session exit"
)
return
try:
exiter()
self._load_entries(query="", reset_page=True)
self._clear_failure()
self._set_status("Exited managed account session")
except Exception as exc:
self._record_failure(
"managed-exit failed",
exc,
retry=lambda: self._run_palette_command(raw),
hint="Press 'x' to retry.",
)
return
if cmd == "edit-doc":
+8
View File
@@ -460,6 +460,14 @@ class TestEntryService:
service.display_totp_codes()
mock_manager.handle_display_totp_codes.assert_called_once()
def test_load_managed_account(self, service, mock_manager):
service.load_managed_account(7)
mock_manager.load_managed_account.assert_called_once_with(7)
def test_exit_managed_account(self, service, mock_manager):
service.exit_managed_account()
mock_manager.exit_managed_account.assert_called_once()
class TestConfigService:
@pytest.fixture
+15
View File
@@ -39,6 +39,8 @@ class MatrixEntryService:
self.secret_mode_enabled = False
self.clipboard_delay = 30
self.clipboard_values: list[str] = []
self.managed_load_calls: list[int] = []
self.managed_exit_calls = 0
def _next_id(self) -> int:
return (max(self._entries.keys()) + 1) if self._entries else 1
@@ -373,6 +375,16 @@ class MatrixEntryService:
self._links[int(entry_id)] = keep
return [dict(link) for link in keep]
def load_managed_account(self, entry_id: int) -> None:
entry = self._entries.get(int(entry_id), {})
kind = str(entry.get("kind", ""))
if kind != "managed_account":
raise ValueError("Entry is not a managed account")
self.managed_load_calls.append(int(entry_id))
def exit_managed_account(self) -> None:
self.managed_exit_calls += 1
def _build_app(*, service: MatrixEntryService | None = None, factory=None):
holder: dict[str, object] = {}
@@ -539,6 +551,9 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
app._run_palette_command("db-import /tmp/seedpass-db.enc")
app._run_palette_command("totp-export /tmp/seedpass-totp.json")
app._run_palette_command("parent-seed-backup")
app._run_palette_command("managed-load 1 2")
app._run_palette_command("managed-load nope")
app._run_palette_command("managed-exit now")
app._run_palette_command("doc-export one two")
app._run_palette_command("filter")
app._run_palette_command("unknown-cmd")
@@ -22,6 +22,8 @@ class FakeEntryService:
self.secret_mode_enabled = False
self.clipboard_delay = 30
self.clipboard_values: list[str] = []
self.managed_load_calls: list[int] = []
self.managed_exit_calls = 0
def _next_id(self) -> int:
return (max(self._entries.keys()) + 1) if self._entries else 1
@@ -349,6 +351,16 @@ class FakeEntryService:
self._links[int(entry_id)] = kept
return [dict(link) for link in kept]
def load_managed_account(self, entry_id: int) -> None:
entry = self._entries.get(int(entry_id), {})
kind = str(entry.get("kind", ""))
if kind != "managed_account":
raise ValueError("Entry is not a managed account")
self.managed_load_calls.append(int(entry_id))
def exit_managed_account(self) -> None:
self.managed_exit_calls += 1
class FakeProfileService:
def __init__(self, profiles: list[str]) -> None:
@@ -1405,6 +1417,39 @@ async def test_tui2_textual_profiles_and_settings_palette_commands() -> None:
)
@pytest.mark.anyio
async def test_tui2_textual_managed_account_session_palette_commands() -> None:
service = FakeEntryService(
[
{"id": 1, "kind": "managed_account", "label": "Acct"},
{"id": 2, "kind": "document", "label": "Doc"},
]
)
app = _build_app(service)
async with app.run_test() as pilot:
await pilot.pause()
app._run_palette_command("managed-load")
await pilot.pause()
assert "Loaded managed account session from entry #1" in _status_text(app)
assert service.managed_load_calls == [1]
app._run_palette_command("managed-exit")
await pilot.pause()
assert "Exited managed account session" in _status_text(app)
assert service.managed_exit_calls == 1
app._run_palette_command("open 2")
app._run_palette_command("managed-load")
await pilot.pause()
assert "Selected entry is not a managed account" in _status_text(app)
app._run_palette_command("managed-load nope")
await pilot.pause()
assert "managed-load entry_id must be an integer" in _status_text(app)
@pytest.mark.anyio
async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
service = FakeEntryService([{"id": 1, "kind": "document", "label": "Doc"}])
@@ -1530,3 +1575,11 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
"Usage: parent-seed-backup (optional: path) (optional: password)"
in _status_text(app)
)
app._run_palette_command("managed-load 1 2")
await pilot.pause()
assert "Usage: managed-load (optional: entry_id)" in _status_text(app)
app._run_palette_command("managed-exit now")
await pilot.pause()
assert "Usage: managed-exit" in _status_text(app)