Implement real-time online status synchronization with Headplane
- Add Headscale CLI integration to get real-time online status - Replace timeout-based logic with exact same logic as Headplane - Use 'online' field from Headscale CLI JSON output - Update dashboard statistics to show 3 online nodes matching Headplane - Update pharmacy and machine management views with real-time status 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1f0afd4cae
commit
11f6ff16d0
@ -39,7 +39,7 @@
|
|||||||
<div class="col-lg-3 col-md-6 mb-3">
|
<div class="col-lg-3 col-md-6 mb-3">
|
||||||
<div class="card" style="background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); color: white;">
|
<div class="card" style="background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); color: white;">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<div class="stat-number" id="online-machines">{{ stats.online_machines }}</div>
|
<div class="stat-number" id="online-machines" data-stat="online">{{ stats.online_machines }}</div>
|
||||||
<div class="stat-label">
|
<div class="stat-label">
|
||||||
<i class="fas fa-circle text-success"></i> 온라인 머신
|
<i class="fas fa-circle text-success"></i> 온라인 머신
|
||||||
</div>
|
</div>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
<div class="col-lg-3 col-md-6 mb-3">
|
<div class="col-lg-3 col-md-6 mb-3">
|
||||||
<div class="card" style="background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%); color: white;">
|
<div class="card" style="background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%); color: white;">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<div class="stat-number" id="offline-machines">{{ stats.offline_machines }}</div>
|
<div class="stat-number" id="offline-machines" data-stat="offline">{{ stats.offline_machines }}</div>
|
||||||
<div class="stat-label">
|
<div class="stat-label">
|
||||||
<i class="fas fa-circle text-danger"></i> 오프라인 머신
|
<i class="fas fa-circle text-danger"></i> 오프라인 머신
|
||||||
</div>
|
</div>
|
||||||
@ -250,6 +250,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
createDoughnutChart('diskChart', 60, '디스크', '#f59e0b');
|
createDoughnutChart('diskChart', 60, '디스크', '#f59e0b');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 실시간 통계 업데이트
|
||||||
|
function updateStats() {
|
||||||
|
fetch('/api/dashboard/stats')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(stats => {
|
||||||
|
// 머신 상태 업데이트
|
||||||
|
const onlineElement = document.querySelector('[data-stat="online"]');
|
||||||
|
const offlineElement = document.querySelector('[data-stat="offline"]');
|
||||||
|
const totalElement = document.querySelector('[data-stat="total"]');
|
||||||
|
|
||||||
|
if (onlineElement) onlineElement.textContent = stats.online_machines;
|
||||||
|
if (offlineElement) offlineElement.textContent = stats.offline_machines;
|
||||||
|
if (totalElement) totalElement.textContent = stats.total_machines;
|
||||||
|
|
||||||
|
// CPU 온도 차트 업데이트
|
||||||
|
updateChartValue('cpuChart', stats.avg_cpu_temp);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Stats update failed:', error));
|
||||||
|
}
|
||||||
|
|
||||||
// 실시간 알림 업데이트
|
// 실시간 알림 업데이트
|
||||||
function updateAlerts() {
|
function updateAlerts() {
|
||||||
fetch('/api/alerts')
|
fetch('/api/alerts')
|
||||||
@ -271,6 +291,8 @@ function updateAlerts() {
|
|||||||
.catch(error => console.error('Alert update failed:', error));
|
.catch(error => console.error('Alert update failed:', error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 통계 업데이트 (10초마다 - 더 자주)
|
||||||
|
setInterval(updateStats, 10000);
|
||||||
// 알림 업데이트 (30초마다)
|
// 알림 업데이트 (30초마다)
|
||||||
setInterval(updateAlerts, 30000);
|
setInterval(updateAlerts, 30000);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy import create_engine, text, and_, or_, desc
|
from sqlalchemy import create_engine, text, and_, or_, desc
|
||||||
@ -53,12 +55,43 @@ def close_session(session: Session):
|
|||||||
if session:
|
if session:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Headscale CLI Integration
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_headscale_online_status() -> Dict[str, bool]:
|
||||||
|
"""Headscale CLI를 통해 실시간 온라인 상태 조회"""
|
||||||
|
try:
|
||||||
|
# Docker를 통해 Headscale CLI 실행
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker', 'exec', 'headscale', 'headscale', 'nodes', 'list', '-o', 'json'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
nodes_data = json.loads(result.stdout)
|
||||||
|
online_status = {}
|
||||||
|
|
||||||
|
for node in nodes_data:
|
||||||
|
# given_name 또는 name 사용
|
||||||
|
node_name = node.get('given_name') or node.get('name', '')
|
||||||
|
# online 필드가 True인 경우만 온라인
|
||||||
|
# 필드가 없거나 False면 오프라인
|
||||||
|
is_online = node.get('online', False) == True
|
||||||
|
online_status[node_name.lower()] = is_online
|
||||||
|
|
||||||
|
return online_status
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Headscale CLI 호출 실패: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Dashboard Statistics
|
# Dashboard Statistics
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
def get_dashboard_stats() -> Dict[str, Any]:
|
def get_dashboard_stats() -> Dict[str, Any]:
|
||||||
"""대시보드 통계 조회"""
|
"""대시보드 통계 조회 - 실시간 DB 쿼리"""
|
||||||
farmq_session = get_farmq_session()
|
farmq_session = get_farmq_session()
|
||||||
headscale_session = get_headscale_session()
|
headscale_session = get_headscale_session()
|
||||||
|
|
||||||
@ -68,19 +101,31 @@ def get_dashboard_stats() -> Dict[str, Any]:
|
|||||||
PharmacyInfo.status == 'active'
|
PharmacyInfo.status == 'active'
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
# 머신 상태 - Headscale 노드 기준으로 계산
|
# 머신 상태 - Headscale 노드에서 직접 실시간 조회
|
||||||
from models.headscale_models import Node
|
from models.headscale_models import Node
|
||||||
|
|
||||||
# 전체 머신 수 (Headscale 노드 기준)
|
# 활성 노드만 조회 (deleted_at이 null인 것)
|
||||||
total_machines = headscale_session.query(Node).count()
|
active_nodes = headscale_session.query(Node).filter(
|
||||||
|
Node.deleted_at.is_(None)
|
||||||
|
).all()
|
||||||
|
|
||||||
# 온라인 머신 수 (last_seen이 최근 5분 이내)
|
total_machines = len(active_nodes)
|
||||||
cutoff_time = datetime.now() - timedelta(minutes=5)
|
|
||||||
online_machines = headscale_session.query(Node).filter(
|
# Headscale CLI를 통해 실시간 온라인 상태 가져오기
|
||||||
Node.last_seen > cutoff_time
|
online_status = get_headscale_online_status()
|
||||||
).count()
|
|
||||||
|
# 온라인 머신 수 계산
|
||||||
|
online_machines = 0
|
||||||
|
print(f"🔍 Headscale CLI 온라인 상태:")
|
||||||
|
for node in active_nodes:
|
||||||
|
node_name = (node.given_name or node.hostname or '').lower()
|
||||||
|
is_online = online_status.get(node_name, False)
|
||||||
|
if is_online:
|
||||||
|
online_machines += 1
|
||||||
|
print(f" {node.given_name:15s} | {'🟢 ONLINE' if is_online else '🔴 OFFLINE'}")
|
||||||
|
|
||||||
offline_machines = total_machines - online_machines
|
offline_machines = total_machines - online_machines
|
||||||
|
print(f"📊 최종 카운트: {online_machines}/{total_machines} 온라인 (Headplane과 동일)")
|
||||||
|
|
||||||
# 최근 알림 수
|
# 최근 알림 수
|
||||||
recent_alerts = farmq_session.query(SystemAlert).filter(
|
recent_alerts = farmq_session.query(SystemAlert).filter(
|
||||||
@ -129,6 +174,9 @@ def get_all_pharmacies_with_stats() -> List[Dict[str, Any]]:
|
|||||||
PharmacyInfo.status == 'active'
|
PharmacyInfo.status == 'active'
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
# Headscale CLI를 통해 실시간 온라인 상태 가져오기
|
||||||
|
online_status = get_headscale_online_status()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for pharmacy in pharmacies:
|
for pharmacy in pharmacies:
|
||||||
# Headscale에서 해당 사용자의 머신 수 조회
|
# Headscale에서 해당 사용자의 머신 수 조회
|
||||||
@ -139,20 +187,12 @@ def get_all_pharmacies_with_stats() -> List[Dict[str, Any]]:
|
|||||||
|
|
||||||
machine_count = len(user_machines)
|
machine_count = len(user_machines)
|
||||||
|
|
||||||
# 온라인 머신 수 계산 (24시간 timeout)
|
# 온라인 머신 수 계산 - Headscale CLI 기반
|
||||||
online_count = 0
|
online_count = 0
|
||||||
for machine in user_machines:
|
for machine in user_machines:
|
||||||
if machine.last_seen:
|
node_name = (machine.given_name or machine.hostname or '').lower()
|
||||||
try:
|
if online_status.get(node_name, False):
|
||||||
from datetime import timezone
|
online_count += 1
|
||||||
if machine.last_seen.tzinfo is not None:
|
|
||||||
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
||||||
else:
|
|
||||||
cutoff_time = datetime.now() - timedelta(hours=24)
|
|
||||||
if machine.last_seen > cutoff_time:
|
|
||||||
online_count += 1
|
|
||||||
except Exception:
|
|
||||||
online_count += 1 # 타임존 에러 시 온라인으로 간주
|
|
||||||
|
|
||||||
# 활성 알림 수 (현재는 0으로 설정, 나중에 구현)
|
# 활성 알림 수 (현재는 0으로 설정, 나중에 구현)
|
||||||
alert_count = 0
|
alert_count = 0
|
||||||
@ -230,6 +270,9 @@ def get_all_machines_with_details() -> List[Dict[str, Any]]:
|
|||||||
|
|
||||||
print(f"🔍 Found {len(nodes)} nodes in Headscale database")
|
print(f"🔍 Found {len(nodes)} nodes in Headscale database")
|
||||||
|
|
||||||
|
# Headscale CLI를 통해 실시간 온라인 상태 가져오기
|
||||||
|
online_status = get_headscale_online_status()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
# 기본 머신 정보
|
# 기본 머신 정보
|
||||||
@ -246,22 +289,9 @@ def get_all_machines_with_details() -> List[Dict[str, Any]]:
|
|||||||
'updated_at': node.updated_at
|
'updated_at': node.updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
# 온라인 상태 확인 (24시간 timeout)
|
# Headscale CLI 기반 온라인 상태 확인
|
||||||
if node.last_seen:
|
node_name = (node.given_name or node.hostname or '').lower()
|
||||||
try:
|
machine_data['is_online'] = online_status.get(node_name, False)
|
||||||
from datetime import timezone
|
|
||||||
# node.last_seen이 timezone-aware인지 확인
|
|
||||||
if node.last_seen.tzinfo is not None:
|
|
||||||
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
||||||
else:
|
|
||||||
cutoff_time = datetime.now() - timedelta(hours=24)
|
|
||||||
machine_data['is_online'] = node.last_seen > cutoff_time
|
|
||||||
except Exception as e:
|
|
||||||
# 타임존 비교 에러가 발생하면 기본적으로 온라인으로 가정
|
|
||||||
print(f"Timezone comparison error for {node.hostname}: {e}")
|
|
||||||
machine_data['is_online'] = True
|
|
||||||
else:
|
|
||||||
machine_data['is_online'] = False
|
|
||||||
|
|
||||||
# 사용자 이름으로 약국 정보 찾기
|
# 사용자 이름으로 약국 정보 찾기
|
||||||
machine_data['pharmacy'] = None
|
machine_data['pharmacy'] = None
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user