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:
Miles Ward
2026-06-09 22:44:19 -04:00
co-authored by Claude Opus 4.8
parent a71057fce6
commit 934cfbab46
4 changed files with 49 additions and 50 deletions
Generated
+8 -8
View File
@@ -2836,7 +2836,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openpxe"
version = "0.7.2"
version = "0.7.3"
dependencies = [
"anyhow",
"axum",
@@ -2858,7 +2858,7 @@ dependencies = [
[[package]]
name = "openpxe-core"
version = "0.7.2"
version = "0.7.3"
dependencies = [
"anyhow",
"base64",
@@ -2885,7 +2885,7 @@ dependencies = [
[[package]]
name = "openpxe-dhcp-proxy"
version = "0.7.2"
version = "0.7.3"
dependencies = [
"anyhow",
"bytes",
@@ -2902,7 +2902,7 @@ dependencies = [
[[package]]
name = "openpxe-http-api"
version = "0.7.2"
version = "0.7.3"
dependencies = [
"anyhow",
"axum",
@@ -2938,7 +2938,7 @@ dependencies = [
[[package]]
name = "openpxe-ipxe-assets"
version = "0.7.2"
version = "0.7.3"
dependencies = [
"openpxe-core",
"rust-embed",
@@ -2948,7 +2948,7 @@ dependencies = [
[[package]]
name = "openpxe-iso-store"
version = "0.7.2"
version = "0.7.3"
dependencies = [
"anyhow",
"bcrypt",
@@ -2977,7 +2977,7 @@ dependencies = [
[[package]]
name = "openpxe-tftp"
version = "0.7.2"
version = "0.7.3"
dependencies = [
"anyhow",
"bytes",
@@ -2991,7 +2991,7 @@ dependencies = [
[[package]]
name = "openpxe-webui"
version = "0.7.2"
version = "0.7.3"
[[package]]
name = "p256"
+1 -1
View File
@@ -12,7 +12,7 @@ members = [
]
[workspace.package]
version = "0.7.2"
version = "0.7.3"
edition = "2021"
rust-version = "1.95"
license = "MIT OR Apache-2.0"
+5 -3
View File
@@ -920,6 +920,8 @@ tr.unbootable td:first-child { border-left: 3px solid var(--warn); }
the same 16px below its form. */
.card .body > label.field:has(+ button) { margin-bottom: 0; }
/* v0.7.2: inline list filter (Available images / Unattended files). */
.list-search { padding: 14px 16px 0; }
.list-search input { width: 100%; }
/* v0.7.2: inline list filter above a table (Available images). The input
is wrapped in a label.field so it borrows the standard text-field chrome
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
View File
@@ -439,17 +439,18 @@
el('div', {class:'v'}, net.server_ip || '?'),
el('div', {class:'k'}, 'NIC name'),
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:'v'}, net.subnet_mask || '?'),
el('div', {class:'k'}, 'Gateway'),
el('div', {class:'v'}, net.gateway || '?'),
el('div', {class:'k'}, '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'},
'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
// (row, password-editor) pairs; filtering hides both, and an open
// 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: () => {
const q = isoSearch.value.trim().toLowerCase();
for (let k = 0; k + 1 < rowsAndEditors.length; k += 2) {
@@ -1277,14 +1278,14 @@
unattDrop, unattFile, unattMsg,
// v0.7.2: filter for big answer-file libraries.
unattendedFiles.length > 1 ? (() => {
const search = el('input', {type:'search', placeholder:'Filter files… (name, kind)',
spellcheck:'false', style:'margin-top:14px', oninput: () => {
const search = el('input', {type:'search', placeholder:'Filter files by name or kind',
spellcheck:'false', oninput: () => {
const q = search.value.trim().toLowerCase();
unattRows.forEach(r => {
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,
el('div', {style:'margin-top:16px;display:grid;gap:8px'}, unattRows),
el('p', {class:'msg', style:'margin-top:14px'},
@@ -1332,7 +1333,9 @@
el('h2', {}, 'Available images'),
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,
]),
]), unattendedAdvanced]);
@@ -1358,7 +1361,7 @@
{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"'});
// v0.7.2: the former separate "Boot rules" card folded into this
// form. A full MAC with no architecture saves a per-host pin
@@ -1476,38 +1479,32 @@
el('div', {class:'card'}, [
el('header', {}, el('h2', {}, 'Pin MAC to boot target')),
el('div', {class:'body'}, [
el('div', {class:'form-row cols-3'}, [
el('label', {class:'field'}, [
el('span', {class:'name'}, 'MAC address or prefix'),
macInput,
el('span', {class:'hint'}, 'Full MAC pins one machine; a prefix (OUI) makes a group rule.'),
]),
// v0.7.3: MAC · Label · Architecture · Boot binary share one
// 4-up row so the controls line up across the page; the
// per-field guidance that used to sit under them moved into the
// note below to keep the inputs flush. Target spans full width
// 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'}, 'Architecture (optional)'),
archSel,
el('span', {class:'hint'}, 'Selecting one makes a group rule for that firmware.'),
]),
el('label', {class:'field', style:'grid-column:1 / -1'}, [
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('label', {class:'field'}, [el('span', {class:'name'}, 'Architecture (optional)'), archSel]),
el('label', {class:'field'}, [el('span', {class:'name'}, 'Boot binary (optional)'), binSel]),
]),
el('label', {class:'field', style:'margin-top:14px'}, [
el('span', {class:'name'}, 'Target'),
targetSel,
]),
el('div', {style:'margin-top:16px'}, profileFields.wrap),
upsertBtn, msg,
el('p', {class:'msg', style:'margin-top:14px'},
'When a matching client requests boot.ipxe, OpenPXE short-circuits ' +
'past the interactive menu and chains directly. Decision order: ' +
'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.'),
'A full MAC pins one machine; a MAC prefix (OUI) or an architecture ' +
'saves a first-match group rule. Pin the boot binary to “shim” for ' +
'Secure Boot racks — zero failed boot cycles. When a matching client ' +
'requests boot.ipxe, OpenPXE short-circuits past the interactive menu ' +
'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'}, [