mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
19cbceeba3
* 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>
94 lines
3.5 KiB
Swift
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))
|
|
}
|
|
}
|