diff --git a/farmq-admin/utils/database_new.py b/farmq-admin/utils/database_new.py
index 7a705a0..a95ccbc 100644
--- a/farmq-admin/utils/database_new.py
+++ b/farmq-admin/utils/database_new.py
@@ -323,54 +323,101 @@ def get_all_machines_with_details() -> List[Dict[str, Any]]:
close_session(headscale_session)
def get_machine_detail(machine_id: int) -> Optional[Dict[str, Any]]:
- """머신 상세 정보 조회"""
- farmq_session = get_farmq_session()
+ """Headscale CLI를 통한 머신 상세 정보 조회"""
+ import subprocess
+ import json
+ from datetime import datetime
try:
- machine = farmq_session.query(MachineProfile).filter(
- MachineProfile.id == machine_id
- ).first()
+ # 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)
+
+ # machine_id로 노드 찾기 (id 기준)
+ machine = None
+ for node in nodes_data:
+ if node.get('id') == machine_id:
+ machine = node
+ break
if not machine:
return None
- machine_data = machine.to_dict()
+ # 온라인 상태 체크
+ online_status = get_headscale_online_status()
+ node_name = machine.get('given_name') or machine.get('name', '')
+ is_online = online_status.get(node_name.lower(), False)
- # 약국 정보
- if machine.pharmacy_id:
- pharmacy = farmq_session.query(PharmacyInfo).filter(
- PharmacyInfo.id == machine.pharmacy_id
- ).first()
- if pharmacy:
- machine_data['pharmacy'] = pharmacy.to_dict()
+ # 시간 변환 함수
+ def convert_timestamp(ts_obj):
+ if isinstance(ts_obj, dict) and 'seconds' in ts_obj:
+ return datetime.fromtimestamp(ts_obj['seconds'])
+ elif isinstance(ts_obj, str):
+ try:
+ return datetime.fromisoformat(ts_obj.replace('Z', '+00:00'))
+ except:
+ return None
+ return None
- # 최근 모니터링 데이터 (24시간)
- cutoff_time = datetime.now() - timedelta(hours=24)
- metrics = farmq_session.query(MonitoringMetrics).filter(
- MonitoringMetrics.machine_profile_id == machine_id,
- MonitoringMetrics.collected_at > cutoff_time
- ).order_by(desc(MonitoringMetrics.collected_at)).limit(100).all()
+ # 엔드포인트 추출
+ endpoints = []
+ if 'endpoints' in machine:
+ endpoints = machine['endpoints']
- machine_data['metrics_history'] = [metric.to_dict() for metric in metrics]
+ # 약국 정보 매핑 (사용자명을 통해)
+ pharmacy_info = None
+ user_name = machine.get('user', {}).get('name', '')
+ if user_name:
+ farmq_session = get_farmq_session()
+ try:
+ pharmacy = farmq_session.query(PharmacyInfo).filter(
+ PharmacyInfo.headscale_user_name == user_name
+ ).first()
+ if pharmacy:
+ pharmacy_info = {
+ 'id': pharmacy.id,
+ 'name': pharmacy.pharmacy_name,
+ 'manager': pharmacy.manager_name,
+ 'address': pharmacy.address
+ }
+ finally:
+ farmq_session.close()
- # 최신 메트릭스
- if metrics:
- latest = metrics[0]
- machine_data['latest_metrics'] = latest.to_dict()
- machine_data['alerts'] = latest.get_alert_status()
-
- # 활성 알림들
- active_alerts = farmq_session.query(SystemAlert).filter(
- SystemAlert.machine_profile_id == machine_id,
- SystemAlert.status == 'active'
- ).order_by(desc(SystemAlert.created_at)).limit(10).all()
-
- machine_data['active_alerts'] = [alert.to_dict() for alert in active_alerts]
+ # 반환 데이터 구성
+ machine_data = {
+ 'id': machine.get('id'),
+ 'given_name': machine.get('given_name'),
+ 'hostname': machine.get('name'),
+ 'ipv4': machine.get('ip_addresses', [])[0] if machine.get('ip_addresses') else None,
+ 'ipv6': machine.get('ip_addresses', [])[1] if len(machine.get('ip_addresses', [])) > 1 else None,
+ 'machine_key': machine.get('machine_key'),
+ 'node_key': machine.get('node_key'),
+ 'disco_key': machine.get('disco_key'),
+ 'user': machine.get('user'),
+ 'last_seen': convert_timestamp(machine.get('last_seen')),
+ 'created_at': convert_timestamp(machine.get('created_at')),
+ 'register_method': 'CLI' if machine.get('register_method') == 1 else 'Pre-auth Key',
+ 'online': is_online,
+ 'endpoints': endpoints,
+ 'pharmacy': pharmacy_info,
+ # 헬퍼 메서드
+ 'get_endpoints': lambda: endpoints,
+ }
return machine_data
-
- finally:
- close_session(farmq_session)
+
+ except subprocess.CalledProcessError as e:
+ print(f"❌ Headscale CLI 오류: {e}")
+ return None
+ except Exception as e:
+ print(f"❌ 머신 상세 정보 조회 오류: {e}")
+ return None
# ==========================================
# Headscale Synchronization