use anyhow::Result;
use axum::extract::{Path, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{Html, IntoResponse, Response};
use axum::routing::get;
use axum::{Json, Router};
use serde_json::{json, Value};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
const PAGE: &str = include_str!("local_dashboard.html");
#[derive(Clone)]
struct Ctx {
token: Arc<String>,
port: u16,
db_path: Arc<PathBuf>,
}
fn allowed(ctx: &Ctx, token: &str, headers: &HeaderMap) -> Result<(), StatusCode> {
let host = headers
.get("host")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
if host != format!("127.0.0.1:{}", ctx.port) && host != format!("localhost:{}", ctx.port) {
return Err(StatusCode::FORBIDDEN);
}
if token != ctx.token.as_str() {
return Err(StatusCode::NOT_FOUND);
}
Ok(())
}
async fn page(State(ctx): State<Ctx>, Path(token): Path<String>, headers: HeaderMap) -> Response {
match allowed(&ctx, &token, &headers) {
Ok(()) => Html(PAGE).into_response(),
Err(code) => code.into_response(),
}
}
async fn summary(
State(ctx): State<Ctx>,
Path(token): Path<String>,
headers: HeaderMap,
) -> Response {
if let Err(code) = allowed(&ctx, &token, &headers) {
return code.into_response();
}
match summary_json(&ctx.db_path) {
Ok(v) => Json(v).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
fn summary_json(db_path: &PathBuf) -> Result<Value> {
let store = forge::tracker::store::ExecutionStore::open(db_path)?;
let (before, after, count) = store.totals()?;
let saved = before.saturating_sub(after);
let pct = if before == 0 {
0.0
} else {
saved as f64 * 100.0 / before as f64
};
let by_day: Vec<Value> = store
.by_day(30)?
.into_iter()
.map(|(day, b, a)| json!({ "day": day, "tokens_before": b, "tokens_after": a }))
.collect();
let by_program: Vec<Value> = store
.summary()?
.into_iter()
.map(|(p, b, a, avg)| json!({ "program": p, "tokens_before": b, "tokens_after": a, "avg_pct": avg }))
.collect();
let recent: Vec<Value> = store
.recent(50)?
.into_iter()
.map(|r| {
json!({
"program": r.program, "args": r.args,
"tokens_before": r.tokens_before, "tokens_after": r.tokens_after,
"exit_code": r.exit_code, "duration_ms": r.duration_ms, "timestamp": r.timestamp,
})
})
.collect();
Ok(json!({
"totals": { "commands": count, "tokens_before": before, "tokens_after": after, "saved": saved, "saved_pct": pct },
"by_day": by_day,
"by_program": by_program,
"recent": recent,
}))
}
pub fn serve(port: u16, open_browser: bool) -> Result<()> {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_IGN);
}
let db_path = PathBuf::from(crate::commands::home_dir())
.join(".bctx")
.join("executions.db");
let token = format!(
"{}{}",
uuid::Uuid::new_v4().simple(),
uuid::Uuid::new_v4().simple()
);
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()?;
rt.block_on(async move {
let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await?;
let port = listener.local_addr()?.port();
let ctx = Ctx {
token: Arc::new(token.clone()),
port,
db_path: Arc::new(db_path),
};
let app = Router::new()
.route("/:token/", get(page))
.route("/:token/api/summary", get(summary))
.with_state(ctx);
let url = format!("http://127.0.0.1:{port}/{token}/");
let mut out = std::io::stdout();
let _ = writeln!(out, "bctx dashboard: {url}");
let _ = writeln!(out, "Served to this machine only. Ctrl-C to stop.");
let _ = out.flush();
if open_browser {
let _ = open::that(&url);
}
axum::serve(listener, app).await?;
Ok::<(), anyhow::Error>(())
})
}