diff --git a/farmq-admin/templates/dashboard/index.html b/farmq-admin/templates/dashboard/index.html index 0ba500c..14c1ce1 100644 --- a/farmq-admin/templates/dashboard/index.html +++ b/farmq-admin/templates/dashboard/index.html @@ -39,7 +39,7 @@
-
{{ stats.online_machines }}
+
{{ stats.online_machines }}
온라인 머신
@@ -50,7 +50,7 @@
-
{{ 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