|
|
@@ -20,7 +20,7 @@ public class UsageViewModel : INotifyPropertyChanged
|
|
|
|
private string? _errorMessage;
|
|
|
|
private string? _errorMessage;
|
|
|
|
private bool _isSignedIn;
|
|
|
|
private bool _isSignedIn;
|
|
|
|
private string _userEmail = "";
|
|
|
|
private string _userEmail = "";
|
|
|
|
private string _planLabel = "Pro";
|
|
|
|
private string _planLabel = "Claude";
|
|
|
|
private DateTime? _lastUpdated;
|
|
|
|
private DateTime? _lastUpdated;
|
|
|
|
private OverageSpendLimit? _overage;
|
|
|
|
private OverageSpendLimit? _overage;
|
|
|
|
private PrepaidCredits? _prepaid;
|
|
|
|
private PrepaidCredits? _prepaid;
|
|
|
@@ -100,9 +100,25 @@ public class UsageViewModel : INotifyPropertyChanged
|
|
|
|
// HttpClient refresh — WebView2 is NOT used here to avoid spawning a heavy
|
|
|
|
// HttpClient refresh — WebView2 is NOT used here to avoid spawning a heavy
|
|
|
|
// browser process every refresh cycle (causes memory accumulation).
|
|
|
|
// browser process every refresh cycle (causes memory accumulation).
|
|
|
|
// SaveAndClose at login time is responsible for fetching live data via WebView2.
|
|
|
|
// SaveAndClose at login time is responsible for fetching live data via WebView2.
|
|
|
|
var (limits, overage, prepaid, extraUsage, email, orgId, ok) = hasCookies
|
|
|
|
List<AgentLimit> limits = [];
|
|
|
|
? await TryHttpRefreshAsync(cookies)
|
|
|
|
OverageSpendLimit? overage = null;
|
|
|
|
: ([], null, null, null, null, null, false);
|
|
|
|
PrepaidCredits? prepaid = null;
|
|
|
|
|
|
|
|
ExtraUsage? extraUsage = null;
|
|
|
|
|
|
|
|
string? email = null, orgId = null, planLabel = null;
|
|
|
|
|
|
|
|
string? refreshError = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (hasCookies)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
(limits, overage, prepaid, extraUsage, email, orgId, planLabel, _) =
|
|
|
|
|
|
|
|
await TryHttpRefreshAsync(cookies);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
refreshError = ex.Message;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fill in any blanks from cached values saved at login time
|
|
|
|
// Fill in any blanks from cached values saved at login time
|
|
|
|
if (string.IsNullOrEmpty(email)) email = AppSettings.Default.Email;
|
|
|
|
if (string.IsNullOrEmpty(email)) email = AppSettings.Default.Email;
|
|
|
@@ -132,9 +148,10 @@ public class UsageViewModel : INotifyPropertyChanged
|
|
|
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
|
|
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (limits.Count > 0) { Limits = limits; Overage = overage; Prepaid = prepaid; ExtraUsage = extraUsage; }
|
|
|
|
if (limits.Count > 0) { Limits = limits; Overage = overage; Prepaid = prepaid; ExtraUsage = extraUsage; }
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(planLabel)) PlanLabel = planLabel;
|
|
|
|
if (!string.IsNullOrEmpty(email)) UserEmail = email;
|
|
|
|
if (!string.IsNullOrEmpty(email)) UserEmail = email;
|
|
|
|
IsSignedIn = true;
|
|
|
|
IsSignedIn = true;
|
|
|
|
ErrorMessage = null;
|
|
|
|
ErrorMessage = refreshError;
|
|
|
|
LastUpdated = DateTime.Now;
|
|
|
|
LastUpdated = DateTime.Now;
|
|
|
|
IsLoading = false;
|
|
|
|
IsLoading = false;
|
|
|
|
});
|
|
|
|
});
|
|
|
@@ -149,17 +166,24 @@ public class UsageViewModel : INotifyPropertyChanged
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<(List<AgentLimit>, OverageSpendLimit?, PrepaidCredits?, ExtraUsage?, string?, string?, bool)>
|
|
|
|
private async Task<(List<AgentLimit>, OverageSpendLimit?, PrepaidCredits?, ExtraUsage?, string?, string?, string?, bool)>
|
|
|
|
TryHttpRefreshAsync(List<(string Name, string Value, string Domain, string Path)> cookies)
|
|
|
|
TryHttpRefreshAsync(List<(string Name, string Value, string Domain, string Path)> cookies)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
try
|
|
|
|
try
|
|
|
|
{
|
|
|
|
{
|
|
|
|
using var http = BuildClient(cookies);
|
|
|
|
using var http = BuildClient(cookies);
|
|
|
|
var bootstrapResp = await http.GetAsync("https://claude.ai/api/bootstrap");
|
|
|
|
var bootstrapResp = await http.GetAsync("https://claude.ai/api/bootstrap");
|
|
|
|
|
|
|
|
if ((int)bootstrapResp.StatusCode is 401 or 403)
|
|
|
|
|
|
|
|
throw new Exception("Session expired — please re-authenticate.");
|
|
|
|
if (!bootstrapResp.IsSuccessStatusCode)
|
|
|
|
if (!bootstrapResp.IsSuccessStatusCode)
|
|
|
|
return ([], null, null, null, null, false);
|
|
|
|
throw new Exception($"Bootstrap failed ({(int)bootstrapResp.StatusCode}).");
|
|
|
|
|
|
|
|
|
|
|
|
var (email, orgId) = ParseBootstrap(await bootstrapResp.Content.ReadAsStringAsync());
|
|
|
|
var bootstrapJson = await bootstrapResp.Content.ReadAsStringAsync();
|
|
|
|
|
|
|
|
// Save raw bootstrap for diagnostics — lets us see if 'capabilities' is returned
|
|
|
|
|
|
|
|
AppSettings.Default.DebugInfo = bootstrapJson.Length > 2000
|
|
|
|
|
|
|
|
? bootstrapJson.Substring(0, 2000) : bootstrapJson;
|
|
|
|
|
|
|
|
AppSettings.Default.Save();
|
|
|
|
|
|
|
|
var (email, orgId, planLabel) = ParseBootstrap(bootstrapJson);
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(orgId))
|
|
|
|
if (string.IsNullOrEmpty(orgId))
|
|
|
|
orgId = await FetchOrgIdFromListAsync(http);
|
|
|
|
orgId = await FetchOrgIdFromListAsync(http);
|
|
|
@@ -167,19 +191,34 @@ public class UsageViewModel : INotifyPropertyChanged
|
|
|
|
orgId = AppSettings.Default.OrgId;
|
|
|
|
orgId = AppSettings.Default.OrgId;
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(orgId))
|
|
|
|
if (string.IsNullOrEmpty(orgId))
|
|
|
|
return ([], null, null, null, email, orgId, true);
|
|
|
|
return ([], null, null, null, email, orgId, planLabel, true);
|
|
|
|
|
|
|
|
|
|
|
|
AppSettings.Default.OrgId = orgId;
|
|
|
|
AppSettings.Default.OrgId = orgId;
|
|
|
|
AppSettings.Default.Save();
|
|
|
|
AppSettings.Default.Save();
|
|
|
|
|
|
|
|
|
|
|
|
var ut = FetchAsync<UsageResponse>(http, $"https://claude.ai/api/organizations/{orgId}/usage");
|
|
|
|
var usageUrl = $"https://claude.ai/api/organizations/{orgId}/usage";
|
|
|
|
|
|
|
|
var ut = http.GetAsync(usageUrl);
|
|
|
|
var ot = FetchAsync<OverageSpendLimit>(http, $"https://claude.ai/api/organizations/{orgId}/overage_spend_limit");
|
|
|
|
var ot = FetchAsync<OverageSpendLimit>(http, $"https://claude.ai/api/organizations/{orgId}/overage_spend_limit");
|
|
|
|
var pt = FetchAsync<PrepaidCredits>(http, $"https://claude.ai/api/organizations/{orgId}/prepaid/credits");
|
|
|
|
var pt = FetchAsync<PrepaidCredits>(http, $"https://claude.ai/api/organizations/{orgId}/prepaid/credits");
|
|
|
|
await Task.WhenAll(ut, ot, pt);
|
|
|
|
await Task.WhenAll(ut, ot, pt);
|
|
|
|
|
|
|
|
|
|
|
|
return (BuildLimits(ut.Result), ot.Result, pt.Result, ut.Result?.ExtraUsage, email, orgId, true);
|
|
|
|
var usageResp = ut.Result;
|
|
|
|
|
|
|
|
if (!usageResp.IsSuccessStatusCode)
|
|
|
|
|
|
|
|
throw new Exception($"Usage fetch failed ({(int)usageResp.StatusCode}).");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var usageJson = await usageResp.Content.ReadAsStringAsync();
|
|
|
|
|
|
|
|
var usage = JsonSerializer.Deserialize<UsageResponse>(usageJson, JsonOpts);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Persist fresh usage so cache reflects live data
|
|
|
|
|
|
|
|
AppSettings.Default.UsageJson = usageJson;
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(planLabel)) AppSettings.Default.PlanLabel = planLabel;
|
|
|
|
|
|
|
|
if (ot.Result != null) AppSettings.Default.OverageJson = JsonSerializer.Serialize(ot.Result);
|
|
|
|
|
|
|
|
if (pt.Result != null) AppSettings.Default.PrepaidJson = JsonSerializer.Serialize(pt.Result);
|
|
|
|
|
|
|
|
AppSettings.Default.Save();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (BuildLimits(usage), ot.Result, pt.Result, usage?.ExtraUsage, email, orgId, planLabel, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch { return ([], null, null, null, null, null, false); }
|
|
|
|
catch { throw; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static async Task<(List<AgentLimit>, string?, string?)> TryWebView2RefreshAsync()
|
|
|
|
private static async Task<(List<AgentLimit>, string?, string?)> TryWebView2RefreshAsync()
|
|
|
@@ -255,22 +294,48 @@ public class UsageViewModel : INotifyPropertyChanged
|
|
|
|
if (!isAuth) return;
|
|
|
|
if (!isAuth) return;
|
|
|
|
|
|
|
|
|
|
|
|
List<AgentLimit> limits = [];
|
|
|
|
List<AgentLimit> limits = [];
|
|
|
|
|
|
|
|
ExtraUsage? extraUsage = null;
|
|
|
|
|
|
|
|
OverageSpendLimit? overage = null;
|
|
|
|
|
|
|
|
PrepaidCredits? prepaid = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(AppSettings.Default.UsageJson))
|
|
|
|
if (!string.IsNullOrEmpty(AppSettings.Default.UsageJson))
|
|
|
|
{
|
|
|
|
{
|
|
|
|
try
|
|
|
|
try
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var cached = JsonSerializer.Deserialize<UsageResponse>(
|
|
|
|
var cached = JsonSerializer.Deserialize<UsageResponse>(
|
|
|
|
AppSettings.Default.UsageJson, JsonOpts);
|
|
|
|
AppSettings.Default.UsageJson, JsonOpts);
|
|
|
|
limits = BuildLimits(cached);
|
|
|
|
limits = BuildLimits(cached);
|
|
|
|
|
|
|
|
extraUsage = cached?.ExtraUsage;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch { }
|
|
|
|
catch { }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(AppSettings.Default.OverageJson))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
try { overage = JsonSerializer.Deserialize<OverageSpendLimit>(AppSettings.Default.OverageJson, JsonOpts); }
|
|
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(AppSettings.Default.PrepaidJson))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
try { prepaid = JsonSerializer.Deserialize<PrepaidCredits>(AppSettings.Default.PrepaidJson, JsonOpts); }
|
|
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Attach persisted burn history to cached limits
|
|
|
|
|
|
|
|
foreach (var limit in limits)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var key = limit.Window.ToString();
|
|
|
|
|
|
|
|
if (_burnHistory.TryGetValue(key, out var hist) && hist.Count > 0)
|
|
|
|
|
|
|
|
limit.BurnHistory = [.. hist];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
|
|
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (!string.IsNullOrEmpty(email)) UserEmail = email;
|
|
|
|
if (!string.IsNullOrEmpty(email)) UserEmail = email;
|
|
|
|
// Always show cards — real data if available, placeholders if not
|
|
|
|
if (!string.IsNullOrEmpty(AppSettings.Default.PlanLabel)) PlanLabel = AppSettings.Default.PlanLabel;
|
|
|
|
Limits = limits.Count > 0 ? limits : LoadPlaceholderLimits();
|
|
|
|
Limits = limits.Count > 0 ? limits : LoadPlaceholderLimits();
|
|
|
|
|
|
|
|
Overage = overage;
|
|
|
|
|
|
|
|
Prepaid = prepaid;
|
|
|
|
|
|
|
|
ExtraUsage = extraUsage;
|
|
|
|
IsSignedIn = true;
|
|
|
|
IsSignedIn = true;
|
|
|
|
ErrorMessage = null;
|
|
|
|
ErrorMessage = null;
|
|
|
|
LastUpdated = limits.Count > 0 ? DateTime.Now : LastUpdated;
|
|
|
|
LastUpdated = limits.Count > 0 ? DateTime.Now : LastUpdated;
|
|
|
@@ -351,7 +416,7 @@ public class UsageViewModel : INotifyPropertyChanged
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse email and org ID out of bootstrap JSON (multiple fallback paths for org ID)
|
|
|
|
// Parse email and org ID out of bootstrap JSON (multiple fallback paths for org ID)
|
|
|
|
private static (string? Email, string? OrgId) ParseBootstrap(string json)
|
|
|
|
private static (string? Email, string? OrgId, string? PlanLabel) ParseBootstrap(string json)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
try
|
|
|
|
try
|
|
|
|
{
|
|
|
|
{
|
|
|
@@ -393,9 +458,28 @@ public class UsageViewModel : INotifyPropertyChanged
|
|
|
|
orgId = uuid.GetString();
|
|
|
|
orgId = uuid.GetString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (email, orgId);
|
|
|
|
string? planLabel = null;
|
|
|
|
|
|
|
|
if (root.TryGetProperty("account", out var acctPlan) &&
|
|
|
|
|
|
|
|
acctPlan.TryGetProperty("memberships", out var plMems) && plMems.GetArrayLength() > 0 &&
|
|
|
|
|
|
|
|
plMems[0].TryGetProperty("organization", out var plOrg) &&
|
|
|
|
|
|
|
|
plOrg.TryGetProperty("capabilities", out var caps) &&
|
|
|
|
|
|
|
|
caps.ValueKind == System.Text.Json.JsonValueKind.Array)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
foreach (var cap in caps.EnumerateArray())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var s = cap.GetString() ?? "";
|
|
|
|
|
|
|
|
if (s.StartsWith("claude_", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var name = s.Substring("claude_".Length);
|
|
|
|
|
|
|
|
if (name.Length > 0)
|
|
|
|
|
|
|
|
planLabel = char.ToUpper(name[0]) + name.Substring(1).ToLower();
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return (email, orgId, planLabel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch { return (null, null); }
|
|
|
|
catch { return (null, null, null); }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static async Task<string?> FetchOrgIdFromListAsync(HttpClient http)
|
|
|
|
private static async Task<string?> FetchOrgIdFromListAsync(HttpClient http)
|
|
|
|