|
|
|
|
@@ -71,10 +71,10 @@ async function renderDashboard() {
|
|
|
|
|
all.forEach(i => { states[i.state] = (states[i.state] || 0) + 1; });
|
|
|
|
|
|
|
|
|
|
document.getElementById('stats-bar').innerHTML = `
|
|
|
|
|
<div class="stat-cell"><div class="stat-label">total</div><div class="stat-value accent">${all.length}</div></div>
|
|
|
|
|
<div class="stat-cell"><div class="stat-label">deployed</div><div class="stat-value">${states['deployed'] || 0}</div></div>
|
|
|
|
|
<div class="stat-cell"><div class="stat-label">testing</div><div class="stat-value amber">${states['testing'] || 0}</div></div>
|
|
|
|
|
<div class="stat-cell"><div class="stat-label">degraded</div><div class="stat-value red">${states['degraded'] || 0}</div></div>
|
|
|
|
|
<div class="stat-cell stat-clickable" onclick="setStateFilter('')"><div class="stat-label">total</div><div class="stat-value accent">${all.length}</div></div>
|
|
|
|
|
<div class="stat-cell stat-clickable" onclick="setStateFilter('deployed')"><div class="stat-label">deployed</div><div class="stat-value">${states['deployed'] || 0}</div></div>
|
|
|
|
|
<div class="stat-cell stat-clickable" onclick="setStateFilter('testing')"><div class="stat-label">testing</div><div class="stat-value amber">${states['testing'] || 0}</div></div>
|
|
|
|
|
<div class="stat-cell stat-clickable" onclick="setStateFilter('degraded')"><div class="stat-label">degraded</div><div class="stat-value red">${states['degraded'] || 0}</div></div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
await populateStackFilter();
|
|
|
|
|
@@ -95,6 +95,11 @@ async function populateStackFilter() {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setStateFilter(state) {
|
|
|
|
|
document.getElementById('filter-state').value = state;
|
|
|
|
|
filterInstances();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function filterInstances() {
|
|
|
|
|
const search = document.getElementById('search-input').value;
|
|
|
|
|
const state = document.getElementById('filter-state').value;
|
|
|
|
|
@@ -289,6 +294,10 @@ async function saveInstance() {
|
|
|
|
|
hardware_acceleration: +document.getElementById('f-hardware-accel').checked,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Snapshot job state before creation — jobs fire immediately after the 201
|
|
|
|
|
// so the baseline must be captured before the POST, not after.
|
|
|
|
|
const jobBaseline = !editingVmid ? await _snapshotJobBaseline() : null;
|
|
|
|
|
|
|
|
|
|
const result = editingVmid
|
|
|
|
|
? await updateInstance(editingVmid, data)
|
|
|
|
|
: await createInstance(data);
|
|
|
|
|
@@ -298,7 +307,7 @@ async function saveInstance() {
|
|
|
|
|
showToast(`${name} ${editingVmid ? 'updated' : 'created'}`, 'success');
|
|
|
|
|
closeModal();
|
|
|
|
|
|
|
|
|
|
if (!editingVmid) await _waitForOnCreateJobs();
|
|
|
|
|
if (jobBaseline) await _waitForOnCreateJobs(jobBaseline);
|
|
|
|
|
|
|
|
|
|
if (currentVmid && document.getElementById('page-detail').classList.contains('active')) {
|
|
|
|
|
await renderDetailPage(vmid);
|
|
|
|
|
@@ -307,15 +316,15 @@ async function saveInstance() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function _waitForOnCreateJobs() {
|
|
|
|
|
async function _snapshotJobBaseline() {
|
|
|
|
|
const jobs = await fetch('/api/jobs').then(r => r.json());
|
|
|
|
|
const relevant = jobs.filter(j => {
|
|
|
|
|
try { return JSON.parse(j.config || '{}').run_on_create; } catch { return false; }
|
|
|
|
|
});
|
|
|
|
|
if (!relevant.length) return;
|
|
|
|
|
return new Map(jobs.map(j => [j.id, j.last_run_id ?? null]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Snapshot run IDs before jobs fire so we can detect new completions
|
|
|
|
|
const baseline = new Map(relevant.map(j => [j.id, j.last_run_id ?? null]));
|
|
|
|
|
async function _waitForOnCreateJobs(baseline) {
|
|
|
|
|
const jobs = await fetch('/api/jobs').then(r => r.json());
|
|
|
|
|
const relevant = jobs.filter(j => (j.config ?? {}).run_on_create);
|
|
|
|
|
if (!relevant.length) return;
|
|
|
|
|
|
|
|
|
|
const deadline = Date.now() + 30_000;
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
|