### 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>
371 lines
15 KiB
HTML
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>© 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> |