Ticomix.WebJobs 2026.6.12.2

Ticomix.WebJobs

Purpose Statement

A .NET library that provides comprehensive background job scheduling and execution capabilities for web applications, including support for both scheduled jobs and on-demand background tasks. Built with Quartz.NET for robust job scheduling with distributed locking, progress tracking, and SignalR integration for real-time updates. Ideal for enterprise applications requiring reliable background processing.

Database Models Used

The package requires the following database models to be added to your application:

  • JobStatus: Main job configuration and scheduling settings
  • JobRun: Individual job execution history and status tracking
  • SysJobStatus: System job status lookup values
  • ErrorLog: Comprehensive error logging and monitoring (for logging job errors)

Additional context file required:

  • ApplicationDbContextWebJob.cs

Classes and Functions

Core Job Management

  • CommonJobBase: Abstract base class for all background jobs
  • JobHelper: Utility class for job execution and management
  • JobManager: Static manager for job discovery, execution, and lifecycle
  • JobHostedService: Hosted service for automatic job scheduling

Background Tasks

  • IBackgroundTask: Interface for long-running background tasks with progress reporting
  • BackgroundTaskHelper: Manager for executing background tasks with progress tracking
  • TaskState: State management for background task execution

SignalR Integration

  • TaskProgressHub: SignalR hub for real-time progress updates
  • TaskProgressHubClient: Client for sending progress updates to connected clients

Quartz Integration

  • TicomixJobRunner: Quartz job runner for scheduled job execution
  • TicomixQuartzExt: Extension methods for Quartz configuration

Attributes

  • JobSettingsAttribute: Metadata attribute for job configuration (Category, DisplayName)

Installation Instructions

Prerequisites

Ensure your application has the following NuGet packages installed:

  • Ticomix.WebJobs
  • Ticomix.EFCore
  • Ticomix.Common
  • Ticomix.Attachments.Common
  • Ticomix.EmailNote

Database Setup

  1. Add the required database models (JobStatus, JobRun, SysJobStatus) to your project
  2. Include the context file ApplicationDbContextWebJob.cs in your \Data folder
  3. Run database migrations to create the necessary tables

ASP.NET Core Configuration

Startup.cs / Program.cs Changes

  1. Add Required Using Statements:
using Ticomix.WebJobs;
using Ticomix.WebJobs.Extensions;
using Ticomix.WebJobs.QuartzJob;
  1. Configure Services:
// Background Task Helper
services.AddSingleton<IBackgroundTaskHelper, BackgroundTaskHelper>();

// Distributed Locking (required for job execution)
services.AddSingleton<IDistributedLockProvider>((services) => 
    new SqlDistributedSynchronizationProvider(Configuration.GetConnectionString("DefaultConnection")));

// Quartz Scheduler with Ticomix Integration
services.AddTicomixQuartz(RunJobsSeconds); // RunJobsSeconds is interval in seconds

// Hosted Service for automatic job scheduling (alternative to Quartz)
services.AddHostedService<JobHostedService<int>>();

// Job Status API endpoints (for Angular UI)
services.AddJobStatusApi<int>(options =>
{
    options.ReadRole = "Job Status";
    options.EditRole = "Job Status";
});

// SignalR for real-time progress updates (optional)
services.AddSignalR();
services.AddSingleton<TaskProgressHubClient>();
  1. Map Endpoints (in Program.cs after app.Build()):
app.UseRouting();

// Map Job Status API endpoints (required for Angular UI)
app.MapJobStatusEndpoints<int>();

// Map SignalR hub (optional, for real-time updates)
app.MapHub<TaskProgressHub>("/taskProgressHub");

app.MapControllers();
  1. Setup Jobs on Application Start:
// In Configure method
await JobManager<int>.SetupJobs(app.ApplicationServices);

// Configure Quartz Logging (optional)
Quartz.Logging.LogProvider.SetCurrentLogProvider(new ILoggerLogProvider(app.ApplicationServices));

Database Context Implementation

Create ApplicationDbContextWebJob.cs in your Data folder:

using Ticomix.Common.EFCore;
using Ticomix.Common.Models;
using Ticomix.WebJobs.Data;

namespace YourProject.Data
{
    public partial class ApplicationDbContext : IWebJobDbContext<int>
    {
        GenericDbSet<IJobRun<int>> IWebJobDbContext<int>.JobRun => 
            new GenericDbSet<IJobRun<int>>(this, this.JobRun);

        GenericDbSet<IJobStatus<int>> IWebJobDbContext<int>.JobStatus => 
            new GenericDbSet<IJobStatus<int>>(this, this.JobStatus);
    }
}

Implementation Examples

Creating a Scheduled Job

Example implementation of a scheduled background job:

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ticomix.Common.Helpers;
using Ticomix.WebJobs;

namespace YourProject.Jobs
{
    [JobSettings(Category = "Maintenance", DisplayName = "Cleanup Old Records")]
    public class CleanupJob : CommonJobBase<int>
    {
        public CleanupJob(IServiceProvider serviceProvider) : base(serviceProvider)
        {
        }

        public override async Task<JobResult> RunJob(CancellationToken cancellationToken)
        {
            try
            {
                Logger.LogMessageBatch(LogEntryErrorLevel.Information, $"{JobName} Started.");

                // Your job logic here
                var recordsToDelete = await db.SomeTable
                    .Where(x => x.CreatedDate < DateTime.Now.AddDays(-30))
                    .CountAsync(cancellationToken);

                // Process in batches to avoid timeout
                int batchSize = 1000;
                int deletedCount = 0;
                
                while (true)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    
                    var batch = await db.SomeTable
                        .Where(x => x.CreatedDate < DateTime.Now.AddDays(-30))
                        .Take(batchSize)
                        .ToListAsync(cancellationToken);

                    if (batch.Count == 0)
                        break;

                    db.SomeTable.RemoveRange(batch);
                    await db.SaveChangesAsync(cancellationToken);
                    deletedCount += batch.Count;
                }

                Logger.LogMessageBatch(LogEntryErrorLevel.Information, 
                    $"{JobName} Finished. Deleted {deletedCount} records.");

                return new JobResult()
                {
                    Success = true,
                    Message = $"Successfully deleted {deletedCount} old records",
                    Batch = Logger.Batch
                };
            }
            catch (Exception ex)
            {
                Logger.LogErrorBatch(ex);
                
                return new JobResult()
                {
                    Success = false,
                    Message = ex.Message,
                    Batch = Logger.Batch,
                    Exception = ex
                };
            }
        }
    }
}

Creating a Background Task with Progress

Example implementation of a background task with progress reporting:

using System;
using System.Threading;
using System.Threading.Tasks;
using Ticomix.WebJobs;

namespace YourProject.BackgroundTasks
{
    public class DataImportTask : IBackgroundTask
    {
        private readonly IServiceProvider serviceProvider;
        private readonly string filePath;

        public event EventHandler<BackgroundTaskProgressEventArgs> ProgressChanged;

        public DataImportTask(IServiceProvider serviceProvider, string filePath)
        {
            this.serviceProvider = serviceProvider;
            this.filePath = filePath;
        }

        public async Task<object> RunTask(CancellationToken cancellationToken = default)
        {
            var records = await LoadRecordsFromFile(filePath);
            int totalRecords = records.Count;
            int processedRecords = 0;

            foreach (var record in records)
            {
                cancellationToken.ThrowIfCancellationRequested();

                // Process individual record
                await ProcessRecord(record);
                processedRecords++;

                // Report progress
                decimal progress = (decimal)processedRecords / totalRecords;
                ProgressChanged?.Invoke(this, new BackgroundTaskProgressEventArgs(
                    new BackgroundTaskProgress 
                    { 
                        progress = progress,
                        state = $"Processed {processedRecords} of {totalRecords} records"
                    }));

                // Small delay to prevent overwhelming the system
                await Task.Delay(10, cancellationToken);
            }

            return $"Successfully imported {processedRecords} records from {filePath}";
        }

        private async Task<List<object>> LoadRecordsFromFile(string path)
        {
            // Your file loading logic here
            await Task.Delay(100); // Simulate async operation
            return new List<object>(); // Return actual records
        }

        private async Task ProcessRecord(object record)
        {
            // Your record processing logic here
            await Task.Delay(50); // Simulate processing time
        }
    }
}

Controller Implementation for Background Tasks

Example controller for managing background tasks:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ticomix.WebJobs;
using YourProject.BackgroundTasks;

namespace YourProject.Controllers
{
    [Authorize]
    public class BackgroundTaskController : Controller
    {
        private readonly IBackgroundTaskHelper backgroundTaskHelper;

        public BackgroundTaskController(IBackgroundTaskHelper backgroundTaskHelper)
        {
            this.backgroundTaskHelper = backgroundTaskHelper;
        }

        [HttpPost]
        public TaskState StartImport(string filePath)
        {
            var result = backgroundTaskHelper.RunTaskInBackground((sp) =>
            {
                return new DataImportTask(sp, filePath);
            });
            return result;
        }

        [HttpGet]
        public TaskState GetTaskStatus(string taskId)
        {
            var result = backgroundTaskHelper.GetTask(taskId, false);
            
            // Auto-remove completed tasks
            if (result?.TaskInfo.type == "BackgroundTaskFinished" || 
                result?.TaskInfo.type == "BackgroundTaskError")
            {
                backgroundTaskHelper.RemoveTask(taskId);
            }

            return result;
        }
    }
}

Job Status Controller (Legacy MVC Applications)

Example MVC controller for managing scheduled jobs with Kendo UI integration.

Legacy approach: This manually-written controller is only needed for MVC applications that cannot use the built-in endpoint registration. For Angular applications (and new MVC projects), use AddJobStatusApi<TKey>() + MapJobStatusEndpoints<TKey>() instead, which provides all these endpoints automatically. See the Angular UI Components section for details.

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Kendo.Mvc;
using Kendo.Mvc.Extensions;
using Kendo.Mvc.UI;
using Ticomix.WebJobs;
using YourProject.Data;
using YourProject.ViewModels;

namespace YourProject.Controllers
{
    [Authorize]
    public class JobStatusController : Controller
    {
        private readonly ApplicationDbContext db;
        private readonly IServiceScopeFactory serviceScopeFactory;

        public JobStatusController(IHttpContextAccessor accessor, ApplicationDbContext context, 
            IServiceScopeFactory serviceScopeFactory)
        {
            db = context;
            db.SetUserName(accessor.HttpContext);
            this.serviceScopeFactory = serviceScopeFactory;
        }

        public ActionResult Index()
        {
            return View();
        }

        public async Task<JsonResult> JobStatus_Read([DataSourceRequest] DataSourceRequest request)
        {
            // Ensure all discovered jobs exist in database
            var jobs = JobManager<int>.GetJobs();
            var dbJobStatus = await db.JobStatus.ToListAsync();
            foreach (var job in jobs)
            {
                if (!dbJobStatus.Any(a => a.JobName == job.Name))
                {
                    var model = new JobStatus()
                    {
                        JobName = job.Name
                    };
                    db.JobStatus.Add(model);
                    await db.SaveChangesAsync();
                }
            }

            // Build job status view model with runtime information
            var data = await (from a in db.JobStatus
                              join r in db.JobRun on a.LastJobRunID equals r.IDJobRun into _r
                              from r in _r.DefaultIfEmpty()
                              join s in db.SysJobStatus on r.JobStatusSys equals s.SysValue into _s
                              from s in _s.DefaultIfEmpty()
                              select new JobStatusViewModel
                              {
                                  Active = a.Active,
                                  IDJobStatus = a.IDJobStatus,
                                  JobName = a.JobName,
                                  LastErrorDate = a.LastErrorDate,
                                  LastRun = r.StartDateTime,
                                  LastStatus = s.Description,
                                  StatusMessage = r.StatusMessage,
                                  Interval = a.Interval,
                                  Frequency = a.Frequency,
                                  NextScheduledRun = a.NextScheduledRun
                              }).ToListAsync();

            // Enhance with runtime status and job attributes
            foreach (var d in data)
            {
                d.Hidden = !jobs.Any(a => a.Name == d.JobName);
                d.Running = await JobManager<int>.IsRunning(d.JobName, db);

                if (d.LastStatus == "Running" && !d.Running)
                {
                    d.LastStatus = "Error";
                    d.StatusMessage = "Web application stopped by system";
                }
                else if (d.Running)
                {
                    d.LastStatus = "Running";
                }

                var type = jobs.Where(a => a.Name == d.JobName).FirstOrDefault();
                if (type != null)
                {
                    var attr = JobManager<int>.GetJobAttribute(type);
                    if (attr != null)
                    {
                        d.DisplayName = attr.DisplayName ?? d.JobName;
                        d.Category = attr.Category;
                    }
                }
            }

            DataSourceResult result = data.ToDataSourceResult(request);
            return Json(result);
        }

        public async Task<ActionResult> RunJob(int IDJobStatus)
        {
            var jobStatus = await db.JobStatus.FindAsync(IDJobStatus);

            bool isRunning = await JobManager<int>.IsRunning(jobStatus.JobName, db);
            if (isRunning)
            {
                return Json(new JobResult()
                {
                    Success = false,
                    Message = "Job already running"
                });
            }

            var guid = Guid.NewGuid();
            var result = JobManager<int>.RunJobBackground(serviceScopeFactory, jobStatus.JobName);

            return Json(new JobResult()
            {
                Success = true,
                Message = "Job started",
                guid = guid
            });
        }

        [HttpPost]
        public ActionResult<StopJobResult> StopJob(string JobName)
        {
            var result = JobManager<int>.StopJob(JobName);
            return result;
        }
    }
}

Creating a Custom JobBase Class

Create a base class for your specific application jobs:

using System;
using YourProject.Data;
using Ticomix.WebJobs;

namespace YourProject.Helpers
{
    public abstract class JobBase : CommonJobBase<int>
    {
        public JobBase(IServiceProvider serviceProvider) : base(serviceProvider)
        {
        }

        public new ApplicationDbContext db 
        { 
            get { return (ApplicationDbContext)base.db; } 
        }
    }
}

TypeScript Integration (Frontend)

Example TypeScript helper for working with background tasks:

namespace BackgroundTask {
    export async function RunBackgroundTask(settings: JQueryAjaxSettings): Promise<TaskStatus> {
        let result = await $.ajaxAsync<TaskStatus>(settings);
        return await HandleResult(result);
    }

    async function GetStatus(taskId: string): Promise<TaskStatus> {
        return $.ajaxAsync<TaskStatus>({
            url: "/BackgroundTask/GetTaskStatus?taskId=" + taskId,
            type: "GET"
        });
    }

    async function HandleResult(result: TaskStatus): Promise<TaskStatus> {
        if (result.TaskInfo.type === "BackgroundTaskFinished" || 
            result.TaskInfo.type === "BackgroundTaskError") {
            return result;
        } else {
            await new Promise(resolve => setTimeout(resolve, 1000));
            let updatedResult = await GetStatus(result.TaskInfo.taskId);
            return await HandleResult(updatedResult);
        }
    }

    interface TaskStatus {
        TaskInfo: {
            taskId: string;
            type: string;
            message?: string;
            progress?: number;
            result?: any;
        };
    }
}

Configuration Options

JobHostedServiceOptions

Configure the job runner interval:

services.Configure<JobHostedServiceOptions>(options =>
{
    options.RunJobsSeconds = 30; // Check for scheduled jobs every 30 seconds
});

Job Scheduling

Jobs are automatically discovered and can be configured in the database:

  • Active: Enable/disable the job
  • Interval: "MINUTES", "HOURS", "DAYS", "WEEKS", "MONTHS"
  • Frequency: Number of intervals (e.g., 5 for every 5 minutes)
  • StartDateTime: When the job should start running
  • ActiveEnvironments: Comma-separated list of environments (e.g., "Development,Production")
  • Daily time restrictions: Configure specific time windows for job execution

Dependencies

Core Dependencies

  • Quartz (3.13.0) - Job scheduling framework
  • Quartz.Extensions.Hosting (3.13.0) - ASP.NET Core integration
  • DistributedLock.SqlServer (1.0.5) - Distributed locking for job coordination
  • Microsoft.Extensions.Hosting.Abstractions (8.0.0) - Hosted service support

Ticomix Dependencies

  • Ticomix.EFCore (via project reference)
  • Ticomix.Attachments.Common (via project reference)
  • Ticomix.EmailNote (via project reference)

Angular UI Components

For Angular applications, the package provides built-in API endpoints via AddJobStatusApi<TKey>() and MapJobStatusEndpoints<TKey>() that are designed to work with the companion NPM package:

NPM Package: @ticomix/job-status

Default Endpoints

The following endpoints are automatically mapped by MapJobStatusEndpoints<TKey>(). The default route prefix is api/JobStatus:

Method Route Description
POST /{prefix}/JobStatusRead Grid data source for job status list (Kendo DataSourceResult)
GET /{prefix}/GetJobStatus/{id} Get individual job configuration
POST /{prefix}/UpdateJobStatus Create or update job configuration
POST /{prefix}/RunJob/{id} Execute a job manually
GET /{prefix}/RunJobStatus/{guid} Check job execution tracking status by GUID
GET /{prefix}/GetRunJobStatus/{id} Get job run status by job ID
POST /{prefix}/StopJob Stop a running job
GET /{prefix}/GetAvailableJobs Get list of available job types
POST /{prefix}/JobHistoryRead Grid data source for job execution history (Kendo DataSourceResult)

Server-Side Registration

Register the endpoints in two steps:

1. Add services (in ConfigureServices / Program.cs):

services.AddJobStatusApi<int>(options =>
{
    options.ReadRole = "Job Status";
    options.EditRole = "Job Status";
});

2. Map endpoints (after app.Build()):

app.MapJobStatusEndpoints<int>();

JobStatusApiOptions

Configure endpoint behavior through JobStatusApiOptions:

services.AddJobStatusApi<int>(options =>
{
    // Route prefix (default: "api/JobStatus")
    options.RoutePrefix = "api/JobStatus";

    // Role-based authorization
    options.ReadRole = "Job Status View";   // Read operations (grid, get, available jobs)
    options.EditRole = "Job Status Edit";   // Edit operations (update, run, stop)

    // Or use a named authorization policy instead of roles
    options.AuthorizationPolicy = "MyPolicy";

    // Or use a custom authorization delegate
    options.AuthorizationDelegate = async (httpContext) =>
    {
        return httpContext.User.IsInRole("Admin");
    };

    // Allow anonymous access globally or per-endpoint
    options.AllowAnonymous = false;
    options.AnonymousEndpoints.Add(JobStatusEndpoint.GetAvailableJobs);

    // Disable specific endpoints (to replace with custom implementations)
    options.DisabledEndpoints.Add(JobStatusEndpoint.JobHistoryRead);

    // Add custom endpoints within the same route group
    options.ConfigureEndpoints = (group, opts) =>
    {
        group.MapPost("CustomEndpoint", async (HttpContext ctx) =>
        {
            return Results.Ok("custom");
        });
    };
});

Custom Logic Implementation

Override the default business logic by providing a custom IJobStatusLogic<TKey> implementation:

services.AddJobStatusApi<int, MyCustomJobStatusLogic>(options =>
{
    options.ReadRole = "Job Status";
});

Angular Client Integration

The Angular app consumes these endpoints through a generated TypeScript API client and a JobStatusService implementation:

// Provide the service in your app module
providers: [
  { provide: JobStatusService, useClass: AppJobStatusService }
]

The AppJobStatusService extends the abstract JobStatusService from @ticomix/job-status and delegates to the generated API client, which calls the endpoints mapped by MapJobStatusEndpoints<TKey>().

The NPM package provides:

  • Abstract list and form components (JobStatusListAbstractComponent, JobStatusFormAbstractComponent)
  • Job execution service with polling and exponential backoff
  • Real-time progress tracking via SignalR
  • Configurable navigation routes, grid settings, and permissions

Version Compatibility

  • .NET 8.0+
  • .NET 10.0+
  • Entity Framework Core 8.0+
  • SQL Server 2016+
  • Angular 19+ (for UI components via @ticomix/job-status)

No packages depend on Ticomix.WebJobs.

Version Downloads Last updated
2026.6.12.2 0 6/12/2026
2026.6.12.1 2 6/12/2026
2026.6.9.1 1 6/9/2026
2026.5.29.3 8 5/30/2026
2026.5.29.2 1 5/29/2026
2026.5.29.1 1 5/29/2026
2026.5.8.4 21 5/8/2026
2026.5.8.2 5 5/8/2026
2026.5.8.1 3 5/8/2026
2026.4.15.1 7 4/15/2026
2026.4.13.2 7 4/13/2026
2026.4.2.2 73 4/2/2026
2026.3.11.1 50 3/11/2026
2026.3.5.1 6 3/5/2026
2026.2.3.1 152 2/3/2026
2026.1.20.1 78 1/20/2026
2026.1.19.1 21 1/19/2026
2026.1.5.1 10 1/5/2026
2025.12.19.3 67 12/19/2025
2025.12.14.1 8 12/14/2025
2025.12.3.2 7 12/3/2025
2025.11.25.1 10 11/25/2025
2025.11.24.1 51 11/24/2025
2025.11.22.1 14 11/22/2025
2025.11.21.2 9 11/22/2025
2025.11.21.1 8 11/21/2025
2025.11.20.4 10 11/20/2025
2025.11.20.3 9 11/20/2025
2025.11.20.2 7 11/20/2025
2025.11.20.1 9 11/20/2025
2025.11.18.2 11 11/18/2025
2025.11.18.1 11 11/18/2025
2025.11.4.1 11 11/4/2025
2025.10.31.1 21 10/31/2025
2025.10.29.1 32 10/29/2025
2025.10.22.1 28 10/22/2025
2025.10.15.1 126 10/15/2025
2025.10.9.1 20 10/10/2025
2025.10.3.2 27 10/3/2025
2025.10.3.1 10 10/3/2025
2025.10.1.4 31 10/1/2025
2025.10.1.3 12 10/1/2025
2025.10.1.2 10 10/1/2025
2025.9.24.1 40 9/24/2025
2025.9.2.3 90 9/2/2025
2025.8.21.1 13 8/21/2025
2025.8.19.3 48 8/19/2025
2025.8.19.2 11 8/19/2025
2025.7.25.1 253 7/25/2025
2025.7.21.3 200 7/21/2025
2025.7.16.1 47 7/16/2025
2025.7.15.1 27 7/15/2025
2025.7.11.2 33 7/11/2025
2025.6.25.3 32 6/25/2025
2025.6.24.1 17 6/24/2025
2025.6.23.1 15 6/23/2025
2025.6.20.2 104 6/20/2025
2025.6.18.1 34 6/18/2025
2025.6.16.5 18 6/16/2025
2025.6.5.2 110 6/5/2025
2025.6.4.1 89 6/4/2025
2025.6.2.1 25 6/2/2025
2025.5.22.1 228 5/22/2025
2025.5.1.1 82 5/1/2025
2025.4.18.12 46 4/18/2025
2025.4.15.1 157 4/15/2025
2025.4.10.1 46 4/10/2025
2025.4.8.4 17 4/8/2025
2025.4.4.10 18 4/4/2025
2025.4.4.7 35 4/4/2025
2025.3.28.1 41 3/28/2025
2025.3.25.3 84 3/25/2025
2025.3.25.2 29 3/25/2025
2025.3.20.1 34 3/20/2025
2025.3.19.5 26 3/19/2025
2025.3.19.3 19 3/19/2025
2025.3.18.5 23 3/18/2025
2025.3.18.4 21 3/18/2025
2025.3.18.3 18 3/18/2025
2025.3.18.2 17 3/18/2025
2025.3.12.1 47 3/12/2025
2025.3.7.1 31 3/7/2025
2025.3.4.1 33 3/4/2025
2025.2.17.2 49 2/17/2025
2025.2.17.1 27 2/17/2025
2025.2.14.2 69 2/14/2025
2025.2.7.1 34 2/7/2025
2025.1.30.1 18 1/30/2025
2025.1.29.2 20 1/29/2025
2025.1.29.1 16 1/29/2025
2025.1.28.2 16 1/28/2025
2025.1.28.1 54 1/28/2025
2025.1.27.4 24 1/27/2025
2025.1.27.3 26 1/27/2025
2025.1.27.2 21 1/27/2025
2025.1.27.1 16 1/27/2025
2025.1.6.1 19 1/6/2025
2024.12.31.2 56 12/31/2024
2024.12.31.1 20 12/31/2024
2024.12.30.1 22 12/30/2024
2024.12.20.2 35 12/20/2024
2024.12.17.18 36 12/17/2024
2024.12.17.2 21 12/17/2024
2024.12.11.3 36 12/11/2024
2024.12.10.1 27 12/10/2024
2024.12.5.3 26 12/5/2024
2024.12.5.2 20 12/5/2024
2024.12.4.10 17 12/4/2024
2024.12.4.9 22 12/4/2024
2024.11.15.4 56 11/16/2024
2024.11.15.1 20 11/15/2024
2024.11.6.3 44 11/6/2024
2024.11.6.1 21 11/6/2024
2024.11.5.6 22 11/5/2024
2024.11.5.4 24 11/5/2024
2024.10.28.3 37 10/28/2024
2024.10.28.2 22 10/28/2024
2024.10.28.1 23 10/28/2024
2024.10.24.1 22 10/24/2024
2024.10.23.1 19 10/23/2024
2024.10.17.2 29 10/17/2024
2024.10.8.1 37 10/8/2024
2024.8.20.1 85 8/20/2024
2024.8.6.1 54 8/6/2024
2024.7.17.1 63 7/17/2024
2024.7.15.1 28 7/15/2024
2024.7.9.4 39 7/9/2024
2024.7.9.3 27 7/9/2024
2024.7.9.2 33 7/9/2024
2024.7.2.1 33 7/2/2024
2024.7.1.4 29 7/1/2024
2024.6.14.1 34 6/14/2024
2024.6.12.2 48 6/12/2024
2024.6.10.1 27 6/10/2024
2024.5.31.2 41 5/31/2024
2024.5.22.2 41 5/22/2024
2024.5.21.3 25 5/21/2024
2024.5.21.1 21 5/21/2024
2024.4.25.1 51 4/25/2024
2024.4.19.3 307 4/19/2024
2024.4.17.1 42 4/17/2024
2024.4.12.3 31 4/12/2024
2024.4.9.1 39 4/9/2024
2024.4.5.3 36 4/5/2024
2024.4.4.2 34 4/4/2024
2024.4.3.5 28 4/3/2024
2024.4.2.1 26 4/2/2024
2024.3.27.2 26 3/27/2024
2024.3.19.4 36 3/19/2024
2024.3.18.2 23 3/18/2024
2024.3.13.5 37 3/13/2024
2024.3.13.2 38 3/13/2024
2024.3.8.2 41 3/8/2024
2024.3.8.1 27 3/8/2024
2024.3.6.1 34 3/6/2024
2024.2.27.1 51 2/27/2024
2024.2.19.1 38 2/19/2024