/**
* Deploy UI — deploy dashboard
*/
import { renderPage } from './page-layout.js'
const PAGE_CSS = `
.section { margin-bottom: 40px; }
.section h2 { margin-bottom: 15px; font-size: 20px; }
.repo-card {
background: #161b22; border: 1px solid #30363d;
border-radius: 6px; padding: 20px; margin-bottom: 15px;
}
.repo-card-header {
display: flex; justify-content: space-between;
align-items: center; margin-bottom: 15px;
}
.repo-card-header h3 { font-size: 18px; }
.repo-meta {
display: flex; gap: 20px; font-size: 13px;
color: #8b949e; margin-bottom: 15px;
}
.repo-meta span { display: flex; align-items: center; gap: 5px; }
.badge.seeding { background: #238636; color: white; }
.badge.idle { background: #6e7681; color: white; }
.badge.configured { background: #1f6feb; color: white; }
.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; }
.deploy-actions { display: flex; gap: 10px; align-items: center; }
.setup-form {
background: #0d1117; border: 1px solid #30363d;
border-radius: 6px; padding: 20px; margin-top: 15px; display: none;
}
.setup-form.visible { display: block; }
`
const PAGE_SCRIPTS = `
function getBasePath() {
const parts = window.location.pathname.split('/');
if (parts.length > 2 && parts[1] !== 'deploy') return '/' + parts[1];
return '';
}
function showStatus(msg, type) {
const area = document.getElementById('status-area');
area.innerHTML = '<div class="status-msg ' + type + '">' + msg + '</div>';
if (type !== 'info') setTimeout(() => { area.innerHTML = ''; }, 5000);
}
async function setupDeploy(repoName) {
const form = document.getElementById('setup-' + repoName);
if (form) form.classList.toggle('visible');
}
async function saveConfig(repoName) {
const channel = document.getElementById('channel-' + repoName).value.trim();
if (!channel) { alert('Channel name is required'); return; }
const autoRelease = document.getElementById('autoRelease-' + repoName).checked;
const autoSeed = document.getElementById('autoSeed-' + repoName).checked;
const autoDeploy = document.getElementById('autoDeploy-' + repoName).checked;
try {
const resp = await window.fetch(getBasePath() + '/api/deploy/config/' + repoName, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel, autoRelease, autoSeed, autoDeploy, enabled: true })
});
const data = await resp.json();
if (data.success) { showStatus('Deploy configured for ' + repoName, 'success'); setTimeout(() => location.reload(), 1000); }
else showStatus('Error: ' + data.error, 'error');
} catch (err) { showStatus('Error: ' + err.message, 'error'); }
}
async function triggerDeploy(repoName) {
const btn = event.target;
btn.disabled = true; btn.innerHTML = '<span class="spinner"></span>Deploying...';
showStatus('Deploying ' + repoName + '...', 'info');
try {
const resp = await window.fetch(getBasePath() + '/api/deploy/trigger/' + repoName, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
});
const data = await resp.json();
if (data.success) { showStatus('Deployed ' + repoName + ' v' + data.version + ' in ' + data.duration + 'ms', 'success'); loadLog(repoName); loadStatus(repoName); }
else showStatus('Deploy failed: ' + data.error, 'error');
} catch (err) { showStatus('Deploy error: ' + err.message, 'error'); }
finally { btn.disabled = false; btn.textContent = 'Deploy Now'; }
}
async function disableDeploy(repoName) {
if (!confirm('Disable deploy for ' + repoName + '?')) return;
try {
const resp = await window.fetch(getBasePath() + '/api/deploy/config/' + repoName, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel: '_disabled', enabled: false })
});
const data = await resp.json();
if (data.success) { showStatus('Deploy disabled for ' + repoName, 'success'); setTimeout(() => location.reload(), 1000); }
} catch (err) { showStatus('Error: ' + err.message, 'error'); }
}
async function loadLog(repoName) {
try {
const resp = await window.fetch(getBasePath() + '/api/deploy/log/' + repoName);
const entries = await resp.json();
const logEl = document.getElementById('deploy-log');
if (entries.length === 0) { logEl.innerHTML = '<div class="empty-state">No deploys yet for ' + repoName + '</div>'; return; }
let html = '<table class="data-table"><thead><tr><th>Time</th><th>Version</th><th>Channel</th><th>Duration</th></tr></thead><tbody>';
for (const e of entries) html += '<tr><td>' + new Date(e.timestamp).toLocaleString() + '</td><td>' + (e.version||'-') + '</td><td>' + (e.channel||'-') + '</td><td>' + (e.duration||0) + 'ms</td></tr>';
html += '</tbody></table>';
logEl.innerHTML = html;
} catch (err) { console.error('Failed to load log:', err); }
}
async function loadStatus(repoName) {
try {
const resp = await window.fetch(getBasePath() + '/api/deploy/status/' + repoName);
const data = await resp.json();
const el = document.getElementById('status-' + repoName);
if (!el) return;
let parts = [];
if (data.lastVersion) parts.push('v' + data.lastVersion);
if (data.seeding) parts.push('<span class="badge seeding">Seeding</span>');
else parts.push('<span class="badge idle">Not seeding</span>');
if (data.lastDeploy) parts.push(new Date(data.lastDeploy).toLocaleString());
el.innerHTML = parts.join(' · ');
const keyEl = document.getElementById('key-' + repoName);
if (keyEl && data.lastKey) keyEl.innerHTML = '<code style="font-size:12px;color:#58a6ff;word-break:break-all;">pear://' + data.lastKey + '</code>';
} catch (err) { console.error('Status error:', err); }
}
`
export function renderDeployDashboard (repos = [], pathPrefix = '') {
const configured = repos.filter(r => r.deploy && r.deploy.enabled)
const unconfigured = repos.filter(r => !r.deploy || !r.deploy.enabled)
const content = `
<div id="status-area"></div>
<div class="section">
<h2>Configured (${configured.length})</h2>
<div id="configured-repos">
${configured.length === 0 ? '<div class="empty-state">No repos configured for deploy yet.</div>' : ''}
${configured.map(r => renderConfiguredRepo(r)).join('')}
</div>
</div>
<div class="section">
<h2>Available (${unconfigured.length})</h2>
<div id="unconfigured-repos">
${unconfigured.length === 0 ? '<div class="empty-state">All repos are configured.</div>' : ''}
${unconfigured.map(r => renderUnconfiguredRepo(r)).join('')}
</div>
</div>
<div class="section">
<h2>Recent Deploys</h2>
<div id="deploy-log" class="card">
<div class="empty-state">Select a repo to view deploy history.</div>
</div>
</div>
`
const scripts = PAGE_SCRIPTS + '\n ' + configured.map(r => `loadStatus('${r.name}');`).join('\n ')
return renderPage('Deploy', content, {
pathPrefix,
activePage: 'deploy',
css: PAGE_CSS,
scripts
})
}
function renderConfiguredRepo (repo) {
const d = repo.deploy
return `
<div class="repo-card">
<div class="repo-card-header">
<h3>${repo.name}</h3>
<div class="deploy-actions">
<button onclick="triggerDeploy('${repo.name}')">Deploy Now</button>
<button class="secondary small" onclick="loadLog('${repo.name}')">View Log</button>
<button class="danger small" onclick="disableDeploy('${repo.name}')">Disable</button>
</div>
</div>
<div class="repo-meta">
<span>Channel: <strong>${d.channel}</strong></span>
<span>Auto-release: ${d.autoRelease ? 'Yes' : 'No'}</span>
<span>Auto-seed: ${d.autoSeed ? 'Yes' : 'No'}</span>
<span>Auto-deploy: ${d.autoDeploy !== false ? 'Yes' : 'No'}</span>
</div>
<div id="key-${repo.name}" class="repo-meta">${d.lastKey ? '<code style="font-size:12px;color:#58a6ff;word-break:break-all;">pear://' + d.lastKey + '</code>' : ''}</div>
<div id="status-${repo.name}" class="repo-meta">Loading status...</div>
</div>
`
}
function renderUnconfiguredRepo (repo) {
return `
<div class="repo-card">
<div class="repo-card-header">
<h3>${repo.name}</h3>
<button class="secondary" onclick="setupDeploy('${repo.name}')">Configure Deploy</button>
</div>
<div id="setup-${repo.name}" class="setup-form">
<div class="form-group">
<label for="channel-${repo.name}">Pear Channel Name</label>
<input type="text" id="channel-${repo.name}" value="${repo.name}" placeholder="e.g. my-app">
<div class="help-text">Used in pear stage/release/seed commands. Usually matches the app name.</div>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" id="autoRelease-${repo.name}" checked> Auto-release after staging</label>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" id="autoSeed-${repo.name}" checked> Auto-seed after release</label>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" id="autoDeploy-${repo.name}" checked> Auto-deploy on git push</label>
</div>
<div style="display: flex; gap: 10px; margin-top: 15px;">
<button onclick="saveConfig('${repo.name}')">Save Configuration</button>
<button class="secondary" onclick="setupDeploy('${repo.name}')">Cancel</button>
</div>
</div>
</div>
`
}