// --- Tab navigation --- document.querySelectorAll('.tab').forEach(function(btn) { btn.addEventListener('click', function() { document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); }); document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active'); }); btn.classList.add('active'); document.getElementById('tab-' + btn.dataset.tab).classList.add('active'); if (btn.dataset.tab === 'wifi') loadWifiConfig(); if (btn.dataset.tab === 'mqtt') loadMqttConfig(); if (btn.dataset.tab === 'sleep') loadSleepConfig(); if (btn.dataset.tab === 'gsm') loadGsmTab(); if (btn.dataset.tab === 'device') loadDeviceInfo(); }); }); // --- Toast --- function toast(msg, ok) { var el = document.getElementById('toast'); el.textContent = msg; el.className = 'toast ' + (ok ? 'ok' : 'err'); setTimeout(function() { el.classList.add('hidden'); }, 3000); } // --- API helpers --- function api(url, opts) { return fetch(url, opts).then(function(r) { return r.json(); }); } function postJson(url, data) { return api(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); } // --- Formatting --- function fmtUptime(sec) { var d = Math.floor(sec / 86400); var h = Math.floor((sec % 86400) / 3600); var m = Math.floor((sec % 3600) / 60); var s = sec % 60; var parts = []; if (d > 0) parts.push(d + 'd'); if (h > 0) parts.push(h + 'h'); if (m > 0) parts.push(m + 'm'); parts.push(s + 's'); return parts.join(' '); } function fmtBytes(b) { if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MB'; if (b >= 1024) return (b / 1024).toFixed(0) + ' KB'; return b + ' B'; } function setDot(id, on) { var el = document.getElementById(id); if (el) { el.className = 'dot ' + (on ? 'on' : 'off'); } } // --- Status Tab --- function loadStatus() { api('/api/status').then(function(d) { document.getElementById('s-devid').textContent = d.device_id; document.getElementById('s-fw').textContent = 'v' + d.firmware_version; document.getElementById('s-uptime').textContent = fmtUptime(d.uptime_sec); setDot('hdr-status', d.wifi_connected || d.gsm_connected); setDot('s-wifi-dot', d.wifi_connected); setDot('s-mqtt-dot', d.mqtt_connected); setDot('s-gsm-dot', d.gsm_connected); setDot('s-mb-dot', d.modbus_connected); document.getElementById('s-wifi').textContent = d.wifi_connected ? 'WiFi: ' + d.wifi_ssid + ' (' + d.wifi_rssi + ' dBm)' : 'WiFi: Disconnected'; document.getElementById('s-mqtt').textContent = d.mqtt_connected ? 'MQTT: Connected' : 'MQTT: Disconnected'; document.getElementById('s-gsm').textContent = d.gsm_connected ? 'GSM: Connected (' + d.gsm_signal + '%)' : (d.gsm_available ? 'GSM: Disconnected' : 'GSM: Not available'); document.getElementById('s-modbus').textContent = 'Modbus: ' + d.modbus_success_count + ' ok / ' + d.modbus_error_count + ' err'; setDot('s-sd-dot', d.sd_available); document.getElementById('s-sd').textContent = d.sd_available ? 'SD: Available' : 'SD: Not available'; document.getElementById('s-sd-queued').textContent = d.sd_queued_records + ' records'; }); if (document.getElementById('s-sd-files')) { api('/api/sd/status').then(function(d) { if (d.available) { document.getElementById('s-sd').textContent = 'SD: ' + fmtBytes(d.total_bytes) + ' (' + (d.total_bytes > 0 ? Math.round(d.used_bytes * 100 / d.total_bytes) : 0) + '% used)'; document.getElementById('s-sd-files').textContent = d.file_count; document.getElementById('s-sd-retention').textContent = d.retention_days + ' days'; } else { document.getElementById('s-sd-files').textContent = '--'; document.getElementById('s-sd-retention').textContent = '--'; } }); } api('/api/telemetry/latest').then(function(d) { var el = document.getElementById('s-readings'); if (!d.valid) { el.innerHTML = '

No data yet

'; return; } var v = d.v, i = d.i, p = d.p; el.innerHTML = '
' + rd('Va', v.a, 'V') + rd('Vb', v.b, 'V') + rd('Vc', v.c, 'V') + rd('Ia', i.a, 'A') + rd('Ib', i.b, 'A') + rd('Ic', i.c, 'A') + rd('P', p.total, 'kW') + rd('Q', p.reactive, 'kVAR') + rd('S', p.apparent, 'kVA') + rd('PF', p.pf, '') + rd('F', d.f, 'Hz') + rd('Import', d.e.imp_act, 'kWh') + '
'; }); } function rd(label, value, unit) { return '
' + label + '
' + value + ' ' + unit + '
'; } // --- WiFi Tab --- function loadWifiConfig() { api('/api/wifi/config').then(function(d) { document.getElementById('w-enabled').checked = d.enabled; document.getElementById('w-ssid').value = d.ssid || ''; document.getElementById('w-current').textContent = d.connected ? d.ssid + ' (' + d.ip + ')' : 'Not connected'; }); } function scanWifi() { var el = document.getElementById('w-networks'); el.innerHTML = '

Scanning...

'; doScan(); } function doScan() { api('/api/wifi/scan').then(function(d) { if (d.scanning) { setTimeout(doScan, 1500); return; } var el = document.getElementById('w-networks'); if (!d.networks || d.networks.length === 0) { el.innerHTML = '

No networks found

'; return; } var html = ''; d.networks.forEach(function(n) { html += '
' + '' + n.ssid + '' + '' + n.rssi + ' dBm · ' + n.encryption + '
'; }); el.innerHTML = html; }); } function selectNetwork(ssid) { document.getElementById('w-ssid').value = ssid; document.getElementById('w-pass').focus(); } function saveWifi() { postJson('/api/wifi/config', { ssid: document.getElementById('w-ssid').value, password: document.getElementById('w-pass').value, enabled: document.getElementById('w-enabled').checked }).then(function(d) { toast(d.message, d.success); }); } // --- MQTT Tab --- function loadMqttConfig() { api('/api/mqtt/config').then(function(d) { document.getElementById('m-broker').value = d.broker || ''; document.getElementById('m-port').value = d.port; document.getElementById('m-user').value = d.username || ''; document.getElementById('m-prefix').value = d.topic_prefix || 'acuvim'; document.getElementById('m-tls').checked = d.tls; document.getElementById('m-status').textContent = d.connected ? 'Connected' : 'Disconnected'; }); api('/api/console/config').then(function(d) { document.getElementById('c-url').value = d.url || ''; }); } function saveMqtt() { postJson('/api/mqtt/config', { broker: document.getElementById('m-broker').value, port: parseInt(document.getElementById('m-port').value) || 1883, username: document.getElementById('m-user').value, password: document.getElementById('m-pass').value, topic_prefix: document.getElementById('m-prefix').value, tls: document.getElementById('m-tls').checked }).then(function(d) { toast(d.message, d.success); }); } function saveConsole() { postJson('/api/console/config', { url: document.getElementById('c-url').value }).then(function(d) { toast(d.message, d.success); }); } // --- Sleep Tab --- function loadSleepConfig() { api('/api/sleep/config').then(function(d) { document.getElementById('sl-dur').value = d.sleep_duration_min; document.getElementById('sl-wake').value = d.wake_duration_sec; document.getElementById('sl-poll').value = d.poll_interval_sec; document.getElementById('sl-hb').value = d.heartbeat_interval_sec; }); } function saveSleep() { postJson('/api/sleep/config', { sleep_duration_min: parseInt(document.getElementById('sl-dur').value) || 0, wake_duration_sec: parseInt(document.getElementById('sl-wake').value) || 300 }).then(function(d) { toast(d.message, d.success); }); } function savePolling() { postJson('/api/sleep/config', { poll_interval_sec: parseInt(document.getElementById('sl-poll').value) || 5, heartbeat_interval_sec: parseInt(document.getElementById('sl-hb').value) || 60 }).then(function(d) { toast(d.message, d.success); }); } // --- Device Tab --- function loadDeviceInfo() { api('/api/device/info').then(function(d) { document.getElementById('d-id').textContent = d.device_id; document.getElementById('d-fw').textContent = 'v' + d.firmware_version; document.getElementById('d-hw').textContent = d.hardware; document.getElementById('d-mac').textContent = d.mac_address; document.getElementById('d-heap').textContent = fmtBytes(d.free_heap); document.getElementById('d-flash').textContent = fmtBytes(d.flash_size); document.getElementById('d-psram').textContent = fmtBytes(d.psram_size); }); api('/api/modbus/config').then(function(d) { document.getElementById('mb-addr').value = d.slave_address; document.getElementById('mb-baud').value = d.baud_rate; }); loadOtaStatus(); } function saveModbus() { postJson('/api/modbus/config', { slave_address: parseInt(document.getElementById('mb-addr').value) || 1, baud_rate: parseInt(document.getElementById('mb-baud').value) || 9600 }).then(function(d) { toast(d.message, d.success); }); } function saveGsm() { postJson('/api/gsm/config', { enabled: document.getElementById('g-enabled').checked, apn: document.getElementById('g-apn').value, username: document.getElementById('g-user').value, password: document.getElementById('g-pass').value }).then(function(d) { toast(d.message, d.success); }); } // --- GSM Tab --- function loadGsmTab() { api('/api/gsm/status').then(function(d) { document.getElementById('g-modem').textContent = d.modem_available ? 'Detected' : 'Not found'; document.getElementById('g-sim').textContent = d.sim_inserted ? 'Inserted' : 'Not detected'; document.getElementById('g-status').textContent = d.connected ? 'Connected' : 'Disconnected'; setDot('g-status-dot', d.connected); document.getElementById('g-operator').textContent = d.operator || '--'; document.getElementById('g-signal').textContent = d.signal_percent > 0 ? d.signal_percent + '% (CSQ: ' + d.signal_quality + ')' : '--'; document.getElementById('g-imei').textContent = d.imei || '--'; document.getElementById('g-iccid').textContent = d.iccid || '--'; }); api('/api/gsm/config').then(function(d) { document.getElementById('g-enabled').checked = d.enabled; document.getElementById('g-apn').value = d.apn || ''; document.getElementById('g-user').value = d.username || ''; }); api('/api/transport/config').then(function(d) { var radios = document.querySelectorAll('input[name="t-prio"]'); radios.forEach(function(r) { r.checked = (parseInt(r.value) === d.priority); }); document.getElementById('g-poll').value = d.gsm_poll_interval_sec; document.getElementById('g-batch').checked = d.gsm_batch_enabled; document.getElementById('g-batchsize').value = d.gsm_batch_size; }); } function saveTransport() { var prio = 0; document.querySelectorAll('input[name="t-prio"]').forEach(function(r) { if (r.checked) prio = parseInt(r.value); }); postJson('/api/transport/config', { priority: prio, gsm_poll_interval_sec: parseInt(document.getElementById('g-poll').value) || 30, gsm_batch_enabled: document.getElementById('g-batch').checked, gsm_batch_size: parseInt(document.getElementById('g-batchsize').value) || 6 }).then(function(d) { toast(d.message, d.success); }); } // --- OTA --- function loadOtaStatus() { api('/api/ota/status').then(function(d) { document.getElementById('ota-ver').textContent = 'v' + d.firmware_version; document.getElementById('ota-part').textContent = d.running_partition; var statusNames = ['Idle', 'Checking...', 'Downloading...', 'Installing...', 'Success', 'Failed']; var statusText = statusNames[d.status] || 'Unknown'; if (d.progress > 0 && d.status >= 2 && d.status <= 3) { statusText += ' (' + d.progress + '%)'; } if (d.message && d.status === 5) { statusText += ': ' + d.message; } document.getElementById('ota-status').textContent = statusText; var valEl = document.getElementById('ota-validation'); valEl.style.display = d.needs_validation ? 'block' : 'none'; if (d.update_available && d.available) { var infoEl = document.getElementById('ota-update-info'); infoEl.style.display = 'block'; document.getElementById('ota-avail-ver').textContent = 'v' + d.available.version; document.getElementById('ota-avail-size').textContent = fmtBytes(d.available.size); document.getElementById('ota-avail-notes').textContent = d.available.release_notes || ''; } }); api('/api/ota/config').then(function(d) { document.getElementById('ota-auto').value = d.auto_update; document.getElementById('ota-interval').value = d.check_interval_hours; }); } function checkOtaUpdate() { api('/api/ota/check', { method: 'POST' }).then(function(d) { toast(d.message, d.success); if (d.update_available) { var infoEl = document.getElementById('ota-update-info'); infoEl.style.display = 'block'; document.getElementById('ota-avail-ver').textContent = 'v' + d.version; document.getElementById('ota-avail-size').textContent = fmtBytes(d.size); document.getElementById('ota-avail-notes').textContent = d.release_notes || ''; } }); } function installOtaAvailable() { if (!confirm('Install the available firmware update? Device will reboot.')) return; api('/api/ota/status').then(function(d) { if (d.update_available && d.available) { postJson('/api/ota/install', { url: d.available ? d.available.url || '' : '' }).then(function(r) { toast(r.message, r.success); }); } }); } function saveOtaConfig() { postJson('/api/ota/config', { check_interval_hours: parseInt(document.getElementById('ota-interval').value) || 6, auto_update: parseInt(document.getElementById('ota-auto').value) || 1 }).then(function(d) { toast(d.message, d.success); }); } function markOtaValid() { postJson('/api/ota/install', { mark_valid: true }).then(function() { toast('Firmware marked as valid', true); loadOtaStatus(); }); } function rollbackOta() { if (!confirm('Rollback to previous firmware? Device will reboot.')) return; api('/api/ota/rollback', { method: 'POST' }).then(function(d) { toast(d.message, d.success); }); } function uploadFirmware() { var fileEl = document.getElementById('ota-file'); if (!fileEl.files.length) { toast('Select a firmware file', false); return; } if (!confirm('Upload and install firmware? Device will reboot.')) return; var formData = new FormData(); formData.append('firmware', fileEl.files[0]); var progEl = document.getElementById('ota-progress'); var barEl = document.getElementById('ota-progress-bar'); var textEl = document.getElementById('ota-progress-text'); progEl.style.display = 'block'; var xhr = new XMLHttpRequest(); xhr.open('POST', '/api/ota/upload'); xhr.upload.addEventListener('progress', function(e) { if (e.lengthComputable) { var pct = Math.round(e.loaded * 100 / e.total); barEl.style.width = pct + '%'; textEl.textContent = pct + '%'; } }); xhr.onload = function() { if (xhr.status === 200) { toast('Upload complete. Rebooting...', true); } else { toast('Upload failed', false); progEl.style.display = 'none'; } }; xhr.onerror = function() { toast('Upload error', false); progEl.style.display = 'none'; }; xhr.send(formData); } // --- SD Card --- function drainSd() { postJson('/api/sd/drain', {}).then(function(d) { toast(d.message, d.success); }); } function cleanupSd() { if (!confirm('Delete old buffered files?')) return; postJson('/api/sd/cleanup', {}).then(function(d) { toast(d.message, d.success); }); } function restartDevice() { if (!confirm('Restart the device?')) return; postJson('/api/device/restart', {}).then(function(d) { toast(d.message, d.success); }); } function factoryReset() { if (!confirm('Factory reset will erase ALL settings. Continue?')) return; if (!confirm('Are you sure? This cannot be undone.')) return; postJson('/api/device/factory-reset', {}).then(function(d) { toast(d.message, d.success); }); } // --- Auto-refresh status --- loadStatus(); setInterval(loadStatus, 5000);