Compare commits

...

5 Commits

Author SHA1 Message Date
24f5f91704 update readme
All checks were successful
Build Docker Images / test (map[name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m28s
Build Docker Images / test (map[name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m27s
Build Docker Images / build-and-push (map[image_suffix:morska name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m41s
Build Docker Images / build-and-push (map[image_suffix:pedrollopl name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m41s
2025-12-08 22:07:16 +01:00
00c9584d03 Schedule Jobs from UI 2025-12-08 22:02:57 +01:00
c94a3b41c9 Duplicate models fields fix 2025-12-08 21:54:48 +01:00
e25cdc4441 UI timezone 2025-12-08 21:42:10 +01:00
1f95d57717 JobList filter fix 2025-12-08 21:28:24 +01:00
22 changed files with 994 additions and 63 deletions

View File

@@ -1,10 +1,56 @@
# DiunaBI Project Context
> This file is auto-generated for Claude Code to quickly understand the project structure.
> Last updated: 2025-12-06
> Last updated: 2025-12-08
## RECENT CHANGES (This Session)
**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
-**Auto-Refresh on Filter Change** - Filters now automatically trigger data reload without requiring manual button click
-**API Updates** - JobsController GetAll endpoint accepts `List<JobStatus>? statuses` instead of single status
-**JobService Updates** - Sends status values as integers in query string for multi-select support
- Files modified: [JobsController.cs](DiunaBI.API/Controllers/JobsController.cs), [JobService.cs](DiunaBI.UI.Shared/Services/JobService.cs), [Index.razor](DiunaBI.UI.Shared/Pages/Jobs/Index.razor), [Index.razor.cs](DiunaBI.UI.Shared/Pages/Jobs/Index.razor.cs)
- Status: Jobs list now sortable by priority/type/date with working multi-select filters
**User Timezone Support (Dec 8, 2025):**
-**DateTimeHelper Service** - Created JS Interop service to detect user's browser timezone
-**UTC to Local Conversion** - All date displays now show user's local timezone instead of UTC
-**Database Consistency** - Database continues to store UTC (correct), conversion only for display
-**Updated Pages** - Applied timezone conversion to all date fields in:
- Jobs Index and Details pages
- Layers Details page (CreatedAt, ModifiedAt, record history)
- DataInbox Index page
-**Service Registration** - Registered DateTimeHelper as scoped service in DI container
- Files created: [DateTimeHelper.cs](DiunaBI.UI.Shared/Services/DateTimeHelper.cs)
- Files modified: [ServiceCollectionExtensions.cs](DiunaBI.UI.Shared/Extensions/ServiceCollectionExtensions.cs), [Jobs/Index.razor.cs](DiunaBI.UI.Shared/Pages/Jobs/Index.razor.cs), [Jobs/Details.razor](DiunaBI.UI.Shared/Pages/Jobs/Details.razor), [Layers/Details.razor](DiunaBI.UI.Shared/Pages/Layers/Details.razor), [Layers/Details.razor.cs](DiunaBI.UI.Shared/Pages/Layers/Details.razor.cs), [DataInbox/Index.razor.cs](DiunaBI.UI.Shared/Pages/DataInbox/Index.razor.cs)
- Status: All dates display in user's local timezone with format "yyyy-MM-dd HH:mm:ss"
**QueueJob Model Cleanup and AutoImport User (Dec 8, 2025):**
-**Removed Duplicate Fields** - Removed CreatedAtUtc and ModifiedAtUtc from QueueJob (were duplicates of CreatedAt/ModifiedAt)
-**Added ModifiedAt Field** - Was missing, now tracks job modification timestamp
-**AutoImport User ID** - Created User.AutoImportUserId constant: `f392209e-123e-4651-a5a4-0b1d6cf9ff9d`
-**System Operations** - All system-created/modified jobs now use AutoImportUserId for CreatedById and ModifiedById
-**Database Migration** - Created migration: RemoveQueueJobDuplicateUTCFields
- Files modified: [QueueJob.cs](DiunaBI.Domain/Entities/QueueJob.cs), [User.cs](DiunaBI.Domain/Entities/User.cs), [JobWorkerService.cs](DiunaBI.Infrastructure/Services/JobWorkerService.cs), [JobSchedulerService.cs](DiunaBI.Infrastructure/Services/JobSchedulerService.cs), [AppDbContext.cs](DiunaBI.Infrastructure/Data/AppDbContext.cs), [JobsController.cs](DiunaBI.API/Controllers/JobsController.cs)
- Files created: [20251208205202_RemoveQueueJobDuplicateUTCFields.cs](DiunaBI.Infrastructure/Migrations/20251208205202_RemoveQueueJobDuplicateUTCFields.cs)
- Status: QueueJob model cleaned up, all automated operations tracked with AutoImport user ID
**Job Scheduling UI with JWT Authorization (Dec 8, 2025):**
-**New JWT Endpoints** - Created UI-specific endpoints at `/jobs/ui/schedule/*` with JWT authorization (parallel to API key endpoints)
-**Three Scheduling Options** - MudMenu dropdown in Jobs Index with:
- Run All Jobs - schedules all import and process jobs
- Run All Imports - schedules import jobs only
- Run All Processes - schedules process jobs only
-**JobService Methods** - Added three scheduling methods returning (success, jobsCreated, message) tuples
-**Auto-Refresh** - Jobs list automatically reloads after scheduling with success/failure notifications
-**Dual Authorization** - Existing `/jobs/schedule/{apiKey}` endpoints for automation, new `/jobs/ui/schedule` endpoints for UI users
- Files modified: [JobsController.cs](DiunaBI.API/Controllers/JobsController.cs), [JobService.cs](DiunaBI.UI.Shared/Services/JobService.cs), [Index.razor](DiunaBI.UI.Shared/Pages/Jobs/Index.razor), [Index.razor.cs](DiunaBI.UI.Shared/Pages/Jobs/Index.razor.cs)
- Status: UI users can now schedule jobs directly from Jobs page using JWT authentication
---
**API Key Authorization Fix for Cron Jobs (Dec 6, 2025):**
-**Fixed 401 Unauthorized on API Key Endpoints** - Cron jobs calling `/jobs/schedule` endpoints were getting rejected despite valid API keys
-**Added [AllowAnonymous] Attribute** - Bypasses controller-level `[Authorize]` to allow `[ApiKeyAuth]` filter to handle authorization

7
.gitignore vendored
View File

@@ -563,3 +563,10 @@ coverage/
##
tmp/
temp/
##
## LocalDB Development Files
##
DevTools/LocalDB/backups/*.bak
DevTools/LocalDB/backups/*.bacpac
DevTools/LocalDB/data/

View File

@@ -36,7 +36,7 @@ public class JobsController : Controller
public async Task<IActionResult> GetAll(
[FromQuery] int start = 0,
[FromQuery] int limit = 50,
[FromQuery] JobStatus? status = null,
[FromQuery] List<JobStatus>? statuses = null,
[FromQuery] JobType? jobType = null,
[FromQuery] Guid? layerId = null)
{
@@ -54,9 +54,9 @@ public class JobsController : Controller
var query = _db.QueueJobs.AsQueryable();
if (status.HasValue)
if (statuses != null && statuses.Count > 0)
{
query = query.Where(j => j.Status == status.Value);
query = query.Where(j => statuses.Contains(j.Status));
}
if (jobType.HasValue)
@@ -71,8 +71,11 @@ public class JobsController : Controller
var totalCount = await query.CountAsync();
// Sort by: Priority ASC (0=highest), JobType, then CreatedAt DESC
var items = await query
.OrderByDescending(j => j.CreatedAt)
.OrderBy(j => j.Priority)
.ThenBy(j => j.JobType)
.ThenByDescending(j => j.CreatedAt)
.Skip(start)
.Take(limit)
.AsNoTracking()
@@ -201,6 +204,79 @@ public class JobsController : Controller
}
}
// UI-friendly endpoints (JWT auth)
[HttpPost]
[Route("ui/schedule")]
public async Task<IActionResult> ScheduleJobsUI([FromQuery] string? nameFilter = null)
{
try
{
var jobsCreated = await _jobScheduler.ScheduleAllJobsAsync(nameFilter);
_logger.LogInformation("ScheduleJobsUI: Created {Count} jobs by user {UserId}", jobsCreated, User.Identity?.Name);
return Ok(new
{
success = true,
jobsCreated,
message = $"Successfully scheduled {jobsCreated} jobs"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "ScheduleJobsUI: Error scheduling jobs");
return BadRequest("An error occurred processing your request");
}
}
[HttpPost]
[Route("ui/schedule/imports")]
public async Task<IActionResult> ScheduleImportJobsUI([FromQuery] string? nameFilter = null)
{
try
{
var jobsCreated = await _jobScheduler.ScheduleImportJobsAsync(nameFilter);
_logger.LogInformation("ScheduleImportJobsUI: Created {Count} import jobs by user {UserId}", jobsCreated, User.Identity?.Name);
return Ok(new
{
success = true,
jobsCreated,
message = $"Successfully scheduled {jobsCreated} import jobs"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "ScheduleImportJobsUI: Error scheduling import jobs");
return BadRequest("An error occurred processing your request");
}
}
[HttpPost]
[Route("ui/schedule/processes")]
public async Task<IActionResult> ScheduleProcessJobsUI()
{
try
{
var jobsCreated = await _jobScheduler.ScheduleProcessJobsAsync();
_logger.LogInformation("ScheduleProcessJobsUI: Created {Count} process jobs by user {UserId}", jobsCreated, User.Identity?.Name);
return Ok(new
{
success = true,
jobsCreated,
message = $"Successfully scheduled {jobsCreated} process jobs"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "ScheduleProcessJobsUI: Error scheduling process jobs");
return BadRequest("An error occurred processing your request");
}
}
[HttpPost]
[Route("{id:guid}/retry")]
public async Task<IActionResult> RetryJob(Guid id)
@@ -224,7 +300,8 @@ public class JobsController : Controller
job.Status = JobStatus.Pending;
job.RetryCount = 0;
job.LastError = null;
job.ModifiedAtUtc = DateTime.UtcNow;
job.ModifiedAt = DateTime.UtcNow;
job.ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId;
await _db.SaveChangesAsync();
@@ -271,7 +348,8 @@ public class JobsController : Controller
job.Status = JobStatus.Failed;
job.LastError = "Cancelled by user";
job.ModifiedAtUtc = DateTime.UtcNow;
job.ModifiedAt = DateTime.UtcNow;
job.ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId;
await _db.SaveChangesAsync();
@@ -402,10 +480,9 @@ public class JobsController : Controller
MaxRetries = maxRetries,
Status = JobStatus.Pending,
CreatedAt = DateTime.UtcNow,
CreatedAtUtc = DateTime.UtcNow,
ModifiedAtUtc = DateTime.UtcNow,
CreatedById = Guid.Empty,
ModifiedById = Guid.Empty
ModifiedAt = DateTime.UtcNow,
CreatedById = DiunaBI.Domain.Entities.User.AutoImportUserId,
ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId
};
_db.QueueJobs.Add(job);

View File

@@ -12,6 +12,7 @@ public class QueueJob
public JobType JobType { get; set; }
public int Priority { get; set; } = 0; // 0 = highest priority
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime ModifiedAt { get; set; } = DateTime.UtcNow;
public int RetryCount { get; set; } = 0;
public int MaxRetries { get; set; } = 5;
public JobStatus Status { get; set; } = JobStatus.Pending;
@@ -19,9 +20,7 @@ public class QueueJob
public DateTime? LastAttemptAt { get; set; }
public DateTime? CompletedAt { get; set; }
public Guid CreatedById { get; set; }
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public Guid ModifiedById { get; set; }
public DateTime ModifiedAtUtc { get; set; } = DateTime.UtcNow;
}
public enum JobType

View File

@@ -5,6 +5,11 @@ namespace DiunaBI.Domain.Entities;
public class User
{
/// <summary>
/// System user ID for automated operations (imports, scheduled jobs, etc.)
/// </summary>
public static readonly Guid AutoImportUserId = Guid.Parse("f392209e-123e-4651-a5a4-0b1d6cf9ff9d");
#region Properties
public Guid Id { get; init; }
public string? Email { get; init; }

View File

@@ -136,9 +136,8 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
modelBuilder.Entity<QueueJob>().Property(x => x.LastAttemptAt);
modelBuilder.Entity<QueueJob>().Property(x => x.CompletedAt);
modelBuilder.Entity<QueueJob>().Property(x => x.CreatedById).IsRequired();
modelBuilder.Entity<QueueJob>().Property(x => x.CreatedAtUtc).IsRequired();
modelBuilder.Entity<QueueJob>().Property(x => x.ModifiedById).IsRequired();
modelBuilder.Entity<QueueJob>().Property(x => x.ModifiedAtUtc).IsRequired();
modelBuilder.Entity<QueueJob>().Property(x => x.ModifiedAt).IsRequired();
// Configure automatic timestamps for entities with CreatedAt/ModifiedAt
ConfigureTimestamps(modelBuilder);

View File

@@ -22,8 +22,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Google.Apis.Sheets.v4" Version="1.68.0.3525" />
<PackageReference Include="Google.Apis.Drive.v3" Version="1.68.0.3490" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,489 @@
// <auto-generated />
using System;
using DiunaBI.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DiunaBI.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251208205202_RemoveQueueJobDuplicateUTCFields")]
partial class RemoveQueueJobDuplicateUTCFields
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("DiunaBI.Domain.Entities.DataInbox", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("DataInbox");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.Layer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<Guid>("CreatedById")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsCancelled")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTime>("ModifiedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<Guid>("ModifiedById")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("Number")
.HasColumnType("int");
b.Property<Guid?>("ParentId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("ModifiedById");
b.ToTable("Layers");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.ProcessSource", b =>
{
b.Property<Guid>("LayerId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("SourceId")
.HasColumnType("uniqueidentifier");
b.HasKey("LayerId", "SourceId");
b.HasIndex("SourceId");
b.ToTable("ProcessSources");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.QueueJob", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<Guid>("CreatedById")
.HasColumnType("uniqueidentifier");
b.Property<int>("JobType")
.HasColumnType("int");
b.Property<DateTime?>("LastAttemptAt")
.HasColumnType("datetime2");
b.Property<string>("LastError")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<Guid>("LayerId")
.HasColumnType("uniqueidentifier");
b.Property<string>("LayerName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<int>("MaxRetries")
.HasColumnType("int");
b.Property<DateTime>("ModifiedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<Guid>("ModifiedById")
.HasColumnType("uniqueidentifier");
b.Property<string>("PluginName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<int>("RetryCount")
.HasColumnType("int");
b.Property<int>("Status")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("QueueJobs");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.Record", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<Guid>("CreatedById")
.HasColumnType("uniqueidentifier");
b.Property<string>("Desc1")
.HasMaxLength(10000)
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<Guid>("LayerId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("ModifiedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<Guid>("ModifiedById")
.HasColumnType("uniqueidentifier");
b.Property<double?>("Value1")
.HasColumnType("float");
b.Property<double?>("Value10")
.HasColumnType("float");
b.Property<double?>("Value11")
.HasColumnType("float");
b.Property<double?>("Value12")
.HasColumnType("float");
b.Property<double?>("Value13")
.HasColumnType("float");
b.Property<double?>("Value14")
.HasColumnType("float");
b.Property<double?>("Value15")
.HasColumnType("float");
b.Property<double?>("Value16")
.HasColumnType("float");
b.Property<double?>("Value17")
.HasColumnType("float");
b.Property<double?>("Value18")
.HasColumnType("float");
b.Property<double?>("Value19")
.HasColumnType("float");
b.Property<double?>("Value2")
.HasColumnType("float");
b.Property<double?>("Value20")
.HasColumnType("float");
b.Property<double?>("Value21")
.HasColumnType("float");
b.Property<double?>("Value22")
.HasColumnType("float");
b.Property<double?>("Value23")
.HasColumnType("float");
b.Property<double?>("Value24")
.HasColumnType("float");
b.Property<double?>("Value25")
.HasColumnType("float");
b.Property<double?>("Value26")
.HasColumnType("float");
b.Property<double?>("Value27")
.HasColumnType("float");
b.Property<double?>("Value28")
.HasColumnType("float");
b.Property<double?>("Value29")
.HasColumnType("float");
b.Property<double?>("Value3")
.HasColumnType("float");
b.Property<double?>("Value30")
.HasColumnType("float");
b.Property<double?>("Value31")
.HasColumnType("float");
b.Property<double?>("Value32")
.HasColumnType("float");
b.Property<double?>("Value4")
.HasColumnType("float");
b.Property<double?>("Value5")
.HasColumnType("float");
b.Property<double?>("Value6")
.HasColumnType("float");
b.Property<double?>("Value7")
.HasColumnType("float");
b.Property<double?>("Value8")
.HasColumnType("float");
b.Property<double?>("Value9")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("LayerId");
b.HasIndex("ModifiedById");
b.ToTable("Records");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.RecordHistory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("ChangeType")
.HasColumnType("int");
b.Property<DateTime>("ChangedAt")
.HasColumnType("datetime2");
b.Property<Guid>("ChangedById")
.HasColumnType("uniqueidentifier");
b.Property<string>("ChangedFields")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("ChangesSummary")
.HasMaxLength(4000)
.HasColumnType("nvarchar(4000)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Desc1")
.HasMaxLength(10000)
.HasColumnType("nvarchar(max)");
b.Property<Guid>("LayerId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RecordId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("ChangedById");
b.HasIndex("LayerId", "ChangedAt");
b.HasIndex("RecordId", "ChangedAt");
b.ToTable("RecordHistory");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Email")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("UserName")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.Layer", b =>
{
b.HasOne("DiunaBI.Domain.Entities.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("DiunaBI.Domain.Entities.User", "ModifiedBy")
.WithMany()
.HasForeignKey("ModifiedById")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedBy");
b.Navigation("ModifiedBy");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.ProcessSource", b =>
{
b.HasOne("DiunaBI.Domain.Entities.Layer", null)
.WithMany()
.HasForeignKey("LayerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DiunaBI.Domain.Entities.Layer", "Source")
.WithMany()
.HasForeignKey("SourceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Source");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.Record", b =>
{
b.HasOne("DiunaBI.Domain.Entities.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("DiunaBI.Domain.Entities.Layer", null)
.WithMany("Records")
.HasForeignKey("LayerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DiunaBI.Domain.Entities.User", "ModifiedBy")
.WithMany()
.HasForeignKey("ModifiedById")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CreatedBy");
b.Navigation("ModifiedBy");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.RecordHistory", b =>
{
b.HasOne("DiunaBI.Domain.Entities.User", "ChangedBy")
.WithMany()
.HasForeignKey("ChangedById")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ChangedBy");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.Layer", b =>
{
b.Navigation("Records");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DiunaBI.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class RemoveQueueJobDuplicateUTCFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedAtUtc",
table: "QueueJobs");
migrationBuilder.DropColumn(
name: "ModifiedAtUtc",
table: "QueueJobs");
migrationBuilder.AddColumn<DateTime>(
name: "ModifiedAt",
table: "QueueJobs",
type: "datetime2",
nullable: false,
defaultValueSql: "GETUTCDATE()");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ModifiedAt",
table: "QueueJobs");
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAtUtc",
table: "QueueJobs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "ModifiedAtUtc",
table: "QueueJobs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
}
}

View File

@@ -49,7 +49,7 @@ namespace DiunaBI.Infrastructure.Migrations
b.HasKey("Id");
b.ToTable("DataInbox", (string)null);
b.ToTable("DataInbox");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.Layer", b =>
@@ -104,7 +104,7 @@ namespace DiunaBI.Infrastructure.Migrations
b.HasIndex("ModifiedById");
b.ToTable("Layers", (string)null);
b.ToTable("Layers");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.ProcessSource", b =>
@@ -119,7 +119,7 @@ namespace DiunaBI.Infrastructure.Migrations
b.HasIndex("SourceId");
b.ToTable("ProcessSources", (string)null);
b.ToTable("ProcessSources");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.QueueJob", b =>
@@ -136,9 +136,6 @@ namespace DiunaBI.Infrastructure.Migrations
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid>("CreatedById")
.HasColumnType("uniqueidentifier");
@@ -163,8 +160,10 @@ namespace DiunaBI.Infrastructure.Migrations
b.Property<int>("MaxRetries")
.HasColumnType("int");
b.Property<DateTime>("ModifiedAtUtc")
.HasColumnType("datetime2");
b.Property<DateTime>("ModifiedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<Guid>("ModifiedById")
.HasColumnType("uniqueidentifier");
@@ -185,7 +184,7 @@ namespace DiunaBI.Infrastructure.Migrations
b.HasKey("Id");
b.ToTable("QueueJobs", (string)null);
b.ToTable("QueueJobs");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.Record", b =>
@@ -329,7 +328,7 @@ namespace DiunaBI.Infrastructure.Migrations
b.HasIndex("ModifiedById");
b.ToTable("Records", (string)null);
b.ToTable("Records");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.RecordHistory", b =>
@@ -378,7 +377,7 @@ namespace DiunaBI.Infrastructure.Migrations
b.HasIndex("RecordId", "ChangedAt");
b.ToTable("RecordHistory", (string)null);
b.ToTable("RecordHistory");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.User", b =>
@@ -402,7 +401,7 @@ namespace DiunaBI.Infrastructure.Migrations
b.HasKey("Id");
b.ToTable("Users", (string)null);
b.ToTable("Users");
});
modelBuilder.Entity("DiunaBI.Domain.Entities.Layer", b =>

View File

@@ -85,10 +85,9 @@ public class JobSchedulerService
MaxRetries = maxRetries,
Status = JobStatus.Pending,
CreatedAt = DateTime.UtcNow,
CreatedAtUtc = DateTime.UtcNow,
ModifiedAtUtc = DateTime.UtcNow,
CreatedById = Guid.Empty, // System user
ModifiedById = Guid.Empty
ModifiedAt = DateTime.UtcNow,
CreatedById = DiunaBI.Domain.Entities.User.AutoImportUserId,
ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId
};
_db.QueueJobs.Add(job);
@@ -175,10 +174,9 @@ public class JobSchedulerService
MaxRetries = maxRetries,
Status = JobStatus.Pending,
CreatedAt = DateTime.UtcNow,
CreatedAtUtc = DateTime.UtcNow,
ModifiedAtUtc = DateTime.UtcNow,
CreatedById = Guid.Empty,
ModifiedById = Guid.Empty
ModifiedAt = DateTime.UtcNow,
CreatedById = DiunaBI.Domain.Entities.User.AutoImportUserId,
ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId
};
_db.QueueJobs.Add(job);

View File

@@ -66,7 +66,8 @@ public class JobWorkerService : BackgroundService
// Mark job as running
job.Status = JobStatus.Running;
job.LastAttemptAt = DateTime.UtcNow;
job.ModifiedAtUtc = DateTime.UtcNow;
job.ModifiedAt = DateTime.UtcNow;
job.ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId;
await db.SaveChangesAsync(stoppingToken);
try
@@ -114,7 +115,8 @@ public class JobWorkerService : BackgroundService
job.Status = JobStatus.Completed;
job.CompletedAt = DateTime.UtcNow;
job.LastError = null;
job.ModifiedAtUtc = DateTime.UtcNow;
job.ModifiedAt = DateTime.UtcNow;
job.ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId;
_logger.LogInformation("JobWorker: Job {JobId} completed successfully", job.Id);
@@ -131,7 +133,8 @@ public class JobWorkerService : BackgroundService
// Capture full error details including inner exceptions
job.LastError = GetFullErrorMessage(ex);
job.ModifiedAtUtc = DateTime.UtcNow;
job.ModifiedAt = DateTime.UtcNow;
job.ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId;
if (job.RetryCount >= job.MaxRetries)
{
@@ -157,7 +160,8 @@ public class JobWorkerService : BackgroundService
// Increment retry count for next attempt
job.RetryCount++;
job.ModifiedAtUtc = DateTime.UtcNow;
job.ModifiedAt = DateTime.UtcNow;
job.ModifiedById = DiunaBI.Domain.Entities.User.AutoImportUserId;
}
}
finally

View File

@@ -44,6 +44,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<LayerService>();
services.AddScoped<DataInboxService>();
services.AddScoped<JobService>();
services.AddScoped<DateTimeHelper>();
// Filter state services (scoped to maintain state during user session)
services.AddScoped<LayerFilterStateService>();

View File

@@ -52,7 +52,7 @@
<RowTemplate Context="row">
<MudTd DataLabel="Name"><div @oncontextmenu="@(async (e) => await OnRowRightClick(e, row))" @oncontextmenu:preventDefault="true">@row.Name</div></MudTd>
<MudTd DataLabel="Source"><div @oncontextmenu="@(async (e) => await OnRowRightClick(e, row))" @oncontextmenu:preventDefault="true">@row.Source</div></MudTd>
<MudTd DataLabel="Created At"><div @oncontextmenu="@(async (e) => await OnRowRightClick(e, row))" @oncontextmenu:preventDefault="true">@row.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")</div></MudTd>
<MudTd DataLabel="Created At"><div @oncontextmenu="@(async (e) => await OnRowRightClick(e, row))" @oncontextmenu:preventDefault="true">@DateTimeHelper.FormatDateTime(row.CreatedAt)</div></MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>No data inbox items to display</MudText>

View File

@@ -15,6 +15,7 @@ public partial class Index : ComponentBase
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private DataInboxFilterStateService FilterStateService { get; set; } = default!;
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
[Inject] private DateTimeHelper DateTimeHelper { get; set; } = default!;
private PagedResult<DataInboxDto> dataInbox = new();
@@ -23,6 +24,7 @@ public partial class Index : ComponentBase
protected override async Task OnInitializedAsync()
{
await DateTimeHelper.InitializeAsync();
filterRequest = FilterStateService.FilterRequest;
await LoadDataInbox();
}

View File

@@ -6,6 +6,7 @@
@inject EntityChangeHubService HubService
@inject NavigationManager NavigationManager
@inject ISnackbar Snackbar
@inject DateTimeHelper DateTimeHelper
@implements IDisposable
<MudCard>
@@ -92,14 +93,14 @@
</MudItem>
<MudItem xs="12" md="6">
<MudTextField Value="@job.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")"
<MudTextField Value="@DateTimeHelper.FormatDateTime(job.CreatedAt)"
Label="Created At"
Variant="Variant.Outlined"
ReadOnly="true"
FullWidth="true"/>
</MudItem>
<MudItem xs="12" md="6">
<MudTextField Value="@(job.LastAttemptAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-")"
<MudTextField Value="@DateTimeHelper.FormatDateTime(job.LastAttemptAt)"
Label="Last Attempt At"
Variant="Variant.Outlined"
ReadOnly="true"
@@ -107,7 +108,7 @@
</MudItem>
<MudItem xs="12" md="6">
<MudTextField Value="@(job.CompletedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-")"
<MudTextField Value="@DateTimeHelper.FormatDateTime(job.CompletedAt)"
Label="Completed At"
Variant="Variant.Outlined"
ReadOnly="true"
@@ -161,6 +162,7 @@
protected override async Task OnInitializedAsync()
{
await DateTimeHelper.InitializeAsync();
await LoadJob();
// Subscribe to SignalR entity changes

View File

@@ -12,25 +12,28 @@
Expanded="true">
<MudGrid AlignItems="Center">
<MudItem xs="12" sm="6" md="3">
<MudSelect T="JobStatus?"
@bind-Value="selectedStatus"
<MudSelect T="JobStatus"
SelectedValues="selectedStatuses"
Label="Status"
Placeholder="All statuses"
MultiSelection="true"
Clearable="true"
SelectedValuesChanged="OnStatusFilterChanged"
OnClearButtonClick="OnStatusClear">
@foreach (JobStatus status in Enum.GetValues(typeof(JobStatus)))
{
<MudSelectItem T="JobStatus?" Value="@status">@status.ToString()</MudSelectItem>
<MudSelectItem T="JobStatus" Value="@status">@status.ToString()</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="JobType?"
@bind-Value="selectedJobType"
Value="selectedJobType"
Label="Job Type"
Placeholder="All types"
Clearable="true"
ValueChanged="OnJobTypeFilterChanged"
OnClearButtonClick="OnJobTypeClear">
@foreach (JobType type in Enum.GetValues(typeof(JobType)))
{
@@ -39,7 +42,33 @@
</MudSelect>
</MudItem>
<MudItem xs="12" sm="12" md="6" Class="d-flex justify-end align-center">
<MudItem xs="12" sm="12" md="6" Class="d-flex justify-end align-center gap-2">
<MudMenu Icon="@Icons.Material.Filled.PlayArrow"
Label="Schedule Jobs"
Variant="Variant.Filled"
Color="Color.Success"
Size="Size.Medium"
EndIcon="@Icons.Material.Filled.KeyboardArrowDown">
<MudMenuItem OnClick="@(() => ScheduleJobs("all"))">
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.PlayCircle" Class="mr-2" />
<span>Run All Jobs</span>
</div>
</MudMenuItem>
<MudMenuItem OnClick="@(() => ScheduleJobs("imports"))">
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.FileDownload" Class="mr-2" />
<span>Run All Imports</span>
</div>
</MudMenuItem>
<MudMenuItem OnClick="@(() => ScheduleJobs("processes"))">
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.Settings" Class="mr-2" />
<span>Run All Processes</span>
</div>
</MudMenuItem>
</MudMenu>
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
OnClick="LoadJobs"
Color="Color.Primary"
@@ -108,12 +137,12 @@
</MudTd>
<MudTd DataLabel="Created">
<div @oncontextmenu="@(async (e) => await OnRowRightClick(e, row))" @oncontextmenu:preventDefault="true">
@row.CreatedAt.ToString("yyyy-MM-dd HH:mm")
@DateTimeHelper.FormatDateTime(row.CreatedAt, "yyyy-MM-dd HH:mm")
</div>
</MudTd>
<MudTd DataLabel="Last Attempt">
<div @oncontextmenu="@(async (e) => await OnRowRightClick(e, row))" @oncontextmenu:preventDefault="true">
@(row.LastAttemptAt?.ToString("yyyy-MM-dd HH:mm") ?? "-")
@DateTimeHelper.FormatDateTime(row.LastAttemptAt, "yyyy-MM-dd HH:mm")
</div>
</MudTd>
</RowTemplate>

View File

@@ -15,16 +15,18 @@ public partial class Index : ComponentBase, IDisposable
[Inject] private ISnackbar Snackbar { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
[Inject] private DateTimeHelper DateTimeHelper { get; set; } = default!;
private PagedResult<QueueJob> jobs = new();
private bool isLoading = false;
private int currentPage = 1;
private int pageSize = 50;
private JobStatus? selectedStatus = null;
private IEnumerable<JobStatus> selectedStatuses = new HashSet<JobStatus>();
private JobType? selectedJobType = null;
protected override async Task OnInitializedAsync()
{
await DateTimeHelper.InitializeAsync();
await LoadJobs();
// Subscribe to SignalR entity changes
@@ -60,7 +62,8 @@ public partial class Index : ComponentBase, IDisposable
try
{
jobs = await JobService.GetJobsAsync(currentPage, pageSize, selectedStatus, selectedJobType);
var statusList = selectedStatuses?.ToList();
jobs = await JobService.GetJobsAsync(currentPage, pageSize, statusList, selectedJobType);
}
catch (Exception ex)
{
@@ -81,15 +84,29 @@ public partial class Index : ComponentBase, IDisposable
private async Task ClearFilters()
{
selectedStatus = null;
selectedStatuses = new HashSet<JobStatus>();
selectedJobType = null;
currentPage = 1;
await LoadJobs();
}
private async Task OnStatusFilterChanged(IEnumerable<JobStatus> values)
{
selectedStatuses = values;
currentPage = 1;
await LoadJobs();
}
private async Task OnJobTypeFilterChanged(JobType? value)
{
selectedJobType = value;
currentPage = 1;
await LoadJobs();
}
private async Task OnStatusClear()
{
selectedStatus = null;
selectedStatuses = new HashSet<JobStatus>();
currentPage = 1;
await LoadJobs();
}
@@ -112,6 +129,41 @@ public partial class Index : ComponentBase, IDisposable
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
}
private async Task ScheduleJobs(string type)
{
isLoading = true;
try
{
(bool success, int jobsCreated, string message) result = type switch
{
"all" => await JobService.ScheduleAllJobsAsync(),
"imports" => await JobService.ScheduleImportJobsAsync(),
"processes" => await JobService.ScheduleProcessJobsAsync(),
_ => (false, 0, "Unknown job type")
};
if (result.success)
{
Snackbar.Add($"{result.message} ({result.jobsCreated} jobs created)", Severity.Success);
await LoadJobs();
}
else
{
Snackbar.Add(result.message, Severity.Error);
}
}
catch (Exception ex)
{
Console.WriteLine($"Scheduling jobs failed: {ex.Message}");
Snackbar.Add($"Failed to schedule jobs: {ex.Message}", Severity.Error);
}
finally
{
isLoading = false;
}
}
private Color GetStatusColor(JobStatus status)
{
return status switch

View File

@@ -58,7 +58,7 @@
}
</MudItem>
<MudItem xs="12" md="6">
<MudTextField Value="@layer.CreatedAt.ToString("g")"
<MudTextField Value="@DateTimeHelper.FormatDateTime(layer.CreatedAt, "yyyy-MM-dd HH:mm")"
Label="Created"
Variant="Variant.Outlined"
ReadOnly="true"
@@ -67,7 +67,7 @@
AdornmentText="@(layer.CreatedBy?.Username ?? "")"/>
</MudItem>
<MudItem xs="12" md="6">
<MudTextField Value="@layer.ModifiedAt.ToString("g")"
<MudTextField Value="@DateTimeHelper.FormatDateTime(layer.ModifiedAt, "yyyy-MM-dd HH:mm")"
Label="Modified"
Variant="Variant.Outlined"
ReadOnly="true"
@@ -316,7 +316,7 @@
<RowTemplate>
<MudTd DataLabel="Code">@context.Code</MudTd>
<MudTd DataLabel="Description">@context.Desc1</MudTd>
<MudTd DataLabel="Modified">@context.ModifiedAt.ToString("g")</MudTd>
<MudTd DataLabel="Modified">@DateTimeHelper.FormatDateTime(context.ModifiedAt, "yyyy-MM-dd HH:mm")</MudTd>
<MudTd DataLabel="Modified By">@GetModifiedByUsername(context.ModifiedById)</MudTd>
</RowTemplate>
</MudTable>

View File

@@ -26,6 +26,9 @@ public partial class Details : ComponentBase
[Inject]
private ISnackbar Snackbar { get; set; } = null!;
[Inject]
private DateTimeHelper DateTimeHelper { get; set; } = null!;
private LayerDto? layer;
private List<RecordDto> records = new();
private List<string> displayedColumns = new();
@@ -52,6 +55,7 @@ public partial class Details : ComponentBase
protected override async Task OnInitializedAsync()
{
await DateTimeHelper.InitializeAsync();
await LoadLayer();
}

View File

@@ -0,0 +1,80 @@
using Microsoft.JSInterop;
namespace DiunaBI.UI.Shared.Services;
public class DateTimeHelper
{
private readonly IJSRuntime _jsRuntime;
private TimeZoneInfo? _userTimeZone;
private bool _initialized = false;
public DateTimeHelper(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task InitializeAsync()
{
if (_initialized) return;
try
{
// Get the user's timezone from JavaScript
var timeZoneId = await _jsRuntime.InvokeAsync<string>("eval", "Intl.DateTimeFormat().resolvedOptions().timeZone");
// Try to find the TimeZoneInfo
try
{
_userTimeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
}
catch
{
// Fallback to local timezone if the IANA timezone ID is not found
_userTimeZone = TimeZoneInfo.Local;
}
}
catch
{
// Fallback to local timezone if JavaScript interop fails
_userTimeZone = TimeZoneInfo.Local;
}
_initialized = true;
}
public string FormatDateTime(DateTime? dateTime, string format = "yyyy-MM-dd HH:mm:ss")
{
if (!dateTime.HasValue)
return "-";
if (!_initialized)
{
// If not initialized yet, just format as-is (will be UTC)
return dateTime.Value.ToString(format);
}
// Convert UTC to user's timezone
var localDateTime = TimeZoneInfo.ConvertTimeFromUtc(dateTime.Value, _userTimeZone ?? TimeZoneInfo.Local);
return localDateTime.ToString(format);
}
public string FormatDate(DateTime? dateTime, string format = "yyyy-MM-dd")
{
return FormatDateTime(dateTime, format);
}
public string FormatTime(DateTime? dateTime, string format = "HH:mm:ss")
{
return FormatDateTime(dateTime, format);
}
public string GetTimeZoneAbbreviation()
{
if (!_initialized || _userTimeZone == null)
return "UTC";
return _userTimeZone.IsDaylightSavingTime(DateTime.Now)
? _userTimeZone.DaylightName
: _userTimeZone.StandardName;
}
}

View File

@@ -19,13 +19,18 @@ public class JobService
PropertyNameCaseInsensitive = true
};
public async Task<PagedResult<QueueJob>> GetJobsAsync(int page = 1, int pageSize = 50, JobStatus? status = null, JobType? jobType = null, Guid? layerId = null)
public async Task<PagedResult<QueueJob>> GetJobsAsync(int page = 1, int pageSize = 50, List<JobStatus>? statuses = null, JobType? jobType = null, Guid? layerId = null)
{
var start = (page - 1) * pageSize;
var query = $"Jobs?start={start}&limit={pageSize}";
if (status.HasValue)
query += $"&status={(int)status.Value}";
if (statuses != null && statuses.Count > 0)
{
foreach (var status in statuses)
{
query += $"&statuses={(int)status}";
}
}
if (jobType.HasValue)
query += $"&jobType={(int)jobType.Value}";
@@ -83,6 +88,89 @@ public class JobService
return await response.Content.ReadFromJsonAsync<CreateJobResult>();
}
public async Task<(bool success, int jobsCreated, string message)> ScheduleAllJobsAsync(string? nameFilter = null)
{
try
{
var query = string.IsNullOrEmpty(nameFilter) ? "" : $"?nameFilter={Uri.EscapeDataString(nameFilter)}";
var response = await _httpClient.PostAsync($"Jobs/ui/schedule{query}", null);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
return (false, 0, $"Failed to schedule jobs: {error}");
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<JsonElement>(json, _jsonOptions);
var jobsCreated = result.GetProperty("jobsCreated").GetInt32();
var message = result.GetProperty("message").GetString() ?? "Jobs scheduled";
return (true, jobsCreated, message);
}
catch (Exception ex)
{
Console.WriteLine($"Scheduling jobs failed: {ex.Message}");
return (false, 0, $"Error: {ex.Message}");
}
}
public async Task<(bool success, int jobsCreated, string message)> ScheduleImportJobsAsync(string? nameFilter = null)
{
try
{
var query = string.IsNullOrEmpty(nameFilter) ? "" : $"?nameFilter={Uri.EscapeDataString(nameFilter)}";
var response = await _httpClient.PostAsync($"Jobs/ui/schedule/imports{query}", null);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
return (false, 0, $"Failed to schedule import jobs: {error}");
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<JsonElement>(json, _jsonOptions);
var jobsCreated = result.GetProperty("jobsCreated").GetInt32();
var message = result.GetProperty("message").GetString() ?? "Import jobs scheduled";
return (true, jobsCreated, message);
}
catch (Exception ex)
{
Console.WriteLine($"Scheduling import jobs failed: {ex.Message}");
return (false, 0, $"Error: {ex.Message}");
}
}
public async Task<(bool success, int jobsCreated, string message)> ScheduleProcessJobsAsync()
{
try
{
var response = await _httpClient.PostAsync("Jobs/ui/schedule/processes", null);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
return (false, 0, $"Failed to schedule process jobs: {error}");
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<JsonElement>(json, _jsonOptions);
var jobsCreated = result.GetProperty("jobsCreated").GetInt32();
var message = result.GetProperty("message").GetString() ?? "Process jobs scheduled";
return (true, jobsCreated, message);
}
catch (Exception ex)
{
Console.WriteLine($"Scheduling process jobs failed: {ex.Message}");
return (false, 0, $"Error: {ex.Message}");
}
}
}
public class JobStats