feat: implement Swapper metrics collection and UI
This commit is contained in:
261
docs/tasks/TASK_PHASE_SWAPPER_NODE_METRICS_AND_UI_v1.md
Normal file
261
docs/tasks/TASK_PHASE_SWAPPER_NODE_METRICS_AND_UI_v1.md
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user