Dot.QuartzDashboard
4.2.0
dotnet add package Dot.QuartzDashboard --version 4.2.0
NuGet\Install-Package Dot.QuartzDashboard -Version 4.2.0
<PackageReference Include="Dot.QuartzDashboard" Version="4.2.0" />
<PackageVersion Include="Dot.QuartzDashboard" Version="4.2.0" />
<PackageReference Include="Dot.QuartzDashboard" />
paket add Dot.QuartzDashboard --version 4.2.0
#r "nuget: Dot.QuartzDashboard, 4.2.0"
#:package Dot.QuartzDashboard@4.2.0
#addin nuget:?package=Dot.QuartzDashboard&version=4.2.0
#tool nuget:?package=Dot.QuartzDashboard&version=4.2.0
Dot.QuartzDashboard
<p align="center"> <img src="https://raw.githubusercontent.com/nathan5580/QuartzDashboard/main/assets/logo.svg" width="200" alt="Dot.QuartzDashboard"> </p>
A beautiful, self-contained Quartz.NET scheduler dashboard โ drop it into any ASP.NET Core app with two lines of code.
Contents
- What's New in v4.1.0
- What's New in v4.0.0
- What it does
- Quick Start
- Dashboard Pages
- Configuration
- Migrating from v3.x to v4.0
- Middleware Placement
- API Endpoints
- SignalR Real-Time Updates
- History & Stats
- Testing
- Common Issues
- Architecture
- Demo
- ๐ค AI Prompt
- Changelog
- License
What's New in v4.1.0
UX polish + anti-flicker refresh on top of v4.0:
- No more flicker on refresh. Job/trigger/history lists update in place via
mergeArrayInPlace, so Alpinex-forreuses DOM nodes. Scroll position, open drawers, and expanded rows survive auto-refreshes. - Silent background refresh. Auto-refresh and SignalR fan-out skip loading spinners and error toasts; visible refresh actions stay loud.
- Row density toggle (comfortable / compact) in Settings, persisted to localStorage.
- Desktop notifications for job failures โ opt-in browser permission flow in Settings.
- Per-job sparkline column on the Jobs page, showing recent duration trend (visible from
xlโฅ 1280px). - "In-memory only" banner on the History page when no persistent store is registered. Dismissible.
- Triggers group header โ context-aware Pause/Resume buttons and a
N/M pausedcounter. - One-click Copy key and Retrigger buttons on jobs / triggers / history rows.
Plus targeted fixes: health nav badge no longer floats at the far-right of the sidebar, sparkline column actually renders (added missing Tailwind xl: utilities), timeline tooltip no longer flashes 01:00:00 AM on cursor-init, graph "Current Rate" is correctly labeled /min, history trigger column gets more width, executing empty state uses an SVG icon instead of an emoji.
No API or wire-format changes. Drop-in upgrade from v4.0.x.
What's New in v4.0.0
The package now ships as three NuGets, each with a focused dependency footprint:
| Package | Contents | Depends on |
|---|---|---|
Dot.QuartzDashboard |
Middleware, handlers, SPA, in-memory + JSON history stores | ASP.NET Core, Quartz |
Dot.QuartzDashboard.Abstractions |
IFireHistoryStore + FireRecord only |
โ |
Dot.QuartzDashboard.Sqlite |
SQLite-backed IFireHistoryStore + DI extension |
Abstractions, Microsoft.Data.Sqlite |
Breaking changes
IFireHistoryStore/FireRecordmoved namespace:QuartzDashboard.InternalโQuartzDashboard.Abstractions. Updateusingstatements in any custom store.QuartzDashboardOptions.PersistHistoryToSqliteremoved. Use the newservices.AddQuartzDashboardSqliteHistory(...)extension method fromDot.QuartzDashboard.Sqlite.
Internal improvements
- API routing replaced with a declarative route table (
ApiRouter) โ adding endpoints is a one-line entry instead of an if/else branch. IQuartzDashboardOptionsread-only interface exposed so handlers can opt out of mutating options at runtime.- Common response shapes (
PagedResponse<T>,StatusResponse,ErrorResponse,FireRecordDto) now have typed records inQuartzDashboard.Models. Wire format unchanged.
What's New in v3.0.5 / v3.0.6
UI & UX overhaul
Overview
- Stat cards are now clickable โ each navigates to its target page (Jobs, Triggers, Executing, History)
- 2-column stat grid on mobile (was a single stacked column)
- Pin affordance hint guides new users to the pinning feature before they discover it on their own
Jobs
- Last Run column shows relative time ("3m ago") with absolute date on hover
- Actions dropdown gains a View history entry โ navigates to History pre-filtered for that job
- Mobile search toggle: a ๐ button reveals/hides the search input on small screens
Triggers
- Removed redundant
(4s)countdown parenthetical โ the "Next Fire" value already shows "in 4s" - Action buttons wrap instead of clipping on narrow viewports
History
- Date-range quick filters โ 1h / 6h / 24h / All toggle buttons sit beside the status filter
- Failed rows show an inline error snippet so you can spot failures without opening the detail modal
- CSV export button uses the correct dark-theme styling
- History search and date filter automatically reset when navigating away from the page
Health
- Success Rate card now shows "based on N loaded records" so the denominator is never ambiguous
Executing
- Richer empty state explains the live-connection model with an animated green dot
Command palette (โK)
- Job actions renamed from "Trigger job X.Y" to "Run now: X.Y" โ more intuitive
- Keyword aliases โ searching "run", "fire", "execute", or "trigger" now finds job actions
- History results deduplicated by job key (was one entry per history record)
Mobile
- Bottom tab bar now includes Triggers and Executing (7 items, horizontally scrollable)
Formatting
.NET TimeSpanstrings (00:00:16.594) parsed correctly in the uptime display
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/Oto 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).
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
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.
RecordFireis 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.OnFireRecordedis optional โ fire it after the write succeeds; the dashboard uses it for SignalR real-time fan-out.Countis read on the/api/healthendpoint; expensiveCOUNT(*)s on huge tables should be approximated (e.g., a cached value updated byRecordFire).
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:
RequireAuthentication(defaulttruesince v4.2) โ unauthenticated requests โ 401RequiredPolicyโ usesIAuthorizationService(named policy) โ 403 on failureAllowedRolesโ 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";
});
Migrating from v3.x to v4.0
Update
usingstatements for custom history stores.IFireHistoryStoreandFireRecordmoved namespace:- using QuartzDashboard.Internal; + using QuartzDashboard.Abstractions;Or add
<PackageReference Include="Dot.QuartzDashboard.Abstractions" />if you only need the interface.Replace
options.PersistHistoryToSqlitewith 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 mainDot.QuartzDashboardpackage no longer shipsMicrosoft.Data.Sqlite.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
- Remove
builder.Services.AddQuartzDashboardHistory();โAddQuartzDashboard()now registers history automatically. - Remove any
UseSystemFontsoption usage โ system fonts are now the default. - 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()afterMapFallbackToFilewill cause all/quartzrequests to returnindex.htmlinstead 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 viaAddQuartzDashboardSqliteHistory(fromDot.QuartzDashboard.Sqlite) - Auto-prunes records older than
HistoryRetentionHours(default 24h) - Buckets executions per-minute into 120 rolling
ExecutionBucketentries - 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 inApiRouterโ{}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
Copy this into any Copilot / AI assistant for instant, complete knowledge of the package.
You are integrating Dot.QuartzDashboard (NuGet) into an ASP.NET Core app.
PACKAGES (v4 โ split into three):
Dot.QuartzDashboard โ middleware + handlers + SPA + in-memory/JSON history
Dot.QuartzDashboard.Abstractions โ IFireHistoryStore + FireRecord (no ASP.NET deps)
Dot.QuartzDashboard.Sqlite โ SqliteFireHistoryStore + AddQuartzDashboardSqliteHistory()
CURRENT VERSION: 4.0.0
TARGETS: net8.0, net9.0, net10.0
NAMESPACES:
QuartzDashboard โ middleware, options, hub
QuartzDashboard.Abstractions โ interfaces + records
QuartzDashboard.Sqlite โ SQLite store + DI extension
QuartzDashboard.Models โ request + response DTOs
DASHBOARD URL: /quartz (or options.Path)
--- MINIMUM SETUP (2 lines) ---
// 1. In DI (Program.cs or ServiceExtensions):
builder.Services.AddQuartzDashboard();
// 2. In middleware pipeline (before MapControllers and MapFallbackToFile):
app.UseQuartzDashboard();
That's it. Open /quartz in the browser.
--- FULL OPTIONS ---
builder.Services.AddQuartzDashboard(options =>
{
options.Path = "/quartz"; // dashboard route prefix (default: "/quartz")
options.Enabled = true; // false = UseQuartzDashboard() is a no-op
options.ReadOnly = false; // disable trigger/start/stop/delete actions
options.UseSignalR = true; // real-time push updates via SignalR
// Auth (checked in order: auth โ policy โ roles)
options.RequireAuthentication = false; // require authenticated user (401 if not)
options.RequiredPolicy = ""; // named IAuthorizationService policy (403 if fails)
options.AllowedRoles = []; // role whitelist โ checked if no policy set (403 if fails)
// History limits
options.MaxFireHistory = 500; // max fire records in memory (default: 500)
options.MaxExecutionLogsPerJob = 50; // max log lines per job
options.HistoryRetentionHours = 24; // auto-prune records older than this (0 = keep all)
options.Title = "My App Dashboard"; // custom title in sidebar + browser tab
// Persistence (survive restarts)
// NOTE: For SQLite persistence, do NOT set options.PersistHistoryToSqlite (removed in v4).
// Instead, reference Dot.QuartzDashboard.Sqlite and call:
// builder.Services.AddQuartzDashboardSqliteHistory("quartz-history.db");
options.PersistHistoryPath = "quartz-history.json"; // optional JSON fallback when SQLite is not used
// Callbacks
options.OnJobFailed = async (jobKey, ex) => { /* Slack/PagerDuty alert */ };
options.WebhookUrl = "https://hooks.slack.com/..."; // POST JSON on job failure
});
--- APPSETTINGS BINDING (bind a config section directly) ---
// In appsettings.json:
{
"QuartzDashboard": {
"Enabled": true,
"Path": "/quartz",
"ReadOnly": false,
"UseSignalR": true,
"RequireAuthentication": false,
"RequiredPolicy": "",
"AllowedRoles": [],
"MaxFireHistory": 500,
"MaxExecutionLogsPerJob": 50
}
}
// In code:
builder.Services.AddQuartzDashboard(options =>
builder.Configuration.GetSection("QuartzDashboard").Bind(options));
--- ENVIRONMENT GATING ---
builder.Services.AddQuartzDashboard(options =>
{
options.Enabled = !builder.Environment.IsProduction();
});
--- MIDDLEWARE ORDER RULES ---
- UseQuartzDashboard() must come BEFORE app.MapControllers() and app.MapFallbackToFile()
- If using auth, app.UseAuthentication() and app.UseAuthorization() must come BEFORE UseQuartzDashboard()
- UseSignalR = true makes the NuGet register its own SignalR hub โ do NOT manually call app.MapHub<QuartzDashboardHub>()
- The NuGet handles /quartz โ /quartz/ redirect automatically
--- API ENDPOINTS (all under {Path}/api/) ---
GET /scheduler - scheduler metadata, status, uptime
POST /scheduler/start - start or resume from standby
POST /scheduler/standby - put scheduler in standby
GET /schedulers - all registered schedulers
GET /jobs - all jobs with triggers (?offset=0&limit=50)
POST /jobs - create a 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
POST /jobs/{group}/{name}/trigger - fire 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 group
POST /jobs/group/{group}/resume - resume all jobs in group
POST /jobs/batch/pause - pause a set of jobs
POST /jobs/batch/resume - resume a set of jobs
POST /jobs/batch/trigger - fire a set of jobs
POST /jobs/batch/delete - delete a set of jobs
GET /triggers - all triggers (?offset=0&limit=50)
POST /triggers - create a trigger (cron or simple)
GET /triggers/{group}/{name} - single trigger detail
PUT /triggers/{group}/{name} - update trigger schedule
DELETE /triggers/{group}/{name} - unschedule trigger
GET /triggers/{group}/{name}/next-fires - next N fire times (?count=10)
POST /triggers/{group}/{name}/pause - pause trigger
POST /triggers/{group}/{name}/resume - resume trigger
POST /triggers/group/{group}/pause - pause all triggers in group
POST /triggers/group/{group}/resume - resume all triggers in group
GET /calendars - all calendars
POST /calendars - create a calendar
DELETE /calendars/{name} - delete a calendar
GET /executing - currently running jobs with duration
GET /history - paginated fire events (?offset=0&limit=50&job=group.name)
GET /stats - per-minute buckets + rates + P50/P95/P99
GET /stats/history - rolling history for the graph
GET /health - success rate, thread pool, failure list
GET /timeline - execution timeline (up to 500 records)
GET /heatmap - execution density grid (day ร hour)
GET /config - dashboard config snapshot
POST /cron/describe - validate CRON + return next 5 fire times
GET /export - export all jobs+triggers as JSON
POST /import - import jobs+triggers from export payload
--- SIGNALR HUB ---
Hub class: QuartzDashboard.QuartzDashboardHub
Default endpoint: {Path}/hub (e.g. /quartz/hub)
Registered automatically when UseSignalR = true โ no manual MapHub needed.
POST {Path}/hub/negotiate?negotiateVersion=1 โ 200 when working
--- UI FEATURES (v3.0.6) ---
- Clickable stat cards on Overview โ navigate to Jobs / Triggers / Executing / History
- History date-range filters: 1h / 6h / 24h / All
- History failed rows show inline error snippet
- Jobs "View history" action pre-filters History for that job
- Command palette (โK): "Run now: X.Y" label, keyword aliases (run/fire/trigger/execute)
- Mobile bottom nav covers all 10 pages (scrollable)
- Jobs search toggle button on mobile
- Success rate card shows record-count context
--- COMMON MISTAKES ---
- Do NOT call app.MapHub<QuartzDashboardHub>() yourself when UseSignalR = true
- Do NOT place UseQuartzDashboard() after MapFallbackToFile โ Blazor WASM will swallow all /quartz routes
- Visiting /quartz (no trailing slash) works โ NuGet redirects to /quartz/ automatically
- Dashboard assets are embedded in the package โ no external CDN allowlist is required
Changelog
See CHANGELOG.md for the full version history.
v4.1.0 (2026-05-12)
- In-place refresh (no flicker) โ
mergeArrayInPlacemutates job/trigger/history arrays in place so Alpinex-forreuses DOM nodes; scroll position, open drawers, and expanded rows survive auto-refreshes - Silent background refresh โ auto-refresh and SignalR fan-out skip loading spinners and error toasts
- Row density toggle (comfortable / compact), persisted to localStorage
- Desktop notifications for job failures (opt-in)
- Per-job sparkline column on Jobs page (visible from
xlโฅ 1280px) - "In-memory only" banner on History when no persistent store is registered
- Triggers group header โ context-aware Pause/Resume + paused counter
- Fixed: health nav badge position, sparkline column never rendering (Tailwind pre-build gap), timeline tooltip epoch flash, graph CURRENT RATE unit mislabel (
/sโ/min), history trigger truncation, executing empty-state emoji โ SVG icon
v4.0.0 (2026-05-11)
- Breaking:
IFireHistoryStore/FireRecordmoved toQuartzDashboard.Abstractionsnamespace (Dot.QuartzDashboard.Abstractionspackage) - Breaking:
QuartzDashboardOptions.PersistHistoryToSqliteremoved โ useAddQuartzDashboardSqliteHistory()fromDot.QuartzDashboard.Sqlite - New
Dot.QuartzDashboard.AbstractionsandDot.QuartzDashboard.Sqlitepackages; main package no longer depends onMicrosoft.Data.Sqlite - Declarative
ApiRouterreplaces 250-line if/else dispatcher - Typed
PagedResponse<T>,StatusResponse,FireRecordDto,ErrorResponserecords (wire format unchanged) IQuartzDashboardOptionsread-only interface registered in DI- Fixed
ExecutionBucketServiceprune arithmetic (unix-epoch minutes) - SQLite: WAL mode, throttled TTL prune,
job_keyindex FileFireHistoryStore: debounced writes (1s), synchronous flush onDispose- Options validated at
AddQuartzDashboard()registration time
v3.0.6 / v3.0.5 (2026-05-11)
See CHANGELOG.md for the full UI/UX overhaul details.
v3.0.0 โ v3.0.4
See CHANGELOG.md.
License
MIT โ use it, ship it, open-source it.
| Product | Versions 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. |
-
net10.0
- Dot.QuartzDashboard.Abstractions (>= 4.2.0)
- Quartz (>= 3.18.0 && < 4.0.0)
- Quartz.Extensions.DependencyInjection (>= 3.18.0 && < 4.0.0)
-
net8.0
- Dot.QuartzDashboard.Abstractions (>= 4.2.0)
- Quartz (>= 3.18.0 && < 4.0.0)
- Quartz.Extensions.DependencyInjection (>= 3.18.0 && < 4.0.0)
-
net9.0
- Dot.QuartzDashboard.Abstractions (>= 4.2.0)
- Quartz (>= 3.18.0 && < 4.0.0)
- Quartz.Extensions.DependencyInjection (>= 3.18.0 && < 4.0.0)
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.2.0 | 87 | 5/16/2026 |
| 4.1.0 | 90 | 5/12/2026 |
| 4.0.1 | 111 | 5/11/2026 |
| 4.0.0 | 86 | 5/11/2026 |
| 3.0.6 | 88 | 5/11/2026 |
| 3.0.5 | 93 | 5/11/2026 |
| 3.0.4 | 95 | 5/10/2026 |
| 3.0.3 | 105 | 5/10/2026 |
| 3.0.2 | 89 | 5/10/2026 |
| 2.4.5 | 106 | 5/9/2026 |
| 2.4.1 | 94 | 5/9/2026 |
| 2.3.2 | 96 | 5/9/2026 |
| 2.3.1 | 91 | 5/9/2026 |
| 2.3.0 | 92 | 5/9/2026 |
| 2.2.0 | 95 | 5/9/2026 |
| 2.1.47 | 94 | 5/9/2026 |
| 2.1.46 | 94 | 5/9/2026 |
| 2.1.45 | 90 | 5/9/2026 |
| 2.1.44 | 91 | 5/9/2026 |
| 2.1.43 | 98 | 5/9/2026 |
v4.2.0 โ Security hardening + perf + a11y polish.
โข Safer defaults: RequireAuthentication now defaults to true; new RequireCsrfHeader option (default true) blocks cross-site triggering of jobs.
โข Defence in depth: X-Content-Type-Options, X-Frame-Options, Referrer-Policy on dashboard responses.
โข Perf: GetAllJobs / GetAllTriggers batch trigger-state lookups in parallel (was N round-trips per request).
โข Reliability: SignalR bridge now unsubscribes event handlers in StopAsync, preventing handler accumulation across host recycles.
โข A11y: prefers-reduced-motion respected on all animations; toast queue announced via aria-live="polite".
โข Cleanup: FireRecord properties are init-only; DashboardEventBus internals no longer leak as public; polling fallback timer cleared on pagehide/beforeunload.
โข ExportImport: jobs imported as PlaceholderJob now reported back to the caller instead of failing silently.
See CHANGELOG.md for the full list.