Spaces:
Sleeping
Sleeping
| {% 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 %} | |