version 1.2: Telegram-бот, мониторинг сервера на дашборде, README
- bot.py: Telegram-бот (/users, /create, /delete, /warp, /stats, /dashboard)
- install-bot.sh: установщик бота (systemd, конфиг /etc/mita/bot.json)
- app.py: API /api/bot/{status,install,uninstall,config}, /api/stats (CPU/RAM/диск)
- templates: раздел «Telegram бот» в UI, мониторинг CPU/RAM/диск на дашборде (2s auto-refresh)
- mita-ctl.sh: пункт 11 «Telegram-бот», удаление бота в полном uninstall
- install.sh: опциональная установка бота с инструкцией
- README.md: описание проекта и дорожная карта
This commit is contained in:
+7
-1
@@ -125,6 +125,10 @@ tr:hover td{background:rgba(255,255,255,.02)}
|
||||
<svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
|
||||
Безопасность
|
||||
</button>
|
||||
<button class="nav-item" onclick="showPage('bot',this)">
|
||||
<svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||||
Telegram бот
|
||||
</button>
|
||||
<div class="sidebar-footer"><a href="{{ base }}/logout">Выйти</a></div>
|
||||
</nav>
|
||||
<main class="main">{% block content %}{% endblock %}</main>
|
||||
@@ -148,10 +152,12 @@ function showPage(id,btn){
|
||||
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
|
||||
document.getElementById('page-'+id).classList.add('active');
|
||||
if(btn)btn.classList.add('active');
|
||||
if(id==='dashboard')loadDashboard();
|
||||
if(id==='dashboard'){loadDashboard();if(typeof startStatsPolling==='function')startStatsPolling();}
|
||||
else{if(typeof stopStatsPolling==='function')stopStatsPolling();}
|
||||
if(id==='users')loadUsers();
|
||||
if(id==='ssl')loadSSL();
|
||||
if(id==='security')loadSecurityPage();
|
||||
if(id==='bot')loadBotPage();
|
||||
}
|
||||
function openModal(title,html){
|
||||
document.getElementById('modal-title-text').textContent=title;
|
||||
|
||||
+258
-6
@@ -30,6 +30,32 @@
|
||||
<div id="s-mita-status" style="margin-top:6px"><span class="badge badge-off">—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Мониторинг сервера</div>
|
||||
<div class="grid-4" style="margin-bottom:0">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">CPU</div>
|
||||
<div class="stat-value accent" id="s-cpu">—</div>
|
||||
<div class="stat-sub">%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">RAM</div>
|
||||
<div class="stat-value" id="s-ram-pct">—</div>
|
||||
<div class="stat-sub" id="s-ram-detail" style="white-space:nowrap">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Диск</div>
|
||||
<div class="stat-value" id="s-disk-pct">—</div>
|
||||
<div class="stat-sub" id="s-disk-detail" style="white-space:nowrap">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Аптайм</div>
|
||||
<div class="stat-value" style="font-size:1.1rem" id="s-uptime">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Быстрые действия</div>
|
||||
<div class="btn-group">
|
||||
@@ -88,8 +114,19 @@
|
||||
<div class="card-title">Список пользователей</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Имя</th><th>WARP вкл</th><th>Правила WARP</th><th>Конфиг клиента</th><th>Действия</th></tr></thead>
|
||||
<tbody id="users-tbody"><tr><td colspan="4" style="color:var(--muted);text-align:center;padding:20px">Загрузка...</td></tr></tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Пользователь</th>
|
||||
<th title="Трафик за последние 24 часа">День</th>
|
||||
<th title="Примерный трафик за 7 дней (на основе 30-дневных данных)">7 дней</th>
|
||||
<th title="Трафик за последние 30 дней">30 дней</th>
|
||||
<th>WARP</th>
|
||||
<th>Правила WARP</th>
|
||||
<th>Конфиг</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-tbody"><tr><td colspan="8" style="color:var(--muted);text-align:center;padding:20px">Загрузка...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,6 +246,63 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TELEGRAM BOT -->
|
||||
<div class="page" id="page-bot">
|
||||
<h1>Telegram бот</h1>
|
||||
<p class="subtitle">Управление сервером через Telegram</p>
|
||||
|
||||
<div class="card" id="bot-status-card">
|
||||
<div class="card-title">Статус бота</div>
|
||||
<div id="bot-status-body"><div style="color:var(--muted)">Загрузка...</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="bot-install-card" style="display:none">
|
||||
<div class="card-title">Установка бота</div>
|
||||
<p style="color:var(--muted);font-size:.875rem;margin-bottom:18px;line-height:1.6">
|
||||
Создайте бота через <strong style="color:var(--accent)">@BotFather</strong> в Telegram,
|
||||
скопируйте токен и узнайте ваш Telegram ID через <strong style="color:var(--accent)">@userinfobot</strong>.
|
||||
</p>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Токен бота (от @BotFather)</label>
|
||||
<input type="text" id="bot-token" placeholder="123456:ABC-DEF1234gh...">
|
||||
</div>
|
||||
<div class="form-group" style="max-width:200px">
|
||||
<label>Ваш Telegram ID</label>
|
||||
<input type="text" id="bot-admin-id" placeholder="123456789">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="installBot()">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4"/></svg>
|
||||
Установить / обновить
|
||||
</button>
|
||||
<div id="bot-install-result" style="margin-top:12px"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="bot-manage-card" style="display:none">
|
||||
<div class="card-title">Управление</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="restartBot()">⟳ Перезапустить</button>
|
||||
<button class="btn btn-danger" onclick="uninstallBot()">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
Удалить бота
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="card-title" style="margin-bottom:12px">Добавить админа</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="max-width:220px">
|
||||
<label>Telegram ID</label>
|
||||
<input type="text" id="bot-add-admin-id" placeholder="123456789">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="addBotAdmin()">Добавить</button>
|
||||
</div>
|
||||
<div id="bot-admin-result" style="margin-top:12px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@@ -237,6 +331,54 @@ async function loadDashboard(){
|
||||
}
|
||||
loadDashboard();
|
||||
|
||||
let _statsTimer=null;
|
||||
startStatsPolling();
|
||||
|
||||
function fmtBytes(b){
|
||||
if(b>=1024**4)return(b/1024**4).toFixed(1)+' ТБ';
|
||||
if(b>=1024**3)return(b/1024**3).toFixed(1)+' ГБ';
|
||||
if(b>=1024**2)return(b/1024**2).toFixed(0)+' МБ';
|
||||
if(b>=1024) return(b/1024).toFixed(0)+' КБ';
|
||||
return b+' Б';
|
||||
}
|
||||
|
||||
async function loadServerStats(){
|
||||
const d=await api('/api/stats');
|
||||
if(!d)return;
|
||||
|
||||
const cpuEl=document.getElementById('s-cpu');
|
||||
if(cpuEl){
|
||||
cpuEl.textContent=d.cpu.toFixed(1);
|
||||
cpuEl.style.color=d.cpu>80?'var(--danger)':d.cpu>50?'var(--warn)':'var(--accent)';
|
||||
}
|
||||
|
||||
const ramPctEl=document.getElementById('s-ram-pct');
|
||||
if(ramPctEl){
|
||||
ramPctEl.textContent=d.ram_pct.toFixed(1)+'%';
|
||||
ramPctEl.style.color=d.ram_pct>90?'var(--danger)':d.ram_pct>70?'var(--warn)':'var(--text)';
|
||||
}
|
||||
const ramDetail=document.getElementById('s-ram-detail');
|
||||
if(ramDetail)ramDetail.textContent=fmtBytes(d.ram_used)+' / '+fmtBytes(d.ram_total);
|
||||
|
||||
const diskPctEl=document.getElementById('s-disk-pct');
|
||||
if(diskPctEl){
|
||||
diskPctEl.textContent=d.disk_pct.toFixed(1)+'%';
|
||||
diskPctEl.style.color=d.disk_pct>90?'var(--danger)':d.disk_pct>75?'var(--warn)':'var(--text)';
|
||||
}
|
||||
const diskDetail=document.getElementById('s-disk-detail');
|
||||
if(diskDetail)diskDetail.textContent=fmtBytes(d.disk_used)+' / '+fmtBytes(d.disk_total);
|
||||
}
|
||||
|
||||
function startStatsPolling(){
|
||||
stopStatsPolling();
|
||||
loadServerStats();
|
||||
_statsTimer=setInterval(loadServerStats,2000);
|
||||
}
|
||||
|
||||
function stopStatsPolling(){
|
||||
if(_statsTimer){clearInterval(_statsTimer);_statsTimer=null;}
|
||||
}
|
||||
|
||||
function showClients(){
|
||||
const clients=[
|
||||
{name:'mieru (официальный CLI)',url:'https://github.com/enfein/mieru',desc:'CLI-клиент для macOS, Linux, Windows, OpenWrt'},
|
||||
@@ -271,19 +413,46 @@ function showClients(){
|
||||
// ──────────────── USERS ────────────────
|
||||
let warpUsers=new Set();
|
||||
|
||||
function fmtMB(mb){
|
||||
if(mb==null||mb===0)return'—';
|
||||
if(mb>=1024)return(mb/1024).toFixed(2)+' ГБ';
|
||||
if(mb>=1)return mb.toFixed(1)+' МБ';
|
||||
return Math.round(mb*1024)+' КБ';
|
||||
}
|
||||
|
||||
async function loadUsers(){
|
||||
const [ud,wd]=await Promise.all([api('/api/users'),api('/api/users/warp_status')]);
|
||||
const [ud,wd,sd]=await Promise.all([
|
||||
api('/api/users'),
|
||||
api('/api/users/warp_status'),
|
||||
api('/api/users/stats'),
|
||||
]);
|
||||
if(!ud)return;
|
||||
warpUsers=new Set(wd?.warp_users||[]);
|
||||
const statsMap={};
|
||||
(sd?.users||[]).forEach(s=>{statsMap[s.name]=s;});
|
||||
|
||||
const tbody=document.getElementById('users-tbody');
|
||||
if(!ud.users.length){
|
||||
tbody.innerHTML='<tr><td colspan="4" style="color:var(--muted);text-align:center;padding:20px">Нет пользователей</td></tr>';
|
||||
tbody.innerHTML='<tr><td colspan="8" style="color:var(--muted);text-align:center;padding:20px">Нет пользователей</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML=ud.users.map(u=>{
|
||||
const wOn=warpUsers.has(u.name);
|
||||
const s=statsMap[u.name]||{};
|
||||
const statusBadge=s.online
|
||||
?'<span class="badge badge-ok" style="margin-left:6px">● онлайн</span>'
|
||||
:s.last_active&&s.last_active!=='никогда'
|
||||
?`<span class="badge badge-off" style="margin-left:6px;white-space:nowrap">● ${escH(s.last_active)}</span>`
|
||||
:'<span class="badge badge-off" style="margin-left:6px">● никогда</span>';
|
||||
return `<tr>
|
||||
<td><strong>${escH(u.name)}</strong></td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:4px">
|
||||
<strong>${escH(u.name)}</strong>${statusBadge}
|
||||
</div>
|
||||
</td>
|
||||
<td style="white-space:nowrap;font-size:.82rem;color:var(--muted)">${fmtMB(s.day_mb)}</td>
|
||||
<td style="white-space:nowrap;font-size:.82rem;color:var(--muted)">${fmtMB(s.week_mb)}</td>
|
||||
<td style="white-space:nowrap;font-size:.82rem;color:var(--muted)">${fmtMB(s.month_mb)}</td>
|
||||
<td>
|
||||
<label class="toggle" title="Весь WARP для этого пользователя">
|
||||
<input type="checkbox" ${wOn?'checked':''} onchange="setWarp('${escH(u.name)}',this.checked)">
|
||||
@@ -717,7 +886,90 @@ async function getLetsEncrypt(){
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────── SECURITY / FAIL2BAN ────────────────
|
||||
// ──────────────── TELEGRAM BOT ────────────────
|
||||
async function loadBotPage() {
|
||||
const d = await api('/api/bot/status');
|
||||
if (!d) return;
|
||||
const statusEl = document.getElementById('bot-status-body');
|
||||
const installEl = document.getElementById('bot-install-card');
|
||||
const manageEl = document.getElementById('bot-manage-card');
|
||||
|
||||
if (d.installed) {
|
||||
statusEl.innerHTML = `
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px">
|
||||
<span class="badge badge-ok">Установлен</span>
|
||||
${d.running
|
||||
? '<span class="badge badge-ok">● Активен</span>'
|
||||
: '<span class="badge" style="background:rgba(255,95,109,.15);color:var(--danger)">● Остановлен</span>'}
|
||||
</div>
|
||||
<div style="font-size:.85rem;color:var(--muted)">
|
||||
Токен: <code style="color:var(--accent)">${escH(d.token||'—')}</code>
|
||||
</div>
|
||||
<div style="font-size:.85rem;color:var(--muted);margin-top:4px">
|
||||
Админы (Telegram ID): <strong style="color:var(--text)">${escH((d.admin_ids||[]).join(', ')||'—')}</strong>
|
||||
</div>`;
|
||||
installEl.style.display = 'none';
|
||||
manageEl.style.display = '';
|
||||
} else {
|
||||
statusEl.innerHTML = '<div style="color:var(--warn)">Бот не установлен</div>';
|
||||
installEl.style.display = '';
|
||||
manageEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function installBot() {
|
||||
const token = document.getElementById('bot-token').value.trim();
|
||||
const adminId = document.getElementById('bot-admin-id').value.trim();
|
||||
if (!token) { showToast('Введите токен', true); return; }
|
||||
if (!adminId) { showToast('Введите Telegram ID', true); return; }
|
||||
|
||||
const btn = document.querySelector('#bot-install-card .btn-primary');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Установка...';
|
||||
const d = await api('/api/bot/install', 'POST', {token, admin_id: adminId});
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4"/></svg> Установить / обновить';
|
||||
|
||||
const el = document.getElementById('bot-install-result');
|
||||
if (d?.ok) {
|
||||
el.innerHTML = `<div class="alert alert-ok">✓ Бот установлен и ${d.running ? 'запущен' : 'установлен (проверьте логи)'}</div>`;
|
||||
showToast('Бот установлен');
|
||||
loadBotPage();
|
||||
} else {
|
||||
el.innerHTML = `<div class="alert alert-err">Ошибка: ${escH(d?.error||d?.output||'')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallBot() {
|
||||
if (!confirm('Удалить Telegram-бота? Это действие необратимо.')) return;
|
||||
await api('/api/bot/uninstall', 'POST');
|
||||
showToast('Бот удалён');
|
||||
loadBotPage();
|
||||
}
|
||||
|
||||
async function restartBot() {
|
||||
const d = await api('/api/bot/config', 'POST', {});
|
||||
if (d?.ok) {
|
||||
showToast('Бот перезапущен');
|
||||
setTimeout(loadBotPage, 1500);
|
||||
} else {
|
||||
showToast('Ошибка перезапуска', true);
|
||||
}
|
||||
}
|
||||
|
||||
async function addBotAdmin() {
|
||||
const id = document.getElementById('bot-add-admin-id').value.trim();
|
||||
if (!id) { showToast('Введите Telegram ID', true); return; }
|
||||
const d = await api('/api/bot/config', 'POST', {admin_id: id});
|
||||
const el = document.getElementById('bot-admin-result');
|
||||
if (d?.ok) {
|
||||
el.innerHTML = '<div class="alert alert-ok">✓ Админ добавлен</div>';
|
||||
document.getElementById('bot-add-admin-id').value = '';
|
||||
loadBotPage();
|
||||
} else {
|
||||
el.innerHTML = `<div class="alert alert-err">Ошибка: ${escH(d?.error||'')}</div>`;
|
||||
}
|
||||
}
|
||||
async function loadSecurityPage() {
|
||||
const d = await api('/api/fail2ban/status');
|
||||
if (!d) return;
|
||||
|
||||
Reference in New Issue
Block a user