<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Evolve Dashboard</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 2rem;
background: #0a0a0a;
color: #e0e0e0;
max-width: 1100px;
margin: 0 auto;
}
h1 { color: #0A84FF; margin-bottom: 0.25rem; }
h2 { color: #0A84FF; margin-top: 2rem; font-size: 1.1rem; border-bottom: 1px solid #222; padding-bottom: 0.25rem; }
.subtitle { color: #808080; margin-bottom: 2rem; font-size: 0.9rem; }
.project {
background: #151515;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 1rem 1.5rem;
margin-bottom: 1.5rem;
}
.project-header { font-weight: 600; color: #0A84FF; margin-bottom: 0.25rem; }
.meta { color: #808080; font-size: 0.85rem; }
.badge {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
margin-left: 0.5rem;
}
.badge-running { background: #1a3a5a; color: #7fb8ff; }
.badge-promoted { background: #1a4a1a; color: #7fe07f; }
.badge-aborted { background: #4a1a1a; color: #ff8080; }
.badge-held { background: #3a3a1a; color: #ffd080; }
.empty { color: #808080; font-style: italic; }
.experiment {
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
background: #0a0a0a;
border-radius: 4px;
border-left: 3px solid #0A84FF;
}
.promotion-entry {
margin-top: 0.5rem;
padding: 0.4rem 0.6rem;
background: #0a0a0a;
border-radius: 4px;
font-size: 0.85rem;
}
code { font-family: ui-monospace, Menlo, monospace; font-size: 0.85rem; color: #b8b8b8; }
</style>
</head>
<body>
<h1>Evolve</h1>
<div class="subtitle">Local champion-vs-challenger evolution for your AI coding assistants.</div>
<div id="app">Loading...</div>
<script>
function fmtDate(iso) {
if (!iso) return 'never';
return new Date(iso).toLocaleString();
}
function statusBadge(status) {
const cls = `badge-${status.toLowerCase()}`;
return `<span class="badge ${cls}">${status}</span>`;
}
async function fetchProjectDetail(project) {
const [exp, promotionLog, sessions] = await Promise.all([
fetch(`/api/projects/${project.id}/experiment`).then(r => r.json()).catch(() => null),
fetch(`/api/projects/${project.id}/promotion-log`).then(r => r.json()).catch(() => []),
fetch(`/api/projects/${project.id}/sessions`).then(r => r.json()).catch(() => []),
]);
return { project, exp, promotionLog, sessions };
}
function renderProject(detail) {
const { project, exp, promotionLog, sessions } = detail;
const expHtml = exp ? `
<h2>Active experiment</h2>
<div class="experiment">
${statusBadge('Running')}
<div class="meta">Started ${fmtDate(exp.started_at)} · ${(exp.traffic_share * 100).toFixed(0)}% to challenger</div>
<div class="meta">Champion config: <code>${exp.champion_config_id}</code></div>
<div class="meta">Challenger config: <code>${exp.challenger_config_id}</code></div>
</div>
` : '<h2>Active experiment</h2><div class="empty">No running experiment.</div>';
const logHtml = promotionLog.length ? `
<h2>Promotion log (${promotionLog.length})</h2>
${promotionLog.map(e => `
<div class="promotion-entry">
${statusBadge(e.status)}
<code>${e.id.slice(0, 8)}</code>
· decided ${fmtDate(e.decided_at)}
· posterior ${e.decision_posterior !== null ? e.decision_posterior.toFixed(3) : '—'}
</div>
`).join('')}
` : '<h2>Promotion log</h2><div class="empty">No completed experiments yet.</div>';
const sessionsHtml = sessions.length
? `<h2>Recent sessions (${sessions.length})</h2>
<div class="meta">Most recent: ${fmtDate(sessions[0].started_at)} · variant ${sessions[0].variant}</div>`
: '<h2>Recent sessions</h2><div class="empty">No sessions recorded yet.</div>';
return `
<div class="project">
<div class="project-header">${project.name}</div>
<div class="meta">${project.adapter_id} · ${project.root_path}</div>
<div class="meta">Created ${fmtDate(project.created_at)} · champion <code>${project.champion_config_id || '—'}</code></div>
${expHtml}
${logHtml}
${sessionsHtml}
</div>
`;
}
async function refresh() {
const app = document.getElementById("app");
try {
const projects = await fetch("/api/projects").then(r => r.json());
if (!projects.length) {
app.innerHTML = '<p class="empty">No projects registered. Run <code>evolve init <adapter></code> in a repo to get started.</p>';
return;
}
const details = await Promise.all(projects.map(fetchProjectDetail));
app.innerHTML = details.map(renderProject).join('');
} catch (e) {
app.innerHTML = `<p class="empty">Failed to load: ${e.message}</p>`;
}
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>