diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3f9834..adcec60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,16 +112,30 @@ jobs: - name: Generate release notes shell: bash run: | - # Try to find release notes from the merged PR - COMMIT_SHA=$(git rev-list -1 ${{ github.ref_name }}) - PR_BODY=$(gh api "repos/TobiiNT/ClaudeTracker/commits/$COMMIT_SHA/pulls" --jq '.[0].body // empty' 2>/dev/null || echo "") + PREV_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || echo "") + PR_BODY="" + + # Strategy 1: Check each commit between tags for an associated PR + if [ -n "$PREV_TAG" ]; then + for sha in $(git rev-list "$PREV_TAG"..${{ github.ref_name }}); do + PR_BODY=$(gh api "repos/TobiiNT/ClaudeTracker/commits/$sha/pulls" --jq '.[0].body // empty' 2>/dev/null || echo "") + if [ -n "$PR_BODY" ]; then + break + fi + done + fi + + # Strategy 2: Find most recent merged PR targeting main + if [ -z "$PR_BODY" ]; then + PR_BODY=$(gh api "repos/TobiiNT/ClaudeTracker/pulls?state=closed&base=main&sort=updated&direction=desc&per_page=5" \ + --jq '[.[] | select(.merged_at != null)] | .[0].body // empty' 2>/dev/null || echo "") + fi if [ -n "$PR_BODY" ]; then # Use PR body, strip test plan section - echo "$PR_BODY" | sed '/^## Test plan/,$d' | sed -e :a -e '/^\n*$/{$d;N;ba}' > release-notes.md + echo "$PR_BODY" | sed '/^## Test [Pp]lan/,$d' | sed -e :a -e '/^\n*$/{$d;N;ba}' > release-notes.md else # Fallback: list commits since previous tag - PREV_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || echo "") echo "## What's Changed" > release-notes.md if [ -n "$PREV_TAG" ]; then git log --oneline "$PREV_TAG"..${{ github.ref_name }} | head -20 | while read line; do echo "- $line"; done >> release-notes.md diff --git a/src/ClaudeTracker/App.xaml.cs b/src/ClaudeTracker/App.xaml.cs index 21a19bc..afc7cf8 100644 --- a/src/ClaudeTracker/App.xaml.cs +++ b/src/ClaudeTracker/App.xaml.cs @@ -522,4 +522,20 @@ public static bool IsSystemDarkMode() return false; } } + + /// Checks if the taskbar/tray uses dark mode (separate from app theme on W11). + public static bool IsTaskbarDarkMode() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey( + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + var value = key?.GetValue("SystemUsesLightTheme"); + return value is int intValue && intValue == 0; + } + catch + { + return false; + } + } } diff --git a/src/ClaudeTracker/ClaudeTracker.csproj b/src/ClaudeTracker/ClaudeTracker.csproj index 55c4172..5a6c2ec 100644 --- a/src/ClaudeTracker/ClaudeTracker.csproj +++ b/src/ClaudeTracker/ClaudeTracker.csproj @@ -9,9 +9,9 @@ Assets\app_icon.ico ClaudeTracker ClaudeTracker - 2.1.1 - 2.1.1.0 - 2.1.1.0 + 2.1.2 + 2.1.2.0 + 2.1.2.0 Claude AI Usage Tracker for Windows TobiiNT Tobii Development diff --git a/src/ClaudeTracker/Services/Interfaces/IUsageRefreshCoordinator.cs b/src/ClaudeTracker/Services/Interfaces/IUsageRefreshCoordinator.cs index a44559e..17fa8d8 100644 --- a/src/ClaudeTracker/Services/Interfaces/IUsageRefreshCoordinator.cs +++ b/src/ClaudeTracker/Services/Interfaces/IUsageRefreshCoordinator.cs @@ -14,6 +14,8 @@ public interface IUsageRefreshCoordinator void Stop(); /// Triggers an immediate out-of-cycle refresh. void RefreshNow(); + /// Invalidates API fetch cache so next RefreshNow() re-fetches immediately. + void InvalidateApiCache(); /// Updates the refresh interval in seconds. void UpdateInterval(double seconds); /// Whether the refresh timer is currently running. diff --git a/src/ClaudeTracker/Services/UsageRefreshCoordinator.cs b/src/ClaudeTracker/Services/UsageRefreshCoordinator.cs index 704cf16..f7ebef9 100644 --- a/src/ClaudeTracker/Services/UsageRefreshCoordinator.cs +++ b/src/ClaudeTracker/Services/UsageRefreshCoordinator.cs @@ -18,7 +18,7 @@ public class UsageRefreshCoordinator : IUsageRefreshCoordinator, IDisposable private DateTime _lastStatusFetch = DateTime.MinValue; private bool _isRefreshing; private DateTime _rateLimitedUntil = DateTime.MinValue; - private DateTime _lastApiFetch = DateTime.MinValue; + private long _lastApiFetchTicks = DateTime.MinValue.Ticks; private static readonly TimeSpan ApiRefreshInterval = TimeSpan.FromMinutes(5); public bool IsRunning => _timer?.IsEnabled ?? false; @@ -73,6 +73,11 @@ public void RefreshNow() _ = RefreshAsync(); } + public void InvalidateApiCache() + { + Interlocked.Exchange(ref _lastApiFetchTicks, DateTime.MinValue.Ticks); + } + public void UpdateInterval(double seconds) { seconds = Math.Clamp(seconds, Constants.RefreshIntervals.MinSeconds, Constants.RefreshIntervals.MaxSeconds); @@ -107,8 +112,8 @@ private async Task RefreshAsync() try { - // Fetch Claude.ai usage - if (profile.HasClaudeAI || !string.IsNullOrEmpty(profile.CliCredentialsJSON)) + // Fetch Claude.ai subscription usage (session key or CLI auto-detect) + if (profile.HasClaudeAI || profile.HasCliAccount) { var usage = await _apiService.FetchUsageData(); _profileService.UpdateUsageData(profile.Id, claudeUsage: usage); @@ -121,8 +126,9 @@ private async Task RefreshAsync() _notificationService.CheckKeyExpiry(profile); // Fetch API Console + personal metrics (non-fatal, throttled to every 5 min) + var lastApiFetch = new DateTime(Interlocked.Read(ref _lastApiFetchTicks)); var shouldFetchApi = profile.HasAPIConsole - && (DateTime.UtcNow - _lastApiFetch) >= ApiRefreshInterval; + && (DateTime.UtcNow - lastApiFetch) >= ApiRefreshInterval; if (shouldFetchApi) { try @@ -160,7 +166,7 @@ private async Task RefreshAsync() } } - _lastApiFetch = DateTime.UtcNow; + Interlocked.Exchange(ref _lastApiFetchTicks, DateTime.UtcNow.Ticks); } // Fetch Claude system status (every 5 minutes) diff --git a/src/ClaudeTracker/TrayIcon/TrayIconManager.cs b/src/ClaudeTracker/TrayIcon/TrayIconManager.cs index e9666bf..ccc6467 100644 --- a/src/ClaudeTracker/TrayIcon/TrayIconManager.cs +++ b/src/ClaudeTracker/TrayIcon/TrayIconManager.cs @@ -96,20 +96,43 @@ public void UpdateIcon() try { var profile = _profileService.ActiveProfile; - var usage = profile?.ClaudeUsage; + var claudeUsage = profile?.HasClaudeAI == true ? profile.ClaudeUsage : null; + var apiUsage = profile?.HasAPIConsole == true ? profile.ApiUsage : null; var iconConfig = profile?.IconConfig ?? MenuBarIconConfiguration.Default; var sessionConfig = iconConfig.GetConfig(MenuBarMetricType.Session); - var percentage = usage?.EffectiveSessionPercentage ?? 0; + double percentage; + string tooltipText; + + if (claudeUsage != null) + { + // Subscription user: show session usage % + percentage = claudeUsage.EffectiveSessionPercentage; + tooltipText = $"Claude Usage: {FormatterHelper.FormatPercentage(percentage)} used\nResets: {FormatterHelper.FormatTimeRemaining(claudeUsage.SessionResetTime)}"; + } + else if (apiUsage != null) + { + // API-only user: show budget usage % + percentage = apiUsage.UsagePercentage; + tooltipText = $"API Budget: {apiUsage.FormattedUsed} / {apiUsage.FormattedTotal} ({FormatterHelper.FormatPercentage(percentage)} used)"; + } + else + { + percentage = 0; + tooltipText = "Claude Tracker - No usage data"; + } + var displayPercentage = UsageStatusCalculator.GetDisplayPercentage( percentage, iconConfig.ShowRemainingPercentage); var status = UsageStatusCalculator.CalculateStatus( percentage, iconConfig.ShowRemainingPercentage); var style = sessionConfig?.IconStyle ?? MenuBarIconStyle.Battery; - var isDark = App.IsSystemDarkMode(); + var isDark = App.IsTaskbarDarkMode(); var customColor = iconConfig.UseCustomColor ? iconConfig.CustomColorHex : null; - var metricPrefix = iconConfig.ShowIconNames ? "S:" : null; + var metricPrefix = iconConfig.ShowIconNames + ? (claudeUsage != null ? "S:" : "$:") + : null; var icon = _renderer.RenderIcon( displayPercentage, status, style, iconConfig.MonochromeMode, isDark, customColor, @@ -121,9 +144,7 @@ public void UpdateIcon() _trayIcon.Icon = icon; oldIcon?.Dispose(); - _tooltipText = usage != null - ? $"Claude Usage: {FormatterHelper.FormatPercentage(percentage)} used\nResets: {FormatterHelper.FormatTimeRemaining(usage.SessionResetTime)}" - : "Claude Tracker - No usage data"; + _tooltipText = tooltipText; }); } catch (Exception ex) diff --git a/src/ClaudeTracker/Utilities/PopupStackManager.cs b/src/ClaudeTracker/Utilities/PopupStackManager.cs index 82bb267..d72b64b 100644 --- a/src/ClaudeTracker/Utilities/PopupStackManager.cs +++ b/src/ClaudeTracker/Utilities/PopupStackManager.cs @@ -67,12 +67,24 @@ private static double GetDpiScale() { try { - var source = PresentationSource.FromVisual(Application.Current.MainWindow); + var window = Application.Current?.MainWindow; + if (window == null) return GetDpiScaleFromSystem(); + + var source = PresentationSource.FromVisual(window); if (source?.CompositionTarget != null) return source.CompositionTarget.TransformToDevice.M11; } catch { /* fall through */ } - return 1.0; + return GetDpiScaleFromSystem(); + } + + [System.Runtime.InteropServices.DllImport("user32.dll")] + private static extern uint GetDpiForSystem(); + + private static double GetDpiScaleFromSystem() + { + try { return GetDpiForSystem() / 96.0; } + catch { return 1.0; } } public static void PositionWindow(Window popup) diff --git a/src/ClaudeTracker/ViewModels/ApiBillingViewModel.cs b/src/ClaudeTracker/ViewModels/ApiBillingViewModel.cs index c5ab7cf..2935da4 100644 --- a/src/ClaudeTracker/ViewModels/ApiBillingViewModel.cs +++ b/src/ClaudeTracker/ViewModels/ApiBillingViewModel.cs @@ -11,6 +11,7 @@ public partial class ApiBillingViewModel : ObservableObject private readonly IClaudeApiService _apiService; private readonly IProfileService _profileService; private readonly ISettingsService _settingsService; + private readonly IUsageRefreshCoordinator _refreshCoordinator; [ObservableProperty] private string _apiKey = ""; [ObservableProperty] private bool _isTesting; @@ -20,19 +21,24 @@ public partial class ApiBillingViewModel : ObservableObject [ObservableProperty] private APIOrganization? _selectedOrg; [ObservableProperty] private bool _isConfigured; [ObservableProperty] private bool _showUserPicker; + [ObservableProperty] private bool _isLoadingUsers; [ObservableProperty] private ClaudeCodeUserMetrics? _selectedUser; + [ObservableProperty] private string _trackedUserLabel = ""; public ObservableCollection Organizations { get; } = new(); public ObservableCollection ClaudeCodeUsers { get; } = new(); - public ApiBillingViewModel(IClaudeApiService apiService, IProfileService profileService, ISettingsService settingsService) + public ApiBillingViewModel(IClaudeApiService apiService, IProfileService profileService, + ISettingsService settingsService, IUsageRefreshCoordinator refreshCoordinator) { _apiService = apiService; _profileService = profileService; _settingsService = settingsService; + _refreshCoordinator = refreshCoordinator; var profile = _profileService.ActiveProfile; IsConfigured = profile?.HasAPIConsole ?? false; + TrackedUserLabel = profile?.ApiUserSearch ?? ""; } [RelayCommand] @@ -42,6 +48,8 @@ private async Task TestConnection() IsTesting = true; TestStatus = "Testing connection..."; + ShowOrgPicker = false; + ShowUserPicker = false; try { @@ -54,12 +62,20 @@ private async Task TestConnection() if (orgs.Count == 1) { SelectedOrg = orgs[0]; - await SaveConfiguration(); + TestStatus = $"Connected to {orgs[0].DisplayName}"; + TestSuccess = true; + await FetchUsersForOrg(orgs[0]); } - else + else if (orgs.Count > 1) { ShowOrgPicker = true; - TestStatus = $"Found {orgs.Count} organizations."; + TestStatus = "Select your organization:"; + TestSuccess = true; + } + else + { + TestStatus = "No organizations found."; + TestSuccess = false; } } catch (Exception ex) @@ -73,33 +89,30 @@ private async Task TestConnection() } } + /// + /// Called when org is selected from the dropdown — immediately fetches users. + /// [RelayCommand] private async Task SelectOrganization() { if (SelectedOrg == null) return; - await SaveConfiguration(); + TestStatus = $"Connected to {SelectedOrg.DisplayName}"; + TestSuccess = true; + await FetchUsersForOrg(SelectedOrg); } - private async Task SaveConfiguration() + private async Task FetchUsersForOrg(APIOrganization org) { var profile = _profileService.ActiveProfile; - if (profile == null || SelectedOrg == null) return; - - var credentials = _profileService.LoadCredentials(profile.Id); - credentials.ApiSessionKey = ApiKey.Trim(); - credentials.ApiOrganizationId = SelectedOrg.Id; - _profileService.SaveCredentials(profile.Id, credentials); + if (profile == null) return; - TestStatus = $"Connected to {SelectedOrg.DisplayName}"; - TestSuccess = true; - IsConfigured = true; - ShowOrgPicker = false; + IsLoadingUsers = true; + ClaudeCodeUsers.Clear(); + ShowUserPicker = false; - // Auto-fetch Claude Code users for identity picker try { - var users = await _apiService.FetchClaudeCodeAllUsers(SelectedOrg.Id, ApiKey.Trim()); - ClaudeCodeUsers.Clear(); + var users = await _apiService.FetchClaudeCodeAllUsers(org.Id, ApiKey.Trim()); foreach (var user in users) ClaudeCodeUsers.Add(user); @@ -109,22 +122,70 @@ private async Task SaveConfiguration() if (!string.IsNullOrEmpty(profile.ApiUserSearch)) SelectedUser = users.FirstOrDefault(u => u.DisplayName == profile.ApiUserSearch); } + else + { + TestStatus = $"Connected to {org.DisplayName}, but no users found."; + } } catch (Exception ex) { + TestStatus = $"Connected, but failed to load users: {ex.Message}"; + TestSuccess = false; Services.LoggingService.Instance.LogWarning($"Failed to fetch Claude Code users: {ex.Message}"); } + finally + { + IsLoadingUsers = false; + } } + /// + /// Final confirm — saves org + user selection together. + /// [RelayCommand] - private async Task LoadClaudeCodeUsers() + private void SaveUserSelection() + { + if (SelectedUser == null || SelectedOrg == null) return; + var profile = _profileService.ActiveProfile; + if (profile == null) return; + + // Save credentials + var credentials = _profileService.LoadCredentials(profile.Id); + credentials.ApiSessionKey = ApiKey.Trim(); + credentials.ApiOrganizationId = SelectedOrg.Id; + _profileService.SaveCredentials(profile.Id, credentials); + + // Save user selection + profile.ApiUserSearch = SelectedUser.DisplayName; + _settingsService.Save(); + + ShowUserPicker = false; + ShowOrgPicker = false; + IsConfigured = true; + TrackedUserLabel = SelectedUser.DisplayName; + TestStatus = $"Tracking: {SelectedUser.DisplayName}"; + TestSuccess = true; + + _refreshCoordinator.InvalidateApiCache(); + _refreshCoordinator.RefreshNow(); + } + + [RelayCommand] + private async Task ChangeUser() { var profile = _profileService.ActiveProfile; if (profile == null) return; var credentials = _profileService.LoadCredentials(profile.Id); if (string.IsNullOrEmpty(credentials.ApiSessionKey) || string.IsNullOrEmpty(credentials.ApiOrganizationId)) + { + TestStatus = "No API credentials configured."; + TestSuccess = false; return; + } + + ApiKey = credentials.ApiSessionKey; + IsLoadingUsers = true; try { @@ -139,26 +200,22 @@ private async Task LoadClaudeCodeUsers() if (!string.IsNullOrEmpty(profile.ApiUserSearch)) SelectedUser = users.FirstOrDefault(u => u.DisplayName == profile.ApiUserSearch); } + else + { + TestStatus = "No users found in this organization."; + TestSuccess = false; + } } catch (Exception ex) { + TestStatus = $"Failed to load users: {ex.Message}"; + TestSuccess = false; Services.LoggingService.Instance.LogWarning($"Failed to fetch Claude Code users: {ex.Message}"); } - } - - [RelayCommand] - private void SaveUserSelection() - { - if (SelectedUser == null) return; - var profile = _profileService.ActiveProfile; - if (profile == null) return; - - profile.ApiUserSearch = SelectedUser.DisplayName; - _settingsService.Save(); - - ShowUserPicker = false; - TestStatus = $"Tracking: {SelectedUser.DisplayName}"; - TestSuccess = true; + finally + { + IsLoadingUsers = false; + } } [RelayCommand] @@ -172,8 +229,20 @@ private void Disconnect() credentials.ApiOrganizationId = null; _profileService.SaveCredentials(profile.Id, credentials); + profile.ApiUserSearch = null; + profile.ApiUsage = null; + profile.PersonalMetrics = null; + profile.DailyMetrics = null; + _settingsService.Save(); + ApiKey = ""; IsConfigured = false; + ShowUserPicker = false; + ShowOrgPicker = false; + TrackedUserLabel = ""; TestStatus = ""; + + _refreshCoordinator.InvalidateApiCache(); + _refreshCoordinator.RefreshNow(); } } diff --git a/src/ClaudeTracker/ViewModels/PersonalUsageViewModel.cs b/src/ClaudeTracker/ViewModels/PersonalUsageViewModel.cs index 74b6a3a..f8df100 100644 --- a/src/ClaudeTracker/ViewModels/PersonalUsageViewModel.cs +++ b/src/ClaudeTracker/ViewModels/PersonalUsageViewModel.cs @@ -11,6 +11,7 @@ public partial class PersonalUsageViewModel : ObservableObject { private readonly IClaudeApiService _apiService; private readonly IProfileService _profileService; + private readonly IUsageRefreshCoordinator _refreshCoordinator; private readonly ClaudeCodeSyncService _cliSyncService; [ObservableProperty] private string _sessionKey = ""; @@ -30,14 +31,16 @@ public partial class PersonalUsageViewModel : ObservableObject public PersonalUsageViewModel( IClaudeApiService apiService, IProfileService profileService, + IUsageRefreshCoordinator refreshCoordinator, ClaudeCodeSyncService cliSyncService) { _apiService = apiService; _profileService = profileService; + _refreshCoordinator = refreshCoordinator; _cliSyncService = cliSyncService; var profile = _profileService.ActiveProfile; - IsConfigured = profile?.HasAnyCredentials ?? false; + IsConfigured = profile?.HasClaudeAI == true || !string.IsNullOrEmpty(profile?.CliCredentialsJSON); if (profile?.ClaudeSessionKey != null) SessionKey = profile.ClaudeSessionKey; @@ -178,6 +181,7 @@ private void Disconnect() profile.CliCredentialsJSON = null; profile.HasCliAccount = false; profile.CliAccountSyncedAt = null; + profile.ClaudeUsage = null; _profileService.UpdateProfile(profile); SessionKey = ""; @@ -188,6 +192,9 @@ private void Disconnect() AutoDetectSuccess = false; ConnectedLabel = ""; ConnectedDetail = ""; + + _refreshCoordinator.InvalidateApiCache(); + _refreshCoordinator.RefreshNow(); } private void UpdateConnectedInfo() diff --git a/src/ClaudeTracker/ViewModels/PopoverViewModel.cs b/src/ClaudeTracker/ViewModels/PopoverViewModel.cs index 5a1c8c3..1ac091c 100644 --- a/src/ClaudeTracker/ViewModels/PopoverViewModel.cs +++ b/src/ClaudeTracker/ViewModels/PopoverViewModel.cs @@ -161,7 +161,7 @@ public void RefreshData() ProfileName = profile.Name; HasCredentials = profile.HasUsageCredentials; - var usage = profile.ClaudeUsage; + var usage = profile.HasClaudeAI ? profile.ClaudeUsage : null; HasClaudeUsage = usage != null; if (usage != null) { diff --git a/src/ClaudeTracker/ViewModels/SettingsViewModel.cs b/src/ClaudeTracker/ViewModels/SettingsViewModel.cs index 383112b..74e269e 100644 --- a/src/ClaudeTracker/ViewModels/SettingsViewModel.cs +++ b/src/ClaudeTracker/ViewModels/SettingsViewModel.cs @@ -11,7 +11,6 @@ public partial class SettingsViewModel : ObservableObject private object? _currentView; public PersonalUsageViewModel PersonalUsage { get; } - public ApiBillingViewModel ApiBilling { get; } public CliAccountViewModel CliAccount { get; } public AppearanceViewModel Appearance { get; } public GeneralSettingsViewModel GeneralSettings { get; } @@ -21,7 +20,6 @@ public partial class SettingsViewModel : ObservableObject public SettingsViewModel( PersonalUsageViewModel personalUsage, - ApiBillingViewModel apiBilling, CliAccountViewModel cliAccount, AppearanceViewModel appearance, GeneralSettingsViewModel generalSettings, @@ -30,7 +28,6 @@ public SettingsViewModel( AboutViewModel about) { PersonalUsage = personalUsage; - ApiBilling = apiBilling; CliAccount = cliAccount; Appearance = appearance; GeneralSettings = generalSettings; @@ -44,7 +41,6 @@ partial void OnSelectedTabChanged(string value) CurrentView = value switch { "PersonalUsage" => PersonalUsage, - "ApiBilling" => ApiBilling, "CliAccount" => CliAccount, "Appearance" => Appearance, "General" => GeneralSettings, diff --git a/src/ClaudeTracker/Views/FloatingUsageWindow.xaml.cs b/src/ClaudeTracker/Views/FloatingUsageWindow.xaml.cs index e0104df..873c711 100644 --- a/src/ClaudeTracker/Views/FloatingUsageWindow.xaml.cs +++ b/src/ClaudeTracker/Views/FloatingUsageWindow.xaml.cs @@ -57,7 +57,7 @@ public FloatingUsageWindow() var profile = App.Services.GetRequiredService().ActiveProfile; if (profile == null) return; - var usage = profile.ClaudeUsage; + var usage = profile.HasClaudeAI ? profile.ClaudeUsage : null; if (usage != null) LastUpdatedText.Text = $"Updated {Utilities.FormatterHelper.FormatTimeAgo(usage.LastUpdated)}"; else if (profile.ApiUsage != null) diff --git a/src/ClaudeTracker/Views/PopoverWindow.xaml.cs b/src/ClaudeTracker/Views/PopoverWindow.xaml.cs index 5a896e5..21ac11c 100644 --- a/src/ClaudeTracker/Views/PopoverWindow.xaml.cs +++ b/src/ClaudeTracker/Views/PopoverWindow.xaml.cs @@ -215,7 +215,10 @@ private void UpdateUI() else if (!_viewModel.HasClaudeUsage && !_viewModel.HasApiUsage && !_viewModel.HasPersonalMetrics) { StatusDot.Fill = new SolidColorBrush(Color.FromRgb(0xFF, 0x98, 0x00)); // orange - StatusText.Text = "Waiting for usage data..."; + // Show error/rate-limit message if available, otherwise generic waiting text + StatusText.Text = !string.IsNullOrEmpty(_viewModel.LastUpdatedText) + ? _viewModel.LastUpdatedText + : "Waiting for usage data..."; } else { diff --git a/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml b/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml deleted file mode 100644 index 4ecac32..0000000 --- a/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - -