feat(ui): add device from devices modal

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 16:00:59 +12:00
parent 78dc8ffc77
commit de0547615c
2 changed files with 97 additions and 0 deletions

View File

@@ -76,6 +76,13 @@ function normalizeDeviceMacKey(mac) {
.replace(/[:-]/g, '');
}
function normalizeMacInput(raw) {
return String(raw || '')
.trim()
.toLowerCase()
.replace(/[:-]/g, '');
}
function findPingResponse(responses, deviceId) {
if (!responses || typeof responses !== 'object') return null;
const want = normalizeDeviceMacKey(deviceId);
@@ -430,6 +437,69 @@ async function loadDevicesModal() {
}
}
async function createDeviceFromModal() {
const nameEl = document.getElementById('devices-add-name');
const trEl = document.getElementById('devices-add-transport');
const macEl = document.getElementById('devices-add-mac');
const addrEl = document.getElementById('devices-add-address');
const statusEl = document.getElementById('devices-add-status');
const btn = document.getElementById('devices-add-btn');
const name = (nameEl && nameEl.value.trim()) || '';
const transport = (trEl && trEl.value) || 'espnow';
const mac = normalizeMacInput(macEl && macEl.value);
const address = (addrEl && addrEl.value.trim()) || '';
if (!name) {
if (statusEl) statusEl.textContent = 'Name is required';
return;
}
if (mac.length !== 12) {
if (statusEl) statusEl.textContent = 'MAC must be 12 hex characters';
return;
}
if (transport === 'wifi' && !address) {
if (statusEl) statusEl.textContent = 'Address is required for Wi-Fi devices';
return;
}
if (btn) {
btn.disabled = true;
btn.textContent = 'Adding…';
}
if (statusEl) statusEl.textContent = 'Creating device…';
try {
const payload = {
name,
transport,
type: 'led',
mac,
address: transport === 'wifi' ? address : mac,
};
const res = await fetch('/devices', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
if (statusEl) statusEl.textContent = data.error || 'Create failed';
return;
}
if (statusEl) statusEl.textContent = 'Device added';
if (nameEl) nameEl.value = '';
if (macEl) macEl.value = '';
if (addrEl) addrEl.value = '';
await loadDevicesModal();
} catch (e) {
if (statusEl) statusEl.textContent = e.message || 'Create failed';
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = 'Add device';
}
}
}
function renderDevicesList(devices) {
const container = document.getElementById('devices-list-modal');
if (!container) return;
@@ -750,6 +820,9 @@ document.addEventListener('DOMContentLoaded', () => {
const editForm = document.getElementById('edit-device-form');
const editCloseBtn = document.getElementById('edit-device-close-btn');
const editDeviceModal = document.getElementById('edit-device-modal');
const addTransport = document.getElementById('devices-add-transport');
const addAddress = document.getElementById('devices-add-address');
const addBtn = document.getElementById('devices-add-btn');
if (devicesBtn && devicesModal) {
devicesBtn.addEventListener('click', () => {
@@ -768,6 +841,17 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
if (addTransport && addAddress) {
const syncAddAddress = () => {
addAddress.hidden = addTransport.value !== 'wifi';
};
addTransport.addEventListener('change', syncAddAddress);
syncAddAddress();
}
if (addBtn) {
addBtn.addEventListener('click', () => createDeviceFromModal());
}
const devicesPingBtn = document.getElementById('devices-ping-btn');
if (devicesPingBtn) {
devicesPingBtn.addEventListener('click', () => {

View File

@@ -166,6 +166,19 @@
<div id="devices-modal" class="modal">
<div class="modal-content">
<h2>Devices</h2>
<div class="form-group" style="margin-bottom:0.75rem;">
<div class="profiles-actions" style="gap:0.5rem;flex-wrap:wrap;">
<input type="text" id="devices-add-name" placeholder="Device name" autocomplete="off" style="min-width:10rem;">
<select id="devices-add-transport">
<option value="espnow">ESP-NOW</option>
<option value="wifi">Wi-Fi</option>
</select>
<input type="text" id="devices-add-mac" placeholder="MAC (12 hex)" autocomplete="off" style="min-width:10rem;">
<input type="text" id="devices-add-address" placeholder="Address (IP/host for Wi-Fi)" autocomplete="off" style="min-width:12rem;" hidden>
<button type="button" class="btn btn-primary btn-small" id="devices-add-btn">Add device</button>
</div>
<small id="devices-add-status" class="muted-text" aria-live="polite"></small>
</div>
<div id="devices-list-modal" class="profiles-list"></div>
<div class="modal-actions" style="align-items:center;gap:0.75rem;flex-wrap:wrap;">
<button type="button" class="btn btn-secondary" id="devices-ping-btn" title="ESP-NOW broadcast ping (3 s)">Ping drivers</button>