// ========== 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...
DashCaddy
Loading...
📦No self-update history.
`); 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
${escapeHtml(u.containerName)}${escapeHtml(u.imageName)}${escapeHtml(u.currentDigest)}${escapeHtml(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: ${escapeHtml(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)}${escapeHtml(h.containerName)}${escapeHtml(h.imageName)}${dur}${ok ? '✓ success' : '✗ failed'}
${escapeHtml(h.error)}
'; historyContainer.innerHTML = html; } catch (e) { historyContainer.innerHTML = `
Failed: ${escapeHtml(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
${escapeHtml(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: ${escapeHtml(e.message)}
`; } } // ===== DASHCADDY SELF-UPDATE ===== const dcVersionInfo = document.getElementById('dashcaddy-current-version'); const dcUpdateBadge = document.getElementById('dashcaddy-update-badge'); const dcUpdateDetails = document.getElementById('dashcaddy-update-details'); const dcNewVersion = document.getElementById('dashcaddy-new-version'); const dcChangelog = document.getElementById('dashcaddy-changelog'); const dcApplyBtn = document.getElementById('dashcaddy-apply-btn'); const dcCheckBtn = document.getElementById('dashcaddy-check-btn'); const dcRollbackBtn = document.getElementById('dashcaddy-rollback-btn'); const dcStatusBar = document.getElementById('dashcaddy-status-bar'); const dcHistoryContainer = document.getElementById('dashcaddy-history-container'); let dcLastCheck = null; function dcShowStatus(msg, type) { if (!dcStatusBar) return; dcStatusBar.style.display = 'block'; dcStatusBar.style.background = type === 'error' ? 'var(--bad-bg)' : type === 'success' ? 'var(--ok-bg)' : 'var(--bg)'; dcStatusBar.style.color = type === 'error' ? 'var(--bad-fg)' : type === 'success' ? 'var(--ok-fg)' : 'var(--fg)'; dcStatusBar.textContent = msg; } async function dcLoadVersion() { try { const res = await fetch('/api/v1/system/version'); const data = await res.json(); if (data.success) { dcVersionInfo.textContent = 'v' + data.version + (data.commit ? ' (' + data.commit.substring(0, 7) + ')' : ''); } } catch (_) { dcVersionInfo.textContent = 'Unable to fetch version'; } } async function dcCheckForUpdate(silent) { if (!silent) { dcCheckBtn.textContent = 'Checking...'; dcCheckBtn.disabled = true; } try { const res = await fetch('/api/v1/system/update-check'); const data = await res.json(); dcLastCheck = data; if (data.success && data.available && data.remote) { dcUpdateBadge.style.display = ''; dcUpdateDetails.style.display = ''; dcNewVersion.textContent = 'v' + data.remote.version; dcChangelog.textContent = data.remote.changelog || 'No changelog available.'; // Add notification dot to the Updates button const updatesBtn = document.getElementById('updates-btn'); if (updatesBtn && !updatesBtn.querySelector('.update-dot')) { const dot = document.createElement('span'); dot.className = 'update-dot'; dot.style.cssText = 'position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);'; updatesBtn.style.position = 'relative'; updatesBtn.appendChild(dot); } // Also add dot to the DashCaddy tab const dcTab = document.getElementById('updates-dashcaddy-tab'); if (dcTab && !dcTab.querySelector('.update-dot')) { const dot = document.createElement('span'); dot.className = 'update-dot'; dot.style.cssText = 'display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;'; dcTab.appendChild(dot); } } else { dcUpdateBadge.style.display = 'none'; dcUpdateDetails.style.display = 'none'; if (!silent) dcShowStatus('You are running the latest version.', 'success'); } if (!silent) { dcCheckBtn.textContent = 'Check for Updates'; dcCheckBtn.disabled = false; } } catch (e) { if (!silent) { dcShowStatus('Failed to check: ' + e.message, 'error'); dcCheckBtn.textContent = 'Check for Updates'; dcCheckBtn.disabled = false; } } } async function dcApplyUpdate() { if (!confirm('Apply DashCaddy update? The API container will restart.')) return; dcApplyBtn.textContent = 'Updating...'; dcApplyBtn.disabled = true; dcShowStatus('Downloading and applying update...', 'info'); try { const res = await secureFetch('/api/v1/system/update-apply', { method: 'POST' }); const data = await res.json(); if (data.success) { dcShowStatus('Update initiated: v' + (data.fromVersion || '?') + ' → v' + (data.toVersion || '?') + '. The container will restart shortly.', 'success'); dcApplyBtn.textContent = 'Applied!'; // Remove notification dots document.querySelectorAll('.update-dot').forEach(d => d.remove()); } else { throw new Error(data.error || 'Update failed'); } } catch (e) { dcShowStatus('Update failed: ' + e.message, 'error'); dcApplyBtn.textContent = 'Update Now'; dcApplyBtn.disabled = false; } } async function dcLoadHistory() { try { const res = await fetch('/api/v1/system/update-history'); const data = await res.json(); const history = data.success && data.history ? data.history : []; if (history.length === 0) { dcHistoryContainer.innerHTML = '
📦No self-update history.
'; return; } let html = ''; html += ''; for (const h of history) { const st = h.status === 'success' ? '✓ success' : h.status === 'pending' ? '⏳ pending' : h.status === 'partial' ? '⚠ partial' : '✗ ' + h.status; const stColor = h.status === 'success' ? 'var(--ok-fg)' : h.status === 'pending' ? 'var(--muted)' : 'var(--bad-fg)'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; if (h.error) { html += ''; } if (h.note) { html += ''; } } html += '
WhenVersionFromStatus
' + timeAgo(h.timestamp) + 'v' + escapeHtml(h.version) + (h.rollback ? ' (rollback)' : '') + 'v' + escapeHtml(h.fromVersion || '?') + '' + st + '
' + escapeHtml(h.error) + '
' + escapeHtml(h.note) + '
'; dcHistoryContainer.innerHTML = html; } catch (e) { dcHistoryContainer.innerHTML = '
Failed: ' + escapeHtml(e.message) + '
'; } } async function dcShowRollback() { try { const res = await fetch('/api/v1/system/rollback-versions'); const data = await res.json(); const versions = data.success && data.versions ? data.versions : []; if (versions.length === 0) { showNotification('No rollback versions available.', 'info'); return; } const version = prompt('Available rollback versions:\n' + versions.join('\n') + '\n\nEnter version to rollback to:'); if (!version) return; if (!versions.includes(version)) { showNotification('Invalid version: ' + version, 'error'); return; } if (!confirm('Rollback DashCaddy to v' + version + '? The container will restart.')) return; dcShowStatus('Rolling back to v' + version + '...', 'info'); const r = await secureFetch('/api/v1/system/rollback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version }) }); const d = await r.json(); if (d.success) { dcShowStatus('Rollback to v' + version + ' initiated. Container will restart.', 'success'); } else { throw new Error(d.error || 'Rollback failed'); } } catch (e) { dcShowStatus('Rollback failed: ' + e.message, 'error'); } } dcCheckBtn?.addEventListener('click', () => dcCheckForUpdate(false)); dcApplyBtn?.addEventListener('click', dcApplyUpdate); dcRollbackBtn?.addEventListener('click', dcShowRollback); 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); document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener('click', () => { dcLoadVersion(); dcLoadHistory(); if (!dcLastCheck) dcCheckForUpdate(true); }); // Non-blocking check on page load — just adds notification dot if update available setTimeout(() => dcCheckForUpdate(true), 5000); })();