mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-03 03:12:07 +00:00
tui2: add managed account session commands and parity matrix
This commit is contained in:
1 parent
3769153033
commit
924f32fefc
9 files changed
+264
-2
No files matched your search
@@ -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`
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -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."""
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user