feat: implement swapper metrics and node cabinet ui
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useNodeSwapper, SwapperModel } from '@/hooks/useNodeSwapper';
|
||||
import { useState } from 'react';
|
||||
import { useNodeSwapper } from '@/hooks/useNodeSwapper';
|
||||
|
||||
interface NodeSwapperCardProps {
|
||||
nodeId: string;
|
||||
@@ -9,95 +9,111 @@ interface NodeSwapperCardProps {
|
||||
|
||||
export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
||||
const { swapper, isLoading, error, mutate } = useNodeSwapper(nodeId);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 animate-pulse">
|
||||
<div className="h-6 bg-white/10 rounded w-1/3 mb-4" />
|
||||
<div className="h-4 bg-white/10 rounded w-1/2" />
|
||||
<div className="h-6 w-32 bg-white/10 rounded mb-4" />
|
||||
<div className="h-4 w-full bg-white/5 rounded" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-500/10 backdrop-blur-md rounded-2xl border border-red-500/20 p-6 text-red-400">
|
||||
<h3 className="font-semibold flex items-center gap-2">
|
||||
<span>🧠</span> Swapper Service
|
||||
</h3>
|
||||
<p className="text-sm mt-2">Failed to load status</p>
|
||||
<div className="bg-red-500/10 backdrop-blur-md rounded-2xl border border-red-500/20 p-6">
|
||||
<h3 className="text-red-400 font-medium mb-2">Swapper Service Unavailable</h3>
|
||||
<p className="text-white/50 text-sm">Failed to load swapper details.</p>
|
||||
<button
|
||||
onClick={() => mutate()}
|
||||
className="mt-4 text-xs text-white/40 hover:text-white hover:underline"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!swapper) return null;
|
||||
|
||||
const statusColor = swapper.healthy ? 'text-green-400' : 'text-red-400';
|
||||
const statusIcon = swapper.healthy ? '🟢' : '🔴';
|
||||
const statusText = swapper.healthy ? 'Healthy' : 'Down';
|
||||
|
||||
return (
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 transition-all hover:border-cyan-500/30">
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<span>🧠</span> Swapper Service
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => mutate()}
|
||||
className="text-white/30 hover:text-white/70 transition-colors"
|
||||
title="Refresh"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => mutate()}
|
||||
className="p-1.5 rounded-lg hover:bg-white/5 text-white/30 hover:text-white transition-colors"
|
||||
title="Refresh"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Status Row */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-white/50">Status:</span>
|
||||
<span className={`font-medium flex items-center gap-1.5 ${statusColor}`}>
|
||||
{statusIcon} {statusText}
|
||||
</span>
|
||||
<div className="space-y-4">
|
||||
{/* Status */}
|
||||
<div className="bg-white/5 rounded-xl p-4 border border-white/5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-white/50 text-sm">Status</span>
|
||||
<div className={`flex items-center gap-2 px-2 py-1 rounded-full text-xs font-medium ${
|
||||
swapper.healthy ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||
swapper.healthy ? 'bg-green-400' : 'bg-red-400'
|
||||
}`} />
|
||||
{swapper.healthy ? 'Healthy' : 'Unhealthy'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/50 text-sm">Models Loaded</span>
|
||||
<span className="text-white font-mono">
|
||||
{swapper.models_loaded} <span className="text-white/30">/ {swapper.models_total}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Row */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-white/50">Models:</span>
|
||||
<span className="text-white font-medium">
|
||||
{swapper.models_loaded} / {swapper.models_total} loaded
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Details Toggle */}
|
||||
{/* Models List Toggle */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full mt-2 py-2 text-xs font-medium text-white/40 hover:text-white/70 bg-white/5 hover:bg-white/10 rounded-lg transition-colors flex items-center justify-center gap-1"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full flex items-center justify-between px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors text-sm text-white/70"
|
||||
>
|
||||
{expanded ? 'Hide Models' : 'View Models'}
|
||||
<span>{expanded ? '▲' : '▼'}</span>
|
||||
<span>Available Models</span>
|
||||
<span className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`}>▼</span>
|
||||
</button>
|
||||
|
||||
{/* Models List */}
|
||||
{expanded && (
|
||||
<div className="mt-3 space-y-2 border-t border-white/10 pt-3">
|
||||
{isExpanded && (
|
||||
<div className="space-y-2 mt-2">
|
||||
{swapper.models.length === 0 ? (
|
||||
<p className="text-white/30 text-xs text-center py-2">No models found</p>
|
||||
<p className="text-center text-white/30 text-sm py-2">No models found</p>
|
||||
) : (
|
||||
swapper.models.map((model, idx) => (
|
||||
<div key={`${model.name}-${idx}`} className="flex items-center justify-between text-xs bg-black/20 p-2 rounded border border-white/5">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-white font-medium">{model.name}</span>
|
||||
<span className="text-white/40 uppercase text-[10px]">{model.type || 'unknown'}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<span className={model.loaded ? 'text-green-400' : 'text-white/30'}>
|
||||
{model.loaded ? '● LOADED' : '○ UNLOADED'}
|
||||
</span>
|
||||
{model.vram_gb && (
|
||||
<span className="text-white/30">{model.vram_gb.toFixed(1)} GB</span>
|
||||
)}
|
||||
<div
|
||||
key={`${model.name}-${idx}`}
|
||||
className="flex items-center justify-between p-3 rounded-lg bg-white/5 border border-white/5"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white text-sm font-medium">{model.name}</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-white/40 uppercase px-1.5 py-0.5 rounded bg-white/5">
|
||||
{model.type || 'unknown'}
|
||||
</span>
|
||||
{model.vram_gb && (
|
||||
<span className="text-xs text-white/30">
|
||||
{model.vram_gb.toFixed(1)} GB VRAM
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
model.loaded ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]' : 'bg-white/10'
|
||||
}`} title={model.loaded ? 'Loaded in VRAM' : 'Not loaded'} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -107,4 +123,3 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
export { StatusBadge } from './StatusBadge';
|
||||
export { ProgressBar } from './ProgressBar';
|
||||
export { NodeSummaryCard } from './NodeSummaryCard';
|
||||
export { InfraCard } from './InfraCard';
|
||||
export { AIServicesCard } from './AIServicesCard';
|
||||
export { AgentsCard } from './AgentsCard';
|
||||
export { MatrixCard } from './MatrixCard';
|
||||
export { ModulesCard } from './ModulesCard';
|
||||
export { NodeStandardComplianceCard } from './NodeStandardComplianceCard';
|
||||
export { DAGIRouterCard } from './DAGIRouterCard';
|
||||
export { NodeMetricsCard } from './NodeMetricsCard';
|
||||
import { NodeSummaryCard } from './NodeSummaryCard';
|
||||
import { InfraCard } from './InfraCard';
|
||||
import { AIServicesCard } from './AIServicesCard';
|
||||
import { AgentsCard } from './AgentsCard';
|
||||
import { MatrixCard } from './MatrixCard';
|
||||
import { ModulesCard } from './ModulesCard';
|
||||
import { NodeStandardComplianceCard } from './NodeStandardComplianceCard';
|
||||
import { DAGIRouterCard } from './DAGIRouterCard';
|
||||
import { NodeMetricsCard } from './NodeMetricsCard';
|
||||
import { NodeSwapperCard } from './NodeSwapperCard';
|
||||
|
||||
export {
|
||||
NodeSummaryCard,
|
||||
InfraCard,
|
||||
AIServicesCard,
|
||||
AgentsCard,
|
||||
MatrixCard,
|
||||
ModulesCard,
|
||||
NodeStandardComplianceCard,
|
||||
DAGIRouterCard,
|
||||
NodeMetricsCard,
|
||||
NodeSwapperCard
|
||||
};
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
const fetcher = (url: string) => fetch(url, { credentials: "include" }).then(r => {
|
||||
if (!r.ok) throw new Error(`Failed to load ${url}`);
|
||||
return r.json();
|
||||
});
|
||||
import useSWR from 'swr';
|
||||
|
||||
export interface SwapperModel {
|
||||
name: string;
|
||||
@@ -20,7 +15,9 @@ export interface NodeSwapperDetail {
|
||||
models: SwapperModel[];
|
||||
}
|
||||
|
||||
export function useNodeSwapper(nodeId?: string) {
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
export function useNodeSwapper(nodeId: string) {
|
||||
const { data, error, isLoading, mutate } = useSWR<NodeSwapperDetail>(
|
||||
nodeId ? `/api/v1/internal/node/${nodeId}/swapper` : null,
|
||||
fetcher
|
||||
@@ -33,4 +30,3 @@ export function useNodeSwapper(nodeId?: string) {
|
||||
mutate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,7 @@
|
||||
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; -- Для повного списку моделей
|
||||
add column swapper_models_total integer;
|
||||
```
|
||||
|
||||
Міграція: `migrations/039_node_cache_swapper_metrics.sql`.
|
||||
@@ -91,9 +90,8 @@ 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": {}
|
||||
"swapper_models_loaded": None,
|
||||
"swapper_models_total": None,
|
||||
}
|
||||
try:
|
||||
# healthz
|
||||
@@ -111,8 +109,8 @@ def collect_swapper_metrics(swapper_base_url: str) -> dict:
|
||||
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:
|
||||
# залишаємо None → UI покаже "невідомо"
|
||||
pass
|
||||
|
||||
return result
|
||||
@@ -133,7 +131,6 @@ payload = {
|
||||
"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)
|
||||
```
|
||||
@@ -145,6 +142,16 @@ requests.post(f"{CITY_URL}/internal/node/{node_id}/metrics/update", json=payload
|
||||
* приймати нові поля;
|
||||
* оновлювати відповідні колонки в `node_cache`.
|
||||
|
||||
Приклад (ескіз):
|
||||
|
||||
```python
|
||||
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
|
||||
@@ -170,7 +177,11 @@ requests.post(f"{CITY_URL}/internal/node/{node_id}/metrics/update", json=payload
|
||||
```
|
||||
|
||||
Джерело:
|
||||
* `node_cache` (нові колонки + jsonb `swapper_state`).
|
||||
|
||||
* по можливості прямо через Swapper API;
|
||||
* частину агрегованих значень (loaded/total) можна брати з `node_cache`.
|
||||
|
||||
Цей endpoint буде використовуватись Node Cabinet для розгорнутої таблиці моделей.
|
||||
|
||||
---
|
||||
|
||||
@@ -194,18 +205,42 @@ export function useNodeSwapper(nodeId: string) {
|
||||
|
||||
* заголовок: `Swapper Service`;
|
||||
* статус:
|
||||
|
||||
* `🟢 Healthy` / `🟡 Degraded` / `🔴 Down` (на основі `healthy` + `models_loaded`);
|
||||
* коротке резюме:
|
||||
|
||||
* `Моделі: 3/5 завантажено`;
|
||||
* кнопка/кнопка-розкривалка `Переглянути моделі`:
|
||||
|
||||
* список моделей (name, type, loaded).
|
||||
|
||||
Макет (ескіз):
|
||||
|
||||
```txt
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 🧠 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
|
||||
@@ -214,14 +249,17 @@ export function useNodeSwapper(nodeId: string) {
|
||||
|
||||
* Тест міграції `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.
|
||||
@@ -232,17 +270,21 @@ export function useNodeSwapper(nodeId: string) {
|
||||
|
||||
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.
|
||||
6. `scripts/check-deploy-post.py` можна оновити, щоб:
|
||||
|
||||
* додати 1–2 перевірки Swapper-статусу (healthy/models_loaded),
|
||||
* і вони проходили при нормальному стані.
|
||||
|
||||
---
|
||||
|
||||
@@ -251,11 +293,12 @@ export function useNodeSwapper(nodeId: string) {
|
||||
* `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)
|
||||
|
||||
|
||||
@@ -1,13 +1,71 @@
|
||||
-- Migration 039: Add Swapper metrics to node_cache
|
||||
-- Додаємо поля для метрик Swapper Service в node_cache
|
||||
|
||||
ALTER TABLE node_cache ADD COLUMN IF NOT EXISTS swapper_healthy boolean DEFAULT false;
|
||||
ALTER TABLE node_cache ADD COLUMN IF NOT EXISTS swapper_models_loaded integer DEFAULT 0;
|
||||
ALTER TABLE node_cache ADD COLUMN IF NOT EXISTS swapper_models_total integer DEFAULT 0;
|
||||
ALTER TABLE node_cache ADD COLUMN IF NOT EXISTS swapper_state jsonb DEFAULT '{}'::jsonb;
|
||||
|
||||
COMMENT ON COLUMN node_cache.swapper_healthy IS 'Статус здоров`я Swapper Service';
|
||||
COMMENT ON COLUMN node_cache.swapper_models_loaded IS 'Кількість завантажених моделей';
|
||||
COMMENT ON COLUMN node_cache.swapper_models_total IS 'Загальна кількість відомих моделей';
|
||||
COMMENT ON COLUMN node_cache.swapper_state IS 'Повний стан моделей Swapper (JSON)';
|
||||
|
||||
-- Function to handle atomic node heartbeat and metrics update
|
||||
-- Updated to include Swapper metrics
|
||||
CREATE OR REPLACE FUNCTION fn_node_heartbeat(
|
||||
p_node_id text,
|
||||
p_metrics jsonb
|
||||
) RETURNS void AS $$
|
||||
BEGIN
|
||||
INSERT INTO node_cache (
|
||||
node_id, node_name, hostname, roles, environment, status, gpu, last_sync,
|
||||
cpu_model, cpu_cores, cpu_usage, gpu_model, gpu_vram_total, gpu_vram_used,
|
||||
ram_total, ram_used, disk_total, disk_used, agent_count_router, agent_count_system,
|
||||
last_heartbeat, dagi_router_url, self_healing_status,
|
||||
swapper_healthy, swapper_models_loaded, swapper_models_total
|
||||
)
|
||||
VALUES (
|
||||
p_node_id,
|
||||
p_metrics->>'node_name',
|
||||
p_metrics->>'hostname',
|
||||
(SELECT array_agg(elem) FROM jsonb_array_elements_text(p_metrics->'roles') AS elem),
|
||||
p_metrics->>'environment',
|
||||
p_metrics->>'status',
|
||||
p_metrics->>'gpu',
|
||||
NOW(),
|
||||
p_metrics->>'cpu_model',
|
||||
(p_metrics->>'cpu_cores')::integer,
|
||||
(p_metrics->>'cpu_usage')::numeric,
|
||||
p_metrics->>'gpu_model',
|
||||
(p_metrics->>'gpu_vram_total')::integer,
|
||||
(p_metrics->>'gpu_vram_used')::integer,
|
||||
(p_metrics->>'ram_total')::integer,
|
||||
(p_metrics->>'ram_used')::integer,
|
||||
(p_metrics->>'disk_total')::integer,
|
||||
(p_metrics->>'disk_used')::integer,
|
||||
(p_metrics->>'agent_count_router')::integer,
|
||||
(p_metrics->>'agent_count_system')::integer,
|
||||
NOW(),
|
||||
p_metrics->>'dagi_router_url',
|
||||
p_metrics->>'self_healing_status',
|
||||
(p_metrics->>'swapper_healthy')::boolean,
|
||||
(p_metrics->>'swapper_models_loaded')::integer,
|
||||
(p_metrics->>'swapper_models_total')::integer
|
||||
)
|
||||
ON CONFLICT (node_id) DO UPDATE SET
|
||||
node_name = COALESCE(EXCLUDED.node_name, node_cache.node_name),
|
||||
hostname = COALESCE(EXCLUDED.hostname, node_cache.hostname),
|
||||
roles = COALESCE(EXCLUDED.roles, node_cache.roles),
|
||||
environment = COALESCE(EXCLUDED.environment, node_cache.environment),
|
||||
status = COALESCE(EXCLUDED.status, node_cache.status),
|
||||
gpu = COALESCE(EXCLUDED.gpu, node_cache.gpu),
|
||||
last_sync = NOW(),
|
||||
cpu_model = COALESCE(EXCLUDED.cpu_model, node_cache.cpu_model),
|
||||
cpu_cores = COALESCE(EXCLUDED.cpu_cores, node_cache.cpu_cores),
|
||||
cpu_usage = COALESCE(EXCLUDED.cpu_usage, node_cache.cpu_usage),
|
||||
gpu_model = COALESCE(EXCLUDED.gpu_model, node_cache.gpu_model),
|
||||
gpu_vram_total = COALESCE(EXCLUDED.gpu_vram_total, node_cache.gpu_vram_total),
|
||||
gpu_vram_used = COALESCE(EXCLUDED.gpu_vram_used, node_cache.gpu_vram_used),
|
||||
ram_total = COALESCE(EXCLUDED.ram_total, node_cache.ram_total),
|
||||
ram_used = COALESCE(EXCLUDED.ram_used, node_cache.ram_used),
|
||||
disk_total = COALESCE(EXCLUDED.disk_total, node_cache.disk_total),
|
||||
disk_used = COALESCE(EXCLUDED.disk_used, node_cache.disk_used),
|
||||
agent_count_router = COALESCE(EXCLUDED.agent_count_router, node_cache.agent_count_router),
|
||||
agent_count_system = COALESCE(EXCLUDED.agent_count_system, node_cache.agent_count_system),
|
||||
last_heartbeat = NOW(),
|
||||
dagi_router_url = COALESCE(EXCLUDED.dagi_router_url, node_cache.dagi_router_url),
|
||||
self_healing_status = COALESCE(EXCLUDED.self_healing_status, node_cache.self_healing_status),
|
||||
swapper_healthy = COALESCE(EXCLUDED.swapper_healthy, node_cache.swapper_healthy),
|
||||
swapper_models_loaded = COALESCE(EXCLUDED.swapper_models_loaded, node_cache.swapper_models_loaded),
|
||||
swapper_models_total = COALESCE(EXCLUDED.swapper_models_total, node_cache.swapper_models_total),
|
||||
updated_at = NOW();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
Reference in New Issue
Block a user