@andrian.yablonskyy/thub-coordinator 1.1.8 → 1.1.10

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.
@@ -185,7 +185,7 @@ test('dashboard: only maintainers and admins sign in; security pages are admins\
185
185
  assert.match(dev.text, /no dashboard access/);
186
186
 
187
187
  const mo = await signIn('mo', 'pw');
188
- assert.equal((await get(mo.cookie, '/resources')).status, 200);
188
+ assert.equal((await get(mo.cookie, '/runners')).status, 200);
189
189
  assert.equal((await get(mo.cookie, '/admin/settings')).status, 403); // admins' only
190
190
  for (const url of ['/admin/users', '/admin/agents']){
191
191
  assert.equal((await get(mo.cookie, url)).status, 200, url); // within limits — see below
@@ -195,7 +195,7 @@ test('dashboard: only maintainers and admins sign in; security pages are admins\
195
195
 
196
196
  // Blocked while signed in: the next click goes to the sign-in page.
197
197
  u.setBlocked(maint.id, true, { actorId: admin.id });
198
- const after = await get(mo.cookie, '/resources');
198
+ const after = await get(mo.cookie, '/runners');
199
199
  assert.equal(after.status, 302);
200
200
  assert.equal(after.headers.get('location'), '/login');
201
201
  assert.match((await signIn('mo', 'pw')).text, /blocked/);
@@ -10,7 +10,7 @@ block content
10
10
  .d-flex.flex-wrap.align-items-center.gap-2.mb-3
11
11
  +updatesToolbar('agent', 'agents', list.pageUrl())
12
12
  .d-flex.flex-wrap.align-items-center.gap-2.mb-3
13
- +searchForm(q, 'Search agents: name, kind, group, version, status', list.urlFor({ q: '' }))
13
+ +searchForm(q, 'Search CI tokens: name, group, version, status', list.urlFor({ q: '' }))
14
14
  .ms-auto
15
15
  +pageSizePicker(list)
16
16
 
@@ -30,7 +30,6 @@ block content
30
30
  thead
31
31
  tr
32
32
  +sortHeader(list, 'name', 'Name')
33
- +sortHeader(list, 'kind', 'Kind')
34
33
  +sortHeader(list, 'group', 'Group')
35
34
  +sortHeader(list, 'version', 'Version', 'desc')
36
35
  +sortHeader(list, 'created', 'Created', 'desc')
@@ -43,7 +42,6 @@ block content
43
42
  //- The name opens Edit (name and group).
44
43
  td
45
44
  a.fw-medium(href=`#editAgentModal-${a.id}` data-bs-toggle="modal" role="button")= a.name
46
- td.text-uppercase= a.kind
47
45
  td
48
46
  if a.group_id && groupsById[a.group_id]
49
47
  span.badge.text-bg-info= groupsById[a.group_id].name
@@ -108,7 +106,7 @@ block content
108
106
  button.btn.btn-primary(type="submit") Save
109
107
  else
110
108
  tr
111
- td.text-body-secondary(colspan="8")
109
+ td.text-body-secondary(colspan="7")
112
110
  if q
113
111
  | No agent matches “#{q}”.
114
112
  a(href=list.urlFor({ q: '' })) Show all agents
@@ -9,7 +9,7 @@ block content
9
9
 
10
10
  p.text-body-secondary.small: i.
11
11
  Give an agent a group — a user on #[a(href="/admin/users") Users], a CI token on #[a(href="/admin/agents") CI tokens] —
12
- and its jobs run only on resources in that group (§13.1). Which groups a Client is in is set on its resource card (Groups tab).
12
+ and its jobs run only on runners in that group (§13.1). Which groups a Client is in is set on its runner card (Groups tab).
13
13
 
14
14
  if totalCount
15
15
  .d-flex.flex-wrap.align-items-center.gap-2.mb-2
@@ -25,7 +25,7 @@ block content
25
25
  tr
26
26
  th Name
27
27
  th Comment
28
- th Resources
28
+ th Runners
29
29
  th Agents
30
30
  th Created
31
31
  if canManage
@@ -44,7 +44,7 @@ block content
44
44
  form.d-inline.ms-1(
45
45
  method="post"
46
46
  action=`/groups/${g.id}/delete`
47
- data-confirm=`Delete group "${g.name}"? Resources listing it just stop matching on it, and its agents go back to running their jobs on any resource.`
47
+ data-confirm=`Delete group "${g.name}"? Runners listing it just stop matching on it, and its agents go back to running their jobs on any runner.`
48
48
  data-confirm-title="Delete group?"
49
49
  data-confirm-ok="Delete group"
50
50
  )
@@ -18,9 +18,8 @@
18
18
  -
19
19
  const groups = [
20
20
  { title: 'Where to run', rows: [
21
- ['--type hw|sw', 'Required. Resource type.', '--type hw'],
22
- ['--board <name>', 'Shorthand for --label board:<name>.', '--board nucleo-f401re'],
23
- ['--label <l>', 'Required label (repeatable). The Client must have all of them.', '--label uart --label stlink'],
21
+ ['--type hw|sw', 'Required. Runner type.', '--type hw'],
22
+ ['--label <l>', 'Required label (repeatable). The runner must have all of them. 1–64 characters, no spaces, commas or semicolons.', '--label uart --label stlink'],
24
23
  ['--client <name|id>', 'Only this Client; waits for it even if others are idle.', '--client lab-hw-01']
25
24
  ] },
26
25
  { title: 'What to run', rows: [
@@ -73,13 +72,13 @@
73
72
 
74
73
  h3.h6 Examples
75
74
  +code('Flash and test a board, fail CI on failure').
76
- thub run --type hw --board nucleo-f401re \
75
+ thub run --type hw --label board:nucleo-f401re \
77
76
  --download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
78
77
  --git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
79
78
  --command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh "$@"' \
80
79
  --arg --junit --timeout 45m --wait
81
80
  +code('Reproduce a failure on the exact bench, labeled with your name').
82
- thub run --type hw --board nucleo-f401re --client lab-hw-01 --user "$USER" \
81
+ thub run --type hw --label board:nucleo-f401re --client lab-hw-01 --user "$USER" \
83
82
  --download-file "$IMAGE_URL" --git-repo "$TESTS_REPO" --command ./ci/test.sh
84
83
  # Ctrl-C detaches; the job keeps running. Re-attach:
85
84
  thub status M-00126
@@ -44,7 +44,7 @@
44
44
  thub config set key thk_…
45
45
  thub config set user "Your Name" # optional: job owner label
46
46
  p.small.
47
- These are saved in #[code ~/.config/thub/agent.json]. Which resource group your jobs run in isn't an Agent
47
+ These are saved in #[code ~/.config/thub/agent.json]. Which runner group your jobs run in isn't an Agent
48
48
  setting: an admin or maintainer sets it for your user on the dashboard (#[code thub whoami] shows it). Order of precedence: flags (#[code --url], #[code --key]) →
49
49
  environment → config file.
50
50
  table.table.table-sm.small
@@ -31,7 +31,7 @@
31
31
  steps:
32
32
  - run: |
33
33
  npx -y @andrian.yablonskyy/thub-agent run \
34
- --type hw --board nucleo-f401re \
34
+ --type hw --label board:nucleo-f401re \
35
35
  --download-file "${{ needs.build.outputs.image_url }}" \
36
36
  --git-repo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" "$GITHUB_SHA" \
37
37
  --command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/hw-tests.sh' \
@@ -40,17 +40,17 @@
40
40
  lsusb -t # USB tree
41
41
  udevadm info -a -n /dev/ttyUSB0 | grep -m1 'ATTRS{devpath}'
42
42
  st-info --probe # ST-Link serials
43
- | Or open the resource card → #[strong Connected USB devices] → #[strong Refresh], then #[strong Import to config].
43
+ | Or open the runner card → #[strong Connected USB devices] → #[strong Refresh], then #[strong Import to config].
44
44
  li
45
45
  | Put the devpaths into #[code hw-devices] and restart. On every start the Client writes
46
46
  | #[code /etc/udev/rules.d/99-thub-&lt;instance&gt;.rules], so the devices appear as #[code /dev/thub/dut&lt;N&gt;-stlink|uart|usb]:
47
47
  +code('HW machine').
48
48
  sudo systemctl restart thub-client@client
49
49
  ls -l /dev/thub/
50
- li Check the resource card's #[strong Capabilities]: each configured device must be present (a missing one is flagged).
50
+ li Check the runner card's #[strong Capabilities]: each configured device must be present (a missing one is flagged).
51
51
  li Your job's #[code --command] flashes the board. The Client never flashes by itself; it passes the ST-Link serials and the downloaded files in the environment (#[a(href="#env") Environment variables]).
52
52
  +code('A typical HW job').
53
- thub run --type hw --board nucleo-f401re \
53
+ thub run --type hw --label board:nucleo-f401re \
54
54
  --download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
55
55
  --git-repo https://github.com/yourorg/firmware-tests.git v1.4.0 \
56
56
  --command 'st-flash --serial "$THUB_DUT_STLINK" --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' \
@@ -71,7 +71,7 @@
71
71
  p.small.
72
72
  To scale out, clone the VM template. In the clone, #[strong before its first start], run
73
73
  #[code rm -f ~/var/lib/thub/client/*/.client-id ~/var/lib/thub/client/*.token] and give it its own #[code name].
74
- Otherwise both VMs register as the same resource.
74
+ Otherwise both VMs register as the same runner.
75
75
  +code('A typical SW job').
76
76
  thub run --type sw \
77
77
  --docker-image registry.lab.local:5000/dut-emulator:2026.08 \
@@ -84,4 +84,4 @@
84
84
  li Outbound HTTPS to the Coordinator; test it with #[code curl -sI #{coordinatorUrl}/login].
85
85
  li The Client's service user has SSH keys and git credentials for private repos (#[a(href="#git") Git repositories]) and Docker logins for private registries (#[a(href="#docker") Docker registry]).
86
86
  li Under systemd, the service can only write to its state directory. Jobs write to #[code $THUB_WORK_DIR], not to #[code ~].
87
- li Optional: a nightly reboot from the resource card's #[strong Reboot] tab (cron, host-local time). It waits for running jobs.
87
+ li Optional: a nightly reboot from the runner card's #[strong Reboot] tab (cron, host-local time). It waits for running jobs.
@@ -74,15 +74,15 @@
74
74
  tr
75
75
  td: code name
76
76
  td No
77
- td Resource name. Under systemd, the instance name (#[code thub-client@dut1] → #[code dut1]) is used unless an admin renames it on the dashboard.
77
+ td Runner name. Under systemd, the instance name (#[code thub-client@dut1] → #[code dut1]) is used unless an admin renames it on the dashboard.
78
78
  tr
79
79
  td: code labels
80
80
  td No
81
- td Matched against a job's #[code --board]/#[code --label]. Use #[code board:&lt;name>] for the board.
81
+ td Matched against a job's #[code --label]s. Use #[code board:&lt;name>] for the board (#[code --label board:&lt;name>]). Also set on its runner card (Labels tab), which writes them here.
82
82
  tr
83
83
  td: code groups
84
84
  td No
85
- td The resource groups this Client belongs to. Set them on its resource card (Groups tab), which writes them here.
85
+ td The runner groups this Client belongs to. Set them on its runner card (Groups tab), which writes them here.
86
86
  tr
87
87
  td: code varDir
88
88
  td No
@@ -101,7 +101,7 @@
101
101
  td An explicit identity UUID (or #[code THUB_CLIENT_ID]). Normally generated once and kept in #[code .client-id].
102
102
  p.small.
103
103
  SW Clients have no settings of their own: they run whatever DUT image a job brings (#[code --docker-image]).
104
- Admins can also edit an HW Client's devices from its resource card (#[strong ST-Link | UART | DUT USB]
104
+ Admins can also edit an HW Client's devices from its runner card (#[strong ST-Link | UART | DUT USB]
105
105
  tabs). The tabs show the config the Client runs, as it reported it at its last start, or a saved
106
106
  change it hasn't applied yet. #[strong Connected USB devices → Import to config] fills them from the host's #[code lsusb].
107
107
  The card's #[strong Export] and #[strong Import] icons download a Client's config file (never its
@@ -63,13 +63,13 @@
63
63
  td The shared secret Clients self-register with. Unset or #[code null] turns registration off (#[code 503]).
64
64
  tr
65
65
  td: code heartbeat.*
66
- td Clients heartbeat every #[code intervalSec] — the Coordinator tells them, so it's set here only (and on Settings). After #[code missedLimit] misses a resource goes #[code OUT_OF_SERVICE] and its job becomes #[code LOST]; #[code sweepIntervalSec] is how often that's checked.
66
+ td Clients heartbeat every #[code intervalSec] — the Coordinator tells them, so it's set here only (and on Settings). After #[code missedLimit] misses a runner goes #[code OUT_OF_SERVICE] and its job becomes #[code LOST]; #[code sweepIntervalSec] is how often that's checked.
67
67
  tr
68
68
  td: code scheduler.*
69
69
  td #[code assignAckTimeoutSec]: how long an assignment waits for the Client to accept. #[code requeueOnLost]: retry a #[code LOST] job once. #[code maxQueuedPerAgent]: queue limit per agent.
70
70
  tr
71
71
  td: code rateLimit.*
72
- td Requests per minute per caller (agent/resource token, dashboard user, else IP; default 500), and sign-in attempts per IP (default 10). Past it: #[code 429] with #[code Retry-After]. #[code 0] turns a limit off.
72
+ td Requests per minute per caller (agent/runner token, dashboard user, else IP; default 500), and sign-in attempts per IP (default 10). Past it: #[code 429] with #[code Retry-After]. #[code 0] turns a limit off.
73
73
  tr
74
74
  td: code jobs.*
75
75
  td Default and maximum job timeout, in seconds.
@@ -152,5 +152,5 @@
152
152
  p.small.mb-0.
153
153
  The Coordinator checks npm for new Coordinator, Agent and Client versions. Admins update the Coordinator
154
154
  with #[strong Update app] in the navbar, and Agents and Clients from the #[a(href="/admin/agents") Agents]
155
- and #[a(href="/resources") Resources] pages. Clients install an update only between jobs.
155
+ and #[a(href="/runners") Runners] pages. Clients install an update only between jobs.
156
156
  Manual alternatives: #[code thub-admin self-update], #[code thub self-update] and #[code thub-client self-update].
@@ -121,4 +121,4 @@
121
121
  td Files written to #[code /work] stay owned by the Client user, so the workspace can be cleaned up.
122
122
  +note('info').
123
123
  Docker access is effectively root access on that host. The Client host must have Docker and the service user must be in the
124
- #[code docker] group. The resource card's #[strong Capabilities] show whether a Client can use Docker, and why not.
124
+ #[code docker] group. The runner card's #[strong Capabilities] show whether a Client can use Docker, and why not.
@@ -35,8 +35,7 @@
35
35
  -
36
36
  const jobVars = [
37
37
  ['JOB_TYPE', '--type', 'hw or sw'],
38
- ['JOB_BOARD', '--board', 'the board name'],
39
- ['JOB_LABEL / JOB_LABEL_<n>', '--board, --label', 'all labels, comma-separated / each'],
38
+ ['JOB_LABEL / JOB_LABEL_<n>', '--label', 'all labels, comma-separated / each'],
40
39
  ['JOB_GROUP', '(dashboard)', 'the submitting agent\'s group, if it has one'],
41
40
  ['JOB_CLIENT', '--client', 'this Client\'s name'],
42
41
  ['JOB_USER', '--user', 'as given'],
@@ -118,7 +117,7 @@
118
117
  python:3.14 ./run-tests.sh
119
118
  +code('Submitting the job that feeds these').
120
119
  export API_TOKEN=…
121
- thub run --type hw --board nucleo-f401re \
120
+ thub run --type hw --label board:nucleo-f401re \
122
121
  --download-file https://artifactory.example.com/fw-local/app/1.4.0-42/app.bin \
123
122
  --git-repo git@github.com:yourorg/firmware-tests.git main \
124
123
  --env TARGET=production --env API_TOKEN \
@@ -12,7 +12,7 @@
12
12
  i.bi.bi-hdd-network.me-1
13
13
  | Coordinator
14
14
  p.card-text.small.mb-0.
15
- This service. It holds the job queue and the resource registry, and runs the scheduler and
15
+ This service. It holds the job queue and the runner registry, and runs the scheduler and
16
16
  the heartbeat monitor. It stores job state and logs in SQLite,
17
17
  and serves this dashboard. It's the only thing that needs a public HTTPS port.
18
18
  .col-md-4
@@ -51,13 +51,13 @@
51
51
  li #[strong The Coordinator is the single source of truth.] It assigns a job inside a single SQLite transaction, so a job can never be assigned twice.
52
52
  li #[strong A job is a command, not a script upload.] The Client runs your #[code --command] as its service user, never as root. Issue agent tokens only to trusted pipelines and people.
53
53
 
54
- h3.h6 Resource and job states
54
+ h3.h6 Runner and job states
55
55
  .row.g-3.small
56
56
  .col-md-6
57
57
  table.table.table-sm.mb-0
58
58
  thead
59
59
  tr
60
- th Resource
60
+ th Runner
61
61
  th Meaning
62
62
  tbody
63
63
  tr
@@ -18,7 +18,7 @@
18
18
  sudo npm i -g @andrian.yablonskyy/thub-client
19
19
  nano ~/.config/thub/client.json # coordinatorUrl, type, joinKey
20
20
  sudo systemctl enable --now thub-client@client
21
- | It shows up on #[a(href="/resources") Resources] by itself, usually within 10 s.
21
+ | It shows up on #[a(href="/runners") Runners] by itself, usually within 10 s.
22
22
  li
23
23
  strong Create users and their access keys
24
24
  | on #[a(href="/admin/users") Users] (admins and maintainers), or on the Coordinator host with
@@ -3,10 +3,10 @@
3
3
  //- <code>, with everything else HTML-escaped first (so `<instance>` shows as text).
4
4
  -
5
5
  const faq = [
6
- ['A Client doesn\'t appear on Resources', 'Check `journalctl -u thub-client@<instance> -f`. A `503` at registration means the Coordinator has no `clientJoinKey`; a `401` means the `joinKey` differs. Also check that `coordinatorUrl` is reachable from the lab (`curl -sI <url>/login`).'],
7
- ['A Client flaps or two machines show as one resource', 'Both use the same `.client-id` (a cloned VM or a shared config file). Delete `<varDir>/<instance>/.client-id` on one of them and restart it, or set a distinct `clientId`.'],
8
- ['Name already in use (`409`)', 'Another live Client holds that name. Rename one of them in its config or from the resource card. A name held by an `OUT_OF_SERVICE` resource is reclaimed automatically.'],
9
- ['The job is rejected with `422`', 'No registered Client can ever match its type, labels, group or client. Compare `thub resources` with your `--type` and `--board`/`--label`; your group (set on the dashboard, see `thub whoami`) must have a Client of that kind.'],
6
+ ['A Client doesn\'t appear on Runners', 'Check `journalctl -u thub-client@<instance> -f`. A `503` at registration means the Coordinator has no `clientJoinKey`; a `401` means the `joinKey` differs. Also check that `coordinatorUrl` is reachable from the lab (`curl -sI <url>/login`).'],
7
+ ['A Client flaps or two machines show as one runner', 'Both use the same `.client-id` (a cloned VM or a shared config file). Delete `<varDir>/<instance>/.client-id` on one of them and restart it, or set a distinct `clientId`.'],
8
+ ['Name already in use (`409`)', 'Another live Client holds that name. Rename one of them in its config or from the runner card. A name held by an `OUT_OF_SERVICE` runner is reclaimed automatically.'],
9
+ ['The job is rejected with `422`', 'No registered Client can ever match its type, labels, group or client. Compare `thub resources` with your `--type` and `--label`s; your group (set on the dashboard, see `thub whoami`) must have a Client of that kind.'],
10
10
  ['The job stays `QUEUED`', 'Matching Clients are all `BUSY`, locked (`local`), in `MAINTENANCE` or offline, or the job is pinned with `--client`. It times out after `--timeout`.'],
11
11
  ['`ERROR` during prepare', 'The clone, download or DUT image pull failed; the job log names the reason. For git, check the Client user\'s keys and `known_hosts` (`ssh -T git@<host>` as that user). For images, check the Client user\'s `docker login`.'],
12
12
  ['Permission denied on `/dev/ttyUSB*` or `docker.sock`', 'Docker or the device groups were added after the Client was installed. Re-run `sudo npm i -g @andrian.yablonskyy/thub-client` so the unit gets the `dialout`/`plugdev`/`docker` groups, then restart.'],
@@ -14,11 +14,11 @@
14
14
  ['Job `LOST`', 'The Client missed 3 heartbeats (network, reboot, crash). With `requeueOnLost` the job is retried once on another Client.'],
15
15
  ['A job can\'t write to `~` (read-only file system)', 'Under systemd the Client can only write to its state directory. Write to `$THUB_WORK_DIR`, and set `DOCKER_CONFIG` there for `docker login`.'],
16
16
  ['Live logs don\'t stream through the proxy', 'Turn off response buffering for the Coordinator (nginx: `proxy_buffering off;`, Caddy: `flush_interval -1`).'],
17
- ['Finding a job, resource, group or agent', 'Use the search box on its list page (press `/` to jump to it). Every word must match, e.g. `lab-hw-01 FAILED` or a git ref. Job search covers ids, users, resources, agents, commands, repos and `--meta`, but not `--env` values (secrets). On a job page, search the log above the log viewer: `Enter`/`Shift+Enter` step through matches.'],
17
+ ['Finding a job, runner, group or agent', 'Use the search box on its list page (press `/` to jump to it). Every word must match, e.g. `lab-hw-01 FAILED` or a git ref. Job search covers ids, users, runners, agents, commands, repos and `--meta`, but not `--env` values (secrets). On a job page, search the log above the log viewer: `Enter`/`Shift+Enter` step through matches.'],
18
18
  ['Lists don\'t update by themselves (navbar shows Offline or Paused)', '`Paused`: you\'re editing something on the page (a field, the inline rename, a dialog); it resumes when you\'re done. `Offline`: the browser can\'t hold the `/live` stream open, often a proxy buffering it (nginx: `proxy_buffering off;`); reloading the page always shows current data. `Session ended`: sign in again. Background updates never extend the session\'s idle timeout.'],
19
19
  ['Sign-in says it needs HTTPS', 'The session cookie is HTTPS-only because `publicUrl` is `https://`, but the request arrived as plain HTTP. Open the dashboard through its https:// address; behind a reverse proxy, make it send `X-Forwarded-Proto: https` (nginx: `proxy_set_header X-Forwarded-Proto $scheme;`) and check `trustProxy`. To allow plain HTTP, set `session.secureCookie` to `false`.'],
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
- ['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`).'],
21
+ ['429 Too Many Requests', 'A caller went over `rateLimit.requestsPerMinute` (per agent or runner 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
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.'],
24
24
  ['The Coordinator won\'t start after an upgrade: `SqliteError: disk I/O error` (`SQLITE_IOERR_GETTEMPPATH`)', 'Its systemd unit is older than 1.1.1: it has `ProtectSystem=strict` without `PrivateTmp=yes`, so SQLite has no writable temp directory for a large migration. The migration rolls back, so no data is lost. Install the latest version with `sudo npm i -g @andrian.yablonskyy/thub-coordinator` (the dashboard can\'t update a Coordinator that\'s down), or run `sudo systemctl edit thub-coordinator`, add `[Service]` and `PrivateTmp=yes`, then `sudo systemctl restart thub-coordinator`.'],
package/views/index.pug CHANGED
@@ -21,7 +21,7 @@ block content
21
21
  .col-md-4
22
22
  .card
23
23
  .card-body
24
- h6.card-subtitle.mb-2.text-body-secondary Resources online
24
+ h6.card-subtitle.mb-2.text-body-secondary Runners online
25
25
  p.display-6= `${onlineCount} / ${resources.length}`
26
26
 
27
27
  .row.g-3(data-live="overview-cards")
@@ -13,7 +13,7 @@ block content
13
13
 
14
14
  form.row.g-2.mb-3(method="get" role="search")
15
15
  .col-12.col-md-5.col-lg-4
16
- +searchBox(filters.q, 'Search jobs: id, user, resource, command, repo…', list.urlFor({ q: '' }))
16
+ +searchBox(filters.q, 'Search jobs: id, user, runner, command, repo…', list.urlFor({ q: '' }))
17
17
  .col-auto
18
18
  select.form-select.form-select-sm(name="state" data-autosubmit)
19
19
  option(value="") any state
@@ -37,7 +37,7 @@ block content
37
37
  +sortHeader(list, 'source', 'Source')
38
38
  +sortHeader(list, 'user', 'User')
39
39
  +sortHeader(list, 'state', 'State')
40
- +sortHeader(list, 'resource', 'Resource')
40
+ +sortHeader(list, 'resource', 'Runner')
41
41
  +sortHeader(list, 'created', 'Created', 'desc')
42
42
  +sortHeader(list, 'duration', 'Duration', 'desc')
43
43
  if canManage
@@ -132,7 +132,7 @@ block content
132
132
  data-value=preset.value
133
133
  )= preset.label
134
134
  p
135
- | Deletes finished jobs that ended before then, with their logs, and the resource, agent and group history logged before then — then compacts the database file.
135
+ | Deletes finished jobs that ended before then, with their logs, and the runner, agent and group history logged before then — then compacts the database file.
136
136
  |
137
137
  strong This cannot be undone.
138
138
  | Active jobs are never touched.
@@ -29,7 +29,7 @@ block content
29
29
  if job.spec.user
30
30
  dt.col-sm-2 User
31
31
  dd.col-sm-10= job.spec.user
32
- dt.col-sm-2 Resource
32
+ dt.col-sm-2 Runner
33
33
  dd.col-sm-10= job.resource ? job.resource.name : job.resource_name ? `${job.resource_name} (removed)` : '—'
34
34
  dt.col-sm-2 Target
35
35
  dd.col-sm-10= `${job.spec.target.type} — ${(job.spec.target.labels || []).join(', ')}`
package/views/layout.pug CHANGED
@@ -53,7 +53,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
53
53
  //- Below lg the menu drops down over the page (thub.css .thub-nav,
54
54
  //- closed by public/js/actions.js): pages as a two-column grid with
55
55
  //- icons, then the tools in a row, then the account row.
56
- - const pages = user ? [['/', 'overview', 'Overview', 'speedometer2'], ['/resources', 'resources', 'Resources', 'hdd-rack'], ['/groups', 'groups', 'Groups', 'collection'], ['/jobs', 'jobs', 'Jobs', 'list-task'], ['/admin/users', 'users', 'Users', 'people'], ['/admin/agents', 'agents', 'CI tokens', 'key']].concat(user.role === 'admin' ? [['/admin/settings', 'settings', 'Settings', 'gear']] : []) : []
56
+ - const pages = user ? [['/', 'overview', 'Overview', 'speedometer2'], ['/runners', 'resources', 'Runners', 'hdd-rack'], ['/groups', 'groups', 'Groups', 'collection'], ['/jobs', 'jobs', 'Jobs', 'list-task'], ['/admin/users', 'users', 'Users', 'people'], ['/admin/agents', 'agents', 'CI tokens', 'key']].concat(user.role === 'admin' ? [['/admin/settings', 'settings', 'Settings', 'gear']] : []) : []
57
57
  ul.navbar-nav.me-auto.thub-nav-pages
58
58
  each pg in pages
59
59
  li.nav-item
@@ -88,7 +88,7 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
88
88
  aria-label="Check for updates"
89
89
  data-bs-toggle="tooltip"
90
90
  data-bs-placement="bottom"
91
- 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'
91
+ data-bs-title=updates.checkedAt ? `Check for updates now — the Coordinator and connected runners (last checked ${fmtDate(updates.checkedAt)})` : 'Check for updates now — the Coordinator and connected runners'
92
92
  )
93
93
  i.bi.bi-arrow-repeat
94
94
  if user && can.admin && updates.coordinatorUpdate
@@ -96,9 +96,56 @@ mixin configTabItems(r)
96
96
  if r.type === 'hw'
97
97
  each t in [['stlinks', 'ST-Link'], ['uarts', 'UART'], ['usbs', 'DUT USB']]
98
98
  +configTab(p, t[0], t[1])
99
+ +configTab(p, 'labels', 'Labels')
99
100
  +configTab(p, 'groups', 'Groups')
100
101
  +configTab(p, 'reboot', 'Reboot')
101
102
 
103
+ //- Labels tab: what a job's --label / --label board:must find on this Client,
104
+ //- one per line. Saved like the Groups tab (registry.setClientLabels): in
105
+ //- effect on the Coordinator at once, written to the config file's
106
+ //- `labels` by the Client.
107
+ mixin labelsPane(r, returnTo)
108
+ - const p = `cfg-${r.id}`
109
+ - const canEdit = can.operate && r.config_support === 'file'
110
+ - const pending = r.config_import && r.config_import.revision > r.config_applied_revision && r.config_import.fields && r.config_import.fields.labels
111
+ +configPane(p, 'labels')
112
+ if pending || r.config_error
113
+ .d-flex.justify-content-end.gap-1.mb-2
114
+ if pending
115
+ span.badge.text-bg-warning(
116
+ data-bs-toggle="tooltip"
117
+ data-bs-title="In effect on the Coordinator; the Client writes them to its config file on its next heartbeat, then restarts once idle"
118
+ ) not written to the Client yet
119
+ if r.config_error
120
+ span.badge.text-bg-danger(data-bs-toggle="tooltip" data-bs-title=r.config_error) refused by the Client
121
+ if r.config_support !== 'file'
122
+ p.small.text-body-secondary Labels can't be set here: #{noConfigReport} Or set #[code labels] in its config file.
123
+ form(
124
+ method="post"
125
+ action=`/runners/${r.id}/labels`
126
+ data-confirm=`Save the labels of ${r.name}? They apply at once; the Client also writes them to its config file and restarts once it has no job running.`
127
+ data-confirm-title="Save runner labels?"
128
+ data-confirm-ok="Save"
129
+ data-confirm-tone="primary"
130
+ )
131
+ input(type="hidden" name="returnTo" value=returnTo)
132
+ fieldset(disabled=!canEdit)
133
+ label.form-label.small(for=`${p}-labels-input`) One label per line
134
+ textarea.form-control.font-monospace(
135
+ id=`${p}-labels-input`
136
+ name="labels"
137
+ rows="6"
138
+ spellcheck="false"
139
+ autocapitalize="off"
140
+ placeholder="board:nucleo-f401re\nuart\nstlink"
141
+ )= r.labels.join('\n')
142
+ .form-text
143
+ | A job runs here only if this runner has every label it asks for (#[code thub run --label uart --label board:nucleo-f401re]).
144
+ | Each up to 64 characters, no spaces, commas or semicolons; up to 64 labels.
145
+ if canEdit
146
+ .d-flex.justify-content-end.mt-3
147
+ button.btn.btn-sm.btn-primary(type="submit" data-bs-toggle="tooltip" data-bs-title="Save the labels — in effect at once; the Client writes them to its config file") Save labels
148
+
102
149
  //- Groups tab: which resource groups this Client belongs to (thub run
103
150
  //- --group). Saved as a config revision the Client writes to its config
104
151
  //- file's `groups` (registry.setClientGroups); in effect on the Coordinator
@@ -129,7 +176,7 @@ mixin groupsPane(r, returnTo, groupsById)
129
176
  else
130
177
  form(
131
178
  method="post"
132
- action=`/resources/${r.id}/groups`
179
+ action=`/runners/${r.id}/groups`
133
180
  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.`
134
181
  data-confirm-title="Save Client groups?"
135
182
  data-confirm-ok="Save"
@@ -173,7 +220,7 @@ mixin configPanes(r, returnTo, groupsById)
173
220
  else
174
221
  form(
175
222
  method="post"
176
- action=`/resources/${r.id}/config`
223
+ action=`/runners/${r.id}/config`
177
224
  data-client-config="hw"
178
225
  data-confirm=`Apply these devices to ${r.name}? The Client writes them to its config file on its next heartbeat and restarts once it has no job running.`
179
226
  data-confirm-title="Save Client devices?"
@@ -198,6 +245,7 @@ mixin configPanes(r, returnTo, groupsById)
198
245
  +deviceTable('usbs', 'DUT USB devices', current.usbs || [], ['Device', 'udev devpath'])
199
246
  .form-text Device: a udev index 1–8 (→ /dev/thub/dut&lt;N&gt;-usb) or a /dev path. udev devpath: the USB port it's plugged into.
200
247
  +saveCapabilities(canEdit)
248
+ +labelsPane(r, returnTo)
201
249
  +groupsPane(r, returnTo, groupsById || {})
202
250
  +configPane(p, 'reboot')
203
251
  dl.row.mb-0
@@ -3,7 +3,7 @@
3
3
  //- --group. Groups by name; none = any resource.
4
4
  mixin groupSelect(id, selected, groups)
5
5
  select.form-select(id=id name="groupId")
6
- option(value="" selected=!selected) Any resource (no group)
6
+ option(value="" selected=!selected) Any runner (no group)
7
7
  each g in groups
8
8
  option(value=g.id selected=g.id === selected)= g.name
9
- .form-text Its jobs run only on resources in this group. Manage groups on #[a(href="/groups") Groups].
9
+ .form-text Its jobs run only on runners in this group. Manage groups on #[a(href="/groups") Groups].