Compare commits

...
2 changed files with 50 additions and 37 deletions
+5 -6
View File
@@ -1,7 +1,6 @@
## What's new in beta.19 ## What's new in beta.20
- Fixed dark mode: button hover no longer white, ComboBox fully themed - Fixed: app now correctly shows you as signed in after login
- Fixed logo and version badge in Settings panel - Fixed: org ID is fetched from API rather than hardcoded; tries multiple JSON paths
- Fixed signed-in email showing blank (TextBrush instead of hardcoded white) - Fixed: auth check now uses HTTP 200 from bootstrap (not whether org ID was found)
- Fixed session limits not loading: org ID now fetched dynamically from API - Added browser User-Agent header so API responds correctly
- Added anthropic-client-platform header for correct API responses
+45 -31
View File
@@ -91,9 +91,9 @@ public class UsageViewModel : INotifyPropertyChanged
using var http = BuildClient(cookies); using var http = BuildClient(cookies);
// Bootstrap gives us email + org ID. If it fails, cookies are invalid. // Bootstrap: 200 = authenticated. Non-200 = session expired.
var (email, orgId) = await FetchBootstrapAsync(http); var bootstrapResp = await http.GetAsync("https://claude.ai/api/bootstrap");
if (string.IsNullOrEmpty(orgId)) if (!bootstrapResp.IsSuccessStatusCode)
{ {
await Application.Current.Dispatcher.InvokeAsync(() => await Application.Current.Dispatcher.InvokeAsync(() =>
{ {
@@ -104,18 +104,34 @@ public class UsageViewModel : INotifyPropertyChanged
return; return;
} }
// Cache org ID for resilience // Parse email and org ID (best-effort — don't fail auth if org ID is missing)
AppSettings.Default.OrgId = orgId; var bootstrapJson = await bootstrapResp.Content.ReadAsStringAsync();
AppSettings.Default.Save(); var (email, orgId) = ParseBootstrap(bootstrapJson);
var usageTask = FetchAsync<UsageResponse>(http, $"https://claude.ai/api/organizations/{orgId}/usage"); // Resolve org ID through fallback chain
var overageTask = FetchAsync<OverageSpendLimit>(http, $"https://claude.ai/api/organizations/{orgId}/overage_spend_limit"); if (string.IsNullOrEmpty(orgId))
var prepaidTask = FetchAsync<PrepaidCredits>(http, $"https://claude.ai/api/organizations/{orgId}/prepaid/credits"); orgId = await FetchOrgIdFromListAsync(http);
if (string.IsNullOrEmpty(orgId) && !string.IsNullOrEmpty(AppSettings.Default.OrgId))
orgId = AppSettings.Default.OrgId;
await Task.WhenAll(usageTask, overageTask, prepaidTask); List<AgentLimit> limits = [];
OverageSpendLimit? overage = null;
PrepaidCredits? prepaid = null;
var usage = usageTask.Result; if (!string.IsNullOrEmpty(orgId))
var limits = BuildLimits(usage); {
AppSettings.Default.OrgId = orgId;
AppSettings.Default.Save();
var usageTask = FetchAsync<UsageResponse>(http, $"https://claude.ai/api/organizations/{orgId}/usage");
var overageTask = FetchAsync<OverageSpendLimit>(http, $"https://claude.ai/api/organizations/{orgId}/overage_spend_limit");
var prepaidTask = FetchAsync<PrepaidCredits>(http, $"https://claude.ai/api/organizations/{orgId}/prepaid/credits");
await Task.WhenAll(usageTask, overageTask, prepaidTask);
limits = BuildLimits(usageTask.Result);
overage = overageTask.Result;
prepaid = prepaidTask.Result;
}
foreach (var limit in limits) foreach (var limit in limits)
{ {
@@ -131,11 +147,11 @@ public class UsageViewModel : INotifyPropertyChanged
await Application.Current.Dispatcher.InvokeAsync(() => await Application.Current.Dispatcher.InvokeAsync(() =>
{ {
Limits = limits; Limits = limits;
Overage = overageTask.Result; Overage = overage;
Prepaid = prepaidTask.Result; Prepaid = prepaid;
UserEmail = email ?? UserEmail; UserEmail = email ?? UserEmail;
IsSignedIn = true; IsSignedIn = true;
ErrorMessage = null; ErrorMessage = string.IsNullOrEmpty(orgId) ? "Signed in, but couldn't find your organization data." : null;
LastUpdated = DateTime.Now; LastUpdated = DateTime.Now;
IsLoading = false; IsLoading = false;
}); });
@@ -193,7 +209,9 @@ public class UsageViewModel : INotifyPropertyChanged
var handler = new HttpClientHandler { UseCookies = false }; var handler = new HttpClientHandler { UseCookies = false };
var http = new HttpClient(handler); var http = new HttpClient(handler);
http.DefaultRequestHeaders.Add("accept", "application/json"); http.DefaultRequestHeaders.Add("accept", "application/json");
http.DefaultRequestHeaders.Add("anthropic-client-platform", "web"); http.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36");
var cookieHeader = string.Join("; ", cookies.Select(c => $"{c.Name}={c.Value}")); var cookieHeader = string.Join("; ", cookies.Select(c => $"{c.Name}={c.Value}"));
http.DefaultRequestHeaders.Add("Cookie", cookieHeader); http.DefaultRequestHeaders.Add("Cookie", cookieHeader);
return http; return http;
@@ -211,17 +229,13 @@ public class UsageViewModel : INotifyPropertyChanged
catch { return null; } catch { return null; }
} }
// Returns (email, orgId). orgId null means auth failed. // Parse email and org ID out of bootstrap JSON (multiple fallback paths for org ID)
private static async Task<(string? Email, string? OrgId)> FetchBootstrapAsync(HttpClient http) private static (string? Email, string? OrgId) ParseBootstrap(string json)
{ {
try try
{ {
var resp = await http.GetAsync("https://claude.ai/api/bootstrap");
if (!resp.IsSuccessStatusCode) return (null, null);
var json = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json); using var doc = JsonDocument.Parse(json);
var root = doc.RootElement; var root = doc.RootElement;
string? email = null; string? email = null;
string? orgId = null; string? orgId = null;
@@ -230,7 +244,7 @@ public class UsageViewModel : INotifyPropertyChanged
acct.TryGetProperty("email_address", out var em)) acct.TryGetProperty("email_address", out var em))
email = em.GetString(); email = em.GetString();
// Try memberships[0].organization.uuid // Path 1: memberships[0].organization.uuid
if (root.TryGetProperty("memberships", out var mems) && mems.GetArrayLength() > 0) if (root.TryGetProperty("memberships", out var mems) && mems.GetArrayLength() > 0)
{ {
var first = mems[0]; var first = mems[0];
@@ -239,13 +253,13 @@ public class UsageViewModel : INotifyPropertyChanged
orgId = uuid.GetString(); orgId = uuid.GetString();
} }
// Fall back to cached org ID // Path 2: organizations[0].uuid (flat list on root)
if (string.IsNullOrEmpty(orgId) && !string.IsNullOrEmpty(AppSettings.Default.OrgId)) if (string.IsNullOrEmpty(orgId) &&
orgId = AppSettings.Default.OrgId; root.TryGetProperty("organizations", out var orgs) && orgs.GetArrayLength() > 0)
{
// Last resort: dedicated organizations endpoint if (orgs[0].TryGetProperty("uuid", out var uuid))
if (string.IsNullOrEmpty(orgId)) orgId = uuid.GetString();
orgId = await FetchOrgIdFromListAsync(http); }
return (email, orgId); return (email, orgId);
} }