mirror of
https://github.com/andrey271192/vps_monitoring.git
synced 2026-09-20 11:55:34 +00:00
feat: add light/dark theme, RU/EN i18n, instructions page, green/red monitoring
- Light and dark theme toggle with localStorage persistence - Russian and English language support (full i18n) - Instructions & cheat sheet modal with install/SSH/API/commands - Green pulsing dot for online, red for offline servers - Color-coded metrics: green (ok), yellow (warn >70%), red (crit >90%) - Top bar on server cards: green=online, red=offline Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
:root {
|
/* Dark Theme (default) */
|
||||||
|
:root, [data-theme="dark"] {
|
||||||
--bg-primary: #0f0f23;
|
--bg-primary: #0f0f23;
|
||||||
--bg-secondary: #1a1a3e;
|
--bg-secondary: #1a1a3e;
|
||||||
--bg-card: #1e1e4a;
|
--bg-card: #1e1e4a;
|
||||||
@@ -6,12 +7,36 @@
|
|||||||
--text-secondary: #a0a0cc;
|
--text-secondary: #a0a0cc;
|
||||||
--accent: #6366f1;
|
--accent: #6366f1;
|
||||||
--accent-hover: #818cf8;
|
--accent-hover: #818cf8;
|
||||||
--success: #4ade80;
|
--success: #22c55e;
|
||||||
--danger: #f87171;
|
--success-bg: rgba(34, 197, 94, 0.12);
|
||||||
--warning: #fbbf24;
|
--danger: #ef4444;
|
||||||
|
--danger-bg: rgba(239, 68, 68, 0.12);
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--warning-bg: rgba(245, 158, 11, 0.12);
|
||||||
--border: rgba(255, 255, 255, 0.08);
|
--border: rgba(255, 255, 255, 0.08);
|
||||||
--radius: 12px;
|
--radius: 12px;
|
||||||
--shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
--shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||||
|
--input-bg: #0f0f23;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Theme */
|
||||||
|
[data-theme="light"] {
|
||||||
|
--bg-primary: #f8fafc;
|
||||||
|
--bg-secondary: #ffffff;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--accent: #4f46e5;
|
||||||
|
--accent-hover: #6366f1;
|
||||||
|
--success: #16a34a;
|
||||||
|
--success-bg: rgba(22, 163, 74, 0.1);
|
||||||
|
--danger: #dc2626;
|
||||||
|
--danger-bg: rgba(220, 38, 38, 0.1);
|
||||||
|
--warning: #d97706;
|
||||||
|
--warning-bg: rgba(217, 119, 6, 0.1);
|
||||||
|
--border: rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
--input-bg: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -25,6 +50,7 @@ body {
|
|||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
transition: background 0.3s, color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Login Page */
|
/* Login Page */
|
||||||
@@ -33,7 +59,6 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
@@ -81,7 +106,7 @@ body {
|
|||||||
.form-group input, .form-group select, .form-group textarea {
|
.form-group input, .form-group select, .form-group textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: var(--bg-primary);
|
background: var(--input-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -114,8 +139,8 @@ body {
|
|||||||
.error-msg {
|
.error-msg {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: rgba(248, 113, 113, 0.1);
|
background: var(--danger-bg);
|
||||||
border: 1px solid rgba(248, 113, 113, 0.3);
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -142,6 +167,7 @@ header {
|
|||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
@@ -156,10 +182,29 @@ header {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon:hover {
|
.btn-icon:hover {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main Content */
|
/* Main Content */
|
||||||
@@ -183,6 +228,11 @@ main {
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item .stat-label {
|
.stat-item .stat-label {
|
||||||
@@ -204,6 +254,9 @@ main {
|
|||||||
/* Toolbar */
|
/* Toolbar */
|
||||||
.toolbar {
|
.toolbar {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar .btn-primary {
|
.toolbar .btn-primary {
|
||||||
@@ -211,6 +264,22 @@ main {
|
|||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar .btn-secondary {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar .btn-secondary:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
/* Server Grid */
|
/* Server Grid */
|
||||||
.servers-grid {
|
.servers-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -225,6 +294,25 @@ main {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-card.online::before {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-card.offline::before {
|
||||||
|
background: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-card:hover {
|
.server-card:hover {
|
||||||
@@ -255,18 +343,50 @@ main {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.online {
|
.status-badge.online {
|
||||||
background: rgba(74, 222, 128, 0.15);
|
background: var(--success-bg);
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.offline {
|
.status-badge.offline {
|
||||||
background: rgba(248, 113, 113, 0.15);
|
background: var(--danger-bg);
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.online {
|
||||||
|
background: var(--success);
|
||||||
|
box-shadow: 0 0 8px var(--success);
|
||||||
|
animation: pulse-green 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.offline {
|
||||||
|
background: var(--danger);
|
||||||
|
box-shadow: 0 0 8px var(--danger);
|
||||||
|
animation: pulse-red 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-green {
|
||||||
|
0%, 100% { box-shadow: 0 0 4px var(--success); }
|
||||||
|
50% { box-shadow: 0 0 12px var(--success); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-red {
|
||||||
|
0%, 100% { box-shadow: 0 0 4px var(--danger); }
|
||||||
|
50% { box-shadow: 0 0 12px var(--danger); }
|
||||||
|
}
|
||||||
|
|
||||||
.metrics-grid {
|
.metrics-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
@@ -288,6 +408,7 @@ main {
|
|||||||
.metric-item .value {
|
.metric-item .value {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-item .value.warn { color: var(--warning); }
|
.metric-item .value.warn { color: var(--warning); }
|
||||||
@@ -335,6 +456,7 @@ main {
|
|||||||
.server-card-actions button:hover {
|
.server-card-actions button:hover {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-card-actions button.danger:hover {
|
.server-card-actions button.danger:hover {
|
||||||
@@ -374,6 +496,10 @@ main {
|
|||||||
height: 80vh;
|
height: 80vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content.modal-wide {
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -401,6 +527,7 @@ main {
|
|||||||
|
|
||||||
.btn-close:hover {
|
.btn-close:hover {
|
||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content h3 {
|
.modal-content h3 {
|
||||||
@@ -411,6 +538,59 @@ main {
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Instructions */
|
||||||
|
.instructions-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instr-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instr-card h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instr-card p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instr-card code {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--success);
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instr-card .cmd-list {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instr-card .cmd-list li {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Terminal */
|
/* Terminal */
|
||||||
#terminal-container {
|
#terminal-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -426,4 +606,5 @@ main {
|
|||||||
.servers-grid { grid-template-columns: 1fr; }
|
.servers-grid { grid-template-columns: 1fr; }
|
||||||
.stats-bar { flex-direction: column; gap: 8px; }
|
.stats-bar { flex-direction: column; gap: 8px; }
|
||||||
header { padding: 12px 16px; }
|
header { padding: 12px 16px; }
|
||||||
|
.header-right { gap: 4px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ let servers = [];
|
|||||||
let currentTerminal = null;
|
let currentTerminal = null;
|
||||||
let currentWs = null;
|
let currentWs = null;
|
||||||
|
|
||||||
|
// i18n render
|
||||||
|
function renderPage() {
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n');
|
||||||
|
el.textContent = t(key);
|
||||||
|
});
|
||||||
|
document.getElementById('langBtn').textContent = currentLang.toUpperCase();
|
||||||
|
document.getElementById('addServerBtn').textContent = t('addServer');
|
||||||
|
renderServers();
|
||||||
|
}
|
||||||
|
|
||||||
// Load servers
|
// Load servers
|
||||||
async function loadServers() {
|
async function loadServers() {
|
||||||
try {
|
try {
|
||||||
@@ -27,7 +38,7 @@ function renderServers() {
|
|||||||
document.getElementById('offline-count').textContent = offline;
|
document.getElementById('offline-count').textContent = offline;
|
||||||
|
|
||||||
if (!servers.length) {
|
if (!servers.length) {
|
||||||
grid.innerHTML = '<div style="text-align:center;padding:60px;opacity:0.5;grid-column:1/-1">Нет серверов. Нажмите "+ Добавить сервер"</div>';
|
grid.innerHTML = `<div style="text-align:center;padding:60px;opacity:0.5;grid-column:1/-1">${t('noServers')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,13 +56,14 @@ function renderServers() {
|
|||||||
const id = srv.id || srv.host;
|
const id = srv.id || srv.host;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="server-card" onclick="showServerDetail('${id}')">
|
<div class="server-card ${isOnline ? 'online' : 'offline'}" onclick="showServerDetail('${id}')">
|
||||||
<div class="server-card-header">
|
<div class="server-card-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="name">${srv.name}</div>
|
<div class="name">${srv.name}</div>
|
||||||
<div class="host">${srv.host}:${srv.port || 22}</div>
|
<div class="host">${srv.host}:${srv.port || 22}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="status-badge ${isOnline ? 'online' : 'offline'}">
|
<span class="status-badge ${isOnline ? 'online' : 'offline'}">
|
||||||
|
<span class="status-dot ${isOnline ? 'online' : 'offline'}"></span>
|
||||||
${isOnline ? 'Online' : 'Offline'}
|
${isOnline ? 'Online' : 'Offline'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +85,7 @@ function renderServers() {
|
|||||||
<div class="progress-bar"><div class="fill ${diskClass || 'ok'}" style="width:${disk}%"></div></div>
|
<div class="progress-bar"><div class="fill ${diskClass || 'ok'}" style="width:${disk}%"></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : '<div style="padding:20px 0;text-align:center;opacity:0.5">Нет данных</div>'}
|
` : `<div style="padding:20px 0;text-align:center;opacity:0.5">${t('noData')}</div>`}
|
||||||
<div class="server-card-actions">
|
<div class="server-card-actions">
|
||||||
<button onclick="event.stopPropagation(); openSSH('${id}', '${srv.name}')">💻 SSH</button>
|
<button onclick="event.stopPropagation(); openSSH('${id}', '${srv.name}')">💻 SSH</button>
|
||||||
<button onclick="event.stopPropagation(); rebootServer('${id}')">🔄 Reboot</button>
|
<button onclick="event.stopPropagation(); rebootServer('${id}')">🔄 Reboot</button>
|
||||||
@@ -96,38 +108,41 @@ function showServerDetail(id) {
|
|||||||
<div style="display:grid;gap:12px">
|
<div style="display:grid;gap:12px">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Host</label>
|
<label>Host</label>
|
||||||
<div style="padding:8px;background:var(--bg-primary);border-radius:6px;font-family:monospace">${srv.host}:${srv.port || 22}</div>
|
<div style="padding:8px;background:var(--input-bg);border-radius:6px;font-family:monospace">${srv.host}:${srv.port || 22}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Status</label>
|
<label>${t('status')}</label>
|
||||||
<div><span class="status-badge ${m.online ? 'online' : 'offline'}">${m.online ? 'Online' : 'Offline'}</span></div>
|
<div><span class="status-badge ${m.online ? 'online' : 'offline'}">
|
||||||
|
<span class="status-dot ${m.online ? 'online' : 'offline'}"></span>
|
||||||
|
${m.online ? 'Online' : 'Offline'}
|
||||||
|
</span></div>
|
||||||
</div>
|
</div>
|
||||||
${m.online ? `
|
${m.online ? `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Uptime</label>
|
<label>${t('uptime')}</label>
|
||||||
<div style="padding:8px;background:var(--bg-primary);border-radius:6px">${m.uptime || 'N/A'}</div>
|
<div style="padding:8px;background:var(--input-bg);border-radius:6px">${m.uptime || 'N/A'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Load Average</label>
|
<label>${t('load')}</label>
|
||||||
<div style="padding:8px;background:var(--bg-primary);border-radius:6px">${m.load_average || 'N/A'}</div>
|
<div style="padding:8px;background:var(--input-bg);border-radius:6px">${m.load_average || 'N/A'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>RAM</label>
|
<label>RAM</label>
|
||||||
<div style="padding:8px;background:var(--bg-primary);border-radius:6px">${(m.ram_used_mb||0).toFixed(0)} / ${(m.ram_total_mb||0).toFixed(0)} MB (${m.ram_percent||0}%)</div>
|
<div style="padding:8px;background:var(--input-bg);border-radius:6px">${(m.ram_used_mb||0).toFixed(0)} / ${(m.ram_total_mb||0).toFixed(0)} MB (${m.ram_percent||0}%)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Disk</label>
|
<label>Disk</label>
|
||||||
<div style="padding:8px;background:var(--bg-primary);border-radius:6px">${(m.disk_used_gb||0).toFixed(1)} / ${(m.disk_total_gb||0).toFixed(1)} GB (${m.disk_percent||0}%)</div>
|
<div style="padding:8px;background:var(--input-bg);border-radius:6px">${(m.disk_used_gb||0).toFixed(1)} / ${(m.disk_total_gb||0).toFixed(1)} GB (${m.disk_percent||0}%)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Network</label>
|
<label>${t('network')}</label>
|
||||||
<div style="padding:8px;background:var(--bg-primary);border-radius:6px">↓ ${(m.network_in_mb||0).toFixed(0)} MB / ↑ ${(m.network_out_mb||0).toFixed(0)} MB</div>
|
<div style="padding:8px;background:var(--input-bg);border-radius:6px">↓ ${(m.network_in_mb||0).toFixed(0)} MB / ↑ ${(m.network_out_mb||0).toFixed(0)} MB</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
${srv.description ? `
|
${srv.description ? `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Описание</label>
|
<label>${t('description')}</label>
|
||||||
<div style="padding:8px;background:var(--bg-primary);border-radius:6px">${srv.description}</div>
|
<div style="padding:8px;background:var(--input-bg);border-radius:6px">${srv.description}</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -136,17 +151,74 @@ function showServerDetail(id) {
|
|||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
function showInstructions() {
|
||||||
|
const content = document.getElementById('instructions-content');
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="instructions-section">
|
||||||
|
<div class="instr-card">
|
||||||
|
<h4>📥 ${t('instr_install')}</h4>
|
||||||
|
<p>${t('instr_add_server_text').includes('Click') ? 'One command to install on any Ubuntu/Debian server:' : 'Одна команда для установки на Ubuntu/Debian:'}</p>
|
||||||
|
<code>${t('instr_install_text')}</code>
|
||||||
|
</div>
|
||||||
|
<div class="instr-card">
|
||||||
|
<h4>➕ ${t('instr_add_server')}</h4>
|
||||||
|
<p>${t('instr_add_server_text')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="instr-card">
|
||||||
|
<h4>💻 ${t('instr_ssh')}</h4>
|
||||||
|
<p>${t('instr_ssh_text')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="instr-card">
|
||||||
|
<h4>🤖 ${t('instr_telegram')}</h4>
|
||||||
|
<p>${t('instr_telegram_text')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="instr-card">
|
||||||
|
<h4>🔄 ${t('instr_reboot')}</h4>
|
||||||
|
<p>${t('instr_reboot_text')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="instr-card">
|
||||||
|
<h4>⚠️ ${t('instr_thresholds')}</h4>
|
||||||
|
<p>${t('instr_thresholds_text')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="instr-card">
|
||||||
|
<h4>🔧 ${t('instr_commands')}</h4>
|
||||||
|
<ul class="cmd-list">
|
||||||
|
<li>📊 ${t('instr_cmd_status')}</li>
|
||||||
|
<li>🔄 ${t('instr_cmd_restart')}</li>
|
||||||
|
<li>📋 ${t('instr_cmd_logs')}</li>
|
||||||
|
<li>⬆️ ${t('instr_cmd_update')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="instr-card">
|
||||||
|
<h4>🔌 ${t('instr_api')}</h4>
|
||||||
|
<ul class="cmd-list">
|
||||||
|
<li>GET /api/servers — list all</li>
|
||||||
|
<li>POST /api/servers — add server</li>
|
||||||
|
<li>DELETE /api/servers/{id} — remove</li>
|
||||||
|
<li>POST /api/servers/{id}/reboot — reboot</li>
|
||||||
|
<li>POST /api/servers/{id}/exec — run command</li>
|
||||||
|
<li>GET /api/settings — get settings</li>
|
||||||
|
<li>PUT /api/settings — update settings</li>
|
||||||
|
<li>WS /ws/ssh/{id} — SSH terminal</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('instructionsModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
// SSH Terminal
|
// SSH Terminal
|
||||||
function openSSH(id, name) {
|
function openSSH(id, name) {
|
||||||
const modal = document.getElementById('sshModal');
|
const modal = document.getElementById('sshModal');
|
||||||
document.getElementById('ssh-title').textContent = `SSH — ${name}`;
|
document.getElementById('ssh-title').textContent = `${t('sshTerminal')} — ${name}`;
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
const container = document.getElementById('terminal-container');
|
const container = document.getElementById('terminal-container');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
if (typeof Terminal === 'undefined') {
|
if (typeof Terminal === 'undefined') {
|
||||||
container.innerHTML = '<div style="padding:20px;color:#f87171">xterm.js не загружен</div>';
|
container.innerHTML = '<div style="padding:20px;color:var(--danger)">xterm.js not loaded</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,22 +277,22 @@ function closeSSH() {
|
|||||||
|
|
||||||
// Server actions
|
// Server actions
|
||||||
async function rebootServer(id) {
|
async function rebootServer(id) {
|
||||||
if (!confirm('Перезагрузить сервер?')) return;
|
if (!confirm(t('rebootConfirm'))) return;
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/servers/${id}/reboot`, {method: 'POST', credentials: 'include'});
|
await fetch(`/api/servers/${id}/reboot`, {method: 'POST', credentials: 'include'});
|
||||||
alert('Команда перезагрузки отправлена');
|
alert(t('rebootSent'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Ошибка: ' + e.message);
|
alert('Error: ' + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteServer(id) {
|
async function deleteServer(id) {
|
||||||
if (!confirm('Удалить сервер из мониторинга?')) return;
|
if (!confirm(t('deleteConfirm'))) return;
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/servers/${id}`, {method: 'DELETE', credentials: 'include'});
|
await fetch(`/api/servers/${id}`, {method: 'DELETE', credentials: 'include'});
|
||||||
loadServers();
|
loadServers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Ошибка: ' + e.message);
|
alert('Error: ' + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,10 +326,10 @@ document.getElementById('addServerForm')?.addEventListener('submit', async (e) =
|
|||||||
loadServers();
|
loadServers();
|
||||||
} else {
|
} else {
|
||||||
const err = await resp.json();
|
const err = await resp.json();
|
||||||
alert(err.detail || 'Ошибка');
|
alert(err.detail || 'Error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Ошибка: ' + e.message);
|
alert('Error: ' + e.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -301,9 +373,9 @@ document.getElementById('settingsForm')?.addEventListener('submit', async (e) =>
|
|||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
closeModal('settingsModal');
|
closeModal('settingsModal');
|
||||||
alert('Настройки сохранены');
|
alert(t('settingsSaved'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Ошибка: ' + e.message);
|
alert('Error: ' + e.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -322,18 +394,21 @@ async function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Click outside modal to close
|
// Click outside modal to close
|
||||||
document.querySelectorAll('.modal').forEach(modal => {
|
document.addEventListener('click', (e) => {
|
||||||
modal.addEventListener('click', (e) => {
|
if (e.target.classList.contains('modal')) {
|
||||||
if (e.target === modal) {
|
if (e.target.id === 'sshModal') {
|
||||||
if (modal.id === 'sshModal') {
|
|
||||||
closeSSH();
|
closeSSH();
|
||||||
} else {
|
} else {
|
||||||
modal.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
loadServers();
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
applyTheme(currentTheme);
|
||||||
|
renderPage();
|
||||||
|
loadServers();
|
||||||
|
});
|
||||||
|
|
||||||
setInterval(loadServers, 30000);
|
setInterval(loadServers, 30000);
|
||||||
|
|||||||
169
server/static/js/i18n.js
Normal file
169
server/static/js/i18n.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
const translations = {
|
||||||
|
ru: {
|
||||||
|
title: "VPS Monitoring",
|
||||||
|
login: "Вход",
|
||||||
|
username: "Логин",
|
||||||
|
password: "Пароль",
|
||||||
|
loginBtn: "Войти",
|
||||||
|
invalidCreds: "Неверный логин или пароль",
|
||||||
|
connError: "Ошибка подключения",
|
||||||
|
total: "Всего",
|
||||||
|
online: "Online",
|
||||||
|
offline: "Offline",
|
||||||
|
addServer: "+ Добавить сервер",
|
||||||
|
refresh: "Обновить",
|
||||||
|
settings: "Настройки",
|
||||||
|
logout: "Выход",
|
||||||
|
serverName: "Название",
|
||||||
|
host: "IP / Host",
|
||||||
|
sshPort: "Порт SSH",
|
||||||
|
sshLogin: "Логин",
|
||||||
|
sshPassword: "Пароль",
|
||||||
|
description: "Описание",
|
||||||
|
add: "Добавить",
|
||||||
|
save: "Сохранить",
|
||||||
|
cancel: "Отмена",
|
||||||
|
auth: "Авторизация",
|
||||||
|
newPassword: "Новый пароль",
|
||||||
|
leaveEmpty: "Оставьте пустым, чтобы не менять",
|
||||||
|
telegram: "Telegram",
|
||||||
|
botToken: "Bot Token",
|
||||||
|
chatId: "Chat ID",
|
||||||
|
monitoring: "Мониторинг",
|
||||||
|
pollInterval: "Интервал опроса (сек)",
|
||||||
|
cpuThreshold: "Порог CPU (%)",
|
||||||
|
ramThreshold: "Порог RAM (%)",
|
||||||
|
diskThreshold: "Порог Disk (%)",
|
||||||
|
settingsSaved: "Настройки сохранены",
|
||||||
|
rebootConfirm: "Перезагрузить сервер?",
|
||||||
|
rebootSent: "Команда перезагрузки отправлена",
|
||||||
|
deleteConfirm: "Удалить сервер из мониторинга?",
|
||||||
|
noServers: "Нет серверов. Нажмите \"+ Добавить сервер\"",
|
||||||
|
noData: "Нет данных",
|
||||||
|
sshTerminal: "SSH Терминал",
|
||||||
|
instructions: "Инструкции",
|
||||||
|
quickStart: "Быстрый старт",
|
||||||
|
cheatSheet: "Шпаргалка",
|
||||||
|
uptime: "Время работы",
|
||||||
|
load: "Нагрузка",
|
||||||
|
network: "Сеть",
|
||||||
|
networkIn: "Входящий",
|
||||||
|
networkOut: "Исходящий",
|
||||||
|
status: "Статус",
|
||||||
|
theme: "Тема",
|
||||||
|
language: "Язык",
|
||||||
|
dark: "Тёмная",
|
||||||
|
light: "Светлая",
|
||||||
|
instructions_title: "Инструкции и шпаргалки",
|
||||||
|
instr_install: "Установка",
|
||||||
|
instr_install_text: "curl -sSL https://raw.githubusercontent.com/andrey271192/vps_monitoring/main/install.sh | bash",
|
||||||
|
instr_add_server: "Добавление сервера",
|
||||||
|
instr_add_server_text: "Нажмите '+ Добавить сервер', введите IP, логин и пароль SSH",
|
||||||
|
instr_ssh: "SSH подключение",
|
||||||
|
instr_ssh_text: "Нажмите кнопку 'SSH' на карточке сервера для терминала в браузере",
|
||||||
|
instr_telegram: "Telegram бот",
|
||||||
|
instr_telegram_text: "Отправьте /start боту. Получайте алерты при проблемах",
|
||||||
|
instr_reboot: "Перезагрузка",
|
||||||
|
instr_reboot_text: "Через панель: кнопка 'Reboot'. Через бот: Управление → Сервер → Перезагрузить",
|
||||||
|
instr_commands: "Команды управления сервисом",
|
||||||
|
instr_cmd_status: "systemctl status vps-monitoring",
|
||||||
|
instr_cmd_restart: "systemctl restart vps-monitoring",
|
||||||
|
instr_cmd_logs: "journalctl -u vps-monitoring -f",
|
||||||
|
instr_cmd_update: "cd /opt/vps-monitoring && git pull && systemctl restart vps-monitoring",
|
||||||
|
instr_api: "API endpoints",
|
||||||
|
instr_thresholds: "Пороги алертов",
|
||||||
|
instr_thresholds_text: "Настройки → Мониторинг. По умолчанию: CPU 90%, RAM 90%, Disk 90%",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
title: "VPS Monitoring",
|
||||||
|
login: "Login",
|
||||||
|
username: "Username",
|
||||||
|
password: "Password",
|
||||||
|
loginBtn: "Sign In",
|
||||||
|
invalidCreds: "Invalid username or password",
|
||||||
|
connError: "Connection error",
|
||||||
|
total: "Total",
|
||||||
|
online: "Online",
|
||||||
|
offline: "Offline",
|
||||||
|
addServer: "+ Add Server",
|
||||||
|
refresh: "Refresh",
|
||||||
|
settings: "Settings",
|
||||||
|
logout: "Logout",
|
||||||
|
serverName: "Name",
|
||||||
|
host: "IP / Host",
|
||||||
|
sshPort: "SSH Port",
|
||||||
|
sshLogin: "Login",
|
||||||
|
sshPassword: "Password",
|
||||||
|
description: "Description",
|
||||||
|
add: "Add",
|
||||||
|
save: "Save",
|
||||||
|
cancel: "Cancel",
|
||||||
|
auth: "Authentication",
|
||||||
|
newPassword: "New password",
|
||||||
|
leaveEmpty: "Leave empty to keep current",
|
||||||
|
telegram: "Telegram",
|
||||||
|
botToken: "Bot Token",
|
||||||
|
chatId: "Chat ID",
|
||||||
|
monitoring: "Monitoring",
|
||||||
|
pollInterval: "Poll interval (sec)",
|
||||||
|
cpuThreshold: "CPU threshold (%)",
|
||||||
|
ramThreshold: "RAM threshold (%)",
|
||||||
|
diskThreshold: "Disk threshold (%)",
|
||||||
|
settingsSaved: "Settings saved",
|
||||||
|
rebootConfirm: "Reboot server?",
|
||||||
|
rebootSent: "Reboot command sent",
|
||||||
|
deleteConfirm: "Remove server from monitoring?",
|
||||||
|
noServers: "No servers. Click \"+ Add Server\"",
|
||||||
|
noData: "No data",
|
||||||
|
sshTerminal: "SSH Terminal",
|
||||||
|
instructions: "Instructions",
|
||||||
|
quickStart: "Quick Start",
|
||||||
|
cheatSheet: "Cheat Sheet",
|
||||||
|
uptime: "Uptime",
|
||||||
|
load: "Load",
|
||||||
|
network: "Network",
|
||||||
|
networkIn: "In",
|
||||||
|
networkOut: "Out",
|
||||||
|
status: "Status",
|
||||||
|
theme: "Theme",
|
||||||
|
language: "Language",
|
||||||
|
dark: "Dark",
|
||||||
|
light: "Light",
|
||||||
|
instructions_title: "Instructions & Cheat Sheet",
|
||||||
|
instr_install: "Installation",
|
||||||
|
instr_install_text: "curl -sSL https://raw.githubusercontent.com/andrey271192/vps_monitoring/main/install.sh | bash",
|
||||||
|
instr_add_server: "Adding a server",
|
||||||
|
instr_add_server_text: "Click '+ Add Server', enter IP, SSH login and password",
|
||||||
|
instr_ssh: "SSH connection",
|
||||||
|
instr_ssh_text: "Click 'SSH' button on server card for browser terminal",
|
||||||
|
instr_telegram: "Telegram bot",
|
||||||
|
instr_telegram_text: "Send /start to the bot. Get alerts when issues arise",
|
||||||
|
instr_reboot: "Reboot",
|
||||||
|
instr_reboot_text: "Panel: 'Reboot' button. Bot: Manage → Server → Reboot",
|
||||||
|
instr_commands: "Service management commands",
|
||||||
|
instr_cmd_status: "systemctl status vps-monitoring",
|
||||||
|
instr_cmd_restart: "systemctl restart vps-monitoring",
|
||||||
|
instr_cmd_logs: "journalctl -u vps-monitoring -f",
|
||||||
|
instr_cmd_update: "cd /opt/vps-monitoring && git pull && systemctl restart vps-monitoring",
|
||||||
|
instr_api: "API endpoints",
|
||||||
|
instr_thresholds: "Alert thresholds",
|
||||||
|
instr_thresholds_text: "Settings → Monitoring. Default: CPU 90%, RAM 90%, Disk 90%",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentLang = localStorage.getItem('vps_lang') || 'ru';
|
||||||
|
|
||||||
|
function t(key) {
|
||||||
|
return translations[currentLang]?.[key] || translations['en']?.[key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLang(lang) {
|
||||||
|
currentLang = lang;
|
||||||
|
localStorage.setItem('vps_lang', lang);
|
||||||
|
document.documentElement.lang = lang;
|
||||||
|
if (typeof renderPage === 'function') renderPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLang() {
|
||||||
|
setLang(currentLang === 'ru' ? 'en' : 'ru');
|
||||||
|
}
|
||||||
16
server/static/js/theme.js
Normal file
16
server/static/js/theme.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
let currentTheme = localStorage.getItem('vps_theme') || 'dark';
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
currentTheme = theme;
|
||||||
|
localStorage.setItem('vps_theme', theme);
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
const btn = document.getElementById('themeToggleBtn');
|
||||||
|
if (btn) btn.textContent = theme === 'dark' ? '☀️' : '🌙';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply on load
|
||||||
|
document.addEventListener('DOMContentLoaded', () => applyTheme(currentTheme));
|
||||||
@@ -1,82 +1,85 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>VPS Monitoring</title>
|
<title>VPS Monitoring</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.3.0/xterm.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.3.0/xterm.min.css">
|
||||||
|
<script src="/static/js/theme.js"></script>
|
||||||
|
<script src="/static/js/i18n.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1>🖥 VPS Monitoring</h1>
|
<h1 id="pageTitle">🖥 VPS Monitoring</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button class="btn-icon" onclick="refreshAll()" title="Обновить">🔄</button>
|
<button class="lang-btn" id="langBtn" onclick="toggleLang()">RU</button>
|
||||||
<button class="btn-icon" onclick="showSettings()" title="Настройки">⚙️</button>
|
<button class="btn-icon" id="themeToggleBtn" onclick="toggleTheme()" title="Theme">🌙</button>
|
||||||
<button class="btn-icon" onclick="logout()" title="Выход">🚪</button>
|
<button class="btn-icon" onclick="refreshAll()" title="Refresh">🔄</button>
|
||||||
|
<button class="btn-icon" onclick="showInstructions()" title="Instructions">📖</button>
|
||||||
|
<button class="btn-icon" onclick="showSettings()" title="Settings">⚙️</button>
|
||||||
|
<button class="btn-icon" onclick="logout()" title="Logout">🚪</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="stats-bar">
|
<div class="stats-bar">
|
||||||
<div class="stat-item" id="stat-total">
|
<div class="stat-item" id="stat-total">
|
||||||
<span class="stat-label">Всего</span>
|
<span class="stat-label" data-i18n="total">Всего</span>
|
||||||
<span class="stat-value" id="total-count">0</span>
|
<span class="stat-value" id="total-count">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item online" id="stat-online">
|
<div class="stat-item online" id="stat-online">
|
||||||
<span class="stat-label">Online</span>
|
<span class="stat-label" data-i18n="online">Online</span>
|
||||||
<span class="stat-value" id="online-count">0</span>
|
<span class="stat-value" id="online-count">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item offline" id="stat-offline">
|
<div class="stat-item offline" id="stat-offline">
|
||||||
<span class="stat-label">Offline</span>
|
<span class="stat-label" data-i18n="offline">Offline</span>
|
||||||
<span class="stat-value" id="offline-count">0</span>
|
<span class="stat-value" id="offline-count">0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button class="btn-primary" onclick="showAddServer()">+ Добавить сервер</button>
|
<button class="btn-primary" id="addServerBtn" onclick="showAddServer()">+ Добавить сервер</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="servers-grid" id="servers-grid">
|
<div class="servers-grid" id="servers-grid"></div>
|
||||||
<!-- Server cards rendered here -->
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Add Server Modal -->
|
<!-- Add Server Modal -->
|
||||||
<div class="modal" id="addServerModal" style="display:none">
|
<div class="modal" id="addServerModal" style="display:none">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Добавить сервер</h2>
|
<h2 data-i18n="addServer">Добавить сервер</h2>
|
||||||
<button class="btn-close" onclick="closeModal('addServerModal')">×</button>
|
<button class="btn-close" onclick="closeModal('addServerModal')">×</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="addServerForm">
|
<form id="addServerForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Название</label>
|
<label data-i18n="serverName">Название</label>
|
||||||
<input type="text" name="name" required placeholder="My Server">
|
<input type="text" name="name" required placeholder="My Server">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>IP / Host</label>
|
<label data-i18n="host">IP / Host</label>
|
||||||
<input type="text" name="host" required placeholder="192.168.1.1">
|
<input type="text" name="host" required placeholder="192.168.1.1">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Порт SSH</label>
|
<label data-i18n="sshPort">Порт SSH</label>
|
||||||
<input type="number" name="port" value="22">
|
<input type="number" name="port" value="22">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Логин</label>
|
<label data-i18n="sshLogin">Логин</label>
|
||||||
<input type="text" name="username" value="root">
|
<input type="text" name="username" value="root">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Пароль</label>
|
<label data-i18n="sshPassword">Пароль</label>
|
||||||
<input type="password" name="password" placeholder="Пароль SSH">
|
<input type="password" name="password" placeholder="SSH Password">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Описание</label>
|
<label data-i18n="description">Описание</label>
|
||||||
<input type="text" name="description" placeholder="Описание сервера">
|
<input type="text" name="description" placeholder="">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary">Добавить</button>
|
<button type="submit" class="btn-primary" data-i18n="add">Добавить</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,50 +88,61 @@
|
|||||||
<div class="modal" id="settingsModal" style="display:none">
|
<div class="modal" id="settingsModal" style="display:none">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Настройки</h2>
|
<h2 data-i18n="settings">Настройки</h2>
|
||||||
<button class="btn-close" onclick="closeModal('settingsModal')">×</button>
|
<button class="btn-close" onclick="closeModal('settingsModal')">×</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="settingsForm">
|
<form id="settingsForm">
|
||||||
<h3>Авторизация</h3>
|
<h3 data-i18n="auth">Авторизация</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Логин</label>
|
<label data-i18n="username">Логин</label>
|
||||||
<input type="text" name="admin_login" id="set_login">
|
<input type="text" name="admin_login" id="set_login">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Новый пароль</label>
|
<label data-i18n="newPassword">Новый пароль</label>
|
||||||
<input type="password" name="admin_password" id="set_password" placeholder="Оставьте пустым, чтобы не менять">
|
<input type="password" name="admin_password" id="set_password" placeholder="">
|
||||||
</div>
|
</div>
|
||||||
<h3>Telegram</h3>
|
<h3 data-i18n="telegram">Telegram</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Bot Token</label>
|
<label data-i18n="botToken">Bot Token</label>
|
||||||
<input type="text" name="telegram_bot_token" id="set_tg_token">
|
<input type="text" name="telegram_bot_token" id="set_tg_token">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Chat ID</label>
|
<label data-i18n="chatId">Chat ID</label>
|
||||||
<input type="text" name="telegram_chat_id" id="set_tg_chat">
|
<input type="text" name="telegram_chat_id" id="set_tg_chat">
|
||||||
</div>
|
</div>
|
||||||
<h3>Мониторинг</h3>
|
<h3 data-i18n="monitoring">Мониторинг</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Интервал опроса (сек)</label>
|
<label data-i18n="pollInterval">Интервал опроса (сек)</label>
|
||||||
<input type="number" name="monitor_interval" id="set_interval" value="60">
|
<input type="number" name="monitor_interval" id="set_interval" value="60">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Порог CPU (%)</label>
|
<label data-i18n="cpuThreshold">Порог CPU (%)</label>
|
||||||
<input type="number" name="alert_cpu_threshold" id="set_cpu" value="90">
|
<input type="number" name="alert_cpu_threshold" id="set_cpu" value="90">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Порог RAM (%)</label>
|
<label data-i18n="ramThreshold">Порог RAM (%)</label>
|
||||||
<input type="number" name="alert_ram_threshold" id="set_ram" value="90">
|
<input type="number" name="alert_ram_threshold" id="set_ram" value="90">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Порог Disk (%)</label>
|
<label data-i18n="diskThreshold">Порог Disk (%)</label>
|
||||||
<input type="number" name="alert_disk_threshold" id="set_disk" value="90">
|
<input type="number" name="alert_disk_threshold" id="set_disk" value="90">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary">Сохранить</button>
|
<button type="submit" class="btn-primary" data-i18n="save">Сохранить</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Instructions Modal -->
|
||||||
|
<div class="modal" id="instructionsModal" style="display:none">
|
||||||
|
<div class="modal-content modal-wide">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="instructions_title">Инструкции и шпаргалки</h2>
|
||||||
|
<button class="btn-close" onclick="closeModal('instructionsModal')">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="instructions-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SSH Terminal Modal -->
|
<!-- SSH Terminal Modal -->
|
||||||
<div class="modal" id="sshModal" style="display:none">
|
<div class="modal" id="sshModal" style="display:none">
|
||||||
<div class="modal-content modal-large">
|
<div class="modal-content modal-large">
|
||||||
|
|||||||
@@ -1,33 +1,50 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>VPS Monitoring — Вход</title>
|
<title>VPS Monitoring — Login</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<script src="/static/js/theme.js"></script>
|
||||||
|
<script src="/static/js/i18n.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="login-page">
|
<body class="login-page">
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<h1>🖥 VPS Monitoring</h1>
|
<h1>🖥 VPS Monitoring</h1>
|
||||||
<p>Панель мониторинга серверов</p>
|
<p data-i18n="login">Вход</p>
|
||||||
|
<div style="margin-top:12px;display:flex;gap:8px;justify-content:center">
|
||||||
|
<button class="lang-btn" id="langBtn" onclick="toggleLang(); updateLoginTexts()">RU</button>
|
||||||
|
<button class="btn-icon" id="themeToggleBtn" onclick="toggleTheme()" style="width:32px;height:32px;font-size:14px">🌙</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Логин</label>
|
<label for="username" id="lbl-user">Логин</label>
|
||||||
<input type="text" id="username" name="username" required autofocus>
|
<input type="text" id="username" name="username" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Пароль</label>
|
<label for="password" id="lbl-pass">Пароль</label>
|
||||||
<input type="password" id="password" name="password" required>
|
<input type="password" id="password" name="password" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary">Войти</button>
|
<button type="submit" class="btn-primary" id="loginBtn">Войти</button>
|
||||||
<div id="error" class="error-msg" style="display:none"></div>
|
<div id="error" class="error-msg" style="display:none"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
function updateLoginTexts() {
|
||||||
|
document.getElementById('langBtn').textContent = currentLang.toUpperCase();
|
||||||
|
document.getElementById('lbl-user').textContent = t('username');
|
||||||
|
document.getElementById('lbl-pass').textContent = t('password');
|
||||||
|
document.getElementById('loginBtn').textContent = t('loginBtn');
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
applyTheme(currentTheme);
|
||||||
|
updateLoginTexts();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const username = document.getElementById('username').value;
|
const username = document.getElementById('username').value;
|
||||||
@@ -43,11 +60,11 @@
|
|||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
} else {
|
} else {
|
||||||
errorEl.textContent = 'Неверный логин или пароль';
|
errorEl.textContent = t('invalidCreds');
|
||||||
errorEl.style.display = 'block';
|
errorEl.style.display = 'block';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorEl.textContent = 'Ошибка подключения';
|
errorEl.textContent = t('connError');
|
||||||
errorEl.style.display = 'block';
|
errorEl.style.display = 'block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user