Close the UI-review follow-ups: names typeahead, fleet form, overflow flip.
Some checks are pending
offline / test (push) Waiting to run

Staff pages typeahead customers by name (Ada, not cust_1). Add-machine is
two rows with a wide identity path and filename picker. Overflow menus
flip up near the viewport edge. Customer list no longer leaks password
hashes. UI-REVIEW.pdf remaining list is the three leftover items.
This commit is contained in:
George Lambert 2026-09-11 18:18:37 -04:00
parent 9cc0018708
commit b68fefdea8
41 changed files with 569 additions and 454 deletions

View file

@ -390,9 +390,10 @@ class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="
<tbody>
<tr>
<td><strong>Fleet</strong></td>
<td>Machines (SSH/local), replica floors, green/yellow/red, RTT
min/avg/p50/p90 spark, pause/restart/off with confirm, ssh-check
toast</td>
<td>Machines (SSH/local), replica floors, green/yellow/red, RTT spark.
Replica and service actions live in one <strong></strong> overflow
menu. Power column shows on/paused/off as status, not toggles.
Add-machine roles are chips.</td>
</tr>
<tr>
<td><strong>Trace</strong></td>

View file

@ -67,7 +67,10 @@
tr.down { background:var(--err-bg); }
.inst { font:12px ui-monospace,Menlo,monospace; margin:.2rem 0; display:flex; gap:.4rem; align-items:center; flex-wrap:wrap; }
form.add { display:flex; flex-wrap:wrap; gap:.4rem; margin:.4rem 0 1rem; align-items:end; }
form.add .roles-field { flex: 1 0 100%; }
form.add .row1, form.add .row2 { display:flex; flex-wrap:wrap; gap:.4rem; align-items:end; width:100%; }
form.add .identity-field { flex: 1 1 22rem; }
form.add .identity-field input[name="identityFile"] { min-width:18rem; width:100%; }
form.add .roles-field { flex: 1 1 100%; }
form.add label { font-size:11px; color:var(--muted); display:flex; flex-direction:column; gap:.15rem; }
form.add input, form.add select, aside input, aside select, aside textarea {
padding:.4rem .5rem; border:1px solid var(--line); border-radius:8px; font:13px ui-monospace,Menlo,monospace;
@ -104,6 +107,7 @@
box-shadow:var(--shadow); min-width:11rem; padding:.25rem; z-index:40;
}
.more.open .more-pop { display:block; }
.more.open.up .more-pop { top:auto; bottom:calc(100% + 4px); }
.more-pop button {
display:block; width:100%; text-align:left; border:0; background:none;
padding:.45rem .6rem; border-radius:8px; font:650 12px system-ui; cursor:pointer; color:var(--ink);
@ -134,13 +138,21 @@
<p id="meta" class="muted"></p>
<h2>Machines</h2>
<form class="add" id="addMachine">
<div class="row1">
<label>id <input name="id" required placeholder="ns1-b"/></label>
<label>host <input name="host" required placeholder="70.88.205.138"/></label>
<label>kind <select name="kind"><option value="local">local</option><option value="ssh" selected>ssh</option><option value="agent">agent</option></select></label>
<label>user <input name="user" value="marchon"/></label>
<label>ssh port <input name="sshPort" type="number" value="22"/></label>
<label>identity file <input name="identityFile" value="~/.ssh/id_ed25519"/></label>
<label>capacity <input name="capacity" type="number" min="1" value="8"/></label>
</div>
<div class="row2">
<label class="identity-field">identity file (path on this host)
<span style="display:flex;gap:.35rem;align-items:center">
<input name="identityFile" value="~/.ssh/id_ed25519"/>
<input type="file" id="identityPick" title="Use filename only; SSH keys stay on disk" style="max-width:11rem"/>
</span>
</label>
<label class="roles-field">roles
<div class="roles" id="roleChips">
<label><input type="checkbox" name="role" value="*"/> *</label>
@ -154,6 +166,7 @@
</div>
</label>
<button class="act primary" type="submit">Add machine</button>
</div>
</form>
<div class="cards" id="machines"></div>
<h2>Services</h2>
@ -290,6 +303,11 @@ document.addEventListener('click', (e) => {
const wrap = btn.closest('.more');
document.querySelectorAll('.more.open').forEach((m) => { if (m !== wrap) m.classList.remove('open'); });
wrap.classList.toggle('open');
if (wrap.classList.contains('open')) {
const pop = wrap.querySelector('.more-pop');
const space = window.innerHeight - wrap.getBoundingClientRect().bottom;
wrap.classList.toggle('up', space < (pop?.offsetHeight || 140) + 16);
}
e.stopPropagation();
return;
}
@ -357,6 +375,11 @@ async function drawFleet() {
</tr>`;
}).join('') + '</tbody></table></div>';
}
$('identityPick')?.addEventListener('change', (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
document.querySelector('input[name="identityFile"]').value = '~/.ssh/' + f.name;
});
$('addMachine').addEventListener('submit', async (e) => {
e.preventDefault();
const f = e.target;