@andrian.yablonskyy/thub-coordinator 1.0.42 → 1.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/bin/thub-admin.js +98 -7
- package/package.json +2 -2
- package/public/css/thub.css +108 -0
- package/public/js/actions.js +33 -0
- package/public/js/tooltips.js +5 -0
- package/src/api/agent.js +36 -9
- package/src/auth.js +39 -6
- package/src/db/migrations/026_users.sql +74 -0
- package/src/server.js +1 -1
- package/src/services/admin-users.js +291 -39
- package/src/services/agents.js +1 -1
- package/src/services/list-prefs.js +4 -0
- package/src/web/routes.js +213 -32
- package/test/admin-users.test.js +4 -4
- package/test/groups-tab.test.js +3 -3
- package/test/users.test.js +269 -0
- package/test/views.test.js +3 -1
- package/views/admin/agents.pug +7 -8
- package/views/admin/users.pug +249 -0
- package/views/help/_agent-cli.pug +4 -2
- package/views/help/_agent-setup.pug +28 -23
- package/views/help/_ci.pug +4 -4
- package/views/help/_coordinator.pug +4 -3
- package/views/help/_env.pug +1 -1
- package/views/help/_overview.pug +1 -1
- package/views/help/_quick-start.pug +5 -5
- package/views/help/_troubleshooting.pug +1 -0
- package/views/index.pug +1 -1
- package/views/layout.pug +19 -22
- package/views/mixins/client-config.pug +2 -2
- package/views/mixins/resource.pug +23 -21
- package/views/mixins/updates.pug +2 -2
- package/views/profile.pug +56 -0
- package/views/resources/list.pug +3 -3
|
@@ -10,40 +10,42 @@
|
|
|
10
10
|
p.small.
|
|
11
11
|
In CI you don't need a global install: #[code npx -y @andrian.yablonskyy/thub-agent run …].
|
|
12
12
|
|
|
13
|
-
h3.h6 Get
|
|
13
|
+
h3.h6 Get your access key
|
|
14
14
|
p.small.
|
|
15
|
-
An admin
|
|
16
|
-
|
|
15
|
+
Every person has a user with one #[strong access key]. An admin creates you on #[a(href="/admin/users") Users]
|
|
16
|
+
(or with #[code thub-admin user add]) and hands you the key, which is shown only once. Maintainers and admins can also
|
|
17
|
+
create or replace their own on their #[a(href="/profile") profile]. Your #[strong role] decides what you can do:
|
|
17
18
|
table.table.table-sm.small.w-auto
|
|
18
19
|
thead
|
|
19
20
|
tr
|
|
20
|
-
th
|
|
21
|
-
th
|
|
22
|
-
th
|
|
23
|
-
th Default priority
|
|
24
|
-
th Can see
|
|
21
|
+
th Role
|
|
22
|
+
th Agent
|
|
23
|
+
th Dashboard
|
|
25
24
|
tbody
|
|
26
25
|
tr
|
|
27
|
-
td:
|
|
28
|
-
td
|
|
29
|
-
td
|
|
30
|
-
td 50
|
|
31
|
-
td every job (details, log)
|
|
26
|
+
td: strong User
|
|
27
|
+
td submits jobs, sees and cancels only their own
|
|
28
|
+
td no
|
|
32
29
|
tr
|
|
33
|
-
td:
|
|
34
|
-
td
|
|
35
|
-
td
|
|
36
|
-
|
|
37
|
-
td
|
|
30
|
+
td: strong Maintainer
|
|
31
|
+
td submits jobs, sees and cancels every job
|
|
32
|
+
td yes, without Users, Settings and CI tokens
|
|
33
|
+
tr
|
|
34
|
+
td: strong Admin
|
|
35
|
+
td like a Maintainer
|
|
36
|
+
td everything
|
|
37
|
+
p.small.
|
|
38
|
+
Pipelines use a #[strong CI token] instead (#[a(href="/admin/agents") CI tokens], admins): their jobs get A- ids and they
|
|
39
|
+
read every job. People's jobs get M- ids.
|
|
38
40
|
|
|
39
41
|
h3.h6 Configure
|
|
40
42
|
+code('Save the settings once').
|
|
41
43
|
thub config set url #{coordinatorUrl}
|
|
42
|
-
thub config set
|
|
44
|
+
thub config set key thk_…
|
|
43
45
|
thub config set user "Your Name" # optional: job owner label
|
|
44
46
|
thub config set group <groupId> # optional: default --group
|
|
45
47
|
p.small.
|
|
46
|
-
These are saved in #[code ~/.config/thub/agent.json]. Order of precedence: flags (#[code --url], #[code --
|
|
48
|
+
These are saved in #[code ~/.config/thub/agent.json]. Order of precedence: flags (#[code --url], #[code --key]) →
|
|
47
49
|
environment → config file.
|
|
48
50
|
table.table.table-sm.small
|
|
49
51
|
thead
|
|
@@ -55,8 +57,8 @@
|
|
|
55
57
|
td: code THUB_URL
|
|
56
58
|
td Coordinator URL, e.g. #[code #{coordinatorUrl}]
|
|
57
59
|
tr
|
|
58
|
-
td: code
|
|
59
|
-
td
|
|
60
|
+
td: code THUB_KEY
|
|
61
|
+
td Your access key, or a CI token in a pipeline. Keep it in your CI's secret store. (#[code THUB_TOKEN], #[code --token] and a saved #[code token] still work: the old names.)
|
|
60
62
|
tr
|
|
61
63
|
td: code THUB_GROUP
|
|
62
64
|
td Default #[code --group]
|
|
@@ -66,7 +68,10 @@
|
|
|
66
68
|
tr
|
|
67
69
|
td: code THUB_NO_SELF_UPDATE=1
|
|
68
70
|
td Don't install admin-requested Agent updates on this machine
|
|
69
|
-
+code('Check the connection').
|
|
71
|
+
+code('Check the connection and your key').
|
|
72
|
+
thub whoami # jane (user) <jane@example.com>
|
|
73
|
+
thub key show # your key's last characters, created, last used
|
|
74
|
+
thub key rotate # replace it — the old one stops at once; saved to the config file if it came from there
|
|
70
75
|
thub resources # lists the Clients and their status
|
|
71
76
|
thub jobs --mine # your recent jobs
|
|
72
77
|
p.small.mb-0.
|
package/views/help/_ci.pug
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
+section('ci', 'CI/CD integration (GitHub Actions)', 'github')
|
|
2
2
|
p.
|
|
3
3
|
The test job only needs the Agent and HTTPS access to the Coordinator, so it runs on an ordinary hosted runner.
|
|
4
|
-
Create a #[strong
|
|
5
|
-
#[code
|
|
4
|
+
Create a #[strong CI token] on #[a(href="/admin/agents") CI tokens] (admins) and store it as the repository secret
|
|
5
|
+
#[code THUB_CI_TOKEN].
|
|
6
6
|
+code('.github/workflows/firmware.yml').
|
|
7
7
|
name: firmware
|
|
8
8
|
on: [push, pull_request]
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
runs-on: ubuntu-latest
|
|
28
28
|
env:
|
|
29
29
|
THUB_URL: #{coordinatorUrl}
|
|
30
|
-
|
|
30
|
+
THUB_KEY: ${{ secrets.THUB_CI_TOKEN }}
|
|
31
31
|
steps:
|
|
32
32
|
- run: |
|
|
33
33
|
npx -y @andrian.yablonskyy/thub-agent run \
|
|
@@ -41,4 +41,4 @@
|
|
|
41
41
|
li The step's exit code is the verdict (#[a(href="#agent-cli") exit codes]), so the workflow fails exactly when the tests do.
|
|
42
42
|
li If the workflow is canceled, the Agent (in #[code --wait] mode) cancels the TestHub job, so abandoned runs don't hold hardware.
|
|
43
43
|
li Downloads carry no credentials. Publish the image to a URL the Client can read, or fetch it in #[code --command] with a token passed as #[code --env].
|
|
44
|
-
li Other CI systems (GitLab CI, Jenkins, Azure Pipelines) work the same way: install Node.js, set #[code THUB_URL]/#[code
|
|
44
|
+
li Other CI systems (GitLab CI, Jenkins, Azure Pipelines) work the same way: install Node.js, set #[code THUB_URL]/#[code THUB_KEY] and run #[code thub run … --wait].
|
|
@@ -114,10 +114,11 @@
|
|
|
114
114
|
p.small.
|
|
115
115
|
#[code thub-admin] works directly on the database. Run it on the Coordinator host as the service user.
|
|
116
116
|
+code('Coordinator host').
|
|
117
|
-
thub-admin
|
|
117
|
+
thub-admin user add alice --email alice@example.com --role admin --password 's3cret'
|
|
118
|
+
thub-admin user add bob --email bob@example.com --key # role user (Agent only); prints his key once
|
|
119
|
+
thub-admin user list | block | unblock | role | password | key | delete …
|
|
118
120
|
thub-admin join-key generate # → clientJoinKey here, joinKey on every Client
|
|
119
|
-
thub-admin agent add ci-firmware
|
|
120
|
-
thub-admin agent add alice-laptop --kind cli # developer token: job ids M-00001…
|
|
121
|
+
thub-admin agent add ci-firmware # CI token for a pipeline: job ids A-00001…
|
|
121
122
|
thub-admin group add ci-nightly --comment "nightly pool" # → a group id for thub run --group
|
|
122
123
|
thub-admin resource maintenance <resourceId> --on
|
|
123
124
|
thub-admin jobs reset --yes # cancel every active job
|
package/views/help/_env.pug
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
td: code.text-nowrap= v[1]
|
|
69
69
|
td= v[2]
|
|
70
70
|
p.small.
|
|
71
|
-
#[code --url], #[code --
|
|
71
|
+
#[code --url], #[code --key], #[code --wait], #[code --detach], #[code --json] and #[code --dry-run] only affect the Agent and
|
|
72
72
|
have no variable. The Client's own git commands also get #[code GIT_TERMINAL_PROMPT=0] and #[code GIT_ALLOW_PROTOCOL=http:https:ssh:git].
|
|
73
73
|
|
|
74
74
|
h3.h6 Your own variables: #[code --env]
|
package/views/help/_overview.pug
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
h3.h6 How a job flows
|
|
39
39
|
ol.small
|
|
40
|
-
li #[code thub run …] submits a job spec (type, labels, command, inputs) with
|
|
40
|
+
li #[code thub run …] submits a job spec (type, labels, command, inputs) with your access key (or a pipeline's CI token).
|
|
41
41
|
li The Coordinator queues it (#[code QUEUED]) and the scheduler assigns it to an idle Client whose type, labels and group match.
|
|
42
42
|
li The Client accepts it, clones #[code --git-repo], downloads #[code --download-file] files and prepares the DUT (#[code PREPARING]).
|
|
43
43
|
li It runs #[code --command] with #[code sh -c], and output streams live to the Agent and the dashboard (#[code RUNNING]).
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
thub-admin join-key generate # → put it in clientJoinKey
|
|
10
10
|
nano ~/.config/thub/coordinator.json # publicUrl, clientJoinKey
|
|
11
11
|
sudo systemctl restart thub-coordinator
|
|
12
|
-
thub-admin
|
|
12
|
+
thub-admin user add admin --email you@example.com --role admin --password 'a-strong-password'
|
|
13
13
|
li
|
|
14
14
|
strong Install a Client
|
|
15
15
|
| on each lab machine, with the same join key (#[a(href="#client-setup") Client setup]):
|
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
sudo systemctl enable --now thub-client@client
|
|
21
21
|
| It shows up on #[a(href="/resources") Resources] by itself, usually within 10 s.
|
|
22
22
|
li
|
|
23
|
-
strong
|
|
24
|
-
| on #[a(href="/admin/
|
|
25
|
-
| #[code thub-admin
|
|
23
|
+
strong Create users and their access keys
|
|
24
|
+
| on #[a(href="/admin/users") Users] (admins), or on the Coordinator host with
|
|
25
|
+
| #[code thub-admin user add <name> --email <email> --key]. The key is shown once.
|
|
26
26
|
li
|
|
27
27
|
strong Install and configure the Agent
|
|
28
28
|
| (#[a(href="#agent-setup") Agent setup]):
|
|
29
29
|
+code('Developer machine').
|
|
30
30
|
npm i -g @andrian.yablonskyy/thub-agent
|
|
31
31
|
thub config set url #{coordinatorUrl}
|
|
32
|
-
thub config set
|
|
32
|
+
thub config set key thk_…
|
|
33
33
|
li
|
|
34
34
|
strong Run a job
|
|
35
35
|
| (#[a(href="#agent-cli") Agent CLI reference]):
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
['Where are a job\'s test reports and output files?', 'The Coordinator keeps only the log and the result (verdict, exit code, JUnit test counts). Files the job creates stay in its workspace on the Client and are deleted when the job ends. To keep them, publish them from `--command`, e.g. `curl -fsS -H "Authorization: Bearer $ART_TOKEN" -T results/report.xml "https://artifactory.example.com/qa/$THUB_JOB_ID/report.xml"` with `--env ART_TOKEN`. To show them on the job page and in `thub status`, list them in `$THUB_ARTIFACTS_FILE` as `[{"name", "size", "link", "timestamp"}]` — only that metadata is kept. The full log is always available from the job page\'s Raw button.'],
|
|
21
21
|
['429 Too Many Requests', 'A caller went over `rateLimit.requestsPerMinute` (per agent or resource token, per dashboard user, else per IP; default 500). The Agent and Client wait for `Retry-After` and retry by themselves; a script calling the API should do the same. Raise the limit in the config file or on Settings if your normal traffic needs more (a Client streaming logs makes up to ~240 requests a minute). Too many sign-in attempts from one address: wait a minute (`rateLimit.loginPerMinute`).'],
|
|
22
22
|
['Sign-in says “Too many failed sign-ins”', 'After repeated wrong passwords the Coordinator makes each further attempt wait, doubling each time: per address from the 5th failure (up to 15 min), per username from the 10th from any address (up to 5 min). Wait the time shown, or sign in from another address; a successful sign-in clears it, and an hour without failures forgets it. The Coordinator never tells whether a username exists.'],
|
|
23
|
+
['Agent says “Invalid or revoked access key” or “… is blocked”', 'The key was replaced (`thub key rotate`, an admin, your profile) or revoked, or your user is blocked. Ask an admin for a new key (Users), or create one on your profile if you can sign in. Then `thub config set key <key>`; `thub whoami` checks it.'],
|
|
23
24
|
['Locked out of the dashboard', 'Restart the Coordinator once with `THUB_BOOTSTRAP_ADMIN_PASSWORD=<new>` (resets the `admin` user), then unset it.']
|
|
24
25
|
]
|
|
25
26
|
const withCode = (text) => text
|
package/views/index.pug
CHANGED
package/views/layout.pug
CHANGED
|
@@ -21,7 +21,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
21
21
|
data-bs-title=`Coordinator v${coordinatorVersion} · thub-common v${commonVersion} (validates job specs)`
|
|
22
22
|
)
|
|
23
23
|
| ver. #{coordinatorVersion}
|
|
24
|
-
if user &&
|
|
24
|
+
if user && !can.admin && updates.coordinatorUpdate
|
|
25
25
|
span.badge.text-bg-warning.ms-1(
|
|
26
26
|
data-bs-toggle="tooltip"
|
|
27
27
|
data-bs-placement="bottom"
|
|
@@ -29,23 +29,18 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
29
29
|
)= `v${updates.coordinatorUpdate} available`
|
|
30
30
|
button.navbar-toggler(type="button" data-bs-toggle="collapse" data-bs-target="#nav")
|
|
31
31
|
span.navbar-toggler-icon
|
|
32
|
-
#nav.collapse.navbar-collapse
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
#nav.collapse.navbar-collapse.thub-nav
|
|
33
|
+
//- Below lg the menu drops down over the page (thub.css .thub-nav,
|
|
34
|
+
//- closed by public/js/actions.js): pages as a two-column grid with
|
|
35
|
+
//- icons, then the tools in a row, then the account row.
|
|
36
|
+
- const pages = user ? [['/', 'overview', 'Overview', 'speedometer2'], ['/resources', 'resources', 'Resources', 'hdd-rack'], ['/groups', 'groups', 'Groups', 'collection'], ['/jobs', 'jobs', 'Jobs', 'list-task']].concat(user.role === 'admin' ? [['/admin/users', 'users', 'Users', 'people'], ['/admin/agents', 'agents', 'CI tokens', 'key'], ['/admin/settings', 'settings', 'Settings', 'gear']] : []) : []
|
|
37
|
+
ul.navbar-nav.me-auto.thub-nav-pages
|
|
38
|
+
each pg in pages
|
|
39
39
|
li.nav-item
|
|
40
|
-
a.nav-link(href=
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
li.nav-item
|
|
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
|
|
48
|
-
ul.navbar-nav
|
|
40
|
+
a.nav-link(href=pg[0] class=active === pg[1] ? 'active' : '' aria-current=active === pg[1] ? 'page' : false)
|
|
41
|
+
i.bi.me-2.d-lg-none(class=`bi-${pg[3]}` aria-hidden="true")
|
|
42
|
+
= pg[2]
|
|
43
|
+
ul.navbar-nav.thub-nav-tools
|
|
49
44
|
if user && liveTopics
|
|
50
45
|
//- Live-update status, driven by public/js/live.js.
|
|
51
46
|
li.nav-item.d-flex.align-items-center.me-3
|
|
@@ -64,7 +59,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
64
59
|
)
|
|
65
60
|
i.bi.bi-question-circle.me-1
|
|
66
61
|
| Help
|
|
67
|
-
if user &&
|
|
62
|
+
if user && can.operate
|
|
68
63
|
li.nav-item.d-flex.align-items-center.me-2
|
|
69
64
|
form(method="post" action="/updates/check" data-submit-busy)
|
|
70
65
|
input(type="hidden" name="returnTo" value=currentPath)
|
|
@@ -76,7 +71,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
76
71
|
data-bs-title=updates.checkedAt ? `Check for updates now — the Coordinator and connected resources (last checked ${fmtDate(updates.checkedAt)})` : 'Check for updates now — the Coordinator and connected resources'
|
|
77
72
|
)
|
|
78
73
|
i.bi.bi-arrow-repeat
|
|
79
|
-
if user &&
|
|
74
|
+
if user && can.admin && updates.coordinatorUpdate
|
|
80
75
|
li.nav-item.d-flex.align-items-center.me-3
|
|
81
76
|
if updates.coordinatorPending
|
|
82
77
|
//- public/js/update-watch.js polls /updates/status and
|
|
@@ -117,12 +112,13 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
117
112
|
)
|
|
118
113
|
i.bi.bi-circle-half
|
|
119
114
|
if user
|
|
120
|
-
li.nav-
|
|
115
|
+
li.thub-nav-break(aria-hidden="true")
|
|
116
|
+
li.nav-item.d-flex.align-items-center.me-3.thub-nav-account
|
|
121
117
|
a.nav-link.d-flex.align-items-center.p-0(
|
|
122
118
|
href="/profile"
|
|
123
119
|
data-bs-toggle="tooltip"
|
|
124
120
|
data-bs-placement="bottom"
|
|
125
|
-
data-bs-title=`Profile settings (${user.role})`
|
|
121
|
+
data-bs-title=`Profile settings (${roleLabel(user.role)})`
|
|
126
122
|
)
|
|
127
123
|
if user.avatarPath
|
|
128
124
|
img.rounded-circle.me-2(src=user.avatarPath width="24" height="24" style="object-fit: cover;")
|
|
@@ -130,6 +126,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
130
126
|
span.rounded-circle.bg-secondary.text-white.d-inline-flex.align-items-center.justify-content-center.me-2(style="width: 24px; height: 24px; font-size: 12px;")
|
|
131
127
|
= ((user.firstName ? user.firstName[0] : user.username[0]) + (user.lastName ? user.lastName[0] : '')).toUpperCase()
|
|
132
128
|
span= user.firstName ? `${user.firstName} ${user.lastName || ''}`.trim() : user.username
|
|
129
|
+
span.small.text-body-secondary.ms-2.d-lg-none= `${roleLabel(user.role)} · profile`
|
|
133
130
|
li.nav-item.d-flex.align-items-center
|
|
134
131
|
form(action="/logout" method="post")
|
|
135
132
|
button.btn.btn-sm.btn-outline-danger(
|
|
@@ -197,6 +194,6 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
197
194
|
script(src="/js/actions.js")
|
|
198
195
|
if user && liveTopics
|
|
199
196
|
script(src="/js/live.js")
|
|
200
|
-
if user &&
|
|
197
|
+
if user && can.admin && updates.coordinatorPending
|
|
201
198
|
script(src="/js/update-watch.js")
|
|
202
199
|
block scripts
|
|
@@ -106,7 +106,7 @@ mixin configTabItems(r)
|
|
|
106
106
|
//- at once.
|
|
107
107
|
mixin groupsPane(r, returnTo, groupsById)
|
|
108
108
|
- const p = `cfg-${r.id}`
|
|
109
|
-
- const canEdit =
|
|
109
|
+
- const canEdit = can.operate
|
|
110
110
|
- const pending = r.config_import && r.config_import.revision > r.config_applied_revision && r.config_import.fields && r.config_import.fields.groups
|
|
111
111
|
- const all = Object.values(groupsById).sort((a, b) => a.name.localeCompare(b.name))
|
|
112
112
|
- const unknown = r.group_ids.filter((id) => !groupsById[id])
|
|
@@ -163,7 +163,7 @@ mixin groupsPane(r, returnTo, groupsById)
|
|
|
163
163
|
mixin configPanes(r, returnTo, groupsById)
|
|
164
164
|
- const p = `cfg-${r.id}`
|
|
165
165
|
- const current = r.config_desired || r.client_config
|
|
166
|
-
- const canEdit =
|
|
166
|
+
- const canEdit = can.operate
|
|
167
167
|
if r.type !== 'hw'
|
|
168
168
|
//- no device tabs
|
|
169
169
|
else if !current
|
|
@@ -25,22 +25,24 @@ mixin resourceActions(r, returnTo)
|
|
|
25
25
|
data-bs-title="Take this resource out of rotation for maintenance — the scheduler will skip it"
|
|
26
26
|
)
|
|
27
27
|
i.bi.bi-tools
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
button.btn.btn-sm.btn-outline-warning(
|
|
38
|
-
type="submit"
|
|
39
|
-
aria-label="Rotate token"
|
|
40
|
-
data-bs-toggle="tooltip"
|
|
41
|
-
data-bs-title="Invalidate the current resource token and issue a new one — the Client won't reconnect until it has it"
|
|
28
|
+
//- Credentials: admins only (§10.3).
|
|
29
|
+
if can.admin
|
|
30
|
+
form.d-inline.ms-1(
|
|
31
|
+
method="post"
|
|
32
|
+
action=`/resources/${r.id}/rotate-token`
|
|
33
|
+
data-confirm=`Issue a new token for ${r.name}? Its current token stops working at once, and the Client won't reconnect until it has the new one.`
|
|
34
|
+
data-confirm-title="Rotate resource token?"
|
|
35
|
+
data-confirm-ok="Rotate token"
|
|
36
|
+
data-confirm-tone="warning"
|
|
42
37
|
)
|
|
43
|
-
|
|
38
|
+
input(type="hidden" name="returnTo" value=returnTo)
|
|
39
|
+
button.btn.btn-sm.btn-outline-warning(
|
|
40
|
+
type="submit"
|
|
41
|
+
aria-label="Rotate token"
|
|
42
|
+
data-bs-toggle="tooltip"
|
|
43
|
+
data-bs-title="Invalidate the current resource token and issue a new one — the Client won't reconnect until it has it"
|
|
44
|
+
)
|
|
45
|
+
i.bi.bi-arrow-repeat
|
|
44
46
|
+removeResourceButton(r, returnTo)
|
|
45
47
|
|
|
46
48
|
//- Remove: a plain confirm when the resource is idle; with a running job,
|
|
@@ -188,7 +190,7 @@ mixin uptimeRows(r, returnTo)
|
|
|
188
190
|
span.text-body-secondary.small.ms-1 (at last heartbeat)
|
|
189
191
|
else
|
|
190
192
|
span.text-body-secondary —
|
|
191
|
-
if
|
|
193
|
+
if can.operate
|
|
192
194
|
+cancelActivityButton(r, returnTo)
|
|
193
195
|
|
|
194
196
|
//- Scheduled host reboot (cron, in the Client host's local time): what's
|
|
@@ -226,7 +228,7 @@ mixin rebootScheduleRow(r, returnTo)
|
|
|
226
228
|
data-bs-toggle="tooltip"
|
|
227
229
|
data-bs-title="This host has no reboot helper (thub-client-reboot.path), so the schedule can't reboot it — reinstall the Client as root: sudo npm i -g @andrian.yablonskyy/thub-client"
|
|
228
230
|
) can't reboot
|
|
229
|
-
if
|
|
231
|
+
if can.operate
|
|
230
232
|
form.d-flex.gap-1.mt-1(
|
|
231
233
|
method="post"
|
|
232
234
|
action=`/resources/${r.id}/reboot-schedule`
|
|
@@ -338,7 +340,7 @@ mixin usbScanPane(r)
|
|
|
338
340
|
- const importWhy = r.type !== 'hw' ? 'Only HW Clients have USB device lists to import into' : !(r.config_desired || r.client_config) ? 'This Client is too old to report its config — update it first' : null
|
|
339
341
|
div(data-usb-scan=`/resources/${r.id}/usb-scan` data-pending=r.usb_scan_request_id ? '1' : null data-requested-at=r.usb_scan_requested_at)
|
|
340
342
|
.d-flex.align-items-center.gap-2.mb-2
|
|
341
|
-
if
|
|
343
|
+
if can.operate
|
|
342
344
|
if offline
|
|
343
345
|
span.d-inline-block(tabindex="0" data-bs-toggle="tooltip" data-bs-title="Offline — a Client can only be scanned while it's connected")
|
|
344
346
|
button.btn.btn-sm.btn-outline-primary(type="button" disabled style="pointer-events: none;")
|
|
@@ -366,7 +368,7 @@ mixin usbScanPane(r)
|
|
|
366
368
|
if scan
|
|
367
369
|
= `lsusb -tvv at ${fmtDate(scan.at)}`
|
|
368
370
|
else
|
|
369
|
-
| Not scanned yet#{
|
|
371
|
+
| Not scanned yet#{can.operate ? ' — press Refresh to run lsusb -tvv on the Client' : ''}.
|
|
370
372
|
.alert.alert-danger.py-2.small(data-usb-error class=scan && scan.error ? null : 'd-none')= scan && scan.error || ''
|
|
371
373
|
pre.bg-body-tertiary.p-2.rounded.small.mb-0(data-usb-output style="max-height: 360px; overflow: auto;" class=scan && scan.output ? null : 'd-none')= scan && scan.output || ''
|
|
372
374
|
|
|
@@ -453,7 +455,7 @@ mixin resourceDetails(r, groupsById, returnTo, local, external)
|
|
|
453
455
|
data-bs-toggle="tooltip"
|
|
454
456
|
data-bs-title="Renamed on the dashboard; this is the name the Client itself reports"
|
|
455
457
|
)= `(Client: ${r.reported_name})`
|
|
456
|
-
if
|
|
458
|
+
if can.operate
|
|
457
459
|
button.btn.btn-sm.btn-link.p-0.ms-2.align-baseline(
|
|
458
460
|
type="button"
|
|
459
461
|
data-bs-toggle="collapse"
|
|
@@ -513,7 +515,7 @@ mixin resourceDetails(r, groupsById, returnTo, local, external)
|
|
|
513
515
|
dd.col-sm-8= r.busy_source ? `${r.busy_source}${r.busy_reason ? ' — ' + r.busy_reason : ''}` : '—'
|
|
514
516
|
dt.col-sm-4 Last heartbeat
|
|
515
517
|
dd.col-sm-8= fmtDate(r.last_heartbeat_at, 'never')
|
|
516
|
-
if
|
|
518
|
+
if can.operate
|
|
517
519
|
dt.col-sm-4 Actions
|
|
518
520
|
dd.col-sm-8.mb-0
|
|
519
521
|
+resourceActions(r, returnTo)
|
package/views/mixins/updates.pug
CHANGED
|
@@ -25,7 +25,7 @@ mixin versionCell(current, latest, updateTo)
|
|
|
25
25
|
//- an update). Agents table, Resources table, resource card.
|
|
26
26
|
mixin versionWithUpdate(current, latest, updateTo, action, returnTo, canUpdate = true, holding = false)
|
|
27
27
|
+versionCell(current, latest, updateTo)
|
|
28
|
-
if
|
|
28
|
+
if can.operate && canUpdate && (updateTo || holding || (current && isNewer(latest, current)))
|
|
29
29
|
+updateButton(action, updateTo || (holding ? true : null), returnTo)
|
|
30
30
|
|
|
31
31
|
//- Latest published version of `app` (agent | client), "check now" and,
|
|
@@ -40,7 +40,7 @@ mixin updatesToolbar(app, label, returnTo)
|
|
|
40
40
|
= ` · checked ${fmtDate(updates.checkedAt)}`
|
|
41
41
|
if updates.error
|
|
42
42
|
i.bi.bi-exclamation-triangle.text-warning.ms-1(data-bs-toggle="tooltip" data-bs-title=updates.error)
|
|
43
|
-
if
|
|
43
|
+
if can.operate
|
|
44
44
|
form.d-inline(method="post" action="/updates/check")
|
|
45
45
|
input(type="hidden" name="returnTo" value=returnTo)
|
|
46
46
|
button.btn.btn-sm.btn-outline-secondary(type="submit")
|
package/views/profile.pug
CHANGED
|
@@ -3,6 +3,24 @@ extends layout
|
|
|
3
3
|
block content
|
|
4
4
|
h1.h4.mb-3 Profile settings
|
|
5
5
|
|
|
6
|
+
if user.mustChangePassword
|
|
7
|
+
.alert.alert-warning
|
|
8
|
+
i.bi.bi-shield-exclamation.me-1
|
|
9
|
+
| You signed in with a temporary password — choose your own below before using the dashboard.
|
|
10
|
+
|
|
11
|
+
if secret
|
|
12
|
+
//- Shown once (§10.3): nothing keeps it.
|
|
13
|
+
.alert.alert-warning(role="alert")
|
|
14
|
+
h2.h6.alert-heading
|
|
15
|
+
i.bi.bi-shield-lock.me-1
|
|
16
|
+
= secret.title
|
|
17
|
+
.input-group.input-group-sm.mb-2
|
|
18
|
+
span.input-group-text Access key
|
|
19
|
+
input.form-control.font-monospace(type="text" value=secret.key readonly aria-label="Access key")
|
|
20
|
+
button.btn.btn-outline-secondary(type="button" data-copy=secret.key aria-label="Copy access key")
|
|
21
|
+
i.bi.bi-clipboard
|
|
22
|
+
p.small.mb-0 Shown only now. Save it with #[code thub config set key …] (or use #[code THUB_KEY] / #[code --key]).
|
|
23
|
+
|
|
6
24
|
.row
|
|
7
25
|
.col-md-6
|
|
8
26
|
form(method="post" action="/profile" enctype="multipart/form-data")
|
|
@@ -21,6 +39,12 @@ block content
|
|
|
21
39
|
label.form-label(for="username") Username
|
|
22
40
|
input#username.form-control(type="text" name="username" value=user.username required)
|
|
23
41
|
|
|
42
|
+
.mb-3
|
|
43
|
+
label.form-label(for="email") Email
|
|
44
|
+
input#email.form-control(type="email" name="email" value=user.email required)
|
|
45
|
+
if !user.email
|
|
46
|
+
.form-text.text-warning Every user needs an email — please add yours.
|
|
47
|
+
|
|
24
48
|
.row
|
|
25
49
|
.col-6.mb-3
|
|
26
50
|
label.form-label(for="firstName") First name
|
|
@@ -58,6 +82,38 @@ block content
|
|
|
58
82
|
data-set-theme=t
|
|
59
83
|
)= t.charAt(0).toUpperCase() + t.slice(1)
|
|
60
84
|
|
|
85
|
+
//- Your access key for the Agent (§10.3); also: thub key show|rotate.
|
|
86
|
+
.card.mb-3
|
|
87
|
+
.card-body
|
|
88
|
+
h6.card-subtitle.mb-3.text-body-secondary My access key
|
|
89
|
+
if accessKey
|
|
90
|
+
p.small.mb-2
|
|
91
|
+
| #[code= `…${accessKey.token_hint || '????'}`] · created #{fmtDate(accessKey.token_created_at || accessKey.created_at)} · last used #{fmtDate(accessKey.last_used_at, 'never')}
|
|
92
|
+
else
|
|
93
|
+
p.small.text-body-secondary.mb-2 None yet — create one to use the Agent (thub).
|
|
94
|
+
.d-flex.gap-2
|
|
95
|
+
form(
|
|
96
|
+
method="post"
|
|
97
|
+
action="/profile/key"
|
|
98
|
+
data-confirm=accessKey ? 'Replace your access key? The current one stops working at once — everywhere you use it.' : 'Create your access key?'
|
|
99
|
+
data-confirm-title=accessKey ? 'Replace access key?' : 'Create access key?'
|
|
100
|
+
data-confirm-ok=accessKey ? 'Replace key' : 'Create key'
|
|
101
|
+
data-confirm-tone="primary"
|
|
102
|
+
)
|
|
103
|
+
button.btn.btn-sm.btn-outline-primary(type="submit")
|
|
104
|
+
i.bi.bi-key.me-1
|
|
105
|
+
= accessKey ? 'Replace key' : 'Create key'
|
|
106
|
+
if accessKey
|
|
107
|
+
form(
|
|
108
|
+
method="post"
|
|
109
|
+
action="/profile/key/revoke"
|
|
110
|
+
data-confirm="Revoke your access key? The Agent stops working for you until you create a new one."
|
|
111
|
+
data-confirm-title="Revoke access key?"
|
|
112
|
+
data-confirm-ok="Revoke key"
|
|
113
|
+
)
|
|
114
|
+
button.btn.btn-sm.btn-outline-warning(type="submit") Revoke
|
|
115
|
+
.form-text.mt-2 From the command line: #[code thub key show] and #[code thub key rotate].
|
|
116
|
+
|
|
61
117
|
.card
|
|
62
118
|
.card-body
|
|
63
119
|
h6.card-subtitle.mb-3.text-body-secondary Change password
|
package/views/resources/list.pug
CHANGED
|
@@ -30,7 +30,7 @@ block content
|
|
|
30
30
|
th Groups
|
|
31
31
|
th Busy source / reason
|
|
32
32
|
+sortHeader(list, 'heartbeat', 'Last heartbeat', 'desc')
|
|
33
|
-
if
|
|
33
|
+
if can.operate
|
|
34
34
|
th Actions
|
|
35
35
|
tbody(data-live="rows")
|
|
36
36
|
each r in resources
|
|
@@ -50,14 +50,14 @@ block content
|
|
|
50
50
|
span.text-body-secondary —
|
|
51
51
|
td= r.busy_source ? `${r.busy_source}${r.busy_reason ? ' — ' + r.busy_reason : ''}` : '—'
|
|
52
52
|
td= fmtDate(r.last_heartbeat_at, 'never')
|
|
53
|
-
if
|
|
53
|
+
if can.operate
|
|
54
54
|
td
|
|
55
55
|
+resourceActions(r, list.pageUrl())
|
|
56
56
|
|
|
57
57
|
each r in resources
|
|
58
58
|
+resourceCard(r, groupsById, list.pageUrl())
|
|
59
59
|
|
|
60
|
-
if
|
|
60
|
+
if can.operate
|
|
61
61
|
+removeResourceModal()
|
|
62
62
|
|
|
63
63
|
block scripts
|