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:
@@ -1173,24 +1173,40 @@ def control_container(
|
|||||||
|
|
||||||
@router.get("/api/{server_id}/nodes")
|
@router.get("/api/{server_id}/nodes")
|
||||||
def get_server_nodes(
|
def get_server_nodes(
|
||||||
server_id: int,
|
server_id: int,
|
||||||
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")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = _get_proxmox_client(server)
|
client = _get_proxmox_client(server)
|
||||||
|
|
||||||
if not client.is_connected():
|
if not client.is_connected():
|
||||||
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))
|
||||||
|
|||||||
@@ -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,22 +93,26 @@ 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}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработать ВСЕ ноды этого сервера
|
// Обработать ВСЕ ноды этого сервера
|
||||||
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`);
|
||||||
|
|||||||
Reference in New Issue
Block a user