Skip to main content

codex_cost/
server.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use anyhow::Result;
5use axum::{
6    Json, Router,
7    extract::State,
8    http::header,
9    response::{Html, IntoResponse, Response},
10    routing::{get, post},
11};
12use chrono::Utc;
13use serde::Serialize;
14use tokio::{net::TcpListener, sync::RwLock};
15
16use crate::{
17    aggregate::build_summary,
18    models::UsageSummary,
19    provider::{LocalCodexProvider, UsageProvider},
20};
21
22#[derive(Clone)]
23struct AppState {
24    provider: Arc<LocalCodexProvider>,
25    summary: Arc<RwLock<UsageSummary>>,
26}
27
28#[derive(Debug, Serialize)]
29struct Health {
30    status: &'static str,
31}
32
33pub fn create_app(provider: LocalCodexProvider) -> Router {
34    let initial_summary = load_summary(&provider);
35    let state = AppState {
36        provider: Arc::new(provider),
37        summary: Arc::new(RwLock::new(initial_summary)),
38    };
39
40    Router::new()
41        .route("/", get(index))
42        .route("/number-format.js", get(number_format_js))
43        .route("/app.js", get(app_js))
44        .route("/styles.css", get(styles_css))
45        .route("/health", get(health))
46        .route("/api/summary", get(summary_route))
47        .route("/api/refresh", post(refresh))
48        .with_state(state)
49}
50
51pub async fn serve(codex_home: Option<PathBuf>, listen: String) -> Result<()> {
52    let provider = LocalCodexProvider::new(codex_home);
53    let app = create_app(provider);
54    let listener = TcpListener::bind(&listen).await?;
55    let address = listener.local_addr()?;
56    println!("Codex Cost dashboard: http://{address}");
57    axum::serve(listener, app).await?;
58    Ok(())
59}
60
61fn load_summary(provider: &LocalCodexProvider) -> UsageSummary {
62    let load = provider.load_events();
63    build_summary(load.events, load.warnings, load.session_count, Utc::now())
64}
65
66async fn index() -> Html<&'static str> {
67    Html(include_str!("../static/index.html"))
68}
69
70async fn app_js() -> Response {
71    (
72        [(
73            header::CONTENT_TYPE,
74            "application/javascript; charset=utf-8",
75        )],
76        include_str!("../static/app.js"),
77    )
78        .into_response()
79}
80
81async fn number_format_js() -> Response {
82    (
83        [(
84            header::CONTENT_TYPE,
85            "application/javascript; charset=utf-8",
86        )],
87        include_str!("../static/number-format.js"),
88    )
89        .into_response()
90}
91
92async fn styles_css() -> Response {
93    (
94        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
95        include_str!("../static/styles.css"),
96    )
97        .into_response()
98}
99
100async fn health() -> Json<Health> {
101    Json(Health { status: "ok" })
102}
103
104async fn summary_route(State(state): State<AppState>) -> Json<UsageSummary> {
105    Json(state.summary.read().await.clone())
106}
107
108async fn refresh(State(state): State<AppState>) -> Json<UsageSummary> {
109    let summary = load_summary(&state.provider);
110    *state.summary.write().await = summary.clone();
111    Json(summary)
112}