Sync: Complete project state with all MEGA SPRINT V1-V3 features and Codex stubs
This commit is contained in:
491
ralph/gui/app.py
Normal file
491
ralph/gui/app.py
Normal file
@@ -0,0 +1,491 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal Ralph dashboard for localhost monitoring."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
RALPH_ROOT = Path(__file__).resolve().parents[1]
|
||||
STATE_DIR = RALPH_ROOT / "state"
|
||||
RUNS_DIR = RALPH_ROOT / "runs"
|
||||
|
||||
|
||||
def _read_json(path: Path, default: Any) -> Any:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _read_text(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _tail_jsonl(path: Path, limit: int = 80) -> List[Dict[str, Any]]:
|
||||
if not path.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
events: List[Dict[str, Any]] = []
|
||||
for line in lines[-limit:]:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
events.append(json.loads(line))
|
||||
except Exception:
|
||||
continue
|
||||
events.reverse()
|
||||
return events
|
||||
|
||||
|
||||
def _is_pid_running(pid: Any) -> bool:
|
||||
try:
|
||||
pid_value = int(pid)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if pid_value <= 0:
|
||||
return False
|
||||
|
||||
try:
|
||||
os.kill(pid_value, 0)
|
||||
except PermissionError:
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _iso_to_local(value: Any) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
try:
|
||||
return datetime.fromisoformat(str(value).replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _collect_recent_runs(limit: int = 8) -> List[Dict[str, Any]]:
|
||||
runs: List[Dict[str, Any]] = []
|
||||
if not RUNS_DIR.exists():
|
||||
return runs
|
||||
|
||||
for run_dir in sorted([p for p in RUNS_DIR.iterdir() if p.is_dir()], key=lambda item: item.name, reverse=True)[:limit]:
|
||||
summary_path = run_dir / "SUMMARY.md"
|
||||
final_status_path = run_dir / "final_status.txt"
|
||||
summary_text = _read_text(summary_path).strip()
|
||||
final_status_text = _read_text(final_status_path).strip()
|
||||
changes_exists = (run_dir / "CHANGES.md").exists()
|
||||
implementer_patch = (run_dir / "implementer.patch").exists()
|
||||
final_patch = (run_dir / "final.patch").exists()
|
||||
runs.append(
|
||||
{
|
||||
"run_id": run_dir.name,
|
||||
"path": str(run_dir),
|
||||
"updated_at": datetime.fromtimestamp(run_dir.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"summary_excerpt": "\n".join(summary_text.splitlines()[:10]),
|
||||
"final_status_excerpt": "\n".join(final_status_text.splitlines()[:8]),
|
||||
"changes_exists": changes_exists,
|
||||
"implementer_patch": implementer_patch,
|
||||
"final_patch": final_patch,
|
||||
}
|
||||
)
|
||||
return runs
|
||||
|
||||
|
||||
def build_dashboard() -> Dict[str, Any]:
|
||||
current_run = _read_json(STATE_DIR / "current_run.json", {})
|
||||
background = _read_json(STATE_DIR / "last_background_run.json", {})
|
||||
background["alive"] = _is_pid_running(background.get("pid"))
|
||||
|
||||
return {
|
||||
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"current_run": current_run,
|
||||
"background": background,
|
||||
"events": _tail_jsonl(STATE_DIR / "events.jsonl", limit=120),
|
||||
"recent_runs": _collect_recent_runs(),
|
||||
"provider_smoke": _read_json(STATE_DIR / "provider_smoke.json", {}),
|
||||
}
|
||||
|
||||
|
||||
HTML_TEMPLATE = """<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Ralph Dashboard</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f3efe5;
|
||||
--ink: #1a1a1a;
|
||||
--muted: #5f5a52;
|
||||
--card: #fffaf2;
|
||||
--line: #d8cfbf;
|
||||
--ok: #2a7f62;
|
||||
--warn: #a96814;
|
||||
--bad: #ad2e24;
|
||||
--run: #144c7d;
|
||||
--shadow: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
background: linear-gradient(180deg, #efe7d8 0%, var(--bg) 100%);
|
||||
color: var(--ink);
|
||||
}
|
||||
header {
|
||||
padding: 20px 24px;
|
||||
background: #17324d;
|
||||
color: #fff8eb;
|
||||
border-bottom: 4px solid #c98b2b;
|
||||
}
|
||||
header h1 { margin: 0 0 6px; font-size: 28px; }
|
||||
header p { margin: 0; color: #d7e4ef; }
|
||||
main { padding: 20px 24px 32px; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 8px 24px var(--shadow);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.muted { color: var(--muted); }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
background: #ece4d6;
|
||||
color: var(--ink);
|
||||
}
|
||||
.status-completed, .status-success, .status-running { color: var(--ok); }
|
||||
.status-failed, .status-error { color: var(--bad); }
|
||||
.status-pending, .status-warning { color: var(--warn); }
|
||||
.agents { display: grid; gap: 10px; }
|
||||
.agent {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #fffdf8;
|
||||
}
|
||||
.agent strong { display: block; margin-bottom: 4px; }
|
||||
.timeline {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
max-height: 540px;
|
||||
overflow: auto;
|
||||
}
|
||||
.timeline li {
|
||||
border-left: 4px solid #c98b2b;
|
||||
padding: 8px 10px 8px 12px;
|
||||
background: #fffdf8;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.run-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.run-item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: #fffdf8;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: #f6f1e8;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.empty {
|
||||
padding: 16px;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 10px;
|
||||
color: var(--muted);
|
||||
background: #faf6ef;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Ralph Dashboard</h1>
|
||||
<p>Simple local view of who is working, what stage the swarm is in, and what happened last.</p>
|
||||
</header>
|
||||
<main>
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Overview</h2>
|
||||
<div id="overview" class="muted">Loading...</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Background Runner</h2>
|
||||
<div id="background" class="muted">Loading...</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Provider Smoke</h2>
|
||||
<div id="providers" class="muted">Loading...</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Current Run</h2>
|
||||
<div id="current-run" class="muted">Loading...</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Agent Status</h2>
|
||||
<div id="agents" class="agents"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Timeline</h2>
|
||||
<ul id="timeline" class="timeline"></ul>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>Recent Runs</h2>
|
||||
<div id="recent-runs" class="run-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
function statusBadge(value) {
|
||||
const text = value || "unknown";
|
||||
const cls = "badge status-" + String(text).toLowerCase();
|
||||
return `<span class="${cls}">${text}</span>`;
|
||||
}
|
||||
|
||||
function renderAgent(name, data) {
|
||||
if (!data) return "";
|
||||
const started = data.started_at ? new Date(data.started_at).toLocaleString() : "";
|
||||
const finished = data.finished_at ? new Date(data.finished_at).toLocaleString() : "";
|
||||
const output = data.output_file ? `<div class="muted">${data.output_file}</div>` : "";
|
||||
return `
|
||||
<div class="agent">
|
||||
<strong>${name}</strong>
|
||||
<div>${statusBadge(data.status)}</div>
|
||||
${started ? `<div class="muted">Start: ${started}</div>` : ""}
|
||||
${finished ? `<div class="muted">End: ${finished}</div>` : ""}
|
||||
${output}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOverview(data) {
|
||||
const run = data.current_run || {};
|
||||
if (!run.run_id) {
|
||||
return `<div class="empty">No current run state has been written yet.</div>`;
|
||||
}
|
||||
return `
|
||||
<div><strong>Run ID:</strong> ${run.run_id}</div>
|
||||
<div><strong>Stage:</strong> ${statusBadge(run.stage)}</div>
|
||||
<div><strong>Status:</strong> ${statusBadge(run.status)}</div>
|
||||
<div><strong>Message:</strong> ${run.latest_message || "-"}</div>
|
||||
<div class="muted">Updated: ${data.generated_at}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderBackground(data) {
|
||||
const bg = data.background || {};
|
||||
if (!bg.pid) {
|
||||
return `<div class="empty">No background launch metadata found.</div>`;
|
||||
}
|
||||
return `
|
||||
<div><strong>PID:</strong> ${bg.pid}</div>
|
||||
<div><strong>Alive:</strong> ${statusBadge(bg.alive ? "running" : "stopped")}</div>
|
||||
<div><strong>Run label:</strong> ${bg.run_label || "-"}</div>
|
||||
<div class="muted">${bg.stdout_log || ""}</div>
|
||||
<div class="muted">${bg.stderr_log || ""}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderProviders(data) {
|
||||
const smoke = data.provider_smoke || {};
|
||||
const checks = smoke.checks || [];
|
||||
if (!checks.length) {
|
||||
return `<div class="empty">No provider smoke data found yet.</div>`;
|
||||
}
|
||||
return checks.map((item) => `
|
||||
<div class="agent">
|
||||
<strong>${item.provider || "provider"}</strong>
|
||||
<div>${statusBadge(item.status || "unknown")}</div>
|
||||
<div class="muted">${item.model || ""}</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function renderCurrentRun(data) {
|
||||
const run = data.current_run || {};
|
||||
if (!run.run_id) {
|
||||
return `<div class="empty">No active or recent run state.</div>`;
|
||||
}
|
||||
return `
|
||||
<div><strong>Run:</strong> ${run.run_id}</div>
|
||||
<div><strong>Task dir:</strong> ${run.task_directory || "-"}</div>
|
||||
<div><strong>Worktree:</strong> ${run.worktree || "-"}</div>
|
||||
<div><strong>Started:</strong> ${run.started_at ? new Date(run.started_at).toLocaleString() : "-"}</div>
|
||||
<div><strong>Finished:</strong> ${run.finished_at ? new Date(run.finished_at).toLocaleString() : "-"}</div>
|
||||
${(run.errors || []).length ? `<pre>${(run.errors || []).join("\\n")}</pre>` : ""}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAgents(data) {
|
||||
const run = data.current_run || {};
|
||||
const html = [];
|
||||
if (run.implementer) {
|
||||
html.push(renderAgent("Implementer: " + (run.implementer.name || "unknown"), run.implementer));
|
||||
}
|
||||
(run.reviewers || []).forEach((reviewer) => {
|
||||
html.push(renderAgent("Reviewer: " + (reviewer.name || "unknown"), reviewer));
|
||||
});
|
||||
if (run.codex_master) {
|
||||
html.push(renderAgent("Codex Master", run.codex_master));
|
||||
}
|
||||
if (run.fix_pass) {
|
||||
html.push(renderAgent("Fix Pass", run.fix_pass));
|
||||
}
|
||||
document.getElementById("agents").innerHTML = html.length ? html.join("") : `<div class="empty">No agent state yet.</div>`;
|
||||
}
|
||||
|
||||
function renderTimeline(data) {
|
||||
const events = data.events || [];
|
||||
const target = document.getElementById("timeline");
|
||||
if (!events.length) {
|
||||
target.innerHTML = `<li class="empty">No events yet.</li>`;
|
||||
return;
|
||||
}
|
||||
target.innerHTML = events.map((event) => `
|
||||
<li>
|
||||
<div><strong>${event.actor || "system"}</strong> ${statusBadge(event.status || "unknown")}</div>
|
||||
<div>${event.message || "-"}</div>
|
||||
<div class="muted">${event.stage || "-"} · ${new Date(event.timestamp).toLocaleString()}</div>
|
||||
</li>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function renderRecentRuns(data) {
|
||||
const runs = data.recent_runs || [];
|
||||
const target = document.getElementById("recent-runs");
|
||||
if (!runs.length) {
|
||||
target.innerHTML = `<div class="empty">No run folders found.</div>`;
|
||||
return;
|
||||
}
|
||||
target.innerHTML = runs.map((run) => `
|
||||
<div class="run-item">
|
||||
<div><strong>${run.run_id}</strong></div>
|
||||
<div class="muted">${run.updated_at}</div>
|
||||
<div>${run.changes_exists ? statusBadge("changes") : statusBadge("no changes")}</div>
|
||||
<div>${run.final_patch ? statusBadge("final patch") : statusBadge("no final patch")}</div>
|
||||
${run.final_status_excerpt ? `<pre>${run.final_status_excerpt}</pre>` : ""}
|
||||
${run.summary_excerpt ? `<pre>${run.summary_excerpt}</pre>` : ""}
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const response = await fetch("/api/dashboard", { cache: "no-store" });
|
||||
const data = await response.json();
|
||||
document.getElementById("overview").innerHTML = renderOverview(data);
|
||||
document.getElementById("background").innerHTML = renderBackground(data);
|
||||
document.getElementById("providers").innerHTML = renderProviders(data);
|
||||
document.getElementById("current-run").innerHTML = renderCurrentRun(data);
|
||||
renderAgents(data);
|
||||
renderTimeline(data);
|
||||
renderRecentRuns(data);
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class RalphHandler(BaseHTTPRequestHandler):
|
||||
def _send_json(self, payload: Dict[str, Any]) -> None:
|
||||
body = json.dumps(payload, indent=2).encode("utf-8")
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def _send_html(self, payload: str) -> None:
|
||||
body = payload.encode("utf-8")
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
route = urlparse(self.path).path
|
||||
if route == "/api/dashboard":
|
||||
self._send_json(build_dashboard())
|
||||
return
|
||||
if route == "/" or route == "/index.html":
|
||||
self._send_html(HTML_TEMPLATE)
|
||||
return
|
||||
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "Not found")
|
||||
|
||||
def log_message(self, fmt: str, *args: Any) -> None:
|
||||
return
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Run the Ralph localhost dashboard.")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)")
|
||||
parser.add_argument("--port", type=int, default=8765, help="Bind port (default: 8765)")
|
||||
args = parser.parse_args()
|
||||
|
||||
server = ThreadingHTTPServer((args.host, args.port), RalphHandler)
|
||||
print(f"Ralph dashboard listening on http://{args.host}:{args.port}")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user