mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-06 04:42:35 +00:00
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:
1 parent
3b4e3c481e
commit
af177a8516
3 files changed
+104
-13
No files matched your search
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in new issue
Block a user