Compare commits

..
Author SHA1 Message Date
SuperDooper a74d0cece5 chore: release notes for beta.25 2026-05-08 16:06:16 +02:00
SuperDooper b5cd7b1bc9 debug: show DebugInfo in settings panel after login 2026-05-08 16:06:06 +02:00
SuperDooper 3249dbd4aa debug: add DebugText block in settings panel 2026-05-08 16:06:05 +02:00
SuperDooper 189c513677 debug: capture bootstrap keys and save to DebugInfo 2026-05-08 16:06:04 +02:00
SuperDooper 05e66b22d1 feat: add DebugInfo field to AppSettings 2026-05-08 16:06:03 +02:00
SuperDooper 48e6f64c9a chore: release notes for beta.24 2026-05-08 16:00:28 +02:00
SuperDooper d55a92623c fix: add LoadFromCacheAsync, same JS fallbacks in WebView2 refresh path 2026-05-08 16:00:04 +02:00
SuperDooper c70a090983 fix: load from cache immediately after login, background refresh after 3s 2026-05-08 16:00:01 +02:00
SuperDooper cc0270326e fix: add /api/organizations + personal /api/usage fallbacks in login JS 2026-05-08 16:00:00 +02:00
SuperDooper 7601452f93 chore: release notes for beta.23 2026-05-08 15:50:04 +02:00
SuperDooper 5a78a021cb fix: trust cookies/cached auth for IsSignedIn, wider cookie domain filter 2026-05-08 15:49:47 +02:00
SuperDooper 7d465c13d8 fix: show update label in About, delay before refresh after login 2026-05-08 15:49:47 +02:00
SuperDooper ca67039e3e feat: add update-available label in About section 2026-05-08 15:49:46 +02:00
SuperDooper 1e4cdc09b3 fix: broaden JS orgId/email paths, URL-only auto-close 2026-05-08 15:49:45 +02:00
SuperDooper 4e154bcaa7 chore: release notes for beta.22 2026-05-08 15:30:34 +02:00
SuperDooper ea784384c1 fix: remove unused _dialogOpen field 2026-05-08 15:30:27 +02:00
SuperDooper 4e813995c9 fix: Unwrap Task<Task<string>> from Dispatcher.InvokeAsync with async lambda 2026-05-08 15:30:26 +02:00
6 changed files with 150 additions and 52 deletions
+1
View File
@@ -14,6 +14,7 @@ public class AppSettings
public int RefreshInterval { get; set; } = 120; public int RefreshInterval { get; set; } = 120;
public bool ShowInTaskbar { get; set; } = true; public bool ShowInTaskbar { get; set; } = true;
public bool BetaChannel { get; set; } = false; public bool BetaChannel { get; set; } = false;
public string DebugInfo { get; set; } = "";
private static readonly string FilePath = Path.Combine( private static readonly string FilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+26 -8
View File
@@ -47,8 +47,8 @@ public partial class LoginWindow : Window
: "Complete sign-in, then click Done."; : "Complete sign-in, then click Done.";
}); });
// Auto-close as soon as we detect sign-in on any claude.ai page // Auto-close once we land on any claude.ai page that isn't the login flow
if (signedIn && !uri.Contains("/login") && !uri.Contains("/signin")) if (!uri.Contains("/login") && !uri.Contains("/signin") && signedIn)
await SaveAndClose(cookies); await SaveAndClose(cookies);
}; };
} }
@@ -71,12 +71,25 @@ public partial class LoginWindow : Window
try try
{ {
const string script = @"(async()=>{try{ const string script = @"(async()=>{try{
const b=await(await fetch('/api/bootstrap',{headers:{accept:'application/json'}})).json(); const h={headers:{accept:'application/json'}};
const id=b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid||null; const b=await(await fetch('/api/bootstrap',h)).json();
const e=b?.account?.email_address||null; const bkeys=Object.keys(b||{}).join(',');
if(!id)return{email:e,orgId:null,usage:null}; const akeys=Object.keys(b?.account||{}).join(',');
const u=await(await fetch('/api/organizations/'+id+'/usage',{headers:{accept:'application/json'}})).json(); let id=b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid
return{email:e,orgId:id,usage:u}; ||b?.default_organization?.uuid||null;
const e=b?.account?.email_address||b?.account?.email||b?.email||null;
let orgSrc='bootstrap';
if(!id){
try{const ol=await(await fetch('/api/organizations',h)).json();
if(Array.isArray(ol)&&ol.length>0){id=ol[0]?.uuid||null;orgSrc='orgs-list';}}catch(e2){}
}
if(!id){
try{const pu=await(await fetch('/api/usage',h)).json();
if(pu&&!pu.error)return{email:e,orgId:null,usage:pu,debug:'no-org|bkeys:'+bkeys+'|akeys:'+akeys};}catch(e3){}
return{email:e,orgId:null,usage:null,debug:'no-org|bkeys:'+bkeys+'|akeys:'+akeys};
}
const u=await(await fetch('/api/organizations/'+id+'/usage',h)).json();
return{email:e,orgId:id,usage:u,debug:'orgSrc:'+orgSrc+'|bkeys:'+bkeys+'|akeys:'+akeys};
}catch(ex){return{error:String(ex)};}})()"; }catch(ex){return{error:String(ex)};}})()";
var json = await Browser.CoreWebView2.ExecuteScriptAsync(script); var json = await Browser.CoreWebView2.ExecuteScriptAsync(script);
@@ -94,6 +107,11 @@ public partial class LoginWindow : Window
if (root.TryGetProperty("usage", out var us) && us.ValueKind == JsonValueKind.Object) if (root.TryGetProperty("usage", out var us) && us.ValueKind == JsonValueKind.Object)
AppSettings.Default.UsageJson = us.GetRawText(); AppSettings.Default.UsageJson = us.GetRawText();
if (root.TryGetProperty("debug", out var dbg) && dbg.ValueKind == JsonValueKind.String)
AppSettings.Default.DebugInfo = dbg.GetString() ?? "";
else if (root.TryGetProperty("error", out var err))
AppSettings.Default.DebugInfo = "error:" + err.GetRawText();
AppSettings.Default.Save(); AppSettings.Default.Save();
} }
} }
+13 -3
View File
@@ -173,12 +173,22 @@
<TextBlock Text="Built by superdooper86 &amp; claude" <TextBlock Text="Built by superdooper86 &amp; claude"
FontSize="11" Foreground="{DynamicResource SecondaryBrush}"/> FontSize="11" Foreground="{DynamicResource SecondaryBrush}"/>
</StackPanel> </StackPanel>
<Button Grid.Column="2" x:Name="CheckUpdateButton" <StackPanel Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right">
Style="{StaticResource SmallButton}" <Button x:Name="CheckUpdateButton"
Click="CheckUpdate_Click" VerticalAlignment="Center"/> Style="{StaticResource SmallButton}"
Click="CheckUpdate_Click"/>
<TextBlock x:Name="UpdateAvailableText" FontSize="10"
Foreground="{DynamicResource BlueBrush}"
HorizontalAlignment="Center"
Visibility="Collapsed" Margin="0,4,0,0"/>
</StackPanel>
</Grid> </Grid>
</Border> </Border>
<TextBlock x:Name="DebugText" FontSize="10" Foreground="{DynamicResource SecondaryBrush}"
TextWrapping="Wrap" Margin="0,12,0,0" FontFamily="Consolas"
Visibility="Collapsed"/>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</DockPanel> </DockPanel>
+42 -5
View File
@@ -14,7 +14,7 @@ public partial class PopupWindow : Window
private static readonly UsageViewModel VM = App.ViewModel; private static readonly UsageViewModel VM = App.ViewModel;
private static readonly UpdateManager Updater = App.Updater; private static readonly UpdateManager Updater = App.Updater;
private readonly DispatcherTimer _clockTimer; private readonly DispatcherTimer _clockTimer;
private bool _dialogOpen;
[DllImport("dwmapi.dll")] [DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size); private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size);
@@ -199,7 +199,10 @@ public partial class PopupWindow : Window
private void OnUpdaterChanged(string? prop) private void OnUpdaterChanged(string? prop)
{ {
if (prop is nameof(UpdateManager.UpdateAvailable) or nameof(UpdateManager.LatestVersion)) if (prop is nameof(UpdateManager.UpdateAvailable) or nameof(UpdateManager.LatestVersion))
{
UpdateBannerState(); UpdateBannerState();
UpdateAboutSection();
}
if (prop is nameof(UpdateManager.DownloadProgress)) if (prop is nameof(UpdateManager.DownloadProgress))
UpdateProgress.Value = Updater.DownloadProgress * 100; UpdateProgress.Value = Updater.DownloadProgress * 100;
if (prop is nameof(UpdateManager.StatusMessage)) if (prop is nameof(UpdateManager.StatusMessage))
@@ -208,6 +211,21 @@ public partial class PopupWindow : Window
InstallButton.Visibility = Visibility.Collapsed; InstallButton.Visibility = Visibility.Collapsed;
} }
private void UpdateAboutSection()
{
if (Updater.UpdateAvailable)
{
UpdateAvailableText.Text = $"v{Updater.LatestVersion} available";
UpdateAvailableText.Visibility = Visibility.Visible;
CheckUpdateButton.Content = "Install";
}
else
{
UpdateAvailableText.Visibility = Visibility.Collapsed;
CheckUpdateButton.Content = "Check";
}
}
private void UpdateBannerState() private void UpdateBannerState()
{ {
var visible = Updater.UpdateAvailable; var visible = Updater.UpdateAvailable;
@@ -236,7 +254,7 @@ public partial class PopupWindow : Window
{ {
VersionLabel.Text = $"v{Updater.CurrentVersion}"; VersionLabel.Text = $"v{Updater.CurrentVersion}";
BetaToggle.IsChecked = Updater.BetaChannel; BetaToggle.IsChecked = Updater.BetaChannel;
CheckUpdateButton.Content = Updater.UpdateAvailable ? "Install" : "Check"; UpdateAboutSection();
var intervals = new[] { ("1 min", 60), ("2 min", 120), ("3 min", 180), var intervals = new[] { ("1 min", 60), ("2 min", 120), ("3 min", 180),
("4 min", 240), ("5 min", 300), ("10 min", 600) }; ("4 min", 240), ("5 min", 300), ("10 min", 600) };
@@ -265,6 +283,17 @@ public partial class PopupWindow : Window
SignOutButton.Visibility = VM.IsSignedIn ? Visibility.Visible : Visibility.Collapsed; SignOutButton.Visibility = VM.IsSignedIn ? Visibility.Visible : Visibility.Collapsed;
SignInButton.Content = VM.IsSignedIn ? "Re-authenticate" : "Sign In"; SignInButton.Content = VM.IsSignedIn ? "Re-authenticate" : "Sign In";
var dbg = AppSettings.Default.DebugInfo;
if (!string.IsNullOrEmpty(dbg))
{
DebugText.Text = dbg;
DebugText.Visibility = Visibility.Visible;
}
else
{
DebugText.Visibility = Visibility.Collapsed;
}
} }
// ── Event handlers ─────────────────────────────────────────────── // ── Event handlers ───────────────────────────────────────────────
@@ -312,14 +341,22 @@ public partial class PopupWindow : Window
private async void SignIn_Click(object s, RoutedEventArgs e) private async void SignIn_Click(object s, RoutedEventArgs e)
{ {
_dialogOpen = true;
var login = new LoginWindow { Owner = this }; var login = new LoginWindow { Owner = this };
if (login.ShowDialog() == true) if (login.ShowDialog() == true)
{ {
await VM.RefreshAsync(); // Show cached data from SaveAndClose immediately (no WebView2 needed)
await VM.LoadFromCacheAsync();
InitSettings(); InitSettings();
ShowMain();
// Full refresh in background after WebView2 folder is definitely released
_ = Task.Run(async () =>
{
await Task.Delay(3000);
await VM.RefreshAsync();
await Application.Current.Dispatcher.InvokeAsync(InitSettings);
});
} }
_dialogOpen = false;
Show(); Show();
Activate(); Activate();
} }
+2 -5
View File
@@ -1,6 +1,3 @@
## What's new in beta.21 ## What's new in beta.25
- Fixed: clicking Sign In now auto-closes as soon as you are detected as signed in (no need to click Done) - Debug: Settings panel now shows bootstrap diagnostic info after sign-in (helps diagnose missing data)
- Fixed: login now fetches and caches your data directly via the browser session (no more authentication failures)
- Fixed: periodic refresh falls back to browser-session API calls when HttpClient is rejected
- Fixed: cached usage from login is shown immediately while refreshing
+66 -31
View File
@@ -78,7 +78,13 @@ public class UsageViewModel : INotifyPropertyChanged
try try
{ {
var cookies = await GetCookiesAsync(); var cookies = await GetCookiesAsync();
if (cookies.Count == 0)
// Check if we have any persistent proof that the user authenticated
bool hasCookies = cookies.Count > 0;
bool hasCachedAuth = !string.IsNullOrEmpty(AppSettings.Default.Email) ||
!string.IsNullOrEmpty(AppSettings.Default.OrgId);
if (!hasCookies && !hasCachedAuth)
{ {
await Application.Current.Dispatcher.InvokeAsync(() => await Application.Current.Dispatcher.InvokeAsync(() =>
{ {
@@ -89,20 +95,18 @@ public class UsageViewModel : INotifyPropertyChanged
return; return;
} }
// Try HttpClient first (fast path) // Try HttpClient first (fast path) — may be rejected by server CORS/header checks
var (limits, overage, prepaid, email, orgId, ok) = await TryHttpRefreshAsync(cookies); var (limits, overage, prepaid, email, orgId, ok) = hasCookies
? await TryHttpRefreshAsync(cookies)
: ([], null, null, null, null, false);
// If HttpClient failed, fall back to WebView2 (uses existing browser session) // Fall back to WebView2 (uses the shared browser session, not HttpClient)
if (!ok) if (!ok)
(limits, email, orgId) = await TryWebView2RefreshAsync(); (limits, email, orgId) = await TryWebView2RefreshAsync();
// Still no org ID? Use cached values from last successful login // Fill in any blanks from cached values saved at login time
if (string.IsNullOrEmpty(orgId) && !string.IsNullOrEmpty(AppSettings.Default.OrgId)) if (string.IsNullOrEmpty(email)) email = AppSettings.Default.Email;
orgId = AppSettings.Default.OrgId; if (string.IsNullOrEmpty(orgId)) orgId = AppSettings.Default.OrgId;
if (string.IsNullOrEmpty(email) && !string.IsNullOrEmpty(AppSettings.Default.Email))
email = AppSettings.Default.Email;
// If we have cached usage from login and still got nothing live, use that
if (limits.Count == 0 && !string.IsNullOrEmpty(AppSettings.Default.UsageJson)) if (limits.Count == 0 && !string.IsNullOrEmpty(AppSettings.Default.UsageJson))
{ {
try try
@@ -114,19 +118,6 @@ public class UsageViewModel : INotifyPropertyChanged
catch { } catch { }
} }
bool isAuth = !string.IsNullOrEmpty(email) || !string.IsNullOrEmpty(orgId) || limits.Count > 0;
if (!isAuth && cookies.Count > 0)
{
// Cookies exist but nothing worked — session likely expired
await Application.Current.Dispatcher.InvokeAsync(() =>
{
IsSignedIn = false;
ErrorMessage = "Session expired — click Sign In to re-authenticate.";
IsLoading = false;
});
return;
}
foreach (var limit in limits) foreach (var limit in limits)
{ {
var key = limit.Window.ToString(); var key = limit.Window.ToString();
@@ -144,6 +135,8 @@ public class UsageViewModel : INotifyPropertyChanged
Overage = overage; Overage = overage;
Prepaid = prepaid; Prepaid = prepaid;
if (!string.IsNullOrEmpty(email)) UserEmail = email; if (!string.IsNullOrEmpty(email)) UserEmail = email;
// Trust that cookies / cached auth mean the user IS signed in,
// even if the live fetch failed this cycle.
IsSignedIn = true; IsSignedIn = true;
ErrorMessage = null; ErrorMessage = null;
LastUpdated = DateTime.Now; LastUpdated = DateTime.Now;
@@ -198,11 +191,21 @@ public class UsageViewModel : INotifyPropertyChanged
try try
{ {
const string script = @"(async()=>{try{ const string script = @"(async()=>{try{
const b=await(await fetch('/api/bootstrap',{headers:{accept:'application/json'}})).json(); const h={headers:{accept:'application/json'}};
const id=b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid||null; const b=await(await fetch('/api/bootstrap',h)).json();
const em=b?.account?.email_address||null; let id=b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid
if(!id)return{email:em,orgId:null,usage:null}; ||b?.default_organization?.uuid||null;
const u=await(await fetch('/api/organizations/'+id+'/usage',{headers:{accept:'application/json'}})).json(); const em=b?.account?.email_address||b?.account?.email||b?.email||null;
if(!id){
try{const ol=await(await fetch('/api/organizations',h)).json();
if(Array.isArray(ol)&&ol.length>0)id=ol[0]?.uuid||null;}catch(e2){}
}
if(!id){
try{const pu=await(await fetch('/api/usage',h)).json();
if(pu&&!pu.error)return{email:em,orgId:null,usage:pu};}catch(e3){}
return{email:em,orgId:null,usage:null};
}
const u=await(await fetch('/api/organizations/'+id+'/usage',h)).json();
return{email:em,orgId:id,usage:u}; return{email:em,orgId:id,usage:u};
}catch(ex){return null;}})()"; }catch(ex){return null;}})()";
@@ -212,7 +215,7 @@ public class UsageViewModel : INotifyPropertyChanged
host.Show(); host.Show();
try { return await host.FetchAsync("https://claude.ai", script); } try { return await host.FetchAsync("https://claude.ai", script); }
finally { host.Close(); } finally { host.Close(); }
}).Task; }).Task.Unwrap();
if (resultJson == null || resultJson == "null") return ([], null, null); if (resultJson == null || resultJson == "null") return ([], null, null);
@@ -243,6 +246,38 @@ public class UsageViewModel : INotifyPropertyChanged
catch { return ([], null, null); } catch { return ([], null, null); }
} }
public async Task LoadFromCacheAsync()
{
var email = AppSettings.Default.Email;
var orgId = AppSettings.Default.OrgId;
var cookie = AppSettings.Default.CookieStore;
bool isAuth = !string.IsNullOrEmpty(email) || !string.IsNullOrEmpty(orgId)
|| !string.IsNullOrEmpty(cookie);
if (!isAuth) return;
List<AgentLimit> limits = [];
if (!string.IsNullOrEmpty(AppSettings.Default.UsageJson))
{
try
{
var cached = JsonSerializer.Deserialize<UsageResponse>(
AppSettings.Default.UsageJson, JsonOpts);
limits = BuildLimits(cached);
}
catch { }
}
await Application.Current.Dispatcher.InvokeAsync(() =>
{
if (!string.IsNullOrEmpty(email)) UserEmail = email;
if (limits.Count > 0) Limits = limits;
IsSignedIn = true;
ErrorMessage = limits.Count == 0 ? "Signed in — refreshing data…" : null;
LastUpdated = limits.Count > 0 ? DateTime.Now : LastUpdated;
});
}
public async Task SignOutAsync() public async Task SignOutAsync()
{ {
AppSettings.Default.CookieStore = ""; AppSettings.Default.CookieStore = "";
@@ -276,7 +311,7 @@ public class UsageViewModel : INotifyPropertyChanged
public static void SaveCookies(IEnumerable<CoreWebView2Cookie> cookies) public static void SaveCookies(IEnumerable<CoreWebView2Cookie> cookies)
{ {
var entries = cookies var entries = cookies
.Where(c => c.Domain.Contains("claude.ai")) .Where(c => c.Domain.Contains("claude.ai") || c.Domain.Contains("anthropic.com"))
.Select(c => new CookieEntry { Name = c.Name, Value = c.Value, Domain = c.Domain, Path = c.Path }) .Select(c => new CookieEntry { Name = c.Name, Value = c.Value, Domain = c.Domain, Path = c.Path })
.ToList(); .ToList();
AppSettings.Default.CookieStore = JsonSerializer.Serialize(entries); AppSettings.Default.CookieStore = JsonSerializer.Serialize(entries);