AutoMLOps / templates /dashboard.html
mnoorchenar's picture
Update 2026-03-25 16:44:28
6cd2d76
{% extends "base.html" %}
{% set active_page = "dashboard" %}
{% block title %}Dashboard{% endblock %}
{% block page_title %}<i class="fa-solid fa-gauge-high" style="color:var(--accent)"></i> Dashboard{% endblock %}
{% block topnav_actions %}
<button class="btn btn-primary btn-sm" onclick="openTrainModal()">
<i class="fa-solid fa-play"></i> New Run
</button>
{% endblock %}
{% block content %}
<div class="page-title">Platform Overview</div>
<div class="page-sub">AutoML experiment sweeps + Airflow-style pipeline orchestration, tracked end-to-end with MLflow</div>
<!-- ── Tech stack explanation ─────────────────────────────────────────── -->
<div class="grid-2 mb-20">
<div class="card tech-card" style="border-top:3px solid var(--accent)">
<div class="card-header" style="margin-bottom:10px">
<div class="card-title">
<i class="fa-solid fa-wand-magic-sparkles" style="color:var(--accent)"></i>
AutoML Engine
</div>
<span class="badge badge-purple">scikit-learn Β· XGBoost Β· LightGBM</span>
</div>
<p style="font-size:.84rem;color:var(--text-secondary);line-height:1.6;margin-bottom:12px">
Sweeps <strong>50+ algorithms</strong> across Linear, Tree-Based, Ensemble, SVM, KNN and Neural Network families.
Every trial is logged to MLflow β€” metrics, params, and serialised model artefacts.
Use the <a href="/automl">AutoML</a> page to rank algorithms on any dataset in one click.
</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<span class="badge badge-muted"><i class="fa-solid fa-robot" style="color:var(--accent)"></i> 50+ Algorithms</span>
<span class="badge badge-muted"><i class="fa-solid fa-chart-line" style="color:var(--accent-blue)"></i> MLflow Tracking</span>
<span class="badge badge-muted"><i class="fa-solid fa-trophy" style="color:var(--warning)"></i> Auto-Ranking</span>
<span class="badge badge-muted"><i class="fa-solid fa-code-branch" style="color:var(--success)"></i> Model Registry</span>
</div>
</div>
<div class="card tech-card" style="border-top:3px solid var(--accent-blue)">
<div class="card-header" style="margin-bottom:10px">
<div class="card-title">
<i class="fa-solid fa-diagram-project" style="color:var(--accent-blue)"></i>
Pipeline Orchestration
</div>
<span class="badge badge-info">Airflow-inspired DAG engine</span>
</div>
<p style="font-size:.84rem;color:var(--text-secondary);line-height:1.6;margin-bottom:12px">
Runs ML workflows as <strong>Directed Acyclic Graphs</strong> β€” the same paradigm used by Apache Airflow.
Three built-in pipelines: <em>Training</em> (ingest β†’ train β†’ register β†’ deploy),
<em>Retraining</em> (drift detection β†’ retrain β†’ A/B test β†’ promote), and
<em>Data Processing</em> (clean β†’ encode β†’ scale β†’ feature store).
</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<span class="badge badge-muted"><i class="fa-solid fa-network-wired" style="color:var(--accent-blue)"></i> DAG Execution</span>
<span class="badge badge-muted"><i class="fa-solid fa-wave-square" style="color:var(--danger)"></i> Drift Detection</span>
<span class="badge badge-muted"><i class="fa-solid fa-rotate" style="color:var(--success)"></i> Auto-Retraining</span>
<span class="badge badge-muted"><i class="fa-solid fa-rocket" style="color:var(--warning)"></i> Staging Deploy</span>
</div>
</div>
</div>
<!-- ── Workflow banner ────────────────────────────────────────────────── -->
<div class="card mb-20" style="background:linear-gradient(135deg,#8b5cf608,#3b82f608);border-color:var(--border-color);padding:16px 22px">
<div style="display:flex;align-items:center;gap:0;flex-wrap:wrap;justify-content:center">
<div class="workflow-step"><i class="fa-solid fa-database" style="color:var(--accent-blue)"></i><span>Load Dataset</span></div>
<div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div>
<div class="workflow-step"><i class="fa-solid fa-wand-magic-sparkles" style="color:var(--accent)"></i><span>AutoML Sweep</span></div>
<div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div>
<div class="workflow-step"><i class="fa-solid fa-flask" style="color:var(--success)"></i><span>MLflow Tracking</span></div>
<div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div>
<div class="workflow-step"><i class="fa-solid fa-diagram-project" style="color:var(--accent-blue)"></i><span>Pipeline DAG</span></div>
<div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div>
<div class="workflow-step"><i class="fa-solid fa-box-archive" style="color:var(--warning)"></i><span>Model Registry</span></div>
<div class="workflow-arrow"><i class="fa-solid fa-arrow-right"></i></div>
<div class="workflow-step"><i class="fa-solid fa-rocket" style="color:var(--danger)"></i><span>Deploy</span></div>
</div>
</div>
<!-- ── Live metrics ───────────────────────────────────────────────────── -->
<div class="page-sub" style="margin-top:4px;margin-bottom:12px">
<i class="fa-solid fa-chart-bar" style="color:var(--accent)"></i> Live Experiment Metrics
</div>
<!-- Stat cards -->
<div class="stat-grid">
<div class="stat-card purple">
<div class="stat-label">Total Runs</div>
<div class="stat-value" id="stat-total">{{ total_runs }}</div>
<div class="stat-sub"><i class="fa-solid fa-arrow-trend-up"></i> all time</div>
</div>
<div class="stat-card blue">
<div class="stat-label">Completed</div>
<div class="stat-value" id="stat-completed">{{ completed_runs }}</div>
<div class="stat-sub">finished successfully</div>
</div>
<div class="stat-card green">
<div class="stat-label">Best Score</div>
<div class="stat-value" id="stat-best">{{ best_metric }}</div>
<div class="stat-sub">accuracy / RΒ²</div>
</div>
<div class="stat-card yellow">
<div class="stat-label">Experiments</div>
<div class="stat-value" id="stat-exps">{{ n_experiments }}</div>
<div class="stat-sub">active datasets</div>
</div>
</div>
<!-- Charts row -->
<div class="grid-2 mb-20">
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fa-solid fa-chart-pie" style="color:var(--accent)"></i> Algorithm Categories</div>
</div>
<div id="chart-algo" style="height:220px"></div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fa-solid fa-database" style="color:var(--accent-blue)"></i> Runs by Dataset</div>
</div>
<div id="chart-ds" style="height:220px"></div>
</div>
</div>
<!-- Recent runs -->
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fa-solid fa-clock-rotate-left" style="color:var(--warning)"></i> Recent Training Runs <span style="font-size:.75rem;font-weight:400;color:var(--text-muted);margin-left:6px">β€” each row is one MLflow-tracked experiment</span></div>
<a href="/experiments" class="btn btn-ghost btn-sm">View all <i class="fa-solid fa-arrow-right"></i></a>
</div>
<div class="table-wrap">
<table id="recent-table">
<thead>
<tr>
<th>Run ID</th><th>Algorithm</th><th>Category</th><th>Dataset</th>
<th>Primary Metric</th><th>Duration</th><th>Status</th>
</tr>
</thead>
<tbody>
{% for r in recent_runs %}
<tr>
<td><code style="font-size:.8rem;color:var(--accent-light)">{{ r.run_id }}</code></td>
<td><strong>{{ r.algorithm }}</strong></td>
<td><span class="badge badge-purple">{{ r.category }}</span></td>
<td>{{ r.dataset }}</td>
<td>
<span class="metric-val {% if r.primary_metric >= 0.9 %}metric-good{% elif r.primary_metric >= 0.7 %}metric-medium{% else %}metric-bad{% endif %}">
{{ r.primary_metric }}
</span>
</td>
<td>{{ r.duration }}s</td>
<td>
{% if r.status == 'FINISHED' %}
<span class="badge badge-success"><i class="fa-solid fa-check"></i> Done</span>
{% elif r.status == 'RUNNING' %}
<span class="badge badge-info"><span class="spinner" style="width:10px;height:10px;border-width:1.5px"></span> Running</span>
{% else %}
<span class="badge badge-muted">{{ r.status }}</span>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">πŸ”¬</div>
<div class="empty-state-title">No runs yet</div>
<div>Click <strong>New Run</strong> to train your first model</div>
</div>
</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- ── Quick Train Modal ──────────────────────────────────────────────────── -->
<div class="modal-overlay" id="train-modal">
<div class="modal">
<div class="modal-header">
<div class="modal-title"><i class="fa-solid fa-play" style="color:var(--accent)"></i> New Training Run</div>
<button class="modal-close" onclick="closeTrainModal()"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="form-group">
<label class="form-label">Dataset</label>
<select class="form-select" id="sel-dataset" onchange="updateTaskType()">
{% for name, cfg in datasets.items() %}
<option value="{{ name }}" data-task="{{ cfg.task }}">{{ cfg.icon }} {{ name }} β€” {{ cfg.description[:55] }}…</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Task Type</label>
<select class="form-select" id="sel-task" onchange="populateCategories()">
<option value="classification">Classification</option>
<option value="regression">Regression</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Algorithm Category</label>
<select class="form-select" id="sel-category" onchange="populateAlgorithms()"></select>
</div>
<div class="form-group">
<label class="form-label">Algorithm</label>
<select class="form-select" id="sel-algorithm"></select>
</div>
<!-- Progress (hidden until running) -->
<div id="train-progress-wrap" style="display:none;margin-top:12px">
<div class="flex-between mb-20" style="margin-bottom:6px">
<span style="font-size:.85rem;color:var(--text-secondary)" id="train-status-text">Initialising…</span>
<span style="font-size:.85rem;font-weight:600" id="train-pct">0%</span>
</div>
<div class="progress-bar-wrap"><div class="progress-bar" id="train-bar" style="width:0%"></div></div>
</div>
<!-- Result (hidden until done) -->
<div id="train-result" style="display:none;margin-top:14px"></div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeTrainModal()">Cancel</button>
<button class="btn btn-primary" id="btn-start-train" onclick="startTraining()">
<i class="fa-solid fa-play"></i> Train
</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const ALGO_DATA = {{ algorithms | tojson }};
let _algoCounts = {{ algo_counts }};
let _dsCounts = {{ ds_counts }};
const CHART_COLORS = ['#8b5cf6','#3b82f6','#22c55e','#f59e0b','#ef4444','#06b6d4','#ec4899','#a855f7'];
// Returns Plotly-compatible colors matching the current theme
function getChartTheme() {
const light = document.documentElement.getAttribute('data-theme') === 'light';
return {
bg: light ? '#ffffff' : '#161b22',
border: light ? '#d0d7de' : '#30363d',
txt: light ? '#57606a' : '#8b949e',
annotation: light ? '#24292f' : '#e6edf3',
};
}
// ── Chart rendering (called on load, after refresh, and on theme change) ──
function renderCharts(algoCounts, dsCounts) {
const c = getChartTheme();
const cats = Object.keys(algoCounts);
const vals = Object.values(algoCounts);
Plotly.react('chart-algo', [{
type: 'pie', hole: .55,
labels: cats, values: vals,
marker: { colors: CHART_COLORS.slice(0, cats.length) },
textinfo: 'none',
hovertemplate: '<b>%{label}</b><br>%{value} runs<extra></extra>',
}], {
paper_bgcolor: c.bg, plot_bgcolor: c.bg,
margin: { t:8, b:8, l:8, r:8 },
legend: { font: { color: c.txt, size: 11 }, bgcolor: 'transparent', x: 1.05 },
showlegend: cats.length > 0,
annotations: [{ text: `<b>${vals.reduce((a,b)=>a+b,0)}</b><br><span style="font-size:10px">runs</span>`,
x:.5, y:.5, font:{size:14, color:c.annotation}, showarrow:false }],
}, { responsive: true, displayModeBar: false });
const dsKeys = Object.keys(dsCounts);
const dsVals = Object.values(dsCounts);
Plotly.react('chart-ds', [{
type: 'bar', orientation: 'h',
y: dsKeys, x: dsVals,
marker: { color: '#3b82f6', opacity: .85 },
hovertemplate: '<b>%{y}</b>: %{x} runs<extra></extra>',
}], {
paper_bgcolor: c.bg, plot_bgcolor: c.bg,
margin: { t:8, b:24, l:8, r:16 },
xaxis: { gridcolor: c.border, color: c.txt, tickfont:{size:10} },
yaxis: { color: c.txt, tickfont:{size:10}, automargin:true },
bargap: .35,
}, { responsive: true, displayModeBar: false });
}
// ── Live-update the recent-runs table ──────────────────────────────────────
function updateRecentTable(rows) {
const tbody = document.querySelector('#recent-table tbody');
if (!tbody) return;
if (!rows || rows.length === 0) {
tbody.innerHTML = `<tr><td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">πŸ”¬</div>
<div class="empty-state-title">No runs yet</div>
<div>Click <strong>New Run</strong> to train your first model</div>
</div></td></tr>`;
return;
}
tbody.innerHTML = rows.map(r => {
const mc = r.primary_metric >= 0.9 ? 'metric-good' : r.primary_metric >= 0.7 ? 'metric-medium' : 'metric-bad';
const sb = r.status === 'FINISHED'
? `<span class="badge badge-success"><i class="fa-solid fa-check"></i> Done</span>`
: r.status === 'RUNNING'
? `<span class="badge badge-info"><span class="spinner" style="width:10px;height:10px;border-width:1.5px"></span> Running</span>`
: `<span class="badge badge-muted">${r.status}</span>`;
return `<tr>
<td><code style="font-size:.8rem;color:var(--accent-light)">${r.run_id}</code></td>
<td><strong>${r.algorithm}</strong></td>
<td><span class="badge badge-purple">${r.category}</span></td>
<td>${r.dataset}</td>
<td><span class="metric-val ${mc}">${r.primary_metric}</span></td>
<td>${r.duration != null ? r.duration + 's' : 'β€”'}</td>
<td>${sb}</td>
</tr>`;
}).join('');
}
document.addEventListener('DOMContentLoaded', () => {
renderCharts(_algoCounts, _dsCounts);
populateCategories();
});
// Re-render charts when theme changes so colors match
document.addEventListener('themechange', () => renderCharts(_algoCounts, _dsCounts));
// ── Train modal helpers ────────────────────────────────────────────────────
function updateTaskType() {
const ds = document.getElementById('sel-dataset');
const opt = ds.options[ds.selectedIndex];
document.getElementById('sel-task').value = opt.dataset.task || 'classification';
populateCategories();
}
function populateCategories() {
const task = document.getElementById('sel-task').value;
const sel = document.getElementById('sel-category');
sel.innerHTML = '';
const cats = Object.keys(ALGO_DATA[task] || {});
cats.forEach(c => { const o = document.createElement('option'); o.value = c; o.text = c; sel.add(o); });
populateAlgorithms();
}
function populateAlgorithms() {
const task = document.getElementById('sel-task').value;
const cat = document.getElementById('sel-category').value;
const sel = document.getElementById('sel-algorithm');
sel.innerHTML = '';
const algos = Object.keys((ALGO_DATA[task] || {})[cat] || {});
algos.forEach(a => { const o = document.createElement('option'); o.value = a; o.text = a; sel.add(o); });
}
function openTrainModal() { document.getElementById('train-modal').classList.add('open'); resetTrainModal(); }
function closeTrainModal() { document.getElementById('train-modal').classList.remove('open'); }
function resetTrainModal() {
document.getElementById('train-progress-wrap').style.display = 'none';
document.getElementById('train-result').style.display = 'none';
document.getElementById('btn-start-train').disabled = false;
document.getElementById('btn-start-train').innerHTML = '<i class="fa-solid fa-play"></i> Train';
}
async function startTraining() {
const dataset = document.getElementById('sel-dataset').value;
const task_type = document.getElementById('sel-task').value;
const category = document.getElementById('sel-category').value;
const algorithm = document.getElementById('sel-algorithm').value;
document.getElementById('btn-start-train').disabled = true;
document.getElementById('btn-start-train').innerHTML = '<span class="spinner"></span> Running…';
document.getElementById('train-progress-wrap').style.display = 'block';
document.getElementById('train-result').style.display = 'none';
try {
const res = await fetch('/api/train', {
method: 'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ dataset, task_type, category, algorithm }),
});
const data = await res.json();
if (data.error) { showToast(data.error, 'error'); resetTrainModal(); return; }
pollTraining(data.job_id);
} catch(e) { showToast('Request failed', 'error'); resetTrainModal(); }
}
function pollTraining(jobId) {
const bar = document.getElementById('train-bar');
const pct = document.getElementById('train-pct');
const statusT = document.getElementById('train-status-text');
const resultD = document.getElementById('train-result');
const iv = setInterval(async () => {
const res = await fetch(`/api/run/${jobId}/status`);
const job = await res.json();
bar.style.width = job.progress + '%';
pct.textContent = job.progress + '%';
statusT.textContent = job.status.charAt(0).toUpperCase() + job.status.slice(1) + '…';
if (job.status === 'completed') {
clearInterval(iv);
statusT.textContent = 'Completed';
const m = job.metrics || {};
const keys = Object.keys(m);
const primary = keys[0];
resultD.style.display = 'block';
resultD.innerHTML = `
<div style="background:var(--bg-tertiary);border-radius:8px;padding:14px">
<div style="font-weight:600;margin-bottom:10px;color:var(--success)">
<i class="fa-solid fa-circle-check"></i> Training complete
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px">
${keys.map(k=>`<div style="background:var(--bg-secondary);padding:8px 10px;border-radius:6px">
<div style="font-size:.72rem;color:var(--text-muted);text-transform:uppercase">${k}</div>
<div style="font-size:1.1rem;font-weight:700;color:var(--success)">${m[k]}</div>
</div>`).join('')}
</div>
</div>`;
document.getElementById('btn-start-train').innerHTML = '<i class="fa-solid fa-rotate-right"></i> Run Again';
document.getElementById('btn-start-train').disabled = false;
showToast(`${job.algorithm} β†’ ${primary}: ${m[primary]}`, 'success');
refreshStats();
} else if (job.status === 'failed') {
clearInterval(iv);
showToast('Training failed: ' + (job.error || 'unknown'), 'error');
resetTrainModal();
}
}, 1000);
}
// ── Full dashboard refresh (stats + table + charts) ────────────────────────
async function refreshStats() {
try {
const r = await fetch('/api/stats');
const s = await r.json();
document.getElementById('stat-total').textContent = s.total_runs;
document.getElementById('stat-completed').textContent = s.completed_runs;
document.getElementById('stat-best').textContent = s.best_metric;
document.getElementById('stat-exps').textContent = s.n_experiments;
if (s.recent_runs) updateRecentTable(s.recent_runs);
if (s.algo_counts && s.ds_counts) {
_algoCounts = s.algo_counts;
_dsCounts = s.ds_counts;
renderCharts(_algoCounts, _dsCounts);
}
} catch(_) {}
}
</script>
{% endblock %}