@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrian.yablonskyy/thub-coordinator",
3
- "version": "1.0.20",
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": {
@@ -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
- res.locals.fmtDate = (iso, fallback = '—') =>
137
- iso ? new Date(iso).toLocaleString('en-US', { timeZone: tz }) : fallback;
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
@@ -10,5 +10,6 @@ mixin logViewer(jobId, jobActive)
10
10
  style="height: 420px; overflow-y: auto;"
11
11
  data-job-id=jobId
12
12
  data-job-active=jobActive ? 'true' : 'false'
13
+ data-time-zone=userTimeZone
13
14
  )
14
15
  script(src="/js/log-viewer.js")
@@ -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
- mixin resourceActions(r, returnTo)
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
- +updateButton(`/resources/${r.id}/update`, r.update_to, returnTo)
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
- mixin uptimeRows(r)
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); disabled while it's idle.
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-1(
210
+ form.d-inline.ms-2(
204
211
  method="post"
205
212
  action=`/resources/${r.id}/cancel-activity`
206
- onsubmit=`return confirm('Cancel ${what} on ${r.name}?')`
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(type="submit" data-bs-toggle="tooltip" data-bs-title=`Cancel ${what}`)
210
- i.bi.bi-x-octagon.me-1
211
- | Cancel
212
- else
213
- span.d-inline-block.ms-1(tabindex="0" data-bs-toggle="tooltip" data-bs-title="Nothing to cancel — the Client is idle")
214
- button.btn.btn-sm.btn-outline-danger(type="button" disabled style="pointer-events: none;")
215
- i.bi.bi-x-octagon.me-1
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: +versionCell(r.client_version, updates.latest.client, r.update_to)
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 })