@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
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artian Techs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="templates/orbit.svg" width="96" alt="Orbit">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# adonis-orbit
|
|
6
|
+
|
|
7
|
+
What the queue of AdonisJS did with each job, kept in a table, read on a page,
|
|
8
|
+
and asked again when it failed.
|
|
9
|
+
|
|
10
|
+
The queue holds its jobs in Redis and lets them go when they end. This package
|
|
11
|
+
listens to the channels that the queue publishes, writes one row for each job,
|
|
12
|
+
and serves a page that reads the table. A job that failed keeps the values it
|
|
13
|
+
carried, so a person can ask for it again — from the page, or from a command.
|
|
14
|
+
|
|
15
|
+
It works with AdonisJS 6 and 7.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @artian-techs/adonis-orbit
|
|
21
|
+
node ace configure @artian-techs/adonis-orbit
|
|
22
|
+
node ace migration:run
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The configure step writes the provider in `adonisrc.ts`, publishes
|
|
26
|
+
`config/orbit.ts`, publishes the two migrations — the table of the jobs, and
|
|
27
|
+
the one that names the silenced classes — and declares the two environment
|
|
28
|
+
variables that guard the page.
|
|
29
|
+
|
|
30
|
+
## The page
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
http://localhost:3333/queue
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It asks for a name and a word, in Basic auth. **While one of the two is
|
|
37
|
+
missing, the page answers "not found"**: a machine that carries neither shows
|
|
38
|
+
nothing.
|
|
39
|
+
|
|
40
|
+
The page holds no session on purpose: it belongs to the team that runs the
|
|
41
|
+
machine, not to the users of the application. It speaks English only.
|
|
42
|
+
|
|
43
|
+
A menu on the left leads to each view, and carries the count of each status.
|
|
44
|
+
Under it, one button reads the page in the light or in the dark. The page reads
|
|
45
|
+
the mode of the machine while nobody chose; a choice is held in a cookie. The
|
|
46
|
+
page holds no JavaScript.
|
|
47
|
+
|
|
48
|
+
| Entry | What it shows |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| Overview | four numbers, then the last jobs of every status |
|
|
51
|
+
| Pending, Running, Retrying, Completed, Failed | the same list, of one status |
|
|
52
|
+
| Silenced | the classes that the lists leave out |
|
|
53
|
+
| Metrics | what each class and each queue cost over the last 24 hours |
|
|
54
|
+
|
|
55
|
+
The four numbers of the overview:
|
|
56
|
+
|
|
57
|
+
| Number | What it counts |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| Jobs per minute | the jobs that ended in the last minute |
|
|
60
|
+
| Jobs past hour | the jobs that ended in the last hour |
|
|
61
|
+
| Failed jobs past 7 days | the jobs whose last try fell |
|
|
62
|
+
| Jobs running now | the jobs a worker holds at this moment |
|
|
63
|
+
|
|
64
|
+
Metrics reads the same table: for each class and for each queue, how many jobs
|
|
65
|
+
ended, how many fell, and the average and the longest of their durations.
|
|
66
|
+
|
|
67
|
+
## Silence a job
|
|
68
|
+
|
|
69
|
+
A job that runs every minute fills the list and hides the rest. The button
|
|
70
|
+
**Silence** on any row takes its class out of the lists, and out of the counts
|
|
71
|
+
of the menu.
|
|
72
|
+
|
|
73
|
+
Nothing else changes: the worker keeps its work, and the table keeps its trace.
|
|
74
|
+
The page **Silenced** shows what is left out, and **Show again** brings the
|
|
75
|
+
class back — with everything it did while it was silent.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { DatabaseSink, basicAuth, defineConfig } from '@artian-techs/adonis-orbit'
|
|
81
|
+
|
|
82
|
+
import env from '#start/env'
|
|
83
|
+
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
path: '/queue',
|
|
86
|
+
|
|
87
|
+
authorize: basicAuth({
|
|
88
|
+
user: env.get('ORBIT_DASHBOARD_USER'),
|
|
89
|
+
password: env.get('ORBIT_DASHBOARD_PASSWORD'),
|
|
90
|
+
}),
|
|
91
|
+
|
|
92
|
+
sinks: [new DatabaseSink()],
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Who may read the page
|
|
97
|
+
|
|
98
|
+
`authorize` reads the request and answers `true` or `false`. It answers `false`,
|
|
99
|
+
and the page says "not found": a machine that guards nothing shows nothing.
|
|
100
|
+
|
|
101
|
+
`basicAuth` asks for a name and a word, with no session. It is what the
|
|
102
|
+
configure step writes, and it answers "not found" while one of the two is
|
|
103
|
+
missing.
|
|
104
|
+
|
|
105
|
+
An application that already knows who is asking writes its own guard:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const TEAM = ['someone@example.com', 'someone-else@example.com']
|
|
109
|
+
|
|
110
|
+
export default defineConfig({
|
|
111
|
+
path: '/queue',
|
|
112
|
+
|
|
113
|
+
authorize: async ({ auth }) => {
|
|
114
|
+
await auth.check()
|
|
115
|
+
|
|
116
|
+
return TEAM.includes(auth.user?.email ?? '')
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
sinks: [new DatabaseSink()],
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The package imports no authentication of its own: the guard reads the context
|
|
124
|
+
of the request, and the application says what it reads there.
|
|
125
|
+
|
|
126
|
+
A guard that wants to say something other than "not found" writes its answer
|
|
127
|
+
before it says `false`:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
authorize: async (ctx) => {
|
|
131
|
+
if (await ctx.auth.check()) {
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ctx.response.redirect('/login')
|
|
136
|
+
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Where the trace goes
|
|
142
|
+
|
|
143
|
+
**Nothing is kept that the configuration does not name.** The table of
|
|
144
|
+
`job_runs` is one sink among others, and the configure step writes it in
|
|
145
|
+
`config/orbit.ts`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { DatabaseSink, defineConfig } from '@artian-techs/adonis-orbit'
|
|
149
|
+
|
|
150
|
+
export default defineConfig({
|
|
151
|
+
path: '/queue',
|
|
152
|
+
auth: { user: env.get('ORBIT_DASHBOARD_USER'), password: env.get('ORBIT_DASHBOARD_PASSWORD') },
|
|
153
|
+
|
|
154
|
+
sinks: [new DatabaseSink(), new MySink()],
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Take `DatabaseSink` away, and the table stays empty and the page shows nothing:
|
|
159
|
+
the page reads the table, and reads nothing else. A machine that sends its
|
|
160
|
+
trace somewhere else thus keeps none of it.
|
|
161
|
+
|
|
162
|
+
A sink reads one step of one job, and gives nothing back:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import type { JobEvent, Sink } from '@artian-techs/adonis-orbit'
|
|
166
|
+
|
|
167
|
+
class MySink implements Sink {
|
|
168
|
+
async write(events: JobEvent[]) {
|
|
169
|
+
// { type: 'dispatched' | 'started' | 'finished', at, job: { id, name, queue, payload, attempts } }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The events of one step arrive together: a dispatch of ten jobs calls `write`
|
|
175
|
+
once, with ten events. A sink that fails is written in the log, and the job
|
|
176
|
+
that runs goes on.
|
|
177
|
+
|
|
178
|
+
## What of the payload is kept
|
|
179
|
+
|
|
180
|
+
A payload holds what a person wrote: an address, a name, a number. The
|
|
181
|
+
configuration says how much of it the trace keeps.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
export default defineConfig({
|
|
185
|
+
// ...
|
|
186
|
+
|
|
187
|
+
/** 'full' by default. 'none' keeps nothing. */
|
|
188
|
+
payload: (payload, job) => ({ ...payload, password: undefined }),
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The rule is applied before the first sink reads the event, thus what it takes
|
|
193
|
+
out reaches nothing and nobody.
|
|
194
|
+
|
|
195
|
+
## What the table holds
|
|
196
|
+
|
|
197
|
+
One row for one job, completed at each step.
|
|
198
|
+
|
|
199
|
+
| Status | What it says |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| `pending` | the job waits for a worker |
|
|
202
|
+
| `running` | a worker holds it |
|
|
203
|
+
| `retrying` | a try failed, the next one is set |
|
|
204
|
+
| `completed` | done |
|
|
205
|
+
| `failed` | every try fell |
|
|
206
|
+
|
|
207
|
+
The row that the dispatch writes and the row that the worker writes are the
|
|
208
|
+
same row. Either of the two can arrive first, thus the first one writes and the
|
|
209
|
+
second one completes.
|
|
210
|
+
|
|
211
|
+
## Ask a job again
|
|
212
|
+
|
|
213
|
+
From the page, with the button on a row that failed. Or with the job that the
|
|
214
|
+
package carries:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { JobRun, JobRunStatus, retryRun } from '@artian-techs/adonis-orbit'
|
|
218
|
+
|
|
219
|
+
const runs = await JobRun.query().where('status', JobRunStatus.FAILED)
|
|
220
|
+
|
|
221
|
+
for (const run of runs) {
|
|
222
|
+
await retryRun(run)
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The row is never replaced: the new job takes a new identifier and opens its own
|
|
227
|
+
row, and the old one names it.
|
|
228
|
+
|
|
229
|
+
## Tests
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pnpm test
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The tests read a PostgreSQL server, as the package does. The environment names
|
|
236
|
+
it, and without it the tests read `127.0.0.1:5432` and the database
|
|
237
|
+
`orbit_test`:
|
|
238
|
+
|
|
239
|
+
| Variable | What it names |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| `PG_HOST` | the machine, `127.0.0.1` |
|
|
242
|
+
| `PG_PORT` | the port, `5432` |
|
|
243
|
+
| `PG_USER` | the name, `postgres` |
|
|
244
|
+
| `PG_PASSWORD` | the word, `postgres` |
|
|
245
|
+
| `PG_DATABASE` | the database, `orbit_test` |
|
|
246
|
+
|
|
247
|
+
A server for the tests alone:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
docker run -d --name orbit-test-pg -p 55432:5432 \
|
|
251
|
+
-e POSTGRES_USER=orbit -e POSTGRES_PASSWORD=orbit -e POSTGRES_DB=orbit_test \
|
|
252
|
+
postgres:16
|
|
253
|
+
|
|
254
|
+
PG_PORT=55432 PG_USER=orbit PG_PASSWORD=orbit pnpm test
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The tests create the two tables from the stubs of `stubs/migrations`, and take
|
|
258
|
+
them away at the end.
|
|
259
|
+
|
|
260
|
+
`pnpm preview` opens the page on `http://localhost:3333/queue`, with jobs of
|
|
261
|
+
every status to read and the name `orbit` and the word `orbit`. It reads a
|
|
262
|
+
database of its own, `orbit_preview`, and makes it if it is not there.
|
|
263
|
+
|
|
264
|
+
## Take out the traces that ended well
|
|
265
|
+
|
|
266
|
+
The package carries `PruneJobRuns` and names it to the queue itself. Give it a
|
|
267
|
+
place in the schedule of the application:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { PruneJobRuns } from '@artian-techs/adonis-orbit'
|
|
271
|
+
|
|
272
|
+
await PruneJobRuns.schedule({ days: 7 }).cron('10 0 * * *').run()
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
A row that failed is never taken out.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type Configure from '@adonisjs/core/commands/configure';
|
|
2
|
+
/**
|
|
3
|
+
* Put Orbit in an application.
|
|
4
|
+
*
|
|
5
|
+
* The table belongs to the application, thus the migration is published there
|
|
6
|
+
* and read like the others.
|
|
7
|
+
*/
|
|
8
|
+
export declare function configure(command: Configure): Promise<void>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { stubsRoot } from './stubs/main.js';
|
|
2
|
+
/**
|
|
3
|
+
* Put Orbit in an application.
|
|
4
|
+
*
|
|
5
|
+
* The table belongs to the application, thus the migration is published there
|
|
6
|
+
* and read like the others.
|
|
7
|
+
*/
|
|
8
|
+
export async function configure(command) {
|
|
9
|
+
const codemods = await command.createCodemods();
|
|
10
|
+
await codemods.updateRcFile((rcFile) => {
|
|
11
|
+
rcFile.addProvider('@artian-techs/adonis-orbit/orbit_provider');
|
|
12
|
+
});
|
|
13
|
+
await codemods.makeUsingStub(stubsRoot, 'config/orbit.stub', {});
|
|
14
|
+
const now = new Date().getTime();
|
|
15
|
+
await codemods.makeUsingStub(stubsRoot, 'migrations/create_job_runs_table.stub', {
|
|
16
|
+
migration: {
|
|
17
|
+
folder: 'database/migrations',
|
|
18
|
+
fileName: `${now}_create_job_runs_table.ts`,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
await codemods.makeUsingStub(stubsRoot, 'migrations/create_silenced_jobs_table.stub', {
|
|
22
|
+
migration: {
|
|
23
|
+
folder: 'database/migrations',
|
|
24
|
+
fileName: `${now + 1}_create_silenced_jobs_table.ts`,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
await codemods.defineEnvVariables({
|
|
28
|
+
ORBIT_DASHBOARD_USER: '',
|
|
29
|
+
ORBIT_DASHBOARD_PASSWORD: '',
|
|
30
|
+
});
|
|
31
|
+
await codemods.defineEnvValidations({
|
|
32
|
+
variables: {
|
|
33
|
+
ORBIT_DASHBOARD_USER: 'Env.schema.string.optional()',
|
|
34
|
+
ORBIT_DASHBOARD_PASSWORD: 'Env.schema.string.optional()',
|
|
35
|
+
},
|
|
36
|
+
leadingComment: 'Variables for the page of the queue',
|
|
37
|
+
});
|
|
38
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { defineConfig } from './src/define_config.js';
|
|
2
|
+
export { basicAuth } from './src/auth/basic_auth.js';
|
|
3
|
+
export { default as DatabaseSink } from './src/sinks/database_sink.js';
|
|
4
|
+
export type { Authorize, JobEvent, JobIdentity, JobOutcome, OrbitConfig, PayloadRule, Sink, } from './src/types.js';
|
|
5
|
+
export { default as JobRun, JobRunStatus, type JobRunStatusValue } from './src/models/job_run.js';
|
|
6
|
+
export { default as SilencedJob } from './src/models/silenced_job.js';
|
|
7
|
+
export { default as PruneJobRuns } from './src/jobs/prune_job_runs.js';
|
|
8
|
+
export { retryRun } from './src/retry.js';
|
|
9
|
+
export { stubsRoot } from './stubs/main.js';
|
|
10
|
+
export { configure } from './configure.js';
|
package/build/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { defineConfig } from './src/define_config.js';
|
|
2
|
+
export { basicAuth } from './src/auth/basic_auth.js';
|
|
3
|
+
export { default as DatabaseSink } from './src/sinks/database_sink.js';
|
|
4
|
+
export { default as JobRun, JobRunStatus } from './src/models/job_run.js';
|
|
5
|
+
export { default as SilencedJob } from './src/models/silenced_job.js';
|
|
6
|
+
export { default as PruneJobRuns } from './src/jobs/prune_job_runs.js';
|
|
7
|
+
export { retryRun } from './src/retry.js';
|
|
8
|
+
export { stubsRoot } from './stubs/main.js';
|
|
9
|
+
export { configure } from './configure.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Watch what the queue does, and keep it.
|
|
4
|
+
*
|
|
5
|
+
* The provider listens to the channels of the queue, writes each job in the
|
|
6
|
+
* table, and opens the page that reads it. The application gives it a path and
|
|
7
|
+
* the two words that guard the page, and nothing else.
|
|
8
|
+
*/
|
|
9
|
+
export default class OrbitProvider {
|
|
10
|
+
protected app: ApplicationService;
|
|
11
|
+
constructor(app: ApplicationService);
|
|
12
|
+
boot(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Locator } from '@adonisjs/queue';
|
|
2
|
+
import edge from 'edge.js';
|
|
3
|
+
import PruneJobRuns from '../src/jobs/prune_job_runs.js';
|
|
4
|
+
const DEFAULTS = { path: '/queue', authorize: () => false, sinks: [] };
|
|
5
|
+
/**
|
|
6
|
+
* Watch what the queue does, and keep it.
|
|
7
|
+
*
|
|
8
|
+
* The provider listens to the channels of the queue, writes each job in the
|
|
9
|
+
* table, and opens the page that reads it. The application gives it a path and
|
|
10
|
+
* the two words that guard the page, and nothing else.
|
|
11
|
+
*/
|
|
12
|
+
export default class OrbitProvider {
|
|
13
|
+
app;
|
|
14
|
+
constructor(app) {
|
|
15
|
+
this.app = app;
|
|
16
|
+
}
|
|
17
|
+
async boot() {
|
|
18
|
+
const config = this.app.config.get('orbit', DEFAULTS);
|
|
19
|
+
/**
|
|
20
|
+
* The page of Orbit lives with Orbit, thus the application carries no
|
|
21
|
+
* view for it.
|
|
22
|
+
*/
|
|
23
|
+
edge.mount('orbit', new URL('../templates/', import.meta.url));
|
|
24
|
+
/**
|
|
25
|
+
* The application looks for its jobs in its own folders, and this one
|
|
26
|
+
* lives here. It names itself to the queue.
|
|
27
|
+
*/
|
|
28
|
+
Locator.register(PruneJobRuns.name, PruneJobRuns);
|
|
29
|
+
const { recordJobRuns } = await import('../src/recorder.js');
|
|
30
|
+
recordJobRuns(config);
|
|
31
|
+
const router = await this.app.container.make('router');
|
|
32
|
+
const { registerRoutes } = await import('../src/routes.js');
|
|
33
|
+
registerRoutes(router, config);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Authorize } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read the page with a name and a word, and no session.
|
|
4
|
+
*
|
|
5
|
+
* The page belongs to the team that runs the machine, not to the users of the
|
|
6
|
+
* application. It shows nothing at all while one of the two is missing, thus a
|
|
7
|
+
* machine that carries neither shows nothing.
|
|
8
|
+
*/
|
|
9
|
+
export declare function basicAuth(credentials: {
|
|
10
|
+
user?: string;
|
|
11
|
+
password?: string;
|
|
12
|
+
}): Authorize;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Compare the two words in a time that does not change with the number of
|
|
4
|
+
* letters that agree.
|
|
5
|
+
*/
|
|
6
|
+
function matches(given, expected) {
|
|
7
|
+
if (!given) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const a = Buffer.from(given);
|
|
11
|
+
const b = Buffer.from(expected);
|
|
12
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read the page with a name and a word, and no session.
|
|
16
|
+
*
|
|
17
|
+
* The page belongs to the team that runs the machine, not to the users of the
|
|
18
|
+
* application. It shows nothing at all while one of the two is missing, thus a
|
|
19
|
+
* machine that carries neither shows nothing.
|
|
20
|
+
*/
|
|
21
|
+
export function basicAuth(credentials) {
|
|
22
|
+
return ({ request, response }) => {
|
|
23
|
+
const { user, password } = credentials;
|
|
24
|
+
if (!user || !password) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const [scheme, given] = (request.header('authorization') ?? '').split(' ');
|
|
28
|
+
const expected = Buffer.from(`${user}:${password}`).toString('base64');
|
|
29
|
+
if (scheme !== 'Basic' || !matches(given, expected)) {
|
|
30
|
+
response.header('www-authenticate', 'Basic realm="Queue"');
|
|
31
|
+
response.unauthorized({ success: false, error: 'Authentication is required' });
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/**
|
|
3
|
+
* The page that shows what the queue does.
|
|
4
|
+
*
|
|
5
|
+
* It reads the table, not Redis: a job that ended left Redis, and the table
|
|
6
|
+
* keeps it. The page is served by the API itself, outside the front, thus it
|
|
7
|
+
* answers even when the front is down.
|
|
8
|
+
*/
|
|
9
|
+
export default class DashboardController {
|
|
10
|
+
#private;
|
|
11
|
+
/**
|
|
12
|
+
* Everything the queue did, and the four numbers that say how it goes.
|
|
13
|
+
*
|
|
14
|
+
* The side menu leads here with a status. A row that was asked again leads
|
|
15
|
+
* here with the identifier of the job that answered. Without one and without
|
|
16
|
+
* the other, the page shows the numbers and the last jobs of every status.
|
|
17
|
+
*/
|
|
18
|
+
index(ctx: HttpContext): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* The classes that the lists leave out.
|
|
21
|
+
*
|
|
22
|
+
* Their work goes on and their trace is written. Take a name away from here
|
|
23
|
+
* and everything it did comes back, including what it did while it was
|
|
24
|
+
* silent.
|
|
25
|
+
*/
|
|
26
|
+
silenced(ctx: HttpContext): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* What each class and each queue cost in the last day.
|
|
29
|
+
*
|
|
30
|
+
* A job that takes long, or that fails often, is read here before it is read
|
|
31
|
+
* in the list.
|
|
32
|
+
*/
|
|
33
|
+
metrics(ctx: HttpContext): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Ask the queue for one job again.
|
|
36
|
+
*
|
|
37
|
+
* The row that answers stays where it is. The new job opens a row of its
|
|
38
|
+
* own, thus the page shows the two.
|
|
39
|
+
*/
|
|
40
|
+
retry({ params, response }: HttpContext): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Read the page in the light, or in the dark.
|
|
43
|
+
*
|
|
44
|
+
* The choice is held in a cookie, thus the page carries it on the next
|
|
45
|
+
* request and no script reads it. A person who chose nothing reads the mode
|
|
46
|
+
* of their machine.
|
|
47
|
+
*/
|
|
48
|
+
theme({ params, response }: HttpContext): Promise<void>;
|
|
49
|
+
/** Take a class out of the lists, or put it back. */
|
|
50
|
+
silence({ params, response }: HttpContext): Promise<void>;
|
|
51
|
+
}
|