#!/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 = """ Ralph Dashboard

Ralph Dashboard

Simple local view of who is working, what stage the swarm is in, and what happened last.

Overview

Loading...

Background Runner

Loading...

Provider Smoke

Loading...

Current Run

Loading...

Agent Status

Timeline

    Recent Runs

    """ 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()