Spaces:
Sleeping
Sleeping
Commit Β·
9f28e8f
1
Parent(s): 84e5a7a
Update 2026-03-25 16:05:19
Browse files- .claude/settings.local.json +7 -0
- app.py +31 -1
- static/css/style.css +30 -0
- static/js/app.js +19 -0
- templates/base.html +6 -0
- templates/dashboard.html +80 -25
.claude/settings.local.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"permissions": {
|
| 3 |
+
"allow": [
|
| 4 |
+
"Bash(find /e/HuggingFace/AutoMLOps -not -path */__pycache__/* -not -path */.git/* -not -path */node_modules/*)"
|
| 5 |
+
]
|
| 6 |
+
}
|
| 7 |
+
}
|
app.py
CHANGED
|
@@ -444,7 +444,10 @@ def api_datasets():
|
|
| 444 |
def api_stats():
|
| 445 |
client = _mlflow_client()
|
| 446 |
try:
|
| 447 |
-
runs = client.search_runs(
|
|
|
|
|
|
|
|
|
|
| 448 |
except Exception:
|
| 449 |
runs = []
|
| 450 |
finished = [r for r in runs if r.info.status == "FINISHED"]
|
|
@@ -453,11 +456,38 @@ def api_stats():
|
|
| 453 |
v = r.data.metrics.get("accuracy") or r.data.metrics.get("r2_score") or 0
|
| 454 |
if v > best:
|
| 455 |
best = v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
return jsonify({
|
| 457 |
"total_runs": len(runs),
|
| 458 |
"completed_runs": len(finished),
|
| 459 |
"best_metric": round(best, 4),
|
| 460 |
"n_experiments": len(set(r.info.experiment_id for r in runs)),
|
|
|
|
|
|
|
|
|
|
| 461 |
})
|
| 462 |
|
| 463 |
|
|
|
|
| 444 |
def api_stats():
|
| 445 |
client = _mlflow_client()
|
| 446 |
try:
|
| 447 |
+
runs = client.search_runs(
|
| 448 |
+
experiment_ids=[], max_results=500,
|
| 449 |
+
order_by=["start_time DESC"],
|
| 450 |
+
)
|
| 451 |
except Exception:
|
| 452 |
runs = []
|
| 453 |
finished = [r for r in runs if r.info.status == "FINISHED"]
|
|
|
|
| 456 |
v = r.data.metrics.get("accuracy") or r.data.metrics.get("r2_score") or 0
|
| 457 |
if v > best:
|
| 458 |
best = v
|
| 459 |
+
|
| 460 |
+
recent = []
|
| 461 |
+
for r in runs[:8]:
|
| 462 |
+
m = r.data.metrics
|
| 463 |
+
primary = m.get("accuracy") or m.get("r2_score") or 0
|
| 464 |
+
recent.append({
|
| 465 |
+
"run_id": r.info.run_id[:8],
|
| 466 |
+
"algorithm": r.data.tags.get("algorithm", "β"),
|
| 467 |
+
"category": r.data.tags.get("category", "β"),
|
| 468 |
+
"dataset": r.data.tags.get("dataset", "β"),
|
| 469 |
+
"primary_metric": round(primary, 4),
|
| 470 |
+
"status": r.info.status,
|
| 471 |
+
"duration": round((r.info.end_time - r.info.start_time) / 1000, 1)
|
| 472 |
+
if r.info.end_time else None,
|
| 473 |
+
})
|
| 474 |
+
|
| 475 |
+
algo_counts: dict = {}
|
| 476 |
+
ds_counts: dict = {}
|
| 477 |
+
for r in finished:
|
| 478 |
+
cat = r.data.tags.get("category", "Other")
|
| 479 |
+
algo_counts[cat] = algo_counts.get(cat, 0) + 1
|
| 480 |
+
ds = r.data.tags.get("dataset", "Other")
|
| 481 |
+
ds_counts[ds] = ds_counts.get(ds, 0) + 1
|
| 482 |
+
|
| 483 |
return jsonify({
|
| 484 |
"total_runs": len(runs),
|
| 485 |
"completed_runs": len(finished),
|
| 486 |
"best_metric": round(best, 4),
|
| 487 |
"n_experiments": len(set(r.info.experiment_id for r in runs)),
|
| 488 |
+
"recent_runs": recent,
|
| 489 |
+
"algo_counts": algo_counts,
|
| 490 |
+
"ds_counts": ds_counts,
|
| 491 |
})
|
| 492 |
|
| 493 |
|
static/css/style.css
CHANGED
|
@@ -484,3 +484,33 @@ tr:hover td { background: var(--bg-hover); }
|
|
| 484 |
}
|
| 485 |
.empty-state-icon { font-size: 3rem; margin-bottom: 12px; }
|
| 486 |
.empty-state-title { font-size: 1rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
}
|
| 485 |
.empty-state-icon { font-size: 3rem; margin-bottom: 12px; }
|
| 486 |
.empty-state-title { font-size: 1rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
|
| 487 |
+
|
| 488 |
+
/* ββ Light theme ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 489 |
+
[data-theme="light"] {
|
| 490 |
+
--bg-primary: #f6f8fa;
|
| 491 |
+
--bg-secondary: #ffffff;
|
| 492 |
+
--bg-tertiary: #eaeef2;
|
| 493 |
+
--bg-hover: #d0d7de30;
|
| 494 |
+
--border-color: #d0d7de;
|
| 495 |
+
--text-primary: #24292f;
|
| 496 |
+
--text-secondary:#57606a;
|
| 497 |
+
--text-muted: #8c959f;
|
| 498 |
+
--shadow: 0 4px 24px rgba(0,0,0,.10);
|
| 499 |
+
}
|
| 500 |
+
[data-theme="light"] .form-select {
|
| 501 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2357606a' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
| 502 |
+
}
|
| 503 |
+
[data-theme="light"] .topnav-badge {
|
| 504 |
+
color: #fff;
|
| 505 |
+
}
|
| 506 |
+
[data-theme="light"] .pipeline-log {
|
| 507 |
+
color: var(--text-secondary);
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/* ββ Smooth theme transition ββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 511 |
+
body.theme-transition,
|
| 512 |
+
body.theme-transition *,
|
| 513 |
+
body.theme-transition *::before,
|
| 514 |
+
body.theme-transition *::after {
|
| 515 |
+
transition: background-color .22s ease, color .22s ease, border-color .22s ease !important;
|
| 516 |
+
}
|
static/js/app.js
CHANGED
|
@@ -47,3 +47,22 @@ function activateTab(panelId, btn, groupClass) {
|
|
| 47 |
document.getElementById(panelId)?.classList.add('active');
|
| 48 |
if (btn) btn.classList.add('active');
|
| 49 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
document.getElementById(panelId)?.classList.add('active');
|
| 48 |
if (btn) btn.classList.add('active');
|
| 49 |
}
|
| 50 |
+
|
| 51 |
+
// ββ Light / dark theme βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
+
function toggleTheme() {
|
| 53 |
+
document.body.classList.add('theme-transition');
|
| 54 |
+
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
| 55 |
+
const next = current === 'dark' ? 'light' : 'dark';
|
| 56 |
+
document.documentElement.setAttribute('data-theme', next);
|
| 57 |
+
localStorage.setItem('theme', next);
|
| 58 |
+
const icon = document.getElementById('theme-icon');
|
| 59 |
+
if (icon) icon.className = next === 'dark' ? 'fa-solid fa-moon' : 'fa-solid fa-sun';
|
| 60 |
+
document.dispatchEvent(new CustomEvent('themechange', { detail: next }));
|
| 61 |
+
setTimeout(() => document.body.classList.remove('theme-transition'), 300);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 65 |
+
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
| 66 |
+
const icon = document.getElementById('theme-icon');
|
| 67 |
+
if (icon) icon.className = current === 'dark' ? 'fa-solid fa-moon' : 'fa-solid fa-sun';
|
| 68 |
+
});
|
templates/base.html
CHANGED
|
@@ -18,6 +18,9 @@
|
|
| 18 |
|
| 19 |
<link rel="stylesheet" href="/static/css/style.css">
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
{% block head_extra %}{% endblock %}
|
| 22 |
</head>
|
| 23 |
<body>
|
|
@@ -73,6 +76,9 @@
|
|
| 73 |
</button>
|
| 74 |
<span class="topnav-title">{% block page_title %}AutoMLOps{% endblock %}</span>
|
| 75 |
<span class="topnav-badge"><i class="fa-solid fa-circle" style="color:#22c55e;font-size:.55rem"></i> Live</span>
|
|
|
|
|
|
|
|
|
|
| 76 |
{% block topnav_actions %}{% endblock %}
|
| 77 |
</header>
|
| 78 |
|
|
|
|
| 18 |
|
| 19 |
<link rel="stylesheet" href="/static/css/style.css">
|
| 20 |
|
| 21 |
+
<!-- Apply saved theme before render to prevent flash -->
|
| 22 |
+
<script>document.documentElement.setAttribute('data-theme',localStorage.getItem('theme')||'dark');</script>
|
| 23 |
+
|
| 24 |
{% block head_extra %}{% endblock %}
|
| 25 |
</head>
|
| 26 |
<body>
|
|
|
|
| 76 |
</button>
|
| 77 |
<span class="topnav-title">{% block page_title %}AutoMLOps{% endblock %}</span>
|
| 78 |
<span class="topnav-badge"><i class="fa-solid fa-circle" style="color:#22c55e;font-size:.55rem"></i> Live</span>
|
| 79 |
+
<button class="btn btn-ghost btn-sm" id="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark theme" style="width:32px;padding:5px 0;justify-content:center">
|
| 80 |
+
<i class="fa-solid fa-moon" id="theme-icon"></i>
|
| 81 |
+
</button>
|
| 82 |
{% block topnav_actions %}{% endblock %}
|
| 83 |
</header>
|
| 84 |
|
templates/dashboard.html
CHANGED
|
@@ -164,52 +164,100 @@
|
|
| 164 |
|
| 165 |
{% block scripts %}
|
| 166 |
<script>
|
| 167 |
-
const ALGO_DATA
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
// ββ
|
| 172 |
-
|
| 173 |
-
const
|
|
|
|
|
|
|
| 174 |
|
| 175 |
-
|
| 176 |
-
const cats = Object.keys(ALGO_COUNTS);
|
| 177 |
-
const vals = Object.values(ALGO_COUNTS);
|
| 178 |
-
const COLORS = ['#8b5cf6','#3b82f6','#22c55e','#f59e0b','#ef4444','#06b6d4','#ec4899','#a855f7'];
|
| 179 |
-
Plotly.newPlot('chart-algo', [{
|
| 180 |
type: 'pie', hole: .55,
|
| 181 |
labels: cats, values: vals,
|
| 182 |
-
marker: { colors:
|
| 183 |
textinfo: 'none',
|
| 184 |
hovertemplate: '<b>%{label}</b><br>%{value} runs<extra></extra>',
|
| 185 |
}], {
|
| 186 |
-
paper_bgcolor:
|
| 187 |
margin: { t:8, b:8, l:8, r:8 },
|
| 188 |
-
legend: { font: { color: txt, size: 11 }, bgcolor: 'transparent', x: 1.05 },
|
| 189 |
showlegend: cats.length > 0,
|
| 190 |
annotations: [{ text: `<b>${vals.reduce((a,b)=>a+b,0)}</b><br><span style="font-size:10px">runs</span>`,
|
| 191 |
-
x:.5, y:.5, font:{size:14,color:
|
| 192 |
}, { responsive: true, displayModeBar: false });
|
| 193 |
|
| 194 |
-
|
| 195 |
-
const
|
| 196 |
-
|
| 197 |
-
Plotly.newPlot('chart-ds', [{
|
| 198 |
type: 'bar', orientation: 'h',
|
| 199 |
y: dsKeys, x: dsVals,
|
| 200 |
marker: { color: '#3b82f6', opacity: .85 },
|
| 201 |
hovertemplate: '<b>%{y}</b>: %{x} runs<extra></extra>',
|
| 202 |
}], {
|
| 203 |
-
paper_bgcolor:
|
| 204 |
margin: { t:8, b:24, l:8, r:16 },
|
| 205 |
-
xaxis: { gridcolor: border, color: txt, tickfont:{size:10} },
|
| 206 |
-
yaxis: { color: txt, tickfont:{size:10}, automargin:true },
|
| 207 |
bargap: .35,
|
| 208 |
}, { responsive: true, displayModeBar: false });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
|
|
|
|
|
|
| 210 |
populateCategories();
|
| 211 |
});
|
| 212 |
|
|
|
|
|
|
|
|
|
|
| 213 |
// ββ Train modal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 214 |
function updateTaskType() {
|
| 215 |
const ds = document.getElementById('sel-dataset');
|
|
@@ -275,8 +323,8 @@ function pollTraining(jobId) {
|
|
| 275 |
const resultD = document.getElementById('train-result');
|
| 276 |
|
| 277 |
const iv = setInterval(async () => {
|
| 278 |
-
const res
|
| 279 |
-
const job
|
| 280 |
|
| 281 |
bar.style.width = job.progress + '%';
|
| 282 |
pct.textContent = job.progress + '%';
|
|
@@ -285,7 +333,7 @@ function pollTraining(jobId) {
|
|
| 285 |
if (job.status === 'completed') {
|
| 286 |
clearInterval(iv);
|
| 287 |
statusT.textContent = 'Completed';
|
| 288 |
-
const m
|
| 289 |
const keys = Object.keys(m);
|
| 290 |
const primary = keys[0];
|
| 291 |
resultD.style.display = 'block';
|
|
@@ -313,6 +361,7 @@ function pollTraining(jobId) {
|
|
| 313 |
}, 1000);
|
| 314 |
}
|
| 315 |
|
|
|
|
| 316 |
async function refreshStats() {
|
| 317 |
try {
|
| 318 |
const r = await fetch('/api/stats');
|
|
@@ -321,6 +370,12 @@ async function refreshStats() {
|
|
| 321 |
document.getElementById('stat-completed').textContent = s.completed_runs;
|
| 322 |
document.getElementById('stat-best').textContent = s.best_metric;
|
| 323 |
document.getElementById('stat-exps').textContent = s.n_experiments;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
} catch(_) {}
|
| 325 |
}
|
| 326 |
</script>
|
|
|
|
| 164 |
|
| 165 |
{% block scripts %}
|
| 166 |
<script>
|
| 167 |
+
const ALGO_DATA = {{ algorithms | tojson }};
|
| 168 |
+
let _algoCounts = {{ algo_counts }};
|
| 169 |
+
let _dsCounts = {{ ds_counts }};
|
| 170 |
+
|
| 171 |
+
const CHART_COLORS = ['#8b5cf6','#3b82f6','#22c55e','#f59e0b','#ef4444','#06b6d4','#ec4899','#a855f7'];
|
| 172 |
+
|
| 173 |
+
// Returns Plotly-compatible colors matching the current theme
|
| 174 |
+
function getChartTheme() {
|
| 175 |
+
const light = document.documentElement.getAttribute('data-theme') === 'light';
|
| 176 |
+
return {
|
| 177 |
+
bg: light ? '#ffffff' : '#161b22',
|
| 178 |
+
border: light ? '#d0d7de' : '#30363d',
|
| 179 |
+
txt: light ? '#57606a' : '#8b949e',
|
| 180 |
+
annotation: light ? '#24292f' : '#e6edf3',
|
| 181 |
+
};
|
| 182 |
+
}
|
| 183 |
|
| 184 |
+
// ββ Chart rendering (called on load, after refresh, and on theme change) ββ
|
| 185 |
+
function renderCharts(algoCounts, dsCounts) {
|
| 186 |
+
const c = getChartTheme();
|
| 187 |
+
const cats = Object.keys(algoCounts);
|
| 188 |
+
const vals = Object.values(algoCounts);
|
| 189 |
|
| 190 |
+
Plotly.react('chart-algo', [{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
type: 'pie', hole: .55,
|
| 192 |
labels: cats, values: vals,
|
| 193 |
+
marker: { colors: CHART_COLORS.slice(0, cats.length) },
|
| 194 |
textinfo: 'none',
|
| 195 |
hovertemplate: '<b>%{label}</b><br>%{value} runs<extra></extra>',
|
| 196 |
}], {
|
| 197 |
+
paper_bgcolor: c.bg, plot_bgcolor: c.bg,
|
| 198 |
margin: { t:8, b:8, l:8, r:8 },
|
| 199 |
+
legend: { font: { color: c.txt, size: 11 }, bgcolor: 'transparent', x: 1.05 },
|
| 200 |
showlegend: cats.length > 0,
|
| 201 |
annotations: [{ text: `<b>${vals.reduce((a,b)=>a+b,0)}</b><br><span style="font-size:10px">runs</span>`,
|
| 202 |
+
x:.5, y:.5, font:{size:14, color:c.annotation}, showarrow:false }],
|
| 203 |
}, { responsive: true, displayModeBar: false });
|
| 204 |
|
| 205 |
+
const dsKeys = Object.keys(dsCounts);
|
| 206 |
+
const dsVals = Object.values(dsCounts);
|
| 207 |
+
Plotly.react('chart-ds', [{
|
|
|
|
| 208 |
type: 'bar', orientation: 'h',
|
| 209 |
y: dsKeys, x: dsVals,
|
| 210 |
marker: { color: '#3b82f6', opacity: .85 },
|
| 211 |
hovertemplate: '<b>%{y}</b>: %{x} runs<extra></extra>',
|
| 212 |
}], {
|
| 213 |
+
paper_bgcolor: c.bg, plot_bgcolor: c.bg,
|
| 214 |
margin: { t:8, b:24, l:8, r:16 },
|
| 215 |
+
xaxis: { gridcolor: c.border, color: c.txt, tickfont:{size:10} },
|
| 216 |
+
yaxis: { color: c.txt, tickfont:{size:10}, automargin:true },
|
| 217 |
bargap: .35,
|
| 218 |
}, { responsive: true, displayModeBar: false });
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// ββ Live-update the recent-runs table ββββββββββββββββββββββββββββββββββββββ
|
| 222 |
+
function updateRecentTable(rows) {
|
| 223 |
+
const tbody = document.querySelector('#recent-table tbody');
|
| 224 |
+
if (!tbody) return;
|
| 225 |
+
if (!rows || rows.length === 0) {
|
| 226 |
+
tbody.innerHTML = `<tr><td colspan="7">
|
| 227 |
+
<div class="empty-state">
|
| 228 |
+
<div class="empty-state-icon">π¬</div>
|
| 229 |
+
<div class="empty-state-title">No runs yet</div>
|
| 230 |
+
<div>Click <strong>New Run</strong> to train your first model</div>
|
| 231 |
+
</div></td></tr>`;
|
| 232 |
+
return;
|
| 233 |
+
}
|
| 234 |
+
tbody.innerHTML = rows.map(r => {
|
| 235 |
+
const mc = r.primary_metric >= 0.9 ? 'metric-good' : r.primary_metric >= 0.7 ? 'metric-medium' : 'metric-bad';
|
| 236 |
+
const sb = r.status === 'FINISHED'
|
| 237 |
+
? `<span class="badge badge-success"><i class="fa-solid fa-check"></i> Done</span>`
|
| 238 |
+
: r.status === 'RUNNING'
|
| 239 |
+
? `<span class="badge badge-info"><span class="spinner" style="width:10px;height:10px;border-width:1.5px"></span> Running</span>`
|
| 240 |
+
: `<span class="badge badge-muted">${r.status}</span>`;
|
| 241 |
+
return `<tr>
|
| 242 |
+
<td><code style="font-size:.8rem;color:var(--accent-light)">${r.run_id}</code></td>
|
| 243 |
+
<td><strong>${r.algorithm}</strong></td>
|
| 244 |
+
<td><span class="badge badge-purple">${r.category}</span></td>
|
| 245 |
+
<td>${r.dataset}</td>
|
| 246 |
+
<td><span class="metric-val ${mc}">${r.primary_metric}</span></td>
|
| 247 |
+
<td>${r.duration != null ? r.duration + 's' : 'β'}</td>
|
| 248 |
+
<td>${sb}</td>
|
| 249 |
+
</tr>`;
|
| 250 |
+
}).join('');
|
| 251 |
+
}
|
| 252 |
|
| 253 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 254 |
+
renderCharts(_algoCounts, _dsCounts);
|
| 255 |
populateCategories();
|
| 256 |
});
|
| 257 |
|
| 258 |
+
// Re-render charts when theme changes so colors match
|
| 259 |
+
document.addEventListener('themechange', () => renderCharts(_algoCounts, _dsCounts));
|
| 260 |
+
|
| 261 |
// ββ Train modal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 262 |
function updateTaskType() {
|
| 263 |
const ds = document.getElementById('sel-dataset');
|
|
|
|
| 323 |
const resultD = document.getElementById('train-result');
|
| 324 |
|
| 325 |
const iv = setInterval(async () => {
|
| 326 |
+
const res = await fetch(`/api/run/${jobId}/status`);
|
| 327 |
+
const job = await res.json();
|
| 328 |
|
| 329 |
bar.style.width = job.progress + '%';
|
| 330 |
pct.textContent = job.progress + '%';
|
|
|
|
| 333 |
if (job.status === 'completed') {
|
| 334 |
clearInterval(iv);
|
| 335 |
statusT.textContent = 'Completed';
|
| 336 |
+
const m = job.metrics || {};
|
| 337 |
const keys = Object.keys(m);
|
| 338 |
const primary = keys[0];
|
| 339 |
resultD.style.display = 'block';
|
|
|
|
| 361 |
}, 1000);
|
| 362 |
}
|
| 363 |
|
| 364 |
+
// ββ Full dashboard refresh (stats + table + charts) ββββββββββββββββββββββββ
|
| 365 |
async function refreshStats() {
|
| 366 |
try {
|
| 367 |
const r = await fetch('/api/stats');
|
|
|
|
| 370 |
document.getElementById('stat-completed').textContent = s.completed_runs;
|
| 371 |
document.getElementById('stat-best').textContent = s.best_metric;
|
| 372 |
document.getElementById('stat-exps').textContent = s.n_experiments;
|
| 373 |
+
if (s.recent_runs) updateRecentTable(s.recent_runs);
|
| 374 |
+
if (s.algo_counts && s.ds_counts) {
|
| 375 |
+
_algoCounts = s.algo_counts;
|
| 376 |
+
_dsCounts = s.ds_counts;
|
| 377 |
+
renderCharts(_algoCounts, _dsCounts);
|
| 378 |
+
}
|
| 379 |
} catch(_) {}
|
| 380 |
}
|
| 381 |
</script>
|