-
{{ stats.offline_machines }}
+
{{ stats.offline_machines }}
오프라인 머신
@@ -250,6 +250,26 @@ document.addEventListener('DOMContentLoaded', function() {
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() {
fetch('/api/alerts')
@@ -271,6 +291,8 @@ function updateAlerts() {
.catch(error => console.error('Alert update failed:', error));
}
+// 통계 업데이트 (10초마다 - 더 자주)
+setInterval(updateStats, 10000);
// 알림 업데이트 (30초마다)
setInterval(updateAlerts, 30000);
diff --git a/farmq-admin/utils/database_new.py b/farmq-admin/utils/database_new.py
index 4bc3306..7a705a0 100644
--- a/farmq-admin/utils/database_new.py
+++ b/farmq-admin/utils/database_new.py
@@ -4,6 +4,8 @@
"""
import os
+import json
+import subprocess
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
from sqlalchemy import create_engine, text, and_, or_, desc
@@ -53,12 +55,43 @@ def close_session(session: Session):
if session:
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
# ==========================================
def get_dashboard_stats() -> Dict[str, Any]:
- """대시보드 통계 조회"""
+ """대시보드 통계 조회 - 실시간 DB 쿼리"""
farmq_session = get_farmq_session()
headscale_session = get_headscale_session()
@@ -68,19 +101,31 @@ def get_dashboard_stats() -> Dict[str, Any]:
PharmacyInfo.status == 'active'
).count()
- # 머신 상태 - Headscale 노드 기준으로 계산
+ # 머신 상태 - Headscale 노드에서 직접 실시간 조회
from models.headscale_models import Node
- # 전체 머신 수 (Headscale 노드 기준)
- total_machines = headscale_session.query(Node).count()
+ # 활성 노드만 조회 (deleted_at이 null인 것)
+ active_nodes = headscale_session.query(Node).filter(
+ Node.deleted_at.is_(None)
+ ).all()
- # 온라인 머신 수 (last_seen이 최근 5분 이내)
- cutoff_time = datetime.now() - timedelta(minutes=5)
- online_machines = headscale_session.query(Node).filter(
- Node.last_seen > cutoff_time
- ).count()
+ total_machines = len(active_nodes)
+
+ # Headscale CLI를 통해 실시간 온라인 상태 가져오기
+ online_status = get_headscale_online_status()
+
+ # 온라인 머신 수 계산
+ 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
+ print(f"📊 최종 카운트: {online_machines}/{total_machines} 온라인 (Headplane과 동일)")
# 최근 알림 수
recent_alerts = farmq_session.query(SystemAlert).filter(
@@ -129,6 +174,9 @@ def get_all_pharmacies_with_stats() -> List[Dict[str, Any]]:
PharmacyInfo.status == 'active'
).all()
+ # Headscale CLI를 통해 실시간 온라인 상태 가져오기
+ online_status = get_headscale_online_status()
+
result = []
for pharmacy in pharmacies:
# Headscale에서 해당 사용자의 머신 수 조회
@@ -139,20 +187,12 @@ def get_all_pharmacies_with_stats() -> List[Dict[str, Any]]:
machine_count = len(user_machines)
- # 온라인 머신 수 계산 (24시간 timeout)
+ # 온라인 머신 수 계산 - Headscale CLI 기반
online_count = 0
for machine in user_machines:
- if machine.last_seen:
- try:
- from datetime import timezone
- 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 # 타임존 에러 시 온라인으로 간주
+ node_name = (machine.given_name or machine.hostname or '').lower()
+ if online_status.get(node_name, False):
+ online_count += 1
# 활성 알림 수 (현재는 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")
+ # Headscale CLI를 통해 실시간 온라인 상태 가져오기
+ online_status = get_headscale_online_status()
+
result = []
for node in nodes:
# 기본 머신 정보
@@ -246,22 +289,9 @@ def get_all_machines_with_details() -> List[Dict[str, Any]]:
'updated_at': node.updated_at
}
- # 온라인 상태 확인 (24시간 timeout)
- if node.last_seen:
- try:
- 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
+ # Headscale CLI 기반 온라인 상태 확인
+ node_name = (node.given_name or node.hostname or '').lower()
+ machine_data['is_online'] = online_status.get(node_name, False)
# 사용자 이름으로 약국 정보 찾기
machine_data['pharmacy'] = None