From 204e04bf50d2db0a5a05e76777f945833c4e757d Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Wed, 22 Apr 2026 09:27:07 +0200 Subject: [PATCH] fix(eye-gateway): Align emulator metrics with SecuBox API field names - Add hostname, uptime_seconds to metrics output - Add field aliases: mem_percent, cpu_temp, load_avg_1, wifi_rssi - Add test-dashboard-amd64.py for testing on x86_64 with pygame Co-Authored-By: Claude Opus 4.5 --- remote-ui/round/test-dashboard-amd64.py | 382 ++++++++++++++++++ tools/secubox-eye-gateway/gateway/emulator.py | 5 +- tools/secubox-eye-gateway/gateway/profiles.py | 17 +- 3 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 remote-ui/round/test-dashboard-amd64.py diff --git a/remote-ui/round/test-dashboard-amd64.py b/remote-ui/round/test-dashboard-amd64.py new file mode 100644 index 00000000..9a3f8123 --- /dev/null +++ b/remote-ui/round/test-dashboard-amd64.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +""" +SecuBox Eye Remote — AMD64 Dashboard Tester +Runs the framebuffer dashboard in a window for testing on x86_64. + +Uses pygame for display instead of /dev/fb0. +Connect to the gateway emulator (secubox-eye-gateway) for testing. + +CyberMind — https://cybermind.fr +Author: Gérald Kerma + +Requirements: + pip install pygame pillow requests + +Usage: + # Start the gateway emulator first: + secubox-eye-gateway --profile stressed --port 8765 + + # Then run this test: + python3 test-dashboard-amd64.py --api http://localhost:8765 +""" +import os +import sys +import time +import math +import json +import argparse +import socket as sock_module +from datetime import datetime + +try: + import pygame +except ImportError: + print("pygame required: pip install pygame") + sys.exit(1) + +try: + from PIL import Image, ImageDraw, ImageFont +except ImportError: + print("pillow required: pip install pillow") + sys.exit(1) + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + print("requests not available, using simulation mode") + +# Display config +WIDTH = 480 +HEIGHT = 480 + +# Colors (RGB) — Neon theme +BG_COLOR = (8, 8, 12) +TEXT_COLOR = (240, 240, 250) +TEXT_MUTED = (130, 130, 150) +STATUS_OK = (0, 255, 65) +STATUS_WARN = (255, 100, 0) +STATUS_SIM = (100, 100, 120) + +# Module colors — Fluorescent/Phosphorescent neon palette +MODULES = { + 'AUTH': {'color': (255, 0, 100), 'metric': 'cpu', 'unit': '%', 'r': 214}, + 'WALL': {'color': (255, 100, 0), 'metric': 'mem', 'unit': '%', 'r': 201}, + 'BOOT': {'color': (220, 255, 0), 'metric': 'disk', 'unit': '%', 'r': 188}, + 'MIND': {'color': (0, 255, 65), 'metric': 'load', 'unit': 'x', 'r': 175}, + 'ROOT': {'color': (0, 255, 255), 'metric': 'temp', 'unit': '°', 'r': 162}, + 'MESH': {'color': (185, 0, 255), 'metric': 'wifi', 'unit': 'dB', 'r': 149}, +} + + +class APIMetricsSource: + """Fetch metrics from SecuBox API or gateway emulator.""" + + def __init__(self, api_base: str): + self.api_base = api_base.rstrip('/') + self.mode = 'API' + self.host = api_base + self.device_name = '' + self._connected = False + self._probe() + + def _probe(self): + """Try to connect to API.""" + if not HAS_REQUESTS: + self.mode = 'SIM' + return + + try: + r = requests.get(f'{self.api_base}/api/v1/health', timeout=3) + if r.status_code == 200: + data = r.json() + self.mode = data.get('mode', 'API').upper() + self.device_name = f"emulator:{data.get('profile', 'normal')}" + self._connected = True + print(f'Connected to API at {self.api_base}') + print(f' Mode: {self.mode}, Profile: {data.get("profile")}') + return + except Exception as e: + print(f'API probe failed: {e}') + + self.mode = 'SIM' + print('Using simulation mode') + + def get_metrics(self): + """Fetch current metrics. + + Returns: + tuple: (metrics_dict, mode, host, device_name) + """ + if not self._connected or not HAS_REQUESTS: + return self._simulate(), 'SIM', '', '' + + try: + r = requests.get(f'{self.api_base}/api/v1/system/metrics', timeout=3) + if r.status_code == 200: + data = r.json() + return { + 'cpu': data.get('cpu_percent', 0), + 'mem': data.get('memory_percent', data.get('mem_percent', 0)), + 'disk': data.get('disk_percent', 0), + 'load': data.get('load_avg_1', 0), + 'temp': data.get('cpu_temp', 0), + 'wifi': data.get('wifi_rssi', -50), + 'uptime': data.get('uptime_seconds', 0), + 'hostname': data.get('hostname', 'secubox'), + }, self.mode, self.host, self.device_name + except Exception as e: + print(f'API error: {e}') + + return self._simulate(), 'SIM', '', '' + + def _simulate(self): + """Generate simulated metrics.""" + import random + return { + 'cpu': 25 + random.uniform(-5, 5), + 'mem': 45 + random.uniform(-3, 3), + 'disk': 32 + random.uniform(-1, 1), + 'load': 0.5 + random.uniform(-0.2, 0.2), + 'temp': 42 + random.uniform(-2, 2), + 'wifi': -55 + random.randint(-5, 5), + 'uptime': int(time.time()) % 86400, + 'hostname': 'test-secubox', + } + + +def format_uptime(seconds): + """Format uptime as 'up XhYY'.""" + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return f'up {hours}h{minutes:02d}' + + +def draw_dashboard(metrics, mode='SIM', host='', device_name=''): + """Draw the dashboard to a PIL image.""" + img = Image.new('RGBA', (WIDTH, HEIGHT), BG_COLOR + (255,)) + draw = ImageDraw.Draw(img) + + cx, cy = WIDTH // 2, HEIGHT // 2 + + # Load fonts + try: + font_large = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 42) + font_medium = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 18) + font_small = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 14) + font_tiny = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 11) + except: + font_large = ImageFont.load_default() + font_medium = font_large + font_small = font_large + font_tiny = font_large + + # Draw circular border + draw.ellipse([10, 10, WIDTH-10, HEIGHT-10], outline=(40, 40, 40), width=2) + + # Draw module rings + for name, mod in MODULES.items(): + r = mod['r'] + color = mod['color'] + metric_name = mod['metric'] + value = metrics.get(metric_name, 0) + + # Calculate percentage for arc + if metric_name == 'load': + pct = min(100, value * 25) + elif metric_name == 'wifi': + pct = min(100, max(0, (value + 80) * 2)) + elif metric_name == 'temp': + pct = min(100, max(0, (value - 30) * 2.5)) + else: + pct = min(100, max(0, value)) + + # Draw background arc + for angle in range(0, 360, 2): + rad = math.radians(angle - 90) + x = cx + r * math.cos(rad) + y = cy + r * math.sin(rad) + draw.ellipse([x-2, y-2, x+2, y+2], fill=(30, 30, 30)) + + # Draw value arc + arc_end = int(pct * 3.6) + for angle in range(0, arc_end, 2): + rad = math.radians(angle - 90) + x = cx + r * math.cos(rad) + y = cy + r * math.sin(rad) + draw.ellipse([x-3, y-3, x+3, y+3], fill=color) + + # Draw head dot + if arc_end > 0: + rad = math.radians(arc_end - 90) + x = cx + r * math.cos(rad) + y = cy + r * math.sin(rad) + draw.ellipse([x-5, y-5, x+5, y+5], fill=(255, 255, 255)) + + # Center info + now = datetime.now() + time_str = now.strftime('%H:%M:%S') + date_str = now.strftime('%a %d %b') + + # Time + bbox = draw.textbbox((0, 0), time_str, font=font_large) + tw = bbox[2] - bbox[0] + draw.text((cx - tw//2, cy - 50), time_str, fill=TEXT_COLOR, font=font_large) + + # Date + bbox = draw.textbbox((0, 0), date_str, font=font_medium) + tw = bbox[2] - bbox[0] + draw.text((cx - tw//2, cy), date_str, fill=TEXT_MUTED, font=font_medium) + + # Hostname + hostname = metrics.get('hostname', 'secubox') + bbox = draw.textbbox((0, 0), hostname, font=font_small) + tw = bbox[2] - bbox[0] + draw.text((cx - tw//2, cy + 25), hostname, fill=TEXT_MUTED, font=font_small) + + # Uptime + uptime_str = format_uptime(metrics.get('uptime', 0)) + bbox = draw.textbbox((0, 0), uptime_str, font=font_small) + tw = bbox[2] - bbox[0] + draw.text((cx - tw//2, cy + 45), uptime_str, fill=TEXT_MUTED, font=font_small) + + # Draw pods with values + pod_positions = [ + ('AUTH', cx, cy - 115), + ('WALL', cx + 100, cy - 60), + ('BOOT', cx + 100, cy + 60), + ('MESH', cx, cy + 115), + ('ROOT', cx - 100, cy + 60), + ('MIND', cx - 100, cy - 60), + ] + + for name, px, py in pod_positions: + mod = MODULES[name] + metric_name = mod['metric'] + value = metrics.get(metric_name, 0) + unit = mod['unit'] + color = mod['color'] + + # Format value + if metric_name in ['cpu', 'mem', 'disk']: + val_str = f'{int(value)}' + elif metric_name == 'load': + val_str = f'{value:.1f}' + elif metric_name == 'temp': + val_str = f'{int(value)}' + else: + val_str = f'{int(value)}' + + # Value + bbox = draw.textbbox((0, 0), val_str, font=font_medium) + tw = bbox[2] - bbox[0] + draw.text((px - tw//2, py - 10), val_str, fill=color, font=font_medium) + + # Unit and name + label = f'{unit} {name}' + bbox = draw.textbbox((0, 0), label, font=font_small) + tw = bbox[2] - bbox[0] + draw.text((px - tw//2, py + 10), label, fill=TEXT_MUTED, font=font_small) + + # Top: SecuBox branding + device name + brand = 'SECUBOX EYE' + bbox = draw.textbbox((0, 0), brand, font=font_tiny) + tw = bbox[2] - bbox[0] + draw.text((cx - tw//2, 20), brand, fill=(201, 168, 76), font=font_tiny) + + # Device name/host at top + if device_name or host: + device_text = device_name if device_name else host + if len(device_text) > 20: + device_text = device_text[:18] + '..' + bbox = draw.textbbox((0, 0), device_text, font=font_tiny) + tw = bbox[2] - bbox[0] + draw.text((cx - tw//2, 35), device_text, fill=TEXT_MUTED, font=font_tiny) + + # Status bar at bottom + if mode in ['OTG', 'API', 'EMULATOR']: + status = f'● {mode} CONNECTED' + status_color = STATUS_OK + elif mode == 'WiFi': + status = '● WiFi CONNECTED' + status_color = (0, 191, 255) + else: + status = '○ SIMULATION' + status_color = STATUS_SIM + + bbox = draw.textbbox((0, 0), status, font=font_small) + tw = bbox[2] - bbox[0] + draw.text((cx - tw//2, HEIGHT - 55), status, fill=status_color, font=font_small) + + # Host address at bottom + if host and mode != 'SIM': + bbox = draw.textbbox((0, 0), host, font=font_tiny) + tw = bbox[2] - bbox[0] + draw.text((cx - tw//2, HEIGHT - 35), host, fill=TEXT_MUTED, font=font_tiny) + + return img + + +def pil_to_pygame(img): + """Convert PIL image to pygame surface.""" + raw = img.convert('RGB').tobytes() + return pygame.image.fromstring(raw, (WIDTH, HEIGHT), 'RGB') + + +def main(): + parser = argparse.ArgumentParser( + description='SecuBox Eye Remote - AMD64 Dashboard Tester' + ) + parser.add_argument( + '--api', '-a', + default='http://localhost:8765', + help='API base URL (default: http://localhost:8765)' + ) + parser.add_argument( + '--fullscreen', '-f', + action='store_true', + help='Run in fullscreen mode' + ) + args = parser.parse_args() + + print('SecuBox Eye Remote - AMD64 Test Dashboard') + print(f'API: {args.api}') + print('Press Q or ESC to exit') + print() + + pygame.init() + pygame.display.set_caption('SecuBox Eye Remote - Test Dashboard') + + if args.fullscreen: + screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN) + else: + screen = pygame.display.set_mode((WIDTH, HEIGHT)) + + clock = pygame.time.Clock() + source = APIMetricsSource(args.api) + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key in (pygame.K_q, pygame.K_ESCAPE): + running = False + + # Get metrics and draw + metrics, mode, host, device_name = source.get_metrics() + img = draw_dashboard(metrics, mode, host, device_name) + surface = pil_to_pygame(img) + screen.blit(surface, (0, 0)) + pygame.display.flip() + + clock.tick(2) # 2 FPS + + pygame.quit() + print('Done.') + + +if __name__ == '__main__': + main() diff --git a/tools/secubox-eye-gateway/gateway/emulator.py b/tools/secubox-eye-gateway/gateway/emulator.py index 4406bce9..3077df14 100644 --- a/tools/secubox-eye-gateway/gateway/emulator.py +++ b/tools/secubox-eye-gateway/gateway/emulator.py @@ -39,10 +39,13 @@ class SecuBoxEmulator: """ self._metrics.update() metrics = self._metrics.to_dict() + uptime_secs = int(time.time() - self.start_time) metrics.update({ "device_name": self.name, "device_id": self.device_id, - "uptime": int(time.time() - self.start_time), + "hostname": self.name, + "uptime": uptime_secs, + "uptime_seconds": uptime_secs, "emulated": True, }) return metrics diff --git a/tools/secubox-eye-gateway/gateway/profiles.py b/tools/secubox-eye-gateway/gateway/profiles.py index 94a5678a..a0ca0cba 100644 --- a/tools/secubox-eye-gateway/gateway/profiles.py +++ b/tools/secubox-eye-gateway/gateway/profiles.py @@ -146,14 +146,21 @@ class EmulatedMetrics: return self def to_dict(self) -> Dict[str, Any]: - """Convert metrics to dictionary format.""" + """Convert metrics to dictionary format. + + Field names match the SecuBox API for compatibility with Eye Remote dashboard. + """ return { "cpu_percent": self.cpu, - "memory_percent": self.mem, + "mem_percent": self.mem, + "memory_percent": self.mem, # Alias "disk_percent": self.disk, - "temperature": self.temp, - "load_avg": self.load, - "wifi_signal": self.wifi, + "cpu_temp": self.temp, + "temperature": self.temp, # Alias + "load_avg_1": self.load, + "load_avg": self.load, # Alias + "wifi_rssi": self.wifi, + "wifi_signal": self.wifi, # Alias "active_alerts": self.alerts, "profile": self.profile, "timestamp": time.time(),