fix: NODE1_REPAIR - healthchecks, dependencies, SSR env, telegram gateway

TASK_PHASE_NODE1_REPAIR:
- Fix daarion-web SSR: use CITY_API_BASE_URL instead of 127.0.0.1
- Fix auth API routes: use AUTH_API_URL env var
- Add wget to Dockerfiles for healthchecks (stt, ocr, web-search, swapper, vector-db, rag)
- Update healthchecks to use wget instead of curl
- Fix vector-db-service: update torch==2.4.0, sentence-transformers==2.6.1
- Fix rag-service: correct haystack imports for v2.x
- Fix telegram-gateway: remove msg.ack() for non-JetStream NATS
- Add /health endpoint to nginx mvp-routes.conf
- Add room_role, is_public, sort_order columns to city_rooms migration
- Add TASK_PHASE_NODE1_REPAIR.md and DEPLOY_NODE1_REPAIR.md docs

Previous tasks included:
- TASK 039-044: Orchestrator rooms, Matrix chat cleanup, CrewAI integration
This commit is contained in:
Apple
2025-11-29 05:17:08 -08:00
parent 0bab4bba08
commit a6e531a098
69 changed files with 4693 additions and 1310 deletions

View File

@@ -1,114 +1,34 @@
/**
* Connect Node Page - Спрощений UI для підключення ноди
* Для звичайних користувачів (без термінала)
* Connect Node Page - Інструкції з підключення ноди
*/
import React, { useState } from 'react';
import { Download, Copy, CheckCircle, Monitor, Cpu, HardDrive } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import { Download, Copy, CheckCircle, Monitor, Cpu, HardDrive, Terminal, ExternalLink, AlertCircle } from 'lucide-react';
import { Link } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import { apiGet } from '../api/client';
export default function ConnectNodePage() {
const [copied, setCopied] = useState(false);
const [selectedOS, setSelectedOS] = useState<'macos' | 'linux' | 'windows'>('macos');
const [instructions, setInstructions] = useState<string>('');
const [loading, setLoading] = useState(true);
const registryUrl = 'http://localhost:9205'; // TODO: змінити на production URL
useEffect(() => {
const fetchInstructions = async () => {
try {
const response = await apiGet<{ content: string }>('/public/nodes/join/instructions');
if (response.content) {
setInstructions(response.content);
}
} catch (error) {
console.error('Failed to fetch instructions:', error);
} finally {
setLoading(false);
}
};
// Інструкції для різних ОС
const instructions = {
macos: {
title: '🍎 macOS',
steps: [
{
title: '1. Завантажити Bootstrap Agent',
description: 'Скачайте скрипт автоматичної реєстрації',
action: 'download',
code: 'curl -O http://localhost:9205/bootstrap/node_bootstrap.py',
},
{
title: '2. Встановити залежності',
description: 'Встановіть необхідні Python бібліотеки',
code: 'pip3 install --user requests psutil',
},
{
title: '3. Запустити Bootstrap Agent',
description: 'Запустіть агент для автоматичної реєстрації',
code: `export NODE_REGISTRY_URL="${registryUrl}"
export NODE_ROLE="worker"
python3 node_bootstrap.py`,
},
],
},
linux: {
title: '🐧 Linux',
steps: [
{
title: '1. Завантажити Bootstrap Agent',
description: 'Скачайте скрипт автоматичної реєстрації',
code: 'curl -O http://localhost:9205/bootstrap/node_bootstrap.py',
},
{
title: '2. Встановити залежності',
description: 'Встановіть необхідні Python бібліотеки',
code: 'pip3 install requests psutil',
},
{
title: '3. Запустити Bootstrap Agent',
description: 'Запустіть агент для автоматичної реєстрації',
code: `export NODE_REGISTRY_URL="${registryUrl}"
export NODE_ROLE="worker"
python3 node_bootstrap.py`,
},
{
title: '4. (Опціонально) Додати як systemd service',
description: 'Для автоматичного запуску при перезавантаженні',
code: `sudo tee /etc/systemd/system/node-bootstrap.service << EOF
[Unit]
Description=DAGI Node Bootstrap
After=network.target
[Service]
Type=simple
Environment="NODE_REGISTRY_URL=${registryUrl}"
Environment="NODE_ROLE=worker"
ExecStart=/usr/bin/python3 /opt/dagi/node_bootstrap.py
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable node-bootstrap
sudo systemctl start node-bootstrap`,
},
],
},
windows: {
title: '🪟 Windows',
steps: [
{
title: '1. Завантажити Bootstrap Agent',
description: 'Скачайте скрипт автоматичної реєстрації',
code: 'curl -O http://localhost:9205/bootstrap/node_bootstrap.py',
},
{
title: '2. Встановити Python',
description: 'Завантажте Python 3.9+ з python.org',
link: 'https://www.python.org/downloads/',
},
{
title: '3. Встановити залежності',
description: 'Відкрийте PowerShell та виконайте',
code: 'pip install requests psutil',
},
{
title: '4. Запустити Bootstrap Agent',
description: 'Запустіть агент для автоматичної реєстрації',
code: `$env:NODE_REGISTRY_URL="${registryUrl}"
$env:NODE_ROLE="worker"
python node_bootstrap.py`,
},
],
},
};
fetchInstructions();
}, []);
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
@@ -116,182 +36,161 @@ python node_bootstrap.py`,
setTimeout(() => setCopied(false), 2000);
};
const currentInstructions = instructions[selectedOS];
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent"></div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-purple-950/20 to-slate-950 text-white p-6">
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-5xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
🔌 Підключити Ноду до DAGI
<Link to="/nodes" className="text-blue-600 hover:underline mb-4 inline-block">&larr; Назад до списку нод</Link>
<h1 className="text-4xl font-bold mb-2 text-gray-900">
🔌 Підключити Ноду
</h1>
<p className="text-slate-400">
Простий спосіб підключити ваш комп'ютер до децентралізованої мережі AI
<p className="text-gray-600">
Інструкція з розгортання обчислювальної ноди DAARION
</p>
</div>
{/* Benefits */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="bg-gradient-to-br from-purple-900/30 to-purple-950/30 border border-purple-800/30 rounded-xl p-6">
<div className="bg-white border border-blue-100 rounded-xl p-6 shadow-sm">
<div className="text-3xl mb-3">💰</div>
<h3 className="text-lg font-semibold mb-2">Заробляйте μGOV</h3>
<p className="text-slate-400 text-sm">
Отримуйте токени за надання обчислювальних ресурсів
<h3 className="text-lg font-semibold mb-2 text-gray-900">Заробляйте токени</h3>
<p className="text-gray-500 text-sm">
Отримуйте винагороду за надання обчислювальних ресурсів
</p>
</div>
<div className="bg-gradient-to-br from-blue-900/30 to-blue-950/30 border border-blue-800/30 rounded-xl p-6">
<div className="bg-white border border-purple-100 rounded-xl p-6 shadow-sm">
<div className="text-3xl mb-3">🤖</div>
<h3 className="text-lg font-semibold mb-2">Доступ до AI</h3>
<p className="text-slate-400 text-sm">
Використовуйте AI моделі мережі безкоштовно
<h3 className="text-lg font-semibold mb-2 text-gray-900">Доступ до AI</h3>
<p className="text-gray-500 text-sm">
Використовуйте AI моделі мережі безкоштовно для своїх агентів
</p>
</div>
<div className="bg-gradient-to-br from-green-900/30 to-green-950/30 border border-green-800/30 rounded-xl p-6">
<div className="bg-white border border-green-100 rounded-xl p-6 shadow-sm">
<div className="text-3xl mb-3">🌱</div>
<h3 className="text-lg font-semibold mb-2">Підтримайте спільноту</h3>
<p className="text-slate-400 text-sm">
Станьте частиною децентралізованої AI мережі
<h3 className="text-lg font-semibold mb-2 text-gray-900">Розвивайте мережу</h3>
<p className="text-gray-500 text-sm">
Станьте частиною децентралізованої інфраструктури
</p>
</div>
</div>
{/* System Requirements */}
<div className="bg-slate-900/50 backdrop-blur border border-slate-800 rounded-xl p-6 mb-8">
<h2 className="text-xl font-bold mb-4">📋 Системні вимоги</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center gap-3">
<Cpu className="w-6 h-6 text-purple-400" />
<div>
<div className="text-slate-400 text-sm">CPU</div>
<div className="font-medium">4+ ядра</div>
</div>
</div>
<div className="flex items-center gap-3">
<Monitor className="w-6 h-6 text-blue-400" />
<div>
<div className="text-slate-400 text-sm">RAM</div>
<div className="font-medium">8+ GB</div>
</div>
</div>
<div className="flex items-center gap-3">
<HardDrive className="w-6 h-6 text-green-400" />
<div>
<div className="text-slate-400 text-sm">Disk</div>
<div className="font-medium">50+ GB вільно</div>
</div>
</div>
</div>
</div>
{/* OS Selector */}
<div className="mb-6">
<div className="flex gap-2">
<button
onClick={() => setSelectedOS('macos')}
className={`flex-1 py-3 px-4 rounded-lg font-medium transition-all ${
selectedOS === 'macos'
? 'bg-purple-600 text-white'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
🍎 macOS
</button>
<button
onClick={() => setSelectedOS('linux')}
className={`flex-1 py-3 px-4 rounded-lg font-medium transition-all ${
selectedOS === 'linux'
? 'bg-purple-600 text-white'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
🐧 Linux
</button>
<button
onClick={() => setSelectedOS('windows')}
className={`flex-1 py-3 px-4 rounded-lg font-medium transition-all ${
selectedOS === 'windows'
? 'bg-purple-600 text-white'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
🪟 Windows
</button>
</div>
</div>
{/* Instructions */}
<div className="space-y-6">
<h2 className="text-2xl font-bold">{currentInstructions.title}</h2>
{currentInstructions.steps.map((step, index) => (
<div
key={index}
className="bg-slate-900/50 backdrop-blur border border-slate-800 rounded-xl p-6"
>
<h3 className="text-lg font-semibold mb-2">{step.title}</h3>
<p className="text-slate-400 text-sm mb-4">{step.description}</p>
{step.code && (
<div className="relative">
<pre className="bg-slate-950 border border-slate-700 rounded-lg p-4 overflow-x-auto text-sm">
<code className="text-green-400">{step.code}</code>
</pre>
<button
onClick={() => copyToClipboard(step.code)}
className="absolute top-2 right-2 p-2 bg-slate-800 hover:bg-slate-700 rounded-lg transition-colors"
title="Скопіювати"
>
{copied ? (
<CheckCircle className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-slate-400" />
)}
</button>
{/* Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Instructions */}
<div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 prose prose-blue max-w-none">
{instructions ? (
<ReactMarkdown
components={{
code({node, inline, className, children, ...props}: any) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<div className="relative group">
<pre className={className} {...props}>
<code>{children}</code>
</pre>
<button
onClick={() => copyToClipboard(String(children).replace(/\n$/, ''))}
className="absolute top-2 right-2 p-2 bg-gray-700 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity"
title="Copy"
>
{copied ? <CheckCircle className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</button>
</div>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{instructions}
</ReactMarkdown>
) : (
<div className="text-center py-12 text-gray-500">
<AlertCircle className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>Інструкції не знайдено. Зверніться до адміністратора.</p>
</div>
)}
{step.link && (
<a
href={step.link}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
<Download className="w-4 h-4" />
Завантажити Python
</a>
)}
</div>
))}
</div>
</div>
{/* Alternative: One-Click Installer */}
<div className="mt-8 bg-gradient-to-r from-purple-900/30 to-pink-900/30 border border-purple-800/30 rounded-xl p-6">
<h2 className="text-xl font-bold mb-4">⚡ Швидке підключення (Coming Soon)</h2>
<p className="text-slate-400 mb-4">
Скоро буде доступний інсталятор в один клік для автоматичного налаштування ноди
</p>
<button
disabled
className="px-6 py-3 bg-purple-600/50 text-white rounded-lg cursor-not-allowed opacity-50"
>
<Download className="w-4 h-4 inline mr-2" />
Завантажити інсталятор (незабаром)
</button>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Help Section */}
<div className="bg-blue-50 border border-blue-100 rounded-xl p-6">
<h2 className="text-xl font-bold mb-4 text-blue-900 flex items-center gap-2">
<AlertCircle className="w-5 h-5" />
Потрібна допомога?
</h2>
<div className="space-y-3 text-blue-800 text-sm">
<p>Для отримання токенів доступу (NATS credentials) зверніться до адміністраторів:</p>
<a
href="https://matrix.to/#/#daarion:daarion.space"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-3 bg-white rounded-lg hover:shadow-md transition-shadow"
>
<span className="font-semibold">Matrix Chat</span>
<ExternalLink className="w-4 h-4 ml-auto" />
</a>
<a
href="#"
className="flex items-center gap-2 p-3 bg-white rounded-lg hover:shadow-md transition-shadow"
>
<span className="font-semibold">Discord Server</span>
<ExternalLink className="w-4 h-4 ml-auto" />
</a>
</div>
</div>
{/* Help Section */}
<div className="mt-8 bg-slate-900/50 backdrop-blur border border-slate-800 rounded-xl p-6">
<h2 className="text-xl font-bold mb-4">❓ Потрібна допомога?</h2>
<div className="space-y-2 text-slate-400">
<p>• 📚 Документація: <a href="#" className="text-purple-400 hover:underline">docs.dagi.ai</a></p>
<p>• 💬 Telegram спільнота: <a href="#" className="text-purple-400 hover:underline">@dagi_community</a></p>
<p>• 🐛 Проблеми: <a href="#" className="text-purple-400 hover:underline">GitHub Issues</a></p>
{/* System Requirements Summary */}
<div className="bg-white border border-gray-200 rounded-xl p-6">
<h2 className="text-lg font-bold mb-4 text-gray-900">Вимоги</h2>
<div className="space-y-4">
<div className="flex items-center gap-3">
<Cpu className="w-5 h-5 text-gray-400" />
<div>
<div className="text-xs text-gray-500">CPU</div>
<div className="font-medium text-gray-900">4+ Cores</div>
</div>
</div>
<div className="flex items-center gap-3">
<Monitor className="w-5 h-5 text-gray-400" />
<div>
<div className="text-xs text-gray-500">RAM</div>
<div className="font-medium text-gray-900">16GB+ (32GB rec.)</div>
</div>
</div>
<div className="flex items-center gap-3">
<HardDrive className="w-5 h-5 text-gray-400" />
<div>
<div className="text-xs text-gray-500">Storage</div>
<div className="font-medium text-gray-900">100GB+ SSD</div>
</div>
</div>
<div className="flex items-center gap-3">
<Terminal className="w-5 h-5 text-gray-400" />
<div>
<div className="text-xs text-gray-500">OS</div>
<div className="font-medium text-gray-900">Ubuntu 22.04 / Debian</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,7 +1,8 @@
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Server, Activity, Cpu, HardDrive, Network, Users, Settings, BarChart3, Plug, RefreshCw, CheckCircle2, XCircle, AlertCircle, Filter, Play, Loader2, Wrench, Download, Bot, Database, AlertTriangle, PlusCircle } from 'lucide-react';
import { ArrowLeft, Server, Activity, Cpu, HardDrive, Network, Users, Settings, BarChart3, Plug, RefreshCw, CheckCircle2, XCircle, AlertCircle, Filter, Play, Loader2, Wrench, Download, Bot, Database, AlertTriangle, PlusCircle, Boxes, Shield } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { apiGet } from '../api/client';
import { getNode2Agents, type Node2Agent } from '../api/node2Agents';
import { getNode1Agents, type Node1Agent } from '../api/node1Agents';
import { deployAgentToNode2, deployAllAgentsToNode2, checkNode2AgentsDeployment } from '../api/node2Deployment';
@@ -41,6 +42,24 @@ interface NodeDetails {
network_in: number;
network_out: number;
};
microdaos?: Array<{
id: string;
name: string;
slug: string;
role?: string;
}>;
guardian_agent?: {
id: string;
name: string;
slug?: string;
status?: string;
};
steward_agent?: {
id: string;
name: string;
slug?: string;
status?: string;
};
}
// Grafana та Prometheus URLs (налаштувати під ваші сервери)
@@ -103,6 +122,14 @@ export function NodeCabinetPage() {
const agents = agentsData?.items || [];
const isNode1 = nodeId?.includes('node-1');
// Отримуємо профіль з API (для MicroDAOs та агентів)
let apiNodeProfile: any = null;
try {
apiNodeProfile = await apiGet(`/public/nodes/${nodeId}`);
} catch (e) {
console.warn('Failed to fetch node profile from API', e);
}
// Отримуємо реальні дані з інвентаризації
const inv = inventory;
@@ -195,6 +222,9 @@ export function NodeCabinetPage() {
network_in: 1250,
network_out: 890,
},
microdaos: apiNodeProfile?.microdaos || [],
guardian_agent: apiNodeProfile?.guardian_agent,
steward_agent: apiNodeProfile?.steward_agent,
};
},
enabled: !!nodeId,
@@ -551,6 +581,113 @@ export function NodeCabinetPage() {
↓ {nodeDetails.metrics?.network_in || 0} MB/s ↑ {nodeDetails.metrics?.network_out || 0} MB/s
</div>
</div>
</div>
</div>
{/* Core Runtime & Participation */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Core Runtime */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Activity className="w-5 h-5 text-blue-600" />
Core Runtime
</h3>
<div className="space-y-3">
{[
{ name: 'Node Registry', icon: Database },
{ name: 'NATS JetStream', icon: Network },
{ name: 'Swapper Service', icon: RefreshCw },
{ name: 'Ollama', icon: Bot },
].map((service) => {
const s = nodeDetails.services?.find(s => s.name.includes(service.name) || (service.name === 'Ollama' && s.name.includes('ollama')));
const status = s?.status === 'running' ? 'online' : 'offline'; // Simple mapping
const Icon = service.icon;
return (
<div key={service.name} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center gap-3">
<Icon className="w-4 h-4 text-gray-500" />
<span className="font-medium text-gray-700">{service.name}</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${status === 'online' ? 'bg-green-500' : 'bg-gray-400'}`} />
<span className="text-sm text-gray-600">{status === 'online' ? 'Active' : 'Unknown'}</span>
</div>
</div>
);
})}
{/* Guardian & Steward */}
<div className="mt-4 pt-4 border-t border-gray-200 space-y-3">
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg border border-purple-100">
<div className="flex items-center gap-3">
<Shield className="w-4 h-4 text-purple-600" />
<span className="font-medium text-purple-900">Guardian Agent</span>
</div>
<span className="text-sm font-medium text-purple-700">
{nodeDetails.guardian_agent?.name || 'Not active'}
</span>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-center gap-3">
<Users className="w-4 h-4 text-blue-600" />
<span className="font-medium text-blue-900">Steward Agent</span>
</div>
<span className="text-sm font-medium text-blue-700">
{nodeDetails.steward_agent?.name || 'Not active'}
</span>
</div>
</div>
</div>
</div>
{/* Participation */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Boxes className="w-5 h-5 text-green-600" />
Participation
</h3>
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2 uppercase">Hosted MicroDAOs</h4>
{nodeDetails.microdaos && nodeDetails.microdaos.length > 0 ? (
<div className="grid grid-cols-1 gap-2">
{nodeDetails.microdaos.map(dao => (
<div key={dao.id} className="flex items-center justify-between p-3 bg-green-50 border border-green-100 rounded-lg">
<span className="font-medium text-green-900">{dao.name}</span>
<span className="text-xs px-2 py-1 bg-white text-green-700 rounded border border-green-200">
Hosting
</span>
</div>
))}
</div>
) : (
<div className="text-center py-6 bg-gray-50 rounded-lg border border-dashed border-gray-200">
<p className="text-gray-500 text-sm">No MicroDAOs hosted yet</p>
<button
onClick={() => navigate('/microdao')}
className="mt-2 text-sm text-blue-600 hover:underline"
>
Join a MicroDAO
</button>
</div>
)}
</div>
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2 uppercase">Agent Capabilities</h4>
<div className="grid grid-cols-2 gap-2">
<div className="p-3 bg-gray-50 rounded-lg text-center">
<div className="text-xl font-bold text-gray-900">{departments.length}</div>
<div className="text-xs text-gray-500">Teams</div>
</div>
<div className="p-3 bg-gray-50 rounded-lg text-center">
<div className="text-xl font-bold text-gray-900">{nodeDetails.agents?.length || 0}</div>
<div className="text-xs text-gray-500">Agents</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Quick Stats */}

View File

@@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { Server, Activity, Cpu, HardDrive, Network, ArrowRight, RefreshCw, Zap } from 'lucide-react';
import { Server, Activity, Cpu, HardDrive, Network, ArrowRight, RefreshCw, Zap, Plus } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
@@ -216,6 +216,13 @@ export function NodesPage() {
<p className="text-gray-600 mt-2">Управління нодами та моніторинг системи</p>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/connect-node')}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2 shadow-sm"
>
<Plus className="w-4 h-4" />
Підключити ноду
</button>
<button
onClick={fetchAllNodesStatus}
disabled={loading}