// ========== UPDATE MANAGEMENT ========== (function() { // Inject modal HTML injectModal('updates-modal', `

⬆️ Update Management

📦 Click "Check for Updates" to scan containers.
Loading update history...
🤖 Loading auto-update configuration...
`); const modal = document.getElementById('updates-modal'); const openBtn = document.getElementById('updates-btn'); const cancelBtn = document.getElementById('updates-cancel'); const checkBtn = document.getElementById('updates-check-btn'); const availableContainer = document.getElementById('updates-available-container'); const historyContainer = document.getElementById('updates-history-container'); const autoContainer = document.getElementById('updates-auto-container'); const lastCheckSpan = document.getElementById('updates-last-check'); async function loadAvailable() { try { const res = await fetch('/api/v1/updates/available'); const data = await res.json(); if (!data.success) throw new Error(data.error); const updates = data.updates || []; if (updates.length === 0) { availableContainer.innerHTML = '
All containers are up to date.
'; lastCheckSpan.textContent = ''; return; } let html = ''; html += ''; for (const u of updates) { html += ``; html += ``; html += ``; html += ``; html += ``; html += `'; } html += '
ContainerImageCurrentLatestActions
${u.containerName}${u.imageName}${u.currentDigest}${u.latestDigest}`; html += ``; html += ``; html += '
'; availableContainer.innerHTML = html; lastCheckSpan.textContent = updates.length + ' update(s) available'; // Wire update buttons availableContainer.querySelectorAll('.update-now-btn').forEach(btn => { btn.addEventListener('click', async () => { const id = btn.dataset.id; const name = btn.dataset.name; if (!confirm(`Update "${name}" to the latest version? The container will restart.`)) return; btn.textContent = 'Updating...'; btn.disabled = true; try { const r = await secureFetch(`/api/v1/updates/update/${encodeURIComponent(id)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ autoRollback: true }) }); const d = await r.json(); if (d.success) { btn.textContent = 'Done!'; btn.style.background = 'var(--ok-fg)'; setTimeout(() => loadAvailable(), 2000); } else { throw new Error(d.error || 'Update failed'); } } catch (e) { btn.textContent = 'Failed'; btn.style.color = 'var(--bad-fg)'; showNotification('Update error: ' + e.message, 'error'); setTimeout(() => { btn.textContent = 'Update'; btn.disabled = false; btn.style.color = ''; btn.style.background = ''; }, 3000); } }); }); // Wire rollback buttons availableContainer.querySelectorAll('.rollback-btn').forEach(btn => { btn.addEventListener('click', async () => { const id = btn.dataset.id; const name = btn.dataset.name; if (!confirm(`Rollback "${name}" to its previous version?`)) return; btn.textContent = 'Rolling back...'; btn.disabled = true; try { const r = await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(id)}`, { method: 'POST' }); const d = await r.json(); if (d.success) { btn.textContent = 'Rolled back!'; setTimeout(() => loadAvailable(), 2000); } else { throw new Error(d.error || 'Rollback failed'); } } catch (e) { btn.textContent = 'Failed'; showNotification('Rollback error: ' + e.message, 'error'); setTimeout(() => { btn.textContent = 'Rollback'; btn.disabled = false; }, 3000); } }); }); } catch (e) { availableContainer.innerHTML = `
Failed: ${e.message}
`; } } async function checkForUpdates() { checkBtn.textContent = '🔍 Checking...'; checkBtn.disabled = true; try { const res = await secureFetch('/api/v1/updates/check', { method: 'POST' }); const data = await res.json(); if (!data.success) throw new Error(data.error); checkBtn.textContent = '✅ Done!'; await loadAvailable(); } catch (e) { checkBtn.textContent = '❌ Failed'; showNotification('Check error: ' + e.message, 'error'); } setTimeout(() => { checkBtn.textContent = '🔍 Check for Updates'; checkBtn.disabled = false; }, 3000); } async function loadHistory() { try { historyContainer.innerHTML = '
Loading...
'; const res = await fetch('/api/v1/updates/history?limit=50'); const data = await res.json(); const history = data.success && data.history ? data.history : []; if (history.length === 0) { historyContainer.innerHTML = '
📋No update history yet.
'; return; } let html = ''; html += ''; for (const h of history) { const ok = h.status === 'success'; const dur = h.duration ? (h.duration < 1000 ? h.duration + 'ms' : Math.round(h.duration / 1000) + 's') : '-'; html += ``; html += ``; html += ``; html += ``; html += ``; html += ``; html += ''; if (!ok && h.error) { html += ``; } } html += '
WhenContainerImageDurationStatus
${timeAgo(h.timestamp)}${h.containerName}${h.imageName}${dur}${ok ? '✓ success' : '✗ failed'}
${h.error}
'; historyContainer.innerHTML = html; } catch (e) { historyContainer.innerHTML = `
Failed: ${e.message}
`; } } async function loadAutoConfig() { try { autoContainer.innerHTML = '
Loading...
'; // Get running containers to show auto-update toggles const res = await fetch('/api/v1/stats/containers'); const data = await res.json(); const containers = data.success && data.stats ? data.stats : []; if (containers.length === 0) { autoContainer.innerHTML = '
🤖No running containers found.
'; return; } let html = ''; html += ''; for (const c of containers) { const name = c.name || c.Names?.[0]?.replace(/^\//, '') || c.Id?.substring(0, 12); const cid = c.containerId || c.Id; html += ``; html += ``; html += ``; html += ``; html += ``; html += ''; } html += '
ContainerScheduleAuto-RollbackActions
${name}
'; autoContainer.innerHTML = html; // Wire save buttons autoContainer.querySelectorAll('.save-auto-btn').forEach(btn => { btn.addEventListener('click', async () => { const id = btn.dataset.id; const row = btn.closest('tr'); const schedule = row.querySelector('.auto-schedule').value; const rollback = row.querySelector('.auto-rollback').checked; btn.textContent = 'Saving...'; btn.disabled = true; try { const r = await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(id)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: !!schedule, schedule: schedule || 'weekly', autoRollback: rollback }) }); const d = await r.json(); if (d.success) { btn.textContent = '✓ Saved'; } else { throw new Error(d.error); } } catch (e) { btn.textContent = '✗ Error'; showNotification('Save error: ' + e.message, 'error'); } setTimeout(() => { btn.textContent = 'Save'; btn.disabled = false; }, 2000); }); }); } catch (e) { autoContainer.innerHTML = `
Failed: ${e.message}
`; } } checkBtn?.addEventListener('click', checkForUpdates); openBtn?.addEventListener('click', () => { modal?.classList.add('show'); loadAvailable(); }); wireModal(modal, cancelBtn); // Lazy-load tabs document.querySelector('[data-panel="updates-history"]')?.addEventListener('click', loadHistory); document.querySelector('[data-panel="updates-auto"]')?.addEventListener('click', loadAutoConfig); })();