diff --git a/moci/app.css b/moci/app.css index 8c5fc71..ade52af 100644 --- a/moci/app.css +++ b/moci/app.css @@ -442,6 +442,12 @@ body { transform: scale(0.98); } +.action-btn-sm.danger:hover { + border-color: var(--alert-red); + color: var(--alert-red); + box-shadow: 0 0 8px rgba(255, 51, 51, 0.15); +} + .section-title { font-family: var(--font-mono); font-size: 12px; diff --git a/moci/app.js b/moci/app.js deleted file mode 100644 index 4ec2874..0000000 --- a/moci/app.js +++ /dev/null @@ -1,3944 +0,0 @@ -class OpenWrtApp { - constructor() { - this.sessionId = localStorage.getItem('ubus_session'); - this.pollInterval = null; - this.loadHistory = []; - this.bandwidthHistory = { down: [], up: [] }; - this.lastNetStats = null; - this.lastCpuStats = null; - this.bandwidthCanvas = null; - this.bandwidthCtx = null; - this.init(); - } - - async init() { - if (this.sessionId) { - const valid = await this.validateSession(); - if (valid) { - this.showMainView(); - this.loadDashboard(); - this.startPolling(); - } else { - const savedCreds = this.getSavedCredentials(); - if (savedCreds) { - await this.autoLogin(savedCreds.username, savedCreds.password); - } else { - this.showLoginView(); - } - } - } else { - const savedCreds = this.getSavedCredentials(); - if (savedCreds) { - await this.autoLogin(savedCreds.username, savedCreds.password); - } else { - this.showLoginView(); - } - } - this.attachEventListeners(); - } - - getSavedCredentials() { - try { - const saved = localStorage.getItem('saved_credentials'); - return saved ? JSON.parse(atob(saved)) : null; - } catch { - return null; - } - } - - saveCredentials(username, password) { - const creds = btoa(JSON.stringify({ username, password })); - localStorage.setItem('saved_credentials', creds); - } - - clearSavedCredentials() { - localStorage.removeItem('saved_credentials'); - } - - async autoLogin(username, password) { - try { - await this.login(username, password, true); - } catch { - this.clearSavedCredentials(); - this.showLoginView(); - } - } - - async ubusCall(object, method, params = {}) { - const response = await fetch('/ubus', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: Math.random(), - method: 'call', - params: [this.sessionId || '00000000000000000000000000000000', object, method, params] - }) - }); - - const data = await response.json(); - if (data.error) throw new Error(data.error.message); - return data.result; - } - - openModal(modalId) { - document.getElementById(modalId).classList.remove('hidden'); - } - - closeModal(modalId) { - document.getElementById(modalId).classList.add('hidden'); - } - - setupModal(modalId, openBtnId, closeBtnId, cancelBtnId, saveBtnId, saveHandler) { - if (openBtnId) { - document.getElementById(openBtnId).addEventListener('click', () => this.openModal(modalId)); - } - if (closeBtnId) { - document.getElementById(closeBtnId).addEventListener('click', () => this.closeModal(modalId)); - } - if (cancelBtnId) { - document.getElementById(cancelBtnId).addEventListener('click', () => this.closeModal(modalId)); - } - if (saveBtnId && saveHandler) { - document.getElementById(saveBtnId).addEventListener('click', saveHandler); - } - } - - async uciGet(config, section = null) { - const params = { config }; - if (section) params.section = section; - return await this.ubusCall('uci', 'get', params); - } - - async uciSet(config, section, values) { - await this.ubusCall('uci', 'set', { config, section, values }); - } - - async uciAdd(config, type, name, values) { - await this.ubusCall('uci', 'add', { config, type, name, values }); - } - - async uciDelete(config, section) { - await this.ubusCall('uci', 'delete', { config, section }); - } - - async uciCommit(config) { - await this.ubusCall('uci', 'commit', { config }); - } - - async serviceReload(service) { - await this.ubusCall('file', 'exec', { - command: `/etc/init.d/${service}`, - params: ['reload'] - }); - } - - renderEmptyTable(tbody, colspan, message) { - tbody.innerHTML = `${message}`; - } - - renderBadge(type, text) { - return `${text}`; - } - - renderStatusBadge(condition, trueText = 'ENABLED', falseText = 'DISABLED') { - return condition ? this.renderBadge('success', trueText) : this.renderBadge('error', falseText); - } - - renderActionButtons(editFn, deleteFn, id) { - return ` - - - `; - } - - getFormValue(id) { - const el = document.getElementById(id); - return el ? el.value.trim() : ''; - } - - async saveUciConfig({ - config, - section, - values, - service, - modal, - successMsg, - reload, - isAdd = false, - addType = 'rule' - }) { - try { - if (isAdd) { - await this.uciAdd(config, addType, section || `cfg_${addType}_${Date.now()}`, values); - } else { - await this.uciSet(config, section, values); - } - - await this.uciCommit(config); - - if (service) { - await this.serviceReload(service); - } - - if (modal) { - this.closeModal(modal); - } - - this.showToast('Success', successMsg, 'success'); - - if (reload) { - await reload(); - } - } catch (err) { - console.error(`Failed to save ${config}:`, err); - this.showToast('Error', `Failed to save ${successMsg.toLowerCase()}`, 'error'); - } - } - - async login(username, password, isAutoLogin = false) { - try { - const result = await fetch('/ubus', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'call', - params: [ - '00000000000000000000000000000000', - 'session', - 'login', - { - username, - password - } - ] - }) - }).then(r => r.json()); - - if (result.result && result.result[1] && result.result[1].ubus_rpc_session) { - this.sessionId = result.result[1].ubus_rpc_session; - localStorage.setItem('ubus_session', this.sessionId); - - if (!isAutoLogin) { - const rememberMe = document.getElementById('remember-me'); - if (rememberMe && rememberMe.checked) { - this.saveCredentials(username, password); - } else { - this.clearSavedCredentials(); - } - } - - return true; - } - return false; - } catch (err) { - console.error('Login error:', err); - return false; - } - } - - async validateSession() { - try { - await this.ubusCall('session', 'access', {}); - return true; - } catch { - return false; - } - } - - async logout() { - try { - await this.ubusCall('session', 'destroy', {}); - } catch {} - localStorage.removeItem('ubus_session'); - this.clearSavedCredentials(); - this.sessionId = null; - this.stopPolling(); - this.showLoginView(); - } - - async loadDashboard() { - try { - const [status, systemInfo] = await this.ubusCall('system', 'info', {}); - const [boardStatus, boardInfo] = await this.ubusCall('system', 'board', {}); - - const hostnameEl = document.getElementById('hostname'); - const uptimeEl = document.getElementById('uptime'); - const memoryEl = document.getElementById('memory'); - const memoryBarEl = document.getElementById('memory-bar'); - - if (hostnameEl) hostnameEl.textContent = boardInfo.hostname || 'OpenWrt'; - if (uptimeEl) uptimeEl.textContent = this.formatUptime(systemInfo.uptime); - - const memPercent = ( - ((systemInfo.memory.total - systemInfo.memory.free) / systemInfo.memory.total) * - 100 - ).toFixed(0); - if (memoryEl) memoryEl.textContent = this.formatMemory(systemInfo.memory); - if (memoryBarEl) memoryBarEl.style.width = memPercent + '%'; - - await this.updateCpuUsage(); - await this.updateNetworkStats(); - await this.updateWANStatus(); - await this.updateSystemLog(); - await this.updateConnections(); - this.initBandwidthGraph(); - } catch (err) { - console.error('Failed to load dashboard:', err); - this.showToast('Error', 'Failed to load system information', 'error'); - } - } - - async updateCpuUsage() { - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/proc/stat' - }); - - if (result && result.data) { - const content = result.data; - const cpuLine = content.split('\n')[0]; - const values = cpuLine.split(/\s+/).slice(1).map(Number); - const idle = values[3]; - const total = values.reduce((a, b) => a + b, 0); - - if (this.lastCpuStats) { - const idleDelta = idle - this.lastCpuStats.idle; - const totalDelta = total - this.lastCpuStats.total; - const usage = ((1 - idleDelta / totalDelta) * 100).toFixed(1); - document.getElementById('cpu').textContent = usage + '%'; - document.getElementById('cpu-bar').style.width = usage + '%'; - } - - this.lastCpuStats = { idle, total }; - } - } catch (err) { - document.getElementById('cpu').textContent = 'N/A'; - } - } - - async updateNetworkStats() { - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/proc/net/dev' - }); - - if (result && result.data) { - const content = result.data; - const lines = content.split('\n').slice(2); - let totalRx = 0, - totalTx = 0; - - lines.forEach(line => { - if (!line.trim()) return; - const parts = line.trim().split(/\s+/); - if (parts[0].startsWith('lo:')) return; - totalRx += parseInt(parts[1]) || 0; - totalTx += parseInt(parts[9]) || 0; - }); - - if (this.lastNetStats) { - const rxRate = (totalRx - this.lastNetStats.rx) / 1024 / 5; - const txRate = (totalTx - this.lastNetStats.tx) / 1024 / 5; - - const downEl = document.getElementById('bandwidth-down'); - const upEl = document.getElementById('bandwidth-up'); - - if (downEl) downEl.textContent = this.formatRate(rxRate); - if (upEl) upEl.textContent = this.formatRate(txRate); - - this.bandwidthHistory.down.push(rxRate); - this.bandwidthHistory.up.push(txRate); - - if (this.bandwidthHistory.down.length > 60) { - this.bandwidthHistory.down.shift(); - this.bandwidthHistory.up.shift(); - } - - this.updateBandwidthGraph(); - } - - this.lastNetStats = { rx: totalRx, tx: totalTx }; - } - } catch (err) { - console.error('updateNetworkStats error:', err); - document.getElementById('net-rx').textContent = 'N/A'; - document.getElementById('net-tx').textContent = 'N/A'; - } - } - - async updateSystemLog() { - try { - const [status, result] = await this.ubusCall('file', 'exec', { - command: '/usr/libexec/syslog-wrapper' - }); - - if (status === 0 && result && result.stdout) { - const lines = result.stdout - .split('\n') - .filter(l => l.trim()) - .slice(-20); - const logHtml = lines - .map(line => { - let className = 'log-line'; - if (line.toLowerCase().includes('error') || line.toLowerCase().includes('fail')) { - className += ' error'; - } else if (line.toLowerCase().includes('warn')) { - className += ' warn'; - } - return `
${this.escapeHtml(line)}
`; - }) - .join(''); - document.getElementById('system-log').innerHTML = - logHtml || '
No logs available
'; - } else { - document.getElementById('system-log').innerHTML = - '
System log not available
'; - } - } catch (err) { - console.error('Failed to load system log:', err); - document.getElementById('system-log').innerHTML = - '
System log not available
'; - } - } - - async updateConnections() { - try { - const [arpStatus, arpResult] = await this.ubusCall('file', 'read', { - path: '/proc/net/arp' - }).catch(() => [1, null]); - - let deviceCount = 0; - if (arpResult && arpResult.data) { - const lines = arpResult.data.split('\n').slice(1); - deviceCount = lines.filter(line => { - if (!line.trim()) return false; - const parts = line.trim().split(/\s+/); - return parts.length >= 4 && parts[2] !== '0x0'; - }).length; - } - - document.getElementById('clients').textContent = deviceCount; - - const [status, leases] = await this.ubusCall('luci-rpc', 'getDHCPLeases', {}).catch(() => [1, null]); - const tbody = document.querySelector('#connections-table tbody'); - - if (!leases || !leases.dhcp_leases || leases.dhcp_leases.length === 0) { - tbody.innerHTML = - 'No active connections'; - return; - } - - const rows = leases.dhcp_leases - .map( - lease => ` - - ${this.escapeHtml(lease.ipaddr || 'Unknown')} - ${this.escapeHtml(lease.macaddr || 'Unknown')} - ${this.escapeHtml(lease.hostname || 'Unknown')} - Active - - ` - ) - .join(''); - - tbody.innerHTML = rows; - } catch (err) { - console.error('Failed to load connections:', err); - document.getElementById('clients').textContent = 'N/A'; - } - } - - updateLoadGraph() { - const svg = document.getElementById('load-graph'); - const width = 300; - const height = 80; - const data = this.loadHistory; - - if (data.length < 2) return; - - const max = Math.max(...data, 1); - const points = data - .map((val, i) => { - const x = (i / (data.length - 1)) * width; - const y = height - (val / max) * height; - return `${x},${y}`; - }) - .join(' '); - - const line = ``; - const fill = ``; - - svg.innerHTML = svg.innerHTML.split('')[0] + '' + fill + line; - } - - async updateWANStatus() { - try { - const heroCard = document.getElementById('wan-status-hero'); - const wanStatusEl = document.getElementById('wan-status'); - const wanIpEl = document.getElementById('wan-ip'); - const lanIpEl = document.getElementById('lan-ip'); - - if (!heroCard || !wanStatusEl || !wanIpEl || !lanIpEl) return; - - const [status, result] = await this.ubusCall('network.interface', 'dump', {}); - - if (status !== 0 || !result || !result.interface) { - heroCard.classList.add('offline'); - heroCard.classList.remove('online'); - wanStatusEl.textContent = 'UNKNOWN'; - return; - } - - const interfaces = result.interface; - - let lanIface = interfaces.find(i => i.interface === 'lan' || i.device === 'br-lan'); - if (!lanIface) { - lanIface = interfaces.find( - i => i.up && i['ipv4-address'] && i['ipv4-address'].length > 0 && i.interface !== 'loopback' - ); - } - - let internetIface = null; - let gateway = null; - - for (const iface of interfaces) { - if (!iface.up || iface.interface === 'loopback') continue; - if (iface.route) { - const defaultRoute = iface.route.find(r => r.target === '0.0.0.0'); - if (defaultRoute) { - internetIface = iface; - gateway = defaultRoute.nexthop; - break; - } - } - } - - if (lanIface && lanIface['ipv4-address'] && lanIface['ipv4-address'][0]) { - lanIpEl.textContent = lanIface['ipv4-address'][0].address; - } else { - lanIpEl.textContent = '---.---.---.---'; - } - - if (internetIface) { - heroCard.classList.add('online'); - heroCard.classList.remove('offline'); - wanStatusEl.textContent = 'ONLINE'; - - if (internetIface['ipv4-address'] && internetIface['ipv4-address'][0]) { - wanIpEl.textContent = internetIface['ipv4-address'][0].address; - } else if (gateway) { - wanIpEl.textContent = `Gateway: ${gateway}`; - } else { - wanIpEl.textContent = 'Connected'; - } - } else { - heroCard.classList.add('offline'); - heroCard.classList.remove('online'); - wanStatusEl.textContent = 'OFFLINE'; - wanIpEl.textContent = 'No internet route'; - } - } catch (err) { - console.error('Failed to load WAN status:', err); - } - } - - initBandwidthGraph() { - if (this.bandwidthCanvas && this.bandwidthCtx) return; - - const canvas = document.getElementById('bandwidth-graph'); - if (!canvas) return; - - this.bandwidthCanvas = canvas; - this.bandwidthCtx = canvas.getContext('2d'); - - canvas.width = canvas.offsetWidth; - canvas.height = 200; - } - - updateBandwidthGraph() { - if (!this.bandwidthCtx || !this.bandwidthCanvas) return; - - const ctx = this.bandwidthCtx; - const canvas = this.bandwidthCanvas; - const width = canvas.width; - const height = canvas.height; - const padding = 20; - - ctx.clearRect(0, 0, width, height); - - const downData = this.bandwidthHistory.down; - const upData = this.bandwidthHistory.up; - - if (downData.length < 2) return; - - const max = Math.max(...downData, ...upData, 100); - const stepX = (width - padding * 2) / (downData.length - 1); - - ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; - ctx.lineWidth = 1; - for (let i = 0; i <= 4; i++) { - const y = padding + (i * (height - padding * 2)) / 4; - ctx.beginPath(); - ctx.moveTo(padding, y); - ctx.lineTo(width - padding, y); - ctx.stroke(); - } - - ctx.fillStyle = 'rgba(226, 226, 229, 0.15)'; - ctx.beginPath(); - ctx.moveTo(padding, height - padding); - downData.forEach((val, i) => { - const x = padding + i * stepX; - const y = height - padding - (val / max) * (height - padding * 2); - ctx.lineTo(x, y); - }); - ctx.lineTo(width - padding, height - padding); - ctx.closePath(); - ctx.fill(); - - ctx.strokeStyle = 'rgba(226, 226, 229, 0.9)'; - ctx.lineWidth = 2; - ctx.beginPath(); - downData.forEach((val, i) => { - const x = padding + i * stepX; - const y = height - padding - (val / max) * (height - padding * 2); - if (i === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - }); - ctx.stroke(); - - ctx.fillStyle = 'rgba(226, 226, 229, 0.08)'; - ctx.beginPath(); - ctx.moveTo(padding, height - padding); - upData.forEach((val, i) => { - const x = padding + i * stepX; - const y = height - padding - (val / max) * (height - padding * 2); - ctx.lineTo(x, y); - }); - ctx.lineTo(width - padding, height - padding); - ctx.closePath(); - ctx.fill(); - - ctx.strokeStyle = 'rgba(226, 226, 229, 0.5)'; - ctx.lineWidth = 2; - ctx.beginPath(); - upData.forEach((val, i) => { - const x = padding + i * stepX; - const y = height - padding - (val / max) * (height - padding * 2); - if (i === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - }); - ctx.stroke(); - } - - startPolling() { - this.stopPolling(); - this.pollInterval = setInterval(() => { - const currentPage = document.querySelector('.page:not(.hidden)'); - if (currentPage && currentPage.id === 'dashboard-page') { - this.loadDashboard(); - } - }, 5000); - } - - stopPolling() { - if (this.pollInterval) { - clearInterval(this.pollInterval); - this.pollInterval = null; - } - } - - async rebootSystem() { - if (!confirm('Are you sure you want to reboot the system?')) return; - try { - await this.ubusCall('system', 'reboot', {}); - this.showToast('Success', 'System is rebooting...', 'success'); - setTimeout(() => this.logout(), 2000); - } catch (err) { - this.showToast('Error', 'Failed to reboot system', 'error'); - } - } - - async restartNetwork() { - if (!confirm('Restart network services? This may interrupt connectivity.')) return; - try { - await this.ubusCall('file', 'exec', { command: '/etc/init.d/network', params: ['restart'] }); - this.showToast('Success', 'Network services restarting...', 'success'); - } catch (err) { - this.showToast('Error', 'Failed to restart network', 'error'); - } - } - - async restartFirewall() { - try { - await this.ubusCall('file', 'exec', { command: '/etc/init.d/firewall', params: ['restart'] }); - this.showToast('Success', 'Firewall restarted successfully', 'success'); - } catch (err) { - this.showToast('Error', 'Failed to restart firewall', 'error'); - } - } - - formatUptime(seconds) { - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - return `${days}d ${hours}h ${minutes}m`; - } - - formatMemory(mem) { - const total = (mem.total / 1024 / 1024).toFixed(0); - const free = (mem.free / 1024 / 1024).toFixed(0); - const used = total - free; - const percent = ((used / total) * 100).toFixed(0); - return `${used}MB / ${total}MB (${percent}%)`; - } - - formatRate(kbps) { - const mbps = (kbps * 8) / 1024; - if (mbps < 0.01) return '0 Mbps'; - if (mbps < 1) return `${mbps.toFixed(2)} Mbps`; - return `${mbps.toFixed(1)} Mbps`; - } - - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - showLoginView() { - document.getElementById('login-view').classList.remove('hidden'); - document.getElementById('main-view').classList.add('hidden'); - - const savedCreds = this.getSavedCredentials(); - if (savedCreds) { - document.getElementById('username').value = savedCreds.username; - document.getElementById('remember-me').checked = true; - } - } - - showMainView() { - document.getElementById('login-view').classList.add('hidden'); - document.getElementById('main-view').classList.remove('hidden'); - } - - showError(message) { - const errorEl = document.getElementById('login-error'); - if (errorEl) { - errorEl.textContent = message; - setTimeout(() => (errorEl.textContent = ''), 3000); - } - } - - showToast(title, message, type = 'info') { - const toast = document.createElement('div'); - toast.className = `toast ${type}`; - toast.innerHTML = ` -
${this.escapeHtml(title)}
-
${this.escapeHtml(message)}
- `; - document.body.appendChild(toast); - setTimeout(() => toast.remove(), 4000); - } - - attachEventListeners() { - document.getElementById('login-form').addEventListener('submit', async e => { - e.preventDefault(); - const username = document.getElementById('username').value; - const password = document.getElementById('password').value; - - const success = await this.login(username, password); - if (success) { - this.showMainView(); - this.loadDashboard(); - this.startPolling(); - } else { - this.showError('Invalid credentials'); - } - }); - - document.getElementById('logout-btn').addEventListener('click', () => { - this.logout(); - }); - - document.getElementById('reboot-btn').addEventListener('click', () => { - this.rebootSystem(); - }); - - document.getElementById('restart-network-btn').addEventListener('click', () => { - this.restartNetwork(); - }); - - document.getElementById('restart-firewall-btn').addEventListener('click', () => { - this.restartFirewall(); - }); - - document.querySelectorAll('.nav a').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const page = e.target.dataset.page; - this.navigateTo(page); - }); - }); - - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.addEventListener('click', e => { - const tabName = e.target.dataset.tab; - const page = e.target.closest('.page'); - this.switchTab(page, tabName); - }); - }); - - document.getElementById('ping-btn').addEventListener('click', () => { - this.runPing(); - }); - - document.getElementById('traceroute-btn').addEventListener('click', () => { - this.runTraceroute(); - }); - - document.getElementById('wol-btn').addEventListener('click', () => { - this.sendWakeOnLan(); - }); - - document.getElementById('close-interface-modal').addEventListener('click', () => { - this.closeInterfaceConfig(); - }); - - document.getElementById('cancel-interface-btn').addEventListener('click', () => { - this.closeInterfaceConfig(); - }); - - document.getElementById('save-interface-btn').addEventListener('click', () => { - this.saveInterfaceConfig(); - }); - - document.getElementById('edit-iface-proto').addEventListener('change', () => { - this.updateStaticConfigVisibility(); - }); - - document.querySelector('.modal-backdrop')?.addEventListener('click', () => { - this.closeInterfaceConfig(); - }); - - document.getElementById('close-wireless-modal').addEventListener('click', () => { - this.closeWirelessConfig(); - }); - - document.getElementById('cancel-wireless-btn').addEventListener('click', () => { - this.closeWirelessConfig(); - }); - - document.getElementById('save-wireless-btn').addEventListener('click', () => { - this.saveWirelessConfig(); - }); - - document.getElementById('edit-wifi-encryption').addEventListener('change', () => { - this.updateWirelessKeyVisibility(); - }); - - document.getElementById('add-forward-btn').addEventListener('click', () => { - this.openForwardRule(); - }); - - document.getElementById('close-forward-modal').addEventListener('click', () => { - this.closeForwardRule(); - }); - - document.getElementById('cancel-forward-btn').addEventListener('click', () => { - this.closeForwardRule(); - }); - - document.getElementById('save-forward-btn').addEventListener('click', () => { - this.saveForwardRule(); - }); - - document.getElementById('add-fw-rule-btn').addEventListener('click', () => { - document.getElementById('edit-fw-rule-section').value = ''; - document.getElementById('edit-fw-rule-name').value = ''; - document.getElementById('edit-fw-rule-target').value = 'ACCEPT'; - document.getElementById('edit-fw-rule-src').value = ''; - document.getElementById('edit-fw-rule-dest').value = ''; - document.getElementById('edit-fw-rule-proto').value = ''; - document.getElementById('edit-fw-rule-dest-port').value = ''; - document.getElementById('edit-fw-rule-src-ip').value = ''; - this.openModal('fw-rule-modal'); - }); - - this.setupModal('fw-rule-modal', null, 'close-fw-rule-modal', 'cancel-fw-rule-btn', 'save-fw-rule-btn', () => - this.saveFirewallRule() - ); - - document.getElementById('add-static-lease-btn').addEventListener('click', () => { - this.openStaticLease(); - }); - - document.getElementById('close-static-lease-modal').addEventListener('click', () => { - this.closeStaticLease(); - }); - - document.getElementById('cancel-static-lease-btn').addEventListener('click', () => { - this.closeStaticLease(); - }); - - document.getElementById('save-static-lease-btn').addEventListener('click', () => { - this.saveStaticLease(); - }); - - document.getElementById('add-dns-entry-btn').addEventListener('click', () => { - document.getElementById('edit-dns-entry-section').value = ''; - document.getElementById('edit-dns-hostname').value = ''; - document.getElementById('edit-dns-ip').value = ''; - this.openModal('dns-entry-modal'); - }); - - this.setupModal( - 'dns-entry-modal', - null, - 'close-dns-entry-modal', - 'cancel-dns-entry-btn', - 'save-dns-entry-btn', - () => this.saveDNSEntry() - ); - - document.getElementById('add-host-entry-btn').addEventListener('click', () => { - document.getElementById('edit-host-entry-index').value = ''; - document.getElementById('edit-host-ip').value = ''; - document.getElementById('edit-host-names').value = ''; - this.openModal('host-entry-modal'); - }); - - this.setupModal( - 'host-entry-modal', - null, - 'close-host-entry-modal', - 'cancel-host-entry-btn', - 'save-host-entry-btn', - () => this.saveHostEntry() - ); - - document.getElementById('add-ddns-btn').addEventListener('click', () => { - document.getElementById('edit-ddns-section').value = ''; - document.getElementById('edit-ddns-name').value = ''; - document.getElementById('edit-ddns-service').value = 'dyndns.org'; - document.getElementById('edit-ddns-hostname').value = ''; - document.getElementById('edit-ddns-username').value = ''; - document.getElementById('edit-ddns-password').value = ''; - document.getElementById('edit-ddns-check-interval').value = '10'; - document.getElementById('edit-ddns-enabled').value = '1'; - this.openModal('ddns-modal'); - }); - - this.setupModal('ddns-modal', null, 'close-ddns-modal', 'cancel-ddns-btn', 'save-ddns-btn', () => - this.saveDDNS() - ); - - document.getElementById('save-qos-config-btn').addEventListener('click', () => { - this.saveQoSConfig(); - }); - - document.getElementById('add-qos-rule-btn').addEventListener('click', () => { - document.getElementById('edit-qos-rule-section').value = ''; - document.getElementById('edit-qos-rule-name').value = ''; - document.getElementById('edit-qos-rule-priority').value = 'Normal'; - document.getElementById('edit-qos-rule-proto').value = ''; - document.getElementById('edit-qos-rule-ports').value = ''; - document.getElementById('edit-qos-rule-srchost').value = ''; - this.openModal('qos-rule-modal'); - }); - - this.setupModal( - 'qos-rule-modal', - null, - 'close-qos-rule-modal', - 'cancel-qos-rule-btn', - 'save-qos-rule-btn', - () => this.saveQoSRule() - ); - - document.getElementById('generate-wg-keys-btn').addEventListener('click', () => { - this.generateWireGuardKeys(); - }); - - document.getElementById('save-wg-config-btn').addEventListener('click', () => { - this.saveWireGuardConfig(); - }); - - document.getElementById('add-wg-peer-btn').addEventListener('click', () => { - document.getElementById('edit-wg-peer-section').value = ''; - document.getElementById('edit-wg-peer-name').value = ''; - document.getElementById('edit-wg-peer-public-key').value = ''; - document.getElementById('edit-wg-peer-allowed-ips').value = ''; - document.getElementById('edit-wg-peer-keepalive').value = '25'; - document.getElementById('edit-wg-peer-preshared-key').value = ''; - this.openModal('wg-peer-modal'); - }); - - this.setupModal('wg-peer-modal', null, 'close-wg-peer-modal', 'cancel-wg-peer-btn', 'save-wg-peer-btn', () => - this.saveWireGuardPeer() - ); - - document.getElementById('add-cron-btn').addEventListener('click', () => { - this.openCronJob(); - }); - - document.getElementById('close-cron-modal').addEventListener('click', () => { - this.closeCronJob(); - }); - - document.getElementById('cancel-cron-btn').addEventListener('click', () => { - this.closeCronJob(); - }); - - document.getElementById('save-cron-btn').addEventListener('click', () => { - this.saveCronJob(); - }); - - document.getElementById('add-ssh-key-btn').addEventListener('click', () => { - this.openSSHKey(); - }); - - document.getElementById('close-ssh-key-modal').addEventListener('click', () => { - this.closeSSHKey(); - }); - - document.getElementById('cancel-ssh-key-btn').addEventListener('click', () => { - this.closeSSHKey(); - }); - - document.getElementById('parse-keys-btn').addEventListener('click', () => { - this.parseSSHKeys(); - }); - - document.getElementById('save-ssh-keys-btn').addEventListener('click', () => { - this.saveSSHKeys(); - }); - - document.getElementById('backup-btn').addEventListener('click', () => { - this.generateBackup(); - }); - - document.getElementById('reset-btn').addEventListener('click', () => { - this.resetToDefaults(); - }); - - document.getElementById('change-password-btn')?.addEventListener('click', () => { - this.changePassword(); - }); - - document.getElementById('save-general-btn')?.addEventListener('click', () => { - this.saveGeneralSettings(); - }); - - document.addEventListener('visibilitychange', () => { - if (document.hidden) { - this.stopPolling(); - } else { - const currentPage = document.querySelector('.page:not(.hidden)'); - if (currentPage && currentPage.id === 'dashboard-page') { - this.startPolling(); - } - } - }); - } - - navigateTo(page) { - document.querySelectorAll('.page').forEach(p => p.classList.add('hidden')); - document.querySelectorAll('.nav a').forEach(a => a.classList.remove('active')); - - document.getElementById(`${page}-page`).classList.remove('hidden'); - document.querySelector(`[data-page="${page}"]`).classList.add('active'); - - if (page === 'dashboard') { - this.loadDashboard(); - this.startPolling(); - } else { - this.stopPolling(); - if (page === 'network') { - this.loadNetworkData(); - } else if (page === 'system') { - this.loadSystemData(); - } - } - } - - switchTab(page, tabName) { - page.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); - page.querySelectorAll('.tab-content').forEach(content => content.classList.add('hidden')); - - page.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); - page.querySelector(`#tab-${tabName}`).classList.remove('hidden'); - - const tabLoaders = { - interfaces: () => this.loadNetworkInterfaces(), - wireless: () => this.loadWireless(), - firewall: () => this.loadFirewallRules(), - dhcp: () => this.loadDHCPLeases(), - dns: () => this.loadDNS(), - ddns: () => this.loadDDNS(), - qos: () => this.loadQoS(), - vpn: () => this.loadWireGuard(), - startup: () => this.loadServices(), - software: () => this.loadPackages(), - cron: () => this.loadCronJobs(), - 'ssh-keys': () => this.loadSSHKeys(), - mounts: () => this.loadMountPoints(), - led: () => this.loadLEDs(), - upgrade: () => this.initFirmwareUpgrade() - }; - - tabLoaders[tabName]?.(); - } - - async loadNetworkData() { - this.loadNetworkInterfaces(); - } - - async loadSystemData() { - const [status, boardInfo] = await this.ubusCall('system', 'board', {}); - if (boardInfo) { - const hostnameInput = document.getElementById('system-hostname'); - if (hostnameInput) { - hostnameInput.value = boardInfo.hostname || 'OpenWrt'; - } - } - } - - async loadNetworkInterfaces() { - try { - const [status, result] = await this.ubusCall('network.interface', 'dump', {}); - const tbody = document.querySelector('#interfaces-table tbody'); - - if (!result || !result.interface || result.interface.length === 0) { - tbody.innerHTML = - 'No interfaces found'; - return; - } - - const rows = result.interface - .map(iface => { - const statusBadge = iface.up - ? 'UP' - : 'DOWN'; - const ipaddr = - iface['ipv4-address'] && iface['ipv4-address'][0] ? iface['ipv4-address'][0].address : 'N/A'; - const rxBytes = ((iface.statistics?.rx_bytes || 0) / 1024 / 1024).toFixed(2); - const txBytes = ((iface.statistics?.tx_bytes || 0) / 1024 / 1024).toFixed(2); - const proto = iface.proto || 'unknown'; - - return ` - - ${this.escapeHtml(iface.interface || 'Unknown')} - ${this.escapeHtml(proto).toUpperCase()} - ${statusBadge} - ${this.escapeHtml(ipaddr)} - ${rxBytes} / ${txBytes} MB - - Configure - - - `; - }) - .join(''); - - tbody.innerHTML = rows; - - document.querySelectorAll('#interfaces-table .action-link').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const ifaceName = e.target.dataset.iface; - this.openInterfaceConfig(ifaceName); - }); - }); - } catch (err) { - console.error('Failed to load network interfaces:', err); - const tbody = document.querySelector('#interfaces-table tbody'); - tbody.innerHTML = - 'Failed to load interfaces'; - } - } - - async openInterfaceConfig(ifaceName) { - try { - const [status, config] = await this.ubusCall('uci', 'get', { - config: 'network', - section: ifaceName - }); - - document.getElementById('edit-iface-name').value = ifaceName; - document.getElementById('edit-iface-proto').value = config.values.proto || 'static'; - document.getElementById('edit-iface-ipaddr').value = config.values.ipaddr || ''; - document.getElementById('edit-iface-netmask').value = config.values.netmask || ''; - document.getElementById('edit-iface-gateway').value = config.values.gateway || ''; - - const dns = config.values.dns || []; - const dnsStr = Array.isArray(dns) ? dns.join(' ') : dns || ''; - document.getElementById('edit-iface-dns').value = dnsStr; - - this.updateStaticConfigVisibility(); - document.getElementById('interface-modal').classList.remove('hidden'); - } catch (err) { - console.error('Failed to load interface config:', err); - this.showToast('Error', 'Failed to load interface configuration', 'error'); - } - } - - closeInterfaceConfig() { - document.getElementById('interface-modal').classList.add('hidden'); - } - - updateStaticConfigVisibility() { - const proto = document.getElementById('edit-iface-proto').value; - const staticConfig = document.getElementById('static-config'); - if (proto === 'static') { - staticConfig.style.display = 'block'; - } else { - staticConfig.style.display = 'none'; - } - } - - async saveInterfaceConfig() { - try { - const ifaceName = document.getElementById('edit-iface-name').value; - const proto = document.getElementById('edit-iface-proto').value; - - await this.ubusCall('uci', 'set', { - config: 'network', - section: ifaceName, - values: { - proto: proto - } - }); - - if (proto === 'static') { - const ipaddr = document.getElementById('edit-iface-ipaddr').value; - const netmask = document.getElementById('edit-iface-netmask').value; - const gateway = document.getElementById('edit-iface-gateway').value; - const dns = document - .getElementById('edit-iface-dns') - .value.split(/\s+/) - .filter(d => d); - - const staticValues = { proto }; - if (ipaddr) staticValues.ipaddr = ipaddr; - if (netmask) staticValues.netmask = netmask; - if (gateway) staticValues.gateway = gateway; - if (dns.length > 0) staticValues.dns = dns; - - await this.ubusCall('uci', 'set', { - config: 'network', - section: ifaceName, - values: staticValues - }); - } - - await this.ubusCall('uci', 'commit', { - config: 'network' - }); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/network', - params: ['reload'] - }); - - this.showToast('Success', 'Interface configuration saved', 'success'); - this.closeInterfaceConfig(); - setTimeout(() => this.loadNetworkInterfaces(), 2000); - } catch (err) { - console.error('Failed to save interface config:', err); - this.showToast('Error', 'Failed to save configuration', 'error'); - } - } - - async loadWireless() { - try { - const [status, config] = await this.ubusCall('uci', 'get', { - config: 'wireless' - }); - - const tbody = document.querySelector('#wireless-table tbody'); - const rows = []; - - if (!config || !config.values) { - tbody.innerHTML = - 'No wireless devices found'; - return; - } - - for (const [section, sectionData] of Object.entries(config.values)) { - if (sectionData['.type'] === 'wifi-iface') { - const radio = sectionData.device || 'unknown'; - const ssid = sectionData.ssid || 'N/A'; - const disabled = sectionData.disabled === '1'; - const encryption = sectionData.encryption || 'none'; - - const statusBadge = disabled - ? 'DISABLED' - : 'ENABLED'; - - let radioInfo = await this.getRadioInfo(radio); - const channel = radioInfo.channel || 'Auto'; - const signal = radioInfo.signal || 'N/A'; - - rows.push(` - - ${this.escapeHtml(radio)} - ${this.escapeHtml(ssid)} - ${this.escapeHtml(String(channel))} - ${statusBadge} - ${this.escapeHtml(encryption)} - - Configure - - - `); - } - } - - if (rows.length === 0) { - tbody.innerHTML = - 'No wireless interfaces found'; - } else { - tbody.innerHTML = rows.join(''); - - document.querySelectorAll('#wireless-table .action-link').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const section = e.target.dataset.wifiSection; - const radio = e.target.dataset.wifiRadio; - this.openWirelessConfig(section, radio); - }); - }); - } - } catch (err) { - console.error('Failed to load wireless:', err); - const tbody = document.querySelector('#wireless-table tbody'); - tbody.innerHTML = - 'Failed to load wireless'; - } - } - - async getRadioInfo(radio) { - try { - const [status, config] = await this.ubusCall('uci', 'get', { - config: 'wireless', - section: radio - }); - return config?.values || {}; - } catch { - return {}; - } - } - - async openWirelessConfig(section, radio) { - try { - const [status, config] = await this.ubusCall('uci', 'get', { - config: 'wireless', - section: section - }); - - const values = config.values; - document.getElementById('edit-wifi-section').value = section; - document.getElementById('edit-wifi-radio').value = radio; - document.getElementById('edit-wifi-ssid').value = values.ssid || ''; - document.getElementById('edit-wifi-encryption').value = values.encryption || 'none'; - document.getElementById('edit-wifi-key').value = values.key || ''; - document.getElementById('edit-wifi-disabled').value = values.disabled || '0'; - document.getElementById('edit-wifi-hidden').value = values.hidden || '0'; - - const [radioStatus, radioConfig] = await this.ubusCall('uci', 'get', { - config: 'wireless', - section: radio - }); - - const radioValues = radioConfig.values; - const channelSelect = document.getElementById('edit-wifi-channel'); - const currentChannel = radioValues.channel || 'auto'; - const band = radioValues.band || radioValues.hwmode || '2g'; - - channelSelect.innerHTML = ''; - if (band.includes('5') || band.includes('a')) { - for (let ch of [36, 40, 44, 48, 149, 153, 157, 161, 165]) { - channelSelect.innerHTML += ``; - } - } else { - for (let ch = 1; ch <= 13; ch++) { - channelSelect.innerHTML += ``; - } - } - channelSelect.value = currentChannel; - - document.getElementById('edit-wifi-txpower').value = radioValues.txpower || ''; - - this.updateWirelessKeyVisibility(); - document.getElementById('wireless-modal').classList.remove('hidden'); - } catch (err) { - console.error('Failed to load wireless config:', err); - this.showToast('Error', 'Failed to load wireless configuration', 'error'); - } - } - - closeWirelessConfig() { - document.getElementById('wireless-modal').classList.add('hidden'); - } - - updateWirelessKeyVisibility() { - const encryption = document.getElementById('edit-wifi-encryption').value; - const keyGroup = document.getElementById('wifi-key-group'); - if (encryption === 'none') { - keyGroup.style.display = 'none'; - } else { - keyGroup.style.display = 'block'; - } - } - - async saveWirelessConfig() { - try { - const section = document.getElementById('edit-wifi-section').value; - const radio = document.getElementById('edit-wifi-radio').value; - const ssid = document.getElementById('edit-wifi-ssid').value; - const encryption = document.getElementById('edit-wifi-encryption').value; - const key = document.getElementById('edit-wifi-key').value; - const disabled = document.getElementById('edit-wifi-disabled').value; - const hidden = document.getElementById('edit-wifi-hidden').value; - const channel = document.getElementById('edit-wifi-channel').value; - const txpower = document.getElementById('edit-wifi-txpower').value; - - if (!ssid) { - this.showToast('Error', 'SSID is required', 'error'); - return; - } - - if (encryption !== 'none' && (!key || key.length < 8)) { - this.showToast('Error', 'Password must be at least 8 characters', 'error'); - return; - } - - const ifaceValues = { ssid, encryption, disabled, hidden }; - if (encryption !== 'none') { - ifaceValues.key = key; - } - - await this.ubusCall('uci', 'set', { - config: 'wireless', - section: section, - values: ifaceValues - }); - - const radioValues = {}; - if (channel) radioValues.channel = channel; - if (txpower) radioValues.txpower = txpower; - - if (Object.keys(radioValues).length > 0) { - await this.ubusCall('uci', 'set', { - config: 'wireless', - section: radio, - values: radioValues - }); - } - - await this.ubusCall('uci', 'commit', { - config: 'wireless' - }); - - await this.ubusCall('file', 'exec', { - command: '/sbin/wifi', - params: ['reload'] - }); - - this.showToast('Success', 'Wireless configuration saved. WiFi reloading...', 'success'); - this.closeWirelessConfig(); - setTimeout(() => this.loadWireless(), 3000); - } catch (err) { - console.error('Failed to save wireless config:', err); - this.showToast('Error', 'Failed to save configuration', 'error'); - } - } - - async loadFirewallRules() { - await this.loadPortForwarding(); - await this.loadFirewallGeneralRules(); - } - - async loadPortForwarding() { - try { - const [status, config] = await this.uciGet('firewall'); - const tbody = document.querySelector('#firewall-table tbody'); - const rows = []; - - if (!config || !config.values) { - tbody.innerHTML = - 'No rules configured'; - return; - } - - for (const [section, sectionData] of Object.entries(config.values)) { - if (sectionData['.type'] === 'redirect') { - const name = sectionData.name || section; - const proto = sectionData.proto || 'tcp'; - const srcDport = sectionData.src_dport || 'N/A'; - const destIp = sectionData.dest_ip || 'N/A'; - const destPort = sectionData.dest_port || srcDport; - const enabled = sectionData.enabled !== '0'; - - const statusBadge = enabled - ? 'YES' - : 'NO'; - - rows.push(` - - ${this.escapeHtml(name)} - ${this.escapeHtml(proto).toUpperCase()} - ${this.escapeHtml(srcDport)} - ${this.escapeHtml(destIp)} - ${this.escapeHtml(destPort)} - ${statusBadge} - - Edit | - Delete - - - `); - } - } - - if (rows.length === 0) { - tbody.innerHTML = - 'No rules configured'; - } else { - tbody.innerHTML = rows.join(''); - - document.querySelectorAll('#firewall-table .action-link').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const section = e.target.dataset.forwardSection; - this.openForwardRule(section); - }); - }); - - document.querySelectorAll('#firewall-table .action-link-danger').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const section = e.target.dataset.forwardDelete; - this.deleteForwardRule(section); - }); - }); - } - } catch (err) { - console.error('Failed to load firewall rules:', err); - const tbody = document.querySelector('#firewall-table tbody'); - tbody.innerHTML = - 'Failed to load rules'; - } - } - - async openForwardRule(section = null) { - try { - if (section) { - const [status, config] = await this.ubusCall('uci', 'get', { - config: 'firewall', - section: section - }); - - const values = config.values; - document.getElementById('edit-forward-section').value = section; - document.getElementById('edit-forward-name').value = values.name || ''; - document.getElementById('edit-forward-proto').value = values.proto || 'tcp'; - document.getElementById('edit-forward-src-dport').value = values.src_dport || ''; - document.getElementById('edit-forward-dest-ip').value = values.dest_ip || ''; - document.getElementById('edit-forward-dest-port').value = values.dest_port || ''; - document.getElementById('edit-forward-enabled').value = values.enabled === '0' ? '0' : '1'; - } else { - document.getElementById('edit-forward-section').value = ''; - document.getElementById('edit-forward-name').value = ''; - document.getElementById('edit-forward-proto').value = 'tcp'; - document.getElementById('edit-forward-src-dport').value = ''; - document.getElementById('edit-forward-dest-ip').value = ''; - document.getElementById('edit-forward-dest-port').value = ''; - document.getElementById('edit-forward-enabled').value = '1'; - } - - document.getElementById('forward-modal').classList.remove('hidden'); - } catch (err) { - console.error('Failed to load forward rule:', err); - this.showToast('Error', 'Failed to load rule configuration', 'error'); - } - } - - closeForwardRule() { - document.getElementById('forward-modal').classList.add('hidden'); - } - - async saveForwardRule() { - try { - const section = document.getElementById('edit-forward-section').value; - const name = document.getElementById('edit-forward-name').value; - const proto = document.getElementById('edit-forward-proto').value; - const srcDport = document.getElementById('edit-forward-src-dport').value; - const destIp = document.getElementById('edit-forward-dest-ip').value; - const destPort = document.getElementById('edit-forward-dest-port').value; - const enabled = document.getElementById('edit-forward-enabled').value; - - if (!name || !srcDport || !destIp) { - this.showToast('Error', 'Name, external port, and internal IP are required', 'error'); - return; - } - - const values = { - name, - src: 'wan', - proto, - src_dport: srcDport, - dest: 'lan', - dest_ip: destIp, - target: 'DNAT', - enabled - }; - - if (destPort) { - values.dest_port = destPort; - } - - if (section) { - await this.ubusCall('uci', 'set', { - config: 'firewall', - section: section, - values: values - }); - } else { - await this.ubusCall('uci', 'add', { - config: 'firewall', - type: 'redirect', - name: name, - values: values - }); - } - - await this.ubusCall('uci', 'commit', { - config: 'firewall' - }); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/firewall', - params: ['reload'] - }); - - this.showToast('Success', 'Port forwarding rule saved', 'success'); - this.closeForwardRule(); - setTimeout(() => this.loadFirewallRules(), 2000); - } catch (err) { - console.error('Failed to save forward rule:', err); - this.showToast('Error', 'Failed to save rule', 'error'); - } - } - - async deleteForwardRule(section) { - if (!confirm('Delete this port forwarding rule?')) return; - - try { - await this.ubusCall('uci', 'delete', { - config: 'firewall', - section: section - }); - - await this.ubusCall('uci', 'commit', { - config: 'firewall' - }); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/firewall', - params: ['reload'] - }); - - this.showToast('Success', 'Rule deleted', 'success'); - setTimeout(() => this.loadFirewallRules(), 2000); - } catch (err) { - console.error('Failed to delete rule:', err); - this.showToast('Error', 'Failed to delete rule', 'error'); - } - } - - async loadFirewallGeneralRules() { - try { - const [status, config] = await this.uciGet('firewall'); - const tbody = document.querySelector('#fw-rules-table tbody'); - - if (!config || !config.values) { - this.renderEmptyTable(tbody, 7, 'No firewall rules'); - return; - } - - const rows = []; - for (const [section, data] of Object.entries(config.values)) { - if (data['.type'] === 'rule' && data.name) { - const name = data.name || section; - const src = data.src || 'any'; - const dest = data.dest || 'any'; - const proto = data.proto || 'any'; - const destPort = data.dest_port || 'any'; - const target = data.target || 'ACCEPT'; - - rows.push(` - - ${this.escapeHtml(name)} - ${this.escapeHtml(src)} - ${this.escapeHtml(dest)} - ${this.escapeHtml(proto).toUpperCase()} - ${this.escapeHtml(destPort)} - ${this.renderBadge(target === 'ACCEPT' ? 'success' : 'error', target)} - ${this.renderActionButtons('editFirewallRule', 'deleteFirewallRule', section)} - - `); - } - } - - if (rows.length === 0) { - this.renderEmptyTable(tbody, 7, 'No firewall rules'); - } else { - tbody.innerHTML = rows.join(''); - } - } catch (err) { - console.error('Failed to load firewall rules:', err); - this.renderEmptyTable(document.querySelector('#fw-rules-table tbody'), 7, 'Failed to load rules'); - } - } - - async editFirewallRule(section) { - try { - const [status, data] = await this.uciGet('firewall', section); - - if (status === 0 && data && data.values) { - const values = data.values; - document.getElementById('edit-fw-rule-section').value = section; - document.getElementById('edit-fw-rule-name').value = values.name || ''; - document.getElementById('edit-fw-rule-target').value = values.target || 'ACCEPT'; - document.getElementById('edit-fw-rule-src').value = values.src || ''; - document.getElementById('edit-fw-rule-dest').value = values.dest || ''; - document.getElementById('edit-fw-rule-proto').value = values.proto || ''; - document.getElementById('edit-fw-rule-dest-port').value = values.dest_port || ''; - document.getElementById('edit-fw-rule-src-ip').value = values.src_ip || ''; - this.openModal('fw-rule-modal'); - } - } catch (err) { - console.error('Failed to load firewall rule:', err); - this.showToast('Error', 'Failed to load rule', 'error'); - } - } - - async saveFirewallRule() { - try { - const section = this.getFormValue('edit-fw-rule-section'); - const name = this.getFormValue('edit-fw-rule-name'); - const target = this.getFormValue('edit-fw-rule-target'); - const src = this.getFormValue('edit-fw-rule-src'); - const dest = this.getFormValue('edit-fw-rule-dest'); - const proto = this.getFormValue('edit-fw-rule-proto'); - const destPort = this.getFormValue('edit-fw-rule-dest-port'); - const srcIp = this.getFormValue('edit-fw-rule-src-ip'); - - if (!name) { - this.showToast('Error', 'Please provide a rule name', 'error'); - return; - } - - const values = { name, target }; - if (src) values.src = src; - if (dest) values.dest = dest; - if (proto) values.proto = proto; - if (destPort) values.dest_port = destPort; - if (srcIp) values.src_ip = srcIp; - - await this.saveUciConfig({ - config: 'firewall', - section: section, - values: values, - service: 'firewall', - modal: 'fw-rule-modal', - successMsg: 'Firewall rule saved', - reload: () => this.loadFirewallGeneralRules(), - isAdd: !section, - addType: 'rule' - }); - } catch (err) { - console.error('Failed to save firewall rule:', err); - this.showToast('Error', 'Failed to save rule', 'error'); - } - } - - async deleteFirewallRule(section) { - if (!confirm('Delete this firewall rule?')) return; - - try { - await this.uciDelete('firewall', section); - await this.uciCommit('firewall'); - await this.serviceReload('firewall'); - - this.showToast('Success', 'Firewall rule deleted', 'success'); - await this.loadFirewallGeneralRules(); - } catch (err) { - console.error('Failed to delete firewall rule:', err); - this.showToast('Error', 'Failed to delete rule', 'error'); - } - } - - async loadDHCPLeases() { - try { - const [status, result] = await this.ubusCall('luci-rpc', 'getDHCPLeases', {}).catch(() => [1, null]); - const tbody = document.querySelector('#dhcp-leases-table tbody'); - - if (!result || !result.dhcp_leases || result.dhcp_leases.length === 0) { - tbody.innerHTML = - 'No active leases'; - } else { - const rows = result.dhcp_leases - .map(lease => { - const expires = lease.expires ? `${Math.floor(lease.expires / 60)}m` : 'Static'; - return ` - - ${this.escapeHtml(lease.hostname || 'Unknown')} - ${this.escapeHtml(lease.ipaddr || 'Unknown')} - ${this.escapeHtml(lease.macaddr || 'Unknown')} - ${expires} - - `; - }) - .join(''); - tbody.innerHTML = rows; - } - - await this.loadStaticLeases(); - } catch (err) { - console.error('Failed to load DHCP leases:', err); - } - } - - async loadStaticLeases() { - try { - const [status, config] = await this.ubusCall('uci', 'get', { - config: 'dhcp' - }); - - const tbody = document.querySelector('#dhcp-static-table tbody'); - const rows = []; - - if (!config || !config.values) { - tbody.innerHTML = - 'No static leases'; - return; - } - - for (const [section, sectionData] of Object.entries(config.values)) { - if (sectionData['.type'] === 'host') { - const name = sectionData.name || section; - const mac = sectionData.mac || 'N/A'; - const ip = sectionData.ip || 'N/A'; - - rows.push(` - - ${this.escapeHtml(name)} - ${this.escapeHtml(mac)} - ${this.escapeHtml(ip)} - - Edit | - Delete - - - `); - } - } - - if (rows.length === 0) { - tbody.innerHTML = - 'No static leases'; - } else { - tbody.innerHTML = rows.join(''); - - document.querySelectorAll('#dhcp-static-table .action-link').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const section = e.target.dataset.staticLeaseSection; - this.openStaticLease(section); - }); - }); - - document.querySelectorAll('#dhcp-static-table .action-link-danger').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const section = e.target.dataset.staticLeaseDelete; - this.deleteStaticLease(section); - }); - }); - } - } catch (err) { - console.error('Failed to load static leases:', err); - const tbody = document.querySelector('#dhcp-static-table tbody'); - tbody.innerHTML = - 'Failed to load static leases'; - } - } - - async openStaticLease(section = null) { - try { - if (section) { - const [status, config] = await this.ubusCall('uci', 'get', { - config: 'dhcp', - section: section - }); - - const values = config.values; - document.getElementById('edit-static-lease-section').value = section; - document.getElementById('edit-static-lease-name').value = values.name || ''; - document.getElementById('edit-static-lease-mac').value = values.mac || ''; - document.getElementById('edit-static-lease-ip').value = values.ip || ''; - } else { - document.getElementById('edit-static-lease-section').value = ''; - document.getElementById('edit-static-lease-name').value = ''; - document.getElementById('edit-static-lease-mac').value = ''; - document.getElementById('edit-static-lease-ip').value = ''; - } - - document.getElementById('static-lease-modal').classList.remove('hidden'); - } catch (err) { - console.error('Failed to load static lease:', err); - this.showToast('Error', 'Failed to load lease configuration', 'error'); - } - } - - closeStaticLease() { - document.getElementById('static-lease-modal').classList.add('hidden'); - } - - async saveStaticLease() { - try { - const section = document.getElementById('edit-static-lease-section').value; - const name = document.getElementById('edit-static-lease-name').value; - const mac = document.getElementById('edit-static-lease-mac').value; - const ip = document.getElementById('edit-static-lease-ip').value; - - if (!mac || !ip) { - this.showToast('Error', 'MAC address and IP address are required', 'error'); - return; - } - - const values = { name: name || mac, mac, ip }; - - if (section) { - await this.ubusCall('uci', 'set', { - config: 'dhcp', - section: section, - values: values - }); - } else { - await this.ubusCall('uci', 'add', { - config: 'dhcp', - type: 'host', - name: name || mac, - values: values - }); - } - - await this.ubusCall('uci', 'commit', { - config: 'dhcp' - }); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/dnsmasq', - params: ['reload'] - }); - - this.showToast('Success', 'Static DHCP lease saved', 'success'); - this.closeStaticLease(); - setTimeout(() => this.loadStaticLeases(), 2000); - } catch (err) { - console.error('Failed to save static lease:', err); - this.showToast('Error', 'Failed to save lease', 'error'); - } - } - - async deleteStaticLease(section) { - if (!confirm('Delete this static DHCP lease?')) return; - - try { - await this.ubusCall('uci', 'delete', { - config: 'dhcp', - section: section - }); - - await this.ubusCall('uci', 'commit', { - config: 'dhcp' - }); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/dnsmasq', - params: ['reload'] - }); - - this.showToast('Success', 'Static lease deleted', 'success'); - setTimeout(() => this.loadStaticLeases(), 2000); - } catch (err) { - console.error('Failed to delete static lease:', err); - this.showToast('Error', 'Failed to delete lease', 'error'); - } - } - - async loadDNS() { - await this.loadDNSEntries(); - await this.loadHostsEntries(); - } - - async loadDNSEntries() { - try { - const [status, result] = await this.uciGet('dhcp'); - const tbody = document.querySelector('#dns-entries-table tbody'); - - if (status !== 0 || !result || !result.values) { - this.renderEmptyTable(tbody, 3, 'No DNS entries'); - return; - } - - const domains = []; - for (const [section, config] of Object.entries(result.values)) { - if (config['.type'] === 'domain' && config.name && config.ip) { - domains.push({ section, name: config.name, ip: config.ip }); - } - } - - if (domains.length === 0) { - this.renderEmptyTable(tbody, 3, 'No DNS entries'); - return; - } - - tbody.innerHTML = domains - .map( - d => ` - - ${this.escapeHtml(d.name)} - ${this.escapeHtml(d.ip)} - - - - - - ` - ) - .join(''); - } catch (err) { - console.error('Failed to load DNS entries:', err); - this.showToast('Error', 'Failed to load DNS entries', 'error'); - } - } - - editDNSEntry(section, name, ip) { - document.getElementById('edit-dns-entry-section').value = section; - document.getElementById('edit-dns-hostname').value = name; - document.getElementById('edit-dns-ip').value = ip; - document.getElementById('dns-entry-modal').classList.remove('hidden'); - } - - async saveDNSEntry() { - try { - const section = document.getElementById('edit-dns-entry-section').value; - const hostname = document.getElementById('edit-dns-hostname').value.trim(); - const ip = document.getElementById('edit-dns-ip').value.trim(); - - if (!hostname || !ip) { - this.showToast('Error', 'Please fill all fields', 'error'); - return; - } - - const values = { name: hostname, ip }; - - if (section) { - await this.uciSet('dhcp', section, values); - } else { - await this.uciAdd('dhcp', 'domain', 'cfg_dns_' + Date.now(), values); - } - - await this.uciCommit('dhcp'); - await this.serviceReload('dnsmasq'); - - this.closeModal('dns-entry-modal'); - this.showToast('Success', 'DNS entry saved', 'success'); - await this.loadDNSEntries(); - } catch (err) { - console.error('Failed to save DNS entry:', err); - this.showToast('Error', 'Failed to save DNS entry', 'error'); - } - } - - async deleteDNSEntry(section) { - if (!confirm('Delete this DNS entry?')) return; - - try { - await this.uciDelete('dhcp', section); - await this.uciCommit('dhcp'); - await this.serviceReload('dnsmasq'); - - this.showToast('Success', 'DNS entry deleted', 'success'); - await this.loadDNSEntries(); - } catch (err) { - console.error('Failed to delete DNS entry:', err); - this.showToast('Error', 'Failed to delete DNS entry', 'error'); - } - } - - async loadHostsEntries() { - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/hosts' - }); - - const tbody = document.querySelector('#hosts-table tbody'); - - if (status !== 0 || !result || !result.data) { - tbody.innerHTML = - 'No host entries'; - return; - } - - const lines = result.data.split('\n'); - const hosts = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line || line.startsWith('#')) continue; - - const parts = line.split(/\s+/); - if (parts.length >= 2) { - hosts.push({ - index: i, - ip: parts[0], - names: parts.slice(1).join(' ') - }); - } - } - - if (hosts.length === 0) { - tbody.innerHTML = - 'No host entries'; - return; - } - - tbody.innerHTML = hosts - .map( - h => ` - - ${this.escapeHtml(h.ip)} - ${this.escapeHtml(h.names)} - - - - - - ` - ) - .join(''); - } catch (err) { - console.error('Failed to load hosts entries:', err); - this.showToast('Error', 'Failed to load hosts entries', 'error'); - } - } - - editHostEntry(index, ip, names) { - document.getElementById('edit-host-entry-index').value = index; - document.getElementById('edit-host-ip').value = ip; - document.getElementById('edit-host-names').value = names; - document.getElementById('host-entry-modal').classList.remove('hidden'); - } - - async saveHostEntry() { - try { - const index = document.getElementById('edit-host-entry-index').value; - const ip = document.getElementById('edit-host-ip').value.trim(); - const names = document.getElementById('edit-host-names').value.trim(); - - if (!ip || !names) { - this.showToast('Error', 'Please fill all fields', 'error'); - return; - } - - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/hosts' - }); - - let lines = result && result.data ? result.data.split('\n') : []; - - if (index !== '') { - lines[parseInt(index)] = `${ip}\t${names}`; - } else { - lines.push(`${ip}\t${names}`); - } - - await this.ubusCall('file', 'write', { - path: '/etc/hosts', - data: lines.join('\n') - }); - - document.getElementById('host-entry-modal').classList.add('hidden'); - this.showToast('Success', 'Host entry saved', 'success'); - await this.loadHostsEntries(); - } catch (err) { - console.error('Failed to save host entry:', err); - this.showToast('Error', 'Failed to save host entry', 'error'); - } - } - - async deleteHostEntry(index) { - if (!confirm('Delete this host entry?')) return; - - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/hosts' - }); - - if (status !== 0 || !result || !result.data) { - this.showToast('Error', 'Failed to read hosts file', 'error'); - return; - } - - const lines = result.data.split('\n'); - lines.splice(index, 1); - - await this.ubusCall('file', 'write', { - path: '/etc/hosts', - data: lines.join('\n') - }); - - this.showToast('Success', 'Host entry deleted', 'success'); - await this.loadHostsEntries(); - } catch (err) { - console.error('Failed to delete host entry:', err); - this.showToast('Error', 'Failed to delete host entry', 'error'); - } - } - - async loadDDNS() { - try { - const [status, config] = await this.uciGet('ddns'); - const tbody = document.querySelector('#ddns-table tbody'); - - if (!config || !config.values) { - this.renderEmptyTable(tbody, 6, 'No DDNS services configured'); - return; - } - - const rows = []; - for (const [section, data] of Object.entries(config.values)) { - if (data['.type'] === 'service') { - const name = data.name || section; - const hostname = data.lookup_host || 'N/A'; - const service = data.service_name || 'custom'; - const enabled = data.enabled === '1'; - const status = this.renderStatusBadge(enabled); - - rows.push(` - - ${this.escapeHtml(name)} - ${this.escapeHtml(hostname)} - ${this.escapeHtml(service)} - - - ${status} - - - - - - `); - } - } - - if (rows.length === 0) { - this.renderEmptyTable(tbody, 6, 'No DDNS services configured'); - } else { - tbody.innerHTML = rows.join(''); - } - } catch (err) { - console.error('Failed to load DDNS:', err); - this.renderEmptyTable(document.querySelector('#ddns-table tbody'), 6, 'Failed to load DDNS services'); - } - } - - async editDDNS(section) { - try { - const [status, data] = await this.uciGet('ddns', section); - - if (status === 0 && data && data.values) { - const values = data.values; - document.getElementById('edit-ddns-section').value = section; - document.getElementById('edit-ddns-name').value = values.name || ''; - document.getElementById('edit-ddns-service').value = values.service_name || 'dyndns.org'; - document.getElementById('edit-ddns-hostname').value = values.lookup_host || ''; - document.getElementById('edit-ddns-username').value = values.username || ''; - document.getElementById('edit-ddns-password').value = values.password || ''; - document.getElementById('edit-ddns-check-interval').value = values.check_interval || '10'; - document.getElementById('edit-ddns-enabled').value = values.enabled || '1'; - this.openModal('ddns-modal'); - } - } catch (err) { - console.error('Failed to load DDNS service:', err); - this.showToast('Error', 'Failed to load service', 'error'); - } - } - - async saveDDNS() { - try { - const section = document.getElementById('edit-ddns-section').value; - const name = document.getElementById('edit-ddns-name').value.trim(); - const service = document.getElementById('edit-ddns-service').value; - const hostname = document.getElementById('edit-ddns-hostname').value.trim(); - const username = document.getElementById('edit-ddns-username').value.trim(); - const password = document.getElementById('edit-ddns-password').value.trim(); - const interval = document.getElementById('edit-ddns-check-interval').value; - const enabled = document.getElementById('edit-ddns-enabled').value; - - if (!name || !hostname) { - this.showToast('Error', 'Please provide service name and hostname', 'error'); - return; - } - - const values = { - name, - service_name: service, - lookup_host: hostname, - enabled, - check_interval: interval, - use_ipv6: '0', - interface: 'wan' - }; - - if (username) values.username = username; - if (password) values.password = password; - - if (section) { - await this.uciSet('ddns', section, values); - } else { - await this.uciAdd('ddns', 'service', 'cfg_ddns_' + Date.now(), values); - } - - await this.uciCommit('ddns'); - await this.serviceReload('ddns'); - - this.closeModal('ddns-modal'); - this.showToast('Success', 'DDNS service saved', 'success'); - await this.loadDDNS(); - } catch (err) { - console.error('Failed to save DDNS service:', err); - this.showToast('Error', 'Failed to save service', 'error'); - } - } - - async deleteDDNS(section) { - if (!confirm('Delete this DDNS service?')) return; - - try { - await this.uciDelete('ddns', section); - await this.uciCommit('ddns'); - await this.serviceReload('ddns'); - - this.showToast('Success', 'DDNS service deleted', 'success'); - await this.loadDDNS(); - } catch (err) { - console.error('Failed to delete DDNS service:', err); - this.showToast('Error', 'Failed to delete service', 'error'); - } - } - - async loadQoS() { - await this.loadQoSConfig(); - await this.loadQoSRules(); - } - - async loadQoSConfig() { - try { - const [status, config] = await this.uciGet('qos'); - if (status === 0 && config && config.values && config.values.wan) { - const wan = config.values.wan; - document.getElementById('qos-enabled').value = wan.enabled || '0'; - document.getElementById('qos-download').value = wan.download || ''; - document.getElementById('qos-upload').value = wan.upload || ''; - } - } catch (err) { - console.error('Failed to load QoS config:', err); - } - } - - async saveQoSConfig() { - try { - const enabled = document.getElementById('qos-enabled').value; - const download = document.getElementById('qos-download').value; - const upload = document.getElementById('qos-upload').value; - - await this.uciSet('qos', 'wan', { enabled, download, upload, classgroup: 'Default' }); - await this.uciCommit('qos'); - if (enabled === '1') { - await this.serviceReload('qos'); - } - - this.showToast('Success', 'QoS configuration saved', 'success'); - } catch (err) { - console.error('Failed to save QoS config:', err); - this.showToast('Error', 'Failed to save configuration', 'error'); - } - } - - async loadQoSRules() { - try { - const [status, config] = await this.uciGet('qos'); - const tbody = document.querySelector('#qos-rules-table tbody'); - - if (!config || !config.values) { - this.renderEmptyTable(tbody, 6, 'No QoS rules'); - return; - } - - const rows = []; - for (const [section, data] of Object.entries(config.values)) { - if (data['.type'] === 'classify') { - rows.push(` - - ${this.escapeHtml(data.target || section)} - ${this.escapeHtml(data.priority || 'Normal')} - ${this.escapeHtml(data.proto || 'any')} - ${this.escapeHtml(data.ports || 'any')} - ${this.escapeHtml(data.srchost || 'any')} - - - - - - `); - } - } - - tbody.innerHTML = rows.length - ? rows.join('') - : 'No QoS rules'; - } catch (err) { - console.error('Failed to load QoS rules:', err); - this.renderEmptyTable(document.querySelector('#qos-rules-table tbody'), 6, 'Failed to load rules'); - } - } - - async editQoSRule(section) { - try { - const [status, data] = await this.uciGet('qos', section); - if (status === 0 && data && data.values) { - const v = data.values; - document.getElementById('edit-qos-rule-section').value = section; - document.getElementById('edit-qos-rule-name').value = v.target || ''; - document.getElementById('edit-qos-rule-priority').value = v.priority || 'Normal'; - document.getElementById('edit-qos-rule-proto').value = v.proto || ''; - document.getElementById('edit-qos-rule-ports').value = v.ports || ''; - document.getElementById('edit-qos-rule-srchost').value = v.srchost || ''; - this.openModal('qos-rule-modal'); - } - } catch (err) { - this.showToast('Error', 'Failed to load rule', 'error'); - } - } - - async saveQoSRule() { - try { - const section = document.getElementById('edit-qos-rule-section').value; - const name = document.getElementById('edit-qos-rule-name').value.trim(); - const priority = document.getElementById('edit-qos-rule-priority').value; - const proto = document.getElementById('edit-qos-rule-proto').value; - const ports = document.getElementById('edit-qos-rule-ports').value.trim(); - const srchost = document.getElementById('edit-qos-rule-srchost').value.trim(); - - if (!name) { - this.showToast('Error', 'Please provide a rule name', 'error'); - return; - } - - const values = { target: name, priority }; - if (proto) values.proto = proto; - if (ports) values.ports = ports; - if (srchost) values.srchost = srchost; - - if (section) { - await this.uciSet('qos', section, values); - } else { - await this.uciAdd('qos', 'classify', 'cfg_qos_' + Date.now(), values); - } - - await this.uciCommit('qos'); - await this.serviceReload('qos'); - - this.closeModal('qos-rule-modal'); - this.showToast('Success', 'QoS rule saved', 'success'); - await this.loadQoSRules(); - } catch (err) { - this.showToast('Error', 'Failed to save rule', 'error'); - } - } - - async deleteQoSRule(section) { - if (!confirm('Delete this QoS rule?')) return; - try { - await this.uciDelete('qos', section); - await this.uciCommit('qos'); - await this.serviceReload('qos'); - this.showToast('Success', 'QoS rule deleted', 'success'); - await this.loadQoSRules(); - } catch (err) { - this.showToast('Error', 'Failed to delete rule', 'error'); - } - } - - async loadWireGuard() { - await this.loadWireGuardConfig(); - await this.loadWireGuardPeers(); - } - - async loadWireGuardConfig() { - try { - const [statusNet, configNet] = await this.uciGet('network'); - const [statusWg, configWg] = await this.uciGet('network', 'wg0'); - - if (statusWg === 0 && configWg && configWg.values) { - const wg = configWg.values; - document.getElementById('wg-interface').value = 'wg0'; - document.getElementById('wg-port').value = wg.listen_port || '51820'; - document.getElementById('wg-private-key').value = wg.private_key || ''; - document.getElementById('wg-address').value = wg.addresses ? wg.addresses[0] : '10.0.0.1/24'; - document.getElementById('wg-enabled').value = wg.auto === '0' ? '0' : '1'; - - if (wg.private_key) { - const [status, result] = await this.ubusCall('file', 'exec', { - command: 'echo', - params: [wg.private_key, '|', 'wg', 'pubkey'] - }); - if (status === 0 && result.stdout) { - document.getElementById('wg-public-key').value = result.stdout.trim(); - } - } - } else { - document.getElementById('wg-interface').value = 'wg0'; - document.getElementById('wg-port').value = '51820'; - document.getElementById('wg-address').value = '10.0.0.1/24'; - document.getElementById('wg-enabled').value = '0'; - document.getElementById('wg-private-key').value = ''; - document.getElementById('wg-public-key').value = ''; - } - } catch (err) { - console.error('Failed to load WireGuard config:', err); - } - } - - async generateWireGuardKeys() { - try { - const [status, result] = await this.ubusCall('file', 'exec', { - command: 'wg', - params: ['genkey'] - }); - - if (status === 0 && result.stdout) { - const privateKey = result.stdout.trim(); - document.getElementById('wg-private-key').value = privateKey; - - const [pubStatus, pubResult] = await this.ubusCall('file', 'exec', { - command: 'echo', - params: [privateKey, '|', 'wg', 'pubkey'] - }); - - if (pubStatus === 0 && pubResult.stdout) { - document.getElementById('wg-public-key').value = pubResult.stdout.trim(); - } - - this.showToast('Success', 'Keys generated', 'success'); - } else { - this.showToast('Error', 'Failed to generate keys', 'error'); - } - } catch (err) { - console.error('Failed to generate WireGuard keys:', err); - this.showToast('Error', 'Failed to generate keys', 'error'); - } - } - - async saveWireGuardConfig() { - try { - const enabled = document.getElementById('wg-enabled').value; - const port = document.getElementById('wg-port').value; - const privateKey = document.getElementById('wg-private-key').value.trim(); - const address = document.getElementById('wg-address').value.trim(); - - if (!privateKey || !address) { - this.showToast('Error', 'Private key and address required', 'error'); - return; - } - - const values = { - proto: 'wireguard', - private_key: privateKey, - listen_port: port, - addresses: [address], - auto: enabled - }; - - await this.uciSet('network', 'wg0', values); - await this.uciCommit('network'); - - if (enabled === '1') { - await this.serviceReload('network'); - } - - this.showToast('Success', 'WireGuard configuration saved', 'success'); - } catch (err) { - console.error('Failed to save WireGuard config:', err); - this.showToast('Error', 'Failed to save configuration', 'error'); - } - } - - async loadWireGuardPeers() { - try { - const [status, config] = await this.uciGet('network'); - const tbody = document.querySelector('#wg-peers-table tbody'); - - if (!config || !config.values) { - this.renderEmptyTable(tbody, 6, 'No WireGuard peers configured'); - return; - } - - const rows = []; - for (const [section, data] of Object.entries(config.values)) { - if (data['.type'] === 'wireguard_wg0') { - const name = data.description || section; - const publicKey = data.public_key || 'N/A'; - const allowedIps = data.allowed_ips ? data.allowed_ips.join(', ') : 'N/A'; - const endpoint = data.endpoint_host ? `${data.endpoint_host}:${data.endpoint_port}` : 'N/A'; - const status = 'CONFIGURED'; - - rows.push(` - - ${this.escapeHtml(name)} - ${this.escapeHtml(publicKey.substring(0, 20))}... - ${this.escapeHtml(allowedIps)} - ${this.escapeHtml(endpoint)} - ${status} - - - - - - `); - } - } - - if (rows.length === 0) { - this.renderEmptyTable(tbody, 6, 'No WireGuard peers configured'); - } else { - tbody.innerHTML = rows.join(''); - } - } catch (err) { - console.error('Failed to load WireGuard peers:', err); - this.renderEmptyTable(document.querySelector('#wg-peers-table tbody'), 6, 'Failed to load peers'); - } - } - - async editWireGuardPeer(section) { - try { - const [status, config] = await this.uciGet('network', section); - if (status === 0 && config && config.values) { - const peer = config.values; - document.getElementById('edit-wg-peer-section').value = section; - document.getElementById('edit-wg-peer-name').value = peer.description || ''; - document.getElementById('edit-wg-peer-public-key').value = peer.public_key || ''; - document.getElementById('edit-wg-peer-allowed-ips').value = peer.allowed_ips - ? peer.allowed_ips.join(', ') - : ''; - document.getElementById('edit-wg-peer-keepalive').value = peer.persistent_keepalive || '25'; - document.getElementById('edit-wg-peer-preshared-key').value = peer.preshared_key || ''; - this.openModal('wg-peer-modal'); - } - } catch (err) { - console.error('Failed to load peer:', err); - this.showToast('Error', 'Failed to load peer', 'error'); - } - } - - async saveWireGuardPeer() { - try { - const section = document.getElementById('edit-wg-peer-section').value; - const name = document.getElementById('edit-wg-peer-name').value.trim(); - const publicKey = document.getElementById('edit-wg-peer-public-key').value.trim(); - const allowedIps = document.getElementById('edit-wg-peer-allowed-ips').value.trim(); - const keepalive = document.getElementById('edit-wg-peer-keepalive').value; - const presharedKey = document.getElementById('edit-wg-peer-preshared-key').value.trim(); - - if (!name || !publicKey || !allowedIps) { - this.showToast('Error', 'Name, public key, and allowed IPs required', 'error'); - return; - } - - const values = { - description: name, - public_key: publicKey, - allowed_ips: allowedIps.split(',').map(ip => ip.trim()), - persistent_keepalive: keepalive, - route_allowed_ips: '1' - }; - - if (presharedKey) { - values.preshared_key = presharedKey; - } - - if (section) { - await this.uciSet('network', section, values); - } else { - await this.uciAdd('network', 'wireguard_wg0', 'wgpeer_' + Date.now(), values); - } - - await this.uciCommit('network'); - await this.serviceReload('network'); - - this.closeModal('wg-peer-modal'); - this.showToast('Success', 'WireGuard peer saved', 'success'); - await this.loadWireGuardPeers(); - } catch (err) { - console.error('Failed to save peer:', err); - this.showToast('Error', 'Failed to save peer', 'error'); - } - } - - async deleteWireGuardPeer(section) { - if (!confirm('Delete this WireGuard peer?')) return; - try { - await this.uciDelete('network', section); - await this.uciCommit('network'); - await this.serviceReload('network'); - this.showToast('Success', 'Peer deleted', 'success'); - await this.loadWireGuardPeers(); - } catch (err) { - console.error('Failed to delete peer:', err); - this.showToast('Error', 'Failed to delete peer', 'error'); - } - } - - async loadServices() { - try { - const [status, result] = await this.ubusCall('file', 'exec', { - command: '/bin/ls', - params: ['/etc/init.d'] - }); - - const tbody = document.querySelector('#services-table tbody'); - - if (!result || !result.stdout) { - tbody.innerHTML = - 'Failed to load services'; - return; - } - - const services = result.stdout - .trim() - .split('\n') - .filter(s => s && !s.startsWith('README') && !s.includes('rcS') && !s.includes('rc.') && s !== 'boot') - .sort(); - - const rows = await Promise.all( - services.map(async service => { - const enabled = await this.isServiceEnabled(service); - const running = await this.isServiceRunning(service); - - const statusBadge = running - ? 'RUNNING' - : 'STOPPED'; - - const enabledBadge = enabled - ? 'YES' - : 'NO'; - - return ` - - ${this.escapeHtml(service)} - ${statusBadge} - ${enabledBadge} - - Start | - Stop | - Restart | - ${enabled ? 'Disable' : 'Enable'} - - - `; - }) - ); - - tbody.innerHTML = rows.join(''); - - document.querySelectorAll('#services-table .action-link').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const service = e.target.dataset.service; - const action = e.target.dataset.action; - this.manageService(service, action); - }); - }); - } catch (err) { - console.error('Failed to load services:', err); - const tbody = document.querySelector('#services-table tbody'); - tbody.innerHTML = - 'Failed to load services'; - } - } - - async isServiceEnabled(service) { - try { - const [status, result] = await this.ubusCall('file', 'exec', { - command: '/etc/init.d/' + service, - params: ['enabled'] - }); - return result && result.code === 0; - } catch { - return false; - } - } - - async isServiceRunning(service) { - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/var/run/' + service + '.pid' - }); - return result && result.data; - } catch { - return false; - } - } - - async manageService(service, action) { - try { - this.showToast('Info', `${action}ing ${service}...`, 'info'); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/' + service, - params: [action] - }); - - this.showToast('Success', `Service ${action} completed`, 'success'); - setTimeout(() => this.loadServices(), 2000); - } catch (err) { - console.error('Failed to manage service:', err); - this.showToast('Error', `Failed to ${action} service`, 'error'); - } - } - - async loadPackages() { - const tbody = document.querySelector('#packages-table tbody'); - try { - let status, result; - - const paths = ['/usr/lib/opkg/status', '/var/lib/opkg/status']; - - for (const path of paths) { - [status, result] = await this.ubusCall('file', 'read', { path }); - - if (status === 0 && result && result.data) { - const packages = []; - const entries = result.data.split('\n\n'); - for (const entry of entries) { - const nameMatch = entry.match(/^Package: (.+)$/m); - const versionMatch = entry.match(/^Version: (.+)$/m); - if (nameMatch && versionMatch) { - packages.push({ - name: nameMatch[1], - version: versionMatch[1] - }); - } - } - - if (packages.length === 0) continue; - - packages.sort((a, b) => a.name.localeCompare(b.name)); - - const rows = packages - .map( - pkg => ` - - ${this.escapeHtml(pkg.name)} - ${this.escapeHtml(pkg.version)} - - Remove - - - ` - ) - .join(''); - - tbody.innerHTML = rows; - - document.querySelectorAll('#packages-table .action-link-danger').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const pkg = e.target.dataset.package; - this.removePackage(pkg); - }); - }); - return; - } - } - - tbody.innerHTML = ` - - -
Package viewing requires ACL configuration. Run these commands:
- - scp rpcd-acl.json root@192.168.1.1:/usr/share/rpcd/acl.d/moci.json
- ssh root@192.168.1.1 "/etc/init.d/rpcd restart" -
- - - `; - } catch (err) { - console.error('Failed to load packages:', err); - tbody.innerHTML = ` - - -
Package viewing requires ACL configuration. Run these commands:
- - scp rpcd-acl.json root@192.168.1.1:/usr/share/rpcd/acl.d/moci.json
- ssh root@192.168.1.1 "/etc/init.d/rpcd restart" -
- - - `; - } - } - - async removePackage(pkg) { - if (!confirm(`Remove package ${pkg}? This may break dependencies.`)) return; - - try { - this.showToast('Info', `Removing ${pkg}...`, 'info'); - - await this.ubusCall('file', 'exec', { - command: '/bin/opkg', - params: ['remove', pkg] - }); - - this.showToast('Success', `Package ${pkg} removed`, 'success'); - setTimeout(() => this.loadPackages(), 2000); - } catch (err) { - console.error('Failed to remove package:', err); - this.showToast('Error', 'Failed to remove package', 'error'); - } - } - - async loadCronJobs() { - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/crontabs/root' - }); - - const tbody = document.querySelector('#cron-table tbody'); - - if (!result || !result.data) { - tbody.innerHTML = - 'No cron jobs configured'; - return; - } - - const crontab = result.data; - const lines = crontab.split('\n').filter(l => l.trim() && !l.startsWith('#')); - - if (lines.length === 0) { - tbody.innerHTML = - 'No cron jobs configured'; - return; - } - - const rows = lines - .map((line, idx) => { - const disabled = line.trim().startsWith('#'); - const actualLine = disabled ? line.trim().substring(1) : line; - const parts = actualLine.trim().split(/\s+/); - const schedule = parts.slice(0, 5).join(' '); - const command = parts.slice(5).join(' '); - - return ` - - ${this.escapeHtml(schedule)} - ${this.escapeHtml(command)} - ${disabled ? 'No' : 'Yes'} - - Edit - Delete - - - `; - }) - .join(''); - - tbody.innerHTML = rows; - - document.querySelectorAll('#cron-table .action-link').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const idx = parseInt(e.target.dataset.cronIdx); - this.openCronJob(idx); - }); - }); - - document.querySelectorAll('#cron-table .action-link-danger').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const idx = parseInt(e.target.dataset.cronIdx); - this.deleteCronJob(idx); - }); - }); - } catch (err) { - console.error('Failed to load cron jobs:', err); - document.querySelector('#cron-table tbody').innerHTML = - 'Failed to load cron jobs'; - } - } - - openCronJob(index = null) { - if (index !== null) { - this.ubusCall('file', 'read', { path: '/etc/crontabs/root' }).then(([status, result]) => { - if (result && result.data) { - const crontab = result.data; - const lines = crontab.split('\n').filter(l => l.trim() && !l.startsWith('#')); - const line = lines[index]; - - if (line) { - const disabled = line.trim().startsWith('#'); - const actualLine = disabled ? line.trim().substring(1) : line; - const parts = actualLine.trim().split(/\s+/); - - document.getElementById('edit-cron-minute').value = parts[0] || '*'; - document.getElementById('edit-cron-hour').value = parts[1] || '*'; - document.getElementById('edit-cron-day').value = parts[2] || '*'; - document.getElementById('edit-cron-month').value = parts[3] || '*'; - document.getElementById('edit-cron-weekday').value = parts[4] || '*'; - document.getElementById('edit-cron-command').value = parts.slice(5).join(' '); - document.getElementById('edit-cron-enabled').checked = !disabled; - document.getElementById('edit-cron-index').value = index; - } - } - }); - } else { - document.getElementById('edit-cron-minute').value = '*'; - document.getElementById('edit-cron-hour').value = '*'; - document.getElementById('edit-cron-day').value = '*'; - document.getElementById('edit-cron-month').value = '*'; - document.getElementById('edit-cron-weekday').value = '*'; - document.getElementById('edit-cron-command').value = ''; - document.getElementById('edit-cron-enabled').checked = true; - document.getElementById('edit-cron-index').value = ''; - } - - document.getElementById('cron-modal').classList.remove('hidden'); - } - - closeCronJob() { - document.getElementById('cron-modal').classList.add('hidden'); - } - - async saveCronJob() { - try { - const minute = document.getElementById('edit-cron-minute').value.trim() || '*'; - const hour = document.getElementById('edit-cron-hour').value.trim() || '*'; - const day = document.getElementById('edit-cron-day').value.trim() || '*'; - const month = document.getElementById('edit-cron-month').value.trim() || '*'; - const weekday = document.getElementById('edit-cron-weekday').value.trim() || '*'; - const command = document.getElementById('edit-cron-command').value.trim(); - const enabled = document.getElementById('edit-cron-enabled').checked; - const index = document.getElementById('edit-cron-index').value; - - if (!command) { - this.showToast('Error', 'Command is required', 'error'); - return; - } - - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/crontabs/root' - }); - - let lines = []; - if (result && result.data) { - const crontab = result.data; - lines = crontab.split('\n').filter(l => l.trim() && !l.startsWith('#')); - } - - const cronLine = `${minute} ${hour} ${day} ${month} ${weekday} ${command}`; - const finalLine = enabled ? cronLine : `# ${cronLine}`; - - if (index !== '') { - lines[parseInt(index)] = finalLine; - } else { - lines.push(finalLine); - } - - const newCrontab = lines.join('\n') + '\n'; - - await this.ubusCall('file', 'write', { - path: '/etc/crontabs/root', - data: btoa(newCrontab), - base64: true - }); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/cron', - params: ['restart'] - }); - - this.showToast('Success', 'Cron job saved', 'success'); - this.closeCronJob(); - setTimeout(() => this.loadCronJobs(), 1000); - } catch (err) { - console.error('Failed to save cron job:', err); - this.showToast('Error', 'Failed to save cron job', 'error'); - } - } - - async deleteCronJob(index) { - if (!confirm('Delete this cron job?')) return; - - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/crontabs/root' - }); - - if (!result || !result.data) { - this.showToast('Error', 'Failed to read crontab', 'error'); - return; - } - - const crontab = result.data; - let lines = crontab.split('\n').filter(l => l.trim() && !l.startsWith('#')); - lines.splice(index, 1); - - const newCrontab = lines.join('\n') + '\n'; - - await this.ubusCall('file', 'write', { - path: '/etc/crontabs/root', - data: btoa(newCrontab), - base64: true - }); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/cron', - params: ['restart'] - }); - - this.showToast('Success', 'Cron job deleted', 'success'); - setTimeout(() => this.loadCronJobs(), 1000); - } catch (err) { - console.error('Failed to delete cron job:', err); - this.showToast('Error', 'Failed to delete cron job', 'error'); - } - } - - async loadSSHKeys() { - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/dropbear/authorized_keys' - }); - - const tbody = document.querySelector('#ssh-keys-table tbody'); - - if (!result || !result.data) { - tbody.innerHTML = - 'No SSH keys configured'; - return; - } - - const keys = result.data; - const lines = keys.split('\n').filter(l => l.trim() && !l.startsWith('#')); - - if (lines.length === 0) { - tbody.innerHTML = - 'No SSH keys configured'; - return; - } - - const rows = lines - .map((line, idx) => { - const parts = line.trim().split(/\s+/); - const type = parts[0]; - const key = parts[1]; - const comment = parts.slice(2).join(' ') || ''; - const keyPreview = key.substring(0, 40) + '...'; - - return ` - - ${this.escapeHtml(type)} - ${this.escapeHtml(keyPreview)} - ${this.escapeHtml(comment)} - - Delete - - - `; - }) - .join(''); - - tbody.innerHTML = rows; - - document.querySelectorAll('#ssh-keys-table .action-link-danger').forEach(link => { - link.addEventListener('click', e => { - e.preventDefault(); - const idx = parseInt(e.target.dataset.keyIdx); - this.deleteSSHKey(idx); - }); - }); - } catch (err) { - console.error('Failed to load SSH keys:', err); - document.querySelector('#ssh-keys-table tbody').innerHTML = - 'Failed to load SSH keys'; - } - } - - openSSHKey() { - document.getElementById('ssh-key-paste-area').value = ''; - document.getElementById('parsed-keys-preview').style.display = 'none'; - document.getElementById('parsed-keys-list').innerHTML = ''; - document.getElementById('save-ssh-keys-btn').style.display = 'none'; - document.getElementById('ssh-key-modal').classList.remove('hidden'); - } - - closeSSHKey() { - document.getElementById('ssh-key-modal').classList.add('hidden'); - } - - parseSSHKeys() { - const pasteArea = document.getElementById('ssh-key-paste-area'); - const content = pasteArea.value.trim(); - - if (!content) { - this.showToast('Error', 'Please paste SSH keys', 'error'); - return; - } - - const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#')); - const validKeys = []; - const invalidLines = []; - - lines.forEach((line, idx) => { - const trimmed = line.trim(); - const match = trimmed.match( - /^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-dss)\s+([A-Za-z0-9+\/=]+)(\s+(.*))?$/ - ); - - if (match) { - const type = match[1]; - const key = match[2]; - const comment = match[4] || ''; - validKeys.push({ - type, - key, - comment, - full: trimmed - }); - } else { - invalidLines.push(idx + 1); - } - }); - - if (validKeys.length === 0) { - this.showToast('Error', 'No valid SSH keys found', 'error'); - return; - } - - const previewDiv = document.getElementById('parsed-keys-preview'); - const listDiv = document.getElementById('parsed-keys-list'); - - listDiv.innerHTML = validKeys - .map((key, idx) => { - const keyPreview = key.key.substring(0, 40) + '...'; - return ` -
- -
-
- ${this.escapeHtml(key.type)} - ${key.comment ? `${this.escapeHtml(key.comment)}` : 'no comment'} -
-
${this.escapeHtml(keyPreview)}
-
-
- `; - }) - .join(''); - - if (invalidLines.length > 0) { - listDiv.innerHTML += ` -
-
SKIPPED INVALID LINES
-
Lines: ${invalidLines.join(', ')}
-
- `; - } - - previewDiv.style.display = 'block'; - document.getElementById('save-ssh-keys-btn').style.display = 'inline-block'; - - this.parsedKeys = validKeys; - this.showToast('Success', `Parsed ${validKeys.length} valid key${validKeys.length > 1 ? 's' : ''}`, 'success'); - } - - async saveSSHKeys() { - try { - const selectedKeys = []; - this.parsedKeys.forEach((key, idx) => { - const checkbox = document.getElementById(`key-checkbox-${idx}`); - if (checkbox && checkbox.checked) { - selectedKeys.push(key.full); - } - }); - - if (selectedKeys.length === 0) { - this.showToast('Error', 'Please select at least one key to add', 'error'); - return; - } - - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/dropbear/authorized_keys' - }); - - let lines = []; - if (result && result.data) { - const keys = result.data; - lines = keys.split('\n').filter(l => l.trim() && !l.startsWith('#')); - } - - lines.push(...selectedKeys); - - const newKeys = lines.join('\n') + '\n'; - - await this.ubusCall('file', 'write', { - path: '/etc/dropbear/authorized_keys', - data: btoa(newKeys), - base64: true, - mode: '0600' - }); - - this.showToast( - 'Success', - `Added ${selectedKeys.length} SSH key${selectedKeys.length > 1 ? 's' : ''}`, - 'success' - ); - this.closeSSHKey(); - setTimeout(() => this.loadSSHKeys(), 1000); - } catch (err) { - console.error('Failed to save SSH keys:', err); - this.showToast('Error', 'Failed to save SSH keys', 'error'); - } - } - - async deleteSSHKey(index) { - if (!confirm('Delete this SSH key?')) return; - - try { - const [status, result] = await this.ubusCall('file', 'read', { - path: '/etc/dropbear/authorized_keys' - }); - - if (!result || !result.data) { - this.showToast('Error', 'Failed to read keys', 'error'); - return; - } - - const keys = result.data; - let lines = keys.split('\n').filter(l => l.trim() && !l.startsWith('#')); - lines.splice(index, 1); - - const newKeys = lines.join('\n') + '\n'; - - await this.ubusCall('file', 'write', { - path: '/etc/dropbear/authorized_keys', - data: btoa(newKeys), - base64: true, - mode: '0600' - }); - - this.showToast('Success', 'SSH key deleted', 'success'); - setTimeout(() => this.loadSSHKeys(), 1000); - } catch (err) { - console.error('Failed to delete SSH key:', err); - this.showToast('Error', 'Failed to delete SSH key', 'error'); - } - } - - formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; - } - - async loadMountPoints() { - try { - const [status, result] = await this.ubusCall('luci', 'getMountPoints', {}); - - const tbody = document.querySelector('#mounts-table tbody'); - const chartsContainer = document.getElementById('storage-charts'); - - if (status !== 0 || !result || !result.result) { - tbody.innerHTML = - 'Failed to load mount points'; - chartsContainer.innerHTML = - '
Failed to load storage data
'; - return; - } - - const mounts = result.result; - - if (!mounts || mounts.length === 0) { - tbody.innerHTML = - 'No mount points found'; - chartsContainer.innerHTML = - '
No storage data available
'; - return; - } - - const charts = mounts - .map(m => { - const size = this.formatBytes(m.size); - const used = this.formatBytes(m.size - m.avail); - const available = this.formatBytes(m.avail); - const percent = m.size > 0 ? Math.round(((m.size - m.avail) / m.size) * 100) : 0; - - let barClass = ''; - if (percent > 90) barClass = 'critical'; - else if (percent > 75) barClass = 'warning'; - - return ` -
-
${this.escapeHtml(m.device)}
-
${this.escapeHtml(m.mount)}
-
${percent}%
-
-
-
-
- ${used} used - ${available} free -
-
- `; - }) - .join(''); - - chartsContainer.innerHTML = charts; - - const rows = mounts - .map(m => { - const size = this.formatBytes(m.size); - const used = this.formatBytes(m.size - m.avail); - const available = this.formatBytes(m.avail); - const percent = m.size > 0 ? Math.round(((m.size - m.avail) / m.size) * 100) : 0; - - return ` - - ${this.escapeHtml(m.device)} - ${this.escapeHtml(m.mount)} - auto - ${size} - ${used} (${percent}%) - ${available} - - `; - }) - .join(''); - - tbody.innerHTML = rows; - } catch (err) { - console.error('Failed to load mount points:', err); - document.querySelector('#mounts-table tbody').innerHTML = - 'Failed to load mount points'; - document.getElementById('storage-charts').innerHTML = - '
Failed to load storage data
'; - } - } - - async loadLEDs() { - try { - const [status, result] = await this.ubusCall('luci', 'getLEDs', {}); - - const tbody = document.querySelector('#led-table tbody'); - - if (status !== 0 || !result) { - tbody.innerHTML = - 'Failed to load LEDs'; - return; - } - - const leds = Object.entries(result); - - if (leds.length === 0) { - tbody.innerHTML = - 'No LEDs found'; - return; - } - - const rows = leds - .map(([name, info]) => { - const trigger = info.active_trigger || 'none'; - const brightness = info.brightness || 0; - const status = brightness > 0 ? 'ON' : 'OFF'; - - return ` - - ${this.escapeHtml(name)} - ${this.escapeHtml(trigger)} - - ${status} - - - `; - }) - .join(''); - - tbody.innerHTML = rows; - } catch (err) { - console.error('Failed to load LEDs:', err); - document.querySelector('#led-table tbody').innerHTML = - 'Failed to load LEDs'; - } - } - - async runPing() { - const host = document.getElementById('ping-host').value.trim(); - if (!host) { - this.showToast('Error', 'Please enter a hostname or IP address', 'error'); - return; - } - - const output = document.getElementById('ping-output'); - output.innerHTML = '
Running ping...
'; - - try { - const [status, result] = await this.ubusCall('file', 'exec', { - command: '/bin/ping', - params: ['-c', '4', host] - }); - - if (result && result.stdout) { - const lines = result.stdout.split('\n').filter(l => l.trim()); - output.innerHTML = lines.map(l => `
${this.escapeHtml(l)}
`).join(''); - } else { - output.innerHTML = '
Ping failed or permission denied
'; - } - } catch (err) { - output.innerHTML = '
Failed to execute ping
'; - } - } - - async generateBackup() { - try { - this.showToast('Info', 'Generating backup...', 'info'); - - const [status, result] = await this.ubusCall('file', 'exec', { - command: '/sbin/sysupgrade', - params: ['-b', '/tmp/backup.tar.gz'] - }); - - const [readStatus, backupData] = await this.ubusCall('file', 'read', { - path: '/tmp/backup.tar.gz', - base64: true - }); - - if (backupData && backupData.data) { - const blob = this.base64ToBlob(backupData.data, 'application/gzip'); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `openwrt-backup-${new Date().toISOString().slice(0, 10)}.tar.gz`; - a.click(); - URL.revokeObjectURL(url); - - this.showToast('Success', 'Backup downloaded', 'success'); - } else { - this.showToast('Error', 'Failed to read backup file', 'error'); - } - } catch (err) { - console.error('Failed to generate backup:', err); - this.showToast('Error', 'Failed to generate backup', 'error'); - } - } - - base64ToBlob(base64, mimeType) { - const byteCharacters = atob(base64); - const byteArrays = []; - - for (let offset = 0; offset < byteCharacters.length; offset += 512) { - const slice = byteCharacters.slice(offset, offset + 512); - const byteNumbers = new Array(slice.length); - for (let i = 0; i < slice.length; i++) { - byteNumbers[i] = slice.charCodeAt(i); - } - const byteArray = new Uint8Array(byteNumbers); - byteArrays.push(byteArray); - } - - return new Blob(byteArrays, { type: mimeType }); - } - - async resetToDefaults() { - if ( - !confirm('Reset all settings to factory defaults? This will ERASE ALL CONFIGURATION and reboot the router.') - ) - return; - if (!confirm('Are you ABSOLUTELY SURE? This cannot be undone!')) return; - - try { - this.showToast('Warning', 'Resetting to factory defaults...', 'error'); - - await this.ubusCall('file', 'exec', { - command: '/sbin/firstboot', - params: ['-y'] - }); - - await this.ubusCall('system', 'reboot', {}); - - this.showToast('Info', 'Router is resetting and rebooting...', 'info'); - setTimeout(() => this.logout(), 2000); - } catch (err) { - console.error('Failed to reset:', err); - this.showToast('Error', 'Failed to reset to defaults', 'error'); - } - } - - async changePassword() { - const newPassword = document.getElementById('new-password').value; - const confirmPassword = document.getElementById('confirm-password').value; - - if (!newPassword || !confirmPassword) { - this.showToast('Error', 'Please enter both password fields', 'error'); - return; - } - - if (newPassword !== confirmPassword) { - this.showToast('Error', 'Passwords do not match', 'error'); - return; - } - - if (newPassword.length < 6) { - this.showToast('Error', 'Password must be at least 6 characters', 'error'); - return; - } - - try { - await this.ubusCall('file', 'exec', { - command: '/bin/sh', - params: ['-c', `echo -e "${newPassword}\\n${newPassword}" | passwd root`] - }); - - this.showToast('Success', 'Password changed successfully', 'success'); - document.getElementById('new-password').value = ''; - document.getElementById('confirm-password').value = ''; - } catch (err) { - console.error('Failed to change password:', err); - this.showToast('Error', 'Failed to change password', 'error'); - } - } - - async saveGeneralSettings() { - try { - const hostname = document.getElementById('system-hostname').value; - const timezone = document.getElementById('system-timezone').value; - - if (!hostname) { - this.showToast('Error', 'Hostname is required', 'error'); - return; - } - - await this.ubusCall('uci', 'set', { - config: 'system', - section: '@system[0]', - values: { - hostname: hostname, - timezone: timezone || 'UTC' - } - }); - - await this.ubusCall('uci', 'commit', { - config: 'system' - }); - - await this.ubusCall('file', 'exec', { - command: '/etc/init.d/system', - params: ['reload'] - }); - - this.showToast('Success', 'Settings saved successfully', 'success'); - } catch (err) { - console.error('Failed to save settings:', err); - this.showToast('Error', 'Failed to save settings', 'error'); - } - } - - async runTraceroute() { - const host = document.getElementById('traceroute-host').value.trim(); - if (!host) { - this.showToast('Error', 'Please enter a hostname or IP address', 'error'); - return; - } - - const output = document.getElementById('traceroute-output'); - output.innerHTML = '
Running traceroute...
'; - - try { - const [status, result] = await this.ubusCall('file', 'exec', { - command: '/usr/bin/traceroute', - params: ['-m', '15', host] - }); - - if (result && result.stdout) { - const lines = result.stdout.split('\n').filter(l => l.trim()); - output.innerHTML = lines.map(l => `
${this.escapeHtml(l)}
`).join(''); - } else { - output.innerHTML = '
Traceroute failed or permission denied
'; - } - } catch (err) { - output.innerHTML = '
Failed to execute traceroute
'; - } - } - - async sendWakeOnLan() { - const mac = document.getElementById('wol-mac').value.trim(); - if (!mac) { - this.showToast('Error', 'Please enter a MAC address', 'error'); - return; - } - - const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; - if (!macRegex.test(mac)) { - this.showToast('Error', 'Invalid MAC address format', 'error'); - return; - } - - const output = document.getElementById('wol-output'); - output.innerHTML = '
Sending WOL packet...
'; - - try { - const [status, result] = await this.ubusCall('file', 'exec', { - command: '/usr/bin/etherwake', - params: [mac] - }).catch(() => { - return this.ubusCall('file', 'exec', { - command: '/usr/bin/wol', - params: [mac] - }); - }); - - output.innerHTML = - '
WOL packet sent successfully to ' + - this.escapeHtml(mac) + - '
'; - this.showToast('Success', 'Wake-on-LAN packet sent', 'success'); - } catch (err) { - output.innerHTML = - '
Failed to send WOL packet. Make sure etherwake or wol package is installed.
'; - this.showToast('Error', 'Failed to send WOL packet', 'error'); - } - } - - initFirmwareUpgrade() { - const fileInput = document.getElementById('firmware-file'); - const fileUploadArea = document.getElementById('file-upload-area'); - const fileUploadText = document.getElementById('file-upload-text'); - const validateBtn = document.getElementById('validate-firmware-btn'); - const flashBtn = document.getElementById('flash-firmware-btn'); - - fileUploadArea.addEventListener('click', () => { - fileInput.click(); - }); - - fileUploadArea.addEventListener('dragover', e => { - e.preventDefault(); - fileUploadArea.style.borderColor = 'var(--neon-cyan)'; - fileUploadArea.style.background = 'rgba(0, 255, 255, 0.05)'; - }); - - fileUploadArea.addEventListener('dragleave', () => { - fileUploadArea.style.borderColor = 'var(--slate-border)'; - fileUploadArea.style.background = 'transparent'; - }); - - fileUploadArea.addEventListener('drop', e => { - e.preventDefault(); - fileUploadArea.style.borderColor = 'var(--slate-border)'; - fileUploadArea.style.background = 'transparent'; - - const files = e.dataTransfer.files; - if (files.length > 0) { - fileInput.files = files; - fileInput.dispatchEvent(new Event('change')); - } - }); - - fileInput.addEventListener('change', e => { - const file = e.target.files[0]; - if (file) { - fileUploadText.innerHTML = `Selected: ${this.escapeHtml(file.name)} (${(file.size / 1024 / 1024).toFixed(2)} MB)`; - validateBtn.disabled = false; - flashBtn.disabled = true; - document.getElementById('firmware-info').style.display = 'none'; - } - }); - - validateBtn.addEventListener('click', () => { - this.validateFirmware(); - }); - - flashBtn.addEventListener('click', () => { - this.flashFirmware(); - }); - } - - async validateFirmware() { - try { - const fileInput = document.getElementById('firmware-file'); - const file = fileInput.files[0]; - - if (!file) { - this.showToast('Error', 'Please select a firmware file', 'error'); - return; - } - - this.showToast('Info', 'Validating firmware...', 'info'); - - const reader = new FileReader(); - reader.onload = async e => { - try { - const arrayBuffer = e.target.result; - const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); - - await this.ubusCall('file', 'write', { - path: '/tmp/firmware.bin', - data: base64, - base64: true - }); - - const [status, result] = await this.ubusCall('system', 'validate_firmware_image', { - path: '/tmp/firmware.bin' - }); - - const infoDiv = document.getElementById('firmware-info'); - const detailsDiv = document.getElementById('firmware-details'); - - if (status === 0 && result && result.valid) { - detailsDiv.innerHTML = ` -
✓ Firmware image is valid
-
- ${ - result.tests - ? Object.entries(result.tests) - .map( - ([test, passed]) => - `
- ${passed ? '✓' : '✗'} ${test} -
` - ) - .join('') - : '' - } -
- `; - infoDiv.style.display = 'block'; - document.getElementById('flash-firmware-btn').disabled = false; - this.showToast('Success', 'Firmware validated successfully', 'success'); - } else { - detailsDiv.innerHTML = ` -
✗ Firmware image validation failed
- ${ - result && result.tests - ? ` -
- ${Object.entries(result.tests) - .map( - ([test, passed]) => - `
- ${passed ? '✓' : '✗'} ${test} -
` - ) - .join('')} -
- ` - : '' - } - `; - infoDiv.style.display = 'block'; - this.showToast('Error', 'Firmware validation failed', 'error'); - } - } catch (err) { - console.error('Firmware validation error:', err); - this.showToast('Error', 'Failed to validate firmware', 'error'); - } - }; - - reader.readAsArrayBuffer(file); - } catch (err) { - console.error('Failed to validate firmware:', err); - this.showToast('Error', 'Failed to validate firmware', 'error'); - } - } - - async flashFirmware() { - const keepSettings = document.getElementById('keep-settings').checked; - - if ( - !confirm( - '⚠ WARNING: This will upgrade the firmware and reboot the device.\n\n' + - (keepSettings ? 'Settings will be preserved.' : 'Settings will be reset to defaults.') + - '\n\nDo you want to continue?' - ) - ) { - return; - } - - try { - const progressDiv = document.getElementById('upgrade-progress'); - const statusDiv = document.getElementById('upgrade-status'); - - progressDiv.style.display = 'block'; - statusDiv.innerHTML = '
Starting firmware upgrade...
'; - - document.getElementById('validate-firmware-btn').disabled = true; - document.getElementById('flash-firmware-btn').disabled = true; - document.getElementById('firmware-file').disabled = true; - - const command = keepSettings - ? '/sbin/sysupgrade /tmp/firmware.bin' - : '/sbin/sysupgrade -n /tmp/firmware.bin'; - - statusDiv.innerHTML += '
Flashing firmware...
'; - statusDiv.innerHTML += - '
This may take several minutes. Do not power off the device.
'; - - await this.ubusCall('file', 'exec', { - command: '/sbin/sysupgrade', - params: keepSettings ? ['/tmp/firmware.bin'] : ['-n', '/tmp/firmware.bin'] - }); - - statusDiv.innerHTML += - '
✓ Firmware flashed successfully
'; - statusDiv.innerHTML += '
Device is rebooting...
'; - statusDiv.innerHTML += - '
The device will be available in approximately 2-3 minutes.
'; - - this.showToast('Success', 'Firmware upgrade initiated', 'success'); - - setTimeout(() => { - statusDiv.innerHTML += - '
Waiting for device to come back online...
'; - }, 5000); - } catch (err) { - console.error('Failed to flash firmware:', err); - document.getElementById('upgrade-status').innerHTML += - '
✗ Firmware upgrade failed: ' + - this.escapeHtml(err.message) + - '
'; - this.showToast('Error', 'Failed to flash firmware', 'error'); - } - } -} - -window.app = new OpenWrtApp(); diff --git a/moci/index.html b/moci/index.html index 33b65c8..6c4377c 100644 --- a/moci/index.html +++ b/moci/index.html @@ -13,7 +13,7 @@
-
+