Files
vps_monitoring/server/templates/telegram_app.html
Андрей Бобырев e6e88314d5 feat: initial VPS monitoring platform
- FastAPI backend with auth, server CRUD, metrics collection
- Web dashboard with dark theme, server cards, SSH terminal (xterm.js)
- Telegram bot with inline keyboards, server management, reboot
- Telegram Mini App for mobile monitoring
- Background monitoring via asyncssh (CPU, RAM, Disk, Network, Uptime)
- Alert system with configurable thresholds
- WebSocket SSH terminal in browser
- One-command install script
- Systemd service configuration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-20 05:47:28 +03:00

256 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPS Monitor</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--tg-theme-bg-color, #1a1a2e);
color: var(--tg-theme-text-color, #eee);
padding: 16px;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 20px;
padding: 16px;
background: var(--tg-theme-secondary-bg-color, #16213e);
border-radius: 12px;
}
.header h1 { font-size: 20px; margin-bottom: 4px; }
.header .subtitle { opacity: 0.7; font-size: 13px; }
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 20px;
}
.stat-card {
background: var(--tg-theme-secondary-bg-color, #16213e);
border-radius: 10px;
padding: 12px;
text-align: center;
}
.stat-card .value { font-size: 24px; font-weight: bold; }
.stat-card .label { font-size: 11px; opacity: 0.7; margin-top: 4px; }
.stat-card.online .value { color: #4ade80; }
.stat-card.offline .value { color: #f87171; }
.server-list { list-style: none; }
.server-item {
background: var(--tg-theme-secondary-bg-color, #16213e);
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: transform 0.1s;
}
.server-item:active { transform: scale(0.98); }
.server-info { flex: 1; }
.server-name { font-weight: 600; font-size: 15px; }
.server-host { font-size: 12px; opacity: 0.6; margin-top: 2px; }
.server-metrics {
display: flex;
gap: 8px;
margin-top: 8px;
}
.metric-badge {
font-size: 11px;
padding: 3px 8px;
border-radius: 6px;
background: rgba(255,255,255,0.1);
}
.metric-badge.warn { background: rgba(251, 191, 36, 0.2); color: #fbbf24; }
.metric-badge.crit { background: rgba(248, 113, 113, 0.2); color: #f87171; }
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-left: 12px;
}
.status-dot.online { background: #4ade80; box-shadow: 0 0 6px #4ade80; }
.status-dot.offline { background: #f87171; box-shadow: 0 0 6px #f87171; }
.server-detail {
display: none;
background: var(--tg-theme-secondary-bg-color, #16213e);
border-radius: 12px;
padding: 16px;
margin-bottom: 10px;
}
.server-detail.active { display: block; }
.detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.detail-row:last-child { border: none; }
.detail-label { opacity: 0.7; font-size: 13px; }
.detail-value { font-weight: 500; font-size: 13px; }
.action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 12px;
}
.action-btn {
padding: 10px;
border: none;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
background: var(--tg-theme-button-color, #0a84ff);
color: var(--tg-theme-button-text-color, #fff);
}
.action-btn.danger { background: #dc2626; }
.loading { text-align: center; padding: 40px; opacity: 0.5; }
.refresh-btn {
display: block;
width: 100%;
padding: 14px;
margin-top: 16px;
border: none;
border-radius: 12px;
font-size: 15px;
cursor: pointer;
background: var(--tg-theme-button-color, #0a84ff);
color: var(--tg-theme-button-text-color, #fff);
}
</style>
</head>
<body>
<div class="header">
<h1>🖥 VPS Monitor</h1>
<div class="subtitle">Мониторинг серверов</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="value" id="total">-</div>
<div class="label">Всего</div>
</div>
<div class="stat-card online">
<div class="value" id="online">-</div>
<div class="label">Online</div>
</div>
<div class="stat-card offline">
<div class="value" id="offline">-</div>
<div class="label">Offline</div>
</div>
</div>
<ul class="server-list" id="serverList">
<li class="loading">Загрузка...</li>
</ul>
<button class="refresh-btn" onclick="loadServers()">🔄 Обновить</button>
<script>
const tg = window.Telegram?.WebApp;
if (tg) {
tg.ready();
tg.expand();
}
let servers = [];
async function loadServers() {
try {
const resp = await fetch('/api/servers', {credentials: 'include'});
if (resp.status === 401) {
document.getElementById('serverList').innerHTML =
'<li class="loading">Требуется авторизация в панели</li>';
return;
}
servers = await resp.json();
renderServers();
} catch (e) {
document.getElementById('serverList').innerHTML =
'<li class="loading">Ошибка загрузки</li>';
}
}
function renderServers() {
const list = document.getElementById('serverList');
const online = servers.filter(s => s.metrics?.online).length;
const offline = servers.length - online;
document.getElementById('total').textContent = servers.length;
document.getElementById('online').textContent = online;
document.getElementById('offline').textContent = offline;
if (!servers.length) {
list.innerHTML = '<li class="loading">Нет серверов</li>';
return;
}
list.innerHTML = servers.map((srv, i) => {
const m = srv.metrics || {};
const isOnline = m.online;
const cpu = m.cpu_percent || 0;
const ram = m.ram_percent || 0;
const disk = m.disk_percent || 0;
const cpuClass = cpu > 90 ? 'crit' : cpu > 70 ? 'warn' : '';
const ramClass = ram > 90 ? 'crit' : ram > 70 ? 'warn' : '';
const diskClass = disk > 90 ? 'crit' : disk > 70 ? 'warn' : '';
return `
<li class="server-item" onclick="toggleDetail(${i})">
<div class="server-info">
<div class="server-name">${srv.name}</div>
<div class="server-host">${srv.host}</div>
${isOnline ? `
<div class="server-metrics">
<span class="metric-badge ${cpuClass}">CPU ${cpu}%</span>
<span class="metric-badge ${ramClass}">RAM ${ram}%</span>
<span class="metric-badge ${diskClass}">Disk ${disk}%</span>
</div>` : ''}
</div>
<div class="status-dot ${isOnline ? 'online' : 'offline'}"></div>
</li>
<div class="server-detail" id="detail-${i}">
<div class="detail-row"><span class="detail-label">Uptime</span><span class="detail-value">${m.uptime || 'N/A'}</span></div>
<div class="detail-row"><span class="detail-label">Load</span><span class="detail-value">${m.load_average || 'N/A'}</span></div>
<div class="detail-row"><span class="detail-label">RAM</span><span class="detail-value">${(m.ram_used_mb||0).toFixed(0)} / ${(m.ram_total_mb||0).toFixed(0)} MB</span></div>
<div class="detail-row"><span class="detail-label">Disk</span><span class="detail-value">${(m.disk_used_gb||0).toFixed(1)} / ${(m.disk_total_gb||0).toFixed(1)} GB</span></div>
<div class="detail-row"><span class="detail-label">Network In</span><span class="detail-value">${(m.network_in_mb||0).toFixed(0)} MB</span></div>
<div class="detail-row"><span class="detail-label">Network Out</span><span class="detail-value">${(m.network_out_mb||0).toFixed(0)} MB</span></div>
<div class="action-buttons">
<button class="action-btn" onclick="event.stopPropagation(); rebootServer('${srv.id || srv.host}')">🔄 Reboot</button>
<button class="action-btn danger" onclick="event.stopPropagation(); deleteServer('${srv.id || srv.host}')">🗑 Удалить</button>
</div>
</div>
`;
}).join('');
}
function toggleDetail(idx) {
const el = document.getElementById(`detail-${idx}`);
el.classList.toggle('active');
}
async function rebootServer(id) {
if (!confirm('Перезагрузить сервер?')) return;
await fetch(`/api/servers/${id}/reboot`, {method: 'POST', credentials: 'include'});
if (tg) tg.showAlert('Команда перезагрузки отправлена');
}
async function deleteServer(id) {
if (!confirm('Удалить сервер из мониторинга?')) return;
await fetch(`/api/servers/${id}`, {method: 'DELETE', credentials: 'include'});
loadServers();
}
loadServers();
setInterval(loadServers, 30000);
</script>
</body>
</html>