Add live Windows Gaming PC stats with metric bars
- New API /api/windows/stats: SSHes into gamingpc, runs PowerShell for CPU/RAM/Disk/Uptime - Live metric bars on the Windows PC card (CPU/RAM/C:) updating every 30s - Duplicate barColor() removed
This commit is contained in:
+50
-1
@@ -268,10 +268,13 @@
|
|||||||
<div class="card-expand"><div class="detail-row"><span class="detail-label">WAN</span><span class="detail-val">68.202.6.107</span></div><div class="detail-row"><span class="detail-label">Proxy</span><span class="detail-val">192.168.0.115</span></div><div class="detail-row"><span class="detail-label">VPN</span><span class="detail-val">Tailscale mesh — 2 nodes</span></div></div>
|
<div class="card-expand"><div class="detail-row"><span class="detail-label">WAN</span><span class="detail-val">68.202.6.107</span></div><div class="detail-row"><span class="detail-label">Proxy</span><span class="detail-val">192.168.0.115</span></div><div class="detail-row"><span class="detail-label">VPN</span><span class="detail-val">Tailscale mesh — 2 nodes</span></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card nolink" onclick="toggleCardExpand(this)">
|
<div class="card nolink" id="win-pc-card" onclick="toggleCardExpand(this)">
|
||||||
<div class="card-header"><div class="card-icon" style="background:#fce7f3;">🖥️</div><span class="card-title">Windows Gaming PC</span><span class="expand-indicator">▾</span></div>
|
<div class="card-header"><div class="card-icon" style="background:#fce7f3;">🖥️</div><span class="card-title">Windows Gaming PC</span><span class="expand-indicator">▾</span></div>
|
||||||
<p class="card-desc"><strong>i7-8700K · RTX 2080 4GB · 32GB RAM</strong> — ComfyUI :8188, Ollama qwen2.5vl :11434. Tailscale 100.84.53.121</p>
|
<p class="card-desc"><strong>i7-8700K · RTX 2080 4GB · 32GB RAM</strong> — ComfyUI :8188, Ollama qwen2.5vl :11434. Tailscale 100.84.53.121</p>
|
||||||
<div class="card-meta"><span class="tag">Win 11 Pro</span><span class="tag">NVMe</span><span class="tag">C: 931GB</span></div>
|
<div class="card-meta"><span class="tag">Win 11 Pro</span><span class="tag">NVMe</span><span class="tag">C: 931GB</span></div>
|
||||||
|
<div id="win-pc-metrics" style="margin: 10px 0 0 0; font-size: 13px;">
|
||||||
|
<span class="stats-error">Loading Windows stats...</span>
|
||||||
|
</div>
|
||||||
<div class="card-expand">
|
<div class="card-expand">
|
||||||
<div class="detail-row"><span class="detail-label">CPU</span><span class="detail-val">Intel Core i7-8700K @ 3.70GHz (6C/12T)</span></div>
|
<div class="detail-row"><span class="detail-label">CPU</span><span class="detail-val">Intel Core i7-8700K @ 3.70GHz (6C/12T)</span></div>
|
||||||
<div class="detail-row"><span class="detail-label">GPU</span><span class="detail-val">NVIDIA RTX 2080 (4GB) + Intel UHD 630</span></div>
|
<div class="detail-row"><span class="detail-label">GPU</span><span class="detail-val">NVIDIA RTX 2080 (4GB) + Intel UHD 630</span></div>
|
||||||
@@ -506,14 +509,60 @@ async function updateDocker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Windows Gaming PC Live Stats ──────────────────────────────────────
|
||||||
|
async function updateWindowsStats() {
|
||||||
|
const el = document.getElementById("win-pc-metrics");
|
||||||
|
if (!el) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch("/api/windows/stats");
|
||||||
|
const d = await r.json();
|
||||||
|
if (!d.online) {
|
||||||
|
el.innerHTML = `<span class="stats-error">Windows PC offline</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const memPct = d.mem_total_gb > 0 ? Math.round((d.mem_used_gb / d.mem_total_gb) * 100) : 0;
|
||||||
|
const diskPct = d.disk_c_total_gb > 0 ? Math.round((d.disk_c_used_gb / d.disk_c_total_gb) * 100) : 0;
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="metric-row" style="margin-bottom:2px">
|
||||||
|
<span class="metric-label" style="font-size:12px">CPU</span>
|
||||||
|
<div class="metric-bar-bg" style="height:6px">
|
||||||
|
<div class="metric-bar-fill" style="width:${Math.min(d.cpu, 100)}%;background:${barColor(d.cpu)}"></div>
|
||||||
|
</div>
|
||||||
|
<span class="metric-val" style="font-size:11px">${d.cpu}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row" style="margin-bottom:2px">
|
||||||
|
<span class="metric-label" style="font-size:12px">RAM</span>
|
||||||
|
<div class="metric-bar-bg" style="height:6px">
|
||||||
|
<div class="metric-bar-fill" style="width:${memPct}%;background:${barColor(memPct)}"></div>
|
||||||
|
</div>
|
||||||
|
<span class="metric-val" style="font-size:11px">${d.mem_used_gb}/${d.mem_total_gb} GB</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row" style="margin-bottom:2px">
|
||||||
|
<span class="metric-label" style="font-size:12px">C:</span>
|
||||||
|
<div class="metric-bar-bg" style="height:6px">
|
||||||
|
<div class="metric-bar-fill" style="width:${diskPct}%;background:${barColor(diskPct)}"></div>
|
||||||
|
</div>
|
||||||
|
<span class="metric-val" style="font-size:11px">${d.disk_c_used_gb}/${d.disk_c_total_gb} GB</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;color:var(--gray-400);margin-top:4px">
|
||||||
|
Uptime: ${d.uptime_days}d · Win 11 Pro
|
||||||
|
</div>`;
|
||||||
|
} catch(e) {
|
||||||
|
el.innerHTML = `<span class="stats-error">Windows stats unavailable</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Init ─────────────────────────────────────────────────────────────
|
// ─── Init ─────────────────────────────────────────────────────────────
|
||||||
renderServices();
|
renderServices();
|
||||||
updateStatus();
|
updateStatus();
|
||||||
updateHosts();
|
updateHosts();
|
||||||
updateDocker();
|
updateDocker();
|
||||||
|
updateWindowsStats();
|
||||||
setInterval(updateStatus, 60000);
|
setInterval(updateStatus, 60000);
|
||||||
setInterval(updateHosts, 30000);
|
setInterval(updateHosts, 30000);
|
||||||
setInterval(updateDocker, 30000);
|
setInterval(updateDocker, 30000);
|
||||||
|
setInterval(updateWindowsStats, 30000);
|
||||||
|
|
||||||
// ─── Dark Mode Toggle ──────────────────────────────────────────────────
|
// ─── Dark Mode Toggle ──────────────────────────────────────────────────
|
||||||
function toggleDark() {
|
function toggleDark() {
|
||||||
|
|||||||
@@ -241,6 +241,49 @@ async def docker_stats():
|
|||||||
return {"error": str(e)[:200], "containers": []}
|
return {"error": str(e)[:200], "containers": []}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/windows/stats")
|
||||||
|
async def windows_stats():
|
||||||
|
"""Get live stats from Windows Gaming PC via PowerShell over SSH."""
|
||||||
|
import subprocess, base64, json as _json
|
||||||
|
|
||||||
|
# PowerShell script: backslash counter paths need single \, Python raw string preserves them
|
||||||
|
ps_script = r'''
|
||||||
|
$cpu=(Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples[0].CookedValue
|
||||||
|
$os=Get-CimInstance Win32_OperatingSystem
|
||||||
|
$disk=Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"
|
||||||
|
$uptime=(Get-Date)-$os.LastBootUpTime
|
||||||
|
@{
|
||||||
|
'cpu'=[math]::Round($cpu,1)
|
||||||
|
'mem_total_gb'=[math]::Round($os.TotalVisibleMemorySize/1MB,1)
|
||||||
|
'mem_free_gb'=[math]::Round($os.FreePhysicalMemory/1MB,1)
|
||||||
|
'disk_c_total_gb'=[math]::Round($disk.Size/1GB,1)
|
||||||
|
'disk_c_free_gb'=[math]::Round($disk.FreeSpace/1GB,1)
|
||||||
|
'uptime_days'=[math]::Round($uptime.TotalDays,1)
|
||||||
|
} | ConvertTo-Json
|
||||||
|
'''.strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
encoded = base64.b64encode(ps_script.encode('utf-16le')).decode()
|
||||||
|
result = subprocess.run(
|
||||||
|
["ssh", "gamingpc",
|
||||||
|
"powershell", "-NoProfile", "-EncodedCommand", encoded],
|
||||||
|
capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return {"error": result.stderr.strip()[:300], "online": False}
|
||||||
|
|
||||||
|
# Filter CLIXML noise from stdout
|
||||||
|
clean = "\n".join(l for l in result.stdout.split("\n")
|
||||||
|
if l.strip() and not l.startswith("#<") and not l.startswith("<"))
|
||||||
|
data = _json.loads(clean)
|
||||||
|
data["online"] = True
|
||||||
|
data["mem_used_gb"] = round(data["mem_total_gb"] - data["mem_free_gb"], 1)
|
||||||
|
data["disk_c_used_gb"] = round(data["disk_c_total_gb"] - data["disk_c_free_gb"], 1)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)[:300], "online": False}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/{path:path}")
|
@app.get("/{path:path}")
|
||||||
async def index(path: str = ""):
|
async def index(path: str = ""):
|
||||||
"""Serve static files, fallback to index.html."""
|
"""Serve static files, fallback to index.html."""
|
||||||
|
|||||||
Reference in New Issue
Block a user