Compare commits

..
10 Commits
Author SHA1 Message Date
SuperDooper 2235a627b5 Bump version to 1.3.3-beta.1 (build 77) 2026-05-11 20:34:15 +02:00
SuperDooper d6c138f1a2 Bug template: update diagnostics copy note 2026-05-11 20:28:38 +02:00
SuperDooper 432819639e Diagnostics: remove email, redact IDs on copy 2026-05-11 20:28:12 +02:00
SuperDooper 52db469c13 Remove org ID from diagnostics output 2026-05-11 20:24:56 +02:00
SuperDooper 40c86aaaf2 Windows beta release win-v0.0.1-beta.51 2026-05-11 13:09:31 +00:00
SuperDooper 46b09934d7 Windows beta release win-v0.0.1-beta.50 2026-05-11 12:20:21 +00:00
SuperDooper a79a23ac24 update bug report template: add diagnostics instructions and Windows field 2026-05-11 14:19:06 +02:00
github-actions[bot] 3785629ee4 Release v1.3.2 2026-05-11 12:01:55 +00:00
superdooper86 a338f4e5fe release 1.3.2: anchor primer WebView in hidden window for reliable startup fetch
Every launch now loads claude.ai in a real (off-screen, invisible) NSWindow so
WebKit doesn't throttle JS execution. The nav delegate fires adoptPrimerWebView
once the page loads, replacing the old 1.5 s sleep-then-refresh approach.
2026-05-11 14:01:07 +02:00
github-actions[bot] 88e7d684c0 Release v1.3.1 2026-05-11 11:45:59 +00:00
9 changed files with 80 additions and 65 deletions
+8 -2
View File
@@ -18,12 +18,18 @@ Steps to reproduce the behaviour:
**Expected behaviour**
What you expected to happen.
**Diagnostics**
Open the app → Settings → Diagnostics, then click "Copy All" and paste the output here.
This shows the error, request state, and cookie info. Email and org IDs are automatically redacted when you click Copy All.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment**
- macOS version: [e.g. Sequoia 15.4]
- ClaudeChecker version: [e.g. 1.1.3]
- Platform: [macOS / Windows]
- macOS version: [e.g. Sequoia 15.4] *(if applicable)*
- Windows version: [e.g. Windows 11 24H2] *(if applicable)*
- ClaudeChecker version: [e.g. 1.3.2]
- Claude plan: [e.g. Pro, Max]
**Additional context**
+40 -10
View File
@@ -18,16 +18,43 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var updateManager = UpdateManager()
var refreshTimer: Timer?
var cookiePrimerView: WKWebView?
var cookiePrimerWindow: NSWindow?
var primerNavDelegate: PrimerNavDelegate?
var cancellables = Set<AnyCancellable>()
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
// Hidden WKWebView to prime the shared cookie store
// Background WebView that loads claude.ai on every launch.
// Anchored in a real (off-screen, invisible) NSWindow so WebKit doesn't throttle
// JS execution. When the page finishes loading the nav delegate calls
// adoptPrimerWebView, which sets it as the API WebView and runs the first fetch.
let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.default()
cookiePrimerView = WKWebView(frame: .zero, configuration: config)
cookiePrimerView?.load(URLRequest(url: URL(string: "https://claude.ai")!))
let wv = WKWebView(frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: config)
let win = NSWindow(
contentRect: NSRect(x: -9999, y: -9999, width: 1, height: 1),
styleMask: [],
backing: .buffered,
defer: false
)
win.contentView = wv
win.alphaValue = 0
win.orderFront(nil)
cookiePrimerWindow = win
cookiePrimerView = wv
let delegate = PrimerNavDelegate { [weak self] in
guard let self, let wv = self.cookiePrimerView else { return }
Task { @MainActor [weak self] in
guard let self else { return }
await self.usageViewModel.adoptPrimerWebView(wv)
await self.updateManager.checkForUpdates()
}
}
primerNavDelegate = delegate
wv.navigationDelegate = delegate
wv.load(URLRequest(url: URL(string: "https://claude.ai")!))
// Status item
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
@@ -134,12 +161,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
// Initial fetch after cookie primer + update check
Task {
try? await Task.sleep(nanoseconds: 1_500_000_000)
await usageViewModel.refresh()
await updateManager.checkForUpdates()
}
}
private func setMenubarIcon(button: NSStatusBarButton) {
@@ -240,4 +261,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
// One-shot WKNavigationDelegate: fires onDone on first finish or error, then goes silent.
final class PrimerNavDelegate: NSObject, WKNavigationDelegate {
private var done = false
private let onDone: () -> Void
init(_ onDone: @escaping () -> Void) { self.onDone = onDone }
private func finish() { guard !done else { return }; done = true; onDone() }
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { finish() }
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { finish() }
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { finish() }
}
+9 -5
View File
@@ -24,7 +24,7 @@ struct DiagnosticsView: View {
.buttonStyle(.bordered)
.controlSize(.small)
if copied {
Text("Copied!")
Text("Copied! (IDs redacted)")
.font(.system(size: 11))
.foregroundColor(.green)
}
@@ -46,7 +46,6 @@ struct DiagnosticsView: View {
DiagRow("Version", Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?")
DiagRow("Build", Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?")
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)")
@@ -149,8 +148,6 @@ struct DiagnosticsView: View {
lines.append("Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"))")
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)")
@@ -165,7 +162,14 @@ struct DiagnosticsView: View {
for c in liveAllCookies {
lines.append(" \(c.domain) \(c.name) httpOnly=\(c.isHTTPOnly) secure=\(c.isSecure)")
}
return lines.joined(separator: "\n")
return redactUUIDs(lines.joined(separator: "\n"))
}
private func redactUUIDs(_ text: String) -> String {
let pattern = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
guard let regex = try? NSRegularExpression(pattern: pattern) else { return text }
let range = NSRange(text.startIndex..., in: text)
return regex.stringByReplacingMatches(in: text, range: range, withTemplate: "****")
}
}
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.3.1</string>
<string>1.3.3-beta.1</string>
<key>CFBundleVersion</key>
<string>75</string>
<string>77</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
+11 -36
View File
@@ -32,7 +32,6 @@ class UsageViewModel: ObservableObject {
// WebView used for JS-based API calls (avoids URLSession 403 fingerprinting issues)
private var apiWebView: WKWebView?
private var navWaiter: WKNavWaiter?
// Diagnostics
@Published var diagCookieCount: Int = 0
@@ -55,38 +54,24 @@ class UsageViewModel: ObservableObject {
Task { await checkInitialSignInState() }
}
// Called after login: store the webview (already on claude.ai) so jsFetch can use it.
// Called by AppDelegate once the primer WebView has loaded claude.ai.
// Sets it as the API WebView and triggers the first data fetch.
func adoptPrimerWebView(_ wv: WKWebView) async {
apiWebView = wv
await refresh()
}
// Called after login: store the login WebView (already on claude.ai) so jsFetch can use it.
func adoptAndRefresh(_ loginWebView: WKWebView) async {
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)
}
if loginWebView.url?.host?.hasSuffix("claude.ai") == true {
apiWebView = loginWebView
} else {
await prepareApiWebView()
}
apiWebView = loginWebView
await refresh()
}
// Creates (or reuses) a background WKWebView navigated to claude.ai for JS fetch.
private func prepareApiWebView() async {
if let wv = apiWebView, wv.url?.host?.hasSuffix("claude.ai") == true { return }
let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.default()
let wv = WKWebView(frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: config)
apiWebView = wv
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
let waiter = WKNavWaiter { cont.resume() }
self.navWaiter = waiter
wv.navigationDelegate = waiter
wv.load(URLRequest(url: URL(string: "https://claude.ai/")!))
}
wv.navigationDelegate = nil
navWaiter = nil
}
// Runs a fetch() inside the live claude.ai WebView same browser context as the frontend,
// so no CORS/fingerprinting issues that URLSession hits.
private func jsFetch(_ path: String) async throws -> (Int, String) {
@@ -194,8 +179,8 @@ class UsageViewModel: ObservableObject {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
guard cookies.contains(where: { $0.domain.contains("claude.ai") }) else { return }
isSignedIn = true
await prepareApiWebView()
await refresh()
// AppDelegate's primer WebView will call adoptPrimerWebView once it loads,
// which triggers the first real data fetch.
}
func signOut() async {
@@ -486,16 +471,6 @@ class UsageViewModel: ObservableObject {
}
// Navigation delegate that resolves a continuation on first finish/error (idempotent).
private class WKNavWaiter: NSObject, WKNavigationDelegate {
private var done = false
private let onDone: () -> Void
init(_ onDone: @escaping () -> Void) { self.onDone = onDone }
private func finish() { guard !done else { return }; done = true; onDone() }
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { finish() }
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { finish() }
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { finish() }
}
enum AppError: LocalizedError {
case notAuthenticated
+1 -1
View File
@@ -8,7 +8,7 @@
[![macOS](https://img.shields.io/badge/macOS-13.0+-000000?style=flat&logo=apple&logoColor=white)](https://www.apple.com/macos/)
[![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.3.0-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![Version](https://img.shields.io/badge/version-1.3.2-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
<!-- BETA_BADGE -->
+3 -3
View File
@@ -1,4 +1,4 @@
## What's new in v1.3.1
## What's new in v1.3.2
### Privacy
- 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.
### Reliability
- Data now loads automatically on every app launch without requiring manual re-authentication. The background WebView is anchored in a hidden window so macOS no longer throttles its JavaScript execution.
+3 -3
View File
@@ -1,5 +1,5 @@
{
"version": "0.0.1-beta.49",
"url": "https://github.com/superdooper86/claudechecker/releases/download/win-v0.0.1-beta.49/ClaudeChecker-Windows.zip",
"notes": "## What's new in beta.36\r\n\r\n### Bug fixes\r\n- Settings no longer incorrectly shows \"Not signed in\" when limits are working\r\n- Session Diary now shows correctly after the first successful refresh\r\n- Extra Usage Credits section now restored from cache on startup\r\n- Plan name, overage, and prepaid credits are now cached and shown immediately on launch\r\n- App now loads cached state instantly on startup before the background refresh completes"
"version": "0.0.1-beta.51",
"url": "https://github.com/superdooper86/claudechecker/releases/download/win-v0.0.1-beta.51/ClaudeChecker-Windows.zip",
"notes": "## What's new in beta.49\r\n\r\n### Architecture\r\n- All data (usage, plan, overage, prepaid credits) now fetched via the persistent background WebView2 — the same always-live session as the macOS app, so Cloudflare cookies never expire between refreshes\r\n- HttpClient path kept only as a brief startup fallback before the browser is ready\r\n- Settings file simplified: only stores session signal, burn history, and user preferences — no more cached API responses\r\n\r\n### Bug fixes\r\n- Plan label (e.g. \"Pro\") now updates on every refresh, not just at login\r\n- Extra Usage Credits values now correct (were 100× too large)\r\n- Limit and balance now update live on every refresh\r\n- Refresh interval dropdown now shows \"1 min\", \"2 min\" etc. correctly\r\n- ComboBox no longer flashes bright blue when clicked\r\n- Buttons now show a visible pressed state\r\n- Footer \"Updated X ago\" counter now ticks every second; shows \"X min, Y sec ago\" past 60 s\r\n- Removed stray sparkline bars from the 5 h and 7 d limit cards\r\n- App window now shown on launch\r\n\r\n### UI improvements\r\n- Gauge percentage larger, \"used\" label removed\r\n- Each limit card now shows an \"after reset\" or \"today HH:MM\" badge\r\n- Session Diary card redesigned to match macOS layout (stats row + sparkline, no Claude header)\r\n- Reset date moved inline with time remaining"
}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"version": "1.3.0",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.3.0/ClaudeChecker.zip",
"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."
"version": "1.3.2",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.3.2/ClaudeChecker.zip",
"notes": "## What's new in v1.3.2\n\n### Reliability\n- Data now loads automatically on every app launch without requiring manual re-authentication. The background WebView is anchored in a hidden window so macOS no longer throttles its JavaScript execution."
}