feat: add 'Додати ноду' button to Node Directory, create /nodes/register page, add node discovery script

This commit is contained in:
Apple
2025-12-01 06:47:27 -08:00
parent d5aae67b50
commit f5c58358a0
5 changed files with 744 additions and 8 deletions

View File

@@ -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">

View 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>
);
}