1ca019fa48
Phase 1 MVP implementation: - PostgreSQL database schema for tasks, agents, executions, time tracking - Backend API: task CRUD, agent management, automatic time tracking - Frontend: Kanban board with drag-and-drop (Parking Lot → In Progress → Done) - Task modal for creating/editing tasks with tags, priority, assignee - 3 core agents seeded: PM, CTO, COO - Agent approval workflow: CxO level requires approval, others auto-execute - Automatic time tracking: starts on in_progress, stops on done - RBAC integration: OPC page accessible to admin/member roles Features: - Drag-and-drop tasks between columns - Assign tasks to humans or AI agents - Priority badges (low, medium, high, urgent) - Tags and due dates - Task history tracking - Time entry tracking with automatic timer - Agent execution records with approval workflow Next steps: - Initialize OPC database on NAS - Implement agent executor service - Add WebSocket for real-time updates - Add email notifications - Add PDF invoice generation
198 lines
5.8 KiB
Python
198 lines
5.8 KiB
Python
import time
|
|
|
|
import docker
|
|
import httpx
|
|
from fastapi import APIRouter
|
|
|
|
from config import CC_CONNECT_URL, DOCKER_HOST
|
|
|
|
router = APIRouter()
|
|
|
|
_client = None
|
|
|
|
def get_docker_client():
|
|
"""Get or create Docker client (lazy initialization)."""
|
|
global _client
|
|
if _client is None:
|
|
_client = docker.DockerClient(base_url=DOCKER_HOST, timeout=5)
|
|
return _client
|
|
|
|
|
|
def _get_cc_connect_container():
|
|
client = get_docker_client()
|
|
matches = client.containers.list(all=True, filters={"name": "cc-connect"})
|
|
return matches[0] if matches else None
|
|
|
|
|
|
@router.post("/start")
|
|
def start_cc_connect():
|
|
try:
|
|
container = _get_cc_connect_container()
|
|
except Exception as exc:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"container_status": None,
|
|
"error": f"docker check failed: {exc}",
|
|
}
|
|
|
|
if not container:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"container_status": "not_found",
|
|
"error": "cc-connect container not found",
|
|
}
|
|
|
|
try:
|
|
container.start()
|
|
container.reload()
|
|
container_status = container.status
|
|
return {
|
|
"ok": container_status == "running",
|
|
"status": "up" if container_status == "running" else "down",
|
|
"container_status": container_status,
|
|
"error": None if container_status == "running" else "cc-connect failed to start",
|
|
}
|
|
except Exception as exc:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"container_status": container.status,
|
|
"error": f"docker start failed: {exc}",
|
|
}
|
|
|
|
|
|
@router.post("/stop")
|
|
def stop_cc_connect():
|
|
try:
|
|
container = _get_cc_connect_container()
|
|
except Exception as exc:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"container_status": None,
|
|
"error": f"docker check failed: {exc}",
|
|
}
|
|
|
|
if not container:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"container_status": "not_found",
|
|
"error": "cc-connect container not found",
|
|
}
|
|
|
|
try:
|
|
container.stop()
|
|
container.reload()
|
|
container_status = container.status
|
|
return {
|
|
"ok": container_status != "running",
|
|
"status": "down",
|
|
"container_status": container_status,
|
|
"error": None if container_status != "running" else "cc-connect failed to stop",
|
|
}
|
|
except Exception as exc:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"container_status": container.status,
|
|
"error": f"docker stop failed: {exc}",
|
|
}
|
|
|
|
|
|
@router.get("/health")
|
|
async def cc_connect_health():
|
|
url = (CC_CONNECT_URL or "").strip()
|
|
|
|
container = None
|
|
try:
|
|
container = _get_cc_connect_container()
|
|
except Exception as exc:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"source": "docker",
|
|
"container_name": None,
|
|
"container_status": None,
|
|
"endpoint": None,
|
|
"http_status": None,
|
|
"latency_ms": None,
|
|
"error": f"docker check failed: {exc}",
|
|
}
|
|
|
|
container_name = container.name if container else None
|
|
container_status = container.status if container else "not_found"
|
|
container_up = container_status == "running"
|
|
|
|
if not url:
|
|
return {
|
|
"ok": container_up,
|
|
"status": "up" if container_up else "down",
|
|
"source": "docker",
|
|
"container_name": container_name,
|
|
"container_status": container_status,
|
|
"endpoint": None,
|
|
"http_status": None,
|
|
"latency_ms": None,
|
|
"error": None if container_up else "cc-connect container is not running",
|
|
}
|
|
|
|
timeout = httpx.Timeout(2.0)
|
|
started = time.perf_counter()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=timeout) as http_client:
|
|
response = await http_client.get(url)
|
|
latency_ms = round((time.perf_counter() - started) * 1000, 1)
|
|
|
|
if response.is_success and container_up:
|
|
return {
|
|
"ok": True,
|
|
"status": "up",
|
|
"source": "docker+http",
|
|
"container_name": container_name,
|
|
"container_status": container_status,
|
|
"endpoint": url,
|
|
"http_status": response.status_code,
|
|
"latency_ms": latency_ms,
|
|
"error": None,
|
|
}
|
|
|
|
if response.is_success and not container_up:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"source": "docker+http",
|
|
"container_name": container_name,
|
|
"container_status": container_status,
|
|
"endpoint": url,
|
|
"http_status": response.status_code,
|
|
"latency_ms": latency_ms,
|
|
"error": "HTTP probe succeeded but cc-connect container is not running",
|
|
}
|
|
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"source": "docker+http",
|
|
"container_name": container_name,
|
|
"container_status": container_status,
|
|
"endpoint": url,
|
|
"http_status": response.status_code,
|
|
"latency_ms": latency_ms,
|
|
"error": f"HTTP probe returned {response.status_code}",
|
|
}
|
|
except Exception as exc:
|
|
return {
|
|
"ok": False,
|
|
"status": "down",
|
|
"source": "docker+http",
|
|
"container_name": container_name,
|
|
"container_status": container_status,
|
|
"endpoint": url,
|
|
"http_status": None,
|
|
"latency_ms": None,
|
|
"error": str(exc),
|
|
}
|