Fix workflows scroll, add CPU/mem/disk sparklines during job runs
This commit is contained in:
@@ -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"""
|
||||
|
||||
@@ -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 @@
|
||||
<button id="cancelButton" class="btn-cancel" onclick="cancelJob()">Cancel</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<div id="sys-stats">
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">CPU</span>
|
||||
<canvas id="cpu-canvas" width="180" height="22"></canvas>
|
||||
<span class="stat-val" id="cpu-val">—</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">MEM</span>
|
||||
<canvas id="mem-canvas" width="180" height="22"></canvas>
|
||||
<span class="stat-val" id="mem-val">—</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">DISK</span>
|
||||
<canvas id="disk-canvas" width="180" height="22"></canvas>
|
||||
<span class="stat-val" id="disk-val">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logs"><span class="log-placeholder">Run a step to see output here.</span></div>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user