mirror of
https://github.com/andrey271192/vps_monitoring.git
synced 2026-09-20 11:55:34 +00:00
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>
This commit is contained in:
255
server/templates/telegram_app.html
Normal file
255
server/templates/telegram_app.html
Normal file
@@ -0,0 +1,255 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user