port multi-org fix and add diagnostics panel to Windows app
Read lastActiveOrg cookie in both the browser JS path and HttpClient fallback, then prefer the matching org in ParseBootstrap for both org ID and plan label selection — same fix as macOS v1.3.0. Add Settings → Diagnostics panel showing app state, last request details, cookie store counts/domains, and stored cookie names.
This commit is contained in:
@@ -150,7 +150,7 @@
|
|||||||
<TextBlock Text="ABOUT" Style="{StaticResource HeaderText}" Margin="0,0,0,8"/>
|
<TextBlock Text="ABOUT" Style="{StaticResource HeaderText}" Margin="0,0,0,8"/>
|
||||||
<Border Background="{DynamicResource CardBrush}" CornerRadius="6"
|
<Border Background="{DynamicResource CardBrush}" CornerRadius="6"
|
||||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
|
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
|
||||||
Padding="14,12">
|
Padding="14,12" Margin="0,0,0,16">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
@@ -185,6 +185,19 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="DIAGNOSTICS" Style="{StaticResource HeaderText}" Margin="0,0,0,8"/>
|
||||||
|
<Border Background="{DynamicResource CardBrush}" CornerRadius="6"
|
||||||
|
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
|
||||||
|
Padding="14,12">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="View app state, last request details, and stored cookies."
|
||||||
|
FontSize="11" Foreground="{DynamicResource SecondaryBrush}"
|
||||||
|
TextWrapping="Wrap" Margin="0,0,0,10"/>
|
||||||
|
<Button Content="Open diagnostics" Style="{StaticResource SmallButton}"
|
||||||
|
HorizontalAlignment="Left" Click="Diagnostics_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -250,5 +263,30 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Diagnostics panel -->
|
||||||
|
<Border x:Name="DiagnosticsPanel" Visibility="Collapsed"
|
||||||
|
Background="{DynamicResource BackgroundBrush}">
|
||||||
|
<DockPanel>
|
||||||
|
<Border DockPanel.Dock="Top" BorderBrush="{DynamicResource BorderBrush}"
|
||||||
|
BorderThickness="0,0,0,1" Background="{DynamicResource SurfaceBrush}" Padding="12,10">
|
||||||
|
<Grid>
|
||||||
|
<Button Content="← Back" HorizontalAlignment="Left"
|
||||||
|
Style="{StaticResource SmallButton}" BorderThickness="0"
|
||||||
|
Foreground="{DynamicResource BlueBrush}" Background="Transparent"
|
||||||
|
Click="BackFromDiag_Click"/>
|
||||||
|
<TextBlock Text="Diagnostics" FontSize="14" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource TextBrush}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
<Button x:Name="CopyDiagButton" Content="Copy All" HorizontalAlignment="Right"
|
||||||
|
Style="{StaticResource SmallButton}" Click="CopyDiag_Click"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel x:Name="DiagContentPanel" Margin="16,4,16,16"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
+189
-9
@@ -1,6 +1,10 @@
|
|||||||
using ClaudeCheckerWindows.Controls;
|
using ClaudeCheckerWindows.Controls;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
@@ -463,23 +467,198 @@ public partial class PopupWindow : Window
|
|||||||
SignInButton.Content = VM.IsSignedIn ? "Re-authenticate" : "Sign In";
|
SignInButton.Content = VM.IsSignedIn ? "Re-authenticate" : "Sign In";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Diagnostics ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void Diagnostics_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RebuildDiag();
|
||||||
|
MainPanel.Visibility = Visibility.Collapsed;
|
||||||
|
SettingsPanel.Visibility = Visibility.Collapsed;
|
||||||
|
UpdatePanel.Visibility = Visibility.Collapsed;
|
||||||
|
DiagnosticsPanel.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BackFromDiag_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DiagnosticsPanel.Visibility = Visibility.Collapsed;
|
||||||
|
SettingsPanel.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyDiag_Click(object s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Clipboard.SetText(BuildDiagText());
|
||||||
|
CopyDiagButton.Content = "Copied!";
|
||||||
|
var t = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) };
|
||||||
|
t.Tick += (_, _) => { CopyDiagButton.Content = "Copy All"; t.Stop(); };
|
||||||
|
t.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildDiag()
|
||||||
|
{
|
||||||
|
DiagContentPanel.Children.Clear();
|
||||||
|
var secondary = (SolidColorBrush)Application.Current.Resources["SecondaryBrush"];
|
||||||
|
var border = (SolidColorBrush)Application.Current.Resources["BorderBrush"];
|
||||||
|
var text = (SolidColorBrush)Application.Current.Resources["TextBrush"];
|
||||||
|
|
||||||
|
FrameworkElement MakeRow(string label, string value)
|
||||||
|
{
|
||||||
|
var grid = new Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(110) });
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
var lbl = new TextBlock
|
||||||
|
{
|
||||||
|
Text = label, FontSize = 11, Foreground = secondary,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
};
|
||||||
|
var val = new TextBlock
|
||||||
|
{
|
||||||
|
Text = value, FontSize = 11, FontFamily = new FontFamily("Consolas"),
|
||||||
|
Foreground = text, TextWrapping = TextWrapping.Wrap,
|
||||||
|
};
|
||||||
|
Grid.SetColumn(lbl, 0);
|
||||||
|
Grid.SetColumn(val, 1);
|
||||||
|
grid.Children.Add(lbl);
|
||||||
|
grid.Children.Add(val);
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddSection(string title, IEnumerable<FrameworkElement> rows)
|
||||||
|
{
|
||||||
|
DiagContentPanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = title, Style = (Style)Application.Current.Resources["HeaderText"],
|
||||||
|
Margin = new Thickness(0, 12, 0, 6),
|
||||||
|
});
|
||||||
|
var panel = new StackPanel { Margin = new Thickness(12, 10, 12, 10) };
|
||||||
|
foreach (var row in rows) panel.Children.Add(row);
|
||||||
|
DiagContentPanel.Children.Add(new Border
|
||||||
|
{
|
||||||
|
Background = (SolidColorBrush)Application.Current.Resources["CardBrush"],
|
||||||
|
CornerRadius = new CornerRadius(6),
|
||||||
|
BorderBrush = border, BorderThickness = new Thickness(1),
|
||||||
|
Child = panel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// APP
|
||||||
|
AddSection("APP",
|
||||||
|
[
|
||||||
|
MakeRow("Version", Updater.CurrentVersion),
|
||||||
|
MakeRow("Signed in", VM.IsSignedIn ? "Yes" : "No"),
|
||||||
|
MakeRow("Email", string.IsNullOrEmpty(VM.UserEmail) ? "(none)" : VM.UserEmail),
|
||||||
|
MakeRow("Org ID", string.IsNullOrEmpty(VM.DiagOrgId) ? "(none)" : VM.DiagOrgId),
|
||||||
|
MakeRow("lastActiveOrg", string.IsNullOrEmpty(VM.DiagLastActiveOrg) ? "(not read)" : VM.DiagLastActiveOrg),
|
||||||
|
MakeRow("Error", VM.ErrorMessage ?? "(none)"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// LAST REQUEST
|
||||||
|
var reqRows = new List<FrameworkElement>
|
||||||
|
{
|
||||||
|
MakeRow("Path", string.IsNullOrEmpty(VM.DiagLastPath) ? "(none)" : VM.DiagLastPath),
|
||||||
|
MakeRow("Status", VM.DiagLastStatus == 0 ? "(none)" : VM.DiagLastStatus.ToString()),
|
||||||
|
MakeRow("Error", string.IsNullOrEmpty(VM.DiagLastError) ? "(none)" : VM.DiagLastError),
|
||||||
|
};
|
||||||
|
if (VM.DiagLastFetch.HasValue)
|
||||||
|
reqRows.Add(MakeRow("Time", VM.DiagLastFetch.Value.ToString("HH:mm:ss")));
|
||||||
|
AddSection("LAST REQUEST", reqRows);
|
||||||
|
|
||||||
|
// COOKIE STORE
|
||||||
|
AddSection("COOKIE STORE",
|
||||||
|
[
|
||||||
|
MakeRow("Total cookies", VM.DiagCookieCount.ToString()),
|
||||||
|
MakeRow("Claude cookies", VM.DiagClaudeCookieCount.ToString()),
|
||||||
|
MakeRow("All domains", VM.DiagCookieDomains.Count == 0
|
||||||
|
? "(none)" : string.Join(", ", VM.DiagCookieDomains)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// STORED COOKIE NAMES
|
||||||
|
var storedNames = GetStoredCookieNames();
|
||||||
|
var nameRows = new List<FrameworkElement>();
|
||||||
|
if (storedNames.Count == 0)
|
||||||
|
{
|
||||||
|
nameRows.Add(MakeRow("(none)", ""));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var claudeC = storedNames.Where(c =>
|
||||||
|
c.Domain.Contains("claude.ai") || c.Domain.Contains("anthropic.com")).ToList();
|
||||||
|
var otherC = storedNames.Where(c =>
|
||||||
|
!c.Domain.Contains("claude.ai") && !c.Domain.Contains("anthropic.com")).ToList();
|
||||||
|
foreach (var c in claudeC)
|
||||||
|
nameRows.Add(MakeRow(c.Domain, c.Name));
|
||||||
|
if (otherC.Count > 0)
|
||||||
|
nameRows.Add(MakeRow($"other ({otherC.Count})",
|
||||||
|
string.Join(", ", otherC.Take(5).Select(c => c.Name))));
|
||||||
|
}
|
||||||
|
AddSection("STORED COOKIE NAMES", nameRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<(string Name, string Domain)> GetStoredCookieNames()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var raw = AppSettings.Default.CookieStore;
|
||||||
|
if (string.IsNullOrEmpty(raw)) return [];
|
||||||
|
using var doc = JsonDocument.Parse(raw);
|
||||||
|
var list = new List<(string Name, string Domain)>();
|
||||||
|
foreach (var elem in doc.RootElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
var name = elem.TryGetProperty("Name", out var n) ? n.GetString() ?? "" : "";
|
||||||
|
var domain = elem.TryGetProperty("Domain", out var d) ? d.GetString() ?? "" : "";
|
||||||
|
list.Add((name, domain));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildDiagText()
|
||||||
|
{
|
||||||
|
var storedNames = GetStoredCookieNames();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("=== ClaudeChecker Diagnostics (Windows) ===");
|
||||||
|
sb.AppendLine($"Version: {Updater.CurrentVersion}");
|
||||||
|
sb.AppendLine($"Signed in: {(VM.IsSignedIn ? "Yes" : "No")}");
|
||||||
|
sb.AppendLine($"Email: {(string.IsNullOrEmpty(VM.UserEmail) ? "(none)" : VM.UserEmail)}");
|
||||||
|
sb.AppendLine($"Org ID: {(string.IsNullOrEmpty(VM.DiagOrgId) ? "(none)" : VM.DiagOrgId)}");
|
||||||
|
sb.AppendLine($"lastActiveOrg: {(string.IsNullOrEmpty(VM.DiagLastActiveOrg) ? "(not read)" : VM.DiagLastActiveOrg)}");
|
||||||
|
sb.AppendLine($"Error: {VM.ErrorMessage ?? "(none)"}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Last path: {VM.DiagLastPath}");
|
||||||
|
sb.AppendLine($"Last status: {VM.DiagLastStatus}");
|
||||||
|
sb.AppendLine($"Last error: {VM.DiagLastError}");
|
||||||
|
if (VM.DiagLastFetch.HasValue)
|
||||||
|
sb.AppendLine($"Last fetch: {VM.DiagLastFetch.Value:HH:mm:ss}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Total cookies: {VM.DiagCookieCount}");
|
||||||
|
sb.AppendLine($"Claude cookies: {VM.DiagClaudeCookieCount}");
|
||||||
|
sb.AppendLine($"Domains: {string.Join(", ", VM.DiagCookieDomains)}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Stored cookies:");
|
||||||
|
foreach (var c in storedNames)
|
||||||
|
sb.AppendLine($" {c.Domain} {c.Name}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Event handlers ───────────────────────────────────────────────
|
// ── Event handlers ───────────────────────────────────────────────
|
||||||
|
|
||||||
private void Settings_Click(object s, RoutedEventArgs e)
|
private void Settings_Click(object s, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
InitSettings();
|
InitSettings();
|
||||||
MainPanel.Visibility = Visibility.Collapsed;
|
MainPanel.Visibility = Visibility.Collapsed;
|
||||||
SettingsPanel.Visibility = Visibility.Visible;
|
SettingsPanel.Visibility = Visibility.Visible;
|
||||||
UpdatePanel.Visibility = Visibility.Collapsed;
|
UpdatePanel.Visibility = Visibility.Collapsed;
|
||||||
|
DiagnosticsPanel.Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BackFromSettings_Click(object s, RoutedEventArgs e) => ShowMain();
|
private void BackFromSettings_Click(object s, RoutedEventArgs e) => ShowMain();
|
||||||
|
|
||||||
private void ShowMain()
|
private void ShowMain()
|
||||||
{
|
{
|
||||||
MainPanel.Visibility = Visibility.Visible;
|
MainPanel.Visibility = Visibility.Visible;
|
||||||
SettingsPanel.Visibility = Visibility.Collapsed;
|
SettingsPanel.Visibility = Visibility.Collapsed;
|
||||||
UpdatePanel.Visibility = Visibility.Collapsed;
|
UpdatePanel.Visibility = Visibility.Collapsed;
|
||||||
|
DiagnosticsPanel.Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Refresh_Click(object s, RoutedEventArgs e) => await VM.RefreshAsync();
|
private async void Refresh_Click(object s, RoutedEventArgs e) => await VM.RefreshAsync();
|
||||||
@@ -491,9 +670,10 @@ public partial class PopupWindow : Window
|
|||||||
CurrentVerLabel.Text = $"v{Updater.CurrentVersion}";
|
CurrentVerLabel.Text = $"v{Updater.CurrentVersion}";
|
||||||
NewVerLabel.Text = $"v{Updater.LatestVersion}";
|
NewVerLabel.Text = $"v{Updater.LatestVersion}";
|
||||||
ReleaseNotesText.Text = Updater.ReleaseNotes;
|
ReleaseNotesText.Text = Updater.ReleaseNotes;
|
||||||
MainPanel.Visibility = Visibility.Collapsed;
|
MainPanel.Visibility = Visibility.Collapsed;
|
||||||
SettingsPanel.Visibility = Visibility.Collapsed;
|
SettingsPanel.Visibility = Visibility.Collapsed;
|
||||||
UpdatePanel.Visibility = Visibility.Visible;
|
UpdatePanel.Visibility = Visibility.Visible;
|
||||||
|
DiagnosticsPanel.Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseUpdate_Click(object s, RoutedEventArgs e) => ShowMain();
|
private void CloseUpdate_Click(object s, RoutedEventArgs e) => ShowMain();
|
||||||
|
|||||||
+89
-28
@@ -40,6 +40,17 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
public PrepaidCredits? Prepaid { get => _prepaid; set => Set(ref _prepaid, value); }
|
public PrepaidCredits? Prepaid { get => _prepaid; set => Set(ref _prepaid, value); }
|
||||||
public ExtraUsage? ExtraUsage { get => _extraUsage; set => Set(ref _extraUsage, value); }
|
public ExtraUsage? ExtraUsage { get => _extraUsage; set => Set(ref _extraUsage, value); }
|
||||||
|
|
||||||
|
// Diagnostics — updated after each refresh, read by DiagnosticsPanel on demand
|
||||||
|
public string DiagLastActiveOrg { get; private set; } = "";
|
||||||
|
public string DiagOrgId { get; private set; } = "";
|
||||||
|
public string DiagLastPath { get; private set; } = "";
|
||||||
|
public int DiagLastStatus { get; private set; }
|
||||||
|
public string DiagLastError { get; private set; } = "";
|
||||||
|
public int DiagCookieCount { get; private set; }
|
||||||
|
public int DiagClaudeCookieCount { get; private set; }
|
||||||
|
public List<string> DiagCookieDomains { get; private set; } = [];
|
||||||
|
public DateTime? DiagLastFetch { get; private set; }
|
||||||
|
|
||||||
private int _refreshInterval = 120;
|
private int _refreshInterval = 120;
|
||||||
public int RefreshInterval
|
public int RefreshInterval
|
||||||
{
|
{
|
||||||
@@ -77,6 +88,9 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
{
|
{
|
||||||
await Application.Current.Dispatcher.InvokeAsync(() => IsLoading = true);
|
await Application.Current.Dispatcher.InvokeAsync(() => IsLoading = true);
|
||||||
|
|
||||||
|
// Snapshot stored cookies once — used for both diag and HttpClient fallback path.
|
||||||
|
var storedCookies = await GetCookiesAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// CookieStore being non-empty is our signal that the user has signed in.
|
// CookieStore being non-empty is our signal that the user has signed in.
|
||||||
@@ -115,13 +129,12 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var cookies = await GetCookiesAsync();
|
if (storedCookies.Count > 0)
|
||||||
if (cookies.Count > 0)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(limits, overage, prepaid, extraUsage, email, orgId, planLabel, _) =
|
(limits, overage, prepaid, extraUsage, email, orgId, planLabel, _) =
|
||||||
await TryHttpRefreshAsync(cookies);
|
await TryHttpRefreshAsync(storedCookies);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -156,6 +169,16 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
ErrorMessage = refreshError;
|
ErrorMessage = refreshError;
|
||||||
LastUpdated = DateTime.Now;
|
LastUpdated = DateTime.Now;
|
||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
|
|
||||||
|
// Update diagnostics
|
||||||
|
if (!string.IsNullOrEmpty(orgId)) DiagOrgId = orgId;
|
||||||
|
DiagLastFetch = DateTime.Now;
|
||||||
|
DiagLastError = refreshError ?? "";
|
||||||
|
DiagCookieCount = storedCookies.Count;
|
||||||
|
DiagClaudeCookieCount = storedCookies.Count(c =>
|
||||||
|
c.Domain.Contains("claude.ai") || c.Domain.Contains("anthropic.com"));
|
||||||
|
DiagCookieDomains = storedCookies
|
||||||
|
.Select(c => c.Domain).Distinct().OrderBy(d => d).ToList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -174,14 +197,19 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
private async Task<(List<AgentLimit>, OverageSpendLimit?, PrepaidCredits?, ExtraUsage?, string?, string?, string?)>
|
private async Task<(List<AgentLimit>, OverageSpendLimit?, PrepaidCredits?, ExtraUsage?, string?, string?, string?)>
|
||||||
TryBrowserRefreshAsync()
|
TryBrowserRefreshAsync()
|
||||||
{
|
{
|
||||||
|
// Read lastActiveOrg from document.cookie — the only reliable org source when the
|
||||||
|
// user belongs to multiple orgs. memberships[0] is always the "Individual Org" and
|
||||||
|
// would return 403 on usage endpoints if the paid plan is on a different org.
|
||||||
const string script = @"(async()=>{try{
|
const string script = @"(async()=>{try{
|
||||||
const h={headers:{accept:'application/json'}};
|
const h={headers:{accept:'application/json'}};
|
||||||
const b=await(await fetch('/api/bootstrap',h)).json();
|
const b=await(await fetch('/api/bootstrap',h)).json();
|
||||||
if(b?.error_type==='authentication_error'){window.chrome.webview.postMessage({authError:true});return;}
|
if(b?.error_type==='authentication_error'){window.chrome.webview.postMessage({authError:true});return;}
|
||||||
const org0=b?.account?.memberships?.[0]?.organization;
|
const lastActiveOrg=(document.cookie.split(';').map(c=>c.trim().split('=')).find(p=>p[0]==='lastActiveOrg')||[])[1]||null;
|
||||||
let id=org0?.uuid||b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid||null;
|
const allOrgs=(b?.account?.memberships||b?.memberships||[]).map(m=>m?.organization).filter(Boolean);
|
||||||
|
const activeOrg=(lastActiveOrg?allOrgs.find(o=>o?.uuid===lastActiveOrg):null)||allOrgs[0];
|
||||||
|
let id=activeOrg?.uuid||b?.organizations?.[0]?.uuid||null;
|
||||||
const email=b?.account?.email_address||b?.account?.email||null;
|
const email=b?.account?.email_address||b?.account?.email||null;
|
||||||
const caps=org0?.capabilities||[];
|
const caps=activeOrg?.capabilities||[];
|
||||||
const capStr=caps.find(c=>typeof c==='string'&&c.startsWith('claude_'))||null;
|
const capStr=caps.find(c=>typeof c==='string'&&c.startsWith('claude_'))||null;
|
||||||
const planLabel=capStr?(capStr.slice(7,8).toUpperCase()+capStr.slice(8).toLowerCase()):null;
|
const planLabel=capStr?(capStr.slice(7,8).toUpperCase()+capStr.slice(8).toLowerCase()):null;
|
||||||
if(!id){
|
if(!id){
|
||||||
@@ -194,7 +222,7 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
fetch('/api/organizations/'+id+'/overage_spend_limit',h).then(r=>r.ok?r.json():null).catch(()=>null),
|
fetch('/api/organizations/'+id+'/overage_spend_limit',h).then(r=>r.ok?r.json():null).catch(()=>null),
|
||||||
fetch('/api/organizations/'+id+'/prepaid/credits',h).then(r=>r.ok?r.json():null).catch(()=>null)
|
fetch('/api/organizations/'+id+'/prepaid/credits',h).then(r=>r.ok?r.json():null).catch(()=>null)
|
||||||
]);
|
]);
|
||||||
window.chrome.webview.postMessage({email,orgId:id,planLabel,usage:u,overage:ov,prepaid:pp});
|
window.chrome.webview.postMessage({email,orgId:id,planLabel,lastActiveOrg,usage:u,overage:ov,prepaid:pp});
|
||||||
}catch(ex){window.chrome.webview.postMessage({error:String(ex)});}})()";
|
}catch(ex){window.chrome.webview.postMessage({error:String(ex)});}})()";
|
||||||
|
|
||||||
var json = await Application.Current.Dispatcher.InvokeAsync(
|
var json = await Application.Current.Dispatcher.InvokeAsync(
|
||||||
@@ -216,6 +244,11 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
string? email = root.TryGetProperty("email", out var em) && em.ValueKind == JsonValueKind.String ? em.GetString() : null;
|
string? email = root.TryGetProperty("email", out var em) && em.ValueKind == JsonValueKind.String ? em.GetString() : null;
|
||||||
string? orgId = root.TryGetProperty("orgId", out var oi) && oi.ValueKind == JsonValueKind.String ? oi.GetString() : null;
|
string? orgId = root.TryGetProperty("orgId", out var oi) && oi.ValueKind == JsonValueKind.String ? oi.GetString() : null;
|
||||||
string? planLabel = root.TryGetProperty("planLabel", out var pl) && pl.ValueKind == JsonValueKind.String ? pl.GetString() : null;
|
string? planLabel = root.TryGetProperty("planLabel", out var pl) && pl.ValueKind == JsonValueKind.String ? pl.GetString() : null;
|
||||||
|
string? lao = root.TryGetProperty("lastActiveOrg", out var laoProp) && laoProp.ValueKind == JsonValueKind.String ? laoProp.GetString() : null;
|
||||||
|
|
||||||
|
DiagLastActiveOrg = lao ?? "";
|
||||||
|
DiagLastPath = $"/api/bootstrap + /api/organizations/…/usage (browser)";
|
||||||
|
DiagLastStatus = 200;
|
||||||
|
|
||||||
UsageResponse? usage = null;
|
UsageResponse? usage = null;
|
||||||
OverageSpendLimit? overage = null;
|
OverageSpendLimit? overage = null;
|
||||||
@@ -236,15 +269,22 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
private async Task<(List<AgentLimit>, OverageSpendLimit?, PrepaidCredits?, ExtraUsage?, string?, 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)
|
||||||
{
|
{
|
||||||
|
// Prefer the org the user last had active, not necessarily memberships[0].
|
||||||
|
var lastActiveOrg = cookies.FirstOrDefault(c => c.Name == "lastActiveOrg").Value;
|
||||||
|
DiagLastActiveOrg = lastActiveOrg ?? "";
|
||||||
|
|
||||||
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");
|
||||||
|
DiagLastPath = "/api/bootstrap";
|
||||||
|
DiagLastStatus = (int)bootstrapResp.StatusCode;
|
||||||
if ((int)bootstrapResp.StatusCode is 401 or 403)
|
if ((int)bootstrapResp.StatusCode is 401 or 403)
|
||||||
throw new Exception("Session expired — please re-authenticate.");
|
throw new Exception("Session expired — please re-authenticate.");
|
||||||
if (!bootstrapResp.IsSuccessStatusCode)
|
if (!bootstrapResp.IsSuccessStatusCode)
|
||||||
throw new Exception($"Bootstrap failed ({(int)bootstrapResp.StatusCode}).");
|
throw new Exception($"Bootstrap failed ({(int)bootstrapResp.StatusCode}).");
|
||||||
|
|
||||||
var bootstrapJson = await bootstrapResp.Content.ReadAsStringAsync();
|
var bootstrapJson = await bootstrapResp.Content.ReadAsStringAsync();
|
||||||
var (email, orgId, planLabel) = ParseBootstrap(bootstrapJson);
|
var (email, orgId, planLabel) = ParseBootstrap(bootstrapJson, lastActiveOrg);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(orgId))
|
if (string.IsNullOrEmpty(orgId))
|
||||||
orgId = await FetchOrgIdFromListAsync(http);
|
orgId = await FetchOrgIdFromListAsync(http);
|
||||||
@@ -258,6 +298,8 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
await Task.WhenAll(ut, ot, pt);
|
await Task.WhenAll(ut, ot, pt);
|
||||||
|
|
||||||
var usageResp = ut.Result;
|
var usageResp = ut.Result;
|
||||||
|
DiagLastPath = $"/api/organizations/{orgId}/usage";
|
||||||
|
DiagLastStatus = (int)usageResp.StatusCode;
|
||||||
if (!usageResp.IsSuccessStatusCode)
|
if (!usageResp.IsSuccessStatusCode)
|
||||||
throw new Exception($"Usage fetch failed ({(int)usageResp.StatusCode}).");
|
throw new Exception($"Usage fetch failed ({(int)usageResp.StatusCode}).");
|
||||||
|
|
||||||
@@ -361,7 +403,11 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
catch { return null; }
|
catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (string? Email, string? OrgId, string? PlanLabel) ParseBootstrap(string json)
|
// Parses bootstrap JSON and returns email, org ID, and plan label.
|
||||||
|
// preferredOrgId (from the lastActiveOrg cookie) selects the correct org when the
|
||||||
|
// user belongs to multiple — memberships[0] is always the "Individual Org" which
|
||||||
|
// returns 403 on usage endpoints if the paid plan is on a different org.
|
||||||
|
private static (string? Email, string? OrgId, string? PlanLabel) ParseBootstrap(string json, string? preferredOrgId = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -369,42 +415,57 @@ public class UsageViewModel : INotifyPropertyChanged
|
|||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
||||||
string? email = null;
|
string? email = null;
|
||||||
string? orgId = null;
|
|
||||||
|
|
||||||
if (root.TryGetProperty("account", out var acct) &&
|
if (root.TryGetProperty("account", out var acct) &&
|
||||||
acct.TryGetProperty("email_address", out var em))
|
acct.TryGetProperty("email_address", out var em))
|
||||||
email = em.GetString();
|
email = em.GetString();
|
||||||
|
|
||||||
|
// Collect all org objects from memberships
|
||||||
|
var allOrgs = new List<JsonElement>();
|
||||||
if (root.TryGetProperty("account", out var acctNode) &&
|
if (root.TryGetProperty("account", out var acctNode) &&
|
||||||
acctNode.TryGetProperty("memberships", out var mems) && mems.GetArrayLength() > 0)
|
acctNode.TryGetProperty("memberships", out var mems))
|
||||||
{
|
{
|
||||||
var first = mems[0];
|
foreach (var mem in mems.EnumerateArray())
|
||||||
if (first.TryGetProperty("organization", out var org) &&
|
if (mem.TryGetProperty("organization", out var org))
|
||||||
org.TryGetProperty("uuid", out var uuid))
|
allOrgs.Add(org);
|
||||||
orgId = uuid.GetString();
|
}
|
||||||
|
if (allOrgs.Count == 0 && root.TryGetProperty("memberships", out var rootMems))
|
||||||
|
{
|
||||||
|
foreach (var mem in rootMems.EnumerateArray())
|
||||||
|
if (mem.TryGetProperty("organization", out var org))
|
||||||
|
allOrgs.Add(org);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(orgId) &&
|
// Prefer the org matching preferredOrgId, fall back to first
|
||||||
root.TryGetProperty("memberships", out var rootMems) && rootMems.GetArrayLength() > 0)
|
JsonElement activeOrg = default;
|
||||||
|
if (!string.IsNullOrEmpty(preferredOrgId))
|
||||||
{
|
{
|
||||||
var first = rootMems[0];
|
foreach (var org in allOrgs)
|
||||||
if (first.TryGetProperty("organization", out var org) &&
|
{
|
||||||
org.TryGetProperty("uuid", out var uuid))
|
if (org.TryGetProperty("uuid", out var u) && u.GetString() == preferredOrgId)
|
||||||
orgId = uuid.GetString();
|
{
|
||||||
|
activeOrg = org;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (activeOrg.ValueKind == JsonValueKind.Undefined && allOrgs.Count > 0)
|
||||||
|
activeOrg = allOrgs[0];
|
||||||
|
|
||||||
|
string? orgId = null;
|
||||||
|
if (activeOrg.ValueKind != JsonValueKind.Undefined &&
|
||||||
|
activeOrg.TryGetProperty("uuid", out var uuid))
|
||||||
|
orgId = uuid.GetString();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(orgId) &&
|
if (string.IsNullOrEmpty(orgId) &&
|
||||||
root.TryGetProperty("organizations", out var orgs) && orgs.GetArrayLength() > 0)
|
root.TryGetProperty("organizations", out var orgs) && orgs.GetArrayLength() > 0)
|
||||||
{
|
{
|
||||||
if (orgs[0].TryGetProperty("uuid", out var uuid))
|
if (orgs[0].TryGetProperty("uuid", out var u))
|
||||||
orgId = uuid.GetString();
|
orgId = u.GetString();
|
||||||
}
|
}
|
||||||
|
|
||||||
string? planLabel = null;
|
string? planLabel = null;
|
||||||
if (root.TryGetProperty("account", out var acctPlan) &&
|
if (activeOrg.ValueKind != JsonValueKind.Undefined &&
|
||||||
acctPlan.TryGetProperty("memberships", out var plMems) && plMems.GetArrayLength() > 0 &&
|
activeOrg.TryGetProperty("capabilities", out var caps) &&
|
||||||
plMems[0].TryGetProperty("organization", out var plOrg) &&
|
|
||||||
plOrg.TryGetProperty("capabilities", out var caps) &&
|
|
||||||
caps.ValueKind == JsonValueKind.Array)
|
caps.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
foreach (var cap in caps.EnumerateArray())
|
foreach (var cap in caps.EnumerateArray())
|
||||||
|
|||||||
Reference in New Issue
Block a user