Fine tune the admin UI.
This commit is contained in:
599
data/www/js/app.js
Normal file
599
data/www/js/app.js
Normal file
@@ -0,0 +1,599 @@
|
||||
let token = '';
|
||||
let roles = [];
|
||||
let users = [];
|
||||
let apis = [];
|
||||
let selectedUser = '';
|
||||
let selectedRole = '';
|
||||
let selectedApi = -1;
|
||||
let currentFilePath = '/';
|
||||
let filesLoaded = false;
|
||||
let busyCount = 0;
|
||||
|
||||
const el = id => document.getElementById(id);
|
||||
|
||||
function setGlobalBusy(on) {
|
||||
busyCount += on ? 1 : -1;
|
||||
if (busyCount < 0) busyCount = 0;
|
||||
el('busyChip').classList.toggle('hidden', busyCount == 0);
|
||||
}
|
||||
|
||||
async function tracked(fn) {
|
||||
setGlobalBusy(true);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
setGlobalBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function apiFetch(url, options) {
|
||||
return await tracked(() => fetch(url, options));
|
||||
}
|
||||
|
||||
async function req(method, url, body) {
|
||||
const options = {method, headers: {'Content-Type': 'application/json'}};
|
||||
if (token) options.headers.Authorization = 'Bearer ' + token;
|
||||
if (body !== undefined && method != 'GET') options.body = JSON.stringify(body);
|
||||
const response = await apiFetch(url, options);
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
window.App = {
|
||||
apiFetch,
|
||||
req,
|
||||
token: () => token
|
||||
};
|
||||
|
||||
function showTab(id) {
|
||||
document.querySelectorAll('.tab[data-tab]').forEach(button => button.classList.toggle('active', button.dataset.tab == id));
|
||||
document.querySelectorAll('.panel').forEach(panel => panel.classList.toggle('active', panel.id == id));
|
||||
if (id == 'network') loadSettings();
|
||||
if (id == 'ops' && !filesLoaded) loadFiles('/');
|
||||
}
|
||||
|
||||
function showAccessTab(id) {
|
||||
document.querySelectorAll('.subtab').forEach(button => button.classList.toggle('active', button.dataset.subtab == id));
|
||||
document.querySelectorAll('.subpanel').forEach(panel => panel.classList.toggle('active', panel.id == id));
|
||||
if (id == 'users') showUserOverview();
|
||||
if (id == 'roles') showRoleOverview();
|
||||
if (id == 'apis') showApiOverview();
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
const text = await req('POST', '/api/login', {username: el('user').value, password: el('pass').value});
|
||||
el('loginOut').textContent = text;
|
||||
const json = JSON.parse(text);
|
||||
if (!json.success) return;
|
||||
token = json.token;
|
||||
el('loginPanel').classList.add('hidden');
|
||||
el('app').classList.remove('hidden');
|
||||
window.dispatchEvent(new CustomEvent('app:login', {detail: {token}}));
|
||||
loadSettings();
|
||||
loadAccess();
|
||||
loadFiles('/');
|
||||
}
|
||||
|
||||
function esc(value) {
|
||||
return String(value).replace(/[&<>"']/g, char => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[char]));
|
||||
}
|
||||
|
||||
function roleNames() {
|
||||
return roles.map(role => role.name);
|
||||
}
|
||||
|
||||
function userHasRole(user, role) {
|
||||
return (user.roles || '').split('|').includes(role);
|
||||
}
|
||||
|
||||
function roleChecks(id, selected) {
|
||||
el(id).innerHTML = roleNames().map(role => '<label><input type=checkbox value="' + esc(role) + '" ' + (selected.includes(role) ? 'checked' : '') + '> ' + esc(role) + '</label>').join('');
|
||||
}
|
||||
|
||||
function selectedRoles() {
|
||||
return [...el('userRoles').querySelectorAll('input:checked')].map(input => input.value);
|
||||
}
|
||||
|
||||
function showUserOverview() {
|
||||
el('usersOverview').classList.remove('hidden');
|
||||
el('userDetail').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showRoleOverview() {
|
||||
el('rolesOverview').classList.remove('hidden');
|
||||
el('roleDetail').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showApiOverview() {
|
||||
el('apisOverview').classList.remove('hidden');
|
||||
el('apiDetail').classList.add('hidden');
|
||||
}
|
||||
|
||||
function renderRoles() {
|
||||
el('rolesOut').innerHTML = roles.map((role, index) => '<button class=item data-role-index="' + index + '"><strong>' + esc(role.name) + '</strong><span class=meta><span class=badge>' + (role.system ? 'System' : 'Custom') + '</span></span></button>').join('');
|
||||
}
|
||||
|
||||
function renderRoleUsage(role) {
|
||||
const roleUsers = users.filter(user => userHasRole(user, role.name)).sort((a, b) => a.username.localeCompare(b.username));
|
||||
const roleApis = apis.filter(api => api.role == role.name).sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
||||
const usersHtml = roleUsers.length
|
||||
? roleUsers.map(user => '<div class="usageItem"><strong>' + esc(user.username) + '</strong><span class="badge">' + (user.active ? 'Active' : 'Inactive') + '</span></div>').join('')
|
||||
: '<div class="muted">No users</div>';
|
||||
const apisHtml = roleApis.length
|
||||
? roleApis.map(api => '<div class="usageItem"><span class="badge">' + esc(api.method) + '</span><code>' + esc(api.path) + '</code></div>').join('')
|
||||
: '<div class="muted">No API endpoints</div>';
|
||||
el('roleUsage').innerHTML = '<div><h3>Users</h3><div class="usageList">' + usersHtml + '</div></div><div><h3>API endpoints</h3><div class="usageList">' + apisHtml + '</div></div>';
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
const json = JSON.parse(await req('GET', '/api/roles'));
|
||||
if (!json.success) return;
|
||||
roles = json.roles;
|
||||
renderRoles();
|
||||
roleOptions();
|
||||
if (selectedUser && !el('userDetail').classList.contains('hidden')) selectUser(selectedUser);
|
||||
}
|
||||
|
||||
function newRoleRecord() {
|
||||
selectedRole = '';
|
||||
el('rolesOverview').classList.add('hidden');
|
||||
el('roleDetail').classList.remove('hidden');
|
||||
el('roleTitle').textContent = 'New role';
|
||||
el('roleName').value = '';
|
||||
el('roleName').readOnly = false;
|
||||
el('roleMeta').innerHTML = '';
|
||||
el('roleUsage').innerHTML = '';
|
||||
el('roleSave').classList.remove('hidden');
|
||||
el('roleDelete').classList.add('hidden');
|
||||
el('roleMsg').textContent = '';
|
||||
}
|
||||
|
||||
function selectRole(index) {
|
||||
const role = roles[index];
|
||||
if (!role) return;
|
||||
selectedRole = role.name;
|
||||
el('rolesOverview').classList.add('hidden');
|
||||
el('roleDetail').classList.remove('hidden');
|
||||
el('roleTitle').textContent = role.name;
|
||||
el('roleName').value = role.name;
|
||||
el('roleName').readOnly = true;
|
||||
el('roleMeta').innerHTML = '<span class=badge>' + (role.system ? 'System role' : 'Custom role') + '</span>';
|
||||
renderRoleUsage(role);
|
||||
el('roleSave').classList.add('hidden');
|
||||
el('roleDelete').classList.toggle('hidden', role.system);
|
||||
el('roleMsg').textContent = '';
|
||||
}
|
||||
|
||||
async function addRole() {
|
||||
const text = await req('POST', '/api/roles', {role: el('roleName').value});
|
||||
el('roleMsg').textContent = text;
|
||||
await loadAccess();
|
||||
showRoleOverview();
|
||||
}
|
||||
|
||||
async function deleteSelectedRole() {
|
||||
if (!selectedRole) return;
|
||||
const text = await req('DELETE', '/api/roles', {role: selectedRole});
|
||||
el('roleMsg').textContent = text;
|
||||
selectedRole = '';
|
||||
await loadAccess();
|
||||
showRoleOverview();
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
el('userList').innerHTML = users.map(user => '<button class=item data-user="' + esc(user.username) + '"><strong>' + esc(user.username) + '</strong><span class=meta><span class=badge>' + (user.active ? 'Active' : 'Inactive') + '</span><span class=badge>' + esc(user.roles || 'No roles') + '</span></span></button>').join('');
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const json = JSON.parse(await req('GET', '/api/users'));
|
||||
if (!json.success) return;
|
||||
users = json.users;
|
||||
renderUsers();
|
||||
if (selectedUser && !el('userDetail').classList.contains('hidden')) selectUser(selectedUser);
|
||||
}
|
||||
|
||||
function newUserRecord() {
|
||||
selectedUser = '';
|
||||
el('usersOverview').classList.add('hidden');
|
||||
el('userDetail').classList.remove('hidden');
|
||||
el('userTitle').textContent = 'New user';
|
||||
el('userName').value = '';
|
||||
el('userName').readOnly = false;
|
||||
el('userPass').value = '';
|
||||
el('userActive').checked = true;
|
||||
roleChecks('userRoles', []);
|
||||
el('usersOut').textContent = '';
|
||||
}
|
||||
|
||||
function selectUser(name) {
|
||||
selectedUser = name;
|
||||
const user = users.find(item => item.username == name);
|
||||
if (!user) return;
|
||||
el('usersOverview').classList.add('hidden');
|
||||
el('userDetail').classList.remove('hidden');
|
||||
el('userTitle').textContent = name;
|
||||
el('userName').value = name;
|
||||
el('userName').readOnly = true;
|
||||
el('userPass').value = '';
|
||||
el('userActive').checked = user.active;
|
||||
roleChecks('userRoles', (user.roles || '').split('|'));
|
||||
el('usersOut').textContent = '';
|
||||
}
|
||||
|
||||
async function saveSelectedUser() {
|
||||
const text = await req('POST', '/api/users', {username: el('userName').value, password: el('userPass').value, active: el('userActive').checked, roles: selectedRoles()});
|
||||
el('usersOut').textContent = text;
|
||||
selectedUser = el('userName').value;
|
||||
await loadUsers();
|
||||
showUserOverview();
|
||||
}
|
||||
|
||||
function roleOptions() {
|
||||
const options = '<option>PUBLIC</option>' + roleNames().map(role => '<option>' + esc(role) + '</option>').join('');
|
||||
el('apiRole').innerHTML = options;
|
||||
}
|
||||
|
||||
function renderApis() {
|
||||
const groups = new Map();
|
||||
apis.forEach((api, index) => {
|
||||
if (!groups.has(api.path)) groups.set(api.path, []);
|
||||
groups.get(api.path).push({...api, index});
|
||||
});
|
||||
const methodOrder = {GET: 1, POST: 2, PUT: 3, PATCH: 4, DELETE: 5};
|
||||
el('apiList').innerHTML = [...groups.entries()]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([path, items]) => {
|
||||
items.sort((a, b) => (methodOrder[a.method] || 99) - (methodOrder[b.method] || 99) || a.method.localeCompare(b.method));
|
||||
const verbs = items.map(api => '<button class="verbBadge" data-api-index="' + api.index + '"><strong>' + esc(api.method) + '</strong><span>' + esc(api.role) + '</span></button>').join('');
|
||||
return '<div class="apiRow"><div class="apiPath" title="' + esc(path) + '">' + esc(path) + '</div><div class="apiVerbs">' + verbs + '</div></div>';
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
async function loadApis() {
|
||||
const json = JSON.parse(await req('GET', '/api/apis'));
|
||||
if (!json.success) return;
|
||||
apis = json.apis;
|
||||
renderApis();
|
||||
if (selectedApi >= 0 && !el('apiDetail').classList.contains('hidden')) selectApi(selectedApi);
|
||||
}
|
||||
|
||||
function selectApi(index) {
|
||||
selectedApi = index;
|
||||
const api = apis[index];
|
||||
if (!api) return;
|
||||
el('apisOverview').classList.add('hidden');
|
||||
el('apiDetail').classList.remove('hidden');
|
||||
el('apiTitle').textContent = api.method + ' ' + api.path;
|
||||
el('apiPath').value = api.path;
|
||||
el('apiMethod').value = api.method;
|
||||
roleOptions();
|
||||
el('apiRole').value = api.role;
|
||||
el('apisOut').textContent = '';
|
||||
el('apiTestOut').textContent = '';
|
||||
}
|
||||
|
||||
async function saveSelectedApi() {
|
||||
if (selectedApi < 0) return;
|
||||
const text = await req('POST', '/api/apis', {path: el('apiPath').value, method: el('apiMethod').value, role: el('apiRole').value});
|
||||
el('apisOut').textContent = text;
|
||||
await loadApis();
|
||||
}
|
||||
|
||||
function clearApiTestBody() {
|
||||
el('apiTestBody').value = '';
|
||||
}
|
||||
|
||||
async function testSelectedApi() {
|
||||
if (selectedApi < 0) return;
|
||||
const api = apis[selectedApi];
|
||||
const body = el('apiTestBody').value.trim();
|
||||
let payload;
|
||||
if (body) {
|
||||
try {
|
||||
payload = JSON.parse(body);
|
||||
} catch (error) {
|
||||
el('apiTestOut').textContent = 'Invalid JSON body';
|
||||
return;
|
||||
}
|
||||
}
|
||||
el('apiTestOut').textContent = await req(api.method, api.path, payload);
|
||||
}
|
||||
|
||||
async function loadAccess() {
|
||||
await loadRoles();
|
||||
await loadUsers();
|
||||
await loadApis();
|
||||
}
|
||||
|
||||
function fileName(path) {
|
||||
const index = String(path).lastIndexOf('/');
|
||||
return index >= 0 ? path.substring(index + 1) : path;
|
||||
}
|
||||
|
||||
function fileIcon(file) {
|
||||
if (file.directory) return '<span class="folderIcon" title="Folder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7h7l2 2h9v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h4"/></svg></span>';
|
||||
return '<button class="iconBtn" data-download="' + esc(file.path) + '" title="Download" aria-label="Download ' + esc(file.path) + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></svg></button>';
|
||||
}
|
||||
|
||||
function fileSize(file) {
|
||||
if (file.directory) return 'Folder';
|
||||
return file.size + ' B';
|
||||
}
|
||||
|
||||
function parentPath(path) {
|
||||
if (!path || path == '/') return '/';
|
||||
const end = path.lastIndexOf('/');
|
||||
return end <= 0 ? '/' : path.substring(0, end);
|
||||
}
|
||||
|
||||
function renderFiles(files) {
|
||||
el('fileList').classList.add('fileList');
|
||||
files.sort((a, b) => Number(b.directory) - Number(a.directory) || a.path.localeCompare(b.path));
|
||||
const rows = currentFilePath == '/' ? [] : [{name: '..', path: parentPath(currentFilePath), directory: true, parent: true}];
|
||||
rows.push(...files);
|
||||
el('fileList').innerHTML = rows.map(file => '<div class="fileRow" ' + (file.directory ? 'data-path="' + esc(file.path) + '"' : '') + '>' + fileIcon(file) + '<div class="filePath" title="' + esc(file.path) + '">' + esc(file.name || file.path) + '</div><div class="fileSize">' + (file.parent ? '' : fileSize(file)) + '</div></div>').join('');
|
||||
}
|
||||
|
||||
async function loadFiles(path = currentFilePath) {
|
||||
const text = await req('GET', '/api/files?path=' + encodeURIComponent(path));
|
||||
el('filesOut').textContent = '';
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (json.success) {
|
||||
currentFilePath = json.path || path || '/';
|
||||
filesLoaded = true;
|
||||
el('filesPath').textContent = currentFilePath;
|
||||
renderFiles(json.files || []);
|
||||
if (!(json.files || []).length) el('filesOut').textContent = 'No files found';
|
||||
} else {
|
||||
el('filesOut').textContent = text;
|
||||
}
|
||||
} catch (error) {
|
||||
el('filesOut').textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(path) {
|
||||
const response = await apiFetch('/api/files/download?path=' + encodeURIComponent(path), {headers: {Authorization: 'Bearer ' + token}});
|
||||
if (!response.ok) {
|
||||
el('filesOut').textContent = await response.text();
|
||||
return;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = fileName(path) || 'download';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
link.remove();
|
||||
}
|
||||
|
||||
async function downloadConfig() {
|
||||
const response = await apiFetch('/api/config/export', {headers: {Authorization: 'Bearer ' + token}});
|
||||
if (!response.ok) {
|
||||
el('maintOut').textContent = await response.text();
|
||||
return;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = 'config-backup.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
link.remove();
|
||||
el('maintOut').textContent = 'Configuration backup downloaded';
|
||||
}
|
||||
|
||||
async function restoreConfig() {
|
||||
if (!el('configFile').files.length) {
|
||||
el('maintOut').textContent = 'Select a configuration JSON file first';
|
||||
return;
|
||||
}
|
||||
if (!confirm('Restore configuration from this file? WiFi settings will not change.')) return;
|
||||
setBusy('restoreConfigBtn', true, 'Restoring');
|
||||
try {
|
||||
await tracked(async () => {
|
||||
const body = await el('configFile').files[0].text();
|
||||
const response = await fetch('/api/config/import', {method: 'POST', headers: {'Content-Type': 'application/json', Authorization: 'Bearer ' + token}, body});
|
||||
el('maintOut').textContent = await response.text();
|
||||
if (response.ok) {
|
||||
loadSettings();
|
||||
loadAccess();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
el('maintOut').textContent = 'Restore failed: ' + error.message;
|
||||
} finally {
|
||||
setBusy('restoreConfigBtn', false, 'Restore config');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCert() {
|
||||
const text = await req('GET', '/api/cert');
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
el('certOut').textContent = json.success ? (json.certificate || 'No certificate stored') : text;
|
||||
} catch (error) {
|
||||
el('certOut').textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCertFile() {
|
||||
if (!el('certFile').files.length) {
|
||||
el('certOut').textContent = 'Select a PEM/CER/CRT file first';
|
||||
return;
|
||||
}
|
||||
setBusy('certSaveBtn', true, 'Saving');
|
||||
try {
|
||||
await tracked(async () => {
|
||||
const certificate = await el('certFile').files[0].text();
|
||||
el('certOut').textContent = await req('POST', '/api/cert', {certificate});
|
||||
});
|
||||
} catch (error) {
|
||||
el('certOut').textContent = 'Certificate save failed: ' + error.message;
|
||||
} finally {
|
||||
setBusy('certSaveBtn', false, 'Save certificate');
|
||||
}
|
||||
}
|
||||
|
||||
async function factoryResetDevice() {
|
||||
if (!confirm('Factory reset this device and restart?')) return;
|
||||
el('maintOut').textContent = await req('POST', '/api/factory-reset');
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const text = await req('GET', '/api/logs');
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
el('logsOut').textContent = json.success ? (json.logs || '') : text;
|
||||
} catch (error) {
|
||||
el('logsOut').textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearLogs() {
|
||||
const text = await req('POST', '/api/logs/clear');
|
||||
el('logsOut').textContent = text;
|
||||
try {
|
||||
if (JSON.parse(text).success) el('logsOut').textContent = 'Logs cleared';
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
function syncNetworkMode() {
|
||||
const staticMode = el('netDhcp').value == 'false';
|
||||
['netIp', 'netGateway', 'netSubnet', 'netDns1', 'netDns2'].forEach(id => el(id).disabled = !staticMode);
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const text = await req('GET', '/api/settings');
|
||||
el('settingsOut').textContent = text;
|
||||
const json = JSON.parse(text);
|
||||
if (!json.success) return;
|
||||
const settings = json.settings;
|
||||
const projectName = settings.projectName || 'ESP32-C3';
|
||||
el('appTitle').textContent = projectName + ' Admin';
|
||||
document.title = projectName + ' Admin';
|
||||
el('logLevel').value = settings.logLevel;
|
||||
el('logMax').value = settings.maxLogBytes;
|
||||
el('upd').value = settings.updateUrl;
|
||||
el('netHost').value = settings.hostname || '';
|
||||
el('netHost').placeholder = settings.defaultHostname || 'Default host name';
|
||||
el('netDhcp').value = String(settings.dhcp !== false);
|
||||
el('netIp').value = settings.ip || '';
|
||||
el('netGateway').value = settings.gateway || '';
|
||||
el('netSubnet').value = settings.subnet || '';
|
||||
el('netDns1').value = settings.dns1 || '';
|
||||
el('netDns2').value = settings.dns2 || '';
|
||||
el('netCurrent').textContent = 'Current IP: ' + (settings.currentIp || 'not connected');
|
||||
syncNetworkMode();
|
||||
window.dispatchEvent(new CustomEvent('app:settings-loaded', {detail: {settings}}));
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
el('settingsOut').textContent = await req('POST', '/api/settings', {logLevel: +el('logLevel').value, maxLogBytes: +el('logMax').value, updateUrl: el('upd').value});
|
||||
}
|
||||
|
||||
async function saveNetwork() {
|
||||
setBusy('networkSaveBtn', true, 'Saving');
|
||||
try {
|
||||
const body = {hostname: el('netHost').value, dhcp: el('netDhcp').value == 'true', ip: el('netIp').value, gateway: el('netGateway').value, subnet: el('netSubnet').value, dns1: el('netDns1').value, dns2: el('netDns2').value};
|
||||
const text = await req('POST', '/api/settings', body);
|
||||
el('networkOut').textContent = text + '\nNetwork changes apply on the next WiFi reconnect or reboot.';
|
||||
if (JSON.parse(text).success) await loadSettings();
|
||||
} catch (error) {
|
||||
el('networkOut').textContent = 'Network save failed: ' + error.message;
|
||||
} finally {
|
||||
setBusy('networkSaveBtn', false, 'Save network');
|
||||
}
|
||||
}
|
||||
|
||||
function setBusy(id, busy, label) {
|
||||
const button = el(id);
|
||||
if (!button) return;
|
||||
button.disabled = busy;
|
||||
button.innerHTML = busy ? '<span class=spinner></span>' + label : label;
|
||||
}
|
||||
|
||||
async function uploadPkg() {
|
||||
if (!el('pkg').files.length) {
|
||||
el('fwOut').textContent = 'Select an update package first';
|
||||
return;
|
||||
}
|
||||
setBusy('uploadPkgBtn', true, 'Uploading');
|
||||
el('fwOut').textContent = 'Uploading update package...';
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('package', el('pkg').files[0]);
|
||||
const response = await apiFetch('/api/update', {method: 'POST', headers: {Authorization: 'Bearer ' + token}, body: formData});
|
||||
el('fwOut').textContent = await response.text();
|
||||
} catch (error) {
|
||||
el('fwOut').textContent = 'Upload failed: ' + error.message;
|
||||
} finally {
|
||||
setBusy('uploadPkgBtn', false, 'Upload package');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkOta() {
|
||||
el('fwOut').textContent = await req('POST', '/api/ota/check');
|
||||
}
|
||||
|
||||
async function runOta() {
|
||||
el('fwOut').textContent = await req('POST', '/api/ota/run');
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
el('loginBtn').addEventListener('click', doLogin);
|
||||
document.querySelectorAll('.tab[data-tab]').forEach(button => button.addEventListener('click', () => showTab(button.dataset.tab)));
|
||||
document.querySelectorAll('.subtab').forEach(button => button.addEventListener('click', () => showAccessTab(button.dataset.subtab)));
|
||||
el('usersRefresh').addEventListener('click', loadUsers);
|
||||
el('userNew').addEventListener('click', newUserRecord);
|
||||
el('userBack').addEventListener('click', showUserOverview);
|
||||
el('userSave').addEventListener('click', saveSelectedUser);
|
||||
el('rolesRefresh').addEventListener('click', loadRoles);
|
||||
el('roleNew').addEventListener('click', newRoleRecord);
|
||||
el('roleBack').addEventListener('click', showRoleOverview);
|
||||
el('roleSave').addEventListener('click', addRole);
|
||||
el('roleDelete').addEventListener('click', deleteSelectedRole);
|
||||
el('apiBack').addEventListener('click', showApiOverview);
|
||||
el('apiSave').addEventListener('click', saveSelectedApi);
|
||||
el('apiTest').addEventListener('click', testSelectedApi);
|
||||
el('apiTestClear').addEventListener('click', clearApiTestBody);
|
||||
el('rolesOut').addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-role-index]');
|
||||
if (button) selectRole(+button.dataset.roleIndex);
|
||||
});
|
||||
el('userList').addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-user]');
|
||||
if (button) selectUser(button.dataset.user);
|
||||
});
|
||||
el('apiList').addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-api-index]');
|
||||
if (button) selectApi(+button.dataset.apiIndex);
|
||||
});
|
||||
el('netDhcp').addEventListener('change', syncNetworkMode);
|
||||
el('networkSaveBtn').addEventListener('click', saveNetwork);
|
||||
el('certSaveBtn').addEventListener('click', saveCertFile);
|
||||
el('certLoad').addEventListener('click', loadCert);
|
||||
el('settingsSave').addEventListener('click', saveSettings);
|
||||
el('uploadPkgBtn').addEventListener('click', uploadPkg);
|
||||
el('otaCheck').addEventListener('click', checkOta);
|
||||
el('otaRun').addEventListener('click', runOta);
|
||||
el('configDownload').addEventListener('click', downloadConfig);
|
||||
el('restoreConfigBtn').addEventListener('click', restoreConfig);
|
||||
el('factoryReset').addEventListener('click', factoryResetDevice);
|
||||
el('fileList').addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-download]');
|
||||
if (button) {
|
||||
downloadFile(button.dataset.download);
|
||||
return;
|
||||
}
|
||||
const directory = event.target.closest('.fileRow[data-path]');
|
||||
if (directory) loadFiles(directory.dataset.path);
|
||||
});
|
||||
el('logsLoad').addEventListener('click', loadLogs);
|
||||
el('logsClear').addEventListener('click', clearLogs);
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
Reference in New Issue
Block a user