약국 상세 페이지 Headscale CLI 기반 완전 구현
- get_pharmacy_detail 함수를 Headscale CLI 기반으로 완전 재작성 - 기존 FARMQ DB의 MachineProfile 의존성 제거 - 약국 상세 페이지 템플릿 신규 생성 (detail.html) - 실시간 머신 상태 및 통계 표시: "머신: 2/4 온라인" - 사용자-약국 매핑을 통한 머신 연결 관리 - 연결된 머신 목록: IP, 상태, 등록방식, 마지막 접속시간 - datetime 객체 안전 처리로 strftime 오류 방지 - 머신별 상세보기/재연결/연결해제 액션 버튼 - 빈 상태 처리 및 사용자 가이드 제공 - 약국 기본정보: 사업자번호, 담당자, 연락처, 주소 - 네트워크 정보: Proxmox 호스트, Headscale 사용자 연결 - 상태별 아이콘 및 배지 시각화 (온라인/부분연결/오프라인) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user