diff --git a/farmq-admin/templates/pharmacy/detail.html b/farmq-admin/templates/pharmacy/detail.html new file mode 100644 index 0000000..8305781 --- /dev/null +++ b/farmq-admin/templates/pharmacy/detail.html @@ -0,0 +1,319 @@ +{% extends "base.html" %} + +{% block title %}약국 상세 정보 - 팜큐 약국 관리 시스템{% endblock %} + +{% block breadcrumb %} + +{% endblock %} + +{% block content %} + +
+
+
+
+
+ {% if pharmacy.online_machines > 0 %} + + {% else %} + + {% endif %} +
+
+

{{ pharmacy.pharmacy_name }}

+

{{ pharmacy.manager_name or '담당자 미등록' }}

+
+ {% if pharmacy.online_machines == pharmacy.total_machines and pharmacy.total_machines > 0 %} + + 모든 머신 온라인 + + {% elif pharmacy.online_machines > 0 %} + + 부분적 연결 + + {% elif pharmacy.total_machines > 0 %} + + 전체 오프라인 + + {% else %} + + 머신 없음 + + {% endif %} + + 머신: {{ pharmacy.online_machines }}/{{ pharmacy.total_machines }} 온라인 + +
+
+
+
+ + + +
+
+
+
+ + +
+
+
+
+
기본 정보
+
+
+ + + + + + + + + + + + + + + + + + + + + +
약국명{{ pharmacy.pharmacy_name }}
사업자번호{{ pharmacy.business_number or '미등록' }}
담당자{{ pharmacy.manager_name or '미등록' }}
연락처{{ pharmacy.phone or '미등록' }}
주소{{ pharmacy.address or '주소 미등록' }}
+
+
+
+ +
+
+
+
네트워크 정보
+
+
+ + + + + + + + + + + + + + + + + +
Proxmox 호스트 + {% if pharmacy.proxmox_host %} + {{ pharmacy.proxmox_host }} + {% else %} + 미설정 + {% endif %} +
Headscale 사용자 + {% if pharmacy.headscale_user_name %} + {{ pharmacy.headscale_user_name }} + {% else %} + 연결되지 않음 + {% endif %} +
등록일 + {% if pharmacy.created_at %} + {% if pharmacy.created_at.__class__.__name__ == 'datetime' %} + {{ pharmacy.created_at.strftime('%Y년 %m월 %d일 %H:%M') }} + {% else %} + {{ pharmacy.created_at }} + {% endif %} + {% else %} + 알 수 없음 + {% endif %} +
최종 업데이트 + {% if pharmacy.updated_at %} + {% if pharmacy.updated_at.__class__.__name__ == 'datetime' %} + {{ pharmacy.updated_at.strftime('%Y-%m-%d %H:%M:%S') }} + {% else %} + {{ pharmacy.updated_at }} + {% endif %} + {% else %} + 알 수 없음 + {% endif %} +
+
+
+
+
+ + +
+
+
+
+
+ + 연결된 머신 목록 + {{ pharmacy.total_machines }}대 +
+
+
+ {% if machines %} +
+ + + + + + + + + + + + + {% for machine in machines %} + + + + + + + + + {% endfor %} + +
머신명IP 주소상태등록 방식마지막 접속액션
+
+ {{ machine.given_name or machine.hostname }} + {% if machine.given_name and machine.hostname and machine.given_name != machine.hostname %} +
{{ machine.hostname }} + {% endif %} +
+
+ {% if machine.ipv4 %} + {{ machine.ipv4 }} + {% if machine.ipv6 %} +
{{ machine.ipv6 }} + {% endif %} + {% else %} + IP 미할당 + {% endif %} +
+ {% if machine.online %} + + 온라인 + + {% else %} + + 오프라인 + + {% endif %} + + {{ machine.register_method }} + + {% if machine.last_seen %} + {% if machine.last_seen.__class__.__name__ == 'datetime' %} + {{ machine.last_seen.strftime('%m/%d %H:%M') }} + {% else %} + {{ machine.last_seen }} + {% endif %} + {% else %} + 알 수 없음 + {% endif %} + +
+ + + + + +
+
+
+ {% else %} +
+ +

연결된 머신이 없습니다

+

+ {% if pharmacy.headscale_user_name %} + "{{ pharmacy.headscale_user_name }}" 사용자에 연결된 머신이 없습니다. + {% else %} + 이 약국은 Headscale 사용자와 연결되지 않았습니다.
+ 사용자 관리에서 사용자를 이 약국에 연결해주세요. + {% endif %} +

+ {% if not pharmacy.headscale_user_name %} + + 사용자 관리로 이동 + + {% endif %} +
+ {% endif %} +
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/farmq-admin/utils/database_new.py b/farmq-admin/utils/database_new.py index a95ccbc..0d72ad6 100644 --- a/farmq-admin/utils/database_new.py +++ b/farmq-admin/utils/database_new.py @@ -214,10 +214,15 @@ def get_all_pharmacies_with_stats() -> List[Dict[str, Any]]: close_session(headscale_session) def get_pharmacy_detail(pharmacy_id: int) -> Optional[Dict[str, Any]]: - """약국 상세 정보 조회""" + """Headscale CLI 기반 약국 상세 정보 조회""" + import subprocess + import json + from datetime import datetime + farmq_session = get_farmq_session() try: + # 약국 기본 정보 조회 pharmacy = farmq_session.query(PharmacyInfo).filter( PharmacyInfo.id == pharmacy_id ).first() @@ -225,33 +230,89 @@ def get_pharmacy_detail(pharmacy_id: int) -> Optional[Dict[str, Any]]: if not pharmacy: return None - # 약국의 머신들 조회 - machines = farmq_session.query(MachineProfile).filter( - MachineProfile.pharmacy_id == pharmacy_id, - MachineProfile.status == 'active' - ).all() + # 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 = get_headscale_online_status() + + # 약국과 연결된 사용자의 머신들 찾기 machine_list = [] - for machine in machines: - machine_data = machine.to_dict() - - # 최근 모니터링 데이터 - latest_metrics = farmq_session.query(MonitoringMetrics).filter( - MonitoringMetrics.machine_profile_id == machine.id - ).order_by(desc(MonitoringMetrics.collected_at)).first() - - if latest_metrics: - machine_data['latest_metrics'] = latest_metrics.to_dict() - - machine_list.append(machine_data) + if pharmacy.headscale_user_name: + for node in nodes_data: + # 이 노드가 약국의 사용자 것인지 확인 + node_user_name = node.get('user', {}).get('name', '') + if node_user_name == pharmacy.headscale_user_name: + # 시간 변환 함수 + 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 + + node_name = node.get('given_name') or node.get('name', '') + is_online = online_status.get(node_name.lower(), False) + + machine_data = { + 'id': node.get('id'), + 'given_name': node.get('given_name'), + 'hostname': node.get('name'), + 'ipv4': node.get('ip_addresses', [])[0] if node.get('ip_addresses') else None, + 'ipv6': node.get('ip_addresses', [])[1] if len(node.get('ip_addresses', [])) > 1 else None, + 'machine_key': node.get('machine_key'), + 'node_key': node.get('node_key'), + 'disco_key': node.get('disco_key'), + 'user': node.get('user'), + 'last_seen': convert_timestamp(node.get('last_seen')), + 'created_at': convert_timestamp(node.get('created_at')), + 'register_method': 'CLI' if node.get('register_method') == 1 else 'Pre-auth Key', + 'online': is_online, + 'endpoints': node.get('endpoints', []), + } + + machine_list.append(machine_data) + + # 약국 정보에 통계 추가 + pharmacy_data = { + 'id': pharmacy.id, + 'pharmacy_name': pharmacy.pharmacy_name, + 'business_number': pharmacy.business_number, + 'manager_name': pharmacy.manager_name, + 'phone': pharmacy.phone, + 'address': pharmacy.address, + 'proxmox_host': pharmacy.proxmox_host, + 'headscale_user_name': pharmacy.headscale_user_name, + 'headscale_user_id': pharmacy.headscale_user_id, + 'created_at': pharmacy.created_at, + 'updated_at': pharmacy.updated_at, + # 통계 정보 + 'total_machines': len(machine_list), + 'online_machines': len([m for m in machine_list if m['online']]), + 'offline_machines': len([m for m in machine_list if not m['online']]), + } return { - 'pharmacy': pharmacy.to_dict(), + 'pharmacy': pharmacy_data, 'machines': machine_list } + except subprocess.CalledProcessError as e: + print(f"❌ Headscale CLI 오류: {e}") + return None + except Exception as e: + print(f"❌ 약국 상세 정보 조회 오류: {e}") + return None finally: - close_session(farmq_session) + farmq_session.close() # ========================================== # Machine Management