/* * relay_admin.js - Arduino relay configuration panel in the CONFIGURATION tab * * Uses /api/relay/config GET/POST. * Renders 8 relay config fields (name, mode, pulse duration, visibility). */ (function() { const RELAY_COUNT = 8; let _config = { enabled: false, port: '', baudrate: 9600, relays: [] }; let _ports = []; let _maxPulse = 10.0; async function load() { try { const r = await fetch('/api/relay/config', { credentials: 'include' }); if (!r.ok) return; const data = await r.json(); if (!data.ok) return; _config = data.config || { enabled: false, port: '', baudrate: 9600, relays: [] }; _ports = data.ports || []; _maxPulse = data.max_pulse_s || 10.0; // Populate the UI document.getElementById('relay-enabled').checked = !!_config.enabled; const portSel = document.getElementById('relay-port'); portSel.innerHTML = ``; _ports.forEach(p => { const opt = document.createElement('option'); opt.value = p.device; opt.textContent = `${p.device}${p.description ? ' — ' + p.description : ''}`; if (p.device === _config.port) opt.selected = true; portSel.appendChild(opt); }); // If the saved port isn't in the list, add it if (_config.port && !_ports.find(p => p.device === _config.port)) { const opt = document.createElement('option'); opt.value = _config.port; opt.textContent = _config.port + I18n.t('adm_port_unavailable'); opt.selected = true; portSel.appendChild(opt); } document.getElementById('relay-baudrate').value = String(_config.baudrate || 9600); // Status badge const badge = document.getElementById('relay-status-badge'); if (badge) { if (data.connected) { badge.textContent = I18n.t('adm_relay_connected_badge'); badge.style.color = 'var(--green)'; badge.style.background = 'rgba(var(--green-rgb),0.15)'; } else if (_config.enabled) { badge.textContent = I18n.t('adm_relay_error_badge'); badge.style.color = 'var(--red)'; badge.style.background = 'rgba(var(--red-rgb),0.15)'; } else { badge.textContent = I18n.t('in_stopped_badge'); badge.style.color = 'var(--dim)'; } } renderRelays(); } catch(e) { console.warn('[relay-admin] load error:', e); } } function renderRelays() { const list = document.getElementById('relay-list'); if (!list) return; // Pad the array to 8 elements with default values const relays = []; for (let i = 0; i < RELAY_COUNT; i++) { const existing = (_config.relays || []).find(r => r.id === i); relays.push(existing || { id: i, name: I18n.t('perm_relay_n').replace('{n}', i), mode: 'manual', pulse_s: 1.0, visible: true, }); } list.innerHTML = relays.map(r => `
${r.id}
`).join(''); // Hook up the event: changing the mode disables/enables the pulse_s field list.querySelectorAll('select[data-field="mode"]').forEach(sel => { sel.addEventListener('change', (e) => { const rid = e.target.dataset.relayId; const pulseInput = list.querySelector(`input[data-relay-id="${rid}"][data-field="pulse_s"]`); if (pulseInput) pulseInput.disabled = e.target.value === 'manual'; }); }); } function _escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, m => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } async function save() { const status = document.getElementById('relay-save-status'); if (status) { status.textContent = I18n.t('dx_saving'); status.style.color = 'var(--dim)'; } // Collect the data const list = document.getElementById('relay-list'); const relays = []; for (let i = 0; i < RELAY_COUNT; i++) { const nameEl = list.querySelector(`input[data-relay-id="${i}"][data-field="name"]`); const modeEl = list.querySelector(`select[data-relay-id="${i}"][data-field="mode"]`); const pulseEl = list.querySelector(`input[data-relay-id="${i}"][data-field="pulse_s"]`); const visEl = list.querySelector(`input[data-relay-id="${i}"][data-field="visible"]`); relays.push({ id: i, name: (nameEl?.value || I18n.t('perm_relay_n').replace('{n}', i)).trim(), mode: modeEl?.value || 'manual', pulse_s: parseFloat(pulseEl?.value || '1.0'), visible: !!(visEl?.checked), }); } const payload = { enabled: document.getElementById('relay-enabled').checked, port: document.getElementById('relay-port').value, baudrate: parseInt(document.getElementById('relay-baudrate').value, 10), relays, }; try { const r = await fetch('/api/relay/config', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await r.json(); if (r.ok && data.ok) { if (status) { status.textContent = I18n.t('cfg_saved_capital'); status.style.color = 'var(--green)'; } setTimeout(load, 1500); // refresh connection status } else { if (status) { status.textContent = '✕ ' + (data.error || I18n.t('profile_error_fallback')); status.style.color = 'var(--red)'; } } } catch(e) { if (status) { status.textContent = '✕ ' + e.message; status.style.color = 'var(--red)'; } } } // Hook into the global Admin object window.Admin = window.Admin || {}; window.Admin.loadRelayConfig = load; window.Admin.renderRelays = renderRelays; window.Admin.saveRelayConfig = save; // Auto-load on entering page-config document.addEventListener('DOMContentLoaded', () => { // Fire when the user switches to the configuration tab document.getElementById('tab-config')?.addEventListener('click', () => { setTimeout(load, 300); }); // Fallback: check every 500ms whether the config page is visible and not yet loaded let lastLoad = 0; setInterval(() => { const page = document.getElementById('page-config'); if (page && page.classList.contains('active') && Date.now() - lastLoad > 5000) { // Only load if at least 5s have passed since the last load (don't spam on refresh) const list = document.getElementById('relay-list'); if (list && list.children.length === 0) { lastLoad = Date.now(); load(); } } }, 500); }); })();