From 934cfbab46bc9301cc385418a48ecdddc36c8bb2 Mon Sep 17 00:00:00 2001 From: Miles Ward Date: Tue, 9 Jun 2026 22:44:19 -0400 Subject: [PATCH] =?UTF-8?q?v0.7.3:=20UI=20cleanup=20=E2=80=94=20aligned=20?= =?UTF-8?q?Hosts=20pin=20form,=20native-chrome=20list=20filters,=20Link=20?= =?UTF-8?q?row=20reorder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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) --- Cargo.lock | 16 ++++----- Cargo.toml | 2 +- crates/webui/src/app.css | 8 +++-- crates/webui/src/app.js | 73 +++++++++++++++++++--------------------- 4 files changed, 49 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f8ce95..75932f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index e054cf7..82d8b9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/webui/src/app.css b/crates/webui/src/app.css index 1346cfe..6803ee3 100644 --- a/crates/webui/src/app.css +++ b/crates/webui/src/app.css @@ -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; } diff --git a/crates/webui/src/app.js b/crates/webui/src/app.js index d6bfa17..021e9cb 100644 --- a/crates/webui/src/app.js +++ b/crates/webui/src/app.js @@ -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'}, [