661 lines
27 KiB
JavaScript
661 lines
27 KiB
JavaScript
// Device registry: name, id (storage key), type (led), transport (wifi|espnow), address
|
|
|
|
const HEX_BOX_COUNT = 12;
|
|
|
|
/** Last TCP snapshot from WebSocket (so we can apply after async list render). */
|
|
let lastTcpSnapshotIps = null;
|
|
|
|
/** Match server-side ``normalize_tcp_peer_ip`` for WS events vs registry rows. */
|
|
function normalizeWifiAddressForMatch(addr) {
|
|
let s = String(addr || '').trim();
|
|
if (s.toLowerCase().startsWith('::ffff:')) {
|
|
s = s.slice(7);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
const DEVICES_MODAL_POLL_MS = 1000;
|
|
|
|
let devicesModalLiveTimer = null;
|
|
|
|
function stopDevicesModalLiveRefresh() {
|
|
if (devicesModalLiveTimer != null) {
|
|
clearInterval(devicesModalLiveTimer);
|
|
devicesModalLiveTimer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refetch registry and re-render the list (no loading spinner). Keeps scroll position.
|
|
* Used while the devices modal stays open so new TCP devices, renames, and removals appear live.
|
|
*/
|
|
async function refreshDevicesListQuiet() {
|
|
const modal = document.getElementById('devices-modal');
|
|
if (!modal || !modal.classList.contains('active')) return;
|
|
const container = document.getElementById('devices-list-modal');
|
|
if (!container) return;
|
|
const prevTop = container.scrollTop;
|
|
try {
|
|
const res = await fetch('/devices', { headers: { Accept: 'application/json' } });
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
renderDevicesList(data || {});
|
|
container.scrollTop = prevTop;
|
|
} catch (_) {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
function startDevicesModalLiveRefresh() {
|
|
stopDevicesModalLiveRefresh();
|
|
devicesModalLiveTimer = setInterval(() => {
|
|
refreshDevicesListQuiet();
|
|
}, DEVICES_MODAL_POLL_MS);
|
|
}
|
|
|
|
function updateWifiRowDot(row, connected) {
|
|
const dot = row.querySelector('.device-status-dot');
|
|
if (!dot) return;
|
|
if ((row.dataset.deviceTransport || '') !== 'wifi') return;
|
|
dot.classList.remove('device-status-dot--online', 'device-status-dot--offline', 'device-status-dot--unknown');
|
|
if (connected) {
|
|
dot.classList.add('device-status-dot--online');
|
|
dot.title = 'Connected (Wi-Fi TCP session)';
|
|
} else {
|
|
dot.classList.add('device-status-dot--offline');
|
|
dot.title = 'Not connected (no Wi-Fi TCP session)';
|
|
}
|
|
dot.setAttribute('aria-label', dot.title);
|
|
}
|
|
|
|
function applyTcpSnapshot(ips) {
|
|
const set = new Set(
|
|
(ips || []).map((x) => normalizeWifiAddressForMatch(x)).filter(Boolean),
|
|
);
|
|
const container = document.getElementById('devices-list-modal');
|
|
if (!container) return;
|
|
container.querySelectorAll('.profiles-row[data-device-transport="wifi"]').forEach((row) => {
|
|
const addr = normalizeWifiAddressForMatch(row.dataset.deviceAddress);
|
|
updateWifiRowDot(row, set.has(addr));
|
|
});
|
|
}
|
|
|
|
/** Keep cached snapshot aligned with incremental WS events (connect/disconnect). */
|
|
function mergeTcpSnapshotPresence(ip, connected) {
|
|
const n = normalizeWifiAddressForMatch(ip);
|
|
if (!n) return;
|
|
const prev = lastTcpSnapshotIps;
|
|
const set = new Set(
|
|
(Array.isArray(prev) ? prev : []).map((x) => normalizeWifiAddressForMatch(x)).filter(Boolean),
|
|
);
|
|
if (connected) {
|
|
set.add(n);
|
|
} else {
|
|
set.delete(n);
|
|
}
|
|
lastTcpSnapshotIps = Array.from(set);
|
|
}
|
|
|
|
function makeHexAddressBoxes(container) {
|
|
if (!container || container.querySelector('.hex-addr-box')) return;
|
|
container.innerHTML = '';
|
|
for (let i = 0; i < HEX_BOX_COUNT; i++) {
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.className = 'hex-addr-box';
|
|
input.maxLength = 1;
|
|
input.autocomplete = 'off';
|
|
input.setAttribute('data-index', i);
|
|
input.setAttribute('inputmode', 'numeric');
|
|
input.setAttribute('aria-label', `Hex digit ${i + 1}`);
|
|
input.addEventListener('input', (e) => {
|
|
const v = e.target.value.replace(/[^0-9a-fA-F]/g, '');
|
|
e.target.value = v;
|
|
if (v && e.target.nextElementSibling && e.target.nextElementSibling.classList.contains('hex-addr-box')) {
|
|
e.target.nextElementSibling.focus();
|
|
}
|
|
});
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Backspace' && !e.target.value && e.target.previousElementSibling) {
|
|
e.target.previousElementSibling.focus();
|
|
}
|
|
});
|
|
input.addEventListener('paste', (e) => {
|
|
e.preventDefault();
|
|
const pasted = (e.clipboardData.getData('text') || '').replace(/[^0-9a-fA-F]/g, '').slice(0, HEX_BOX_COUNT);
|
|
const boxes = container.querySelectorAll('.hex-addr-box');
|
|
for (let j = 0; j < pasted.length && j < boxes.length; j++) {
|
|
boxes[j].value = pasted[j];
|
|
}
|
|
if (pasted.length > 0) {
|
|
const nextIdx = Math.min(pasted.length, boxes.length - 1);
|
|
boxes[nextIdx].focus();
|
|
}
|
|
});
|
|
container.appendChild(input);
|
|
}
|
|
}
|
|
|
|
function setAddressToBoxes(container, addrStr) {
|
|
if (!container) return;
|
|
const s = (addrStr || '').replace(/[^0-9a-fA-F]/g, '').slice(0, HEX_BOX_COUNT);
|
|
const boxes = container.querySelectorAll('.hex-addr-box');
|
|
boxes.forEach((b, i) => {
|
|
b.value = s[i] || '';
|
|
});
|
|
}
|
|
|
|
function applyTransportVisibility(transport) {
|
|
const isWifi = transport === 'wifi';
|
|
const esp = document.getElementById('edit-device-address-espnow');
|
|
const wifiWrap = document.getElementById('edit-device-address-wifi-wrap');
|
|
const drvWrap = document.getElementById('edit-device-wifi-driver-wrap');
|
|
if (esp) esp.hidden = isWifi;
|
|
if (wifiWrap) wifiWrap.hidden = !isWifi;
|
|
if (drvWrap) drvWrap.hidden = !isWifi;
|
|
}
|
|
|
|
function getAddressForPayload(transport) {
|
|
if (transport === 'wifi') {
|
|
const el = document.getElementById('edit-device-address-wifi');
|
|
const v = (el && el.value.trim()) || '';
|
|
return v || null;
|
|
}
|
|
const boxEl = document.getElementById('edit-device-address-boxes');
|
|
if (!boxEl) return null;
|
|
const boxes = boxEl.querySelectorAll('.hex-addr-box');
|
|
const hex = Array.from(boxes).map((b) => b.value).join('').toLowerCase();
|
|
return hex || null;
|
|
}
|
|
|
|
function collectDeviceEditPayload() {
|
|
const idInput = document.getElementById('edit-device-id');
|
|
const nameInput = document.getElementById('edit-device-name');
|
|
const typeSel = document.getElementById('edit-device-type');
|
|
const transportSel = document.getElementById('edit-device-transport');
|
|
const devId = idInput && idInput.value;
|
|
const transport = (transportSel && transportSel.value) || 'espnow';
|
|
const address = getAddressForPayload(transport);
|
|
const obr = document.getElementById('edit-device-output-brightness');
|
|
let output_brightness = 255;
|
|
if (obr && obr.value !== '') {
|
|
const n = parseInt(obr.value, 10);
|
|
output_brightness = !Number.isNaN(n) ? Math.max(0, Math.min(255, n)) : 255;
|
|
}
|
|
const payload = {
|
|
name: nameInput ? nameInput.value.trim() : '',
|
|
type: (typeSel && typeSel.value) || 'led',
|
|
transport,
|
|
address,
|
|
output_brightness,
|
|
};
|
|
if (transport === 'wifi') {
|
|
const dn = document.getElementById('edit-device-wifi-driver-name');
|
|
const nl = document.getElementById('edit-device-wifi-num-leds');
|
|
const co = document.getElementById('edit-device-wifi-color-order');
|
|
const ws = document.getElementById('edit-device-wifi-startup-mode');
|
|
if (dn && dn.value.trim()) payload.wifi_driver_display_name = dn.value.trim();
|
|
if (nl && nl.value !== '') {
|
|
const n = parseInt(nl.value, 10);
|
|
if (!Number.isNaN(n) && n >= 1) payload.wifi_driver_num_leds = n;
|
|
}
|
|
if (co && co.value) payload.wifi_color_order = co.value;
|
|
if (ws && ws.value) payload.wifi_startup_mode = ws.value;
|
|
}
|
|
return { devId, payload };
|
|
}
|
|
|
|
function refreshEditDeviceDebug() {
|
|
const ta = document.getElementById('edit-device-debug');
|
|
if (!ta) return;
|
|
try {
|
|
const { devId, payload } = collectDeviceEditPayload();
|
|
const loaded = window.__editDeviceLoadedSnapshot;
|
|
ta.value = JSON.stringify(
|
|
{
|
|
device_id: devId || null,
|
|
loaded_from_server: loaded != null ? loaded : null,
|
|
save_payload_preview: payload,
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
} catch (e) {
|
|
ta.value = String(e);
|
|
}
|
|
}
|
|
|
|
async function loadDevicesModal() {
|
|
const container = document.getElementById('devices-list-modal');
|
|
if (!container) return;
|
|
if (typeof window.getEspnowSocket === 'function') {
|
|
window.getEspnowSocket();
|
|
}
|
|
container.innerHTML = '<span class="muted-text">Loading...</span>';
|
|
try {
|
|
const response = await fetch('/devices', { headers: { Accept: 'application/json' } });
|
|
if (!response.ok) throw new Error('Failed to load devices');
|
|
const devices = await response.json();
|
|
renderDevicesList(devices || {});
|
|
} catch (e) {
|
|
console.error('loadDevicesModal:', e);
|
|
container.innerHTML = '<span class="muted-text">Failed to load devices.</span>';
|
|
}
|
|
}
|
|
|
|
function renderDevicesList(devices) {
|
|
const container = document.getElementById('devices-list-modal');
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
const ids = Object.keys(devices).filter((k) => devices[k] && typeof devices[k] === 'object');
|
|
if (ids.length === 0) {
|
|
const p = document.createElement('p');
|
|
p.className = 'muted-text';
|
|
p.textContent = 'No devices yet. Wi-Fi drivers will appear here when they connect over TCP.';
|
|
container.appendChild(p);
|
|
return;
|
|
}
|
|
ids.forEach((devId) => {
|
|
const dev = devices[devId];
|
|
const t = (dev && dev.type) || 'led';
|
|
const tr = (dev && dev.transport) || 'espnow';
|
|
const addrRaw = (dev && dev.address) != null ? String(dev.address).trim() : '';
|
|
const addrDisplay = addrRaw || '—';
|
|
|
|
const row = document.createElement('div');
|
|
row.className = 'profiles-row';
|
|
row.style.display = 'flex';
|
|
row.style.alignItems = 'center';
|
|
row.style.gap = '0.5rem';
|
|
row.style.flexWrap = 'wrap';
|
|
row.dataset.deviceId = devId;
|
|
row.dataset.deviceTransport = tr;
|
|
row.dataset.deviceAddress = addrRaw;
|
|
|
|
const dot = document.createElement('span');
|
|
dot.className = 'device-status-dot';
|
|
dot.setAttribute('role', 'img');
|
|
const live = dev && Object.prototype.hasOwnProperty.call(dev, 'connected') ? dev.connected : null;
|
|
if (live === true) {
|
|
dot.classList.add('device-status-dot--online');
|
|
dot.title = 'Connected (Wi-Fi TCP session)';
|
|
dot.setAttribute('aria-label', dot.title);
|
|
} else if (live === false) {
|
|
dot.classList.add('device-status-dot--offline');
|
|
dot.title = 'Not connected (no Wi-Fi TCP session)';
|
|
dot.setAttribute('aria-label', dot.title);
|
|
} else {
|
|
dot.classList.add('device-status-dot--unknown');
|
|
dot.title = 'ESP-NOW — TCP status does not apply';
|
|
dot.setAttribute('aria-label', dot.title);
|
|
}
|
|
|
|
const label = document.createElement('span');
|
|
label.textContent = (dev && dev.name) || devId;
|
|
label.style.flex = '1';
|
|
label.style.minWidth = '100px';
|
|
|
|
const macEl = document.createElement('code');
|
|
macEl.className = 'device-row-mac';
|
|
macEl.textContent = devId;
|
|
macEl.title = 'MAC (registry id)';
|
|
|
|
const meta = document.createElement('span');
|
|
meta.className = 'muted-text';
|
|
meta.style.fontSize = '0.85em';
|
|
meta.textContent = `${t} · ${tr} · ${addrDisplay}`;
|
|
|
|
const editBtn = document.createElement('button');
|
|
editBtn.className = 'btn btn-secondary btn-small';
|
|
editBtn.textContent = 'Edit';
|
|
editBtn.addEventListener('click', () => openEditDeviceModal(devId, dev));
|
|
|
|
const identifyBtn = document.createElement('button');
|
|
identifyBtn.className = 'btn btn-primary btn-small';
|
|
identifyBtn.type = 'button';
|
|
identifyBtn.textContent = 'Identify';
|
|
identifyBtn.title = 'Red blink at 10 Hz (~50% brightness) for 2 s, then off (not saved as a preset)';
|
|
identifyBtn.addEventListener('click', async () => {
|
|
try {
|
|
const res = await fetch(`/devices/${encodeURIComponent(devId)}/identify`, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { Accept: 'application/json' },
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
alert(data.error || 'Identify failed');
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Identify failed');
|
|
}
|
|
});
|
|
|
|
const deleteBtn = document.createElement('button');
|
|
deleteBtn.className = 'btn btn-secondary btn-small';
|
|
deleteBtn.textContent = 'Delete';
|
|
deleteBtn.addEventListener('click', async () => {
|
|
if (!confirm(`Delete device "${(dev && dev.name) || devId}"?`)) return;
|
|
try {
|
|
const res = await fetch(`/devices/${encodeURIComponent(devId)}`, { method: 'DELETE' });
|
|
if (res.ok) await loadDevicesModal();
|
|
else {
|
|
const data = await res.json().catch(() => ({}));
|
|
alert(data.error || 'Delete failed');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Delete failed');
|
|
}
|
|
});
|
|
|
|
row.appendChild(dot);
|
|
row.appendChild(label);
|
|
row.appendChild(macEl);
|
|
row.appendChild(meta);
|
|
row.appendChild(editBtn);
|
|
row.appendChild(identifyBtn);
|
|
row.appendChild(deleteBtn);
|
|
container.appendChild(row);
|
|
});
|
|
// Do not re-apply lastTcpSnapshotIps here: it is only updated on WS open and
|
|
// device_tcp events; re-applying after each /devices poll overwrites correct
|
|
// API "connected" with a stale list and leaves Wi-Fi rows stuck online.
|
|
}
|
|
|
|
function openEditDeviceModal(devId, dev) {
|
|
try {
|
|
window.__editDeviceLoadedSnapshot = dev ? JSON.parse(JSON.stringify(dev)) : null;
|
|
} catch (e) {
|
|
window.__editDeviceLoadedSnapshot = dev || null;
|
|
}
|
|
const modal = document.getElementById('edit-device-modal');
|
|
const idInput = document.getElementById('edit-device-id');
|
|
const storageLabel = document.getElementById('edit-device-storage-id');
|
|
const nameInput = document.getElementById('edit-device-name');
|
|
const typeSel = document.getElementById('edit-device-type');
|
|
const transportSel = document.getElementById('edit-device-transport');
|
|
const addressBoxes = document.getElementById('edit-device-address-boxes');
|
|
const wifiInput = document.getElementById('edit-device-address-wifi');
|
|
if (!modal || !idInput) return;
|
|
idInput.value = devId;
|
|
if (storageLabel) storageLabel.textContent = devId;
|
|
if (nameInput) nameInput.value = (dev && dev.name) || '';
|
|
if (typeSel) typeSel.value = (dev && dev.type) || 'led';
|
|
const tr = (dev && dev.transport) || 'espnow';
|
|
if (transportSel) transportSel.value = tr;
|
|
applyTransportVisibility(tr);
|
|
setAddressToBoxes(addressBoxes, tr === 'espnow' ? ((dev && dev.address) || '') : '');
|
|
if (wifiInput) wifiInput.value = tr === 'wifi' ? ((dev && dev.address) || '') : '';
|
|
const wName = document.getElementById('edit-device-wifi-driver-name');
|
|
const wLeds = document.getElementById('edit-device-wifi-num-leds');
|
|
const wCo = document.getElementById('edit-device-wifi-color-order');
|
|
const wStart = document.getElementById('edit-device-wifi-startup-mode');
|
|
if (wName) {
|
|
const savedDisp =
|
|
dev && Object.prototype.hasOwnProperty.call(dev, 'wifi_driver_display_name')
|
|
? dev.wifi_driver_display_name
|
|
: undefined;
|
|
if (savedDisp != null && String(savedDisp).trim() !== '') {
|
|
wName.value = String(savedDisp).trim();
|
|
} else {
|
|
wName.value = dev && dev.name ? String(dev.name) : '';
|
|
}
|
|
}
|
|
if (wLeds) {
|
|
wLeds.value =
|
|
dev && dev.wifi_driver_num_leds != null && dev.wifi_driver_num_leds !== ''
|
|
? String(dev.wifi_driver_num_leds)
|
|
: '';
|
|
}
|
|
if (wCo) {
|
|
const co = (dev && dev.wifi_color_order) || 'rgb';
|
|
wCo.value = ['rgb', 'rbg', 'grb', 'gbr', 'brg', 'bgr'].includes(String(co).toLowerCase())
|
|
? String(co).toLowerCase()
|
|
: 'rgb';
|
|
}
|
|
if (wStart) {
|
|
const sm = (dev && dev.wifi_startup_mode) || 'default';
|
|
wStart.value = ['default', 'last', 'off'].includes(String(sm).toLowerCase())
|
|
? String(sm).toLowerCase()
|
|
: 'default';
|
|
}
|
|
const obr = document.getElementById('edit-device-output-brightness');
|
|
const obv = document.getElementById('edit-device-output-brightness-value');
|
|
if (obr) {
|
|
let bv = 255;
|
|
if (dev && dev.output_brightness != null && dev.output_brightness !== '') {
|
|
const n = parseInt(String(dev.output_brightness), 10);
|
|
if (!Number.isNaN(n)) bv = Math.max(0, Math.min(255, n));
|
|
}
|
|
obr.value = String(bv);
|
|
if (obv) obv.textContent = String(bv);
|
|
}
|
|
refreshEditDeviceDebug();
|
|
modal.classList.add('active');
|
|
}
|
|
|
|
async function updateDevice(devId, name, type, transport, address, wifiDriverFields, outputBrightness) {
|
|
try {
|
|
const payload = {
|
|
name,
|
|
type: type || 'led',
|
|
transport: transport || 'espnow',
|
|
address,
|
|
};
|
|
if (typeof outputBrightness === 'number') {
|
|
payload.output_brightness = Math.max(0, Math.min(255, Math.round(outputBrightness)));
|
|
}
|
|
if (transport === 'wifi' && wifiDriverFields && typeof wifiDriverFields === 'object') {
|
|
if (wifiDriverFields.wifi_driver_display_name != null) {
|
|
payload.wifi_driver_display_name = wifiDriverFields.wifi_driver_display_name;
|
|
}
|
|
if (wifiDriverFields.wifi_driver_num_leds != null) {
|
|
payload.wifi_driver_num_leds = wifiDriverFields.wifi_driver_num_leds;
|
|
}
|
|
if (wifiDriverFields.wifi_color_order != null) {
|
|
payload.wifi_color_order = wifiDriverFields.wifi_color_order;
|
|
}
|
|
if (wifiDriverFields.wifi_startup_mode != null) {
|
|
payload.wifi_startup_mode = wifiDriverFields.wifi_startup_mode;
|
|
}
|
|
}
|
|
const res = await fetch(`/devices/${encodeURIComponent(devId)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.ok) {
|
|
await loadDevicesModal();
|
|
return true;
|
|
}
|
|
alert(data.error || 'Failed to update device');
|
|
return false;
|
|
} catch (e) {
|
|
console.error('updateDevice:', e);
|
|
alert('Failed to update device');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function pushWifiDriverConfig(devId, fields) {
|
|
const push = {};
|
|
if (fields.name != null && String(fields.name).trim()) push.name = String(fields.name).trim();
|
|
if (fields.num_leds != null && fields.num_leds !== '') {
|
|
const n = parseInt(String(fields.num_leds), 10);
|
|
if (!Number.isNaN(n) && n >= 1) push.num_leds = n;
|
|
}
|
|
if (fields.color_order != null && String(fields.color_order).trim()) {
|
|
push.color_order = String(fields.color_order).trim().toLowerCase();
|
|
}
|
|
if (fields.startup_mode != null && String(fields.startup_mode).trim()) {
|
|
const sm = String(fields.startup_mode).trim().toLowerCase();
|
|
if (sm === 'default' || sm === 'last' || sm === 'off') push.startup_mode = sm;
|
|
}
|
|
if (Object.keys(push).length === 0) return { ok: true, skipped: true };
|
|
try {
|
|
const res = await fetch(`/devices/${encodeURIComponent(devId)}/driver-config`, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(push),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
alert(data.error || 'Could not send settings to the driver (is it connected?)');
|
|
return { ok: false };
|
|
}
|
|
return { ok: true };
|
|
} catch (e) {
|
|
console.error('pushWifiDriverConfig:', e);
|
|
alert('Could not send settings to the driver');
|
|
return { ok: false };
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.addEventListener('deviceTcpStatus', (ev) => {
|
|
const { ip, connected } = ev.detail || {};
|
|
if (ip == null || typeof connected !== 'boolean') return;
|
|
mergeTcpSnapshotPresence(ip, connected);
|
|
const norm = normalizeWifiAddressForMatch(ip);
|
|
const container = document.getElementById('devices-list-modal');
|
|
if (!container) return;
|
|
container.querySelectorAll('.profiles-row[data-device-transport="wifi"]').forEach((row) => {
|
|
if (normalizeWifiAddressForMatch(row.dataset.deviceAddress) === norm) {
|
|
updateWifiRowDot(row, connected);
|
|
}
|
|
});
|
|
});
|
|
window.addEventListener('deviceTcpSnapshot', (ev) => {
|
|
const ips = ev.detail && ev.detail.connectedIps;
|
|
lastTcpSnapshotIps = ips;
|
|
applyTcpSnapshot(ips);
|
|
});
|
|
|
|
window.addEventListener('deviceTcpWsOpen', () => {
|
|
refreshDevicesListQuiet();
|
|
});
|
|
|
|
makeHexAddressBoxes(document.getElementById('edit-device-address-boxes'));
|
|
|
|
const devOutBr = document.getElementById('edit-device-output-brightness');
|
|
const devOutBrVal = document.getElementById('edit-device-output-brightness-value');
|
|
if (devOutBr && devOutBrVal) {
|
|
devOutBr.addEventListener('input', () => {
|
|
devOutBrVal.textContent = devOutBr.value;
|
|
});
|
|
}
|
|
|
|
const transportEdit = document.getElementById('edit-device-transport');
|
|
if (transportEdit) {
|
|
transportEdit.addEventListener('change', () => {
|
|
applyTransportVisibility(transportEdit.value);
|
|
refreshEditDeviceDebug();
|
|
});
|
|
}
|
|
|
|
const devicesBtn = document.getElementById('devices-btn');
|
|
const devicesModal = document.getElementById('devices-modal');
|
|
const devicesCloseBtn = document.getElementById('devices-close-btn');
|
|
const editForm = document.getElementById('edit-device-form');
|
|
const editCloseBtn = document.getElementById('edit-device-close-btn');
|
|
const editDeviceModal = document.getElementById('edit-device-modal');
|
|
|
|
if (devicesBtn && devicesModal) {
|
|
devicesBtn.addEventListener('click', () => {
|
|
devicesModal.classList.add('active');
|
|
if (typeof window.getEspnowSocket === 'function') {
|
|
window.getEspnowSocket();
|
|
}
|
|
loadDevicesModal();
|
|
startDevicesModalLiveRefresh();
|
|
});
|
|
}
|
|
if (devicesCloseBtn) {
|
|
devicesCloseBtn.addEventListener('click', () => {
|
|
if (devicesModal) devicesModal.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
const devicesModalEl = document.getElementById('devices-modal');
|
|
if (devicesModalEl) {
|
|
new MutationObserver(() => {
|
|
if (!devicesModalEl.classList.contains('active')) {
|
|
stopDevicesModalLiveRefresh();
|
|
}
|
|
}).observe(devicesModalEl, { attributes: true, attributeFilter: ['class'] });
|
|
}
|
|
|
|
if (editForm) {
|
|
editForm.addEventListener('input', () => refreshEditDeviceDebug());
|
|
editForm.addEventListener('change', () => refreshEditDeviceDebug());
|
|
editForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const { devId, payload } = collectDeviceEditPayload();
|
|
if (!devId) return;
|
|
const transport = payload.transport || 'espnow';
|
|
let wifiDriverFields = null;
|
|
if (transport === 'wifi') {
|
|
wifiDriverFields = {};
|
|
if (payload.wifi_driver_display_name != null) {
|
|
wifiDriverFields.wifi_driver_display_name = payload.wifi_driver_display_name;
|
|
}
|
|
if (payload.wifi_driver_num_leds != null) {
|
|
wifiDriverFields.wifi_driver_num_leds = payload.wifi_driver_num_leds;
|
|
}
|
|
if (payload.wifi_color_order != null) {
|
|
wifiDriverFields.wifi_color_order = payload.wifi_color_order;
|
|
}
|
|
if (payload.wifi_startup_mode != null) {
|
|
wifiDriverFields.wifi_startup_mode = payload.wifi_startup_mode;
|
|
}
|
|
}
|
|
const ok = await updateDevice(
|
|
devId,
|
|
payload.name,
|
|
payload.type,
|
|
transport,
|
|
payload.address,
|
|
wifiDriverFields,
|
|
payload.output_brightness,
|
|
);
|
|
if (!ok) return;
|
|
try {
|
|
const brRes = await fetch(`/devices/${encodeURIComponent(devId)}/brightness`, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
});
|
|
if (!brRes.ok && brRes.status !== 503) {
|
|
const brData = await brRes.json().catch(() => ({}));
|
|
console.warn('brightness push:', brData.error || brRes.status);
|
|
}
|
|
} catch (e) {
|
|
console.warn('brightness push failed', e);
|
|
}
|
|
if (transport === 'wifi' && wifiDriverFields) {
|
|
const dn = document.getElementById('edit-device-wifi-driver-name');
|
|
const nl = document.getElementById('edit-device-wifi-num-leds');
|
|
const co = document.getElementById('edit-device-wifi-color-order');
|
|
const ws = document.getElementById('edit-device-wifi-startup-mode');
|
|
const pushRes = await pushWifiDriverConfig(devId, {
|
|
name: dn ? dn.value : '',
|
|
num_leds: nl ? nl.value : '',
|
|
color_order: co ? co.value : '',
|
|
startup_mode: ws ? ws.value : '',
|
|
});
|
|
if (!pushRes.ok) return;
|
|
}
|
|
editDeviceModal.classList.remove('active');
|
|
});
|
|
}
|
|
if (editCloseBtn) {
|
|
editCloseBtn.addEventListener('click', () => editDeviceModal && editDeviceModal.classList.remove('active'));
|
|
}
|
|
});
|