Compare commits

...
5 changed files with 60 additions and 13 deletions
+22 -2
View File
@@ -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:")
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.2.1-beta.23</string>
<string>1.2.1-beta.25</string>
<key>CFBundleVersion</key>
<string>71</string>
<string>73</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
+33 -6
View File
@@ -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 {
let (fetchedOrgId, fetchedEmail, fetchedPlan) = try await fetchBootstrap()
// 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(preferredOrgId: cookieOrgId)
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") {
@@ -283,7 +306,7 @@ class UsageViewModel: ObservableObject {
// MARK: - Bootstrap
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
private func fetchBootstrap(preferredOrgId: String? = nil) async throws -> (orgId: String?, email: String?, planLabel: String?) {
let (status, body) = try await apiFetch("/api/bootstrap")
if status == 401 || status == 403 {
throw AppError.detail("HTTP \(status)\(body.prefix(120))")
@@ -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)
@@ -299,7 +323,8 @@ class UsageViewModel: ObservableObject {
let account = json["account"] as? [String: Any]
let memberships = (account?["memberships"] ?? json["memberships"]) as? [[String: Any]]
let firstOrg = memberships?.first?["organization"] as? [String: Any]
let allOrgs = memberships?.compactMap { $0["organization"] as? [String: Any] } ?? []
let firstOrg = allOrgs.first
var orgId: String? = firstOrg?["uuid"] as? String
if orgId == nil {
@@ -316,8 +341,10 @@ class UsageViewModel: ObservableObject {
let email = account?["email_address"] as? String
// Read capabilities from the preferred (active) org, falling back to first org.
let capOrg = preferredOrgId.flatMap { id in allOrgs.first(where: { $0["uuid"] as? String == id }) } ?? firstOrg
var planLabel: String? = nil
if let caps = firstOrg?["capabilities"] as? [String],
if let caps = capOrg?["capabilities"] as? [String],
let cap = caps.first(where: { $0.hasPrefix("claude_") }) {
let name = String(cap.dropFirst("claude_".count))
planLabel = name.prefix(1).uppercased() + name.dropFirst().lowercased()
+1 -1
View File
@@ -10,7 +10,7 @@
[![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org)
[![Version](https://img.shields.io/badge/version-1.2.0-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
[![Beta](https://img.shields.io/badge/beta-1.2.1--beta.22-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.22) <!-- BETA_BADGE -->
[![Beta](https://img.shields.io/badge/beta-1.2.1--beta.24-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.24) <!-- BETA_BADGE -->
</div>
+2 -2
View File
@@ -1,5 +1,5 @@
{
"version": "1.2.1-beta.22",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.22/ClaudeChecker.zip",
"version": "1.2.1-beta.24",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.24/ClaudeChecker.zip",
"notes": "## What's new in v1.2.1-beta.18\n\n### Bug fixes\n- Replaced callAsyncJavaScript fetch approach with WebKit navigation: instead of running `fetch()` in the page's JS context (which was returning 401 because it bypasses the SPA's auth interceptors), each API call now navigates the WebView to the API URL directly. WebKit sends full browser headers and cookies automatically at the HTTP layer, the same way a real browser navigation works. This is more reliable regardless of what server-side auth mechanism claude.ai uses.\n- API calls are now sequential to share a single WebView for all navigations\n- Added redirect detection: if WebKit follows a 302 to /login, the response is treated as an auth failure rather than returning HTML to the JSON parser\n\n## What's new in v1.2.1-beta.17\n\n### Bug fixes\n- Fixed \"Not signed in\" after login by adopting the login WebView directly for API calls\n- Added diagnostic error messages for JS errors and unexpected responses\n\n## What's new in v1.2.1\n\n### Bug fixes\n- Fixed WebKit suspending the background WKWebView: anchored in a transparent 1×1 NSWindow\n- Fixed login window auto-closing before sign-in completes\n- Added /api/organizations as a final fallback for org ID resolution\n- Fixed Settings incorrectly showing \"Signed in\" after a failed refresh"
}