beta.21: diagnostics view, cookie polling, detailed error messages
This commit is contained in:
@@ -30,6 +30,16 @@ class UsageViewModel: ObservableObject {
|
|||||||
private var previousPercents: [String: Double] = [:]
|
private var previousPercents: [String: Double] = [:]
|
||||||
private var firedThresholds: [String: Set<Int>] = [:]
|
private var firedThresholds: [String: Set<Int>] = [:]
|
||||||
|
|
||||||
|
// Diagnostics — populated on every urlFetch call
|
||||||
|
@Published var diagCookieCount: Int = 0
|
||||||
|
@Published var diagClaudeCookieCount: Int = 0
|
||||||
|
@Published var diagCookieDomains: [String] = []
|
||||||
|
@Published var diagLastPath: String = ""
|
||||||
|
@Published var diagLastStatus: Int = 0
|
||||||
|
@Published var diagLastBody: String = ""
|
||||||
|
@Published var diagLastError: String = ""
|
||||||
|
@Published var diagLastFetch: Date? = nil
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let saved = UserDefaults.standard.double(forKey: "refresh_interval")
|
let saved = UserDefaults.standard.double(forKey: "refresh_interval")
|
||||||
refreshInterval = saved > 0 ? saved : 60
|
refreshInterval = saved > 0 ? saved : 60
|
||||||
@@ -41,10 +51,14 @@ class UsageViewModel: ObservableObject {
|
|||||||
Task { await checkInitialSignInState() }
|
Task { await checkInitialSignInState() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called after login: cookies are already in WKWebsiteDataStore.default() —
|
// Called after login: poll until session cookies appear (they may commit slightly
|
||||||
// brief sleep lets the store commit, then refresh uses URLSession.
|
// after the auth redirect fires), then refresh.
|
||||||
func adoptAndRefresh(_ loginWebView: WKWebView) async {
|
func adoptAndRefresh(_ loginWebView: WKWebView) async {
|
||||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
for _ in 0..<30 {
|
||||||
|
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
||||||
|
if cookies.contains(where: { $0.domain.contains("claude.ai") }) { break }
|
||||||
|
try? await Task.sleep(nanoseconds: 300_000_000)
|
||||||
|
}
|
||||||
await refresh()
|
await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +70,20 @@ class UsageViewModel: ObservableObject {
|
|||||||
let claudeCookies = cookies.filter {
|
let claudeCookies = cookies.filter {
|
||||||
$0.domain.contains("claude.ai") || $0.domain.contains("anthropic.com")
|
$0.domain.contains("claude.ai") || $0.domain.contains("anthropic.com")
|
||||||
}
|
}
|
||||||
guard !claudeCookies.isEmpty else { throw AppError.notAuthenticated }
|
|
||||||
|
// Record cookie diagnostics
|
||||||
|
diagCookieCount = cookies.count
|
||||||
|
diagClaudeCookieCount = claudeCookies.count
|
||||||
|
diagCookieDomains = Array(Set(cookies.map { $0.domain })).sorted()
|
||||||
|
diagLastPath = path
|
||||||
|
diagLastFetch = Date()
|
||||||
|
diagLastError = ""
|
||||||
|
|
||||||
|
guard !claudeCookies.isEmpty else {
|
||||||
|
let msg = "No claude.ai cookies (\(cookies.count) total in store)"
|
||||||
|
diagLastError = msg
|
||||||
|
throw AppError.detail(msg)
|
||||||
|
}
|
||||||
|
|
||||||
var req = URLRequest(url: URL(string: "https://claude.ai\(path)")!)
|
var req = URLRequest(url: URL(string: "https://claude.ai\(path)")!)
|
||||||
req.httpShouldHandleCookies = false
|
req.httpShouldHandleCookies = false
|
||||||
@@ -75,13 +102,25 @@ class UsageViewModel: ObservableObject {
|
|||||||
req.setValue(
|
req.setValue(
|
||||||
"\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"",
|
"\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"",
|
||||||
forHTTPHeaderField: "sec-ch-ua")
|
forHTTPHeaderField: "sec-ch-ua")
|
||||||
req.setValue("?0", forHTTPHeaderField: "sec-ch-ua-mobile")
|
req.setValue("?0", forHTTPHeaderField: "sec-ch-ua-mobile")
|
||||||
req.setValue("\"macOS\"", forHTTPHeaderField: "sec-ch-ua-platform")
|
req.setValue("\"macOS\"", forHTTPHeaderField: "sec-ch-ua-platform")
|
||||||
|
|
||||||
let (data, response) = try await URLSession.shared.data(for: req)
|
do {
|
||||||
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
let (data, response) = try await URLSession.shared.data(for: req)
|
||||||
let body = String(data: data, encoding: .utf8) ?? ""
|
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
||||||
return (http.statusCode, body)
|
let body = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
diagLastStatus = http.statusCode
|
||||||
|
diagLastBody = String(body.prefix(500))
|
||||||
|
if http.statusCode != 200 {
|
||||||
|
diagLastError = "HTTP \(http.statusCode)"
|
||||||
|
}
|
||||||
|
return (http.statusCode, body)
|
||||||
|
} catch let e as AppError { throw e }
|
||||||
|
catch {
|
||||||
|
let msg = error.localizedDescription
|
||||||
|
diagLastError = msg
|
||||||
|
throw AppError.detail(msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkInitialSignInState() async {
|
private func checkInitialSignInState() async {
|
||||||
@@ -173,11 +212,12 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
|
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
|
||||||
let (status, body) = try await urlFetch("/api/bootstrap")
|
let (status, body) = try await urlFetch("/api/bootstrap")
|
||||||
if status == 401 || status == 403 { throw AppError.notAuthenticated }
|
if status == 401 || status == 403 {
|
||||||
guard status == 200 else { throw AppError.detail("bootstrap \(status): \(body.prefix(80))") }
|
throw AppError.detail("HTTP \(status) — \(body.prefix(120))")
|
||||||
// If WebKit followed a redirect to the login page we get HTML instead of JSON.
|
}
|
||||||
|
guard status == 200 else { throw AppError.detail("HTTP \(status): \(body.prefix(80))") }
|
||||||
guard body.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") else {
|
guard body.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") else {
|
||||||
throw AppError.notAuthenticated
|
throw AppError.detail("Non-JSON response (HTML?): \(body.prefix(80))")
|
||||||
}
|
}
|
||||||
guard let data = body.data(using: .utf8),
|
guard let data = body.data(using: .utf8),
|
||||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||||
|
|||||||
Reference in New Issue
Block a user