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

@@ -195,6 +195,45 @@ fun fetchSessionFile(settings: ServerSettings, sessionId: String, name: String):
it.inputStream.readBytes()
}
// One rate-limit window, rendered as a labeled bar on the usage screen.
data class UsageWindow(
val label: String,
val percent: Double,
val resetsAt: String?,
val active: Boolean,
)
data class UsageSnapshot(
val provider: String,
val available: Boolean,
val windows: List<UsageWindow>,
val error: String?,
)
/** The backend caches; refreshing more often than its poll interval just re-reads the cache. */
fun fetchUsage(settings: ServerSettings): List<UsageSnapshot> =
requestFromServer(settings, "/usage", readTimeoutMs = 30000) { connection ->
val snapshots = JSONArray(connection.inputStream.bufferedReader().readText())
(0 until snapshots.length()).map { i ->
val snapshot = snapshots.getJSONObject(i)
val windows = snapshot.getJSONArray("windows")
UsageSnapshot(
provider = snapshot.getString("provider"),
available = snapshot.getBoolean("available"),
error = snapshot.optString("error").ifEmpty { null },
windows = (0 until windows.length()).map { j ->
val window = windows.getJSONObject(j)
UsageWindow(
label = window.getString("label"),
percent = window.getDouble("percent"),
resetsAt = window.optString("resetsAt").ifEmpty { null },
active = window.getBoolean("active"),
)
},
)
}
}
fun answerQuestion(settings: ServerSettings, sessionId: String, questionId: String, answer: String) {
requestFromServer(
settings,