diff --git a/Makefile b/Makefile
index 7ae2817..86d8bdc 100644
--- a/Makefile
+++ b/Makefile
@@ -38,8 +38,6 @@ define Package/moci/install
$(INSTALL_DATA) ./dist/moci/js/modules/dashboard.js $(1)/www/moci/js/modules/
$(INSTALL_DATA) ./dist/moci/js/modules/network.js $(1)/www/moci/js/modules/
$(INSTALL_DATA) ./dist/moci/js/modules/system.js $(1)/www/moci/js/modules/
- $(INSTALL_DATA) ./dist/moci/js/modules/vpn.js $(1)/www/moci/js/modules/
- $(INSTALL_DATA) ./dist/moci/js/modules/services.js $(1)/www/moci/js/modules/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./rpcd-acl.json $(1)/usr/share/rpcd/acl.d/moci.json
diff --git a/moci/app.js.bak b/moci/app.js.bak
deleted file mode 100644
index 9330c0d..0000000
--- a/moci/app.js.bak
+++ /dev/null
@@ -1,3772 +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.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/js/core.js b/moci/js/core.js
index b55ffc7..e78666e 100644
--- a/moci/js/core.js
+++ b/moci/js/core.js
@@ -144,9 +144,7 @@ export class OpenWrtCore {
return {
dashboard: './modules/dashboard.js',
network: './modules/network.js',
- system: './modules/system.js',
- vpn: './modules/vpn.js',
- services: './modules/services.js'
+ system: './modules/system.js'
};
}
@@ -178,10 +176,8 @@ export class OpenWrtCore {
shouldLoadModule(moduleName) {
const moduleFeatures = {
dashboard: ['dashboard'],
- network: ['network', 'wireless', 'firewall', 'dhcp', 'dns', 'diagnostics'],
- system: ['system', 'backup', 'packages', 'services', 'ssh_keys', 'storage', 'leds', 'firmware'],
- vpn: ['wireguard'],
- services: ['qos', 'ddns']
+ network: ['network', 'wireless', 'firewall', 'dhcp', 'dns', 'diagnostics', 'wireguard', 'qos', 'ddns'],
+ system: ['system', 'backup', 'packages', 'services', 'ssh_keys', 'storage', 'leds', 'firmware']
};
const features = moduleFeatures[moduleName] || [];
@@ -597,6 +593,135 @@ export class OpenWrtCore {
return () => container.removeEventListener('click', handler);
}
+ renderTable(tableSelector, items, colspan, emptyMsg, rowFn) {
+ const tbody = document.querySelector(`${tableSelector} tbody`);
+ if (!tbody) return;
+ if (items.length === 0) {
+ this.renderEmptyTable(tbody, colspan, emptyMsg);
+ return;
+ }
+ tbody.innerHTML = items.map(rowFn).join('');
+ }
+
+ filterUciSections(config, type) {
+ return Object.entries(config)
+ .filter(([, v]) => v['.type'] === type)
+ .map(([k, v]) => ({ section: k, ...v }));
+ }
+
+ getFormValues(fieldMap) {
+ const values = {};
+ for (const [elementId, uciKey] of Object.entries(fieldMap)) {
+ const el = document.getElementById(elementId);
+ if (!el) continue;
+ const formVal = el.type === 'checkbox' ? el.checked : el.value;
+ if (Array.isArray(uciKey)) {
+ for (const key of uciKey) values[key] = formVal;
+ } else {
+ values[uciKey] = formVal;
+ }
+ }
+ return values;
+ }
+
+ setFormValues(fieldMap, data) {
+ for (const [elementId, uciKey] of Object.entries(fieldMap)) {
+ const el = document.getElementById(elementId);
+ if (!el) continue;
+ let val;
+ if (Array.isArray(uciKey)) {
+ for (const key of uciKey) {
+ if (data[key] !== undefined && data[key] !== '') {
+ val = data[key];
+ break;
+ }
+ }
+ } else {
+ val = data[uciKey];
+ }
+ if (el.type === 'checkbox') {
+ el.checked = !!val;
+ } else {
+ el.value = Array.isArray(val) ? val.join(', ') : val || '';
+ }
+ }
+ }
+
+ async uciEdit(config, id, fieldMap, modalId, sectionIdField) {
+ try {
+ const [status, result] = await this.uciGet(config, id);
+ if (status !== 0 || !result?.values) throw new Error('Not found');
+ if (sectionIdField) document.getElementById(sectionIdField).value = id;
+ this.setFormValues(fieldMap, result.values);
+ this.openModal(modalId);
+ } catch {
+ this.showToast('Failed to load config', 'error');
+ }
+ }
+
+ async uciSave({
+ config,
+ uciType,
+ modalId,
+ sectionIdField,
+ fieldMap,
+ defaults,
+ reloadFn,
+ successMsg,
+ sectionNameField
+ }) {
+ const section = sectionIdField ? document.getElementById(sectionIdField)?.value : '';
+ const values = { ...this.getFormValues(fieldMap), ...defaults };
+ try {
+ if (section) {
+ await this.uciSet(config, section, values);
+ } else {
+ const name = sectionNameField ? document.getElementById(sectionNameField)?.value || null : null;
+ const [, res] = await this.uciAdd(config, uciType, name);
+ if (!res?.section) throw new Error('Failed to create section');
+ await this.uciSet(config, res.section, values);
+ }
+ await this.uciCommit(config);
+ this.closeModal(modalId);
+ this.showToast(successMsg || 'Saved', 'success');
+ if (reloadFn) await reloadFn();
+ } catch {
+ this.showToast('Failed to save', 'error');
+ }
+ }
+
+ async uciDeleteEntry(config, id, confirmMsg, reloadFn) {
+ if (!confirm(confirmMsg)) return;
+ try {
+ await this.uciDelete(config, id);
+ await this.uciCommit(config);
+ this.showToast('Deleted', 'success');
+ if (reloadFn) await reloadFn();
+ } catch {
+ this.showToast('Failed to delete', 'error');
+ }
+ }
+
+ spliceFileLines(raw, dataFilter, index, newLine) {
+ const lines = raw.split('\n');
+ const dataIndices = lines.map((l, i) => (dataFilter(l) ? i : -1)).filter(i => i >= 0);
+ if (index !== '' && index !== undefined) {
+ const origIdx = dataIndices[parseInt(index)];
+ if (origIdx !== undefined) {
+ if (newLine === null) {
+ lines.splice(origIdx, 1);
+ } else {
+ lines[origIdx] = newLine;
+ }
+ }
+ } else if (newLine !== null) {
+ if (lines.length && lines[lines.length - 1] === '') lines.pop();
+ lines.push(newLine);
+ }
+ const result = lines.join('\n');
+ return result.endsWith('\n') ? result : result + '\n';
+ }
+
resetModal(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
diff --git a/moci/js/modules/dashboard.js b/moci/js/modules/dashboard.js
index 3ec51ac..eb68e04 100644
--- a/moci/js/modules/dashboard.js
+++ b/moci/js/modules/dashboard.js
@@ -13,24 +13,16 @@ export default class DashboardModule {
async fetchSystemInfo() {
const [status, result] = await this.core.ubusCall('system', 'info', {});
- if (status !== 0 || !result) {
- throw new Error('Failed to fetch system info');
- }
+ if (status !== 0 || !result) throw new Error('Failed to fetch system info');
return result;
}
async fetchBoardInfo() {
const [status, result] = await this.core.ubusCall('system', 'board', {});
- if (status !== 0 || !result) {
- throw new Error('Failed to fetch board info');
- }
+ if (status !== 0 || !result) throw new Error('Failed to fetch board info');
return result;
}
- parseMemoryPercent(memory) {
- return (((memory.total - memory.free) / memory.total) * 100).toFixed(0);
- }
-
renderSystemInfo(boardInfo, systemInfo) {
const hostnameEl = document.getElementById('hostname');
const uptimeEl = document.getElementById('uptime');
@@ -40,7 +32,8 @@ export default class DashboardModule {
if (hostnameEl) hostnameEl.textContent = boardInfo.hostname || 'OpenWrt';
if (uptimeEl) uptimeEl.textContent = this.core.formatUptime(systemInfo.uptime);
- const memPercent = this.parseMemoryPercent(systemInfo.memory);
+ const memTotal = systemInfo.memory.total || 1;
+ const memPercent = (((memTotal - systemInfo.memory.free) / memTotal) * 100).toFixed(0);
if (memoryEl) memoryEl.textContent = this.core.formatMemory(systemInfo.memory);
if (memoryBarEl) memoryBarEl.style.width = memPercent + '%';
}
@@ -71,158 +64,102 @@ export default class DashboardModule {
await this.updateWANStatus();
}
- async fetchCpuStats() {
- const [status, result] = await this.core.ubusCall('file', 'read', {
- path: '/proc/stat'
- });
- if (status !== 0 || !result?.data) {
- throw new Error('Failed to fetch CPU stats');
- }
- return result.data;
- }
-
- parseCpuStats(content) {
- 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);
- return { idle, total };
- }
-
- calculateCpuUsage(current, previous) {
- if (!previous) return null;
- const idleDelta = current.idle - previous.idle;
- const totalDelta = current.total - previous.total;
- return ((1 - idleDelta / totalDelta) * 100).toFixed(1);
- }
-
- renderCpuUsage(usage) {
- const cpuEl = document.getElementById('cpu');
- const cpuBarEl = document.getElementById('cpu-bar');
-
- if (usage !== null) {
- if (cpuEl) cpuEl.textContent = usage + '%';
- if (cpuBarEl) cpuBarEl.style.width = usage + '%';
- } else {
- if (cpuEl) cpuEl.textContent = 'N/A';
- }
- }
-
async updateCpuUsage() {
try {
- const content = await this.fetchCpuStats();
- const currentStats = this.parseCpuStats(content);
- const usage = this.calculateCpuUsage(currentStats, this.lastCpuStats);
- this.renderCpuUsage(usage);
- this.lastCpuStats = currentStats;
- } catch (err) {
- this.renderCpuUsage(null);
- }
- }
-
- async fetchNetworkStats() {
- const [status, result] = await this.core.ubusCall('file', 'read', {
- path: '/proc/net/dev'
- });
- if (status !== 0 || !result?.data) {
- throw new Error('Failed to fetch network stats');
- }
- return result.data;
- }
-
- parseNetworkStats(content) {
- 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;
- });
-
- return { rx: totalRx, tx: totalTx };
- }
-
- calculateBandwidthRates(current, previous) {
- if (!previous) return null;
- const rxRate = (current.rx - previous.rx) / 1024 / 3;
- const txRate = (current.tx - previous.tx) / 1024 / 3;
- return { rxRate, txRate };
- }
-
- renderBandwidthRates(rates) {
- if (!rates) return;
-
- const downEl = document.getElementById('bandwidth-down');
- const upEl = document.getElementById('bandwidth-up');
-
- if (downEl) downEl.textContent = this.core.formatRate(rates.rxRate);
- if (upEl) upEl.textContent = this.core.formatRate(rates.txRate);
- }
-
- updateBandwidthHistory(rxRate, 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();
+ const [status, result] = await this.core.ubusCall('file', 'read', { path: '/proc/stat' });
+ if (status !== 0 || !result?.data) throw new Error('Failed');
+ const cpuLine = result.data.split('\n')[0];
+ const values = cpuLine.split(/\s+/).slice(1).map(Number);
+ const current = { idle: values[3], total: values.reduce((a, b) => a + b, 0) };
+
+ if (this.lastCpuStats) {
+ const idleDelta = current.idle - this.lastCpuStats.idle;
+ const totalDelta = current.total - this.lastCpuStats.total;
+ const usage = totalDelta > 0 ? ((1 - idleDelta / totalDelta) * 100).toFixed(1) : '0.0';
+ const cpuEl = document.getElementById('cpu');
+ const cpuBarEl = document.getElementById('cpu-bar');
+ if (cpuEl) cpuEl.textContent = usage + '%';
+ if (cpuBarEl) cpuBarEl.style.width = usage + '%';
+ }
+ this.lastCpuStats = current;
+ } catch {
+ const cpuEl = document.getElementById('cpu');
+ if (cpuEl) cpuEl.textContent = 'N/A';
+ const cpuBarEl = document.getElementById('cpu-bar');
+ if (cpuBarEl) cpuBarEl.style.width = '0%';
}
}
async updateNetworkStats() {
try {
- const content = await this.fetchNetworkStats();
- const currentStats = this.parseNetworkStats(content);
- const rates = this.calculateBandwidthRates(currentStats, this.lastNetStats);
-
- if (rates) {
- this.renderBandwidthRates(rates);
- this.updateBandwidthHistory(rates.rxRate, rates.txRate);
+ const [status, result] = await this.core.ubusCall('file', 'read', { path: '/proc/net/dev' });
+ if (status !== 0 || !result?.data) throw new Error('Failed');
+
+ let totalRx = 0,
+ totalTx = 0;
+ result.data
+ .split('\n')
+ .slice(2)
+ .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;
+ });
+
+ const current = { rx: totalRx, tx: totalTx };
+ if (this.lastNetStats) {
+ const rxRate = Math.max(0, current.rx - this.lastNetStats.rx) / 1024 / 3;
+ const txRate = Math.max(0, current.tx - this.lastNetStats.tx) / 1024 / 3;
+
+ const downEl = document.getElementById('bandwidth-down');
+ const upEl = document.getElementById('bandwidth-up');
+ if (downEl) downEl.textContent = this.core.formatRate(rxRate);
+ if (upEl) upEl.textContent = this.core.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 = currentStats;
+ this.lastNetStats = current;
} catch (err) {
console.error('updateNetworkStats error:', err);
}
}
- async fetchWANInterfaces() {
- const [status, result] = await this.core.ubusCall('network.interface', 'dump', {});
- if (status !== 0 || !result?.interface) {
- throw new Error('Failed to fetch WAN interfaces');
- }
- return result.interface;
- }
-
- parseWANStatus(interfaces) {
- 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'
- );
- }
+ async updateWANStatus() {
+ try {
+ const [status, result] = await this.core.ubusCall('network.interface', 'dump', {});
+ if (status !== 0 || !result?.interface) throw new Error('Failed');
+ const interfaces = result.interface;
- let internetIface = null;
- let gateway = null;
+ let lanIface = interfaces.find(i => i.interface === 'lan' || i.device === 'br-lan');
+ if (!lanIface) {
+ lanIface = interfaces.find(i => i.up && i['ipv4-address']?.length > 0 && i.interface !== 'loopback');
+ }
- 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');
+ let internetIface = null,
+ gateway = null;
+ for (const iface of interfaces) {
+ if (!iface.up || iface.interface === 'loopback') continue;
+ const defaultRoute = iface.route?.find(r => r.target === '0.0.0.0');
if (defaultRoute) {
internetIface = iface;
gateway = defaultRoute.nexthop;
break;
}
}
- }
- return { lanIface, internetIface, gateway };
+ this.renderWANStatus({ lanIface, internetIface, gateway });
+ } catch (err) {
+ console.error('Failed to load WAN status:', err);
+ this.renderWANStatus(null);
+ }
}
renderWANStatus(wanStatus) {
@@ -242,18 +179,14 @@ export default class DashboardModule {
const { lanIface, internetIface, gateway } = wanStatus;
- if (lanIface && lanIface['ipv4-address'] && lanIface['ipv4-address'][0]) {
- lanIpEl.textContent = lanIface['ipv4-address'][0].address;
- } else {
- lanIpEl.textContent = '---.---.---.---';
- }
+ lanIpEl.textContent = lanIface?.['ipv4-address']?.[0]?.address || '---.---.---.---';
if (internetIface) {
heroCard.classList.add('online');
heroCard.classList.remove('offline');
wanStatusEl.textContent = 'ONLINE';
- if (internetIface['ipv4-address'] && internetIface['ipv4-address'][0]) {
+ if (internetIface['ipv4-address']?.[0]) {
wanIpEl.textContent = internetIface['ipv4-address'][0].address;
} else if (gateway) {
wanIpEl.textContent = `Gateway: ${gateway}`;
@@ -268,174 +201,135 @@ export default class DashboardModule {
}
}
- async updateWANStatus() {
- try {
- const interfaces = await this.fetchWANInterfaces();
- const wanStatus = this.parseWANStatus(interfaces);
- this.renderWANStatus(wanStatus);
- } catch (err) {
- console.error('Failed to load WAN status:', err);
- this.renderWANStatus(null);
- }
- }
-
- async fetchSystemLog() {
- const [status, result] = await this.core.ubusCall('file', 'exec', {
- command: '/usr/libexec/syslog-wrapper',
- params: []
- });
- if (status !== 0 || !result?.stdout) {
- throw new Error('Failed to fetch system log');
- }
- return result.stdout;
- }
-
- parseSystemLog(stdout) {
- return stdout
- .split('\n')
- .filter(l => l.trim())
- .slice(-20);
- }
-
- renderSystemLog(lines) {
- const logEl = document.getElementById('system-log');
- if (!logEl) return;
-
- if (!lines || lines.length === 0) {
- logEl.innerHTML = 'No logs available
';
- return;
- }
-
- 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.core.escapeHtml(line)}
`;
- })
- .join('');
-
- logEl.innerHTML = logHtml;
- }
-
async updateSystemLog() {
try {
- const stdout = await this.fetchSystemLog();
- const lines = this.parseSystemLog(stdout);
- this.renderSystemLog(lines);
+ const [status, result] = await this.core.ubusCall('file', 'exec', {
+ command: '/usr/libexec/syslog-wrapper',
+ params: []
+ });
+ if (status !== 0 || !result?.stdout) throw new Error('Failed');
+
+ const lines = result.stdout
+ .split('\n')
+ .filter(l => l.trim())
+ .slice(-20);
+ const logEl = document.getElementById('system-log');
+ if (!logEl) return;
+
+ if (lines.length === 0) {
+ logEl.innerHTML = 'No logs available
';
+ return;
+ }
+
+ logEl.innerHTML = lines
+ .map(line => {
+ let className = 'log-line';
+ const lower = line.toLowerCase();
+ if (lower.includes('error') || lower.includes('fail')) className += ' error';
+ else if (lower.includes('warn')) className += ' warn';
+ return `${this.core.escapeHtml(line)}
`;
+ })
+ .join('');
} catch (err) {
console.error('Failed to load system log:', err);
- this.renderSystemLog(null);
- }
- }
-
- async fetchARPTable() {
- const [status, result] = await this.core.ubusCall('file', 'read', {
- path: '/proc/net/arp'
- });
- if (status !== 0 || !result?.data) {
- throw new Error('Failed to fetch ARP table');
- }
- return result.data;
- }
-
- async fetchDHCPLeases() {
- const [status, result] = await this.core.ubusCall('luci-rpc', 'getDHCPLeases', {});
- if (status !== 0 || !result?.dhcp_leases) {
- throw new Error('Failed to fetch DHCP leases');
+ const logEl = document.getElementById('system-log');
+ if (logEl) logEl.innerHTML = 'No logs available
';
}
- return result.dhcp_leases;
- }
-
- parseARPCount(arpData) {
- const lines = arpData.split('\n').slice(1);
- return lines.filter(line => {
- if (!line.trim()) return false;
- const parts = line.trim().split(/\s+/);
- return parts.length >= 4 && parts[2] !== '0x0';
- }).length;
}
- renderClientCount(count) {
- const clientsEl = document.getElementById('clients');
- if (clientsEl) {
- clientsEl.textContent = count !== null ? count : 'N/A';
- }
- }
-
- renderConnectionRow(lease) {
- return `
-
+ async updateConnections() {
+ try {
+ let deviceCount = 0;
+ try {
+ const [status, result] = await this.core.ubusCall('file', 'read', { path: '/proc/net/arp' });
+ if (status === 0 && result?.data) {
+ deviceCount = result.data
+ .split('\n')
+ .slice(1)
+ .filter(line => {
+ if (!line.trim()) return false;
+ const parts = line.trim().split(/\s+/);
+ return parts.length >= 4 && parts[2] !== '0x0';
+ }).length;
+ }
+ } catch {}
+
+ const clientsEl = document.getElementById('clients');
+ if (clientsEl) clientsEl.textContent = deviceCount;
+
+ let leases = [];
+ try {
+ const [s, r] = await this.core.ubusCall('luci-rpc', 'getDHCPLeases', {});
+ if (s === 0 && r?.dhcp_leases) leases = r.dhcp_leases;
+ } catch {}
+
+ this.core.renderTable(
+ '#connections-table',
+ leases,
+ 4,
+ 'No active connections',
+ lease => `
| ${this.core.escapeHtml(lease.ipaddr || 'Unknown')} |
${this.core.escapeHtml(lease.macaddr || 'Unknown')} |
${this.core.escapeHtml(lease.hostname || 'Unknown')} |
Active |
-
- `;
- }
-
- renderConnectionsTable(leases) {
- const tbody = document.querySelector('#connections-table tbody');
- if (!tbody) return;
-
- if (!leases || leases.length === 0) {
- this.core.renderEmptyTable(tbody, 4, 'No active connections');
- return;
- }
-
- const rows = leases.map(lease => this.renderConnectionRow(lease)).join('');
- tbody.innerHTML = rows;
- }
-
- async updateConnections() {
- try {
- const arpData = await this.fetchARPTable().catch(() => null);
- const deviceCount = arpData ? this.parseARPCount(arpData) : 0;
- this.renderClientCount(deviceCount);
-
- const leases = await this.fetchDHCPLeases().catch(() => []);
- this.renderConnectionsTable(leases);
+ `
+ );
} catch (err) {
console.error('Failed to load connections:', err);
- this.renderClientCount(null);
- this.renderConnectionsTable([]);
+ const clientsEl = document.getElementById('clients');
+ if (clientsEl) clientsEl.textContent = 'N/A';
+ this.core.renderTable('#connections-table', [], 4, 'No active connections', () => '');
}
}
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;
}
+ drawSeries(data, max, stepX, padding, height, fillColor, strokeColor) {
+ if (data.length < 2) return;
+ const ctx = this.bandwidthCtx;
+ ctx.fillStyle = fillColor;
+ ctx.beginPath();
+ ctx.moveTo(padding, height - padding);
+ data.forEach((val, i) => {
+ ctx.lineTo(padding + i * stepX, height - padding - (val / max) * (height - padding * 2));
+ });
+ ctx.lineTo(padding + (data.length - 1) * stepX, height - padding);
+ ctx.closePath();
+ ctx.fill();
+
+ ctx.strokeStyle = strokeColor;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ data.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();
+ }
+
updateBandwidthGraph() {
if (!this.bandwidthCtx || !this.bandwidthCanvas) return;
const ctx = this.bandwidthCtx;
- const canvas = this.bandwidthCanvas;
- const width = canvas.width;
- const height = canvas.height;
+ const { width, height } = this.bandwidthCanvas;
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.clearRect(0, 0, width, height);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
@@ -447,50 +341,10 @@ export default class DashboardModule {
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();
+ const max = Math.max(...downData, ...upData, 100);
+ const stepX = (width - padding * 2) / (downData.length - 1);
- 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();
+ this.drawSeries(downData, max, stepX, padding, height, 'rgba(226, 226, 229, 0.15)', 'rgba(226, 226, 229, 0.9)');
+ this.drawSeries(upData, max, stepX, padding, height, 'rgba(226, 226, 229, 0.08)', 'rgba(226, 226, 229, 0.5)');
}
}
diff --git a/moci/js/modules/network.js b/moci/js/modules/network.js
index c43d214..9aaeb82 100644
--- a/moci/js/modules/network.js
+++ b/moci/js/modules/network.js
@@ -1,3 +1,49 @@
+const FORWARD_FIELDS = {
+ 'edit-forward-name': 'name',
+ 'edit-forward-proto': 'proto',
+ 'edit-forward-src-dport': 'src_dport',
+ 'edit-forward-dest-ip': 'dest_ip',
+ 'edit-forward-dest-port': 'dest_port',
+ 'edit-forward-enabled': 'enabled'
+};
+
+const FW_RULE_FIELDS = {
+ 'edit-fw-rule-name': 'name',
+ 'edit-fw-rule-target': 'target',
+ 'edit-fw-rule-src': 'src',
+ 'edit-fw-rule-dest': 'dest',
+ 'edit-fw-rule-proto': 'proto',
+ 'edit-fw-rule-dest-port': 'dest_port',
+ 'edit-fw-rule-src-ip': 'src_ip'
+};
+
+const STATIC_LEASE_FIELDS = {
+ 'edit-static-lease-name': 'name',
+ 'edit-static-lease-mac': 'mac',
+ 'edit-static-lease-ip': 'ip'
+};
+
+const DNS_ENTRY_FIELDS = {
+ 'edit-dns-hostname': 'name',
+ 'edit-dns-ip': 'ip'
+};
+
+const QOS_RULE_FIELDS = {
+ 'edit-qos-rule-priority': 'target',
+ 'edit-qos-rule-proto': 'proto',
+ 'edit-qos-rule-ports': 'ports',
+ 'edit-qos-rule-srchost': 'srchost'
+};
+
+const DDNS_FIELDS = {
+ 'edit-ddns-service': 'service_name',
+ 'edit-ddns-hostname': ['lookup_host', 'domain'],
+ 'edit-ddns-username': 'username',
+ 'edit-ddns-password': 'password',
+ 'edit-ddns-check-interval': 'check_interval',
+ 'edit-ddns-enabled': 'enabled'
+};
+
export default class NetworkModule {
constructor(core) {
this.core = core;
@@ -32,101 +78,46 @@ export default class NetworkModule {
}
setupModals() {
- this.core.setupModal({
- modalId: 'interface-modal',
- closeBtnId: 'close-interface-modal',
- cancelBtnId: 'cancel-interface-btn',
- saveBtnId: 'save-interface-btn',
- saveHandler: () => this.saveInterface()
- });
-
- this.core.setupModal({
- modalId: 'wireless-modal',
- closeBtnId: 'close-wireless-modal',
- cancelBtnId: 'cancel-wireless-btn',
- saveBtnId: 'save-wireless-btn',
- saveHandler: () => this.saveWireless()
- });
-
- this.core.setupModal({
- modalId: 'forward-modal',
- closeBtnId: 'close-forward-modal',
- cancelBtnId: 'cancel-forward-btn',
- saveBtnId: 'save-forward-btn',
- saveHandler: () => this.saveForward()
- });
-
- this.core.setupModal({
- modalId: 'fw-rule-modal',
- closeBtnId: 'close-fw-rule-modal',
- cancelBtnId: 'cancel-fw-rule-btn',
- saveBtnId: 'save-fw-rule-btn',
- saveHandler: () => this.saveFirewallRule()
- });
-
- this.core.setupModal({
- modalId: 'static-lease-modal',
- closeBtnId: 'close-static-lease-modal',
- cancelBtnId: 'cancel-static-lease-btn',
- saveBtnId: 'save-static-lease-btn',
- saveHandler: () => this.saveStaticLease()
- });
-
- this.core.setupModal({
- modalId: 'dns-entry-modal',
- closeBtnId: 'close-dns-entry-modal',
- cancelBtnId: 'cancel-dns-entry-btn',
- saveBtnId: 'save-dns-entry-btn',
- saveHandler: () => this.saveDnsEntry()
- });
-
- this.core.setupModal({
- modalId: 'host-entry-modal',
- closeBtnId: 'close-host-entry-modal',
- cancelBtnId: 'cancel-host-entry-btn',
- saveBtnId: 'save-host-entry-btn',
- saveHandler: () => this.saveHostEntry()
- });
-
- this.core.setupModal({
- modalId: 'ddns-modal',
- closeBtnId: 'close-ddns-modal',
- cancelBtnId: 'cancel-ddns-btn',
- saveBtnId: 'save-ddns-btn',
- saveHandler: () => this.saveDDNS()
- });
-
- this.core.setupModal({
- modalId: 'qos-rule-modal',
- closeBtnId: 'close-qos-rule-modal',
- cancelBtnId: 'cancel-qos-rule-btn',
- saveBtnId: 'save-qos-rule-btn',
- saveHandler: () => this.saveQoSRule()
- });
-
- this.core.setupModal({
- modalId: 'wg-peer-modal',
- closeBtnId: 'close-wg-peer-modal',
- cancelBtnId: 'cancel-wg-peer-btn',
- saveBtnId: 'save-wg-peer-btn',
- saveHandler: () => this.saveWgPeer()
+ const modals = [
+ { prefix: 'interface', save: () => this.saveInterface() },
+ { prefix: 'wireless', save: () => this.saveWireless() },
+ { prefix: 'forward', save: () => this.saveForward() },
+ { prefix: 'fw-rule', save: () => this.saveFirewallRule() },
+ { prefix: 'static-lease', save: () => this.saveStaticLease() },
+ { prefix: 'dns-entry', save: () => this.saveDnsEntry() },
+ { prefix: 'host-entry', save: () => this.saveHostEntry() },
+ { prefix: 'ddns', save: () => this.saveDDNS() },
+ { prefix: 'qos-rule', save: () => this.saveQoSRule() },
+ { prefix: 'wg-peer', save: () => this.saveWgPeer() }
+ ];
+
+ modals.forEach(m => {
+ this.core.setupModal({
+ modalId: `${m.prefix}-modal`,
+ closeBtnId: `close-${m.prefix}-modal`,
+ cancelBtnId: `cancel-${m.prefix}-btn`,
+ saveBtnId: `save-${m.prefix}-btn`,
+ saveHandler: m.save
+ });
});
- const addBtn = (id, modalId) => {
- document.getElementById(id)?.addEventListener('click', () => {
+ const addButtons = [
+ ['add-forward-btn', 'forward-modal'],
+ ['add-fw-rule-btn', 'fw-rule-modal'],
+ ['add-static-lease-btn', 'static-lease-modal'],
+ ['add-dns-entry-btn', 'dns-entry-modal'],
+ ['add-host-entry-btn', 'host-entry-modal'],
+ ['add-ddns-btn', 'ddns-modal'],
+ ['add-qos-rule-btn', 'qos-rule-modal'],
+ ['add-wg-peer-btn', 'wg-peer-modal']
+ ];
+
+ addButtons.forEach(([btnId, modalId]) => {
+ document.getElementById(btnId)?.addEventListener('click', () => {
this.core.resetModal(modalId);
this.core.openModal(modalId);
});
- };
-
- addBtn('add-forward-btn', 'forward-modal');
- addBtn('add-fw-rule-btn', 'fw-rule-modal');
- addBtn('add-static-lease-btn', 'static-lease-modal');
- addBtn('add-dns-entry-btn', 'dns-entry-modal');
- addBtn('add-host-entry-btn', 'host-entry-modal');
- addBtn('add-ddns-btn', 'ddns-modal');
- addBtn('add-qos-rule-btn', 'qos-rule-modal');
- addBtn('add-wg-peer-btn', 'wg-peer-modal');
+ });
const tables = {
'interfaces-table': { edit: id => this.editInterface(id), delete: id => this.deleteInterface(id) },
@@ -172,18 +163,11 @@ export default class NetworkModule {
await this.core.loadResource('interfaces-table', 6, 'network', async () => {
const [, result] = await this.core.ubusCall('network.interface', 'dump', {});
if (!result?.interface) throw new Error('No data');
- const tbody = document.querySelector('#interfaces-table tbody');
- if (!tbody) return;
- if (result.interface.length === 0) {
- this.core.renderEmptyTable(tbody, 6, 'No interfaces found');
- return;
- }
- tbody.innerHTML = result.interface
- .map(iface => {
- const ipv4 = iface['ipv4-address']?.[0]?.address || '---.---.---.---';
- const rx = this.core.formatBytes(iface.statistics?.rx_bytes || 0);
- const tx = this.core.formatBytes(iface.statistics?.tx_bytes || 0);
- return `
+ this.core.renderTable('#interfaces-table', result.interface, 6, 'No interfaces found', iface => {
+ const ipv4 = iface['ipv4-address']?.[0]?.address || '---.---.---.---';
+ const rx = this.core.formatBytes(iface.statistics?.rx_bytes || 0);
+ const tx = this.core.formatBytes(iface.statistics?.tx_bytes || 0);
+ return `
| ${this.core.escapeHtml(iface.interface)} |
${this.core.escapeHtml(iface.proto || 'none').toUpperCase()} |
${iface.up ? this.core.renderBadge('success', 'UP') : this.core.renderBadge('error', 'DOWN')} |
@@ -191,8 +175,7 @@ export default class NetworkModule {
${rx} / ${tx} |
${this.core.renderActionButtons(iface.interface)} |
`;
- })
- .join('');
+ });
});
}
@@ -239,15 +222,7 @@ export default class NetworkModule {
}
async deleteInterface(id) {
- if (!confirm(`Delete interface "${id}"?`)) return;
- try {
- await this.core.uciDelete('network', id);
- await this.core.uciCommit('network');
- this.core.showToast('Interface deleted', 'success');
- this.loadInterfaces();
- } catch {
- this.core.showToast('Failed to delete interface', 'error');
- }
+ await this.core.uciDeleteEntry('network', id, `Delete interface "${id}"?`, () => this.loadInterfaces());
}
async loadWireless() {
@@ -263,17 +238,10 @@ export default class NetworkModule {
if (val['.type'] === 'wifi-iface') ifaces.push({ section: key, ...val });
}
- const tbody = document.querySelector('#wireless-table tbody');
- if (!tbody) return;
- if (ifaces.length === 0) {
- this.core.renderEmptyTable(tbody, 6, 'No wireless interfaces found');
- return;
- }
- tbody.innerHTML = ifaces
- .map(iface => {
- const radio = radios[iface.device] || {};
- const disabled = iface.disabled === '1';
- return `
+ this.core.renderTable('#wireless-table', ifaces, 6, 'No wireless interfaces found', iface => {
+ const radio = radios[iface.device] || {};
+ const disabled = iface.disabled === '1';
+ return `
| ${this.core.escapeHtml(iface.device || 'N/A')} |
${this.core.escapeHtml(iface.ssid || 'N/A')} |
${this.core.escapeHtml(radio.channel || 'auto')} |
@@ -281,8 +249,7 @@ export default class NetworkModule {
${this.core.escapeHtml(iface.encryption || 'none').toUpperCase()} |
${this.core.renderActionButtons(iface.section)} |
`;
- })
- .join('');
+ });
});
}
@@ -299,9 +266,8 @@ export default class NetworkModule {
document.getElementById('edit-wifi-disabled').value = c.disabled || '0';
document.getElementById('edit-wifi-hidden').value = c.hidden || '0';
- const radioSection = c.device;
- if (radioSection) {
- const [rs, rr] = await this.core.uciGet('wireless', radioSection);
+ if (c.device) {
+ const [rs, rr] = await this.core.uciGet('wireless', c.device);
if (rs === 0 && rr?.values) {
document.getElementById('edit-wifi-channel').value = rr.values.channel || 'auto';
document.getElementById('edit-wifi-txpower').value = rr.values.txpower || '';
@@ -347,192 +313,90 @@ export default class NetworkModule {
}
async deleteWireless(id) {
- if (!confirm('Delete this wireless interface?')) return;
- try {
- await this.core.uciDelete('wireless', id);
- await this.core.uciCommit('wireless');
- this.core.showToast('Wireless interface deleted', 'success');
- this.loadWireless();
- } catch {
- this.core.showToast('Failed to delete wireless interface', 'error');
- }
+ await this.core.uciDeleteEntry('wireless', id, 'Delete this wireless interface?', () => this.loadWireless());
}
async loadFirewall() {
await this.core.loadResource('firewall-table', 7, 'firewall', async () => {
const [status, result] = await this.core.uciGet('firewall');
if (status !== 0 || !result?.values) throw new Error('No data');
- const config = result.values;
-
- const forwards = Object.entries(config)
- .filter(([, v]) => v['.type'] === 'redirect')
- .map(([k, v]) => ({ section: k, ...v }));
- const rules = Object.entries(config)
- .filter(([, v]) => v['.type'] === 'rule')
- .map(([k, v]) => ({ section: k, ...v }));
-
- const fwTbody = document.querySelector('#firewall-table tbody');
- if (fwTbody) {
- if (forwards.length === 0) {
- this.core.renderEmptyTable(fwTbody, 7, 'No port forwarding rules');
- } else {
- fwTbody.innerHTML = forwards
- .map(
- f => `
- | ${this.core.escapeHtml(f.name || f.section)} |
- ${this.core.escapeHtml(f.proto || 'tcp')} |
- ${this.core.escapeHtml(f.src_dport || 'N/A')} |
- ${this.core.escapeHtml(f.dest_ip || 'N/A')} |
- ${this.core.escapeHtml(f.dest_port || f.src_dport || 'N/A')} |
- ${this.core.renderStatusBadge(f.enabled !== '0')} |
- ${this.core.renderActionButtons(f.section)} |
-
`
- )
- .join('');
- }
- }
-
- const rulesTbody = document.querySelector('#fw-rules-table tbody');
- if (rulesTbody) {
- if (rules.length === 0) {
- this.core.renderEmptyTable(rulesTbody, 7, 'No firewall rules');
- } else {
- rulesTbody.innerHTML = rules
- .map(
- r => `
- | ${this.core.escapeHtml(r.name || r.section)} |
- ${this.core.escapeHtml(r.src || 'Any')} |
- ${this.core.escapeHtml(r.dest || 'Any')} |
- ${this.core.escapeHtml(r.proto || 'Any')} |
- ${this.core.escapeHtml(r.dest_port || 'Any')} |
- ${this.core.renderBadge(r.target === 'ACCEPT' ? 'success' : 'error', r.target || 'DROP')} |
- ${this.core.renderActionButtons(r.section)} |
-
`
- )
- .join('');
- }
- }
+ const forwards = this.core.filterUciSections(result.values, 'redirect');
+ const rules = this.core.filterUciSections(result.values, 'rule');
+
+ this.core.renderTable(
+ '#firewall-table',
+ forwards,
+ 7,
+ 'No port forwarding rules',
+ f => `
+ | ${this.core.escapeHtml(f.name || f.section)} |
+ ${this.core.escapeHtml(f.proto || 'tcp')} |
+ ${this.core.escapeHtml(f.src_dport || 'N/A')} |
+ ${this.core.escapeHtml(f.dest_ip || 'N/A')} |
+ ${this.core.escapeHtml(f.dest_port || f.src_dport || 'N/A')} |
+ ${this.core.renderStatusBadge(f.enabled !== '0')} |
+ ${this.core.renderActionButtons(f.section)} |
+
`
+ );
+
+ this.core.renderTable(
+ '#fw-rules-table',
+ rules,
+ 7,
+ 'No firewall rules',
+ r => `
+ | ${this.core.escapeHtml(r.name || r.section)} |
+ ${this.core.escapeHtml(r.src || 'Any')} |
+ ${this.core.escapeHtml(r.dest || 'Any')} |
+ ${this.core.escapeHtml(r.proto || 'Any')} |
+ ${this.core.escapeHtml(r.dest_port || 'Any')} |
+ ${this.core.renderBadge(r.target === 'ACCEPT' ? 'success' : 'error', r.target || 'DROP')} |
+ ${this.core.renderActionButtons(r.section)} |
+
`
+ );
});
}
- async editForward(id) {
- try {
- const [status, result] = await this.core.uciGet('firewall', id);
- if (status !== 0 || !result?.values) throw new Error('Not found');
- const c = result.values;
- document.getElementById('edit-forward-section').value = id;
- document.getElementById('edit-forward-name').value = c.name || '';
- document.getElementById('edit-forward-proto').value = c.proto || 'tcp';
- document.getElementById('edit-forward-src-dport').value = c.src_dport || '';
- document.getElementById('edit-forward-dest-ip').value = c.dest_ip || '';
- document.getElementById('edit-forward-dest-port').value = c.dest_port || '';
- document.getElementById('edit-forward-enabled').value = c.enabled !== '0' ? '1' : '0';
- this.core.openModal('forward-modal');
- } catch {
- this.core.showToast('Failed to load rule', 'error');
- }
+ editForward(id) {
+ this.core.uciEdit('firewall', id, FORWARD_FIELDS, 'forward-modal', 'edit-forward-section');
}
- async saveForward() {
- const section = document.getElementById('edit-forward-section').value;
- const values = {
- name: document.getElementById('edit-forward-name').value,
- proto: document.getElementById('edit-forward-proto').value,
- src_dport: document.getElementById('edit-forward-src-dport').value,
- dest_ip: document.getElementById('edit-forward-dest-ip').value,
- dest_port: document.getElementById('edit-forward-dest-port').value,
- enabled: document.getElementById('edit-forward-enabled').value,
- src: 'wan',
- dest: 'lan',
- target: 'DNAT'
- };
- try {
- if (section) {
- await this.core.uciSet('firewall', section, values);
- } else {
- const [, res] = await this.core.uciAdd('firewall', 'redirect');
- if (!res?.section) throw new Error('Failed to create section');
- await this.core.uciSet('firewall', res.section, values);
- }
- await this.core.uciCommit('firewall');
- this.core.closeModal('forward-modal');
- this.core.showToast('Port forward saved', 'success');
- this.loadFirewall();
- } catch {
- this.core.showToast('Failed to save port forward', 'error');
- }
+ saveForward() {
+ this.core.uciSave({
+ config: 'firewall',
+ uciType: 'redirect',
+ modalId: 'forward-modal',
+ sectionIdField: 'edit-forward-section',
+ fieldMap: FORWARD_FIELDS,
+ defaults: { src: 'wan', dest: 'lan', target: 'DNAT' },
+ reloadFn: () => this.loadFirewall(),
+ successMsg: 'Port forward saved'
+ });
}
- async deleteForward(id) {
- if (!confirm('Delete this port forwarding rule?')) return;
- try {
- await this.core.uciDelete('firewall', id);
- await this.core.uciCommit('firewall');
- this.core.showToast('Rule deleted', 'success');
- this.loadFirewall();
- } catch {
- this.core.showToast('Failed to delete rule', 'error');
- }
+ deleteForward(id) {
+ this.core.uciDeleteEntry('firewall', id, 'Delete this port forwarding rule?', () => this.loadFirewall());
}
- async editFirewallRule(id) {
- try {
- const [status, result] = await this.core.uciGet('firewall', id);
- if (status !== 0 || !result?.values) throw new Error('Not found');
- const c = result.values;
- document.getElementById('edit-fw-rule-section').value = id;
- document.getElementById('edit-fw-rule-name').value = c.name || '';
- document.getElementById('edit-fw-rule-target').value = c.target || 'ACCEPT';
- document.getElementById('edit-fw-rule-src').value = c.src || '';
- document.getElementById('edit-fw-rule-dest').value = c.dest || '';
- document.getElementById('edit-fw-rule-proto').value = c.proto || '';
- document.getElementById('edit-fw-rule-dest-port').value = c.dest_port || '';
- document.getElementById('edit-fw-rule-src-ip').value = c.src_ip || '';
- this.core.openModal('fw-rule-modal');
- } catch {
- this.core.showToast('Failed to load rule', 'error');
- }
+ editFirewallRule(id) {
+ this.core.uciEdit('firewall', id, FW_RULE_FIELDS, 'fw-rule-modal', 'edit-fw-rule-section');
}
- async saveFirewallRule() {
- const section = document.getElementById('edit-fw-rule-section').value;
- const values = {
- name: document.getElementById('edit-fw-rule-name').value,
- target: document.getElementById('edit-fw-rule-target').value,
- src: document.getElementById('edit-fw-rule-src').value,
- dest: document.getElementById('edit-fw-rule-dest').value,
- proto: document.getElementById('edit-fw-rule-proto').value,
- dest_port: document.getElementById('edit-fw-rule-dest-port').value,
- src_ip: document.getElementById('edit-fw-rule-src-ip').value
- };
- try {
- if (section) {
- await this.core.uciSet('firewall', section, values);
- } else {
- const [, res] = await this.core.uciAdd('firewall', 'rule');
- if (!res?.section) throw new Error('Failed to create section');
- await this.core.uciSet('firewall', res.section, values);
- }
- await this.core.uciCommit('firewall');
- this.core.closeModal('fw-rule-modal');
- this.core.showToast('Firewall rule saved', 'success');
- this.loadFirewall();
- } catch {
- this.core.showToast('Failed to save firewall rule', 'error');
- }
+ saveFirewallRule() {
+ this.core.uciSave({
+ config: 'firewall',
+ uciType: 'rule',
+ modalId: 'fw-rule-modal',
+ sectionIdField: 'edit-fw-rule-section',
+ fieldMap: FW_RULE_FIELDS,
+ reloadFn: () => this.loadFirewall(),
+ successMsg: 'Firewall rule saved'
+ });
}
- async deleteFirewallRule(id) {
- if (!confirm('Delete this firewall rule?')) return;
- try {
- await this.core.uciDelete('firewall', id);
- await this.core.uciCommit('firewall');
- this.core.showToast('Rule deleted', 'success');
- this.loadFirewall();
- } catch {
- this.core.showToast('Failed to delete rule', 'error');
- }
+ deleteFirewallRule(id) {
+ this.core.uciDeleteEntry('firewall', id, 'Delete this firewall rule?', () => this.loadFirewall());
}
async loadDHCP() {
@@ -543,126 +407,74 @@ export default class NetworkModule {
if (s === 0 && r?.dhcp_leases) leases = r.dhcp_leases;
} catch {}
- const leasesTbody = document.querySelector('#dhcp-leases-table tbody');
- if (leasesTbody) {
- if (leases.length === 0) {
- this.core.renderEmptyTable(leasesTbody, 4, 'No active DHCP leases');
- } else {
- leasesTbody.innerHTML = leases
- .map(
- l => `
- | ${this.core.escapeHtml(l.hostname || 'Unknown')} |
- ${this.core.escapeHtml(l.ipaddr || 'N/A')} |
- ${this.core.escapeHtml(l.macaddr || 'N/A')} |
- ${l.expires > 0 ? l.expires + 's' : 'Permanent'} |
-
`
- )
- .join('');
- }
- }
+ this.core.renderTable(
+ '#dhcp-leases-table',
+ leases,
+ 4,
+ 'No active DHCP leases',
+ l => `
+ | ${this.core.escapeHtml(l.hostname || 'Unknown')} |
+ ${this.core.escapeHtml(l.ipaddr || 'N/A')} |
+ ${this.core.escapeHtml(l.macaddr || 'N/A')} |
+ ${l.expires > 0 ? l.expires + 's' : 'Permanent'} |
+
`
+ );
const [status, result] = await this.core.uciGet('dhcp');
if (status !== 0 || !result?.values) return;
- const statics = Object.entries(result.values)
- .filter(([, v]) => v['.type'] === 'host')
- .map(([k, v]) => ({ section: k, ...v }));
-
- const staticTbody = document.querySelector('#dhcp-static-table tbody');
- if (staticTbody) {
- if (statics.length === 0) {
- this.core.renderEmptyTable(staticTbody, 4, 'No static leases');
- } else {
- staticTbody.innerHTML = statics
- .map(
- s => `
- | ${this.core.escapeHtml(s.name || 'N/A')} |
- ${this.core.escapeHtml(s.mac || 'N/A')} |
- ${this.core.escapeHtml(s.ip || 'N/A')} |
- ${this.core.renderActionButtons(s.section)} |
-
`
- )
- .join('');
- }
- }
+ const statics = this.core.filterUciSections(result.values, 'host');
+ this.core.renderTable(
+ '#dhcp-static-table',
+ statics,
+ 4,
+ 'No static leases',
+ s => `
+ | ${this.core.escapeHtml(s.name || 'N/A')} |
+ ${this.core.escapeHtml(s.mac || 'N/A')} |
+ ${this.core.escapeHtml(s.ip || 'N/A')} |
+ ${this.core.renderActionButtons(s.section)} |
+
`
+ );
});
}
- async editStaticLease(id) {
- try {
- const [status, result] = await this.core.uciGet('dhcp', id);
- if (status !== 0 || !result?.values) throw new Error('Not found');
- const c = result.values;
- document.getElementById('edit-static-lease-section').value = id;
- document.getElementById('edit-static-lease-name').value = c.name || '';
- document.getElementById('edit-static-lease-mac').value = c.mac || '';
- document.getElementById('edit-static-lease-ip').value = c.ip || '';
- this.core.openModal('static-lease-modal');
- } catch {
- this.core.showToast('Failed to load static lease', 'error');
- }
+ editStaticLease(id) {
+ this.core.uciEdit('dhcp', id, STATIC_LEASE_FIELDS, 'static-lease-modal', 'edit-static-lease-section');
}
- async saveStaticLease() {
- const section = document.getElementById('edit-static-lease-section').value;
- const values = {
- name: document.getElementById('edit-static-lease-name').value,
- mac: document.getElementById('edit-static-lease-mac').value,
- ip: document.getElementById('edit-static-lease-ip').value
- };
- try {
- if (section) {
- await this.core.uciSet('dhcp', section, values);
- } else {
- const [, res] = await this.core.uciAdd('dhcp', 'host');
- if (!res?.section) throw new Error('Failed to create section');
- await this.core.uciSet('dhcp', res.section, values);
- }
- await this.core.uciCommit('dhcp');
- this.core.closeModal('static-lease-modal');
- this.core.showToast('Static lease saved', 'success');
- this.loadDHCP();
- } catch {
- this.core.showToast('Failed to save static lease', 'error');
- }
+ saveStaticLease() {
+ this.core.uciSave({
+ config: 'dhcp',
+ uciType: 'host',
+ modalId: 'static-lease-modal',
+ sectionIdField: 'edit-static-lease-section',
+ fieldMap: STATIC_LEASE_FIELDS,
+ reloadFn: () => this.loadDHCP(),
+ successMsg: 'Static lease saved'
+ });
}
- async deleteStaticLease(id) {
- if (!confirm('Delete this static lease?')) return;
- try {
- await this.core.uciDelete('dhcp', id);
- await this.core.uciCommit('dhcp');
- this.core.showToast('Static lease deleted', 'success');
- this.loadDHCP();
- } catch {
- this.core.showToast('Failed to delete static lease', 'error');
- }
+ deleteStaticLease(id) {
+ this.core.uciDeleteEntry('dhcp', id, 'Delete this static lease?', () => this.loadDHCP());
}
async loadDNS() {
await this.core.loadResource('dns-entries-table', 3, 'dns', async () => {
const [status, result] = await this.core.uciGet('dhcp');
if (status === 0 && result?.values) {
- const domains = Object.entries(result.values)
- .filter(([, v]) => v['.type'] === 'domain')
- .map(([k, v]) => ({ section: k, ...v }));
-
- const dnsTbody = document.querySelector('#dns-entries-table tbody');
- if (dnsTbody) {
- if (domains.length === 0) {
- this.core.renderEmptyTable(dnsTbody, 3, 'No custom DNS entries');
- } else {
- dnsTbody.innerHTML = domains
- .map(
- d => `
- | ${this.core.escapeHtml(d.name || 'N/A')} |
- ${this.core.escapeHtml(d.ip || 'N/A')} |
- ${this.core.renderActionButtons(d.section)} |
-
`
- )
- .join('');
- }
- }
+ const domains = this.core.filterUciSections(result.values, 'domain');
+ this.core.renderTable(
+ '#dns-entries-table',
+ domains,
+ 3,
+ 'No custom DNS entries',
+ d => `
+ | ${this.core.escapeHtml(d.name || 'N/A')} |
+ ${this.core.escapeHtml(d.ip || 'N/A')} |
+ ${this.core.renderActionButtons(d.section)} |
+
`
+ );
}
try {
@@ -670,22 +482,17 @@ export default class NetworkModule {
if (hs === 0 && hr?.data) {
this.hostsRaw = hr.data;
const entries = this.parseHosts(hr.data);
- const hostsTbody = document.querySelector('#hosts-table tbody');
- if (hostsTbody) {
- if (entries.length === 0) {
- this.core.renderEmptyTable(hostsTbody, 3, 'No hosts entries');
- } else {
- hostsTbody.innerHTML = entries
- .map(
- (e, i) => `
- | ${this.core.escapeHtml(e.ip)} |
- ${this.core.escapeHtml(e.names)} |
- ${this.core.renderActionButtons(String(i))} |
-
`
- )
- .join('');
- }
- }
+ this.core.renderTable(
+ '#hosts-table',
+ entries,
+ 3,
+ 'No hosts entries',
+ (e, i) => `
+ | ${this.core.escapeHtml(e.ip)} |
+ ${this.core.escapeHtml(e.names)} |
+ ${this.core.renderActionButtons(String(i))} |
+
`
+ );
}
} catch {}
});
@@ -702,53 +509,24 @@ export default class NetworkModule {
.filter(e => e.ip && e.names);
}
- async editDnsEntry(id) {
- try {
- const [status, result] = await this.core.uciGet('dhcp', id);
- if (status !== 0 || !result?.values) throw new Error('Not found');
- const c = result.values;
- document.getElementById('edit-dns-entry-section').value = id;
- document.getElementById('edit-dns-hostname').value = c.name || '';
- document.getElementById('edit-dns-ip').value = c.ip || '';
- this.core.openModal('dns-entry-modal');
- } catch {
- this.core.showToast('Failed to load DNS entry', 'error');
- }
+ editDnsEntry(id) {
+ this.core.uciEdit('dhcp', id, DNS_ENTRY_FIELDS, 'dns-entry-modal', 'edit-dns-entry-section');
}
- async saveDnsEntry() {
- const section = document.getElementById('edit-dns-entry-section').value;
- const values = {
- name: document.getElementById('edit-dns-hostname').value,
- ip: document.getElementById('edit-dns-ip').value
- };
- try {
- if (section) {
- await this.core.uciSet('dhcp', section, values);
- } else {
- const [, res] = await this.core.uciAdd('dhcp', 'domain');
- if (!res?.section) throw new Error('Failed to create section');
- await this.core.uciSet('dhcp', res.section, values);
- }
- await this.core.uciCommit('dhcp');
- this.core.closeModal('dns-entry-modal');
- this.core.showToast('DNS entry saved', 'success');
- this.loadDNS();
- } catch {
- this.core.showToast('Failed to save DNS entry', 'error');
- }
+ saveDnsEntry() {
+ this.core.uciSave({
+ config: 'dhcp',
+ uciType: 'domain',
+ modalId: 'dns-entry-modal',
+ sectionIdField: 'edit-dns-entry-section',
+ fieldMap: DNS_ENTRY_FIELDS,
+ reloadFn: () => this.loadDNS(),
+ successMsg: 'DNS entry saved'
+ });
}
- async deleteDnsEntry(id) {
- if (!confirm('Delete this DNS entry?')) return;
- try {
- await this.core.uciDelete('dhcp', id);
- await this.core.uciCommit('dhcp');
- this.core.showToast('DNS entry deleted', 'success');
- this.loadDNS();
- } catch {
- this.core.showToast('Failed to delete DNS entry', 'error');
- }
+ deleteDnsEntry(id) {
+ this.core.uciDeleteEntry('dhcp', id, 'Delete this DNS entry?', () => this.loadDNS());
}
editHostEntry(index) {
@@ -770,18 +548,21 @@ export default class NetworkModule {
return;
}
- const lines = this.hostsRaw.split('\n');
- const dataIndices = lines.map((l, i) => (l.trim() && !l.trim().startsWith('#') ? i : -1)).filter(i => i >= 0);
-
- if (index !== '') {
- const origIdx = dataIndices[parseInt(index)];
- if (origIdx !== undefined) lines[origIdx] = `${ip}\t${names}`;
- } else {
- if (lines.length && lines[lines.length - 1] === '') lines.pop();
- lines.push(`${ip}\t${names}`);
+ const entries = this.parseHosts(this.hostsRaw);
+ const parsedIndex = index === '' ? null : parseInt(index, 10);
+ if (
+ parsedIndex !== null &&
+ (!Number.isInteger(parsedIndex) || parsedIndex < 0 || parsedIndex >= entries.length)
+ ) {
+ this.core.showToast('Hosts entry is out of date. Reload and try again.', 'error');
+ return;
}
-
- const newContent = lines.join('\n') + (this.hostsRaw.endsWith('\n') ? '' : '\n');
+ const newContent = this.core.spliceFileLines(
+ this.hostsRaw,
+ l => l.trim() && !l.trim().startsWith('#'),
+ index,
+ `${ip}\t${names}`
+ );
try {
await this.core.ubusCall('file', 'write', { path: '/etc/hosts', data: newContent });
this.core.closeModal('host-entry-modal');
@@ -794,11 +575,18 @@ export default class NetworkModule {
async deleteHostEntry(index) {
if (!confirm('Delete this hosts entry?')) return;
- const lines = this.hostsRaw.split('\n');
- const dataIndices = lines.map((l, i) => (l.trim() && !l.trim().startsWith('#') ? i : -1)).filter(i => i >= 0);
- const origIdx = dataIndices[parseInt(index)];
- if (origIdx !== undefined) lines.splice(origIdx, 1);
- const newContent = lines.join('\n') + (this.hostsRaw.endsWith('\n') ? '' : '\n');
+ const entries = this.parseHosts(this.hostsRaw);
+ const parsedIndex = parseInt(index, 10);
+ if (!Number.isInteger(parsedIndex) || parsedIndex < 0 || parsedIndex >= entries.length) {
+ this.core.showToast('Hosts entry is out of date. Reload and try again.', 'error');
+ return;
+ }
+ const newContent = this.core.spliceFileLines(
+ this.hostsRaw,
+ l => l.trim() && !l.trim().startsWith('#'),
+ index,
+ null
+ );
try {
await this.core.ubusCall('file', 'write', { path: '/etc/hosts', data: newContent });
this.core.showToast('Hosts entry deleted', 'success');
@@ -812,19 +600,14 @@ export default class NetworkModule {
await this.core.loadResource('ddns-table', 6, 'ddns', async () => {
const [status, result] = await this.core.uciGet('ddns');
if (status !== 0 || !result?.values) throw new Error('No data');
- const services = Object.entries(result.values)
- .filter(([, v]) => v['.type'] === 'service')
- .map(([k, v]) => ({ section: k, ...v }));
-
- const tbody = document.querySelector('#ddns-table tbody');
- if (!tbody) return;
- if (services.length === 0) {
- this.core.renderEmptyTable(tbody, 6, 'No DDNS services configured');
- return;
- }
- tbody.innerHTML = services
- .map(
- s => `
+ const services = this.core.filterUciSections(result.values, 'service');
+
+ this.core.renderTable(
+ '#ddns-table',
+ services,
+ 6,
+ 'No DDNS services configured',
+ s => `
| ${this.core.escapeHtml(s.section)} |
${this.core.escapeHtml(s.lookup_host || s.domain || 'N/A')} |
${this.core.escapeHtml(s.service_name || 'Custom')} |
@@ -832,74 +615,36 @@ export default class NetworkModule {
${this.core.renderStatusBadge(s.enabled === '1')} |
${this.core.renderActionButtons(s.section)} |
`
- )
- .join('');
+ );
});
}
async editDDNS(id) {
- try {
- const [status, result] = await this.core.uciGet('ddns', id);
- if (status !== 0 || !result?.values) throw new Error('Not found');
- const c = result.values;
- document.getElementById('edit-ddns-section').value = id;
- document.getElementById('edit-ddns-name').value = id;
- document.getElementById('edit-ddns-service').value = c.service_name || 'cloudflare.com-v4';
- document.getElementById('edit-ddns-hostname').value = c.lookup_host || c.domain || '';
- document.getElementById('edit-ddns-username').value = c.username || '';
- document.getElementById('edit-ddns-password').value = c.password || '';
- document.getElementById('edit-ddns-check-interval').value = c.check_interval || '10';
- document.getElementById('edit-ddns-enabled').value = c.enabled || '0';
- this.core.openModal('ddns-modal');
- } catch {
- this.core.showToast('Failed to load DDNS service', 'error');
- }
+ document.getElementById('edit-ddns-name').value = id;
+ await this.core.uciEdit('ddns', id, DDNS_FIELDS, 'ddns-modal', 'edit-ddns-section');
}
- async saveDDNS() {
- const section = document.getElementById('edit-ddns-section').value;
- const name = document.getElementById('edit-ddns-name').value;
- const values = {
- service_name: document.getElementById('edit-ddns-service').value,
- lookup_host: document.getElementById('edit-ddns-hostname').value,
- domain: document.getElementById('edit-ddns-hostname').value,
- username: document.getElementById('edit-ddns-username').value,
- password: document.getElementById('edit-ddns-password').value,
- check_interval: document.getElementById('edit-ddns-check-interval').value,
- enabled: document.getElementById('edit-ddns-enabled').value,
- ip_source: 'network',
- ip_network: 'wan',
- interface: 'wan',
- use_https: '1'
- };
- try {
- if (section) {
- await this.core.uciSet('ddns', section, values);
- } else {
- const sectionName = name || null;
- const [, res] = await this.core.uciAdd('ddns', 'service', sectionName);
- if (!res?.section) throw new Error('Failed to create section');
- await this.core.uciSet('ddns', res.section, values);
- }
- await this.core.uciCommit('ddns');
- this.core.closeModal('ddns-modal');
- this.core.showToast('DDNS service saved', 'success');
- this.loadDDNS();
- } catch {
- this.core.showToast('Failed to save DDNS service', 'error');
- }
+ saveDDNS() {
+ this.core.uciSave({
+ config: 'ddns',
+ uciType: 'service',
+ modalId: 'ddns-modal',
+ sectionIdField: 'edit-ddns-section',
+ sectionNameField: 'edit-ddns-name',
+ fieldMap: DDNS_FIELDS,
+ defaults: {
+ ip_source: 'network',
+ ip_network: 'wan',
+ interface: 'wan',
+ use_https: '1'
+ },
+ reloadFn: () => this.loadDDNS(),
+ successMsg: 'DDNS service saved'
+ });
}
- async deleteDDNS(id) {
- if (!confirm('Delete this DDNS service?')) return;
- try {
- await this.core.uciDelete('ddns', id);
- await this.core.uciCommit('ddns');
- this.core.showToast('DDNS service deleted', 'success');
- this.loadDDNS();
- } catch {
- this.core.showToast('Failed to delete DDNS service', 'error');
- }
+ deleteDDNS(id) {
+ this.core.uciDeleteEntry('ddns', id, 'Delete this DDNS service?', () => this.loadDDNS());
}
async loadQoS() {
@@ -916,19 +661,13 @@ export default class NetworkModule {
el('qos-upload').value = iface[1].upload || '';
}
- const rules = Object.entries(config)
- .filter(([, v]) => v['.type'] === 'classify')
- .map(([k, v]) => ({ section: k, ...v }));
-
- const tbody = document.querySelector('#qos-rules-table tbody');
- if (!tbody) return;
- if (rules.length === 0) {
- this.core.renderEmptyTable(tbody, 6, 'No QoS rules');
- return;
- }
- tbody.innerHTML = rules
- .map(
- r => `
+ const rules = this.core.filterUciSections(config, 'classify');
+ this.core.renderTable(
+ '#qos-rules-table',
+ rules,
+ 6,
+ 'No QoS rules',
+ r => `
| ${this.core.escapeHtml(r.section)} |
${this.core.escapeHtml(r.target || 'Normal')} |
${this.core.escapeHtml(r.proto || 'Any')} |
@@ -936,8 +675,7 @@ export default class NetworkModule {
${this.core.escapeHtml(r.srchost || 'Any')} |
${this.core.renderActionButtons(r.section)} |
`
- )
- .join('');
+ );
});
}
@@ -960,57 +698,24 @@ export default class NetworkModule {
}
async editQoSRule(id) {
- try {
- const [status, result] = await this.core.uciGet('qos', id);
- if (status !== 0 || !result?.values) throw new Error('Not found');
- const c = result.values;
- document.getElementById('edit-qos-rule-section').value = id;
- document.getElementById('edit-qos-rule-name').value = id;
- document.getElementById('edit-qos-rule-priority').value = c.target || 'Normal';
- document.getElementById('edit-qos-rule-proto').value = c.proto || '';
- document.getElementById('edit-qos-rule-ports').value = c.ports || '';
- document.getElementById('edit-qos-rule-srchost').value = c.srchost || '';
- this.core.openModal('qos-rule-modal');
- } catch {
- this.core.showToast('Failed to load QoS rule', 'error');
- }
+ document.getElementById('edit-qos-rule-name').value = id;
+ await this.core.uciEdit('qos', id, QOS_RULE_FIELDS, 'qos-rule-modal', 'edit-qos-rule-section');
}
- async saveQoSRule() {
- const section = document.getElementById('edit-qos-rule-section').value;
- const values = {
- target: document.getElementById('edit-qos-rule-priority').value,
- proto: document.getElementById('edit-qos-rule-proto').value,
- ports: document.getElementById('edit-qos-rule-ports').value,
- srchost: document.getElementById('edit-qos-rule-srchost').value
- };
- try {
- if (section) {
- await this.core.uciSet('qos', section, values);
- } else {
- const [, res] = await this.core.uciAdd('qos', 'classify');
- if (!res?.section) throw new Error('Failed to create section');
- await this.core.uciSet('qos', res.section, values);
- }
- await this.core.uciCommit('qos');
- this.core.closeModal('qos-rule-modal');
- this.core.showToast('QoS rule saved', 'success');
- this.loadQoS();
- } catch {
- this.core.showToast('Failed to save QoS rule', 'error');
- }
+ saveQoSRule() {
+ this.core.uciSave({
+ config: 'qos',
+ uciType: 'classify',
+ modalId: 'qos-rule-modal',
+ sectionIdField: 'edit-qos-rule-section',
+ fieldMap: QOS_RULE_FIELDS,
+ reloadFn: () => this.loadQoS(),
+ successMsg: 'QoS rule saved'
+ });
}
- async deleteQoSRule(id) {
- if (!confirm('Delete this QoS rule?')) return;
- try {
- await this.core.uciDelete('qos', id);
- await this.core.uciCommit('qos');
- this.core.showToast('QoS rule deleted', 'success');
- this.loadQoS();
- } catch {
- this.core.showToast('Failed to delete QoS rule', 'error');
- }
+ deleteQoSRule(id) {
+ this.core.uciDeleteEntry('qos', id, 'Delete this QoS rule?', () => this.loadQoS());
}
async loadVPN() {
@@ -1035,20 +740,13 @@ export default class NetworkModule {
.filter(([, v]) => v['.type']?.startsWith('wireguard_'))
.map(([k, v]) => ({ section: k, ...v }));
- const tbody = document.querySelector('#wg-peers-table tbody');
- if (!tbody) return;
- if (peers.length === 0) {
- this.core.renderEmptyTable(tbody, 6, 'No WireGuard peers configured');
- return;
- }
- tbody.innerHTML = peers
- .map(p => {
- const pubKey = p.public_key ? this.core.escapeHtml(p.public_key.substring(0, 20)) + '...' : 'N/A';
- const endpoint =
- p.endpoint_host && p.endpoint_port
- ? `${this.core.escapeHtml(p.endpoint_host)}:${this.core.escapeHtml(String(p.endpoint_port))}`
- : 'N/A';
- return `
+ this.core.renderTable('#wg-peers-table', peers, 6, 'No WireGuard peers configured', p => {
+ const pubKey = p.public_key ? this.core.escapeHtml(p.public_key.substring(0, 20)) + '...' : 'N/A';
+ const endpoint =
+ p.endpoint_host && p.endpoint_port
+ ? `${this.core.escapeHtml(p.endpoint_host)}:${this.core.escapeHtml(String(p.endpoint_port))}`
+ : 'N/A';
+ return `
| ${this.core.escapeHtml(p.description || p.section)} |
${pubKey} |
${this.core.escapeHtml(Array.isArray(p.allowed_ips) ? p.allowed_ips.join(', ') : p.allowed_ips || 'N/A')} |
@@ -1056,24 +754,26 @@ export default class NetworkModule {
${this.core.renderBadge('success', 'CONFIGURED')} |
${this.core.renderActionButtons(p.section)} |
`;
- })
- .join('');
+ });
});
}
async saveWgConfig() {
try {
const ifaceName = document.getElementById('wg-interface').value || 'wg0';
+ const disabled = document.getElementById('wg-enabled').value === '0';
+ const addr = (document.getElementById('wg-address').value || '').trim();
+ if (!addr) {
+ this.core.showToast('WireGuard address is required', 'error');
+ return;
+ }
const values = {
proto: 'wireguard',
listen_port: document.getElementById('wg-port').value,
private_key: document.getElementById('wg-private-key').value,
- addresses: [document.getElementById('wg-address').value]
+ addresses: [addr],
+ disabled: disabled ? '1' : '0'
};
- const disabled = document.getElementById('wg-enabled').value === '0';
- if (disabled) values.disabled = '1';
- else values.disabled = '0';
-
await this.core.uciSet('network', ifaceName, values);
await this.core.uciCommit('network');
this.core.showToast('WireGuard configuration saved', 'success');
@@ -1124,65 +824,48 @@ export default class NetworkModule {
}
}
- async editWgPeer(id) {
- try {
- const [status, result] = await this.core.uciGet('network', id);
- if (status !== 0 || !result?.values) throw new Error('Not found');
- const c = result.values;
- document.getElementById('edit-wg-peer-section').value = id;
- document.getElementById('edit-wg-peer-name').value = c.description || '';
- document.getElementById('edit-wg-peer-public-key').value = c.public_key || '';
- document.getElementById('edit-wg-peer-allowed-ips').value = Array.isArray(c.allowed_ips)
- ? c.allowed_ips.join(', ')
- : c.allowed_ips || '';
- document.getElementById('edit-wg-peer-keepalive').value = c.persistent_keepalive || '';
- document.getElementById('edit-wg-peer-preshared-key').value = c.preshared_key || '';
- this.core.openModal('wg-peer-modal');
- } catch {
- this.core.showToast('Failed to load peer config', 'error');
- }
+ editWgPeer(id) {
+ this.core.uciEdit(
+ 'network',
+ id,
+ {
+ 'edit-wg-peer-name': 'description',
+ 'edit-wg-peer-public-key': 'public_key',
+ 'edit-wg-peer-allowed-ips': 'allowed_ips',
+ 'edit-wg-peer-keepalive': 'persistent_keepalive',
+ 'edit-wg-peer-preshared-key': 'preshared_key'
+ },
+ 'wg-peer-modal',
+ 'edit-wg-peer-section'
+ );
}
- async saveWgPeer() {
- const section = document.getElementById('edit-wg-peer-section').value;
+ saveWgPeer() {
const ifaceName = document.getElementById('wg-interface').value || 'wg0';
- const values = {
- description: document.getElementById('edit-wg-peer-name').value,
- public_key: document.getElementById('edit-wg-peer-public-key').value,
- allowed_ips: document
- .getElementById('edit-wg-peer-allowed-ips')
- .value.split(/[,\s]+/)
- .filter(Boolean),
- persistent_keepalive: document.getElementById('edit-wg-peer-keepalive').value,
- preshared_key: document.getElementById('edit-wg-peer-preshared-key').value
- };
- try {
- if (section) {
- await this.core.uciSet('network', section, values);
- } else {
- const [, res] = await this.core.uciAdd('network', `wireguard_${ifaceName}`);
- if (!res?.section) throw new Error('Failed to create section');
- await this.core.uciSet('network', res.section, values);
- }
- await this.core.uciCommit('network');
- this.core.closeModal('wg-peer-modal');
- this.core.showToast('WireGuard peer saved', 'success');
- this.loadVPN();
- } catch {
- this.core.showToast('Failed to save WireGuard peer', 'error');
- }
+ const allowedIpsRaw = document.getElementById('edit-wg-peer-allowed-ips')?.value || '';
+ const allowed_ips = allowedIpsRaw
+ .split(/[,\s]+/)
+ .map(s => s.trim())
+ .filter(Boolean);
+ this.core.uciSave({
+ config: 'network',
+ uciType: `wireguard_${ifaceName}`,
+ modalId: 'wg-peer-modal',
+ sectionIdField: 'edit-wg-peer-section',
+ fieldMap: {
+ 'edit-wg-peer-name': 'description',
+ 'edit-wg-peer-public-key': 'public_key',
+ 'edit-wg-peer-keepalive': 'persistent_keepalive',
+ 'edit-wg-peer-preshared-key': 'preshared_key'
+ },
+ defaults: { allowed_ips },
+ reloadFn: () => this.loadVPN(),
+ successMsg: 'WireGuard peer saved'
+ });
}
- async deleteWgPeer(id) {
- if (!confirm('Delete this WireGuard peer?')) return;
- try {
- await this.core.uciDelete('network', id);
- await this.core.uciCommit('network');
- this.core.showToast('Peer deleted', 'success');
- this.loadVPN();
- } catch {
- this.core.showToast('Failed to delete peer', 'error');
- }
+ deleteWgPeer(id) {
+ this.core.uciDeleteEntry('network', id, 'Delete this WireGuard peer?', () => this.loadVPN());
}
async loadDiagnostics() {
@@ -1194,22 +877,18 @@ export default class NetworkModule {
if (s === 0 && r?.dhcp_leases) leases = r.dhcp_leases;
} catch {}
- const tbody = document.querySelector('#dhcp-clients-table tbody');
- if (!tbody) return;
- if (leases.length === 0) {
- this.core.renderEmptyTable(tbody, 4, 'No DHCP clients');
- return;
- }
- tbody.innerHTML = leases
- .map(
- l => `
+ this.core.renderTable(
+ '#dhcp-clients-table',
+ leases,
+ 4,
+ 'No DHCP clients',
+ l => `
| ${this.core.escapeHtml(l.ipaddr || 'N/A')} |
${this.core.escapeHtml(l.macaddr || 'N/A')} |
${this.core.escapeHtml(l.hostname || 'Unknown')} |
${l.expires > 0 ? l.expires + 's' : 'Permanent'} |
`
- )
- .join('');
+ );
});
}
diff --git a/moci/js/modules/services.js b/moci/js/modules/services.js
deleted file mode 100644
index 4c8b4ab..0000000
--- a/moci/js/modules/services.js
+++ /dev/null
@@ -1,140 +0,0 @@
-export default class ServicesModule {
- constructor(core) {
- this.core = core;
- }
-
- async fetchQoSConfig() {
- const [status, result] = await this.core.uciGet('qos');
-
- if (status !== 0 || !result?.values) {
- throw new Error('QoS not configured');
- }
-
- return result.values;
- }
-
- parseQoSRules(config) {
- return Object.entries(config)
- .filter(([key, val]) => val['.type'] === 'classify')
- .map(([key, val]) => ({
- name: key,
- ...val
- }));
- }
-
- renderQoSRow(rule) {
- const enabled = rule.enabled !== '0';
- const statusBadge = enabled
- ? this.core.renderBadge('success', 'ACTIVE')
- : this.core.renderBadge('error', 'INACTIVE');
-
- return `
-
- | ${this.core.escapeHtml(rule.name)} |
- ${this.core.escapeHtml(rule.target || 'Default')} |
- ${this.core.escapeHtml(rule.proto || 'all')} |
- ${this.core.escapeHtml(rule.srchost || 'any')} |
- ${statusBadge} |
-
- `;
- }
-
- renderQoSTable(rules) {
- return rules.map(rule => this.renderQoSRow(rule)).join('');
- }
-
- updateQoSTable(rules) {
- const tbody = document.querySelector('#qos-table tbody');
- if (!tbody) return;
-
- if (rules.length === 0) {
- this.core.renderEmptyTable(tbody, 5, 'No QoS rules configured');
- return;
- }
-
- tbody.innerHTML = this.renderQoSTable(rules);
- }
-
- async loadQoS() {
- if (!this.core.isFeatureEnabled('qos')) return;
-
- try {
- const config = await this.fetchQoSConfig();
- const rules = this.parseQoSRules(config);
- this.updateQoSTable(rules);
- } catch (err) {
- console.error('Failed to load QoS:', err);
- const tbody = document.querySelector('#qos-table tbody');
- if (tbody) {
- this.core.renderEmptyTable(tbody, 5, 'QoS not configured');
- }
- }
- }
-
- async fetchDDNSConfig() {
- const [status, result] = await this.core.uciGet('ddns');
-
- if (status !== 0 || !result?.values) {
- throw new Error('DDNS not configured');
- }
-
- return result.values;
- }
-
- parseDDNSServices(config) {
- return Object.entries(config)
- .filter(([key, val]) => val['.type'] === 'service')
- .map(([key, val]) => ({
- name: key,
- ...val
- }));
- }
-
- renderDDNSRow(service) {
- const enabled = service.enabled === '1';
- const statusBadge = enabled
- ? this.core.renderBadge('success', 'ENABLED')
- : this.core.renderBadge('error', 'DISABLED');
-
- return `
-
- | ${this.core.escapeHtml(service.name)} |
- ${this.core.escapeHtml(service.service_name || 'Custom')} |
- ${this.core.escapeHtml(service.domain || 'N/A')} |
- ${statusBadge} |
-
- `;
- }
-
- renderDDNSTable(services) {
- return services.map(service => this.renderDDNSRow(service)).join('');
- }
-
- updateDDNSTable(services) {
- const tbody = document.querySelector('#ddns-table tbody');
- if (!tbody) return;
-
- if (services.length === 0) {
- this.core.renderEmptyTable(tbody, 4, 'No DDNS services configured');
- return;
- }
-
- tbody.innerHTML = this.renderDDNSTable(services);
- }
-
- async loadDDNS() {
- if (!this.core.isFeatureEnabled('ddns')) return;
-
- try {
- const config = await this.fetchDDNSConfig();
- const services = this.parseDDNSServices(config);
- this.updateDDNSTable(services);
- } catch (err) {
- console.error('Failed to load DDNS:', err);
- const tbody = document.querySelector('#ddns-table tbody');
- if (tbody) {
- this.core.renderEmptyTable(tbody, 4, 'DDNS not configured');
- }
- }
- }
-}
diff --git a/moci/js/modules/system.js b/moci/js/modules/system.js
index e1b0bc3..19801a5 100644
--- a/moci/js/modules/system.js
+++ b/moci/js/modules/system.js
@@ -34,17 +34,30 @@ export default class SystemModule {
}
setupHandlers() {
- document.getElementById('save-general-btn')?.addEventListener('click', () => this.saveGeneral());
- document.getElementById('change-password-btn')?.addEventListener('click', () => this.changePassword());
- document.getElementById('backup-btn')?.addEventListener('click', () => this.createBackup());
- document.getElementById('reset-btn')?.addEventListener('click', () => this.factoryReset());
- document.getElementById('reboot-btn')?.addEventListener('click', () => this.rebootSystem());
- document
- .getElementById('restart-network-btn')
- ?.addEventListener('click', () => this.core.serviceReload('network'));
- document
- .getElementById('restart-firewall-btn')
- ?.addEventListener('click', () => this.core.serviceReload('firewall'));
+ const buttons = {
+ 'save-general-btn': () => this.saveGeneral(),
+ 'change-password-btn': () => this.changePassword(),
+ 'backup-btn': () => this.createBackup(),
+ 'reset-btn': () => this.factoryReset(),
+ 'reboot-btn': () => this.rebootSystem(),
+ 'restart-network-btn': () => this.core.serviceReload('network'),
+ 'restart-firewall-btn': () => this.core.serviceReload('firewall'),
+ 'add-cron-btn': () => {
+ this.core.resetModal('cron-modal');
+ this.core.openModal('cron-modal');
+ },
+ 'add-ssh-key-btn': () => {
+ this.core.resetModal('ssh-key-modal');
+ document.getElementById('parsed-keys-preview').style.display = 'none';
+ document.getElementById('save-ssh-keys-btn').style.display = 'none';
+ this.core.openModal('ssh-key-modal');
+ },
+ 'parse-keys-btn': () => this.parseSSHKeyInput()
+ };
+
+ for (const [id, handler] of Object.entries(buttons)) {
+ document.getElementById(id)?.addEventListener('click', handler);
+ }
this.core.setupModal({
modalId: 'cron-modal',
@@ -62,35 +75,16 @@ export default class SystemModule {
saveHandler: () => this.saveSSHKeys()
});
- document.getElementById('add-cron-btn')?.addEventListener('click', () => {
- this.core.resetModal('cron-modal');
- this.core.openModal('cron-modal');
- });
-
- document.getElementById('add-ssh-key-btn')?.addEventListener('click', () => {
- this.core.resetModal('ssh-key-modal');
- document.getElementById('parsed-keys-preview').style.display = 'none';
- document.getElementById('save-ssh-keys-btn').style.display = 'none';
- this.core.openModal('ssh-key-modal');
- });
-
- document.getElementById('parse-keys-btn')?.addEventListener('click', () => this.parseSSHKeyInput());
-
- const cronCleanup = this.core.delegateActions('cron-table', {
- edit: id => this.editCronEntry(id),
- delete: id => this.deleteCronEntry(id)
- });
- if (cronCleanup) this.cleanups.push(cronCleanup);
-
- const sshCleanup = this.core.delegateActions('ssh-keys-table', {
- delete: id => this.deleteSSHKey(id)
- });
- if (sshCleanup) this.cleanups.push(sshCleanup);
+ const delegations = {
+ 'cron-table': { edit: id => this.editCronEntry(id), delete: id => this.deleteCronEntry(id) },
+ 'ssh-keys-table': { delete: id => this.deleteSSHKey(id) },
+ 'services-table': { toggle: id => this.toggleService(id) }
+ };
- const servicesCleanup = this.core.delegateActions('services-table', {
- toggle: id => this.toggleService(id)
- });
- if (servicesCleanup) this.cleanups.push(servicesCleanup);
+ for (const [tableId, handlers] of Object.entries(delegations)) {
+ const cleanup = this.core.delegateActions(tableId, handlers);
+ if (cleanup) this.cleanups.push(cleanup);
+ }
this.setupFirmwareUpload();
}
@@ -179,7 +173,7 @@ export default class SystemModule {
async createBackup() {
try {
- const [s, r] = await this.core.ubusCall(
+ const [s] = await this.core.ubusCall(
'file',
'exec',
{ command: '/sbin/sysupgrade', params: ['--create-backup', '/tmp/backup.tar.gz'] },
@@ -218,10 +212,7 @@ export default class SystemModule {
if (!confirm('This will erase all settings and restore factory defaults. Continue?')) return;
if (!confirm('This action cannot be undone. Are you absolutely sure?')) return;
try {
- await this.core.ubusCall('file', 'exec', {
- command: '/sbin/firstboot',
- params: ['-y']
- });
+ await this.core.ubusCall('file', 'exec', { command: '/sbin/firstboot', params: ['-y'] });
this.core.showToast('Factory reset initiated, rebooting...', 'success');
setTimeout(async () => {
try {
@@ -247,9 +238,7 @@ export default class SystemModule {
async loadPackages() {
await this.core.loadResource('packages-table', 3, 'packages', async () => {
- const [status, result] = await this.core.ubusCall('file', 'read', {
- path: '/usr/lib/opkg/status'
- });
+ const [status, result] = await this.core.ubusCall('file', 'read', { path: '/usr/lib/opkg/status' });
if (status !== 0 || !result?.data) throw new Error('No data');
const packages = [];
@@ -262,12 +251,6 @@ export default class SystemModule {
if (pkg.name) packages.push(pkg);
}
- const tbody = document.querySelector('#packages-table tbody');
- if (!tbody) return;
- if (packages.length === 0) {
- this.core.renderEmptyTable(tbody, 3, 'No packages found');
- return;
- }
const display = packages.slice(0, 100);
let html = display
.map(
@@ -281,7 +264,14 @@ export default class SystemModule {
if (packages.length > 100) {
html += `| Showing 100 of ${packages.length} packages |
`;
}
- tbody.innerHTML = html;
+ const tbody = document.querySelector('#packages-table tbody');
+ if (tbody) {
+ if (packages.length === 0) {
+ this.core.renderEmptyTable(tbody, 3, 'No packages found');
+ } else {
+ tbody.innerHTML = html;
+ }
+ }
});
}
@@ -290,20 +280,17 @@ export default class SystemModule {
const [status, result] = await this.core.ubusCall('service', 'list', {});
if (status !== 0 || !result) throw new Error('No data');
- const services = Object.entries(result).map(([name, info]) => {
- const running = info.instances && Object.keys(info.instances).length > 0;
- return { name, running };
- });
-
- const tbody = document.querySelector('#services-table tbody');
- if (!tbody) return;
- if (services.length === 0) {
- this.core.renderEmptyTable(tbody, 4, 'No services found');
- return;
- }
- tbody.innerHTML = services
- .map(
- s => `
+ const services = Object.entries(result).map(([name, info]) => ({
+ name,
+ running: info.instances && Object.keys(info.instances).length > 0
+ }));
+
+ this.core.renderTable(
+ '#services-table',
+ services,
+ 4,
+ 'No services found',
+ s => `
| ${this.core.escapeHtml(s.name)} |
${s.running ? this.core.renderBadge('success', 'RUNNING') : this.core.renderBadge('error', 'STOPPED')} |
${this.core.renderBadge('info', 'N/A')} |
@@ -315,8 +302,7 @@ export default class SystemModule {
`
- )
- .join('');
+ );
});
}
@@ -346,39 +332,31 @@ export default class SystemModule {
async loadCron() {
await this.core.loadResource('cron-table', 4, null, async () => {
try {
- const [s, r] = await this.core.ubusCall('file', 'read', {
- path: '/etc/crontabs/root'
- });
- if (s === 0 && r?.data) this.cronRaw = r.data;
- else this.cronRaw = '';
+ const [s, r] = await this.core.ubusCall('file', 'read', { path: '/etc/crontabs/root' });
+ this.cronRaw = s === 0 && r?.data ? r.data : '';
} catch {
this.cronRaw = '';
}
const entries = this.parseCron(this.cronRaw);
- const tbody = document.querySelector('#cron-table tbody');
- if (!tbody) return;
- if (entries.length === 0) {
- this.core.renderEmptyTable(tbody, 4, 'No scheduled tasks');
- return;
- }
- tbody.innerHTML = entries
- .map(
- (e, i) => `
+ this.core.renderTable(
+ '#cron-table',
+ entries,
+ 4,
+ 'No scheduled tasks',
+ (e, i) => `
| ${this.core.escapeHtml(e.schedule)} |
${this.core.escapeHtml(e.command)} |
${e.enabled ? this.core.renderBadge('success', 'ENABLED') : this.core.renderBadge('error', 'DISABLED')} |
${this.core.renderActionButtons(String(i))} |
`
- )
- .join('');
+ );
});
}
parseCron(data) {
const result = [];
- const lines = data.split('\n');
- lines.forEach((line, rawIndex) => {
+ data.split('\n').forEach((line, rawIndex) => {
if (!line.trim()) return;
const enabled = !line.trim().startsWith('#');
const clean = line.replace(/^#\s*/, '').trim();
@@ -431,9 +409,9 @@ export default class SystemModule {
const newLine = `${enabled ? '' : '# '}${minute} ${hour} ${day} ${month} ${weekday} ${command}`;
const lines = this.cronRaw.split('\n');
+ const entries = this.parseCron(this.cronRaw);
if (index !== '') {
- const entries = this.parseCron(this.cronRaw);
const entry = entries[parseInt(index)];
if (entry) lines[entry.rawIndex] = newLine;
} else {
@@ -476,25 +454,19 @@ export default class SystemModule {
async loadSSHKeys() {
await this.core.loadResource('ssh-keys-table', 4, 'ssh_keys', async () => {
try {
- const [s, r] = await this.core.ubusCall('file', 'read', {
- path: '/etc/dropbear/authorized_keys'
- });
- if (s === 0 && r?.data) this.sshKeysRaw = r.data;
- else this.sshKeysRaw = '';
+ const [s, r] = await this.core.ubusCall('file', 'read', { path: '/etc/dropbear/authorized_keys' });
+ this.sshKeysRaw = s === 0 && r?.data ? r.data : '';
} catch {
this.sshKeysRaw = '';
}
const keys = this.parseSSHKeys(this.sshKeysRaw);
- const tbody = document.querySelector('#ssh-keys-table tbody');
- if (!tbody) return;
- if (keys.length === 0) {
- this.core.renderEmptyTable(tbody, 4, 'No SSH keys');
- return;
- }
- tbody.innerHTML = keys
- .map(
- (k, i) => `
+ this.core.renderTable(
+ '#ssh-keys-table',
+ keys,
+ 4,
+ 'No SSH keys',
+ (k, i) => `
| ${this.core.escapeHtml(k.type)} |
${this.core.escapeHtml(k.key.substring(0, 30))}... |
${this.core.escapeHtml(k.comment || 'N/A')} |
@@ -506,15 +478,13 @@ export default class SystemModule {
`
- )
- .join('');
+ );
});
}
parseSSHKeys(data) {
const result = [];
- const lines = data.split('\n');
- lines.forEach((line, rawIndex) => {
+ data.split('\n').forEach((line, rawIndex) => {
if (!line.trim() || line.startsWith('#')) return;
const parts = line.trim().split(/\s+/);
if (!parts[1]) return;
@@ -544,10 +514,8 @@ export default class SystemModule {
list.innerHTML = keys
.map(
- (
- k,
- i
- ) => `
+ (k, i) =>
+ `
${this.core.escapeHtml(k.type)} ${this.core.escapeHtml(k.comment || 'No comment')}
@@ -610,27 +578,24 @@ export default class SystemModule {
async loadMounts() {
await this.core.loadResource('mounts-table', 6, 'storage', async () => {
- const [s, r] = await this.core.ubusCall('file', 'exec', {
- command: '/bin/df',
- params: ['-h']
- });
+ const [s, r] = await this.core.ubusCall('file', 'exec', { command: '/bin/df', params: ['-h'] });
if (s !== 0 || !r?.stdout) throw new Error('No data');
- const lines = r.stdout
+ const mounts = r.stdout
.split('\n')
.slice(1)
- .filter(l => l.trim());
- const mounts = lines.map(line => {
- const parts = line.trim().split(/\s+/);
- return {
- device: parts[0],
- size: parts[1],
- used: parts[2],
- available: parts[3],
- usePercent: parts[4],
- mountPoint: parts[5]
- };
- });
+ .filter(l => l.trim())
+ .map(line => {
+ const parts = line.trim().split(/\s+/);
+ return {
+ device: parts[0],
+ size: parts[1],
+ used: parts[2],
+ available: parts[3],
+ usePercent: parts[4],
+ mountPoint: parts[5]
+ };
+ });
const charts = document.getElementById('storage-charts');
if (charts) {
@@ -648,15 +613,12 @@ export default class SystemModule {
.join('');
}
- const tbody = document.querySelector('#mounts-table tbody');
- if (!tbody) return;
- if (mounts.length === 0) {
- this.core.renderEmptyTable(tbody, 6, 'No mount points');
- return;
- }
- tbody.innerHTML = mounts
- .map(
- m => `
+ this.core.renderTable(
+ '#mounts-table',
+ mounts,
+ 6,
+ 'No mount points',
+ m => `
| ${this.core.escapeHtml(m.device)} |
${this.core.escapeHtml(m.mountPoint)} |
N/A |
@@ -664,8 +626,7 @@ export default class SystemModule {
${this.core.escapeHtml(m.used)} |
${this.core.escapeHtml(m.available)} |
`
- )
- .join('');
+ );
});
}
@@ -674,32 +635,24 @@ export default class SystemModule {
const [status, result] = await this.core.uciGet('system');
if (status !== 0 || !result?.values) throw new Error('No data');
- const leds = Object.entries(result.values)
- .filter(([, v]) => v['.type'] === 'led')
- .map(([k, v]) => ({ section: k, ...v }));
-
- const tbody = document.querySelector('#led-table tbody');
- if (!tbody) return;
- if (leds.length === 0) {
- this.core.renderEmptyTable(tbody, 3, 'No LEDs configured');
- return;
- }
- tbody.innerHTML = leds
- .map(
- l => `
+ const leds = this.core.filterUciSections(result.values, 'led');
+ this.core.renderTable(
+ '#led-table',
+ leds,
+ 3,
+ 'No LEDs configured',
+ l => `
| ${this.core.escapeHtml(l.sysfs || l.section)} |
${this.core.escapeHtml(l.trigger || 'default-on')} |
${this.core.renderBadge('info', 'CONFIGURED')} |
`
- )
- .join('');
+ );
});
}
setupFirmwareUpload() {
const fileInput = document.getElementById('firmware-file');
const uploadArea = document.getElementById('file-upload-area');
- const uploadText = document.getElementById('file-upload-text');
const validateBtn = document.getElementById('validate-firmware-btn');
const flashBtn = document.getElementById('flash-firmware-btn');
@@ -719,15 +672,11 @@ export default class SystemModule {
uploadArea.addEventListener('drop', e => {
e.preventDefault();
uploadArea.style.borderColor = 'var(--slate-border)';
- if (e.dataTransfer.files.length) {
- this.handleFirmwareFile(e.dataTransfer.files[0]);
- }
+ if (e.dataTransfer.files.length) this.handleFirmwareFile(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', () => {
- if (fileInput.files.length) {
- this.handleFirmwareFile(fileInput.files[0]);
- }
+ if (fileInput.files.length) this.handleFirmwareFile(fileInput.files[0]);
});
validateBtn?.addEventListener('click', () => this.validateFirmware());
diff --git a/moci/js/modules/vpn.js b/moci/js/modules/vpn.js
deleted file mode 100644
index 060b86b..0000000
--- a/moci/js/modules/vpn.js
+++ /dev/null
@@ -1,73 +0,0 @@
-export default class VPNModule {
- constructor(core) {
- this.core = core;
- }
-
- async fetchNetworkConfig() {
- const [status, result] = await this.core.uciGet('network');
-
- if (status !== 0 || !result?.values) {
- throw new Error('Failed to fetch network config');
- }
-
- return result.values;
- }
-
- parseWireGuardInterfaces(config) {
- return Object.entries(config)
- .filter(([key, val]) => val.proto === 'wireguard')
- .map(([key, val]) => ({
- name: key,
- ...val
- }));
- }
-
- renderWireGuardRow(iface) {
- const enabled = iface.disabled !== '1';
- const statusBadge = enabled
- ? this.core.renderBadge('success', 'ENABLED')
- : this.core.renderBadge('error', 'DISABLED');
-
- return `
-
- | ${this.core.escapeHtml(iface.name)} |
- ${this.core.escapeHtml(iface.private_key?.substring(0, 20) || 'N/A')}... |
- ${this.core.escapeHtml(iface.listen_port || 'N/A')} |
- ${statusBadge} |
-
- `;
- }
-
- renderWireGuardTable(interfaces) {
- return interfaces.map(iface => this.renderWireGuardRow(iface)).join('');
- }
-
- updateWireGuardTable(interfaces) {
- const tbody = document.querySelector('#wireguard-table tbody');
- if (!tbody) return;
-
- if (interfaces.length === 0) {
- this.core.renderEmptyTable(tbody, 4, 'No WireGuard interfaces configured');
- return;
- }
-
- tbody.innerHTML = this.renderWireGuardTable(interfaces);
- }
-
- async loadWireGuard() {
- if (!this.core.isFeatureEnabled('wireguard')) return;
-
- try {
- const config = await this.fetchNetworkConfig();
- const interfaces = this.parseWireGuardInterfaces(config);
- this.updateWireGuardTable(interfaces);
- } catch (err) {
- console.error('Failed to load WireGuard config:', err);
- this.core.showToast('Failed to load WireGuard configuration', 'error');
- const tbody = document.querySelector('#wireguard-table tbody');
- if (tbody) {
- this.core.renderEmptyTable(tbody, 4, 'Failed to load WireGuard configuration');
- }
- }
- }
-}
diff --git a/scripts/build.js b/scripts/build.js
index d7eebd4..527def8 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -12,9 +12,7 @@ async function buildJS() {
'moci/js/core.js',
'moci/js/modules/dashboard.js',
'moci/js/modules/network.js',
- 'moci/js/modules/system.js',
- 'moci/js/modules/vpn.js',
- 'moci/js/modules/services.js'
+ 'moci/js/modules/system.js'
];
await mkdir(join(distDir, 'js/modules'), { recursive: true });
diff --git a/scripts/watch.js b/scripts/watch.js
index 00878f1..d4a0fa5 100644
--- a/scripts/watch.js
+++ b/scripts/watch.js
@@ -85,10 +85,6 @@ function deploy() {
execSync(`cat moci/js/modules/system.js | ${SSH} "cat > /www/moci/js/modules/system.js"`, {
stdio: 'pipe'
});
- execSync(`cat moci/js/modules/vpn.js | ${SSH} "cat > /www/moci/js/modules/vpn.js"`, { stdio: 'pipe' });
- execSync(`cat moci/js/modules/services.js | ${SSH} "cat > /www/moci/js/modules/services.js"`, {
- stdio: 'pipe'
- });
console.log('Deployed successfully\n');
} catch (err) {