feat: implement Swapper metrics collection and UI

This commit is contained in:
Apple
2025-11-30 15:12:49 -08:00
parent 5b5160ad8b
commit fd814b2059
11 changed files with 1224 additions and 4543 deletions

View File

@@ -0,0 +1,261 @@
# 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. Оновлення Кабінету Ноди:
- блок «Swapper Service» зі статусом і основною інформацією.
5. Базові тести (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 немає — додати міграцію, наприклад:
```sql
alter table node_cache
add column swapper_healthy boolean,
add column swapper_models_loaded integer,
add column swapper_models_total integer,
add column swapper_state jsonb; -- Для повного списку моделей
```
Міграція: `migrations/039_node_cache_swapper_metrics.sql`.
### 3.2. Оновити node-guardian-loop
У `node-guardian-loop.py` (або відповідному worker):
1. Додати функцію для збору Swapper-метрик:
```python
import requests
def collect_swapper_metrics(swapper_base_url: str) -> dict:
result = {
"swapper_healthy": False,
"swapper_models_loaded": 0,
"swapper_models_total": 0,
"swapper_state": {}
}
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
result["swapper_state"] = data # Зберігаємо весь стан
except Exception:
pass
return result
```
2. У циклі node-guardian-loop (перед `metrics/update`):
* викликати `collect_swapper_metrics(...)`;
* передавати ці поля до city-service через `POST /internal/node/{id}/metrics/update`:
```python
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"],
"swapper_state": swapper_metrics["swapper_state"]
}
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`.
---
## 4. Backend: Swapper detail endpoint
Додати окремий internal endpoint для детального перегляду Swapper-стану ноди:
`GET /internal/node/{node_id}/swapper`
Відповідь:
```json
{
"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" }
]
}
```
Джерело:
* `node_cache` (нові колонки + jsonb `swapper_state`).
---
## 5. Frontend: Кабінет Ноди → Swapper
### 5.1. Хук `useNodeSwapper(nodeId)`
У `apps/web/src/hooks`:
```ts
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).
### 5.3. Інтеграція в Node Cabinet
На сторінці `/nodes/[nodeId]`:
* додати `NodeSwapperCard` поруч із `NodeMetricsCard` / `DAGIRouterCard` у секцію Service Agents.
---
## 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` містить свіжі поля:
* `swapper_healthy`
* `swapper_models_loaded`
* `swapper_models_total`
* `swapper_state` (JSONB)
3. `GET /internal/node/{id}/metrics/current` відображає Swapper-метрики для обох нод.
4. `GET /internal/node/{id}/swapper` повертає детальну інформацію про Swapper (мінімум: healthy + models).
5. У Кабінеті Ноди (`/nodes/[nodeId]`) є блок Swapper Service:
* показує статус,
* показує кількість завантажених моделей,
* дозволяє переглянути список моделей.
6. `scripts/check-deploy-post.py` оновлено для перевірки Swapper.
---
## 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)