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:
Андрей Бобырев
2026-05-20 13:25:14 +03:00
parent f9f9195e14
commit 399e9ab2fb
8 changed files with 632 additions and 4 deletions

95
server/api/pc.py Normal file
View 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"}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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);

View File

@@ -40,11 +40,28 @@
</div> </div>
</div> </div>
<!-- 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>
<!-- Servers Tab -->
<div id="panel-servers">
<div class="toolbar"> <div class="toolbar">
<button class="btn-primary" id="addServerBtn" onclick="showAddServer()">+ Добавить сервер</button> <button class="btn-primary" id="addServerBtn" onclick="showAddServer()">+ Добавить сервер</button>
</div> </div>
<div class="servers-grid" id="servers-grid"></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')">&times;</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">
автор:&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> автор:&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> </footer>

50
windows/README_PC.md Normal file
View 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
View 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 ""

View 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
}