From 32cd6cba594caa276128eaaf3daa7882290194e4 Mon Sep 17 00:00:00 2001 From: zyphlar Date: Sat, 18 Apr 2026 22:50:54 -0700 Subject: [PATCH] Fix workflows scroll, add CPU/mem/disk sparklines during job runs --- web/server.py | 18 ++++++ web/templates/index.html | 130 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/web/server.py b/web/server.py index f7b4e30..89a02c4 100644 --- a/web/server.py +++ b/web/server.py @@ -9,6 +9,11 @@ import threading import json from datetime import datetime import yaml +try: + import psutil + _has_psutil = True +except ImportError: + _has_psutil = False app = Flask(__name__, static_folder='static', template_folder='templates') @@ -251,6 +256,19 @@ def upload_file(): f.save(dest) return jsonify({'success': True, 'path': dest}) +@app.route('/api/system-stats') +def system_stats(): + """Return current CPU, memory, and disk usage percentages.""" + if not _has_psutil: + return jsonify({'error': 'psutil not installed'}), 503 + cpu = psutil.cpu_percent(interval=None) + mem = psutil.virtual_memory().percent + try: + disk = psutil.disk_usage('/data').percent + except Exception: + disk = psutil.disk_usage('/').percent + return jsonify({'cpu': cpu, 'mem': mem, 'disk': disk}) + @app.route('/api/list-files') def list_files(): """List available GeoJSON files""" diff --git a/web/templates/index.html b/web/templates/index.html index 02c73d8..9d382bb 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -109,6 +109,7 @@ /* Scrollable workflow area */ .workflows { flex: 1; + min-height: 0; overflow-y: auto; padding: 14px 16px; display: flex; @@ -300,6 +301,37 @@ #status.error { background: #3a1e1e; color: #e07070; } #status.info { background: #1e2e3a; color: #70b8e0; } + #sys-stats { + flex-shrink: 0; + display: none; + padding: 6px 14px 8px; + background: #141414; + border-bottom: 1px solid #2a2a2a; + } + + .stat-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; + } + + .stat-label { + font-size: 10px; + font-weight: 700; + color: #666; + width: 30px; + letter-spacing: 0.3px; + } + + .stat-val { + font-size: 10px; + color: #888; + width: 34px; + text-align: right; + font-family: 'Courier New', monospace; + } + #logs { flex: 1; overflow-y: auto; @@ -479,6 +511,23 @@
+
+
+ CPU + + +
+
+ MEM + + +
+
+ DISK + + +
+
Run a step to see output here.
@@ -570,6 +619,7 @@ currentJobId = data.job_id; if (logCheckInterval) clearInterval(logCheckInterval); logCheckInterval = setInterval(checkJobStatus, 1000); + startStatsPolling(); }) .catch(err => { showStatus(`Error: ${err.message}`, 'error'); @@ -593,6 +643,7 @@ enableButtons(); document.getElementById('cancelButton').classList.remove('active'); currentJobId = null; + stopStatsPolling(); } else { showStatus(`Cancel failed: ${data.error}`, 'error'); } @@ -613,6 +664,7 @@ enableButtons(); document.getElementById('cancelButton').classList.remove('active'); currentJobId = null; + stopStatsPolling(); } }) .catch(err => console.error('Status check error:', err)); @@ -660,6 +712,84 @@ function enableButtons() { document.querySelectorAll('.btn-run').forEach(b => b.disabled = false); } + + /* ── System stats sparklines ── */ + const MAX_SAMPLES = 60; + const statHistory = { cpu: [], mem: [], disk: [] }; + let statsInterval = null; + + const sparkColors = { cpu: '#e07070', mem: '#70b8e0', disk: '#f0a830' }; + + function drawSparkline(canvasId, data, color) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const W = canvas.width, H = canvas.height; + ctx.clearRect(0, 0, W, H); + + // Background + ctx.fillStyle = '#1e1e1e'; + ctx.fillRect(0, 0, W, H); + + if (data.length < 2) return; + + const step = W / (MAX_SAMPLES - 1); + const startX = W - (data.length - 1) * step; + + // Fill + ctx.beginPath(); + ctx.moveTo(startX, H); + data.forEach((v, i) => { + ctx.lineTo(startX + i * step, H - (v / 100) * (H - 2) - 1); + }); + ctx.lineTo(startX + (data.length - 1) * step, H); + ctx.closePath(); + ctx.fillStyle = color + '33'; + ctx.fill(); + + // Line + ctx.beginPath(); + data.forEach((v, i) => { + const x = startX + i * step; + const y = H - (v / 100) * (H - 2) - 1; + i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); + }); + ctx.strokeStyle = color; + ctx.lineWidth = 1.5; + ctx.stroke(); + } + + function pollSystemStats() { + fetch('/api/system-stats') + .then(r => r.json()) + .then(data => { + if (data.error) return; + ['cpu', 'mem', 'disk'].forEach(k => { + statHistory[k].push(data[k]); + if (statHistory[k].length > MAX_SAMPLES) statHistory[k].shift(); + }); + document.getElementById('cpu-val').textContent = data.cpu.toFixed(0) + '%'; + document.getElementById('mem-val').textContent = data.mem.toFixed(0) + '%'; + document.getElementById('disk-val').textContent = data.disk.toFixed(0) + '%'; + drawSparkline('cpu-canvas', statHistory.cpu, sparkColors.cpu); + drawSparkline('mem-canvas', statHistory.mem, sparkColors.mem); + drawSparkline('disk-canvas', statHistory.disk, sparkColors.disk); + }) + .catch(() => {}); + } + + function startStatsPolling() { + document.getElementById('sys-stats').style.display = 'block'; + statHistory.cpu = []; statHistory.mem = []; statHistory.disk = []; + pollSystemStats(); + statsInterval = setInterval(pollSystemStats, 2000); + } + + function stopStatsPolling() { + clearInterval(statsInterval); + statsInterval = null; + document.getElementById('sys-stats').style.display = 'none'; + }