@luisrodrigues/nestjs-scheduler-dashboard 0.0.6 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -57
- package/dist/index.d.ts +2 -4
- package/dist/index.js +3 -36
- package/dist/jobs.service.js +24 -2
- package/dist/public/assets/index-Boe9HKvT.js +234 -0
- package/{ui → dist/public}/index.html +2 -2
- package/dist/public/public/assets/index-BMy-QxOf.css +1 -0
- package/dist/public/public/assets/index-BQiKk0pa.js +235 -0
- package/dist/public/public/assets/index-BVFtTwJJ.css +1 -0
- package/dist/public/public/assets/index-Boe9HKvT.js +234 -0
- package/dist/public/public/assets/index-DmY-xViB.js +234 -0
- package/dist/public/public/assets/index-cxgVvG8b.js +234 -0
- package/dist/public/public/assets/index-vJzJcDsD.css +1 -0
- package/dist/public/public/index.html +21 -0
- package/dist/scheduler-dash-root.module.d.ts +21 -0
- package/dist/scheduler-dash-root.module.js +191 -0
- package/dist/scheduler-dash.constants.d.ts +2 -0
- package/dist/scheduler-dash.constants.js +5 -0
- package/dist/scheduler-dash.module.d.ts +15 -0
- package/dist/scheduler-dash.module.js +137 -0
- package/dist/scheduler-dash.options.d.ts +7 -1
- package/dist/scheduler-dash.schema.d.ts +1 -1
- package/dist/scheduler-dash.schema.js +3 -5
- package/package.json +10 -7
- package/ui/assets/index-6LGPlcL_.js +0 -234
- /package/{ui → dist/public}/assets/index-BVFtTwJJ.css +0 -0
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# @luisrodrigues/nestjs-scheduler-dashboard
|
|
2
2
|
|
|
3
|
-
A plug-and-play dashboard for [`@nestjs/schedule`](https://docs.nestjs.com/techniques/task-scheduling). Visualize cron job executions, track history and metrics, trigger jobs manually, and stop running executions — all from an embedded UI
|
|
4
|
-
|
|
5
|
-
The dashboard runs on its own dedicated port (default **3636**), completely isolated from your main application.
|
|
3
|
+
A plug-and-play dashboard for [`@nestjs/schedule`](https://docs.nestjs.com/techniques/task-scheduling). Visualize cron job executions, track history and metrics, trigger jobs manually, and stop running executions — all from an embedded UI served on your existing application port.
|
|
6
4
|
|
|
7
5
|
---
|
|
8
6
|
|
|
9
7
|
## Features
|
|
10
8
|
|
|
9
|
+
- Mounted directly on your app — no separate port or process
|
|
11
10
|
- Execution history per job with status, duration, and error details
|
|
12
11
|
- Persistent metrics: total runs, failed runs, average duration — independent of history retention
|
|
13
12
|
- Manual job triggering and execution stop from the UI
|
|
@@ -15,7 +14,6 @@ The dashboard runs on its own dedicated port (default **3636**), completely isol
|
|
|
15
14
|
- No-overlap mode: skip a job if it is already running
|
|
16
15
|
- Optional HTTP Basic Auth to protect the dashboard
|
|
17
16
|
- Light / dark mode
|
|
18
|
-
- Zero external runtime dependencies — served from a single self-contained HTML file
|
|
19
17
|
|
|
20
18
|
---
|
|
21
19
|
|
|
@@ -30,37 +28,34 @@ pnpm add @luisrodrigues/nestjs-scheduler-dashboard
|
|
|
30
28
|
**Peer dependencies** (install if not already present):
|
|
31
29
|
|
|
32
30
|
```bash
|
|
33
|
-
npm install @nestjs/common @nestjs/core @nestjs/schedule
|
|
31
|
+
npm install @nestjs/common @nestjs/core @nestjs/schedule express
|
|
34
32
|
```
|
|
35
33
|
|
|
36
34
|
---
|
|
37
35
|
|
|
38
36
|
## Quick start
|
|
39
37
|
|
|
40
|
-
### 1.
|
|
38
|
+
### 1. Import `SchedulerDashModule` in your `AppModule`
|
|
41
39
|
|
|
42
40
|
```ts
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log('App running at http://localhost:3000');
|
|
55
|
-
console.log('Dashboard at http://localhost:3636');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
bootstrap();
|
|
41
|
+
import { Module } from '@nestjs/common';
|
|
42
|
+
import { ScheduleModule } from '@nestjs/schedule';
|
|
43
|
+
import { SchedulerDashModule } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
44
|
+
|
|
45
|
+
@Module({
|
|
46
|
+
imports: [
|
|
47
|
+
ScheduleModule.forRoot(),
|
|
48
|
+
SchedulerDashModule.forRoot({ route: '_scheduler' }),
|
|
49
|
+
],
|
|
50
|
+
})
|
|
51
|
+
export class AppModule {}
|
|
59
52
|
```
|
|
60
53
|
|
|
61
|
-
### 2.
|
|
54
|
+
### 2. Replace `@Cron` with `@TrackJob` on every job
|
|
55
|
+
|
|
56
|
+
`@TrackJob` is a wrapper around NestJS's `@Cron` decorator. It registers the cron schedule exactly as `@Cron` does, and additionally records every execution (start time, duration, status, errors) so the dashboard can display them.
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
> **Jobs decorated with `@Cron` instead of `@TrackJob` will still run on schedule but will not appear in the dashboard history.**
|
|
64
59
|
|
|
65
60
|
```ts
|
|
66
61
|
import { Injectable } from '@nestjs/common';
|
|
@@ -69,29 +64,39 @@ import { TrackJob } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
|
69
64
|
|
|
70
65
|
@Injectable()
|
|
71
66
|
export class ReportJob {
|
|
67
|
+
// Before: @Cron(CronExpression.EVERY_HOUR)
|
|
72
68
|
@TrackJob(CronExpression.EVERY_HOUR, { name: 'generate-report' })
|
|
73
69
|
async run() {
|
|
74
|
-
// your job logic
|
|
70
|
+
// your job logic — no other changes needed
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
```
|
|
78
74
|
|
|
79
|
-
That's it.
|
|
75
|
+
That's it. Start your app and open `http://localhost:3000/_scheduler`.
|
|
80
76
|
|
|
81
77
|
---
|
|
82
78
|
|
|
83
79
|
## Configuration
|
|
84
80
|
|
|
85
|
-
`
|
|
81
|
+
`SchedulerDashModule.forRoot(options?)` accepts:
|
|
86
82
|
|
|
87
83
|
| Option | Type | Default | Description |
|
|
88
84
|
|---|---|---|---|
|
|
89
|
-
| `
|
|
85
|
+
| `route` | `string` | `'_scheduler'` | URL path where the dashboard is mounted |
|
|
90
86
|
| `storage` | `Storage` | `new MemoryStorage()` | Storage backend for execution history and metrics |
|
|
91
87
|
| `maxConcurrent` | `number` | — | Maximum number of jobs that can run simultaneously. Excess jobs are queued |
|
|
92
88
|
| `noOverlap` | `boolean` | `false` | Globally prevent a job from starting if it is already running |
|
|
93
89
|
| `auth` | `{ username, password }` | — | Protect the dashboard with HTTP Basic Auth |
|
|
94
90
|
|
|
91
|
+
### `route`
|
|
92
|
+
|
|
93
|
+
The URL path where the dashboard is served, relative to your app's root.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
SchedulerDashModule.forRoot({ route: 'admin/scheduler' })
|
|
97
|
+
// dashboard → http://localhost:3000/admin/scheduler
|
|
98
|
+
```
|
|
99
|
+
|
|
95
100
|
### `storage`
|
|
96
101
|
|
|
97
102
|
The default `MemoryStorage` keeps everything in-process. You can limit how many history entries are kept per job:
|
|
@@ -99,11 +104,11 @@ The default `MemoryStorage` keeps everything in-process. You can limit how many
|
|
|
99
104
|
```ts
|
|
100
105
|
import { MemoryStorage } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
SchedulerDashModule.forRoot({
|
|
103
108
|
storage: new MemoryStorage({
|
|
104
109
|
historyRetention: 50, // keep the last 50 executions per job (default: unlimited)
|
|
105
110
|
}),
|
|
106
|
-
})
|
|
111
|
+
})
|
|
107
112
|
```
|
|
108
113
|
|
|
109
114
|
> **Metrics are independent of `historyRetention`.** Even after old history entries are trimmed, the counters for total runs, failed runs, and average duration keep accumulating.
|
|
@@ -132,9 +137,7 @@ export class RedisStorage extends Storage {
|
|
|
132
137
|
Limits how many `@TrackJob` jobs can run simultaneously across the entire application. Jobs that exceed the limit are saved to storage with status `"queued"` and run in FIFO order as slots free up.
|
|
133
138
|
|
|
134
139
|
```ts
|
|
135
|
-
|
|
136
|
-
maxConcurrent: 5,
|
|
137
|
-
});
|
|
140
|
+
SchedulerDashModule.forRoot({ maxConcurrent: 5 })
|
|
138
141
|
```
|
|
139
142
|
|
|
140
143
|
### `noOverlap`
|
|
@@ -142,12 +145,10 @@ await setupSchedulerDash(app, {
|
|
|
142
145
|
Prevents a job from firing again if it is still running. Applies globally to all `@TrackJob` methods.
|
|
143
146
|
|
|
144
147
|
```ts
|
|
145
|
-
|
|
146
|
-
noOverlap: true,
|
|
147
|
-
});
|
|
148
|
+
SchedulerDashModule.forRoot({ noOverlap: true })
|
|
148
149
|
```
|
|
149
150
|
|
|
150
|
-
Can also be overridden per job
|
|
151
|
+
Can also be overridden per job via the decorator:
|
|
151
152
|
|
|
152
153
|
```ts
|
|
153
154
|
@TrackJob(CronExpression.EVERY_MINUTE, { name: 'sync', noOverlap: true })
|
|
@@ -157,19 +158,21 @@ async sync() { /* ... */ }
|
|
|
157
158
|
### `auth`
|
|
158
159
|
|
|
159
160
|
```ts
|
|
160
|
-
|
|
161
|
+
SchedulerDashModule.forRoot({
|
|
161
162
|
auth: {
|
|
162
163
|
username: process.env.DASH_USER ?? 'admin',
|
|
163
164
|
password: process.env.DASH_PASS ?? 'secret',
|
|
164
165
|
},
|
|
165
|
-
})
|
|
166
|
+
})
|
|
166
167
|
```
|
|
167
168
|
|
|
168
169
|
---
|
|
169
170
|
|
|
170
171
|
## `@TrackJob` decorator
|
|
171
172
|
|
|
172
|
-
|
|
173
|
+
`@TrackJob` is a **wrapper around `@Cron`** from `@nestjs/schedule`. It accepts every argument that `@Cron` accepts and passes them through unchanged, so migrating an existing job is a one-line change. The only addition is the `noOverlap` option and the automatic execution tracking.
|
|
174
|
+
|
|
175
|
+
**Every scheduled job must use `@TrackJob` instead of `@Cron`.** Jobs that remain on `@Cron` will run normally but their executions will not be recorded or visible in the dashboard.
|
|
173
176
|
|
|
174
177
|
```ts
|
|
175
178
|
@TrackJob(cronTime, options?)
|
|
@@ -197,38 +200,54 @@ async cleanup() {
|
|
|
197
200
|
|
|
198
201
|
## API
|
|
199
202
|
|
|
200
|
-
The dashboard exposes a small REST API
|
|
203
|
+
The dashboard exposes a small REST API under the configured route.
|
|
201
204
|
|
|
202
205
|
| Method | Path | Description |
|
|
203
206
|
|---|---|---|
|
|
204
|
-
| `GET` |
|
|
205
|
-
| `
|
|
206
|
-
| `POST` |
|
|
207
|
+
| `GET` | `/<route>/api` | Returns all jobs with history and metrics |
|
|
208
|
+
| `GET` | `/<route>/api/:name` | Returns a single job with history and metrics |
|
|
209
|
+
| `POST` | `/<route>/api/:name/trigger` | Manually trigger a cron job by name |
|
|
210
|
+
| `POST` | `/<route>/api/executions/:id/stop` | Stop a running or queued execution by ID |
|
|
207
211
|
|
|
208
212
|
---
|
|
209
213
|
|
|
210
214
|
## Full example
|
|
211
215
|
|
|
212
216
|
```ts
|
|
217
|
+
// app.module.ts
|
|
218
|
+
import { Module } from '@nestjs/common';
|
|
219
|
+
import { ScheduleModule } from '@nestjs/schedule';
|
|
220
|
+
import { SchedulerDashModule, MemoryStorage } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
221
|
+
import { ReportJob } from './jobs/report.job';
|
|
222
|
+
|
|
223
|
+
@Module({
|
|
224
|
+
imports: [
|
|
225
|
+
ScheduleModule.forRoot(),
|
|
226
|
+
SchedulerDashModule.forRoot({
|
|
227
|
+
route: '_scheduler',
|
|
228
|
+
storage: new MemoryStorage({ historyRetention: 100 }),
|
|
229
|
+
maxConcurrent: 3,
|
|
230
|
+
noOverlap: true,
|
|
231
|
+
auth: {
|
|
232
|
+
username: process.env.DASH_USER ?? 'admin',
|
|
233
|
+
password: process.env.DASH_PASS ?? 'secret',
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
],
|
|
237
|
+
providers: [ReportJob],
|
|
238
|
+
})
|
|
239
|
+
export class AppModule {}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// main.ts
|
|
213
244
|
import { NestFactory } from '@nestjs/core';
|
|
214
245
|
import { AppModule } from './app.module';
|
|
215
|
-
import { setupSchedulerDash, MemoryStorage } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
216
246
|
|
|
217
247
|
async function bootstrap() {
|
|
218
248
|
const app = await NestFactory.create(AppModule);
|
|
219
|
-
|
|
220
|
-
await setupSchedulerDash(app, {
|
|
221
|
-
port: 3636,
|
|
222
|
-
storage: new MemoryStorage({ historyRetention: 100 }),
|
|
223
|
-
maxConcurrent: 3,
|
|
224
|
-
noOverlap: true,
|
|
225
|
-
auth: {
|
|
226
|
-
username: process.env.DASH_USER ?? 'admin',
|
|
227
|
-
password: process.env.DASH_PASS ?? 'secret',
|
|
228
|
-
},
|
|
229
|
-
});
|
|
230
|
-
|
|
231
249
|
await app.listen(3000);
|
|
250
|
+
// Dashboard at http://localhost:3000/_scheduler
|
|
232
251
|
}
|
|
233
252
|
|
|
234
253
|
bootstrap();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import { SchedulerDashOptions } from './scheduler-dash.options';
|
|
1
|
+
export { SchedulerDashModule } from './scheduler-dash.module';
|
|
3
2
|
export { TrackJob } from './decorators/track-job.decorator';
|
|
4
3
|
export { Storage } from './storage/storage.abstract';
|
|
5
4
|
export type { IStorageOptions } from './storage/storage.abstract';
|
|
6
5
|
export { MemoryStorage } from './storage/memory.storage';
|
|
7
6
|
export type { JobExecution } from './storage/job-execution.interface';
|
|
8
7
|
export type { JobMetrics } from './storage/job-metrics.interface';
|
|
9
|
-
export type { SchedulerDashOptions, SchedulerDashAuth } from './scheduler-dash.options';
|
|
10
|
-
export declare function setupSchedulerDash(app: INestApplication, options?: SchedulerDashOptions): Promise<void>;
|
|
8
|
+
export type { SchedulerDashOptions, SchedulerDashAuth, SchedulerDashAsyncOptions } from './scheduler-dash.options';
|
package/dist/index.js
CHANGED
|
@@ -1,44 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
-
};
|
|
8
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.MemoryStorage = exports.Storage = exports.TrackJob = void 0;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const common_1 = require("@nestjs/common");
|
|
13
|
-
const schedule_1 = require("@nestjs/schedule");
|
|
14
|
-
const scheduler_dash_context_1 = require("./scheduler-dash.context");
|
|
15
|
-
const scheduler_dash_schema_1 = require("./scheduler-dash.schema");
|
|
16
|
-
const dashboard_module_1 = require("./dashboard.module");
|
|
17
|
-
const jobs_service_1 = require("./jobs.service");
|
|
18
|
-
const standalone_server_1 = require("./standalone-server");
|
|
3
|
+
exports.MemoryStorage = exports.Storage = exports.TrackJob = exports.SchedulerDashModule = void 0;
|
|
4
|
+
var scheduler_dash_module_1 = require("./scheduler-dash.module");
|
|
5
|
+
Object.defineProperty(exports, "SchedulerDashModule", { enumerable: true, get: function () { return scheduler_dash_module_1.SchedulerDashModule; } });
|
|
19
6
|
var track_job_decorator_1 = require("./decorators/track-job.decorator");
|
|
20
7
|
Object.defineProperty(exports, "TrackJob", { enumerable: true, get: function () { return track_job_decorator_1.TrackJob; } });
|
|
21
8
|
var storage_abstract_1 = require("./storage/storage.abstract");
|
|
22
9
|
Object.defineProperty(exports, "Storage", { enumerable: true, get: function () { return storage_abstract_1.Storage; } });
|
|
23
10
|
var memory_storage_1 = require("./storage/memory.storage");
|
|
24
11
|
Object.defineProperty(exports, "MemoryStorage", { enumerable: true, get: function () { return memory_storage_1.MemoryStorage; } });
|
|
25
|
-
const DEFAULT_PORT = 3636;
|
|
26
|
-
async function setupSchedulerDash(app, options = {}) {
|
|
27
|
-
const parsed = scheduler_dash_schema_1.SchedulerDashOptionsSchema.safeParse(options);
|
|
28
|
-
if (!parsed.success) {
|
|
29
|
-
throw new Error(`[SchedulerDash] Invalid options:\n${parsed.error.issues.map(i => ` • ${i.path.join('.')}: ${i.message}`).join('\n')}`);
|
|
30
|
-
}
|
|
31
|
-
const port = options.port ?? DEFAULT_PORT;
|
|
32
|
-
const logger = new common_1.Logger('SchedulerDashboard', { timestamp: true });
|
|
33
|
-
let _InternalDashboardApp = class _InternalDashboardApp {
|
|
34
|
-
};
|
|
35
|
-
_InternalDashboardApp = __decorate([
|
|
36
|
-
(0, common_1.Module)({ imports: [dashboard_module_1.DashboardModule.forRoot(options)] })
|
|
37
|
-
], _InternalDashboardApp);
|
|
38
|
-
await core_1.NestFactory.createApplicationContext(_InternalDashboardApp, { logger: false });
|
|
39
|
-
const storage = scheduler_dash_context_1.SchedulerDashContext.storage;
|
|
40
|
-
const schedulerRegistry = app.get(schedule_1.SchedulerRegistry);
|
|
41
|
-
const jobsService = new jobs_service_1.JobsService(schedulerRegistry, storage);
|
|
42
|
-
logger.log(`Dashboard initialized`);
|
|
43
|
-
await (0, standalone_server_1.startStandaloneServer)(port, jobsService, options.auth, logger);
|
|
44
|
-
}
|
package/dist/jobs.service.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
2
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
15
|
exports.JobsService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const schedule_1 = require("@nestjs/schedule");
|
|
18
|
+
const storage_abstract_1 = require("./storage/storage.abstract");
|
|
4
19
|
const job_concurrency_1 = require("./decorators/job-concurrency");
|
|
5
|
-
|
|
20
|
+
const scheduler_dash_constants_1 = require("./scheduler-dash.constants");
|
|
21
|
+
let JobsService = class JobsService {
|
|
6
22
|
constructor(schedulerRegistry, storage) {
|
|
7
23
|
this.schedulerRegistry = schedulerRegistry;
|
|
8
24
|
this.storage = storage;
|
|
@@ -43,5 +59,11 @@ class JobsService {
|
|
|
43
59
|
stopExecution(executionId) {
|
|
44
60
|
return (0, job_concurrency_1.stopExecutionById)(executionId);
|
|
45
61
|
}
|
|
46
|
-
}
|
|
62
|
+
};
|
|
47
63
|
exports.JobsService = JobsService;
|
|
64
|
+
exports.JobsService = JobsService = __decorate([
|
|
65
|
+
(0, common_1.Injectable)(),
|
|
66
|
+
__param(1, (0, common_1.Inject)(scheduler_dash_constants_1.STORAGE_TOKEN)),
|
|
67
|
+
__metadata("design:paramtypes", [schedule_1.SchedulerRegistry,
|
|
68
|
+
storage_abstract_1.Storage])
|
|
69
|
+
], JobsService);
|