diff --git a/apps/web/src/app/api/nodes/[nodeId]/route.ts b/apps/web/src/app/api/nodes/[nodeId]/route.ts new file mode 100644 index 00000000..d2a6a266 --- /dev/null +++ b/apps/web/src/app/api/nodes/[nodeId]/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const CITY_API_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || 'http://daarion-city-service:7001'; + +export async function GET( + _req: NextRequest, + { params }: { params: { nodeId: string } } +) { + try { + const { nodeId } = params; + + const response = await fetch(`${CITY_API_URL}/public/nodes/${nodeId}`, { + headers: { + 'Content-Type': 'application/json', + }, + cache: 'no-store', + }); + + if (!response.ok) { + const text = await response.text(); + console.error('Failed to fetch node:', response.status, text); + return NextResponse.json( + { error: 'Failed to fetch node' }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching node:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + diff --git a/apps/web/src/app/api/nodes/list/route.ts b/apps/web/src/app/api/nodes/list/route.ts new file mode 100644 index 00000000..16f23aa4 --- /dev/null +++ b/apps/web/src/app/api/nodes/list/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const CITY_API_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || 'http://daarion-city-service:7001'; + +export async function GET(_req: NextRequest) { + try { + const response = await fetch(`${CITY_API_URL}/public/nodes`, { + headers: { + 'Content-Type': 'application/json', + }, + cache: 'no-store', + }); + + if (!response.ok) { + const text = await response.text(); + console.error('Failed to fetch nodes:', response.status, text); + return NextResponse.json( + { error: 'Failed to fetch nodes' }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching nodes:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + diff --git a/apps/web/src/app/nodes/[nodeId]/page.tsx b/apps/web/src/app/nodes/[nodeId]/page.tsx new file mode 100644 index 00000000..415bef9a --- /dev/null +++ b/apps/web/src/app/nodes/[nodeId]/page.tsx @@ -0,0 +1,269 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import Link from 'next/link'; +import { Server, ArrowLeft, Cpu, Users, Activity, ExternalLink } from 'lucide-react'; +import { useNodeProfile } from '@/hooks/useNodes'; +import { useNodeDashboard } from '@/hooks/useNodeDashboard'; +import { + NodeSummaryCard, + InfraCard, + AIServicesCard, + AgentsCard, + MatrixCard, + ModulesCard, + NodeStandardComplianceCard +} from '@/components/node-dashboard'; + +function getNodeLabel(nodeId: string): string { + if (nodeId.includes('node-1')) return 'НОДА1'; + if (nodeId.includes('node-2')) return 'НОДА2'; + return 'НОДА'; +} + +export default function NodeCabinetPage() { + const params = useParams(); + const nodeId = params.nodeId as string; + const nodeLabel = getNodeLabel(nodeId); + + // Basic node profile from node_cache + const { node: nodeProfile, isLoading: profileLoading, error: profileError } = useNodeProfile(nodeId); + + // Full dashboard (if available - currently only for NODE1) + const { dashboard, isLoading: dashboardLoading, error: dashboardError, refresh, lastUpdated } = useNodeDashboard({ + refreshInterval: 30000, + enabled: nodeId === 'node-1-hetzner-gex44' // Only enable for NODE1 + }); + + const isLoading = profileLoading || dashboardLoading; + const error = profileError || (dashboardError && nodeId === 'node-1-hetzner-gex44'); + const isProduction = nodeProfile?.environment === 'production'; + + if (isLoading && !nodeProfile && !dashboard) { + return ( +
+
+
+
+
+

Loading node cabinet...

+
+
+
+
+ ); + } + + if (error || (!nodeProfile && !dashboard)) { + return ( +
+
+
+

Failed to load node

+

{(error as Error)?.message || 'Node not found'}

+ + Back to Nodes + +
+
+
+ ); + } + + // If we have full dashboard (NODE1), show it + if (dashboard) { + return ( +
+
+ {/* Header */} +
+
+ + + +
+

{nodeLabel}

+

{dashboard.node.name}

+
+
+
+ {lastUpdated && ( +

+ Updated: {lastUpdated.toLocaleTimeString()} +

+ )} + +
+
+ + {/* Main Grid */} +
+
+ + +
+ + +
+ + +
+ +
+ + + +
+
+ + {/* Link to agents */} +
+ + + Агенти цієї ноди + + +
+
+
+ ); + } + + // Basic profile view (for NODE2 and others) + return ( +
+
+ {/* Header */} +
+ + + +
+

{nodeLabel}

+

{nodeProfile?.name}

+
+
+ + {/* Node Card */} +
+
+
+ +
+
+
+

{nodeProfile?.name}

+ + + {nodeProfile?.status} + +
+

{nodeProfile?.hostname}

+
+
+ + {/* Info Grid */} +
+
+
+

Environment

+

+ {nodeProfile?.environment} +

+
+
+

Node ID

+

{nodeProfile?.node_id}

+
+
+
+
+

Agents

+
+ + {nodeProfile?.agents_total} + total + {nodeProfile?.agents_online} online +
+
+ {nodeProfile?.last_heartbeat && ( +
+

Last Heartbeat

+

+ {new Date(nodeProfile.last_heartbeat).toLocaleString()} +

+
+ )} +
+
+ + {/* Roles */} + {nodeProfile?.roles && nodeProfile.roles.length > 0 && ( +
+

Roles

+
+ {nodeProfile.roles.map((role) => ( + + {role} + + ))} +
+
+ )} +
+ + {/* Notice for non-NODE1 */} +
+

+ ⚠️ Детальний моніторинг доступний тільки для НОДА1 (Production). + Для цієї ноди показано базову інформацію з node_cache. +

+
+ + {/* Link to agents */} + + + Агенти цієї ноди + + +
+
+ ); +} + diff --git a/apps/web/src/app/nodes/page.tsx b/apps/web/src/app/nodes/page.tsx index 61f88361..c7d8f231 100644 --- a/apps/web/src/app/nodes/page.tsx +++ b/apps/web/src/app/nodes/page.tsx @@ -1,107 +1,202 @@ 'use client'; -import { useNodeDashboard } from '@/hooks/useNodeDashboard'; -import { - NodeSummaryCard, - InfraCard, - AIServicesCard, - AgentsCard, - MatrixCard, - ModulesCard, - NodeStandardComplianceCard -} from '@/components/node-dashboard'; +import Link from 'next/link'; +import { Server, Cpu, Users, Activity, ExternalLink } from 'lucide-react'; +import { useNodeList } from '@/hooks/useNodes'; +import { NodeProfile } from '@/lib/types/nodes'; -export default function NodeDashboardPage() { - const { dashboard, isLoading, error, refresh, lastUpdated } = useNodeDashboard({ - refreshInterval: 30000 // 30 seconds - }); - - if (isLoading && !dashboard) { - return ( -
-
-
-
-
-

Loading node dashboard...

+function getNodeLabel(nodeId: string): string { + if (nodeId.includes('node-1')) return 'НОДА1'; + if (nodeId.includes('node-2')) return 'НОДА2'; + return 'НОДА'; +} + +function NodeCard({ node }: { node: NodeProfile }) { + const isOnline = node.status === 'online'; + const nodeLabel = getNodeLabel(node.node_id); + const isProduction = node.environment === 'production'; + + return ( + + {/* Header */} +
+
+
+ +
+
+

+ {nodeLabel} +

+

{node.name}

+
+
+ + + {isOnline ? 'online' : 'offline'} + +
+ + {/* Info */} +
+ {node.hostname && ( +
+ + {node.hostname} +
+ )} +
+ + {node.environment} +
+
+ + {/* Roles */} + {node.roles.length > 0 && ( +
+ {node.roles.map((role) => ( + + {role} + + ))} +
+ )} + + {/* Stats */} +
+
+
+ + {node.agents_total} + агентів +
+
+ {node.agents_online} + online +
+
+ + Open → + +
+ + ); +} + +export default function NodesPage() { + const { nodes, total, isLoading, error } = useNodeList(); + + return ( +
+
+ {/* Header */} +
+
+ +

+ Node Directory +

+
+

+ Всі ноди мережі DAARION +

+

+ Знайдено нод: {total} +

+
+ + {/* Legend */} +
+
+
+
+ Production +
+
+
+ Development
-
- ); - } - - if (error) { - return ( -
-
-
-

Failed to load dashboard

-

{error.message}

- + + {/* Content */} + {error && ( +
+

Помилка завантаження нод

-
-
- ); - } - - if (!dashboard) return null; - - return ( -
-
- {/* Header */} -
-
-

Node Dashboard

-

- Real-time monitoring and status + )} + + {isLoading ? ( +

+ {Array.from({ length: 2 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : nodes.length === 0 ? ( +
+ +

+ Ноди не знайдені +

+

+ Наразі немає зареєстрованих нод.

-
- {lastUpdated && ( -

- Updated: {lastUpdated.toLocaleTimeString()} -

- )} - -
-
- - {/* Main Grid */} -
- {/* Left Column - Node Summary */} -
- - -
- - -
- - -
- - {/* Right Column */} -
- - - + ) : ( +
+ {nodes.map((node) => ( + + ))}
+ )} + + {/* Links */} +
+ + + Agent Console + + + + + Публічні громадяни + +
); } - diff --git a/apps/web/src/hooks/useNodes.ts b/apps/web/src/hooks/useNodes.ts new file mode 100644 index 00000000..a32ea31d --- /dev/null +++ b/apps/web/src/hooks/useNodes.ts @@ -0,0 +1,48 @@ +'use client'; + +import useSWR from 'swr'; +import { NodeProfile, NodeListResponse } from '@/lib/types/nodes'; + +const fetcher = async (url: string) => { + const res = await fetch(url); + if (!res.ok) { + throw new Error('Failed to fetch'); + } + return res.json(); +}; + +export function useNodeList() { + const { data, error, isLoading, mutate } = useSWR( + '/api/nodes/list', + fetcher, + { + revalidateOnFocus: false, + } + ); + + return { + nodes: data?.items || [], + total: data?.total || 0, + isLoading, + error, + mutate, + }; +} + +export function useNodeProfile(nodeId: string | undefined) { + const { data, error, isLoading, mutate } = useSWR( + nodeId ? `/api/nodes/${nodeId}` : null, + fetcher, + { + revalidateOnFocus: false, + } + ); + + return { + node: data, + isLoading, + error, + mutate, + }; +} + diff --git a/apps/web/src/lib/types/agents.ts b/apps/web/src/lib/types/agents.ts index 5efd1dec..a1c5aec7 100644 --- a/apps/web/src/lib/types/agents.ts +++ b/apps/web/src/lib/types/agents.ts @@ -1,4 +1,4 @@ -import { HomeNodeInfo } from './citizens'; +import { HomeNode } from './citizens'; export interface AgentMicrodaoMembership { microdao_id: string; @@ -18,7 +18,7 @@ export interface AgentSummary { public_slug?: string | null; public_title?: string | null; district?: string | null; - home_node?: HomeNodeInfo | null; + home_node?: HomeNode | null; microdao_memberships: AgentMicrodaoMembership[]; } @@ -39,7 +39,7 @@ export interface AgentDashboard { public_tagline?: string | null; public_skills: string[]; district?: string | null; - home_node?: HomeNodeInfo | null; + home_node?: HomeNode | null; microdao_memberships: AgentMicrodaoMembership[]; system_prompts?: { core?: string; diff --git a/apps/web/src/lib/types/nodes.ts b/apps/web/src/lib/types/nodes.ts new file mode 100644 index 00000000..f93b1ec9 --- /dev/null +++ b/apps/web/src/lib/types/nodes.ts @@ -0,0 +1,18 @@ +export interface NodeProfile { + node_id: string; + name: string; + hostname?: string | null; + roles: string[]; + environment: string; + status: string; + gpu_info?: string | null; + agents_total: number; + agents_online: number; + last_heartbeat?: string | null; +} + +export interface NodeListResponse { + items: NodeProfile[]; + total: number; +} +