Compare commits
3 Commits
151ecaa98f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 16eb688607 | |||
| 2132c130a3 | |||
| dffbc31432 |
@@ -5,6 +5,26 @@
|
|||||||
|
|
||||||
## RECENT CHANGES (This Session)
|
## RECENT CHANGES (This Session)
|
||||||
|
|
||||||
|
**SignalR Real-Time Updates & UI Consistency (Dec 8, 2025):**
|
||||||
|
- ✅ **Removed Manual Refresh Button** - Removed refresh button from Jobs/Index.razor (SignalR auto-refresh eliminates need)
|
||||||
|
- ✅ **SignalR on Layers List** - Added real-time updates to Layers/Index with EntityChangeHubService subscription
|
||||||
|
- ✅ **SignalR on DataInbox List** - Added real-time updates to DataInbox/Index with EntityChangeHubService subscription
|
||||||
|
- ✅ **SignalR on Layer Details** - Added real-time updates to Layers/Details for both layer and record changes
|
||||||
|
- ✅ **Consistent UI Behavior** - All lists now have uniform SignalR-based real-time updates
|
||||||
|
- ✅ **Proper Cleanup** - Implemented IDisposable pattern to unsubscribe from SignalR events on all pages
|
||||||
|
- ✅ **Jobs Sorting Fix** - Changed sorting from Priority→JobType→CreatedAt DESC to CreatedAt DESC→Priority ASC (newest jobs first, then by priority)
|
||||||
|
- ✅ **Faster Job Processing** - Reduced JobWorkerService poll interval from 10 seconds to 5 seconds
|
||||||
|
- Files modified:
|
||||||
|
- [Jobs/Index.razor](DiunaBI.UI.Shared/Pages/Jobs/Index.razor) - removed refresh button
|
||||||
|
- [Layers/Index.razor](DiunaBI.UI.Shared/Pages/Layers/Index.razor), [Layers/Index.razor.cs](DiunaBI.UI.Shared/Pages/Layers/Index.razor.cs) - added SignalR + IDisposable
|
||||||
|
- [DataInbox/Index.razor](DiunaBI.UI.Shared/Pages/DataInbox/Index.razor), [DataInbox/Index.razor.cs](DiunaBI.UI.Shared/Pages/DataInbox/Index.razor.cs) - added SignalR + IDisposable
|
||||||
|
- [Layers/Details.razor](DiunaBI.UI.Shared/Pages/Layers/Details.razor), [Layers/Details.razor.cs](DiunaBI.UI.Shared/Pages/Layers/Details.razor.cs) - added SignalR + IDisposable
|
||||||
|
- [JobsController.cs](DiunaBI.API/Controllers/JobsController.cs) - fixed sorting logic
|
||||||
|
- [JobWorkerService.cs](DiunaBI.Infrastructure/Services/JobWorkerService.cs) - reduced poll interval to 5 seconds
|
||||||
|
- Status: All lists have consistent real-time behavior, no manual refresh needed, jobs sorted by date first
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**Job Scheduler Race Condition Fix (Dec 8, 2025):**
|
**Job Scheduler Race Condition Fix (Dec 8, 2025):**
|
||||||
- ✅ **In-Memory Deduplication** - Added `HashSet<Guid>` to track LayerIds scheduled within the same batch
|
- ✅ **In-Memory Deduplication** - Added `HashSet<Guid>` to track LayerIds scheduled within the same batch
|
||||||
- ✅ **Prevents Duplicate Jobs** - Fixed race condition where same layer could be scheduled multiple times during single "Run All Jobs" operation
|
- ✅ **Prevents Duplicate Jobs** - Fixed race condition where same layer could be scheduled multiple times during single "Run All Jobs" operation
|
||||||
|
|||||||
@@ -71,11 +71,10 @@ public class JobsController : Controller
|
|||||||
|
|
||||||
var totalCount = await query.CountAsync();
|
var totalCount = await query.CountAsync();
|
||||||
|
|
||||||
// Sort by: Priority ASC (0=highest), JobType, then CreatedAt DESC
|
// Sort by: CreatedAt DESC (newest first), then Priority ASC (0=highest)
|
||||||
var items = await query
|
var items = await query
|
||||||
.OrderBy(j => j.Priority)
|
.OrderByDescending(j => j.CreatedAt)
|
||||||
.ThenBy(j => j.JobType)
|
.ThenBy(j => j.Priority)
|
||||||
.ThenByDescending(j => j.CreatedAt)
|
|
||||||
.Skip(start)
|
.Skip(start)
|
||||||
.Take(limit)
|
.Take(limit)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class JobWorkerService : BackgroundService
|
|||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<JobWorkerService> _logger;
|
private readonly ILogger<JobWorkerService> _logger;
|
||||||
private readonly TimeSpan _pollInterval = TimeSpan.FromSeconds(10);
|
private readonly TimeSpan _pollInterval = TimeSpan.FromSeconds(5);
|
||||||
private readonly TimeSpan _rateLimitDelay = TimeSpan.FromSeconds(5);
|
private readonly TimeSpan _rateLimitDelay = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
public JobWorkerService(IServiceProvider serviceProvider, ILogger<JobWorkerService> logger)
|
public JobWorkerService(IServiceProvider serviceProvider, ILogger<JobWorkerService> logger)
|
||||||
|
|||||||
@@ -7,33 +7,26 @@
|
|||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<MudThemeProvider Theme="_theme"/>
|
<MudThemeProvider Theme="_theme" />
|
||||||
<MudPopoverProvider/>
|
<MudPopoverProvider />
|
||||||
<MudDialogProvider/>
|
<MudDialogProvider />
|
||||||
<MudSnackbarProvider/>
|
<MudSnackbarProvider />
|
||||||
|
|
||||||
<MudLayout>
|
<MudLayout>
|
||||||
<MudBreakpointProvider OnBreakpointChanged="OnBreakpointChanged"></MudBreakpointProvider>
|
<MudBreakpointProvider OnBreakpointChanged="OnBreakpointChanged"></MudBreakpointProvider>
|
||||||
<MudAppBar Elevation="0">
|
<MudAppBar Elevation="0">
|
||||||
<MudIconButton
|
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start"
|
||||||
Icon="@Icons.Material.Filled.Menu"
|
OnClick="ToggleDrawer" Class="mud-hidden-md-up" />
|
||||||
Color="Color.Inherit"
|
<MudSpacer />
|
||||||
Edge="Edge.Start"
|
|
||||||
OnClick="ToggleDrawer"
|
|
||||||
Class="mud-hidden-md-up"/>
|
|
||||||
<MudSpacer/>
|
|
||||||
<MudText Typo="Typo.h6">@AppConfig.AppName</MudText>
|
<MudText Typo="Typo.h6">@AppConfig.AppName</MudText>
|
||||||
</MudAppBar>
|
</MudAppBar>
|
||||||
|
|
||||||
<MudDrawer @bind-Open="_drawerOpen"
|
<MudDrawer @bind-Open="_drawerOpen" Anchor="Anchor.Start" Variant="@_drawerVariant" Elevation="1"
|
||||||
Anchor="Anchor.Start"
|
ClipMode="DrawerClipMode.Always" Class="mud-width-250">
|
||||||
Variant="@_drawerVariant"
|
|
||||||
Elevation="1"
|
|
||||||
ClipMode="DrawerClipMode.Always"
|
|
||||||
Class="mud-width-250">
|
|
||||||
<div class="nav-logo" style="text-align: center; padding: 20px;">
|
<div class="nav-logo" style="text-align: center; padding: 20px;">
|
||||||
<a href="https://www.diunabi.com" target="_blank">
|
<a href="https://www.diunabi.com" target="_blank">
|
||||||
<img src="_content/DiunaBI.UI.Shared/images/logo.png" alt="DiunaBI" style="max-width: 180px; height: auto;" />
|
<img src="_content/DiunaBI.UI.Shared/images/logo.png" alt="DiunaBI"
|
||||||
|
style="max-width: 180px; height: auto;" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<MudNavMenu>
|
<MudNavMenu>
|
||||||
@@ -42,6 +35,10 @@
|
|||||||
<MudNavLink Href="/datainbox" Icon="@Icons.Material.Filled.Inbox">Data Inbox</MudNavLink>
|
<MudNavLink Href="/datainbox" Icon="@Icons.Material.Filled.Inbox">Data Inbox</MudNavLink>
|
||||||
<MudNavLink Href="/jobs" Icon="@Icons.Material.Filled.WorkHistory">Jobs</MudNavLink>
|
<MudNavLink Href="/jobs" Icon="@Icons.Material.Filled.WorkHistory">Jobs</MudNavLink>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
<div class="nav-logo" style="text-align: center; padding: 20px;">
|
||||||
|
<img src="_content/DiunaBI.UI.Shared/images/clients/@AppConfig.ClientLogo" alt="DiunaBI"
|
||||||
|
style="max-width: 180px; height: auto;" />
|
||||||
|
</div>
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
|
|
||||||
<MudMainContent>
|
<MudMainContent>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@page "/datainbox"
|
@page "/datainbox"
|
||||||
@using MudBlazor.Internal
|
@using MudBlazor.Internal
|
||||||
@using DiunaBI.Application.DTOModels
|
@using DiunaBI.Application.DTOModels
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<PageTitle>Data Inbox</PageTitle>
|
<PageTitle>Data Inbox</PageTitle>
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ using Microsoft.JSInterop;
|
|||||||
|
|
||||||
namespace DiunaBI.UI.Shared.Pages.DataInbox;
|
namespace DiunaBI.UI.Shared.Pages.DataInbox;
|
||||||
|
|
||||||
public partial class Index : ComponentBase
|
public partial class Index : ComponentBase, IDisposable
|
||||||
{
|
{
|
||||||
[Inject] private DataInboxService DataInboxService { get; set; } = default!;
|
[Inject] private DataInboxService DataInboxService { get; set; } = default!;
|
||||||
|
[Inject] private EntityChangeHubService HubService { get; set; } = default!;
|
||||||
[Inject] private ISnackbar Snackbar { get; set; } = default!;
|
[Inject] private ISnackbar Snackbar { get; set; } = default!;
|
||||||
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||||
[Inject] private DataInboxFilterStateService FilterStateService { get; set; } = default!;
|
[Inject] private DataInboxFilterStateService FilterStateService { get; set; } = default!;
|
||||||
@@ -27,6 +28,22 @@ public partial class Index : ComponentBase
|
|||||||
await DateTimeHelper.InitializeAsync();
|
await DateTimeHelper.InitializeAsync();
|
||||||
filterRequest = FilterStateService.FilterRequest;
|
filterRequest = FilterStateService.FilterRequest;
|
||||||
await LoadDataInbox();
|
await LoadDataInbox();
|
||||||
|
|
||||||
|
// Subscribe to SignalR entity changes
|
||||||
|
HubService.EntityChanged += OnEntityChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnEntityChanged(string module, string id, string operation)
|
||||||
|
{
|
||||||
|
// Only react if it's a DataInbox change
|
||||||
|
if (module.Equals("DataInbox", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await LoadDataInbox();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadDataInbox()
|
private async Task LoadDataInbox()
|
||||||
@@ -77,4 +94,9 @@ public partial class Index : ComponentBase
|
|||||||
var url = NavigationManager.ToAbsoluteUri($"/datainbox/{dataInboxItem.Id}").ToString();
|
var url = NavigationManager.ToAbsoluteUri($"/datainbox/{dataInboxItem.Id}").ToString();
|
||||||
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
HubService.EntityChanged -= OnEntityChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,11 +69,6 @@
|
|||||||
</MudMenuItem>
|
</MudMenuItem>
|
||||||
</MudMenu>
|
</MudMenu>
|
||||||
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
|
||||||
OnClick="LoadJobs"
|
|
||||||
Color="Color.Primary"
|
|
||||||
Size="Size.Medium"
|
|
||||||
Title="Refresh"/>
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Clear"
|
<MudIconButton Icon="@Icons.Material.Filled.Clear"
|
||||||
OnClick="ClearFilters"
|
OnClick="ClearFilters"
|
||||||
Color="Color.Default"
|
Color="Color.Default"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@using DiunaBI.UI.Shared.Services
|
@using DiunaBI.UI.Shared.Services
|
||||||
@using DiunaBI.Application.DTOModels
|
@using DiunaBI.Application.DTOModels
|
||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using System.Reflection;
|
|||||||
|
|
||||||
namespace DiunaBI.UI.Shared.Pages.Layers;
|
namespace DiunaBI.UI.Shared.Pages.Layers;
|
||||||
|
|
||||||
public partial class Details : ComponentBase
|
public partial class Details : ComponentBase, IDisposable
|
||||||
{
|
{
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@@ -20,6 +20,9 @@ public partial class Details : ComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
private JobService JobService { get; set; } = null!;
|
private JobService JobService { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private EntityChangeHubService HubService { get; set; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private NavigationManager NavigationManager { get; set; } = null!;
|
private NavigationManager NavigationManager { get; set; } = null!;
|
||||||
|
|
||||||
@@ -57,6 +60,39 @@ public partial class Details : ComponentBase
|
|||||||
{
|
{
|
||||||
await DateTimeHelper.InitializeAsync();
|
await DateTimeHelper.InitializeAsync();
|
||||||
await LoadLayer();
|
await LoadLayer();
|
||||||
|
|
||||||
|
// Subscribe to SignalR entity changes
|
||||||
|
HubService.EntityChanged += OnEntityChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnEntityChanged(string module, string id, string operation)
|
||||||
|
{
|
||||||
|
// React to Layers or Records changes for this layer
|
||||||
|
if (module.Equals("Layers", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
module.Equals("Records", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Check if it's this layer or its records that changed
|
||||||
|
if (Guid.TryParse(id, out var changedId))
|
||||||
|
{
|
||||||
|
if (module.Equals("Layers", StringComparison.OrdinalIgnoreCase) && changedId == Id)
|
||||||
|
{
|
||||||
|
await InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await LoadLayer();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (module.Equals("Records", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// For records, we reload to get the latest data
|
||||||
|
await InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await LoadLayer();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
@@ -495,4 +531,9 @@ public partial class Details : ComponentBase
|
|||||||
isRunningJob = false;
|
isRunningJob = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
HubService.EntityChanged -= OnEntityChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@page "/layers"
|
@page "/layers"
|
||||||
@using MudBlazor.Internal
|
@using MudBlazor.Internal
|
||||||
@using DiunaBI.Application.DTOModels
|
@using DiunaBI.Application.DTOModels
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<PageTitle>Layers</PageTitle>
|
<PageTitle>Layers</PageTitle>
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ using Microsoft.JSInterop;
|
|||||||
|
|
||||||
namespace DiunaBI.UI.Shared.Pages.Layers;
|
namespace DiunaBI.UI.Shared.Pages.Layers;
|
||||||
|
|
||||||
public partial class Index : ComponentBase
|
public partial class Index : ComponentBase, IDisposable
|
||||||
{
|
{
|
||||||
[Inject] private LayerService LayerService { get; set; } = default!;
|
[Inject] private LayerService LayerService { get; set; } = default!;
|
||||||
|
[Inject] private EntityChangeHubService HubService { get; set; } = default!;
|
||||||
[Inject] private ISnackbar Snackbar { get; set; } = default!;
|
[Inject] private ISnackbar Snackbar { get; set; } = default!;
|
||||||
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||||
[Inject] private LayerFilterStateService FilterStateService { get; set; } = default!;
|
[Inject] private LayerFilterStateService FilterStateService { get; set; } = default!;
|
||||||
@@ -25,6 +26,22 @@ public partial class Index : ComponentBase
|
|||||||
{
|
{
|
||||||
filterRequest = FilterStateService.FilterRequest;
|
filterRequest = FilterStateService.FilterRequest;
|
||||||
await LoadLayers();
|
await LoadLayers();
|
||||||
|
|
||||||
|
// Subscribe to SignalR entity changes
|
||||||
|
HubService.EntityChanged += OnEntityChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnEntityChanged(string module, string id, string operation)
|
||||||
|
{
|
||||||
|
// Only react if it's a Layers change
|
||||||
|
if (module.Equals("Layers", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await LoadLayers();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadLayers()
|
private async Task LoadLayers()
|
||||||
@@ -89,4 +106,9 @@ public partial class Index : ComponentBase
|
|||||||
var url = NavigationManager.ToAbsoluteUri($"/layers/{layer.Id}").ToString();
|
var url = NavigationManager.ToAbsoluteUri($"/layers/{layer.Id}").ToString();
|
||||||
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
HubService.EntityChanged -= OnEntityChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ namespace DiunaBI.UI.Shared.Services;
|
|||||||
public class AppConfig
|
public class AppConfig
|
||||||
{
|
{
|
||||||
public string AppName { get; set; } = "DiunaBI";
|
public string AppName { get; set; } = "DiunaBI";
|
||||||
|
public string ClientLogo {get; set;} = "pedrollopl.png";
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
DiunaBI.UI.Shared/wwwroot/images/clients/morska.png
Normal file
BIN
DiunaBI.UI.Shared/wwwroot/images/clients/morska.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
DiunaBI.UI.Shared/wwwroot/images/clients/pedrollopl.png
Normal file
BIN
DiunaBI.UI.Shared/wwwroot/images/clients/pedrollopl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -17,9 +17,6 @@ builder.Services.AddSharedServices(apiBaseUrl);
|
|||||||
|
|
||||||
// Configure App settings
|
// Configure App settings
|
||||||
var appConfig = builder.Configuration.GetSection("App").Get<AppConfig>() ?? new AppConfig();
|
var appConfig = builder.Configuration.GetSection("App").Get<AppConfig>() ?? new AppConfig();
|
||||||
Console.WriteLine($"[DEBUG] AppConfig.AppName from config: {appConfig.AppName}");
|
|
||||||
Console.WriteLine($"[DEBUG] App:AppName from Configuration: {builder.Configuration["App:AppName"]}");
|
|
||||||
Console.WriteLine($"[DEBUG] App__AppName env var: {Environment.GetEnvironmentVariable("App__AppName")}");
|
|
||||||
builder.Services.AddSingleton(appConfig);
|
builder.Services.AddSingleton(appConfig);
|
||||||
|
|
||||||
builder.Services.AddScoped<IGoogleAuthService, WebGoogleAuthService>();
|
builder.Services.AddScoped<IGoogleAuthService, WebGoogleAuthService>();
|
||||||
|
|||||||
Reference in New Issue
Block a user