diff --git a/.claude/project-context.md b/.claude/project-context.md index 6197bb3..8fa1e45 100644 --- a/.claude/project-context.md +++ b/.claude/project-context.md @@ -5,6 +5,30 @@ ## RECENT CHANGES (This Session) +**Job Scheduler Race Condition Fix (Dec 8, 2025):** +- ✅ **In-Memory Deduplication** - Added `HashSet` 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 +- ✅ **Two-Level Protection** - In-memory check (HashSet) runs before database check for O(1) performance +- ✅ **Applied to Both Methods** - Fixed both ScheduleImportJobsAsync and ScheduleProcessJobsAsync +- ✅ **Better Logging** - Added debug log message "Job already scheduled in this batch" for transparency +- Root cause: When multiple layers had same ID in query results or import plugins created new layers during scheduling loop, database check couldn't detect duplicates added in same batch before SaveChangesAsync() +- Solution: Track scheduled LayerIds in HashSet during loop iteration to prevent within-batch duplicates +- Files modified: [JobSchedulerService.cs](DiunaBI.Infrastructure/Services/JobSchedulerService.cs) +- Status: Race condition resolved, duplicate job creation prevented + +--- + +**Blazor Server Reconnection UI Customization (Dec 8, 2025):** +- ✅ **Custom Reconnection Modal** - Replaced default Blazor "Rejoin failed..." dialog with custom-styled modal +- ✅ **Theme-Matched Styling** - Changed loader and button colors from blue to app's primary red (#e7163d) matching navbar +- ✅ **Timer with Elapsed Seconds** - Added real-time timer showing elapsed reconnection time (0s, 1s, 2s...) +- ✅ **CSS Classes Integration** - Used Blazor's built-in `.components-reconnect-show/failed/rejected` classes for state management +- ✅ **MutationObserver Timer** - JavaScript watches for CSS class changes to start/stop elapsed time counter +- ✅ **Professional Design** - Modal backdrop blur, spinner animation, red reload button with hover effects +- Files modified: [App.razor](DiunaBI.UI.Web/Components/App.razor), [app.css](DiunaBI.UI.Web/wwwroot/app.css) +- Files created: [reconnect.js](DiunaBI.UI.Web/wwwroot/js/reconnect.js) +- Status: Blazor reconnection UI now matches app theme with timer indicator + **Jobs List Sorting and Multi-Select Filtering (Dec 8, 2025):** - ✅ **Fixed Job Sorting** - Changed from single CreatedAt DESC to Priority ASC → JobType → CreatedAt DESC - ✅ **Multi-Select Status Filter** - Replaced single status dropdown with multi-select supporting multiple JobStatus values diff --git a/DevTools/sql-scripts/PedrolloPL/PedrolloImport.sql b/DevTools/sql-scripts/PedrolloPL/PedrolloImport.sql index 4aeea25..5e5b662 100644 --- a/DevTools/sql-scripts/PedrolloPL/PedrolloImport.sql +++ b/DevTools/sql-scripts/PedrolloPL/PedrolloImport.sql @@ -1,7 +1,9 @@ DECLARE @JustForDebug TINYINT = 0; +-- FIX DATAINBOX! + -- SETUP VARIABLES -DECLARE @Year INT = 2024; +DECLARE @Year INT = 2025; DECLARE @Type NVARCHAR(5) = 'B3'; DECLARE @StartDate NVARCHAR(10) = '2025.01.02'; DECLARE @EndDate NVARCHAR(10) = '2026.12.31' @@ -22,7 +24,7 @@ SET @Plugin = DECLARE @DataInboxName NVARCHAR(100); SET @DataInboxName = CASE @Type - WHEN 'B3' THEN 'B3_2024' + WHEN 'B3' THEN 'P2_2025' ELSE NULL -- If @Type doesn't match, set it to NULL END; diff --git a/DevTools/sql-scripts/PedrolloPL/PedrolloProcessP2 b/DevTools/sql-scripts/PedrolloPL/PedrolloProcessP2.sql similarity index 76% rename from DevTools/sql-scripts/PedrolloPL/PedrolloProcessP2 rename to DevTools/sql-scripts/PedrolloPL/PedrolloProcessP2.sql index c786559..dec7f85 100644 --- a/DevTools/sql-scripts/PedrolloPL/PedrolloProcessP2 +++ b/DevTools/sql-scripts/PedrolloPL/PedrolloProcessP2.sql @@ -1,7 +1,7 @@ DECLARE @JustForDebug TINYINT = 0; -- SETUP VARIABLES -DECLARE @Year INT = 2024; +DECLARE @Year INT = 2025; DECLARE @Number INT = (SELECT COUNT(id) + 1 FROM [DiunaBI-PedrolloPL].[dbo].[Layers]); DECLARE @CurrentTimestamp NVARCHAR(14) = FORMAT(GETDATE(), 'yyyyMMddHHmm'); @@ -56,3 +56,16 @@ VALUES ((SELECT NEWID()), 'Plugin', 'PedrolloPL.Process.P2', GETDATE(), GETDATE( INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records] ([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId]) VALUES ((SELECT NEWID()), 'Priority', '110', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId); +-- +INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records] +([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId]) +VALUES ((SELECT NEWID()), 'GoogleSheetId', '1jI-3QrlBADm5slEl2Balf29cKmHwkYi4pboaHY-gRqc', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId); + +INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records] +([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId]) +VALUES ((SELECT NEWID()), 'GoogleSheetTab', 'P2_Export_DiunaBI', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId); + +INSERT INTO [DiunaBI-PedrolloPL].[dbo].[Records] +([Id], [Code], [Desc1], [CreatedAt], [ModifiedAt], [CreatedById], [ModifiedById], [IsDeleted], [LayerId]) +VALUES ((SELECT NEWID()), 'GoogleSheetRange', 'C32:O48', GETDATE(), GETDATE(), '117be4f0-b5d1-41a1-a962-39dc30cce368', '117be4f0-b5d1-41a1-a962-39dc30cce368', 0, @LayerId); + diff --git a/DiunaBI.Infrastructure/Services/JobSchedulerService.cs b/DiunaBI.Infrastructure/Services/JobSchedulerService.cs index 06a77ad..d7efec5 100644 --- a/DiunaBI.Infrastructure/Services/JobSchedulerService.cs +++ b/DiunaBI.Infrastructure/Services/JobSchedulerService.cs @@ -40,6 +40,7 @@ public class JobSchedulerService _logger.LogInformation("JobScheduler: Found {Count} import workers to schedule", importWorkers.Count); var jobsCreated = 0; + var scheduledLayerIds = new HashSet(); // Track LayerIds scheduled in this batch foreach (var worker in importWorkers) { @@ -61,7 +62,15 @@ public class JobSchedulerService var maxRetriesStr = worker.Records?.FirstOrDefault(r => r.Code == "MaxRetries")?.Desc1; var maxRetries = int.TryParse(maxRetriesStr, out var mr) ? mr : 3; - // Check if there's already a pending/running job for this layer + // Check in-memory: already scheduled in this batch? + if (scheduledLayerIds.Contains(worker.Id)) + { + _logger.LogDebug("JobScheduler: Job already scheduled in this batch for {LayerName} ({LayerId})", + worker.Name, worker.Id); + continue; + } + + // Check if there's already a pending/running job for this layer in database var existingJob = await _db.QueueJobs .Where(j => j.LayerId == worker.Id && (j.Status == JobStatus.Pending || j.Status == JobStatus.Running)) @@ -91,6 +100,7 @@ public class JobSchedulerService }; _db.QueueJobs.Add(job); + scheduledLayerIds.Add(worker.Id); // Track that we've scheduled this layer jobsCreated++; _logger.LogInformation("JobScheduler: Created import job for {LayerName} ({LayerId}) with priority {Priority}", @@ -129,6 +139,7 @@ public class JobSchedulerService _logger.LogInformation("JobScheduler: Found {Count} process workers to schedule", processWorkers.Count); var jobsCreated = 0; + var scheduledLayerIds = new HashSet(); // Track LayerIds scheduled in this batch foreach (var worker in processWorkers) { @@ -150,7 +161,15 @@ public class JobSchedulerService var maxRetriesStr = worker.Records?.FirstOrDefault(r => r.Code == "MaxRetries")?.Desc1; var maxRetries = int.TryParse(maxRetriesStr, out var mr) ? mr : 3; - // Check if there's already a pending/running job for this layer + // Check in-memory: already scheduled in this batch? + if (scheduledLayerIds.Contains(worker.Id)) + { + _logger.LogDebug("JobScheduler: Job already scheduled in this batch for {LayerName} ({LayerId})", + worker.Name, worker.Id); + continue; + } + + // Check if there's already a pending/running job for this layer in database var existingJob = await _db.QueueJobs .Where(j => j.LayerId == worker.Id && (j.Status == JobStatus.Pending || j.Status == JobStatus.Running)) @@ -180,6 +199,7 @@ public class JobSchedulerService }; _db.QueueJobs.Add(job); + scheduledLayerIds.Add(worker.Id); // Track that we've scheduled this layer jobsCreated++; _logger.LogInformation("JobScheduler: Created process job for {LayerName} ({LayerId}) with priority {Priority}",