Remove Uptime Kuma integration

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-12-14 12:04:05 +01:00
co-authored by Claude Opus 4.5
parent 2d3c2d23b5
commit ac92607003
16 changed files with 412 additions and 1671 deletions
-561
View File
@@ -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
-10
View File
@@ -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"
+7 -233
View File
@@ -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)
# ========================================================================
-5
View File
@@ -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"
-1
View File
@@ -10,7 +10,6 @@ ALWAYS_ON_SERVICES: Set[str] = {
"portainer",
"nginx-proxy-manager",
"core-api",
"uptime-kuma",
"organizr",
"headscale",
"watchtower",