From eb60e6edec39a220c77da811e858f95248a110ed Mon Sep 17 00:00:00 2001 From: divocat Date: Sun, 5 Oct 2025 16:12:56 +0300 Subject: [PATCH] fix: run prettier for luci app js assets --- .../resources/view/podkop/additionalTab.js | 501 +++-- .../resources/view/podkop/configSection.js | 1191 ++++++----- .../resources/view/podkop/diagnosticTab.js | 1803 ++++++++++------- .../resources/view/podkop/utils.js | 205 +- 4 files changed, 2216 insertions(+), 1484 deletions(-) diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js index 501de00..2686fb0 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js @@ -5,235 +5,358 @@ 'require view.podkop.main as main'; function createAdditionalSection(mainSection) { - let o = mainSection.tab('additional', _('Additional Settings')); + let o = mainSection.tab('additional', _('Additional Settings')); - o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), `${main.getBaseUrl()}:9090/ui`); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.Flag, + 'yacd', + _('Yacd enable'), + `${main.getBaseUrl()}:9090/ui`, + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('Allows you to exclude NTP protocol traffic from the tunnel')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.Flag, + 'exclude_ntp', + _('Exclude NTP'), + _('Allows you to exclude NTP protocol traffic from the tunnel'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('additional', form.Flag, 'quic_disable', _('QUIC disable'), _('For issues with the video stream')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.Flag, + 'quic_disable', + _('QUIC disable'), + _('For issues with the video stream'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('additional', form.ListValue, 'update_interval', _('List Update Frequency'), _('Select how often the lists will be updated')); - Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '1d'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.ListValue, + 'update_interval', + _('List Update Frequency'), + _('Select how often the lists will be updated'), + ); + Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = '1d'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('additional', form.ListValue, 'dns_type', _('DNS Protocol Type'), _('Select DNS protocol to use')); - o.value('doh', _('DNS over HTTPS (DoH)')); - o.value('dot', _('DNS over TLS (DoT)')); - o.value('udp', _('UDP (Unprotected DNS)')); - o.default = 'udp'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.ListValue, + 'dns_type', + _('DNS Protocol Type'), + _('Select DNS protocol to use'), + ); + o.value('doh', _('DNS over HTTPS (DoH)')); + o.value('dot', _('DNS over TLS (DoT)')); + o.value('udp', _('UDP (Unprotected DNS)')); + o.default = 'udp'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('additional', form.Value, 'dns_server', _('DNS Server'), _('Select or enter DNS server address')); - Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '8.8.8.8'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); + o = mainSection.taboption( + 'additional', + form.Value, + 'dns_server', + _('DNS Server'), + _('Select or enter DNS server address'), + ); + Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = '8.8.8.8'; + o.rmempty = false; + o.ucisection = 'main'; + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); - if (validation.valid) { - return true; - } + if (validation.valid) { + return true; + } - return _(validation.message); - }; + return _(validation.message); + }; - o = mainSection.taboption('additional', form.Value, 'bootstrap_dns_server', _('Bootstrap DNS server'), _('The DNS server used to look up the IP address of an upstream DNS server')); - Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '77.88.8.8'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); + o = mainSection.taboption( + 'additional', + form.Value, + 'bootstrap_dns_server', + _('Bootstrap DNS server'), + _( + 'The DNS server used to look up the IP address of an upstream DNS server', + ), + ); + Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = '77.88.8.8'; + o.rmempty = false; + o.ucisection = 'main'; + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); - if (validation.valid) { - return true; - } + if (validation.valid) { + return true; + } - return _(validation.message); - }; + return _(validation.message); + }; - o = mainSection.taboption('additional', form.Value, 'dns_rewrite_ttl', _('DNS Rewrite TTL'), _('Time in seconds for DNS record caching (default: 60)')); - o.default = '60'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - if (!value) { - return _('TTL value cannot be empty'); - } + o = mainSection.taboption( + 'additional', + form.Value, + 'dns_rewrite_ttl', + _('DNS Rewrite TTL'), + _('Time in seconds for DNS record caching (default: 60)'), + ); + o.default = '60'; + o.rmempty = false; + o.ucisection = 'main'; + o.validate = function (section_id, value) { + if (!value) { + return _('TTL value cannot be empty'); + } - const ttl = parseInt(value); - if (isNaN(ttl) || ttl < 0) { - return _('TTL must be a positive number'); - } + const ttl = parseInt(value); + if (isNaN(ttl) || ttl < 0) { + return _('TTL must be a positive number'); + } - return true; - }; + return true; + }; - o = mainSection.taboption('additional', form.ListValue, 'config_path', _('Config File Path'), _('Select path for sing-box config file. Change this ONLY if you know what you are doing')); - o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)'); - o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)'); - o.default = '/etc/sing-box/config.json'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.ListValue, + 'config_path', + _('Config File Path'), + _( + 'Select path for sing-box config file. Change this ONLY if you know what you are doing', + ), + ); + o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)'); + o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)'); + o.default = '/etc/sing-box/config.json'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('additional', form.Value, 'cache_path', _('Cache File Path'), _('Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing')); - o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)'); - o.value('/usr/share/sing-box/cache.db', 'Flash (/usr/share/sing-box/cache.db)'); - o.default = '/tmp/sing-box/cache.db'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - if (!value) { - return _('Cache file path cannot be empty'); - } + o = mainSection.taboption( + 'additional', + form.Value, + 'cache_path', + _('Cache File Path'), + _( + 'Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing', + ), + ); + o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)'); + o.value( + '/usr/share/sing-box/cache.db', + 'Flash (/usr/share/sing-box/cache.db)', + ); + o.default = '/tmp/sing-box/cache.db'; + o.rmempty = false; + o.ucisection = 'main'; + o.validate = function (section_id, value) { + if (!value) { + return _('Cache file path cannot be empty'); + } - if (!value.startsWith('/')) { - return _('Path must be absolute (start with /)'); - } + if (!value.startsWith('/')) { + return _('Path must be absolute (start with /)'); + } - if (!value.endsWith('cache.db')) { - return _('Path must end with cache.db'); - } + if (!value.endsWith('cache.db')) { + return _('Path must end with cache.db'); + } - const parts = value.split('/').filter(Boolean); - if (parts.length < 2) { - return _('Path must contain at least one directory (like /tmp/cache.db)'); - } + const parts = value.split('/').filter(Boolean); + if (parts.length < 2) { + return _('Path must contain at least one directory (like /tmp/cache.db)'); + } - return true; - }; + return true; + }; - o = mainSection.taboption('additional', widgets.DeviceSelect, 'iface', _('Source Network Interface'), _('Select the network interface from which the traffic will originate')); - o.ucisection = 'main'; - o.default = 'br-lan'; - o.noaliases = true; - o.nobridges = false; - o.noinactive = false; - o.multiple = true; - o.filter = function (section_id, value) { - // Block specific interface names from being selectable - const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']; - if (blocked.includes(value)) { - return false; - } + o = mainSection.taboption( + 'additional', + widgets.DeviceSelect, + 'iface', + _('Source Network Interface'), + _('Select the network interface from which the traffic will originate'), + ); + o.ucisection = 'main'; + o.default = 'br-lan'; + o.noaliases = true; + o.nobridges = false; + o.noinactive = false; + o.multiple = true; + o.filter = function (section_id, value) { + // Block specific interface names from being selectable + const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']; + if (blocked.includes(value)) { + return false; + } - // Try to find the device object by its name - const device = this.devices.find(dev => dev.getName() === value); + // Try to find the device object by its name + const device = this.devices.find((dev) => dev.getName() === value); - // If no device is found, allow the value - if (!device) { - return true; - } + // If no device is found, allow the value + if (!device) { + return true; + } - // Check the type of the device - const type = device.getType(); + // Check the type of the device + const type = device.getType(); - // Consider any Wi-Fi / wireless / wlan device as invalid - const isWireless = - type === 'wifi' || type === 'wireless' || type.includes('wlan'); + // Consider any Wi-Fi / wireless / wlan device as invalid + const isWireless = + type === 'wifi' || type === 'wireless' || type.includes('wlan'); - // Allow only non-wireless devices - return !isWireless; - }; + // Allow only non-wireless devices + return !isWireless; + }; - o = mainSection.taboption('additional', form.Flag, 'mon_restart_ifaces', _('Interface monitoring'), _('Interface monitoring for bad WAN')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.Flag, + 'mon_restart_ifaces', + _('Interface monitoring'), + _('Interface monitoring for bad WAN'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('additional', widgets.NetworkSelect, 'restart_ifaces', _('Interface for monitoring'), _('Select the WAN interfaces to be monitored')); - o.ucisection = 'main'; - o.depends('mon_restart_ifaces', '1'); - o.multiple = true; - o.filter = function (section_id, value) { - // Reject if the value is in the blocked list ['lan', 'loopback'] - if (['lan', 'loopback'].includes(value)) { - return false; - } + o = mainSection.taboption( + 'additional', + widgets.NetworkSelect, + 'restart_ifaces', + _('Interface for monitoring'), + _('Select the WAN interfaces to be monitored'), + ); + o.ucisection = 'main'; + o.depends('mon_restart_ifaces', '1'); + o.multiple = true; + o.filter = function (section_id, value) { + // Reject if the value is in the blocked list ['lan', 'loopback'] + if (['lan', 'loopback'].includes(value)) { + return false; + } - // Reject if the value starts with '@' (means it's an alias/reference) - if (value.startsWith('@')) { - return false; - } + // Reject if the value starts with '@' (means it's an alias/reference) + if (value.startsWith('@')) { + return false; + } - // Otherwise allow it - return true; - }; + // Otherwise allow it + return true; + }; - o = mainSection.taboption('additional', form.Value, 'procd_reload_delay', _('Interface Monitoring Delay'), _('Delay in milliseconds before reloading podkop after interface UP')); - o.ucisection = 'main'; - o.depends('mon_restart_ifaces', '1'); - o.default = '2000'; - o.rmempty = false; - o.validate = function (section_id, value) { - if (!value) { - return _('Delay value cannot be empty'); - } - return true; - }; + o = mainSection.taboption( + 'additional', + form.Value, + 'procd_reload_delay', + _('Interface Monitoring Delay'), + _('Delay in milliseconds before reloading podkop after interface UP'), + ); + o.ucisection = 'main'; + o.depends('mon_restart_ifaces', '1'); + o.default = '2000'; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value) { + return _('Delay value cannot be empty'); + } + return true; + }; - o = mainSection.taboption('additional', form.Flag, 'dont_touch_dhcp', _('Dont touch my DHCP!'), _('Podkop will not change the DHCP config')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.Flag, + 'dont_touch_dhcp', + _('Dont touch my DHCP!'), + _('Podkop will not change the DHCP config'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('additional', form.Flag, 'detour', _('Proxy download of lists'), _('Downloading all lists via main Proxy/VPN')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'additional', + form.Flag, + 'detour', + _('Proxy download of lists'), + _('Downloading all lists via main Proxy/VPN'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; - // Extra IPs and exclusions (main section) - o = mainSection.taboption('basic', form.Flag, 'exclude_from_ip_enabled', _('IP for exclusion'), _('Specify local IP addresses that will never use the configured route')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; + // Extra IPs and exclusions (main section) + o = mainSection.taboption( + 'basic', + form.Flag, + 'exclude_from_ip_enabled', + _('IP for exclusion'), + _('Specify local IP addresses that will never use the configured route'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; - o = mainSection.taboption('basic', form.DynamicList, 'exclude_traffic_ip', _('Local IPs'), _('Enter valid IPv4 addresses')); - o.placeholder = 'IP'; - o.depends('exclude_from_ip_enabled', '1'); - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } + o = mainSection.taboption( + 'basic', + form.DynamicList, + 'exclude_traffic_ip', + _('Local IPs'), + _('Enter valid IPv4 addresses'), + ); + o.placeholder = 'IP'; + o.depends('exclude_from_ip_enabled', '1'); + o.rmempty = false; + o.ucisection = 'main'; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } - const validation = main.validateIPV4(value); + const validation = main.validateIPV4(value); - if (validation.valid) { - return true; - } + if (validation.valid) { + return true; + } - return _(validation.message) - }; + return _(validation.message); + }; - o = mainSection.taboption('basic', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080')); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; + o = mainSection.taboption( + 'basic', + form.Flag, + 'socks5', + _('Mixed enable'), + _('Browser port: 2080'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = 'main'; } return baseclass.extend({ - createAdditionalSection + createAdditionalSection, }); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js index 0c215c6..08d8fae 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js @@ -6,541 +6,772 @@ 'require view.podkop.main as main'; 'require tools.widgets as widgets'; - function createConfigSection(section) { - const s = section; + const s = section; - let o = s.tab('basic', _('Basic Settings')); + let o = s.tab('basic', _('Basic Settings')); - o = s.taboption('basic', form.ListValue, 'mode', _('Connection Type'), _('Select between VPN and Proxy connection methods for traffic routing')); - o.value('proxy', ('Proxy')); - o.value('vpn', ('VPN')); - o.value('block', ('Block')); - o.ucisection = s.section; + o = s.taboption( + 'basic', + form.ListValue, + 'mode', + _('Connection Type'), + _('Select between VPN and Proxy connection methods for traffic routing'), + ); + o.value('proxy', 'Proxy'); + o.value('vpn', 'VPN'); + o.value('block', 'Block'); + o.ucisection = s.section; - o = s.taboption('basic', form.ListValue, 'proxy_config_type', _('Configuration Type'), _('Select how to configure the proxy')); - o.value('url', _('Connection URL')); - o.value('outbound', _('Outbound Config')); - o.value('urltest', _('URLTest')); - o.default = 'url'; - o.depends('mode', 'proxy'); - o.ucisection = s.section; + o = s.taboption( + 'basic', + form.ListValue, + 'proxy_config_type', + _('Configuration Type'), + _('Select how to configure the proxy'), + ); + o.value('url', _('Connection URL')); + o.value('outbound', _('Outbound Config')); + o.value('urltest', _('URLTest')); + o.default = 'url'; + o.depends('mode', 'proxy'); + o.ucisection = s.section; - o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), ''); - o.depends('proxy_config_type', 'url'); - o.rows = 5; - o.wrap = 'soft'; - o.textarea = true; - o.rmempty = false; - o.ucisection = s.section; - o.sectionDescriptions = new Map(); - o.placeholder = 'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none'; + o = s.taboption( + 'basic', + form.TextValue, + 'proxy_string', + _('Proxy Configuration URL'), + '', + ); + o.depends('proxy_config_type', 'url'); + o.rows = 5; + o.wrap = 'soft'; + o.textarea = true; + o.rmempty = false; + o.ucisection = s.section; + o.sectionDescriptions = new Map(); + o.placeholder = + 'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none'; - o.renderWidget = function (section_id, option_index, cfgvalue) { - const original = form.TextValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]); - const container = E('div', {}); - container.appendChild(original); + o.renderWidget = function (section_id, option_index, cfgvalue) { + const original = form.TextValue.prototype.renderWidget.apply(this, [ + section_id, + option_index, + cfgvalue, + ]); + const container = E('div', {}); + container.appendChild(original); - if (cfgvalue) { - try { - const activeConfig = cfgvalue.split('\n') - .map(line => line.trim()) - .find(line => line && !line.startsWith('//')); + if (cfgvalue) { + try { + const activeConfig = cfgvalue + .split('\n') + .map((line) => line.trim()) + .find((line) => line && !line.startsWith('//')); - if (activeConfig) { - if (activeConfig.includes('#')) { - const label = activeConfig.split('#').pop(); - if (label && label.trim()) { - const decodedLabel = decodeURIComponent(label); - const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel); - container.appendChild(descDiv); - } else { - const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); - container.appendChild(descDiv); - } - } else { - const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); - container.appendChild(descDiv); - } - } - } catch (e) { - console.error('Error parsing config label:', e); - const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); - container.appendChild(descDiv); + if (activeConfig) { + if (activeConfig.includes('#')) { + const label = activeConfig.split('#').pop(); + if (label && label.trim()) { + const decodedLabel = decodeURIComponent(label); + const descDiv = E( + 'div', + { class: 'cbi-value-description' }, + _('Current config: ') + decodedLabel, + ); + container.appendChild(descDiv); + } else { + const descDiv = E( + 'div', + { class: 'cbi-value-description' }, + _('Config without description'), + ); + container.appendChild(descDiv); } - } else { - const defaultDesc = E('div', { 'class': 'cbi-value-description' }, - _('Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs')); - container.appendChild(defaultDesc); - } - - return container; - }; - - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } - - try { - const activeConfig = value.split('\n') - .map(line => line.trim()) - .find(line => line && !line.startsWith('//')); - - if (!activeConfig) { - return _('No active configuration found. At least one non-commented line is required.'); - } - - const validation = main.validateProxyUrl(activeConfig); - - if (validation.valid) { - return true; - } - - return _(validation.message) - } catch (e) { - return `${_('Invalid URL format:')} ${e?.message}`; - } - }; - - o = s.taboption('basic', form.TextValue, 'outbound_json', _('Outbound Configuration'), _('Enter complete outbound configuration in JSON format')); - o.depends('proxy_config_type', 'outbound'); - o.rows = 10; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } - - const validation = main.validateOutboundJson(value); - - if (validation.valid) { - return true; - } - - return _(validation.message) - }; - - o = s.taboption('basic', form.DynamicList, 'urltest_proxy_links', _('URLTest Proxy Links')); - o.depends('proxy_config_type', 'urltest'); - o.placeholder = 'vless://, ss://, trojan:// links'; - o.rmempty = false; - o.textarea = true; - o.rows = 3; - o.wrap = 'soft'; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } - - const validation = main.validateProxyUrl(value); - - if (validation.valid) { - return true; - } - - return _(validation.message) - }; - - o = s.taboption('basic', form.Flag, 'ss_uot', _('Shadowsocks UDP over TCP'), _('Apply for SS2022')); - o.default = '0'; - o.depends('mode', 'proxy'); - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption('basic', widgets.DeviceSelect, 'interface', _('Network Interface'), _('Select network interface for VPN connection')); - o.depends('mode', 'vpn'); - o.ucisection = s.section; - o.noaliases = true; - o.nobridges = false; - o.noinactive = false; - o.filter = function (section_id, value) { - // Blocked interface names that should never be selectable - const blockedInterfaces = [ - 'br-lan', - 'eth0', - 'eth1', - 'wan', - 'phy0-ap0', - 'phy1-ap0', - 'pppoe-wan', - 'lan', - ]; - - // Reject immediately if the value matches any blocked interface - if (blockedInterfaces.includes(value)) { - return false; - } - - // Try to find the device object with the given name - const device = this.devices.find(dev => dev.getName() === value); - - // If no device is found, allow the value - if (!device) { - return true; - } - - // Get the device type (e.g., "wifi", "ethernet", etc.) - const type = device.getType(); - - // Reject wireless-related devices - const isWireless = - type === 'wifi' || type === 'wireless' || type.includes('wlan'); - - return !isWireless; - }; - - o = s.taboption('basic', form.Flag, 'domain_resolver_enabled', _('Domain Resolver'), _('Enable built-in DNS resolver for domains handled by this section')); - o.default = '0'; - o.rmempty = false; - o.depends('mode', 'vpn'); - o.ucisection = s.section; - - o = s.taboption('basic', form.ListValue, 'domain_resolver_dns_type', _('DNS Protocol Type'), _('Select the DNS protocol type for the domain resolver')); - o.value('doh', _('DNS over HTTPS (DoH)')); - o.value('dot', _('DNS over TLS (DoT)')); - o.value('udp', _('UDP (Unprotected DNS)')); - o.default = 'udp'; - o.rmempty = false; - o.depends('domain_resolver_enabled', '1'); - o.ucisection = s.section; - - o = s.taboption('basic', form.Value, 'domain_resolver_dns_server', _('DNS Server'), _('Select or enter DNS server address')); - Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '8.8.8.8'; - o.rmempty = false; - o.depends('domain_resolver_enabled', '1'); - o.ucisection = s.section; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); - - if (validation.valid) { - return true; - } - - return _(validation.message) - }; - - o = s.taboption('basic', form.Flag, 'community_lists_enabled', _('Community Lists')); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption('basic', form.DynamicList, 'community_lists', _('Service List'), _('Select predefined service for routing') + ' github.com/itdoginfo/allow-domains'); - o.placeholder = 'Service list'; - Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.depends('community_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - - let lastValues = []; - let isProcessing = false; - - o.onchange = function (ev, section_id, value) { - if (isProcessing) return; - isProcessing = true; - - try { - const values = Array.isArray(value) ? value : [value]; - let newValues = [...values]; - let notifications = []; - - const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter(opt => newValues.includes(opt)); - - if (selectedRegionalOptions.length > 1) { - const lastSelected = selectedRegionalOptions[selectedRegionalOptions.length - 1]; - const removedRegions = selectedRegionalOptions.slice(0, -1); - newValues = newValues.filter(v => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v)); - notifications.push(E('p', { class: 'alert-message warning' }, [ - E('strong', {}, _('Regional options cannot be used together')), E('br'), - _('Warning: %s cannot be used together with %s. Previous selections have been removed.') - .format(removedRegions.join(', '), lastSelected) - ])); - } - - if (newValues.includes('russia_inside')) { - const removedServices = newValues.filter(v => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v)); - if (removedServices.length > 0) { - newValues = newValues.filter(v => main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v)); - notifications.push(E('p', { class: 'alert-message warning' }, [ - E('strong', {}, _('Russia inside restrictions')), E('br'), - _('Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.') - .format( - main.ALLOWED_WITH_RUSSIA_INSIDE.map(key => main.DOMAIN_LIST_OPTIONS[key]).filter(label => label !== 'Russia inside').join(', '), - removedServices.join(', ') - ) - ])); - } - } - - if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) { - this.getUIElement(section_id).setValue(newValues); - } - - notifications.forEach(notification => ui.addNotification(null, notification)); - lastValues = newValues; - } catch (e) { - console.error('Error in onchange handler:', e); - } finally { - isProcessing = false; - } - }; - - o = s.taboption('basic', form.ListValue, 'user_domain_list_type', _('User Domain List Type'), _('Select how to add your custom domains')); - o.value('disabled', _('Disabled')); - o.value('dynamic', _('Dynamic List')); - o.value('text', _('Text List')); - o.default = 'disabled'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption('basic', form.DynamicList, 'user_domains', _('User Domains'), _('Enter domain names without protocols (example: sub.example.com or example.com)')); - o.placeholder = 'Domains list'; - o.depends('user_domain_list_type', 'dynamic'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } - - const validation = main.validateDomain(value); - - if (validation.valid) { - return true; - } - - return _(validation.message) - }; - - o = s.taboption('basic', form.TextValue, 'user_domains_text', _('User Domains List'), _('Enter domain names separated by comma, space or newline. You can add comments after //')); - o.placeholder = 'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains'; - o.depends('user_domain_list_type', 'text'); - o.rows = 8; - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } - - const domains = main.parseValueList(value); - - if (!domains.length) { - return _( - 'At least one valid domain must be specified. Comments-only content is not allowed.' + } else { + const descDiv = E( + 'div', + { class: 'cbi-value-description' }, + _('Config without description'), ); + container.appendChild(descDiv); + } } + } catch (e) { + console.error('Error parsing config label:', e); + const descDiv = E( + 'div', + { class: 'cbi-value-description' }, + _('Config without description'), + ); + container.appendChild(descDiv); + } + } else { + const defaultDesc = E( + 'div', + { class: 'cbi-value-description' }, + _( + 'Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs', + ), + ); + container.appendChild(defaultDesc); + } - const { valid, results } = main.bulkValidate(domains, main.validateDomain); + return container; + }; - if (!valid) { - const errors = results - .filter(validation => !validation.valid) // Leave only failed validations - .map((validation) => _(`${validation.value}: ${validation.message}`)) // Collect validation errors + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } - return [_('Validation errors:'), ...errors].join('\n'); - } + try { + const activeConfig = value + .split('\n') + .map((line) => line.trim()) + .find((line) => line && !line.startsWith('//')); + if (!activeConfig) { + return _( + 'No active configuration found. At least one non-commented line is required.', + ); + } + + const validation = main.validateProxyUrl(activeConfig); + + if (validation.valid) { return true; - }; + } - o = s.taboption('basic', form.Flag, 'local_domain_lists_enabled', _('Local Domain Lists'), _('Use the list from the router filesystem')); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; + return _(validation.message); + } catch (e) { + return `${_('Invalid URL format:')} ${e?.message}`; + } + }; - o = s.taboption('basic', form.DynamicList, 'local_domain_lists', _('Local Domain List Paths'), _('Enter the list file path')); - o.placeholder = '/path/file.lst'; - o.depends('local_domain_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true + o = s.taboption( + 'basic', + form.TextValue, + 'outbound_json', + _('Outbound Configuration'), + _('Enter complete outbound configuration in JSON format'), + ); + o.depends('proxy_config_type', 'outbound'); + o.rows = 10; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateOutboundJson(value); + + if (validation.valid) { + return true; + } + + return _(validation.message); + }; + + o = s.taboption( + 'basic', + form.DynamicList, + 'urltest_proxy_links', + _('URLTest Proxy Links'), + ); + o.depends('proxy_config_type', 'urltest'); + o.placeholder = 'vless://, ss://, trojan:// links'; + o.rmempty = false; + o.textarea = true; + o.rows = 3; + o.wrap = 'soft'; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateProxyUrl(value); + + if (validation.valid) { + return true; + } + + return _(validation.message); + }; + + o = s.taboption( + 'basic', + form.Flag, + 'ss_uot', + _('Shadowsocks UDP over TCP'), + _('Apply for SS2022'), + ); + o.default = '0'; + o.depends('mode', 'proxy'); + o.rmempty = false; + o.ucisection = s.section; + + o = s.taboption( + 'basic', + widgets.DeviceSelect, + 'interface', + _('Network Interface'), + _('Select network interface for VPN connection'), + ); + o.depends('mode', 'vpn'); + o.ucisection = s.section; + o.noaliases = true; + o.nobridges = false; + o.noinactive = false; + o.filter = function (section_id, value) { + // Blocked interface names that should never be selectable + const blockedInterfaces = [ + 'br-lan', + 'eth0', + 'eth1', + 'wan', + 'phy0-ap0', + 'phy1-ap0', + 'pppoe-wan', + 'lan', + ]; + + // Reject immediately if the value matches any blocked interface + if (blockedInterfaces.includes(value)) { + return false; + } + + // Try to find the device object with the given name + const device = this.devices.find((dev) => dev.getName() === value); + + // If no device is found, allow the value + if (!device) { + return true; + } + + // Get the device type (e.g., "wifi", "ethernet", etc.) + const type = device.getType(); + + // Reject wireless-related devices + const isWireless = + type === 'wifi' || type === 'wireless' || type.includes('wlan'); + + return !isWireless; + }; + + o = s.taboption( + 'basic', + form.Flag, + 'domain_resolver_enabled', + _('Domain Resolver'), + _('Enable built-in DNS resolver for domains handled by this section'), + ); + o.default = '0'; + o.rmempty = false; + o.depends('mode', 'vpn'); + o.ucisection = s.section; + + o = s.taboption( + 'basic', + form.ListValue, + 'domain_resolver_dns_type', + _('DNS Protocol Type'), + _('Select the DNS protocol type for the domain resolver'), + ); + o.value('doh', _('DNS over HTTPS (DoH)')); + o.value('dot', _('DNS over TLS (DoT)')); + o.value('udp', _('UDP (Unprotected DNS)')); + o.default = 'udp'; + o.rmempty = false; + o.depends('domain_resolver_enabled', '1'); + o.ucisection = s.section; + + o = s.taboption( + 'basic', + form.Value, + 'domain_resolver_dns_server', + _('DNS Server'), + _('Select or enter DNS server address'), + ); + Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = '8.8.8.8'; + o.rmempty = false; + o.depends('domain_resolver_enabled', '1'); + o.ucisection = s.section; + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); + + if (validation.valid) { + return true; + } + + return _(validation.message); + }; + + o = s.taboption( + 'basic', + form.Flag, + 'community_lists_enabled', + _('Community Lists'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = s.section; + + o = s.taboption( + 'basic', + form.DynamicList, + 'community_lists', + _('Service List'), + _('Select predefined service for routing') + + ' github.com/itdoginfo/allow-domains', + ); + o.placeholder = 'Service list'; + Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.depends('community_lists_enabled', '1'); + o.rmempty = false; + o.ucisection = s.section; + + let lastValues = []; + let isProcessing = false; + + o.onchange = function (ev, section_id, value) { + if (isProcessing) return; + isProcessing = true; + + try { + const values = Array.isArray(value) ? value : [value]; + let newValues = [...values]; + let notifications = []; + + const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) => + newValues.includes(opt), + ); + + if (selectedRegionalOptions.length > 1) { + const lastSelected = + selectedRegionalOptions[selectedRegionalOptions.length - 1]; + const removedRegions = selectedRegionalOptions.slice(0, -1); + newValues = newValues.filter( + (v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v), + ); + notifications.push( + E('p', { class: 'alert-message warning' }, [ + E('strong', {}, _('Regional options cannot be used together')), + E('br'), + _( + 'Warning: %s cannot be used together with %s. Previous selections have been removed.', + ).format(removedRegions.join(', '), lastSelected), + ]), + ); + } + + if (newValues.includes('russia_inside')) { + const removedServices = newValues.filter( + (v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), + ); + if (removedServices.length > 0) { + newValues = newValues.filter((v) => + main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), + ); + notifications.push( + E('p', { class: 'alert-message warning' }, [ + E('strong', {}, _('Russia inside restrictions')), + E('br'), + _( + 'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.', + ).format( + main.ALLOWED_WITH_RUSSIA_INSIDE.map( + (key) => main.DOMAIN_LIST_OPTIONS[key], + ) + .filter((label) => label !== 'Russia inside') + .join(', '), + removedServices.join(', '), + ), + ]), + ); } + } - const validation = main.validatePath(value); + if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) { + this.getUIElement(section_id).setValue(newValues); + } - if (validation.valid) { - return true; - } + notifications.forEach((notification) => + ui.addNotification(null, notification), + ); + lastValues = newValues; + } catch (e) { + console.error('Error in onchange handler:', e); + } finally { + isProcessing = false; + } + }; - return _(validation.message) - }; + o = s.taboption( + 'basic', + form.ListValue, + 'user_domain_list_type', + _('User Domain List Type'), + _('Select how to add your custom domains'), + ); + o.value('disabled', _('Disabled')); + o.value('dynamic', _('Dynamic List')); + o.value('text', _('Text List')); + o.default = 'disabled'; + o.rmempty = false; + o.ucisection = s.section; - o = s.taboption('basic', form.Flag, 'remote_domain_lists_enabled', _('Remote Domain Lists'), _('Download and use domain lists from remote URLs')); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; + o = s.taboption( + 'basic', + form.DynamicList, + 'user_domains', + _('User Domains'), + _( + 'Enter domain names without protocols (example: sub.example.com or example.com)', + ), + ); + o.placeholder = 'Domains list'; + o.depends('user_domain_list_type', 'dynamic'); + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } - o = s.taboption('basic', form.DynamicList, 'remote_domain_lists', _('Remote Domain URLs'), _('Enter full URLs starting with http:// or https://')); - o.placeholder = 'URL'; - o.depends('remote_domain_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } + const validation = main.validateDomain(value); - const validation = main.validateUrl(value); + if (validation.valid) { + return true; + } - if (validation.valid) { - return true; - } + return _(validation.message); + }; - return _(validation.message) - }; + o = s.taboption( + 'basic', + form.TextValue, + 'user_domains_text', + _('User Domains List'), + _( + 'Enter domain names separated by comma, space or newline. You can add comments after //', + ), + ); + o.placeholder = + 'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains'; + o.depends('user_domain_list_type', 'text'); + o.rows = 8; + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } - o = s.taboption('basic', form.Flag, 'local_subnet_lists_enabled', _('Local Subnet Lists'), _('Use the list from the router filesystem')); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; + const domains = main.parseValueList(value); - o = s.taboption('basic', form.DynamicList, 'local_subnet_lists', _('Local Subnet List Paths'), _('Enter the list file path')); - o.placeholder = '/path/file.lst'; - o.depends('local_subnet_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } + if (!domains.length) { + return _( + 'At least one valid domain must be specified. Comments-only content is not allowed.', + ); + } - const validation = main.validatePath(value); + const { valid, results } = main.bulkValidate(domains, main.validateDomain); - if (validation.valid) { - return true; - } + if (!valid) { + const errors = results + .filter((validation) => !validation.valid) // Leave only failed validations + .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors - return _(validation.message) - }; + return [_('Validation errors:'), ...errors].join('\n'); + } - o = s.taboption('basic', form.ListValue, 'user_subnet_list_type', _('User Subnet List Type'), _('Select how to add your custom subnets')); - o.value('disabled', _('Disabled')); - o.value('dynamic', _('Dynamic List')); - o.value('text', _('Text List (comma/space/newline separated)')); - o.default = 'disabled'; - o.rmempty = false; - o.ucisection = s.section; + return true; + }; - o = s.taboption('basic', form.DynamicList, 'user_subnets', _('User Subnets'), _('Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses')); - o.placeholder = 'IP or subnet'; - o.depends('user_subnet_list_type', 'dynamic'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } + o = s.taboption( + 'basic', + form.Flag, + 'local_domain_lists_enabled', + _('Local Domain Lists'), + _('Use the list from the router filesystem'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = s.section; - const validation = main.validateSubnet(value); + o = s.taboption( + 'basic', + form.DynamicList, + 'local_domain_lists', + _('Local Domain List Paths'), + _('Enter the list file path'), + ); + o.placeholder = '/path/file.lst'; + o.depends('local_domain_lists_enabled', '1'); + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } - if (validation.valid) { - return true; - } + const validation = main.validatePath(value); - return _(validation.message) - }; + if (validation.valid) { + return true; + } - o = s.taboption('basic', form.TextValue, 'user_subnets_text', _('User Subnets List'), _('Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //')); - o.placeholder = '103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9'; - o.depends('user_subnet_list_type', 'text'); - o.rows = 10; - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } + return _(validation.message); + }; - const subnets = main.parseValueList(value); + o = s.taboption( + 'basic', + form.Flag, + 'remote_domain_lists_enabled', + _('Remote Domain Lists'), + _('Download and use domain lists from remote URLs'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = s.section; - if (!subnets.length) { - return _( - 'At least one valid subnet or IP must be specified. Comments-only content is not allowed.' - ); - } + o = s.taboption( + 'basic', + form.DynamicList, + 'remote_domain_lists', + _('Remote Domain URLs'), + _('Enter full URLs starting with http:// or https://'), + ); + o.placeholder = 'URL'; + o.depends('remote_domain_lists_enabled', '1'); + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } - const { valid, results } = main.bulkValidate(subnets, main.validateSubnet); + const validation = main.validateUrl(value); - if (!valid) { - const errors = results - .filter(validation => !validation.valid) // Leave only failed validations - .map((validation) => _(`${validation.value}: ${validation.message}`)) // Collect validation errors + if (validation.valid) { + return true; + } - return [_('Validation errors:'), ...errors].join('\n'); - } + return _(validation.message); + }; - return true; - }; + o = s.taboption( + 'basic', + form.Flag, + 'local_subnet_lists_enabled', + _('Local Subnet Lists'), + _('Use the list from the router filesystem'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = s.section; - o = s.taboption('basic', form.Flag, 'remote_subnet_lists_enabled', _('Remote Subnet Lists'), _('Download and use subnet lists from remote URLs')); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; + o = s.taboption( + 'basic', + form.DynamicList, + 'local_subnet_lists', + _('Local Subnet List Paths'), + _('Enter the list file path'), + ); + o.placeholder = '/path/file.lst'; + o.depends('local_subnet_lists_enabled', '1'); + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } - o = s.taboption('basic', form.DynamicList, 'remote_subnet_lists', _('Remote Subnet URLs'), _('Enter full URLs starting with http:// or https://')); - o.placeholder = 'URL'; - o.depends('remote_subnet_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } + const validation = main.validatePath(value); - const validation = main.validateUrl(value); + if (validation.valid) { + return true; + } - if (validation.valid) { - return true; - } + return _(validation.message); + }; - return _(validation.message) - }; + o = s.taboption( + 'basic', + form.ListValue, + 'user_subnet_list_type', + _('User Subnet List Type'), + _('Select how to add your custom subnets'), + ); + o.value('disabled', _('Disabled')); + o.value('dynamic', _('Dynamic List')); + o.value('text', _('Text List (comma/space/newline separated)')); + o.default = 'disabled'; + o.rmempty = false; + o.ucisection = s.section; - o = s.taboption('basic', form.Flag, 'all_traffic_from_ip_enabled', _('IP for full redirection'), _('Specify local IP addresses whose traffic will always use the configured route')); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; + o = s.taboption( + 'basic', + form.DynamicList, + 'user_subnets', + _('User Subnets'), + _( + 'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses', + ), + ); + o.placeholder = 'IP or subnet'; + o.depends('user_subnet_list_type', 'dynamic'); + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } - o = s.taboption('basic', form.DynamicList, 'all_traffic_ip', _('Local IPs'), _('Enter valid IPv4 addresses')); - o.placeholder = 'IP'; - o.depends('all_traffic_from_ip_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true - } + const validation = main.validateSubnet(value); - const validation = main.validateIPV4(value); + if (validation.valid) { + return true; + } - if (validation.valid) { - return true; - } + return _(validation.message); + }; - return _(validation.message) - }; + o = s.taboption( + 'basic', + form.TextValue, + 'user_subnets_text', + _('User Subnets List'), + _( + 'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //', + ), + ); + o.placeholder = + '103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9'; + o.depends('user_subnet_list_type', 'text'); + o.rows = 10; + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const subnets = main.parseValueList(value); + + if (!subnets.length) { + return _( + 'At least one valid subnet or IP must be specified. Comments-only content is not allowed.', + ); + } + + const { valid, results } = main.bulkValidate(subnets, main.validateSubnet); + + if (!valid) { + const errors = results + .filter((validation) => !validation.valid) // Leave only failed validations + .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors + + return [_('Validation errors:'), ...errors].join('\n'); + } + + return true; + }; + + o = s.taboption( + 'basic', + form.Flag, + 'remote_subnet_lists_enabled', + _('Remote Subnet Lists'), + _('Download and use subnet lists from remote URLs'), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = s.section; + + o = s.taboption( + 'basic', + form.DynamicList, + 'remote_subnet_lists', + _('Remote Subnet URLs'), + _('Enter full URLs starting with http:// or https://'), + ); + o.placeholder = 'URL'; + o.depends('remote_subnet_lists_enabled', '1'); + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateUrl(value); + + if (validation.valid) { + return true; + } + + return _(validation.message); + }; + + o = s.taboption( + 'basic', + form.Flag, + 'all_traffic_from_ip_enabled', + _('IP for full redirection'), + _( + 'Specify local IP addresses whose traffic will always use the configured route', + ), + ); + o.default = '0'; + o.rmempty = false; + o.ucisection = s.section; + + o = s.taboption( + 'basic', + form.DynamicList, + 'all_traffic_ip', + _('Local IPs'), + _('Enter valid IPv4 addresses'), + ); + o.placeholder = 'IP'; + o.depends('all_traffic_from_ip_enabled', '1'); + o.rmempty = false; + o.ucisection = s.section; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateIPV4(value); + + if (validation.valid) { + return true; + } + + return _(validation.message); + }; } return baseclass.extend({ - createConfigSection + createConfigSection, }); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js index 1768b43..5b5b2cf 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js @@ -12,553 +12,770 @@ const fetchCache = {}; // Helper function to fetch with cache async function cachedFetch(url, options = {}) { - const cacheKey = url; - const currentTime = Date.now(); + const cacheKey = url; + const currentTime = Date.now(); - // If we have a valid cached response, return it - if (fetchCache[cacheKey] && currentTime - fetchCache[cacheKey].timestamp < main.CACHE_TIMEOUT) { - console.log(`Using cached response for ${url}`); - return Promise.resolve(fetchCache[cacheKey].response.clone()); - } + // If we have a valid cached response, return it + if ( + fetchCache[cacheKey] && + currentTime - fetchCache[cacheKey].timestamp < main.CACHE_TIMEOUT + ) { + console.log(`Using cached response for ${url}`); + return Promise.resolve(fetchCache[cacheKey].response.clone()); + } - // Otherwise, make a new request - try { - const response = await fetch(url, options); + // Otherwise, make a new request + try { + const response = await fetch(url, options); - // Cache the response - fetchCache[cacheKey] = { - response: response.clone(), - timestamp: currentTime - }; + // Cache the response + fetchCache[cacheKey] = { + response: response.clone(), + timestamp: currentTime, + }; - return response; - } catch (error) { - throw error; - } + return response; + } catch (error) { + throw error; + } } // Helper functions for command execution with prioritization - Using from utils.js now -function safeExec(command, args, priority, callback, timeout = main.COMMAND_TIMEOUT) { - return utils.safeExec(command, args, priority, callback, timeout); +function safeExec( + command, + args, + priority, + callback, + timeout = main.COMMAND_TIMEOUT, +) { + return utils.safeExec(command, args, priority, callback, timeout); } // Helper functions for handling checks function runCheck(checkFunction, priority, callback) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; + // Default to highest priority execution if priority is not provided or invalid + let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - // If priority is a string, try to get the corresponding delay value - if (typeof priority === 'string' && main.COMMAND_SCHEDULING[priority] !== undefined) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; + // If priority is a string, try to get the corresponding delay value + if ( + typeof priority === 'string' && + main.COMMAND_SCHEDULING[priority] !== undefined + ) { + schedulingDelay = main.COMMAND_SCHEDULING[priority]; + } + + const executeCheck = async () => { + try { + const result = await checkFunction(); + if (callback && typeof callback === 'function') { + callback(result); + } + return result; + } catch (error) { + if (callback && typeof callback === 'function') { + callback({ error }); + } + return { error }; } + }; - const executeCheck = async () => { - try { - const result = await checkFunction(); - if (callback && typeof callback === 'function') { - callback(result); - } - return result; - } catch (error) { - if (callback && typeof callback === 'function') { - callback({ error }); - } - return { error }; - } - }; - - if (callback && typeof callback === 'function') { - setTimeout(executeCheck, schedulingDelay); - return; - } else { - return executeCheck(); - } + if (callback && typeof callback === 'function') { + setTimeout(executeCheck, schedulingDelay); + return; + } else { + return executeCheck(); + } } function runAsyncTask(taskFunction, priority) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; + // Default to highest priority execution if priority is not provided or invalid + let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - // If priority is a string, try to get the corresponding delay value - if (typeof priority === 'string' && main.COMMAND_SCHEDULING[priority] !== undefined) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; + // If priority is a string, try to get the corresponding delay value + if ( + typeof priority === 'string' && + main.COMMAND_SCHEDULING[priority] !== undefined + ) { + schedulingDelay = main.COMMAND_SCHEDULING[priority]; + } + + setTimeout(async () => { + try { + await taskFunction(); + } catch (error) { + console.error('Async task error:', error); } - - setTimeout(async () => { - try { - await taskFunction(); - } catch (error) { - console.error('Async task error:', error); - } - }, schedulingDelay); + }, schedulingDelay); } // Helper Functions for UI and formatting function createStatus(state, message, color) { - return { - state, - message: _(message), - color: main.STATUS_COLORS[color] - }; + return { + state, + message: _(message), + color: main.STATUS_COLORS[color], + }; } function formatDiagnosticOutput(output) { - if (typeof output !== 'string') return ''; - return output.trim() - .replace(/\x1b\[[0-9;]*m/g, '') - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n'); + if (typeof output !== 'string') return ''; + return output + .trim() + .replace(/\x1b\[[0-9;]*m/g, '') + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n'); } function copyToClipboard(text, button) { - const textarea = document.createElement('textarea'); - textarea.value = text; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - const originalText = button.textContent; - button.textContent = _('Copied!'); - setTimeout(() => button.textContent = originalText, main.BUTTON_FEEDBACK_TIMEOUT); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); + const textarea = document.createElement('textarea'); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + const originalText = button.textContent; + button.textContent = _('Copied!'); + setTimeout( + () => (button.textContent = originalText), + main.BUTTON_FEEDBACK_TIMEOUT, + ); + } catch (err) { + ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); + } + document.body.removeChild(textarea); } // IP masking function function maskIP(ip) { - if (!ip) return ''; - const parts = ip.split('.'); - if (parts.length !== 4) return ip; - return ['XX', 'XX', 'XX', parts[3]].join('.'); + if (!ip) return ''; + const parts = ip.split('.'); + if (parts.length !== 4) return ip; + return ['XX', 'XX', 'XX', parts[3]].join('.'); } // Status Check Functions async function checkFakeIP() { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); + try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); + const response = await cachedFetch( + `https://${main.FAKEIP_CHECK_DOMAIN}/check`, + { signal: controller.signal }, + ); + const data = await response.json(); + clearTimeout(timeoutId); - try { - const response = await cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal }); - const data = await response.json(); - clearTimeout(timeoutId); - - if (data.fakeip === true) { - return createStatus('working', 'working', 'SUCCESS'); - } else { - return createStatus('not_working', 'not working', 'ERROR'); - } - } catch (fetchError) { - clearTimeout(timeoutId); - const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error'; - return createStatus('error', message, 'WARNING'); - } - } catch (error) { - return createStatus('error', 'check error', 'WARNING'); + if (data.fakeip === true) { + return createStatus('working', 'working', 'SUCCESS'); + } else { + return createStatus('not_working', 'not working', 'ERROR'); + } + } catch (fetchError) { + clearTimeout(timeoutId); + const message = + fetchError.name === 'AbortError' ? 'timeout' : 'check error'; + return createStatus('error', message, 'WARNING'); } + } catch (error) { + return createStatus('error', 'check error', 'WARNING'); + } } async function checkFakeIPCLI() { - try { - return new Promise((resolve) => { - safeExec('nslookup', ['-timeout=2', main.FAKEIP_CHECK_DOMAIN, '127.0.0.42'], 'P0_PRIORITY', result => { - if (result.stdout && result.stdout.includes('198.18')) { - resolve(createStatus('working', 'working on router', 'SUCCESS')); - } else { - resolve(createStatus('not_working', 'not working on router', 'ERROR')); - } - }); - }); - } catch (error) { - return createStatus('error', 'CLI check error', 'WARNING'); - } + try { + return new Promise((resolve) => { + safeExec( + 'nslookup', + ['-timeout=2', main.FAKEIP_CHECK_DOMAIN, '127.0.0.42'], + 'P0_PRIORITY', + (result) => { + if (result.stdout && result.stdout.includes('198.18')) { + resolve(createStatus('working', 'working on router', 'SUCCESS')); + } else { + resolve( + createStatus('not_working', 'not working on router', 'ERROR'), + ); + } + }, + ); + }); + } catch (error) { + return createStatus('error', 'CLI check error', 'WARNING'); + } } function checkDNSAvailability() { - return new Promise(async (resolve) => { - try { - safeExec('/usr/bin/podkop', ['check_dns_available'], 'P0_PRIORITY', dnsStatusResult => { - if (!dnsStatusResult || !dnsStatusResult.stdout) { - return resolve({ - remote: createStatus('error', 'DNS check timeout', 'WARNING'), - local: createStatus('error', 'DNS check timeout', 'WARNING') - }); - } - - try { - const dnsStatus = JSON.parse(dnsStatusResult.stdout); - - const remoteStatus = dnsStatus.is_available ? - createStatus('available', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, 'SUCCESS') : - createStatus('unavailable', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, 'ERROR'); - - const localStatus = dnsStatus.local_dns_working ? - createStatus('available', 'Router DNS working', 'SUCCESS') : - createStatus('unavailable', 'Router DNS not working', 'ERROR'); - - return resolve({ - remote: remoteStatus, - local: localStatus - }); - } catch (parseError) { - return resolve({ - remote: createStatus('error', 'DNS check parse error', 'WARNING'), - local: createStatus('error', 'DNS check parse error', 'WARNING') - }); - } - }); - } catch (error) { + return new Promise(async (resolve) => { + try { + safeExec( + '/usr/bin/podkop', + ['check_dns_available'], + 'P0_PRIORITY', + (dnsStatusResult) => { + if (!dnsStatusResult || !dnsStatusResult.stdout) { return resolve({ - remote: createStatus('error', 'DNS check error', 'WARNING'), - local: createStatus('error', 'DNS check error', 'WARNING') + remote: createStatus('error', 'DNS check timeout', 'WARNING'), + local: createStatus('error', 'DNS check timeout', 'WARNING'), }); - } - }); + } + + try { + const dnsStatus = JSON.parse(dnsStatusResult.stdout); + + const remoteStatus = dnsStatus.is_available + ? createStatus( + 'available', + `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, + 'SUCCESS', + ) + : createStatus( + 'unavailable', + `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, + 'ERROR', + ); + + const localStatus = dnsStatus.local_dns_working + ? createStatus('available', 'Router DNS working', 'SUCCESS') + : createStatus('unavailable', 'Router DNS not working', 'ERROR'); + + return resolve({ + remote: remoteStatus, + local: localStatus, + }); + } catch (parseError) { + return resolve({ + remote: createStatus('error', 'DNS check parse error', 'WARNING'), + local: createStatus('error', 'DNS check parse error', 'WARNING'), + }); + } + }, + ); + } catch (error) { + return resolve({ + remote: createStatus('error', 'DNS check error', 'WARNING'), + local: createStatus('error', 'DNS check error', 'WARNING'), + }); + } + }); } async function checkBypass() { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); + try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); + const response1 = await cachedFetch( + `https://${main.FAKEIP_CHECK_DOMAIN}/check`, + { signal: controller.signal }, + ); + const data1 = await response1.json(); - try { - const response1 = await cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal }); - const data1 = await response1.json(); + const response2 = await cachedFetch( + `https://${main.IP_CHECK_DOMAIN}/check`, + { signal: controller.signal }, + ); + const data2 = await response2.json(); - const response2 = await cachedFetch(`https://${main.IP_CHECK_DOMAIN}/check`, { signal: controller.signal }); - const data2 = await response2.json(); + clearTimeout(timeoutId); - clearTimeout(timeoutId); - - if (data1.IP && data2.IP) { - if (data1.IP !== data2.IP) { - return createStatus('working', 'working', 'SUCCESS'); - } else { - return createStatus('not_working', 'same IP for both domains', 'ERROR'); - } - } else { - return createStatus('error', 'check error (no IP)', 'WARNING'); - } - } catch (fetchError) { - clearTimeout(timeoutId); - const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error'; - return createStatus('error', message, 'WARNING'); + if (data1.IP && data2.IP) { + if (data1.IP !== data2.IP) { + return createStatus('working', 'working', 'SUCCESS'); + } else { + return createStatus( + 'not_working', + 'same IP for both domains', + 'ERROR', + ); } - } catch (error) { - return createStatus('error', 'check error', 'WARNING'); + } else { + return createStatus('error', 'check error (no IP)', 'WARNING'); + } + } catch (fetchError) { + clearTimeout(timeoutId); + const message = + fetchError.name === 'AbortError' ? 'timeout' : 'check error'; + return createStatus('error', message, 'WARNING'); } + } catch (error) { + return createStatus('error', 'check error', 'WARNING'); + } } function showConfigModal(command, title) { - // Create and show modal immediately with loading state - const modalContent = E('div', { 'class': 'panel-body' }, [ - E('div', { - 'class': 'panel-body', - style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' + - 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' + - 'line-height: 1.5; font-size: 14px;' - }, [ - E('pre', { - 'id': 'modal-content-pre', - style: 'margin: 0;' - }, _('Loading...')) - ]), - E('div', { - 'class': 'right', - style: 'margin-top: 1em;' - }, [ - E('button', { - 'class': 'btn', - 'id': 'copy-button', - 'click': ev => copyToClipboard('```txt\n' + document.getElementById('modal-content-pre').innerText + '\n```', ev.target) - }, _('Copy to Clipboard')), - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Close')) - ]) - ]); + // Create and show modal immediately with loading state + const modalContent = E('div', { class: 'panel-body' }, [ + E( + 'div', + { + class: 'panel-body', + style: + 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' + + 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' + + 'line-height: 1.5; font-size: 14px;', + }, + [ + E( + 'pre', + { + id: 'modal-content-pre', + style: 'margin: 0;', + }, + _('Loading...'), + ), + ], + ), + E( + 'div', + { + class: 'right', + style: 'margin-top: 1em;', + }, + [ + E( + 'button', + { + class: 'btn', + id: 'copy-button', + click: (ev) => + copyToClipboard( + '```txt\n' + + document.getElementById('modal-content-pre').innerText + + '\n```', + ev.target, + ), + }, + _('Copy to Clipboard'), + ), + E( + 'button', + { + class: 'btn', + click: ui.hideModal, + }, + _('Close'), + ), + ], + ), + ]); - ui.showModal(_(title), modalContent); + ui.showModal(_(title), modalContent); - // Function to update modal content - const updateModalContent = (content) => { - const pre = document.getElementById('modal-content-pre'); - if (pre) { - pre.textContent = content; - } - }; - - try { - let formattedOutput = ''; - - if (command === 'global_check') { - safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => { - formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); - - cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal }) - .then(response => response.json()) - .then(data => { - clearTimeout(timeoutId); - - if (data.fakeip === true) { - formattedOutput += '\n✅ ' + _('FakeIP is working in browser!') + '\n'; - } else { - formattedOutput += '\n❌ ' + _('FakeIP is not working in browser') + '\n'; - formattedOutput += _('Check DNS server on current device (PC, phone)') + '\n'; - formattedOutput += _('Its must be router!') + '\n'; - } - - // Bypass check - cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal }) - .then(bypassResponse => bypassResponse.json()) - .then(bypassData => { - cachedFetch(`https://${main.IP_CHECK_DOMAIN}/check`, { signal: controller.signal }) - .then(bypassResponse2 => bypassResponse2.json()) - .then(bypassData2 => { - formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; - - if (bypassData.IP && bypassData2.IP && bypassData.IP !== bypassData2.IP) { - formattedOutput += '✅ ' + _('Proxy working correctly') + '\n'; - formattedOutput += _('Direct IP: ') + maskIP(bypassData.IP) + '\n'; - formattedOutput += _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n'; - } else if (bypassData.IP === bypassData2.IP) { - formattedOutput += '❌ ' + _('Proxy is not working - same IP for both domains') + '\n'; - formattedOutput += _('IP: ') + maskIP(bypassData.IP) + '\n'; - } else { - formattedOutput += '❌ ' + _('Proxy check failed') + '\n'; - } - - updateModalContent(formattedOutput); - }) - .catch(error => { - formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n'; - updateModalContent(formattedOutput); - }); - }) - .catch(error => { - formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n'; - updateModalContent(formattedOutput); - }); - }) - .catch(error => { - formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n'; - updateModalContent(formattedOutput); - }); - } catch (error) { - formattedOutput += '\n❌ ' + _('Check failed: ') + error.message + '\n'; - updateModalContent(formattedOutput); - } - }); - } else { - safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => { - formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - updateModalContent(formattedOutput); - }); - } - } catch (error) { - updateModalContent(_('Error: ') + error.message); + // Function to update modal content + const updateModalContent = (content) => { + const pre = document.getElementById('modal-content-pre'); + if (pre) { + pre.textContent = content; } + }; + + try { + let formattedOutput = ''; + + if (command === 'global_check') { + safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', (res) => { + formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); + + try { + const controller = new AbortController(); + const timeoutId = setTimeout( + () => controller.abort(), + main.FETCH_TIMEOUT, + ); + + cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { + signal: controller.signal, + }) + .then((response) => response.json()) + .then((data) => { + clearTimeout(timeoutId); + + if (data.fakeip === true) { + formattedOutput += + '\n✅ ' + _('FakeIP is working in browser!') + '\n'; + } else { + formattedOutput += + '\n❌ ' + _('FakeIP is not working in browser') + '\n'; + formattedOutput += + _('Check DNS server on current device (PC, phone)') + '\n'; + formattedOutput += _('Its must be router!') + '\n'; + } + + // Bypass check + cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { + signal: controller.signal, + }) + .then((bypassResponse) => bypassResponse.json()) + .then((bypassData) => { + cachedFetch(`https://${main.IP_CHECK_DOMAIN}/check`, { + signal: controller.signal, + }) + .then((bypassResponse2) => bypassResponse2.json()) + .then((bypassData2) => { + formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; + + if ( + bypassData.IP && + bypassData2.IP && + bypassData.IP !== bypassData2.IP + ) { + formattedOutput += + '✅ ' + _('Proxy working correctly') + '\n'; + formattedOutput += + _('Direct IP: ') + maskIP(bypassData.IP) + '\n'; + formattedOutput += + _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n'; + } else if (bypassData.IP === bypassData2.IP) { + formattedOutput += + '❌ ' + + _('Proxy is not working - same IP for both domains') + + '\n'; + formattedOutput += + _('IP: ') + maskIP(bypassData.IP) + '\n'; + } else { + formattedOutput += + '❌ ' + _('Proxy check failed') + '\n'; + } + + updateModalContent(formattedOutput); + }) + .catch((error) => { + formattedOutput += + '\n❌ ' + + _('Check failed: ') + + (error.name === 'AbortError' + ? _('timeout') + : error.message) + + '\n'; + updateModalContent(formattedOutput); + }); + }) + .catch((error) => { + formattedOutput += + '\n❌ ' + + _('Check failed: ') + + (error.name === 'AbortError' + ? _('timeout') + : error.message) + + '\n'; + updateModalContent(formattedOutput); + }); + }) + .catch((error) => { + formattedOutput += + '\n❌ ' + + _('Check failed: ') + + (error.name === 'AbortError' ? _('timeout') : error.message) + + '\n'; + updateModalContent(formattedOutput); + }); + } catch (error) { + formattedOutput += + '\n❌ ' + _('Check failed: ') + error.message + '\n'; + updateModalContent(formattedOutput); + } + }); + } else { + safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', (res) => { + formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); + updateModalContent(formattedOutput); + }); + } + } catch (error) { + updateModalContent(_('Error: ') + error.message); + } } // Button Factory const ButtonFactory = { - createButton: function (config) { - return E('button', { - 'class': `btn ${config.additionalClass || ''}`.trim(), - 'click': config.onClick, - 'style': config.style || '' - }, _(config.label)); - }, + createButton: function (config) { + return E( + 'button', + { + class: `btn ${config.additionalClass || ''}`.trim(), + click: config.onClick, + style: config.style || '', + }, + _(config.label), + ); + }, - createActionButton: function (config) { - return this.createButton({ - label: config.label, - additionalClass: `cbi-button-${config.type || ''}`, - onClick: () => safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY') - .then(() => config.reload && location.reload()), - style: config.style - }); - }, + createActionButton: function (config) { + return this.createButton({ + label: config.label, + additionalClass: `cbi-button-${config.type || ''}`, + onClick: () => + safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY').then( + () => config.reload && location.reload(), + ), + style: config.style, + }); + }, - createInitActionButton: function (config) { - return this.createButton({ - label: config.label, - additionalClass: `cbi-button-${config.type || ''}`, - onClick: () => safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY') - .then(() => config.reload && location.reload()), - style: config.style - }); - }, + createInitActionButton: function (config) { + return this.createButton({ + label: config.label, + additionalClass: `cbi-button-${config.type || ''}`, + onClick: () => + safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY').then( + () => config.reload && location.reload(), + ), + style: config.style, + }); + }, - createModalButton: function (config) { - return this.createButton({ - label: config.label, - onClick: () => showConfigModal(config.command, config.title), - additionalClass: `cbi-button-${config.type || ''}`, - style: config.style - }); - } + createModalButton: function (config) { + return this.createButton({ + label: config.label, + onClick: () => showConfigModal(config.command, config.title), + additionalClass: `cbi-button-${config.type || ''}`, + style: config.style, + }); + }, }; // Create a loading placeholder for status text function createLoadingStatusText() { - return E('span', { 'class': 'loading-indicator' }, _('Loading...')); + return E('span', { class: 'loading-indicator' }, _('Loading...')); } // Create the status section with buttons loaded immediately but status indicators loading asynchronously let createStatusSection = async function () { - // Get initial podkop status - let initialPodkopStatus = { enabled: false }; - try { - const result = await fs.exec('/usr/bin/podkop', ['get_status']); - if (result && result.stdout) { - const status = JSON.parse(result.stdout); - initialPodkopStatus.enabled = status.enabled === 1; - } - } catch (e) { - console.error('Error getting initial podkop status:', e); + // Get initial podkop status + let initialPodkopStatus = { enabled: false }; + try { + const result = await fs.exec('/usr/bin/podkop', ['get_status']); + if (result && result.stdout) { + const status = JSON.parse(result.stdout); + initialPodkopStatus.enabled = status.enabled === 1; } + } catch (e) { + console.error('Error getting initial podkop status:', e); + } - return E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [ - // Podkop Status Panel - E('div', { 'id': 'podkop-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [ - E('div', { 'class': 'panel-heading' }, [ - E('strong', {}, _('Podkop Status')), - E('br'), - E('span', { 'id': 'podkop-status-text' }, createLoadingStatusText()) - ]), - E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ - ButtonFactory.createActionButton({ - label: 'Restart Podkop', - type: 'apply', - action: 'restart', - reload: true - }), - ButtonFactory.createActionButton({ - label: 'Stop Podkop', - type: 'apply', - action: 'stop', - reload: true - }), - // Autostart button - create with initial state - ButtonFactory.createInitActionButton({ - label: initialPodkopStatus.enabled ? 'Disable Autostart' : 'Enable Autostart', - type: initialPodkopStatus.enabled ? 'remove' : 'apply', - action: initialPodkopStatus.enabled ? 'disable' : 'enable', - reload: true - }), - ButtonFactory.createModalButton({ - label: _('Global check'), - command: 'global_check', - title: _('Click here for all the info') - }), - ButtonFactory.createModalButton({ - label: 'View Logs', - command: 'check_logs', - title: 'Podkop Logs' - }), - ButtonFactory.createModalButton({ - label: _('Update Lists'), - command: 'list_update', - title: _('Lists Update Results') - }) - ]) - ]), + return E('div', { class: 'cbi-section' }, [ + E('div', { class: 'table', style: 'display: flex; gap: 20px;' }, [ + // Podkop Status Panel + E( + 'div', + { + id: 'podkop-status-panel', + class: 'panel', + style: 'flex: 1; padding: 15px;', + }, + [ + E('div', { class: 'panel-heading' }, [ + E('strong', {}, _('Podkop Status')), + E('br'), + E('span', { id: 'podkop-status-text' }, createLoadingStatusText()), + ]), + E( + 'div', + { + class: 'panel-body', + style: 'display: flex; flex-direction: column; gap: 8px;', + }, + [ + ButtonFactory.createActionButton({ + label: 'Restart Podkop', + type: 'apply', + action: 'restart', + reload: true, + }), + ButtonFactory.createActionButton({ + label: 'Stop Podkop', + type: 'apply', + action: 'stop', + reload: true, + }), + // Autostart button - create with initial state + ButtonFactory.createInitActionButton({ + label: initialPodkopStatus.enabled + ? 'Disable Autostart' + : 'Enable Autostart', + type: initialPodkopStatus.enabled ? 'remove' : 'apply', + action: initialPodkopStatus.enabled ? 'disable' : 'enable', + reload: true, + }), + ButtonFactory.createModalButton({ + label: _('Global check'), + command: 'global_check', + title: _('Click here for all the info'), + }), + ButtonFactory.createModalButton({ + label: 'View Logs', + command: 'check_logs', + title: 'Podkop Logs', + }), + ButtonFactory.createModalButton({ + label: _('Update Lists'), + command: 'list_update', + title: _('Lists Update Results'), + }), + ], + ), + ], + ), - // Sing-box Status Panel - E('div', { 'id': 'singbox-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [ - E('div', { 'class': 'panel-heading' }, [ - E('strong', {}, _('Sing-box Status')), - E('br'), - E('span', { 'id': 'singbox-status-text' }, createLoadingStatusText()) - ]), - E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ - ButtonFactory.createModalButton({ - label: 'Show Config', - command: 'show_sing_box_config', - title: 'Sing-box Configuration' - }), - ButtonFactory.createModalButton({ - label: 'View Logs', - command: 'check_sing_box_logs', - title: 'Sing-box Logs' - }), - ButtonFactory.createModalButton({ - label: 'Check Connections', - command: 'check_sing_box_connections', - title: 'Active Connections' - }), - ButtonFactory.createModalButton({ - label: _('Check NFT Rules'), - command: 'check_nft', - title: _('NFT Rules') - }), - ButtonFactory.createModalButton({ - label: _('Check DNSMasq'), - command: 'check_dnsmasq', - title: _('DNSMasq Configuration') - }) - ]) - ]), + // Sing-box Status Panel + E( + 'div', + { + id: 'singbox-status-panel', + class: 'panel', + style: 'flex: 1; padding: 15px;', + }, + [ + E('div', { class: 'panel-heading' }, [ + E('strong', {}, _('Sing-box Status')), + E('br'), + E('span', { id: 'singbox-status-text' }, createLoadingStatusText()), + ]), + E( + 'div', + { + class: 'panel-body', + style: 'display: flex; flex-direction: column; gap: 8px;', + }, + [ + ButtonFactory.createModalButton({ + label: 'Show Config', + command: 'show_sing_box_config', + title: 'Sing-box Configuration', + }), + ButtonFactory.createModalButton({ + label: 'View Logs', + command: 'check_sing_box_logs', + title: 'Sing-box Logs', + }), + ButtonFactory.createModalButton({ + label: 'Check Connections', + command: 'check_sing_box_connections', + title: 'Active Connections', + }), + ButtonFactory.createModalButton({ + label: _('Check NFT Rules'), + command: 'check_nft', + title: _('NFT Rules'), + }), + ButtonFactory.createModalButton({ + label: _('Check DNSMasq'), + command: 'check_dnsmasq', + title: _('DNSMasq Configuration'), + }), + ], + ), + ], + ), - // FakeIP Status Panel - E('div', { 'id': 'fakeip-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [ - E('div', { 'class': 'panel-heading' }, [ - E('strong', {}, _('FakeIP Status')) + // FakeIP Status Panel + E( + 'div', + { + id: 'fakeip-status-panel', + class: 'panel', + style: 'flex: 1; padding: 15px;', + }, + [ + E('div', { class: 'panel-heading' }, [ + E('strong', {}, _('FakeIP Status')), + ]), + E( + 'div', + { + class: 'panel-body', + style: 'display: flex; flex-direction: column; gap: 8px;', + }, + [ + E('div', { style: 'margin-bottom: 5px;' }, [ + E('div', {}, [ + E( + 'span', + { id: 'fakeip-browser-status' }, + createLoadingStatusText(), + ), ]), - E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E('span', { 'id': 'fakeip-browser-status' }, createLoadingStatusText()) - ]), - E('div', {}, [ - E('span', { 'id': 'fakeip-router-status' }, createLoadingStatusText()) - ]) - ]), - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E('strong', {}, _('DNS Status')), - E('br'), - E('span', { 'id': 'dns-remote-status' }, createLoadingStatusText()), - E('br'), - E('span', { 'id': 'dns-local-status' }, createLoadingStatusText()) - ]) - ]), - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E('strong', { 'id': 'config-name-text' }, _('Main config')), - E('br'), - E('span', { 'id': 'bypass-status' }, createLoadingStatusText()) - ]) - ]) - ]) - ]), + E('div', {}, [ + E( + 'span', + { id: 'fakeip-router-status' }, + createLoadingStatusText(), + ), + ]), + ]), + E('div', { style: 'margin-bottom: 5px;' }, [ + E('div', {}, [ + E('strong', {}, _('DNS Status')), + E('br'), + E( + 'span', + { id: 'dns-remote-status' }, + createLoadingStatusText(), + ), + E('br'), + E( + 'span', + { id: 'dns-local-status' }, + createLoadingStatusText(), + ), + ]), + ]), + E('div', { style: 'margin-bottom: 5px;' }, [ + E('div', {}, [ + E('strong', { id: 'config-name-text' }, _('Main config')), + E('br'), + E('span', { id: 'bypass-status' }, createLoadingStatusText()), + ]), + ]), + ], + ), + ], + ), - // Version Information Panel - E('div', { 'id': 'version-info-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [ - E('div', { 'class': 'panel-heading' }, [ - E('strong', {}, _('Version Information')) - ]), - E('div', { 'class': 'panel-body' }, [ - E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [ - E('strong', {}, _('Podkop: ')), E('span', { 'id': 'podkop-version' }, _('Loading...')), '\n', - E('strong', {}, _('LuCI App: ')), E('span', { 'id': 'luci-version' }, _('Loading...')), '\n', - E('strong', {}, _('Sing-box: ')), E('span', { 'id': 'singbox-version' }, _('Loading...')), '\n', - E('strong', {}, _('OpenWrt Version: ')), E('span', { 'id': 'openwrt-version' }, _('Loading...')), '\n', - E('strong', {}, _('Device Model: ')), E('span', { 'id': 'device-model' }, _('Loading...')) - ]) - ]) - ]) - ]) - ]); + // Version Information Panel + E( + 'div', + { + id: 'version-info-panel', + class: 'panel', + style: 'flex: 1; padding: 15px;', + }, + [ + E('div', { class: 'panel-heading' }, [ + E('strong', {}, _('Version Information')), + ]), + E('div', { class: 'panel-body' }, [ + E( + 'div', + { + style: + 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;', + }, + [ + E('strong', {}, _('Podkop: ')), + E('span', { id: 'podkop-version' }, _('Loading...')), + '\n', + E('strong', {}, _('LuCI App: ')), + E('span', { id: 'luci-version' }, _('Loading...')), + '\n', + E('strong', {}, _('Sing-box: ')), + E('span', { id: 'singbox-version' }, _('Loading...')), + '\n', + E('strong', {}, _('OpenWrt Version: ')), + E('span', { id: 'openwrt-version' }, _('Loading...')), + '\n', + E('strong', {}, _('Device Model: ')), + E('span', { id: 'device-model' }, _('Loading...')), + ], + ), + ]), + ], + ), + ]), + ]); }; // Global variables for tracking state @@ -567,294 +784,444 @@ let isInitialCheck = true; showConfigModal.busy = false; function startDiagnosticsUpdates() { - if (diagnosticsUpdateTimer) { - clearInterval(diagnosticsUpdateTimer); - } + if (diagnosticsUpdateTimer) { + clearInterval(diagnosticsUpdateTimer); + } - // Immediately update when started - updateDiagnostics(); + // Immediately update when started + updateDiagnostics(); - // Then set up periodic updates - diagnosticsUpdateTimer = setInterval(updateDiagnostics, main.DIAGNOSTICS_UPDATE_INTERVAL); + // Then set up periodic updates + diagnosticsUpdateTimer = setInterval( + updateDiagnostics, + main.DIAGNOSTICS_UPDATE_INTERVAL, + ); } function stopDiagnosticsUpdates() { - if (diagnosticsUpdateTimer) { - clearInterval(diagnosticsUpdateTimer); - diagnosticsUpdateTimer = null; - } + if (diagnosticsUpdateTimer) { + clearInterval(diagnosticsUpdateTimer); + diagnosticsUpdateTimer = null; + } } // Update individual text element with new content function updateTextElement(elementId, content) { - const element = document.getElementById(elementId); - if (element) { - element.innerHTML = ''; - element.appendChild(content); - } + const element = document.getElementById(elementId); + if (element) { + element.innerHTML = ''; + element.appendChild(content); + } } async function updateDiagnostics() { - // Podkop Status check - safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', result => { - try { - const parsedPodkopStatus = JSON.parse(result.stdout || '{"enabled":0,"status":"error"}'); + // Podkop Status check + safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', (result) => { + try { + const parsedPodkopStatus = JSON.parse( + result.stdout || '{"enabled":0,"status":"error"}', + ); - // Update Podkop status text - updateTextElement('podkop-status-text', - E('span', { - 'style': `color: ${parsedPodkopStatus.enabled ? main.STATUS_COLORS.SUCCESS : main.STATUS_COLORS.ERROR}` - }, [ - parsedPodkopStatus.enabled ? '✔ Autostart enabled' : '✘ Autostart disabled' - ]) - ); + // Update Podkop status text + updateTextElement( + 'podkop-status-text', + E( + 'span', + { + style: `color: ${parsedPodkopStatus.enabled ? main.STATUS_COLORS.SUCCESS : main.STATUS_COLORS.ERROR}`, + }, + [ + parsedPodkopStatus.enabled + ? '✔ Autostart enabled' + : '✘ Autostart disabled', + ], + ), + ); - // Update autostart button - const autostartButton = parsedPodkopStatus.enabled ? - ButtonFactory.createInitActionButton({ - label: 'Disable Autostart', - type: 'remove', - action: 'disable', - reload: true - }) : - ButtonFactory.createInitActionButton({ - label: 'Enable Autostart', - type: 'apply', - action: 'enable', - reload: true - }); + // Update autostart button + const autostartButton = parsedPodkopStatus.enabled + ? ButtonFactory.createInitActionButton({ + label: 'Disable Autostart', + type: 'remove', + action: 'disable', + reload: true, + }) + : ButtonFactory.createInitActionButton({ + label: 'Enable Autostart', + type: 'apply', + action: 'enable', + reload: true, + }); - // Find the autostart button and replace it - const panel = document.getElementById('podkop-status-panel'); - if (panel) { - const buttons = panel.querySelectorAll('.cbi-button'); - if (buttons.length >= 3) { - buttons[2].parentNode.replaceChild(autostartButton, buttons[2]); - } + // Find the autostart button and replace it + const panel = document.getElementById('podkop-status-panel'); + if (panel) { + const buttons = panel.querySelectorAll('.cbi-button'); + if (buttons.length >= 3) { + buttons[2].parentNode.replaceChild(autostartButton, buttons[2]); + } + } + } catch (error) { + updateTextElement( + 'podkop-status-text', + E('span', { style: `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error'), + ); + } + }); + + // Sing-box Status check + safeExec( + '/usr/bin/podkop', + ['get_sing_box_status'], + 'P0_PRIORITY', + (result) => { + try { + const parsedSingboxStatus = JSON.parse( + result.stdout || '{"running":0,"enabled":0,"status":"error"}', + ); + + // Update Sing-box status text + updateTextElement( + 'singbox-status-text', + E( + 'span', + { + style: `color: ${ + parsedSingboxStatus.running && !parsedSingboxStatus.enabled + ? main.STATUS_COLORS.SUCCESS + : main.STATUS_COLORS.ERROR + }`, + }, + [ + parsedSingboxStatus.running && !parsedSingboxStatus.enabled + ? '✔ running' + : '✘ ' + parsedSingboxStatus.status, + ], + ), + ); + } catch (error) { + updateTextElement( + 'singbox-status-text', + E('span', { style: `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error'), + ); + } + }, + ); + + // Version Information checks + safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', (result) => { + updateTextElement( + 'podkop-version', + document.createTextNode( + result.stdout ? result.stdout.trim() : _('Unknown'), + ), + ); + }); + + safeExec( + '/usr/bin/podkop', + ['show_luci_version'], + 'P2_PRIORITY', + (result) => { + updateTextElement( + 'luci-version', + document.createTextNode( + result.stdout ? result.stdout.trim() : _('Unknown'), + ), + ); + }, + ); + + safeExec( + '/usr/bin/podkop', + ['show_sing_box_version'], + 'P2_PRIORITY', + (result) => { + updateTextElement( + 'singbox-version', + document.createTextNode( + result.stdout ? result.stdout.trim() : _('Unknown'), + ), + ); + }, + ); + + safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', (result) => { + if (result.stdout) { + updateTextElement( + 'openwrt-version', + document.createTextNode(result.stdout.split('\n')[1].trim()), + ); + updateTextElement( + 'device-model', + document.createTextNode(result.stdout.split('\n')[4].trim()), + ); + } else { + updateTextElement( + 'openwrt-version', + document.createTextNode(_('Unknown')), + ); + updateTextElement('device-model', document.createTextNode(_('Unknown'))); + } + }); + + // FakeIP and DNS status checks + runCheck(checkFakeIP, 'P3_PRIORITY', (result) => { + updateTextElement( + 'fakeip-browser-status', + E( + 'span', + { + style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, + }, + [ + result.error + ? '! ' + : result.state === 'working' + ? '✔ ' + : result.state === 'not_working' + ? '✘ ' + : '! ', + result.error + ? 'check error' + : result.state === 'working' + ? _('works in browser') + : _('does not work in browser'), + ], + ), + ); + }); + + runCheck(checkFakeIPCLI, 'P8_PRIORITY', (result) => { + updateTextElement( + 'fakeip-router-status', + E( + 'span', + { + style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, + }, + [ + result.error + ? '! ' + : result.state === 'working' + ? '✔ ' + : result.state === 'not_working' + ? '✘ ' + : '! ', + result.error + ? 'check error' + : result.state === 'working' + ? _('works on router') + : _('does not work on router'), + ], + ), + ); + }); + + runCheck(checkDNSAvailability, 'P4_PRIORITY', (result) => { + if (result.error) { + updateTextElement( + 'dns-remote-status', + E( + 'span', + { style: `color: ${main.STATUS_COLORS.WARNING}` }, + '! DNS check error', + ), + ); + updateTextElement( + 'dns-local-status', + E( + 'span', + { style: `color: ${main.STATUS_COLORS.WARNING}` }, + '! DNS check error', + ), + ); + } else { + updateTextElement( + 'dns-remote-status', + E('span', { style: `color: ${result.remote.color}` }, [ + result.remote.state === 'available' + ? '✔ ' + : result.remote.state === 'unavailable' + ? '✘ ' + : '! ', + result.remote.message, + ]), + ); + + updateTextElement( + 'dns-local-status', + E('span', { style: `color: ${result.local.color}` }, [ + result.local.state === 'available' + ? '✔ ' + : result.local.state === 'unavailable' + ? '✘ ' + : '! ', + result.local.message, + ]), + ); + } + }); + + runCheck( + checkBypass, + 'P1_PRIORITY', + (result) => { + updateTextElement( + 'bypass-status', + E( + 'span', + { + style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, + }, + [ + result.error + ? '! ' + : result.state === 'working' + ? '✔ ' + : result.state === 'not_working' + ? '✘ ' + : '! ', + result.error ? 'check error' : result.message, + ], + ), + ); + }, + 'P1_PRIORITY', + ); + + // Config name + runAsyncTask(async () => { + try { + let configName = _('Main config'); + const data = await uci.load('podkop'); + const proxyString = uci.get('podkop', 'main', 'proxy_string'); + + if (proxyString) { + const activeConfig = proxyString + .split('\n') + .map((line) => line.trim()) + .find((line) => line && !line.startsWith('//')); + + if (activeConfig) { + if (activeConfig.includes('#')) { + const label = activeConfig.split('#').pop(); + if (label && label.trim()) { + configName = _('Config: ') + decodeURIComponent(label); } - } catch (error) { - updateTextElement('podkop-status-text', - E('span', { 'style': `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error') - ); + } } - }); + } - // Sing-box Status check - safeExec('/usr/bin/podkop', ['get_sing_box_status'], 'P0_PRIORITY', result => { - try { - const parsedSingboxStatus = JSON.parse(result.stdout || '{"running":0,"enabled":0,"status":"error"}'); - - // Update Sing-box status text - updateTextElement('singbox-status-text', - E('span', { - 'style': `color: ${parsedSingboxStatus.running && !parsedSingboxStatus.enabled ? - main.STATUS_COLORS.SUCCESS : main.STATUS_COLORS.ERROR}` - }, [ - parsedSingboxStatus.running && !parsedSingboxStatus.enabled ? - '✔ running' : '✘ ' + parsedSingboxStatus.status - ]) - ); - } catch (error) { - updateTextElement('singbox-status-text', - E('span', { 'style': `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error') - ); - } - }); - - // Version Information checks - safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', result => { - updateTextElement('podkop-version', - document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown')) - ); - }); - - safeExec('/usr/bin/podkop', ['show_luci_version'], 'P2_PRIORITY', result => { - updateTextElement('luci-version', - document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown')) - ); - }); - - safeExec('/usr/bin/podkop', ['show_sing_box_version'], 'P2_PRIORITY', result => { - updateTextElement('singbox-version', - document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown')) - ); - }); - - safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', result => { - if (result.stdout) { - updateTextElement('openwrt-version', - document.createTextNode(result.stdout.split('\n')[1].trim()) - ); - updateTextElement('device-model', - document.createTextNode(result.stdout.split('\n')[4].trim()) - ); - } else { - updateTextElement('openwrt-version', document.createTextNode(_('Unknown'))); - updateTextElement('device-model', document.createTextNode(_('Unknown'))); - } - }); - - // FakeIP and DNS status checks - runCheck(checkFakeIP, 'P3_PRIORITY', result => { - updateTextElement('fakeip-browser-status', - E('span', { style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}` }, [ - result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ', - result.error ? 'check error' : result.state === 'working' ? _('works in browser') : _('does not work in browser') - ]) - ); - }); - - runCheck(checkFakeIPCLI, 'P8_PRIORITY', result => { - updateTextElement('fakeip-router-status', - E('span', { style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}` }, [ - result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ', - result.error ? 'check error' : result.state === 'working' ? _('works on router') : _('does not work on router') - ]) - ); - }); - - runCheck(checkDNSAvailability, 'P4_PRIORITY', result => { - if (result.error) { - updateTextElement('dns-remote-status', - E('span', { style: `color: ${main.STATUS_COLORS.WARNING}` }, '! DNS check error') - ); - updateTextElement('dns-local-status', - E('span', { style: `color: ${main.STATUS_COLORS.WARNING}` }, '! DNS check error') - ); - } else { - updateTextElement('dns-remote-status', - E('span', { style: `color: ${result.remote.color}` }, [ - result.remote.state === 'available' ? '✔ ' : result.remote.state === 'unavailable' ? '✘ ' : '! ', - result.remote.message - ]) - ); - - updateTextElement('dns-local-status', - E('span', { style: `color: ${result.local.color}` }, [ - result.local.state === 'available' ? '✔ ' : result.local.state === 'unavailable' ? '✘ ' : '! ', - result.local.message - ]) - ); - } - }); - - runCheck(checkBypass, 'P1_PRIORITY', result => { - updateTextElement('bypass-status', - E('span', { style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}` }, [ - result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ', - result.error ? 'check error' : result.message - ]) - ); - }, 'P1_PRIORITY'); - - // Config name - runAsyncTask(async () => { - try { - let configName = _('Main config'); - const data = await uci.load('podkop'); - const proxyString = uci.get('podkop', 'main', 'proxy_string'); - - if (proxyString) { - const activeConfig = proxyString.split('\n') - .map(line => line.trim()) - .find(line => line && !line.startsWith('//')); - - if (activeConfig) { - if (activeConfig.includes('#')) { - const label = activeConfig.split('#').pop(); - if (label && label.trim()) { - configName = _('Config: ') + decodeURIComponent(label); - } - } - } - } - - updateTextElement('config-name-text', document.createTextNode(configName)); - } catch (e) { - console.error('Error getting config name from UCI:', e); - } - }, 'P1_PRIORITY'); + updateTextElement( + 'config-name-text', + document.createTextNode(configName), + ); + } catch (e) { + console.error('Error getting config name from UCI:', e); + } + }, 'P1_PRIORITY'); } function createDiagnosticsSection(mainSection) { - let o = mainSection.tab('diagnostics', _('Diagnostics')); + let o = mainSection.tab('diagnostics', _('Diagnostics')); - o = mainSection.taboption('diagnostics', form.DummyValue, '_status'); - o.rawhtml = true; - o.cfgvalue = () => E('div', { - id: 'diagnostics-status', - 'data-loading': 'true' + o = mainSection.taboption('diagnostics', form.DummyValue, '_status'); + o.rawhtml = true; + o.cfgvalue = () => + E('div', { + id: 'diagnostics-status', + 'data-loading': 'true', }); } function setupDiagnosticsEventHandlers(node) { - const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop')); - node.insertBefore(titleDiv, node.firstChild); + const titleDiv = E('h2', { class: 'cbi-map-title' }, _('Podkop')); + node.insertBefore(titleDiv, node.firstChild); - // Function to initialize diagnostics - function initDiagnostics(container) { - if (container && container.hasAttribute('data-loading')) { - container.innerHTML = ''; - showConfigModal.busy = false; - createStatusSection().then(section => { - container.appendChild(section); - startDiagnosticsUpdates(); - // Start error polling when diagnostics tab is active - utils.startErrorPolling(); - }); - } + // Function to initialize diagnostics + function initDiagnostics(container) { + if (container && container.hasAttribute('data-loading')) { + container.innerHTML = ''; + showConfigModal.busy = false; + createStatusSection().then((section) => { + container.appendChild(section); + startDiagnosticsUpdates(); + // Start error polling when diagnostics tab is active + utils.startErrorPolling(); + }); + } + } + + document.addEventListener('visibilitychange', function () { + const diagnosticsContainer = document.getElementById('diagnostics-status'); + const diagnosticsTab = document.querySelector( + '.cbi-tab[data-tab="diagnostics"]', + ); + + if ( + document.hidden || + !diagnosticsTab || + !diagnosticsTab.classList.contains('cbi-tab-active') + ) { + stopDiagnosticsUpdates(); + // Don't stop error polling here - it's managed in podkop.js for all tabs + } else if ( + diagnosticsContainer && + diagnosticsContainer.hasAttribute('data-loading') + ) { + startDiagnosticsUpdates(); + // Ensure error polling is running when diagnostics tab is active + utils.startErrorPolling(); + } + }); + + setTimeout(() => { + const diagnosticsContainer = document.getElementById('diagnostics-status'); + const diagnosticsTab = document.querySelector( + '.cbi-tab[data-tab="diagnostics"]', + ); + const otherTabs = document.querySelectorAll( + '.cbi-tab:not([data-tab="diagnostics"])', + ); + + // Check for direct page load case + const noActiveTabsExist = !Array.from(otherTabs).some((tab) => + tab.classList.contains('cbi-tab-active'), + ); + + if ( + diagnosticsContainer && + diagnosticsTab && + (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist) + ) { + initDiagnostics(diagnosticsContainer); } - document.addEventListener('visibilitychange', function () { - const diagnosticsContainer = document.getElementById('diagnostics-status'); - const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]'); - - if (document.hidden || !diagnosticsTab || !diagnosticsTab.classList.contains('cbi-tab-active')) { + const tabs = node.querySelectorAll('.cbi-tabmenu'); + if (tabs.length > 0) { + tabs[0].addEventListener('click', function (e) { + const tab = e.target.closest('.cbi-tab'); + if (tab) { + const tabName = tab.getAttribute('data-tab'); + if (tabName === 'diagnostics') { + const container = document.getElementById('diagnostics-status'); + container.setAttribute('data-loading', 'true'); + initDiagnostics(container); + } else { stopDiagnosticsUpdates(); - // Don't stop error polling here - it's managed in podkop.js for all tabs - } else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) { - startDiagnosticsUpdates(); - // Ensure error polling is running when diagnostics tab is active - utils.startErrorPolling(); + // Don't stop error polling - it should continue on all tabs + } } - }); + }); + } + }, main.DIAGNOSTICS_INITIAL_DELAY); - setTimeout(() => { - const diagnosticsContainer = document.getElementById('diagnostics-status'); - const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]'); - const otherTabs = document.querySelectorAll('.cbi-tab:not([data-tab="diagnostics"])'); - - // Check for direct page load case - const noActiveTabsExist = !Array.from(otherTabs).some(tab => tab.classList.contains('cbi-tab-active')); - - if (diagnosticsContainer && diagnosticsTab && (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist)) { - initDiagnostics(diagnosticsContainer); - } - - const tabs = node.querySelectorAll('.cbi-tabmenu'); - if (tabs.length > 0) { - tabs[0].addEventListener('click', function (e) { - const tab = e.target.closest('.cbi-tab'); - if (tab) { - const tabName = tab.getAttribute('data-tab'); - if (tabName === 'diagnostics') { - const container = document.getElementById('diagnostics-status'); - container.setAttribute('data-loading', 'true'); - initDiagnostics(container); - } else { - stopDiagnosticsUpdates(); - // Don't stop error polling - it should continue on all tabs - } - } - }); - } - }, main.DIAGNOSTICS_INITIAL_DELAY); - - node.classList.add('fade-in'); - return node; + node.classList.add('fade-in'); + return node; } return baseclass.extend({ - createDiagnosticsSection, - setupDiagnosticsEventHandlers + createDiagnosticsSection, + setupDiagnosticsEventHandlers, }); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js index 46cb086..f358670 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js @@ -15,138 +15,149 @@ let errorPollTimer = null; // Helper function to fetch errors from the podkop command async function getPodkopErrors() { - return new Promise(resolve => { - safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', result => { - if (!result || !result.stdout) return resolve([]); + return new Promise((resolve) => { + safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', (result) => { + if (!result || !result.stdout) return resolve([]); - const logs = result.stdout.split('\n'); - const errors = logs.filter(log => - log.includes('[critical]') - ); + const logs = result.stdout.split('\n'); + const errors = logs.filter((log) => log.includes('[critical]')); - resolve(errors); - }); + resolve(errors); }); + }); } // Show error notification to the user function showErrorNotification(error, isMultiple = false) { - const notificationContent = E('div', { 'class': 'alert-message error' }, [ - E('pre', { 'class': 'error-log' }, error) - ]); + const notificationContent = E('div', { class: 'alert-message error' }, [ + E('pre', { class: 'error-log' }, error), + ]); - ui.addNotification(null, notificationContent); + ui.addNotification(null, notificationContent); } // Helper function for command execution with prioritization -function safeExec(command, args, priority, callback, timeout = main.COMMAND_TIMEOUT) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; +function safeExec( + command, + args, + priority, + callback, + timeout = main.COMMAND_TIMEOUT, +) { + // Default to highest priority execution if priority is not provided or invalid + let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - // If priority is a string, try to get the corresponding delay value - if (typeof priority === 'string' && main.COMMAND_SCHEDULING[priority] !== undefined) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; + // If priority is a string, try to get the corresponding delay value + if ( + typeof priority === 'string' && + main.COMMAND_SCHEDULING[priority] !== undefined + ) { + schedulingDelay = main.COMMAND_SCHEDULING[priority]; + } + + const executeCommand = async () => { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const result = await Promise.race([ + fs.exec(command, args), + new Promise((_, reject) => { + controller.signal.addEventListener('abort', () => { + reject(new Error('Command execution timed out')); + }); + }), + ]); + + clearTimeout(timeoutId); + + if (callback && typeof callback === 'function') { + callback(result); + } + + return result; + } catch (error) { + console.warn( + `Command execution failed or timed out: ${command} ${args.join(' ')}`, + ); + const errorResult = { stdout: '', stderr: error.message, error: error }; + + if (callback && typeof callback === 'function') { + callback(errorResult); + } + + return errorResult; } + }; - const executeCommand = async () => { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - const result = await Promise.race([ - fs.exec(command, args), - new Promise((_, reject) => { - controller.signal.addEventListener('abort', () => { - reject(new Error('Command execution timed out')); - }); - }) - ]); - - clearTimeout(timeoutId); - - if (callback && typeof callback === 'function') { - callback(result); - } - - return result; - } catch (error) { - console.warn(`Command execution failed or timed out: ${command} ${args.join(' ')}`); - const errorResult = { stdout: '', stderr: error.message, error: error }; - - if (callback && typeof callback === 'function') { - callback(errorResult); - } - - return errorResult; - } - }; - - if (callback && typeof callback === 'function') { - setTimeout(executeCommand, schedulingDelay); - return; - } - else { - return executeCommand(); - } + if (callback && typeof callback === 'function') { + setTimeout(executeCommand, schedulingDelay); + return; + } else { + return executeCommand(); + } } // Check for critical errors and show notifications async function checkForCriticalErrors() { - try { - const errors = await getPodkopErrors(); + try { + const errors = await getPodkopErrors(); - if (errors && errors.length > 0) { - // Filter out errors we've already seen - const newErrors = errors.filter(error => !lastErrorsSet.has(error)); + if (errors && errors.length > 0) { + // Filter out errors we've already seen + const newErrors = errors.filter((error) => !lastErrorsSet.has(error)); - if (newErrors.length > 0) { - // On initial check, just store errors without showing notifications - if (!isInitialCheck) { - // Show each new error as a notification - newErrors.forEach(error => { - showErrorNotification(error, newErrors.length > 1); - }); - } - - // Add new errors to our set of seen errors - newErrors.forEach(error => lastErrorsSet.add(error)); - } + if (newErrors.length > 0) { + // On initial check, just store errors without showing notifications + if (!isInitialCheck) { + // Show each new error as a notification + newErrors.forEach((error) => { + showErrorNotification(error, newErrors.length > 1); + }); } - // After first check, mark as no longer initial - isInitialCheck = false; - } catch (error) { - console.error('Error checking for critical messages:', error); + // Add new errors to our set of seen errors + newErrors.forEach((error) => lastErrorsSet.add(error)); + } } + + // After first check, mark as no longer initial + isInitialCheck = false; + } catch (error) { + console.error('Error checking for critical messages:', error); + } } // Start polling for errors at regular intervals function startErrorPolling() { - if (errorPollTimer) { - clearInterval(errorPollTimer); - } + if (errorPollTimer) { + clearInterval(errorPollTimer); + } - // Reset initial check flag to make sure we show errors - isInitialCheck = false; + // Reset initial check flag to make sure we show errors + isInitialCheck = false; - // Immediately check for errors on start - checkForCriticalErrors(); + // Immediately check for errors on start + checkForCriticalErrors(); - // Then set up periodic checks - errorPollTimer = setInterval(checkForCriticalErrors, main.ERROR_POLL_INTERVAL); + // Then set up periodic checks + errorPollTimer = setInterval( + checkForCriticalErrors, + main.ERROR_POLL_INTERVAL, + ); } // Stop polling for errors function stopErrorPolling() { - if (errorPollTimer) { - clearInterval(errorPollTimer); - errorPollTimer = null; - } + if (errorPollTimer) { + clearInterval(errorPollTimer); + errorPollTimer = null; + } } return baseclass.extend({ - startErrorPolling, - stopErrorPolling, - checkForCriticalErrors, - safeExec + startErrorPolling, + stopErrorPolling, + checkForCriticalErrors, + safeExec, });