머신 상세 페이지 Headscale CLI 기반 완전 재구현
- get_machine_detail 함수를 Headscale CLI 기반으로 완전 교체 - 기존 FARMQ DB 의존성에서 실시간 Headscale 데이터로 전환 - strftime 템플릿 오류 완전 해결 (datetime 객체 타입 체크 추가) - 실제 머신 정보 표시: 호스트명, IP 주소, 온라인 상태, 사용자 정보 - 약국 정보 매핑: Headscale 사용자명을 통한 약국 연동 - 시간 정보 인간화: "N시간 전", "N분 전" 형식으로 표시 - 네트워크 정보: IPv4/IPv6 주소, 엔드포인트, 키 정보 표시 - 조건부 모니터링 데이터 표시 (향후 확장 대비) - 전체 머신 상세 페이지 기능 정상화 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
56b72629f9
commit
f3965a67fd
@ -117,7 +117,33 @@ def create_app(config_name=None):
|
|||||||
hostname = details.get('hostname', 'Unknown')
|
hostname = details.get('hostname', 'Unknown')
|
||||||
print(f"✅ Rendering detail page for machine: {hostname}")
|
print(f"✅ Rendering detail page for machine: {hostname}")
|
||||||
|
|
||||||
return render_template('machines/detail.html', machine=details)
|
# 템플릿에 필요한 추가 변수들
|
||||||
|
is_online = details.get('online', False)
|
||||||
|
last_seen = details.get('last_seen')
|
||||||
|
|
||||||
|
# 시간 형식화
|
||||||
|
if last_seen:
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
if isinstance(last_seen, datetime):
|
||||||
|
now = datetime.now(timezone.utc) if last_seen.tzinfo else datetime.now()
|
||||||
|
delta = now - last_seen
|
||||||
|
if delta.days > 0:
|
||||||
|
last_seen_humanized = f"{delta.days}일 전"
|
||||||
|
elif delta.seconds > 3600:
|
||||||
|
last_seen_humanized = f"{delta.seconds // 3600}시간 전"
|
||||||
|
elif delta.seconds > 60:
|
||||||
|
last_seen_humanized = f"{delta.seconds // 60}분 전"
|
||||||
|
else:
|
||||||
|
last_seen_humanized = "방금 전"
|
||||||
|
else:
|
||||||
|
last_seen_humanized = "알 수 없음"
|
||||||
|
else:
|
||||||
|
last_seen_humanized = "알 수 없음"
|
||||||
|
|
||||||
|
return render_template('machines/detail.html',
|
||||||
|
machine=details,
|
||||||
|
is_online=is_online,
|
||||||
|
last_seen_humanized=last_seen_humanized)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error in machine_detail route: {e}")
|
print(f"❌ Error in machine_detail route: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|||||||
@ -96,7 +96,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>등록일</th>
|
<th>등록일</th>
|
||||||
<td>{{ machine.created_at.strftime('%Y년 %m월 %d일 %H:%M') if machine.created_at else '알 수 없음' }}</td>
|
<td>
|
||||||
|
{% if machine.created_at %}
|
||||||
|
{% if machine.created_at.__class__.__name__ == 'datetime' %}
|
||||||
|
{{ machine.created_at.strftime('%Y년 %m월 %d일 %H:%M') }}
|
||||||
|
{% else %}
|
||||||
|
{{ machine.created_at }}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
알 수 없음
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -141,7 +151,11 @@
|
|||||||
<th>마지막 접속</th>
|
<th>마지막 접속</th>
|
||||||
<td>
|
<td>
|
||||||
{% if machine.last_seen %}
|
{% if machine.last_seen %}
|
||||||
|
{% if machine.last_seen.__class__.__name__ == 'datetime' %}
|
||||||
{{ machine.last_seen.strftime('%Y-%m-%d %H:%M:%S') }}
|
{{ machine.last_seen.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||||
|
{% else %}
|
||||||
|
{{ machine.last_seen }}
|
||||||
|
{% endif %}
|
||||||
<br><small class="text-muted">{{ last_seen_humanized }}</small>
|
<br><small class="text-muted">{{ last_seen_humanized }}</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">알 수 없음</span>
|
<span class="text-muted">알 수 없음</span>
|
||||||
@ -210,7 +224,16 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0"><i class="fas fa-chart-line"></i> 현재 상태</h5>
|
<h5 class="mb-0"><i class="fas fa-chart-line"></i> 현재 상태</h5>
|
||||||
<small class="text-muted">최종 업데이트: {{ latest_monitoring.collected_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
{% if latest_monitoring and latest_monitoring.collected_at %}
|
||||||
|
<small class="text-muted">
|
||||||
|
최종 업데이트:
|
||||||
|
{% if latest_monitoring.collected_at.__class__.__name__ == 'datetime' %}
|
||||||
|
{{ latest_monitoring.collected_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||||
|
{% else %}
|
||||||
|
{{ latest_monitoring.collected_at }}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@ -323,54 +323,101 @@ def get_all_machines_with_details() -> List[Dict[str, Any]]:
|
|||||||
close_session(headscale_session)
|
close_session(headscale_session)
|
||||||
|
|
||||||
def get_machine_detail(machine_id: int) -> Optional[Dict[str, Any]]:
|
def get_machine_detail(machine_id: int) -> Optional[Dict[str, Any]]:
|
||||||
"""머신 상세 정보 조회"""
|
"""Headscale CLI를 통한 머신 상세 정보 조회"""
|
||||||
farmq_session = get_farmq_session()
|
import subprocess
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
machine = farmq_session.query(MachineProfile).filter(
|
# Headscale CLI에서 노드 목록 가져오기
|
||||||
MachineProfile.id == machine_id
|
result = subprocess.run(
|
||||||
).first()
|
['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:
|
if not machine:
|
||||||
return None
|
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:
|
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
|
||||||
|
|
||||||
|
# 엔드포인트 추출
|
||||||
|
endpoints = []
|
||||||
|
if 'endpoints' in machine:
|
||||||
|
endpoints = machine['endpoints']
|
||||||
|
|
||||||
|
# 약국 정보 매핑 (사용자명을 통해)
|
||||||
|
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(
|
pharmacy = farmq_session.query(PharmacyInfo).filter(
|
||||||
PharmacyInfo.id == machine.pharmacy_id
|
PharmacyInfo.headscale_user_name == user_name
|
||||||
).first()
|
).first()
|
||||||
if pharmacy:
|
if pharmacy:
|
||||||
machine_data['pharmacy'] = pharmacy.to_dict()
|
pharmacy_info = {
|
||||||
|
'id': pharmacy.id,
|
||||||
|
'name': pharmacy.pharmacy_name,
|
||||||
|
'manager': pharmacy.manager_name,
|
||||||
|
'address': pharmacy.address
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
farmq_session.close()
|
||||||
|
|
||||||
# 최근 모니터링 데이터 (24시간)
|
# 반환 데이터 구성
|
||||||
cutoff_time = datetime.now() - timedelta(hours=24)
|
machine_data = {
|
||||||
metrics = farmq_session.query(MonitoringMetrics).filter(
|
'id': machine.get('id'),
|
||||||
MonitoringMetrics.machine_profile_id == machine_id,
|
'given_name': machine.get('given_name'),
|
||||||
MonitoringMetrics.collected_at > cutoff_time
|
'hostname': machine.get('name'),
|
||||||
).order_by(desc(MonitoringMetrics.collected_at)).limit(100).all()
|
'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_data['metrics_history'] = [metric.to_dict() for metric in metrics]
|
'machine_key': machine.get('machine_key'),
|
||||||
|
'node_key': machine.get('node_key'),
|
||||||
# 최신 메트릭스
|
'disco_key': machine.get('disco_key'),
|
||||||
if metrics:
|
'user': machine.get('user'),
|
||||||
latest = metrics[0]
|
'last_seen': convert_timestamp(machine.get('last_seen')),
|
||||||
machine_data['latest_metrics'] = latest.to_dict()
|
'created_at': convert_timestamp(machine.get('created_at')),
|
||||||
machine_data['alerts'] = latest.get_alert_status()
|
'register_method': 'CLI' if machine.get('register_method') == 1 else 'Pre-auth Key',
|
||||||
|
'online': is_online,
|
||||||
# 활성 알림들
|
'endpoints': endpoints,
|
||||||
active_alerts = farmq_session.query(SystemAlert).filter(
|
'pharmacy': pharmacy_info,
|
||||||
SystemAlert.machine_profile_id == machine_id,
|
# 헬퍼 메서드
|
||||||
SystemAlert.status == 'active'
|
'get_endpoints': lambda: endpoints,
|
||||||
).order_by(desc(SystemAlert.created_at)).limit(10).all()
|
}
|
||||||
|
|
||||||
machine_data['active_alerts'] = [alert.to_dict() for alert in active_alerts]
|
|
||||||
|
|
||||||
return machine_data
|
return machine_data
|
||||||
|
|
||||||
finally:
|
except subprocess.CalledProcessError as e:
|
||||||
close_session(farmq_session)
|
print(f"❌ Headscale CLI 오류: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 머신 상세 정보 조회 오류: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Headscale Synchronization
|
# Headscale Synchronization
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user