Skip to content
This repository was archived by the owner on Nov 16, 2025. It is now read-only.

Commit fc843ee

Browse files
committed
fix: resolve build error in ClaudeUsageReportSubviews
Replace refreshData() calls with direct dataLoader.loadData(forceRefresh: true) calls to fix compilation error where refreshData() was not accessible from the extension.
1 parent 43d744b commit fc843ee

File tree

2 files changed

+179
-105
lines changed

2 files changed

+179
-105
lines changed

VibeMeter/Presentation/Views/ClaudeUsageReportSubviews.swift

Lines changed: 20 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ extension ClaudeUsageReportView {
6262
}
6363
.width(min: 80, ideal: 100)
6464
} rows: {
65-
ForEach(sortedSummaries) { summary in
65+
ForEach(cachedSummaries) { summary in
6666
TableRow(summary)
6767
}
6868
}
@@ -135,7 +135,7 @@ extension ClaudeUsageReportView {
135135
}
136136
.width(min: 100, ideal: 120)
137137
} rows: {
138-
ForEach(sortedProjectSummaries) { summary in
138+
ForEach(cachedProjectSummaries) { summary in
139139
TableRow(summary)
140140
}
141141
}
@@ -249,7 +249,7 @@ extension ClaudeUsageReportView {
249249
.frame(maxWidth: 300)
250250

251251
Button("Retry") {
252-
refreshData()
252+
dataLoader.loadData(forceRefresh: true)
253253
}
254254
.buttonStyle(.borderedProminent)
255255
Spacer()
@@ -307,92 +307,38 @@ extension ClaudeUsageReportView {
307307
// MARK: - Data Processing
308308

309309
extension ClaudeUsageReportView {
310-
var filteredDailyUsage: [Date: [ClaudeLogEntry]] {
311-
guard selectedProject != "All Projects" else {
312-
return dataLoader.dailyUsage
313-
}
314-
315-
var filtered: [Date: [ClaudeLogEntry]] = [:]
316-
for (date, entries) in dataLoader.dailyUsage {
317-
let projectEntries = entries.filter { $0.projectName == selectedProject }
318-
if !projectEntries.isEmpty {
319-
filtered[date] = projectEntries
320-
}
321-
}
322-
return filtered
323-
}
324-
325-
var sortedDays: [Date] {
326-
filteredDailyUsage.keys.sorted(by: >)
327-
}
328-
329-
var summaries: [DailyUsageSummary] {
330-
sortedDays.compactMap { date in
331-
guard let entries = filteredDailyUsage[date] else { return nil }
332-
return DailyUsageSummary(date: date, entries: entries, costStrategy: selectedCostStrategy)
333-
}
334-
}
335-
336-
var sortedSummaries: [DailyUsageSummary] {
337-
summaries.sorted(using: sortOrder)
338-
}
339-
340-
// Project summaries for "By Project" view
341-
var projectSummaries: [ProjectUsageSummary] {
342-
// Filter entries by date range
343-
let calendar = Calendar.current
344-
let startOfDay = calendar.startOfDay(for: dateRangeStart)
345-
let endOfDay = calendar.startOfDay(for: dateRangeEnd).addingTimeInterval(24 * 60 * 60)
346-
347-
let filteredEntries = dataLoader.dailyUsage.flatMap { date, entries -> [ClaudeLogEntry] in
348-
guard date >= startOfDay, date < endOfDay else { return [] }
349-
return entries
350-
}
351-
352-
// Group by project
353-
let entriesByProject = Dictionary(grouping: filteredEntries) { entry in
354-
entry.projectName ?? "Unknown"
355-
}
356-
357-
// Create summaries
358-
return entriesByProject.map { projectName, entries in
359-
ProjectUsageSummary(projectName: projectName, entries: entries, costStrategy: selectedCostStrategy)
360-
}
361-
}
362-
363-
var sortedProjectSummaries: [ProjectUsageSummary] {
364-
projectSummaries.sorted(using: projectSortOrder)
365-
}
310+
// Note: Data processing has been moved to the main view file
311+
// to support proper debouncing and caching of computed values
366312

367313
var totalInputTokens: Int {
368314
if viewMode == .daily {
369-
filteredDailyUsage.values.flatMap(\.self).reduce(0) { $0 + $1.inputTokens }
315+
cachedSummaries.reduce(0) { $0 + $1.inputTokens }
370316
} else {
371-
projectSummaries.reduce(0) { $0 + $1.inputTokens }
317+
cachedProjectSummaries.reduce(0) { $0 + $1.inputTokens }
372318
}
373319
}
374320

375321
var totalOutputTokens: Int {
376322
if viewMode == .daily {
377-
filteredDailyUsage.values.flatMap(\.self).reduce(0) { $0 + $1.outputTokens }
323+
cachedSummaries.reduce(0) { $0 + $1.outputTokens }
378324
} else {
379-
projectSummaries.reduce(0) { $0 + $1.outputTokens }
325+
cachedProjectSummaries.reduce(0) { $0 + $1.outputTokens }
380326
}
381327
}
382328

383329
var totalCacheCreationTokens: Int {
384330
if viewMode == .daily {
385-
filteredDailyUsage.values.flatMap(\.self).reduce(0) { $0 + ($1.cacheCreationTokens ?? 0) }
331+
cachedSummaries.reduce(0) { $0 + $1.cacheCreationTokens }
386332
} else {
387-
projectSummaries.reduce(0) { $0 + $1.cacheCreationTokens }
333+
cachedProjectSummaries.reduce(0) { $0 + $1.cacheCreationTokens }
388334
}
389335
}
390336

391337
var totalCacheReadTokens: Int {
392338
if viewMode == .daily {
393-
filteredDailyUsage.values.flatMap(\.self).reduce(0) { $0 + ($1.cacheReadTokens ?? 0) }
339+
cachedSummaries.reduce(0) { $0 + $1.cacheReadTokens }
394340
} else {
395-
projectSummaries.reduce(0) { $0 + $1.cacheReadTokens }
341+
cachedProjectSummaries.reduce(0) { $0 + $1.cacheReadTokens }
396342
}
397343
}
398344

@@ -402,11 +348,9 @@ extension ClaudeUsageReportView {
402348

403349
var totalCost: Double {
404350
if viewMode == .daily {
405-
// Calculate costs based on the selected strategy
406-
filteredDailyUsage.values.flatMap(\.self)
407-
.reduce(0) { $0 + $1.calculateCost(strategy: selectedCostStrategy) }
351+
cachedSummaries.reduce(0) { $0 + $1.cost }
408352
} else {
409-
projectSummaries.reduce(0) { $0 + $1.cost }
353+
cachedProjectSummaries.reduce(0) { $0 + $1.cost }
410354
}
411355
}
412356
}
@@ -429,11 +373,11 @@ extension ClaudeUsageReportView {
429373
.frame(width: 180)
430374

431375
// Project filter (only in By Day mode)
432-
if viewMode == .daily, !dataLoader.availableProjects.isEmpty {
376+
if viewMode == .daily, !availableProjects.isEmpty {
433377
Picker(selection: $selectedProject) {
434378
Text("All Projects").tag("All Projects")
435379
Divider()
436-
ForEach(dataLoader.availableProjects, id: \.self) { project in
380+
ForEach(availableProjects, id: \.self) { project in
437381
Text(project).tag(project)
438382
}
439383
} label: {
@@ -482,7 +426,9 @@ extension ClaudeUsageReportView {
482426
.frame(width: 250)
483427

484428
// Refresh button
485-
Button(action: refreshData) {
429+
Button {
430+
dataLoader.loadData(forceRefresh: true)
431+
} label: {
486432
Label("Refresh", systemImage: "arrow.clockwise")
487433
}
488434
.help("Refresh usage data")

VibeMeter/Presentation/Views/ClaudeUsageReportView.swift

Lines changed: 159 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,48 +24,104 @@ struct ClaudeUsageReportView: View {
2424
// Debounced state for progress updates
2525
@DebouncedState(duration: .milliseconds(300)) private var debouncedFilesProcessed = 0
2626
@DebouncedState(duration: .milliseconds(300)) private var debouncedLoadingMessage = ""
27+
28+
// Debounced state for table data to prevent excessive updates
29+
@DebouncedState(duration: .milliseconds(500)) private var debouncedDailyUsage: [Date: [ClaudeLogEntry]] = [:]
30+
@DebouncedState(duration: .milliseconds(500)) private var debouncedDataVersion = 0
31+
@State var cachedSummaries: [DailyUsageSummary] = []
32+
@State var cachedProjectSummaries: [ProjectUsageSummary] = []
33+
@State var availableProjects: [String] = []
2734

2835
@State var animationTrigger = false
2936

3037
// MARK: - View
3138

3239
var body: some View {
40+
mainContent
41+
.frame(maxWidth: .infinity, maxHeight: .infinity)
42+
.onAppear {
43+
handleOnAppear()
44+
}
45+
.toolbar {
46+
ToolbarItemGroup(placement: .automatic) {
47+
toolbarAutomaticItems
48+
}
49+
ToolbarItemGroup(placement: .primaryAction) {
50+
toolbarPrimaryItems
51+
}
52+
}
53+
}
54+
55+
@ViewBuilder
56+
private var mainContent: some View {
57+
baseContent
58+
.onChange(of: dataLoader.filesProcessed) { _, newValue in
59+
debouncedFilesProcessed = newValue
60+
}
61+
.onChange(of: dataLoader.loadingMessage) { _, newValue in
62+
debouncedLoadingMessage = newValue
63+
}
64+
.onChange(of: dataLoader.dailyUsage.count) { _, _ in
65+
// When data changes, update debounced data and increment version
66+
debouncedDailyUsage = dataLoader.dailyUsage
67+
debouncedDataVersion += 1
68+
}
69+
.onChange(of: debouncedDataVersion) { _, _ in
70+
updateCachedSummaries()
71+
}
72+
.onChange(of: sortOrder) { _, _ in
73+
cachedSummaries = cachedSummaries.sorted(using: sortOrder)
74+
}
75+
.onChange(of: projectSortOrder) { _, _ in
76+
cachedProjectSummaries = cachedProjectSummaries.sorted(using: projectSortOrder)
77+
}
78+
.onChange(of: selectedProject) { _, _ in
79+
updateCachedSummaries()
80+
}
81+
.onChange(of: dateRangeStart) { _, _ in
82+
if viewMode == .project {
83+
cachedProjectSummaries = computeProjectSummaries()
84+
}
85+
}
86+
.onChange(of: dateRangeEnd) { _, _ in
87+
if viewMode == .project {
88+
cachedProjectSummaries = computeProjectSummaries()
89+
}
90+
}
91+
.onChange(of: selectedCostStrategy) { _, _ in
92+
updateCachedSummaries()
93+
}
94+
.onChange(of: viewMode) { _, _ in
95+
updateCachedSummaries()
96+
}
97+
}
98+
99+
@ViewBuilder
100+
private var baseContent: some View {
33101
Group {
34102
if let error = dataLoader.errorMessage {
35103
errorView(error: error)
36104
} else {
37-
VStack(spacing: 0) {
38-
if dataLoader.isLoading, dataLoader.dailyUsage.isEmpty {
39-
// Initial loading state
40-
initialLoadingView
41-
} else {
42-
headerSection
43-
Divider()
44-
if dataLoader.isLoading {
45-
progressSection
46-
Divider()
47-
}
48-
contentSection
49-
}
50-
}
105+
dataContentView
51106
}
52107
}
53-
.frame(maxWidth: .infinity, maxHeight: .infinity)
54-
.onChange(of: dataLoader.filesProcessed) { _, newValue in
55-
debouncedFilesProcessed = newValue
56-
}
57-
.onChange(of: dataLoader.loadingMessage) { _, newValue in
58-
debouncedLoadingMessage = newValue
59-
}
60-
.onAppear {
61-
handleOnAppear()
62-
}
63-
.toolbar {
64-
ToolbarItemGroup(placement: .automatic) {
65-
toolbarAutomaticItems
66-
}
67-
ToolbarItemGroup(placement: .primaryAction) {
68-
toolbarPrimaryItems
108+
}
109+
110+
111+
@ViewBuilder
112+
private var dataContentView: some View {
113+
VStack(spacing: 0) {
114+
if dataLoader.isLoading, dataLoader.dailyUsage.isEmpty {
115+
// Initial loading state
116+
initialLoadingView
117+
} else {
118+
headerSection
119+
Divider()
120+
if dataLoader.isLoading {
121+
progressSection
122+
Divider()
123+
}
124+
contentSection
69125
}
70126
}
71127
}
@@ -136,14 +192,86 @@ struct ClaudeUsageReportView: View {
136192
private func handleOnAppear() {
137193
// Initialize cost strategy from settings
138194
selectedCostStrategy = settingsManager.displaySettingsManager.costCalculationStrategy
139-
refreshData()
195+
196+
// Load data WITHOUT forcing refresh - use cache if available
197+
dataLoader.loadData(forceRefresh: false)
140198

141199
// Start animation
142200
animationTrigger = true
143201

144202
// Initialize debounced values with current dataLoader values
145203
debouncedFilesProcessed = dataLoader.filesProcessed
146204
debouncedLoadingMessage = dataLoader.loadingMessage
205+
debouncedDailyUsage = dataLoader.dailyUsage
206+
debouncedDataVersion = 1 // Trigger initial update
207+
208+
// Initialize cached summaries
209+
updateCachedSummaries()
210+
}
211+
212+
private func updateCachedSummaries() {
213+
// Update daily summaries
214+
cachedSummaries = computeSummaries()
215+
216+
// Update project summaries
217+
cachedProjectSummaries = computeProjectSummaries()
218+
219+
// Update available projects from debounced data
220+
let projects = debouncedDailyUsage.values
221+
.flatMap(\.self)
222+
.compactMap(\.projectName)
223+
availableProjects = Array(Set(projects)).sorted()
224+
}
225+
226+
private func computeSummaries() -> [DailyUsageSummary] {
227+
let filtered = getFilteredDailyUsage()
228+
let sortedDays = filtered.keys.sorted(by: >)
229+
230+
let summaries: [DailyUsageSummary] = sortedDays.compactMap { date -> DailyUsageSummary? in
231+
guard let entries = filtered[date] else { return nil }
232+
return DailyUsageSummary(date: date, entries: entries, costStrategy: selectedCostStrategy)
233+
}
234+
235+
return summaries.sorted(using: sortOrder)
236+
}
237+
238+
private func computeProjectSummaries() -> [ProjectUsageSummary] {
239+
// Filter entries by date range
240+
let calendar = Calendar.current
241+
let startOfDay = calendar.startOfDay(for: dateRangeStart)
242+
let endOfDay = calendar.startOfDay(for: dateRangeEnd).addingTimeInterval(24 * 60 * 60)
243+
244+
let filteredEntries = debouncedDailyUsage.flatMap { date, entries -> [ClaudeLogEntry] in
245+
guard date >= startOfDay, date < endOfDay else { return [] }
246+
return entries
247+
}
248+
249+
// Group by project
250+
let entriesByProject = Dictionary(grouping: filteredEntries) { entry in
251+
entry.projectName ?? "Unknown"
252+
}
253+
254+
// Create summaries
255+
let summaries = entriesByProject.map { projectName, entries in
256+
ProjectUsageSummary(projectName: projectName, entries: entries, costStrategy: selectedCostStrategy)
257+
}
258+
259+
return summaries.sorted(using: projectSortOrder)
260+
}
261+
262+
private func getFilteredDailyUsage() -> [Date: [ClaudeLogEntry]] {
263+
guard selectedProject != "All Projects" else {
264+
return debouncedDailyUsage
265+
}
266+
267+
var filtered: [Date: [ClaudeLogEntry]] = [:]
268+
for (date, entries) in debouncedDailyUsage {
269+
let projectEntries = entries.filter { $0.projectName == selectedProject }
270+
if !projectEntries.isEmpty {
271+
filtered[date] = projectEntries
272+
}
273+
}
274+
return filtered
147275
}
148276
}
149277

0 commit comments

Comments
 (0)