mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
feat(eye-gateway): Add remote device connectivity and test dashboard
- Add remote.py with non-blocking SSH connection to Eye Remote - Add /api/v1/remote/* endpoints for device control - Add /dashboard with real-time metrics, services, and command execution - Support both emulator and remote device modes - Add service restart, log viewing, and device info endpoints The gateway now serves as the primary testing interface for Eye Remote without TTY blocking issues. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9516379e93
commit
5ab609d6af
354
tools/secubox-eye-gateway/gateway/remote.py
Normal file
354
tools/secubox-eye-gateway/gateway/remote.py
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
"""
|
||||
SecuBox Eye Gateway — Remote device connection.
|
||||
|
||||
Provides non-blocking interface to Eye Remote devices via SSH.
|
||||
|
||||
CyberMind — https://cybermind.fr
|
||||
Author: Gérald Kerma <gandalf@gk2.net>
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandResult:
|
||||
"""Result of a remote command execution."""
|
||||
|
||||
command: str
|
||||
stdout: str
|
||||
stderr: str
|
||||
return_code: int
|
||||
duration_ms: float
|
||||
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"command": self.command,
|
||||
"stdout": self.stdout,
|
||||
"stderr": self.stderr,
|
||||
"return_code": self.return_code,
|
||||
"duration_ms": self.duration_ms,
|
||||
"timestamp": self.timestamp,
|
||||
"success": self.return_code == 0,
|
||||
}
|
||||
|
||||
|
||||
class EyeRemoteConnection:
|
||||
"""Non-blocking SSH connection to Eye Remote device."""
|
||||
|
||||
# Known Eye Remote IP addresses
|
||||
OTG_IP = "10.55.0.2"
|
||||
WIFI_FALLBACK = "secubox-round.local"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str = OTG_IP,
|
||||
user: str = "pi",
|
||||
port: int = 22,
|
||||
timeout: float = 5.0,
|
||||
) -> None:
|
||||
"""Initialize connection parameters.
|
||||
|
||||
Args:
|
||||
host: Remote host IP or hostname
|
||||
user: SSH username
|
||||
port: SSH port
|
||||
timeout: Command timeout in seconds
|
||||
"""
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self._connected = False
|
||||
self._device_info: Optional[Dict[str, Any]] = None
|
||||
|
||||
@property
|
||||
def ssh_target(self) -> str:
|
||||
"""SSH target string (user@host)."""
|
||||
return f"{self.user}@{self.host}"
|
||||
|
||||
def _ssh_cmd(self, command: str) -> List[str]:
|
||||
"""Build SSH command array.
|
||||
|
||||
Args:
|
||||
command: Command to execute remotely
|
||||
|
||||
Returns:
|
||||
Full SSH command as list
|
||||
"""
|
||||
return [
|
||||
"ssh",
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "ConnectTimeout=3",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-p", str(self.port),
|
||||
self.ssh_target,
|
||||
command,
|
||||
]
|
||||
|
||||
async def check_connection(self) -> bool:
|
||||
"""Check if device is reachable.
|
||||
|
||||
Returns:
|
||||
True if SSH connection succeeds
|
||||
"""
|
||||
try:
|
||||
result = await self.execute("echo ok", timeout=3.0)
|
||||
self._connected = result.return_code == 0 and "ok" in result.stdout
|
||||
return self._connected
|
||||
except Exception as e:
|
||||
logger.warning(f"Connection check failed: {e}")
|
||||
self._connected = False
|
||||
return False
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
command: str,
|
||||
timeout: Optional[float] = None,
|
||||
) -> CommandResult:
|
||||
"""Execute command on remote device.
|
||||
|
||||
Args:
|
||||
command: Shell command to execute
|
||||
timeout: Override default timeout
|
||||
|
||||
Returns:
|
||||
CommandResult with output and status
|
||||
"""
|
||||
timeout = timeout or self.timeout
|
||||
ssh_cmd = self._ssh_cmd(command)
|
||||
|
||||
start = asyncio.get_event_loop().time()
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*ssh_cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
duration = (asyncio.get_event_loop().time() - start) * 1000
|
||||
|
||||
return CommandResult(
|
||||
command=command,
|
||||
stdout=stdout.decode("utf-8", errors="replace").strip(),
|
||||
stderr=stderr.decode("utf-8", errors="replace").strip(),
|
||||
return_code=proc.returncode or 0,
|
||||
duration_ms=round(duration, 2),
|
||||
)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
duration = (asyncio.get_event_loop().time() - start) * 1000
|
||||
return CommandResult(
|
||||
command=command,
|
||||
stdout="",
|
||||
stderr=f"Command timed out after {timeout}s",
|
||||
return_code=-1,
|
||||
duration_ms=round(duration, 2),
|
||||
)
|
||||
except Exception as e:
|
||||
duration = (asyncio.get_event_loop().time() - start) * 1000
|
||||
return CommandResult(
|
||||
command=command,
|
||||
stdout="",
|
||||
stderr=str(e),
|
||||
return_code=-2,
|
||||
duration_ms=round(duration, 2),
|
||||
)
|
||||
|
||||
async def get_device_info(self) -> Dict[str, Any]:
|
||||
"""Get device information.
|
||||
|
||||
Returns:
|
||||
Device info dictionary
|
||||
"""
|
||||
if self._device_info:
|
||||
return self._device_info
|
||||
|
||||
# Gather device info with parallel commands
|
||||
commands = {
|
||||
"hostname": "hostname",
|
||||
"kernel": "uname -r",
|
||||
"uptime": "cat /proc/uptime | cut -d' ' -f1",
|
||||
"cpu_model": "cat /proc/cpuinfo | grep 'model name' | head -1 | cut -d: -f2",
|
||||
"mem_total": "grep MemTotal /proc/meminfo | awk '{print $2}'",
|
||||
"ip_usb0": "ip -4 addr show usb0 2>/dev/null | grep inet | awk '{print $2}'",
|
||||
}
|
||||
|
||||
results = {}
|
||||
for key, cmd in commands.items():
|
||||
result = await self.execute(cmd, timeout=3.0)
|
||||
results[key] = result.stdout if result.return_code == 0 else ""
|
||||
|
||||
self._device_info = {
|
||||
"hostname": results.get("hostname", "unknown"),
|
||||
"kernel": results.get("kernel", ""),
|
||||
"uptime_seconds": float(results.get("uptime", "0") or "0"),
|
||||
"cpu_model": results.get("cpu_model", "").strip(),
|
||||
"memory_kb": int(results.get("mem_total", "0") or "0"),
|
||||
"ip_address": results.get("ip_usb0", "").split("/")[0],
|
||||
"connected": self._connected,
|
||||
}
|
||||
|
||||
return self._device_info
|
||||
|
||||
async def get_metrics(self) -> Dict[str, Any]:
|
||||
"""Get system metrics from device.
|
||||
|
||||
Returns:
|
||||
Metrics dictionary
|
||||
"""
|
||||
cmd = """python3 -c "
|
||||
import json, os
|
||||
stat = os.statvfs('/')
|
||||
mem = {}
|
||||
with open('/proc/meminfo') as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
mem[parts[0].rstrip(':')] = int(parts[1])
|
||||
temp = 0
|
||||
try:
|
||||
with open('/sys/class/thermal/thermal_zone0/temp') as f:
|
||||
temp = int(f.read()) / 1000
|
||||
except: pass
|
||||
load = os.getloadavg()
|
||||
print(json.dumps({
|
||||
'cpu_percent': round(load[0] * 100 / os.cpu_count(), 1),
|
||||
'memory_percent': round(100 * (1 - mem.get('MemAvailable', 0) / mem.get('MemTotal', 1)), 1),
|
||||
'disk_percent': round(100 * (1 - stat.f_bavail / stat.f_blocks), 1),
|
||||
'temperature': round(temp, 1),
|
||||
'load_avg': round(load[0], 2),
|
||||
}))
|
||||
"
|
||||
"""
|
||||
result = await self.execute(cmd.strip(), timeout=5.0)
|
||||
|
||||
if result.return_code == 0:
|
||||
try:
|
||||
import json
|
||||
return json.loads(result.stdout)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"cpu_percent": 0,
|
||||
"memory_percent": 0,
|
||||
"disk_percent": 0,
|
||||
"temperature": 0,
|
||||
"load_avg": 0,
|
||||
"error": result.stderr or "Failed to get metrics",
|
||||
}
|
||||
|
||||
async def get_services_status(self) -> Dict[str, str]:
|
||||
"""Get status of SecuBox services.
|
||||
|
||||
Returns:
|
||||
Dictionary of service names to status
|
||||
"""
|
||||
services = [
|
||||
"secubox-eye-agent",
|
||||
"secubox-eye-gadget",
|
||||
"hyperpixel2r-init",
|
||||
"pigpiod",
|
||||
]
|
||||
|
||||
results = {}
|
||||
for svc in services:
|
||||
result = await self.execute(
|
||||
f"systemctl is-active {svc} 2>/dev/null || echo unknown",
|
||||
timeout=3.0,
|
||||
)
|
||||
results[svc] = result.stdout.strip() if result.return_code == 0 else "error"
|
||||
|
||||
return results
|
||||
|
||||
async def restart_service(self, service: str) -> CommandResult:
|
||||
"""Restart a systemd service.
|
||||
|
||||
Args:
|
||||
service: Service name (without .service suffix)
|
||||
|
||||
Returns:
|
||||
Command result
|
||||
"""
|
||||
return await self.execute(
|
||||
f"sudo systemctl restart {service}",
|
||||
timeout=10.0,
|
||||
)
|
||||
|
||||
async def capture_screenshot(self) -> Optional[bytes]:
|
||||
"""Capture framebuffer screenshot.
|
||||
|
||||
Returns:
|
||||
PNG image bytes or None on failure
|
||||
"""
|
||||
# Capture framebuffer and convert to PNG
|
||||
cmd = """
|
||||
cat /dev/fb0 | python3 -c "
|
||||
import sys
|
||||
from PIL import Image
|
||||
data = sys.stdin.buffer.read()
|
||||
# HyperPixel 2.1 Round: 480x480 RGB888
|
||||
img = Image.frombytes('RGB', (480, 480), data[:480*480*3])
|
||||
img.save(sys.stdout.buffer, 'PNG')
|
||||
" 2>/dev/null
|
||||
"""
|
||||
result = await self.execute(cmd.strip(), timeout=10.0)
|
||||
|
||||
if result.return_code == 0 and result.stdout:
|
||||
# Note: stdout is text, need binary transfer
|
||||
# For now, return None and suggest SCP
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
async def get_journal_logs(
|
||||
self,
|
||||
unit: str = "secubox-eye-agent",
|
||||
lines: int = 50,
|
||||
) -> str:
|
||||
"""Get recent journal logs.
|
||||
|
||||
Args:
|
||||
unit: Systemd unit to filter
|
||||
lines: Number of lines to return
|
||||
|
||||
Returns:
|
||||
Log output
|
||||
"""
|
||||
result = await self.execute(
|
||||
f"journalctl -u {unit} -n {lines} --no-pager",
|
||||
timeout=5.0,
|
||||
)
|
||||
return result.stdout if result.return_code == 0 else result.stderr
|
||||
|
||||
|
||||
# Global connection instance
|
||||
_connection: Optional[EyeRemoteConnection] = None
|
||||
|
||||
|
||||
def get_connection() -> EyeRemoteConnection:
|
||||
"""Get or create the global connection."""
|
||||
global _connection
|
||||
if _connection is None:
|
||||
_connection = EyeRemoteConnection()
|
||||
return _connection
|
||||
|
||||
|
||||
def set_connection(conn: EyeRemoteConnection) -> None:
|
||||
"""Set the global connection."""
|
||||
global _connection
|
||||
_connection = conn
|
||||
|
|
@ -1,52 +1,70 @@
|
|||
"""
|
||||
SecuBox Eye Gateway — FastAPI server for development.
|
||||
|
||||
Provides both emulation mode and real device connectivity.
|
||||
|
||||
CyberMind — https://cybermind.fr
|
||||
Author: Gérald Kerma <gandalf@gk2.net>
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .emulator import SecuBoxEmulator
|
||||
from .remote import EyeRemoteConnection, get_connection, set_connection
|
||||
|
||||
# Global emulator instance
|
||||
_emulator: Optional[SecuBoxEmulator] = None
|
||||
_mode: str = "emulator" # "emulator" or "remote"
|
||||
|
||||
|
||||
def set_emulator(emulator: SecuBoxEmulator) -> None:
|
||||
"""Set the global emulator instance.
|
||||
|
||||
Args:
|
||||
emulator: SecuBoxEmulator instance to use
|
||||
"""
|
||||
global _emulator
|
||||
"""Set the global emulator instance."""
|
||||
global _emulator, _mode
|
||||
_emulator = emulator
|
||||
_mode = "emulator"
|
||||
|
||||
|
||||
def set_remote_mode(host: str = "10.55.0.2", user: str = "pi") -> None:
|
||||
"""Switch to remote device mode."""
|
||||
global _mode
|
||||
conn = EyeRemoteConnection(host=host, user=user)
|
||||
set_connection(conn)
|
||||
_mode = "remote"
|
||||
|
||||
|
||||
def get_emulator() -> SecuBoxEmulator:
|
||||
"""Get the global emulator instance.
|
||||
|
||||
Returns:
|
||||
The configured SecuBoxEmulator
|
||||
|
||||
Raises:
|
||||
HTTPException: If emulator is not configured
|
||||
"""
|
||||
"""Get the global emulator instance."""
|
||||
if _emulator is None:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Emulator not configured"
|
||||
)
|
||||
raise HTTPException(status_code=503, detail="Emulator not configured")
|
||||
return _emulator
|
||||
|
||||
|
||||
def is_remote_mode() -> bool:
|
||||
"""Check if running in remote mode."""
|
||||
return _mode == "remote"
|
||||
|
||||
|
||||
# Pydantic models for API
|
||||
class CommandRequest(BaseModel):
|
||||
"""Request to execute a command."""
|
||||
command: str
|
||||
timeout: float = 5.0
|
||||
|
||||
|
||||
class ServiceAction(BaseModel):
|
||||
"""Request to perform service action."""
|
||||
service: str
|
||||
action: str = "restart"
|
||||
|
||||
|
||||
# FastAPI application
|
||||
app = FastAPI(
|
||||
title="SecuBox Eye Gateway",
|
||||
description="Development gateway for SecuBox Eye Remote",
|
||||
version="1.0.0",
|
||||
description="Development gateway for SecuBox Eye Remote — Emulation and Remote modes",
|
||||
version="2.0.0",
|
||||
)
|
||||
|
||||
# Enable CORS for development
|
||||
|
|
@ -131,11 +149,484 @@ async def discover() -> Dict[str, Any]:
|
|||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, str]:
|
||||
async def root() -> Dict[str, Any]:
|
||||
"""Root endpoint with basic info."""
|
||||
return {
|
||||
"name": "SecuBox Eye Gateway",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Development gateway for Eye Remote",
|
||||
"mode": _mode,
|
||||
"docs": "/docs",
|
||||
"dashboard": "/dashboard",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Remote Device Endpoints
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.get("/api/v1/remote/status")
|
||||
async def remote_status() -> Dict[str, Any]:
|
||||
"""Check remote device connection status."""
|
||||
conn = get_connection()
|
||||
connected = await conn.check_connection()
|
||||
return {
|
||||
"mode": _mode,
|
||||
"host": conn.host,
|
||||
"user": conn.user,
|
||||
"connected": connected,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/v1/remote/connect")
|
||||
async def remote_connect(
|
||||
host: str = Query(default="10.55.0.2"),
|
||||
user: str = Query(default="pi"),
|
||||
) -> Dict[str, Any]:
|
||||
"""Connect to a remote Eye Remote device."""
|
||||
conn = EyeRemoteConnection(host=host, user=user)
|
||||
connected = await conn.check_connection()
|
||||
|
||||
if connected:
|
||||
set_connection(conn)
|
||||
global _mode
|
||||
_mode = "remote"
|
||||
return {
|
||||
"status": "connected",
|
||||
"host": host,
|
||||
"mode": "remote",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "failed",
|
||||
"host": host,
|
||||
"error": f"Cannot connect to {user}@{host}",
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/v1/remote/command")
|
||||
async def remote_command(req: CommandRequest) -> Dict[str, Any]:
|
||||
"""Execute command on remote device."""
|
||||
if not is_remote_mode():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Not in remote mode. Use /api/v1/remote/connect first.",
|
||||
)
|
||||
|
||||
conn = get_connection()
|
||||
result = await conn.execute(req.command, timeout=req.timeout)
|
||||
return result.to_dict()
|
||||
|
||||
|
||||
@app.get("/api/v1/remote/metrics")
|
||||
async def remote_metrics() -> Dict[str, Any]:
|
||||
"""Get metrics from remote device."""
|
||||
if is_remote_mode():
|
||||
conn = get_connection()
|
||||
return await conn.get_metrics()
|
||||
else:
|
||||
# Fallback to emulator
|
||||
emulator = get_emulator()
|
||||
return emulator.get_metrics()
|
||||
|
||||
|
||||
@app.get("/api/v1/remote/services")
|
||||
async def remote_services() -> Dict[str, Any]:
|
||||
"""Get status of SecuBox services on remote device."""
|
||||
if not is_remote_mode():
|
||||
return {
|
||||
"mode": "emulator",
|
||||
"services": {
|
||||
"secubox-eye-agent": "emulated",
|
||||
"secubox-eye-gadget": "emulated",
|
||||
"hyperpixel2r-init": "emulated",
|
||||
"pigpiod": "emulated",
|
||||
},
|
||||
}
|
||||
|
||||
conn = get_connection()
|
||||
services = await conn.get_services_status()
|
||||
return {
|
||||
"mode": "remote",
|
||||
"host": conn.host,
|
||||
"services": services,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/v1/remote/services/restart")
|
||||
async def remote_service_restart(req: ServiceAction) -> Dict[str, Any]:
|
||||
"""Restart a service on remote device."""
|
||||
if not is_remote_mode():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Not in remote mode",
|
||||
)
|
||||
|
||||
conn = get_connection()
|
||||
result = await conn.restart_service(req.service)
|
||||
return {
|
||||
"service": req.service,
|
||||
"action": "restart",
|
||||
"result": result.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/v1/remote/logs")
|
||||
async def remote_logs(
|
||||
unit: str = Query(default="secubox-eye-agent"),
|
||||
lines: int = Query(default=50, le=500),
|
||||
) -> Dict[str, Any]:
|
||||
"""Get journal logs from remote device."""
|
||||
if not is_remote_mode():
|
||||
return {
|
||||
"mode": "emulator",
|
||||
"logs": "[Emulator mode - no logs available]",
|
||||
}
|
||||
|
||||
conn = get_connection()
|
||||
logs = await conn.get_journal_logs(unit=unit, lines=lines)
|
||||
return {
|
||||
"mode": "remote",
|
||||
"unit": unit,
|
||||
"lines": lines,
|
||||
"logs": logs,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/v1/remote/device")
|
||||
async def remote_device_info() -> Dict[str, Any]:
|
||||
"""Get device information from remote."""
|
||||
if not is_remote_mode():
|
||||
emulator = get_emulator()
|
||||
return {
|
||||
"mode": "emulator",
|
||||
**emulator.get_discovery_info(),
|
||||
}
|
||||
|
||||
conn = get_connection()
|
||||
info = await conn.get_device_info()
|
||||
return {
|
||||
"mode": "remote",
|
||||
**info,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Test Dashboard (HTML)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
|
||||
@app.get("/dashboard", response_class=HTMLResponse)
|
||||
async def dashboard():
|
||||
"""Serve the test dashboard HTML."""
|
||||
html = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=480, height=480, user-scalable=no">
|
||||
<title>SecuBox Eye Gateway — Test Dashboard</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #0a0a0f;
|
||||
color: #e8e6d9;
|
||||
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { max-width: 960px; margin: 0 auto; }
|
||||
h1 { color: #00d4ff; margin-bottom: 20px; font-size: 24px; }
|
||||
h2 { color: #c9a84c; margin: 20px 0 10px; font-size: 18px; }
|
||||
.status-bar {
|
||||
display: flex; gap: 20px; padding: 15px;
|
||||
background: #1a1a2e; border-radius: 8px; margin-bottom: 20px;
|
||||
}
|
||||
.status-item { display: flex; align-items: center; gap: 8px; }
|
||||
.status-dot { width: 12px; height: 12px; border-radius: 50%; }
|
||||
.status-dot.ok { background: #00ff41; }
|
||||
.status-dot.error { background: #e63946; }
|
||||
.status-dot.warn { background: #c9a84c; }
|
||||
.card {
|
||||
background: #1a1a2e; border-radius: 8px; padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.metrics-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px;
|
||||
}
|
||||
.metric {
|
||||
background: #252538; border-radius: 6px; padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.metric-value { font-size: 32px; font-weight: bold; color: #00d4ff; }
|
||||
.metric-label { font-size: 12px; color: #6b6b7a; margin-top: 5px; }
|
||||
.btn {
|
||||
background: #3D35A0; color: white; border: none; padding: 10px 20px;
|
||||
border-radius: 4px; cursor: pointer; font-family: inherit; margin: 5px;
|
||||
}
|
||||
.btn:hover { background: #5046c0; }
|
||||
.btn.danger { background: #e63946; }
|
||||
.btn.success { background: #0A5840; }
|
||||
.terminal {
|
||||
background: #000; color: #00ff41; padding: 15px;
|
||||
font-family: monospace; font-size: 12px;
|
||||
height: 200px; overflow-y: auto; border-radius: 4px;
|
||||
white-space: pre-wrap; word-break: break-all;
|
||||
}
|
||||
input[type="text"] {
|
||||
background: #252538; border: 1px solid #3a3a4a; color: #e8e6d9;
|
||||
padding: 10px; border-radius: 4px; font-family: inherit; width: 100%;
|
||||
}
|
||||
.services { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.service {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: #252538; padding: 8px 12px; border-radius: 4px;
|
||||
}
|
||||
.service.active .status-dot { background: #00ff41; }
|
||||
.service.inactive .status-dot { background: #e63946; }
|
||||
.service.unknown .status-dot { background: #6b6b7a; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>⚡ SecuBox Eye Gateway</h1>
|
||||
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<span class="status-dot" id="mode-dot"></span>
|
||||
<span>Mode: <strong id="mode-text">-</strong></span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-dot" id="conn-dot"></span>
|
||||
<span>Connection: <strong id="conn-text">-</strong></span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>Host: <strong id="host-text">-</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>📡 Connect to Device</h2>
|
||||
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
||||
<input type="text" id="host-input" placeholder="10.55.0.2" value="10.55.0.2">
|
||||
<button class="btn success" onclick="connectDevice()">Connect</button>
|
||||
<button class="btn" onclick="useEmulator()">Emulator Mode</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>📊 System Metrics</h2>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric">
|
||||
<div class="metric-value" id="cpu">-</div>
|
||||
<div class="metric-label">CPU %</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value" id="memory">-</div>
|
||||
<div class="metric-label">Memory %</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value" id="disk">-</div>
|
||||
<div class="metric-label">Disk %</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value" id="temp">-</div>
|
||||
<div class="metric-label">Temp °C</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value" id="load">-</div>
|
||||
<div class="metric-label">Load Avg</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value" id="uptime">-</div>
|
||||
<div class="metric-label">Uptime</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>🔧 Services</h2>
|
||||
<div class="services" id="services">
|
||||
<div class="service unknown"><span class="status-dot"></span>Loading...</div>
|
||||
</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<button class="btn" onclick="restartAgent()">Restart Agent</button>
|
||||
<button class="btn" onclick="restartGadget()">Restart Gadget</button>
|
||||
<button class="btn" onclick="refreshServices()">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>💻 Remote Command</h2>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
<input type="text" id="cmd-input" placeholder="Enter command..." value="uname -a">
|
||||
<button class="btn" onclick="runCommand()">Run</button>
|
||||
</div>
|
||||
<div class="terminal" id="terminal">Ready...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>📜 Logs</h2>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<button class="btn" onclick="getLogs('secubox-eye-agent')">Agent Logs</button>
|
||||
<button class="btn" onclick="getLogs('secubox-eye-gadget')">Gadget Logs</button>
|
||||
<button class="btn" onclick="getLogs('hyperpixel2r-init')">Display Logs</button>
|
||||
</div>
|
||||
<div class="terminal" id="logs">Select a log source...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '';
|
||||
let refreshInterval;
|
||||
|
||||
async function fetchJson(url, opts = {}) {
|
||||
const res = await fetch(API + url, opts);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const status = await fetchJson('/api/v1/remote/status');
|
||||
document.getElementById('mode-text').textContent = status.mode;
|
||||
document.getElementById('conn-text').textContent = status.connected ? 'Connected' : 'Disconnected';
|
||||
document.getElementById('host-text').textContent = status.host;
|
||||
document.getElementById('mode-dot').className = 'status-dot ' + (status.mode === 'remote' ? 'ok' : 'warn');
|
||||
document.getElementById('conn-dot').className = 'status-dot ' + (status.connected ? 'ok' : 'error');
|
||||
} catch (e) {
|
||||
console.error('Status error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshMetrics() {
|
||||
try {
|
||||
const m = await fetchJson('/api/v1/remote/metrics');
|
||||
document.getElementById('cpu').textContent = (m.cpu_percent || 0).toFixed(1);
|
||||
document.getElementById('memory').textContent = (m.memory_percent || 0).toFixed(1);
|
||||
document.getElementById('disk').textContent = (m.disk_percent || 0).toFixed(1);
|
||||
document.getElementById('temp').textContent = (m.temperature || 0).toFixed(1);
|
||||
document.getElementById('load').textContent = (m.load_avg || 0).toFixed(2);
|
||||
if (m.uptime_seconds) {
|
||||
const h = Math.floor(m.uptime_seconds / 3600);
|
||||
const min = Math.floor((m.uptime_seconds % 3600) / 60);
|
||||
document.getElementById('uptime').textContent = h + 'h' + min + 'm';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Metrics error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshServices() {
|
||||
try {
|
||||
const data = await fetchJson('/api/v1/remote/services');
|
||||
const container = document.getElementById('services');
|
||||
container.innerHTML = '';
|
||||
for (const [name, status] of Object.entries(data.services)) {
|
||||
const cls = status === 'active' ? 'active' : (status === 'inactive' ? 'inactive' : 'unknown');
|
||||
container.innerHTML += `<div class="service ${cls}"><span class="status-dot"></span>${name}: ${status}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Services error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function connectDevice() {
|
||||
const host = document.getElementById('host-input').value || '10.55.0.2';
|
||||
const term = document.getElementById('terminal');
|
||||
term.textContent = 'Connecting to ' + host + '...';
|
||||
try {
|
||||
const res = await fetchJson('/api/v1/remote/connect?host=' + host, { method: 'POST' });
|
||||
term.textContent = JSON.stringify(res, null, 2);
|
||||
if (res.status === 'connected') {
|
||||
refreshAll();
|
||||
}
|
||||
} catch (e) {
|
||||
term.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function useEmulator() {
|
||||
document.getElementById('mode-text').textContent = 'emulator';
|
||||
document.getElementById('mode-dot').className = 'status-dot warn';
|
||||
document.getElementById('terminal').textContent = 'Switched to emulator mode';
|
||||
refreshAll();
|
||||
}
|
||||
|
||||
async function runCommand() {
|
||||
const cmd = document.getElementById('cmd-input').value;
|
||||
const term = document.getElementById('terminal');
|
||||
term.textContent = '$ ' + cmd + '\\n\\nExecuting...';
|
||||
try {
|
||||
const res = await fetchJson('/api/v1/remote/command', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command: cmd }),
|
||||
});
|
||||
term.textContent = '$ ' + cmd + '\\n\\n' + (res.stdout || res.stderr || 'No output');
|
||||
if (res.return_code !== 0) {
|
||||
term.textContent += '\\n\\n[Exit code: ' + res.return_code + ']';
|
||||
}
|
||||
} catch (e) {
|
||||
term.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function getLogs(unit) {
|
||||
const logsDiv = document.getElementById('logs');
|
||||
logsDiv.textContent = 'Loading ' + unit + ' logs...';
|
||||
try {
|
||||
const res = await fetchJson('/api/v1/remote/logs?unit=' + unit + '&lines=100');
|
||||
logsDiv.textContent = res.logs || 'No logs available';
|
||||
} catch (e) {
|
||||
logsDiv.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function restartAgent() {
|
||||
await restartService('secubox-eye-agent');
|
||||
}
|
||||
|
||||
async function restartGadget() {
|
||||
await restartService('secubox-eye-gadget');
|
||||
}
|
||||
|
||||
async function restartService(name) {
|
||||
const term = document.getElementById('terminal');
|
||||
term.textContent = 'Restarting ' + name + '...';
|
||||
try {
|
||||
const res = await fetchJson('/api/v1/remote/services/restart', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ service: name }),
|
||||
});
|
||||
term.textContent = JSON.stringify(res, null, 2);
|
||||
setTimeout(refreshServices, 2000);
|
||||
} catch (e) {
|
||||
term.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
refreshStatus();
|
||||
refreshMetrics();
|
||||
refreshServices();
|
||||
}
|
||||
|
||||
// Initial load
|
||||
refreshAll();
|
||||
|
||||
// Auto-refresh every 5 seconds
|
||||
refreshInterval = setInterval(() => {
|
||||
refreshStatus();
|
||||
refreshMetrics();
|
||||
}, 5000);
|
||||
|
||||
// Handle Enter key in command input
|
||||
document.getElementById('cmd-input').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') runCommand();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html, status_code=200)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user