From a338f4e5fefb14106583419f98fd9bb9d24ab1ed Mon Sep 17 00:00:00 2001 From: superdooper86 Date: Mon, 11 May 2026 14:01:07 +0200 Subject: [PATCH] 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. --- ClaudeChecker/ClaudeCheckerApp.swift | 50 ++++++++++++++++++++++------ ClaudeChecker/Info.plist | 4 +-- ClaudeChecker/UsageViewModel.swift | 47 ++++++-------------------- RELEASE_NOTES.md | 6 ++-- 4 files changed, 56 insertions(+), 51 deletions(-) diff --git a/ClaudeChecker/ClaudeCheckerApp.swift b/ClaudeChecker/ClaudeCheckerApp.swift index 7745f61..2ffe4b3 100644 --- a/ClaudeChecker/ClaudeCheckerApp.swift +++ b/ClaudeChecker/ClaudeCheckerApp.swift @@ -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() 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() } +} diff --git a/ClaudeChecker/Info.plist b/ClaudeChecker/Info.plist index 14eb249..7df4881 100644 --- a/ClaudeChecker/Info.plist +++ b/ClaudeChecker/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.1 + 1.3.2 CFBundleVersion - 75 + 76 LSMinimumSystemVersion 13.0 LSUIElement diff --git a/ClaudeChecker/UsageViewModel.swift b/ClaudeChecker/UsageViewModel.swift index 069ef13..6bbd4ec 100644 --- a/ClaudeChecker/UsageViewModel.swift +++ b/ClaudeChecker/UsageViewModel.swift @@ -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) 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 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 438105e..17a0458 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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.