v1.19: fix empty usage-history chart (always fetch utilization)
Build APK / build (push) Successful in 1m52s
Build APK / build (push) Successful in 1m52s
recordHistory() only ever stored fiveHourUtilization/weeklyUtilization, but fetchUsage() returned early with message-count data — before calling the /usage utilization endpoint — whenever the org JSON carried a message limit. So utilization was never populated and the history chart stayed stuck on 'Collecting history…'. The prior chart fix only corrected the throttle. - UsageRepository: always attempt /usage (preferred signal that drives the weekly bar + history); fall back to org message-count data only when utilization is unavailable. - UsageData.sessionReadingPct: utilization preferred, message-count % fallback, -1f when no reading — so message-only accounts also build history. - PreferencesManager.recordHistory: record the session line from sessionReadingPct instead of utilization-only. - UsageDataTest: cover sessionReadingPct.
This commit is contained in:
@@ -24,8 +24,8 @@ android {
|
||||
applicationId = "me.khodak.claudeusage"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 19
|
||||
versionName = "1.18"
|
||||
versionCode = 20
|
||||
versionName = "1.19"
|
||||
}
|
||||
|
||||
val releaseStorePassword = signingCred("KEYSTORE_PASSWORD", "storePassword")
|
||||
|
||||
@@ -54,17 +54,15 @@ class UsageRepository(private val prefs: PreferencesManager) {
|
||||
|
||||
if (orgId == null) return@withContext base.copy(errorMessage = "Can't reach claude.ai")
|
||||
|
||||
if (orgUsageData?.hasRateLimitData == true) {
|
||||
prefs.resetAuthFailCount()
|
||||
return@withContext base.copy(
|
||||
messagesUsed = orgUsageData.messagesUsed,
|
||||
messagesLimit = orgUsageData.messagesLimit,
|
||||
messagesRemaining = orgUsageData.messagesRemaining,
|
||||
resetAtEpoch = orgUsageData.resetAtEpoch
|
||||
)
|
||||
}
|
||||
// NOTE: message-count data from the org endpoint (orgUsageData) is a FALLBACK only — do
|
||||
// NOT return here. The /usage utilization endpoint below is the preferred signal and the
|
||||
// one the weekly bar + in-app history chart depend on, so it must be attempted whenever we
|
||||
// have an org id. Returning early here was why the history chart stayed empty: any account
|
||||
// whose org JSON carried a message limit short-circuited before /usage, so
|
||||
// fiveHourUtilization/weeklyUtilization never got populated and recordHistory() had nothing
|
||||
// to store. The org message data is used only if /usage yields nothing (fallback below).
|
||||
|
||||
// Step 2: /usage endpoint — returns utilization percentages
|
||||
// Step 2: /usage endpoint — returns utilization percentages (preferred signal)
|
||||
try {
|
||||
val usageUrl = "https://claude.ai/api/organizations/$orgId/usage"
|
||||
val resp = client.newCall(buildRequest(usageUrl, cookies)).execute()
|
||||
@@ -90,6 +88,18 @@ class UsageRepository(private val prefs: PreferencesManager) {
|
||||
if (BuildConfig.DEBUG) Log.w(TAG, "/usage failed: ${e.message}")
|
||||
}
|
||||
|
||||
// /usage gave no utilization → fall back to the message-count data the org endpoint
|
||||
// already returned (this is the early-return that used to live before Step 2).
|
||||
if (orgUsageData?.hasRateLimitData == true) {
|
||||
prefs.resetAuthFailCount()
|
||||
return@withContext base.copy(
|
||||
messagesUsed = orgUsageData.messagesUsed,
|
||||
messagesLimit = orgUsageData.messagesLimit,
|
||||
messagesRemaining = orgUsageData.messagesRemaining,
|
||||
resetAtEpoch = orgUsageData.resetAtEpoch
|
||||
)
|
||||
}
|
||||
|
||||
// Step 3: fallback endpoints (message-count style)
|
||||
val endpoints = listOf(
|
||||
"https://claude.ai/api/organizations/$orgId/rate_limit_status",
|
||||
|
||||
@@ -94,13 +94,16 @@ class PreferencesManager(context: Context) {
|
||||
// ── Usage history (for the in-app chart) ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* Append a history point if [data] carries a real utilization reading.
|
||||
* Append a history point if [data] carries a real reading.
|
||||
* The session line uses [UsageData.sessionReadingPct] (utilization preferred, message-count
|
||||
* progress as fallback) so accounts that only expose message counts still build history.
|
||||
* De-duplicates rapid double-fires (manual refresh + background worker landing
|
||||
* together) by skipping points within [MIN_HISTORY_GAP_MS] of the last one, and
|
||||
* prunes anything older than [HISTORY_RETENTION_MS] / beyond [MAX_HISTORY_POINTS].
|
||||
*/
|
||||
fun recordHistory(data: UsageData) {
|
||||
if (data.fiveHourUtilization < 0f && data.weeklyUtilization < 0f) return
|
||||
val sessionPct = data.sessionReadingPct
|
||||
if (sessionPct < 0f && data.weeklyUtilization < 0f) return
|
||||
val now = System.currentTimeMillis()
|
||||
val history = getHistory().toMutableList()
|
||||
// Throttle to at most one point per MIN_HISTORY_GAP_MS by SKIPPING a too-soon reading.
|
||||
@@ -111,7 +114,7 @@ class PreferencesManager(context: Context) {
|
||||
history.add(
|
||||
UsageSnapshot(
|
||||
epochMs = now,
|
||||
sessionPct = data.fiveHourUtilization,
|
||||
sessionPct = sessionPct,
|
||||
weeklyPct = data.weeklyUtilization
|
||||
)
|
||||
)
|
||||
|
||||
@@ -43,6 +43,19 @@ data class UsageData(
|
||||
|
||||
val weeklyActiveDays: Int get() = Integer.bitCount(weeklyActiveDaysMask)
|
||||
|
||||
/**
|
||||
* Session utilization as a 0-100 reading for the history chart, preferring the /usage
|
||||
* utilization endpoint and falling back to message-count progress. Returns -1f when there is
|
||||
* no usable reading — so message-only accounts (no utilization endpoint) still build history
|
||||
* instead of leaving the chart permanently on "Collecting history…".
|
||||
*/
|
||||
val sessionReadingPct: Float get() = when {
|
||||
fiveHourUtilization >= 0f -> fiveHourUtilization
|
||||
messagesLimit > 0 && effectiveUsed >= 0 ->
|
||||
((effectiveUsed.toFloat() / messagesLimit.toFloat()) * 100f).coerceIn(0f, 100f)
|
||||
else -> -1f
|
||||
}
|
||||
|
||||
// The best reset time we have (utilization endpoint is preferred)
|
||||
val effectiveResetEpoch: Long get() = when {
|
||||
utilizationResetAtEpoch > 0 -> utilizationResetAtEpoch
|
||||
|
||||
@@ -95,6 +95,32 @@ class UsageDataTest {
|
||||
assertFalse(UsageData().hasAnyReading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionReadingPctPrefersUtilization() {
|
||||
// Utilization present → used verbatim (kept as Float, not truncated), even alongside messages.
|
||||
val d = UsageData(fiveHourUtilization = 42.5f, messagesUsed = 10, messagesLimit = 100)
|
||||
assertEquals(42.5f, d.sessionReadingPct, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionReadingPctFallsBackToMessages() {
|
||||
// No utilization → derive from message counts so message-only accounts still get history.
|
||||
val d = UsageData(messagesUsed = 30, messagesLimit = 120)
|
||||
assertEquals(25f, d.sessionReadingPct, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionReadingPctMessagesCoercedTo100() {
|
||||
val d = UsageData(messagesUsed = 150, messagesLimit = 100)
|
||||
assertEquals(100f, d.sessionReadingPct, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sessionReadingPctNegativeWhenNoReading() {
|
||||
// No utilization and no message data → -1f, so recordHistory skips the point.
|
||||
assertEquals(-1f, UsageData().sessionReadingPct, 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mergedWithEmptyFetchKeepsPreviousMetrics() {
|
||||
// (a) previous has a reading; this fetch is empty (all defaults) → previous metrics kept.
|
||||
|
||||
Reference in New Issue
Block a user