@andrian.yablonskyy/thub-coordinator 1.1.0 → 1.1.1
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 +35 -18
- package/public/js/client-config.js +0 -1
- package/src/services/registry.js +14 -3
- package/test/groups-tab.test.js +45 -0
- package/views/layout.pug +23 -9
- package/views/mixins/client-config.pug +12 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrian.yablonskyy/thub-coordinator",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "TestHub Coordinator — job queue, resource registry, scheduler, heartbeat monitor, log/artifact store and web dashboard",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"engines": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"lint:fix": "eslint . --fix"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@andrian.yablonskyy/thub-common": "^1.1.
|
|
22
|
+
"@andrian.yablonskyy/thub-common": "^1.1.1",
|
|
23
23
|
"better-sqlite3": "^12.4.1",
|
|
24
24
|
"express": "^5.1.0",
|
|
25
25
|
"express-session": "^1.18.2",
|
package/public/css/thub.css
CHANGED
|
@@ -86,17 +86,33 @@
|
|
|
86
86
|
padding: 0.45rem 0.65rem;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
/* The client details modal keeps one size whichever tab is open — the
|
|
90
|
+
tallest tab's — instead of growing and shrinking (and, centered, jumping)
|
|
91
|
+
on every switch. Every pane sits in the same grid cell, so the tab area is
|
|
92
|
+
as tall as the tallest; inactive ones are hidden with `visibility`, which
|
|
93
|
+
keeps their height (and still takes them out of the tab order and of what
|
|
94
|
+
screen readers read). The device tabs sit inside their shared
|
|
95
|
+
<form>/<fieldset> (one Save for all of them), so that fieldset stacks its
|
|
96
|
+
panes the same way. The USB tab's output scrolls in its own box (360 px),
|
|
97
|
+
so a long lsusb doesn't make every tab that tall. */
|
|
98
|
+
.thub-resource-card .tab-content,
|
|
99
|
+
.thub-resource-card .tab-content > form > fieldset {
|
|
100
|
+
display: grid;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.thub-resource-card .tab-content > *,
|
|
104
|
+
.thub-resource-card .tab-content > form > fieldset > * {
|
|
105
|
+
grid-area: 1 / 1;
|
|
106
|
+
min-width: 0;
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
.tab-content .tab-pane
|
|
109
|
+
.thub-resource-card .tab-content .tab-pane {
|
|
99
110
|
display: block;
|
|
111
|
+
visibility: hidden;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.thub-resource-card .tab-content .tab-pane.active {
|
|
115
|
+
visibility: visible;
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
/* Help page (views/help/): sticky table of contents, code blocks with a
|
|
@@ -233,9 +249,6 @@
|
|
|
233
249
|
itself on short screens, and is laid out for thumbs: pages as a two-column
|
|
234
250
|
grid, the tools in one row, the account in the next. public/js/actions.js
|
|
235
251
|
closes it on a tap outside, Esc, or choosing a page. */
|
|
236
|
-
.thub-nav-break {
|
|
237
|
-
display: none;
|
|
238
|
-
}
|
|
239
252
|
|
|
240
253
|
@media (max-width: 991.98px) {
|
|
241
254
|
.navbar .thub-nav {
|
|
@@ -295,15 +308,19 @@
|
|
|
295
308
|
min-height: 2.5rem;
|
|
296
309
|
}
|
|
297
310
|
|
|
298
|
-
/* The account row
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
311
|
+
/* The account row, first in the menu: you, your role, your profile, and
|
|
312
|
+
sign-out at its end. */
|
|
313
|
+
.thub-nav-me {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 0.75rem;
|
|
317
|
+
padding-bottom: 0.75rem;
|
|
318
|
+
margin-bottom: 0.75rem;
|
|
319
|
+
border-bottom: 1px solid var(--bs-border-color);
|
|
303
320
|
}
|
|
304
321
|
|
|
305
|
-
.thub-nav-
|
|
306
|
-
|
|
322
|
+
.thub-nav-me .btn {
|
|
323
|
+
min-width: 2.5rem;
|
|
307
324
|
min-height: 2.5rem;
|
|
308
325
|
}
|
|
309
326
|
}
|
package/src/services/registry.js
CHANGED
|
@@ -655,6 +655,17 @@ function createRegistryService(db, { bus, events }){
|
|
|
655
655
|
return get(resourceId);
|
|
656
656
|
}
|
|
657
657
|
|
|
658
|
+
// The Client's hw/sw section as it stands: what it reported at its last
|
|
659
|
+
// start, unless a saved revision hasn't reached it yet — then that one.
|
|
660
|
+
// The base for a revision that only changes something else (groups, an
|
|
661
|
+
// Import without a section) and for Export: the saved one alone could be
|
|
662
|
+
// older than the Client's file, and sending it would undo what changed
|
|
663
|
+
// there since.
|
|
664
|
+
function currentSection(r){
|
|
665
|
+
const pending = r.config_desired && r.config_applied_revision < r.config_revision;
|
|
666
|
+
return pending ? r.config_desired : (r.client_config || r.config_desired);
|
|
667
|
+
}
|
|
668
|
+
|
|
658
669
|
// { revision, type, config[, file] } while the Client hasn't applied the
|
|
659
670
|
// saved one; `file`: an import's other fields, until it's applied.
|
|
660
671
|
function pendingConfig(resourceId){
|
|
@@ -684,7 +695,7 @@ function createRegistryService(db, { bus, events }){
|
|
|
684
695
|
if (!valid){
|
|
685
696
|
throw Object.assign(new Error(`Can't import this config into ${r.name}: ${errors.join('; ')}`), { status: 400 });
|
|
686
697
|
}
|
|
687
|
-
const desired = section || r
|
|
698
|
+
const desired = section || currentSection(r);
|
|
688
699
|
if (!desired){
|
|
689
700
|
throw Object.assign(new Error(`Can't import this config into ${r.name}: it has no "${HW_DEVICES_SECTION}" section, and the Client hasn't reported one`),
|
|
690
701
|
{ status: 400 });
|
|
@@ -714,7 +725,7 @@ function createRegistryService(db, { bus, events }){
|
|
|
714
725
|
}
|
|
715
726
|
// Writing top-level fields needs a Client that reports its config file
|
|
716
727
|
// (the same that Import needs); an older one would drop them.
|
|
717
|
-
const desired = r
|
|
728
|
+
const desired = currentSection(r);
|
|
718
729
|
if (!desired || !r.client_config_file){
|
|
719
730
|
throw Object.assign(new Error(`${r.name} is too old to take its groups from the dashboard — update its Client first`), { status: 409 });
|
|
720
731
|
}
|
|
@@ -740,7 +751,7 @@ function createRegistryService(db, { bus, events }){
|
|
|
740
751
|
}
|
|
741
752
|
// Re-shared: a file stored from an older Client may still say hw / sw.
|
|
742
753
|
const base = shareableClientConfigFile(r.client_config_file || { name: r.reported_name || r.name, type: r.type, labels: r.labels }),
|
|
743
|
-
section = r.type === 'hw' ? r
|
|
754
|
+
section = r.type === 'hw' ? currentSection(r) : null;
|
|
744
755
|
return { ...base, ...(pendingImport(r) || {}), type: r.type, ...(section ? { [HW_DEVICES_SECTION]: section } : {}) };
|
|
745
756
|
}
|
|
746
757
|
|
package/test/groups-tab.test.js
CHANGED
|
@@ -95,3 +95,48 @@ test('Groups tab: keeps other pending changes; refuses unknown groups and Client
|
|
|
95
95
|
const old = register(services, 'lab-old', { configFile: null });
|
|
96
96
|
assert.throws(() => services.registry.setClientGroups(old, [g.id], { knownGroupIds: [g.id] }), /too old to take its groups/);
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
test('device tabs show what the Client runs — not an old saved revision; groups/import/export build on it', async (t) => {
|
|
100
|
+
const { base, services, admin } = await start(t),
|
|
101
|
+
reg = services.registry,
|
|
102
|
+
register = (devpath) => reg.registerAuto({
|
|
103
|
+
clientId: 'hw1', name: 'lab-hw-01', type: 'hw', labels: [],
|
|
104
|
+
config: { stlinks: [{ index: 1, devpath }] }, configFile: { name: 'lab-hw-01', type: 'hw' }
|
|
105
|
+
}).resourceId,
|
|
106
|
+
id = register('1.1'),
|
|
107
|
+
devpaths = async () => {
|
|
108
|
+
const html = await (await fetch(`${base}/resources`, { headers: { cookie: admin } })).text(),
|
|
109
|
+
table = html.slice(html.indexOf(`id="cfg-${id}-stlinks"`), html.indexOf(`id="cfg-${id}-uarts"`));
|
|
110
|
+
return [...table.matchAll(/data-f="devpath" value="([^"]*)"/g)].map((m) => m[1]).filter(Boolean); // not the blank add-row template
|
|
111
|
+
};
|
|
112
|
+
assert.deepEqual(await devpaths(), ['1.1']); // as the Client reported it
|
|
113
|
+
|
|
114
|
+
// Saved on the dashboard, not applied yet: the tab shows the saved one.
|
|
115
|
+
reg.setClientConfig(id, { stlinks: [{ index: 1, devpath: '2.2' }] }, { by: 'admin' });
|
|
116
|
+
assert.deepEqual(await devpaths(), ['2.2']);
|
|
117
|
+
|
|
118
|
+
// Applied — and later the file was edited on the host; the Client restarts
|
|
119
|
+
// and reports 3.3. That's what the tab shows now, not the old 2.2.
|
|
120
|
+
services.db.prepare('UPDATE resources SET config_applied_revision = config_revision WHERE id = ?').run(id);
|
|
121
|
+
register('3.3');
|
|
122
|
+
assert.deepEqual(await devpaths(), ['3.3']);
|
|
123
|
+
|
|
124
|
+
// …and what a groups save, an export and an Import without a section send.
|
|
125
|
+
const g = services.groups.create({ name: 'g' });
|
|
126
|
+
reg.setClientGroups(id, [g.id], { by: 'admin', knownGroupIds: [g.id] });
|
|
127
|
+
assert.deepEqual(reg.pendingConfig(id).config, { stlinks: [{ index: 1, devpath: '3.3' }] }); // not 2.2 again
|
|
128
|
+
assert.deepEqual(reg.exportClientConfig(id)['hw-devices'], { stlinks: [{ index: 1, devpath: '3.3' }] });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('ST-Link tab has no Serial field; a serial set in the config file is kept on Save', async (t) => {
|
|
132
|
+
const { base, services, admin } = await start(t),
|
|
133
|
+
id = services.registry.registerAuto({
|
|
134
|
+
clientId: 'hw2', name: 'lab-hw-02', type: 'hw', labels: [],
|
|
135
|
+
config: { stlinks: [{ index: 1, serial: '066DFF48', devpath: '1.2' }] }, configFile: { name: 'lab-hw-02', type: 'hw' }
|
|
136
|
+
}).resourceId,
|
|
137
|
+
html = await (await fetch(`${base}/resources`, { headers: { cookie: admin } })).text(),
|
|
138
|
+
table = html.slice(html.indexOf(`id="cfg-${id}-stlinks"`), html.indexOf(`id="cfg-${id}-uarts"`));
|
|
139
|
+
assert.doesNotMatch(table, /data-f="serial"|>Serial</);
|
|
140
|
+
// Rides along in data-extra, which public/js/client-config.js spreads back into the row.
|
|
141
|
+
assert.match(table, /data-extra="\{"serial":"066DFF48"\}"/);
|
|
142
|
+
});
|
package/views/layout.pug
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
//- The signed-in user's avatar, or their initials in a circle.
|
|
2
|
+
mixin navAvatar(px)
|
|
3
|
+
if user.avatarPath
|
|
4
|
+
img.rounded-circle.me-2(src=user.avatarPath width=px height=px style="object-fit: cover;")
|
|
5
|
+
else
|
|
6
|
+
span.rounded-circle.bg-secondary.text-white.d-inline-flex.align-items-center.justify-content-center.me-2.flex-shrink-0(style=`width: ${px}px; height: ${px}px; font-size: ${Math.round(px / 2)}px;`)
|
|
7
|
+
= ((user.firstName ? user.firstName[0] : user.username[0]) + (user.lastName ? user.lastName[0] : '')).toUpperCase()
|
|
8
|
+
|
|
1
9
|
doctype html
|
|
2
10
|
html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
3
11
|
head
|
|
@@ -30,6 +38,18 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
30
38
|
button.navbar-toggler(type="button" data-bs-toggle="collapse" data-bs-target="#nav")
|
|
31
39
|
span.navbar-toggler-icon
|
|
32
40
|
#nav.collapse.navbar-collapse.thub-nav
|
|
41
|
+
//- Phones/tablets: who you are comes first — name, role, profile,
|
|
42
|
+
//- sign-out (thub.css .thub-nav-me). Desktop keeps it on the right.
|
|
43
|
+
if user
|
|
44
|
+
.thub-nav-me.d-lg-none
|
|
45
|
+
a.d-flex.align-items-center.text-reset.text-decoration-none.flex-grow-1(href="/profile")
|
|
46
|
+
+navAvatar(36)
|
|
47
|
+
span.d-flex.flex-column.lh-sm
|
|
48
|
+
span.fw-medium= user.firstName ? `${user.firstName} ${user.lastName || ''}`.trim() : user.username
|
|
49
|
+
span.small.text-body-secondary= `${roleLabel(user.role)} · profile`
|
|
50
|
+
form(action="/logout" method="post")
|
|
51
|
+
button.btn.btn-outline-danger(type="submit" aria-label="Log out")
|
|
52
|
+
i.bi.bi-box-arrow-right
|
|
33
53
|
//- Below lg the menu drops down over the page (thub.css .thub-nav,
|
|
34
54
|
//- closed by public/js/actions.js): pages as a two-column grid with
|
|
35
55
|
//- icons, then the tools in a row, then the account row.
|
|
@@ -112,22 +132,16 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
|
|
|
112
132
|
)
|
|
113
133
|
i.bi.bi-circle-half
|
|
114
134
|
if user
|
|
115
|
-
li.
|
|
116
|
-
li.nav-item.d-flex.align-items-center.me-3.thub-nav-account
|
|
135
|
+
li.nav-item.d-none.d-lg-flex.align-items-center.me-3
|
|
117
136
|
a.nav-link.d-flex.align-items-center.p-0(
|
|
118
137
|
href="/profile"
|
|
119
138
|
data-bs-toggle="tooltip"
|
|
120
139
|
data-bs-placement="bottom"
|
|
121
140
|
data-bs-title=`Profile settings (${roleLabel(user.role)})`
|
|
122
141
|
)
|
|
123
|
-
|
|
124
|
-
img.rounded-circle.me-2(src=user.avatarPath width="24" height="24" style="object-fit: cover;")
|
|
125
|
-
else
|
|
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;")
|
|
127
|
-
= ((user.firstName ? user.firstName[0] : user.username[0]) + (user.lastName ? user.lastName[0] : '')).toUpperCase()
|
|
142
|
+
+navAvatar(24)
|
|
128
143
|
span= user.firstName ? `${user.firstName} ${user.lastName || ''}`.trim() : user.username
|
|
129
|
-
|
|
130
|
-
li.nav-item.d-flex.align-items-center
|
|
144
|
+
li.nav-item.d-none.d-lg-flex.align-items-center
|
|
131
145
|
form(action="/logout" method="post")
|
|
132
146
|
button.btn.btn-sm.btn-outline-danger(
|
|
133
147
|
type="submit"
|
|
@@ -5,14 +5,13 @@
|
|
|
5
5
|
//- what the Client reported at registration.
|
|
6
6
|
|
|
7
7
|
//- One device row: "Device" is a udev index (1-8) or a /dev path; fields the
|
|
8
|
-
//- form doesn't show (vendorId/productId/subsystem
|
|
8
|
+
//- form doesn't show (vendorId/productId/subsystem, an ST-Link serial set in
|
|
9
|
+
//- the config file) ride along in data-extra.
|
|
9
10
|
mixin deviceRow(kind, d)
|
|
10
|
-
- const known = ['index', 'path', '
|
|
11
|
+
- const known = ['index', 'path', 'baudRate', 'devpath']
|
|
11
12
|
- const extra = Object.fromEntries(Object.entries(d || {}).filter(([k]) => !known.includes(k)))
|
|
12
13
|
tr(data-row=kind data-extra=JSON.stringify(extra))
|
|
13
14
|
td: input.form-control.form-control-sm.font-monospace(data-f="device" value=d ? (d.path || d.index || '') : '' placeholder="1 or /dev/…" required)
|
|
14
|
-
if kind === 'stlinks'
|
|
15
|
-
td: input.form-control.form-control-sm.font-monospace(data-f="serial" value=d && d.serial || '' placeholder="(from udev)")
|
|
16
15
|
if kind === 'uarts'
|
|
17
16
|
td: input.form-control.form-control-sm(data-f="baudRate" type="number" min="50" value=d && d.baudRate || '' placeholder="115200")
|
|
18
17
|
td: input.form-control.form-control-sm.font-monospace(data-f="devpath" value=d && d.devpath || '' placeholder="3.3.4.1")
|
|
@@ -162,7 +161,13 @@ mixin groupsPane(r, returnTo, groupsById)
|
|
|
162
161
|
|
|
163
162
|
mixin configPanes(r, returnTo, groupsById)
|
|
164
163
|
- const p = `cfg-${r.id}`
|
|
165
|
-
|
|
164
|
+
//- What the Client runs — its config as it reported it at its last start
|
|
165
|
+
//- — unless a saved revision is still on its way to it: then that one, so
|
|
166
|
+
//- it can be checked or changed before it's applied. (Always preferring the
|
|
167
|
+
//- saved revision showed an old one forever, after the file changed on the
|
|
168
|
+
//- host or an Import.)
|
|
169
|
+
- const pendingRevision = r.config_desired && r.config_applied_revision < r.config_revision
|
|
170
|
+
- const current = pendingRevision ? r.config_desired : (r.client_config || r.config_desired)
|
|
166
171
|
- const canEdit = can.operate
|
|
167
172
|
if r.type !== 'hw'
|
|
168
173
|
//- no device tabs
|
|
@@ -185,8 +190,8 @@ mixin configPanes(r, returnTo, groupsById)
|
|
|
185
190
|
fieldset(disabled=!canEdit)
|
|
186
191
|
+configPane(p, 'stlinks')
|
|
187
192
|
+capabilitiesStatus(r)
|
|
188
|
-
+deviceTable('stlinks', 'ST-Link probes', current.stlinks || [], ['Device', '
|
|
189
|
-
.form-text Device: a udev index 1–8 (→ /dev/thub/dut<N>-stlink) or a /dev path.
|
|
193
|
+
+deviceTable('stlinks', 'ST-Link probes', current.stlinks || [], ['Device', 'udev devpath'])
|
|
194
|
+
.form-text Device: a udev index 1–8 (→ /dev/thub/dut<N>-stlink) or a /dev path. Which probe a job uses is up to its #[code --command] (#[code $THUB_DUT_STLINK_<n>]).
|
|
190
195
|
+saveCapabilities(canEdit)
|
|
191
196
|
+configPane(p, 'uarts')
|
|
192
197
|
+capabilitiesStatus(r)
|