mirror of
https://github.com/andrey271192/vps_monitoring.git
synced 2026-09-20 11:55:34 +00:00
feat: add PC monitoring tab with Windows agent
- New /api/pc endpoints (heartbeat, list, delete) - Dashboard tabs: Серверы / ПК with auto-refresh - PowerShell agent: CPU, RAM, Disk, Network, GPU, Top processes - One-command Windows installer with Scheduled Task - PC setup instructions modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
95
server/api/pc.py
Normal file
95
server/api/pc.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
|
||||
from server.auth import require_auth
|
||||
from server.config import DATA_DIR
|
||||
|
||||
router = APIRouter(prefix="/api/pc", tags=["pc"])
|
||||
|
||||
# In-memory PC metrics cache
|
||||
pc_metrics: Dict[str, dict] = {}
|
||||
|
||||
PC_FILE = DATA_DIR / "pc_agents.json"
|
||||
|
||||
|
||||
def _load_pc_data():
|
||||
if PC_FILE.exists():
|
||||
with open(PC_FILE) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def _save_pc_data(data):
|
||||
with open(PC_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
@router.post("/heartbeat")
|
||||
async def pc_heartbeat(request: Request):
|
||||
"""Receive metrics from Windows PC agent. No auth required (agent sends by name)."""
|
||||
body = await request.json()
|
||||
agent_name = body.get("agent_name", "")
|
||||
if not agent_name:
|
||||
return {"status": "error", "detail": "agent_name required"}
|
||||
|
||||
metrics = body.get("metrics", {})
|
||||
timestamp = body.get("timestamp", datetime.now().isoformat())
|
||||
|
||||
entry = {
|
||||
"agent_name": agent_name,
|
||||
"metrics": metrics,
|
||||
"timestamp": timestamp,
|
||||
"ip": request.client.host if request.client else "",
|
||||
"online": True,
|
||||
"last_seen": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
pc_metrics[agent_name] = entry
|
||||
|
||||
# Persist
|
||||
stored = _load_pc_data()
|
||||
stored[agent_name] = entry
|
||||
_save_pc_data(stored)
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def pc_list(request: Request, user: str = Depends(require_auth)):
|
||||
"""List all PC agents with metrics."""
|
||||
stored = _load_pc_data()
|
||||
|
||||
# Merge with in-memory (fresher)
|
||||
for name, data in pc_metrics.items():
|
||||
stored[name] = data
|
||||
|
||||
# Mark stale agents (no heartbeat > 3 min)
|
||||
now = datetime.now()
|
||||
result = []
|
||||
for name, data in stored.items():
|
||||
last_seen = data.get("last_seen", "")
|
||||
try:
|
||||
last_dt = datetime.fromisoformat(last_seen)
|
||||
stale = (now - last_dt).total_seconds() > 180
|
||||
except Exception:
|
||||
stale = True
|
||||
|
||||
data["online"] = not stale
|
||||
result.append(data)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/{agent_name}")
|
||||
async def pc_delete(agent_name: str, request: Request, user: str = Depends(require_auth)):
|
||||
"""Remove a PC agent."""
|
||||
pc_metrics.pop(agent_name, None)
|
||||
|
||||
stored = _load_pc_data()
|
||||
stored.pop(agent_name, None)
|
||||
_save_pc_data(stored)
|
||||
|
||||
return {"status": "ok"}
|
||||
@@ -16,6 +16,7 @@ from server.auth import get_current_user
|
||||
from server.api.servers import router as servers_router
|
||||
from server.api.auth_routes import router as auth_router
|
||||
from server.api.ssh_ws import router as ssh_router
|
||||
from server.api.pc import router as pc_router
|
||||
from server.services.monitor import monitor_loop
|
||||
from server.services.telegram_bot import start_bot, stop_bot
|
||||
from server.services.alerter import check_alerts
|
||||
@@ -62,10 +63,16 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
|
||||
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
|
||||
|
||||
# Static downloads (Windows agent)
|
||||
DOWNLOADS_DIR = os.path.join(os.path.dirname(BASE_DIR), "windows")
|
||||
if os.path.isdir(DOWNLOADS_DIR):
|
||||
app.mount("/static/downloads", StaticFiles(directory=DOWNLOADS_DIR), name="downloads")
|
||||
|
||||
# Include routers
|
||||
app.include_router(servers_router)
|
||||
app.include_router(auth_router)
|
||||
app.include_router(ssh_router)
|
||||
app.include_router(pc_router)
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
|
||||
@@ -600,6 +600,39 @@ main {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.app-footer {
|
||||
text-align: center;
|
||||
|
||||
@@ -413,6 +413,115 @@ document.addEventListener('click', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Tabs
|
||||
let activeTab = 'servers';
|
||||
|
||||
function switchTab(tab) {
|
||||
activeTab = tab;
|
||||
document.getElementById('tab-servers').classList.toggle('active', tab === 'servers');
|
||||
document.getElementById('tab-pc').classList.toggle('active', tab === 'pc');
|
||||
document.getElementById('panel-servers').style.display = tab === 'servers' ? '' : 'none';
|
||||
document.getElementById('panel-pc').style.display = tab === 'pc' ? '' : 'none';
|
||||
|
||||
if (tab === 'pc') loadPCs();
|
||||
}
|
||||
|
||||
// PC monitoring
|
||||
let pcAgents = [];
|
||||
|
||||
async function loadPCs() {
|
||||
try {
|
||||
const resp = await fetch('/api/pc/list', {credentials: 'include'});
|
||||
if (resp.status === 401) return;
|
||||
pcAgents = await resp.json();
|
||||
renderPCs();
|
||||
} catch (e) {
|
||||
console.error('PC load error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPCs() {
|
||||
const grid = document.getElementById('pc-grid');
|
||||
|
||||
if (!pcAgents.length) {
|
||||
grid.innerHTML = '<div style="text-align:center;padding:60px;opacity:0.5;grid-column:1/-1">Нет подключённых ПК. Установите агент.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = pcAgents.map(pc => {
|
||||
const m = pc.metrics || {};
|
||||
const isOnline = pc.online;
|
||||
const cpu = m.cpu_percent || 0;
|
||||
const ram = m.ram_percent || 0;
|
||||
const disk = m.disk_percent || 0;
|
||||
|
||||
const cpuClass = cpu > 90 ? 'crit' : cpu > 70 ? 'warn' : '';
|
||||
const ramClass = ram > 90 ? 'crit' : ram > 70 ? 'warn' : '';
|
||||
const diskClass = disk > 90 ? 'crit' : disk > 70 ? 'warn' : '';
|
||||
|
||||
const disksHtml = (m.disks || []).map(d =>
|
||||
`<div style="font-size:11px;color:var(--text-secondary);margin-top:2px">${d.drive} ${d.used_gb}/${d.total_gb} GB (${d.percent}%)</div>`
|
||||
).join('');
|
||||
|
||||
const procsHtml = (m.top_processes || []).slice(0, 3).map(p =>
|
||||
`<span style="font-size:10px;padding:2px 6px;background:var(--bg-primary);border-radius:4px;margin:1px">${p.name} ${p.ram_mb}MB</span>`
|
||||
).join(' ');
|
||||
|
||||
return `
|
||||
<div class="server-card ${isOnline ? 'online' : 'offline'}">
|
||||
<div class="server-card-header">
|
||||
<div>
|
||||
<div class="name">💻 ${pc.agent_name}</div>
|
||||
<div class="host">${m.hostname || ''} • ${pc.ip || ''}</div>
|
||||
</div>
|
||||
<span class="status-badge ${isOnline ? 'online' : 'offline'}">
|
||||
<span class="status-dot ${isOnline ? 'online' : 'offline'}"></span>
|
||||
${isOnline ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
${isOnline ? `
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<span class="label">CPU</span>
|
||||
<span class="value ${cpuClass}">${cpu}%</span>
|
||||
<div class="progress-bar"><div class="fill ${cpuClass || 'ok'}" style="width:${cpu}%"></div></div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="label">RAM</span>
|
||||
<span class="value ${ramClass}">${ram}%</span>
|
||||
<div class="progress-bar"><div class="fill ${ramClass || 'ok'}" style="width:${ram}%"></div></div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="label">Disk</span>
|
||||
<span class="value ${diskClass}">${disk}%</span>
|
||||
<div class="progress-bar"><div class="fill ${diskClass || 'ok'}" style="width:${disk}%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">
|
||||
${m.uptime || ''} ${m.os ? '• ' + m.os.substring(0, 30) : ''}
|
||||
</div>
|
||||
${m.gpu_name ? `<div style="font-size:11px;color:var(--text-secondary)">GPU: ${m.gpu_name}</div>` : ''}
|
||||
${disksHtml}
|
||||
${procsHtml ? `<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:2px">${procsHtml}</div>` : ''}
|
||||
` : '<div style="padding:20px 0;text-align:center;opacity:0.5">Нет данных</div>'}
|
||||
<div class="server-card-actions">
|
||||
<button class="danger" onclick="deletePC('${pc.agent_name}')">🗑 Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function deletePC(name) {
|
||||
if (!confirm(`Удалить ПК "${name}" из мониторинга?`)) return;
|
||||
await fetch(`/api/pc/${encodeURIComponent(name)}`, {method: 'DELETE', credentials: 'include'});
|
||||
loadPCs();
|
||||
}
|
||||
|
||||
function showPCSetup() {
|
||||
document.getElementById('pcSetupModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Initial load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
applyTheme(currentTheme);
|
||||
@@ -420,4 +529,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadServers();
|
||||
});
|
||||
|
||||
setInterval(loadServers, 30000);
|
||||
setInterval(() => {
|
||||
if (activeTab === 'servers') loadServers();
|
||||
else loadPCs();
|
||||
}, 30000);
|
||||
|
||||
@@ -40,11 +40,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn-primary" id="addServerBtn" onclick="showAddServer()">+ Добавить сервер</button>
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<button class="tab active" id="tab-servers" onclick="switchTab('servers')">🖥 Серверы</button>
|
||||
<button class="tab" id="tab-pc" onclick="switchTab('pc')">💻 ПК</button>
|
||||
</div>
|
||||
|
||||
<div class="servers-grid" id="servers-grid"></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-secondary" onclick="showPCSetup()">📋 Инструкция установки</button>
|
||||
<button class="btn-secondary" onclick="loadPCs()">🔄 Обновить</button>
|
||||
</div>
|
||||
<div class="servers-grid" id="pc-grid"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Add Server Modal -->
|
||||
@@ -165,6 +182,46 @@
|
||||
</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')">×</button>
|
||||
</div>
|
||||
<div class="instructions-section">
|
||||
<div class="instr-card">
|
||||
<h4>📥 Быстрая установка (PowerShell от Администратора)</h4>
|
||||
<code id="pc-install-cmd">powershell -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri 'http://77.239.126.123:7272/static/downloads/install_agent.ps1' -OutFile install_agent.ps1; .\install_agent.ps1 -ServerUrl 'http://77.239.126.123:7272' -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 class="instr-card">
|
||||
<h4>📁 Файлы</h4>
|
||||
<p>Агент: <code style="display:inline">C:\VPS-Monitor\vps_monitor_agent.ps1</code><br>
|
||||
Конфиг: <code style="display:inline">C:\VPS-Monitor\agent_config.json</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer">
|
||||
автор: <a href="https://github.com/andrey271192" target="_blank">GitHub</a> · <a href="https://boosty.to/iot_andrey" target="_blank">Boosty</a> · <a href="https://t.me/Iot_andrey" target="_blank">Поддержка</a> · <a href="https://t.me/Iot_andrey" target="_blank">@Iot_andrey</a>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user