Phase 3: usage screen
GET /usage serves the numbers behind Claude Code's /usage, read with the CLI's own stored OAuth credentials (nothing to configure). The endpoint is undocumented, so parsing is defensive -- the generic limits[] array becomes labeled window bars, unknown kinds surface under their raw name, and any failure degrades to an 'unavailable' snapshot with the reason. One UsageProvider per paid service behind a caching monitor that enforces the >=180s minimum poll regardless of phone refreshes; no background polling at all. ureq (rustls) does the outbound call, with the process-level CryptoProvider now chosen explicitly in main -- ureq brings ring while axum-server brings aws-lc-rs, and with both in the graph rustls refuses to guess. App: a Usage screen off the session list -- per-window bars colored by utilization with relative reset times. Verified live: 74%/26%/16% windows rendered against the real endpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
1 parent
f2430671a2
commit
3c97a5ef28
10 files changed
+606
-10
No files matched your search
+28
-2
@@ -14,10 +14,10 @@
|
||||
//! POST /sessions/{id}/attachments multipart image upload -> {id}, referenced by /message
|
||||
//! GET /sessions/{id}/files/{name} images the session produced or was sent
|
||||
//! DELETE /sessions/{id} kill process, delete transcript + files
|
||||
//! GET /usage cached usage windows per provider
|
||||
//! ```
|
||||
//!
|
||||
//! Later phases add: `GET /usage`, `GET|PUT /hosts` and `/models` -- see
|
||||
//! PLAN.md's table.
|
||||
//! Later phases add: `GET|PUT /hosts` and `/models` -- see PLAN.md's table.
|
||||
//!
|
||||
//! Everything here works purely in the common event model; nothing may
|
||||
//! branch on the session kind (that's what drivers are for).
|
||||
@@ -26,6 +26,8 @@ use std::convert::Infallible;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use axum::Router;
|
||||
use axum::extract::{Path as UrlPath, Query, State};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
@@ -69,6 +71,8 @@ enum ApiError {
|
||||
UnknownRoute,
|
||||
#[error("{0}")]
|
||||
BadRequest(String),
|
||||
#[error(transparent)]
|
||||
Internal(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
@@ -76,6 +80,12 @@ impl IntoResponse for ApiError {
|
||||
let status = match self {
|
||||
Self::UnknownSession(_) | Self::UnknownRoute => StatusCode::NOT_FOUND,
|
||||
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||
Self::Internal(err) => {
|
||||
// The only variant whose real cause isn't safe to hand
|
||||
// back verbatim, and the only one worth a log line.
|
||||
tracing::error!("{err:#}");
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
};
|
||||
(status, self.to_string()).into_response()
|
||||
}
|
||||
@@ -186,6 +196,22 @@ async fn interrupt(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// Separate router because its state is the usage monitor, not the
|
||||
/// session manager; merged (and auth-wrapped) with the rest in `main`.
|
||||
pub fn usage_router(monitor: Arc<crate::usage::UsageMonitor>) -> Router {
|
||||
Router::new().route("/usage", get(usage)).with_state(monitor)
|
||||
}
|
||||
|
||||
async fn usage(
|
||||
State(monitor): State<Arc<crate::usage::UsageMonitor>>,
|
||||
) -> Result<axum::Json<Vec<crate::usage::UsageSnapshot>>, ApiError> {
|
||||
// The fetch is blocking by design (see `usage`); off the workers.
|
||||
let snapshots = tokio::task::spawn_blocking(move || monitor.snapshots())
|
||||
.await
|
||||
.context("usage fetch panicked")?;
|
||||
Ok(axum::Json(snapshots))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModelRequest {
|
||||
model: String,
|
||||
|
||||
Reference in new issue
Block a user