diff --git a/crates/miyabi-cli/src/main.rs b/crates/miyabi-cli/src/main.rs index a4dab59..9f82b9d 100644 --- a/crates/miyabi-cli/src/main.rs +++ b/crates/miyabi-cli/src/main.rs @@ -4,6 +4,8 @@ use chrono::Duration as ChronoDuration; use clap::{Parser, Subcommand, ValueEnum}; use miyabi_core::{FeatureFlagManager, RulesLoader}; use std::collections::HashMap; +use std::io::{self, BufRead, BufReader, Write}; +use std::net::{TcpListener, TcpStream}; use std::path::PathBuf; use tracing_subscriber::EnvFilter; @@ -214,6 +216,12 @@ enum GateCommand { Dag, /// Show dispatchable tasks Dispatchable, + /// Serve a minimal web dashboard + Serve { + /// Port to bind the dashboard to + #[arg(long, default_value_t = 4848)] + port: u16, + }, /// Analyze recent event logs and extract learnings Dream { /// Analyze only recent events, e.g. 24h, 30m, 7d @@ -1368,6 +1376,10 @@ fn handle_gate_command( } } }), + GateCommand::Serve { port } => { + serve_dashboard(store_path, port)?; + Ok(()) + } GateCommand::Dream { since, auto } => { let since = since .as_deref() @@ -1516,6 +1528,377 @@ fn emit_gate_error(format: &OutputFormat, kind: &str, message: &str) { } } +const POLARIS_DASHBOARD_HTML: &str = r##" + + + + + Polaris Dashboard + + + +
+
+

Polaris Dashboard

+

Deterministic Task Protocol live view

+

Loading...

+
+
+
+

Tasks

+
  • Loading tasks...
+
+
+

DAG Levels

+
  • Loading DAG...
+
+
+

File Locks

+
  • Loading locks...
+
+
+
+ + + +"##; + +fn serve_dashboard(store_path: &std::path::Path, port: u16) -> anyhow::Result<()> { + let protocol = miyabi_core::protocol::DeterministicExecutionProtocol::from_store_path( + store_path.to_path_buf(), + ); + let listener = TcpListener::bind(("127.0.0.1", port))?; + println!("Polaris Dashboard listening on http://127.0.0.1:{port}"); + + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + if let Err(error) = handle_dashboard_connection(&protocol, &mut stream) { + eprintln!("dashboard request error: {error}"); + } + } + Err(error) => eprintln!("dashboard accept error: {error}"), + } + } + + Ok(()) +} + +fn handle_dashboard_connection( + protocol: &miyabi_core::protocol::DeterministicExecutionProtocol, + stream: &mut TcpStream, +) -> anyhow::Result<()> { + let mut request_line = String::new(); + let mut reader = BufReader::new(stream.try_clone()?); + reader.read_line(&mut request_line)?; + + let mut parts = request_line.split_whitespace(); + let method = parts.next().unwrap_or_default(); + let path = parts.next().unwrap_or("/"); + + if method != "GET" { + write_http_response( + stream, + "405 Method Not Allowed", + "text/plain; charset=utf-8", + b"method not allowed", + )?; + return Ok(()); + } + + match path { + "/" => write_http_response( + stream, + "200 OK", + "text/html; charset=utf-8", + POLARIS_DASHBOARD_HTML.as_bytes(), + )?, + "/api/status" => { + let body = + serde_json::to_vec_pretty(&protocol.status(None)?).map_err(io::Error::other)?; + write_http_response(stream, "200 OK", "application/json; charset=utf-8", &body)?; + } + "/api/locks" => { + let body = serde_json::to_vec_pretty(&protocol.locks()?).map_err(io::Error::other)?; + write_http_response(stream, "200 OK", "application/json; charset=utf-8", &body)?; + } + "/api/dag" => { + let body = serde_json::to_vec_pretty(&protocol.dag()?).map_err(io::Error::other)?; + write_http_response(stream, "200 OK", "application/json; charset=utf-8", &body)?; + } + _ => write_http_response( + stream, + "404 Not Found", + "text/plain; charset=utf-8", + b"not found", + )?, + } + + Ok(()) +} + +fn write_http_response( + stream: &mut TcpStream, + status: &str, + content_type: &str, + body: &[u8], +) -> io::Result<()> { + write!( + stream, + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + )?; + stream.write_all(body)?; + stream.flush() +} + fn derive_task_id(issue: u64, title: &str) -> String { if issue > 0 { return format!("issue-{issue}");