v0.7.3: UI cleanup — aligned Hosts pin form, native-chrome list filters, Link row reorder
Design-language cleanup of the v0.7.2 additions (no behaviour change). Hosts: - The Pin MAC form collapses to a single aligned 4-up row: MAC · Label · Architecture · Boot binary. Target drops full-width onto its own line beneath them. The per-field hints that broke the row's alignment moved into the explanatory note below, so every control shares one baseline. Storage: - The image and unattended filter inputs are now wrapped in a label.field, so they inherit the standard text-field chrome (border, radius, height, focus ring) instead of the raw browser <input type=search> look. They span the full card width for continuity with the rest of the page. Network: - The 'Link' row moved below 'Public base URL'. Its joined operstate · speed · duplex · MAC string runs long, so placing it last lets it wrap at the bottom without shoving the other rows around. Validation: clippy clean, fmt clean, 299 workspace tests green, webui syntax-checked. No protocol or boot-path changes in this release. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a71057fce6
commit
ef93357614
Generated
+8
-8
@@ -2836,7 +2836,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe"
|
name = "openpxe"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2858,7 +2858,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-core"
|
name = "openpxe-core"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -2885,7 +2885,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-dhcp-proxy"
|
name = "openpxe-dhcp-proxy"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2902,7 +2902,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-http-api"
|
name = "openpxe-http-api"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2938,7 +2938,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-ipxe-assets"
|
name = "openpxe-ipxe-assets"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"openpxe-core",
|
"openpxe-core",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -2948,7 +2948,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-iso-store"
|
name = "openpxe-iso-store"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
@@ -2977,7 +2977,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-tftp"
|
name = "openpxe-tftp"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2991,7 +2991,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-webui"
|
name = "openpxe-webui"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p256"
|
name = "p256"
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -920,6 +920,8 @@ tr.unbootable td:first-child { border-left: 3px solid var(--warn); }
|
|||||||
the same 16px below its form. */
|
the same 16px below its form. */
|
||||||
.card .body > label.field:has(+ button) { margin-bottom: 0; }
|
.card .body > label.field:has(+ button) { margin-bottom: 0; }
|
||||||
|
|
||||||
/* v0.7.2: inline list filter (Available images / Unattended files). */
|
/* v0.7.2: inline list filter above a table (Available images). The input
|
||||||
.list-search { padding: 14px 16px 0; }
|
is wrapped in a label.field so it borrows the standard text-field chrome
|
||||||
.list-search input { width: 100%; }
|
and matches every other input in the app; this wrapper just insets it
|
||||||
|
from the card edges so it lines up with the header text above. */
|
||||||
|
.list-search { padding: 14px 16px; }
|
||||||
|
|||||||
+35
-38
@@ -439,17 +439,18 @@
|
|||||||
el('div', {class:'v'}, net.server_ip || '?'),
|
el('div', {class:'v'}, net.server_ip || '?'),
|
||||||
el('div', {class:'k'}, 'NIC name'),
|
el('div', {class:'k'}, 'NIC name'),
|
||||||
el('div', {class:'v'}, net.nic_name || '(auto-detect failed)'),
|
el('div', {class:'v'}, net.nic_name || '(auto-detect failed)'),
|
||||||
// v0.7.2: physical link details (operstate · speed · duplex ·
|
|
||||||
// port MAC) so the operator can confirm WHICH port answers
|
|
||||||
// PXE in multi-NIC / trunked environments.
|
|
||||||
el('div', {class:'k'}, 'Link'),
|
|
||||||
el('div', {class:'v'}, net.nic_link || '—'),
|
|
||||||
el('div', {class:'k'}, 'Subnet mask'),
|
el('div', {class:'k'}, 'Subnet mask'),
|
||||||
el('div', {class:'v'}, net.subnet_mask || '?'),
|
el('div', {class:'v'}, net.subnet_mask || '?'),
|
||||||
el('div', {class:'k'}, 'Gateway'),
|
el('div', {class:'k'}, 'Gateway'),
|
||||||
el('div', {class:'v'}, net.gateway || '?'),
|
el('div', {class:'v'}, net.gateway || '?'),
|
||||||
el('div', {class:'k'}, 'Public base URL'),
|
el('div', {class:'k'}, 'Public base URL'),
|
||||||
el('div', {class:'v'}, net.public_base_url),
|
el('div', {class:'v'}, net.public_base_url),
|
||||||
|
// v0.7.2: physical link details (operstate · speed · duplex ·
|
||||||
|
// port MAC) so the operator can confirm WHICH port answers PXE
|
||||||
|
// in multi-NIC / trunked environments. Kept last — the joined
|
||||||
|
// value runs long, so it wraps cleanly at the bottom of the list.
|
||||||
|
el('div', {class:'k'}, 'Link'),
|
||||||
|
el('div', {class:'v'}, net.nic_link || '—'),
|
||||||
]),
|
]),
|
||||||
el('p', {class:'msg'},
|
el('p', {class:'msg'},
|
||||||
'Server IP, NIC, mask, and gateway are auto-detected at startup. ' +
|
'Server IP, NIC, mask, and gateway are auto-detected at startup. ' +
|
||||||
@@ -896,7 +897,7 @@
|
|||||||
// v0.7.2: client-side filter over the image table. Rows travel in
|
// v0.7.2: client-side filter over the image table. Rows travel in
|
||||||
// (row, password-editor) pairs; filtering hides both, and an open
|
// (row, password-editor) pairs; filtering hides both, and an open
|
||||||
// editor stays closed for filtered-out rows.
|
// editor stays closed for filtered-out rows.
|
||||||
const isoSearch = el('input', {type:'search', placeholder:'Filter images… (name, family, category, source)',
|
const isoSearch = el('input', {type:'search', placeholder:'Filter images by name, type, or source',
|
||||||
spellcheck:'false', oninput: () => {
|
spellcheck:'false', oninput: () => {
|
||||||
const q = isoSearch.value.trim().toLowerCase();
|
const q = isoSearch.value.trim().toLowerCase();
|
||||||
for (let k = 0; k + 1 < rowsAndEditors.length; k += 2) {
|
for (let k = 0; k + 1 < rowsAndEditors.length; k += 2) {
|
||||||
@@ -1277,14 +1278,14 @@
|
|||||||
unattDrop, unattFile, unattMsg,
|
unattDrop, unattFile, unattMsg,
|
||||||
// v0.7.2: filter for big answer-file libraries.
|
// v0.7.2: filter for big answer-file libraries.
|
||||||
unattendedFiles.length > 1 ? (() => {
|
unattendedFiles.length > 1 ? (() => {
|
||||||
const search = el('input', {type:'search', placeholder:'Filter files… (name, kind)',
|
const search = el('input', {type:'search', placeholder:'Filter files by name or kind',
|
||||||
spellcheck:'false', style:'margin-top:14px', oninput: () => {
|
spellcheck:'false', oninput: () => {
|
||||||
const q = search.value.trim().toLowerCase();
|
const q = search.value.trim().toLowerCase();
|
||||||
unattRows.forEach(r => {
|
unattRows.forEach(r => {
|
||||||
r.style.display = (!q || (r.dataset.search || '').includes(q)) ? '' : 'none';
|
r.style.display = (!q || (r.dataset.search || '').includes(q)) ? '' : 'none';
|
||||||
});
|
});
|
||||||
}});
|
}});
|
||||||
return search;
|
return el('label', {class:'field', style:'margin-top:14px;margin-bottom:0'}, search);
|
||||||
})() : null,
|
})() : null,
|
||||||
el('div', {style:'margin-top:16px;display:grid;gap:8px'}, unattRows),
|
el('div', {style:'margin-top:16px;display:grid;gap:8px'}, unattRows),
|
||||||
el('p', {class:'msg', style:'margin-top:14px'},
|
el('p', {class:'msg', style:'margin-top:14px'},
|
||||||
@@ -1332,7 +1333,9 @@
|
|||||||
el('h2', {}, 'Available images'),
|
el('h2', {}, 'Available images'),
|
||||||
el('span', {class:'sub'}, isos.length + ' image' + (isos.length === 1 ? '' : 's')),
|
el('span', {class:'sub'}, isos.length + ' image' + (isos.length === 1 ? '' : 's')),
|
||||||
]),
|
]),
|
||||||
isos.length > 1 ? el('div', {class:'list-search'}, isoSearch) : null,
|
isos.length > 1
|
||||||
|
? el('div', {class:'list-search'}, el('label', {class:'field', style:'margin-bottom:0'}, isoSearch))
|
||||||
|
: null,
|
||||||
isoTable,
|
isoTable,
|
||||||
]),
|
]),
|
||||||
]), unattendedAdvanced]);
|
]), unattendedAdvanced]);
|
||||||
@@ -1358,7 +1361,7 @@
|
|||||||
{id: '_tools_menu', title: '↳ Tools menu (built-in)'},
|
{id: '_tools_menu', title: '↳ Tools menu (built-in)'},
|
||||||
];
|
];
|
||||||
|
|
||||||
const macInput = el('input', {type:'text', placeholder:'aa:bb:cc:dd:ee:ff or prefix aa:bb:cc', spellcheck:'false'});
|
const macInput = el('input', {type:'text', placeholder:'aa:bb:cc:dd:ee:ff or aa:bb:cc', spellcheck:'false'});
|
||||||
const labelInput = el('input', {type:'text', placeholder:'optional, e.g. "rack-3 spine"'});
|
const labelInput = el('input', {type:'text', placeholder:'optional, e.g. "rack-3 spine"'});
|
||||||
// v0.7.2: the former separate "Boot rules" card folded into this
|
// v0.7.2: the former separate "Boot rules" card folded into this
|
||||||
// form. A full MAC with no architecture saves a per-host pin
|
// form. A full MAC with no architecture saves a per-host pin
|
||||||
@@ -1476,38 +1479,32 @@
|
|||||||
el('div', {class:'card'}, [
|
el('div', {class:'card'}, [
|
||||||
el('header', {}, el('h2', {}, 'Pin MAC to boot target')),
|
el('header', {}, el('h2', {}, 'Pin MAC to boot target')),
|
||||||
el('div', {class:'body'}, [
|
el('div', {class:'body'}, [
|
||||||
el('div', {class:'form-row cols-3'}, [
|
// v0.7.3: MAC · Label · Architecture · Boot binary share one
|
||||||
el('label', {class:'field'}, [
|
// 4-up row so the controls line up across the page; the
|
||||||
el('span', {class:'name'}, 'MAC address or prefix'),
|
// per-field guidance that used to sit under them moved into the
|
||||||
macInput,
|
// note below to keep the inputs flush. Target spans full width
|
||||||
el('span', {class:'hint'}, 'Full MAC pins one machine; a prefix (OUI) makes a group rule.'),
|
// on its own line beneath them.
|
||||||
]),
|
el('div', {class:'form-row'}, [
|
||||||
|
el('label', {class:'field'}, [el('span', {class:'name'}, 'MAC address or prefix'), macInput]),
|
||||||
el('label', {class:'field'}, [el('span', {class:'name'}, 'Label (optional)'), labelInput]),
|
el('label', {class:'field'}, [el('span', {class:'name'}, 'Label (optional)'), labelInput]),
|
||||||
el('label', {class:'field'}, [
|
el('label', {class:'field'}, [el('span', {class:'name'}, 'Architecture (optional)'), archSel]),
|
||||||
el('span', {class:'name'}, 'Architecture (optional)'),
|
el('label', {class:'field'}, [el('span', {class:'name'}, 'Boot binary (optional)'), binSel]),
|
||||||
archSel,
|
]),
|
||||||
el('span', {class:'hint'}, 'Selecting one makes a group rule for that firmware.'),
|
el('label', {class:'field', style:'margin-top:14px'}, [
|
||||||
]),
|
el('span', {class:'name'}, 'Target'),
|
||||||
el('label', {class:'field', style:'grid-column:1 / -1'}, [
|
targetSel,
|
||||||
el('span', {class:'name'}, 'Target'),
|
|
||||||
targetSel,
|
|
||||||
el('span', {class:'hint'},
|
|
||||||
'Built-in shortcuts skip the menu entirely. Per-ISO entries chain straight to the boot script.'),
|
|
||||||
]),
|
|
||||||
el('label', {class:'field'}, [
|
|
||||||
el('span', {class:'name'}, 'Boot binary (optional)'),
|
|
||||||
binSel,
|
|
||||||
el('span', {class:'hint'}, 'Pin Secure Boot racks to "shim" — zero failed boot cycles.'),
|
|
||||||
]),
|
|
||||||
]),
|
]),
|
||||||
el('div', {style:'margin-top:16px'}, profileFields.wrap),
|
el('div', {style:'margin-top:16px'}, profileFields.wrap),
|
||||||
upsertBtn, msg,
|
upsertBtn, msg,
|
||||||
el('p', {class:'msg', style:'margin-top:14px'},
|
el('p', {class:'msg', style:'margin-top:14px'},
|
||||||
'When a matching client requests boot.ipxe, OpenPXE short-circuits ' +
|
'A full MAC pins one machine; a MAC prefix (OUI) or an architecture ' +
|
||||||
'past the interactive menu and chains directly. Decision order: ' +
|
'saves a first-match group rule. Pin the boot binary to “shim” for ' +
|
||||||
'exact MAC pin → first matching group rule → menu. ' +
|
'Secure Boot racks — zero failed boot cycles. When a matching client ' +
|
||||||
'If an unattended file is selected on a pin, the matching kernel ' +
|
'requests boot.ipxe, OpenPXE short-circuits past the interactive menu ' +
|
||||||
'argument is injected and the hostname/IP are templated into the answer file.'),
|
'and chains directly; decision order is exact MAC pin → first matching ' +
|
||||||
|
'group rule → menu. If an unattended file is selected on a pin, the ' +
|
||||||
|
'matching kernel argument is injected and the hostname/IP are templated ' +
|
||||||
|
'into the answer file.'),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
el('div', {class:'card'}, [
|
el('div', {class:'card'}, [
|
||||||
|
|||||||
Reference in New Issue
Block a user