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:
irisandClaude Fable 5 committed 2026-08-24 21:47:51 -04:00
1 parent f2430671a2
commit 3c97a5ef28
10 files changed
+606 -10

No files matched your search

+20 -4
View File
@@ -17,6 +17,7 @@ mod auth;
mod config;
mod routes;
mod session;
mod usage;
use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf};
@@ -119,6 +120,13 @@ fn print_enrollment(host: IpAddr, port: u16, token: &str) -> Result<()> {
#[tokio::main]
async fn main() -> Result<()> {
// Both rustls crypto providers are in the dependency graph (ureq
// brings ring, axum-server brings aws-lc-rs), so rustls refuses to
// pick one itself; choose before anything touches TLS.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("no other TLS crypto provider is installed before main");
tracing_subscriber::fmt().with_env_filter("info").init();
let args = Args::parse();
@@ -174,13 +182,21 @@ async fn main() -> Result<()> {
.await
.context("failed to load TLS cert/key")?;
let monitor = Arc::new(usage::UsageMonitor::new(vec![Box::new(usage::ClaudeUsage {
credentials_path: std::env::home_dir()
.unwrap_or_else(|| PathBuf::from("/"))
.join(".claude/.credentials.json"),
})]));
// The bearer-token middleware wraps the entire router -- routes and
// fallback alike -- here and only here, so a new route can't forget
// auth. Zero unauthenticated endpoints.
let app = routes::router(Arc::clone(&manager)).layer(axum::middleware::from_fn_with_state(
Arc::clone(&manager),
auth::require_token,
));
let app = routes::router(Arc::clone(&manager))
.merge(routes::usage_router(monitor))
.layer(axum::middleware::from_fn_with_state(
Arc::clone(&manager),
auth::require_token,
));
let addr = SocketAddr::new(bind_ip, args.port);
tracing::info!("serving https://{addr}");