File size: 6,624 Bytes
6973475
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
{% 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 %}