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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user