AutoMLOps / templates /models.html
mnoorchenar's picture
Update 2026-03-25 13:52:27
6973475
{% extends "base.html" %}
{% set active_page = "models" %}
{% block title %}Model Registry{% endblock %}
{% block page_title %}<i class="fa-solid fa-box-archive" style="color:var(--warning)"></i> Model Registry{% endblock %}
{% block content %}
<div class="page-title">Model Registry</div>
<div class="page-sub">Track model versions and manage stage transitions: None → Staging → Production → Archived</div>
{% if models %}
<div style="display:flex;flex-direction:column;gap:16px">
{% for model in models %}
<div class="card">
<div class="flex-between" style="margin-bottom:14px">
<div>
<div style="font-weight:700;font-size:1rem">{{ model.name }}</div>
<div style="font-size:.82rem;color:var(--text-secondary);margin-top:2px">{{ model.description }}</div>
</div>
<span class="badge stage-{{ model.latest_stage | lower }}">
{% if model.latest_stage == 'Production' %}<i class="fa-solid fa-rocket"></i>
{% elif model.latest_stage == 'Staging' %}<i class="fa-solid fa-flask"></i>
{% elif model.latest_stage == 'Archived' %}<i class="fa-solid fa-archive"></i>
{% else %}<i class="fa-solid fa-box"></i>{% endif %}
{{ model.latest_stage }}
</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>Version</th><th>Stage</th><th>Run ID</th><th>Key Metrics</th><th>Registered</th><th>Actions</th></tr>
</thead>
<tbody>
{% for v in model.versions %}
<tr>
<td><span class="badge badge-muted">v{{ v.version }}</span></td>
<td>
<span class="badge stage-{{ v.stage | lower }}">
{{ v.stage }}
</span>
</td>
<td><code style="font-size:.8rem;color:var(--accent-light)">{{ v.run_id }}</code></td>
<td>
{% if v.metrics %}
{% for k, val in v.metrics.items() %}
<span style="font-size:.8rem;margin-right:8px">
<span style="color:var(--text-muted)">{{ k }}:</span>
<strong>{{ val }}</strong>
</span>
{% endfor %}
{% else %}—{% endif %}
</td>
<td style="font-size:.8rem;color:var(--text-muted)">{{ v.created_at }}</td>
<td>
<div style="display:flex;gap:6px;flex-wrap:wrap">
{% if v.stage != 'Staging' and v.stage != 'Production' %}
<button class="btn btn-ghost btn-sm"
onclick="transitionStage('{{ model.name }}','{{ v.version }}','Staging', this)">
→ Staging
</button>
{% endif %}
{% if v.stage == 'Staging' %}
<button class="btn btn-success btn-sm"
onclick="transitionStage('{{ model.name }}','{{ v.version }}','Production', this)">
<i class="fa-solid fa-rocket"></i> Promote
</button>
{% endif %}
{% if v.stage == 'Production' %}
<button class="btn btn-warning btn-sm"
onclick="transitionStage('{{ model.name }}','{{ v.version }}','Archived', this)">
Archive
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card">
<div class="empty-state">
<div class="empty-state-icon">📦</div>
<div class="empty-state-title">No registered models yet</div>
<div style="margin-bottom:16px;max-width:400px;margin-left:auto;margin-right:auto">
Run the <strong>Training Pipeline</strong> or use the AutoML sweep,
then register the best model from the Experiments page.
</div>
<a href="/pipeline" class="btn btn-primary"><i class="fa-solid fa-diagram-project"></i> Go to Pipelines</a>
</div>
</div>
{% endif %}
<!-- Register modal -->
<div class="modal-overlay" id="register-modal">
<div class="modal">
<div class="modal-header">
<div class="modal-title"><i class="fa-solid fa-plus" style="color:var(--accent)"></i> Register Model</div>
<button class="modal-close" onclick="document.getElementById('register-modal').classList.remove('open')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="form-group">
<label class="form-label">MLflow Run ID</label>
<input type="text" class="form-control" id="reg-run-id" placeholder="e.g. abc12345…">
</div>
<div class="form-group">
<label class="form-label">Model Name</label>
<input type="text" class="form-control" id="reg-name" placeholder="e.g. iris-classifier">
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="document.getElementById('register-modal').classList.remove('open')">Cancel</button>
<button class="btn btn-primary" onclick="registerModel()"><i class="fa-solid fa-box-archive"></i> Register</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function transitionStage(name, version, stage, btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner" style="width:12px;height:12px;border-width:1.5px"></span>';
try {
const res = await fetch(`/api/models/${encodeURIComponent(name)}/${version}/stage`, {
method: 'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ stage }),
});
const data = await res.json();
if (data.error) { showToast(data.error, 'error'); btn.disabled = false; return; }
showToast(`${name} v${version}${stage}`, 'success');
setTimeout(() => location.reload(), 900);
} catch(e) { showToast('Request failed', 'error'); btn.disabled = false; }
}
async function registerModel() {
const runId = document.getElementById('reg-run-id').value.trim();
const name = document.getElementById('reg-name').value.trim();
if (!runId || !name) { showToast('Run ID and name are required', 'error'); return; }
try {
const res = await fetch('/api/models/register', {
method: 'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ run_id: runId, name }),
});
const data = await res.json();
if (data.error) { showToast(data.error, 'error'); return; }
showToast(`Registered ${data.name} v${data.version}`, 'success');
setTimeout(() => location.reload(), 900);
} catch(e) { showToast('Request failed', 'error'); }
}
</script>
{% endblock %}