ccs-proxy 0.1.1

Local logging reverse-proxy + dashboard for Claude Code / Codex traffic
Documentation
use crate::AppState;
use axum::response::IntoResponse;
use axum::{Json, Router, extract::State, routing::get};
use serde_json::{Value, json};

type RequestPathParams = axum::extract::Path<(String, u64)>;

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/api/health", get(health))
        .route("/api/sessions", get(list_sessions))
        .route("/api/sessions/{sid}", get(get_session))
        .route("/api/requests/{sid}/{seq}", get(get_request))
        .route("/api/stream", get(crate::api::stream::stream))
}

async fn health(State(state): State<AppState>) -> Json<Value> {
    let request_count = state
        .store
        .list_requests(state.session_id.as_str())
        .await
        .map(|requests| requests.len())
        .unwrap_or(0);
    let store_status =
        if let Some(fs) = state.store.as_any().downcast_ref::<crate::store::FsStore>() {
            if fs.consecutive_write_failures() >= 10 {
                "degraded"
            } else {
                "ok"
            }
        } else {
            "ok"
        };
    Json(json!({
        "status": "ok",
        "provider": state.provider.as_str(),
        "upstream": state.upstream.to_string(),
        "session_id": state.session_id.as_str(),
        "started_at": state.started_at,
        "request_count": request_count,
        "store": store_status,
    }))
}

async fn list_sessions(State(state): State<AppState>) -> Json<Value> {
    let metas = state.store.list_sessions().await.unwrap_or_default();
    Json(serde_json::to_value(metas).unwrap_or(Value::Null))
}

async fn get_session(
    State(state): State<AppState>,
    axum::extract::Path(sid): axum::extract::Path<String>,
) -> axum::response::Response {
    let metas = state.store.list_sessions().await.unwrap_or_default();
    let meta = metas.into_iter().find(|item| item.session_id == sid);
    let requests = state.store.list_requests(&sid).await.unwrap_or_default();
    match meta {
        Some(found) => Json(json!({"meta": found, "requests": requests})).into_response(),
        None => (axum::http::StatusCode::NOT_FOUND, "session not found").into_response(),
    }
}

async fn get_request(
    State(state): State<AppState>,
    axum::extract::Path((sid, seq)): RequestPathParams,
) -> axum::response::Response {
    match state.store.get_request(&sid, seq).await {
        Ok(Some(rec)) => match serde_json::to_value(&rec) {
            Ok(value) => Json(value).into_response(),
            Err(err) => {
                tracing::warn!(?err, "failed to serialize capture record");
                (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "store error").into_response()
            }
        },
        Ok(None) => (axum::http::StatusCode::NOT_FOUND, "request not found").into_response(),
        Err(err) => {
            tracing::warn!(?err, "store error");
            (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "store error").into_response()
        }
    }
}