Skip to content

# TASK_PHASE_SWAPPER_NODE_METRICS_AND_UI_v1

Проєкт: DAARION.city — Swapper Service / Node Cabinet
Фаза: Метрики Swapper + відображення у Кабінеті Ноди
Мета: зробити так, щоб Swapper був «першокласним» сервісом у нодовій архітектурі: - node-guardian-loop збирає метрики Swapper; - ці метрики зберігаються в node_cache / внутрішньому API; - у Кабінеті Ноди з’являється блок Swapper (статус + моделі); - (опційно) Swapper Agent використовує ці дані для self-healing.

## 0. Поточний стан

- Heartbeat нод живий, node-guardian-loop запущений на NODE1 і NODE2.
- Node metrics (`/internal/node/{id}/metrics/current`) оновлюються.
- DAGI Router UI більше не дає «Unknown error» (оновлений URL).
- Swapper Service працює, але:
- не збираються і не передаються метрики Swapper;
- Кабінет Ноди не відображає стан Swapper;
- Swapper Agent поки не має даних для діагностики.

## 1. Scope

### Включено

1. Збір метрик Swapper у node-guardian-loop.
2. Розширення node_cache / node metrics API полями для Swapper.
3. Новий internal endpoint для детальнішої інформації про Swapper:
4. списки моделей / базові показники.
5. Оновлення Кабінету Ноди:
6. блок «Swapper Service» зі статусом і основною інформацією.
7. Базові тести (backend + frontend).

### Виключено

- Повний self-healing Swapper Agent (рестарти, pull моделей) — це окрема фаза (service agents).
- Розгорнуті Charts по Swapper (latency history) — можна додати пізніше.

## 2. Swapper API — припущення

Для MVP вважаємо:

- Swapper HTTP base URL (з кабінетів/compose):
  `http://swapper-service:8890`
- Корисні endpoints:
- `GET /healthz` — здоров’я Swapper.
- `GET /v1/models` — список моделей, щось на кшталт: `json { "models": [ { "name": "model-a", "loaded": true, "type": "llm", "vram_gb": 8.0 }, { "name": "model-b", "loaded": false, "type": "vlm" } ] }`

Якщо формат інший — адаптувати, але інтерфейс на стороні city-service робити уніфікованим.

## 3. Backend: node-guardian-loop → Swapper метрики

### 3.1. Розширити node_cache (якщо потрібно)

Перевірити існуючу структуру `node_cache`. Якщо полів для Swapper немає — додати міграцію, наприклад:

    alter table node_cache
      add column swapper_healthy boolean,
      add column swapper_models_loaded integer,
      add column swapper_models_total integer;

Міграція: `migrations/039_node_cache_swapper_metrics.sql`.

### 3.2. Оновити node-guardian-loop

У `node-guardian-loop.py` (або відповідному worker):

1. Додати функцію для збору Swapper-метрик:

    import requests

    def collect_swapper_metrics(swapper_base_url: str) -> dict:
        result = {
            "swapper_healthy": False,
            "swapper_models_loaded": None,
            "swapper_models_total": None,
        }
        try:
            # healthz
            r = requests.get(f"{swapper_base_url}/healthz", timeout=3)
            result["swapper_healthy"] = (r.status_code == 200)
        except Exception:
            result["swapper_healthy"] = False

        try:
            r = requests.get(f"{swapper_base_url}/v1/models", timeout=5)
            if r.status_code == 200:
                data = r.json()
                models = data.get("models", [])
                total = len(models)
                loaded = sum(1 for m in models if m.get("loaded") is True)
                result["swapper_models_total"] = total
                result["swapper_models_loaded"] = loaded
        except Exception:
            # залишаємо None → UI покаже "невідомо"
            pass

        return result

1. У циклі node-guardian-loop (перед `metrics/update`):

2. викликати `collect_swapper_metrics(...)`;

3. передавати ці поля до city-service через `POST /internal/node/{id}/metrics/update`:

    payload = {
      "agent_count_router": ...,
      "agent_count_system": ...,
      "gpu": {...},
      "cpu": {...},
      # ...
      "swapper_healthy": swapper_metrics["swapper_healthy"],
      "swapper_models_loaded": swapper_metrics["swapper_models_loaded"],
      "swapper_models_total": swapper_metrics["swapper_models_total"],
    }
    requests.post(f"{CITY_URL}/internal/node/{node_id}/metrics/update", json=payload, timeout=5)

### 3.3. Оновити обробник `/internal/node/{id}/metrics/update`

У `routes_city.py`:

- приймати нові поля;
- оновлювати відповідні колонки в `node_cache`.

Приклад (ескіз):

    swapper_healthy = body.get("swapper_healthy")
    swapper_models_loaded = body.get("swapper_models_loaded")
    swapper_models_total = body.get("swapper_models_total")

    # update node_cache set ... where node_id = ...

## 4. Backend: Swapper detail endpoint

Додати окремий internal endpoint для детального перегляду Swapper-стану ноди:

`GET /internal/node/{node_id}/swapper`

Відповідь:

    {
      "node_id": "node-2-macbook-m4max",
      "healthy": true,
      "models_loaded": 3,
      "models_total": 5,
      "models": [
        { "name": "daarion-small-3b", "loaded": true, "type": "llm" },
        { "name": "daarion-code-7b", "loaded": true, "type": "code" },
        { "name": "vision-8b", "loaded": false, "type": "vlm" }
      ]
    }

Джерело:

- по можливості прямо через Swapper API;
- частину агрегованих значень (loaded/total) можна брати з `node_cache`.

Цей endpoint буде використовуватись Node Cabinet для розгорнутої таблиці моделей.

## 5. Frontend: Кабінет Ноди → Swapper

### 5.1. Хук `useNodeSwapper(nodeId)`

У `apps/web/src/hooks`:

    import useSWR from "swr";

    export function useNodeSwapper(nodeId: string) {
      return useSWR(`/internal/node/${nodeId}/swapper`, fetcher);
    }

### 5.2. Компонент `NodeSwapperCard`

Новий компонент у `components/node-dashboard/NodeSwapperCard.tsx`, який показує:

- заголовок: `Swapper Service`;

- статус:

- `🟢 Healthy` / `🟡 Degraded` / `🔴 Down` (на основі `healthy` + `models_loaded`);

- коротке резюме:

- `Моделі: 3/5 завантажено`;

- кнопка/кнопка-розкривалка `Переглянути моделі`:

- список моделей (name, type, loaded).

Макет (ескіз):

    ┌─────────────────────────────────────────────┐
    │ 🧠 Swapper Service                   [↻]    │
    ├─────────────────────────────────────────────┤
    │ Статус: 🟢 Healthy                          │
    │ Моделі: 3 / 5 завантажено                  │
    ├─────────────────────────────────────────────┤
    │ d-model-3b        | llm      | 🟢 loaded   │
    │ d-code-7b         | code     | 🟢 loaded   │
    │ vision-8b         | vlm      | 🔴 not loaded│
    └─────────────────────────────────────────────┘

### 5.3. Інтеграція в Node Cabinet

На сторінці `/nodes/[nodeId]`:

- додати `NodeSwapperCard` поруч із `NodeMetricsCard` / `DAGIRouterCard` у секцію Service Agents.

Наприклад:

- Node Core: Guardian/Steward
- Service Agents: DAGI Router, Swapper, Multimodal (Swapper Card тут)
- Control & Intelligence: Tools & Planner, Security, Archivist.

## 6. Tests

### 6.1. Backend

- Тест міграції `node_cache` (наявність нових колонок).

- Тест `POST /internal/node/{id}/metrics/update`:

- при передачі полів Swapper правильно оновлює `node_cache`.

- Тест `GET /internal/node/{id}/swapper`:

- при коректній відповіді Swapper API повертає очікувану структуру;

- якщо Swapper лежить — `healthy=false`, моделі порожні.

### 6.2. Frontend

- Snapshot-тест `NodeSwapperCard` для випадків:

- healthy + моделі є;

- unhealthy + немає моделей;
- loading/error state.

## 7. Acceptance Criteria

1. node-guardian-loop регулярно збирає Swapper-метрики та відправляє їх у city-service.

2. `node_cache` містить свіжі поля:

3. `swapper_healthy`

4. `swapper_models_loaded`
5. `swapper_models_total`
6. `GET /internal/node/{id}/metrics/current` відображає Swapper-метрики для обох нод.
7. `GET /internal/node/{id}/swapper` повертає детальну інформацію про Swapper (мінімум: healthy + models).

8. У Кабінеті Ноди (`/nodes/[nodeId]`) є блок Swapper Service:

9. показує статус,

10. показує кількість завантажених моделей,
11. дозволяє переглянути список моделей.

12. `scripts/check-deploy-post.py` можна оновити, щоб:

13. додати 1–2 перевірки Swapper-статусу (healthy/models_loaded),

14. і вони проходили при нормальному стані.

## 8. Deliverables

- `migrations/039_node_cache_swapper_metrics.sql`
- Оновлений `node-guardian-loop.py` (з Swapper-метриками)

- Оновлені endpoints:

- `POST /internal/node/{id}/metrics/update`

- `GET /internal/node/{id}/swapper`

- Frontend:

- `useNodeSwapper(nodeId)`

- `NodeSwapperCard`
- Інтеграція в Node Cabinet
- Тести (backend + frontend)