Files
openclaw/apps/ios/Sources/Design/AgentProTab+Usage.swift
funston-bot 19cbceeba3 fix(ios): show recent usage days first (#117222)
* fix(ios): show recent usage days first

Use the device calendar for daily usage buckets so late-evening and travel activity stays on the expected date.

Co-authored-by: Barnett Klane <barnett.klane@gmail.com>

* fix(ios): satisfy native validation

Keep the generated i18n inventory aligned and use the optional Data-to-String conversion required by the iOS lint policy.

Co-authored-by: Barnett Klane <barnett.klane@gmail.com>

* test(ios): publish native usage ordering proof

Add a deterministic 20-day Usage fixture and an exact-head XCUITest that verifies the newest fourteen rows, captures the corrected native screen, and publishes the PNG separately from App Store screenshots. Keep screenshot proof running after unrelated lifecycle-test failures and lock the artifact path into the Fastlane gate tests.

Co-authored-by: Barnett Klane <barnett.klane@gmail.com>

* fix(ci): serialize Swift tests on hosted forks

Keep the 12-vCPU Blacksmith path parallel while avoiding process and timing contention on hosted fork runners.\n\nCo-authored-by: Barnett Klane <barnett.klane@gmail.com>

* fix(ci): budget hosted serial Swift tests

* chore(ci): keep usage fix scoped to iOS

* test(ios): fail closed on missing usage proof

* test(ios): simplify usage proof scope

---------

Co-authored-by: Barnett Klane <barnett.klane@gmail.com>
2026-08-23 11:25:35 -07:00

94 lines
3.5 KiB
Swift

import OpenClawKit
import OpenClawProtocol
import SwiftUI
extension AgentProTab {
var usageTotalsCard: some View {
ProCard(radius: AgentLayout.cardRadius) {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Totals")
.font(OpenClawType.headline)
Spacer()
ProValuePill(
value: "\(self.overview?.usage?.days ?? 31)d",
color: OpenClawBrand.accentForeground)
}
HStack(spacing: 10) {
self.detailMetric(label: "Cost", value: self.usageValue)
self.detailMetric(label: "Tokens", value: self.usageTokenValue)
self.detailMetric(label: "Cache", value: self.usageCacheValue)
}
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
var usageTokenValue: String {
guard let tokens = self.overview?.usage?.totalTokens else { return "0" }
return Self.compactNumber(tokens)
}
var usageCacheValue: String {
guard let cacheStatus = self.normalized(self.overview?.usage?.cacheStatus?["status"]?.value as? String) else {
return "n/a"
}
return cacheStatus
}
var usageDailyList: some View {
VStack(alignment: .leading, spacing: 8) {
ProSectionHeader(title: "Daily")
ProCard(padding: 0, radius: AgentLayout.cardRadius) {
let days = Self.displayedUsageDays(self.overview?.usage?.daily ?? [])
if days.isEmpty {
self.emptyDetailRow(
icon: "chart.bar",
title: "No daily usage yet",
detail: "The gateway returned totals without daily session cost rows.")
.padding(14)
} else {
VStack(spacing: 0) {
ForEach(Array(days.enumerated()), id: \.element.date) { index, day in
self.usageDayRow(day)
if index < days.count - 1 {
Divider().padding(.leading, 60)
}
}
}
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
}
static func displayedUsageDays(_ days: [CostUsageDailyEntryLite]) -> [CostUsageDailyEntryLite] {
Array(days.suffix(14).reversed())
}
func usageDayRow(_ day: CostUsageDailyEntryLite) -> some View {
HStack(spacing: 12) {
ProIconBadge(systemName: "calendar", color: OpenClawBrand.accent)
VStack(alignment: .leading, spacing: 3) {
Text(day.date)
.font(OpenClawType.subheadSemiBold)
Text(verbatim: Self.tokenCountText(day.totalTokens ?? 0))
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
}
Spacer(minLength: 8)
Text(Self.currency(day.totalCost ?? 0))
.font(OpenClawType.caption2SemiBold)
.foregroundStyle(OpenClawBrand.accent)
}
.padding(.vertical, 10)
.padding(.horizontal, 14)
}
private static func tokenCountText(_ count: Int) -> String {
String(
format: String(localized: "%@ tokens"),
self.compactNumber(count))
}
}