< Summary

Information
Class: Pomodoro.Web.Pages.HistoryBase
Assembly: Pomodoro.Web
File(s): /home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Pages/History.razor.cs
Line coverage
100%
Covered lines: 335
Uncovered lines: 0
Coverable lines: 335
Total lines: 683
Line coverage: 100%
Branch coverage
100%
Covered branches: 74
Total branches: 74
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ActivityService()100%11100%
get_JSRuntime()100%11100%
get_InfiniteScrollInterop()100%11100%
get_Logger()100%11100%
get_HistoryStatsService()100%11100%
get_HistoryPagePresenterService()100%11100%
get_LocalDateTimeService()100%11100%
get_SelectedDate()100%11100%
get_SelectedWeekStart()100%11100%
get_ActiveTab()100%11100%
get_CurrentActivities()100%11100%
get_CurrentStats()100%11100%
get_WeeklyStats()100%11100%
get_WeeklyFocusMinutes()100%11100%
get_WeeklyBreakMinutes()100%11100%
get_CurrentSkip()100%11100%
get_HasMoreActivities()100%11100%
get_IsLoadingMore()100%11100%
get_PageSize()100%11100%
get_InitialActiveTab()100%11100%
get_InitialWeeklyStats()100%11100%
get_InitialHasMoreActivities()100%11100%
get_InitialIsLoadingMore()100%11100%
get_InitialActivities()100%11100%
get_InitialCurrentStats()100%11100%
get_InitialSelectedDate()100%11100%
get_InitialSelectedWeekStart()100%11100%
.ctor()100%11100%
OnParametersSet()100%1414100%
OnInitializedAsync()100%11100%
OnAfterRenderAsync()100%44100%
ShouldSetupInfiniteScrollObserver()100%88100%
SetupInfiniteScrollObserverAsync()100%22100%
CanProceedWithObserverSetupAsync()100%11100%
ExecuteObserverSetupWithLockAsync()100%22100%
ShouldRetryObserverSetup(...)100%44100%
AcquireObserverLockAsync()100%22100%
HandleRetryAsync()100%11100%
ReleaseObserverLockIfHeld()100%22100%
TryCreateObserverAsync()100%22100%
IsIntersectionObserverSupportedAsync()100%22100%
CreateObserverWithInteropAsync()100%11100%
HandleObserverCreationResult(...)100%22100%
HandleObserverCreationFailure(...)100%22100%
SetupRetryParameters(...)100%11100%
ExecuteRetryAsync()100%44100%
get_ShouldRetry()100%11100%
get_NextRetryCount()100%11100%
get_BackoffDelay()100%11100%
OnSentinelIntersecting()100%1010100%
OnActivityChanged()100%11100%
LoadDataAsync()100%11100%
CalculateStats(...)100%11100%
FormatFocusTime(...)100%11100%
HandleDateChanged()100%22100%
HandleTabChanged()100%44100%
HandleWeekChanged()100%11100%
LoadMoreActivitiesAsync()100%44100%
DisposeAsync()100%44100%

File(s)

/home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Pages/History.razor.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Components;
 2using Microsoft.JSInterop;
 3using Microsoft.Extensions.Logging;
 4using Pomodoro.Web.Components.History;
 5using Pomodoro.Web.Models;
 6using Pomodoro.Web.Services;
 7using System.Threading;
 8
 9namespace Pomodoro.Web.Pages;
 10
 11/// <summary>
 12/// Code-behind for History page
 13/// Displays activity history with summary cards and timeline
 14/// </summary>
 15public class HistoryBase : ComponentBase, IAsyncDisposable
 16{
 17    #region Services (Dependency Injection)
 18
 19    [Inject]
 187820    protected IActivityService ActivityService { get; set; } = default!;
 21
 22    [Inject]
 31623    protected IJSRuntime JSRuntime { get; set; } = default!;
 24
 25    [Inject]
 56826    protected IInfiniteScrollInterop InfiniteScrollInterop { get; set; } = default!;
 27
 28    [Inject]
 75229    protected ILogger<HistoryBase> Logger { get; set; } = default!;
 30
 31    [Inject]
 49332    protected IHistoryStatsService HistoryStatsService { get; set; } = default!;
 33
 34    [Inject]
 31735    protected HistoryPagePresenterService HistoryPagePresenterService { get; set; } = default!;
 36
 37    [Inject]
 47238    protected ILocalDateTimeService LocalDateTimeService { get; set; } = default!;
 39
 40    #endregion
 41
 42    #region State
 43
 61544    protected DateTime SelectedDate { get; set; } = DateTime.Now.Date;
 35745    protected DateTime SelectedWeekStart { get; set; }
 46846    protected HistoryTab ActiveTab { get; set; } = HistoryTab.Daily;
 79747    protected List<ActivityRecord> CurrentActivities { get; set; } = new();
 77948    protected DailyStatsSummary CurrentStats { get; set; } = new();
 20049    protected WeeklyStats? WeeklyStats { get; set; }
 35150    protected Dictionary<DateTime, int> WeeklyFocusMinutes { get; set; } = new();
 35151    protected Dictionary<DateTime, int> WeeklyBreakMinutes { get; set; } = new();
 56052    protected int CurrentSkip { get; set; }
 73453    protected bool HasMoreActivities { get; set; }
 30554    protected bool IsLoadingMore { get; set; }
 34455    protected int PageSize { get; } = 20;
 56
 57    /// <summary>
 58    /// Component parameter for testing: Sets initial active tab
 59    /// </summary>
 60    [Parameter]
 34561    public HistoryTab InitialActiveTab { get; set; } = HistoryTab.Daily;
 62
 63    /// <summary>
 64    /// Component parameter for testing: Sets the initial weekly stats
 65    /// </summary>
 66    [Parameter]
 17767    public WeeklyStats? InitialWeeklyStats { get; set; }
 68
 69    /// <summary>
 70    /// Component parameter for testing: Sets whether there are more activities to load
 71    /// </summary>
 72    [Parameter]
 33073    public bool InitialHasMoreActivities { get; set; } = false;
 74
 75    /// <summary>
 76    /// Component parameter for testing: Sets whether more activities are loading
 77    /// </summary>
 78    [Parameter]
 32479    public bool InitialIsLoadingMore { get; set; } = false;
 80
 81    /// <summary>
 82    /// Component parameter for testing: Sets the initial activities list
 83    /// </summary>
 84    [Parameter]
 49685    public List<ActivityRecord> InitialActivities { get; set; } = new();
 86
 87    /// <summary>
 88    /// Component parameter for testing: Sets the initial current stats
 89    /// </summary>
 90    [Parameter]
 16591    public DailyStatsSummary? InitialCurrentStats { get; set; }
 92
 93    /// <summary>
 94    /// Component parameter for testing: Sets the initial selected date
 95    /// </summary>
 96    [Parameter]
 16597    public DateTime? InitialSelectedDate { get; set; }
 98
 99    /// <summary>
 100    /// Component parameter for testing: Sets the initial selected week start
 101    /// </summary>
 102    [Parameter]
 165103    public DateTime? InitialSelectedWeekStart { get; set; }
 104
 105    // Infinite scroll state
 106    private DotNetObjectReference<HistoryBase>? _dotNetRef;
 107    private bool _observerInitialized;
 108    private bool _isDisposed;
 109    private bool _isCallbackInProgress;
 158110    private SemaphoreSlim _observerSetupLock = new SemaphoreSlim(1, 1);
 111
 112    #endregion
 113
 114    #region Lifecycle Methods
 115
 116    protected override void OnParametersSet()
 163117    {
 163118        base.OnParametersSet();
 119
 120        // Set protected properties from component parameters for testing purposes
 121        // This allows tests to control conditional rendering paths
 163122        if (InitialActiveTab != HistoryTab.Daily)
 11123        {
 11124            ActiveTab = InitialActiveTab;
 11125        }
 126
 163127        if (InitialWeeklyStats != null)
 7128        {
 7129            WeeklyStats = InitialWeeklyStats;
 7130        }
 131
 163132        HasMoreActivities = InitialHasMoreActivities;
 133
 163134        IsLoadingMore = InitialIsLoadingMore;
 135
 163136        if (InitialActivities != null && InitialActivities.Count > 0)
 6137        {
 6138            CurrentActivities = InitialActivities;
 6139        }
 140
 163141        if (InitialCurrentStats != null)
 1142        {
 1143            CurrentStats = InitialCurrentStats;
 1144        }
 145
 163146        if (InitialSelectedDate.HasValue)
 1147        {
 1148            SelectedDate = InitialSelectedDate.Value;
 1149        }
 150
 163151        if (InitialSelectedWeekStart.HasValue)
 1152        {
 1153            SelectedWeekStart = InitialSelectedWeekStart.Value;
 1154        }
 163155    }
 156
 157    protected override async Task OnInitializedAsync()
 157158    {
 159        // Subscribe to activity changes
 157160        ActivityService.OnActivityChanged += OnActivityChanged;
 161
 162        // Initialize activity service if needed
 157163        await ActivityService.InitializeAsync();
 164
 165        // Initialize SelectedDate and SelectedWeekStart to client's local date
 157166        var localDate = await LocalDateTimeService.GetLocalDateAsync();
 157167        SelectedDate = localDate;
 157168        SelectedWeekStart = WeekNavigatorBase.GetWeekStart(localDate);
 169
 170        // Load initial data
 157171        await LoadDataAsync();
 157172    }
 173
 174    protected override async Task OnAfterRenderAsync(bool firstRender)
 238175    {
 176        // Always recreate DotNetObjectReference if it's null, not just on first render
 177        // This prevents memory leaks if component is re-rendered after a failed initialization
 238178        if (_dotNetRef == null)
 157179        {
 157180            _dotNetRef = DotNetObjectReference.Create(this);
 157181        }
 182
 183        // Set up intersection observer when conditions are met
 238184        if (ShouldSetupInfiniteScrollObserver())
 47185        {
 186            // Small delay ensures DOM update cycle completes before observer creation
 47187            await Task.Delay(50);
 47188            await SetupInfiniteScrollObserverAsync(retryCount: 0);
 35189        }
 226190    }
 191
 192    /// <summary>
 193    /// Determines if the infinite scroll observer should be set up
 194    /// </summary>
 195    /// <returns>True if observer setup should be attempted</returns>
 196    private bool ShouldSetupInfiniteScrollObserver()
 238197    {
 198        // Only attempt if:
 199        // 1. Component is not disposed
 200        // 2. There are more activities to load
 201        // 3. Observer is not already initialized
 202        // 4. DotNet reference is available
 203        // 5. Currently in Daily view (which has timeline)
 238204        return !_isDisposed &&
 238205               HasMoreActivities &&
 238206               !_observerInitialized &&
 238207               _dotNetRef != null &&
 238208               ActiveTab == HistoryTab.Daily;
 238209    }
 210
 211    /// <summary>
 212    /// Sets up the Intersection Observer for infinite scroll
 213    /// </summary>
 214    /// <param name="retryCount">Current retry attempt (0-2 for max 3 attempts)</param>
 215    private async Task SetupInfiniteScrollObserverAsync(int retryCount = 0)
 58216    {
 58217        if (!await CanProceedWithObserverSetupAsync())
 11218        {
 11219            return;
 220        }
 221
 35222        await ExecuteObserverSetupWithLockAsync(retryCount);
 46223    }
 224
 225    /// <summary>
 226    /// Determines if observer setup can proceed
 227    /// </summary>
 228    /// <returns>True if setup can proceed, false otherwise</returns>
 229    private async Task<bool> CanProceedWithObserverSetupAsync()
 58230    {
 58231        return await AcquireObserverLockAsync();
 46232    }
 233
 234    /// <summary>
 235    /// Executes the observer setup with proper lock management
 236    /// </summary>
 237    /// <param name="retryCount">Current retry attempt</param>
 238    private async Task ExecuteObserverSetupWithLockAsync(int retryCount)
 35239    {
 240        try
 35241        {
 35242            var setupResult = await TryCreateObserverAsync(retryCount);
 243
 35244            if (ShouldRetryObserverSetup(setupResult))
 10245            {
 10246                await HandleRetryAsync(setupResult);
 10247                return;
 248            }
 25249        }
 250        finally
 35251        {
 35252            ReleaseObserverLockIfHeld();
 35253        }
 35254    }
 255
 256    /// <summary>
 257    /// Determines if observer setup should be retried
 258    /// </summary>
 259    /// <param name="setupResult">The result of the observer setup attempt</param>
 260    /// <returns>True if retry is needed, false otherwise</returns>
 261    private bool ShouldRetryObserverSetup(ObserverSetupResult setupResult)
 35262    {
 35263        return setupResult.ShouldRetry && !_isDisposed && !_observerInitialized;
 35264    }
 265
 266    /// <summary>
 267    /// Acquires the observer setup lock
 268    /// </summary>
 269    /// <returns>True if lock was acquired, false if another setup is in progress</returns>
 270    private async Task<bool> AcquireObserverLockAsync()
 58271    {
 272        // Prevent concurrent initialization attempts using async lock
 58273        if (!await _observerSetupLock.WaitAsync(0))
 11274        {
 11275            Logger.LogDebug("Infinite scroll observer setup already in progress, skipping");
 11276            return false;
 277        }
 278
 35279        return true;
 46280    }
 281
 282    /// <summary>
 283    /// Handles the retry logic for observer setup
 284    /// </summary>
 285    /// <param name="setupResult">The result of the observer setup attempt</param>
 286    private async Task HandleRetryAsync(ObserverSetupResult setupResult)
 10287    {
 288        // Release lock before retry to allow retry to acquire it
 10289        _observerSetupLock.Release();
 10290        await ExecuteRetryAsync(setupResult.NextRetryCount, setupResult.BackoffDelay);
 10291    }
 292
 293    /// <summary>
 294    /// Releases the observer lock if it's still held
 295    /// </summary>
 296    private void ReleaseObserverLockIfHeld()
 35297    {
 298        // Only release if we didn't already release for retry
 35299        if (_observerSetupLock.CurrentCount == 0)
 25300        {
 25301            _observerSetupLock.Release();
 25302        }
 35303    }
 304
 305    /// <summary>
 306    /// Attempts to create the infinite scroll observer
 307    /// </summary>
 308    /// <param name="retryCount">Current retry attempt</param>
 309    /// <returns>Setup result indicating success and whether retry is needed</returns>
 310    private async Task<ObserverSetupResult> TryCreateObserverAsync(int retryCount)
 35311    {
 35312        var result = new ObserverSetupResult();
 313
 314        try
 35315        {
 35316            if (!await IsIntersectionObserverSupportedAsync())
 11317            {
 11318                return result;
 319            }
 320
 24321            var success = await CreateObserverWithInteropAsync();
 322
 23323            HandleObserverCreationResult(success, retryCount, result);
 23324        }
 1325        catch (Exception ex)
 1326        {
 1327            Logger.LogError(ex, "Failed to initialize infinite scroll observer");
 1328        }
 329
 24330        return result;
 35331    }
 332
 333    /// <summary>
 334    /// Checks if Intersection Observer API is supported
 335    /// </summary>
 336    /// <returns>True if supported, false otherwise</returns>
 337    private async Task<bool> IsIntersectionObserverSupportedAsync()
 35338    {
 35339        var supported = await InfiniteScrollInterop.IsSupportedAsync();
 35340        if (!supported)
 11341        {
 11342            Logger.LogWarning("Intersection Observer API not supported");
 11343        }
 35344        return supported;
 35345    }
 346
 347    /// <summary>
 348    /// Creates the observer using interop
 349    /// </summary>
 350    /// <returns>True if creation was successful, false otherwise</returns>
 351    private async Task<bool> CreateObserverWithInteropAsync()
 24352    {
 24353        return await InfiniteScrollInterop.CreateObserverAsync(
 24354            Constants.UI.InfiniteScrollSentinelId,
 24355            DotNetObjectReference.Create((object)_dotNetRef!.Value),
 24356            Constants.UI.TimelineScrollContainerId,
 24357            Constants.UI.InfiniteScrollRootMargin,
 24358            Constants.UI.InfiniteScrollTimeoutMs);
 23359    }
 360
 361    /// <summary>
 362    /// Handles the result of observer creation
 363    /// </summary>
 364    /// <param name="success">Whether observer creation was successful</param>
 365    /// <param name="retryCount">Current retry attempt</param>
 366    /// <param name="result">Result object to update</param>
 367    private void HandleObserverCreationResult(bool success, int retryCount, ObserverSetupResult result)
 23368    {
 23369        if (success)
 12370        {
 12371            _observerInitialized = true;
 12372            Logger.LogDebug("Infinite scroll observer initialized");
 12373        }
 374        else
 11375        {
 11376            HandleObserverCreationFailure(retryCount, result);
 11377        }
 23378    }
 379
 380    /// <summary>
 381    /// Handles failure of observer creation
 382    /// </summary>
 383    /// <param name="retryCount">Current retry attempt</param>
 384    /// <param name="result">Result object to update</param>
 385    private void HandleObserverCreationFailure(int retryCount, ObserverSetupResult result)
 11386    {
 11387        if (retryCount < 2)
 10388        {
 10389            SetupRetryParameters(retryCount, result);
 10390            Logger.LogDebug("Observer setup failed, retrying in {Delay}ms (attempt {Attempt}/3)",
 10391                result.BackoffDelay, result.NextRetryCount + 1);
 10392        }
 393        else
 1394        {
 1395            Logger.LogWarning("Infinite scroll observer setup failed after 3 attempts");
 1396        }
 11397    }
 398
 399    /// <summary>
 400    /// Sets up retry parameters
 401    /// </summary>
 402    /// <param name="retryCount">Current retry attempt</param>
 403    /// <param name="result">Result object to update</param>
 404    private void SetupRetryParameters(int retryCount, ObserverSetupResult result)
 10405    {
 10406        result.ShouldRetry = true;
 10407        result.NextRetryCount = retryCount + 1;
 10408        result.BackoffDelay = 100 * (retryCount + 1);
 10409    }
 410
 411    /// <summary>
 412    /// Executes retry attempt with proper locking
 413    /// </summary>
 414    /// <param name="nextRetryCount">Next retry attempt number</param>
 415    /// <param name="backoffDelay">Delay before retry in milliseconds</param>
 416    private async Task ExecuteRetryAsync(int nextRetryCount, int backoffDelay)
 11417    {
 11418        await Task.Delay(backoffDelay);
 419
 420        // Re-acquire lock for retry attempt
 11421        if (!await _observerSetupLock.WaitAsync(0))
 1422        {
 1423            Logger.LogDebug("Observer retry skipped - another setup is already in progress");
 1424            return;
 425        }
 426
 427        try
 10428        {
 429            // Check again if observer is still not initialized before retrying
 430            // This prevents race condition where another thread initialized it during delay
 10431            if (!_observerInitialized)
 10432            {
 10433                await SetupInfiniteScrollObserverAsync(nextRetryCount);
 10434            }
 10435        }
 436        finally
 10437        {
 10438            _observerSetupLock.Release();
 10439        }
 11440    }
 441
 442    /// <summary>
 443    /// Result of observer setup attempt
 444    /// </summary>
 445    private class ObserverSetupResult
 446    {
 45447        public bool ShouldRetry { get; set; }
 30448        public int NextRetryCount { get; set; }
 30449        public int BackoffDelay { get; set; }
 450    }
 451
 452    /// <summary>
 453    /// Callback from JavaScript when sentinel element is visible
 454    /// </summary>
 455    [JSInvokable]
 456    public async Task OnSentinelIntersecting()
 21457    {
 21458        if (_isDisposed || _isCallbackInProgress || _dotNetRef == null || IsLoadingMore || !HasMoreActivities)
 14459        {
 14460            return;
 461        }
 462
 7463        _isCallbackInProgress = true;
 464        try
 7465        {
 7466            await LoadMoreActivitiesAsync();
 2467        }
 5468        catch (Exception ex)
 5469        {
 5470            Logger.LogError(ex, "Failed to load more activities on sentinel intersect");
 471            // Don't re-throw - JS side already handles cleanup in the catch block
 472            // Add delay before allowing next callback to prevent rapid retries on errors
 5473            await Task.Delay(1000);
 5474        }
 475        finally
 7476        {
 7477            _isCallbackInProgress = false;
 7478        }
 21479    }
 480
 481    private void OnActivityChanged()
 6482    {
 6483        SafeTaskRunner.RunAndForget(async () =>
 6484        {
 6485            await InvokeAsync(async () =>
 6486            {
 6487                await LoadDataAsync();
 6488                StateHasChanged();
 12489            });
 12490        }, Logger, "OnActivityChanged");
 6491    }
 492
 493    private async Task LoadDataAsync()
 177494    {
 177495        var today = AppState.GetCurrentDayKey();
 177496        var selectedDate = SelectedDate.Date;
 497
 498        // Reset pagination state when loading new date
 177499        CurrentSkip = 0;
 500
 501        // Reset observer state when loading new date
 177502        _observerInitialized = false;
 503
 504        // Load activities for selected date (Daily view) - initial page only using async pagination
 177505        CurrentActivities = await ActivityService.GetActivitiesPagedAsync(
 177506            selectedDate, selectedDate.AddDays(1), 0, PageSize);
 507
 508        // Update skip to reflect loaded count
 177509        CurrentSkip = CurrentActivities.Count;
 510
 511        // Calculate stats for selected date (use all activities for accurate stats)
 177512        var allActivitiesForDate = ActivityService.GetActivitiesForDate(selectedDate);
 177513        CurrentStats = CalculateStats(allActivitiesForDate);
 514
 515        // Check if there are more activities to load
 177516        var totalCount = await ActivityService.GetActivityCountAsync(selectedDate, selectedDate.AddDays(1));
 177517        HasMoreActivities = CurrentSkip < totalCount;
 518
 519        // Log for debugging
 177520        Logger.LogDebug(Constants.Messages.LogHistoryLoadDataFormat,
 177521            selectedDate, today, selectedDate == today);
 177522        Logger.LogDebug(Constants.Messages.LogHistoryStatsFormat,
 177523            CurrentActivities.Count, CurrentStats.PomodoroCount, CurrentStats.FocusMinutes);
 524
 525        // Use SelectedWeekStart for weekly data (independent from daily view)
 177526        var weekStart = SelectedWeekStart;
 177527        var weekEnd = weekStart.AddDays(6); // Friday
 528
 529        // Load weekly data for chart (Saturday to Friday week)
 177530        WeeklyFocusMinutes = ActivityService.GetDailyFocusMinutes(weekStart, weekEnd);
 177531        WeeklyBreakMinutes = ActivityService.GetDailyBreakMinutes(weekStart, weekEnd);
 532
 533        // Load weekly statistics
 177534        WeeklyStats = await ActivityService.GetWeeklyStatsAsync(weekStart);
 535
 536        // Observer will be set up in OnAfterRenderAsync after DOM is fully updated
 537        // This ensures sentinel element exists before observer is created
 177538    }
 539
 540    private DailyStatsSummary CalculateStats(List<ActivityRecord> activities)
 177541    {
 177542        return HistoryStatsService.CalculateStats(activities);
 177543    }
 544
 545    /// <summary>
 546    /// Format focus time for display
 547    /// </summary>
 548    protected string FormatFocusTime(int minutes)
 2549    {
 2550        return HistoryPagePresenterService.FormatFocusTime(minutes);
 2551    }
 552
 553    #endregion
 554
 555    #region Event Handlers
 556
 557    protected async Task HandleDateChanged(DateTime newDate)
 10558    {
 10559        SelectedDate = newDate;
 10560        CurrentSkip = 0;
 561
 562        // Explicitly destroy observer before resetting state
 10563        if (_observerInitialized)
 3564        {
 565            try
 3566            {
 3567                await InfiniteScrollInterop.DestroyObserverAsync(Constants.UI.InfiniteScrollSentinelId);
 1568            }
 2569            catch (Exception ex)
 2570            {
 2571                Logger.LogWarning(ex, "Failed to destroy observer on date change");
 2572            }
 3573        }
 574
 10575        _observerInitialized = false;
 10576        await LoadDataAsync();
 10577        StateHasChanged();
 10578    }
 579
 580    protected async Task HandleTabChanged(HistoryTab newTab)
 11581    {
 11582        ActiveTab = newTab;
 583
 584        // Clean up observer when leaving Daily view
 11585        if (newTab != HistoryTab.Daily && _observerInitialized)
 4586        {
 587            try
 4588            {
 4589                await InfiniteScrollInterop.DestroyObserverAsync(Constants.UI.InfiniteScrollSentinelId);
 2590            }
 2591            catch (Exception ex)
 2592            {
 2593                Logger.LogWarning(ex, "Failed to destroy observer on tab change");
 2594            }
 4595            _observerInitialized = false;
 4596        }
 597
 11598        StateHasChanged();
 11599    }
 600
 601    protected async Task HandleWeekChanged(DateTime newWeekStart)
 4602    {
 4603        SelectedWeekStart = newWeekStart;
 4604        await LoadDataAsync();
 4605        StateHasChanged();
 4606    }
 607
 608    /// <summary>
 609    /// Loads more activities with lazy loading
 610    /// </summary>
 611    protected async Task LoadMoreActivitiesAsync()
 11612    {
 14613        if (IsLoadingMore || !HasMoreActivities) return;
 614
 615        try
 8616        {
 8617            IsLoadingMore = true;
 8618            StateHasChanged();
 619
 8620            var newActivities = await ActivityService.GetActivitiesPagedAsync(
 8621                SelectedDate,
 8622                SelectedDate.AddDays(1),
 8623                CurrentSkip,
 8624                PageSize);
 625
 3626            CurrentActivities.AddRange(newActivities);
 3627            CurrentSkip += newActivities.Count;
 628
 629            // Check if there are more activities
 3630            var totalCount = await ActivityService.GetActivityCountAsync(SelectedDate, SelectedDate.AddDays(1));
 3631            HasMoreActivities = CurrentSkip < totalCount;
 632
 633            // Note: Observer doesn't need re-initialization here because the sentinel element
 634            // remains in the DOM. As new activities are added above it, it moves further down
 635            // the page and will trigger again when scrolled into view.
 636
 637            // After loading more items, the sentinel moves down the page.
 638            // The observer will automatically detect when it comes back into view.
 3639        }
 640        finally
 8641        {
 8642            IsLoadingMore = false;
 8643            StateHasChanged();
 8644        }
 6645    }
 646
 647    #endregion
 648
 649    #region IAsyncDisposable
 650
 651    public async ValueTask DisposeAsync()
 175652    {
 175653        _isDisposed = true;
 175654        _observerSetupLock?.Dispose();
 655
 175656        ActivityService.OnActivityChanged -= OnActivityChanged;
 657
 658        // Clean up JavaScript observer
 175659        if (_dotNetRef != null)
 172660        {
 661            try
 172662            {
 172663                await InfiniteScrollInterop.DestroyObserverAsync(Constants.UI.InfiniteScrollSentinelId);
 158664            }
 14665            catch (Exception ex)
 14666            {
 14667                Logger.LogWarning(ex, "Failed to destroy observer by ID, attempting to destroy all");
 668                try
 14669                {
 14670                    await InfiniteScrollInterop.DestroyAllObserversAsync();
 8671                }
 6672                catch (Exception fallbackEx)
 6673                {
 6674                    Logger.LogWarning(fallbackEx, "Failed to destroy all observers during disposal");
 6675                }
 14676            }
 677
 172678            _dotNetRef.Dispose();
 172679        }
 175680    }
 681
 682    #endregion
 683}

Methods/Properties

get_ActivityService()
get_JSRuntime()
get_InfiniteScrollInterop()
get_Logger()
get_HistoryStatsService()
get_HistoryPagePresenterService()
get_LocalDateTimeService()
get_SelectedDate()
get_SelectedWeekStart()
get_ActiveTab()
get_CurrentActivities()
get_CurrentStats()
get_WeeklyStats()
get_WeeklyFocusMinutes()
get_WeeklyBreakMinutes()
get_CurrentSkip()
get_HasMoreActivities()
get_IsLoadingMore()
get_PageSize()
get_InitialActiveTab()
get_InitialWeeklyStats()
get_InitialHasMoreActivities()
get_InitialIsLoadingMore()
get_InitialActivities()
get_InitialCurrentStats()
get_InitialSelectedDate()
get_InitialSelectedWeekStart()
.ctor()
OnParametersSet()
OnInitializedAsync()
OnAfterRenderAsync()
ShouldSetupInfiniteScrollObserver()
SetupInfiniteScrollObserverAsync()
CanProceedWithObserverSetupAsync()
ExecuteObserverSetupWithLockAsync()
ShouldRetryObserverSetup(Pomodoro.Web.Pages.HistoryBase/ObserverSetupResult)
AcquireObserverLockAsync()
HandleRetryAsync()
ReleaseObserverLockIfHeld()
TryCreateObserverAsync()
IsIntersectionObserverSupportedAsync()
CreateObserverWithInteropAsync()
HandleObserverCreationResult(System.Boolean,System.Int32,Pomodoro.Web.Pages.HistoryBase/ObserverSetupResult)
HandleObserverCreationFailure(System.Int32,Pomodoro.Web.Pages.HistoryBase/ObserverSetupResult)
SetupRetryParameters(System.Int32,Pomodoro.Web.Pages.HistoryBase/ObserverSetupResult)
ExecuteRetryAsync()
get_ShouldRetry()
get_NextRetryCount()
get_BackoffDelay()
OnSentinelIntersecting()
OnActivityChanged()
LoadDataAsync()
CalculateStats(System.Collections.Generic.List`1<Pomodoro.Web.Models.ActivityRecord>)
FormatFocusTime(System.Int32)
HandleDateChanged()
HandleTabChanged()
HandleWeekChanged()
LoadMoreActivitiesAsync()
DisposeAsync()