// ========== SHIPDECK DEPLOYS (DC-130) ========== // Source deploys panel: drives the shipdeck CLI via the DashCaddy // /api/v1/deploys bridge proxy. Modal lists deployable repos + deployed // services, shows the journal, and runs deploy/rollback with live output. // // Follows the security-center.js modal pattern (injectModal + button in the // top bar) and the weather-modal visual language. (function () { injectModal('deploys-modal', `

đźšš Deploys

Loading…
`); const $ = (id) => document.getElementById(id); let openBtn = null; let loadedOnce = false; function esc(s) { return window.escapeHtml ? window.escapeHtml(String(s)) : String(s).replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } async function api(path, opts) { const r = await fetch('/dashcaddy-api/api/v1/deploys' + path, Object.assign({ credentials: 'include' }, opts || {})); let body = null; try { body = await r.json(); } catch (e) { body = { success: false, error: 'non-JSON response' }; } return { status: r.status, body }; } function showOutput(text) { const el = $('dep-output'); el.style.display = 'block'; el.textContent = text || '(no output)'; el.scrollTop = el.scrollHeight; } async function loadServices() { const el = $('dep-services'); const { body } = await api('/services'); if (!body.success) { el.innerHTML = '' + esc(body.error || 'unavailable') + ''; return; } const rows = body.services || []; if (!rows.length) { el.innerHTML = 'No shipdeck deployments on record yet.'; return; } el.innerHTML = '' + '' + '' + rows.map((s) => '' + '' + '' + '' + '' + '' + '' ).join('') + '
ServiceLast actionWhenRelease
' + esc(s.name) + '' + esc(s.last_action) + '' + esc(s.last_time) + '' + esc(s.last_epoch) + ' ' + '
'; } async function loadRepos() { const sel = $('dep-repo-select'); const { body } = await api('/repos'); if (!body.success) { sel.innerHTML = ''; return; } const repos = body.repos || []; sel.innerHTML = repos.length ? repos.map((r) => '').join('') : ''; } async function loadJournal(service) { const el = $('dep-journal'); const qs = service ? '?service=' + encodeURIComponent(service) : ''; const { body } = await api('/journal' + qs); const rows = (body.rows || []); if (!rows.length) { el.innerHTML = 'No journal rows.'; return; } el.innerHTML = '' + '' + '' + rows.map((r) => '' ).join('') + '
TimeServiceActionReleaseDuration
' + esc(r.time) + '' + esc(r.service) + '' + esc(r.action) + '' + esc(r.epoch) + '' + esc(r.duration || '—') + '
'; } function setBusy(busy, btn, label) { if (!btn) return; btn.disabled = busy; if (label) btn.textContent = busy ? 'Working…' : label; } async function runDeploy() { const btn = $('dep-deploy-btn'); const dir = $('dep-repo-select').value; if (!dir) return; setBusy(true, btn, 'Deploy'); showOutput('Deploying ' + dir + '\nThis can take ~30-60s…'); try { const { body } = await api('/deploy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dir }), }); showOutput((body.output || body.error || '') + (body.success ? '\n✅ SUCCESS' : '\n❌ FAILED')); loadServices(); } catch (e) { showOutput('deploy error: ' + e.message); } setBusy(false, btn, 'Deploy'); } async function runRollback(service, btn) { if (!confirm('Roll back ' + service + ' to the previous release?')) return; setBusy(true, btn, 'Rollback'); showOutput('Rolling back ' + service + '…'); try { const { body } = await api('/rollback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service }), }); showOutput((body.output || body.error || '') + (body.success ? '\n✅ ROLLED BACK' : '\n❌ FAILED')); loadServices(); } catch (e) { showOutput('rollback error: ' + e.message); } setBusy(false, btn, 'Rollback'); } async function runStatus(service, btn) { setBusy(true, btn, 'Status'); try { const { body } = await api('/status?service=' + encodeURIComponent(service)); showOutput(body.output || body.error || '(no output)'); } catch (e) { showOutput('status error: ' + e.message); } setBusy(false, btn, 'Status'); } function wire() { openBtn = document.getElementById('deploys-btn'); if (!openBtn) return; openBtn.addEventListener('click', async () => { $('deploys-modal').classList.add('open'); if (!loadedOnce) { loadedOnce = true; loadServices(); loadRepos(); loadJournal(''); } }); $('dep-close').addEventListener('click', () => { $('deploys-modal').classList.remove('open'); }); // tab switching document.querySelectorAll('.dep-tab').forEach((tab) => { tab.addEventListener('click', () => { document.querySelectorAll('.dep-tab').forEach((t) => t.classList.remove('active')); tab.classList.add('active'); document.querySelectorAll('#deploys-modal .dep-panel').forEach((p) => { p.style.display = p.dataset.panel === tab.dataset.tab ? 'block' : 'none'; }); }); }); $('dep-deploy-btn').addEventListener('click', runDeploy); $('dep-journal-btn').addEventListener('click', () => loadJournal($('dep-journal-name').value.trim())); $('dep-status-btn').addEventListener('click', () => { const name = $('dep-status-name').value.trim(); if (name) runStatus(name); }); // dynamic buttons (service table) $('dep-services').addEventListener('click', (e) => { const rb = e.target.closest('.dep-rollback'); if (rb) return runRollback(rb.dataset.service, rb); const sb = e.target.closest('.dep-status'); if (sb) return runStatus(sb.dataset.service, sb); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', wire); } else { wire(); } })();