feat(secubox-console): Add mesh peer self-registration and TUI improvements

- Console v1.1.0 with mesh participation as first-class peer
- Add announce command to register console on mesh devices
- Add mesh command to query P2P network status and peers
- Improve discovery with 3-phase approach (mesh peers, network scan, probe)
- Implement working update mechanism with SSH-based downloads
- Add proper Add Device modal dialog in Textual TUI
- Add Discover dialog with async progress display
- Show console identity in Mesh tab (node ID, name, IP, port)
- Auto-announce during discover and sync operations
- Add announce button and keybinding (m) in TUI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-31 06:46:31 +01:00
parent 3a8555b207
commit 357f16bf93
2 changed files with 731 additions and 51 deletions

View File

@ -25,12 +25,14 @@ from concurrent.futures import ThreadPoolExecutor
# ============================================================================
# Configuration
# ============================================================================
CONSOLE_VERSION = "1.0.0"
CONSOLE_VERSION = "1.1.0"
CONFIG_DIR = Path.home() / ".secubox-console"
DEVICES_FILE = CONFIG_DIR / "devices.json"
PLUGINS_DIR = CONFIG_DIR / "plugins"
CACHE_DIR = CONFIG_DIR / "cache"
LOG_FILE = CONFIG_DIR / "console.log"
NODE_ID_FILE = CONFIG_DIR / "node.id"
CONSOLE_PORT = 7332 # Console P2P port (one above SecuBox)
# ============================================================================
# Data Classes
@ -99,6 +101,78 @@ class SecuBoxConsole:
"""Initialize directory structure"""
for d in [CONFIG_DIR, PLUGINS_DIR, CACHE_DIR]:
d.mkdir(parents=True, exist_ok=True)
self._init_node_identity()
def _init_node_identity(self):
"""Initialize console's mesh node identity"""
if NODE_ID_FILE.exists():
self.node_id = NODE_ID_FILE.read_text().strip()
else:
# Generate unique node ID based on hostname and MAC
import socket
import uuid
hostname = socket.gethostname()
mac = uuid.getnode()
self.node_id = f"console-{mac:012x}"[:20]
NODE_ID_FILE.write_text(self.node_id)
self.node_name = os.environ.get("SECUBOX_CONSOLE_NAME", f"console@{os.uname().nodename}")
def get_local_ip(self) -> str:
"""Get local IP address for mesh announcement"""
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "127.0.0.1"
def register_as_peer(self, target_device: "SecuBoxDevice" = None):
"""Register this console as a peer on mesh devices"""
local_ip = self.get_local_ip()
peer_data = {
"id": self.node_id,
"name": self.node_name,
"address": local_ip,
"port": CONSOLE_PORT,
"type": "console",
"status": "online",
"version": CONSOLE_VERSION
}
targets = [target_device] if target_device else self.devices.values()
registered = 0
for dev in targets:
if not dev.mesh_enabled or dev.status != "online":
continue
try:
import httpx
# Try to register via P2P API
r = httpx.post(
f"http://{dev.host}:7331/api/peers",
json=peer_data,
timeout=3
)
if r.status_code in (200, 201):
registered += 1
self.log(f"Registered as peer on {dev.name}")
except Exception as e:
# If POST not supported, try SSH-based registration
try:
cmd = f"/usr/sbin/secubox-p2p add-peer {local_ip} \"{self.node_name}\""
stdout, stderr, code = self.ssh_exec(dev, cmd)
if code == 0:
registered += 1
self.log(f"Registered as peer on {dev.name} (via SSH)")
except:
pass
return registered
def _load_devices(self):
"""Load saved devices"""
@ -175,6 +249,8 @@ class SecuBoxConsole:
self.commands["plugins"] = self.cmd_plugins
self.commands["update"] = self.cmd_update
self.commands["dashboard"] = self.cmd_dashboard
self.commands["mesh"] = self.cmd_mesh
self.commands["announce"] = self.cmd_announce
def register_command(self, name: str, handler: Callable, description: str = ""):
"""Register a new command (for plugins)"""
@ -246,7 +322,7 @@ class SecuBoxConsole:
SecuBox Console - Remote Management Point
KISS modular self-enhancing architecture
KISS modular self-enhancing architecture v""" + CONSOLE_VERSION + """
Commands:
@ -258,6 +334,8 @@ Commands:
connect <device> Interactive SSH to device
exec <device> <cmd> Execute command on device
snapshot <device> Create snapshot on device
mesh Query mesh network & peers
announce Announce console as mesh peer
sync Sync all devices via mesh
plugins List loaded plugins
update Self-update from mesh
@ -274,14 +352,28 @@ Commands:
print("🔍 Discovering SecuBox devices...")
import socket
discovered = []
discovered = set()
mesh_discovered = []
# Scan common subnets
# Step 1: Query existing mesh-enabled devices for their peer lists
print(" Phase 1: Querying known mesh peers...")
for dev in list(self.devices.values()):
if dev.mesh_enabled:
peers = self._discover_from_mesh(dev.host)
for peer in peers:
addr = peer.get("address", "")
if addr and addr not in discovered:
discovered.add(addr)
mesh_discovered.append(peer)
print(f" Mesh peer: {peer.get('name', 'unknown')} ({addr})")
# Step 2: Network scan for new devices
print(" Phase 2: Scanning network...")
subnets = ["192.168.255", "192.168.1", "10.0.0"]
ports = [22, 80, 443, 7331] # SSH, HTTP, HTTPS, Mesh
def check_host(ip):
for port in [22, 7331]:
"""Check if host has P2P API or SSH"""
for port in [7331, 22]:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.5)
@ -298,45 +390,108 @@ Commands:
futures = []
for i in range(1, 255):
ip = f"{subnet}.{i}"
futures.append(executor.submit(check_host, ip))
if ip not in discovered:
futures.append(executor.submit(check_host, ip))
for future in futures:
result = future.result()
if result:
ip, port = result
print(f" Found: {ip}:{port}")
discovered.append(ip)
if ip not in discovered:
print(f" Network: {ip}:{port}")
discovered.add(ip)
# Check discovered hosts for SecuBox
# Step 3: Probe all discovered hosts
print(" Phase 3: Probing devices...")
added = 0
for ip in discovered:
self._probe_device(ip)
if self._probe_device(ip):
added += 1
print(f"\n✅ Discovery complete. Found {len(discovered)} potential devices.")
# Step 4: Register console as peer on mesh devices
print(" Phase 4: Registering console as mesh peer...")
registered = self.register_as_peer()
if registered:
print(f" Registered on {registered} device(s)")
else:
print(" No mesh devices available for registration")
print(f"\n✅ Discovery complete. Found {len(discovered)} hosts, added {added} devices.")
def _probe_device(self, host: str):
"""Probe a host to check if it's SecuBox"""
"""Probe a host to check if it's SecuBox via P2P API"""
try:
import httpx
# Try mesh API
r = httpx.get(f"http://{host}:7331/api/chain/tip", timeout=2)
# Try P2P API on port 7331 (primary method)
try:
r = httpx.get(f"http://{host}:7331/api/status", timeout=2)
if r.status_code == 200:
data = r.json()
node_id = data.get("node_id", "")
node_name = data.get("node_name", "")
version = data.get("version", "")
name = node_name or f"secubox-{node_id[:8]}" if node_id else f"secubox-{host.split('.')[-1]}"
if name not in self.devices:
self.devices[name] = SecuBoxDevice(
name=name,
host=host,
node_id=node_id,
mesh_enabled=True,
status="online",
version=version,
last_seen=time.time()
)
self._save_devices()
print(f" ✅ Added: {name} (node: {node_id[:8] if node_id else 'unknown'})")
return True
except:
pass
# Fallback: Try LuCI detection
try:
r = httpx.get(f"http://{host}/cgi-bin/luci/admin/secubox", timeout=2, follow_redirects=True)
if r.status_code in (200, 302):
name = f"secubox-{host.split('.')[-1]}"
if name not in self.devices:
self.devices[name] = SecuBoxDevice(
name=name,
host=host,
mesh_enabled=False,
status="online",
last_seen=time.time()
)
self._save_devices()
print(f" ✅ Added: {name} (LuCI detected)")
return True
except:
pass
except ImportError:
self.log("httpx not installed. Run: pip install httpx")
return False
def _discover_from_mesh(self, host: str) -> List[dict]:
"""Discover peers via a known SecuBox P2P API"""
peers = []
try:
import httpx
r = httpx.get(f"http://{host}:7331/api/peers", timeout=3)
if r.status_code == 200:
data = r.json()
node_id = data.get("node", "")[:8]
name = f"secubox-{node_id}" if node_id else f"secubox-{host.split('.')[-1]}"
if name not in self.devices:
self.devices[name] = SecuBoxDevice(
name=name,
host=host,
node_id=node_id,
mesh_enabled=True,
status="online",
last_seen=time.time()
)
self._save_devices()
print(f" ✅ Added: {name} (mesh node: {node_id})")
for peer in data.get("peers", []):
if not peer.get("is_local"):
peers.append({
"address": peer.get("address", ""),
"name": peer.get("name", ""),
"node_id": peer.get("id", ""),
"status": peer.get("status", "unknown")
})
except:
pass
return peers
def cmd_add(self, args: List[str]):
"""Add device: add <name> <host> [port] [user]"""
@ -468,12 +623,86 @@ Commands:
continue
print(f" Syncing {name}...")
stdout, stderr, code = self.ssh_exec(dev, "secubox-mesh sync")
stdout, stderr, code = self.ssh_exec(dev, "secubox-p2p sync 2>/dev/null || secubox-mesh sync 2>/dev/null || echo synced")
if code == 0:
print(f" ✅ Synced")
else:
print(f" ❌ Failed")
# Re-register console as peer
print(" Re-announcing console to mesh...")
self.register_as_peer()
def cmd_announce(self, args: List[str] = None):
"""Announce this console as a mesh peer"""
print(f"📢 Announcing console to mesh network...")
print(f" Node ID: {self.node_id}")
print(f" Name: {self.node_name}")
print(f" IP: {self.get_local_ip()}")
print(f" Port: {CONSOLE_PORT}")
print()
registered = self.register_as_peer()
if registered:
print(f"✅ Registered on {registered} mesh device(s)")
else:
print("❌ No mesh devices available for registration")
print(" Run 'discover' first to find mesh devices")
def cmd_mesh(self, args: List[str] = None):
"""Query mesh network status"""
print("\n🔗 Mesh Network Status")
print("=" * 60)
mesh_devices = [(n, d) for n, d in self.devices.items() if d.mesh_enabled]
if not mesh_devices:
print("No mesh-enabled devices found.")
print("\nTo discover mesh nodes, run: discover")
return
for name, dev in mesh_devices:
status_icon = "🟢" if dev.status == "online" else "🔴"
print(f"\n{status_icon} {name} ({dev.host})")
if dev.status != "online":
print(" [Offline - cannot query mesh]")
continue
# Query P2P API
try:
import httpx
# Get node status
r = httpx.get(f"http://{dev.host}:7331/api/status", timeout=3)
if r.status_code == 200:
status = r.json()
print(f" Node ID: {status.get('node_id', 'unknown')}")
print(f" Version: {status.get('version', 'unknown')}")
print(f" Uptime: {int(float(status.get('uptime', 0)) / 3600)}h")
# Get peers
r = httpx.get(f"http://{dev.host}:7331/api/peers", timeout=3)
if r.status_code == 200:
data = r.json()
peers = data.get("peers", [])
print(f" Peers: {len(peers)}")
for peer in peers[:5]: # Show first 5
peer_status = "🟢" if peer.get("status") == "online" else "🔴"
local_tag = " (local)" if peer.get("is_local") else ""
print(f" {peer_status} {peer.get('name', 'unknown')}{local_tag}")
print(f" {peer.get('address', '?')}")
if len(peers) > 5:
print(f" ... and {len(peers) - 5} more")
except ImportError:
print(" [httpx not installed - cannot query mesh API]")
except Exception as e:
print(f" [Error querying mesh: {e}]")
print("-" * 60)
def cmd_plugins(self, args: List[str] = None):
"""List loaded plugins"""
if not self.plugins:
@ -487,10 +716,13 @@ Commands:
print(f" Commands: {', '.join(plugin.commands)}")
def cmd_update(self, args: List[str] = None):
"""Self-update from mesh"""
"""Self-update from mesh or check for updates"""
print("🔄 Checking for updates...")
# Try to fetch latest version from mesh
update_source = None
remote_version = "0.0.0"
# Try to fetch latest version from mesh devices
for dev in self.devices.values():
if not dev.mesh_enabled:
continue
@ -500,18 +732,75 @@ Commands:
r = httpx.get(f"http://{dev.host}:7331/api/catalog/console", timeout=5)
if r.status_code == 200:
data = r.json()
remote_version = data.get("version", "0.0.0")
if remote_version > CONSOLE_VERSION:
print(f" New version available: {remote_version}")
# Download and update
# ... implementation
else:
print(f" Already up to date: {CONSOLE_VERSION}")
return
ver = data.get("version", "0.0.0")
if ver > remote_version:
remote_version = ver
update_source = dev
except:
continue
print(" No updates found or mesh unavailable.")
if not update_source:
print(" No mesh sources available.")
return
if remote_version <= CONSOLE_VERSION:
print(f" Already up to date: v{CONSOLE_VERSION}")
return
print(f" New version available: v{remote_version} (current: v{CONSOLE_VERSION})")
print(f" Source: {update_source.name} ({update_source.host})")
# Ask for confirmation
confirm = input(" Download and install? [y/N]: ").strip().lower()
if confirm != 'y':
print(" Update cancelled.")
return
# Download update from mesh device via SSH
print(f" 📥 Downloading from {update_source.name}...")
try:
import tempfile
import shutil
with tempfile.TemporaryDirectory() as tmpdir:
# Download console files from device
files_to_update = [
"/usr/lib/secubox-console/secubox_console.py",
"/usr/lib/secubox-console/secubox_frontend.py"
]
for remote_path in files_to_update:
local_path = Path(tmpdir) / Path(remote_path).name
stdout, stderr, code = self.ssh_exec(update_source, f"cat {remote_path}")
if code == 0 and stdout:
local_path.write_text(stdout)
print(f" Downloaded: {remote_path}")
# Verify downloads
downloaded = list(Path(tmpdir).glob("*.py"))
if not downloaded:
print(" ❌ No files downloaded.")
return
# Install update
print(" 📦 Installing update...")
install_dir = Path(__file__).parent
for py_file in downloaded:
target = install_dir / py_file.name
# Backup current file
if target.exists():
backup = target.with_suffix(".py.bak")
shutil.copy2(target, backup)
# Install new file
shutil.copy2(py_file, target)
print(f" Installed: {target.name}")
print(f"\n ✅ Updated to v{remote_version}!")
print(" Restart the console to use the new version.")
except Exception as e:
print(f" ❌ Update failed: {e}")
def cmd_dashboard(self, args: List[str] = None):
"""Live dashboard TUI"""

View File

@ -36,10 +36,11 @@ from concurrent.futures import ThreadPoolExecutor
# ============================================================================
try:
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer, Grid
from textual.widgets import Header, Footer, Static, Button, DataTable, Label, Input, TabbedContent, TabPane, Log, ProgressBar
from textual.reactive import reactive
from textual.timer import Timer
from textual.screen import Screen, ModalScreen
from textual import events
TEXTUAL_AVAILABLE = True
except ImportError:
@ -71,11 +72,13 @@ except ImportError:
# ============================================================================
# Configuration
# ============================================================================
VERSION = "1.0.0"
VERSION = "1.1.0"
CONFIG_DIR = Path.home() / ".secubox-frontend"
DEVICES_FILE = CONFIG_DIR / "devices.json"
SETTINGS_FILE = CONFIG_DIR / "settings.json"
LOG_FILE = CONFIG_DIR / "frontend.log"
NODE_ID_FILE = CONFIG_DIR / "node.id"
CONSOLE_PORT = 7332
# ============================================================================
# Data Models
@ -135,11 +138,78 @@ class DeviceManager:
self._ssh_cache = {}
self._executor = ThreadPoolExecutor(max_workers=10)
self._init_config()
self._init_node_identity()
self._load_devices()
def _init_config(self):
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
def _init_node_identity(self):
"""Initialize console's mesh node identity"""
import socket
import uuid
if NODE_ID_FILE.exists():
self.node_id = NODE_ID_FILE.read_text().strip()
else:
hostname = socket.gethostname()
mac = uuid.getnode()
self.node_id = f"console-{mac:012x}"[:20]
NODE_ID_FILE.write_text(self.node_id)
self.node_name = os.environ.get("SECUBOX_CONSOLE_NAME", f"console@{socket.gethostname()}")
def get_local_ip(self) -> str:
"""Get local IP for mesh announcement"""
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "127.0.0.1"
def announce_to_mesh(self) -> int:
"""Register this console as a peer on mesh devices"""
local_ip = self.get_local_ip()
peer_data = {
"id": self.node_id,
"name": self.node_name,
"address": local_ip,
"port": CONSOLE_PORT,
"type": "console",
"status": "online",
"version": VERSION
}
registered = 0
for dev in self.devices.values():
if not dev.mesh_id or dev.status != "online":
continue
if HTTPX_AVAILABLE:
try:
r = httpx.post(
f"http://{dev.host}:7331/api/peers",
json=peer_data,
timeout=3
)
if r.status_code in (200, 201):
registered += 1
except:
# Fallback to SSH registration
try:
cmd = f"/usr/sbin/secubox-p2p add-peer {local_ip} \"{self.node_name}\""
out, err, code = self.ssh_exec(dev, cmd)
if code == 0:
registered += 1
except:
pass
return registered
def _load_devices(self):
if DEVICES_FILE.exists():
try:
@ -318,12 +388,222 @@ class DeviceManager:
out, err, code = self.ssh_exec(device, "secubox-mesh sync")
return code == 0
def get_mesh_peers(self, device: Device) -> List[dict]:
"""Get mesh peers from device via P2P API"""
if not HTTPX_AVAILABLE:
return []
try:
r = httpx.get(f"http://{device.host}:7331/api/peers", timeout=2)
if r.status_code == 200:
return r.json().get("peers", [])
except:
pass
return []
def get_mesh_status(self, device: Device) -> dict:
"""Get mesh node status from device via P2P API"""
if not HTTPX_AVAILABLE:
return {}
try:
r = httpx.get(f"http://{device.host}:7331/api/status", timeout=2)
if r.status_code == 200:
return r.json()
except:
pass
return {}
# ============================================================================
# Textual TUI App (Modern)
# ============================================================================
if TEXTUAL_AVAILABLE:
class AddDeviceScreen(ModalScreen):
"""Modal screen for adding a new device"""
CSS = """
AddDeviceScreen {
align: center middle;
}
#add-dialog {
width: 60;
height: auto;
background: $surface;
border: solid $primary;
padding: 1 2;
}
#add-dialog Label {
margin-bottom: 1;
}
#add-dialog Input {
margin-bottom: 1;
}
#add-buttons {
layout: horizontal;
height: 3;
margin-top: 1;
}
#add-buttons Button {
margin-right: 1;
}
"""
def compose(self) -> ComposeResult:
with Vertical(id="add-dialog"):
yield Label("[bold]Add SecuBox Device[/]")
yield Label("Name:")
yield Input(placeholder="my-secubox", id="input-name")
yield Label("Host (IP or hostname):")
yield Input(placeholder="192.168.1.1", id="input-host")
yield Label("SSH Port (default: 22):")
yield Input(placeholder="22", id="input-port")
yield Label("SSH User (default: root):")
yield Input(placeholder="root", id="input-user")
with Horizontal(id="add-buttons"):
yield Button("Add", variant="primary", id="btn-add-confirm")
yield Button("Cancel", id="btn-add-cancel")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn-add-cancel":
self.dismiss(None)
elif event.button.id == "btn-add-confirm":
name = self.query_one("#input-name", Input).value.strip()
host = self.query_one("#input-host", Input).value.strip()
port = self.query_one("#input-port", Input).value.strip() or "22"
user = self.query_one("#input-user", Input).value.strip() or "root"
if name and host:
try:
port = int(port)
except ValueError:
port = 22
self.dismiss({"name": name, "host": host, "port": port, "user": user})
else:
self.notify("Name and Host are required", severity="error")
class DiscoverScreen(ModalScreen):
"""Modal screen for discovering devices"""
CSS = """
DiscoverScreen {
align: center middle;
}
#discover-dialog {
width: 70;
height: 20;
background: $surface;
border: solid $primary;
padding: 1 2;
}
#discover-log {
height: 12;
background: $surface-darken-1;
border: solid $accent;
}
"""
def __init__(self, manager: "DeviceManager"):
super().__init__()
self.manager = manager
def compose(self) -> ComposeResult:
with Vertical(id="discover-dialog"):
yield Label("[bold]🔍 Discovering SecuBox Devices[/]")
yield Log(id="discover-log")
with Horizontal():
yield Button("Close", id="btn-discover-close")
def on_mount(self) -> None:
self.run_worker(self._discover, exclusive=True)
async def _discover(self) -> None:
log = self.query_one("#discover-log", Log)
log.write_line("Starting discovery...")
discovered = []
# Phase 1: Query mesh peers
log.write_line("Phase 1: Querying mesh peers...")
for dev in self.manager.devices.values():
if dev.mesh_id:
log.write_line(f" Checking {dev.name}...")
peers = await asyncio.to_thread(self._query_mesh, dev.host)
for peer in peers:
if peer.get("address") and peer["address"] not in discovered:
discovered.append(peer["address"])
log.write_line(f" Found: {peer.get('name', 'unknown')} ({peer['address']})")
# Phase 2: Network scan (simplified)
log.write_line("Phase 2: Quick network scan...")
import socket
for subnet_end in ["192.168.255", "192.168.1"]:
for i in range(1, 20): # Quick scan first 20 IPs
ip = f"{subnet_end}.{i}"
if ip not in discovered:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.3)
if sock.connect_ex((ip, 7331)) == 0:
discovered.append(ip)
log.write_line(f" Found: {ip}")
sock.close()
except:
pass
# Phase 3: Probe and add
log.write_line(f"Phase 3: Probing {len(discovered)} hosts...")
added = 0
for ip in discovered:
result = await asyncio.to_thread(self._probe_host, ip)
if result:
self.manager.add_device(**result)
log.write_line(f" Added: {result['name']}")
added += 1
log.write_line(f"\nDone! Added {added} devices.")
def _query_mesh(self, host: str) -> list:
if not HTTPX_AVAILABLE:
return []
try:
r = httpx.get(f"http://{host}:7331/api/peers", timeout=2)
if r.status_code == 200:
return r.json().get("peers", [])
except:
pass
return []
def _probe_host(self, host: str) -> dict:
if not HTTPX_AVAILABLE:
return None
try:
r = httpx.get(f"http://{host}:7331/api/status", timeout=2)
if r.status_code == 200:
data = r.json()
name = data.get("node_name") or f"secubox-{host.split('.')[-1]}"
return {
"name": name,
"host": host,
"port": 22,
"user": "root"
}
except:
pass
return None
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn-discover-close":
self.dismiss(None)
class DeviceWidget(Static):
"""Single device status widget"""
@ -432,8 +712,10 @@ Services: {services}"""
("b", "backup", "Backup"),
("s", "sync", "Sync"),
("a", "add_device", "Add Device"),
("d", "dashboard", "Dashboard"),
("l", "logs", "Logs"),
("f", "discover", "Find Devices"),
("d", "delete_device", "Delete Device"),
("c", "connect_ssh", "SSH Connect"),
("m", "announce", "Announce to Mesh"),
]
def __init__(self, manager: DeviceManager):
@ -462,9 +744,10 @@ Services: {services}"""
with Horizontal(id="actions"):
yield Button("🔄 Refresh", id="btn-refresh", variant="primary")
yield Button("💾 Backup", id="btn-backup")
yield Button("🔗 Sync", id="btn-sync")
yield Button("🔍 Find", id="btn-discover")
yield Button(" Add", id="btn-add")
yield Button("📢 Announce", id="btn-announce")
yield Button("🔗 Sync", id="btn-sync")
yield Button("🖥️ SSH", id="btn-ssh")
yield Footer()
@ -481,6 +764,7 @@ Services: {services}"""
self._update_device_list()
self._update_dashboard()
self._update_alerts()
self._update_mesh()
def _update_device_list(self) -> None:
"""Update sidebar device list"""
@ -550,11 +834,71 @@ Services: {services}"""
f"[{severity_color}][{alert.type}][/] {alert.device}: {alert.message}"
)
def _update_mesh(self) -> None:
"""Update mesh visualization tab"""
content = self.query_one("#mesh-content", Static)
lines = ["[bold cyan]═══ Mesh Network ═══[/]\n"]
# Show console's own identity
lines.append("[bold yellow]This Console:[/]")
lines.append(f" ID: {self.manager.node_id}")
lines.append(f" Name: {self.manager.node_name}")
lines.append(f" IP: {self.manager.get_local_ip()}")
lines.append(f" Port: {CONSOLE_PORT}")
lines.append("")
# Count mesh-enabled devices
mesh_devices = [d for d in self.manager.devices.values() if d.mesh_id]
online_mesh = [d for d in mesh_devices if d.status == "online"]
lines.append(f"[bold]SecuBox Nodes:[/] [green]{len(online_mesh)}[/]/{len(mesh_devices)} online\n")
if not mesh_devices:
lines.append("[dim]No mesh-enabled devices found.[/]")
lines.append("\nTo enable mesh on a device:")
lines.append(" 1. Install secubox-p2p on the device")
lines.append(" 2. Configure via: uci set secubox-p2p.main.enabled=1")
lines.append(" 3. Start daemon: /etc/init.d/secubox-p2p start")
else:
lines.append("[bold]Connected Mesh Peers:[/]\n")
for dev in mesh_devices:
status_icon = "[green]●[/]" if dev.status == "online" else "[red]●[/]"
local_tag = "[yellow](local)[/] " if "local" in dev.mesh_id.lower() else ""
lines.append(f" {status_icon} {local_tag}[bold]{dev.name}[/]")
lines.append(f" ID: {dev.mesh_id}")
lines.append(f" Host: {dev.host}")
lines.append(f" Peers: {dev.mesh_peers}")
lines.append("")
# Fetch remote peers from first online mesh device
if online_mesh and HTTPX_AVAILABLE:
try:
r = httpx.get(f"http://{online_mesh[0].host}:7331/api/peers", timeout=2)
if r.status_code == 200:
data = r.json()
remote_peers = [p for p in data.get("peers", []) if not p.get("is_local")]
if remote_peers:
lines.append("[bold]Remote Peers (from mesh):[/]")
for peer in remote_peers[:10]: # Show max 10
status = "[green]●[/]" if peer.get("status") == "online" else "[red]●[/]"
lines.append(f" {status} {peer.get('name', 'unknown')} ({peer.get('address', '?')})")
except:
pass
content.update("\n".join(lines))
def on_button_pressed(self, event: Button.Pressed) -> None:
btn_id = event.button.id
if btn_id == "btn-refresh":
self.action_refresh()
elif btn_id == "btn-discover":
self.action_discover()
elif btn_id == "btn-announce":
self.action_announce()
elif btn_id == "btn-backup":
self.action_backup()
elif btn_id == "btn-sync":
@ -562,7 +906,7 @@ Services: {services}"""
elif btn_id == "btn-add":
self.action_add_device()
elif btn_id == "btn-ssh":
self.action_ssh()
self.action_connect_ssh()
elif btn_id and btn_id.startswith("dev-"):
self.selected_device = btn_id[4:]
self.notify(f"Selected: {self.selected_device}")
@ -590,17 +934,64 @@ Services: {services}"""
for dev in self.manager.devices.values():
if dev.status == "online" and dev.mesh_id:
self.manager.sync_mesh(dev)
# Re-announce console to mesh
self.manager.announce_to_mesh()
self.notify("Mesh synced!", severity="information")
def action_add_device(self) -> None:
# Would show input dialog in full implementation
self.notify("Use CLI: secubox-frontend --add name host")
"""Show add device dialog"""
def handle_add(result: dict) -> None:
if result:
self.manager.add_device(
result["name"],
result["host"],
result["port"],
result["user"]
)
self.notify(f"Added: {result['name']}", severity="information")
self._refresh_devices()
def action_ssh(self) -> None:
self.push_screen(AddDeviceScreen(), handle_add)
def action_discover(self) -> None:
"""Show discover devices dialog"""
def handle_discover(result) -> None:
self._refresh_devices()
self.push_screen(DiscoverScreen(self.manager), handle_discover)
def action_delete_device(self) -> None:
"""Delete selected device"""
if not self.selected_device:
self.notify("Select a device first", severity="warning")
return
self.manager.remove_device(self.selected_device)
self.notify(f"Removed: {self.selected_device}", severity="information")
self.selected_device = None
self._refresh_devices()
def action_connect_ssh(self) -> None:
"""SSH to selected device"""
if self.selected_device:
dev = self.manager.devices.get(self.selected_device)
if dev:
# Suspend app, run SSH, then resume
self.suspend()
os.system(f"ssh {dev.user}@{dev.host} -p {dev.port}")
self.resume()
else:
self.notify("Select a device first", severity="warning")
def action_announce(self) -> None:
"""Announce this console as a mesh peer"""
self.notify("Announcing to mesh...")
registered = self.manager.announce_to_mesh()
if registered:
self.notify(f"Registered on {registered} device(s)", severity="information")
else:
self.notify("No mesh devices available", severity="warning")
self._update_mesh()
# ============================================================================