feat: add 'Додати ноду' button to Node Directory, create /nodes/register page, add node discovery script
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Server, Cpu, Users, Activity, ExternalLink, Zap, HardDrive, MemoryStick } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Server, Cpu, Users, Activity, ExternalLink, Zap, HardDrive, MemoryStick, Plus } from 'lucide-react';
|
||||
import { useNodeList } from '@/hooks/useNodes';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { NodeProfile } from '@/lib/types/nodes';
|
||||
|
||||
function getNodeLabel(nodeId: string): string {
|
||||
@@ -170,18 +172,39 @@ function NodeCard({ node }: { node: NodeProfile }) {
|
||||
}
|
||||
|
||||
export default function NodesPage() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { nodes, total, isLoading, error } = useNodeList();
|
||||
|
||||
// Check if user can add nodes (admin or orchestrator)
|
||||
const canAddNode = user && (
|
||||
user.roles?.includes('admin') ||
|
||||
user.roles?.includes('orchestrator') ||
|
||||
user.roles?.includes('is_admin') ||
|
||||
user.roles?.includes('is_orchestrator')
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-900 to-slate-950">
|
||||
<div className="max-w-7xl mx-auto px-4 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Server className="w-8 h-8 text-purple-400" />
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
Node Directory
|
||||
</h1>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="w-8 h-8 text-purple-400" />
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
Node Directory
|
||||
</h1>
|
||||
</div>
|
||||
{canAddNode && (
|
||||
<button
|
||||
onClick={() => router.push('/nodes/register')}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 rounded-xl text-white font-medium transition-all shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Додати ноду
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-white/60 text-lg">
|
||||
Всі ноди мережі DAARION
|
||||
@@ -237,9 +260,18 @@ export default function NodesPage() {
|
||||
<h2 className="text-xl font-semibold text-white mb-2">
|
||||
Ноди не знайдені
|
||||
</h2>
|
||||
<p className="text-white/50">
|
||||
Наразі немає зареєстрованих нод.
|
||||
<p className="text-white/50 mb-6">
|
||||
Наразі немає жодної зареєстрованої ноди.
|
||||
</p>
|
||||
{canAddNode && (
|
||||
<button
|
||||
onClick={() => router.push('/nodes/register')}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 rounded-xl text-white font-medium transition-all shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Додати першу ноду
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
159
apps/web/src/app/nodes/register/page.tsx
Normal file
159
apps/web/src/app/nodes/register/page.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Server, ChevronLeft, Terminal, CheckCircle2, Copy, ExternalLink } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function NodeRegisterPage() {
|
||||
const router = useRouter();
|
||||
const [copiedStep, setCopiedStep] = useState<number | null>(null);
|
||||
|
||||
const copyToClipboard = (text: string, step: number) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopiedStep(step);
|
||||
setTimeout(() => setCopiedStep(null), 2000);
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: 'Клонуйте репозиторій',
|
||||
code: 'git clone https://github.com/IvanTytar/microdao-daarion.git',
|
||||
description: 'Завантажте код проєкту на вашу машину'
|
||||
},
|
||||
{
|
||||
title: 'Налаштуйте змінні оточення',
|
||||
code: `export NODE_ID="your-node-id"
|
||||
export CITY_API_URL="https://daarion.space/api"
|
||||
export SWAPPER_URL="http://localhost:8890"`,
|
||||
description: 'Встановіть унікальний ID для вашої ноди'
|
||||
},
|
||||
{
|
||||
title: 'Запустіть Node Guardian',
|
||||
code: 'python3 scripts/node-guardian-loop.py --node-id=$NODE_ID --city-url=$CITY_API_URL',
|
||||
description: 'Скрипт почне надсилати heartbeat до DAARION.city'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-900 to-slate-950">
|
||||
<div className="max-w-4xl mx-auto px-4 py-12">
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={() => router.push('/nodes')}
|
||||
className="flex items-center gap-2 text-white/60 hover:text-white mb-8 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
Назад до Node Directory
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-purple-500/20 to-pink-500/20 flex items-center justify-center">
|
||||
<Server className="w-8 h-8 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
Додати ноду
|
||||
</h1>
|
||||
<p className="text-white/60 mt-1">
|
||||
Підключіть свій сервер або комп'ютер до мережі DAARION
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 mb-8">
|
||||
<h2 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5 text-cyan-400" />
|
||||
Як це працює
|
||||
</h2>
|
||||
<p className="text-white/70 leading-relaxed">
|
||||
Щоб нода з'явилась у каталозі, потрібно встановити <span className="text-cyan-400 font-medium">Node Guardian</span> на
|
||||
вашому сервері або ноутбуці. Скрипт буде автоматично надсилати heartbeat до DAARION.city,
|
||||
і ваша нода з'явиться в каталозі.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div className="space-y-6 mb-12">
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center shrink-0">
|
||||
<span className="text-purple-400 font-semibold">{index + 1}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-white font-semibold mb-1">{step.title}</h3>
|
||||
<p className="text-white/50 text-sm mb-3">{step.description}</p>
|
||||
<div className="relative">
|
||||
<pre className="bg-slate-800/50 rounded-xl p-4 text-sm text-cyan-300 font-mono overflow-x-auto">
|
||||
{step.code}
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => copyToClipboard(step.code, index)}
|
||||
className="absolute top-2 right-2 p-2 rounded-lg bg-slate-700/50 hover:bg-slate-700 transition-colors"
|
||||
title="Копіювати"
|
||||
>
|
||||
{copiedStep === index ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-white/60" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Requirements */}
|
||||
<div className="bg-amber-500/10 border border-amber-500/30 rounded-2xl p-6 mb-8">
|
||||
<h3 className="text-amber-400 font-semibold mb-3">Вимоги</h3>
|
||||
<ul className="space-y-2 text-white/70">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-amber-400" />
|
||||
Python 3.9+ з встановленим httpx
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-amber-400" />
|
||||
Доступ до інтернету для надсилання heartbeat
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-amber-400" />
|
||||
(Опціонально) Swapper service для AI моделей
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<a
|
||||
href="https://github.com/IvanTytar/microdao-daarion/blob/main/docs/NODE_SETUP.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 rounded-lg text-purple-400 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Повна документація
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/IvanTytar/microdao-daarion/tree/main/scripts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 rounded-lg text-cyan-400 transition-colors"
|
||||
>
|
||||
<Terminal className="w-4 h-4" />
|
||||
Скрипти на GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user