feat: add /public/nodes endpoints for Node Directory
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user