@luisrodrigues/nestjs-scheduler-dashboard 0.0.1 → 0.0.3
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 +266 -0
- package/dist/decorators/track-job.decorator.js +1 -1
- package/dist/embedded-server.js +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# @luisrodrigues/nestjs-scheduler-dashboard
|
|
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 with zero external dependencies.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Embedded UI served directly from your NestJS app (no separate frontend server)
|
|
10
|
+
- Execution history per job with status, duration, and error details
|
|
11
|
+
- Persistent metrics: total runs, failed runs, average duration — independent of history retention
|
|
12
|
+
- Manual job triggering and execution stop from the UI
|
|
13
|
+
- Concurrency control: limit how many jobs run simultaneously with a queue
|
|
14
|
+
- No-overlap mode: skip or queue a job if it is already running
|
|
15
|
+
- Optional HTTP Basic Auth to protect the dashboard
|
|
16
|
+
- Light / dark mode
|
|
17
|
+
- Standalone server mode (separate port) or embedded in your existing app
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @luisrodrigues/nestjs-scheduler-dashboard
|
|
25
|
+
# or
|
|
26
|
+
pnpm add @luisrodrigues/nestjs-scheduler-dashboard
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Peer dependencies** (install if not already present):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @nestjs/common @nestjs/schedule
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
### 1. Initialize the dashboard in `main.ts`
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { NestFactory } from '@nestjs/core';
|
|
43
|
+
import { AppModule } from './app.module';
|
|
44
|
+
import { setupSchedulerDash } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
45
|
+
|
|
46
|
+
async function bootstrap() {
|
|
47
|
+
const app = await NestFactory.create(AppModule);
|
|
48
|
+
|
|
49
|
+
await setupSchedulerDash(app);
|
|
50
|
+
|
|
51
|
+
await app.listen(3000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
bootstrap();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The dashboard is now available at `http://localhost:3000/_jobs`.
|
|
58
|
+
|
|
59
|
+
### 2. Decorate your jobs
|
|
60
|
+
|
|
61
|
+
Replace `@Cron` with `@TrackJob` — it accepts the same arguments:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { Injectable } from '@nestjs/common';
|
|
65
|
+
import { CronExpression } from '@nestjs/schedule';
|
|
66
|
+
import { TrackJob } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
67
|
+
|
|
68
|
+
@Injectable()
|
|
69
|
+
export class ReportJob {
|
|
70
|
+
@TrackJob(CronExpression.EVERY_HOUR, { name: 'generate-report' })
|
|
71
|
+
async run() {
|
|
72
|
+
// your job logic
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
`setupSchedulerDash(app, options?)` accepts the following options:
|
|
82
|
+
|
|
83
|
+
| Option | Type | Default | Description |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| `storage` | `Storage` | `new MemoryStorage()` | Storage backend for execution history and metrics |
|
|
86
|
+
| `basePath` | `string` | `'_jobs'` | URL path where the dashboard is mounted |
|
|
87
|
+
| `port` | `number` | — | When set, serves the dashboard on a separate HTTP server instead of mounting on the main app |
|
|
88
|
+
| `maxConcurrent` | `number` | — | Maximum number of `@TrackJob` jobs that can run at the same time. Excess jobs are queued |
|
|
89
|
+
| `noOverlap` | `boolean` | `false` | Globally prevent a job from starting if it is already running |
|
|
90
|
+
| `auth` | `{ username, password }` | — | Protect all dashboard routes with HTTP Basic Auth |
|
|
91
|
+
|
|
92
|
+
### `storage`
|
|
93
|
+
|
|
94
|
+
The storage backend persists execution history and metrics. The built-in `MemoryStorage` keeps everything in-process.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { MemoryStorage } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
98
|
+
|
|
99
|
+
await setupSchedulerDash(app, {
|
|
100
|
+
storage: new MemoryStorage({
|
|
101
|
+
historyRetention: 50, // keep the last 50 executions per job (default: unlimited)
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> **Metrics are independent of `historyRetention`.** Even if old history entries are removed, the counters for total runs, failed runs, and average duration keep accumulating.
|
|
107
|
+
|
|
108
|
+
To use a custom storage backend, extend the abstract `Storage` class:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { Storage, IStorageOptions, JobExecution, JobMetrics } from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
112
|
+
|
|
113
|
+
export class RedisStorage extends Storage {
|
|
114
|
+
constructor(options: IStorageOptions = {}) {
|
|
115
|
+
super(options);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
save(execution: JobExecution): void { /* ... */ }
|
|
119
|
+
update(id: string, data: Partial<Pick<JobExecution, 'finishedAt' | 'status' | 'error'>>): void { /* ... */ }
|
|
120
|
+
findByJob(jobName: string): JobExecution[] { /* ... */ }
|
|
121
|
+
findAll(): Record<string, JobExecution[]> { /* ... */ }
|
|
122
|
+
getMetrics(jobName: string): JobMetrics { /* ... */ }
|
|
123
|
+
getAllMetrics(): Record<string, JobMetrics> { /* ... */ }
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `basePath`
|
|
128
|
+
|
|
129
|
+
Changes the URL where the dashboard is served. Must contain only alphanumeric characters, hyphens, underscores, or slashes.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
await setupSchedulerDash(app, {
|
|
133
|
+
basePath: 'admin/scheduler',
|
|
134
|
+
// dashboard → http://localhost:3000/admin/scheduler
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `port`
|
|
139
|
+
|
|
140
|
+
Serve the dashboard on a completely separate HTTP server, isolated from your main application.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await setupSchedulerDash(app, {
|
|
144
|
+
port: 3001,
|
|
145
|
+
// dashboard → http://localhost:3001/_jobs
|
|
146
|
+
// main app → http://localhost:3000
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `maxConcurrent`
|
|
151
|
+
|
|
152
|
+
Limits the total number of `@TrackJob` jobs that can execute simultaneously across your entire application. Jobs that cannot start immediately are saved to storage with status `"queued"` and run in FIFO order as slots free up.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
await setupSchedulerDash(app, {
|
|
156
|
+
maxConcurrent: 5, // at most 5 jobs running at the same time
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `noOverlap`
|
|
161
|
+
|
|
162
|
+
Prevents a job from firing again if the same job is still running. Applies globally to all `@TrackJob`-decorated methods.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
await setupSchedulerDash(app, {
|
|
166
|
+
noOverlap: true,
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Can also be set per job via the decorator, which takes precedence over the global setting:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
@TrackJob(CronExpression.EVERY_MINUTE, { name: 'sync', noOverlap: true })
|
|
174
|
+
async sync() { /* ... */ }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `auth`
|
|
178
|
+
|
|
179
|
+
Protect the dashboard with HTTP Basic Auth.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
await setupSchedulerDash(app, {
|
|
183
|
+
auth: {
|
|
184
|
+
username: 'admin',
|
|
185
|
+
password: 'supersecret',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## `@TrackJob` decorator
|
|
193
|
+
|
|
194
|
+
`@TrackJob` is a drop-in replacement for `@Cron` from `@nestjs/schedule`. It accepts all the same options plus `noOverlap`.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
@TrackJob(cronTime, options?)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
| Argument | Type | Description |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `cronTime` | `string \| CronExpression` | Cron expression or `CronExpression` enum value |
|
|
203
|
+
| `options.name` | `string` | Human-readable job name shown in the dashboard. Defaults to `ClassName.methodName` |
|
|
204
|
+
| `options.noOverlap` | `boolean` | Skip this job if it is already running. Overrides the global `noOverlap` setting |
|
|
205
|
+
| `options.*` | — | All other [`CronOptions`](https://docs.nestjs.com/techniques/task-scheduling) from `@nestjs/schedule` are passed through |
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
@TrackJob('0 */6 * * *', {
|
|
209
|
+
name: 'cleanup-old-records',
|
|
210
|
+
noOverlap: true,
|
|
211
|
+
timeZone: 'Europe/Lisbon',
|
|
212
|
+
})
|
|
213
|
+
async cleanup() {
|
|
214
|
+
// runs every 6 hours, skipped if previous run is still in progress
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Full configuration example
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { NestFactory } from '@nestjs/core';
|
|
224
|
+
import { AppModule } from './app.module';
|
|
225
|
+
import {
|
|
226
|
+
setupSchedulerDash,
|
|
227
|
+
MemoryStorage,
|
|
228
|
+
} from '@luisrodrigues/nestjs-scheduler-dashboard';
|
|
229
|
+
|
|
230
|
+
async function bootstrap() {
|
|
231
|
+
const app = await NestFactory.create(AppModule);
|
|
232
|
+
|
|
233
|
+
await setupSchedulerDash(app, {
|
|
234
|
+
storage: new MemoryStorage({ historyRetention: 100 }),
|
|
235
|
+
basePath: 'scheduler',
|
|
236
|
+
maxConcurrent: 3,
|
|
237
|
+
noOverlap: true,
|
|
238
|
+
auth: {
|
|
239
|
+
username: process.env.DASH_USER ?? 'admin',
|
|
240
|
+
password: process.env.DASH_PASS ?? 'secret',
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await app.listen(3000);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
bootstrap();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## API
|
|
253
|
+
|
|
254
|
+
The dashboard exposes a small REST API used by the UI. You can also call it directly.
|
|
255
|
+
|
|
256
|
+
| Method | Path | Description |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `GET` | `/{basePath}/api` | Returns all jobs with history and metrics |
|
|
259
|
+
| `POST` | `/{basePath}/api/:name/trigger` | Manually trigger a cron job by name |
|
|
260
|
+
| `POST` | `/{basePath}/api/executions/:id/stop` | Stop a running or queued execution by ID |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT
|
|
@@ -55,7 +55,7 @@ function queueExecution(instance, args, jobName, noOverlap, original) {
|
|
|
55
55
|
function TrackJob(cronTime, options) {
|
|
56
56
|
return (target, propertyKey, descriptor) => {
|
|
57
57
|
const original = descriptor.value;
|
|
58
|
-
const jobName = options?.name ?? String(propertyKey)
|
|
58
|
+
const jobName = options?.name ?? `${target.constructor.name}.${String(propertyKey)}`;
|
|
59
59
|
const jobNoOverlap = options?.noOverlap;
|
|
60
60
|
descriptor.value = async function (...args) {
|
|
61
61
|
const noOverlap = resolveNoOverlap(jobNoOverlap);
|
package/dist/embedded-server.js
CHANGED
|
@@ -13,7 +13,7 @@ function registerUiRoutes(expressApp, base, guard) {
|
|
|
13
13
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
14
14
|
res.send(html);
|
|
15
15
|
});
|
|
16
|
-
expressApp.get(`${base}/jobs
|
|
16
|
+
expressApp.get(`${base}/jobs/*path`, guard, (_req, res) => {
|
|
17
17
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
18
18
|
res.send(html);
|
|
19
19
|
});
|