evolve-dashboard 0.2.0

Local-only web dashboard for Evolve
Documentation
<!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 &lt;adapter&gt;</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>