/*
* 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 => `
`).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);
});
})();