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.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 */}
+
-
- );
- }
-
- 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;
+}
+