feat(node-dashboard): add MVP dashboard API endpoint
- services/city-service: add /api/v1/nodes/{id}/dashboard endpoint
- services/city-service: add legacy /api/v1/node/dashboard?nodeId=...
- apps/web: update route.ts to use city-service first
- apps/web: fallback to node-registry if city-service fails
Response structure:
node_id, name, kind, status, tags, agents_total,
agents_online, uptime (null), metrics_available (false)
Closes TASK_PHASE_NODE_DASHBOARD_API_MVP
This commit is contained in:
@@ -1,25 +1,39 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
const NODE_REGISTRY_URL = process.env.NODE_REGISTRY_URL || 'http://dagi-node-registry:9205';
|
// Primary: city-service (has /api/v1/nodes/{id}/dashboard)
|
||||||
|
// Fallback: node-registry (legacy, if configured)
|
||||||
|
const CITY_SERVICE_URL = process.env.INTERNAL_API_URL || 'http://daarion-city-service:7001';
|
||||||
|
const NODE_REGISTRY_URL = process.env.NODE_REGISTRY_URL || '';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const nodeId = searchParams.get('nodeId');
|
const nodeId = searchParams.get('nodeId');
|
||||||
|
|
||||||
// Build URL - either specific node or self
|
if (!nodeId) {
|
||||||
const endpoint = nodeId
|
return NextResponse.json(
|
||||||
? `${NODE_REGISTRY_URL}/api/v1/nodes/${nodeId}/dashboard`
|
{ error: 'nodeId query parameter is required' },
|
||||||
: `${NODE_REGISTRY_URL}/api/v1/nodes/self/dashboard`;
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
// Try city-service first (MVP dashboard)
|
||||||
headers: {
|
const cityServiceEndpoint = `${CITY_SERVICE_URL}/api/v1/nodes/${nodeId}/dashboard`;
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
let response = await fetch(cityServiceEndpoint, {
|
||||||
// Revalidate every 10 seconds
|
headers: { 'Content-Type': 'application/json' },
|
||||||
next: { revalidate: 10 }
|
next: { revalidate: 10 }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If city-service fails and node-registry is configured, try it as fallback
|
||||||
|
if (!response.ok && NODE_REGISTRY_URL) {
|
||||||
|
const nodeRegistryEndpoint = `${NODE_REGISTRY_URL}/api/v1/nodes/${nodeId}/dashboard`;
|
||||||
|
response = await fetch(nodeRegistryEndpoint, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
next: { revalidate: 10 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: `Failed to fetch dashboard: ${response.status}` },
|
{ error: `Failed to fetch dashboard: ${response.status}` },
|
||||||
@@ -38,4 +52,3 @@ export async function GET(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -753,6 +753,55 @@ async def ask_citizen(
|
|||||||
raise HTTPException(status_code=500, detail="Failed to ask citizen")
|
raise HTTPException(status_code=500, detail="Failed to ask citizen")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# API v1 — Node Dashboard
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@api_router.get("/nodes/{node_id}/dashboard")
|
||||||
|
async def get_node_dashboard(node_id: str):
|
||||||
|
"""
|
||||||
|
Отримати мінімальний Dashboard для ноди (MVP).
|
||||||
|
|
||||||
|
Повертає базову інформацію + placeholder для метрик:
|
||||||
|
- node_id, name, kind, status
|
||||||
|
- tags (roles)
|
||||||
|
- agents_total, agents_online
|
||||||
|
- uptime (null — placeholder)
|
||||||
|
- metrics_available (false — placeholder)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
node = await repo_city.get_node_by_id(node_id)
|
||||||
|
if not node:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Node not found: {node_id}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"node_id": node["node_id"],
|
||||||
|
"name": node["name"],
|
||||||
|
"kind": node.get("kind"),
|
||||||
|
"status": node.get("status", "unknown"),
|
||||||
|
"tags": list(node.get("roles") or []),
|
||||||
|
"agents_total": node.get("agents_total", 0),
|
||||||
|
"agents_online": node.get("agents_online", 0),
|
||||||
|
"uptime": None, # placeholder — буде заповнено коли з'явиться Prometheus/NATS
|
||||||
|
"metrics_available": False # прапорець, що розширеного дашборду ще немає
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get node dashboard for {node_id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to get node dashboard")
|
||||||
|
|
||||||
|
|
||||||
|
# Legacy endpoint for frontend compatibility
|
||||||
|
@api_router.get("/node/dashboard")
|
||||||
|
async def get_node_dashboard_legacy(node_id: str = Query(..., description="Node ID")):
|
||||||
|
"""
|
||||||
|
Legacy endpoint: /api/v1/node/dashboard?nodeId=...
|
||||||
|
Redirects to canonical /api/v1/nodes/{node_id}/dashboard
|
||||||
|
"""
|
||||||
|
return await get_node_dashboard(node_id)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# API v1 — MicroDAO Membership
|
# API v1 — MicroDAO Membership
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user