@andrian.yablonskyy/thub-coordinator 1.0.37 → 1.0.38
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 +2 -2
- package/public/css/thub.css +129 -0
- package/public/js/config-import.js +37 -33
- package/public/js/copy-to-clipboard.js +21 -18
- package/public/js/help.js +93 -0
- package/public/js/list-search.js +51 -0
- package/public/js/live.js +287 -0
- package/public/js/log-viewer.js +123 -11
- package/public/js/remove-resource.js +5 -3
- package/public/js/resource-card.js +14 -13
- package/public/js/tooltips.js +3 -1
- package/src/server.js +20 -2
- package/src/services/jobs.js +29 -12
- package/src/services/list-prefs.js +4 -0
- package/src/services/live.js +124 -0
- package/src/services/search.js +59 -0
- package/src/web/routes.js +130 -21
- package/test/list-prefs.test.js +6 -0
- package/test/live.test.js +153 -0
- package/test/search.test.js +97 -0
- package/test/views.test.js +30 -0
- package/views/admin/agents.pug +28 -10
- package/views/groups/list.pug +14 -1
- package/views/help/_agent-cli.pug +103 -0
- package/views/help/_agent-setup.pug +72 -0
- package/views/help/_ci.pug +44 -0
- package/views/help/_client-machines.pug +87 -0
- package/views/help/_client-setup.pug +122 -0
- package/views/help/_coordinator.pug +134 -0
- package/views/help/_docker.pug +124 -0
- package/views/help/_env.pug +127 -0
- package/views/help/_git.pug +129 -0
- package/views/help/_mixins.pug +24 -0
- package/views/help/_overview.pug +118 -0
- package/views/help/_quick-start.pug +37 -0
- package/views/help/_troubleshooting.pug +42 -0
- package/views/help/index.pug +67 -0
- package/views/index.pug +3 -2
- package/views/jobs/list.pug +6 -3
- package/views/layout.pug +24 -1
- package/views/mixins/list-controls.pug +36 -0
- package/views/mixins/log-viewer.pug +25 -3
- package/views/mixins/resource.pug +1 -1
- package/views/resources/list.pug +7 -4
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
+section('overview', 'Product overview', 'info-circle')
|
|
2
|
+
p.
|
|
3
|
+
#[strong TestHub] is a small, self-hosted job network. It lets CI/CD pipelines and individual
|
|
4
|
+
developers run firmware and software tests on #[strong real hardware] or #[strong emulators]
|
|
5
|
+
that sit in a private lab behind a firewall. Nothing in the lab has to be reachable from outside.
|
|
6
|
+
|
|
7
|
+
.row.g-3.mb-3
|
|
8
|
+
.col-md-4
|
|
9
|
+
.card.h-100
|
|
10
|
+
.card-body
|
|
11
|
+
h3.h6.card-title
|
|
12
|
+
i.bi.bi-hdd-network.me-1
|
|
13
|
+
| Coordinator
|
|
14
|
+
p.card-text.small.mb-0.
|
|
15
|
+
This service. It holds the job queue and the resource registry, and runs the scheduler and
|
|
16
|
+
the heartbeat monitor. It stores logs and artifacts in SQLite plus a local directory,
|
|
17
|
+
and serves this dashboard. It's the only thing that needs a public HTTPS port.
|
|
18
|
+
.col-md-4
|
|
19
|
+
.card.h-100
|
|
20
|
+
.card-body
|
|
21
|
+
h3.h6.card-title
|
|
22
|
+
i.bi.bi-terminal.me-1
|
|
23
|
+
| Agent (#[code thub])
|
|
24
|
+
p.card-text.small.mb-0.
|
|
25
|
+
A CLI, and the only way to create jobs. The same #[code thub run] command works in a
|
|
26
|
+
GitHub Actions step and on a developer's laptop, so a CI failure can be reproduced exactly.
|
|
27
|
+
.col-md-4
|
|
28
|
+
.card.h-100
|
|
29
|
+
.card-body
|
|
30
|
+
h3.h6.card-title
|
|
31
|
+
i.bi.bi-motherboard.me-1
|
|
32
|
+
| Client (#[code thub-client])
|
|
33
|
+
p.card-text.small.mb-0.
|
|
34
|
+
A daemon (a systemd service) on a lab machine. Each instance owns one DUT slot. It connects
|
|
35
|
+
#[em outbound] to the Coordinator, picks up jobs, runs them and streams logs back.
|
|
36
|
+
Type #[strong HW] drives a physical board; type #[strong SW] runs a DUT emulator in Docker.
|
|
37
|
+
|
|
38
|
+
h3.h6 How a job flows
|
|
39
|
+
ol.small
|
|
40
|
+
li #[code thub run …] submits a job spec (type, labels, command, inputs) with an agent token.
|
|
41
|
+
li The Coordinator queues it (#[code QUEUED]) and the scheduler assigns it to an idle Client whose type, labels and group match.
|
|
42
|
+
li The Client accepts it, clones #[code --git-repo], downloads #[code --download-file] files and prepares the DUT (#[code PREPARING]).
|
|
43
|
+
li It runs #[code --command] with #[code sh -c], and output streams live to the Agent and the dashboard (#[code RUNNING]).
|
|
44
|
+
li The exit code is the verdict: #[code 0] = #[code PASSED], anything else = #[code FAILED]. JUnit files and artifacts are uploaded.
|
|
45
|
+
li The Agent exits with the verdict code, so a CI step passes or fails with it.
|
|
46
|
+
|
|
47
|
+
h3.h6 Key design rules
|
|
48
|
+
ul.small
|
|
49
|
+
li #[strong Clients only connect outbound] over HTTPS: registration, heartbeat, long-poll, logs, results. The lab firewall allows no inbound traffic.
|
|
50
|
+
li #[strong The Agent is the only way to create jobs.] CI and manual runs are identical.
|
|
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
|
+
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
|
+
|
|
54
|
+
h3.h6 Resource and job states
|
|
55
|
+
.row.g-3.small
|
|
56
|
+
.col-md-6
|
|
57
|
+
table.table.table-sm.mb-0
|
|
58
|
+
thead
|
|
59
|
+
tr
|
|
60
|
+
th Resource
|
|
61
|
+
th Meaning
|
|
62
|
+
tbody
|
|
63
|
+
tr
|
|
64
|
+
td: code REGISTERED
|
|
65
|
+
td Just registered, no heartbeat yet
|
|
66
|
+
tr
|
|
67
|
+
td: code IDLE
|
|
68
|
+
td Online and free: schedulable
|
|
69
|
+
tr
|
|
70
|
+
td: code BUSY
|
|
71
|
+
td Running a job (#[code ci] / #[code cli]) or locked by hand (#[code local])
|
|
72
|
+
tr
|
|
73
|
+
td: code OUT_OF_SERVICE
|
|
74
|
+
td 3 heartbeats missed
|
|
75
|
+
tr
|
|
76
|
+
td: code MAINTENANCE
|
|
77
|
+
td Disabled by an admin
|
|
78
|
+
.col-md-6
|
|
79
|
+
table.table.table-sm.mb-0
|
|
80
|
+
thead
|
|
81
|
+
tr
|
|
82
|
+
th Job
|
|
83
|
+
th Meaning
|
|
84
|
+
tbody
|
|
85
|
+
tr
|
|
86
|
+
td: code QUEUED → ASSIGNED
|
|
87
|
+
td Waiting for, then bound to, a Client
|
|
88
|
+
tr
|
|
89
|
+
td: code PREPARING → RUNNING
|
|
90
|
+
td Inputs and DUT being prepared, then the command runs
|
|
91
|
+
tr
|
|
92
|
+
td: code PASSED / FAILED
|
|
93
|
+
td The command's exit code: 0, or non-zero
|
|
94
|
+
tr
|
|
95
|
+
td: code ERROR / TIMEOUT
|
|
96
|
+
td Infrastructure problem, or #[code --timeout] exceeded
|
|
97
|
+
tr
|
|
98
|
+
td: code CANCELED / LOST
|
|
99
|
+
td Canceled, or the Client went offline mid-job
|
|
100
|
+
|
|
101
|
+
+section('use-cases', 'Use cases', 'lightbulb')
|
|
102
|
+
-
|
|
103
|
+
const useCases = [
|
|
104
|
+
{ icon: 'cpu', title: 'Hardware-in-the-loop CI', text: 'Every push builds firmware in the cloud, then a GitHub Actions test job runs `thub run --type hw --wait`: a lab Client flashes a real board through ST-Link, captures its UART, runs the tests and fails the pipeline on a non-zero exit.' },
|
|
105
|
+
{ icon: 'pc-display', title: 'Emulator (SW) test farms', text: 'SW Clients on real or virtual Linux machines run each job’s DUT image (QEMU, Renode, a simulator) in a sandboxed Docker container. The tests talk to it at $THUB_DUT_HOST. Scale out by adding VMs.' },
|
|
106
|
+
{ icon: 'person-workspace', title: 'Remote access for developers', text: 'Developers reproduce a CI failure on the exact bench it happened on (`--client lab-hw-01`) from home, with the same command CI ran, and stream the board’s console live.' },
|
|
107
|
+
{ icon: 'lock', title: 'Shared benches without collisions', text: 'The scheduler hands out one job per bench. An engineer who needs a board by hand runs `thub-client lock` so nothing is scheduled there until `unlock`.' },
|
|
108
|
+
{ icon: 'collection', title: 'Dedicated pools', text: 'Groups and labels (`board:nucleo-f401re`, `uart`) route jobs to the right hardware, e.g. a nightly pool separate from the PR pool.' },
|
|
109
|
+
{ icon: 'box-seam', title: 'Any command-line test suite', text: 'Anything that runs from a shell and reports through its exit code: pytest, ctest, Robot Framework, a Dockerized test runner, a flashing and smoke-test script.' }
|
|
110
|
+
]
|
|
111
|
+
.row.g-3
|
|
112
|
+
each uc in useCases
|
|
113
|
+
.col-md-6
|
|
114
|
+
.d-flex.gap-3
|
|
115
|
+
i.bi.fs-4.text-primary(class=`bi-${uc.icon}`)
|
|
116
|
+
div
|
|
117
|
+
h3.h6.mb-1= uc.title
|
|
118
|
+
p.small.text-body-secondary.mb-0!= uc.text.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
+section('quick-start', 'Quick start', 'rocket-takeoff')
|
|
2
|
+
p Five steps from nothing to a first job. Every step links to its full section.
|
|
3
|
+
ol.thub-help-steps
|
|
4
|
+
li
|
|
5
|
+
strong Install the Coordinator
|
|
6
|
+
| on a cloud VM behind an HTTPS reverse proxy (#[a(href="#coordinator") Coordinator setup]):
|
|
7
|
+
+code('Coordinator host').
|
|
8
|
+
sudo npm i -g @andrian.yablonskyy/thub-coordinator
|
|
9
|
+
thub-admin join-key generate # → put it in clientJoinKey
|
|
10
|
+
nano ~/.config/thub/coordinator.json # publicUrl, clientJoinKey
|
|
11
|
+
sudo systemctl restart thub-coordinator
|
|
12
|
+
thub-admin create-admin admin 'a-strong-password'
|
|
13
|
+
li
|
|
14
|
+
strong Install a Client
|
|
15
|
+
| on each lab machine, with the same join key (#[a(href="#client-setup") Client setup]):
|
|
16
|
+
+code('Lab machine (Ubuntu 26.04)').
|
|
17
|
+
sudo apt install -y nodejs npm docker.io # + stlink-tools openocd for HW
|
|
18
|
+
sudo npm i -g @andrian.yablonskyy/thub-client
|
|
19
|
+
nano ~/.config/thub/client.json # coordinatorUrl, type, joinKey
|
|
20
|
+
sudo systemctl enable --now thub-client@client
|
|
21
|
+
| It shows up on #[a(href="/resources") Resources] by itself, usually within 10 s.
|
|
22
|
+
li
|
|
23
|
+
strong Issue an agent token
|
|
24
|
+
| on #[a(href="/admin/agents") Agents] (admins), or on the Coordinator host with
|
|
25
|
+
| #[code thub-admin agent add <name> --kind ci|cli]. The token is shown once.
|
|
26
|
+
li
|
|
27
|
+
strong Install and configure the Agent
|
|
28
|
+
| (#[a(href="#agent-setup") Agent setup]):
|
|
29
|
+
+code('Developer machine').
|
|
30
|
+
npm i -g @andrian.yablonskyy/thub-agent
|
|
31
|
+
thub config set url #{coordinatorUrl}
|
|
32
|
+
thub config set token agt_…
|
|
33
|
+
li
|
|
34
|
+
strong Run a job
|
|
35
|
+
| (#[a(href="#agent-cli") Agent CLI reference]):
|
|
36
|
+
+code('Developer machine').
|
|
37
|
+
thub run --type sw --command 'uname -a && echo hello from the lab' --wait
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
+section('troubleshooting', 'Troubleshooting', 'life-preserver')
|
|
2
|
+
//- `…` in a question or answer marks a command or parameter: rendered as
|
|
3
|
+
//- <code>, with everything else HTML-escaped first (so `<instance>` shows as text).
|
|
4
|
+
-
|
|
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`, `--board`/`--label` and `--group`.'],
|
|
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
|
+
['`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
|
+
['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.'],
|
|
13
|
+
['A configured device is shown as missing', 'Check the `devpath` with `udevadm info -a -n <dev>`, then `thub-client udev --print`; restart the Client to regenerate the udev rules.'],
|
|
14
|
+
['Job `LOST`', 'The Client missed 3 heartbeats (network, reboot, crash). With `requeueOnLost` the job is retried once on another Client.'],
|
|
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
|
+
['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.'],
|
|
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
|
+
['Locked out of the dashboard', 'Restart the Coordinator once with `THUB_BOOTSTRAP_ADMIN_PASSWORD=<new>` (resets the `admin` user), then unset it.']
|
|
20
|
+
]
|
|
21
|
+
const withCode = (text) => text
|
|
22
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
23
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
24
|
+
.accordion#help-faq
|
|
25
|
+
each item, i in faq
|
|
26
|
+
.accordion-item
|
|
27
|
+
h3.accordion-header
|
|
28
|
+
button.accordion-button.collapsed.small(
|
|
29
|
+
type="button"
|
|
30
|
+
data-bs-toggle="collapse"
|
|
31
|
+
data-bs-target=`#faq-${i}`
|
|
32
|
+
aria-expanded="false"
|
|
33
|
+
aria-controls=`faq-${i}`
|
|
34
|
+
)
|
|
35
|
+
//- One inline box: .accordion-button is flex, which would drop
|
|
36
|
+
//- the spaces around <code>.
|
|
37
|
+
span!= withCode(item[0])
|
|
38
|
+
.accordion-collapse.collapse(id=`faq-${i}` data-bs-parent="#help-faq")
|
|
39
|
+
.accordion-body.small!= withCode(item[1])
|
|
40
|
+
p.small.text-body-secondary.mt-3.mb-0.
|
|
41
|
+
The full design reference (API, state machines, database schema) is in the project README. Everything
|
|
42
|
+
that happens is also recorded per job: open it from #[a(href="/jobs") Jobs] for its spec, timeline and log.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
extends ../layout
|
|
2
|
+
include _mixins
|
|
3
|
+
|
|
4
|
+
block content
|
|
5
|
+
-
|
|
6
|
+
const toc = [
|
|
7
|
+
{ id: 'overview', label: 'Product overview' },
|
|
8
|
+
{ id: 'use-cases', label: 'Use cases' },
|
|
9
|
+
{ id: 'quick-start', label: 'Quick start' },
|
|
10
|
+
{ id: 'coordinator', label: 'Coordinator setup' },
|
|
11
|
+
{ id: 'agent-setup', label: 'Agent setup' },
|
|
12
|
+
{ id: 'client-setup', label: 'Client setup' },
|
|
13
|
+
{ id: 'client-machines', label: 'Client machines (HW / SW)' },
|
|
14
|
+
{ id: 'docker', label: 'Docker registry' },
|
|
15
|
+
{ id: 'git', label: 'Git repositories' },
|
|
16
|
+
{ id: 'agent-cli', label: 'Agent CLI reference' },
|
|
17
|
+
{ id: 'env', label: 'Environment variables' },
|
|
18
|
+
{ id: 'ci', label: 'CI/CD (GitHub Actions)' },
|
|
19
|
+
{ id: 'troubleshooting', label: 'Troubleshooting' }
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
.row.g-4
|
|
23
|
+
.col-lg-3.d-none.d-lg-block
|
|
24
|
+
.thub-help-toc.sticky-top
|
|
25
|
+
input.form-control.form-control-sm.mb-3#help-filter(
|
|
26
|
+
type="search"
|
|
27
|
+
placeholder="Filter help…"
|
|
28
|
+
aria-label="Filter help sections"
|
|
29
|
+
)
|
|
30
|
+
nav.nav.nav-pills.flex-column#help-toc
|
|
31
|
+
each item in toc
|
|
32
|
+
a.nav-link.py-1(href=`#${item.id}` data-help-toc=item.id)= item.label
|
|
33
|
+
p.small.text-body-secondary.mt-3.mb-0
|
|
34
|
+
| Coordinator v#{coordinatorVersion} · examples use
|
|
35
|
+
br
|
|
36
|
+
code.small= coordinatorUrl
|
|
37
|
+
|
|
38
|
+
.col-lg-9
|
|
39
|
+
.d-flex.flex-wrap.align-items-center.justify-content-between.gap-2.mb-3
|
|
40
|
+
h1.h3.mb-0 TestHub help
|
|
41
|
+
//- Small screens have no sidebar: jump menu + filter instead.
|
|
42
|
+
.d-lg-none.d-flex.gap-2.w-100
|
|
43
|
+
select.form-select.form-select-sm#help-jump(aria-label="Jump to section")
|
|
44
|
+
each item in toc
|
|
45
|
+
option(value=item.id)= item.label
|
|
46
|
+
input.form-control.form-control-sm#help-filter-sm(type="search" placeholder="Filter…" aria-label="Filter help sections")
|
|
47
|
+
p.lead.
|
|
48
|
+
Everything needed to run TestHub: what it is, how to install and configure the
|
|
49
|
+
Coordinator, Agent and Client, how to prepare lab machines, and how jobs use Docker,
|
|
50
|
+
git and environment variables.
|
|
51
|
+
p.text-body-secondary.small#help-no-match(hidden) No section matches the filter.
|
|
52
|
+
|
|
53
|
+
include _overview
|
|
54
|
+
include _quick-start
|
|
55
|
+
include _coordinator
|
|
56
|
+
include _agent-setup
|
|
57
|
+
include _client-setup
|
|
58
|
+
include _client-machines
|
|
59
|
+
include _docker
|
|
60
|
+
include _git
|
|
61
|
+
include _agent-cli
|
|
62
|
+
include _env
|
|
63
|
+
include _ci
|
|
64
|
+
include _troubleshooting
|
|
65
|
+
|
|
66
|
+
block scripts
|
|
67
|
+
script(src="/js/help.js")
|
package/views/index.pug
CHANGED
|
@@ -6,7 +6,8 @@ include mixins/updates
|
|
|
6
6
|
block content
|
|
7
7
|
h1.h4.mb-2 Overview
|
|
8
8
|
|
|
9
|
-
.
|
|
9
|
+
//- data-live: swapped in place when it changes (public/js/live.js).
|
|
10
|
+
.row.g-3.mb-4(data-live="overview-stats")
|
|
10
11
|
.col-md-4
|
|
11
12
|
.card
|
|
12
13
|
.card-body
|
|
@@ -23,7 +24,7 @@ block content
|
|
|
23
24
|
h6.card-subtitle.mb-2.text-body-secondary Resources online
|
|
24
25
|
p.display-6= `${onlineCount} / ${resources.length}`
|
|
25
26
|
|
|
26
|
-
.row.g-3
|
|
27
|
+
.row.g-3(data-live="overview-cards")
|
|
27
28
|
each r in resources
|
|
28
29
|
.col-sm-6.col-md-4.col-lg-3
|
|
29
30
|
.card.h-100(role="button" tabindex="0" data-resource-card=`resource-card-${r.id}` class=`border-${{IDLE:'success',BUSY:'primary',OUT_OF_SERVICE:'danger',MAINTENANCE:'secondary',REGISTERED:'secondary'}[r.status] || 'secondary'}`)
|
package/views/jobs/list.pug
CHANGED
|
@@ -11,7 +11,9 @@ block content
|
|
|
11
11
|
button.btn.btn-sm.btn-outline-warning(type="button" data-bs-toggle="modal" data-bs-target="#reset-queue-modal") Reset queue
|
|
12
12
|
button.btn.btn-sm.btn-outline-danger.ms-1(type="button" data-bs-toggle="modal" data-bs-target="#clean-history-modal") Clean up database
|
|
13
13
|
|
|
14
|
-
form.row.g-2.mb-3(method="get")
|
|
14
|
+
form.row.g-2.mb-3(method="get" role="search")
|
|
15
|
+
.col-12.col-md-5.col-lg-4
|
|
16
|
+
+searchBox(filters.q, 'Search jobs: id, user, resource, command, repo…', list.urlFor({ q: '' }))
|
|
15
17
|
.col-auto
|
|
16
18
|
select.form-select.form-select-sm(name="state" onchange="this.form.submit()")
|
|
17
19
|
option(value="") any state
|
|
@@ -25,7 +27,8 @@ block content
|
|
|
25
27
|
.col-auto.ms-auto
|
|
26
28
|
+pageSizePicker(list)
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
div(data-live="list-toolbar")
|
|
31
|
+
+listToolbar(list, 'jobs')
|
|
29
32
|
|
|
30
33
|
table.table.table-hover.align-middle
|
|
31
34
|
thead
|
|
@@ -39,7 +42,7 @@ block content
|
|
|
39
42
|
+sortHeader(list, 'duration', 'Duration', 'desc')
|
|
40
43
|
if canManage
|
|
41
44
|
th
|
|
42
|
-
tbody
|
|
45
|
+
tbody(data-live="rows")
|
|
43
46
|
each j in jobs
|
|
44
47
|
tr
|
|
45
48
|
td: a(href=`/jobs/${j.id}`)= j.id
|
package/views/layout.pug
CHANGED
|
@@ -7,7 +7,9 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
7
7
|
link(rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css")
|
|
8
8
|
link(rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css")
|
|
9
9
|
link(rel="stylesheet" href="/css/thub.css")
|
|
10
|
-
|
|
10
|
+
//- liveTopics (routes.js): the page updates itself when these change
|
|
11
|
+
//- (public/js/live.js, services/live.js).
|
|
12
|
+
body(data-live-topics=user && liveTopics ? liveTopics : false)
|
|
11
13
|
nav.navbar.navbar-expand-lg.bg-body-tertiary.border-bottom
|
|
12
14
|
.container-fluid
|
|
13
15
|
a.navbar-brand.d-flex.flex-column.lh-1.py-1(href="/")
|
|
@@ -42,6 +44,24 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
42
44
|
li.nav-item
|
|
43
45
|
a.nav-link(href="/admin/agents" class=active === 'agents' ? 'active' : '') Agents
|
|
44
46
|
ul.navbar-nav
|
|
47
|
+
if user && liveTopics
|
|
48
|
+
//- Live-update status, driven by public/js/live.js.
|
|
49
|
+
li.nav-item.d-flex.align-items-center.me-3
|
|
50
|
+
span#thub-live.thub-live.small.text-body-secondary(data-state="connecting" title="Connecting to live updates…" role="status")
|
|
51
|
+
span.thub-live-dot(aria-hidden="true")
|
|
52
|
+
span.thub-live-label Live
|
|
53
|
+
if user
|
|
54
|
+
li.nav-item.d-flex.align-items-center.me-2
|
|
55
|
+
a.nav-link.d-flex.align-items-center(
|
|
56
|
+
href="/help"
|
|
57
|
+
class=active === 'help' ? 'active' : ''
|
|
58
|
+
aria-current=active === 'help' ? 'page' : false
|
|
59
|
+
data-bs-toggle="tooltip"
|
|
60
|
+
data-bs-placement="bottom"
|
|
61
|
+
data-bs-title="Product guide, setup and CLI reference"
|
|
62
|
+
)
|
|
63
|
+
i.bi.bi-question-circle.me-1
|
|
64
|
+
| Help
|
|
45
65
|
if user && user.role === 'admin'
|
|
46
66
|
li.nav-item.d-flex.align-items-center.me-2
|
|
47
67
|
form(method="post" action="/updates/check" onsubmit="this.querySelector('button').disabled = true; this.querySelector('i').className = 'spinner-border spinner-border-sm';")
|
|
@@ -171,6 +191,9 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
171
191
|
script(src="/js/toasts.js")
|
|
172
192
|
script(src="/js/copy-to-clipboard.js")
|
|
173
193
|
script(src="/js/confirm.js")
|
|
194
|
+
script(src="/js/list-search.js")
|
|
195
|
+
if user && liveTopics
|
|
196
|
+
script(src="/js/live.js")
|
|
174
197
|
if user && user.role === 'admin' && updates.coordinatorPending
|
|
175
198
|
script(src="/js/update-watch.js")
|
|
176
199
|
block scripts
|
|
@@ -16,6 +16,38 @@ mixin pageSizePicker(list)
|
|
|
16
16
|
each s in list.pageSizes
|
|
17
17
|
option(value=list.urlFor({ size: s }) selected=list.prefs.size === s)= s === 'all' ? 'All' : s
|
|
18
18
|
|
|
19
|
+
//- Search box (?q=) for a list page: every word must match (routes.js,
|
|
20
|
+
//- services/search.js). Goes inside a GET form — the Jobs filter form, or
|
|
21
|
+
//- +searchForm on its own. `/` focuses it, Esc clears (public/js/list-search.js).
|
|
22
|
+
mixin searchBox(q, placeholder, clearUrl)
|
|
23
|
+
.input-group.input-group-sm.thub-search
|
|
24
|
+
span.input-group-text
|
|
25
|
+
i.bi.bi-search
|
|
26
|
+
input.form-control(
|
|
27
|
+
type="search"
|
|
28
|
+
name="q"
|
|
29
|
+
value=q
|
|
30
|
+
placeholder=placeholder
|
|
31
|
+
aria-label=placeholder
|
|
32
|
+
maxlength="200"
|
|
33
|
+
autocomplete="off"
|
|
34
|
+
data-list-search
|
|
35
|
+
data-bs-toggle="tooltip"
|
|
36
|
+
data-bs-title="Press Enter to search · / to focus · Esc to clear"
|
|
37
|
+
)
|
|
38
|
+
if q
|
|
39
|
+
a.btn.btn-outline-secondary(href=clearUrl aria-label="Clear search" data-list-search-clear)
|
|
40
|
+
i.bi.bi-x-lg
|
|
41
|
+
|
|
42
|
+
mixin searchForm(q, placeholder, clearUrl)
|
|
43
|
+
form.m-0(method="get" role="search")
|
|
44
|
+
+searchBox(q, placeholder, clearUrl)
|
|
45
|
+
|
|
46
|
+
//- "matching “foo”" after a list's count, when a search is active.
|
|
47
|
+
mixin searchNote(q)
|
|
48
|
+
if q
|
|
49
|
+
| matching “#{q}”
|
|
50
|
+
|
|
19
51
|
//- Shown above the table: "Showing 26–50 of 312 jobs" and the page links.
|
|
20
52
|
mixin listToolbar(list, noun)
|
|
21
53
|
- const p = list.pagination
|
|
@@ -25,6 +57,10 @@ mixin listToolbar(list, noun)
|
|
|
25
57
|
| Showing #{p.from}–#{p.to} of #{p.total} #{noun}
|
|
26
58
|
else
|
|
27
59
|
| No #{noun}
|
|
60
|
+
+searchNote(list.q)
|
|
61
|
+
if list.q
|
|
62
|
+
| ·
|
|
63
|
+
a(href=list.urlFor({ q: '' })) show all
|
|
28
64
|
if p.pages > 1
|
|
29
65
|
+pager(list)
|
|
30
66
|
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
mixin logViewer(jobId, jobActive)
|
|
2
|
-
.
|
|
3
|
-
|
|
2
|
+
//- Stream filter + search (public/js/log-viewer.js): matches are
|
|
3
|
+
//- highlighted; Enter / Shift+Enter or the arrows step through them.
|
|
4
|
+
.d-flex.flex-wrap.align-items-center.gap-2.mb-2
|
|
5
|
+
select#stream-filter.form-select.form-select-sm.w-auto(aria-label="Log stream")
|
|
4
6
|
option(value="") all streams
|
|
5
7
|
option(value="uart") uart
|
|
6
8
|
option(value="runner") runner
|
|
7
9
|
option(value="flash") flash
|
|
8
10
|
option(value="emulator") emulator
|
|
9
|
-
|
|
11
|
+
.input-group.input-group-sm.thub-search(role="search")
|
|
12
|
+
span.input-group-text
|
|
13
|
+
i.bi.bi-search
|
|
14
|
+
input#log-search.form-control(
|
|
15
|
+
type="search"
|
|
16
|
+
placeholder="Search log…"
|
|
17
|
+
aria-label="Search log"
|
|
18
|
+
autocomplete="off"
|
|
19
|
+
maxlength="200"
|
|
20
|
+
data-bs-toggle="tooltip"
|
|
21
|
+
data-bs-title="Enter: next match · Shift+Enter: previous · Esc: clear"
|
|
22
|
+
)
|
|
23
|
+
span#log-search-count.input-group-text.small(aria-live="polite" hidden)
|
|
24
|
+
button#log-search-prev.btn.btn-outline-secondary(type="button" aria-label="Previous match" disabled)
|
|
25
|
+
i.bi.bi-chevron-up
|
|
26
|
+
button#log-search-next.btn.btn-outline-secondary(type="button" aria-label="Next match" disabled)
|
|
27
|
+
i.bi.bi-chevron-down
|
|
28
|
+
.form-check.form-check-inline.small.mb-0
|
|
29
|
+
input#log-search-only.form-check-input(type="checkbox")
|
|
30
|
+
label.form-check-label(for="log-search-only") Display only matching lines
|
|
31
|
+
pre#log-viewer.bg-body-tertiary.p-3.rounded.position-relative(
|
|
10
32
|
style="height: 420px; overflow-y: auto;"
|
|
11
33
|
data-job-id=jobId
|
|
12
34
|
data-job-active=jobActive ? 'true' : 'false'
|
|
@@ -322,7 +322,7 @@ mixin resourceCard(r, groupsById, returnTo)
|
|
|
322
322
|
) Connected USB devices
|
|
323
323
|
|
|
324
324
|
.tab-content
|
|
325
|
-
.tab-pane.fade.show.active(id=`rc-${r.id}-details` role="tabpanel" aria-labelledby=`rc-${r.id}-details-tab` tabindex="0")
|
|
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
327
|
+configPanes(r, returnTo)
|
|
328
328
|
.tab-pane.fade(id=`rc-${r.id}-usb` role="tabpanel" aria-labelledby=`rc-${r.id}-usb-tab` tabindex="0")
|
package/views/resources/list.pug
CHANGED
|
@@ -6,15 +6,18 @@ include ../mixins/list-controls
|
|
|
6
6
|
|
|
7
7
|
block content
|
|
8
8
|
h1.h4.mb-3 Resources
|
|
9
|
-
|
|
9
|
+
.d-flex.flex-wrap.align-items-center.gap-2.mb-3
|
|
10
|
+
+updatesToolbar('client', 'clients', '/resources')
|
|
11
|
+
p.text-body-secondary.small: i.
|
|
10
12
|
Resources register themselves — start a Client with the shared join key and it appears here. Click a resource for its details, including IP addresses.
|
|
11
13
|
|
|
12
14
|
.d-flex.flex-wrap.align-items-center.gap-2.mb-3
|
|
13
|
-
+
|
|
15
|
+
+searchForm(list.q, 'Search resources: name, label, group, status, IP…', list.urlFor({ q: '' }))
|
|
14
16
|
.ms-auto
|
|
15
17
|
+pageSizePicker(list)
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
div(data-live="list-toolbar")
|
|
20
|
+
+listToolbar(list, 'resources')
|
|
18
21
|
|
|
19
22
|
table.table.table-hover.align-middle
|
|
20
23
|
thead
|
|
@@ -29,7 +32,7 @@ block content
|
|
|
29
32
|
+sortHeader(list, 'heartbeat', 'Last heartbeat', 'desc')
|
|
30
33
|
if user.role === 'admin'
|
|
31
34
|
th Actions
|
|
32
|
-
tbody
|
|
35
|
+
tbody(data-live="rows")
|
|
33
36
|
each r in resources
|
|
34
37
|
tr(role="button" tabindex="0" data-resource-card=`resource-card-${r.id}`)
|
|
35
38
|
td= r.name
|