@andrian.yablonskyy/thub-coordinator 1.0.20 → 1.0.21
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/package.json +1 -1
- package/public/js/log-viewer.js +23 -2
- package/src/web/routes.js +5 -3
- package/views/mixins/log-viewer.pug +1 -0
- package/views/mixins/resource.pug +30 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrian.yablonskyy/thub-coordinator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "TestHub Coordinator — job queue, resource registry, scheduler, heartbeat monitor, log/artifact store and web dashboard",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"engines": {
|
package/public/js/log-viewer.js
CHANGED
|
@@ -21,13 +21,34 @@
|
|
|
21
21
|
const jobId = pre.dataset.jobId,
|
|
22
22
|
jobActive = pre.dataset.jobActive === 'true',
|
|
23
23
|
filter = document.getElementById('stream-filter'),
|
|
24
|
-
lines = []
|
|
24
|
+
lines = [],
|
|
25
|
+
// Same dd/mm/yyyy HH:MM:SS, 24-hour format and profile time zone as
|
|
26
|
+
// every server-rendered date (thub-common formatDateTime).
|
|
27
|
+
timeFormat = new Intl.DateTimeFormat('en-GB', {
|
|
28
|
+
timeZone: pre.dataset.timeZone || undefined,
|
|
29
|
+
day: '2-digit',
|
|
30
|
+
month: '2-digit',
|
|
31
|
+
year: 'numeric',
|
|
32
|
+
hour: '2-digit',
|
|
33
|
+
minute: '2-digit',
|
|
34
|
+
second: '2-digit',
|
|
35
|
+
hourCycle: 'h23'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function fmtTime(iso){
|
|
39
|
+
const d = new Date(iso);
|
|
40
|
+
if (Number.isNaN(d.getTime())){
|
|
41
|
+
return iso;
|
|
42
|
+
}
|
|
43
|
+
const p = Object.fromEntries(timeFormat.formatToParts(d).map(({ type, value }) => [type, value]));
|
|
44
|
+
return `${p.day}/${p.month}/${p.year} ${p.hour}:${p.minute}:${p.second}`;
|
|
45
|
+
}
|
|
25
46
|
|
|
26
47
|
function render(){
|
|
27
48
|
const wanted = filter.value;
|
|
28
49
|
pre.textContent = lines
|
|
29
50
|
.filter((l) => !wanted || l.stream === wanted)
|
|
30
|
-
.map((l) => `[${l.ts}] [${l.stream}] ${l.line}`)
|
|
51
|
+
.map((l) => `[${fmtTime(l.ts)}] [${l.stream}] ${l.line}`)
|
|
31
52
|
.join('\n');
|
|
32
53
|
pre.scrollTop = pre.scrollHeight;
|
|
33
54
|
}
|
package/src/web/routes.js
CHANGED
|
@@ -20,7 +20,7 @@ const fs = require('node:fs'),
|
|
|
20
20
|
multer = require('multer'),
|
|
21
21
|
{ requireAdminSession, requireAdminRole } = require('../auth'),
|
|
22
22
|
{ attachJobStream } = require('../api/sse'),
|
|
23
|
-
{ RESOURCE_STATES, JOB_STATES, ACTIVE_JOB_STATES, isNewer, compareVersions } = require('@andrian.yablonskyy/thub-common'),
|
|
23
|
+
{ RESOURCE_STATES, JOB_STATES, ACTIVE_JOB_STATES, isNewer, compareVersions, formatDateTime } = require('@andrian.yablonskyy/thub-common'),
|
|
24
24
|
listPrefs = require('../services/list-prefs');
|
|
25
25
|
|
|
26
26
|
// Resources page sort keys (list-prefs.js) -> comparators. Empty values
|
|
@@ -133,8 +133,10 @@ function createWebRouter({ services, config }){
|
|
|
133
133
|
m = Math.floor((s % 3600) / 60);
|
|
134
134
|
return d ? `${d}d ${h}h` : h ? `${h}h ${m}m` : m ? `${m}m ${s % 60}s` : `${s}s`;
|
|
135
135
|
};
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
// dd/mm/yyyy HH:MM:SS, 24-hour (thub-common formatDateTime) — the one
|
|
137
|
+
// format for every date/time label.
|
|
138
|
+
res.locals.fmtDate = (iso, fallback = '—') => formatDateTime(iso, { timeZone: tz, fallback });
|
|
139
|
+
res.locals.userTimeZone = tz;
|
|
138
140
|
|
|
139
141
|
// Idle timeout (§10.1): re-applied on every request (not just login)
|
|
140
142
|
// so a mid-session profile change takes effect immediately, and so
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
//- Overview and Resources pages. `returnTo` is the page the action forms
|
|
3
3
|
//- redirect back to.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
//- `opts.update: false` leaves out the Update button — the resource card
|
|
6
|
+
//- shows it on its Client version line instead.
|
|
7
|
+
mixin resourceActions(r, returnTo, opts = {})
|
|
6
8
|
form.d-inline(method="post" action=`/resources/${r.id}/maintenance`)
|
|
7
9
|
input(type="hidden" name="enabled" value=r.status === 'MAINTENANCE' ? '0' : '1')
|
|
8
10
|
input(type="hidden" name="returnTo" value=returnTo)
|
|
@@ -31,7 +33,8 @@ mixin resourceActions(r, returnTo)
|
|
|
31
33
|
data-bs-title="Invalidate the current resource token and issue a new one — the Client won't reconnect until it has it"
|
|
32
34
|
)
|
|
33
35
|
i.bi.bi-arrow-repeat
|
|
34
|
-
|
|
36
|
+
if opts.update !== false
|
|
37
|
+
+updateButton(`/resources/${r.id}/update`, r.update_to, returnTo)
|
|
35
38
|
+removeResourceButton(r, returnTo)
|
|
36
39
|
|
|
37
40
|
//- Remove: a plain confirm when the resource is idle; with a running job,
|
|
@@ -168,7 +171,8 @@ mixin capabilitiesList(caps)
|
|
|
168
171
|
//- Host uptime and the Client's current task/status with its duration,
|
|
169
172
|
//- both from the last heartbeat. Measured up to now — or, for a Client
|
|
170
173
|
//- that stopped heartbeating, up to its last heartbeat.
|
|
171
|
-
|
|
174
|
+
//- `returnTo`: where the Task / status line's Cancel button redirects.
|
|
175
|
+
mixin uptimeRows(r, returnTo)
|
|
172
176
|
- const stale = r.status === 'OUT_OF_SERVICE' && r.last_heartbeat_at
|
|
173
177
|
- const ref = stale ? new Date(r.last_heartbeat_at).getTime() : Date.now()
|
|
174
178
|
- const activityLabel = { idle: 'Idle', job: 'Running job', locked: 'Locked locally', 'update-hold': 'Holding for self-update' }
|
|
@@ -193,27 +197,30 @@ mixin uptimeRows(r)
|
|
|
193
197
|
span.text-body-secondary.small.ms-1 (at last heartbeat)
|
|
194
198
|
else
|
|
195
199
|
span.text-body-secondary —
|
|
200
|
+
if user.role === 'admin'
|
|
201
|
+
+cancelActivityButton(r, returnTo)
|
|
196
202
|
|
|
197
203
|
//- Cancel the Client's current activity (its job, a local lock or a
|
|
198
|
-
//- self-update hold)
|
|
204
|
+
//- self-update hold) — icon only, on the card's Task / status line, and
|
|
205
|
+
//- only while there's something to cancel.
|
|
199
206
|
mixin cancelActivityButton(r, returnTo)
|
|
200
207
|
- const state = r.activity && r.activity.state
|
|
201
208
|
- const what = r.activeJob ? `job ${r.activeJob.id}` : state === 'locked' ? 'the local lock' : state === 'update-hold' ? 'the self-update' : null
|
|
202
209
|
if what
|
|
203
|
-
form.d-inline.ms-
|
|
210
|
+
form.d-inline.ms-2(
|
|
204
211
|
method="post"
|
|
205
212
|
action=`/resources/${r.id}/cancel-activity`
|
|
206
|
-
|
|
213
|
+
data-confirm=`Cancel ${what} on ${r.name}?`
|
|
214
|
+
onsubmit="return confirm(this.dataset.confirm)"
|
|
207
215
|
)
|
|
208
216
|
input(type="hidden" name="returnTo" value=returnTo)
|
|
209
|
-
button.btn.btn-sm.btn-outline-danger(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
i.bi.bi-x-octagon
|
|
216
|
-
| Cancel
|
|
217
|
+
button.btn.btn-sm.btn-outline-danger(
|
|
218
|
+
type="submit"
|
|
219
|
+
aria-label=`Cancel ${what}`
|
|
220
|
+
data-bs-toggle="tooltip"
|
|
221
|
+
data-bs-title=`Cancel ${what}`
|
|
222
|
+
)
|
|
223
|
+
i.bi.bi-x-octagon
|
|
217
224
|
|
|
218
225
|
mixin resourceCard(r, groupsById, returnTo)
|
|
219
226
|
- const isLoopback = (a) => /^127\./.test(a) || a === '::1' || a === 'localhost'
|
|
@@ -233,9 +240,15 @@ mixin resourceCard(r, groupsById, returnTo)
|
|
|
233
240
|
dd.col-sm-8.text-uppercase= r.type
|
|
234
241
|
dt.col-sm-4 Status
|
|
235
242
|
dd.col-sm-8: +resourceBadge(r.status, r.remove_requested_at)
|
|
236
|
-
+uptimeRows(r)
|
|
243
|
+
+uptimeRows(r, returnTo)
|
|
237
244
|
dt.col-sm-4 Client version
|
|
238
|
-
dd.col-sm-8
|
|
245
|
+
dd.col-sm-8
|
|
246
|
+
+versionCell(r.client_version, updates.latest.client, r.update_to)
|
|
247
|
+
//- Admins: Update only when a newer Client is published (or
|
|
248
|
+
//- Cancel, while one is requested). An unknown version (too
|
|
249
|
+
//- old to report it) can't be compared, so no button.
|
|
250
|
+
if user.role === 'admin' && (r.update_to || (r.client_version && isNewer(updates.latest.client, r.client_version)))
|
|
251
|
+
+updateButton(`/resources/${r.id}/update`, r.update_to, returnTo)
|
|
239
252
|
dt.col-sm-4 IP addresses
|
|
240
253
|
dd.col-sm-8
|
|
241
254
|
if !local.length && !external
|
|
@@ -276,5 +289,4 @@ mixin resourceCard(r, groupsById, returnTo)
|
|
|
276
289
|
if user.role === 'admin'
|
|
277
290
|
dt.col-sm-4 Actions
|
|
278
291
|
dd.col-sm-8.mb-0
|
|
279
|
-
+resourceActions(r, returnTo)
|
|
280
|
-
+cancelActivityButton(r, returnTo)
|
|
292
|
+
+resourceActions(r, returnTo, { update: false })
|