diff --git a/services/city-service/models_city.py b/services/city-service/models_city.py index 354523c1..b37321e6 100644 --- a/services/city-service/models_city.py +++ b/services/city-service/models_city.py @@ -200,6 +200,20 @@ class HomeNodeView(BaseModel): environment: Optional[str] = None +class NodeProfile(BaseModel): + """Node profile for Node Directory""" + node_id: str + name: str + hostname: Optional[str] = None + roles: List[str] = [] + environment: str = "unknown" + status: str = "offline" + gpu_info: Optional[str] = None + agents_total: int = 0 + agents_online: int = 0 + last_heartbeat: Optional[str] = None + + class AgentSummary(BaseModel): """Agent summary for Agent Console""" id: str diff --git a/services/city-service/repo_city.py b/services/city-service/repo_city.py index 21ef54cf..affb6e61 100644 --- a/services/city-service/repo_city.py +++ b/services/city-service/repo_city.py @@ -1087,3 +1087,55 @@ async def get_microdao_by_slug(slug: str) -> Optional[dict]: return result + +# ============================================================================= +# Nodes Repository +# ============================================================================= + +async def get_all_nodes() -> List[dict]: + """Отримати список всіх нод з кількістю агентів""" + pool = await get_pool() + + query = """ + SELECT + nc.node_id, + nc.node_name AS name, + nc.hostname, + nc.roles, + nc.environment, + nc.status, + nc.gpu, + nc.last_sync AS last_heartbeat, + (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id) AS agents_total, + (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online + FROM node_cache nc + ORDER BY nc.environment DESC, nc.node_name + """ + + rows = await pool.fetch(query) + return [dict(row) for row in rows] + + +async def get_node_by_id(node_id: str) -> Optional[dict]: + """Отримати ноду по ID""" + pool = await get_pool() + + query = """ + SELECT + nc.node_id, + nc.node_name AS name, + nc.hostname, + nc.roles, + nc.environment, + nc.status, + nc.gpu, + nc.last_sync AS last_heartbeat, + (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id) AS agents_total, + (SELECT COUNT(*) FROM agents a WHERE a.node_id = nc.node_id AND a.status = 'online') AS agents_online + FROM node_cache nc + WHERE nc.node_id = $1 + """ + + row = await pool.fetchrow(query, node_id) + return dict(row) if row else None + diff --git a/services/city-service/routes_city.py b/services/city-service/routes_city.py index 1f67c2b3..23a1065c 100644 --- a/services/city-service/routes_city.py +++ b/services/city-service/routes_city.py @@ -23,6 +23,7 @@ from models_city import ( AgentPresence, AgentSummary, HomeNodeView, + NodeProfile, PublicCitizenSummary, PublicCitizenProfile, CitizenInteractionInfo, @@ -115,6 +116,64 @@ async def list_agents( raise HTTPException(status_code=500, detail="Failed to list agents") +# ============================================================================= +# Nodes API (for Node Directory) +# ============================================================================= + +@public_router.get("/nodes") +async def list_nodes(): + """Список всіх нод мережі""" + try: + nodes = await repo_city.get_all_nodes() + + items: List[NodeProfile] = [] + for node in nodes: + items.append(NodeProfile( + node_id=node["node_id"], + name=node["name"], + hostname=node.get("hostname"), + roles=list(node.get("roles") or []), + environment=node.get("environment", "unknown"), + status=node.get("status", "offline"), + gpu_info=node.get("gpu"), + agents_total=node.get("agents_total", 0), + agents_online=node.get("agents_online", 0), + last_heartbeat=str(node["last_heartbeat"]) if node.get("last_heartbeat") else None + )) + + return {"items": items, "total": len(items)} + except Exception as e: + logger.error(f"Failed to list nodes: {e}") + raise HTTPException(status_code=500, detail="Failed to list nodes") + + +@public_router.get("/nodes/{node_id}") +async def get_node_profile(node_id: str): + """Отримати профіль ноди""" + try: + node = await repo_city.get_node_by_id(node_id) + if not node: + raise HTTPException(status_code=404, detail="Node not found") + + return NodeProfile( + node_id=node["node_id"], + name=node["name"], + hostname=node.get("hostname"), + roles=list(node.get("roles") or []), + environment=node.get("environment", "unknown"), + status=node.get("status", "offline"), + gpu_info=node.get("gpu"), + agents_total=node.get("agents_total", 0), + agents_online=node.get("agents_online", 0), + last_heartbeat=str(node["last_heartbeat"]) if node.get("last_heartbeat") else None + ) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get node {node_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to get node") + + # ============================================================================= # Public Citizens API # =============================================================================