feat(tui-v3): implement 2FA/Seed boards and hierarchical sidebar

Added TotpBoard with live 30s ticking codes and progress bars. Added
SeedBoard for BIP-85 derivations. Upgraded ProfileTree to a true
hierarchical structure that recursively lists managed accounts and agents.
Implemented global UI heartbeat for real-time dashboard updates.
This commit is contained in:
thePR0M3TH3AN committed 2026-03-02 23:07:33 -05:00
1 parent 3b4e3c481e
commit af177a8516
3 files changed
+104 -13

No files matched your search

+14
View File
@@ -117,6 +117,20 @@ class SeedPassTuiV3(App[None]):
self.active_fingerprint = self._initial_fingerprint or ""
self.push_screen(MainScreen())
# Global UI Heartbeat (for 2FA ticking etc)
self.set_interval(1.0, self.action_refresh_ui_quiet)
def action_refresh_ui_quiet(self) -> None:
"""Background refresh for dynamic elements (2FA)."""
try:
# We only refresh the inspector if it's currently showing a 2FA board
board = self.query_one("#board-container")
if board and hasattr(board, "children") and board.children:
current_board = board.children[0]
if current_board.__class__.__name__ == "TotpBoard":
current_board.refresh()
except Exception:
pass
def watch_active_fingerprint(self, old_fp: str, new_fp: str) -> None:
"""Refresh components when the profile changes."""
+73 -6
View File
@@ -11,7 +11,7 @@ class BaseBoard(Static):
def _render_card(self, title: str, rows: list[str]) -> str:
"""Utility to render ASCII cards matching mockup style."""
width = 40
width = 50
top = f"┌─ {title} " + "─" * (width - len(title) - 4) + "┐"
bottom = "└" + "─" * (width - 2) + "┘"
lines = [top]
@@ -52,6 +52,67 @@ class PasswordBoard(BaseBoard):
self._render_card("Quick Actions", action_rows)
])
class TotpBoard(BaseBoard):
"""Matches '2FA Board.png' mockup."""
def render(self) -> str:
d = self.entry_data
app = self.app
# Fetch live code from service
code = "------"
if app.services.get("entry") and not app.session_locked:
try:
code = app.services["entry"].get_totp_code(d.get("id"))
except:
pass
# Simple ASCII progress bar for the 30s cycle
import time
remaining = 30 - (int(time.time()) % 30)
bar_width = 20
filled = int((remaining / 30) * bar_width)
bar = "█" * filled + "░" * (bar_width - filled)
cred_rows = [
f"Label : {d.get('label', '')}",
f"Code : [b][cyan]{code}[/b] ({remaining}s)",
f"Timer : {bar}",
f"Period : {d.get('period', 30)}s",
f"Digits : {d.get('digits', 6)}",
]
action_rows = [
"▣ Copy Code (c) ▣ Reveal Secret (v)",
"▣ Show QR (g) ▣ Archive (a)",
]
return "\n".join([
self._render_card("2FA Authenticator", cred_rows),
"",
self._render_card("Quick Actions", action_rows)
])
class SeedBoard(BaseBoard):
"""Matches 'BIP-39 Seed Board.png' mockup."""
def render(self) -> str:
d = self.entry_data
cred_rows = [
f"Label : {d.get('label', '')}",
f"Fingerprint: {d.get('fingerprint', 'Pending...')}",
f"Index : {d.get('index', 0)}",
f"Word Count : {d.get('word_count', 24)}",
"Seed Phrase: [HIDDEN] (Double-press 'v' to reveal)",
]
action_rows = [
"▣ Reveal Words (vv) ▣ Show SeedQR (gg)",
"▣ Load Session (ml) ▣ Archive (a)",
]
return "\n".join([
self._render_card("BIP-85 Derived Seed", cred_rows),
"",
self._render_card("Quick Actions", action_rows)
])
class NoteBoard(BaseBoard):
"""Matches 'Note Board.png' mockup."""
def render(self) -> str:
@@ -67,7 +128,7 @@ class NoteBoard(BaseBoard):
]
return "\n".join([
self._render_card("Content", content_rows),
self._render_card("Content Preview", content_rows),
"",
self._render_card("Quick Actions", action_rows)
])
@@ -106,14 +167,16 @@ class BoardContainer(Vertical):
# Select appropriate board
if kind in {"password", "stored_password"}:
board = self._show_board("password")
board.entry_data = entry
elif kind in {"document", "note"}:
board = self._show_board("note")
board.entry_data = entry
elif kind == "totp":
board = self._show_board("totp")
elif kind in {"seed", "managed_account"}:
board = self._show_board("seed")
else:
# Fallback for now
board = self._show_board("password")
board.entry_data = entry
board.entry_data = entry
def _show_board(self, board_type: str) -> Any:
self.remove_children()
@@ -123,6 +186,10 @@ class BoardContainer(Vertical):
new_board = PasswordBoard()
elif board_type == "note":
new_board = NoteBoard()
elif board_type == "totp":
new_board = TotpBoard()
elif board_type == "seed":
new_board = SeedBoard()
else:
new_board = IdleBoard()
+17 -7
View File
@@ -29,18 +29,28 @@ class ProfileTree(Tree):
return
service = app.services["profile"]
entry_service = app.services.get("entry")
profiles = service.list_profiles()
# In v3, we will properly fetch nested accounts.
# For this initial scaffold, we populate the top-level fingerprints.
for fp in profiles:
icon = "■ " if fp == app.active_fingerprint else "□ "
is_active = fp == app.active_fingerprint
icon = "■ " if is_active else "□ "
label = f"{icon}{fp[:12]}..." if len(fp) > 12 else f"{icon}{fp}"
node = self.root.add(label, data=fp, expand=True)
node = self.root.add(label, data=fp, expand=is_active)
# Placeholder for child accounts - in a real run,
# we would query the service for sub-entries of this FP.
# node.add_leaf(" ├─ Managed Account 1")
# If this is the active profile or we have the entry service,
# we can fetch nested managed accounts.
if entry_service and is_active:
# Find managed accounts in the current profile
# Search for all managed accounts
managed = entry_service.search_entries("", kinds=["managed_account"])
for mid, mlabel, _, _, _, _ in managed:
node.add_leaf(f"├─ {mlabel}", data=f"managed:{mid}")
# Find agents (nostr)
agents = entry_service.search_entries("", kinds=["nostr"])
for aid, alabel, _, _, _, _ in agents:
node.add_leaf(f"└─ {alabel}", data=f"agent:{aid}")
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
"""Handle profile selection."""