@andrian.yablonskyy/thub-coordinator 1.0.39 → 1.0.41

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.
Files changed (45) hide show
  1. package/bin/thub-admin.js +23 -0
  2. package/config.json +4 -0
  3. package/package.json +2 -2
  4. package/public/js/actions.js +58 -0
  5. package/public/js/settings.js +66 -0
  6. package/src/api/agent.js +27 -15
  7. package/src/api/resource.js +5 -3
  8. package/src/config.js +17 -1
  9. package/src/db/migrations/024_add_settings.sql +10 -0
  10. package/src/db/migrations/025_add_job_artifacts.sql +4 -0
  11. package/src/dev/virtual.js +9 -5
  12. package/src/login-guard.js +111 -0
  13. package/src/rate-limit.js +137 -0
  14. package/src/security.js +106 -0
  15. package/src/server.js +17 -3
  16. package/src/services/admin-users.js +32 -19
  17. package/src/services/heartbeat.js +23 -3
  18. package/src/services/job-artifacts.js +91 -0
  19. package/src/services/jobs.js +13 -2
  20. package/src/services/registry.js +34 -0
  21. package/src/services/settings.js +345 -0
  22. package/src/web/routes.js +157 -10
  23. package/test/admin-users.test.js +16 -16
  24. package/test/agent-visibility.test.js +85 -0
  25. package/test/groups-tab.test.js +97 -0
  26. package/test/job-artifacts.test.js +115 -0
  27. package/test/login-guard.test.js +139 -0
  28. package/test/logs.test.js +1 -1
  29. package/test/rate-limit.test.js +106 -0
  30. package/test/security.test.js +108 -0
  31. package/test/settings.test.js +206 -0
  32. package/views/admin/settings.pug +133 -0
  33. package/views/groups/list.pug +1 -1
  34. package/views/help/_agent-cli.pug +1 -1
  35. package/views/help/_agent-setup.pug +4 -1
  36. package/views/help/_client-setup.pug +1 -1
  37. package/views/help/_coordinator.pug +9 -1
  38. package/views/help/_env.pug +1 -0
  39. package/views/help/_troubleshooting.pug +3 -1
  40. package/views/jobs/list.pug +3 -3
  41. package/views/jobs/show.pug +36 -0
  42. package/views/layout.pug +4 -1
  43. package/views/mixins/client-config.pug +65 -3
  44. package/views/mixins/list-controls.pug +1 -1
  45. package/views/mixins/resource.pug +1 -8
package/views/layout.pug CHANGED
@@ -43,6 +43,8 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
43
43
  if user.role === 'admin'
44
44
  li.nav-item
45
45
  a.nav-link(href="/admin/agents" class=active === 'agents' ? 'active' : '') Agents
46
+ li.nav-item
47
+ a.nav-link(href="/admin/settings" class=active === 'settings' ? 'active' : '') Settings
46
48
  ul.navbar-nav
47
49
  if user && liveTopics
48
50
  //- Live-update status, driven by public/js/live.js.
@@ -64,7 +66,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
64
66
  | Help
65
67
  if user && user.role === 'admin'
66
68
  li.nav-item.d-flex.align-items-center.me-2
67
- form(method="post" action="/updates/check" onsubmit="this.querySelector('button').disabled = true; this.querySelector('i').className = 'spinner-border spinner-border-sm';")
69
+ form(method="post" action="/updates/check" data-submit-busy)
68
70
  input(type="hidden" name="returnTo" value=currentPath)
69
71
  button.btn.btn-sm.btn-outline-secondary(
70
72
  type="submit"
@@ -192,6 +194,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
192
194
  script(src="/js/copy-to-clipboard.js")
193
195
  script(src="/js/confirm.js")
194
196
  script(src="/js/list-search.js")
197
+ script(src="/js/actions.js")
195
198
  if user && liveTopics
196
199
  script(src="/js/live.js")
197
200
  if user && user.role === 'admin' && updates.coordinatorPending
@@ -88,8 +88,8 @@ mixin capabilitiesStatus(r)
88
88
  span.badge.text-bg-danger(data-bs-toggle="tooltip" data-bs-title=r.config_error) refused by the Client
89
89
 
90
90
  //- The Client's config, as tabs of the resource card next to Details:
91
- //- HW — ST-Link, UART, DUT USB (its hw-devices section); both — Reboot (the
92
- //- schedule, its own Save). An SW Client has no settings of its own. The
91
+ //- HW — ST-Link, UART, DUT USB (its hw-devices section); both — Groups
92
+ //- (membership, its own Save) and Reboot (the schedule, its own Save). An SW Client has no settings of its own. The
93
93
  //- device tabs share one form and one "Save capabilities"; a tab with
94
94
  //- unsaved changes shows ⚠ (public/js/client-config.js).
95
95
  mixin configTabItems(r)
@@ -97,9 +97,70 @@ mixin configTabItems(r)
97
97
  if r.type === 'hw'
98
98
  each t in [['stlinks', 'ST-Link'], ['uarts', 'UART'], ['usbs', 'DUT USB']]
99
99
  +configTab(p, t[0], t[1])
100
+ +configTab(p, 'groups', 'Groups')
100
101
  +configTab(p, 'reboot', 'Reboot')
101
102
 
102
- mixin configPanes(r, returnTo)
103
+ //- Groups tab: which resource groups this Client belongs to (thub run
104
+ //- --group). Saved as a config revision the Client writes to its config
105
+ //- file's `groups` (registry.setClientGroups); in effect on the Coordinator
106
+ //- at once.
107
+ mixin groupsPane(r, returnTo, groupsById)
108
+ - const p = `cfg-${r.id}`
109
+ - const canEdit = user.role === 'admin'
110
+ - const pending = r.config_import && r.config_import.revision > r.config_applied_revision && r.config_import.fields && r.config_import.fields.groups
111
+ - const all = Object.values(groupsById).sort((a, b) => a.name.localeCompare(b.name))
112
+ - const unknown = r.group_ids.filter((id) => !groupsById[id])
113
+ +configPane(p, 'groups')
114
+ if pending || r.config_error
115
+ .d-flex.justify-content-end.gap-1.mb-2
116
+ if pending
117
+ span.badge.text-bg-warning(
118
+ data-bs-toggle="tooltip"
119
+ data-bs-title="In effect on the Coordinator; the Client writes it to its config file on its next heartbeat, then restarts once idle"
120
+ ) not written to the Client yet
121
+ if r.config_error
122
+ span.badge.text-bg-danger(data-bs-toggle="tooltip" data-bs-title=r.config_error) refused by the Client
123
+ if !r.client_config_file
124
+ p.small.text-body-secondary This Client is too old to take its groups from the dashboard — update it, or set #[code groups] in its config file.
125
+ if !all.length && !unknown.length
126
+ p.small.text-body-secondary.mb-0
127
+ | No groups exist yet.
128
+ if canEdit
129
+ | Create one on #[a(href="/groups") Groups].
130
+ else
131
+ form(
132
+ method="post"
133
+ action=`/resources/${r.id}/groups`
134
+ data-confirm=`Save the groups of ${r.name}? They apply at once; the Client also writes them to its config file and restarts once it has no job running.`
135
+ data-confirm-title="Save Client groups?"
136
+ data-confirm-ok="Save"
137
+ data-confirm-tone="primary"
138
+ )
139
+ input(type="hidden" name="returnTo" value=returnTo)
140
+ fieldset(disabled=!canEdit || !r.client_config_file)
141
+ .list-group.mb-2
142
+ each g in all
143
+ label.list-group-item.d-flex.gap-2.align-items-start
144
+ input.form-check-input.flex-shrink-0.mt-1(type="checkbox" name="groups" value=g.id checked=r.group_ids.includes(g.id))
145
+ span
146
+ span.fw-medium= g.name
147
+ if g.comment
148
+ span.small.text-body-secondary= ` — ${g.comment}`
149
+ br
150
+ code.small.text-body-secondary= g.id
151
+ each id in unknown
152
+ label.list-group-item.d-flex.gap-2.align-items-start
153
+ input.form-check-input.flex-shrink-0.mt-1(type="checkbox" name="groups" value=id checked)
154
+ span
155
+ span.text-body-secondary (deleted group)
156
+ br
157
+ code.small.text-body-secondary= id
158
+ .form-text A job run with #[code thub run --group <id>] is scheduled only on Clients in that group; one that asks for no group can run on any Client.
159
+ if canEdit && r.client_config_file
160
+ .d-flex.justify-content-end.mt-3
161
+ button.btn.btn-sm.btn-primary(type="submit") Save groups
162
+
163
+ mixin configPanes(r, returnTo, groupsById)
103
164
  - const p = `cfg-${r.id}`
104
165
  - const current = r.config_desired || r.client_config
105
166
  - const canEdit = user.role === 'admin'
@@ -137,6 +198,7 @@ mixin configPanes(r, returnTo)
137
198
  +deviceTable('usbs', 'DUT USB devices', current.usbs || [], ['Device', 'udev devpath'])
138
199
  .form-text Device: a udev index 1–8 (→ /dev/thub/dut<N>-usb) or a /dev path. udev devpath: the USB port it's plugged into.
139
200
  +saveCapabilities(canEdit)
201
+ +groupsPane(r, returnTo, groupsById || {})
140
202
  +configPane(p, 'reboot')
141
203
  dl.row.mb-0
142
204
  +rebootScheduleRow(r, returnTo)
@@ -11,7 +11,7 @@ mixin pageSizePicker(list)
11
11
  label.small.text-body-secondary.mb-0.text-nowrap(for="list-page-size") Show
12
12
  select.form-select.form-select-sm.w-auto#list-page-size(
13
13
  aria-label="Records per page"
14
- onchange="location.href = this.value"
14
+ data-navigate
15
15
  )
16
16
  each s in list.pageSizes
17
17
  option(value=list.urlFor({ size: s }) selected=list.prefs.size === s)= s === 'all' ? 'All' : s
@@ -324,7 +324,7 @@ mixin resourceCard(r, groupsById, returnTo)
324
324
  .tab-content
325
325
  .tab-pane.fade.show.active(id=`rc-${r.id}-details` role="tabpanel" aria-labelledby=`rc-${r.id}-details-tab` tabindex="0" data-live=`rc-details-${r.id}`)
326
326
  +resourceDetails(r, groupsById, returnTo, local, external)
327
- +configPanes(r, returnTo)
327
+ +configPanes(r, returnTo, groupsById)
328
328
  .tab-pane.fade(id=`rc-${r.id}-usb` role="tabpanel" aria-labelledby=`rc-${r.id}-usb-tab` tabindex="0")
329
329
  +usbScanPane(r)
330
330
 
@@ -509,13 +509,6 @@ mixin resourceDetails(r, groupsById, returnTo, local, external)
509
509
  span.badge.text-bg-light.border.me-1= l
510
510
  else
511
511
  span.text-body-secondary —
512
- dt.col-sm-4 Groups
513
- dd.col-sm-8
514
- if r.group_ids.length
515
- each gid in r.group_ids
516
- span.badge.text-bg-info.me-1(data-bs-toggle="tooltip" data-bs-title=gid)= groupsById[gid] ? groupsById[gid].name : `(unknown: ${gid})`
517
- else
518
- span.text-body-secondary —
519
512
  dt.col-sm-4 Busy source / reason
520
513
  dd.col-sm-8= r.busy_source ? `${r.busy_source}${r.busy_reason ? ' — ' + r.busy_reason : ''}` : '—'
521
514
  dt.col-sm-4 Last heartbeat