mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-08 05:42:27 +00:00
tui2: add onboarding flow and session parity affordances
This commit is contained in:
1 parent
5c524b2086
commit
8f7075c39b
6 files changed
+326
-29
No files matched your search
+156
-19
@@ -282,7 +282,7 @@ def launch_tui2(
|
||||
yield Input(
|
||||
placeholder=(
|
||||
"Command palette: help | open <id> | search <q> | "
|
||||
"help-commands | quickstart | stats | "
|
||||
"help-commands | onboarding | quickstart | stats | session-status | lock | unlock <password> | "
|
||||
"filter <kind> | archive | restore | "
|
||||
"archive-filter <active|all|archived> | "
|
||||
"add-password <label> <length> [username] [url] | "
|
||||
@@ -306,7 +306,7 @@ def launch_tui2(
|
||||
"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 | "
|
||||
"managed-load [entry_id] | managed-exit | session-status | lock | unlock <password> | "
|
||||
"doc-export [output_path] | "
|
||||
"link-add <target> [relation] [note] | "
|
||||
"link-rm <target> [relation] | "
|
||||
@@ -382,6 +382,8 @@ def launch_tui2(
|
||||
self._last_status_message = ""
|
||||
self._time_now = time.time
|
||||
self._totp_rows: list[dict[str, Any]] = []
|
||||
self._session_locked = False
|
||||
self._managed_session_entry_id: int | None = None
|
||||
self._totp_tick = self.set_interval(1.0, self._tick_totp_board)
|
||||
try:
|
||||
self._service = (
|
||||
@@ -491,32 +493,41 @@ def launch_tui2(
|
||||
self.query_one("#secret-detail", Static).update(text)
|
||||
|
||||
def _quickstart_text(self) -> str:
|
||||
return self._onboarding_text()
|
||||
|
||||
def _onboarding_text(self) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"Quick Start",
|
||||
"-----------",
|
||||
"Onboarding Quick Start",
|
||||
"----------------------",
|
||||
"",
|
||||
"Welcome to SeedPass TUI v2.",
|
||||
"Your vault currently has no active entries.",
|
||||
"",
|
||||
"Try these first actions in the palette (Ctrl+P):",
|
||||
'- add-password "Site" 16 user https://site.example',
|
||||
'- add-totp "Authenticator" 30 6',
|
||||
'- add-document "Runbook" md "starter notes"',
|
||||
"Step 1: Create your first entry (Ctrl+P then run one command):",
|
||||
'- add-password "Site" 16 user https://site.example (login)',
|
||||
'- add-totp "Authenticator" 30 6 (2FA)',
|
||||
'- add-document "Runbook" md "starter notes" (KB)',
|
||||
"",
|
||||
"Step 2: Inspect and verify",
|
||||
"- open <id> to inspect an entry",
|
||||
"- v to reveal a selected secret, g to render QR when supported",
|
||||
"",
|
||||
"Step 3: Operate and scale",
|
||||
"- stats for vault overview",
|
||||
"- help-commands for full command reference",
|
||||
"- link-add / tag-add / notes-set to build knowledge graph context",
|
||||
"",
|
||||
"Useful navigation:",
|
||||
"- / search, j jump, f kind filter, h archive scope",
|
||||
"- 1/2/3 focus panes, ? keyboard help",
|
||||
"",
|
||||
"Operational shortcuts:",
|
||||
"- stats (vault summary)",
|
||||
"- help-commands (full palette reference)",
|
||||
]
|
||||
)
|
||||
|
||||
def _palette_help_summary(self) -> str:
|
||||
return (
|
||||
"Palette commands: help, help-commands, open, search, filter, "
|
||||
"quickstart, stats, "
|
||||
"onboarding, quickstart, stats, session-status, lock/unlock, "
|
||||
"archive, restore, archive-filter, edit-doc/save-doc/cancel-edit, "
|
||||
"link-add/link-rm/link-filter/link-next/link-prev/link-open, "
|
||||
"add-*, notes/tags/fields, 2fa-*, profile-*, setting-*, relay-*, "
|
||||
@@ -532,7 +543,7 @@ def launch_tui2(
|
||||
"-----------------",
|
||||
"",
|
||||
"Core: help | help-commands | open <id> | jump <id> | search <q>",
|
||||
"Core Ops: quickstart | stats",
|
||||
"Core Ops: onboarding | quickstart | stats | session-status | lock | unlock <password>",
|
||||
"Filters: filter <kind|all> | archive-filter <active|all|archived>",
|
||||
"Pages: page-next | page-prev | page <n>",
|
||||
"",
|
||||
@@ -549,7 +560,7 @@ def launch_tui2(
|
||||
"Settings: setting-secret | setting-offline | setting-quick-unlock | setting-timeout | setting-kdf-iterations | setting-kdf-mode",
|
||||
"Nostr: relay-list | relay-add | relay-rm | relay-reset | npub | nostr-reset-sync-state | nostr-fresh-namespace",
|
||||
"Sync/Utility: sync-now | sync-bg | checksum-verify | checksum-update | db-export | db-import | totp-export | parent-seed-backup",
|
||||
"Sessions: managed-load [entry_id] | managed-exit",
|
||||
"Sessions: managed-load [entry_id] | managed-exit | session-status | lock | unlock <password>",
|
||||
"",
|
||||
"Tip: use '?' for compact keyboard help overlay.",
|
||||
]
|
||||
@@ -661,6 +672,12 @@ def launch_tui2(
|
||||
if fingerprint
|
||||
else "Fingerprint: (default)"
|
||||
)
|
||||
session_state = "locked" if self._session_locked else "unlocked"
|
||||
managed_state = (
|
||||
f"#{self._managed_session_entry_id}"
|
||||
if self._managed_session_entry_id is not None
|
||||
else "(none)"
|
||||
)
|
||||
text = "\n".join(
|
||||
[
|
||||
"SeedPass ◈ TUI v2",
|
||||
@@ -670,6 +687,8 @@ def launch_tui2(
|
||||
f"Filter : {self.filter_kind}",
|
||||
f"Archive: {self.archive_scope}",
|
||||
f"Links : {self.link_relation_filter}",
|
||||
f"Session: {session_state}",
|
||||
f"Managed: {managed_state}",
|
||||
(
|
||||
f"Results: {len(self._all_results)} | "
|
||||
f"Page: {self._result_page + 1}/{self._total_pages()}"
|
||||
@@ -692,7 +711,7 @@ def launch_tui2(
|
||||
"- Ctrl+S save doc",
|
||||
"- Ctrl+P command palette",
|
||||
"- add-* create entries",
|
||||
"- quickstart/stats via palette",
|
||||
"- onboarding/quickstart/stats via palette",
|
||||
"- tags/notes/fields via palette",
|
||||
"- docs: edit/save/export",
|
||||
"- 6 toggle dedicated 2FA board",
|
||||
@@ -700,6 +719,7 @@ def launch_tui2(
|
||||
"- npub + nostr reset/fresh namespace",
|
||||
"- checksum/db/totp/seed backup",
|
||||
"- managed-load/managed-exit session",
|
||||
"- session-status, lock, unlock",
|
||||
"- l cycle link relation",
|
||||
"- [ / ] select link",
|
||||
"- o open link target",
|
||||
@@ -820,7 +840,7 @@ def launch_tui2(
|
||||
)
|
||||
if is_empty_vault:
|
||||
self.query_one("#entry-detail", Static).update(
|
||||
self._quickstart_text()
|
||||
self._onboarding_text()
|
||||
)
|
||||
else:
|
||||
self.query_one("#entry-detail", Static).update("No entries match.")
|
||||
@@ -834,7 +854,7 @@ def launch_tui2(
|
||||
self._current_link_cursor = 0
|
||||
if is_empty_vault:
|
||||
self._set_status(
|
||||
"Vault is empty. Run 'quickstart' or add your first entry."
|
||||
"Vault is empty. Run 'onboarding' (or 'quickstart') to begin."
|
||||
)
|
||||
else:
|
||||
self._set_status("No entries match current filter/search")
|
||||
@@ -931,6 +951,9 @@ def launch_tui2(
|
||||
self._render_current_page(preserve_selected=not reset_page)
|
||||
|
||||
def _show_entry(self, entry_index: int) -> None:
|
||||
if self._session_locked:
|
||||
self._set_status("Vault is locked. Run: unlock <password>")
|
||||
return
|
||||
if self._service is None:
|
||||
return
|
||||
try:
|
||||
@@ -1499,6 +1522,8 @@ def launch_tui2(
|
||||
self._refresh_doc_edit_help()
|
||||
|
||||
def _selected_entry_payload(self) -> dict[str, Any]:
|
||||
if self._session_locked:
|
||||
raise ValueError("Vault is locked. Run: unlock <password>")
|
||||
if self._service is None:
|
||||
raise ValueError("Entry service unavailable")
|
||||
if self._selected_entry_id is None:
|
||||
@@ -1604,11 +1629,18 @@ def launch_tui2(
|
||||
self._set_status(f"Applied search: {query}")
|
||||
return
|
||||
|
||||
if cmd in {"onboarding", "welcome"}:
|
||||
if args:
|
||||
self._set_status("Usage: onboarding")
|
||||
return
|
||||
self.query_one("#entry-detail", Static).update(self._onboarding_text())
|
||||
self._set_status("Displayed onboarding guide")
|
||||
return
|
||||
if cmd == "quickstart":
|
||||
if args:
|
||||
self._set_status("Usage: quickstart")
|
||||
return
|
||||
self.query_one("#entry-detail", Static).update(self._quickstart_text())
|
||||
self.query_one("#entry-detail", Static).update(self._onboarding_text())
|
||||
self._set_status("Displayed quick start guide")
|
||||
return
|
||||
|
||||
@@ -1619,6 +1651,103 @@ def launch_tui2(
|
||||
self.query_one("#entry-detail", Static).update(self._stats_text())
|
||||
self._set_status("Displayed vault stats")
|
||||
return
|
||||
if cmd == "session-status":
|
||||
if args:
|
||||
self._set_status("Usage: session-status")
|
||||
return
|
||||
lock_state = "locked" if self._session_locked else "unlocked"
|
||||
managed_state = (
|
||||
f"entry #{self._managed_session_entry_id}"
|
||||
if self._managed_session_entry_id is not None
|
||||
else "none"
|
||||
)
|
||||
vault_state = (
|
||||
"connected" if self._vault_service is not None else "unavailable"
|
||||
)
|
||||
self.query_one("#entry-detail", Static).update(
|
||||
"\n".join(
|
||||
[
|
||||
"Session Status",
|
||||
"--------------",
|
||||
"",
|
||||
f"Vault lock state: {lock_state}",
|
||||
f"Managed account session: {managed_state}",
|
||||
f"Vault service: {vault_state}",
|
||||
]
|
||||
)
|
||||
)
|
||||
self._update_filters_panel()
|
||||
self._set_status("Displayed session status")
|
||||
return
|
||||
if cmd == "lock":
|
||||
if args:
|
||||
self._set_status("Usage: lock")
|
||||
return
|
||||
if self._vault_service is None:
|
||||
self._set_status("Vault service unavailable")
|
||||
return
|
||||
locker = getattr(self._vault_service, "lock", None)
|
||||
if not callable(locker):
|
||||
self._set_status("Vault service does not support lock")
|
||||
return
|
||||
try:
|
||||
locker()
|
||||
self._session_locked = True
|
||||
self._clear_failure()
|
||||
self._selected_entry_id = None
|
||||
self._selected_entry = None
|
||||
self._current_links = []
|
||||
self._current_link_cursor = 0
|
||||
self._all_results = []
|
||||
self._result_page = 0
|
||||
self._render_current_page(preserve_selected=False)
|
||||
self._set_secret_panel(
|
||||
"Vault locked. Sensitive data hidden.\nRun: unlock <password>"
|
||||
)
|
||||
self._update_filters_panel()
|
||||
self._set_status("Vault locked")
|
||||
except Exception as exc:
|
||||
self._record_failure(
|
||||
"lock failed",
|
||||
exc,
|
||||
retry=lambda: self._run_palette_command(raw),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
if cmd == "unlock":
|
||||
if len(args) != 1:
|
||||
self._set_status("Usage: unlock <password>")
|
||||
return
|
||||
if self._vault_service is None:
|
||||
self._set_status("Vault service unavailable")
|
||||
return
|
||||
unlocker = getattr(self._vault_service, "unlock", None)
|
||||
if not callable(unlocker):
|
||||
self._set_status("Vault service does not support unlock")
|
||||
return
|
||||
try:
|
||||
from seedpass.core.api import UnlockRequest
|
||||
|
||||
response = unlocker(UnlockRequest(password=args[0]))
|
||||
self._session_locked = False
|
||||
self._load_entries(self._last_query, reset_page=False)
|
||||
self._set_secret_panel(
|
||||
"Sensitive data hidden. Use 'v' to reveal 🔑 or 'g' for QR ▦."
|
||||
)
|
||||
self._clear_failure()
|
||||
duration = getattr(response, "duration", None)
|
||||
if isinstance(duration, (int, float)):
|
||||
self._set_status(f"Vault unlocked in {float(duration):.2f}s")
|
||||
else:
|
||||
self._set_status("Vault unlocked")
|
||||
except Exception as exc:
|
||||
self._record_failure(
|
||||
"unlock failed",
|
||||
exc,
|
||||
retry=lambda: self._run_palette_command(raw),
|
||||
hint="Press 'x' to retry.",
|
||||
)
|
||||
return
|
||||
|
||||
if cmd == "filter":
|
||||
if len(args) != 1:
|
||||
@@ -3155,6 +3284,7 @@ def launch_tui2(
|
||||
return
|
||||
try:
|
||||
loader(entry_id)
|
||||
self._managed_session_entry_id = int(entry_id)
|
||||
self._load_entries(query="", reset_page=True)
|
||||
self._clear_failure()
|
||||
self._set_status(
|
||||
@@ -3184,6 +3314,7 @@ def launch_tui2(
|
||||
return
|
||||
try:
|
||||
exiter()
|
||||
self._managed_session_entry_id = None
|
||||
self._load_entries(query="", reset_page=True)
|
||||
self._clear_failure()
|
||||
self._set_status("Exited managed account session")
|
||||
@@ -3544,9 +3675,15 @@ def launch_tui2(
|
||||
self._set_status("Palette closed")
|
||||
|
||||
def action_reveal_selected(self, confirm: bool = False) -> None:
|
||||
if self._session_locked:
|
||||
self._set_status("Vault is locked. Run: unlock <password>")
|
||||
return
|
||||
self._show_sensitive_panel(include_qr=False, confirm=confirm)
|
||||
|
||||
def action_show_qr(self, mode: str = "default", confirm: bool = False) -> None:
|
||||
if self._session_locked:
|
||||
self._set_status("Vault is locked. Run: unlock <password>")
|
||||
return
|
||||
self._show_sensitive_panel(include_qr=True, qr_mode=mode, confirm=confirm)
|
||||
|
||||
def action_toggle_totp_board(self) -> None:
|
||||
|
||||
@@ -505,6 +505,7 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
|
||||
app._run_palette_command("page abc")
|
||||
app._run_palette_command("page 0")
|
||||
app._run_palette_command("help-commands now")
|
||||
app._run_palette_command("onboarding now")
|
||||
app._run_palette_command("quickstart now")
|
||||
app._run_palette_command("stats now")
|
||||
app._run_palette_command("link-filter")
|
||||
@@ -572,6 +573,9 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
|
||||
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("session-status now")
|
||||
app._run_palette_command("lock now")
|
||||
app._run_palette_command("unlock")
|
||||
app._run_palette_command("doc-export one two")
|
||||
app._run_palette_command("filter")
|
||||
app._run_palette_command("archive-filter")
|
||||
@@ -581,8 +585,12 @@ async def test_tui2_matrix_actions_palette_events_and_guards() -> None:
|
||||
|
||||
app._run_palette_command("help")
|
||||
app._run_palette_command("help-commands")
|
||||
app._run_palette_command("onboarding")
|
||||
app._run_palette_command("quickstart")
|
||||
app._run_palette_command("stats")
|
||||
app._run_palette_command("session-status")
|
||||
app._run_palette_command("lock")
|
||||
app._run_palette_command("unlock hunter2")
|
||||
app._run_palette_command("search Doc")
|
||||
app._run_palette_command("filter document")
|
||||
app._run_palette_command("archive-filter all")
|
||||
|
||||
@@ -522,6 +522,9 @@ class FakeVaultService:
|
||||
self.exported: list[Path] = []
|
||||
self.imported: list[Path] = []
|
||||
self.parent_seed_backups: list[tuple[Path | None, str | None]] = []
|
||||
self.lock_calls = 0
|
||||
self.unlock_passwords: list[str] = []
|
||||
self.locked = False
|
||||
|
||||
def export_vault(self, req) -> None:
|
||||
path = Path(getattr(req, "path"))
|
||||
@@ -542,6 +545,22 @@ class FakeVaultService:
|
||||
path.write_text("seed-backup", encoding="utf-8")
|
||||
self.parent_seed_backups.append((path, password))
|
||||
|
||||
def lock(self) -> None:
|
||||
self.lock_calls += 1
|
||||
self.locked = True
|
||||
|
||||
def unlock(self, req):
|
||||
password = str(getattr(req, "password", ""))
|
||||
self.unlock_passwords.append(password)
|
||||
if password != "hunter2":
|
||||
raise ValueError("invalid password")
|
||||
self.locked = False
|
||||
|
||||
class _Resp:
|
||||
duration = 0.42
|
||||
|
||||
return _Resp()
|
||||
|
||||
|
||||
def _build_app(
|
||||
service: FakeEntryService,
|
||||
@@ -773,9 +792,15 @@ async def test_tui2_textual_quickstart_and_stats_commands() -> None:
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
await _run_palette(app, pilot, "onboarding")
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Onboarding Quick Start" in detail
|
||||
assert "Step 1: Create your first entry" in detail
|
||||
assert "Displayed onboarding guide" in _status_text(app)
|
||||
|
||||
await _run_palette(app, pilot, "quickstart")
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Quick Start" in detail
|
||||
assert "Onboarding Quick Start" in detail
|
||||
assert "add-password" in detail
|
||||
assert "Displayed quick start guide" in _status_text(app)
|
||||
|
||||
@@ -794,7 +819,7 @@ async def test_tui2_textual_empty_vault_shows_quickstart() -> None:
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Quick Start" in detail
|
||||
assert "Onboarding Quick Start" in detail
|
||||
assert "add-password" in detail
|
||||
assert "Vault is empty." in _status_text(app)
|
||||
|
||||
@@ -1571,11 +1596,13 @@ async def test_tui2_textual_managed_account_session_palette_commands() -> None:
|
||||
await pilot.pause()
|
||||
assert "Loaded managed account session from entry #1" in _status_text(app)
|
||||
assert service.managed_load_calls == [1]
|
||||
assert "Managed: #1" in _filters_text(app)
|
||||
|
||||
app._run_palette_command("managed-exit")
|
||||
await pilot.pause()
|
||||
assert "Exited managed account session" in _status_text(app)
|
||||
assert service.managed_exit_calls == 1
|
||||
assert "Managed: (none)" in _filters_text(app)
|
||||
|
||||
app._run_palette_command("open 2")
|
||||
app._run_palette_command("managed-load")
|
||||
@@ -1614,6 +1641,14 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
|
||||
await pilot.pause()
|
||||
assert "Vault service unavailable" in _status_text(app)
|
||||
|
||||
app._run_palette_command("lock")
|
||||
await pilot.pause()
|
||||
assert "Vault service unavailable" in _status_text(app)
|
||||
|
||||
app._run_palette_command("unlock secret")
|
||||
await pilot.pause()
|
||||
assert "Vault service unavailable" in _status_text(app)
|
||||
|
||||
app._run_palette_command("totp-export")
|
||||
await pilot.pause()
|
||||
assert "Usage: totp-export <path>" in _status_text(app)
|
||||
@@ -1744,3 +1779,58 @@ async def test_tui2_textual_profiles_and_settings_palette_validation() -> None:
|
||||
app._run_palette_command("managed-exit now")
|
||||
await pilot.pause()
|
||||
assert "Usage: managed-exit" in _status_text(app)
|
||||
|
||||
app._run_palette_command("session-status now")
|
||||
await pilot.pause()
|
||||
assert "Usage: session-status" in _status_text(app)
|
||||
|
||||
app._run_palette_command("lock now")
|
||||
await pilot.pause()
|
||||
assert "Usage: lock" in _status_text(app)
|
||||
|
||||
app._run_palette_command("unlock")
|
||||
await pilot.pause()
|
||||
assert "Usage: unlock <password>" in _status_text(app)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui2_textual_palette_session_status_lock_and_unlock() -> None:
|
||||
service = FakeEntryService(
|
||||
[{"id": 1, "kind": "password", "label": "Demo", "username": "a"}]
|
||||
)
|
||||
vault = FakeVaultService()
|
||||
app = _build_app(service, vault_service=vault)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
app._run_palette_command("session-status")
|
||||
await pilot.pause()
|
||||
assert "Displayed session status" in _status_text(app)
|
||||
assert "Vault lock state: unlocked" in _widget_text(app, "#entry-detail")
|
||||
assert "Session: unlocked" in _filters_text(app)
|
||||
|
||||
app._run_palette_command("lock")
|
||||
await pilot.pause()
|
||||
assert "Vault locked" in _status_text(app)
|
||||
assert vault.lock_calls == 1
|
||||
assert "Session: locked" in _filters_text(app)
|
||||
|
||||
app._run_palette_command("open 1")
|
||||
await pilot.pause()
|
||||
assert "Vault is locked. Run: unlock <password>" in _status_text(app)
|
||||
|
||||
app._run_palette_command("reveal")
|
||||
await pilot.pause()
|
||||
assert "Vault is locked. Run: unlock <password>" in _status_text(app)
|
||||
|
||||
app._run_palette_command("unlock wrong")
|
||||
await pilot.pause()
|
||||
assert "unlock failed: invalid password" in _status_text(app)
|
||||
assert vault.unlock_passwords[-1] == "wrong"
|
||||
|
||||
app._run_palette_command("unlock hunter2")
|
||||
await pilot.pause()
|
||||
assert "Vault unlocked in 0.42s" in _status_text(app)
|
||||
assert vault.unlock_passwords[-1] == "hunter2"
|
||||
assert "Session: unlocked" in _filters_text(app)
|
||||
Reference in new issue
Block a user