Initial commit: GSPro Remote MVP - Phase 1 complete

This commit is contained in:
Ryan Hill 2025-11-13 15:38:58 -06:00
commit 74ca4b38eb
50 changed files with 12818 additions and 0 deletions

409
backend/app/api/system.py Normal file
View file

@ -0,0 +1,409 @@
"""
System API for health checks, system information, and diagnostics.
"""
import logging
import platform
import psutil
import os
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from ..core.config import get_config
from ..core.input_ctrl import is_gspro_running, get_gspro_process_info
logger = logging.getLogger(__name__)
router = APIRouter()
# Track application start time
APP_START_TIME = datetime.now()
@router.get("/health")
async def health_check():
"""
Health check endpoint for monitoring.
Returns basic health status and service availability.
"""
config = get_config()
# Check GSPro status
gspro_running = is_gspro_running(config.gspro.window_title)
# Calculate uptime
uptime = datetime.now() - APP_START_TIME
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"uptime_seconds": int(uptime.total_seconds()),
"services": {
"api": "running",
"gspro": "connected" if gspro_running else "disconnected",
"mdns": "enabled" if config.server.mdns_enabled else "disabled",
},
}
@router.get("/info")
async def system_info():
"""
Get detailed system information.
Returns information about the host system, Python environment, and application.
"""
config = get_config()
# Get system information
system_info = {
"platform": {
"system": platform.system(),
"release": platform.release(),
"version": platform.version(),
"machine": platform.machine(),
"processor": platform.processor(),
"python_version": platform.python_version(),
},
"hardware": {
"cpu_count": psutil.cpu_count(),
"cpu_percent": psutil.cpu_percent(interval=1),
"memory": {
"total": psutil.virtual_memory().total,
"available": psutil.virtual_memory().available,
"percent": psutil.virtual_memory().percent,
"used": psutil.virtual_memory().used,
},
"disk": {
"total": psutil.disk_usage("/").total,
"used": psutil.disk_usage("/").used,
"free": psutil.disk_usage("/").free,
"percent": psutil.disk_usage("/").percent,
},
},
"network": {
"hostname": platform.node(),
"interfaces": _get_network_interfaces(),
},
"application": {
"version": "0.1.0",
"config_path": str(config.config_path),
"debug_mode": config.debug,
"uptime": str(datetime.now() - APP_START_TIME),
"server": {
"host": config.server.host,
"port": config.server.port,
"url": f"http://{config.server.host}:{config.server.port}",
"mdns_url": f"http://gsproapp.local:{config.server.port}" if config.server.mdns_enabled else None,
},
},
}
return system_info
@router.get("/gspro/status")
async def gspro_status():
"""
Get GSPro application status.
Returns information about the GSPro process if it's running.
"""
config = get_config()
is_running = is_gspro_running(config.gspro.window_title)
process_info = get_gspro_process_info() if is_running else None
return {
"running": is_running,
"window_title": config.gspro.window_title,
"process": process_info,
"auto_focus": config.gspro.auto_focus,
"key_delay": config.gspro.key_delay,
}
@router.get("/gspro/find")
async def find_gspro_window():
"""
Search for GSPro windows.
Helps users identify the correct window title for configuration.
"""
try:
import win32gui
def enum_window_callback(hwnd, windows):
if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd):
window_text = win32gui.GetWindowText(hwnd)
if window_text and len(window_text) > 0:
# Look for windows that might be GSPro
if any(keyword in window_text.lower() for keyword in ["gspro", "golf", "simulator"]):
windows.append(
{"title": window_text, "hwnd": hwnd, "suggested": "gspro" in window_text.lower()}
)
return True
windows = []
win32gui.EnumWindows(enum_window_callback, windows)
return {
"found": len(windows) > 0,
"windows": windows,
"message": "Found potential GSPro windows" if windows else "No GSPro windows found",
}
except Exception as e:
logger.error(f"Failed to search for GSPro windows: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/metrics")
async def get_metrics():
"""
Get application metrics and performance statistics.
"""
config = get_config()
# Get current resource usage
process = psutil.Process()
metrics = {
"timestamp": datetime.now().isoformat(),
"uptime_seconds": int((datetime.now() - APP_START_TIME).total_seconds()),
"resources": {
"cpu_percent": process.cpu_percent(),
"memory_mb": process.memory_info().rss / 1024 / 1024,
"memory_percent": process.memory_percent(),
"threads": process.num_threads(),
"open_files": len(process.open_files()) if hasattr(process, "open_files") else 0,
"connections": len(process.connections()) if hasattr(process, "connections") else 0,
},
"system": {
"cpu_percent": psutil.cpu_percent(interval=0.5),
"memory_percent": psutil.virtual_memory().percent,
"disk_io": psutil.disk_io_counters()._asdict() if psutil.disk_io_counters() else {},
"network_io": psutil.net_io_counters()._asdict() if psutil.net_io_counters() else {},
},
}
return metrics
@router.get("/logs")
async def get_logs(lines: int = 100):
"""
Get recent application logs.
Args:
lines: Number of log lines to return (max 1000)
"""
# This is a placeholder - in production, you'd read from actual log files
return {
"message": "Log retrieval not yet implemented",
"lines_requested": min(lines, 1000),
"log_level": logging.getLevelName(logger.getEffectiveLevel()),
}
@router.post("/restart")
async def restart_application():
"""
Restart the application.
Note: This endpoint requires proper process management setup.
"""
# This would typically signal the process manager to restart
# For now, just return a message
return {
"success": False,
"message": "Application restart requires process manager setup. Please restart manually.",
}
@router.get("/dependencies")
async def check_dependencies():
"""
Check if all required dependencies are installed and accessible.
"""
dependencies = {
"required": {},
"optional": {},
}
# Check required dependencies
required_modules = [
"fastapi",
"uvicorn",
"pydantic",
"pydirectinput",
"mss",
"PIL",
"cv2",
"numpy",
"win32gui",
"psutil",
"zeroconf",
]
for module_name in required_modules:
try:
module = __import__(module_name)
version = getattr(module, "__version__", "unknown")
dependencies["required"][module_name] = {"installed": True, "version": version}
except ImportError:
dependencies["required"][module_name] = {"installed": False, "version": None}
# Check optional dependencies (V2 features)
optional_modules = [
"easyocr",
"pytesseract",
]
for module_name in optional_modules:
try:
module = __import__(module_name)
version = getattr(module, "__version__", "unknown")
dependencies["optional"][module_name] = {"installed": True, "version": version}
except ImportError:
dependencies["optional"][module_name] = {"installed": False, "version": None}
# Check if all required dependencies are installed
all_installed = all(dep["installed"] for dep in dependencies["required"].values())
return {
"all_required_installed": all_installed,
"dependencies": dependencies,
"message": "All required dependencies installed" if all_installed else "Some required dependencies are missing",
}
@router.get("/diagnostics")
async def run_diagnostics():
"""
Run comprehensive system diagnostics.
Checks various aspects of the system and application configuration.
"""
config = get_config()
diagnostics = {"timestamp": datetime.now().isoformat(), "checks": []}
# Check 1: GSPro connectivity
gspro_running = is_gspro_running(config.gspro.window_title)
diagnostics["checks"].append(
{
"name": "GSPro Connectivity",
"status": "pass" if gspro_running else "fail",
"message": "GSPro is running and accessible" if gspro_running else "GSPro window not found",
}
)
# Check 2: Network accessibility
try:
import socket
socket.create_connection(("8.8.8.8", 53), timeout=3)
network_ok = True
network_msg = "Network connection is working"
except:
network_ok = False
network_msg = "No internet connection detected"
diagnostics["checks"].append(
{
"name": "Network Connectivity",
"status": "pass" if network_ok else "warning",
"message": network_msg,
}
)
# Check 3: Configuration validity
config_valid = config.config_path and config.config_path.exists()
diagnostics["checks"].append(
{
"name": "Configuration File",
"status": "pass" if config_valid else "warning",
"message": f"Configuration file exists at {config.config_path}"
if config_valid
else "Configuration file not found",
}
)
# Check 4: Available disk space
disk_usage = psutil.disk_usage("/")
disk_ok = disk_usage.percent < 90
diagnostics["checks"].append(
{
"name": "Disk Space",
"status": "pass" if disk_ok else "warning",
"message": f"Disk usage at {disk_usage.percent:.1f}%"
+ (" - Consider freeing space" if not disk_ok else ""),
}
)
# Check 5: Memory availability
memory = psutil.virtual_memory()
memory_ok = memory.percent < 90
diagnostics["checks"].append(
{
"name": "Memory Availability",
"status": "pass" if memory_ok else "warning",
"message": f"Memory usage at {memory.percent:.1f}%"
+ (" - High memory usage detected" if not memory_ok else ""),
}
)
# Check 6: Python version
import sys
python_ok = sys.version_info >= (3, 11)
diagnostics["checks"].append(
{
"name": "Python Version",
"status": "pass" if python_ok else "warning",
"message": f"Python {platform.python_version()}"
+ (" - Consider upgrading to 3.11+" if not python_ok else ""),
}
)
# Calculate overall status
statuses = [check["status"] for check in diagnostics["checks"]]
if "fail" in statuses:
overall = "fail"
elif "warning" in statuses:
overall = "warning"
else:
overall = "pass"
diagnostics["overall_status"] = overall
diagnostics["summary"] = {
"passed": sum(1 for s in statuses if s == "pass"),
"warnings": sum(1 for s in statuses if s == "warning"),
"failures": sum(1 for s in statuses if s == "fail"),
}
return diagnostics
def _get_network_interfaces():
"""Helper function to get network interface information."""
interfaces = []
try:
for interface, addrs in psutil.net_if_addrs().items():
for addr in addrs:
if addr.family == socket.AF_INET: # IPv4
interfaces.append(
{
"name": interface,
"address": addr.address,
"netmask": addr.netmask,
"broadcast": addr.broadcast,
}
)
except Exception as e:
logger.warning(f"Could not get network interfaces: {e}")
return interfaces