diff --git a/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift b/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift index 970cd0b0..3e345d42 100644 --- a/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift +++ b/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift @@ -1522,7 +1522,7 @@ struct AccountProfileSummaryView: View { private var metrics: [(label: String, value: String)] { [ account.profileLifetimeTokens.map { ("tok", formatCompactCount($0)) }, - account.profilePeakDailyTokens.map { ("peak", formatCompactCount($0)) }, + account.profilePeakDailyTokensForDisplay.map { ("peak", formatCompactCount($0)) }, streakText.map { ("streak", $0) }, account.profileLongestTaskSeconds .flatMap(formatActivityDuration) diff --git a/apps/decodex-app/Sources/DecodexApp/Models.swift b/apps/decodex-app/Sources/DecodexApp/Models.swift index 6721cdf3..47d2fc6d 100644 --- a/apps/decodex-app/Sources/DecodexApp/Models.swift +++ b/apps/decodex-app/Sources/DecodexApp/Models.swift @@ -272,6 +272,10 @@ struct CodexAccount: Decodable, Identifiable, Equatable { profileDailyUsage ?? [] } + var profilePeakDailyTokensForDisplay: Int? { + profilePeakDailyTokens ?? recentProfileDailyUsage.map(\.tokens).max() + } + var isUsageLimited: Bool { if let reached = rateLimitReachedType, !reached.isEmpty { return true diff --git a/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift b/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift index 5617ae9b..13064eb1 100644 --- a/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift +++ b/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift @@ -42,6 +42,30 @@ final class AccountModelTests: XCTestCase { XCTAssertEqual(account.currentCapacityLabel, "20x") } + func testProfilePeakFallsBackToDailyUsageBuckets() { + let account = makeAccount( + status: "available", + profileDailyUsage: [ + AccountProfileDailyUsage(date: "2026-05-30", tokens: 123_456), + AccountProfileDailyUsage(date: "2026-05-31", tokens: 789_000), + ] + ) + + XCTAssertEqual(account.profilePeakDailyTokensForDisplay, 789_000) + } + + func testProfilePeakUsesExplicitStatsValueFirst() { + let account = makeAccount( + status: "available", + profilePeakDailyTokens: 1_500_000, + profileDailyUsage: [ + AccountProfileDailyUsage(date: "2026-05-30", tokens: 2_000_000), + ] + ) + + XCTAssertEqual(account.profilePeakDailyTokensForDisplay, 1_500_000) + } + func testCompactEmailKeepsDottedLocalSuffixesConsistent() { XCTAssertEqual(AccountDisplay.compactEmail("aurevoirxavier@gmail.com"), "aur...ier@gmail.com") XCTAssertEqual(AccountDisplay.compactEmail("aurevoirxavier.us@gmail.com"), "aur...us@gmail.com") @@ -219,7 +243,9 @@ final class AccountModelTests: XCTestCase { refreshStatus: String? = nil, planType: String? = nil, checkedAtUnixEpoch: Int? = nil, - primaryRemainingPercent: Int? = nil + primaryRemainingPercent: Int? = nil, + profilePeakDailyTokens: Int? = nil, + profileDailyUsage: [AccountProfileDailyUsage]? = nil ) -> CodexAccount { CodexAccount( accountFingerprint: accountFingerprint, @@ -256,11 +282,11 @@ final class AccountModelTests: XCTestCase { profileUsername: nil, profileCheckedAtUnixEpoch: nil, profileLifetimeTokens: nil, - profilePeakDailyTokens: nil, + profilePeakDailyTokens: profilePeakDailyTokens, profileLongestTaskSeconds: nil, profileCurrentStreakDays: nil, profileLongestStreakDays: nil, - profileDailyUsage: nil, + profileDailyUsage: profileDailyUsage, sevenDayUsedPercent: nil, sevenDayDailyAveragePercent: nil, usageRecords: nil diff --git a/apps/decodex/src/agent/codex_accounts.rs b/apps/decodex/src/agent/codex_accounts.rs index 909c7986..3dd7a77a 100644 --- a/apps/decodex/src/agent/codex_accounts.rs +++ b/apps/decodex/src/agent/codex_accounts.rs @@ -1516,13 +1516,15 @@ fn profile_snapshot_from_payload( .and_then(Value::as_array) .map(|items| items.iter().filter_map(profile_daily_usage_from_value).collect::>()) .unwrap_or_default(); + let peak_daily_tokens = stats + .and_then(|value| nonnegative_number_as_i64(value.get("peak_daily_tokens"))) + .or_else(|| daily_usage.iter().map(|record| record.tokens).max()); let snapshot = AccountProfileSnapshot { display_name: profile.and_then(|value| nonblank_json_string(value.get("display_name"))), username: profile.and_then(|value| nonblank_json_string(value.get("username"))), lifetime_tokens: stats .and_then(|value| nonnegative_number_as_i64(value.get("lifetime_tokens"))), - peak_daily_tokens: stats - .and_then(|value| nonnegative_number_as_i64(value.get("peak_daily_tokens"))), + peak_daily_tokens, longest_task_seconds: stats .and_then(|value| nonnegative_number_as_i64(value.get("longest_running_turn_sec"))), current_streak_days: stats @@ -1927,6 +1929,22 @@ mod tests { assert_eq!(summary.daily_usage[1].tokens, 789_000); } + #[test] + fn profile_summary_falls_back_to_daily_usage_peak() { + let payload = serde_json::json!({ + "stats": { + "daily_usage_buckets": [ + { "start_date": "2026-05-30", "tokens": 123_456 }, + { "start_date": "2026-05-31", "tokens": 789_000 } + ] + } + }); + let summary = codex_accounts::profile_snapshot_from_payload(&payload, 1_800_000_000) + .expect("profile summary should parse from daily usage buckets"); + + assert_eq!(summary.peak_daily_tokens, Some(789_000)); + } + #[test] fn usage_limit_detects_depleted_windows_without_credit_heuristics() { let payload = serde_json::json!({ diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index e068945a..307d9555 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -6594,6 +6594,18 @@

Run History

: []; } + function codexAccountProfilePeakDailyTokens(account) { + const explicitPeak = codexAccountNumber(account?.profile_peak_daily_tokens); + if (explicitPeak != null) { + return explicitPeak; + } + + return codexAccountProfileDailyUsage(account).reduce( + (peak, record) => Math.max(peak, record.tokens), + 0, + ) || null; + } + function previousUsageDate(value) { const match = String(value || "").match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!match) { @@ -6758,12 +6770,13 @@

Run History

? `${longestStreak}d` : ""; const task = formatCodexAccountProfileDuration(account.profile_longest_task_seconds); + const peakDailyTokens = codexAccountProfilePeakDailyTokens(account); const facts = [ codexAccountNumber(account.profile_lifetime_tokens) != null ? ["tok", formatCompactCount(account.profile_lifetime_tokens)] : null, - codexAccountNumber(account.profile_peak_daily_tokens) != null - ? ["peak", formatCompactCount(account.profile_peak_daily_tokens)] + peakDailyTokens != null + ? ["peak", formatCompactCount(peakDailyTokens)] : null, streak ? ["streak", streak] : null, task ? ["task", task] : null, @@ -6785,7 +6798,7 @@

Run History

if (lifetime != null) { lifetimeTokens = (lifetimeTokens || 0) + lifetime; } - const peak = codexAccountNumber(account?.profile_peak_daily_tokens); + const peak = codexAccountProfilePeakDailyTokens(account); if (peak != null) { peakTokensFallback = (peakTokensFallback || 0) + peak; } diff --git a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs index e1cc4d18..04a9149b 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs @@ -728,6 +728,7 @@ fn operator_dashboard_accounts_keeps_compact_table_layout() { assert!(response.contains("function codexAccountProfileAggregate(accounts)")); assert!(response.contains("function renderCodexAccountPoolActivityStrip(account")); assert!(response.contains("function renderCodexAccountProfileActivityStrip(account")); + assert!(response.contains("function codexAccountProfilePeakDailyTokens(account)")); assert!(response.contains("function renderCodexAccountProfileToggle(account, expanded)")); assert!(response.contains("function renderCodexAccountProfilePanel(account, snapshot, profileKey, expanded)")); assert!(response.contains("function toggleCodexAccountProfileKey(key)"));