fix: deduplicate dashboard node charts by cluster, not just by node name

Previously seenNodes used only the node name as the dedup key, causing
independent servers that happen to share a default node name (e.g. "pve")
to hide each other's charts.

Fix: /api/{server_id}/nodes now also returns cluster_id — the actual
Proxmox cluster name for clustered servers, or "standalone-{server_id}"
for single-node servers. The dashboard deduplicates by
"cluster_id::node_name", so:
- nodes in the same cluster → deduped correctly (shown once)
- nodes on independent servers with the same name → both shown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
markmorado
2026-03-08 02:37:18 +01:00
parent 84015ce0bd
commit c07d9e8711
2 changed files with 40 additions and 20 deletions

View File

@@ -1177,7 +1177,7 @@ def get_server_nodes(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("proxmox.view")) current_user: User = Depends(PermissionChecker("proxmox.view"))
): ):
"""Получить список нод Proxmox сервера""" """Получить список нод Proxmox сервера вместе с идентификатором кластера"""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first() server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
if not server: if not server:
raise HTTPException(status_code=404, detail="Proxmox server not found") raise HTTPException(status_code=404, detail="Proxmox server not found")
@@ -1189,8 +1189,24 @@ def get_server_nodes(
raise HTTPException(status_code=503, detail="Failed to connect to Proxmox server") raise HTTPException(status_code=503, detail="Failed to connect to Proxmox server")
nodes = client.get_nodes() nodes = client.get_nodes()
logger.info(f"Nodes for server {server_id} ({server.name}): {nodes}")
return JSONResponse(content={"nodes": nodes}) # Определяем идентификатор кластера для корректной дедупликации на фронте.
# Если сервер входит в кластер — используем имя кластера (все члены одного
# кластера вернут одинаковое значение). Для standalone — используем уникальный
# "standalone-{server_id}", чтобы независимые серверы с одинаковыми именами
# нод не скрывали друг друга.
cluster_id = f"standalone-{server_id}"
try:
cluster_status = client.proxmox.cluster.status.get()
for item in cluster_status:
if item.get('type') == 'cluster' and item.get('name'):
cluster_id = item['name']
break
except Exception:
pass
logger.info(f"Nodes for server {server_id} ({server.name}), cluster_id={cluster_id}: {nodes}")
return JSONResponse(content={"nodes": nodes, "cluster_id": cluster_id})
except Exception as e: except Exception as e:
logger.error(f"Error getting nodes from {server.name}: {e}") logger.error(f"Error getting nodes from {server.name}: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View File

@@ -74,11 +74,11 @@ async function loadProxmoxNodesStats() {
// Собрать все данные сначала, потом отрисовать // Собрать все данные сначала, потом отрисовать
const chartsToRender = []; const chartsToRender = [];
// Глобальная дедупликация по имени ноды // Дедупликация по ключу "cluster_id::node".
// Это работает потому что имена нод в кластере уникальны // Члены одного кластера возвращают одинаковый cluster_id (имя кластера),
// Даже если разные серверы кластера видят разное количество нод // поэтому общие ноды показываются один раз.
// (например, при временном отключении одной ноды), // Независимые серверы с одинаковым именем ноды получают разные cluster_id
// мы всё равно покажем каждую ноду только один раз // ("standalone-{server_id}"), поэтому отображаются отдельно.
const seenNodes = new Set(); const seenNodes = new Set();
// Загрузить данные для каждого сервера // Загрузить данные для каждого сервера
@@ -93,6 +93,7 @@ async function loadProxmoxNodesStats() {
const nodesData = await nodesResponse.json(); const nodesData = await nodesResponse.json();
const nodes = nodesData.nodes || []; const nodes = nodesData.nodes || [];
const clusterId = nodesData.cluster_id || `standalone-${server.id}`;
if (nodes.length === 0) { if (nodes.length === 0) {
console.warn(`No nodes found for ${server.name}`); console.warn(`No nodes found for ${server.name}`);
@@ -103,12 +104,15 @@ async function loadProxmoxNodesStats() {
for (const nodeInfo of nodes) { for (const nodeInfo of nodes) {
const node = nodeInfo.node; const node = nodeInfo.node;
// Глобальная дедупликация по имени ноды // Дедупликация по кластеру: члены одного кластера имеют
// Если нода с таким именем уже обработана - пропускаем // одинаковый clusterId и одинаковые имена нод → пропускаем дубли.
if (seenNodes.has(node)) { // Независимые серверы с одинаковым именем ноды имеют разные
// clusterId → отображаются отдельно.
const dedupeKey = `${clusterId}::${node}`;
if (seenNodes.has(dedupeKey)) {
continue; continue;
} }
seenNodes.add(node); seenNodes.add(dedupeKey);
// Получить данные для графиков // Получить данные для графиков
const rrdResponse = await fetchWithAuth(`/proxmox/api/${server.id}/node/rrddata?node=${node}&timeframe=hour`); const rrdResponse = await fetchWithAuth(`/proxmox/api/${server.id}/node/rrddata?node=${node}&timeframe=hour`);