Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13d3d8ee8 | ||
|
|
a729a37efc | ||
|
|
c7e4c81b9a | ||
|
|
2313c4378a | ||
|
|
c51086c119 | ||
|
|
5840b34b3b | ||
|
|
5a141095e1 | ||
|
|
4b42a631be | ||
|
|
c6d2ed8261 | ||
|
|
747e26e5eb | ||
|
|
b9fd5a27ec | ||
|
|
60336cffa7 | ||
|
|
953c46077c | ||
|
|
3796139178 | ||
|
|
554eb80c0c | ||
|
|
a22bf3a309 | ||
|
|
eaab3eb539 | ||
|
|
323aa174da | ||
|
|
3803cf37f6 | ||
|
|
1977b51b74 | ||
|
|
64a3f15ac5 | ||
|
|
8fbd1c42c3 | ||
|
|
0bf19c3dc8 | ||
|
|
d56a714bb9 | ||
|
|
88f409971a | ||
|
|
9bac2540e8 | ||
|
|
792c5a26e9 | ||
|
|
421727bfb5 | ||
|
|
21b2905bcf | ||
|
|
38d6f2a582 | ||
|
|
7ea5554506 | ||
|
|
87c5992889 | ||
|
|
034e196214 | ||
|
|
2235a627b5 | ||
|
|
d6c138f1a2 | ||
|
|
432819639e | ||
|
|
52db469c13 | ||
|
|
40c86aaaf2 | ||
|
|
46b09934d7 | ||
|
|
a79a23ac24 | ||
|
|
3785629ee4 | ||
|
|
a338f4e5fe | ||
|
|
88e7d684c0 | ||
|
|
9af0d6401c | ||
|
|
7cb2159b4c |
@@ -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**
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
run: |
|
||||
shields_version=$(echo "${{ steps.version.outputs.version }}" | sed 's/-/--/g')
|
||||
tag="${{ github.ref_name }}"
|
||||
export BADGE="[](https://github.com/${{ github.repository }}/releases/tag/${tag}) <!-- BETA_BADGE -->"
|
||||
export BADGE="[](https://github.com/${{ github.repository }}/releases/tag/${tag}) <!-- BETA_BADGE -->"
|
||||
python3 -c "
|
||||
import os
|
||||
badge = os.environ['BADGE']
|
||||
@@ -75,5 +75,7 @@ jobs:
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add version-beta.json README.md
|
||||
git commit -m "Beta release ${{ github.ref_name }}"
|
||||
git pull --rebase origin main
|
||||
git push origin HEAD:main
|
||||
for i in 1 2 3 4 5; do
|
||||
git pull --rebase origin main && git push origin HEAD:main && break
|
||||
echo "Push attempt $i failed, retrying..."; sleep 3
|
||||
done
|
||||
|
||||
@@ -73,15 +73,9 @@ jobs:
|
||||
import os, re
|
||||
v = os.environ['NEW_VERSION']
|
||||
content = open('README.md').read()
|
||||
content = re.sub(r'version-[0-9][^-]*-orange', 'version-' + v + '-orange', content)
|
||||
content = re.sub(r'macOS_Stable-[^-]+-orange', 'macOS_Stable-' + v + '-orange', content)
|
||||
lines = content.splitlines(keepends=True)
|
||||
out = []
|
||||
for line in lines:
|
||||
if '<!-- BETA_BADGE -->' in line:
|
||||
continue
|
||||
out.append(line)
|
||||
if 'license-MIT-blue' in line:
|
||||
out.append('<!-- BETA_BADGE -->' + chr(10))
|
||||
out = ['<!-- BETA_BADGE -->' + chr(10) if '<!-- BETA_BADGE -->' in line else line for line in lines]
|
||||
open('README.md', 'w').writelines(out)
|
||||
"
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -38,6 +38,15 @@ struct DiagnosticsView: View {
|
||||
|
||||
Divider()
|
||||
|
||||
Text("Org IDs are redacted when copied.")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 6)
|
||||
|
||||
Divider()
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
|
||||
@@ -46,7 +55,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)")
|
||||
@@ -66,47 +74,6 @@ 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: "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
|
||||
DiagSection(title: "Cookie Store") {
|
||||
DiagRow("Total cookies", "\(vm.diagCookieCount)")
|
||||
DiagRow("claude.ai cookies", "\(vm.diagClaudeCookieCount)")
|
||||
DiagRow("All domains", vm.diagCookieDomains.isEmpty
|
||||
? "(none — never fetched)"
|
||||
: vm.diagCookieDomains.joined(separator: ", "))
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Live cookie list
|
||||
DiagSection(title: "Live Cookie Names (WKWebsiteDataStore.default)") {
|
||||
if loadingCookies {
|
||||
@@ -174,33 +141,39 @@ struct DiagnosticsView: View {
|
||||
}
|
||||
|
||||
private var fullDiagText: String {
|
||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
|
||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"
|
||||
let orgId = UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)"
|
||||
let lastOrg = vm.diagLastActiveOrg.isEmpty ? "(not read)" : vm.diagLastActiveOrg
|
||||
let lastErr = vm.diagLastError.isEmpty ? "(none)" : vm.diagLastError
|
||||
var lines: [String] = []
|
||||
lines.append("=== ClaudeChecker Diagnostics ===")
|
||||
lines.append("Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"))")
|
||||
lines.append("Version: \(version) (\(build))")
|
||||
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("Org ID: \(orgId)")
|
||||
lines.append("lastActiveOrg: \(lastOrg)")
|
||||
lines.append("Error: \(vm.errorMessage ?? "(none)")")
|
||||
lines.append("")
|
||||
lines.append("Last path: \(vm.diagLastPath)")
|
||||
lines.append("Last status: \(vm.diagLastStatus)")
|
||||
lines.append("Last error: \(vm.diagLastError)")
|
||||
lines.append("Total cookies: \(vm.diagCookieCount)")
|
||||
lines.append("Claude cookies: \(vm.diagClaudeCookieCount)")
|
||||
lines.append("Domains: \(vm.diagCookieDomains.joined(separator: ", "))")
|
||||
lines.append("Last error: \(lastErr)")
|
||||
if let t = vm.diagLastFetch {
|
||||
lines.append("Time: \(t.formatted(date: .omitted, time: .standard))")
|
||||
}
|
||||
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("Live cookies:")
|
||||
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: "****")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.0</string>
|
||||
<string>1.3.3-beta.5</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>74</string>
|
||||
<string>81</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>LSUIElement</key>
|
||||
|
||||
@@ -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
|
||||
@@ -40,10 +39,8 @@ class UsageViewModel: ObservableObject {
|
||||
@Published var diagCookieDomains: [String] = []
|
||||
@Published var diagLastPath: String = ""
|
||||
@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() {
|
||||
@@ -57,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) {
|
||||
@@ -117,7 +100,6 @@ class UsageViewModel: ObservableObject {
|
||||
diagLastPath = path
|
||||
diagLastFetch = Date()
|
||||
diagLastStatus = status
|
||||
diagLastBody = String(body.prefix(500))
|
||||
diagLastError = status != 200 ? "JS HTTP \(status)" : ""
|
||||
return (status, body)
|
||||
}
|
||||
@@ -181,7 +163,6 @@ class UsageViewModel: ObservableObject {
|
||||
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
diagLastStatus = http.statusCode
|
||||
diagLastBody = String(body.prefix(500))
|
||||
if http.statusCode != 200 {
|
||||
diagLastError = "HTTP \(http.statusCode)"
|
||||
}
|
||||
@@ -198,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 {
|
||||
@@ -315,7 +296,6 @@ 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)
|
||||
@@ -491,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
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
**A native macOS menubar app for monitoring your Claude AI usage limits in real time.**
|
||||
|
||||
[](https://www.apple.com/macos/)
|
||||
[](https://www.microsoft.com/windows/)
|
||||
[](https://swift.org)
|
||||
[](https://github.com/superdooper86/claudechecker/releases)
|
||||
[](LICENSE)
|
||||
[](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.25) <!-- BETA_BADGE -->
|
||||
<br/>
|
||||
[](https://github.com/superdooper86/claudechecker/releases)
|
||||
[](https://github.com/superdooper86/claudechecker/releases/tag/v1.3.3-beta.3) <!-- BETA_BADGE -->
|
||||
[](https://github.com/superdooper86/claudechecker/releases/tag/win-v0.0.1-beta.55) <!-- WIN_BETA_BADGE -->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
+3
-11
@@ -1,12 +1,4 @@
|
||||
## What's new in v1.3.0
|
||||
## What's new in v1.3.2
|
||||
|
||||
### Multi-org support
|
||||
- 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.
|
||||
- Plan label (e.g. **Max**, **Pro**, **Team**) now reads capabilities from the active org, not the first membership.
|
||||
|
||||
### Reliable API access via WebView
|
||||
- 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.
|
||||
- A background WebView is created at startup when session cookies are detected, so usage data loads immediately without requiring a manual sign-in.
|
||||
|
||||
### Diagnostics
|
||||
- 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.
|
||||
### 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.
|
||||
|
||||
+1
-5
@@ -1,5 +1 @@
|
||||
{
|
||||
"version": "1.2.1-beta.25",
|
||||
"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"
|
||||
}
|
||||
{"version": "1.3.3-beta.3", "url": "https://github.com/superdooper86/claudechecker/releases/download/v1.3.3-beta.3/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."}
|
||||
@@ -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.55",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/win-v0.0.1-beta.55/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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.2.0",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.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"
|
||||
"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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user