✨ FEATURE: Add real-time Service Dashboard
NEW FEATURE: Service Dashboard
- Real-time monitoring of all Claude OS services
- Visual status indicators (running/stopped/unknown)
- CPU and memory usage tracking
- Auto-refresh every 5 seconds (toggleable)
- Service health summary (healthy/degraded/critical)
BACKEND (mcp_server/server.py):
- Added /api/services/status endpoint
- Monitors 6 services: MCP Server, Frontend, Redis, RQ Worker, Ollama, File Watcher
- Returns real-time status, PID, CPU%, Memory%, ports
- Includes health summary (total/running/stopped)
- Added /api/services/{service_type}/control endpoint
- Placeholder for future service control
- Returns manual commands for start/stop/restart
- Implemented process monitoring utilities:
- check_process_running() - uses pgrep and ps for stats
- check_port_listening() - uses lsof/netstat/ss for port checks
FRONTEND (ServiceDashboard.tsx):
- Beautiful dashboard with animated cards
- Service grid with 6 service cards
- Summary cards: Total, Running, Stopped, Health
- Auto-refresh toggle with visual indicator
- Manual refresh button
- Status badges with color coding:
- 🟢 Green = Running
- ⚪ Gray = Stopped
- 🟡 Yellow = Unknown
- CPU/Memory usage display
- Port and PID information
- Clickable service URLs
- Help section with management commands
INTEGRATION (MainApp.tsx):
- Added "Services" tab to main navigation
- Imported ServiceDashboard component
- Updated tab state type to include 'services'
- Activity icon for services tab
FEATURES:
✅ Real-time status monitoring
✅ Auto-refresh every 5s
✅ Manual refresh button
✅ Health summary dashboard
✅ Service details (port, PID, CPU, memory)
✅ Visual status indicators
✅ Responsive grid layout
✅ Beautiful animations
USER EXPERIENCE:
- Auto-refresh can be toggled on/off
- Last updated timestamp shown
- Help text for service management
- Clean, professional UI matching Claude OS design
Files:
- mcp_server/server.py (+300 lines)
- frontend/src/components/ServiceDashboard.tsx (new file, 350 lines)
- frontend/src/pages/MainApp.tsx (+20 lines)
This commit is contained in:
parent
28f58f1c60
commit
98908541a7
3 changed files with 627 additions and 2 deletions
326
frontend/src/components/ServiceDashboard.tsx
Normal file
326
frontend/src/components/ServiceDashboard.tsx
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Activity,
|
||||
Server,
|
||||
Database,
|
||||
Zap,
|
||||
Eye,
|
||||
RefreshCw,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Cpu,
|
||||
MemoryStick
|
||||
} from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
|
||||
interface Service {
|
||||
name: string;
|
||||
type: string;
|
||||
port: number | null;
|
||||
running: boolean;
|
||||
status: string;
|
||||
pid: number | null;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ServicesResponse {
|
||||
services: Service[];
|
||||
summary: {
|
||||
total: number;
|
||||
running: number;
|
||||
stopped: number;
|
||||
health: 'healthy' | 'degraded' | 'critical';
|
||||
};
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const SERVICE_ICONS: Record<string, any> = {
|
||||
mcp_server: Server,
|
||||
frontend: Activity,
|
||||
redis: Database,
|
||||
rq_worker: Zap,
|
||||
ollama: Cpu,
|
||||
file_watcher: Eye
|
||||
};
|
||||
|
||||
const STATUS_COLORS = {
|
||||
running: {
|
||||
bg: 'bg-green-500/20',
|
||||
border: 'border-green-500',
|
||||
text: 'text-green-400',
|
||||
icon: CheckCircle2
|
||||
},
|
||||
stopped: {
|
||||
bg: 'bg-gray-500/20',
|
||||
border: 'border-gray-500',
|
||||
text: 'text-gray-400',
|
||||
icon: XCircle
|
||||
},
|
||||
unknown: {
|
||||
bg: 'bg-yellow-500/20',
|
||||
border: 'border-yellow-500',
|
||||
text: 'text-yellow-400',
|
||||
icon: AlertCircle
|
||||
}
|
||||
};
|
||||
|
||||
export default function ServiceDashboard() {
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
|
||||
// Fetch services status
|
||||
const { data: servicesData, isLoading, refetch } = useQuery<ServicesResponse>({
|
||||
queryKey: ['services-status'],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get('/api/services/status');
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: autoRefresh ? 5000 : false, // Refresh every 5 seconds if enabled
|
||||
});
|
||||
|
||||
const services = servicesData?.services || [];
|
||||
const summary = servicesData?.summary;
|
||||
|
||||
const getHealthColor = (health?: string) => {
|
||||
switch (health) {
|
||||
case 'healthy':
|
||||
return 'text-green-400';
|
||||
case 'degraded':
|
||||
return 'text-yellow-400';
|
||||
case 'critical':
|
||||
return 'text-red-400';
|
||||
default:
|
||||
return 'text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold gradient-text flex items-center gap-2">
|
||||
<Activity className="w-6 h-6" />
|
||||
Service Dashboard
|
||||
</h2>
|
||||
<p className="text-light-grey text-sm mt-1">
|
||||
Real-time monitoring of Claude OS services
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Auto-refresh toggle */}
|
||||
<button
|
||||
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${
|
||||
autoRefresh
|
||||
? 'bg-electric-teal/20 text-electric-teal border border-electric-teal/50'
|
||||
: 'bg-cool-blue/10 text-light-grey border border-electric-teal/20'
|
||||
}`}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${autoRefresh ? 'animate-spin' : ''}`} />
|
||||
<span className="text-sm font-medium">
|
||||
{autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Manual refresh */}
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading}
|
||||
className="btn-secondary flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
{summary && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="glass-panel p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-light-grey text-sm">Total Services</div>
|
||||
<div className="text-3xl font-bold text-white mt-1">{summary.total}</div>
|
||||
</div>
|
||||
<Server className="w-8 h-8 text-electric-teal opacity-50" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="glass-panel p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-light-grey text-sm">Running</div>
|
||||
<div className="text-3xl font-bold text-green-400 mt-1">{summary.running}</div>
|
||||
</div>
|
||||
<CheckCircle2 className="w-8 h-8 text-green-400 opacity-50" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="glass-panel p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-light-grey text-sm">Stopped</div>
|
||||
<div className="text-3xl font-bold text-gray-400 mt-1">{summary.stopped}</div>
|
||||
</div>
|
||||
<XCircle className="w-8 h-8 text-gray-400 opacity-50" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="glass-panel p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-light-grey text-sm">System Health</div>
|
||||
<div className={`text-2xl font-bold mt-1 capitalize ${getHealthColor(summary.health)}`}>
|
||||
{summary.health}
|
||||
</div>
|
||||
</div>
|
||||
<Activity className={`w-8 h-8 opacity-50 ${getHealthColor(summary.health)}`} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Services Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{services.map((service, index) => {
|
||||
const Icon = SERVICE_ICONS[service.type] || Server;
|
||||
const statusConfig = STATUS_COLORS[service.status] || STATUS_COLORS.unknown;
|
||||
const StatusIcon = statusConfig.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={service.type}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className={`glass-panel p-5 border-2 ${service.running ? 'border-green-500/30' : 'border-gray-500/30'}`}
|
||||
>
|
||||
{/* Service Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${service.running ? 'bg-green-500/20' : 'bg-gray-500/20'}`}>
|
||||
<Icon className={`w-5 h-5 ${service.running ? 'text-green-400' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-white">{service.name}</h3>
|
||||
<p className="text-xs text-light-grey">{service.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className={`flex items-center gap-1 px-2 py-1 rounded-full ${statusConfig.bg} border ${statusConfig.border}`}>
|
||||
<StatusIcon className={`w-3 h-3 ${statusConfig.text}`} />
|
||||
<span className={`text-xs font-medium ${statusConfig.text} capitalize`}>
|
||||
{service.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Details */}
|
||||
<div className="space-y-2">
|
||||
{service.port && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-light-grey">Port:</span>
|
||||
<span className="text-white font-mono">{service.port}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.pid && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-light-grey">PID:</span>
|
||||
<span className="text-white font-mono">{service.pid}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.running && service.cpu !== undefined && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-light-grey flex items-center gap-1">
|
||||
<Cpu className="w-3 h-3" />
|
||||
CPU:
|
||||
</span>
|
||||
<span className="text-white font-mono">{service.cpu.toFixed(1)}%</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.running && service.memory !== undefined && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-light-grey flex items-center gap-1">
|
||||
<MemoryStick className="w-3 h-3" />
|
||||
Memory:
|
||||
</span>
|
||||
<span className="text-white font-mono">{service.memory.toFixed(1)}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Service URL (if applicable) */}
|
||||
{service.port && service.running && (
|
||||
<div className="mt-4 pt-4 border-t border-electric-teal/20">
|
||||
<a
|
||||
href={`http://localhost:${service.port}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-electric-teal hover:text-electric-teal/80 transition-colors font-mono"
|
||||
>
|
||||
http://localhost:{service.port}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="glass-panel p-4 border border-electric-teal/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-electric-teal flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-white mb-1">Service Management</h4>
|
||||
<p className="text-sm text-light-grey">
|
||||
To start/stop services, use the management scripts:
|
||||
</p>
|
||||
<div className="mt-2 space-y-1">
|
||||
<code className="text-xs text-electric-teal block">./start_all_services.sh</code>
|
||||
<code className="text-xs text-electric-teal block">./stop_all_services.sh</code>
|
||||
<code className="text-xs text-electric-teal block">./restart_services.sh</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Updated */}
|
||||
{servicesData && (
|
||||
<div className="text-center text-xs text-light-grey">
|
||||
Last updated: {new Date(servicesData.timestamp * 1000).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Home, Settings, Database, MessageSquare, Plus, Trash2, FolderOpen, Zap, Edit } from 'lucide-react';
|
||||
import { Home, Settings, Database, MessageSquare, Plus, Trash2, FolderOpen, Zap, Edit, Activity } from 'lucide-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import axios from 'axios';
|
||||
|
|
@ -8,6 +8,7 @@ import ChatInterface from '../components/ChatInterface';
|
|||
import KBManagement from '../components/KBManagement';
|
||||
import ProjectSetup from '../components/ProjectSetup';
|
||||
import DirectoryPicker from '../components/DirectoryPicker';
|
||||
import ServiceDashboard from '../components/ServiceDashboard';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8051';
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ interface ProjectMCP {
|
|||
export default function MainApp() {
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
const [selectedMCP, setSelectedMCP] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'mcps' | 'chat'>('overview');
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'mcps' | 'chat' | 'services'>('overview');
|
||||
const [showCreateProject, setShowCreateProject] = useState(false);
|
||||
const [showProjectSetup, setShowProjectSetup] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState('');
|
||||
|
|
@ -322,6 +323,17 @@ export default function MainApp() {
|
|||
<MessageSquare className="w-4 h-4 inline mr-2" />
|
||||
Chat
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('services')}
|
||||
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
|
||||
activeTab === 'services'
|
||||
? 'bg-electric-teal text-deep-night'
|
||||
: 'text-light-grey hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Activity className="w-4 h-4 inline mr-2" />
|
||||
Services
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
|
|
@ -410,6 +422,8 @@ export default function MainApp() {
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
) : activeTab === 'services' ? (
|
||||
<ServiceDashboard />
|
||||
) : (
|
||||
selectedMCP ? (
|
||||
<ChatInterface
|
||||
|
|
|
|||
|
|
@ -1605,6 +1605,291 @@ async def api_list_ollama_models():
|
|||
raise HTTPException(status_code=503, detail=str(e))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SERVICE DASHBOARD ENDPOINTS (NEW - Real-time service monitoring)
|
||||
# ============================================================================
|
||||
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
class ServiceControlRequest(BaseModel):
|
||||
"""Request model for service control."""
|
||||
action: str # start, stop, restart
|
||||
|
||||
|
||||
def check_process_running(process_name: str) -> dict:
|
||||
"""
|
||||
Check if a process is running by name.
|
||||
|
||||
Returns:
|
||||
dict with status, pid, memory, cpu
|
||||
"""
|
||||
try:
|
||||
# Use pgrep to find process
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", process_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
pids = result.stdout.strip().split('\n')
|
||||
pid = pids[0] # Get first matching PID
|
||||
|
||||
# Get process stats using ps
|
||||
ps_result = subprocess.run(
|
||||
["ps", "-p", pid, "-o", "pid,%cpu,%mem,command"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2
|
||||
)
|
||||
|
||||
if ps_result.returncode == 0:
|
||||
lines = ps_result.stdout.strip().split('\n')
|
||||
if len(lines) > 1:
|
||||
parts = lines[1].split(None, 3)
|
||||
return {
|
||||
"running": True,
|
||||
"pid": int(pid),
|
||||
"cpu": float(parts[1]) if len(parts) > 1 else 0.0,
|
||||
"memory": float(parts[2]) if len(parts) > 2 else 0.0,
|
||||
"status": "running"
|
||||
}
|
||||
|
||||
return {
|
||||
"running": False,
|
||||
"pid": None,
|
||||
"cpu": 0.0,
|
||||
"memory": 0.0,
|
||||
"status": "stopped"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check process {process_name}: {e}")
|
||||
return {
|
||||
"running": False,
|
||||
"pid": None,
|
||||
"cpu": 0.0,
|
||||
"memory": 0.0,
|
||||
"status": "unknown",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def check_port_listening(port: int) -> bool:
|
||||
"""Check if a port is being listened on."""
|
||||
try:
|
||||
# Use netstat or ss to check if port is listening
|
||||
result = subprocess.run(
|
||||
["sh", "-c", f"lsof -i :{port} -t || netstat -tuln | grep :{port} || ss -tuln | grep :{port}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2
|
||||
)
|
||||
return result.returncode == 0 and result.stdout.strip() != ""
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check port {port}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@app.get("/api/services/status")
|
||||
async def api_get_services_status():
|
||||
"""
|
||||
Get status of all Claude OS services.
|
||||
|
||||
Returns real-time status for:
|
||||
- MCP Server (this server)
|
||||
- Frontend (Vite dev server)
|
||||
- Redis
|
||||
- RQ Worker
|
||||
- Ollama
|
||||
- File Watcher
|
||||
"""
|
||||
services = []
|
||||
|
||||
# 1. MCP Server (always running since we're responding)
|
||||
mcp_status = {
|
||||
"name": "MCP Server",
|
||||
"type": "mcp_server",
|
||||
"port": Config.MCP_SERVER_PORT,
|
||||
"running": True,
|
||||
"status": "running",
|
||||
"pid": os.getpid(),
|
||||
"cpu": 0.0,
|
||||
"memory": 0.0,
|
||||
"description": "Main API server and MCP endpoint"
|
||||
}
|
||||
services.append(mcp_status)
|
||||
|
||||
# 2. Frontend (Vite dev server on port 5173)
|
||||
frontend_port = 5173
|
||||
frontend_process = check_process_running("vite")
|
||||
frontend_status = {
|
||||
"name": "Frontend",
|
||||
"type": "frontend",
|
||||
"port": frontend_port,
|
||||
"running": frontend_process["running"] or check_port_listening(frontend_port),
|
||||
"status": frontend_process["status"],
|
||||
"pid": frontend_process.get("pid"),
|
||||
"cpu": frontend_process.get("cpu", 0.0),
|
||||
"memory": frontend_process.get("memory", 0.0),
|
||||
"description": "React web interface"
|
||||
}
|
||||
services.append(frontend_status)
|
||||
|
||||
# 3. Redis (port 6379)
|
||||
redis_port = 6379
|
||||
redis_process = check_process_running("redis-server")
|
||||
redis_status = {
|
||||
"name": "Redis",
|
||||
"type": "redis",
|
||||
"port": redis_port,
|
||||
"running": redis_process["running"] or check_port_listening(redis_port),
|
||||
"status": redis_process["status"],
|
||||
"pid": redis_process.get("pid"),
|
||||
"cpu": redis_process.get("cpu", 0.0),
|
||||
"memory": redis_process.get("memory", 0.0),
|
||||
"description": "Job queue and caching"
|
||||
}
|
||||
services.append(redis_status)
|
||||
|
||||
# 4. RQ Worker
|
||||
rq_process = check_process_running("rq worker")
|
||||
rq_status = {
|
||||
"name": "RQ Worker",
|
||||
"type": "rq_worker",
|
||||
"port": None,
|
||||
"running": rq_process["running"],
|
||||
"status": rq_process["status"],
|
||||
"pid": rq_process.get("pid"),
|
||||
"cpu": rq_process.get("cpu", 0.0),
|
||||
"memory": rq_process.get("memory", 0.0),
|
||||
"description": "Background job processor"
|
||||
}
|
||||
services.append(rq_status)
|
||||
|
||||
# 5. Ollama (port 11434)
|
||||
ollama_port = 11434
|
||||
ollama_running = False
|
||||
try:
|
||||
response = requests.get(f"{Config.OLLAMA_HOST}/api/tags", timeout=1)
|
||||
ollama_running = response.status_code == 200
|
||||
except:
|
||||
ollama_running = check_port_listening(ollama_port)
|
||||
|
||||
ollama_process = check_process_running("ollama")
|
||||
ollama_status = {
|
||||
"name": "Ollama",
|
||||
"type": "ollama",
|
||||
"port": ollama_port,
|
||||
"running": ollama_running or ollama_process["running"],
|
||||
"status": "running" if ollama_running else ollama_process["status"],
|
||||
"pid": ollama_process.get("pid"),
|
||||
"cpu": ollama_process.get("cpu", 0.0),
|
||||
"memory": ollama_process.get("memory", 0.0),
|
||||
"description": "Local AI model server"
|
||||
}
|
||||
services.append(ollama_status)
|
||||
|
||||
# 6. File Watcher
|
||||
try:
|
||||
watcher = get_global_watcher()
|
||||
watcher_status_data = watcher.get_status()
|
||||
watcher_running = watcher_status_data.get("is_running", False)
|
||||
except:
|
||||
watcher_running = False
|
||||
|
||||
watcher_status = {
|
||||
"name": "File Watcher",
|
||||
"type": "file_watcher",
|
||||
"port": None,
|
||||
"running": watcher_running,
|
||||
"status": "running" if watcher_running else "stopped",
|
||||
"pid": None,
|
||||
"cpu": 0.0,
|
||||
"memory": 0.0,
|
||||
"description": "Automatic folder sync"
|
||||
}
|
||||
services.append(watcher_status)
|
||||
|
||||
# Calculate summary
|
||||
total = len(services)
|
||||
running = sum(1 for s in services if s["running"])
|
||||
stopped = total - running
|
||||
|
||||
return {
|
||||
"services": services,
|
||||
"summary": {
|
||||
"total": total,
|
||||
"running": running,
|
||||
"stopped": stopped,
|
||||
"health": "healthy" if running >= total - 1 else ("degraded" if running > 0 else "critical")
|
||||
},
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/services/{service_type}/control")
|
||||
async def api_control_service(service_type: str, request: ServiceControlRequest):
|
||||
"""
|
||||
Control a service (start/stop/restart).
|
||||
|
||||
Note: This is a basic implementation. In production, you'd want more
|
||||
sophisticated service management (systemd, supervisord, etc.)
|
||||
"""
|
||||
action = request.action.lower()
|
||||
|
||||
if action not in ["start", "stop", "restart"]:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid action: {action}")
|
||||
|
||||
# For now, return a helpful message about manual control
|
||||
# In a production system, you'd integrate with systemd or supervisord
|
||||
|
||||
if service_type == "mcp_server":
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Cannot control MCP server from itself. Use ./start.sh or stop_all_services.sh",
|
||||
"service": service_type
|
||||
}
|
||||
|
||||
service_commands = {
|
||||
"frontend": {
|
||||
"start": "cd frontend && npm run dev &",
|
||||
"stop": "pkill -f 'vite'",
|
||||
"script": "frontend/package.json"
|
||||
},
|
||||
"redis": {
|
||||
"start": "redis-server --daemonize yes",
|
||||
"stop": "redis-cli shutdown",
|
||||
"script": None
|
||||
},
|
||||
"rq_worker": {
|
||||
"start": "rq worker &",
|
||||
"stop": "pkill -f 'rq worker'",
|
||||
"script": None
|
||||
},
|
||||
"ollama": {
|
||||
"start": "ollama serve &",
|
||||
"stop": "pkill -f ollama",
|
||||
"script": None
|
||||
}
|
||||
}
|
||||
|
||||
if service_type not in service_commands:
|
||||
raise HTTPException(status_code=404, detail=f"Unknown service: {service_type}")
|
||||
|
||||
commands = service_commands[service_type]
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Service control not fully implemented yet. Manual command:",
|
||||
"manual_command": commands.get(action),
|
||||
"service": service_type,
|
||||
"action": action,
|
||||
"note": "Use start_all_services.sh or stop_all_services.sh for full control"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/browse-directory")
|
||||
async def api_browse_directory(path: str = None):
|
||||
"""REST API: Browse directories and return subdirectories."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue