-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathLookerStudioManager.swift
More file actions
225 lines (194 loc) · 7.02 KB
/
LookerStudioManager.swift
File metadata and controls
225 lines (194 loc) · 7.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
//
// LookerStudioManager.swift
// Claude Usage Tracker
//
// Copyright © 2025 Sergio Bañuls. All rights reserved.
// Licensed under Personal Use License (Non-Commercial)
//
import Foundation
import SwiftUI
extension Notification.Name {
static let lookerDataUpdated = Notification.Name("lookerDataUpdated")
}
class LookerStudioManager: ObservableObject {
@Published var isConfigured: Bool = false
@Published var lastError: String? = nil
@Published var totalSpend: Double = 0.0
@Published var totalTokens: Int = 0
@Published var monthlySpend: Double = 0.0
@Published var monthlyTokens: Int = 0
@Published var prevMonthSpend: Double = 0.0
@Published var prevMonthTokens: Int = 0
@Published var monthlyHistory: [MonthlyEntry] = []
@Published var modelBreakdown: [ModelEntry] = []
@Published var team: String = ""
// Legacy cookie fields (kept for manual entry fallback)
@Published var securePSID: String = "" {
didSet { saveCookies() }
}
@Published var securePSIDTS: String = "" {
didSet { saveCookies() }
}
// UserDefaults keys
private let psidKey = "looker_secure_1psid"
private let psidtsKey = "looker_secure_1psidts"
private let connectedKey = "looker_connected"
// WebBridge for background refreshes
private var backgroundBridge: LookerWebBridge?
init() {
loadState()
}
// MARK: - State Management
private func saveCookies() {
UserDefaults.standard.set(securePSID, forKey: psidKey)
UserDefaults.standard.set(securePSIDTS, forKey: psidtsKey)
updateConfiguredState()
}
private func loadState() {
securePSID = UserDefaults.standard.string(forKey: psidKey) ?? ""
securePSIDTS = UserDefaults.standard.string(forKey: psidtsKey) ?? ""
isConfigured = UserDefaults.standard.bool(forKey: connectedKey)
}
private func updateConfiguredState() {
let configured = !securePSID.isEmpty && !securePSIDTS.isEmpty
if configured != isConfigured {
isConfigured = configured
UserDefaults.standard.set(configured, forKey: connectedKey)
}
}
func markConnected() {
isConfigured = true
UserDefaults.standard.set(true, forKey: connectedKey)
}
func clearConnection() {
securePSID = ""
securePSIDTS = ""
isConfigured = false
UserDefaults.standard.set(false, forKey: connectedKey)
lastError = nil
totalSpend = 0
totalTokens = 0
monthlySpend = 0
monthlyTokens = 0
prevMonthSpend = 0
prevMonthTokens = 0
}
func hasValidCookies() -> Bool {
return isConfigured
}
// MARK: - Data Update
func updateWithData(_ data: LookerDashboardData) {
self.totalSpend = data.totalSpend
self.totalTokens = data.totalTokens
self.monthlySpend = data.monthlySpend
self.monthlyTokens = data.monthlyTokens
self.prevMonthSpend = data.prevMonthSpend
self.prevMonthTokens = data.prevMonthTokens
if !data.monthlyHistory.isEmpty {
self.monthlyHistory = data.monthlyHistory
}
if !data.modelBreakdown.isEmpty {
self.modelBreakdown = data.modelBreakdown
}
if !data.team.isEmpty {
self.team = data.team
}
self.lastError = nil
NotificationCenter.default.post(name: .lookerDataUpdated, object: nil)
}
// MARK: - Background Refresh via WebBridge
func refreshData() async {
guard isConfigured else { return }
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
DispatchQueue.main.async {
let bridge = LookerWebBridge()
self.backgroundBridge = bridge
bridge.refreshInBackground(
onDataFetched: { [weak self] data in
DispatchQueue.main.async {
self?.updateWithData(data)
self?.backgroundBridge = nil
continuation.resume()
}
},
onError: { [weak self] error in
DispatchQueue.main.async {
self?.lastError = error
self?.backgroundBridge = nil
continuation.resume()
}
}
)
}
}
}
/// Validates connection by attempting a background data fetch
func validateCookies() async -> Bool {
guard isConfigured else { return false }
return await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
DispatchQueue.main.async {
let bridge = LookerWebBridge()
self.backgroundBridge = bridge
bridge.refreshInBackground(
onDataFetched: { [weak self] data in
DispatchQueue.main.async {
self?.updateWithData(data)
self?.backgroundBridge = nil
continuation.resume(returning: true)
}
},
onError: { [weak self] error in
DispatchQueue.main.async {
self?.lastError = error
self?.backgroundBridge = nil
continuation.resume(returning: false)
}
}
)
}
}
}
// MARK: - Data Structures
struct LookerDashboardData {
var totalSpend: Double = 0.0
var totalTokens: Int = 0
var monthlySpend: Double = 0.0
var monthlyTokens: Int = 0
var prevMonthSpend: Double = 0.0
var prevMonthTokens: Int = 0
var monthlyHistory: [MonthlyEntry] = []
var modelBreakdown: [ModelEntry] = []
var team: String = ""
}
struct MonthlyEntry {
var month: String // YYYYMM format
var spend: Double = 0.0
var tokens: Int = 0
}
struct ModelEntry {
var model: String
var spend: Double = 0.0
var tokens: Int = 0
}
// MARK: - Tier Detection
struct TierInfo {
let name: String
let monthlyLimit: Double
let color: Color
}
static let tiers: [(keyword: String, info: TierInfo)] = [
("iron", TierInfo(name: "Iron", monthlyLimit: 100, color: .gray)),
("bronze", TierInfo(name: "Bronze", monthlyLimit: 200, color: .brown)),
("silver", TierInfo(name: "Silver", monthlyLimit: 500, color: Color(.systemGray))),
("gold", TierInfo(name: "Gold", monthlyLimit: 1000, color: .yellow)),
]
static func detectTier(from team: String) -> TierInfo? {
let lowered = team.lowercased()
for tier in tiers {
if lowered.contains(tier.keyword) {
return tier.info
}
}
return nil
}
}