From 55bc825cb73ff3493d835837db82671910cad0e3 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Wed, 9 Apr 2025 20:24:31 +0200 Subject: [PATCH] Added hangfire. Implemented hangfire statistics. Updated lucide icons --- .../Database/CoreDataContext.cs | 10 +- ...0405172522_AddedHangfireTables.Designer.cs | 393 ++++++++++++++++++ .../20250405172522_AddedHangfireTables.cs | 290 +++++++++++++ .../CoreDataContextModelSnapshot.cs | 304 ++++++++++++++ .../Admin/Sys/HangfireController.cs | 40 ++ .../Moonlight.ApiServer.csproj | 3 + Moonlight.ApiServer/Startup.cs | 52 ++- Moonlight.Client/Styles/additions/fonts.css | 2 +- .../UI/Views/Admin/Sys/Advanced.razor | 2 +- .../UI/Views/Admin/Sys/Hangfire.razor | 42 ++ Moonlight.Client/UiConstants.cs | 12 +- .../Admin/Hangfire/HangfireStatsResponse.cs | 16 + 12 files changed, 1157 insertions(+), 9 deletions(-) create mode 100644 Moonlight.ApiServer/Database/Migrations/20250405172522_AddedHangfireTables.Designer.cs create mode 100644 Moonlight.ApiServer/Database/Migrations/20250405172522_AddedHangfireTables.cs create mode 100644 Moonlight.ApiServer/Http/Controllers/Admin/Sys/HangfireController.cs create mode 100644 Moonlight.Client/UI/Views/Admin/Sys/Hangfire.razor create mode 100644 Moonlight.Shared/Http/Responses/Admin/Hangfire/HangfireStatsResponse.cs diff --git a/Moonlight.ApiServer/Database/CoreDataContext.cs b/Moonlight.ApiServer/Database/CoreDataContext.cs index 06e6f89f..b9875047 100644 --- a/Moonlight.ApiServer/Database/CoreDataContext.cs +++ b/Moonlight.ApiServer/Database/CoreDataContext.cs @@ -1,8 +1,8 @@ -using Microsoft.EntityFrameworkCore; +using Hangfire.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using MoonCore.Extended.SingleDb; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database.Entities; -using Moonlight.ApiServer.Helpers; namespace Moonlight.ApiServer.Database; @@ -24,4 +24,10 @@ public class CoreDataContext : DatabaseContext Database = configuration.Database.Database }; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.OnHangfireModelCreating(); + } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Database/Migrations/20250405172522_AddedHangfireTables.Designer.cs b/Moonlight.ApiServer/Database/Migrations/20250405172522_AddedHangfireTables.Designer.cs new file mode 100644 index 00000000..96b7445b --- /dev/null +++ b/Moonlight.ApiServer/Database/Migrations/20250405172522_AddedHangfireTables.Designer.cs @@ -0,0 +1,393 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.ApiServer.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Moonlight.ApiServer.Database.Migrations +{ + [DbContext(typeof(CoreDataContext))] + [Migration("20250405172522_AddedHangfireTables")] + partial class AddedHangfireTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Value") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("Key", "Value"); + + b.ToTable("HangfireCounter"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Field") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key", "Field"); + + b.HasIndex("ExpireAt"); + + b.ToTable("HangfireHash"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvocationData") + .IsRequired() + .HasColumnType("text"); + + b.Property("StateId") + .HasColumnType("bigint"); + + b.Property("StateName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("StateId"); + + b.HasIndex("StateName"); + + b.ToTable("HangfireJob"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => + { + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("JobId", "Name"); + + b.ToTable("HangfireJobParameter"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key", "Position"); + + b.HasIndex("ExpireAt"); + + b.ToTable("HangfireList"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AcquiredAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("HangfireLock"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FetchedAt") + .IsConcurrencyToken() + .HasColumnType("timestamp with time zone"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Queue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("Queue", "FetchedAt"); + + b.ToTable("HangfireQueuedJob"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Heartbeat") + .HasColumnType("timestamp with time zone"); + + b.Property("Queues") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Heartbeat"); + + b.ToTable("HangfireServer"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Value") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.HasKey("Key", "Value"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("Key", "Score"); + + b.ToTable("HangfireSet"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Reason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.ToTable("HangfireState"); + }); + + modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("Core_ApiKeys", (string)null); + }); + + modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TokenValidTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Core_Users", (string)null); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State") + .WithMany() + .HasForeignKey("StateId"); + + b.Navigation("State"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("Parameters") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("QueuedJobs") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("States") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.Navigation("Parameters"); + + b.Navigation("QueuedJobs"); + + b.Navigation("States"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight.ApiServer/Database/Migrations/20250405172522_AddedHangfireTables.cs b/Moonlight.ApiServer/Database/Migrations/20250405172522_AddedHangfireTables.cs new file mode 100644 index 00000000..9fd060d1 --- /dev/null +++ b/Moonlight.ApiServer/Database/Migrations/20250405172522_AddedHangfireTables.cs @@ -0,0 +1,290 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Moonlight.ApiServer.Database.Migrations +{ + /// + public partial class AddedHangfireTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "HangfireCounter", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Key = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Value = table.Column(type: "bigint", nullable: false), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireCounter", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "HangfireHash", + columns: table => new + { + Key = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Field = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Value = table.Column(type: "text", nullable: true), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireHash", x => new { x.Key, x.Field }); + }); + + migrationBuilder.CreateTable( + name: "HangfireList", + columns: table => new + { + Key = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Position = table.Column(type: "integer", nullable: false), + Value = table.Column(type: "text", nullable: true), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireList", x => new { x.Key, x.Position }); + }); + + migrationBuilder.CreateTable( + name: "HangfireLock", + columns: table => new + { + Id = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + AcquiredAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireLock", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "HangfireServer", + columns: table => new + { + Id = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + StartedAt = table.Column(type: "timestamp with time zone", nullable: false), + Heartbeat = table.Column(type: "timestamp with time zone", nullable: false), + WorkerCount = table.Column(type: "integer", nullable: false), + Queues = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireServer", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "HangfireSet", + columns: table => new + { + Key = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Value = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Score = table.Column(type: "double precision", nullable: false), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireSet", x => new { x.Key, x.Value }); + }); + + migrationBuilder.CreateTable( + name: "HangfireJob", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + StateId = table.Column(type: "bigint", nullable: true), + StateName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ExpireAt = table.Column(type: "timestamp with time zone", nullable: true), + InvocationData = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireJob", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "HangfireJobParameter", + columns: table => new + { + JobId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireJobParameter", x => new { x.JobId, x.Name }); + table.ForeignKey( + name: "FK_HangfireJobParameter_HangfireJob_JobId", + column: x => x.JobId, + principalTable: "HangfireJob", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "HangfireQueuedJob", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + JobId = table.Column(type: "bigint", nullable: false), + Queue = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + FetchedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireQueuedJob", x => x.Id); + table.ForeignKey( + name: "FK_HangfireQueuedJob_HangfireJob_JobId", + column: x => x.JobId, + principalTable: "HangfireJob", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "HangfireState", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + JobId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Reason = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Data = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HangfireState", x => x.Id); + table.ForeignKey( + name: "FK_HangfireState_HangfireJob_JobId", + column: x => x.JobId, + principalTable: "HangfireJob", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_HangfireCounter_ExpireAt", + table: "HangfireCounter", + column: "ExpireAt"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireCounter_Key_Value", + table: "HangfireCounter", + columns: new[] { "Key", "Value" }); + + migrationBuilder.CreateIndex( + name: "IX_HangfireHash_ExpireAt", + table: "HangfireHash", + column: "ExpireAt"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireJob_ExpireAt", + table: "HangfireJob", + column: "ExpireAt"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireJob_StateId", + table: "HangfireJob", + column: "StateId"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireJob_StateName", + table: "HangfireJob", + column: "StateName"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireList_ExpireAt", + table: "HangfireList", + column: "ExpireAt"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireQueuedJob_JobId", + table: "HangfireQueuedJob", + column: "JobId"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireQueuedJob_Queue_FetchedAt", + table: "HangfireQueuedJob", + columns: new[] { "Queue", "FetchedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_HangfireServer_Heartbeat", + table: "HangfireServer", + column: "Heartbeat"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireSet_ExpireAt", + table: "HangfireSet", + column: "ExpireAt"); + + migrationBuilder.CreateIndex( + name: "IX_HangfireSet_Key_Score", + table: "HangfireSet", + columns: new[] { "Key", "Score" }); + + migrationBuilder.CreateIndex( + name: "IX_HangfireState_JobId", + table: "HangfireState", + column: "JobId"); + + migrationBuilder.AddForeignKey( + name: "FK_HangfireJob_HangfireState_StateId", + table: "HangfireJob", + column: "StateId", + principalTable: "HangfireState", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_HangfireJob_HangfireState_StateId", + table: "HangfireJob"); + + migrationBuilder.DropTable( + name: "HangfireCounter"); + + migrationBuilder.DropTable( + name: "HangfireHash"); + + migrationBuilder.DropTable( + name: "HangfireJobParameter"); + + migrationBuilder.DropTable( + name: "HangfireList"); + + migrationBuilder.DropTable( + name: "HangfireLock"); + + migrationBuilder.DropTable( + name: "HangfireQueuedJob"); + + migrationBuilder.DropTable( + name: "HangfireServer"); + + migrationBuilder.DropTable( + name: "HangfireSet"); + + migrationBuilder.DropTable( + name: "HangfireState"); + + migrationBuilder.DropTable( + name: "HangfireJob"); + } + } +} diff --git a/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs b/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs index e0017152..f1945795 100644 --- a/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs +++ b/Moonlight.ApiServer/Database/Migrations/CoreDataContextModelSnapshot.cs @@ -22,6 +22,259 @@ namespace Moonlight.ApiServer.Database.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Value") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("Key", "Value"); + + b.ToTable("HangfireCounter"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Field") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key", "Field"); + + b.HasIndex("ExpireAt"); + + b.ToTable("HangfireHash"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvocationData") + .IsRequired() + .HasColumnType("text"); + + b.Property("StateId") + .HasColumnType("bigint"); + + b.Property("StateName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("StateId"); + + b.HasIndex("StateName"); + + b.ToTable("HangfireJob"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => + { + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("JobId", "Name"); + + b.ToTable("HangfireJobParameter"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key", "Position"); + + b.HasIndex("ExpireAt"); + + b.ToTable("HangfireList"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AcquiredAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("HangfireLock"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FetchedAt") + .IsConcurrencyToken() + .HasColumnType("timestamp with time zone"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Queue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("Queue", "FetchedAt"); + + b.ToTable("HangfireQueuedJob"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Heartbeat") + .HasColumnType("timestamp with time zone"); + + b.Property("Queues") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Heartbeat"); + + b.ToTable("HangfireServer"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Value") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.HasKey("Key", "Value"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("Key", "Score"); + + b.ToTable("HangfireSet"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Reason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.ToTable("HangfireState"); + }); + modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.ApiKey", b => { b.Property("Id") @@ -80,6 +333,57 @@ namespace Moonlight.ApiServer.Database.Migrations b.ToTable("Core_Users", (string)null); }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State") + .WithMany() + .HasForeignKey("StateId"); + + b.Navigation("State"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("Parameters") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("QueuedJobs") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("States") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.Navigation("Parameters"); + + b.Navigation("QueuedJobs"); + + b.Navigation("States"); + }); #pragma warning restore 612, 618 } } diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/Sys/HangfireController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/Sys/HangfireController.cs new file mode 100644 index 00000000..cc3bd93e --- /dev/null +++ b/Moonlight.ApiServer/Http/Controllers/Admin/Sys/HangfireController.cs @@ -0,0 +1,40 @@ +using Hangfire; +using Microsoft.AspNetCore.Mvc; +using MoonCore.Extended.PermFilter; +using Moonlight.Shared.Http.Responses.Admin.Hangfire; + +namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys; + +[ApiController] +[Route("api/admin/system/hangfire")] +[RequirePermission("admin.system.hangfire")] +public class HangfireController : Controller +{ + private readonly JobStorage JobStorage; + + public HangfireController(JobStorage jobStorage) + { + JobStorage = jobStorage; + } + + [HttpGet("stats")] + public Task GetStats() + { + var statistics = JobStorage.GetMonitoringApi().GetStatistics(); + + return Task.FromResult(new HangfireStatsResponse() + { + Awaiting = statistics.Awaiting, + Deleted = statistics.Deleted, + Enqueued = statistics.Enqueued, + Failed = statistics.Failed, + Processing = statistics.Processing, + Queues = statistics.Queues, + Recurring = statistics.Recurring, + Retries = statistics.Retries, + Scheduled = statistics.Scheduled, + Servers = statistics.Servers, + Succeeded = statistics.Succeeded + }); + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index 725dd32f..90d0ca28 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -19,6 +19,9 @@ + + + all diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index c9bf05d2..068f66f7 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -2,6 +2,8 @@ using System.Reflection; using System.Runtime.Loader; using System.Text; using System.Text.Json; +using Hangfire; +using Hangfire.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; @@ -15,6 +17,7 @@ using MoonCore.Extensions; using MoonCore.Helpers; using MoonCore.Services; using Moonlight.ApiServer.Configuration; +using Moonlight.ApiServer.Database; using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Helpers; using Moonlight.ApiServer.Interfaces.Startup; @@ -78,18 +81,20 @@ public class Startup await RegisterBase(); await RegisterDatabase(); await RegisterAuth(); + await RegisterCors(); + await RegisterHangfire(); await HookPluginBuild(); await RegisterPluginAssets(); - await RegisterCors(); await BuildWebApplication(); await PrepareDatabase(); - + await UseCors(); await UsePluginAssets(); // We need to move the plugin assets to the top to allow plugins to override content await UseBase(); await UseAuth(); + await UseHangfire(); await HookPluginConfigure(); await MapBase(); @@ -607,13 +612,54 @@ public class Startup builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().Build(); }); }); - + return Task.CompletedTask; } private Task UseCors() { WebApplication.UseCors(); + + return Task.CompletedTask; + } + + #endregion + + #region Hangfire + + private Task RegisterHangfire() + { + WebApplicationBuilder.Services.AddHangfire((provider, configuration) => + { + configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180); + configuration.UseSimpleAssemblyNameTypeSerializer(); + configuration.UseRecommendedSerializerSettings(); + configuration.UseEFCoreStorage(() => + { + var scope = provider.CreateScope(); + return scope.ServiceProvider.GetRequiredService(); + }, new EFCoreStorageOptions()); + }); + + WebApplicationBuilder.Services.AddHangfireServer(); + + WebApplicationBuilder.Logging.AddFilter( + "Hangfire.Server.BackgroundServerProcess", + LogLevel.Warning + ); + + WebApplicationBuilder.Logging.AddFilter( + "Hangfire.BackgroundJobServer", + LogLevel.Warning + ); + + return Task.CompletedTask; + } + + private Task UseHangfire() + { + if (WebApplication.Environment.IsDevelopment()) + WebApplication.UseHangfireDashboard(); return Task.CompletedTask; } diff --git a/Moonlight.Client/Styles/additions/fonts.css b/Moonlight.Client/Styles/additions/fonts.css index 1f039445..91cdc4c9 100644 --- a/Moonlight.Client/Styles/additions/fonts.css +++ b/Moonlight.Client/Styles/additions/fonts.css @@ -1,3 +1,3 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback'); @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap'); -@import url("https://cdn.jsdelivr.net/npm/lucide-static@0.460.0/font/lucide.css"); \ No newline at end of file +@import url("https://cdn.jsdelivr.net/npm/lucide-static@0.487.0/font/lucide.css"); \ No newline at end of file diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Advanced.razor b/Moonlight.Client/UI/Views/Admin/Sys/Advanced.razor index 3eee8d5e..b36f9f22 100644 --- a/Moonlight.Client/UI/Views/Admin/Sys/Advanced.razor +++ b/Moonlight.Client/UI/Views/Admin/Sys/Advanced.razor @@ -9,7 +9,7 @@ @inject DownloadService DownloadService
- +
diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Hangfire.razor b/Moonlight.Client/UI/Views/Admin/Sys/Hangfire.razor new file mode 100644 index 00000000..020b9062 --- /dev/null +++ b/Moonlight.Client/UI/Views/Admin/Sys/Hangfire.razor @@ -0,0 +1,42 @@ +@page "/admin/system/hangfire" + +@using MoonCore.Attributes +@using MoonCore.Helpers +@using Moonlight.Shared.Http.Responses.Admin.Hangfire +@using Moonlight.Client.UI.Components + +@attribute [RequirePermission("admin.system.hangfire")] + +@inject HttpApiClient ApiClient + +
+ +
+ + +
+ + + + + + + + + + + +
+
+ +@code +{ + private HangfireStatsResponse Stats; + + private async Task Load(LazyLoader _) + { + Stats = await ApiClient.GetJson( + "api/admin/system/hangfire/stats" + ); + } +} diff --git a/Moonlight.Client/UiConstants.cs b/Moonlight.Client/UiConstants.cs index 4e6a12be..9293cc7f 100644 --- a/Moonlight.Client/UiConstants.cs +++ b/Moonlight.Client/UiConstants.cs @@ -2,6 +2,14 @@ namespace Moonlight.Client; public static class UiConstants { - public static readonly string[] AdminNavNames = ["Overview", "Theme", "Files", "Advanced"]; - public static readonly string[] AdminNavLinks = ["/admin/system", "/admin/system/theme", "/admin/system/files", "/admin/system/advanced"]; + public static readonly string[] AdminNavNames = + [ + "Overview", "Theme", "Files", "Hangfire", "Advanced" + ]; + + public static readonly string[] AdminNavLinks = + [ + "/admin/system", "/admin/system/theme", "/admin/system/files", "/admin/system/hangfire", + "/admin/system/advanced" + ]; } \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Admin/Hangfire/HangfireStatsResponse.cs b/Moonlight.Shared/Http/Responses/Admin/Hangfire/HangfireStatsResponse.cs new file mode 100644 index 00000000..c925d814 --- /dev/null +++ b/Moonlight.Shared/Http/Responses/Admin/Hangfire/HangfireStatsResponse.cs @@ -0,0 +1,16 @@ +namespace Moonlight.Shared.Http.Responses.Admin.Hangfire; + +public class HangfireStatsResponse +{ + public long Servers { get; set; } + public long Recurring { get; set; } + public long Enqueued { get; set; } + public long Queues { get; set; } + public long Scheduled { get; set; } + public long Processing { get; set; } + public long Succeeded { get; set; } + public long Failed { get; set; } + public long Deleted { get; set; } + public long? Retries { get; set; } + public long? Awaiting { get; set; } +} \ No newline at end of file