Dot.QuartzDashboard 4.4.0

dotnet add package Dot.QuartzDashboard --version 4.4.0
                    
NuGet\Install-Package Dot.QuartzDashboard -Version 4.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Dot.QuartzDashboard" Version="4.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dot.QuartzDashboard" Version="4.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Dot.QuartzDashboard" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dot.QuartzDashboard --version 4.4.0
                    
#r "nuget: Dot.QuartzDashboard, 4.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Dot.QuartzDashboard@4.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dot.QuartzDashboard&version=4.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Dot.QuartzDashboard&version=4.4.0
                    
Install as a Cake Tool

Dot.QuartzDashboard

<p align="center"> <img src="https://raw.githubusercontent.com/nathan5580/QuartzDashboard/main/assets/logo.svg" width="200" alt="Dot.QuartzDashboard"> </p>

A self-contained, embedded Quartz.NET scheduler dashboard for ASP.NET Core. Two-line install, live SignalR updates, dark mode, persistent history, secure by default.

NuGet Downloads Build .NET License

<p align="center"> <img src="https://raw.githubusercontent.com/nathan5580/QuartzDashboard/main/ux-audit-screenshots/overview-dark.png" alt="Overview page in dark mode" width="900"> <br> <em>Overview page (dark mode). See <a href="#dashboard-pages">all pages</a> below.</em> </p>


Contents


What's New in v4.2.x

v4.2 is the security-defaults release โ€” two breaking default flips that make a misconfigured deployment fail closed instead of fail open.

  • RequireAuthentication now defaults to true. Before v4.2 the dashboard accepted anonymous requests by default, which on an open port meant anonymous remote job control. From v4.2 you must wire up UseAuthentication() / UseAuthorization() (or explicitly opt back into anonymous with options.RequireAuthentication = false plus the startup warning).
  • CSRF guard: RequireCsrfHeader defaults to true. Mutating endpoints require X-Requested-With: XMLHttpRequest or X-CSRF-Token. The bundled SPA sends the header automatically; custom front-ends must add it.
  • Defensive security headers: X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin on dashboard-owned responses only.
  • prefers-reduced-motion respected across every dashboard animation.
  • Toast queue announced to screen readers via aria-live="polite".
  • SignalR bridge memory leak fixed across host recycles โ€” handlers now unsubscribe in StopAsync.
  • N+1 trigger-state lookup eliminated on /api/jobs and /api/triggers โ€” GetTriggerState is batched via Task.WhenAll, dropping latency on schedulers with hundreds of triggers.
  • File history store canonicalizes paths via Path.GetFullPath so writes always land somewhere debuggable.
  • /api/import now surfaces placeholderJobs[] and a placeholderWarning when an IJob type can't be resolved at import time.
  • Polling fallback timer leak after page unload fixed โ€” pagehide and beforeunload stop the interval and SignalR connection.
  • failedHistory :key collision fixed (composite key includes fireInstanceId + fireTime + index).
  • FireRecord properties are now { get; init; } โ€” immutable across consumers and thread-safe by construction.
  • Per-request CancellationToken propagation โ€” ApiRouteContext.Ct is bound to HttpContext.RequestAborted and flows into Quartz scheduler calls.

v4.2.2 โ€” two-round persona audit (2026-05-27)

Twelve focused commits from a 14-persona audit pass. Drop-in upgrade from 4.2.1. Full notes in CHANGELOG.md; highlights:

  • Closed a stored XSS in the timeline row-action overlay (CWE-79) and added defence-in-depth name validation on every create / import endpoint. Security headers now ship on API responses too. CSP-friendly: no more inline onclick= in the bundled SPA.
  • ETag short-circuit for static assets โ€” Day-2 visits 304 instead of redownloading ~264 kB. index.html is cached as a byte[] after first token-replace.
  • Idle tabs stop polling โ€” document.hidden + visibilitychange catch-up.
  • Full WCAG 2.2 AA pass: keyboard-operable job rows + drawer-as-dialog with focus restore, skip-to-content link, aria-label on color-only signals, sidebar contrast lift, color-blind / forced-colors hardening.
  • Mobile responsive cleanup: Triggers right-edge clip, Graph chip overflow, Timeline 1023.8 m[s] clip, 44 ร— 44 pt tap-target floor.
  • AddQuartzDashboard is now idempotent; QuartzDashboardOptions is sealed. Dead QuartzDashboardAuthMiddleware deleted; source-generated regex for scheduler-name validation.
  • <html lang> + dir set from navigator.language at boot; locale-aware durations via Intl.NumberFormat.
  • Brand: unified on the NuGet icon's Q-ring mark (favicon + sidebar + boot splash were three different marks before).
  • Tests: unit suite 116 / 116 green for the first time โ€” pre-existing stale assertions updated to match the actual handler shapes.

v4.2.1 fixes (post-audit)

  • SignalR Subscribe no longer rejects when the dashboard's RequireAuthentication is false. The method-level [Authorize] overrode the hub endpoint's policy, leaving auth-off clients with a permanent "Real-time connection lost" banner.
  • Jobs page Alpine :aria-expanded page-error on every render fixed โ€” (undefined && ...).toString() is now (!! (...)).toString().
  • Auth 401/403 returns an HTML error page for browser navigations (Accept: text/html) instead of a raw JSON blob. Curl / fetch / XHR clients still get JSON.
  • Demo โ€” Program.cs now explicitly sets RequireAuthentication = authMode so plain dotnet run (no flags) lands on a working dashboard instead of a 401.

Migration from v4.1.x

  builder.Services.AddQuartzDashboard(options =>
  {
      options.Path = "/quartz";
+     // v4.2: defaults flipped to secure. Set explicitly only if you have
+     // an external auth / anti-forgery layer or run on a trusted network.
+     options.RequireAuthentication = false;
+     options.RequireCsrfHeader = false;
  });

Otherwise: wire up app.UseAuthentication() / app.UseAuthorization() and set options.AllowedRoles (or options.RequiredPolicy) โ€” see Authentication & Authorization.

For older release notes (v4.1, v4.0, v3.x) see CHANGELOG.md.


What it does

  • See all your Quartz jobs, triggers, fire schedules, and currently executing work in real time
  • Control the scheduler โ€” start, standby, trigger jobs, pause / resume / delete jobs and triggers
  • Navigate between pages with a single click โ€” clickable stat cards on the Overview jump directly to their section
  • Track execution history with date-range filters (1h / 6h / 24h), inline error previews, CSV export, server-side pagination, and live SVG charts
  • Pin key jobs to the Overview dashboard for a permanent heads-up view
  • Preview the next scheduled fire times with next-N-fires trigger inspection
  • Monitor execution rate, average duration, P50/P95/P99 percentiles, and error trends
  • Search jobs instantly with inline filters; global search across jobs, triggers, and history with Ctrl+K
  • Navigate with keyboard shortcuts โ€” ? to see all; G J/T/H/E/G/L/S/O to jump to any page
  • Inspect failed runs at a glance โ€” error snippets appear inline in the History table; full stacktrace in one click
  • Build CRON expressions visually with the built-in builder and presets
  • Alert on job failures via callbacks, webhooks, or the favicon failure badge
  • Secure with authentication, role-based access, and authorization policies
  • Persist fire history to SQLite, JSON, or in-memory storage
  • Embed the dashboard in iframes with ?embed=true (strips sidebar/header)
  • Adapt automatically to dark or light mode; fully responsive with mobile bottom tab bar
  • Stay self-contained โ€” bundled ES module assets embedded in the DLL; no external CDN required

Quick Start

dotnet add package Dot.QuartzDashboard
// Program.cs
using QuartzDashboard;

builder.Services.AddQuartz();
builder.Services.AddQuartzHostedService();

// Line 1: register dashboard services (history tracking included automatically)
builder.Services.AddQuartzDashboard();

var app = builder.Build();

app.UseAuthentication();  // if using auth
app.UseAuthorization();   // if using auth

// Line 2: mount the dashboard (before MapControllers / MapFallbackToFile)
app.UseQuartzDashboard();

app.MapControllers();
app.Run();

Open /quartz in your browser.

Dashboard Pages

Page What you see
Overview Clickable stat cards (Jobs, Triggers, Executing Now, Total Executions) with sparklines ยท scheduler uptime ยท last error card ยท pinned jobs ยท upcoming schedule preview ยท pin affordance hint
Jobs All jobs grouped by group ยท inline trigger details ยท relative last-run time ยท live search/filter with mobile toggle ยท sortable columns ยท trigger / pause / resume / delete / view history ยท server-side pagination
Triggers Grouped by job (accordion) ยท schedule descriptions ยท relative last-fire / next-fire times ยท pause / resume / delete per trigger
Executing Currently running jobs with animated duration bars ยท fire instance ID ยท live elapsed time ยท interrupt action
History Paginated fire events ยท inline error snippets on failed rows ยท date-range quick filters (1h / 6h / 24h / All) ยท status filter ยท full stacktrace on click ยท CSV + JSON export
Graph Dual-line SVG chart: execution count + avg duration + error rate ยท zoom toggles ยท duration overlay
Timeline Full-width Gantt bars color-coded per job ยท crosshair tooltip ยท auto-fit range ยท pulsing now-marker
Health Success rate with record-count context ยท failed executions trend ยท thread pool utilization bar ยท recent failures with error messages ยท scheduler diagnostics
Calendars Quartz calendars list with type badges and descriptions
Settings Refresh interval slider ยท per-page auto-refresh toggles ยท history retention info ยท keyboard shortcuts reference

Auto-refreshes every 5 seconds via SignalR. Dark/light theme with OS auto-detection. Fully responsive โ€” mobile bottom tab bar covers all 10 sections (scrollable). Collapsible sidebar. Sticky/sortable table headers. Full keyboard navigation (? for shortcut reference). Global search (Ctrl+K) with deduped history results. Favicon failure badge. Embed mode (?embed=true).

Iframe Embedding

Append ?embed=true to the dashboard URL to strip the sidebar and header for a cleaner embedded experience:

<iframe src="https://yourapp.com/quartz?embed=true"
        style="width: 100%; height: 700px; border: none;"
        title="Quartz Dashboard">
</iframe>

In embed mode, the sidebar navigation, top header, and breadcrumbs are hidden. All pages, API endpoints, and real-time features remain fully functional.

Configuration

builder.Services.AddQuartzDashboard(options =>
{
    options.Path = "/admin/scheduler";    // default: "/quartz"
    options.Enabled = true;               // false = UseQuartzDashboard() is a no-op
    options.ReadOnly = false;             // disable all write actions (see Read-Only Mode below)
    options.UseSignalR = true;            // real-time updates (registers hub automatically)

    // Auth
    options.RequireAuthentication = true;
    options.AllowedRoles = ["Admin"];          // role whitelist
    options.RequiredPolicy = "CanViewDashboard"; // named policy (takes priority over roles)

    // History limits
    options.MaxFireHistory = 500;
    options.MaxExecutionLogsPerJob = 50;
    options.HistoryRetentionHours = 24;
    options.PersistHistoryPath = "quartz-history.json";  // optional JSON persistence
    options.Title = "My App Dashboard";

    // Alerts
    options.OnJobFailed = async (jobKey, ex) => { /* Slack/PagerDuty */ };
    options.WebhookUrl = "https://hooks.slack.com/...";
});

Custom history store (Postgres, Redis, Mongo, โ€ฆ)

Implement the two-method IFireHistoryStore interface (from Dot.QuartzDashboard.Abstractions) and register it as a singleton after AddQuartzDashboard() โ€” it will replace the default in-memory store. The dashboard reads through the interface; you do not need to fork the package to add a new backend.

using QuartzDashboard.Abstractions;
using Npgsql;

public sealed class PostgresFireHistoryStore : IFireHistoryStore, IDisposable
{
    private readonly NpgsqlDataSource _db;
    public PostgresFireHistoryStore(string connectionString) =>
        _db = NpgsqlDataSource.Create(connectionString);

    public int Count
    {
        get
        {
            using var conn = _db.OpenConnection();
            using var cmd = new NpgsqlCommand("SELECT COUNT(*) FROM fire_history", conn);
            return Convert.ToInt32(cmd.ExecuteScalar());
        }
    }

    public event Action<FireRecord>? OnFireRecorded;

    public void RecordFire(string jobKey, string triggerKey, DateTimeOffset fireTime,
        TimeSpan duration, bool success, int refireCount = 0,
        string? exceptionMessage = null, string? exceptionType = null)
    {
        using var conn = _db.OpenConnection();
        using var cmd = new NpgsqlCommand(
            """
            INSERT INTO fire_history
              (job_key, trigger_key, fire_time, duration_ticks, success, refire_count, exception_message, exception_type)
            VALUES (@j, @t, @f, @d, @s, @r, @em, @et)
            """, conn);
        cmd.Parameters.AddWithValue("@j", jobKey);
        cmd.Parameters.AddWithValue("@t", triggerKey);
        cmd.Parameters.AddWithValue("@f", fireTime.UtcDateTime);
        cmd.Parameters.AddWithValue("@d", duration.Ticks);
        cmd.Parameters.AddWithValue("@s", success);
        cmd.Parameters.AddWithValue("@r", refireCount);
        cmd.Parameters.AddWithValue("@em", (object?)exceptionMessage ?? DBNull.Value);
        cmd.Parameters.AddWithValue("@et", (object?)exceptionType ?? DBNull.Value);
        cmd.ExecuteNonQuery();

        OnFireRecorded?.Invoke(new FireRecord
        {
            JobKey = jobKey,
            TriggerKey = triggerKey,
            FireTime = fireTime,
            Duration = duration,
            Success = success,
            RefireCount = refireCount,
            ExceptionMessage = exceptionMessage,
            ExceptionType = exceptionType,
        });
    }

    public IEnumerable<FireRecord> GetRecent(int count, int offset = 0)
    {
        using var conn = _db.OpenConnection();
        using var cmd = new NpgsqlCommand(
            """
            SELECT job_key, trigger_key, fire_time, duration_ticks, success, refire_count,
                   exception_message, exception_type
            FROM fire_history
            ORDER BY fire_time DESC, id DESC
            LIMIT @count OFFSET @offset
            """, conn);
        cmd.Parameters.AddWithValue("@count", count);
        cmd.Parameters.AddWithValue("@offset", offset);

        using var reader = cmd.ExecuteReader();
        var records = new List<FireRecord>();
        while (reader.Read())
        {
            records.Add(new FireRecord
            {
                JobKey = reader.GetString(0),
                TriggerKey = reader.GetString(1),
                FireTime = new DateTimeOffset(reader.GetDateTime(2), TimeSpan.Zero),
                Duration = TimeSpan.FromTicks(reader.GetInt64(3)),
                Success = reader.GetBoolean(4),
                RefireCount = reader.GetInt32(5),
                ExceptionMessage = reader.IsDBNull(6) ? null : reader.GetString(6),
                ExceptionType = reader.IsDBNull(7) ? null : reader.GetString(7),
            });
        }
        return records;
    }

    public void Clear()
    {
        using var conn = _db.OpenConnection();
        using var cmd = new NpgsqlCommand("DELETE FROM fire_history", conn);
        cmd.ExecuteNonQuery();
    }

    public void Dispose() => _db.Dispose();
}

// Program.cs
builder.Services.AddQuartzDashboard();
builder.Services.AddSingleton<IFireHistoryStore>(
    new PostgresFireHistoryStore(builder.Configuration.GetConnectionString("Quartz")!));

Contract notes:

  • Singleton lifetime โ€” the same instance is shared across the scheduler listener, the REST handlers, and the SignalR bridge. Make implementations thread-safe.
  • RecordFire is called on the Quartz thread pool โ€” keep it short and non-blocking, or buffer writes internally (the SQLite store coalesces to once per second; consider similar).
  • GetRecent(count, offset) is the hot read path. Return the newest record first. The dashboard calls it on every refresh, so make it indexed-by-fire-time descending.
  • OnFireRecorded is optional โ€” fire it after the write succeeds; the dashboard uses it for SignalR real-time fan-out.
  • Count is read on the /api/health endpoint; expensive COUNT(*)s on huge tables should be approximated (e.g., a cached value updated by RecordFire).

The SQLite store in Dot.QuartzDashboard.Sqlite is a useful reference implementation covering write coalescing, WAL mode, and indexed lookups.

SQLite persistent history

SQLite persistence ships in a separate package so the main dashboard NuGet doesn't drag Microsoft.Data.Sqlite into apps that don't need it.

dotnet add package Dot.QuartzDashboard.Sqlite
using QuartzDashboard.Sqlite;

builder.Services.AddQuartzDashboard();
builder.Services.AddQuartzDashboardSqliteHistory("quartz-history.db");
// Order: call AddQuartzDashboardSqliteHistory AFTER AddQuartzDashboard so it
// replaces the default in-memory store registration.

Use SQLite when you want fire history to survive restarts. Omit it for in-memory (default), or set options.PersistHistoryPath for JSON file persistence.

Dark mode

The UI automatically follows the system light/dark preference. No option required โ€” the user can also toggle manually from the Settings page.

Bind from appsettings.json

{
  "QuartzDashboard": {
    "Enabled": true,
    "Path": "/quartz",
    "ReadOnly": false,
    "UseSignalR": true,
    "RequireAuthentication": false,
    "RequiredPolicy": "",
    "AllowedRoles": [],
    "MaxFireHistory": 500,
    "MaxExecutionLogsPerJob": 50,
    "HistoryRetentionHours": 24,
    "PersistHistoryPath": "quartz-history.json",
    "Title": "QuartzDash"
  }
}
builder.Services.AddQuartzDashboard(options =>
    builder.Configuration.GetSection("QuartzDashboard").Bind(options));

Environment gating

builder.Services.AddQuartzDashboard(options =>
{
    options.Enabled = !builder.Environment.IsProduction();
});

Authentication & Authorization

Three levels, checked in order:

  1. RequireAuthentication (default true since v4.2) โ€” unauthenticated requests โ†’ 401
  2. RequiredPolicy โ€” uses IAuthorizationService (named policy) โ†’ 403 on failure
  3. AllowedRoles โ€” role whitelist, checked if no policy is set โ†’ 403 on failure

The dashboard exposes job-trigger, pause, resume, and delete endpoints. Defaulting to "auth on" prevents a casual app.UseQuartzDashboard() from anonymously exposing remote job control. Disable explicitly only when the dashboard is reachable solely from a trusted network (the package logs a startup warning if you do).

CSRF protection

RequireCsrfHeader (default true since v4.2) blocks mutating endpoints (POST / PUT / DELETE / PATCH) unless the request carries a custom header โ€” either X-Requested-With: XMLHttpRequest or X-CSRF-Token: anything. Browsers cannot send custom headers via simple cross-origin form submits without triggering a preflight, so the header acts as a same-origin assertion and stops a logged-in operator's browser from being weaponised by a malicious page. The bundled SPA always sends the header. Custom front-ends (curl, Postman, scripts) must add it themselves:

curl -X POST -H "X-Requested-With: XMLHttpRequest" \
     https://your.app/quartz/api/jobs/demo/MyJob/trigger

Disable only if you have an alternative anti-forgery defence (e.g., an upstream gateway that strips and validates a CSRF cookie); the package logs a startup warning when off.

// Role-based
builder.Services.AddQuartzDashboard(options =>
{
    options.RequireAuthentication = true;
    options.AllowedRoles = ["Admin", "Operator"];
});

// Policy-based
builder.Services.AddAuthorization(o =>
    o.AddPolicy("RequireDashboardAccess", p => p.RequireRole("Admin")));

builder.Services.AddQuartzDashboard(options =>
{
    options.RequireAuthentication = true;
    options.RequiredPolicy = "RequireDashboardAccess";
});

Read-Only Mode

Set ReadOnly = true to expose the dashboard to a wider audience without granting control over the scheduler.

When ReadOnly = true:

Blocked: Trigger job, Pause/Resume job or trigger, Delete job/trigger/calendar, Start/Standby scheduler, Create/Edit triggers, Interrupt executing jobs.

Still available: All GET endpoints, history export (CSV/JSON), print report, real-time updates via SignalR.

Useful for monitoring-only dashboards exposed to a wider audience.

Multi-Scheduler Support

When multiple Quartz.NET schedulers are registered in the same application, the dashboard automatically detects and displays a scheduler picker in the header. API calls are routed to the selected scheduler via a ?scheduler=SchedulerName query parameter.

// Register multiple schedulers with distinct IDs
builder.Services.AddQuartz(q => { q.SchedulerId = "Primary"; });
builder.Services.AddQuartz(q => { q.SchedulerId = "Secondary"; });

// The dashboard picks up all registered ISchedulerFactory instances automatically
builder.Services.AddQuartzDashboard();

No additional configuration is required โ€” the scheduler picker appears automatically when more than one scheduler is detected.

Migrating from v3.x to v4.0

  1. Update using statements for custom history stores. IFireHistoryStore and FireRecord moved namespace:

    - using QuartzDashboard.Internal;
    + using QuartzDashboard.Abstractions;
    

    Or add <PackageReference Include="Dot.QuartzDashboard.Abstractions" /> if you only need the interface.

  2. Replace options.PersistHistoryToSqlite with the new package + extension method.

    - builder.Services.AddQuartzDashboard(o =>
    - {
    -     o.PersistHistoryToSqlite = "quartz-history.db";
    - });
    + using QuartzDashboard.Sqlite;
    +
    + builder.Services.AddQuartzDashboard();
    + builder.Services.AddQuartzDashboardSqliteHistory("quartz-history.db");
    

    Add <PackageReference Include="Dot.QuartzDashboard.Sqlite" />. The main Dot.QuartzDashboard package no longer ships Microsoft.Data.Sqlite.

  3. Nothing else changes. The middleware registration, options surface, dashboard URL, API routes, and JSON wire formats are unchanged.

Migrating from v2.x to v3.0.0

  1. Remove builder.Services.AddQuartzDashboardHistory(); โ€” AddQuartzDashboard() now registers history automatically.
  2. Remove any UseSystemFonts option usage โ€” system fonts are now the default.
  3. Enjoy the smaller package โ€” bundled/minified assets cut package size by ~50%, no code changes required.

Middleware Placement

app.UseAuthentication();   // โ† must be BEFORE UseQuartzDashboard if using auth
app.UseAuthorization();    // โ† must be BEFORE UseQuartzDashboard if using auth

app.UseQuartzDashboard();  // โ† BEFORE MapControllers and MapFallbackToFile

app.MapControllers();
app.MapFallbackToFile("index.html"); // e.g. Blazor WASM

โš ๏ธ Blazor WASM users: placing UseQuartzDashboard() after MapFallbackToFile will cause all /quartz requests to return index.html instead of the dashboard.

API Endpoints

All endpoints under {basePath}/api/ (default: /quartz/api/).

Scheduler

Method Path Description
GET /scheduler Metadata, status, uptime, version
POST /scheduler/start Start / resume from standby
POST /scheduler/standby Put scheduler in standby

Jobs

Method Path Description
GET /jobs All jobs with triggers + schedule descriptions (?offset=0&limit=50)
POST /jobs Create a new job
GET /jobs/{group}/{name} Single job detail with JobDataMap
PUT /jobs/{group}/{name} Update job description / data map
DELETE /jobs/{group}/{name} Delete job
GET /jobs/{group}/{name}/logs Recent execution log lines for a job
POST /jobs/{group}/{name}/trigger Fire job immediately
POST /jobs/{group}/{name}/pause Pause job
POST /jobs/{group}/{name}/resume Resume job
POST /jobs/{group}/{name}/interrupt Interrupt executing job
POST /jobs/group/{group}/pause Pause all jobs in a group
POST /jobs/group/{group}/resume Resume all jobs in a group
POST /jobs/batch/pause Pause a set of jobs by key list
POST /jobs/batch/resume Resume a set of jobs by key list
POST /jobs/batch/trigger Fire a set of jobs immediately
POST /jobs/batch/delete Delete a set of jobs

Triggers

Method Path Description
GET /triggers All triggers with schedule descriptions (?offset=0&limit=50)
POST /triggers Create a new trigger (cron or simple)
GET /triggers/{group}/{name} Single trigger detail
PUT /triggers/{group}/{name} Update trigger schedule / expression
DELETE /triggers/{group}/{name} Unschedule (delete) trigger
GET /triggers/{group}/{name}/next-fires Next N fire times (?count=10, max 100)
POST /triggers/{group}/{name}/pause Pause trigger
POST /triggers/{group}/{name}/resume Resume trigger
POST /triggers/group/{group}/pause Pause all triggers in a group
POST /triggers/group/{group}/resume Resume all triggers in a group

Calendars

Method Path Description
GET /calendars All Quartz calendars
POST /calendars Create a calendar
DELETE /calendars/{name} Delete a calendar

Runtime & Diagnostics

Method Path Description
GET /executing Currently executing jobs with duration
GET /history Paginated fire events (?offset=0&limit=50&job=group.name)
GET /stats Per-minute execution buckets, rate, avg duration, P50/P95/P99
GET /stats/history Rolling history for the graph
GET /health Success rate, thread pool utilization, failure list
GET /timeline Execution timeline data (up to 500 records)
GET /heatmap Execution density grid (day-of-week ร— hour-of-day with success rates)
GET /schedulers All registered schedulers (name, instance ID, status)
GET /config Dashboard config snapshot (readonly flag, features, etc.)

Utilities

Method Path Description
POST /cron/describe Validate a CRON expression and return next 5 fire times
GET /export Export all jobs + triggers as JSON
POST /import Import jobs + triggers from export payload

SignalR Real-Time Updates

When UseSignalR = true (default), the NuGet registers its own hub automatically:

Hub endpoint: {Path}/hub  (e.g. /quartz/hub)

Do NOT call app.MapHub<QuartzDashboardHub>() yourself โ€” it is handled internally.

To verify the hub is active:

curl -X POST http://localhost:5000/quartz/hub/negotiate?negotiateVersion=1
# โ†’ 200 OK = working

History & Stats

AddQuartzDashboard() automatically registers an IJobListener that:

  • Records the last N fire events (configurable via MaxFireHistory, default 500)
  • Persists history to JSON (PersistHistoryPath) if configured, or to SQLite via AddQuartzDashboardSqliteHistory (from Dot.QuartzDashboard.Sqlite)
  • Auto-prunes records older than HistoryRetentionHours (default 24h)
  • Buckets executions per-minute into 120 rolling ExecutionBucket entries
  • Powers /api/stats, /api/stats/history, the timeline chart, and CSV/JSON export

No external storage required โ€” in-memory works out of the box. For production use, SQLite is recommended.

Testing

# Run core unit tests
dotnet test QuartzDashboard.Tests -c Release

# Run integration tests (real WebApplicationFactory with Quartz scheduler)
dotnet test QuartzDashboard.IntegrationTests -c Release

# Run all tests
dotnet test -c Release

Integration tests cover endpoint responses, auth flows, config options, SignalR hub connectivity, read-only mode, host-app coexistence, and history tracking.

Common Issues

Symptom Cause Fix
/quartz returns Blazor index.html UseQuartzDashboard() placed after MapFallbackToFile Move it before
SignalR shows amber / disconnected Hub not registered Set UseSignalR = true (default); do not call MapHub manually
401 on all dashboard requests RequireAuthentication = true but no auth middleware Add UseAuthentication() / UseAuthorization() before UseQuartzDashboard()
SQLite history does not persist App cannot write to the configured path Use a writable relative or absolute path in AddQuartzDashboardSqliteHistory(...)
History/stats stay empty after upgrade Stale history wiring Keep AddQuartzDashboard() and remove any old AddQuartzDashboardHistory() call
Uptime shows raw string like "00:01:23.456" Using an older build Upgrade to 3.0.5+ โ€” .NET TimeSpan strings are now parsed correctly
Stale UI after upgrading Cached browser assets Hard-refresh once (Ctrl+Shift+R) after upgrading

Architecture

Request โ†’ app.Use() (inline middleware, path-matched to basePath)
          โ”œโ”€โ”€ /hub/*                โ†’ pass through to SignalR endpoint routing
          โ”œโ”€โ”€ /api/*                โ†’ feature-specific handlers in Handlers/
          โ”œโ”€โ”€ /quartz               โ†’ 302 redirect โ†’ /quartz/
          โ”œโ”€โ”€ /app.min.js           โ†’ embedded esbuild JavaScript bundle
          โ”œโ”€โ”€ /app.min.css          โ†’ embedded esbuild stylesheet bundle
          โ”œโ”€โ”€ /charts.min.js        โ†’ embedded chart bundle
          โ””โ”€โ”€ anything else         โ†’ SPA fallback (embedded index.html)
  • Backend: Raw ASP.NET Core app.Use() middleware โ€” zero routing conflicts with controllers
  • Router: Declarative (Method, Pattern, Handler)[] route table in ApiRouter โ€” {} wildcard segments, O(routes) dispatch
  • Handlers: API logic split by feature into Handlers/
  • Models: Typed request/response records in Models/ (PagedResponse<T>, StatusResponse, FireRecordDto, ErrorResponse)
  • Services: History persistence and execution buckets in Services/
  • Frontend: ES modules bundled/minified with esbuild, embedded into the DLL at build time
  • Assets: Fully self-contained โ€” no external CDN or CSP allowlist required
  • Packages: Dot.QuartzDashboard (main) ยท Dot.QuartzDashboard.Abstractions (interfaces, no ASP.NET dep) ยท Dot.QuartzDashboard.Sqlite (SQLite store)
  • Target frameworks: net8.0, net9.0, net10.0
  • Dependencies: Quartz โ‰ฅ 3.18.0 < 4.0.0, Quartz.Extensions.DependencyInjection

Demo

cd QuartzDashboard.Demo

dotnet run                         # default port 5190
dotnet run -- -p 8080              # custom port
dotnet run -- --auth               # enable cookie auth (test access control)
dotnet run -- --readonly           # disable write actions
dotnet run -- --sqlite             # SQLite history (writes to demo-history.db)
dotnet run -- -p 5000 --auth --readonly

6 demo jobs with diverse schedules: HealthCheck (15s), CacheWarmup (30s), ReportGeneration (2min), DataSync (CRON :00/:30), UnstableImport (~30% fail rate), ManualNotification (durable, fire from UI).


๐Ÿค– AI Prompt

A copy-pasteable brief covering packages, options, every API endpoint, the SignalR hub, and common mistakes โ€” designed to drop into Copilot / Claude / any coding assistant. See docs/AI-PROMPT.md.

Changelog

See CHANGELOG.md for the full version history.

License

MIT โ€” use it, ship it, open-source it.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.4.0 49 6/8/2026
4.3.0 47 6/8/2026
4.2.2 95 5/27/2026
4.2.0 95 5/16/2026
4.1.0 94 5/12/2026
4.0.1 135 5/11/2026
4.0.0 89 5/11/2026
3.0.6 91 5/11/2026
3.0.5 96 5/11/2026
3.0.4 98 5/10/2026
3.0.3 111 5/10/2026
3.0.2 92 5/10/2026
2.4.5 110 5/9/2026
2.4.1 97 5/9/2026
2.3.2 99 5/9/2026
2.3.1 94 5/9/2026
2.3.0 95 5/9/2026
2.2.0 98 5/9/2026
2.1.47 97 5/9/2026
2.1.46 97 5/9/2026
Loading failed

v4.4.0 โ€” Observability, rate limiting, health checks.
โ€ข Tracing: OpenTelemetry-compatible Activity spans propagate through listener โ†’ event bus โ†’ SignalR bridge for end-to-end job execution visibility.
โ€ข Rate limiting: configurable per-minute request cap on mutating endpoints (pause/resume/trigger/delete), default on.
โ€ข Health check: dashboard status exposed as IHealthCheck so host apps can include it in their own /healthz endpoint.
โ€ข Cleanup: *.png screenshots excluded from repository via .gitignore.