Skip to content

# TASK_PHASE_NODE2_ROUTER_SWAPPER_FIX — Router / Swapper / Node Guardian

## Контекст

- У DAARION.city є дві ноди:
- NODE1 — основний сервер (docker-compose, daji-router, swapper-service тощо).

- NODE2 — MacBook Pro M4 Max (`node-2-macbook-m4max`, IP: `192.168.1.33`).

- UI `https://daarion.space/nodes/node/...` показує:

- Для NODE1:
  - Swapper Service: `Degraded`, моделей 0/0, `No models found`.
  - DAGI Router: `Up`, 9 агентів.

- Для NODE2:

  - Swapper Service: `Degraded`, моделей 0/0, `No models found`.
  - DAGI Router: `Down`, `Router недоступний`.

- Cursor раніше змінював `get_node_endpoints` так, що:

- Для NODE1 використовувалися docker URLs (`http://dagi-router:9102`, `http://swapper-service:8890`).
- Для NODE2 — `http://localhost:9102`, `http://localhost:8890`, з визначенням по `node_id`.

- Це працює локально на Mac, але в прод-оточенні `city-service` крутиться в docker на NODE1, і `localhost` для нього — це контейнер, а не MacBook або DAGI Router.

- Стани в UI беруться не напряму з Router/Swapper, а з таблиці `node_cache` (метрики, які пушить `node-guardian`).

## Ціль

1. Стандартизувати визначення endpoint'ів для DAGI Router і Swapper так, щоб:
2. У PROD усе працювало через один базовий URL (docker-hostи `dagi-router` / `swapper-service`).
3. Не було прив'язки до `node_id` для вибору URL.

4. У DEV/локально на Mac можна було використовувати `localhost` через ENV.

5. Переконатися, що `node-guardian` для NODE2 коректно оновлює `node_cache`:

6. Є записи з `node_id = 'node-2-macbook-m4max'` для `router_healthy` і `swapper_state`.

7. Помилки логуються явно, а не тихо ковтаються.

8. Виправити відображення моделей у Swapper Service:

9. Якщо в Swapper реально є моделі, UI має показувати їх (назву, статус, тип, тощо).

10. Якщо моделей немає/немає зв'язку — показувати чесний `Degraded` з fallback, але не плутати це з "0/0 при наявних моделях".

11. Мінімізувати магію та дублювання логіки: конфіг через ENV, один контракт між `city-service`, `node-guardian` і `DAGI Router / Swapper`.

## Архітектурні принципи

- Один DAGI Router + один Swapper в проді (на NODE1) обслуговує всі ноди.
- `city-service` завжди ходить до Router/Swapper через базовий URL з ENV, без умов по `node_id`.
- `node-guardian`:
- Викликає Router/Swapper.
- Перетворює результати в метрики `node_cache` (`router_healthy`, `swapper_state`, можливо інші).
- Маркує ці записи конкретним `node_id`, що відповідає ноді, де стоїть guardian.

## Завдання

### 1. Нормалізувати `get_node_endpoints` у `services/city-service/repo_city.py`

1. Знайти реалізацію `get_node_endpoints`.
2. Прибрати логіку, яка підміняє URL на `localhost` на основі `node_id` (наприклад, `if "node-2" in node_id` тощо).
3. Замість цього:
4. Винести базові URL у ENV, наприклад:
   - `ROUTER_BASE_URL` (наприклад, `http://dagi-router:9102` у проді).
   - `SWAPPER_BASE_URL` (наприклад, `http://swapper-service:8890` у проді).
5. Для DEV (локальний запуск на Mac без Docker) дозволити дефолт:
   - `ROUTER_BASE_URL=http://localhost:9102`
   - `SWAPPER_BASE_URL=http://localhost:8890`
6. `get_node_endpoints(node)` має повертати структуру типу:

`python return NodeEndpoints( router_base=f"{ROUTER_BASE_URL}", swapper_base=f"{SWAPPER_BASE_URL}", # за потреби — окремі health / metrics / models endpoints )`

1. Не прив'язувати URL до `node_id`. Вся різниця між нодами має відображатись у:
2. `node_cache` (метрики),
3. БД агентів (який агент до якої ноди прив'язаний).

### 2. Виправити `get_node_swapper_detail` у `services/city-service/routes_city.py`

1. Переконатися, що endpoint `GET /api/v1/nodes/{node_id}/swapper`:
2. Використовує `get_node_endpoints` для звернення до Swapper.
3. Коректно обробляє:
   - HTTP 200 з валідною відповіддю `/api/v1/models`.
   - HTTP 5xx / timeout / connection error.
4. Логіка:
5. Якщо відповідь успішна — парсити список моделей:
   - Назва моделі.
   - Статус (loaded/failed/loading).
   - Кількість інстансів, GPU/CPU тощо (як дозволяє API Swapper).
6. Оновлювати / читати кеш (`node_cache`) так, щоб UI міг показувати:
   - Загальну кількість моделей.
   - Loaded / Failed / Pending.
7. Якщо помилка або моделі не повертаються:
   - Повернути `status: "degraded"` та `models: []`, а НЕ 404.
8. Гарантувати, що UI завжди отримує валідний JSON:
9. навіть якщо Swapper мертвий,
10. без сирих трас і HTML помилок.

### 3. Перевірити та поправити `node-guardian` (особливо для NODE2)

1. Знайти код `node-guardian` (швидше за все окремий сервіс / скрипт).
2. Переконатися, що він:
3. Читає ENV:
   - `NODE_ID` (для NODE2: `node-2-macbook-m4max`).
   - `CITY_API_URL` (HTTPS URL до city-service).
4. Периодично:
   - Викликає Router health endpoint (через `ROUTER_BASE_URL` або відповідний URL з ENV).
   - Викликає Swapper `/api/v1/models` або health endpoint.
   - Пушить у `node_cache` записи:
   - `router_healthy` з payload (`{"ok": true/false, "latency_ms": ...}`).
   - `swapper_state` з payload (`{"models_total": X, "models_loaded": Y, "models_failed": Z, "raw": ...}`).
5. Додати нормальні лог-меседжі:
6. На успішне оновлення.
7. На помилки (HTTP статус, текст помилки).
8. Перевірити, що в БД (таблиця `node_cache`):
9. Після запуску guardian на NODE2 з'являються рядки з `node_id = 'node-2-macbook-m4max'` для `router_healthy` і `swapper_state`.

### 4. Сумісність з наявними фільтрами по `node_id`

1. Знайти всі місця, де читається `node_cache` для вузла:
2. Наприклад, `get_node_status`, `get_node_swapper_detail`, `get_node_router_detail` тощо.
3. Переконатися, що фільтрація відбувається по `node_id` + `kind`:
4. `WHERE node_id = :node_id AND kind = :kind`
5. Не використовувати глобальний `swapper_state` без `node_id`, якщо вже перейшли на модель "по нодах".
6. Якщо історично був один глобальний запис без `node_id`:
7. Міграція (якщо потрібно) — або прибрати цей запис, або задовольнитися тим, що UI читає тільки записи з конкретним `node_id`.

### 5. Swapper models → UI

1. Забезпечити, щоб бекенд повертав у UI-модель (DTO) для Swapper дані:
2. `status` (`"ok" | "degraded" | "down"`).
3. `models_total`.
4. `models_loaded`.
5. `models_failed`.
6. `models` (масив з короткою інформацією по кожній моделі).
7. Перевірити, що фронт (Node detail page) читає ці поля й не падає, якщо масив `models` порожній.
8. Якщо потрібен stub/fallback — він має відрізнятись від реального "0 моделей при піднятому Swapper'і".

## Acceptance Criteria

1. Endpoint конфіг:
2. У `.env` / docker-compose є:
   - `ROUTER_BASE_URL` (у проді → `http://dagi-router:9102`).
   - `SWAPPER_BASE_URL` (у проді → `http://swapper-service:8890`).
3. `get_node_endpoints` не використовує `node_id` для визначення URL.

4. У DEV-режимі локальний запуск на Mac використовує `localhost:9102/8890`.

5. NODE2 в UI:

6. На сторінці НОДА2:

   - DAGI Router:
   - Показує реальний статус (`Up/Down`) на основі `router_healthy` з `node_cache`.
   - При живому Router статус `Up` без "Router недоступний".
   - Swapper Service:
   - Показує реальну кількість моделей (якщо вони є у Swapper).
   - При проблемах — `Degraded`, але без 404/порожніх екранiв.

7. Node Guardian:

8. Guardian на НОДА2 працює, логі показують регулярні оновлення.

9. У Postgres (таблиця `node_cache`) є останні записи:

   - `node_id = 'node-2-macbook-m4max'`, `kind = 'router_healthy'`.
   - `node_id = 'node-2-macbook-m4max'`, `kind = 'swapper_state'`.

10. Swapper models:

11. `curl <SWAPPER_BASE_URL>/api/v1/models` повертає список моделей.
12. UI Swapper Service для NODE1 показує ці моделі в таблиці (а не лише `0/0`).

13. При зупиненому Swapper:

    - UI показує `Degraded` або `Down`, але бекенд повертає валідний JSON з fallback.

14. Без регресій:

15. NODE1 продовжує показувати 9 агентів у DAGI Router.
16. Інші частини `nodes` UI працюють як раніше (агенти, статуси, Node Guardian & Steward секція).

## Пріоритет

- Високий. Це критична частина UX для нод та діагностики стану DAGI в DAARION.city.