From ac92607003821788a28e4f9a85ae99ce9d7c59df Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 14 Dec 2025 11:55:17 +0100 Subject: [PATCH] Remove Uptime Kuma integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete kuma_client.py and all Kuma-related code - Remove /infrastructure/monitors endpoints - Update service start/stop to use Portainer only - Simplify service control widget to show container status - Remove Kuma config and credentials - Delete stale memory tests (moved to core-ai service) - Add test infrastructure with pytest - Add tests for service_groups, config, and health endpoints - Bump version to 1.1.0 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 17 + dev-requirements.txt | 8 + pyproject.toml | 8 +- src/clients/kuma_client.py | 561 ------------------- src/config.py | 10 - src/controllers/infrastructure_controller.py | 240 +------- src/credentials.example.py | 5 - src/service_groups.py | 1 - static/widgets/service-control.html | 116 +--- tests/__init__.py | 1 + tests/test_config.py | 98 ++++ tests/test_health.py | 123 ++++ tests/test_memory_integration.py | 269 --------- tests/test_memory_manager.py | 150 ----- tests/test_memory_simple.py | 330 ----------- tests/test_service_groups.py | 146 +++++ 16 files changed, 412 insertions(+), 1671 deletions(-) create mode 100644 dev-requirements.txt delete mode 100644 src/clients/kuma_client.py create mode 100644 tests/__init__.py create mode 100644 tests/test_config.py create mode 100644 tests/test_health.py delete mode 100644 tests/test_memory_integration.py delete mode 100644 tests/test_memory_manager.py delete mode 100644 tests/test_memory_simple.py create mode 100644 tests/test_service_groups.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a8f1c6..3ba12a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2024-12-14 + +### Removed + +- Uptime Kuma integration (kuma_client.py deleted) +- All /infrastructure/monitors endpoints +- Kuma monitor pause/resume from service start/stop operations +- Uptime percentage display from service control widget +- Kuma-related configuration and credentials +- Stale memory tests (memory functionality moved to core-ai service) + +### Changed + +- Service control widget now uses Portainer container status exclusively +- Simplified widget-data endpoint response (removed monitors field) +- Updated service start/stop to only manage containers via Portainer + ## [1.0.0] - 2024-12-14 ### Added diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..2e42dfd --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,8 @@ +# Development and testing dependencies +-r requirements.txt + +# Testing +pytest>=9.0.0 +pytest-asyncio>=0.24.0 +pytest-cov>=6.0.0 +httpx>=0.28.0 # For TestClient diff --git a/pyproject.toml b/pyproject.toml index c20332d..9baac5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "core-api" -version = "1.0.0" +version = "1.1.0" description = "Core Code API - Infrastructure management and tools API" readme = "README.md" requires-python = ">=3.12" @@ -8,3 +8,9 @@ license = {text = "MIT"} [tool.setuptools] packages = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +asyncio_mode = "auto" +addopts = "-v" diff --git a/src/clients/kuma_client.py b/src/clients/kuma_client.py deleted file mode 100644 index 6069438..0000000 --- a/src/clients/kuma_client.py +++ /dev/null @@ -1,561 +0,0 @@ -""" -Uptime Kuma Socket.IO Client - -Provides interface to Uptime Kuma via Socket.IO for monitor management. -Also provides metrics API access for real-time status data. -""" -import socketio -import asyncio -import httpx -import re -from typing import Optional, Dict, List, Any -from src.logging_config import get_logger -from src.config import get_settings - -logger = get_logger(__name__) -settings = get_settings() - - -class KumaClient: - """ - Socket.IO client for Uptime Kuma - - Uses Socket.IO for real-time communication with Uptime Kuma. - """ - - def __init__( - self, - base_url: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - timeout: int = 30 - ): - """ - Initialize Kuma client - - Args: - base_url: Kuma base URL (default from settings) - username: Kuma username (default from settings) - password: Kuma password (default from settings) - timeout: Request timeout in seconds - """ - self.base_url = (base_url or settings.kuma_url).rstrip("/") - self.username = username or settings.kuma_username - self.password = password or settings.kuma_password - self.timeout = timeout - - self.sio = socketio.AsyncClient( - reconnection=True, - reconnection_attempts=3, - reconnection_delay=1, - ) - self._connected = False - self._authenticated = False - self._monitors_cache: Dict[int, Dict[str, Any]] = {} - - if not self.username or not self.password: - logger.warning("Uptime Kuma credentials not configured") - - async def _ensure_connected(self): - """Ensure we have an active connection and authentication""" - if not self._connected: - await self.connect() - if not self._authenticated: - await self.login() - - async def connect(self): - """Connect to Uptime Kuma Socket.IO server""" - if self._connected: - return - - try: - await self.sio.connect(self.base_url, transports=['websocket']) - self._connected = True - logger.info(f"Connected to Uptime Kuma at {self.base_url}") - except Exception as e: - logger.error(f"Failed to connect to Uptime Kuma: {e}") - raise - - async def disconnect(self): - """Disconnect from Uptime Kuma""" - if self._connected: - await self.sio.disconnect() - self._connected = False - self._authenticated = False - logger.info("Disconnected from Uptime Kuma") - - async def login(self): - """Authenticate with Uptime Kuma""" - if not self._connected: - await self.connect() - - try: - # Uptime Kuma login event - login_response = await self.sio.call( - 'login', - { - 'username': self.username, - 'password': self.password, - 'token': None - }, - timeout=self.timeout - ) - - if login_response and login_response.get('ok'): - self._authenticated = True - logger.info("Successfully authenticated with Uptime Kuma") - else: - error_msg = login_response.get('msg', 'Unknown error') if login_response else 'No response' - raise Exception(f"Login failed: {error_msg}") - - except Exception as e: - logger.error(f"Failed to authenticate with Uptime Kuma: {e}") - raise - - async def health_check(self) -> bool: - """ - Check if Uptime Kuma is accessible - - Returns: - True if accessible, False otherwise - """ - try: - await self._ensure_connected() - return self._authenticated - except Exception as e: - logger.error(f"Uptime Kuma health check failed: {e}") - return False - - async def get_monitors(self) -> List[Dict[str, Any]]: - """ - List all monitors with uptime data - - Returns: - List of monitor configurations with uptime_24h field - """ - await self._ensure_connected() - - try: - # Storage for monitor list and uptime data received via events - monitor_list_data = {} - uptime_list_data = {} - monitor_event_received = asyncio.Event() - uptime_event_received = asyncio.Event() - - # Register event handler for monitorList - @self.sio.event - async def monitorList(data): - nonlocal monitor_list_data - monitor_list_data = data - monitor_event_received.set() - - # Register event handler for uptimeList (24h uptime percentages) - @self.sio.event - async def uptimeList(monitor_id, uptime_data): - nonlocal uptime_list_data - # uptime_data is typically a dict with time periods: {"24": 99.5, "720": 98.2, ...} - uptime_list_data[str(monitor_id)] = uptime_data - # Don't set event here as we'll get multiple calls - - # Request monitor list - this triggers the server to send monitorList event - response = await self.sio.call('getMonitorList', timeout=self.timeout) - logger.info(f"getMonitorList call response: {response}") - - # Wait for the monitorList event (with timeout) - try: - await asyncio.wait_for(monitor_event_received.wait(), timeout=5.0) - logger.info(f"Received monitorList event with {len(monitor_list_data)} items") - - # Give time for uptimeList events to arrive - await asyncio.sleep(0.5) - logger.info(f"Received uptime data for {len(uptime_list_data)} monitors") - except asyncio.TimeoutError: - logger.warning("Timeout waiting for monitorList event") - - # Process the monitor list data - if monitor_list_data and isinstance(monitor_list_data, dict): - monitors = [] - for monitor_id, monitor_data in monitor_list_data.items(): - if isinstance(monitor_data, dict): - monitor_data['id'] = int(monitor_id) - - # Add uptime data if available - uptime_info = uptime_list_data.get(str(monitor_id), {}) - if isinstance(uptime_info, dict): - # Uptime Kuma provides 24h uptime as key "24" - monitor_data['uptime_24h'] = float(uptime_info.get('24', 0)) - else: - monitor_data['uptime_24h'] = 0.0 - - monitors.append(monitor_data) - self._monitors_cache[int(monitor_id)] = monitor_data - - logger.info(f"Found {len(monitors)} monitors total") - return monitors - - logger.warning(f"No valid monitor data received") - return [] - - except Exception as e: - logger.error(f"Failed to get monitors: {e}", exc_info=True) - raise - - async def get_monitor(self, monitor_id: int) -> Dict[str, Any]: - """ - Get details of a specific monitor - - Args: - monitor_id: Monitor identifier - - Returns: - Monitor configuration details - """ - await self._ensure_connected() - - try: - response = await self.sio.call('getMonitor', monitor_id, timeout=self.timeout) - - if response: - self._monitors_cache[monitor_id] = response - return response - - raise Exception(f"Monitor {monitor_id} not found") - - except Exception as e: - logger.error(f"Failed to get monitor {monitor_id}: {e}") - raise - - async def find_monitor_by_name(self, name: str) -> Optional[Dict[str, Any]]: - """ - Find a monitor by its name (case-insensitive) - - Args: - name: Monitor name to search for - - Returns: - Monitor object if found, None otherwise - """ - monitors = await self.get_monitors() - name_lower = name.lower() - - for monitor in monitors: - if monitor.get("name", "").lower() == name_lower: - return monitor - - return None - - async def find_monitors_by_tag(self, tag: str) -> List[Dict[str, Any]]: - """ - Find all monitors with a specific tag - - Args: - tag: Tag name to search for - - Returns: - List of monitors with the tag - """ - monitors = await self.get_monitors() - tagged_monitors = [] - - for monitor in monitors: - monitor_tags = monitor.get("tags", []) - if any(t.get("name", "").lower() == tag.lower() for t in monitor_tags): - tagged_monitors.append(monitor) - - return tagged_monitors - - async def pause_monitor(self, monitor_id: int) -> bool: - """ - Pause a monitor (disable monitoring) - - Args: - monitor_id: Monitor identifier - - Returns: - True if successful - """ - await self._ensure_connected() - - try: - # Uptime Kuma pause event - response = await self.sio.call('pauseMonitor', monitor_id, timeout=self.timeout) - - if response and response.get('ok'): - logger.info(f"Paused monitor {monitor_id}") - return True - - error_msg = response.get('msg', 'Unknown error') if response else 'No response' - raise Exception(f"Failed to pause monitor: {error_msg}") - - except Exception as e: - logger.error(f"Failed to pause monitor {monitor_id}: {e}") - raise - - async def resume_monitor(self, monitor_id: int) -> bool: - """ - Resume a monitor (enable monitoring) - - Args: - monitor_id: Monitor identifier - - Returns: - True if successful - """ - await self._ensure_connected() - - try: - # Uptime Kuma resume event - response = await self.sio.call('resumeMonitor', monitor_id, timeout=self.timeout) - - if response and response.get('ok'): - logger.info(f"Resumed monitor {monitor_id}") - return True - - error_msg = response.get('msg', 'Unknown error') if response else 'No response' - raise Exception(f"Failed to resume monitor: {error_msg}") - - except Exception as e: - logger.error(f"Failed to resume monitor {monitor_id}: {e}") - raise - - async def pause_monitor_by_name(self, name: str) -> bool: - """ - Pause a monitor by its name - - Args: - name: Monitor name - - Returns: - True if successful, False if monitor not found - """ - monitor = await self.find_monitor_by_name(name) - if not monitor: - logger.warning(f"Monitor '{name}' not found") - return False - - await self.pause_monitor(monitor["id"]) - return True - - async def resume_monitor_by_name(self, name: str) -> bool: - """ - Resume a monitor by its name - - Args: - name: Monitor name - - Returns: - True if successful, False if monitor not found - """ - monitor = await self.find_monitor_by_name(name) - if not monitor: - logger.warning(f"Monitor '{name}' not found") - return False - - await self.resume_monitor(monitor["id"]) - return True - - async def add_monitor(self, monitor_config: Dict[str, Any]) -> Dict[str, Any]: - """ - Create a new monitor - - Args: - monitor_config: Monitor configuration dict - - Returns: - Created monitor details including ID - """ - await self._ensure_connected() - - try: - # Uptime Kuma add monitor event - response = await self.sio.call('add', monitor_config, timeout=self.timeout) - - if response and response.get('ok'): - monitor_id = response.get('monitorID') - logger.info(f"Created monitor '{monitor_config.get('name')}' with ID {monitor_id}") - - # Get full monitor details - monitor = await self.get_monitor(monitor_id) - return monitor - - error_msg = response.get('msg', 'Unknown error') if response else 'No response' - raise Exception(f"Failed to create monitor: {error_msg}") - - except Exception as e: - logger.error(f"Failed to create monitor '{monitor_config.get('name')}': {e}") - raise - - async def update_monitor(self, monitor_id: int, monitor_config: Dict[str, Any]) -> Dict[str, Any]: - """ - Update an existing monitor - - Args: - monitor_id: Monitor identifier - monitor_config: Updated monitor configuration - - Returns: - Updated monitor details - """ - await self._ensure_connected() - - try: - # Ensure ID is in the config - monitor_config['id'] = monitor_id - - # Uptime Kuma edit monitor event - response = await self.sio.call('editMonitor', monitor_config, timeout=self.timeout) - - if response and response.get('ok'): - logger.info(f"Updated monitor {monitor_id}") - - # Get updated monitor details - monitor = await self.get_monitor(monitor_id) - return monitor - - error_msg = response.get('msg', 'Unknown error') if response else 'No response' - raise Exception(f"Failed to update monitor: {error_msg}") - - except Exception as e: - logger.error(f"Failed to update monitor {monitor_id}: {e}") - raise - - async def delete_monitor(self, monitor_id: int) -> bool: - """ - Delete a monitor - - Args: - monitor_id: Monitor identifier - - Returns: - True if successful - """ - await self._ensure_connected() - - try: - # Uptime Kuma delete monitor event - response = await self.sio.call('deleteMonitor', monitor_id, timeout=self.timeout) - - if response and response.get('ok'): - logger.info(f"Deleted monitor {monitor_id}") - - # Remove from cache - self._monitors_cache.pop(monitor_id, None) - return True - - error_msg = response.get('msg', 'Unknown error') if response else 'No response' - raise Exception(f"Failed to delete monitor: {error_msg}") - - except Exception as e: - logger.error(f"Failed to delete monitor {monitor_id}: {e}") - raise - - async def delete_monitor_by_name(self, name: str) -> bool: - """ - Delete a monitor by its name - - Args: - name: Monitor name - - Returns: - True if successful, False if monitor not found - """ - monitor = await self.find_monitor_by_name(name) - if not monitor: - logger.warning(f"Monitor '{name}' not found") - return False - - await self.delete_monitor(monitor["id"]) - return True - - async def get_metrics_status(self) -> Dict[str, Dict[str, Any]]: - """ - Get monitor status from Prometheus metrics endpoint - - This is simpler and more reliable than Socket.IO for getting current status. - Returns real-time UP/DOWN status but not historical uptime percentages. - - Returns: - Dict mapping monitor names to status info: - { - "Portainer": { - "status": 1, # 1=UP, 0=DOWN, 2=PENDING, 3=MAINTENANCE - "response_time": 5, # ms - "monitor_type": "http", - "url": "http://192.168.86.149:8001" - }, - ... - } - """ - try: - # Use API key authentication - api_key = settings.kuma_api_key - if not api_key: - logger.warning("Kuma API key not configured") - return {} - - # Fetch metrics with HTTP Basic Auth (empty username, API key as password) - async with httpx.AsyncClient(timeout=10.0) as client: - response = await client.get( - f"{self.base_url}/metrics", - auth=("", api_key) - ) - response.raise_for_status() - metrics_text = response.text - - # Parse Prometheus format metrics - # Format: metric_name{label1="value1",label2="value2"} value - monitor_data = {} - - # Parse monitor_status lines - status_pattern = r'monitor_status\{monitor_name="([^"]+)",.*?\} (\d+)' - for match in re.finditer(status_pattern, metrics_text): - monitor_name = match.group(1) - status = int(match.group(2)) - - if monitor_name not in monitor_data: - monitor_data[monitor_name] = {} - monitor_data[monitor_name]['status'] = status - - # Parse monitor_response_time lines - response_pattern = r'monitor_response_time\{monitor_name="([^"]+)",monitor_type="([^"]+)",monitor_url="([^"]+)",.*?\} ([\d.]+)' - for match in re.finditer(response_pattern, metrics_text): - monitor_name = match.group(1) - monitor_type = match.group(2) - monitor_url = match.group(3) - response_time = float(match.group(4)) - - if monitor_name not in monitor_data: - monitor_data[monitor_name] = {} - monitor_data[monitor_name].update({ - 'response_time': response_time, - 'monitor_type': monitor_type, - 'url': monitor_url - }) - - logger.info(f"Fetched metrics for {len(monitor_data)} monitors") - return monitor_data - - except Exception as e: - logger.error(f"Failed to fetch metrics: {e}") - return {} - - async def __aenter__(self): - """Async context manager entry""" - await self._ensure_connected() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async context manager exit""" - await self.disconnect() - - -# Singleton instance -_kuma_client: Optional[KumaClient] = None - - -def get_kuma_client() -> KumaClient: - """Get singleton Kuma client instance""" - global _kuma_client - if _kuma_client is None: - _kuma_client = KumaClient() - return _kuma_client diff --git a/src/config.py b/src/config.py index 220de31..5c3c6b6 100644 --- a/src/config.py +++ b/src/config.py @@ -25,7 +25,6 @@ try: from src.credentials import ( PORTAINER_URL, PORTAINER_API_KEY, NPM_URL, NPM_EMAIL, NPM_PASSWORD, - KUMA_URL, KUMA_USERNAME, KUMA_PASSWORD, KUMA_API_KEY, BRAVE_SEARCH_API_KEY, GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_ENGINE_ID ) @@ -37,10 +36,6 @@ except ImportError: NPM_URL = "http://localhost:81" NPM_EMAIL = "" NPM_PASSWORD = "" - KUMA_URL = "http://localhost:3001" - KUMA_USERNAME = "" - KUMA_PASSWORD = "" - KUMA_API_KEY = "" BRAVE_SEARCH_API_KEY = "" GOOGLE_SEARCH_API_KEY = "" GOOGLE_SEARCH_ENGINE_ID = "" @@ -127,11 +122,6 @@ class Settings(BaseSettings): npm_email: str = NPM_EMAIL npm_password: str = NPM_PASSWORD - kuma_url: str = KUMA_URL - kuma_username: str = KUMA_USERNAME - kuma_password: str = KUMA_PASSWORD - kuma_api_key: str = KUMA_API_KEY - # Core-AI Service (AI performance metrics) core_ai_base_url: str = "http://core-ai:8086" diff --git a/src/controllers/infrastructure_controller.py b/src/controllers/infrastructure_controller.py index a5a180d..76801b7 100644 --- a/src/controllers/infrastructure_controller.py +++ b/src/controllers/infrastructure_controller.py @@ -11,7 +11,6 @@ from pydantic import BaseModel, field_validator from src.controllers.base import BaseController from src.clients.portainer_client import get_portainer_client from src.clients.npm_client import get_npm_client -from src.clients.kuma_client import get_kuma_client from src.logging_config import get_logger from src import service_groups from src.auth.oidc import get_admin_user, get_forward_auth_admin @@ -963,7 +962,7 @@ class InfrastructureController(BaseController): "/services/{name}/stop", response_model=OperationResult, summary="Stop a service or service group", - description="Stop a service or service group by pausing monitors and stopping containers. Requires admin authentication when accessed externally via api.schweitz.net." + description="Stop a service or service group by stopping containers. Requires admin authentication when accessed externally via api.schweitz.net." ) async def stop_service( name: str, @@ -974,8 +973,7 @@ class InfrastructureController(BaseController): This will: 1. Validate service can be stopped (not always-on) - 2. Pause Uptime Kuma monitors for all services in group - 3. Stop the Portainer stack(s) + 2. Stop the Portainer stack containers Args: name: Service or group name @@ -984,7 +982,6 @@ class InfrastructureController(BaseController): Operation result with details """ portainer = get_portainer_client() - kuma = get_kuma_client() try: # Get all services in the group @@ -997,24 +994,13 @@ class InfrastructureController(BaseController): results = { "stopped_services": [], - "paused_monitors": [], "errors": [] } # Stop each service for service_name in services: try: - # 1. Pause Uptime Kuma monitor - try: - monitor_paused = await kuma.pause_monitor_by_name(service_name) - if monitor_paused: - results["paused_monitors"].append(service_name) - logger.info(f"Paused Kuma monitor for {service_name}") - except Exception as e: - logger.warning(f"Failed to pause Kuma monitor for {service_name}: {e}") - results["errors"].append(f"Kuma pause failed for {service_name}: {str(e)}") - - # 2. Stop Portainer stack + # Stop Portainer stack containers stacks = await portainer.get_stacks() stack = next( (s for s in stacks if s.get("Name", "").lower() == service_name.lower()), @@ -1025,9 +1011,6 @@ class InfrastructureController(BaseController): stack_id = stack.get("Id") endpoint_id = stack.get("EndpointId") - # Stop stack by deleting it (Portainer doesn't have a "stop" operation) - # Note: This is destructive. For a gentler approach, we'd need to use docker compose stop - # Let's use docker API instead logger.info(f"Stopping containers for stack: {service_name}") # Get containers for this stack @@ -1078,7 +1061,7 @@ class InfrastructureController(BaseController): "/services/{name}/start", response_model=OperationResult, summary="Start a service or service group", - description="Start a service or service group by starting containers and resuming monitors. Requires admin authentication when accessed externally via api.schweitz.net." + description="Start a service or service group by starting containers. Requires admin authentication when accessed externally via api.schweitz.net." ) async def start_service( name: str, @@ -1088,8 +1071,7 @@ class InfrastructureController(BaseController): Start a service or service group This will: - 1. Start the Portainer stack(s) - 2. Resume Uptime Kuma monitors for all services in group + 1. Start the Portainer stack containers Args: name: Service or group name @@ -1098,7 +1080,6 @@ class InfrastructureController(BaseController): Operation result with details """ portainer = get_portainer_client() - kuma = get_kuma_client() try: # Get all services in the group @@ -1106,14 +1087,13 @@ class InfrastructureController(BaseController): results = { "started_services": [], - "resumed_monitors": [], "errors": [] } # Start each service for service_name in services: try: - # 1. Start Portainer stack (start containers) + # Start Portainer stack containers stacks = await portainer.get_stacks() stack = next( (s for s in stacks if s.get("Name", "").lower() == service_name.lower()), @@ -1147,16 +1127,6 @@ class InfrastructureController(BaseController): }) logger.info(f"Started service: {service_name}") - # 2. Resume Uptime Kuma monitor - try: - monitor_resumed = await kuma.resume_monitor_by_name(service_name) - if monitor_resumed: - results["resumed_monitors"].append(service_name) - logger.info(f"Resumed Kuma monitor for {service_name}") - except Exception as e: - logger.warning(f"Failed to resume Kuma monitor for {service_name}: {e}") - results["errors"].append(f"Kuma resume failed for {service_name}: {str(e)}") - else: results["errors"].append(f"Stack not found: {service_name}") @@ -1181,8 +1151,6 @@ class InfrastructureController(BaseController): logger.error(f"Failed to start service group '{name}': {e}") raise HTTPException(status_code=500, detail=str(e)) - # ===== Monitoring Endpoints ===== - @router.get( "/widget-data", summary="Get combined data for service control widget", @@ -1190,11 +1158,10 @@ class InfrastructureController(BaseController): ) async def get_widget_data(): """ - Get combined service and monitor data for the widget + Get combined service data for the widget Returns all data needed by service-control widget in a single call: - Service list with status and container counts - - Monitor list with uptime percentages - Service groups and always-on list This endpoint is designed for browser-based widgets to avoid @@ -1202,7 +1169,6 @@ class InfrastructureController(BaseController): """ try: portainer = get_portainer_client() - kuma = get_kuma_client() npm = get_npm_client() # Fetch services (same logic as /services endpoint) @@ -1252,38 +1218,9 @@ class InfrastructureController(BaseController): "containers_total": containers_total }) - # Fetch monitors with real-time status from metrics endpoint - monitors_list = [] - try: - # Get real-time status from Prometheus metrics - metrics_data = await kuma.get_metrics_status() - - for monitor_name, monitor_info in metrics_data.items(): - # Status: 1=UP, 0=DOWN, 2=PENDING, 3=MAINTENANCE - status = monitor_info.get('status', 0) - - # Convert status to simple up/down for widget - # Treat UP (1) as 100%, anything else as 0% - status_percentage = 100.0 if status == 1 else 0.0 - - monitors_list.append({ - "id": None, # Not available from metrics - "name": monitor_name, - "uptime_24h": status_percentage, # Current status as percentage - "active": True, # Assume active if in metrics - "status": status, # 1=UP, 0=DOWN, 2=PENDING, 3=MAINTENANCE - "response_time": monitor_info.get('response_time', 0) - }) - - logger.info(f"Fetched status for {len(monitors_list)} monitors from metrics") - except Exception as e: - logger.warning(f"Failed to fetch monitors: {e}") - # Continue without monitor data rather than failing - return { "success": True, "services": services, - "monitors": monitors_list, "service_groups": { "groups": service_groups.list_service_groups(), "always_on": list(service_groups.ALWAYS_ON_SERVICES), @@ -1295,169 +1232,6 @@ class InfrastructureController(BaseController): logger.error(f"Failed to fetch widget data: {e}") raise HTTPException(status_code=500, detail=f"Failed to fetch widget data: {str(e)}") - @router.get( - "/monitors", - summary="List all monitors", - response_model=Dict[str, Any] - ) - async def list_monitors(): - """ - List all Uptime Kuma monitors - - Returns: - List of monitors with their configurations - """ - try: - kuma = get_kuma_client() - monitors = await kuma.get_monitors() - - return { - "success": True, - "monitors": monitors, - "total": len(monitors) - } - - except Exception as e: - logger.error(f"Failed to list monitors: {e}") - raise HTTPException(status_code=500, detail=f"Failed to list monitors: {str(e)}") - - @router.post( - "/monitors", - summary="Create a new monitor", - description="Create a new Uptime Kuma monitor. Requires admin authentication.", - response_model=Dict[str, Any] - ) - async def create_monitor( - monitor_config: Dict[str, Any], - user: Dict = Depends(get_admin_user) - ): - """ - Create a new Uptime Kuma monitor - - Args: - monitor_config: Monitor configuration (name, type, hostname, port, etc.) - - Returns: - Created monitor details including ID - """ - try: - kuma = get_kuma_client() - created_monitor = await kuma.add_monitor(monitor_config) - - return { - "success": True, - "message": f"Monitor '{monitor_config.get('name')}' created successfully", - "monitor": created_monitor - } - - except Exception as e: - logger.error(f"Failed to create monitor: {e}") - raise HTTPException(status_code=500, detail=f"Failed to create monitor: {str(e)}") - - @router.get( - "/monitors/{monitor_id}", - summary="Get monitor details", - response_model=Dict[str, Any] - ) - async def get_monitor(monitor_id: int): - """ - Get details of a specific monitor - - Args: - monitor_id: Monitor identifier - - Returns: - Monitor configuration and status - """ - try: - kuma = get_kuma_client() - monitor = await kuma.get_monitor(monitor_id) - - return { - "success": True, - "monitor": monitor - } - - except Exception as e: - logger.error(f"Failed to get monitor {monitor_id}: {e}") - raise HTTPException(status_code=404, detail=f"Monitor {monitor_id} not found: {str(e)}") - - @router.put( - "/monitors/{monitor_id}", - summary="Update a monitor", - description="Update an existing Uptime Kuma monitor. Requires admin authentication.", - response_model=Dict[str, Any] - ) - async def update_monitor( - monitor_id: int, - updates: Dict[str, Any], - user: Dict = Depends(get_admin_user) - ): - """ - Update an existing monitor - - Args: - monitor_id: Monitor identifier - updates: Fields to update - - Returns: - Updated monitor details - """ - try: - kuma = get_kuma_client() - - # Get existing monitor - existing = await kuma.get_monitor(monitor_id) - - # Merge updates - monitor_config = existing.copy() - monitor_config.update(updates) - - # Update monitor - updated_monitor = await kuma.update_monitor(monitor_id, monitor_config) - - return { - "success": True, - "message": f"Monitor {monitor_id} updated successfully", - "monitor": updated_monitor - } - - except Exception as e: - logger.error(f"Failed to update monitor {monitor_id}: {e}") - raise HTTPException(status_code=500, detail=f"Failed to update monitor: {str(e)}") - - @router.delete( - "/monitors/{monitor_id}", - summary="Delete a monitor", - description="Delete an Uptime Kuma monitor. Requires admin authentication.", - response_model=Dict[str, Any] - ) - async def delete_monitor( - monitor_id: int, - user: Dict = Depends(get_admin_user) - ): - """ - Delete a monitor - - Args: - monitor_id: Monitor identifier - - Returns: - Success confirmation - """ - try: - kuma = get_kuma_client() - await kuma.delete_monitor(monitor_id) - - return { - "success": True, - "message": f"Monitor {monitor_id} deleted successfully" - } - - except Exception as e: - logger.error(f"Failed to delete monitor {monitor_id}: {e}") - raise HTTPException(status_code=500, detail=f"Failed to delete monitor: {str(e)}") - # ======================================================================== # Container Management Endpoints (for core-ai infrastructure tools) # ======================================================================== diff --git a/src/credentials.example.py b/src/credentials.example.py index c2a152e..a9b6b2e 100644 --- a/src/credentials.example.py +++ b/src/credentials.example.py @@ -17,8 +17,3 @@ PORTAINER_API_KEY = "ptr_your_api_token_here" # Create in Portainer UI: User me NPM_URL = "http://localhost:81" NPM_EMAIL = "admin@example.com" NPM_PASSWORD = "your_password_here" - -# Uptime Kuma Configuration -KUMA_URL = "http://localhost:3001" -KUMA_USERNAME = "admin" -KUMA_PASSWORD = "your_password_here" diff --git a/src/service_groups.py b/src/service_groups.py index 55512a4..3a61a24 100644 --- a/src/service_groups.py +++ b/src/service_groups.py @@ -10,7 +10,6 @@ ALWAYS_ON_SERVICES: Set[str] = { "portainer", "nginx-proxy-manager", "core-api", - "uptime-kuma", "organizr", "headscale", "watchtower", diff --git a/static/widgets/service-control.html b/static/widgets/service-control.html index ec2a350..5f8dc7b 100644 --- a/static/widgets/service-control.html +++ b/static/widgets/service-control.html @@ -71,14 +71,14 @@ font-weight: 600; color: #fff; text-transform: capitalize; - min-width: 300px; + min-width: 200px; } .service-status { display: flex; align-items: center; gap: 8px; - min-width: 120px; + min-width: 180px; } .status-indicator { @@ -103,54 +103,6 @@ color: #a0a0a0; } - .uptime-status { - display: flex; - align-items: center; - gap: 8px; - min-width: 150px; - padding: 4px 10px; - background: rgba(0, 0, 0, 0.2); - border-radius: 4px; - cursor: pointer; - transition: background 0.2s; - text-decoration: none; - color: inherit; - } - - .uptime-status:hover { - background: rgba(0, 0, 0, 0.4); - } - - .uptime-percentage { - font-size: 13px; - font-weight: 600; - } - - .uptime-percentage.excellent { - color: #48bb78; - } - - .uptime-percentage.good { - color: #68d391; - } - - .uptime-percentage.warning { - color: #ed8936; - } - - .uptime-percentage.critical { - color: #f56565; - } - - .uptime-percentage.unknown { - color: #718096; - } - - .uptime-icon { - font-size: 11px; - color: #a0a0a0; - } - .service-right { display: flex; align-items: center; @@ -248,10 +200,6 @@ min-width: 100px; } - .uptime-status { - min-width: 100px; - } - .service-right { width: 100%; justify-content: flex-end; @@ -264,12 +212,12 @@
-
šŸŽ›ļø Stoppable Services
+
Stoppable Services
Loading services...
-
šŸ”’ Always-On Infrastructure
+
Always-On Infrastructure
Loading infrastructure...
@@ -277,11 +225,9 @@