feat: add 'Додати ноду' button to Node Directory, create /nodes/register page, add node discovery script
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
# TASK_PHASE_NODE2_ROUTER_SWAPPER_ISOLATION_AND_AGENT_DISCOVERY_v1
|
||||
|
||||
Мета:
|
||||
1) гарантувати, що НОДА2 використовує свій локальний Swapper та DAGI Router для метрик;
|
||||
2) зібрати повний перелік моделей і агентів на обох нодах, розділити «бойових» і тестових;
|
||||
3) зробити так, щоб у MVP показувались тільки реальні метрики та реальні агенти.
|
||||
|
||||
---
|
||||
|
||||
## 0. Симптоми / проблеми
|
||||
|
||||
- Swapper на NODE2 показує ті ж моделі, що на NODE1 (дані «змішані»).
|
||||
- На NODE2 є реальні моделі (більші, ніж на NODE1) і створені агенти core-команди, але:
|
||||
- частина з них періодично зникає з UI;
|
||||
- зʼявляються старі тестові агенти.
|
||||
- Очевидно, що:
|
||||
- або `node-guardian-loop` на NODE2 стукає в Swapper/Router NODE1;
|
||||
- або `fn_node_heartbeat` / `node_cache` перезаписують дані між нодами;
|
||||
- або маршрути `/api/node-internal/{nodeId}/...` не ізолюють nodeId.
|
||||
|
||||
---
|
||||
|
||||
## 1. Інфра: повна ізоляція NODE2
|
||||
|
||||
### 1.1. Перевірити ENV та конфіг
|
||||
|
||||
На NODE2 знайти, чим стартує `node-guardian-loop.py` і DAGI Router / Swapper:
|
||||
|
||||
- Перевірити змінні оточення:
|
||||
|
||||
```bash
|
||||
echo $NODE_ID
|
||||
echo $SWAPPER_BASE_URL
|
||||
echo $DAGI_ROUTER_URL
|
||||
echo $CITY_API_BASE_URL
|
||||
```
|
||||
|
||||
- Встановити для NODE2:
|
||||
|
||||
```env
|
||||
NODE_ID=node-2-macbook-m4max
|
||||
SWAPPER_BASE_URL=http://127.0.0.1:8890 # локальний Swapper NODE2
|
||||
DAGI_ROUTER_URL=http://127.0.0.1:9102 # локальний DAGI Router NODE2
|
||||
CITY_API_BASE_URL=https://daarion.space/api/node-internal
|
||||
```
|
||||
|
||||
### 1.2. Оновити node-guardian-loop.py
|
||||
|
||||
Переконатись, що скрипт використовує **тільки** ENV, а не захардкожені URL:
|
||||
|
||||
```python
|
||||
NODE_ID = os.environ["NODE_ID"]
|
||||
SWAPPER_BASE_URL = os.environ["SWAPPER_BASE_URL"]
|
||||
DAGI_ROUTER_URL = os.environ.get("DAGI_ROUTER_URL")
|
||||
CITY_API_BASE_URL = os.environ["CITY_API_BASE_URL"]
|
||||
```
|
||||
|
||||
- Для Swapper: використовувати `SWAPPER_BASE_URL` (не `http://swapper-service:8890` в коді).
|
||||
- Для Router health (якщо є): використовувати `DAGI_ROUTER_URL`.
|
||||
|
||||
### 1.3. fn_node_heartbeat / node_cache
|
||||
|
||||
Перевірити SQL-функцію `fn_node_heartbeat`:
|
||||
|
||||
- Переконатися, що `update node_cache set ... where node_id = _node_id` (або аналог) і **не** перезаписує рядок іншої ноди.
|
||||
- Додати короткий тест або скрипт, який:
|
||||
- викликає `fn_node_heartbeat('node-1-...', ...)`;
|
||||
- викликає `fn_node_heartbeat('node-2-...', ...)`;
|
||||
- показує, що в `node_cache` два різні рядки з різними `swapper_state`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Discovery: повний список моделей і агентів по нодах
|
||||
|
||||
### 2.1. Скрипт `scripts/discover_node_state.py`
|
||||
|
||||
Створити Python-скрипт, який:
|
||||
|
||||
- Приймає параметр `--node node-1-...` або `--node node-2-...` або `--all`.
|
||||
|
||||
- Для кожної ноди:
|
||||
|
||||
1. Читає `node_cache`:
|
||||
|
||||
```sql
|
||||
select * from node_cache where node_id = :node_id;
|
||||
```
|
||||
|
||||
2. Викликає internal API:
|
||||
|
||||
- `/internal/node/{node_id}/swapper` → реальні моделі.
|
||||
- `/internal/node/{node_id}/dagi-router/agents` → агенти Router vs DB.
|
||||
|
||||
3. Читає таблицю `agents`:
|
||||
|
||||
```sql
|
||||
select id, public_slug, kind, is_test, node_id, created_at, updated_at
|
||||
from agents
|
||||
where node_id = :node_id
|
||||
order by created_at;
|
||||
```
|
||||
|
||||
- Результат записує у:
|
||||
|
||||
- `docs/users/nodes/NODE_STATE_{node_id}.md`
|
||||
|
||||
Формат:
|
||||
|
||||
```md
|
||||
# Node {node_id} — State
|
||||
|
||||
## Swapper
|
||||
|
||||
- healthy: true
|
||||
- models_loaded: X / Y
|
||||
|
||||
| name | type | loaded | source |
|
||||
|------|------|--------|--------|
|
||||
| qwen2.5-7b-instruct | llm | true | swapper |
|
||||
|
||||
## DAGI Router Agents
|
||||
|
||||
| id | status | has_db_record | is_test | last_seen_at |
|
||||
|----|--------|---------------|---------|--------------|
|
||||
| daarwizz | active | true | false | ... |
|
||||
|
||||
## DB Agents (by node_id)
|
||||
|
||||
| id | kind | is_test | public_slug |
|
||||
|----|------|---------|-------------|
|
||||
| ... |
|
||||
```
|
||||
|
||||
Це стає «правдою» по реальному стану нод.
|
||||
|
||||
---
|
||||
|
||||
## 3. Прибирання тестових агентів і відфільтровування метрик
|
||||
|
||||
### 3.1. Маркування тестових агентів
|
||||
|
||||
- У таблиці `agents` вже є або додати поле `is_test boolean default false`.
|
||||
- Міграція: позначити всі старі тестові агенти:
|
||||
- за pattern'ами в `id` / `name` (`test_`, `demo_`, `sandbox_` і т.д.);
|
||||
- або за списком, який уже є в документах (знайти в `docs/`).
|
||||
|
||||
### 3.2. Фільтрація на рівні API
|
||||
|
||||
- Для всіх endpointів, які використовуються в MVP (Node Cabinet, Agent Directory, DAGI Router Card):
|
||||
- повертати **тільки** `is_test = false` за замовчуванням;
|
||||
- додати параметр `include_test=true` тільки для внутрішніх / debug API.
|
||||
|
||||
Наприклад, у `get_dagi_router_agents_for_node`:
|
||||
|
||||
- при формуванні результату **не включати** `is_test=true` у таблицю для MVP.
|
||||
|
||||
### 3.3. Очищення «зомбі»-агентів
|
||||
|
||||
- Окрема міграція/скрипт, який:
|
||||
- або архівує тестових агентів (`archived_at`),
|
||||
- або видаляє їх,
|
||||
- або переносить у окрему microDAO «Sandbox».
|
||||
|
||||
Головне — щоб вони не потрапляли в `router_total`/`system_total` метрик MVP.
|
||||
|
||||
---
|
||||
|
||||
## 4. Перевірка після ізоляції
|
||||
|
||||
Після оновлення:
|
||||
|
||||
1. Запустити `scripts/discover_node_state.py --all` і переглянути два файли:
|
||||
- `NODE_STATE_node-1-hetzner-gex44.md`
|
||||
- `NODE_STATE_node-2-macbook-m4max.md`
|
||||
|
||||
2. Переконатися, що:
|
||||
- Swapper-моделі для NODE1 і NODE2 **різні**, відповідають реальним встановленим моделям на кожній ноді.
|
||||
- Список агентів DAGI Router для NODE2 показує саме тих core-агентів, що були створені для NODE2.
|
||||
|
||||
3. UI:
|
||||
- `/nodes/node-1-hetzner-gex44` → Swapper + DAGI Router + агенти відповідають файлу NODE_STATE_NODE1.
|
||||
- `/nodes/node-2-macbook-m4max` → Swapper + DAGI Router + агенти відповідають файлу NODE_STATE_NODE2.
|
||||
- Тестові агенти в UI не відображаються (тільки бойові).
|
||||
|
||||
---
|
||||
|
||||
## 5. Результат
|
||||
|
||||
- НОДА1 і НОДА2 повністю ізольовані з точки зору Swapper/DAGI Router.
|
||||
- У нас є документована «інвентаризація» моделей і агентів по нодах.
|
||||
- MVP показує тільки реальні метрики й реальних агентів, тестові не потрапляють у кабінети.
|
||||
|
||||
106
docs/tasks/TASK_PHASE_NODE_DIRECTORY_ADD_NODE_CTA_v1.md
Normal file
106
docs/tasks/TASK_PHASE_NODE_DIRECTORY_ADD_NODE_CTA_v1.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# TASK_PHASE_NODE_DIRECTORY_ADD_NODE_CTA_v1
|
||||
|
||||
Мета: повернути та зафіксувати кнопку «Додати ноду» у Node Directory, щоб можна було запускати процес реєстрації нової ноди з інтерфейсу.
|
||||
|
||||
---
|
||||
|
||||
## 0. Поточний стан
|
||||
|
||||
- Сторінка `/nodes` (Node Directory) відображає:
|
||||
- Production / Development фільтри.
|
||||
- Список нод або empty state «Наразі немає зареєстрованих нод».
|
||||
- Раніше була кнопка «Додати ноду», але після рефакторингів зникла.
|
||||
|
||||
---
|
||||
|
||||
## 1. UX / UI вимоги
|
||||
|
||||
### 1.1. Розміщення
|
||||
|
||||
- Сторінка `/nodes`:
|
||||
- У верхній частині (праворуч від заголовка «Node Directory» або під фільтрами) додати кнопку:
|
||||
- Текст: **«Додати ноду»**
|
||||
- Іконка: server / plus (та сама стилістика, що й у кнопки «Створити MicroDAO»).
|
||||
|
||||
### 1.2. Поведінка
|
||||
|
||||
- Натискання → `router.push("/nodes/register")`.
|
||||
- Якщо `/nodes/register` ще не існує:
|
||||
- Створити просту заглушку-сторінку з текстом:
|
||||
- «Реєстрація нової ноди DAARION. На цьому етапі нода реєструється через встановлення Node Agent і heartbeat. Перегляньте інструкцію в docs/NODE_SETUP.md».
|
||||
- Кнопка показується тільки для авторизованих користувачів із ролями:
|
||||
- `is_admin` або `is_orchestrator` (перевірити існуючу систему ролей і використати наявний хук, наприклад `useCurrentUser()`).
|
||||
|
||||
### 1.3. Empty state
|
||||
|
||||
- Якщо `nodes.length === 0`:
|
||||
- Замість просто «Ноди не знайдені» показати:
|
||||
- Текст: «Наразі немає жодної зареєстрованої ноди»;
|
||||
- Під текстом — кнопку **«Додати першу ноду»** з таким самим handler'ом (`/nodes/register`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Технічна реалізація
|
||||
|
||||
### 2.1. Node Directory page
|
||||
|
||||
Файл (ймовірно): `apps/web/src/app/nodes/page.tsx`.
|
||||
|
||||
Зробити:
|
||||
|
||||
- Імпортувати кнопку (Button component) та `useRouter`.
|
||||
- Додати кнопку в header:
|
||||
|
||||
```tsx
|
||||
const router = useRouter();
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => router.push("/nodes/register")}
|
||||
>
|
||||
Додати ноду
|
||||
</Button>
|
||||
```
|
||||
|
||||
- Обгорнути рендер у перевірку ролі користувача.
|
||||
|
||||
### 2.2. Сторінка `/nodes/register`
|
||||
|
||||
Файл: `apps/web/src/app/nodes/register/page.tsx`.
|
||||
|
||||
Мінімум:
|
||||
|
||||
```tsx
|
||||
export default function NodeRegisterPage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-semibold">Додати ноду</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Щоб нода зʼявилась у каталозі, встановіть Node Agent на сервері / ноутбуці
|
||||
і переконайтесь, що heartbeat надсилається до DAARION.city.
|
||||
</p>
|
||||
<a
|
||||
href="https://daarion.space/docs/NODE_SETUP"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Інструкція з встановлення ноди
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
(Якщо є реальний шлях до docs — використати його.)
|
||||
|
||||
---
|
||||
|
||||
## 3. Acceptance Criteria
|
||||
|
||||
1. На `/nodes`:
|
||||
- У header видно кнопку «Додати ноду».
|
||||
- При кліку відкривається `/nodes/register`.
|
||||
2. У випадку 0 нод:
|
||||
- Empty state містить кнопку «Додати першу ноду».
|
||||
3. Кнопка **не** показується гостям та користувачам без прав (тільки admin/orchestrator).
|
||||
4. Після деплою кнопка не зникає при наступних оновленнях (перевірити в `check-deploy-post.py`, за бажанням, що `/nodes` повертає 200).
|
||||
|
||||
Reference in New Issue
Block a user