Compare commits
6
Commits
v1.2.1-beta.24
...
v1.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9af0d6401c | ||
|
|
7cb2159b4c | ||
|
|
206e97780d | ||
|
|
2b79ba4ae0 | ||
|
|
60b6c60c0a | ||
|
|
8140831236 |
@@ -66,36 +66,6 @@ struct DiagnosticsView: View {
|
|||||||
|
|
||||||
Divider()
|
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: "Last API Response (first 500 chars)") {
|
|
||||||
Text(vm.diagLastBody)
|
|
||||||
.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cookie summary
|
// Cookie summary
|
||||||
DiagSection(title: "Cookie Store") {
|
DiagSection(title: "Cookie Store") {
|
||||||
DiagRow("Total cookies", "\(vm.diagCookieCount)")
|
DiagRow("Total cookies", "\(vm.diagCookieCount)")
|
||||||
@@ -190,11 +160,6 @@ struct DiagnosticsView: View {
|
|||||||
lines.append("Claude cookies: \(vm.diagClaudeCookieCount)")
|
lines.append("Claude cookies: \(vm.diagClaudeCookieCount)")
|
||||||
lines.append("Domains: \(vm.diagCookieDomains.joined(separator: ", "))")
|
lines.append("Domains: \(vm.diagCookieDomains.joined(separator: ", "))")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Bootstrap body:")
|
|
||||||
lines.append(vm.diagBootstrapBody)
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Last API response:")
|
|
||||||
lines.append(vm.diagLastBody)
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Live cookies:")
|
lines.append("Live cookies:")
|
||||||
for c in liveAllCookies {
|
for c in liveAllCookies {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.2.1-beta.24</string>
|
<string>1.3.1</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>72</string>
|
<string>75</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>13.0</string>
|
<string>13.0</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|||||||
@@ -40,10 +40,8 @@ class UsageViewModel: ObservableObject {
|
|||||||
@Published var diagCookieDomains: [String] = []
|
@Published var diagCookieDomains: [String] = []
|
||||||
@Published var diagLastPath: String = ""
|
@Published var diagLastPath: String = ""
|
||||||
@Published var diagLastStatus: Int = 0
|
@Published var diagLastStatus: Int = 0
|
||||||
@Published var diagLastBody: String = ""
|
|
||||||
@Published var diagLastError: String = ""
|
@Published var diagLastError: String = ""
|
||||||
@Published var diagLastActiveOrg: String = "" // lastActiveOrg cookie value from JS
|
@Published var diagLastActiveOrg: String = "" // lastActiveOrg cookie value from JS
|
||||||
@Published var diagBootstrapBody: String = "" // raw bootstrap response (first 500 chars)
|
|
||||||
@Published var diagLastFetch: Date? = nil
|
@Published var diagLastFetch: Date? = nil
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -117,7 +115,6 @@ class UsageViewModel: ObservableObject {
|
|||||||
diagLastPath = path
|
diagLastPath = path
|
||||||
diagLastFetch = Date()
|
diagLastFetch = Date()
|
||||||
diagLastStatus = status
|
diagLastStatus = status
|
||||||
diagLastBody = String(body.prefix(500))
|
|
||||||
diagLastError = status != 200 ? "JS HTTP \(status)" : ""
|
diagLastError = status != 200 ? "JS HTTP \(status)" : ""
|
||||||
return (status, body)
|
return (status, body)
|
||||||
}
|
}
|
||||||
@@ -181,7 +178,6 @@ class UsageViewModel: ObservableObject {
|
|||||||
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
||||||
let body = String(data: data, encoding: .utf8) ?? ""
|
let body = String(data: data, encoding: .utf8) ?? ""
|
||||||
diagLastStatus = http.statusCode
|
diagLastStatus = http.statusCode
|
||||||
diagLastBody = String(body.prefix(500))
|
|
||||||
if http.statusCode != 200 {
|
if http.statusCode != 200 {
|
||||||
diagLastError = "HTTP \(http.statusCode)"
|
diagLastError = "HTTP \(http.statusCode)"
|
||||||
}
|
}
|
||||||
@@ -248,7 +244,7 @@ class UsageViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (fetchedOrgId, fetchedEmail, fetchedPlan) = try await fetchBootstrap()
|
let (fetchedOrgId, fetchedEmail, fetchedPlan) = try await fetchBootstrap(preferredOrgId: cookieOrgId)
|
||||||
|
|
||||||
let orgId: String
|
let orgId: String
|
||||||
if let id = cookieOrgId ?? fetchedOrgId {
|
if let id = cookieOrgId ?? fetchedOrgId {
|
||||||
@@ -306,7 +302,7 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Bootstrap
|
// 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")
|
let (status, body) = try await apiFetch("/api/bootstrap")
|
||||||
if status == 401 || status == 403 {
|
if status == 401 || status == 403 {
|
||||||
throw AppError.detail("HTTP \(status) — \(body.prefix(120))")
|
throw AppError.detail("HTTP \(status) — \(body.prefix(120))")
|
||||||
@@ -315,7 +311,6 @@ class UsageViewModel: ObservableObject {
|
|||||||
guard body.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") else {
|
guard body.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") else {
|
||||||
throw AppError.detail("Non-JSON response (HTML?): \(body.prefix(80))")
|
throw AppError.detail("Non-JSON response (HTML?): \(body.prefix(80))")
|
||||||
}
|
}
|
||||||
diagBootstrapBody = String(body.prefix(500))
|
|
||||||
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 {
|
||||||
return (nil, nil, nil)
|
return (nil, nil, nil)
|
||||||
@@ -323,7 +318,8 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
let account = json["account"] as? [String: Any]
|
let account = json["account"] as? [String: Any]
|
||||||
let memberships = (account?["memberships"] ?? json["memberships"]) 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
|
var orgId: String? = firstOrg?["uuid"] as? String
|
||||||
if orgId == nil {
|
if orgId == nil {
|
||||||
@@ -340,8 +336,10 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
let email = account?["email_address"] as? String
|
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
|
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 cap = caps.first(where: { $0.hasPrefix("claude_") }) {
|
||||||
let name = String(cap.dropFirst("claude_".count))
|
let name = String(cap.dropFirst("claude_".count))
|
||||||
planLabel = name.prefix(1).uppercased() + name.dropFirst().lowercased()
|
planLabel = name.prefix(1).uppercased() + name.dropFirst().lowercased()
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
[](https://www.apple.com/macos/)
|
[](https://www.apple.com/macos/)
|
||||||
[](https://swift.org)
|
[](https://swift.org)
|
||||||
[](https://github.com/superdooper86/claudechecker/releases)
|
[](https://github.com/superdooper86/claudechecker/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.23) <!-- BETA_BADGE -->
|
<!-- BETA_BADGE -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+3
-19
@@ -1,20 +1,4 @@
|
|||||||
## What's new in v1.2.1-beta.18
|
## What's new in v1.3.1
|
||||||
|
|
||||||
### Bug fixes
|
### Privacy
|
||||||
- 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.
|
- Diagnostics panel no longer displays API response bodies, which could contain account and financial data. Status codes, error messages, cookie names, and request paths are still shown.
|
||||||
- API calls are now sequential to share a single WebView for all navigations
|
|
||||||
- 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
|
|
||||||
|
|
||||||
## What's new in v1.2.1-beta.17
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
- Fixed "Not signed in" after login by adopting the login WebView directly for API calls
|
|
||||||
- Added diagnostic error messages for JS errors and unexpected responses
|
|
||||||
|
|
||||||
## What's new in v1.2.1
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
- Fixed WebKit suspending the background WKWebView: anchored in a transparent 1×1 NSWindow
|
|
||||||
- Fixed login window auto-closing before sign-in completes
|
|
||||||
- Added /api/organizations as a final fallback for org ID resolution
|
|
||||||
- Fixed Settings incorrectly showing "Signed in" after a failed refresh
|
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.1-beta.23",
|
"version": "1.2.1-beta.25",
|
||||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.23/ClaudeChecker.zip",
|
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.25/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"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.0/ClaudeChecker.zip",
|
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.3.0/ClaudeChecker.zip",
|
||||||
"notes": "## What's new in v1.2.0\n\n### Bug fixes\n- App now works for all users — org ID is fetched dynamically from the API instead of being hardcoded\n- Plan name (e.g. Pro, Max) now updates correctly on every refresh\n\n### Improvements\n- Plan name is read from the API rather than hardcoded\n- Bootstrap API call consolidated — org ID, email, and plan name fetched in a single request per refresh\n- Main panel no longer scrolls — popover auto-sizes to fit content\n- Session Diary card redesigned — sample count and avg burn rate shown left/right above the sparkline, Claude header removed"
|
"notes": "## What's new in v1.3.0\n\n### Multi-org support\n- App now uses the `lastActiveOrg` cookie to identify which organisation is active, matching exactly what the claude.ai frontend does. Previously the app always picked the first org returned by bootstrap, which is the personal \"Individual Org\" for users with multiple organisations — causing persistent 403 errors on all usage endpoints.\n- Plan label (e.g. **Max**, **Pro**, **Team**) now reads capabilities from the active org, not the first membership.\n\n### Reliable API access via WebView\n- All API calls now run through `WKWebView.callAsyncJavaScript`, making real same-origin browser requests instead of URLSession. Claude.ai's org-specific endpoints reject native HTTP clients (403) even with correct cookies and headers; running inside the browser context passes all server-side checks.\n- A background WebView is created at startup when session cookies are detected, so usage data loads immediately without requiring a manual sign-in.\n\n### Diagnostics\n- New **Diagnostics** panel in Settings shows cookie store, last API request/response, bootstrap body, and the `lastActiveOrg` cookie value — making it easy to report issues."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user