headscale-tailscale-replace.../farmq-admin/templates/base.html
시골약사 7aa08682b8 Implement smart Magic DNS copy with automatic port detection
### Magic DNS Smart Copy Features:
- **PBS servers**: Automatically append `:8007` port when copying
- **PVE servers**: Automatically append `:8006` port when copying
- **Other machines**: Copy Magic DNS address without port (existing behavior)

### UI Improvements:
- PBS servers: Blue button with `:8007` port hint
- PVE servers: Orange button with `:8006` port hint
- Enhanced tooltips with service-specific port information
- Visual distinction between different server types

### PBS Backup Server Monitoring:
- Complete PBS API integration with authentication
- Real-time backup/restore task monitoring with detailed logs
- Namespace-separated backup visualization with color coding
- Datastore usage monitoring and status tracking
- Task history with success/failure status and error details

### Technical Implementation:
- Smart port detection via JavaScript `addSmartPort()` function
- Jinja2 template logic for conditional button styling
- PBS API endpoints for comprehensive backup monitoring
- Enhanced clipboard functionality with user feedback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-14 10:48:47 +09:00

371 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}팜큐 약국 관리 시스템{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Custom CSS -->
<style>
:root {
--farmq-primary: #2c5282;
--farmq-secondary: #4299e1;
--farmq-success: #48bb78;
--farmq-warning: #ed8936;
--farmq-danger: #f56565;
--farmq-light: #f7fafc;
--farmq-dark: #2d3748;
}
body {
background-color: var(--farmq-light);
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.navbar-brand {
font-weight: bold;
color: var(--farmq-primary) !important;
}
.sidebar {
min-height: calc(100vh - 56px);
background: linear-gradient(180deg, var(--farmq-primary) 0%, var(--farmq-secondary) 100%);
}
.sidebar .nav-link {
color: rgba(255, 255, 255, 0.8);
border-radius: 8px;
margin: 2px 0;
transition: all 0.3s ease;
}
.sidebar .nav-link:hover,
.sidebar .nav-link.active {
background-color: rgba(255, 255, 255, 0.1);
color: white;
transform: translateX(5px);
}
.main-content {
padding: 2rem;
}
.card {
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
border-radius: 12px;
transition: transform 0.2s ease-in-out;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.stat-card {
background: linear-gradient(135deg, var(--farmq-primary) 0%, var(--farmq-secondary) 100%);
color: white;
}
.stat-card .card-body {
padding: 2rem;
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
line-height: 1;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
.alert-item {
border-left: 4px solid;
border-radius: 0 8px 8px 0;
margin-bottom: 0.5rem;
}
.alert-warning {
border-left-color: var(--farmq-warning);
}
.alert-danger {
border-left-color: var(--farmq-danger);
}
.status-online {
color: var(--farmq-success);
}
.status-offline {
color: var(--farmq-danger);
}
.status-warning {
color: var(--farmq-warning);
}
.footer {
background-color: var(--farmq-dark);
color: white;
text-align: center;
padding: 1rem;
margin-top: 3rem;
}
/* 반응형 디자인 */
@media (max-width: 768px) {
.sidebar {
min-height: auto;
}
.main-content {
padding: 1rem;
}
.stat-number {
font-size: 1.8rem;
}
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 상단 네비게이션 -->
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">
<i class="fas fa-hospital"></i> PharmQ Super Admin (PSA)
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user-circle"></i> 관리자
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#"><i class="fas fa-cog"></i> 설정</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-question-circle"></i> 도움말</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-sign-out-alt"></i> 로그아웃</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- 사이드바 -->
<nav class="col-md-3 col-lg-2 d-md-block sidebar collapse">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}" href="{{ url_for('dashboard') }}">
<i class="fas fa-tachometer-alt"></i> 대시보드
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'pharmacy' in request.endpoint %}active{% endif %}" href="{{ url_for('pharmacy_list') }}">
<i class="fas fa-store"></i> 약국 관리
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'users' in request.endpoint %}active{% endif %}" href="{{ url_for('users_list') }}">
<i class="fas fa-users"></i> PQON 사용자 관리
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'machine' in request.endpoint %}active{% endif %}" href="{{ url_for('machine_list') }}">
<i class="fas fa-desktop"></i> 머신 관리
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'vms' in request.endpoint %}active{% endif %}" href="{{ url_for('vm_list') }}">
<i class="fas fa-tv"></i> VM 관리 (VNC)
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'subscription' in request.endpoint %}active{% endif %}" href="{{ url_for('subscriptions_page') }}">
<i class="fas fa-box text-success"></i> 구독 서비스 관리
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'revenue' in request.endpoint %}active{% endif %}" href="{{ url_for('revenue_dashboard') }}">
<i class="fas fa-chart-pie text-warning"></i> 매출 대시보드
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint and 'pbs' in request.endpoint %}active{% endif %}" href="{{ url_for('pbs_monitoring') }}">
<i class="fas fa-server text-info"></i> PBS 백업 서버
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<i class="fas fa-chart-line"></i> 모니터링
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<i class="fas fa-cog"></i> 설정
</a>
</li>
</ul>
<hr class="my-3" style="border-color: rgba(255,255,255,0.2);">
<!-- 빠른 링크 -->
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="https://medivault.co.kr/" target="_blank">
<i class="fas fa-external-link-alt"></i> Medivault
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://git.0bin.in/thug0bin/headscale-tailscale-replacement/src/branch/feature/working-headscale-setup" target="_blank">
<i class="fab fa-git-alt"></i> Git Repository
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="refreshData()">
<i class="fas fa-sync-alt"></i> 데이터 새로고침
</a>
</li>
</ul>
</div>
</nav>
<!-- 메인 콘텐츠 -->
<main class="col-md-9 ms-sm-auto col-lg-10 main-content">
{% block breadcrumb %}{% endblock %}
<!-- 알림 메시지 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</div>
</div>
<!-- 푸터 -->
<footer class="footer">
<div class="container">
<span>&copy; 2025 PharmQ. Powered by Medivault</span>
</div>
</footer>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 공통 JavaScript -->
<script>
// 데이터 새로고침
function refreshData() {
location.reload();
}
// 실시간 업데이트 (5초마다)
setInterval(function() {
// 현재 페이지가 대시보드인 경우 실시간 업데이트
if (window.location.pathname === '/') {
updateDashboardStats();
}
}, 5000);
function updateDashboardStats() {
fetch('/api/dashboard/stats')
.then(response => response.json())
.then(data => {
// 통계 업데이트
document.getElementById('total-pharmacies').textContent = data.total_pharmacies;
document.getElementById('online-machines').textContent = data.online_machines;
document.getElementById('offline-machines').textContent = data.offline_machines;
document.getElementById('avg-temp').textContent = data.avg_cpu_temp + '°C';
})
.catch(error => console.error('Stats update failed:', error));
}
// 차트 생성 함수
function createDoughnutChart(elementId, value, label, color) {
const ctx = document.getElementById(elementId);
if (!ctx) return;
new Chart(ctx, {
type: 'doughnut',
data: {
datasets: [{
data: [value, 100 - value],
backgroundColor: [color, '#e2e8f0'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '75%',
plugins: {
legend: {
display: false
}
}
}
});
}
// 토스트 알림 표시
function showToast(message, type = 'info') {
const toastHtml = `
<div class="toast align-items-center text-bg-${type} border-0" role="alert">
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
const toastContainer = document.getElementById('toast-container');
if (toastContainer) {
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toast = new bootstrap.Toast(toastContainer.lastElementChild);
toast.show();
}
}
// 준비 중 기능 알림
function showComingSoon(featureName) {
showToast(`🚧 ${featureName} 기능은 현재 개발 중입니다. 곧 만나보실 수 있습니다!`, 'warning');
}
</script>
{% block extra_js %}{% endblock %}
<!-- 토스트 컨테이너 -->
<div id="toast-container" class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1200;"></div>
</body>
</html>