@artian-techs/adonis-orbit 0.1.0
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/LICENSE.md +21 -0
- package/README.md +275 -0
- package/build/configure.d.ts +8 -0
- package/build/configure.js +38 -0
- package/build/index.d.ts +10 -0
- package/build/index.js +9 -0
- package/build/providers/orbit_provider.d.ts +13 -0
- package/build/providers/orbit_provider.js +35 -0
- package/build/src/auth/basic_auth.d.ts +12 -0
- package/build/src/auth/basic_auth.js +36 -0
- package/build/src/controllers/dashboard_controller.d.ts +51 -0
- package/build/src/controllers/dashboard_controller.js +341 -0
- package/build/src/define_config.d.ts +5 -0
- package/build/src/define_config.js +6 -0
- package/build/src/jobs/prune_job_runs.d.ts +20 -0
- package/build/src/jobs/prune_job_runs.js +27 -0
- package/build/src/logo.d.ts +4 -0
- package/build/src/logo.js +12 -0
- package/build/src/middleware/dashboard_auth.d.ts +10 -0
- package/build/src/middleware/dashboard_auth.js +16 -0
- package/build/src/models/job_run.d.ts +35 -0
- package/build/src/models/job_run.js +71 -0
- package/build/src/models/silenced_job.d.ts +14 -0
- package/build/src/models/silenced_job.js +27 -0
- package/build/src/recorder.d.ts +14 -0
- package/build/src/recorder.js +102 -0
- package/build/src/retry.d.ts +9 -0
- package/build/src/retry.js +18 -0
- package/build/src/routes.d.ts +8 -0
- package/build/src/routes.js +20 -0
- package/build/src/sinks/database_sink.d.ts +16 -0
- package/build/src/sinks/database_sink.js +88 -0
- package/build/src/types.d.ts +92 -0
- package/build/src/types.js +1 -0
- package/build/stubs/config/orbit.stub +31 -0
- package/build/stubs/main.d.ts +1 -0
- package/build/stubs/main.js +1 -0
- package/build/stubs/migrations/create_job_runs_table.stub +62 -0
- package/build/stubs/migrations/create_silenced_jobs_table.stub +29 -0
- package/build/templates/components/layout.edge +174 -0
- package/build/templates/index.edge +111 -0
- package/build/templates/metrics.edge +59 -0
- package/build/templates/orbit.svg +35 -0
- package/build/templates/silenced.edge +35 -0
- package/package.json +130 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import app from '@adonisjs/core/services/app';
|
|
2
|
+
import db from '@adonisjs/lucid/services/db';
|
|
3
|
+
import edge from 'edge.js';
|
|
4
|
+
import { DateTime } from 'luxon';
|
|
5
|
+
import { favicon, logo } from '../logo.js';
|
|
6
|
+
import JobRun, { JobRunStatus } from '../models/job_run.js';
|
|
7
|
+
import SilencedJob from '../models/silenced_job.js';
|
|
8
|
+
import { retryRun } from '../retry.js';
|
|
9
|
+
const PER_PAGE = 50;
|
|
10
|
+
/** The two modes that a person chooses between. */
|
|
11
|
+
const THEMES = ['light', 'dark'];
|
|
12
|
+
/** Where the choice of the mode is held, and for how long. */
|
|
13
|
+
const THEME_COOKIE = 'orbit_theme';
|
|
14
|
+
const THEME_AGE = '1y';
|
|
15
|
+
/** How far back the metrics read. */
|
|
16
|
+
const METRICS_WINDOW = { hours: 24 };
|
|
17
|
+
const STATUS_LABELS = {
|
|
18
|
+
[JobRunStatus.PENDING]: 'Pending',
|
|
19
|
+
[JobRunStatus.RUNNING]: 'Running',
|
|
20
|
+
[JobRunStatus.RETRYING]: 'Retrying',
|
|
21
|
+
[JobRunStatus.COMPLETED]: 'Completed',
|
|
22
|
+
[JobRunStatus.FAILED]: 'Failed',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The drawings of the side menu. They are written here, and the page prints
|
|
26
|
+
* them as they are.
|
|
27
|
+
*/
|
|
28
|
+
const ICONS = {
|
|
29
|
+
overview: '<rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/>',
|
|
30
|
+
pending: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
|
31
|
+
running: '<circle cx="12" cy="12" r="9"/><path d="M10 8.5l5 3.5-5 3.5z"/>',
|
|
32
|
+
retrying: '<path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/>',
|
|
33
|
+
completed: '<circle cx="12" cy="12" r="9"/><path d="M8 12.5l3 3 5-6"/>',
|
|
34
|
+
failed: '<circle cx="12" cy="12" r="9"/><path d="M12 7v6"/><path d="M12 16.5v.5"/>',
|
|
35
|
+
silenced: '<path d="M18 8a6 6 0 0 0-9.3-5"/><path d="M6 9v5l-2 3h13"/><path d="M10.5 20a2 2 0 0 0 3 0"/><path d="M3 3l18 18"/>',
|
|
36
|
+
metrics: '<path d="M4 20V10"/><path d="M10 20V4"/><path d="M16 20v-7"/><path d="M22 20H2"/>',
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The page that shows what the queue does.
|
|
40
|
+
*
|
|
41
|
+
* It reads the table, not Redis: a job that ended left Redis, and the table
|
|
42
|
+
* keeps it. The page is served by the API itself, outside the front, thus it
|
|
43
|
+
* answers even when the front is down.
|
|
44
|
+
*/
|
|
45
|
+
export default class DashboardController {
|
|
46
|
+
#silenced;
|
|
47
|
+
/**
|
|
48
|
+
* Everything the queue did, and the four numbers that say how it goes.
|
|
49
|
+
*
|
|
50
|
+
* The side menu leads here with a status. A row that was asked again leads
|
|
51
|
+
* here with the identifier of the job that answered. Without one and without
|
|
52
|
+
* the other, the page shows the numbers and the last jobs of every status.
|
|
53
|
+
*/
|
|
54
|
+
async index(ctx) {
|
|
55
|
+
const { request } = ctx;
|
|
56
|
+
const status = request.input('status');
|
|
57
|
+
const job = request.input('job');
|
|
58
|
+
const page = Number(request.input('page', 1)) || 1;
|
|
59
|
+
const silenced = await this.#silencedNames();
|
|
60
|
+
const query = JobRun.query().orderBy('id', 'desc');
|
|
61
|
+
if (status) {
|
|
62
|
+
query.where('status', status);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A person who asks for one job asks for that job, thus a class that is
|
|
66
|
+
* silenced answers here as any other class.
|
|
67
|
+
*/
|
|
68
|
+
if (job) {
|
|
69
|
+
query.where('jobId', job);
|
|
70
|
+
}
|
|
71
|
+
else if (silenced.length > 0) {
|
|
72
|
+
query.whereNotIn('name', silenced);
|
|
73
|
+
}
|
|
74
|
+
const runs = await query.paginate(page, PER_PAGE);
|
|
75
|
+
const answers = await this.#answers(runs.all());
|
|
76
|
+
const alone = Boolean(status || job);
|
|
77
|
+
return this.#render(ctx, 'index', {
|
|
78
|
+
active: status || 'overview',
|
|
79
|
+
title: this.#title(status, job),
|
|
80
|
+
showStats: !alone,
|
|
81
|
+
stats: alone ? null : await this.#stats(),
|
|
82
|
+
status: status ?? '',
|
|
83
|
+
rows: runs.all().map((run) => this.#row(run, silenced, answers)),
|
|
84
|
+
page: runs.currentPage,
|
|
85
|
+
lastPage: runs.lastPage,
|
|
86
|
+
total: runs.total,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** What the page is called. */
|
|
90
|
+
#title(status, job) {
|
|
91
|
+
if (job) {
|
|
92
|
+
return 'One job';
|
|
93
|
+
}
|
|
94
|
+
return status ? STATUS_LABELS[status] : 'Overview';
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What became of the jobs that were asked again.
|
|
98
|
+
*
|
|
99
|
+
* One reading answers for the whole page: a row that names a new job reads
|
|
100
|
+
* the status of that job beside it, and leads to it.
|
|
101
|
+
*/
|
|
102
|
+
async #answers(runs) {
|
|
103
|
+
const asked = runs.map((run) => run.retriedJobId).filter((id) => id !== null);
|
|
104
|
+
if (asked.length === 0) {
|
|
105
|
+
return new Map();
|
|
106
|
+
}
|
|
107
|
+
const answers = await JobRun.query().whereIn('jobId', asked).select('jobId', 'status');
|
|
108
|
+
return new Map(answers.map((run) => [run.jobId, run.status]));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The classes that the lists leave out.
|
|
112
|
+
*
|
|
113
|
+
* Their work goes on and their trace is written. Take a name away from here
|
|
114
|
+
* and everything it did comes back, including what it did while it was
|
|
115
|
+
* silent.
|
|
116
|
+
*/
|
|
117
|
+
async silenced(ctx) {
|
|
118
|
+
const silenced = await SilencedJob.query().orderBy('name', 'asc');
|
|
119
|
+
return this.#render(ctx, 'silenced', {
|
|
120
|
+
active: 'silenced',
|
|
121
|
+
title: 'Silenced jobs',
|
|
122
|
+
names: silenced.map((row) => row.name),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What each class and each queue cost in the last day.
|
|
127
|
+
*
|
|
128
|
+
* A job that takes long, or that fails often, is read here before it is read
|
|
129
|
+
* in the list.
|
|
130
|
+
*/
|
|
131
|
+
async metrics(ctx) {
|
|
132
|
+
const since = DateTime.now().minus(METRICS_WINDOW).toJSDate();
|
|
133
|
+
return this.#render(ctx, 'metrics', {
|
|
134
|
+
active: 'metrics',
|
|
135
|
+
title: 'Metrics',
|
|
136
|
+
hours: METRICS_WINDOW.hours,
|
|
137
|
+
byJob: await this.#metricsBy('name', since),
|
|
138
|
+
byQueue: await this.#metricsBy('queue', since),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Ask the queue for one job again.
|
|
143
|
+
*
|
|
144
|
+
* The row that answers stays where it is. The new job opens a row of its
|
|
145
|
+
* own, thus the page shows the two.
|
|
146
|
+
*/
|
|
147
|
+
async retry({ params, response }) {
|
|
148
|
+
const run = await JobRun.findOrFail(params.id);
|
|
149
|
+
await retryRun(run);
|
|
150
|
+
return response.redirect().back();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Read the page in the light, or in the dark.
|
|
154
|
+
*
|
|
155
|
+
* The choice is held in a cookie, thus the page carries it on the next
|
|
156
|
+
* request and no script reads it. A person who chose nothing reads the mode
|
|
157
|
+
* of their machine.
|
|
158
|
+
*/
|
|
159
|
+
async theme({ params, response }) {
|
|
160
|
+
const theme = String(params.theme);
|
|
161
|
+
if (!THEMES.includes(theme)) {
|
|
162
|
+
return response.notFound({ success: false, error: 'Not found' });
|
|
163
|
+
}
|
|
164
|
+
response.plainCookie(THEME_COOKIE, theme, {
|
|
165
|
+
path: this.#config().path,
|
|
166
|
+
maxAge: THEME_AGE,
|
|
167
|
+
httpOnly: false,
|
|
168
|
+
sameSite: 'lax',
|
|
169
|
+
});
|
|
170
|
+
return response.redirect().back();
|
|
171
|
+
}
|
|
172
|
+
/** Take a class out of the lists, or put it back. */
|
|
173
|
+
async silence({ params, response }) {
|
|
174
|
+
const name = String(params.name);
|
|
175
|
+
const silenced = await SilencedJob.findBy('name', name);
|
|
176
|
+
if (silenced) {
|
|
177
|
+
await silenced.delete();
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
await SilencedJob.create({ name });
|
|
181
|
+
}
|
|
182
|
+
return response.redirect().back();
|
|
183
|
+
}
|
|
184
|
+
/** The four numbers of the overview. */
|
|
185
|
+
async #stats() {
|
|
186
|
+
const now = DateTime.now();
|
|
187
|
+
return {
|
|
188
|
+
perMinute: await this.#finishedSince(now.minus({ minutes: 1 })),
|
|
189
|
+
pastHour: await this.#finishedSince(now.minus({ hours: 1 })),
|
|
190
|
+
failedPastWeek: await this.#count(JobRun.query()
|
|
191
|
+
.where('status', JobRunStatus.FAILED)
|
|
192
|
+
.where('finishedAt', '>', now.minus({ days: 7 }).toJSDate())),
|
|
193
|
+
running: await this.#count(JobRun.query().where('status', JobRunStatus.RUNNING)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
#finishedSince(moment) {
|
|
197
|
+
return this.#count(JobRun.query().where('finishedAt', '>', moment.toJSDate()));
|
|
198
|
+
}
|
|
199
|
+
async #count(query) {
|
|
200
|
+
const [row] = await query.count('* as total').pojo();
|
|
201
|
+
return Number(row.total);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* What one column cost over the window.
|
|
205
|
+
*
|
|
206
|
+
* The count of the failures rides with the count of the rows: one reading of
|
|
207
|
+
* the table answers both.
|
|
208
|
+
*/
|
|
209
|
+
async #metricsBy(column, since) {
|
|
210
|
+
const result = await db.rawQuery(`select ${column} as label,
|
|
211
|
+
count(*) as total,
|
|
212
|
+
count(*) filter (where status = ?) as failed,
|
|
213
|
+
round(avg(duration)::numeric, 1) as average,
|
|
214
|
+
round(max(duration)::numeric, 1) as longest
|
|
215
|
+
from ${JobRun.table}
|
|
216
|
+
where finished_at > ?
|
|
217
|
+
group by ${column}
|
|
218
|
+
order by count(*) desc`, [JobRunStatus.FAILED, since]);
|
|
219
|
+
return result.rows.map((row) => ({
|
|
220
|
+
label: row.label,
|
|
221
|
+
total: Number(row.total),
|
|
222
|
+
failed: Number(row.failed),
|
|
223
|
+
average: row.average === null ? null : Number(row.average),
|
|
224
|
+
longest: row.longest === null ? null : Number(row.longest),
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The classes that the lists leave out.
|
|
229
|
+
*
|
|
230
|
+
* One request reads them once: the page asks for them, and the frame around
|
|
231
|
+
* it asks again for its counts.
|
|
232
|
+
*/
|
|
233
|
+
async #silencedNames() {
|
|
234
|
+
if (!this.#silenced) {
|
|
235
|
+
const rows = await SilencedJob.query().select('name');
|
|
236
|
+
this.#silenced = rows.map((row) => row.name);
|
|
237
|
+
}
|
|
238
|
+
return this.#silenced;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The word that the buttons of the page carry.
|
|
242
|
+
*
|
|
243
|
+
* A form that asks for something writes it beside what it asks. An
|
|
244
|
+
* application that guards its forms reads it; one that does not, reads
|
|
245
|
+
* nothing, and the page holds no word at all.
|
|
246
|
+
*/
|
|
247
|
+
/** What the application told Orbit. */
|
|
248
|
+
#config() {
|
|
249
|
+
return app.config.get('orbit', {
|
|
250
|
+
path: '/queue',
|
|
251
|
+
authorize: () => false,
|
|
252
|
+
sinks: [],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/** The mode that the person chose, if they chose one. */
|
|
256
|
+
#theme(request) {
|
|
257
|
+
const theme = request.plainCookie(THEME_COOKIE);
|
|
258
|
+
return THEMES.includes(theme) ? theme : null;
|
|
259
|
+
}
|
|
260
|
+
#csrfToken(request) {
|
|
261
|
+
return request.csrfToken ?? null;
|
|
262
|
+
}
|
|
263
|
+
/** What one line of the table shows. The page reads, it does not calculate. */
|
|
264
|
+
#row(run, silenced, answers) {
|
|
265
|
+
const answer = run.retriedJobId ? answers.get(run.retriedJobId) : undefined;
|
|
266
|
+
return {
|
|
267
|
+
id: run.id,
|
|
268
|
+
jobId: run.jobId,
|
|
269
|
+
name: run.name,
|
|
270
|
+
queue: run.queue,
|
|
271
|
+
status: run.status,
|
|
272
|
+
statusLabel: STATUS_LABELS[run.status],
|
|
273
|
+
attempts: run.attempts,
|
|
274
|
+
duration: run.duration,
|
|
275
|
+
payload: JSON.stringify(run.payload ?? {}, null, 2),
|
|
276
|
+
error: run.error,
|
|
277
|
+
dispatchedAt: run.dispatchedAt?.toFormat('dd LLL yyyy HH:mm:ss') ?? null,
|
|
278
|
+
finishedAt: run.finishedAt?.toFormat('dd LLL yyyy HH:mm:ss') ?? null,
|
|
279
|
+
retriedAt: run.retriedAt?.toFormat('dd LLL yyyy HH:mm:ss') ?? null,
|
|
280
|
+
retriedJobId: run.retriedJobId,
|
|
281
|
+
retriedStatus: answer ? STATUS_LABELS[answer] : null,
|
|
282
|
+
canRetry: run.status === JobRunStatus.FAILED && !run.retriedJobId,
|
|
283
|
+
isSilenced: silenced.includes(run.name),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The page, with everything the frame around it asks for.
|
|
288
|
+
*
|
|
289
|
+
* The side menu carries the count of each status, thus every page says how
|
|
290
|
+
* many jobs wait and how many fell.
|
|
291
|
+
*/
|
|
292
|
+
async #render({ request, response }, template, data) {
|
|
293
|
+
const config = this.#config();
|
|
294
|
+
/**
|
|
295
|
+
* A silenced class counts nowhere: a badge that says two, over a list that
|
|
296
|
+
* shows nothing, says nothing true.
|
|
297
|
+
*/
|
|
298
|
+
const silenced = await this.#silencedNames();
|
|
299
|
+
const query = db.from(JobRun.table).select('status').count('* as total').groupBy('status');
|
|
300
|
+
if (silenced.length > 0) {
|
|
301
|
+
query.whereNotIn('name', silenced);
|
|
302
|
+
}
|
|
303
|
+
const counts = await query;
|
|
304
|
+
const byStatus = Object.fromEntries(counts.map((row) => [row.status, Number(row.total)]));
|
|
305
|
+
const menu = [
|
|
306
|
+
{ key: 'overview', label: 'Overview', href: config.path, icon: ICONS.overview, count: null },
|
|
307
|
+
...Object.values(JobRunStatus).map((status) => ({
|
|
308
|
+
key: status,
|
|
309
|
+
label: STATUS_LABELS[status],
|
|
310
|
+
href: `${config.path}?status=${status}`,
|
|
311
|
+
icon: ICONS[status],
|
|
312
|
+
count: byStatus[status] ?? 0,
|
|
313
|
+
})),
|
|
314
|
+
{
|
|
315
|
+
key: 'silenced',
|
|
316
|
+
label: 'Silenced',
|
|
317
|
+
href: `${config.path}/silenced`,
|
|
318
|
+
icon: ICONS.silenced,
|
|
319
|
+
count: null,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
key: 'metrics',
|
|
323
|
+
label: 'Metrics',
|
|
324
|
+
href: `${config.path}/metrics`,
|
|
325
|
+
icon: ICONS.metrics,
|
|
326
|
+
count: null,
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
const html = await edge.render(`orbit::${template}`, {
|
|
330
|
+
...data,
|
|
331
|
+
path: config.path,
|
|
332
|
+
logo,
|
|
333
|
+
favicon,
|
|
334
|
+
theme: this.#theme(request),
|
|
335
|
+
menu,
|
|
336
|
+
csrfToken: this.#csrfToken(request),
|
|
337
|
+
});
|
|
338
|
+
response.header('content-type', 'text/html; charset=utf-8');
|
|
339
|
+
return response.send(html);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Job } from '@adonisjs/queue';
|
|
2
|
+
import type { JobOptions } from '@adonisjs/queue/types';
|
|
3
|
+
interface PruneJobRunsPayload {
|
|
4
|
+
/** How long a job that succeeded stays readable, in days. */
|
|
5
|
+
days: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Take out of the table the jobs that ended well.
|
|
9
|
+
*
|
|
10
|
+
* A job that succeeded says nothing after a few days, and the table would grow
|
|
11
|
+
* with every mail of every day.
|
|
12
|
+
*
|
|
13
|
+
* A job that failed is never taken out. It waits for a person, however long
|
|
14
|
+
* that takes.
|
|
15
|
+
*/
|
|
16
|
+
export default class PruneJobRuns extends Job<PruneJobRunsPayload> {
|
|
17
|
+
static options: JobOptions;
|
|
18
|
+
execute(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import logger from '@adonisjs/core/services/logger';
|
|
2
|
+
import { Job } from '@adonisjs/queue';
|
|
3
|
+
import { DateTime } from 'luxon';
|
|
4
|
+
import JobRun, { JobRunStatus } from '../models/job_run.js';
|
|
5
|
+
/**
|
|
6
|
+
* Take out of the table the jobs that ended well.
|
|
7
|
+
*
|
|
8
|
+
* A job that succeeded says nothing after a few days, and the table would grow
|
|
9
|
+
* with every mail of every day.
|
|
10
|
+
*
|
|
11
|
+
* A job that failed is never taken out. It waits for a person, however long
|
|
12
|
+
* that takes.
|
|
13
|
+
*/
|
|
14
|
+
export default class PruneJobRuns extends Job {
|
|
15
|
+
static options = {
|
|
16
|
+
queue: 'default',
|
|
17
|
+
maxRetries: 1,
|
|
18
|
+
};
|
|
19
|
+
async execute() {
|
|
20
|
+
const before = DateTime.now().minus({ days: this.payload.days });
|
|
21
|
+
const removed = await JobRun.query()
|
|
22
|
+
.where('status', JobRunStatus.COMPLETED)
|
|
23
|
+
.where('finishedAt', '<', before.toJSDate())
|
|
24
|
+
.delete();
|
|
25
|
+
logger.info(`${removed} trace(s) of jobs removed.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* The mark of Orbit.
|
|
4
|
+
*
|
|
5
|
+
* The package serves no file of its own, thus the mark is written in the page
|
|
6
|
+
* and given to the tab as an address of data. One file answers for the two.
|
|
7
|
+
*/
|
|
8
|
+
const source = readFileSync(new URL('../templates/orbit.svg', import.meta.url), 'utf-8');
|
|
9
|
+
/** The mark, to write in the page. */
|
|
10
|
+
export const logo = source;
|
|
11
|
+
/** The mark, as the address that the tab of the browser reads. */
|
|
12
|
+
export const favicon = `data:image/svg+xml;base64,${Buffer.from(source).toString('base64')}`;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { NextFn } from '@adonisjs/core/types/http';
|
|
3
|
+
import type { OrbitConfig } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Ask the guard of the configuration who may read the page.
|
|
6
|
+
*
|
|
7
|
+
* A guard that says `false` and writes nothing leaves the page as a page that
|
|
8
|
+
* does not exist: a machine that guards nothing shows nothing.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dashboardAuth(config: OrbitConfig): (ctx: HttpContext, next: NextFn) => Promise<any>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the guard of the configuration who may read the page.
|
|
3
|
+
*
|
|
4
|
+
* A guard that says `false` and writes nothing leaves the page as a page that
|
|
5
|
+
* does not exist: a machine that guards nothing shows nothing.
|
|
6
|
+
*/
|
|
7
|
+
export function dashboardAuth(config) {
|
|
8
|
+
return async (ctx, next) => {
|
|
9
|
+
if (await config.authorize(ctx)) {
|
|
10
|
+
return next();
|
|
11
|
+
}
|
|
12
|
+
if (!ctx.response.hasContent) {
|
|
13
|
+
ctx.response.notFound({ success: false, error: 'Not found' });
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseModel } from '@adonisjs/lucid/orm';
|
|
2
|
+
import type { DateTime } from 'luxon';
|
|
3
|
+
/**
|
|
4
|
+
* What the queue did with one job.
|
|
5
|
+
*
|
|
6
|
+
* `pending` and `running` are what the worker is about to do or does now.
|
|
7
|
+
* `completed` and `failed` are what it did. `retrying` names a job that failed
|
|
8
|
+
* a try and waits for the next one.
|
|
9
|
+
*/
|
|
10
|
+
export declare const JobRunStatus: {
|
|
11
|
+
readonly PENDING: "pending";
|
|
12
|
+
readonly RUNNING: "running";
|
|
13
|
+
readonly RETRYING: "retrying";
|
|
14
|
+
readonly COMPLETED: "completed";
|
|
15
|
+
readonly FAILED: "failed";
|
|
16
|
+
};
|
|
17
|
+
export type JobRunStatusValue = (typeof JobRunStatus)[keyof typeof JobRunStatus];
|
|
18
|
+
export default class JobRun extends BaseModel {
|
|
19
|
+
id: number;
|
|
20
|
+
jobId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
queue: string;
|
|
23
|
+
status: JobRunStatusValue;
|
|
24
|
+
payload: Record<string, unknown>;
|
|
25
|
+
attempts: number;
|
|
26
|
+
error: string | null;
|
|
27
|
+
duration: number | null;
|
|
28
|
+
dispatchedAt: DateTime | null;
|
|
29
|
+
startedAt: DateTime | null;
|
|
30
|
+
finishedAt: DateTime | null;
|
|
31
|
+
retriedAt: DateTime | null;
|
|
32
|
+
retriedJobId: string | null;
|
|
33
|
+
createdAt: DateTime<true>;
|
|
34
|
+
updatedAt: DateTime;
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm';
|
|
8
|
+
/**
|
|
9
|
+
* What the queue did with one job.
|
|
10
|
+
*
|
|
11
|
+
* `pending` and `running` are what the worker is about to do or does now.
|
|
12
|
+
* `completed` and `failed` are what it did. `retrying` names a job that failed
|
|
13
|
+
* a try and waits for the next one.
|
|
14
|
+
*/
|
|
15
|
+
export const JobRunStatus = {
|
|
16
|
+
PENDING: 'pending',
|
|
17
|
+
RUNNING: 'running',
|
|
18
|
+
RETRYING: 'retrying',
|
|
19
|
+
COMPLETED: 'completed',
|
|
20
|
+
FAILED: 'failed',
|
|
21
|
+
};
|
|
22
|
+
export default class JobRun extends BaseModel {
|
|
23
|
+
}
|
|
24
|
+
__decorate([
|
|
25
|
+
column({ isPrimary: true })
|
|
26
|
+
], JobRun.prototype, "id", void 0);
|
|
27
|
+
__decorate([
|
|
28
|
+
column()
|
|
29
|
+
], JobRun.prototype, "jobId", void 0);
|
|
30
|
+
__decorate([
|
|
31
|
+
column()
|
|
32
|
+
], JobRun.prototype, "name", void 0);
|
|
33
|
+
__decorate([
|
|
34
|
+
column()
|
|
35
|
+
], JobRun.prototype, "queue", void 0);
|
|
36
|
+
__decorate([
|
|
37
|
+
column()
|
|
38
|
+
], JobRun.prototype, "status", void 0);
|
|
39
|
+
__decorate([
|
|
40
|
+
column()
|
|
41
|
+
], JobRun.prototype, "payload", void 0);
|
|
42
|
+
__decorate([
|
|
43
|
+
column()
|
|
44
|
+
], JobRun.prototype, "attempts", void 0);
|
|
45
|
+
__decorate([
|
|
46
|
+
column()
|
|
47
|
+
], JobRun.prototype, "error", void 0);
|
|
48
|
+
__decorate([
|
|
49
|
+
column()
|
|
50
|
+
], JobRun.prototype, "duration", void 0);
|
|
51
|
+
__decorate([
|
|
52
|
+
column.dateTime()
|
|
53
|
+
], JobRun.prototype, "dispatchedAt", void 0);
|
|
54
|
+
__decorate([
|
|
55
|
+
column.dateTime()
|
|
56
|
+
], JobRun.prototype, "startedAt", void 0);
|
|
57
|
+
__decorate([
|
|
58
|
+
column.dateTime()
|
|
59
|
+
], JobRun.prototype, "finishedAt", void 0);
|
|
60
|
+
__decorate([
|
|
61
|
+
column.dateTime()
|
|
62
|
+
], JobRun.prototype, "retriedAt", void 0);
|
|
63
|
+
__decorate([
|
|
64
|
+
column()
|
|
65
|
+
], JobRun.prototype, "retriedJobId", void 0);
|
|
66
|
+
__decorate([
|
|
67
|
+
column.dateTime({ autoCreate: true })
|
|
68
|
+
], JobRun.prototype, "createdAt", void 0);
|
|
69
|
+
__decorate([
|
|
70
|
+
column.dateTime({ autoCreate: true, autoUpdate: true })
|
|
71
|
+
], JobRun.prototype, "updatedAt", void 0);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseModel } from '@adonisjs/lucid/orm';
|
|
2
|
+
import type { DateTime } from 'luxon';
|
|
3
|
+
/**
|
|
4
|
+
* A job class that the page leaves out of its lists.
|
|
5
|
+
*
|
|
6
|
+
* The row names a class, not one job. Take it away, and everything that class
|
|
7
|
+
* did comes back, including what it did while it was silent.
|
|
8
|
+
*/
|
|
9
|
+
export default class SilencedJob extends BaseModel {
|
|
10
|
+
id: number;
|
|
11
|
+
name: string;
|
|
12
|
+
createdAt: DateTime<true>;
|
|
13
|
+
updatedAt: DateTime;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm';
|
|
8
|
+
/**
|
|
9
|
+
* A job class that the page leaves out of its lists.
|
|
10
|
+
*
|
|
11
|
+
* The row names a class, not one job. Take it away, and everything that class
|
|
12
|
+
* did comes back, including what it did while it was silent.
|
|
13
|
+
*/
|
|
14
|
+
export default class SilencedJob extends BaseModel {
|
|
15
|
+
}
|
|
16
|
+
__decorate([
|
|
17
|
+
column({ isPrimary: true })
|
|
18
|
+
], SilencedJob.prototype, "id", void 0);
|
|
19
|
+
__decorate([
|
|
20
|
+
column()
|
|
21
|
+
], SilencedJob.prototype, "name", void 0);
|
|
22
|
+
__decorate([
|
|
23
|
+
column.dateTime({ autoCreate: true })
|
|
24
|
+
], SilencedJob.prototype, "createdAt", void 0);
|
|
25
|
+
__decorate([
|
|
26
|
+
column.dateTime({ autoCreate: true, autoUpdate: true })
|
|
27
|
+
], SilencedJob.prototype, "updatedAt", void 0);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OrbitConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Keep what the queue does with its jobs.
|
|
4
|
+
*
|
|
5
|
+
* The queue holds its jobs in Redis and removes them when they end. The trace
|
|
6
|
+
* holds what a job carried, how many tries it took, and the error that stopped
|
|
7
|
+
* it.
|
|
8
|
+
*
|
|
9
|
+
* The channels of the queue answer at once, thus the events are written apart.
|
|
10
|
+
* A sink that refuses an event must never stop the job that runs.
|
|
11
|
+
*
|
|
12
|
+
* A configuration that names no sink keeps nothing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function recordJobRuns(config: OrbitConfig): void;
|