mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-13 11:09:18 +00:00
* Fix per-provider data loss, division-by-zero, and decode fragility - Per-provider multi-day queries only merged cost/calls from cache, dropping categories/models/sessions/tokens. Remove broken cache shortcut and always do full parse for per-provider periods. - Remove per-provider daily history double-counting from overlapping cache + live data. - Guard maxCost against zero in ActivitySection and ModelsSection to prevent NaN in bar width calculations. - Use offset-based ForEach ID in BarTooltipCard to avoid duplicate model name collisions. - Make cacheHitPercent, topActivities, topModels, providers use decodeIfPresent for backward compat with older CLI versions. - Skip currency switch when FX rate fetch fails with no cache, preventing rate/symbol desync. - Use readSessionFile in Gemini parser for 128MB size cap. - Truncate Codex userMessage to 500 chars like other providers. * Restore cache-backed trend history for provider-filtered views The previous commit removed the broken per-provider cache shortcut but also dropped cache-backed daily history, causing provider-filtered views to lose trend data outside the selected period range. Use allCacheDays for historical days (cost/calls per provider is accurate in cache) and today's entry from the full parse. No overlap since cache ends at yesterday.
87 lines
2.8 KiB
Swift
87 lines
2.8 KiB
Swift
import SwiftUI
|
|
|
|
struct ActivitySection: View {
|
|
@Environment(AppStore.self) private var store
|
|
@State private var isExpanded: Bool = true
|
|
|
|
var body: some View {
|
|
CollapsibleSection(
|
|
caption: "Activity",
|
|
isExpanded: $isExpanded,
|
|
trailing: {
|
|
HStack(spacing: 8) {
|
|
Text("Cost").frame(minWidth: 54, alignment: .trailing)
|
|
Text("Turns").frame(minWidth: 52, alignment: .trailing)
|
|
Text("1-shot").frame(minWidth: 44, alignment: .trailing)
|
|
}
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundStyle(.tertiary)
|
|
.tracking(-0.05)
|
|
}
|
|
) {
|
|
VStack(alignment: .leading, spacing: 7) {
|
|
let maxCost = max(store.payload.current.topActivities.map(\.cost).max() ?? 1, 0.01)
|
|
ForEach(store.payload.current.topActivities, id: \.name) { activity in
|
|
ActivityRow(activity: activity, maxCost: maxCost)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ActivityRow: View {
|
|
let activity: ActivityEntry
|
|
let maxCost: Double
|
|
|
|
var body: some View {
|
|
HStack(spacing: 8) {
|
|
FixedBar(fraction: activity.cost / maxCost)
|
|
.frame(width: 56, height: 6)
|
|
|
|
Text(activity.name)
|
|
.font(.system(size: 12.5, weight: .medium))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
Text(activity.cost.asCompactCurrency())
|
|
.font(.codeMono(size: 12, weight: .medium))
|
|
.tracking(-0.2)
|
|
.frame(minWidth: 54, alignment: .trailing)
|
|
|
|
Text("\(activity.turns)")
|
|
.font(.system(size: 11))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.secondary)
|
|
.frame(minWidth: 52, alignment: .trailing)
|
|
|
|
Text(oneShotText)
|
|
.font(.system(size: 10.5))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.secondary)
|
|
.frame(minWidth: 44, alignment: .trailing)
|
|
}
|
|
.padding(.horizontal, 2)
|
|
.padding(.vertical, 1)
|
|
}
|
|
|
|
private var oneShotText: String {
|
|
guard let rate = activity.oneShotRate else { return "—" }
|
|
return "\(Int(rate * 100))%"
|
|
}
|
|
}
|
|
|
|
/// Fixed-width horizontal bar that shows a fill fraction.
|
|
struct FixedBar: View {
|
|
let fraction: Double
|
|
|
|
var body: some View {
|
|
GeometryReader { geo in
|
|
ZStack(alignment: .leading) {
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(.secondary.opacity(0.15))
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(Theme.brandAccent)
|
|
.frame(width: max(0, min(geo.size.width, geo.size.width * CGFloat(fraction))))
|
|
}
|
|
}
|
|
}
|
|
}
|