diff --git a/docs/README.md b/docs/README.md index 52f4f440..1a75fea8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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` diff --git a/docs/dev_control_center.md b/docs/dev_control_center.md index b212b3b2..ea6d4966 100644 --- a/docs/dev_control_center.md +++ b/docs/dev_control_center.md @@ -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: diff --git a/docs/tui_v2_legacy_parity_matrix.md b/docs/tui_v2_legacy_parity_matrix.md new file mode 100644 index 00000000..4d69de65 --- /dev/null +++ b/docs/tui_v2_legacy_parity_matrix.md @@ -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 `, list select | Implemented | | +| Search entries | Main > Search Entries | `/` search input or `search ` | 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. diff --git a/memory-update.md b/memory-update.md index 414f76aa..675b2b94 100644 --- a/memory-update.md +++ b/memory-update.md @@ -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`). diff --git a/src/seedpass/core/api.py b/src/seedpass/core/api.py index dd47115a..84fee86b 100644 --- a/src/seedpass/core/api.py +++ b/src/seedpass/core/api.py @@ -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.""" diff --git a/src/seedpass/tui_v2/app.py b/src/seedpass/tui_v2/app.py index df7e0fcd..6cebba4f 100644 --- a/src/seedpass/tui_v2/app.py +++ b/src/seedpass/tui_v2/app.py @@ -301,6 +301,7 @@ def launch_tui2( "relay-reset | sync-now | sync-bg | " "checksum-verify | checksum-update | db-export | db-import | " "totp-export | parent-seed-backup [path] [password] | " + "managed-load [entry_id] | managed-exit | " "doc-export [output_path] | " "link-add [relation] [note] | " "link-rm [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 , " "retry, jump " @@ -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": diff --git a/src/tests/test_core_api_services.py b/src/tests/test_core_api_services.py index f198810d..1f12f0ad 100644 --- a/src/tests/test_core_api_services.py +++ b/src/tests/test_core_api_services.py @@ -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 diff --git a/src/tests/test_tui_v2_action_matrix.py b/src/tests/test_tui_v2_action_matrix.py index 26a274e2..a001a1ab 100644 --- a/src/tests/test_tui_v2_action_matrix.py +++ b/src/tests/test_tui_v2_action_matrix.py @@ -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") diff --git a/src/tests/test_tui_v2_textual_interactions.py b/src/tests/test_tui_v2_textual_interactions.py index c880f8e9..7c3e1274 100644 --- a/src/tests/test_tui_v2_textual_interactions.py +++ b/src/tests/test_tui_v2_textual_interactions.py @@ -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)