약국 상세 페이지 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:
2025-09-11 12:14:08 +09:00
parent f3965a67fd
commit 8dbf35d955
2 changed files with 400 additions and 20 deletions

View File

@@ -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