inpaintServer/web/index.html

261 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inpaint Server 모니터링 대시보드</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: auto; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.header h1 { color: #2c3e50; }
.status { padding: 8px 15px; border-radius: 5px; color: white; font-weight: bold; }
.status.ok { background-color: #27ae60; }
.status.error { background-color: #c0392b; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
.card { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.card h3 { margin-top: 0; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; color: #34495e; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.info-item { display: flex; justify-content: space-between; padding: 5px 0; }
.info-item span:first-child { font-weight: bold; color: #555; }
#gpuUsage { width: 100%; background-color: #ecf0f1; border-radius: 5px; overflow: hidden; }
#gpuUsage div { height: 20px; background-color: #3498db; text-align: center; color: white; line-height: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background-color: #f2f2f2; }
.setting-item { display: flex; align-items: center; gap: 10px; margin-top: 15px; }
.setting-item input { flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
.setting-item button { padding: 8px 12px; border: none; background-color: #007bff; color: white; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; }
.setting-item button:hover { background-color: #0056b3; }
.status-message { margin-left: 10px; font-size: 0.9em; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎨 Inpaint Server 대시보드</h1>
<div id="healthStatus" class="status">확인 중...</div>
</div>
<div class="grid">
<div class="card">
<h3>시스템 정보</h3>
<div class="info-grid">
<div class="info-item"><span>OS:</span> <span id="os">N/A</span></div>
<div class="info-item"><span>Python:</span> <span id="pythonVersion">N/A</span></div>
<div class="info-item"><span>CPU 사용량:</span> <span id="cpuUsage">N/A</span></div>
<div class="info-item"><span>RAM 사용량:</span> <span id="ramUsage">N/A</span></div>
</div>
</div>
<div class="card">
<h3>GPU 정보</h3>
<div class="info-item"><span>GPU:</span> <span id="gpuName">N/A</span></div>
<div class="info-item"><span>드라이버:</span> <span id="gpuDriver">N/A</span></div>
<div class="info-item"><span>VRAM:</span> <span id="vramUsage">N/A</span></div>
<div class="info-item"><span>사용량:</span> <span id="gpuUtil">N/A</span></div>
<div id="gpuUsage">
<div id="gpuUsageBar" style="width: 0%;">0%</div>
</div>
</div>
<div class="card">
<h3>모델 로딩 통계 (ms)</h3>
<canvas id="modelLoadChart"></canvas>
</div>
<div class="card">
<h3>모델 처리 시간 (ms)</h3>
<canvas id="modelTimeChart"></canvas>
</div>
<div class="card">
<h3>API 응답 시간 (ms)</h3>
<canvas id="apiResponseChart"></canvas>
</div>
</div>
<div class="card">
<h3>설정</h3>
<div class="setting-item">
<label for="webhookUrl">Discord 웹훅 URL:</label>
<input type="text" id="webhookUrl" placeholder="Discord 웹훅 URL을 입력하세요">
<button onclick="saveWebhookUrl()">저장</button>
<p id="webhookStatus" class="status-message"></p>
</div>
</div>
</div>
<script>
const ws = new WebSocket(`ws://${window.location.host}/ws/monitoring`);
let modelLoadChart, apiResponseChart, modelTimeChart;
async function fetchHealthStatus() {
try {
const response = await fetch('/api/v1/health');
const data = await response.json();
const statusEl = document.getElementById('healthStatus');
if (response.ok) {
statusEl.textContent = `정상 (Uptime: ${new Date(data.uptime * 1000).toISOString().substr(11, 8)})`;
statusEl.className = 'status ok';
} else {
throw new Error(data.detail || 'Health check failed');
}
} catch (error) {
console.error('Health check error:', error);
const statusEl = document.getElementById('healthStatus');
statusEl.textContent = '오류';
statusEl.className = 'status error';
}
}
function createChart(ctx, type, data, options) {
return new Chart(ctx, { type, data, options });
}
function setupCharts() {
const commonOptions = {
responsive: true,
scales: { y: { beginAtZero: true } }
};
modelLoadChart = createChart(document.getElementById('modelLoadChart').getContext('2d'), 'bar', { labels: [], datasets: [{ label: 'Loading Time (ms)', data: [], backgroundColor: 'rgba(54, 162, 235, 0.6)' }] }, commonOptions);
apiResponseChart = createChart(document.getElementById('apiResponseChart').getContext('2d'), 'line', { labels: [], datasets: [{ label: 'Avg Response Time (ms)', data: [], borderColor: 'rgba(255, 99, 132, 1)', fill: false }] }, commonOptions);
modelTimeChart = createChart(document.getElementById('modelTimeChart').getContext('2d'), 'bar', { labels: [], datasets: [{ label: 'Avg Processing Time (ms)', data: [], backgroundColor: 'rgba(75, 192, 192, 0.6)' }] }, commonOptions);
}
function updateChart(chart, labels, data) {
chart.data.labels = labels;
chart.data.datasets[0].data = data;
chart.update();
}
async function fetchSystemInfo() {
// Placeholder for system info fetch
}
async function fetchGpuInfo() {
// Placeholder for GPU info fetch
}
async function fetchModelLoadStats() {
try {
const response = await fetch('/api/v1/stats/model-load');
const data = await response.json();
const labels = Object.keys(data);
const values = Object.values(data).map(d => d.avg_load_time_ms);
updateChart(modelLoadChart, labels, values);
} catch (error) {
console.error('Failed to fetch model load stats:', error);
}
}
async function fetchApiStats() {
try {
const response = await fetch('/api/v1/stats/api');
const data = await response.json();
const labels = Object.keys(data.endpoints);
const values = Object.values(data.endpoints).map(e => e.avg_response_time_ms);
updateChart(apiResponseChart, labels, values);
} catch (error) {
console.error('Failed to fetch API stats:', error);
}
}
async function fetchProcessingStats() {
try {
const response = await fetch('/api/v1/stats/processing');
const data = await response.json();
delete data.total; // Total is not needed for this chart
const labels = Object.keys(data);
const values = Object.values(data).map(m => m.avg_time);
updateChart(modelTimeChart, labels, values);
} catch (error) {
console.error('Failed to fetch processing stats:', error);
}
}
async function fetchInitialData() {
await fetchHealthStatus();
await fetchSystemInfo();
await fetchGpuInfo();
await fetchModelLoadStats();
await fetchApiStats();
await fetchProcessingStats();
await fetchWebhookUrl();
}
async function fetchWebhookUrl() {
try {
const response = await fetch('/api/v1/webhook');
if (!response.ok) throw new Error('Server response was not ok.');
const data = await response.json();
document.getElementById('webhookUrl').value = data.url || '';
} catch (error) {
console.error('Failed to load webhook URL:', error);
const statusEl = document.getElementById('webhookStatus');
statusEl.textContent = 'URL 로드 실패';
statusEl.style.color = 'red';
}
}
async function saveWebhookUrl() {
const url = document.getElementById('webhookUrl').value;
const statusEl = document.getElementById('webhookStatus');
statusEl.textContent = '저장 중...';
statusEl.style.color = 'orange';
try {
const response = await fetch('/api/v1/webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await response.json();
if (response.ok) {
statusEl.textContent = '성공적으로 저장되었습니다!';
statusEl.style.color = 'green';
} else {
statusEl.textContent = '저장 실패: ' + (data.detail || 'Server error');
statusEl.style.color = 'red';
}
} catch (error) {
console.error('Failed to save webhook URL:', error);
statusEl.textContent = '저장 요청 실패.';
statusEl.style.color = 'red';
}
setTimeout(() => { statusEl.textContent = ''; }, 3000);
}
ws.onopen = function() {
console.log("WebSocket connection established");
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
document.getElementById('os').textContent = data.system.os;
document.getElementById('pythonVersion').textContent = data.system.python_version;
document.getElementById('cpuUsage').textContent = `${data.system.cpu_percent.toFixed(1)}%`;
document.getElementById('ramUsage').textContent = `${(data.system.ram.used / 1024**3).toFixed(2)} / ${(data.system.ram.total / 1024**3).toFixed(2)} GB (${data.system.ram.percent.toFixed(1)}%)`;
if (data.gpu) {
document.getElementById('gpuName').textContent = data.gpu.name;
document.getElementById('gpuDriver').textContent = data.gpu.driver_version;
document.getElementById('vramUsage').textContent = `${(data.gpu.memory.used / 1024).toFixed(2)} / ${(data.gpu.memory.total / 1024).toFixed(2)} GB (${data.gpu.memory.percent.toFixed(1)}%)`;
document.getElementById('gpuUtil').textContent = `${data.gpu.utilization.toFixed(1)}%`;
const gpuUsageBar = document.getElementById('gpuUsageBar');
gpuUsageBar.style.width = `${data.gpu.utilization}%`;
gpuUsageBar.textContent = `${data.gpu.utilization.toFixed(1)}%`;
}
};
ws.onclose = function() {
console.log("WebSocket connection closed");
document.getElementById('healthStatus').textContent = '연결 끊김';
document.getElementById('healthStatus').className = 'status error';
};
window.onload = () => {
setupCharts();
fetchInitialData();
setInterval(fetchInitialData, 5000); // Poll REST endpoints periodically as well
};
</script>
</body>
</html>