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:
CyberMind-FR 2026-04-24 11:58:00 +02:00
parent 9516379e93
commit 5ab609d6af
2 changed files with 868 additions and 23 deletions

View 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

View File

@ -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)