Files
vps_monitoring/server/templates/dashboard.html
Андрей Бобырев 11eb3d3c45 fix(keenetic): safe delete URLs and not-found handling
Encode router names in Keenetic API calls and return an error when delete targets a missing device.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 19:38:43 +03:00

607 lines
31 KiB
HTML
Raw Permalink 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" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPS Monitoring</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
<script src="/static/js/theme.js"></script>
<script src="/static/js/i18n.js"></script>
</head>
<body>
<header>
<div class="header-left">
<h1 id="pageTitle">🖥 VPS Monitoring</h1>
</div>
<div class="header-right">
<button class="lang-btn" id="langBtn" onclick="toggleLang()">RU</button>
<button class="btn-icon" id="themeToggleBtn" onclick="toggleTheme()" title="Theme">🌙</button>
<button class="btn-icon" onclick="refreshAll()" title="Refresh">🔄</button>
<button class="btn-icon" onclick="showInstructions()" title="Instructions">📖</button>
<button class="btn-icon" onclick="showNotificationSettings()" title="Notifications">🔔</button>
<button class="btn-icon" onclick="showSettings()" title="Settings">⚙️</button>
<button class="btn-icon" onclick="logout()" title="Logout">🚪</button>
</div>
</header>
<main>
<div class="stats-bar">
<div class="stat-item" id="stat-total">
<span class="stat-label" data-i18n="total">Всего</span>
<span class="stat-value" id="total-count">0</span>
</div>
<div class="stat-item online" id="stat-online">
<span class="stat-label" data-i18n="online">Online</span>
<span class="stat-value" id="online-count">0</span>
</div>
<div class="stat-item offline" id="stat-offline">
<span class="stat-label" data-i18n="offline">Offline</span>
<span class="stat-value" id="offline-count">0</span>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab active" id="tab-servers" onclick="switchTab('servers')">🖥 VPS</button>
<button class="tab" id="tab-pc" onclick="switchTab('pc')">💻 PC</button>
<button class="tab" id="tab-synology" onclick="switchTab('synology')">📦 Synology</button>
<button class="tab" id="tab-ha" onclick="switchTab('ha')">🏠 Home Assistant</button>
<button class="tab" id="tab-keenetic" onclick="switchTab('keenetic')">📡 Keenetic</button>
</div>
<!-- Servers Tab -->
<div id="panel-servers">
<div class="toolbar">
<button class="btn-primary" id="addServerBtn" onclick="showAddServer()">+ Добавить сервер</button>
</div>
<div class="servers-grid" id="servers-grid"></div>
</div>
<!-- PC Tab -->
<div id="panel-pc" style="display:none">
<div class="toolbar">
<button class="btn-primary" onclick="showPCDownload()">📥 Скачать агент</button>
<button class="btn-secondary" onclick="showPCSetup()">📋 Инструкция</button>
<button class="btn-secondary" onclick="loadPCs()">🔄 Обновить</button>
</div>
<div class="servers-grid" id="pc-grid"></div>
</div>
<!-- Synology Tab -->
<div id="panel-synology" style="display:none">
<div class="toolbar">
<button class="btn-primary" onclick="showAddSynology()">+ Добавить NAS</button>
<button class="btn-secondary" onclick="refreshAllSynology()">🔄 Обновить все</button>
<button class="btn-secondary" onclick="showSynTunnel()">🔗 Туннель</button>
</div>
<div class="servers-grid" id="synology-grid"></div>
</div>
<!-- Home Assistant Tab -->
<div id="panel-ha" style="display:none">
<div class="toolbar">
<button class="btn-primary" onclick="showAddHA()">+ Добавить HA</button>
<button class="btn-secondary" onclick="refreshAllHA()">🔄 Обновить все</button>
</div>
<div class="servers-grid" id="ha-grid"></div>
</div>
<!-- Keenetic Tab -->
<div id="panel-keenetic" style="display:none">
<div class="toolbar">
<button class="btn-primary" onclick="showAddKeenetic()">+ Добавить роутер</button>
<button class="btn-secondary" onclick="showBulkAddKeenetic()">📋 Массовое добавление</button>
<button class="btn-secondary" onclick="showImportKeenetic()">📥 Импорт таблицы</button>
<button class="btn-secondary" onclick="refreshAllKeenetic()">🔄 Обновить все</button>
</div>
<div class="servers-grid" id="keenetic-grid"></div>
</div>
</main>
<!-- Add Server Modal -->
<div class="modal" id="addServerModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2 data-i18n="addServer">Добавить сервер</h2>
<button class="btn-close" onclick="closeModal('addServerModal')">&times;</button>
</div>
<form id="addServerForm">
<div class="form-group">
<label data-i18n="serverName">Название</label>
<input type="text" name="name" required placeholder="My Server">
</div>
<div class="form-group">
<label data-i18n="host">IP / Host</label>
<input type="text" name="host" required placeholder="192.168.1.1">
</div>
<div class="form-group">
<label data-i18n="sshPort">Порт SSH</label>
<input type="number" name="port" value="22">
</div>
<div class="form-group">
<label data-i18n="sshLogin">Логин</label>
<input type="text" name="username" value="root">
</div>
<div class="form-group">
<label data-i18n="sshPassword">Пароль</label>
<input type="password" name="password" placeholder="SSH Password">
</div>
<div class="form-group">
<label data-i18n="description">Описание</label>
<input type="text" name="description" placeholder="">
</div>
<button type="submit" class="btn-primary" data-i18n="add">Добавить</button>
</form>
</div>
</div>
<!-- Add Synology Modal -->
<div class="modal" id="addSynologyModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2>📦 Добавить Synology NAS</h2>
<button class="btn-close" onclick="closeModal('addSynologyModal')">&times;</button>
</div>
<form id="addSynologyForm">
<div class="form-group">
<label>Название</label>
<input type="text" name="name" required placeholder="My NAS">
</div>
<div class="form-group">
<label>IP / Host</label>
<input type="text" name="host" required placeholder="192.168.1.50">
</div>
<div class="form-group">
<label>Порт DSM</label>
<input type="number" name="port" value="5000">
</div>
<div class="form-group" style="display:flex;align-items:center;gap:8px">
<input type="checkbox" name="https" id="syn-https" style="width:auto">
<label for="syn-https" style="margin:0">HTTPS</label>
</div>
<div class="form-group">
<label>Логин DSM</label>
<input type="text" name="username" required placeholder="admin">
</div>
<div class="form-group">
<label>Пароль DSM</label>
<input type="password" name="password" required>
</div>
<button type="submit" class="btn-primary">Добавить</button>
</form>
</div>
</div>
<!-- Add HA Modal -->
<div class="modal" id="addHAModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2>🏠 Добавить Home Assistant</h2>
<button class="btn-close" onclick="closeModal('addHAModal')">&times;</button>
</div>
<form id="addHAForm">
<div class="form-group">
<label>Название</label>
<input type="text" name="name" required placeholder="Home">
</div>
<div class="form-group">
<label>URL</label>
<input type="text" name="url" required placeholder="http://192.168.1.100:8123">
</div>
<div class="form-group">
<label>Long-Lived Access Token</label>
<textarea name="token" required rows="3" placeholder="Профиль → Безопасность → Долгоживущие токены доступа" style="resize:vertical"></textarea>
</div>
<button type="submit" class="btn-primary">Добавить</button>
</form>
</div>
</div>
<!-- Add Keenetic Modal -->
<div class="modal" id="addKeeneticModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2>📡 Добавить Keenetic</h2>
<button class="btn-close" onclick="closeModal('addKeeneticModal')">&times;</button>
</div>
<form id="addKeeneticForm">
<div class="form-group">
<label>Название</label>
<input type="text" name="name" required placeholder="Home Router">
</div>
<div class="form-group">
<label>Хост / Keenetic URL</label>
<input type="text" name="host" required placeholder="loftliliana.netcraze.pro или https://...">
</div>
<div class="form-group">
<label>AnyDesk ID</label>
<input type="text" name="anydesk" placeholder="1020687391">
</div>
<div class="form-group">
<label>Логин</label>
<input type="text" name="login" value="admin">
</div>
<div class="form-group">
<label>Пароль</label>
<input type="password" name="password" required>
</div>
<button type="submit" class="btn-primary">Добавить</button>
</form>
</div>
</div>
<!-- Bulk Add Keenetic Modal -->
<div class="modal" id="bulkAddKeeneticModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2>📋 Массовое добавление роутеров</h2>
<button class="btn-close" onclick="closeModal('bulkAddKeeneticModal')">&times;</button>
</div>
<form id="bulkAddKeeneticForm">
<div class="form-group">
<label>Домены (по одному на строку)</label>
<textarea name="domains" rows="10" required placeholder="vimpel.netcraze.link
home.netcraze.link
office.netcraze.link" style="width:100%;font-family:monospace;font-size:13px;resize:vertical"></textarea>
</div>
<div class="form-group">
<label>Логин (общий для всех)</label>
<input type="text" name="login" value="admin">
</div>
<div class="form-group">
<label>Пароль (общий для всех)</label>
<input type="password" name="password" required>
</div>
<button type="submit" class="btn-primary" style="width:100%">Добавить все</button>
</form>
<div id="bulkAddResult" style="display:none;margin-top:12px;padding:12px;border-radius:8px;background:var(--bg-primary);font-size:13px"></div>
</div>
</div>
<!-- Import Keenetic Modal -->
<div class="modal" id="importKeeneticModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2>📥 Импорт из таблицы</h2>
<button class="btn-close" onclick="closeModal('importKeeneticModal')">&times;</button>
</div>
<form id="importKeeneticForm">
<p style="font-size:13px;opacity:0.8;margin-bottom:12px">Вставьте строки из Excel: колонки Address, Keenetic, AnyDesk (Tab-разделитель)</p>
<div class="form-group">
<textarea name="tsv" rows="12" required placeholder="Address Keenetic AnyDesk
loftliliana https://loftliliana.netcraze.pro 1020687391" style="width:100%;font-family:monospace;font-size:12px"></textarea>
</div>
<div class="form-group">
<label>Логин (общий)</label>
<input type="text" name="login" value="admin">
</div>
<div class="form-group">
<label>Пароль (общий)</label>
<input type="password" name="password" required>
</div>
<button type="submit" class="btn-primary" style="width:100%">Импортировать</button>
</form>
<div id="importKeeneticResult" style="display:none;margin-top:12px;padding:12px;border-radius:8px;background:var(--bg-primary);font-size:13px"></div>
</div>
</div>
<!-- Keenetic Detail Modal -->
<div class="modal" id="keeneticDetailModal" style="display:none">
<div class="modal-content modal-wide">
<div class="modal-header">
<h2 id="keeneticDetailTitle">📡 Router</h2>
<button class="btn-close" onclick="closeModal('keeneticDetailModal')">&times;</button>
</div>
<div id="keeneticDetailBody"></div>
</div>
</div>
<!-- PC Download Modal -->
<div class="modal" id="pcDownloadModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2>📥 Скачать агент для ПК</h2>
<button class="btn-close" onclick="closeModal('pcDownloadModal')">&times;</button>
</div>
<div class="form-group">
<label>Имя компьютера (агента)</label>
<input type="text" id="pc-agent-name" placeholder="MyPC" value="">
</div>
<button class="btn-primary" onclick="generatePCCommand()" style="margin-bottom:16px">🔧 Сгенерировать команду</button>
<div id="pc-generated-cmd" style="display:none">
<div class="instr-card">
<h4>📋 Команда установки (PowerShell от Администратора)</h4>
<code id="pc-gen-code" style="cursor:pointer" onclick="copyToClipboard(this.textContent)" title="Нажмите чтобы скопировать"></code>
<p style="margin-top:8px;font-size:11px;color:var(--text-secondary)">Нажмите на команду чтобы скопировать</p>
</div>
</div>
</div>
</div>
<!-- Notification Settings Modal -->
<div class="modal" id="notifyModal" style="display:none">
<div class="modal-content modal-wide">
<div class="modal-header">
<h2>🔔 Настройки уведомлений</h2>
<button class="btn-close" onclick="closeModal('notifyModal')">&times;</button>
</div>
<!-- Channel configs -->
<h3>📨 Каналы доставки</h3>
<div class="notify-channels">
<div class="instr-card">
<h4>📱 Telegram</h4>
<p style="font-size:12px;color:var(--text-secondary)">Настраивается в Настройках (⚙️) — Bot Token и Chat ID</p>
<div id="tg-status" style="margin-top:8px"></div>
<button class="btn-sm" onclick="testNotification('telegram')" style="margin-top:8px">🧪 Тест</button>
</div>
<div class="instr-card">
<h4>📧 Email (SMTP)</h4>
<div class="form-group" style="margin-top:8px">
<label>SMTP сервер</label>
<input type="text" id="n-smtp-host" placeholder="smtp.gmail.com">
</div>
<div class="form-group">
<label>Порт</label>
<input type="number" id="n-smtp-port" value="587">
</div>
<div class="form-group">
<label>Email (логин)</label>
<input type="text" id="n-smtp-user" placeholder="user@gmail.com">
</div>
<div class="form-group">
<label>Пароль приложения</label>
<input type="password" id="n-smtp-pass" placeholder="App Password">
</div>
<div class="form-group">
<label>Получатель</label>
<input type="text" id="n-email-to" placeholder="alerts@example.com">
</div>
<button class="btn-sm" onclick="testNotification('email')" style="margin-top:4px">🧪 Тест</button>
</div>
<div class="instr-card">
<h4>💬 WhatsApp (CallMeBot)</h4>
<p style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">
1. Отправьте "I allow callmebot to send me messages" на <b>+34 644 71 85 23</b> в WhatsApp<br>
2. Получите apikey в ответном сообщении<br>
3. Введите данные ниже
</p>
<div class="form-group">
<label>Телефон (с кодом страны)</label>
<input type="text" id="n-wa-phone" placeholder="+79001234567">
</div>
<div class="form-group">
<label>API Key</label>
<input type="text" id="n-wa-apikey" placeholder="От CallMeBot">
</div>
<button class="btn-sm" onclick="testNotification('whatsapp')" style="margin-top:4px">🧪 Тест</button>
</div>
</div>
<!-- Per-category toggles -->
<h3>📊 Какие уведомления отправлять</h3>
<div class="notify-matrix">
<table class="notify-table">
<thead>
<tr>
<th>Категория</th>
<th>📱 Telegram</th>
<th>📧 Email</th>
<th>💬 WhatsApp</th>
</tr>
</thead>
<tbody>
<tr>
<td>🖥 Серверы</td>
<td><input type="checkbox" id="n-servers-tg" checked></td>
<td><input type="checkbox" id="n-servers-email"></td>
<td><input type="checkbox" id="n-servers-wa"></td>
</tr>
<tr>
<td>💻 ПК</td>
<td><input type="checkbox" id="n-pc-tg" checked></td>
<td><input type="checkbox" id="n-pc-email"></td>
<td><input type="checkbox" id="n-pc-wa"></td>
</tr>
<tr>
<td>📦 Synology</td>
<td><input type="checkbox" id="n-synology-tg" checked></td>
<td><input type="checkbox" id="n-synology-email"></td>
<td><input type="checkbox" id="n-synology-wa"></td>
</tr>
<tr>
<td>🏠 Home Assistant</td>
<td><input type="checkbox" id="n-ha-tg" checked></td>
<td><input type="checkbox" id="n-ha-email"></td>
<td><input type="checkbox" id="n-ha-wa"></td>
</tr>
</tbody>
</table>
</div>
<button class="btn-primary" onclick="saveNotificationSettings()" style="margin-top:20px">💾 Сохранить</button>
</div>
</div>
<!-- Settings Modal -->
<div class="modal" id="settingsModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2 data-i18n="settings">Настройки</h2>
<button class="btn-close" onclick="closeModal('settingsModal')">&times;</button>
</div>
<form id="settingsForm">
<h3 data-i18n="auth">Авторизация</h3>
<div class="form-group">
<label data-i18n="username">Логин</label>
<input type="text" name="admin_login" id="set_login">
</div>
<div class="form-group">
<label data-i18n="newPassword">Новый пароль</label>
<input type="password" name="admin_password" id="set_password" placeholder="">
</div>
<h3 data-i18n="telegram">Telegram</h3>
<div class="form-group">
<label data-i18n="botToken">Bot Token</label>
<input type="text" name="telegram_bot_token" id="set_tg_token">
</div>
<div class="form-group">
<label data-i18n="chatId">Chat ID</label>
<input type="text" name="telegram_chat_id" id="set_tg_chat">
</div>
<h3 data-i18n="monitoring">Мониторинг</h3>
<div class="form-group">
<label data-i18n="pollInterval">Интервал опроса (сек)</label>
<input type="number" name="monitor_interval" id="set_interval" value="60">
</div>
<div class="form-group">
<label data-i18n="cpuThreshold">Порог CPU (%)</label>
<input type="number" name="alert_cpu_threshold" id="set_cpu" value="90">
</div>
<div class="form-group">
<label data-i18n="ramThreshold">Порог RAM (%)</label>
<input type="number" name="alert_ram_threshold" id="set_ram" value="90">
</div>
<div class="form-group">
<label data-i18n="diskThreshold">Порог Disk (%)</label>
<input type="number" name="alert_disk_threshold" id="set_disk" value="90">
</div>
<button type="submit" class="btn-primary" data-i18n="save">Сохранить</button>
</form>
</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')">&times;</button>
</div>
<div id="instructions-content"></div>
</div>
</div>
<!-- SSH Terminal Modal -->
<div class="modal" id="sshModal" style="display:none">
<div class="modal-content modal-large">
<div class="modal-header">
<h2 id="ssh-title">SSH Terminal</h2>
<button class="btn-close" onclick="closeSSH()">&times;</button>
</div>
<div id="terminal-container"></div>
</div>
</div>
<!-- Server Detail Modal -->
<div class="modal" id="serverDetailModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h2 id="detail-title">Server</h2>
<button class="btn-close" onclick="closeModal('serverDetailModal')">&times;</button>
</div>
<div id="detail-content"></div>
</div>
</div>
<!-- Synology Detail Modal -->
<div class="modal" id="synDetailModal" style="display:none">
<div class="modal-content modal-wide">
<div class="modal-header">
<h2 id="syn-detail-title">Synology</h2>
<button class="btn-close" onclick="closeModal('synDetailModal')">&times;</button>
</div>
<div id="syn-detail-content"></div>
</div>
</div>
<!-- HA Detail Modal -->
<div class="modal" id="haDetailModal" style="display:none">
<div class="modal-content modal-wide">
<div class="modal-header">
<h2 id="ha-detail-title">Home Assistant</h2>
<button class="btn-close" onclick="closeModal('haDetailModal')">&times;</button>
</div>
<div id="ha-detail-content"></div>
</div>
</div>
<!-- Synology Tunnel Setup Modal -->
<div class="modal" id="synTunnelModal" style="display:none">
<div class="modal-content modal-wide">
<div class="modal-header">
<h2>🔗 Настройка туннеля для Synology</h2>
<button class="btn-close" onclick="closeModal('synTunnelModal')">&times;</button>
</div>
<div class="instructions-section">
<div class="instr-card" style="border-left:3px solid var(--accent)">
<h4>⚠️ Зачем нужен туннель?</h4>
<p>VPS не может напрямую подключиться к вашему Synology NAS — порт 5000 не доступен из интернета.
Туннель через ПК на той же сети решает эту проблему.</p>
</div>
<div class="instr-card">
<h4>📋 Шаг 1 — Запустите на ПК в той же сети (PowerShell от Администратора)</h4>
<code id="syn-tunnel-cmd" style="cursor:pointer" onclick="copyToClipboard(this.textContent)" title="Нажмите чтобы скопировать">Загрузка...</code>
<p style="margin-top:8px;font-size:11px;color:var(--text-secondary)">Нажмите на команду чтобы скопировать</p>
</div>
<div class="instr-card">
<h4>📊 Шаг 2 — Обновите Synology на дашборде</h4>
<p>После запуска туннеля нажмите "Обновить" на карточке Synology — статус должен стать ONLINE.</p>
</div>
<div class="instr-card">
<h4>🔧 Управление туннелем</h4>
<ul class="cmd-list">
<li>Get-ScheduledTask 'VPS-Monitor-SynologyTunnel'</li>
<li>Stop-ScheduledTask 'VPS-Monitor-SynologyTunnel'</li>
<li>Unregister-ScheduledTask 'VPS-Monitor-SynologyTunnel'</li>
</ul>
</div>
</div>
</div>
</div>
<!-- PC Setup Modal -->
<div class="modal" id="pcSetupModal" style="display:none">
<div class="modal-content modal-wide">
<div class="modal-header">
<h2>💻 Установка агента на ПК</h2>
<button class="btn-close" onclick="closeModal('pcSetupModal')">&times;</button>
</div>
<div class="instructions-section">
<div class="instr-card">
<h4>📥 Быстрая установка (PowerShell от Администратора)</h4>
<code id="pc-install-cmd">Set-ExecutionPolicy Bypass -Scope Process -Force; $ProgressPreference = 'SilentlyContinue'; (New-Object Net.WebClient).DownloadFile('http://77.239.126.123/static/downloads/install_agent.ps1', "$PWD\install_agent.ps1"); &amp; "$PWD\install_agent.ps1" -ServerUrl 'http://77.239.126.123' -AgentName 'MyPC'</code>
</div>
<div class="instr-card">
<h4>⚙️ Параметры</h4>
<p><b>-ServerUrl</b> — адрес сервера мониторинга<br>
<b>-AgentName</b> — имя ПК (по умолчанию hostname)<br>
<b>-Interval</b> — интервал отправки в секундах (по умолчанию 60)</p>
</div>
<div class="instr-card">
<h4>📊 Что мониторится</h4>
<p>CPU, RAM, Disk (все диски), Network, GPU, Uptime, Top 5 процессов</p>
</div>
<div class="instr-card">
<h4>🔧 Управление</h4>
<ul class="cmd-list">
<li>Get-ScheduledTask 'VPS-Monitor-Agent'</li>
<li>Stop-ScheduledTask 'VPS-Monitor-Agent'</li>
<li>Start-ScheduledTask 'VPS-Monitor-Agent'</li>
<li>Unregister-ScheduledTask 'VPS-Monitor-Agent'</li>
</ul>
</div>
</div>
</div>
</div>
<footer class="app-footer">
автор:&ensp;<a href="https://github.com/andrey271192" target="_blank">GitHub</a>&ensp;·&ensp;<a href="https://boosty.to/iot_andrey" target="_blank">Boosty</a>&ensp;·&ensp;<a href="https://t.me/Iot_andrey" target="_blank">Поддержка</a>&ensp;·&ensp;<a href="https://t.me/Iot_andrey" target="_blank">@Iot_andrey</a>
</footer>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script src="/static/js/app.js?v=20260529a"></script>
</body>
</html>