tui2: add quickstart and stats parity UX flows

This commit is contained in:
thePR0M3TH3AN committed 2026-03-02 09:31:56 -05:00
1 parent 3f0d4a8d89
commit 5c524b2086
6 files changed
+229 -6

No files matched your search

+2
View File
@@ -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.
+6 -3
View File
@@ -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.
+33
View File
@@ -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
View File
@@ -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>")
+4
View File
@@ -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(