use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use axum::{
Json, Router,
extract::State,
http::header,
response::{Html, IntoResponse, Response},
routing::{get, post},
};
use chrono::Utc;
use serde::Serialize;
use tokio::{net::TcpListener, sync::RwLock};
use crate::{
aggregate::build_summary,
models::UsageSummary,
provider::{LocalCodexProvider, UsageProvider},
};
#[derive(Clone)]
struct AppState {
provider: Arc<LocalCodexProvider>,
summary: Arc<RwLock<UsageSummary>>,
}
#[derive(Debug, Serialize)]
struct Health {
status: &'static str,
}
pub fn create_app(provider: LocalCodexProvider) -> Router {
let initial_summary = load_summary(&provider);
let state = AppState {
provider: Arc::new(provider),
summary: Arc::new(RwLock::new(initial_summary)),
};
Router::new()
.route("/", get(index))
.route("/number-format.js", get(number_format_js))
.route("/app.js", get(app_js))
.route("/styles.css", get(styles_css))
.route("/health", get(health))
.route("/api/summary", get(summary_route))
.route("/api/refresh", post(refresh))
.with_state(state)
}
pub async fn serve(codex_home: Option<PathBuf>, listen: String) -> Result<()> {
let provider = LocalCodexProvider::new(codex_home);
let app = create_app(provider);
let listener = TcpListener::bind(&listen).await?;
let address = listener.local_addr()?;
println!("Codex Cost dashboard: http://{address}");
axum::serve(listener, app).await?;
Ok(())
}
fn load_summary(provider: &LocalCodexProvider) -> UsageSummary {
let load = provider.load_events();
build_summary(load.events, load.warnings, load.session_count, Utc::now())
}
async fn index() -> Html<&'static str> {
Html(include_str!("../static/index.html"))
}
async fn app_js() -> Response {
(
[(
header::CONTENT_TYPE,
"application/javascript; charset=utf-8",
)],
include_str!("../static/app.js"),
)
.into_response()
}
async fn number_format_js() -> Response {
(
[(
header::CONTENT_TYPE,
"application/javascript; charset=utf-8",
)],
include_str!("../static/number-format.js"),
)
.into_response()
}
async fn styles_css() -> Response {
(
[(header::CONTENT_TYPE, "text/css; charset=utf-8")],
include_str!("../static/styles.css"),
)
.into_response()
}
async fn health() -> Json<Health> {
Json(Health { status: "ok" })
}
async fn summary_route(State(state): State<AppState>) -> Json<UsageSummary> {
Json(state.summary.read().await.clone())
}
async fn refresh(State(state): State<AppState>) -> Json<UsageSummary> {
let summary = load_summary(&state.provider);
*state.summary.write().await = summary.clone();
Json(summary)
}