diff --git a/ClaudeChecker/DiagnosticsView.swift b/ClaudeChecker/DiagnosticsView.swift index 7248715..c9bc9f1 100644 --- a/ClaudeChecker/DiagnosticsView.swift +++ b/ClaudeChecker/DiagnosticsView.swift @@ -48,6 +48,7 @@ struct DiagnosticsView: View { DiagRow("Signed in", vm.isSignedIn ? "Yes" : "No") DiagRow("Email", vm.userEmail.isEmpty ? "(none)" : vm.userEmail) DiagRow("Org ID", UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)") + DiagRow("lastActiveOrg", vm.diagLastActiveOrg.isEmpty ? "(not read)" : vm.diagLastActiveOrg) DiagRow("Error", vm.errorMessage ?? "(none)") } @@ -65,9 +66,24 @@ struct DiagnosticsView: View { Divider() + // Bootstrap body + if !vm.diagBootstrapBody.isEmpty { + DiagSection(title: "Bootstrap Response (first 500 chars)") { + Text(vm.diagBootstrapBody) + .font(.system(size: 10, design: .monospaced)) + .foregroundColor(.secondary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + .background(Color.primary.opacity(0.04)) + .cornerRadius(5) + } + Divider() + } + // Response body if !vm.diagLastBody.isEmpty { - DiagSection(title: "Response Body (first 500 chars)") { + DiagSection(title: "Last API Response (first 500 chars)") { Text(vm.diagLastBody) .font(.system(size: 10, design: .monospaced)) .foregroundColor(.secondary) @@ -164,6 +180,7 @@ struct DiagnosticsView: View { lines.append("Signed in: \(vm.isSignedIn ? "Yes" : "No")") lines.append("Email: \(vm.userEmail.isEmpty ? "(none)" : vm.userEmail)") lines.append("Org ID: \(UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)")") + lines.append("lastActiveOrg: \(vm.diagLastActiveOrg.isEmpty ? "(not read)" : vm.diagLastActiveOrg)") lines.append("Error: \(vm.errorMessage ?? "(none)")") lines.append("") lines.append("Last path: \(vm.diagLastPath)") @@ -173,7 +190,10 @@ struct DiagnosticsView: View { lines.append("Claude cookies: \(vm.diagClaudeCookieCount)") lines.append("Domains: \(vm.diagCookieDomains.joined(separator: ", "))") lines.append("") - lines.append("Response body:") + lines.append("Bootstrap body:") + lines.append(vm.diagBootstrapBody) + lines.append("") + lines.append("Last API response:") lines.append(vm.diagLastBody) lines.append("") lines.append("Live cookies:") diff --git a/ClaudeChecker/Info.plist b/ClaudeChecker/Info.plist index 77e5466..a030427 100644 --- a/ClaudeChecker/Info.plist +++ b/ClaudeChecker/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.1-beta.23 + 1.2.1-beta.24 CFBundleVersion - 71 + 72 LSMinimumSystemVersion 13.0 LSUIElement diff --git a/ClaudeChecker/UsageViewModel.swift b/ClaudeChecker/UsageViewModel.swift index 9e88b2f..c60e918 100644 --- a/ClaudeChecker/UsageViewModel.swift +++ b/ClaudeChecker/UsageViewModel.swift @@ -34,7 +34,7 @@ class UsageViewModel: ObservableObject { private var apiWebView: WKWebView? private var navWaiter: WKNavWaiter? - // Diagnostics — populated on every urlFetch call + // Diagnostics @Published var diagCookieCount: Int = 0 @Published var diagClaudeCookieCount: Int = 0 @Published var diagCookieDomains: [String] = [] @@ -42,6 +42,8 @@ class UsageViewModel: ObservableObject { @Published var diagLastStatus: Int = 0 @Published var diagLastBody: String = "" @Published var diagLastError: String = "" + @Published var diagLastActiveOrg: String = "" // lastActiveOrg cookie value from JS + @Published var diagBootstrapBody: String = "" // raw bootstrap response (first 500 chars) @Published var diagLastFetch: Date? = nil init() { @@ -225,10 +227,31 @@ class UsageViewModel: ObservableObject { defer { isLoading = false } do { + // Read lastActiveOrg cookie from JS — most reliable org source since it's + // set by the claude.ai frontend to whichever org is currently active. + var cookieOrgId: String? = nil + if let wv = apiWebView, wv.url?.host?.hasSuffix("claude.ai") == true { + let js = """ + return document.cookie.split(';') + .map(c => c.trim().split('=')) + .filter(p => p[0] === 'lastActiveOrg') + .map(p => p.slice(1).join('='))[0] || null; + """ + let raw: Any? = try? await withCheckedThrowingContinuation { cont in + wv.callAsyncJavaScript(js, arguments: [:], in: nil, in: .defaultClient) { r in + cont.resume(returning: (try? r.get()) ?? nil) + } + } + if let v = raw as? String, !v.isEmpty { + cookieOrgId = v + diagLastActiveOrg = v + } + } + let (fetchedOrgId, fetchedEmail, fetchedPlan) = try await fetchBootstrap() let orgId: String - if let id = fetchedOrgId { + if let id = cookieOrgId ?? fetchedOrgId { UserDefaults.standard.set(id, forKey: "claude_org_id") orgId = id } else if let cached = UserDefaults.standard.string(forKey: "claude_org_id") { @@ -292,6 +315,7 @@ class UsageViewModel: ObservableObject { guard body.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") else { throw AppError.detail("Non-JSON response (HTML?): \(body.prefix(80))") } + diagBootstrapBody = String(body.prefix(500)) guard let data = body.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return (nil, nil, nil)