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

@@ -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 показує тільки реальні метрики й реальних агентів, тестові не потрапляють у кабінети.

View 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).