mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-05 04:12:03 +00:00
tui2: add quickstart and stats parity UX flows
This commit is contained in:
1 parent
3f0d4a8d89
commit
5c524b2086
6 files changed
+229
-6
No files matched your search
@@ -65,6 +65,8 @@ Use these docs as inputs, but treat this file as the decision layer:
|
||||
## 4) Recommended Next Steps (Priority Order)
|
||||
|
||||
1. Continue feature-development parity polish in TUI v2 with user-driven UX gaps.
|
||||
: prioritized gaps from side-by-side legacy vs v2 run:
|
||||
: guided first-run onboarding, stats/ops visibility, explicit lock/session affordances.
|
||||
2. Raise TUI v2 critical-path coverage beyond current gate floor (target >82% for `src/seedpass/tui_v2/app.py`).
|
||||
3. Advance Nostr resilience validation with optional real-relay soak lane after deterministic suites.
|
||||
4. Execute a tagged release integrity run and attach evidence URLs in readiness checklist.
|
||||
|
||||
@@ -9,7 +9,7 @@ Purpose: map legacy interactive workflows to Textual TUI v2 equivalents and isol
|
||||
|
||||
- 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 low-volume utility affordances rather than core workflows.
|
||||
- Gaps: remaining high-value gaps are mostly UX-guidance and operational affordances rather than missing core data operations.
|
||||
|
||||
## Matrix
|
||||
|
||||
@@ -50,6 +50,7 @@ Purpose: map legacy interactive workflows to Textual TUI v2 equivalents and isol
|
||||
| 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 | |
|
||||
| Stats view | Settings > Stats | `stats` (palette) | Implemented | In-app dashboard in detail pane. |
|
||||
| Display npub | Main utility flow | `npub` (alias: `nostr-pubkey`) | Implemented | Displays active profile npub and QR payload in the sensitive panel. |
|
||||
| View archived-only list | Settings/List flow | `h` cycle archive scope or `archive-filter <active|all|archived>` | Implemented | Supports active-only/default, all, and archived-only views. |
|
||||
| Reset Nostr sync state | Settings > Nostr | `nostr-reset-sync-state` | Implemented | Added to TUI v2 palette + tests. |
|
||||
@@ -59,8 +60,10 @@ Purpose: map legacy interactive workflows to Textual TUI v2 equivalents and isol
|
||||
|
||||
## Prioritized Gap Queue
|
||||
|
||||
1. Revalidate and close “Full legacy workflow coverage” in `docs/tui_v2_parity_checklist.md`.
|
||||
1. Medium: guided first-run onboarding parity in TUI v2 (legacy has explicit startup path).
|
||||
2. Medium: explicit lock/session affordances and status visibility parity in TUI v2.
|
||||
|
||||
## Suggested implementation order
|
||||
|
||||
1. Run parity bug-bash + checklist update to move full legacy workflow coverage to complete.
|
||||
1. Land guided onboarding panel/flow parity improvements.
|
||||
2. Add lock/session visibility commands and test coverage.
|
||||
@@ -792,3 +792,36 @@
|
||||
- Docs updated:
|
||||
- `docs/tui_v2_parity_checklist.md`: command discoverability row moved from `Partial` to `Yes`.
|
||||
- `docs/dev_control_center.md`: priorities refocused to feature/parity and test coverage targets first.
|
||||
|
||||
## 2026-03-02 Parity UX slice: guided quick-start + in-app stats for TUI v2
|
||||
- User feedback (side-by-side v2 vs legacy) identified three UX parity gaps:
|
||||
1) guided onboarding/first-run feel,
|
||||
2) stats/operational visibility,
|
||||
3) explicit lock/session affordances.
|
||||
- Implemented first two in `src/seedpass/tui_v2/app.py`:
|
||||
- new palette command: `quickstart`
|
||||
- renders guided first-steps panel in detail pane.
|
||||
- new palette command: `stats`
|
||||
- renders vault stats dashboard in detail pane.
|
||||
- prefers `VaultService.stats()` when available.
|
||||
- falls back to deterministic local aggregate from `EntryService.search_entries(..., include_archived=True)`.
|
||||
- empty-vault UX improvement:
|
||||
- default detail panel now shows Quick Start guidance instead of plain "No entries match".
|
||||
- Discoverability updates:
|
||||
- palette placeholder includes `quickstart` and `stats`.
|
||||
- command help/reference now includes quickstart/stats entries.
|
||||
- left actions panel includes quickstart/stats hint.
|
||||
- Tests added/updated:
|
||||
- `src/tests/test_tui_v2_textual_interactions.py`
|
||||
- `test_tui2_textual_quickstart_and_stats_commands`
|
||||
- `test_tui2_textual_empty_vault_shows_quickstart`
|
||||
- `src/tests/test_tui_v2_action_matrix.py`
|
||||
- command matrix includes quickstart/stats success and usage-error paths.
|
||||
- Validation:
|
||||
- `pytest -q src/tests/test_tui_v2_textual_interactions.py src/tests/test_tui_v2_action_matrix.py src/tests/test_tui_v2_parity_scenarios.py src/tests/test_tui_v2_helpers.py`
|
||||
- Result: `42 passed`.
|
||||
- `./scripts/tui2_coverage_gate.sh` -> PASS (`src/seedpass/tui_v2/app.py` remains above gate at `79.30%`).
|
||||
- Planning docs updated to track remaining UX parity gap:
|
||||
- `docs/dev_control_center.md`
|
||||
- `docs/tui_v2_legacy_parity_matrix.md`
|
||||
(remaining medium gap: explicit lock/session affordances).
|
||||
+143
-3
@@ -282,7 +282,7 @@ def launch_tui2(
|
||||
yield Input(
|
||||
placeholder=(
|
||||
"Command palette: help | open <id> | search <q> | "
|
||||
"help-commands | "
|
||||
"help-commands | quickstart | stats | "
|
||||
"filter <kind> | archive | restore | "
|
||||
"archive-filter <active|all|archived> | "
|
||||
"add-password <label> <length> [username] [url] | "
|
||||
@@ -490,9 +490,33 @@ def launch_tui2(
|
||||
def _set_secret_panel(self, text: str) -> None:
|
||||
self.query_one("#secret-detail", Static).update(text)
|
||||
|
||||
def _quickstart_text(self) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"Quick Start",
|
||||
"-----------",
|
||||
"",
|
||||
"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"',
|
||||
"",
|
||||
"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, "
|
||||
"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-*, "
|
||||
@@ -508,6 +532,7 @@ def launch_tui2(
|
||||
"-----------------",
|
||||
"",
|
||||
"Core: help | help-commands | open <id> | jump <id> | search <q>",
|
||||
"Core Ops: quickstart | stats",
|
||||
"Filters: filter <kind|all> | archive-filter <active|all|archived>",
|
||||
"Pages: page-next | page-prev | page <n>",
|
||||
"",
|
||||
@@ -667,6 +692,7 @@ def launch_tui2(
|
||||
"- Ctrl+S save doc",
|
||||
"- Ctrl+P command palette",
|
||||
"- add-* create entries",
|
||||
"- quickstart/stats via palette",
|
||||
"- tags/notes/fields via palette",
|
||||
"- docs: edit/save/export",
|
||||
"- 6 toggle dedicated 2FA board",
|
||||
@@ -696,6 +722,89 @@ def launch_tui2(
|
||||
)
|
||||
return total_pages
|
||||
|
||||
def _stats_text(self) -> str:
|
||||
lines = ["Vault Stats", "----------", ""]
|
||||
stats_payload: dict[str, Any] | None = None
|
||||
if self._vault_service is not None:
|
||||
getter = getattr(self._vault_service, "stats", None)
|
||||
if callable(getter):
|
||||
try:
|
||||
payload = getter()
|
||||
if isinstance(payload, dict):
|
||||
stats_payload = payload
|
||||
except Exception:
|
||||
stats_payload = None
|
||||
|
||||
if isinstance(stats_payload, dict):
|
||||
total = int(stats_payload.get("total_entries", 0))
|
||||
lines.append(f"Total entries: {total}")
|
||||
entries = stats_payload.get("entries")
|
||||
if isinstance(entries, dict) and entries:
|
||||
lines.append("Kinds:")
|
||||
for kind, count in sorted(entries.items()):
|
||||
lines.append(f"- {kind}: {count}")
|
||||
lines.append(
|
||||
f"Relays configured: {int(stats_payload.get('relay_count', 0))}"
|
||||
)
|
||||
lines.append(f"Backups: {int(stats_payload.get('backup_count', 0))}")
|
||||
lines.append(
|
||||
f"Schema version: {stats_payload.get('schema_version', '(unknown)')}"
|
||||
)
|
||||
lines.append(
|
||||
"Database checksum ok: "
|
||||
+ ("yes" if bool(stats_payload.get("checksum_ok", False)) else "no")
|
||||
)
|
||||
lines.append(
|
||||
"Script checksum ok: "
|
||||
+ (
|
||||
"yes"
|
||||
if bool(stats_payload.get("script_checksum_ok", False))
|
||||
else "no"
|
||||
)
|
||||
)
|
||||
lines.append(
|
||||
f"Snapshot chunks: {int(stats_payload.get('chunk_count', 0))}"
|
||||
)
|
||||
lines.append(
|
||||
f"Pending deltas: {int(stats_payload.get('pending_deltas', 0))}"
|
||||
)
|
||||
delta_since = stats_payload.get("delta_since")
|
||||
if delta_since:
|
||||
lines.append(f"Latest delta timestamp: {delta_since}")
|
||||
return "\n".join(lines)
|
||||
|
||||
if self._service is None:
|
||||
lines.append("Stats unavailable: entry service unavailable.")
|
||||
return "\n".join(lines)
|
||||
try:
|
||||
rows = self._service.search_entries(
|
||||
"",
|
||||
kinds=None,
|
||||
include_archived=True,
|
||||
archived_only=False,
|
||||
)
|
||||
except Exception as exc:
|
||||
lines.append(f"Stats unavailable: {exc}")
|
||||
return "\n".join(lines)
|
||||
|
||||
total = len(rows)
|
||||
archived = sum(1 for row in rows if bool(row[4]))
|
||||
counts: dict[str, int] = {}
|
||||
for _idx, _label, _u, _url, _archived, etype in rows:
|
||||
kind = getattr(etype, "value", str(etype))
|
||||
counts[kind] = counts.get(kind, 0) + 1
|
||||
|
||||
lines.append(f"Total entries: {total}")
|
||||
lines.append(f"Archived entries: {archived}")
|
||||
lines.append(f"Active entries: {max(0, total - archived)}")
|
||||
if counts:
|
||||
lines.append("Kinds:")
|
||||
for kind, count in sorted(counts.items()):
|
||||
lines.append(f"- {kind}: {count}")
|
||||
lines.append("")
|
||||
lines.append("Tip: connect vault service for full operational stats.")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _render_current_page(self, *, preserve_selected: bool = True) -> None:
|
||||
self._entry_ids_in_view = []
|
||||
list_view = self.query_one("#entry-list", ListView)
|
||||
@@ -704,7 +813,17 @@ def launch_tui2(
|
||||
if total == 0:
|
||||
self._selected_entry_id = None
|
||||
self._selected_entry = None
|
||||
self.query_one("#entry-detail", Static).update("No entries match.")
|
||||
is_empty_vault = (
|
||||
not self._last_query.strip()
|
||||
and self.filter_kind == "all"
|
||||
and self.archive_scope == "active"
|
||||
)
|
||||
if is_empty_vault:
|
||||
self.query_one("#entry-detail", Static).update(
|
||||
self._quickstart_text()
|
||||
)
|
||||
else:
|
||||
self.query_one("#entry-detail", Static).update("No entries match.")
|
||||
self.query_one("#link-detail", Static).update(
|
||||
"Links: select an entry first."
|
||||
)
|
||||
@@ -713,7 +832,12 @@ def launch_tui2(
|
||||
)
|
||||
self._current_links = []
|
||||
self._current_link_cursor = 0
|
||||
self._set_status("No entries match current filter/search")
|
||||
if is_empty_vault:
|
||||
self._set_status(
|
||||
"Vault is empty. Run 'quickstart' or add your first entry."
|
||||
)
|
||||
else:
|
||||
self._set_status("No entries match current filter/search")
|
||||
self._update_filters_panel()
|
||||
return
|
||||
|
||||
@@ -1480,6 +1604,22 @@ def launch_tui2(
|
||||
self._set_status(f"Applied search: {query}")
|
||||
return
|
||||
|
||||
if cmd == "quickstart":
|
||||
if args:
|
||||
self._set_status("Usage: quickstart")
|
||||
return
|
||||
self.query_one("#entry-detail", Static).update(self._quickstart_text())
|
||||
self._set_status("Displayed quick start guide")
|
||||
return
|
||||
|
||||
if cmd == "stats":
|
||||
if args:
|
||||
self._set_status("Usage: stats")
|
||||
return
|
||||
self.query_one("#entry-detail", Static).update(self._stats_text())
|
||||
self._set_status("Displayed vault stats")
|
||||
return
|
||||
|
||||
if cmd == "filter":
|
||||
if len(args) != 1:
|
||||
self._set_status("Usage: filter <kind|all>")
|
||||
|
||||
@@ -505,6 +505,8 @@ 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("quickstart now")
|
||||
app._run_palette_command("stats now")
|
||||
app._run_palette_command("link-filter")
|
||||
app._run_palette_command("link-add")
|
||||
app._run_palette_command("link-add no")
|
||||
@@ -579,6 +581,8 @@ 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("quickstart")
|
||||
app._run_palette_command("stats")
|
||||
app._run_palette_command("search Doc")
|
||||
app._run_palette_command("filter document")
|
||||
app._run_palette_command("archive-filter all")
|
||||
|
||||
@@ -758,6 +758,47 @@ async def test_tui2_textual_palette_help_commands_reference() -> None:
|
||||
assert "Palette commands:" in _status_text(app)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui2_textual_quickstart_and_stats_commands() -> None:
|
||||
service = FakeEntryService(
|
||||
[
|
||||
{"id": 1, "kind": "password", "label": "Login 1", "length": 16},
|
||||
{"id": 2, "kind": "document", "label": "Doc 2", "content": "x"},
|
||||
{"id": 3, "kind": "totp", "label": "Auth 3", "secret": "JBSWY3DPEHPK3PXP"},
|
||||
{"id": 4, "kind": "seed", "label": "Seed 4", "archived": True},
|
||||
]
|
||||
)
|
||||
app = _build_app(service)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
await _run_palette(app, pilot, "quickstart")
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Quick Start" in detail
|
||||
assert "add-password" in detail
|
||||
assert "Displayed quick start guide" in _status_text(app)
|
||||
|
||||
await _run_palette(app, pilot, "stats")
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Vault Stats" in detail
|
||||
assert "Total entries: 4" in detail
|
||||
assert "Archived entries: 1" in detail
|
||||
assert "- password: 1" in detail
|
||||
assert "Displayed vault stats" in _status_text(app)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui2_textual_empty_vault_shows_quickstart() -> None:
|
||||
app = _build_app(FakeEntryService([]))
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
detail = _widget_text(app, "#entry-detail")
|
||||
assert "Quick Start" in detail
|
||||
assert "add-password" in detail
|
||||
assert "Vault is empty." in _status_text(app)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_tui2_textual_reveal_and_qr_flow() -> None:
|
||||
service = FakeEntryService(
|
||||
|
||||
Reference in new issue
Block a user