66@using DevMetricsPro .Web .Components .Shared
77@using DevMetricsPro .Web .Components .Shared .Charts
88@using DevMetricsPro .Web .Services
9+ @using Microsoft .AspNetCore .SignalR .Client
10+ @implements IAsyncDisposable
911@inject AuthStateService AuthState
1012@inject IChartDataService ChartDataService
1113@inject ILeaderboardService LeaderboardService
14+ @inject SignalRService SignalR
1215@inject HttpClient Http
1316@inject NavigationManager Navigation
1417@inject ILogger <Home > Logger
1518@inject ISnackbar Snackbar
1619
1720<PageTitle >Dashboard - DevMetrics Pro</PageTitle >
1821
22+ <!-- Sync Status Indicator -->
23+ @if (_isSyncing )
24+ {
25+ <MudAlert Severity =" Severity.Info" Class =" mb-4" Icon =" @Icons.Material.Filled.Sync" >
26+ <div style =" display : flex ; align-items : center ; gap : 12px ;" >
27+ <MudProgressCircular Size =" Size.Small" Indeterminate =" true" />
28+ <span >Syncing data from GitHub .. . Dashboard will update automatically .</span >
29+ </div >
30+ </MudAlert >
31+ }
32+
33+ <!-- Connection Status (only show when disconnected) -->
34+ @if (_isAuthenticated && ! _isSignalRConnected && _signalRConnectionAttempted )
35+ {
36+ <MudAlert Severity =" Severity.Warning" Class =" mb-4" Icon =" @Icons.Material.Filled.CloudOff" >
37+ <span >Real - time updates unavailable . Data will still refresh on page reload .</span >
38+ </MudAlert >
39+ }
40+
1941@if (! _isAuthenticated )
2042{
2143 <div style =" text-align : center ; padding : 60px 20px ;" >
@@ -380,18 +402,31 @@ else
380402 private List <LeaderboardEntryDto >? _leaderboardEntries ;
381403 private bool _isLoadingLeaderboard = false ;
382404 private LeaderboardMetric _selectedMetric = LeaderboardMetric .Commits ;
405+
406+ // SignalR state - Phase 3.7
407+ private bool _isSyncing = false ;
408+ private bool _isSignalRConnected = false ;
409+ private bool _signalRConnectionAttempted = false ;
410+ private string ? _currentUserId ;
411+
383412 protected override async Task OnInitializedAsync ()
384413 {
385414 _isAuthenticated = await AuthState .IsAuthenticatedAsync ();
386415
387416 if (_isAuthenticated )
388417 {
418+ // Get user ID for SignalR
419+ _currentUserId = await AuthState .GetUserIdAsync ();
420+
389421 await CheckGitHubConnectionStatus ();
390422 await LoadRecentCommitsAsync ();
391423 await LoadCommitActivityAsync (7 );
392424 await LoadPRStatsAsync (30 );
393425 await LoadHeatmapAsync (52 );
394426 await LoadLeaderboardAsync (_selectedMetric );
427+
428+ // Initialize SignalR connection
429+ await InitializeSignalRAsync ();
395430 }
396431
397432 // Check if we were redirected back from GitHub OAuth
@@ -404,6 +439,117 @@ else
404439 }
405440 }
406441
442+ private async Task InitializeSignalRAsync ()
443+ {
444+ if (string .IsNullOrEmpty (_currentUserId ))
445+ return ;
446+
447+ try
448+ {
449+ // Register event handlers
450+ SignalR .OnSyncStarted += HandleSyncStarted ;
451+ SignalR .OnSyncCompleted += HandleSyncCompleted ;
452+ SignalR .OnMetricsUpdated += HandleMetricsUpdated ;
453+ SignalR .OnConnectionStateChanged += HandleConnectionStateChanged ;
454+
455+ // Start connection
456+ await SignalR .StartAsync (_currentUserId );
457+ _isSignalRConnected = true ;
458+ _signalRConnectionAttempted = true ;
459+
460+ Logger .LogInformation (" SignalR connected for dashboard updates" );
461+ }
462+ catch (Exception ex )
463+ {
464+ Logger .LogError (ex , " Failed to initialize SignalR connection" );
465+ _isSignalRConnected = false ;
466+ _signalRConnectionAttempted = true ;
467+ // Don't show error - dashboard still works without real-time updates
468+ }
469+ }
470+
471+ private async Task HandleSyncStarted ()
472+ {
473+ await InvokeAsync (() =>
474+ {
475+ _isSyncing = true ;
476+ StateHasChanged ();
477+ });
478+ }
479+
480+ private async Task HandleSyncCompleted (SyncResultDto result )
481+ {
482+ await InvokeAsync (async () =>
483+ {
484+ _isSyncing = false ;
485+
486+ if (result .Success )
487+ {
488+ // Show success notification
489+ Snackbar .Add (
490+ $" Sync complete! {result .RepositoriesSynced } repos, {result .CommitsSynced } commits, {result .PullRequestsSynced } PRs" ,
491+ Severity .Success ,
492+ config => config .Icon = Icons .Material .Filled .CheckCircle );
493+
494+ // Refresh all dashboard data
495+ await RefreshAllDataAsync ();
496+ }
497+ else
498+ {
499+ Snackbar .Add (
500+ $" Sync failed: {result .ErrorMessage ?? " Unknown error" }" ,
501+ Severity .Error ,
502+ config => config .Icon = Icons .Material .Filled .Error );
503+ }
504+
505+ StateHasChanged ();
506+ });
507+ }
508+
509+ private async Task HandleMetricsUpdated ()
510+ {
511+ await InvokeAsync (async () =>
512+ {
513+ Snackbar .Add (" Metrics updated!" , Severity .Info , config => config .Icon = Icons .Material .Filled .Refresh );
514+ await RefreshAllDataAsync ();
515+ StateHasChanged ();
516+ });
517+ }
518+
519+ private void HandleConnectionStateChanged (HubConnectionState state )
520+ {
521+ InvokeAsync (() =>
522+ {
523+ _isSignalRConnected = state == HubConnectionState .Connected ;
524+
525+ if (state == HubConnectionState .Reconnecting )
526+ {
527+ Snackbar .Add (" Connection lost. Reconnecting..." , Severity .Warning );
528+ }
529+ else if (state == HubConnectionState .Connected && _signalRConnectionAttempted )
530+ {
531+ Snackbar .Add (" Reconnected!" , Severity .Success );
532+ }
533+
534+ StateHasChanged ();
535+ });
536+ }
537+
538+ private async Task RefreshAllDataAsync ()
539+ {
540+ // Reload all data in parallel for efficiency
541+ var tasks = new List <Task >
542+ {
543+ LoadRecentCommitsAsync (),
544+ LoadCommitActivityAsync (_selectedDays ),
545+ LoadPRStatsAsync (_selectedPRDays ),
546+ LoadHeatmapAsync (_selectedWeeks ),
547+ LoadLeaderboardAsync (_selectedMetric )
548+ };
549+
550+ await Task .WhenAll (tasks );
551+ }
552+
407553 private async Task CheckGitHubConnectionStatus ()
408554 {
409555 try
@@ -679,4 +825,27 @@ else
679825 public int LinesAdded { get ; set ; }
680826 public int LinesRemoved { get ; set ; }
681827 }
828+
829+ public async ValueTask DisposeAsync ()
830+ {
831+ // Unregister event handlers
832+ SignalR .OnSyncStarted -= HandleSyncStarted ;
833+ SignalR .OnSyncCompleted -= HandleSyncCompleted ;
834+ SignalR .OnMetricsUpdated -= HandleMetricsUpdated ;
835+ SignalR .OnConnectionStateChanged -= HandleConnectionStateChanged ;
836+
837+ // Note: Don't dispose SignalR service here as it's managed by DI
838+ // Just leave the dashboard group
839+ if (! string .IsNullOrEmpty (_currentUserId ))
840+ {
841+ try
842+ {
843+ await SignalR .LeaveDashboardAsync ();
844+ }
845+ catch (Exception ex )
846+ {
847+ Logger .LogWarning (ex , " Error leaving dashboard group during dispose" );
848+ }
849+ }
850+ }
682851}
0 commit comments