# 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 так, щоб: - У PROD усе працювало через один базовий URL (docker-hostи `dagi-router` / `swapper-service`). - Не було прив'язки до `node_id` для вибору URL. - У DEV/локально на Mac можна було використовувати `localhost` через ENV. 2. Переконатися, що `node-guardian` для NODE2 коректно оновлює `node_cache`: - Є записи з `node_id = 'node-2-macbook-m4max'` для `router_healthy` і `swapper_state`. - Помилки логуються явно, а не тихо ковтаються. 3. Виправити відображення моделей у Swapper Service: - Якщо в Swapper реально є моделі, UI має показувати їх (назву, статус, тип, тощо). - Якщо моделей немає/немає зв'язку — показувати чесний `Degraded` з fallback, але не плутати це з "0/0 при наявних моделях". 4. Мінімізувати магію та дублювання логіки: конфіг через 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. Замість цього: - Винести базові URL у ENV, наприклад: - `ROUTER_BASE_URL` (наприклад, `http://dagi-router:9102` у проді). - `SWAPPER_BASE_URL` (наприклад, `http://swapper-service:8890` у проді). - Для DEV (локальний запуск на Mac без Docker) дозволити дефолт: - `ROUTER_BASE_URL=http://localhost:9102` - `SWAPPER_BASE_URL=http://localhost:8890` 4. `get_node_endpoints(node)` має повертати структуру типу: ```python return NodeEndpoints( router_base=f"{ROUTER_BASE_URL}", swapper_base=f"{SWAPPER_BASE_URL}", # за потреби — окремі health / metrics / models endpoints ) ``` 5. Не прив'язувати URL до `node_id`. Вся різниця між нодами має відображатись у: * `node_cache` (метрики), * БД агентів (який агент до якої ноди прив'язаний). ### 2. Виправити `get_node_swapper_detail` у `services/city-service/routes_city.py` 1. Переконатися, що endpoint `GET /api/v1/nodes/{node_id}/swapper`: * Використовує `get_node_endpoints` для звернення до Swapper. * Коректно обробляє: * HTTP 200 з валідною відповіддю `/api/v1/models`. * HTTP 5xx / timeout / connection error. 2. Логіка: * Якщо відповідь успішна — парсити список моделей: * Назва моделі. * Статус (loaded/failed/loading). * Кількість інстансів, GPU/CPU тощо (як дозволяє API Swapper). * Оновлювати / читати кеш (`node_cache`) так, щоб UI міг показувати: * Загальну кількість моделей. * Loaded / Failed / Pending. * Якщо помилка або моделі не повертаються: * Повернути `status: "degraded"` та `models: []`, а НЕ 404. 3. Гарантувати, що UI завжди отримує валідний JSON: * навіть якщо Swapper мертвий, * без сирих трас і HTML помилок. ### 3. Перевірити та поправити `node-guardian` (особливо для NODE2) 1. Знайти код `node-guardian` (швидше за все окремий сервіс / скрипт). 2. Переконатися, що він: * Читає ENV: * `NODE_ID` (для NODE2: `node-2-macbook-m4max`). * `CITY_API_URL` (HTTPS URL до city-service). * Периодично: * Викликає 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": ...}`). 3. Додати нормальні лог-меседжі: * На успішне оновлення. * На помилки (HTTP статус, текст помилки). 4. Перевірити, що в БД (таблиця `node_cache`): * Після запуску guardian на NODE2 з'являються рядки з `node_id = 'node-2-macbook-m4max'` для `router_healthy` і `swapper_state`. ### 4. Сумісність з наявними фільтрами по `node_id` 1. Знайти всі місця, де читається `node_cache` для вузла: * Наприклад, `get_node_status`, `get_node_swapper_detail`, `get_node_router_detail` тощо. 2. Переконатися, що фільтрація відбувається по `node_id` + `kind`: * `WHERE node_id = :node_id AND kind = :kind` 3. Не використовувати глобальний `swapper_state` без `node_id`, якщо вже перейшли на модель "по нодах". 4. Якщо історично був один глобальний запис без `node_id`: * Міграція (якщо потрібно) — або прибрати цей запис, або задовольнитися тим, що UI читає тільки записи з конкретним `node_id`. ### 5. Swapper models → UI 1. Забезпечити, щоб бекенд повертав у UI-модель (DTO) для Swapper дані: * `status` (`"ok" | "degraded" | "down"`). * `models_total`. * `models_loaded`. * `models_failed`. * `models` (масив з короткою інформацією по кожній моделі). 2. Перевірити, що фронт (Node detail page) читає ці поля й не падає, якщо масив `models` порожній. 3. Якщо потрібен stub/fallback — він має відрізнятись від реального "0 моделей при піднятому Swapper'і". --- ## Acceptance Criteria 1. **Endpoint конфіг**: * У `.env` / docker-compose є: * `ROUTER_BASE_URL` (у проді → `http://dagi-router:9102`). * `SWAPPER_BASE_URL` (у проді → `http://swapper-service:8890`). * `get_node_endpoints` не використовує `node_id` для визначення URL. * У DEV-режимі локальний запуск на Mac використовує `localhost:9102/8890`. 2. **NODE2 в UI**: * На сторінці НОДА2: * DAGI Router: * Показує реальний статус (`Up/Down`) на основі `router_healthy` з `node_cache`. * При живому Router статус `Up` без "Router недоступний". * Swapper Service: * Показує реальну кількість моделей (якщо вони є у Swapper). * При проблемах — `Degraded`, але без 404/порожніх екранiв. 3. **Node Guardian**: * Guardian на НОДА2 працює, логі показують регулярні оновлення. * У Postgres (таблиця `node_cache`) є останні записи: * `node_id = 'node-2-macbook-m4max'`, `kind = 'router_healthy'`. * `node_id = 'node-2-macbook-m4max'`, `kind = 'swapper_state'`. 4. **Swapper models**: * `curl /api/v1/models` повертає список моделей. * UI Swapper Service для NODE1 показує ці моделі в таблиці (а не лише `0/0`). * При зупиненому Swapper: * UI показує `Degraded` або `Down`, але бекенд повертає валідний JSON з fallback. 5. **Без регресій**: * NODE1 продовжує показувати 9 агентів у DAGI Router. * Інші частини `nodes` UI працюють як раніше (агенти, статуси, Node Guardian & Steward секція). --- ## Пріоритет * Високий. Це критична частина UX для нод та діагностики стану DAGI в DAARION.city.