From 4846742c605c1fa540641e69b07c89b2d3b8f9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Wed, 20 May 2026 05:55:16 +0300 Subject: [PATCH] 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 --- server/static/css/style.css | 201 ++++++++++++++++++++++++++++++-- server/static/js/app.js | 149 +++++++++++++++++------ server/static/js/i18n.js | 169 +++++++++++++++++++++++++++ server/static/js/theme.js | 16 +++ server/templates/dashboard.html | 86 ++++++++------ server/templates/login.html | 33 ++++-- 6 files changed, 563 insertions(+), 91 deletions(-) create mode 100644 server/static/js/i18n.js create mode 100644 server/static/js/theme.js diff --git a/server/static/css/style.css b/server/static/css/style.css index d37f90e..3f473c3 100644 --- a/server/static/css/style.css +++ b/server/static/css/style.css @@ -1,4 +1,5 @@ -:root { +/* Dark Theme (default) */ +:root, [data-theme="dark"] { --bg-primary: #0f0f23; --bg-secondary: #1a1a3e; --bg-card: #1e1e4a; @@ -6,12 +7,36 @@ --text-secondary: #a0a0cc; --accent: #6366f1; --accent-hover: #818cf8; - --success: #4ade80; - --danger: #f87171; - --warning: #fbbf24; + --success: #22c55e; + --success-bg: rgba(34, 197, 94, 0.12); + --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); --radius: 12px; --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); color: var(--text-primary); min-height: 100vh; + transition: background 0.3s, color 0.3s; } /* Login Page */ @@ -33,7 +59,6 @@ body { align-items: center; justify-content: center; min-height: 100vh; - background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 100%); } .login-container { @@ -81,7 +106,7 @@ body { .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 14px; - background: var(--bg-primary); + background: var(--input-bg); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary); @@ -114,8 +139,8 @@ body { .error-msg { margin-top: 12px; padding: 10px; - background: rgba(248, 113, 113, 0.1); - border: 1px solid rgba(248, 113, 113, 0.3); + background: var(--danger-bg); + border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 8px; color: var(--danger); font-size: 13px; @@ -142,6 +167,7 @@ header { .header-right { display: flex; gap: 8px; + align-items: center; } .btn-icon { @@ -156,10 +182,29 @@ header { cursor: pointer; font-size: 16px; transition: background 0.2s; + color: var(--text-primary); } .btn-icon:hover { 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 */ @@ -183,6 +228,11 @@ main { border-radius: var(--radius); padding: 16px; text-align: center; + transition: transform 0.2s; +} + +.stat-item:hover { + transform: translateY(-2px); } .stat-item .stat-label { @@ -204,6 +254,9 @@ main { /* Toolbar */ .toolbar { margin-bottom: 20px; + display: flex; + gap: 12px; + align-items: center; } .toolbar .btn-primary { @@ -211,6 +264,22 @@ main { 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 */ .servers-grid { display: grid; @@ -225,6 +294,25 @@ main { padding: 20px; transition: transform 0.2s, box-shadow 0.2s; 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 { @@ -255,18 +343,50 @@ main { font-size: 11px; font-weight: 600; text-transform: uppercase; + display: flex; + align-items: center; + gap: 6px; } .status-badge.online { - background: rgba(74, 222, 128, 0.15); + background: var(--success-bg); color: var(--success); } .status-badge.offline { - background: rgba(248, 113, 113, 0.15); + background: var(--danger-bg); 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 { display: grid; grid-template-columns: repeat(3, 1fr); @@ -288,6 +408,7 @@ main { .metric-item .value { font-size: 18px; font-weight: 700; + color: var(--success); } .metric-item .value.warn { color: var(--warning); } @@ -335,6 +456,7 @@ main { .server-card-actions button:hover { background: var(--accent); border-color: var(--accent); + color: white; } .server-card-actions button.danger:hover { @@ -374,6 +496,10 @@ main { height: 80vh; } +.modal-content.modal-wide { + max-width: 700px; +} + .modal-header { display: flex; align-items: center; @@ -401,6 +527,7 @@ main { .btn-close:hover { background: var(--danger); + color: white; } .modal-content h3 { @@ -411,6 +538,59 @@ main { 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-container { width: 100%; @@ -426,4 +606,5 @@ main { .servers-grid { grid-template-columns: 1fr; } .stats-bar { flex-direction: column; gap: 8px; } header { padding: 12px 16px; } + .header-right { gap: 4px; } } diff --git a/server/static/js/app.js b/server/static/js/app.js index 194bc07..88e30ee 100644 --- a/server/static/js/app.js +++ b/server/static/js/app.js @@ -2,6 +2,17 @@ let servers = []; let currentTerminal = 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 async function loadServers() { try { @@ -27,7 +38,7 @@ function renderServers() { document.getElementById('offline-count').textContent = offline; if (!servers.length) { - grid.innerHTML = '
Нет серверов. Нажмите "+ Добавить сервер"
'; + grid.innerHTML = `
${t('noServers')}
`; return; } @@ -45,13 +56,14 @@ function renderServers() { const id = srv.id || srv.host; return ` -
+
${srv.name}
${srv.host}:${srv.port || 22}
+ ${isOnline ? 'Online' : 'Offline'}
@@ -73,7 +85,7 @@ function renderServers() {
- ` : '
Нет данных
'} + ` : `
${t('noData')}
`}
@@ -96,38 +108,41 @@ function showServerDetail(id) {
-
${srv.host}:${srv.port || 22}
+
${srv.host}:${srv.port || 22}
- -
${m.online ? 'Online' : 'Offline'}
+ +
+ + ${m.online ? 'Online' : 'Offline'} +
${m.online ? `
- -
${m.uptime || 'N/A'}
+ +
${m.uptime || 'N/A'}
- -
${m.load_average || 'N/A'}
+ +
${m.load_average || 'N/A'}
-
${(m.ram_used_mb||0).toFixed(0)} / ${(m.ram_total_mb||0).toFixed(0)} MB (${m.ram_percent||0}%)
+
${(m.ram_used_mb||0).toFixed(0)} / ${(m.ram_total_mb||0).toFixed(0)} MB (${m.ram_percent||0}%)
-
${(m.disk_used_gb||0).toFixed(1)} / ${(m.disk_total_gb||0).toFixed(1)} GB (${m.disk_percent||0}%)
+
${(m.disk_used_gb||0).toFixed(1)} / ${(m.disk_total_gb||0).toFixed(1)} GB (${m.disk_percent||0}%)
- -
↓ ${(m.network_in_mb||0).toFixed(0)} MB / ↑ ${(m.network_out_mb||0).toFixed(0)} MB
+ +
↓ ${(m.network_in_mb||0).toFixed(0)} MB / ↑ ${(m.network_out_mb||0).toFixed(0)} MB
` : ''} ${srv.description ? `
- -
${srv.description}
+ +
${srv.description}
` : ''}
@@ -136,17 +151,74 @@ function showServerDetail(id) { modal.style.display = 'flex'; } +// Instructions +function showInstructions() { + const content = document.getElementById('instructions-content'); + content.innerHTML = ` +
+
+

📥 ${t('instr_install')}

+

${t('instr_add_server_text').includes('Click') ? 'One command to install on any Ubuntu/Debian server:' : 'Одна команда для установки на Ubuntu/Debian:'}

+ ${t('instr_install_text')} +
+
+

➕ ${t('instr_add_server')}

+

${t('instr_add_server_text')}

+
+
+

💻 ${t('instr_ssh')}

+

${t('instr_ssh_text')}

+
+
+

🤖 ${t('instr_telegram')}

+

${t('instr_telegram_text')}

+
+
+

🔄 ${t('instr_reboot')}

+

${t('instr_reboot_text')}

+
+
+

⚠️ ${t('instr_thresholds')}

+

${t('instr_thresholds_text')}

+
+
+

🔧 ${t('instr_commands')}

+
    +
  • 📊 ${t('instr_cmd_status')}
  • +
  • 🔄 ${t('instr_cmd_restart')}
  • +
  • 📋 ${t('instr_cmd_logs')}
  • +
  • ⬆️ ${t('instr_cmd_update')}
  • +
+
+
+

🔌 ${t('instr_api')}

+
    +
  • GET /api/servers — list all
  • +
  • POST /api/servers — add server
  • +
  • DELETE /api/servers/{id} — remove
  • +
  • POST /api/servers/{id}/reboot — reboot
  • +
  • POST /api/servers/{id}/exec — run command
  • +
  • GET /api/settings — get settings
  • +
  • PUT /api/settings — update settings
  • +
  • WS /ws/ssh/{id} — SSH terminal
  • +
+
+
+ `; + document.getElementById('instructionsModal').style.display = 'flex'; +} + // SSH Terminal function openSSH(id, name) { const modal = document.getElementById('sshModal'); - document.getElementById('ssh-title').textContent = `SSH — ${name}`; + document.getElementById('ssh-title').textContent = `${t('sshTerminal')} — ${name}`; modal.style.display = 'flex'; const container = document.getElementById('terminal-container'); container.innerHTML = ''; if (typeof Terminal === 'undefined') { - container.innerHTML = '
xterm.js не загружен
'; + container.innerHTML = '
xterm.js not loaded
'; return; } @@ -205,22 +277,22 @@ function closeSSH() { // Server actions async function rebootServer(id) { - if (!confirm('Перезагрузить сервер?')) return; + if (!confirm(t('rebootConfirm'))) return; try { await fetch(`/api/servers/${id}/reboot`, {method: 'POST', credentials: 'include'}); - alert('Команда перезагрузки отправлена'); + alert(t('rebootSent')); } catch (e) { - alert('Ошибка: ' + e.message); + alert('Error: ' + e.message); } } async function deleteServer(id) { - if (!confirm('Удалить сервер из мониторинга?')) return; + if (!confirm(t('deleteConfirm'))) return; try { await fetch(`/api/servers/${id}`, {method: 'DELETE', credentials: 'include'}); loadServers(); } catch (e) { - alert('Ошибка: ' + e.message); + alert('Error: ' + e.message); } } @@ -254,10 +326,10 @@ document.getElementById('addServerForm')?.addEventListener('submit', async (e) = loadServers(); } else { const err = await resp.json(); - alert(err.detail || 'Ошибка'); + alert(err.detail || 'Error'); } } catch (e) { - alert('Ошибка: ' + e.message); + alert('Error: ' + e.message); } }); @@ -301,9 +373,9 @@ document.getElementById('settingsForm')?.addEventListener('submit', async (e) => body: JSON.stringify(data), }); closeModal('settingsModal'); - alert('Настройки сохранены'); + alert(t('settingsSaved')); } catch (e) { - alert('Ошибка: ' + e.message); + alert('Error: ' + e.message); } }); @@ -322,18 +394,21 @@ async function logout() { } // Click outside modal to close -document.querySelectorAll('.modal').forEach(modal => { - modal.addEventListener('click', (e) => { - if (e.target === modal) { - if (modal.id === 'sshModal') { - closeSSH(); - } else { - modal.style.display = 'none'; - } +document.addEventListener('click', (e) => { + if (e.target.classList.contains('modal')) { + if (e.target.id === 'sshModal') { + closeSSH(); + } else { + e.target.style.display = 'none'; } - }); + } }); // Initial load -loadServers(); +document.addEventListener('DOMContentLoaded', () => { + applyTheme(currentTheme); + renderPage(); + loadServers(); +}); + setInterval(loadServers, 30000); diff --git a/server/static/js/i18n.js b/server/static/js/i18n.js new file mode 100644 index 0000000..f14beb4 --- /dev/null +++ b/server/static/js/i18n.js @@ -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'); +} diff --git a/server/static/js/theme.js b/server/static/js/theme.js new file mode 100644 index 0000000..afd0025 --- /dev/null +++ b/server/static/js/theme.js @@ -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)); diff --git a/server/templates/dashboard.html b/server/templates/dashboard.html index f37261a..f9de599 100644 --- a/server/templates/dashboard.html +++ b/server/templates/dashboard.html @@ -1,82 +1,85 @@ - + VPS Monitoring + +
-

🖥 VPS Monitoring

+

🖥 VPS Monitoring

- - - + + + + + +
- Всего + Всего 0
- Online + Online 0
- Offline + Offline 0
- +
-
- -
+
@@ -85,50 +88,61 @@ + + +