Fix workflows scroll, add CPU/mem/disk sparklines during job runs
This commit is contained in:
@@ -9,6 +9,11 @@ import threading
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import yaml
|
import yaml
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
_has_psutil = True
|
||||||
|
except ImportError:
|
||||||
|
_has_psutil = False
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='static', template_folder='templates')
|
app = Flask(__name__, static_folder='static', template_folder='templates')
|
||||||
|
|
||||||
@@ -251,6 +256,19 @@ def upload_file():
|
|||||||
f.save(dest)
|
f.save(dest)
|
||||||
return jsonify({'success': True, 'path': 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')
|
@app.route('/api/list-files')
|
||||||
def list_files():
|
def list_files():
|
||||||
"""List available GeoJSON files"""
|
"""List available GeoJSON files"""
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
/* Scrollable workflow area */
|
/* Scrollable workflow area */
|
||||||
.workflows {
|
.workflows {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -300,6 +301,37 @@
|
|||||||
#status.error { background: #3a1e1e; color: #e07070; }
|
#status.error { background: #3a1e1e; color: #e07070; }
|
||||||
#status.info { background: #1e2e3a; color: #70b8e0; }
|
#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 {
|
#logs {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -479,6 +511,23 @@
|
|||||||
<button id="cancelButton" class="btn-cancel" onclick="cancelJob()">Cancel</button>
|
<button id="cancelButton" class="btn-cancel" onclick="cancelJob()">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="status"></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 id="logs"><span class="log-placeholder">Run a step to see output here.</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -570,6 +619,7 @@
|
|||||||
currentJobId = data.job_id;
|
currentJobId = data.job_id;
|
||||||
if (logCheckInterval) clearInterval(logCheckInterval);
|
if (logCheckInterval) clearInterval(logCheckInterval);
|
||||||
logCheckInterval = setInterval(checkJobStatus, 1000);
|
logCheckInterval = setInterval(checkJobStatus, 1000);
|
||||||
|
startStatsPolling();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
showStatus(`Error: ${err.message}`, 'error');
|
showStatus(`Error: ${err.message}`, 'error');
|
||||||
@@ -593,6 +643,7 @@
|
|||||||
enableButtons();
|
enableButtons();
|
||||||
document.getElementById('cancelButton').classList.remove('active');
|
document.getElementById('cancelButton').classList.remove('active');
|
||||||
currentJobId = null;
|
currentJobId = null;
|
||||||
|
stopStatsPolling();
|
||||||
} else {
|
} else {
|
||||||
showStatus(`Cancel failed: ${data.error}`, 'error');
|
showStatus(`Cancel failed: ${data.error}`, 'error');
|
||||||
}
|
}
|
||||||
@@ -613,6 +664,7 @@
|
|||||||
enableButtons();
|
enableButtons();
|
||||||
document.getElementById('cancelButton').classList.remove('active');
|
document.getElementById('cancelButton').classList.remove('active');
|
||||||
currentJobId = null;
|
currentJobId = null;
|
||||||
|
stopStatsPolling();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Status check error:', err));
|
.catch(err => console.error('Status check error:', err));
|
||||||
@@ -660,6 +712,84 @@
|
|||||||
function enableButtons() {
|
function enableButtons() {
|
||||||
document.querySelectorAll('.btn-run').forEach(b => b.disabled = false);
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user