feat(devices): Scan Now + Manual Add (IP option, MAC colon-mask) → 2.1.4

'Scan Now' triggers POST /api/devices/scan from the band header. '+ Add by MAC'
renamed '+ Manual Add' with an optional IP field (addBody/addManual accept ip)
and a MAC input that auto-inserts colons as you type. Frontend test 4/4; DB-backed
api/repo tests written (run with the suite — skipped locally to avoid colliding
with a concurrent test run on void_test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-08 23:58:19 +10:00
parent 88ef5786ee
commit 26463b5eb6
9 changed files with 54 additions and 17 deletions

View File

@@ -55,9 +55,15 @@ function discoveredRow(d, onDone) {
nameI, grpS, add, ignore);
}
// Manual "add by MAC" form — for offline devices whose MAC you know.
// Manual add form — for offline devices (MAC required; IP optional). The MAC
// field auto-inserts the colons as you type.
function manualAddForm() {
const macI = el('input', { class: 'dv-edit-name', placeholder: 'aa:bb:cc:dd:ee:ff' });
macI.oninput = () => {
const v = macI.value.replace(/[^0-9a-fA-F]/g, '').slice(0, 12).toLowerCase();
macI.value = v.match(/.{1,2}/g)?.join(':') ?? v;
};
const ipI = el('input', { class: 'dv-edit-name', placeholder: 'IP (optional)' });
const nameI = el('input', { class: 'dv-edit-name', placeholder: 'name (optional)' });
const grpS = el('select', { class: 'dv-edit-grp' }, ...GROUPS.map(g => el('option', { value: g }, g)));
const err = el('span', { class: 'muted', style: { fontSize: '11px' } }, '');
@@ -65,10 +71,12 @@ function manualAddForm() {
add.onclick = async () => {
const mac = macI.value.trim().toLowerCase();
if (!/^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/.test(mac)) { err.textContent = 'MAC must look like aa:bb:cc:dd:ee:ff'; return; }
try { await api.post('/api/devices', { mac, name: nameI.value.trim() || undefined, grp: grpS.value }); load(); }
const ip = ipI.value.trim();
if (ip && !/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) { err.textContent = 'IP must look like 192.168.1.x'; return; }
try { await api.post('/api/devices', { mac, ip: ip || undefined, name: nameI.value.trim() || undefined, grp: grpS.value }); load(); }
catch { err.textContent = 'add failed'; }
};
return el('div', { class: 'dv-addform' }, macI, nameI, grpS, add, err);
return el('div', { class: 'dv-addform' }, macI, ipI, nameI, grpS, add, err);
}
async function load() {
@@ -94,15 +102,21 @@ async function load() {
const addForm = manualAddForm();
addForm.style.display = 'none';
const addToggle = el('button', { class: 'ghost dv-addtoggle' }, '+ Add by MAC');
const addToggle = el('button', { class: 'ghost dv-addtoggle' }, '+ Manual Add');
addToggle.onclick = () => { addForm.style.display = addForm.style.display === 'none' ? 'flex' : 'none'; };
const scanBtn = el('button', { class: 'ghost dv-scanbtn' }, 'Scan Now');
scanBtn.onclick = async () => {
scanBtn.textContent = 'Scanning…'; scanBtn.disabled = true;
try { await api.post('/api/devices/scan'); } catch { /* ignore */ }
load();
};
clear(host);
mount(host,
el('div', { class: 'dv-hd' },
el('div', { class: 'dv-title' }, 'Network · Devices'),
el('span', { class: 'dv-count' }, `${total} known${discovered.length ? ` · ${discovered.length} new` : ''}`),
addToggle),
addToggle, scanBtn),
addForm,
...sections,
discPanel);