omk 0.5.0

A Rust runtime for Kimi CLI. Turns prompts into proof-backed engineering runs with gates, worktrees, and replay.
Documentation
use axum::response::Html;

pub(super) async fn dashboard_handler() -> Html<String> {
    Html(DASHBOARD_HTML.replace("{{OMK_VERSION}}", env!("CARGO_PKG_VERSION")))
}

const DASHBOARD_HTML: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>omk dashboard</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #0f0f23;
            color: #e0e0e0;
            line-height: 1.6;
        }
        .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
        header {
            display: flex;
            align-items: center;
            gap: 1rem;
            margin-bottom: 2rem;
            padding-bottom: 1rem;
            border-bottom: 1px solid #333;
        }
        header h1 { font-size: 1.8rem; color: #fff; }
        header .version {
            background: #2563eb;
            color: white;
            padding: 0.25rem 0.75rem;
            border-radius: 9999px;
            font-size: 0.875rem;
            font-weight: 500;
        }
        .grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 1.5rem;
            margin-bottom: 2rem;
        }
        .card {
            background: #1a1a2e;
            border: 1px solid #2a2a4a;
            border-radius: 12px;
            padding: 1.5rem;
            transition: border-color 0.2s;
        }
        .card:hover { border-color: #2563eb; }
        .card h2 {
            font-size: 1.1rem;
            color: #fff;
            margin-bottom: 1rem;
            display: flex;
            align-items: center;
            gap: 0.5rem;
        }
        .metric {
            display: flex;
            justify-content: space-between;
            padding: 0.5rem 0;
            border-bottom: 1px solid #2a2a4a;
        }
        .metric:last-child { border-bottom: none; }
        .metric-value { font-weight: 600; color: #60a5fa; }
        .status-ok { color: #22c55e; }
        .status-warn { color: #f59e0b; }
        .status-err { color: #ef4444; }
        .team-list { list-style: none; }
        .team-item {
            padding: 0.75rem;
            margin-bottom: 0.5rem;
            background: #0f0f23;
            border-radius: 8px;
            border-left: 3px solid #2563eb;
        }
        .team-name { font-weight: 600; color: #fff; }
        .team-meta { font-size: 0.875rem; color: #888; margin-top: 0.25rem; }
        .phase-badge {
            display: inline-block;
            padding: 0.15rem 0.5rem;
            border-radius: 4px;
            font-size: 0.75rem;
            font-weight: 500;
            text-transform: uppercase;
        }
        .phase-planning { background: #f59e0b20; color: #f59e0b; }
        .phase-executing { background: #2563eb20; color: #60a5fa; }
        .phase-complete { background: #22c55e20; color: #22c55e; }
        .phase-failed { background: #ef444420; color: #ef4444; }
        .refresh-btn {
            position: fixed;
            bottom: 2rem;
            right: 2rem;
            background: #2563eb;
            color: white;
            border: none;
            padding: 0.75rem 1.5rem;
            border-radius: 9999px;
            cursor: pointer;
            font-size: 1rem;
            box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .refresh-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); }
        footer {
            text-align: center;
            padding: 2rem;
            color: #666;
            font-size: 0.875rem;
        }
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }
        .live-indicator {
            display: inline-block;
            width: 8px;
            height: 8px;
            background: #22c55e;
            border-radius: 50%;
            animation: pulse 2s infinite;
            margin-right: 0.5rem;
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>🌙 omk dashboard</h1>
            <span class="version">v{{OMK_VERSION}}</span>
            <span style="margin-left:auto;color:#666;font-size:0.875rem;">
                <span class="live-indicator"></span>Live
            </span>
        </header>

        <div class="grid">
            <div class="card">
                <h2>📊 Metrics</h2>
                <div id="metrics">
                    <div class="metric"><span>Total Runs</span><span class="metric-value" id="m-team-runs">—</span></div>
                    <div class="metric"><span>Total Shutdowns</span><span class="metric-value" id="m-shutdowns">—</span></div>
                    <div class="metric"><span>Tasks Created</span><span class="metric-value" id="m-tasks">—</span></div>
                    <div class="metric"><span>Ask Calls</span><span class="metric-value" id="m-ask">—</span></div>
                </div>
            </div>

            <div class="card">
                <h2>🤖 Active Teams</h2>
                <ul class="team-list" id="teams">
                    <li class="team-item">
                        <div class="team-name">No active teams</div>
                        <div class="team-meta">Run <code>omk team run</code> to start</div>
                    </li>
                </ul>
            </div>

            <div class="card">
                <h2>🤖 Autopilots</h2>
                <ul class="team-list" id="autopilots">
                    <li class="team-item">
                        <div class="team-name">No active autopilots</div>
                    </li>
                </ul>
            </div>

            <div class="card">
                <h2>🔄 Ralph Sessions</h2>
                <ul class="team-list" id="ralphs">
                    <li class="team-item">
                        <div class="team-name">No active Ralph sessions</div>
                    </li>
                </ul>
            </div>

            <div class="card">
                <h2>🩺 Health</h2>
                <div id="health">
                    <div class="metric"><span>Status</span><span class="metric-value status-ok" id="h-status">Loading...</span></div>
                    <div class="metric"><span>Version</span><span class="metric-value" id="h-version">—</span></div>
                </div>
            </div>
        </div>

        <footer>
            <p>oh-my-kimi — Multi-agent orchestration for Kimi CLI</p>
            <p><a href="https://github.com/ekhodzitsky/oh-my-kimi" style="color:#2563eb;">GitHub</a></p>
        </footer>
    </div>

    <button class="refresh-btn" onclick="loadData()">↻ Refresh</button>

    <script>
        async function loadData() {
            try {
                const [teamsRes, autopilotsRes, ralphsRes, metricsRes, healthRes] = await Promise.all([
                    fetch('/api/teams'),
                    fetch('/api/autopilots'),
                    fetch('/api/ralphs'),
                    fetch('/api/metrics'),
                    fetch('/api/health')
                ]);

                const teams = await teamsRes.json();
                const autopilots = await autopilotsRes.json();
                const ralphs = await ralphsRes.json();
                const metrics = await metricsRes.json();
                const health = await healthRes.json();

                // Teams
                const teamsList = document.getElementById('teams');
                if (teams.teams && teams.teams.length > 0) {
                    teamsList.innerHTML = teams.teams.map(t => {
                        const phase = t.phase || 'Unknown';
                        const phaseClass = 'phase-' + phase.toLowerCase();
                        return `<li class="team-item">
                            <div class="team-name">${t.name || 'Unnamed'}</div>
                            <div class="team-meta">
                                <span class="phase-badge ${phaseClass}">${phase}</span>
                                ${t.task ? '• ' + t.task.substring(0, 60) + (t.task.length > 60 ? '...' : '') : ''}
                            </div>
                        </li>`;
                    }).join('');
                }

                // Autopilots
                const autopilotsList = document.getElementById('autopilots');
                if (autopilots.autopilots && autopilots.autopilots.length > 0) {
                    autopilotsList.innerHTML = autopilots.autopilots.map(a => {
                        const phase = a.phase || 'Unknown';
                        const phaseClass = 'phase-' + phase.toLowerCase();
                        return `<li class="team-item">
                            <div class="team-name">${a.name || 'Unnamed'}</div>
                            <div class="team-meta">
                                <span class="phase-badge ${phaseClass}">${phase}</span>
                                ${a.task ? '• ' + a.task.substring(0, 60) + (a.task.length > 60 ? '...' : '') : ''}
                            </div>
                        </li>`;
                    }).join('');
                } else {
                    autopilotsList.innerHTML = '<li class="team-item"><div class="team-name">No active autopilots</div></li>';
                }

                // Ralphs
                const ralphsList = document.getElementById('ralphs');
                if (ralphs.ralphs && ralphs.ralphs.length > 0) {
                    ralphsList.innerHTML = ralphs.ralphs.map(r => {
                        const progress = `${r.iteration || 0}/${r.max_iterations || 0}`;
                        return `<li class="team-item">
                            <div class="team-name">${r.task ? r.task.substring(0, 40) + (r.task.length > 40 ? '...' : '') : 'Unnamed'}</div>
                            <div class="team-meta">
                                <span class="phase-badge">${progress}</span>
                            </div>
                        </li>`;
                    }).join('');
                } else {
                    ralphsList.innerHTML = '<li class="team-item"><div class="team-name">No active Ralph sessions</div></li>';
                }

                // Metrics
                if (metrics.metrics) {
                    const m = metrics.metrics;
                    document.getElementById('m-team-runs').textContent = m.total_team_runs || m.total_spawns || 0;
                    document.getElementById('m-shutdowns').textContent = m.total_shutdowns || 0;
                    document.getElementById('m-tasks').textContent = m.total_tasks_created || 0;
                    document.getElementById('m-ask').textContent = m.total_ask_calls || 0;
                }

                // Health
                document.getElementById('h-status').textContent = health.status || 'unknown';
                document.getElementById('h-status').className = 'metric-value ' + (health.status === 'ok' ? 'status-ok' : 'status-err');
                document.getElementById('h-version').textContent = health.version || '—';
            } catch (e) {
                console.error('Failed to load data:', e);
            }
        }

        loadData();
        setInterval(loadData, 5000);
    </script>
</body>
</html>"#;