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.servers import router as servers_router
|
||||||
from server.api.auth_routes import router as auth_router
|
from server.api.auth_routes import router as auth_router
|
||||||
from server.api.ssh_ws import router as ssh_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.monitor import monitor_loop
|
||||||
from server.services.telegram_bot import start_bot, stop_bot
|
from server.services.telegram_bot import start_bot, stop_bot
|
||||||
from server.services.alerter import check_alerts
|
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")
|
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
|
||||||
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
|
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
|
# Include routers
|
||||||
app.include_router(servers_router)
|
app.include_router(servers_router)
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
app.include_router(ssh_router)
|
app.include_router(ssh_router)
|
||||||
|
app.include_router(pc_router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
|||||||
@@ -600,6 +600,39 @@ main {
|
|||||||
overflow: hidden;
|
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 */
|
/* Footer */
|
||||||
.app-footer {
|
.app-footer {
|
||||||
text-align: center;
|
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
|
// Initial load
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
applyTheme(currentTheme);
|
applyTheme(currentTheme);
|
||||||
@@ -420,4 +529,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
loadServers();
|
loadServers();
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(loadServers, 30000);
|
setInterval(() => {
|
||||||
|
if (activeTab === 'servers') loadServers();
|
||||||
|
else loadPCs();
|
||||||
|
}, 30000);
|
||||||
|
|||||||
@@ -40,11 +40,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar">
|
<!-- Tabs -->
|
||||||
<button class="btn-primary" id="addServerBtn" onclick="showAddServer()">+ Добавить сервер</button>
|
<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>
|
||||||
|
|
||||||
<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>
|
</main>
|
||||||
|
|
||||||
<!-- Add Server Modal -->
|
<!-- Add Server Modal -->
|
||||||
@@ -165,6 +182,46 @@
|
|||||||
</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')">×</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">
|
<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>
|
автор: <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>
|
</footer>
|
||||||
|
|||||||
50
windows/README_PC.md
Normal file
50
windows/README_PC.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# VPS Monitor — Windows PC Agent
|
||||||
|
|
||||||
|
## Quick Install (one command)
|
||||||
|
|
||||||
|
Run in PowerShell as Administrator:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://77.239.126.123.nip.io/static/downloads/install_agent.ps1 | iex -ServerUrl "http://77.239.126.123:7272" -AgentName "MyPC"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or download and run:
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest -Uri "http://77.239.126.123:7272/static/downloads/install_agent.ps1" -OutFile install_agent.ps1
|
||||||
|
powershell -ExecutionPolicy Bypass -File install_agent.ps1 -ServerUrl "http://77.239.126.123:7272" -AgentName "Office-PC"
|
||||||
|
```
|
||||||
|
|
||||||
|
## What it monitors
|
||||||
|
|
||||||
|
- CPU usage (%)
|
||||||
|
- RAM usage (total, used, %)
|
||||||
|
- Disk usage (all drives)
|
||||||
|
- Network adapters (speed, traffic)
|
||||||
|
- GPU info
|
||||||
|
- Uptime
|
||||||
|
- Top 5 processes by CPU
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `install_agent.ps1` | Installer (creates task, config) |
|
||||||
|
| `vps_monitor_agent.ps1` | Agent (collects & sends metrics) |
|
||||||
|
| `C:\VPS-Monitor\agent_config.json` | Config (server URL, name) |
|
||||||
|
|
||||||
|
## Management
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Check status
|
||||||
|
Get-ScheduledTask 'VPS-Monitor-Agent'
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
Stop-ScheduledTask 'VPS-Monitor-Agent'
|
||||||
|
|
||||||
|
# Start
|
||||||
|
Start-ScheduledTask 'VPS-Monitor-Agent'
|
||||||
|
|
||||||
|
# Uninstall
|
||||||
|
Unregister-ScheduledTask 'VPS-Monitor-Agent' -Confirm:$false
|
||||||
|
Remove-Item -Recurse C:\VPS-Monitor
|
||||||
|
```
|
||||||
114
windows/install_agent.ps1
Normal file
114
windows/install_agent.ps1
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# VPS Monitor — Windows Agent Installer
|
||||||
|
# Usage: powershell -ExecutionPolicy Bypass -File install_agent.ps1 -ServerUrl "http://IP:7272" -AgentName "MyPC"
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$ServerUrl,
|
||||||
|
|
||||||
|
[string]$AgentName = $env:COMPUTERNAME,
|
||||||
|
|
||||||
|
[int]$Interval = 60
|
||||||
|
)
|
||||||
|
|
||||||
|
$InstallDir = "C:\VPS-Monitor"
|
||||||
|
$TaskName = "VPS-Monitor-Agent"
|
||||||
|
|
||||||
|
Write-Host "================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " VPS Monitor — Agent Installer" -ForegroundColor Cyan
|
||||||
|
Write-Host "================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Server: $ServerUrl" -ForegroundColor White
|
||||||
|
Write-Host " Name: $AgentName" -ForegroundColor White
|
||||||
|
Write-Host " Interval: ${Interval}s" -ForegroundColor White
|
||||||
|
Write-Host " Install: $InstallDir" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Create install directory
|
||||||
|
if (-not (Test-Path $InstallDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
||||||
|
Write-Host "[+] Created $InstallDir" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download agent script
|
||||||
|
Write-Host "[*] Downloading agent..." -ForegroundColor Yellow
|
||||||
|
$AgentUrl = "$ServerUrl/static/downloads/vps_monitor_agent.ps1"
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -Uri $AgentUrl -OutFile "$InstallDir\vps_monitor_agent.ps1" -UseBasicParsing
|
||||||
|
Write-Host "[+] Agent downloaded" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "[!] Download failed, using local copy..." -ForegroundColor Yellow
|
||||||
|
# Fallback: copy from same directory
|
||||||
|
$localAgent = Join-Path $PSScriptRoot "vps_monitor_agent.ps1"
|
||||||
|
if (Test-Path $localAgent) {
|
||||||
|
Copy-Item $localAgent "$InstallDir\vps_monitor_agent.ps1"
|
||||||
|
Write-Host "[+] Local copy used" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "[-] No agent script found!" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create config
|
||||||
|
$config = @{
|
||||||
|
server_url = $ServerUrl
|
||||||
|
agent_name = $AgentName
|
||||||
|
interval = $Interval
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
Set-Content -Path "$InstallDir\agent_config.json" -Value $config
|
||||||
|
Write-Host "[+] Config saved" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create scheduled task (run at startup + every 5 min check)
|
||||||
|
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
|
if ($existingTask) {
|
||||||
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||||
|
Write-Host "[*] Removed old task" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = New-ScheduledTaskAction `
|
||||||
|
-Execute "powershell.exe" `
|
||||||
|
-Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$InstallDir\vps_monitor_agent.ps1`"" `
|
||||||
|
-WorkingDirectory $InstallDir
|
||||||
|
|
||||||
|
$triggerStartup = New-ScheduledTaskTrigger -AtStartup
|
||||||
|
$triggerNow = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 1)
|
||||||
|
|
||||||
|
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
||||||
|
|
||||||
|
$settings = New-ScheduledTaskSettingsSet `
|
||||||
|
-AllowStartIfOnBatteries `
|
||||||
|
-DontStopIfGoingOnBatteries `
|
||||||
|
-RestartCount 3 `
|
||||||
|
-RestartInterval (New-TimeSpan -Minutes 1) `
|
||||||
|
-ExecutionTimeLimit (New-TimeSpan -Days 365)
|
||||||
|
|
||||||
|
Register-ScheduledTask `
|
||||||
|
-TaskName $TaskName `
|
||||||
|
-Action $action `
|
||||||
|
-Trigger $triggerStartup `
|
||||||
|
-Principal $principal `
|
||||||
|
-Settings $settings `
|
||||||
|
-Description "VPS Monitor PC Agent — sends metrics to $ServerUrl" | Out-Null
|
||||||
|
|
||||||
|
Write-Host "[+] Scheduled task created" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Start now
|
||||||
|
Start-ScheduledTask -TaskName $TaskName
|
||||||
|
Write-Host "[+] Agent started!" -ForegroundColor Green
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Installation complete!" -ForegroundColor Green
|
||||||
|
Write-Host "================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Files: $InstallDir" -ForegroundColor White
|
||||||
|
Write-Host " Task: $TaskName" -ForegroundColor White
|
||||||
|
Write-Host " Status: Running" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Commands:" -ForegroundColor Gray
|
||||||
|
Write-Host " Check: Get-ScheduledTask '$TaskName'" -ForegroundColor Gray
|
||||||
|
Write-Host " Stop: Stop-ScheduledTask '$TaskName'" -ForegroundColor Gray
|
||||||
|
Write-Host " Start: Start-ScheduledTask '$TaskName'" -ForegroundColor Gray
|
||||||
|
Write-Host " Remove: Unregister-ScheduledTask '$TaskName'" -ForegroundColor Gray
|
||||||
|
Write-Host " Logs: type $InstallDir\agent_config.json" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
160
windows/vps_monitor_agent.ps1
Normal file
160
windows/vps_monitor_agent.ps1
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# VPS Monitor — Windows PC Agent
|
||||||
|
# Sends system metrics to VPS Monitoring server
|
||||||
|
#
|
||||||
|
# Install: Run install_agent.ps1
|
||||||
|
# Manual: powershell -ExecutionPolicy Bypass -File vps_monitor_agent.ps1
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ServerUrl = "",
|
||||||
|
[string]$AgentName = "",
|
||||||
|
[int]$Interval = 60
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
$ConfigPath = Join-Path $PSScriptRoot "agent_config.json"
|
||||||
|
if (Test-Path $ConfigPath) {
|
||||||
|
$config = Get-Content $ConfigPath | ConvertFrom-Json
|
||||||
|
if (-not $ServerUrl) { $ServerUrl = $config.server_url }
|
||||||
|
if (-not $AgentName) { $AgentName = $config.agent_name }
|
||||||
|
if ($config.interval) { $Interval = $config.interval }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $ServerUrl -or -not $AgentName) {
|
||||||
|
Write-Host "ERROR: server_url and agent_name required" -ForegroundColor Red
|
||||||
|
Write-Host "Edit agent_config.json or pass -ServerUrl and -AgentName"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " VPS Monitor — PC Agent" -ForegroundColor Cyan
|
||||||
|
Write-Host " Server: $ServerUrl" -ForegroundColor Gray
|
||||||
|
Write-Host " Name: $AgentName" -ForegroundColor Gray
|
||||||
|
Write-Host " Interval: ${Interval}s" -ForegroundColor Gray
|
||||||
|
Write-Host "================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
function Get-SystemMetrics {
|
||||||
|
$metrics = @{}
|
||||||
|
|
||||||
|
# Hostname
|
||||||
|
$metrics["hostname"] = $env:COMPUTERNAME
|
||||||
|
|
||||||
|
# OS
|
||||||
|
$os = Get-CimInstance Win32_OperatingSystem
|
||||||
|
$metrics["os"] = "$($os.Caption) $($os.Version)"
|
||||||
|
|
||||||
|
# CPU
|
||||||
|
$cpu = Get-CimInstance Win32_Processor
|
||||||
|
$metrics["cpu_name"] = $cpu.Name
|
||||||
|
$metrics["cpu_percent"] = [math]::Round((Get-Counter '\Processor(_Total)\% Processor Time' -ErrorAction SilentlyContinue).CounterSamples[0].CookedValue, 1)
|
||||||
|
|
||||||
|
# RAM
|
||||||
|
$totalRam = [math]::Round($os.TotalVisibleMemorySize / 1024, 0)
|
||||||
|
$freeRam = [math]::Round($os.FreePhysicalMemory / 1024, 0)
|
||||||
|
$usedRam = $totalRam - $freeRam
|
||||||
|
$metrics["ram_total_mb"] = $totalRam
|
||||||
|
$metrics["ram_used_mb"] = $usedRam
|
||||||
|
$metrics["ram_percent"] = [math]::Round(($usedRam / $totalRam) * 100, 1)
|
||||||
|
|
||||||
|
# Disk (all drives)
|
||||||
|
$disks = @()
|
||||||
|
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
|
||||||
|
$totalGb = [math]::Round($_.Size / 1GB, 1)
|
||||||
|
$freeGb = [math]::Round($_.FreeSpace / 1GB, 1)
|
||||||
|
$usedGb = $totalGb - $freeGb
|
||||||
|
$pct = if ($totalGb -gt 0) { [math]::Round(($usedGb / $totalGb) * 100, 1) } else { 0 }
|
||||||
|
$disks += @{
|
||||||
|
drive = $_.DeviceID
|
||||||
|
total_gb = $totalGb
|
||||||
|
used_gb = $usedGb
|
||||||
|
free_gb = $freeGb
|
||||||
|
percent = $pct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$metrics["disks"] = $disks
|
||||||
|
|
||||||
|
# Primary disk summary
|
||||||
|
$primary = $disks | Where-Object { $_.drive -eq "C:" } | Select-Object -First 1
|
||||||
|
if ($primary) {
|
||||||
|
$metrics["disk_total_gb"] = $primary.total_gb
|
||||||
|
$metrics["disk_used_gb"] = $primary.used_gb
|
||||||
|
$metrics["disk_percent"] = $primary.percent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Network
|
||||||
|
$adapters = Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq "Up" }
|
||||||
|
$netInfo = @()
|
||||||
|
foreach ($a in $adapters) {
|
||||||
|
$stats = Get-NetAdapterStatistics -Name $a.Name -ErrorAction SilentlyContinue
|
||||||
|
$netInfo += @{
|
||||||
|
name = $a.Name
|
||||||
|
speed_mbps = [math]::Round($a.LinkSpeed.Replace(" Gbps","000").Replace(" Mbps","") -as [double], 0)
|
||||||
|
sent_mb = if ($stats) { [math]::Round($stats.SentBytes / 1MB, 0) } else { 0 }
|
||||||
|
received_mb = if ($stats) { [math]::Round($stats.ReceivedBytes / 1MB, 0) } else { 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$metrics["network"] = $netInfo
|
||||||
|
|
||||||
|
# Uptime
|
||||||
|
$uptime = (Get-Date) - $os.LastBootUpTime
|
||||||
|
$metrics["uptime"] = "up $($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m"
|
||||||
|
$metrics["uptime_seconds"] = [math]::Round($uptime.TotalSeconds, 0)
|
||||||
|
|
||||||
|
# GPU (if available)
|
||||||
|
try {
|
||||||
|
$gpu = Get-CimInstance Win32_VideoController | Select-Object -First 1
|
||||||
|
if ($gpu) {
|
||||||
|
$metrics["gpu_name"] = $gpu.Name
|
||||||
|
$metrics["gpu_ram_mb"] = [math]::Round($gpu.AdapterRAM / 1MB, 0)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
# Top processes by CPU
|
||||||
|
$topProcs = Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 |
|
||||||
|
ForEach-Object { @{ name = $_.ProcessName; cpu = [math]::Round($_.CPU, 1); ram_mb = [math]::Round($_.WorkingSet64 / 1MB, 0) } }
|
||||||
|
$metrics["top_processes"] = $topProcs
|
||||||
|
|
||||||
|
return $metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-Metrics($metrics) {
|
||||||
|
$body = @{
|
||||||
|
agent_name = $AgentName
|
||||||
|
timestamp = (Get-Date).ToString("o")
|
||||||
|
metrics = $metrics
|
||||||
|
} | ConvertTo-Json -Depth 5
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Invoke-RestMethod -Uri "$ServerUrl/api/pc/heartbeat" `
|
||||||
|
-Method POST `
|
||||||
|
-Body $body `
|
||||||
|
-ContentType "application/json" `
|
||||||
|
-TimeoutSec 15
|
||||||
|
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-Host " [!] Send failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main loop
|
||||||
|
Write-Host "Agent started. Press Ctrl+C to stop." -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$ts = Get-Date -Format "HH:mm:ss"
|
||||||
|
|
||||||
|
try {
|
||||||
|
$metrics = Get-SystemMetrics
|
||||||
|
$ok = Send-Metrics $metrics
|
||||||
|
|
||||||
|
if ($ok) {
|
||||||
|
Write-Host "[$ts] OK — CPU: $($metrics.cpu_percent)% RAM: $($metrics.ram_percent)% Disk: $($metrics.disk_percent)%" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "[$ts] ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $Interval
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user