// --- 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);